diff --git a/.mockery.yaml b/.mockery.yaml index 8f139231cb..30678c2e28 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -2,15 +2,6 @@ template: testify template-data: unroll-variadic: true packages: - github.com/evstack/ev-node/core/da: - interfaces: - DA: - config: - pkgname: mocks - filename: da.go - configs: - - dir: ./da/internal/mocks - - dir: ./test/mocks github.com/evstack/ev-node/core/execution: interfaces: Executor: diff --git a/CLAUDE.md b/CLAUDE.md index 70bcf6a9ac..c4d6432929 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,7 +52,7 @@ The project uses a zero-dependency core package pattern: - **Executor** (core/executor.go) - Handles state transitions - **Sequencer** (core/sequencer.go) - Orders transactions -- **DA** (core/da.go) - Data availability layer abstraction +- **Blob API** (block/internal/da/client.go) - Data availability client abstraction used by the node ### Modular Design @@ -120,7 +120,7 @@ go test -race ./package/... ### Adding a New DA Layer -1. Implement the `DA` interface from `core/da.go` +1. Implement the `BlobAPI` interface from `block/internal/da/client.go` (or extend the shared helpers in `pkg/blob`) 2. Add configuration in the appropriate config package 3. Wire it up in the initialization code 4. Add tests following existing patterns diff --git a/apps/evm/Dockerfile b/apps/evm/Dockerfile index 202bb9a232..b86a804340 100644 --- a/apps/evm/Dockerfile +++ b/apps/evm/Dockerfile @@ -2,9 +2,9 @@ FROM golang:1.24-alpine AS build-env WORKDIR /src -COPY core core - COPY go.mod go.sum ./ +COPY core core +COPY da da RUN go mod download COPY . . diff --git a/apps/evm/cmd/run.go b/apps/evm/cmd/run.go index eda6d3e1e5..d2851ebdac 100644 --- a/apps/evm/cmd/run.go +++ b/apps/evm/cmd/run.go @@ -12,7 +12,6 @@ import ( "github.com/rs/zerolog" "github.com/spf13/cobra" - "github.com/evstack/ev-node/core/da" "github.com/evstack/ev-node/core/execution" coresequencer "github.com/evstack/ev-node/core/sequencer" "github.com/evstack/ev-node/da/jsonrpc" @@ -22,6 +21,7 @@ import ( "github.com/evstack/ev-node/pkg/config" "github.com/evstack/ev-node/pkg/genesis" genesispkg "github.com/evstack/ev-node/pkg/genesis" + "github.com/evstack/ev-node/pkg/namespace" "github.com/evstack/ev-node/pkg/p2p" "github.com/evstack/ev-node/pkg/p2p/key" "github.com/evstack/ev-node/pkg/store" @@ -52,8 +52,8 @@ var RunCmd = &cobra.Command{ ec.SetLogger(logger.With().Str("module", "engine_client").Logger()) } - headerNamespace := da.NamespaceFromString(nodeConfig.DA.GetNamespace()) - dataNamespace := da.NamespaceFromString(nodeConfig.DA.GetDataNamespace()) + headerNamespace := namespace.NamespaceFromString(nodeConfig.DA.GetNamespace()) + dataNamespace := namespace.NamespaceFromString(nodeConfig.DA.GetDataNamespace()) logger.Info().Str("headerNamespace", headerNamespace.HexString()).Str("dataNamespace", dataNamespace.HexString()).Msg("namespaces") @@ -78,7 +78,7 @@ var RunCmd = &cobra.Command{ } // Create sequencer based on configuration - sequencer, err := createSequencer(context.Background(), logger, datastore, &daJrpc.DA, nodeConfig, genesis) + sequencer, err := createSequencer(context.Background(), logger, datastore, daJrpc, nodeConfig, genesis) if err != nil { return err } @@ -93,7 +93,7 @@ var RunCmd = &cobra.Command{ return err } - return rollcmd.StartNode(logger, cmd, executor, sequencer, &daJrpc.DA, p2pClient, datastore, nodeConfig, genesis, node.NodeOptions{}) + return rollcmd.StartNode(logger, cmd, executor, sequencer, p2pClient, datastore, nodeConfig, genesis, node.NodeOptions{}) }, } @@ -107,17 +107,25 @@ func createSequencer( ctx context.Context, logger zerolog.Logger, datastore datastore.Batching, - da da.DA, + blobVerifier *jsonrpc.Client, nodeConfig config.Config, genesis genesis.Genesis, ) (coresequencer.Sequencer, error) { + singleMetrics, err := single.NopMetrics() + if err != nil { + return nil, fmt.Errorf("failed to create single sequencer metrics: %w", err) + } + + dataNamespace := namespace.NamespaceFromString(nodeConfig.DA.GetDataNamespace()) sequencer, err := single.NewSequencer( ctx, logger, datastore, - da, + blobVerifier, []byte(genesis.ChainID), + dataNamespace.Bytes(), nodeConfig.Node.BlockTime.Duration, + singleMetrics, nodeConfig.Node.Aggregator, ) if err != nil { diff --git a/apps/evm/entrypoint.sh b/apps/evm/entrypoint.sh index 9c1d1c9692..141c9fb3df 100755 --- a/apps/evm/entrypoint.sh +++ b/apps/evm/entrypoint.sh @@ -36,8 +36,10 @@ if [ ! -f "$CONFIG_HOME/config/node_key.json" ]; then init_flags="--home=$CONFIG_HOME" # Add required flags if environment variables are set - if [ -n "$EVM_SIGNER_PASSPHRASE" ]; then - init_flags="$init_flags --rollkit.node.aggregator=true --rollkit.signer.passphrase $EVM_SIGNER_PASSPHRASE" + if [ -n "$EVM_SIGNER_PASSPHRASE_FILE" ]; then + init_flags="$init_flags --evnode.node.aggregator=true --evnode.signer.passphrase_file $EVM_SIGNER_PASSPHRASE_FILE" + elif [ -n "$EVM_SIGNER_PASSPHRASE" ]; then + init_flags="$init_flags --evnode.node.aggregator=true --evnode.signer.passphrase $EVM_SIGNER_PASSPHRASE" fi INIT_COMMAND="evm init $init_flags" @@ -51,7 +53,9 @@ fi default_flags="--home=$CONFIG_HOME" # Add required flags if environment variables are set -if [ -n "$EVM_JWT_SECRET" ]; then +if [ -n "$EVM_JWT_SECRET_FILE" ]; then + default_flags="$default_flags --evm.jwt-secret-file $EVM_JWT_SECRET_FILE" +elif [ -n "$EVM_JWT_SECRET" ]; then default_flags="$default_flags --evm.jwt-secret $EVM_JWT_SECRET" fi @@ -68,28 +72,38 @@ if [ -n "$EVM_ETH_URL" ]; then fi if [ -n "$EVM_BLOCK_TIME" ]; then - default_flags="$default_flags --rollkit.node.block_time $EVM_BLOCK_TIME" + default_flags="$default_flags --evnode.node.block_time $EVM_BLOCK_TIME" fi -if [ -n "$EVM_SIGNER_PASSPHRASE" ]; then - default_flags="$default_flags --rollkit.node.aggregator=true --rollkit.signer.passphrase $EVM_SIGNER_PASSPHRASE" +if [ -n "$EVM_SIGNER_PASSPHRASE_FILE" ]; then + default_flags="$default_flags --evnode.node.aggregator=true --evnode.signer.passphrase_file $EVM_SIGNER_PASSPHRASE_FILE" +elif [ -n "$EVM_SIGNER_PASSPHRASE" ]; then + default_flags="$default_flags --evnode.node.aggregator=true --evnode.signer.passphrase $EVM_SIGNER_PASSPHRASE" fi # Conditionally add DA-related flags if [ -n "$DA_ADDRESS" ]; then - default_flags="$default_flags --rollkit.da.address $DA_ADDRESS" + default_flags="$default_flags --evnode.da.address $DA_ADDRESS" fi if [ -n "$DA_AUTH_TOKEN" ]; then - default_flags="$default_flags --rollkit.da.auth_token $DA_AUTH_TOKEN" + default_flags="$default_flags --evnode.da.auth_token $DA_AUTH_TOKEN" fi if [ -n "$DA_NAMESPACE" ]; then - default_flags="$default_flags --rollkit.da.namespace $DA_NAMESPACE" + default_flags="$default_flags --evnode.da.namespace $DA_NAMESPACE" +fi + +if [ -n "$DA_DATA_NAMESPACE" ]; then + default_flags="$default_flags --evnode.da.data_namespace $DA_DATA_NAMESPACE" fi if [ -n "$DA_SIGNING_ADDRESSES" ]; then - default_flags="$default_flags --rollkit.da.signing_addresses $DA_SIGNING_ADDRESSES" + default_flags="$default_flags --evnode.da.signing_addresses $DA_SIGNING_ADDRESSES" +fi + +if [ -n "$DA_BLOCK_TIME" ]; then + default_flags="$default_flags --evnode.da.block_time $DA_BLOCK_TIME" fi # If no arguments passed, show help diff --git a/apps/evm/go.mod b/apps/evm/go.mod index 5fc0e68b14..38c578e1a3 100644 --- a/apps/evm/go.mod +++ b/apps/evm/go.mod @@ -32,7 +32,9 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.20.0 // indirect github.com/celestiaorg/go-libp2p-messenger v0.2.2 // indirect + github.com/celestiaorg/go-square/merkle v0.0.0-20240627094109-7d01436067a3 // indirect github.com/celestiaorg/go-square/v3 v3.0.2 // indirect + github.com/celestiaorg/nmt v0.24.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/consensys/gnark-crypto v0.18.1 // indirect github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect diff --git a/apps/evm/go.sum b/apps/evm/go.sum index 379b6649ae..83766bad1a 100644 --- a/apps/evm/go.sum +++ b/apps/evm/go.sum @@ -36,8 +36,12 @@ github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBT github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= 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/merkle v0.0.0-20240627094109-7d01436067a3 h1:wP84mtwOCVNOTfS3zErICjxKLnh74Z1uf+tdrlSFjVM= +github.com/celestiaorg/go-square/merkle v0.0.0-20240627094109-7d01436067a3/go.mod h1:86qIYnEhmn/hfW+xvw98NOI3zGaDEB3x8JGjYo2FqLs= github.com/celestiaorg/go-square/v3 v3.0.2 h1:eSQOgNII8inK9IhiBZ+6GADQeWbRq4HYY72BOgcduA4= github.com/celestiaorg/go-square/v3 v3.0.2/go.mod h1:oFReMLsSDMRs82ICFEeFQFCqNvwdsbIM1BzCcb0f7dM= +github.com/celestiaorg/nmt v0.24.2 h1:LlpJSPOd6/Lw1Ig6HUhZuqiINHLka/ZSRTBzlNJpchg= +github.com/celestiaorg/nmt v0.24.2/go.mod h1:vgLBpWBi8F5KLxTdXSwb7AU4NhiIQ1AQRGa+PzdcLEA= 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= @@ -185,6 +189,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -526,6 +532,12 @@ github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZ github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= @@ -764,8 +776,6 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= -gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/apps/grpc/README.md b/apps/grpc/README.md index dac9a847f2..51a49ebc09 100644 --- a/apps/grpc/README.md +++ b/apps/grpc/README.md @@ -150,5 +150,5 @@ If you have issues connecting to the DA layer: ## See Also - [Evolve Documentation](https://ev.xyz) -- [gRPC Execution Interface](../../../execution/grpc/README.md) -- [Single Sequencer Documentation](../../../sequencers/single/README.md) +- [gRPC Execution Interface](../../execution/grpc/README.md) +- [Single Sequencer Documentation](../../sequencers/single/README.md) diff --git a/apps/grpc/cmd/run.go b/apps/grpc/cmd/run.go index 5c35c5a185..835193dc3e 100644 --- a/apps/grpc/cmd/run.go +++ b/apps/grpc/cmd/run.go @@ -9,7 +9,6 @@ import ( "github.com/rs/zerolog" "github.com/spf13/cobra" - "github.com/evstack/ev-node/core/da" "github.com/evstack/ev-node/core/execution" coresequencer "github.com/evstack/ev-node/core/sequencer" "github.com/evstack/ev-node/da/jsonrpc" @@ -19,6 +18,7 @@ import ( "github.com/evstack/ev-node/pkg/config" "github.com/evstack/ev-node/pkg/genesis" rollgenesis "github.com/evstack/ev-node/pkg/genesis" + "github.com/evstack/ev-node/pkg/namespace" "github.com/evstack/ev-node/pkg/p2p" "github.com/evstack/ev-node/pkg/p2p/key" "github.com/evstack/ev-node/pkg/store" @@ -52,8 +52,8 @@ The execution client must implement the Evolve execution gRPC interface.`, logger := rollcmd.SetupLogger(nodeConfig.Log) - headerNamespace := da.NamespaceFromString(nodeConfig.DA.GetNamespace()) - dataNamespace := da.NamespaceFromString(nodeConfig.DA.GetDataNamespace()) + headerNamespace := namespace.NamespaceFromString(nodeConfig.DA.GetNamespace()) + dataNamespace := namespace.NamespaceFromString(nodeConfig.DA.GetDataNamespace()) logger.Info().Str("headerNamespace", headerNamespace.HexString()).Str("dataNamespace", dataNamespace.HexString()).Msg("namespaces") @@ -80,7 +80,7 @@ The execution client must implement the Evolve execution gRPC interface.`, } // Create sequencer based on configuration - sequencer, err := createSequencer(cmd.Context(), logger, datastore, &daJrpc.DA, nodeConfig, genesis) + sequencer, err := createSequencer(cmd.Context(), logger, datastore, daJrpc, nodeConfig, genesis) if err != nil { return err } @@ -98,7 +98,7 @@ The execution client must implement the Evolve execution gRPC interface.`, } // Start the node - return rollcmd.StartNode(logger, cmd, executor, sequencer, &daJrpc.DA, p2pClient, datastore, nodeConfig, genesis, node.NodeOptions{}) + return rollcmd.StartNode(logger, cmd, executor, sequencer, p2pClient, datastore, nodeConfig, genesis, node.NodeOptions{}) }, } @@ -115,17 +115,25 @@ func createSequencer( ctx context.Context, logger zerolog.Logger, datastore datastore.Batching, - da da.DA, + blobVerifier *jsonrpc.Client, nodeConfig config.Config, genesis genesis.Genesis, ) (coresequencer.Sequencer, error) { + singleMetrics, err := single.NopMetrics() + if err != nil { + return nil, fmt.Errorf("failed to create single sequencer metrics: %w", err) + } + + dataNamespace := namespace.NamespaceFromString(nodeConfig.DA.GetDataNamespace()) sequencer, err := single.NewSequencer( ctx, logger, datastore, - da, + blobVerifier, []byte(genesis.ChainID), + dataNamespace.Bytes(), nodeConfig.Node.BlockTime.Duration, + singleMetrics, nodeConfig.Node.Aggregator, ) if err != nil { diff --git a/apps/grpc/go.mod b/apps/grpc/go.mod index a0bb4a9203..1d5dde594a 100644 --- a/apps/grpc/go.mod +++ b/apps/grpc/go.mod @@ -28,7 +28,9 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/celestiaorg/go-header v0.7.4 // indirect github.com/celestiaorg/go-libp2p-messenger v0.2.2 // indirect + github.com/celestiaorg/go-square/merkle v0.0.0-20240627094109-7d01436067a3 // indirect github.com/celestiaorg/go-square/v3 v3.0.2 // indirect + github.com/celestiaorg/nmt v0.24.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect diff --git a/apps/grpc/go.sum b/apps/grpc/go.sum index 078cfa29c8..b7af304c6c 100644 --- a/apps/grpc/go.sum +++ b/apps/grpc/go.sum @@ -24,8 +24,12 @@ github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBT github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= 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/merkle v0.0.0-20240627094109-7d01436067a3 h1:wP84mtwOCVNOTfS3zErICjxKLnh74Z1uf+tdrlSFjVM= +github.com/celestiaorg/go-square/merkle v0.0.0-20240627094109-7d01436067a3/go.mod h1:86qIYnEhmn/hfW+xvw98NOI3zGaDEB3x8JGjYo2FqLs= github.com/celestiaorg/go-square/v3 v3.0.2 h1:eSQOgNII8inK9IhiBZ+6GADQeWbRq4HYY72BOgcduA4= github.com/celestiaorg/go-square/v3 v3.0.2/go.mod h1:oFReMLsSDMRs82ICFEeFQFCqNvwdsbIM1BzCcb0f7dM= +github.com/celestiaorg/nmt v0.24.2 h1:LlpJSPOd6/Lw1Ig6HUhZuqiINHLka/ZSRTBzlNJpchg= +github.com/celestiaorg/nmt v0.24.2/go.mod h1:vgLBpWBi8F5KLxTdXSwb7AU4NhiIQ1AQRGa+PzdcLEA= 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= @@ -123,6 +127,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -431,6 +437,12 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= @@ -656,8 +668,6 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= -gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/apps/testapp/cmd/run.go b/apps/testapp/cmd/run.go index dc913da713..9337941af5 100644 --- a/apps/testapp/cmd/run.go +++ b/apps/testapp/cmd/run.go @@ -8,11 +8,11 @@ import ( "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" genesispkg "github.com/evstack/ev-node/pkg/genesis" + "github.com/evstack/ev-node/pkg/namespace" "github.com/evstack/ev-node/pkg/p2p" "github.com/evstack/ev-node/pkg/p2p/key" "github.com/evstack/ev-node/pkg/store" @@ -48,8 +48,8 @@ var RunCmd = &cobra.Command{ ctx, cancel := context.WithCancel(context.Background()) defer cancel() - headerNamespace := da.NamespaceFromString(nodeConfig.DA.GetNamespace()) - dataNamespace := da.NamespaceFromString(nodeConfig.DA.GetDataNamespace()) + headerNamespace := namespace.NamespaceFromString(nodeConfig.DA.GetNamespace()) + dataNamespace := namespace.NamespaceFromString(nodeConfig.DA.GetDataNamespace()) logger.Info().Str("headerNamespace", headerNamespace.HexString()).Str("dataNamespace", dataNamespace.HexString()).Msg("namespaces") @@ -89,13 +89,20 @@ var RunCmd = &cobra.Command{ logger.Warn().Msg("da_start_height is not set in genesis.json, ask your chain developer") } + singleMetrics, err := single.NopMetrics() + if err != nil { + return fmt.Errorf("failed to create single sequencer metrics: %w", err) + } + sequencer, err := single.NewSequencer( ctx, logger, datastore, - &daJrpc.DA, + daJrpc, []byte(genesis.ChainID), + dataNamespace.Bytes(), nodeConfig.Node.BlockTime.Duration, + singleMetrics, nodeConfig.Node.Aggregator, ) if err != nil { @@ -107,6 +114,6 @@ var RunCmd = &cobra.Command{ return err } - return rollcmd.StartNode(logger, cmd, executor, sequencer, &daJrpc.DA, p2pClient, datastore, nodeConfig, genesis, node.NodeOptions{}) + return rollcmd.StartNode(logger, cmd, executor, sequencer, p2pClient, datastore, nodeConfig, genesis, node.NodeOptions{}) }, } diff --git a/apps/testapp/go.mod b/apps/testapp/go.mod index 27e12177b1..8c368ad681 100644 --- a/apps/testapp/go.mod +++ b/apps/testapp/go.mod @@ -13,8 +13,7 @@ replace ( require ( github.com/celestiaorg/go-header v0.7.4 github.com/evstack/ev-node v1.0.0-beta.9 - github.com/evstack/ev-node/core v1.0.0-beta.5 - github.com/evstack/ev-node/da v0.0.0-00010101000000-000000000000 + github.com/evstack/ev-node/da v1.0.0-beta.6 github.com/ipfs/go-datastore v0.9.0 github.com/spf13/cobra v1.10.1 github.com/stretchr/testify v1.11.1 @@ -26,7 +25,9 @@ require ( github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/celestiaorg/go-libp2p-messenger v0.2.2 // indirect + github.com/celestiaorg/go-square/merkle v0.0.0-20240627094109-7d01436067a3 // indirect github.com/celestiaorg/go-square/v3 v3.0.2 // indirect + github.com/celestiaorg/nmt v0.24.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect @@ -34,6 +35,7 @@ require ( github.com/dgraph-io/badger/v4 v4.5.1 // indirect github.com/dgraph-io/ristretto/v2 v2.1.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/evstack/ev-node/core v1.0.0-beta.5 // indirect github.com/filecoin-project/go-clock v0.1.0 // indirect github.com/filecoin-project/go-jsonrpc v0.9.0 // indirect github.com/flynn/noise v1.1.0 // indirect diff --git a/apps/testapp/go.sum b/apps/testapp/go.sum index 37d53446ad..56ba7c3ab6 100644 --- a/apps/testapp/go.sum +++ b/apps/testapp/go.sum @@ -24,8 +24,12 @@ github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBT github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= 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/merkle v0.0.0-20240627094109-7d01436067a3 h1:wP84mtwOCVNOTfS3zErICjxKLnh74Z1uf+tdrlSFjVM= +github.com/celestiaorg/go-square/merkle v0.0.0-20240627094109-7d01436067a3/go.mod h1:86qIYnEhmn/hfW+xvw98NOI3zGaDEB3x8JGjYo2FqLs= github.com/celestiaorg/go-square/v3 v3.0.2 h1:eSQOgNII8inK9IhiBZ+6GADQeWbRq4HYY72BOgcduA4= github.com/celestiaorg/go-square/v3 v3.0.2/go.mod h1:oFReMLsSDMRs82ICFEeFQFCqNvwdsbIM1BzCcb0f7dM= +github.com/celestiaorg/nmt v0.24.2 h1:LlpJSPOd6/Lw1Ig6HUhZuqiINHLka/ZSRTBzlNJpchg= +github.com/celestiaorg/nmt v0.24.2/go.mod h1:vgLBpWBi8F5KLxTdXSwb7AU4NhiIQ1AQRGa+PzdcLEA= 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= @@ -123,6 +127,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -430,6 +436,12 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= @@ -655,8 +667,6 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= -gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/block/components.go b/block/components.go index 546cda62c3..07b1123b2a 100644 --- a/block/components.go +++ b/block/components.go @@ -13,7 +13,6 @@ import ( "github.com/evstack/ev-node/block/internal/reaping" "github.com/evstack/ev-node/block/internal/submitting" "github.com/evstack/ev-node/block/internal/syncing" - coreda "github.com/evstack/ev-node/core/da" coreexecutor "github.com/evstack/ev-node/core/execution" coresequencer "github.com/evstack/ev-node/core/sequencer" "github.com/evstack/ev-node/pkg/config" @@ -131,7 +130,7 @@ func NewSyncComponents( genesis genesis.Genesis, store store.Store, exec coreexecutor.Executor, - da coreda.DA, + daClient DAClient, headerStore common.Broadcaster[*types.SignedHeader], dataStore common.Broadcaster[*types.Data], logger zerolog.Logger, @@ -139,13 +138,14 @@ func NewSyncComponents( blockOpts BlockOptions, ) (*Components, error) { logger.Info().Msg("Starting in sync-mode") + if daClient == nil { + return nil, fmt.Errorf("da client is required") + } cacheManager, err := cache.NewManager(config, store, logger) if err != nil { return nil, fmt.Errorf("failed to create cache manager: %w", err) } - daClient := NewDAClient(da, config, logger) - // error channel for critical failures errorCh := make(chan error, 1) @@ -196,7 +196,7 @@ func NewAggregatorComponents( store store.Store, exec coreexecutor.Executor, sequencer coresequencer.Sequencer, - da coreda.DA, + daClient DAClient, signer signer.Signer, headerBroadcaster common.Broadcaster[*types.SignedHeader], dataBroadcaster common.Broadcaster[*types.Data], @@ -205,6 +205,9 @@ func NewAggregatorComponents( blockOpts BlockOptions, ) (*Components, error) { logger.Info().Msg("Starting in aggregator-mode") + if daClient == nil { + return nil, fmt.Errorf("da client is required") + } cacheManager, err := cache.NewManager(config, store, logger) if err != nil { return nil, fmt.Errorf("failed to create cache manager: %w", err) @@ -245,8 +248,7 @@ func NewAggregatorComponents( return nil, fmt.Errorf("failed to create reaper: %w", err) } - // Create DA client and submitter for aggregator nodes (with signer for submission) - daClient := NewDAClient(da, config, logger) + // Create DA submitter for aggregator nodes (with signer for submission) daSubmitter := submitting.NewDASubmitter(daClient, config, genesis, blockOpts, metrics, logger) submitter := submitting.NewSubmitter( store, diff --git a/block/components_test.go b/block/components_test.go index eadf45328c..016f8b81af 100644 --- a/block/components_test.go +++ b/block/components_test.go @@ -15,7 +15,8 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - coreda "github.com/evstack/ev-node/core/da" + "github.com/evstack/ev-node/block/internal/common" + blockda "github.com/evstack/ev-node/block/internal/da" coresequencer "github.com/evstack/ev-node/core/sequencer" "github.com/evstack/ev-node/pkg/config" "github.com/evstack/ev-node/pkg/genesis" @@ -92,7 +93,7 @@ func TestNewSyncComponents_Creation(t *testing.T) { } mockExec := testmocks.NewMockExecutor(t) - dummyDA := coreda.NewDummyDA(10_000_000, 10*time.Millisecond) + daClient := newTestDAClient(t, cfg) // Just test that the constructor doesn't panic - don't start the components // to avoid P2P store dependencies @@ -101,7 +102,7 @@ func TestNewSyncComponents_Creation(t *testing.T) { gen, memStore, mockExec, - dummyDA, + daClient, nil, nil, zerolog.Nop(), @@ -143,7 +144,7 @@ func TestNewAggregatorComponents_Creation(t *testing.T) { mockExec := testmocks.NewMockExecutor(t) mockSeq := testmocks.NewMockSequencer(t) - dummyDA := coreda.NewDummyDA(10_000_000, 10*time.Millisecond) + daClient := newTestDAClient(t, cfg) components, err := NewAggregatorComponents( cfg, @@ -151,7 +152,7 @@ func TestNewAggregatorComponents_Creation(t *testing.T) { memStore, mockExec, mockSeq, - dummyDA, + daClient, mockSigner, nil, // header broadcaster nil, // data broadcaster @@ -197,7 +198,7 @@ func TestExecutor_RealExecutionClientFailure_StopsNode(t *testing.T) { // Create mock executor that will fail on ExecuteTxs mockExec := testmocks.NewMockExecutor(t) mockSeq := testmocks.NewMockSequencer(t) - dummyDA := coreda.NewDummyDA(10_000_000, 10*time.Millisecond) + daClient := newTestDAClient(t, cfg) // Mock InitChain to succeed initially mockExec.On("InitChain", mock.Anything, mock.Anything, mock.Anything, mock.Anything). @@ -226,7 +227,7 @@ func TestExecutor_RealExecutionClientFailure_StopsNode(t *testing.T) { memStore, mockExec, mockSeq, - dummyDA, + daClient, testSigner, nil, // header broadcaster nil, // data broadcaster @@ -262,3 +263,15 @@ func TestExecutor_RealExecutionClientFailure_StopsNode(t *testing.T) { stopErr := components.Stop() assert.NoError(t, stopErr) } + +func newTestDAClient(t *testing.T, cfg config.Config) DAClient { + t.Helper() + return blockda.NewClient(blockda.Config{ + BlobAPI: blockda.NewLocalBlobAPI(common.DefaultMaxBlobSize), + Logger: zerolog.Nop(), + DefaultTimeout: time.Second, + Namespace: cfg.DA.GetNamespace(), + DataNamespace: cfg.DA.GetDataNamespace(), + MaxBlobSize: common.DefaultMaxBlobSize, + }) +} diff --git a/block/internal/da/client.go b/block/internal/da/client.go index f10fa6f33f..0cb599fdfb 100644 --- a/block/internal/da/client.go +++ b/block/internal/da/client.go @@ -1,127 +1,183 @@ -// Package da provides a reusable wrapper around the core DA interface -// with common configuration for namespace handling and timeouts. package da import ( "context" + "encoding/json" "errors" "fmt" + "math" "strings" "time" + "github.com/celestiaorg/go-square/v3/share" "github.com/rs/zerolog" - coreda "github.com/evstack/ev-node/core/da" + "github.com/evstack/ev-node/pkg/blob" + datypes "github.com/evstack/ev-node/pkg/da/types" + "github.com/evstack/ev-node/pkg/namespace" ) // Client is the interface representing the DA client. type Client interface { - Submit(ctx context.Context, data [][]byte, gasPrice float64, namespace []byte, options []byte) coreda.ResultSubmit - Retrieve(ctx context.Context, height uint64, namespace []byte) coreda.ResultRetrieve - RetrieveHeaders(ctx context.Context, height uint64) coreda.ResultRetrieve - RetrieveData(ctx context.Context, height uint64) coreda.ResultRetrieve + Submit(ctx context.Context, data [][]byte, namespace []byte, options []byte) datypes.ResultSubmit + Retrieve(ctx context.Context, height uint64, namespace []byte) datypes.ResultRetrieve + RetrieveHeaders(ctx context.Context, height uint64) datypes.ResultRetrieve + RetrieveData(ctx context.Context, height uint64) datypes.ResultRetrieve GetHeaderNamespace() []byte GetDataNamespace() []byte - GetDA() coreda.DA } -// client provides a reusable wrapper around the core DA interface -// with common configuration for namespace handling and timeouts. +// client provides a reusable wrapper around the blob RPC with common configuration for namespaces and timeouts. type client struct { - da coreda.DA + blobClient BlobAPI logger zerolog.Logger defaultTimeout time.Duration - batchSize int namespaceBz []byte - namespaceDataBz []byte + dataNamespaceBz []byte + maxBlobSize uint64 } -const ( - defaultRetrieveBatchSize = 150 -) +// BlobAPI captures the minimal blob RPC methods used by the node. +type BlobAPI interface { + Submit(ctx context.Context, blobs []*blob.Blob, opts *blob.SubmitOptions) (uint64, error) + GetAll(ctx context.Context, height uint64, namespaces []share.Namespace) ([]*blob.Blob, error) + GetProof(ctx context.Context, height uint64, namespace share.Namespace, commitment blob.Commitment) (*blob.Proof, error) + Included(ctx context.Context, height uint64, namespace share.Namespace, proof *blob.Proof, commitment blob.Commitment) (bool, error) +} // Config contains configuration for the DA client. type Config struct { - DA coreda.DA - Logger zerolog.Logger - DefaultTimeout time.Duration - Namespace string - DataNamespace string - RetrieveBatchSize int + BlobAPI BlobAPI + Logger zerolog.Logger + DefaultTimeout time.Duration + Namespace string + DataNamespace string + MaxBlobSize uint64 } -// NewClient creates a new DA client with pre-calculated namespace bytes. +// NewClient creates a new blob client wrapper with pre-calculated namespace bytes. func NewClient(cfg Config) *client { if cfg.DefaultTimeout == 0 { cfg.DefaultTimeout = 60 * time.Second } - if cfg.RetrieveBatchSize <= 0 { - cfg.RetrieveBatchSize = defaultRetrieveBatchSize + if cfg.MaxBlobSize == 0 { + cfg.MaxBlobSize = blob.DefaultMaxBlobSize } return &client{ - da: cfg.DA, + blobClient: cfg.BlobAPI, logger: cfg.Logger.With().Str("component", "da_client").Logger(), defaultTimeout: cfg.DefaultTimeout, - batchSize: cfg.RetrieveBatchSize, - namespaceBz: coreda.NamespaceFromString(cfg.Namespace).Bytes(), - namespaceDataBz: coreda.NamespaceFromString(cfg.DataNamespace).Bytes(), + namespaceBz: namespace.NamespaceFromString(cfg.Namespace).Bytes(), + dataNamespaceBz: namespace.NamespaceFromString(cfg.DataNamespace).Bytes(), + maxBlobSize: cfg.MaxBlobSize, } } // Submit submits blobs to the DA layer with the specified options. -func (c *client) Submit(ctx context.Context, data [][]byte, gasPrice float64, namespace []byte, options []byte) coreda.ResultSubmit { - submitCtx, cancel := context.WithTimeout(ctx, c.defaultTimeout) - defer cancel() - - ids, err := c.da.SubmitWithOptions(submitCtx, data, gasPrice, namespace, options) +func (c *client) Submit(ctx context.Context, data [][]byte, namespace []byte, options []byte) datypes.ResultSubmit { + if data == nil { + return datypes.ResultSubmit{ + BaseResult: datypes.BaseResult{ + Code: datypes.StatusError, + Message: "data cannot be nil", + }, + } + } // calculate blob size var blobSize uint64 - for _, blob := range data { - blobSize += uint64(len(blob)) + for i, b := range data { + if b == nil { + return datypes.ResultSubmit{ + BaseResult: datypes.BaseResult{ + Code: datypes.StatusError, + Message: fmt.Sprintf("data[%d] cannot be nil", i), + }, + } + } + if uint64(len(b)) > math.MaxUint64-blobSize { + return datypes.ResultSubmit{ + BaseResult: datypes.BaseResult{ + Code: datypes.StatusError, + Message: "blob size overflow", + }, + } + } + blobSize += uint64(len(b)) } - // Handle errors returned by Submit + ns, err := share.NewNamespaceFromBytes(namespace) if err != nil { - if errors.Is(err, context.Canceled) { - c.logger.Debug().Msg("DA submission canceled due to context cancellation") - return coreda.ResultSubmit{ - BaseResult: coreda.BaseResult{ - Code: coreda.StatusContextCanceled, - Message: "submission canceled", - IDs: ids, - BlobSize: blobSize, + return datypes.ResultSubmit{ + BaseResult: datypes.BaseResult{ + Code: datypes.StatusError, + Message: fmt.Sprintf("invalid namespace: %v", err), + }, + } + } + + blobs := make([]*blob.Blob, len(data)) + for i, raw := range data { + if uint64(len(raw)) > c.maxBlobSize { + return datypes.ResultSubmit{ + BaseResult: datypes.BaseResult{ + Code: datypes.StatusTooBig, + Message: datypes.ErrBlobSizeOverLimit.Error(), }, } } - status := coreda.StatusError - switch { - case errors.Is(err, coreda.ErrTxTimedOut): - status = coreda.StatusNotIncludedInBlock - case errors.Is(err, coreda.ErrTxAlreadyInMempool): - status = coreda.StatusAlreadyInMempool - case errors.Is(err, coreda.ErrTxIncorrectAccountSequence): - status = coreda.StatusIncorrectAccountSequence - case errors.Is(err, coreda.ErrBlobSizeOverLimit): - status = coreda.StatusTooBig - case errors.Is(err, coreda.ErrContextDeadline): - status = coreda.StatusContextDeadline + blobs[i], err = blob.NewBlobV0(ns, raw) + if err != nil { + return datypes.ResultSubmit{ + BaseResult: datypes.BaseResult{ + Code: datypes.StatusError, + Message: fmt.Sprintf("failed to build blob %d: %v", i, err), + }, + } } + } - // Use debug level for StatusTooBig as it gets handled later in submitToDA through recursive splitting - if status == coreda.StatusTooBig { - c.logger.Debug().Err(err).Uint64("status", uint64(status)).Msg("DA submission failed") + var submitOpts blob.SubmitOptions + if len(options) > 0 { + if err := json.Unmarshal(options, &submitOpts); err != nil { + return datypes.ResultSubmit{ + BaseResult: datypes.BaseResult{ + Code: datypes.StatusError, + Message: fmt.Sprintf("failed to parse submit options: %v", err), + }, + } + } + } + + height, err := c.blobClient.Submit(ctx, blobs, &submitOpts) + if err != nil { + code := datypes.StatusError + switch { + case errors.Is(err, context.Canceled): + code = datypes.StatusContextCanceled + case strings.Contains(err.Error(), datypes.ErrTxTimedOut.Error()): + code = datypes.StatusNotIncludedInBlock + case strings.Contains(err.Error(), datypes.ErrTxAlreadyInMempool.Error()): + code = datypes.StatusAlreadyInMempool + case strings.Contains(err.Error(), datypes.ErrTxIncorrectAccountSequence.Error()): + code = datypes.StatusIncorrectAccountSequence + case strings.Contains(err.Error(), datypes.ErrBlobSizeOverLimit.Error()): + code = datypes.StatusTooBig + case strings.Contains(err.Error(), datypes.ErrContextDeadline.Error()): + code = datypes.StatusContextDeadline + } + if code == datypes.StatusTooBig { + c.logger.Debug().Err(err).Uint64("status", uint64(code)).Msg("DA submission failed") } else { - c.logger.Error().Err(err).Uint64("status", uint64(status)).Msg("DA submission failed") + c.logger.Error().Err(err).Uint64("status", uint64(code)).Msg("DA submission failed") } - return coreda.ResultSubmit{ - BaseResult: coreda.BaseResult{ - Code: status, + return datypes.ResultSubmit{ + BaseResult: datypes.BaseResult{ + Code: code, Message: "failed to submit blobs: " + err.Error(), - IDs: ids, - SubmittedCount: uint64(len(ids)), + SubmittedCount: 0, Height: 0, Timestamp: time.Now(), BlobSize: blobSize, @@ -129,29 +185,24 @@ func (c *client) Submit(ctx context.Context, data [][]byte, gasPrice float64, na } } - if len(ids) == 0 && len(data) > 0 { - c.logger.Warn().Msg("DA submission returned no IDs for non-empty input data") - return coreda.ResultSubmit{ - BaseResult: coreda.BaseResult{ - Code: coreda.StatusError, - Message: "failed to submit blobs: no IDs returned despite non-empty input", + if len(blobs) == 0 { + return datypes.ResultSubmit{ + BaseResult: datypes.BaseResult{ + Code: datypes.StatusSuccess, + BlobSize: blobSize, + Height: height, }, } } - // Get height from the first ID - var height uint64 - if len(ids) > 0 { - height, _, err = coreda.SplitID(ids[0]) - if err != nil { - c.logger.Error().Err(err).Msg("failed to split ID") - } + ids := make([]datypes.ID, len(blobs)) + for i, b := range blobs { + ids[i] = blob.MakeID(height, b.Commitment) } - c.logger.Debug().Int("num_ids", len(ids)).Msg("DA submission successful") - return coreda.ResultSubmit{ - BaseResult: coreda.BaseResult{ - Code: coreda.StatusSuccess, + return datypes.ResultSubmit{ + BaseResult: datypes.BaseResult{ + Code: datypes.StatusSuccess, IDs: ids, SubmittedCount: uint64(len(ids)), Height: height, @@ -162,104 +213,94 @@ func (c *client) Submit(ctx context.Context, data [][]byte, gasPrice float64, na } // Retrieve retrieves blobs from the DA layer at the specified height and namespace. -func (c *client) Retrieve(ctx context.Context, height uint64, namespace []byte) coreda.ResultRetrieve { +func (c *client) Retrieve(ctx context.Context, height uint64, namespace []byte) datypes.ResultRetrieve { + ns, err := share.NewNamespaceFromBytes(namespace) + if err != nil { + return datypes.ResultRetrieve{ + BaseResult: datypes.BaseResult{ + Code: datypes.StatusError, + Message: fmt.Sprintf("invalid namespace: %v", err), + Height: height, + }, + } + } + getIDsCtx, cancel := context.WithTimeout(ctx, c.defaultTimeout) defer cancel() - idsResult, err := c.da.GetIDs(getIDsCtx, height, namespace) + blobs, err := c.blobClient.GetAll(getIDsCtx, height, []share.Namespace{ns}) if err != nil { - // Handle specific "not found" error - if strings.Contains(err.Error(), coreda.ErrBlobNotFound.Error()) { - c.logger.Debug().Uint64("height", height).Msg("Blobs not found at height") - return coreda.ResultRetrieve{ - BaseResult: coreda.BaseResult{ - Code: coreda.StatusNotFound, - Message: coreda.ErrBlobNotFound.Error(), + // Handle known errors by substring because RPC may wrap them. + switch { + case strings.Contains(err.Error(), datypes.ErrBlobNotFound.Error()): + return datypes.ResultRetrieve{ + BaseResult: datypes.BaseResult{ + Code: datypes.StatusNotFound, + Message: datypes.ErrBlobNotFound.Error(), Height: height, Timestamp: time.Now(), }, } - } - if strings.Contains(err.Error(), coreda.ErrHeightFromFuture.Error()) { - c.logger.Debug().Uint64("height", height).Msg("Blobs not found at height") - return coreda.ResultRetrieve{ - BaseResult: coreda.BaseResult{ - Code: coreda.StatusHeightFromFuture, - Message: coreda.ErrHeightFromFuture.Error(), + case strings.Contains(err.Error(), datypes.ErrHeightFromFuture.Error()): + return datypes.ResultRetrieve{ + BaseResult: datypes.BaseResult{ + Code: datypes.StatusHeightFromFuture, + Message: datypes.ErrHeightFromFuture.Error(), + Height: height, + Timestamp: time.Now(), + }, + } + default: + c.logger.Error().Uint64("height", height).Err(err).Msg("failed to get blobs") + return datypes.ResultRetrieve{ + BaseResult: datypes.BaseResult{ + Code: datypes.StatusError, + Message: fmt.Sprintf("failed to get blobs: %s", err.Error()), Height: height, Timestamp: time.Now(), }, } - } - // Handle other errors during GetIDs - c.logger.Error().Uint64("height", height).Err(err).Msg("Failed to get IDs") - return coreda.ResultRetrieve{ - BaseResult: coreda.BaseResult{ - Code: coreda.StatusError, - Message: fmt.Sprintf("failed to get IDs: %s", err.Error()), - Height: height, - Timestamp: time.Now(), - }, } } - // This check should technically be redundant if GetIDs correctly returns ErrBlobNotFound - if idsResult == nil || len(idsResult.IDs) == 0 { - c.logger.Debug().Uint64("height", height).Msg("No IDs found at height") - return coreda.ResultRetrieve{ - BaseResult: coreda.BaseResult{ - Code: coreda.StatusNotFound, - Message: coreda.ErrBlobNotFound.Error(), + if len(blobs) == 0 { + c.logger.Debug().Uint64("height", height).Msg("No blobs found at height") + return datypes.ResultRetrieve{ + BaseResult: datypes.BaseResult{ + Code: datypes.StatusNotFound, + Message: datypes.ErrBlobNotFound.Error(), Height: height, Timestamp: time.Now(), }, } } - // 2. Get Blobs using the retrieved IDs in batches - // Each batch has its own timeout while keeping the link to the parent context - batchSize := c.batchSize - blobs := make([][]byte, 0, len(idsResult.IDs)) - for i := 0; i < len(idsResult.IDs); i += batchSize { - end := min(i+batchSize, len(idsResult.IDs)) - getBlobsCtx, cancel := context.WithTimeout(ctx, c.defaultTimeout) - batchBlobs, err := c.da.Get(getBlobsCtx, idsResult.IDs[i:end], namespace) - cancel() - if err != nil { - // Handle errors during Get - c.logger.Error().Uint64("height", height).Int("num_ids", len(idsResult.IDs)).Err(err).Msg("Failed to get blobs") - return coreda.ResultRetrieve{ - BaseResult: coreda.BaseResult{ - Code: coreda.StatusError, - Message: fmt.Sprintf("failed to get blobs for batch %d-%d: %s", i, end-1, err.Error()), - Height: height, - Timestamp: time.Now(), - }, - } - } - blobs = append(blobs, batchBlobs...) + out := make([][]byte, len(blobs)) + ids := make([][]byte, len(blobs)) + for i, b := range blobs { + out[i] = b.Data() + ids[i] = blob.MakeID(height, b.Commitment) } - // Success - c.logger.Debug().Uint64("height", height).Int("num_blobs", len(blobs)).Msg("Successfully retrieved blobs") - return coreda.ResultRetrieve{ - BaseResult: coreda.BaseResult{ - Code: coreda.StatusSuccess, + + return datypes.ResultRetrieve{ + BaseResult: datypes.BaseResult{ + Code: datypes.StatusSuccess, Height: height, - IDs: idsResult.IDs, - Timestamp: idsResult.Timestamp, + IDs: ids, + Timestamp: time.Now(), }, - Data: blobs, + Data: out, } } // RetrieveHeaders retrieves blobs from the header namespace at the specified height. -func (c *client) RetrieveHeaders(ctx context.Context, height uint64) coreda.ResultRetrieve { +func (c *client) RetrieveHeaders(ctx context.Context, height uint64) datypes.ResultRetrieve { return c.Retrieve(ctx, height, c.namespaceBz) } // RetrieveData retrieves blobs from the data namespace at the specified height. -func (c *client) RetrieveData(ctx context.Context, height uint64) coreda.ResultRetrieve { - return c.Retrieve(ctx, height, c.namespaceDataBz) +func (c *client) RetrieveData(ctx context.Context, height uint64) datypes.ResultRetrieve { + return c.Retrieve(ctx, height, c.dataNamespaceBz) } // GetHeaderNamespace returns the header namespace bytes. @@ -269,10 +310,5 @@ func (c *client) GetHeaderNamespace() []byte { // GetDataNamespace returns the data namespace bytes. func (c *client) GetDataNamespace() []byte { - return c.namespaceDataBz -} - -// GetDA returns the underlying DA interface for advanced usage. -func (c *client) GetDA() coreda.DA { - return c.da + return c.dataNamespaceBz } diff --git a/block/internal/da/client_test.go b/block/internal/da/client_test.go index ae9380f1df..d5cbd9b042 100644 --- a/block/internal/da/client_test.go +++ b/block/internal/da/client_test.go @@ -2,457 +2,143 @@ package da import ( "context" + "encoding/json" "errors" "testing" - "time" + "github.com/celestiaorg/go-square/v3/share" "github.com/rs/zerolog" - "gotest.tools/v3/assert" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" - coreda "github.com/evstack/ev-node/core/da" + "github.com/evstack/ev-node/pkg/blob" + datypes "github.com/evstack/ev-node/pkg/da/types" ) -// mockDA is a simple mock implementation of coreda.DA for testing -type mockDA struct { - submitFunc func(ctx context.Context, blobs []coreda.Blob, gasPrice float64, namespace []byte) ([]coreda.ID, error) - submitWithOptions func(ctx context.Context, blobs []coreda.Blob, gasPrice float64, namespace []byte, options []byte) ([]coreda.ID, error) - getIDsFunc func(ctx context.Context, height uint64, namespace []byte) (*coreda.GetIDsResult, error) - getFunc func(ctx context.Context, ids []coreda.ID, namespace []byte) ([]coreda.Blob, error) +type mockBlobAPI struct { + submitErr error + height uint64 + blobs []*blob.Blob + proof *blob.Proof + included bool } -func (m *mockDA) Submit(ctx context.Context, blobs []coreda.Blob, gasPrice float64, namespace []byte) ([]coreda.ID, error) { - if m.submitFunc != nil { - return m.submitFunc(ctx, blobs, gasPrice, namespace) - } - return nil, nil +func (m *mockBlobAPI) Submit(ctx context.Context, blobs []*blob.Blob, opts *blob.SubmitOptions) (uint64, error) { + return m.height, m.submitErr } -func (m *mockDA) SubmitWithOptions(ctx context.Context, blobs []coreda.Blob, gasPrice float64, namespace []byte, options []byte) ([]coreda.ID, error) { - if m.submitWithOptions != nil { - return m.submitWithOptions(ctx, blobs, gasPrice, namespace, options) - } - return nil, nil +func (m *mockBlobAPI) GetAll(ctx context.Context, height uint64, namespaces []share.Namespace) ([]*blob.Blob, error) { + return m.blobs, m.submitErr } -func (m *mockDA) GetIDs(ctx context.Context, height uint64, namespace []byte) (*coreda.GetIDsResult, error) { - if m.getIDsFunc != nil { - return m.getIDsFunc(ctx, height, namespace) - } - return nil, errors.New("not implemented") +func (m *mockBlobAPI) GetProof(ctx context.Context, height uint64, namespace share.Namespace, commitment blob.Commitment) (*blob.Proof, error) { + return m.proof, m.submitErr } -func (m *mockDA) Get(ctx context.Context, ids []coreda.ID, namespace []byte) ([]coreda.Blob, error) { - if m.getFunc != nil { - return m.getFunc(ctx, ids, namespace) - } - return nil, errors.New("not implemented") +func (m *mockBlobAPI) Included(ctx context.Context, height uint64, namespace share.Namespace, proof *blob.Proof, commitment blob.Commitment) (bool, error) { + return m.included, m.submitErr } -func (m *mockDA) GetProofs(ctx context.Context, ids []coreda.ID, namespace []byte) ([]coreda.Proof, error) { - return nil, errors.New("not implemented") -} - -func (m *mockDA) Commit(ctx context.Context, blobs []coreda.Blob, namespace []byte) ([]coreda.Commitment, error) { - return nil, errors.New("not implemented") -} - -func (m *mockDA) Validate(ctx context.Context, ids []coreda.ID, proofs []coreda.Proof, namespace []byte) ([]bool, error) { - return nil, errors.New("not implemented") -} - -func TestNewClient(t *testing.T) { - tests := []struct { - name string - cfg Config +func TestClient_Submit_ErrorMapping(t *testing.T) { + ns := share.MustNewV0Namespace([]byte("ns")).Bytes() + testCases := []struct { + name string + err error + wantStatus datypes.StatusCode }{ - { - name: "with all namespaces", - cfg: Config{ - DA: &mockDA{}, - Logger: zerolog.Nop(), - DefaultTimeout: 5 * time.Second, - Namespace: "test-ns", - DataNamespace: "test-data-ns", - }, - }, - { - name: "without forced inclusion namespace", - cfg: Config{ - DA: &mockDA{}, - Logger: zerolog.Nop(), - DefaultTimeout: 5 * time.Second, - Namespace: "test-ns", - DataNamespace: "test-data-ns", - }, - }, - { - name: "with default timeout", - cfg: Config{ - DA: &mockDA{}, - Logger: zerolog.Nop(), - Namespace: "test-ns", - DataNamespace: "test-data-ns", - }, - }, + {"timeout", datypes.ErrTxTimedOut, datypes.StatusNotIncludedInBlock}, + {"alreadyInMempool", datypes.ErrTxAlreadyInMempool, datypes.StatusAlreadyInMempool}, + {"seq", datypes.ErrTxIncorrectAccountSequence, datypes.StatusIncorrectAccountSequence}, + {"tooBig", datypes.ErrBlobSizeOverLimit, datypes.StatusTooBig}, + {"deadline", datypes.ErrContextDeadline, datypes.StatusContextDeadline}, + {"other", errors.New("boom"), datypes.StatusError}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - client := NewClient(tt.cfg) - assert.Assert(t, client != nil) - assert.Assert(t, client.da != nil) - assert.Assert(t, len(client.namespaceBz) > 0) - assert.Assert(t, len(client.namespaceDataBz) > 0) - - expectedTimeout := tt.cfg.DefaultTimeout - if expectedTimeout == 0 { - expectedTimeout = 60 * time.Second - } - assert.Equal(t, client.defaultTimeout, expectedTimeout) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cl := NewClient(Config{ + BlobAPI: &mockBlobAPI{submitErr: tc.err}, + Logger: zerolog.Nop(), + Namespace: "ns", + DataNamespace: "ns", + }) + res := cl.Submit(context.Background(), [][]byte{[]byte("data")}, ns, nil) + assert.Equal(t, tc.wantStatus, res.Code) }) } } -func TestClient_GetNamespaces(t *testing.T) { - cfg := Config{ - DA: &mockDA{}, +func TestClient_Submit_Success(t *testing.T) { + ns := share.MustNewV0Namespace([]byte("ns")).Bytes() + mockAPI := &mockBlobAPI{height: 10} + cl := NewClient(Config{ + BlobAPI: mockAPI, Logger: zerolog.Nop(), - Namespace: "test-header", - DataNamespace: "test-data", - } - - client := NewClient(cfg) - - headerNs := client.GetHeaderNamespace() - assert.Assert(t, len(headerNs) > 0) - - dataNs := client.GetDataNamespace() - assert.Assert(t, len(dataNs) > 0) - - // Namespaces should be different - assert.Assert(t, string(headerNs) != string(dataNs)) + Namespace: "ns", + DataNamespace: "ns", + }) + res := cl.Submit(context.Background(), [][]byte{[]byte("data")}, ns, nil) + require.Equal(t, datypes.StatusSuccess, res.Code) + require.Equal(t, uint64(10), res.Height) + require.Len(t, res.IDs, 1) } -func TestClient_GetDA(t *testing.T) { - mockDAInstance := &mockDA{} - cfg := Config{ - DA: mockDAInstance, +func TestClient_Submit_InvalidNamespace(t *testing.T) { + mockAPI := &mockBlobAPI{height: 10} + cl := NewClient(Config{ + BlobAPI: mockAPI, Logger: zerolog.Nop(), - Namespace: "test-ns", - DataNamespace: "test-data-ns", - } - - client := NewClient(cfg) - da := client.GetDA() - assert.Equal(t, da, mockDAInstance) + Namespace: "ns", + DataNamespace: "ns", + }) + res := cl.Submit(context.Background(), [][]byte{[]byte("data")}, []byte{0x01, 0x02}, nil) + require.Equal(t, datypes.StatusError, res.Code) } -func TestClient_Submit(t *testing.T) { - logger := zerolog.Nop() - - testCases := []struct { - name string - data [][]byte - gasPrice float64 - options []byte - submitErr error - submitIDs [][]byte - expectedCode coreda.StatusCode - expectedErrMsg string - expectedIDs [][]byte - expectedCount uint64 - }{ - { - name: "successful submission", - data: [][]byte{[]byte("blob1"), []byte("blob2")}, - gasPrice: 1.0, - options: []byte("opts"), - submitIDs: [][]byte{[]byte("id1"), []byte("id2")}, - expectedCode: coreda.StatusSuccess, - expectedIDs: [][]byte{[]byte("id1"), []byte("id2")}, - expectedCount: 2, - }, - { - name: "context canceled error", - data: [][]byte{[]byte("blob1")}, - gasPrice: 1.0, - options: []byte("opts"), - submitErr: context.Canceled, - expectedCode: coreda.StatusContextCanceled, - expectedErrMsg: "submission canceled", - }, - { - name: "tx timed out error", - data: [][]byte{[]byte("blob1")}, - gasPrice: 1.0, - options: []byte("opts"), - submitErr: coreda.ErrTxTimedOut, - expectedCode: coreda.StatusNotIncludedInBlock, - expectedErrMsg: "failed to submit blobs: " + coreda.ErrTxTimedOut.Error(), - }, - { - name: "tx already in mempool error", - data: [][]byte{[]byte("blob1")}, - gasPrice: 1.0, - options: []byte("opts"), - submitErr: coreda.ErrTxAlreadyInMempool, - expectedCode: coreda.StatusAlreadyInMempool, - expectedErrMsg: "failed to submit blobs: " + coreda.ErrTxAlreadyInMempool.Error(), - }, - { - name: "incorrect account sequence error", - data: [][]byte{[]byte("blob1")}, - gasPrice: 1.0, - options: []byte("opts"), - submitErr: coreda.ErrTxIncorrectAccountSequence, - expectedCode: coreda.StatusIncorrectAccountSequence, - expectedErrMsg: "failed to submit blobs: " + coreda.ErrTxIncorrectAccountSequence.Error(), - }, - { - name: "blob size over limit error", - data: [][]byte{[]byte("blob1")}, - gasPrice: 1.0, - options: []byte("opts"), - submitErr: coreda.ErrBlobSizeOverLimit, - expectedCode: coreda.StatusTooBig, - expectedErrMsg: "failed to submit blobs: " + coreda.ErrBlobSizeOverLimit.Error(), - }, - { - name: "context deadline error", - data: [][]byte{[]byte("blob1")}, - gasPrice: 1.0, - options: []byte("opts"), - submitErr: coreda.ErrContextDeadline, - expectedCode: coreda.StatusContextDeadline, - expectedErrMsg: "failed to submit blobs: " + coreda.ErrContextDeadline.Error(), - }, - { - name: "generic submission error", - data: [][]byte{[]byte("blob1")}, - gasPrice: 1.0, - options: []byte("opts"), - submitErr: errors.New("some generic error"), - expectedCode: coreda.StatusError, - expectedErrMsg: "failed to submit blobs: some generic error", - }, - { - name: "no IDs returned for non-empty data", - data: [][]byte{[]byte("blob1")}, - gasPrice: 1.0, - options: []byte("opts"), - submitIDs: [][]byte{}, - expectedCode: coreda.StatusError, - expectedErrMsg: "failed to submit blobs: no IDs returned despite non-empty input", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - mockDAInstance := &mockDA{ - submitWithOptions: func(ctx context.Context, blobs []coreda.Blob, gasPrice float64, namespace []byte, options []byte) ([]coreda.ID, error) { - return tc.submitIDs, tc.submitErr - }, - } - - client := NewClient(Config{ - DA: mockDAInstance, - Logger: logger, - Namespace: "test-namespace", - DataNamespace: "test-data-namespace", - }) - - encodedNamespace := coreda.NamespaceFromString("test-namespace") - result := client.Submit(context.Background(), tc.data, tc.gasPrice, encodedNamespace.Bytes(), tc.options) - - assert.Equal(t, tc.expectedCode, result.Code) - if tc.expectedErrMsg != "" { - assert.Assert(t, result.Message != "") - } - if tc.expectedIDs != nil { - assert.Equal(t, len(tc.expectedIDs), len(result.IDs)) - } - if tc.expectedCount != 0 { - assert.Equal(t, tc.expectedCount, result.SubmittedCount) - } - }) - } +func TestClient_Retrieve_NotFound(t *testing.T) { + ns := share.MustNewV0Namespace([]byte("ns")).Bytes() + mockAPI := &mockBlobAPI{submitErr: datypes.ErrBlobNotFound} + cl := NewClient(Config{ + BlobAPI: mockAPI, + Logger: zerolog.Nop(), + Namespace: "ns", + DataNamespace: "ns", + }) + res := cl.Retrieve(context.Background(), 5, ns) + require.Equal(t, datypes.StatusNotFound, res.Code) } -func TestClient_Retrieve(t *testing.T) { - logger := zerolog.Nop() - dataLayerHeight := uint64(100) - mockIDs := [][]byte{[]byte("id1"), []byte("id2")} - mockBlobs := [][]byte{[]byte("blobA"), []byte("blobB")} - mockTimestamp := time.Now() - - testCases := []struct { - name string - getIDsResult *coreda.GetIDsResult - getIDsErr error - getBlobsErr error - expectedCode coreda.StatusCode - expectedErrMsg string - expectedIDs [][]byte - expectedData [][]byte - expectedHeight uint64 - }{ - { - name: "successful retrieval", - getIDsResult: &coreda.GetIDsResult{ - IDs: mockIDs, - Timestamp: mockTimestamp, - }, - expectedCode: coreda.StatusSuccess, - expectedIDs: mockIDs, - expectedData: mockBlobs, - expectedHeight: dataLayerHeight, - }, - { - name: "blob not found error during GetIDs", - getIDsErr: coreda.ErrBlobNotFound, - expectedCode: coreda.StatusNotFound, - expectedErrMsg: coreda.ErrBlobNotFound.Error(), - expectedHeight: dataLayerHeight, - }, - { - name: "height from future error during GetIDs", - getIDsErr: coreda.ErrHeightFromFuture, - expectedCode: coreda.StatusHeightFromFuture, - expectedErrMsg: coreda.ErrHeightFromFuture.Error(), - expectedHeight: dataLayerHeight, - }, - { - name: "generic error during GetIDs", - getIDsErr: errors.New("failed to connect to DA"), - expectedCode: coreda.StatusError, - expectedErrMsg: "failed to get IDs: failed to connect to DA", - expectedHeight: dataLayerHeight, - }, - { - name: "GetIDs returns nil result", - getIDsResult: nil, - expectedCode: coreda.StatusNotFound, - expectedErrMsg: coreda.ErrBlobNotFound.Error(), - expectedHeight: dataLayerHeight, - }, - { - name: "GetIDs returns empty IDs", - getIDsResult: &coreda.GetIDsResult{ - IDs: [][]byte{}, - Timestamp: mockTimestamp, - }, - expectedCode: coreda.StatusNotFound, - expectedErrMsg: coreda.ErrBlobNotFound.Error(), - expectedHeight: dataLayerHeight, - }, - { - name: "error during Get (blobs retrieval)", - getIDsResult: &coreda.GetIDsResult{ - IDs: mockIDs, - Timestamp: mockTimestamp, - }, - getBlobsErr: errors.New("network error during blob retrieval"), - expectedCode: coreda.StatusError, - expectedErrMsg: "failed to get blobs for batch 0-1: network error during blob retrieval", - expectedHeight: dataLayerHeight, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - mockDAInstance := &mockDA{ - getIDsFunc: func(ctx context.Context, height uint64, namespace []byte) (*coreda.GetIDsResult, error) { - return tc.getIDsResult, tc.getIDsErr - }, - getFunc: func(ctx context.Context, ids []coreda.ID, namespace []byte) ([]coreda.Blob, error) { - if tc.getBlobsErr != nil { - return nil, tc.getBlobsErr - } - return mockBlobs, nil - }, - } - - client := NewClient(Config{ - DA: mockDAInstance, - Logger: logger, - Namespace: "test-namespace", - DataNamespace: "test-data-namespace", - DefaultTimeout: 5 * time.Second, - }) - - encodedNamespace := coreda.NamespaceFromString("test-namespace") - result := client.Retrieve(context.Background(), dataLayerHeight, encodedNamespace.Bytes()) - - assert.Equal(t, tc.expectedCode, result.Code) - assert.Equal(t, tc.expectedHeight, result.Height) - if tc.expectedErrMsg != "" { - assert.Assert(t, result.Message != "") - } - if tc.expectedIDs != nil { - assert.Equal(t, len(tc.expectedIDs), len(result.IDs)) - } - if tc.expectedData != nil { - assert.Equal(t, len(tc.expectedData), len(result.Data)) - } - }) - } +func TestClient_Retrieve_Success(t *testing.T) { + ns := share.MustNewV0Namespace([]byte("ns")).Bytes() + b, err := blob.NewBlobV0(share.MustNewV0Namespace([]byte("ns")), []byte("payload")) + require.NoError(t, err) + mockAPI := &mockBlobAPI{height: 7, blobs: []*blob.Blob{b}} + cl := NewClient(Config{ + BlobAPI: mockAPI, + Logger: zerolog.Nop(), + Namespace: "ns", + DataNamespace: "ns", + }) + res := cl.Retrieve(context.Background(), 7, ns) + require.Equal(t, datypes.StatusSuccess, res.Code) + require.Len(t, res.Data, 1) + require.Len(t, res.IDs, 1) } -func TestClient_Retrieve_Timeout(t *testing.T) { - logger := zerolog.Nop() - dataLayerHeight := uint64(100) - encodedNamespace := coreda.NamespaceFromString("test-namespace") - - t.Run("timeout during GetIDs", func(t *testing.T) { - mockDAInstance := &mockDA{ - getIDsFunc: func(ctx context.Context, height uint64, namespace []byte) (*coreda.GetIDsResult, error) { - <-ctx.Done() // Wait for context cancellation - return nil, context.DeadlineExceeded - }, - } - - client := NewClient(Config{ - DA: mockDAInstance, - Logger: logger, - Namespace: "test-namespace", - DataNamespace: "test-data-namespace", - DefaultTimeout: 1 * time.Millisecond, - }) - - result := client.Retrieve(context.Background(), dataLayerHeight, encodedNamespace.Bytes()) - - assert.Equal(t, coreda.StatusError, result.Code) - assert.Assert(t, result.Message != "") +func TestClient_SubmitOptionsMerge(t *testing.T) { + ns := share.MustNewV0Namespace([]byte("ns")).Bytes() + mockAPI := &mockBlobAPI{height: 1} + cl := NewClient(Config{ + BlobAPI: mockAPI, + Logger: zerolog.Nop(), + Namespace: "ns", + DataNamespace: "ns", }) - t.Run("timeout during Get", func(t *testing.T) { - mockIDs := [][]byte{[]byte("id1")} - mockTimestamp := time.Now() - - mockDAInstance := &mockDA{ - getIDsFunc: func(ctx context.Context, height uint64, namespace []byte) (*coreda.GetIDsResult, error) { - return &coreda.GetIDsResult{ - IDs: mockIDs, - Timestamp: mockTimestamp, - }, nil - }, - getFunc: func(ctx context.Context, ids []coreda.ID, namespace []byte) ([]coreda.Blob, error) { - <-ctx.Done() // Wait for context cancellation - return nil, context.DeadlineExceeded - }, - } + opts := map[string]any{"signer_address": "celestia1xyz"} + raw, err := json.Marshal(opts) + require.NoError(t, err) - client := NewClient(Config{ - DA: mockDAInstance, - Logger: logger, - Namespace: "test-namespace", - DataNamespace: "test-data-namespace", - DefaultTimeout: 1 * time.Millisecond, - }) - - result := client.Retrieve(context.Background(), dataLayerHeight, encodedNamespace.Bytes()) - - assert.Equal(t, coreda.StatusError, result.Code) - assert.Assert(t, result.Message != "") - }) + res := cl.Submit(context.Background(), [][]byte{[]byte("data")}, ns, raw) + require.Equal(t, datypes.StatusSuccess, res.Code) } diff --git a/block/internal/da/local_blob_api.go b/block/internal/da/local_blob_api.go new file mode 100644 index 0000000000..f174b8315f --- /dev/null +++ b/block/internal/da/local_blob_api.go @@ -0,0 +1,80 @@ +package da + +import ( + "context" + "fmt" + "sync" + + "github.com/celestiaorg/go-square/v3/share" + + "github.com/evstack/ev-node/pkg/blob" +) + +// LocalBlobAPI is a simple in-memory BlobAPI implementation for tests. +type LocalBlobAPI struct { + mu sync.Mutex + height uint64 + maxSize uint64 + byHeight map[uint64][]*blob.Blob +} + +// NewLocalBlobAPI creates an in-memory BlobAPI with a max blob size. +func NewLocalBlobAPI(maxSize uint64) *LocalBlobAPI { + return &LocalBlobAPI{ + maxSize: maxSize, + byHeight: make(map[uint64][]*blob.Blob), + } +} + +func (l *LocalBlobAPI) Submit(ctx context.Context, blobs []*blob.Blob, _ *blob.SubmitOptions) (uint64, error) { + l.mu.Lock() + defer l.mu.Unlock() + + for i, b := range blobs { + if uint64(len(b.Data())) > l.maxSize { + return 0, fmt.Errorf("blob %d too big", i) + } + } + + l.height++ + // store clones to avoid external mutation + stored := make([]*blob.Blob, len(blobs)) + copy(stored, blobs) + l.byHeight[l.height] = append(l.byHeight[l.height], stored...) + return l.height, nil +} + +func (l *LocalBlobAPI) GetAll(ctx context.Context, height uint64, namespaces []share.Namespace) ([]*blob.Blob, error) { + l.mu.Lock() + defer l.mu.Unlock() + + // Return error if requesting a height from the future + if height > l.height { + return nil, fmt.Errorf("height %d is in the future (current: %d): given height is from the future", height, l.height) + } + + nsMap := make(map[string]struct{}, len(namespaces)) + for _, ns := range namespaces { + nsMap[string(ns.Bytes())] = struct{}{} + } + + blobs, ok := l.byHeight[height] + if !ok { + return []*blob.Blob{}, nil + } + var out []*blob.Blob + for _, b := range blobs { + if _, ok := nsMap[string(b.Namespace().Bytes())]; ok { + out = append(out, b) + } + } + return out, nil +} + +func (l *LocalBlobAPI) GetProof(ctx context.Context, height uint64, namespace share.Namespace, commitment blob.Commitment) (*blob.Proof, error) { + return &blob.Proof{}, nil +} + +func (l *LocalBlobAPI) Included(ctx context.Context, height uint64, namespace share.Namespace, proof *blob.Proof, commitment blob.Commitment) (bool, error) { + return true, nil +} diff --git a/block/internal/submitting/da_submitter.go b/block/internal/submitting/da_submitter.go index 34d6951d81..82819b8092 100644 --- a/block/internal/submitting/da_submitter.go +++ b/block/internal/submitting/da_submitter.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "sync" "time" "github.com/rs/zerolog" @@ -13,9 +14,10 @@ import ( "github.com/evstack/ev-node/block/internal/cache" "github.com/evstack/ev-node/block/internal/common" "github.com/evstack/ev-node/block/internal/da" - coreda "github.com/evstack/ev-node/core/da" + "github.com/evstack/ev-node/pkg/blob" "github.com/evstack/ev-node/pkg/config" pkgda "github.com/evstack/ev-node/pkg/da" + datypes "github.com/evstack/ev-node/pkg/da/types" "github.com/evstack/ev-node/pkg/genesis" "github.com/evstack/ev-node/pkg/rpc/server" "github.com/evstack/ev-node/pkg/signer" @@ -116,7 +118,21 @@ func NewDASubmitter( if config.RPC.EnableDAVisualization { visualizerLogger := logger.With().Str("component", "da_visualization").Logger() - server.SetDAVisualizationServer(server.NewDAVisualizationServer(client.GetDA(), visualizerLogger, config.Node.Aggregator)) + server.SetDAVisualizationServer( + server.NewDAVisualizationServer( + func(ctx context.Context, id []byte, ns []byte) ([]datypes.Blob, error) { + // minimal fetch: derive height from ID and use namespace provided + height, _ := blob.SplitID(id) + res := client.Retrieve(ctx, height, ns) + if res.Code != datypes.StatusSuccess || len(res.Data) == 0 { + return nil, fmt.Errorf("blob not found") + } + return res.Data, nil + }, + visualizerLogger, + config.Node.Aggregator, + ), + ) } // Use NoOp metrics if nil to avoid nil checks throughout the code @@ -181,7 +197,7 @@ func (s *DASubmitter) SubmitHeaders(ctx context.Context, cache cache.Manager) er } return proto.Marshal(headerPb) }, - func(submitted []*types.SignedHeader, res *coreda.ResultSubmit) { + func(submitted []*types.SignedHeader, res *datypes.ResultSubmit) { for _, header := range submitted { cache.SetHeaderDAIncluded(header.Hash().String(), res.Height, header.Height()) } @@ -224,7 +240,7 @@ func (s *DASubmitter) SubmitData(ctx context.Context, cache cache.Manager, signe func(signedData *types.SignedData) ([]byte, error) { return signedData.MarshalBinary() }, - func(submitted []*types.SignedData, res *coreda.ResultSubmit) { + func(submitted []*types.SignedData, res *datypes.ResultSubmit) { for _, sd := range submitted { cache.SetDataDAIncluded(sd.Data.DACommitment().String(), res.Height, sd.Height()) } @@ -340,7 +356,7 @@ func submitToDA[T any]( ctx context.Context, items []T, marshalFn func(T) ([]byte, error), - postSubmit func([]T, *coreda.ResultSubmit), + postSubmit func([]T, *datypes.ResultSubmit), itemType string, namespace []byte, options []byte, @@ -400,16 +416,16 @@ func submitToDA[T any]( // Perform submission start := time.Now() - res := s.client.Submit(ctx, marshaled, -1, namespace, mergedOptions) + res := s.client.Submit(ctx, marshaled, namespace, mergedOptions) s.logger.Debug().Int("attempts", rs.Attempt).Dur("elapsed", time.Since(start)).Uint64("code", uint64(res.Code)).Msg("got SubmitWithHelpers response from celestia") // Record submission result for observability if daVisualizationServer := server.GetDAVisualizationServer(); daVisualizationServer != nil { - daVisualizationServer.RecordSubmission(&res, 0, uint64(len(items)), namespace) + daVisualizationServer.RecordSubmission(&res, 0, uint64(len(items))) } switch res.Code { - case coreda.StatusSuccess: + case datypes.StatusSuccess: submitted := items[:res.SubmittedCount] postSubmit(submitted, &res) s.logger.Info().Str("itemType", itemType).Uint64("count", res.SubmittedCount).Msg("successfully submitted items to DA layer") @@ -430,7 +446,7 @@ func submitToDA[T any]( s.metrics.DASubmitterPendingBlobs.Set(float64(getTotalPendingFn())) } - case coreda.StatusTooBig: + case datypes.StatusTooBig: // Record failure metric s.recordFailure(common.DASubmitterFailureReasonTooBig) // Iteratively halve until it fits or single-item too big @@ -454,19 +470,19 @@ func submitToDA[T any]( s.metrics.DASubmitterPendingBlobs.Set(float64(getTotalPendingFn())) } - case coreda.StatusNotIncludedInBlock: + case datypes.StatusNotIncludedInBlock: // Record failure metric s.recordFailure(common.DASubmitterFailureReasonNotIncludedInBlock) s.logger.Info().Dur("backoff", pol.MaxBackoff).Msg("retrying due to mempool state") rs.Next(reasonMempool, pol) - case coreda.StatusAlreadyInMempool: + case datypes.StatusAlreadyInMempool: // Record failure metric s.recordFailure(common.DASubmitterFailureReasonAlreadyInMempool) s.logger.Info().Dur("backoff", pol.MaxBackoff).Msg("retrying due to mempool state") rs.Next(reasonMempool, pol) - case coreda.StatusContextCanceled: + case datypes.StatusContextCanceled: // Record failure metric s.recordFailure(common.DASubmitterFailureReasonContextCanceled) s.logger.Info().Msg("DA layer submission canceled due to context cancellation") @@ -530,9 +546,18 @@ func marshalItems[T any]( resultCh := make(chan error, len(items)) // Marshal items concurrently + var wg sync.WaitGroup for i, item := range items { + wg.Add(1) go func(idx int, itm T) { - sem <- struct{}{} + defer wg.Done() + + select { + case <-ctx.Done(): + resultCh <- ctx.Err() + return + case sem <- struct{}{}: + } defer func() { <-sem }() select { @@ -542,6 +567,7 @@ func marshalItems[T any]( bz, err := marshalFn(itm) if err != nil { resultCh <- fmt.Errorf("failed to marshal %s item at index %d: %w", itemType, idx, err) + cancel() return } marshaled[idx] = bz @@ -551,6 +577,7 @@ func marshalItems[T any]( } // Wait for all goroutines to complete and check for errors + defer wg.Wait() for i := 0; i < len(items); i++ { if err := <-resultCh; err != nil { return nil, err diff --git a/block/internal/submitting/da_submitter_integration_test.go b/block/internal/submitting/da_submitter_integration_test.go index 5b768e1a51..0f64ce42c4 100644 --- a/block/internal/submitting/da_submitter_integration_test.go +++ b/block/internal/submitting/da_submitter_integration_test.go @@ -1,3 +1,5 @@ +//go:build !ignore + package submitting import ( @@ -16,7 +18,6 @@ import ( "github.com/evstack/ev-node/block/internal/cache" "github.com/evstack/ev-node/block/internal/common" "github.com/evstack/ev-node/block/internal/da" - coreda "github.com/evstack/ev-node/core/da" "github.com/evstack/ev-node/pkg/config" "github.com/evstack/ev-node/pkg/genesis" "github.com/evstack/ev-node/pkg/signer/noop" @@ -83,15 +84,13 @@ func TestDASubmitter_SubmitHeadersAndData_MarksInclusionAndUpdatesLastSubmitted( require.NoError(t, batch2.SetHeight(2)) require.NoError(t, batch2.Commit()) - // Dummy DA - dummyDA := coreda.NewDummyDA(10_000_000, 10*time.Millisecond) - - // Create DA submitter + // Create DA submitter with local blob API daClient := da.NewClient(da.Config{ - DA: dummyDA, - Logger: zerolog.Nop(), - Namespace: cfg.DA.Namespace, - DataNamespace: cfg.DA.DataNamespace, + BlobAPI: da.NewLocalBlobAPI(common.DefaultMaxBlobSize), + Logger: zerolog.Nop(), + Namespace: cfg.DA.Namespace, + DataNamespace: cfg.DA.DataNamespace, + DefaultTimeout: 10 * time.Second, }) daSubmitter := NewDASubmitter(daClient, cfg, gen, common.DefaultBlockOptions(), common.NopMetrics(), zerolog.Nop()) diff --git a/block/internal/submitting/da_submitter_mocks_test.go b/block/internal/submitting/da_submitter_mocks_test.go deleted file mode 100644 index b215b0cf2f..0000000000 --- a/block/internal/submitting/da_submitter_mocks_test.go +++ /dev/null @@ -1,271 +0,0 @@ -package submitting - -import ( - "context" - "errors" - "testing" - "time" - - "github.com/rs/zerolog" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - - "github.com/evstack/ev-node/block/internal/common" - "github.com/evstack/ev-node/block/internal/da" - coreda "github.com/evstack/ev-node/core/da" - "github.com/evstack/ev-node/pkg/config" - "github.com/evstack/ev-node/pkg/genesis" - "github.com/evstack/ev-node/test/mocks" -) - -// helper to build a basic submitter with provided DA mock and config overrides -func newTestSubmitter(mockDA *mocks.MockDA, override func(*config.Config)) *DASubmitter { - cfg := config.Config{} - // Keep retries small and backoffs minimal - cfg.DA.BlockTime.Duration = 1 * time.Millisecond - cfg.DA.MaxSubmitAttempts = 3 - cfg.DA.SubmitOptions = "opts" - cfg.DA.Namespace = "ns" - cfg.DA.DataNamespace = "ns-data" - if override != nil { - override(&cfg) - } - daClient := da.NewClient(da.Config{ - DA: mockDA, - Logger: zerolog.Nop(), - Namespace: cfg.DA.Namespace, - DataNamespace: cfg.DA.DataNamespace, - }) - return NewDASubmitter(daClient, cfg, genesis.Genesis{} /*options=*/, common.BlockOptions{}, common.NopMetrics(), zerolog.Nop()) -} - -// marshal helper for simple items -func marshalString(s string) ([]byte, error) { return []byte(s), nil } - -func TestSubmitToDA_MempoolRetry_IncreasesGasAndSucceeds(t *testing.T) { - t.Parallel() - - mockDA := mocks.NewMockDA(t) - - nsBz := coreda.NamespaceFromString("ns").Bytes() - opts := []byte("opts") - var usedGas []float64 - mockDA. - On("SubmitWithOptions", mock.Anything, mock.Anything, mock.AnythingOfType("float64"), nsBz, opts). - Run(func(args mock.Arguments) { - usedGas = append(usedGas, args.Get(2).(float64)) - }). - Return(nil, coreda.ErrTxTimedOut). - Once() - - ids := [][]byte{[]byte("id1"), []byte("id2"), []byte("id3")} - mockDA. - On("SubmitWithOptions", mock.Anything, mock.Anything, mock.AnythingOfType("float64"), nsBz, opts). - Run(func(args mock.Arguments) { - usedGas = append(usedGas, args.Get(2).(float64)) - }). - Return(ids, nil). - Once() - - s := newTestSubmitter(mockDA, nil) - - items := []string{"a", "b", "c"} - ctx := context.Background() - err := submitToDA[string]( - s, - ctx, - items, - marshalString, - func(_ []string, _ *coreda.ResultSubmit) {}, - "item", - nsBz, - opts, - nil, - ) - assert.NoError(t, err) - - // Sentinel value is preserved on retry - assert.Equal(t, []float64{-1, -1}, usedGas) - mockDA.AssertExpectations(t) -} - -func TestSubmitToDA_UnknownError_RetriesSameGasThenSucceeds(t *testing.T) { - t.Parallel() - - mockDA := mocks.NewMockDA(t) - - nsBz := coreda.NamespaceFromString("ns").Bytes() - - opts := []byte("opts") - var usedGas []float64 - - // First attempt: unknown failure -> reasonFailure, gas unchanged for next attempt - mockDA. - On("SubmitWithOptions", mock.Anything, mock.Anything, mock.AnythingOfType("float64"), nsBz, opts). - Run(func(args mock.Arguments) { usedGas = append(usedGas, args.Get(2).(float64)) }). - Return(nil, errors.New("boom")). - Once() - - // Second attempt: same gas, success - ids := [][]byte{[]byte("id1")} - mockDA. - On("SubmitWithOptions", mock.Anything, mock.Anything, mock.AnythingOfType("float64"), nsBz, opts). - Run(func(args mock.Arguments) { usedGas = append(usedGas, args.Get(2).(float64)) }). - Return(ids, nil). - Once() - - s := newTestSubmitter(mockDA, nil) - - items := []string{"x"} - ctx := context.Background() - err := submitToDA[string]( - s, - ctx, - items, - marshalString, - func(_ []string, _ *coreda.ResultSubmit) {}, - "item", - nsBz, - opts, - nil, - ) - assert.NoError(t, err) - assert.Equal(t, []float64{-1, -1}, usedGas) - mockDA.AssertExpectations(t) -} - -func TestSubmitToDA_TooBig_HalvesBatch(t *testing.T) { - t.Parallel() - - mockDA := mocks.NewMockDA(t) - - nsBz := coreda.NamespaceFromString("ns").Bytes() - - opts := []byte("opts") - // record sizes of batches sent to DA - var batchSizes []int - - // First attempt: too big -> should halve and retry - mockDA. - On("SubmitWithOptions", mock.Anything, mock.Anything, mock.Anything, nsBz, opts). - Run(func(args mock.Arguments) { - blobs := args.Get(1).([][]byte) - batchSizes = append(batchSizes, len(blobs)) - }). - Return(nil, coreda.ErrBlobSizeOverLimit). - Once() - - // Second attempt: expect half the size, succeed - ids := [][]byte{[]byte("id1"), []byte("id2")} - mockDA. - On("SubmitWithOptions", mock.Anything, mock.Anything, mock.Anything, nsBz, opts). - Run(func(args mock.Arguments) { - blobs := args.Get(1).([][]byte) - batchSizes = append(batchSizes, len(blobs)) - }). - Return(ids, nil). - Once() - - s := newTestSubmitter(mockDA, nil) - - items := []string{"a", "b", "c", "d"} - ctx := context.Background() - err := submitToDA[string]( - s, - ctx, - items, - marshalString, - func(_ []string, _ *coreda.ResultSubmit) {}, - "item", - nsBz, - opts, - nil, - ) - assert.NoError(t, err) - assert.Equal(t, []int{4, 2}, batchSizes) - mockDA.AssertExpectations(t) -} - -func TestSubmitToDA_SentinelNoGas_PreservesGasAcrossRetries(t *testing.T) { - t.Parallel() - - mockDA := mocks.NewMockDA(t) - - nsBz := coreda.NamespaceFromString("ns").Bytes() - - opts := []byte("opts") - var usedGas []float64 - - // First attempt: mempool-ish error - mockDA. - On("SubmitWithOptions", mock.Anything, mock.Anything, mock.AnythingOfType("float64"), nsBz, opts). - Run(func(args mock.Arguments) { usedGas = append(usedGas, args.Get(2).(float64)) }). - Return(nil, coreda.ErrTxAlreadyInMempool). - Once() - - // Second attempt: should use same sentinel gas (-1), succeed - ids := [][]byte{[]byte("id1")} - mockDA. - On("SubmitWithOptions", mock.Anything, mock.Anything, mock.AnythingOfType("float64"), nsBz, opts). - Run(func(args mock.Arguments) { usedGas = append(usedGas, args.Get(2).(float64)) }). - Return(ids, nil). - Once() - - s := newTestSubmitter(mockDA, nil) - - items := []string{"only"} - ctx := context.Background() - err := submitToDA[string]( - s, - ctx, - items, - marshalString, - func(_ []string, _ *coreda.ResultSubmit) {}, - "item", - nsBz, - opts, - nil, - ) - assert.NoError(t, err) - assert.Equal(t, []float64{-1, -1}, usedGas) - mockDA.AssertExpectations(t) -} - -func TestSubmitToDA_PartialSuccess_AdvancesWindow(t *testing.T) { - t.Parallel() - - mockDA := mocks.NewMockDA(t) - - nsBz := coreda.NamespaceFromString("ns").Bytes() - - opts := []byte("opts") - // track how many items postSubmit sees across attempts - var totalSubmitted int - - // First attempt: success for first 2 of 3 - firstIDs := [][]byte{[]byte("id1"), []byte("id2")} - mockDA.On("SubmitWithOptions", mock.Anything, mock.Anything, mock.Anything, nsBz, opts).Return(firstIDs, nil).Once() - - // Second attempt: success for remaining 1 - secondIDs := [][]byte{[]byte("id3")} - mockDA.On("SubmitWithOptions", mock.Anything, mock.Anything, mock.Anything, nsBz, opts).Return(secondIDs, nil).Once() - - s := newTestSubmitter(mockDA, nil) - - items := []string{"a", "b", "c"} - ctx := context.Background() - err := submitToDA[string]( - s, - ctx, - items, - marshalString, - func(submitted []string, _ *coreda.ResultSubmit) { totalSubmitted += len(submitted) }, - "item", - nsBz, - opts, - nil, - ) - assert.NoError(t, err) - assert.Equal(t, 3, totalSubmitted) - mockDA.AssertExpectations(t) -} diff --git a/block/internal/submitting/da_submitter_retry_test.go b/block/internal/submitting/da_submitter_retry_test.go new file mode 100644 index 0000000000..3e907f1df1 --- /dev/null +++ b/block/internal/submitting/da_submitter_retry_test.go @@ -0,0 +1,102 @@ +package submitting + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/celestiaorg/go-square/v3/share" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/evstack/ev-node/block/internal/common" + "github.com/evstack/ev-node/block/internal/da" + "github.com/evstack/ev-node/pkg/blob" + "github.com/evstack/ev-node/pkg/config" + datypes "github.com/evstack/ev-node/pkg/da/types" + "github.com/evstack/ev-node/pkg/genesis" +) + +type mockBlobAPI struct{ mock.Mock } + +func (m *mockBlobAPI) Submit(ctx context.Context, blobs []*blob.Blob, opts *blob.SubmitOptions) (uint64, error) { + args := m.Called(ctx, blobs, opts) + return args.Get(0).(uint64), args.Error(1) +} +func (m *mockBlobAPI) GetAll(ctx context.Context, height uint64, namespaces []share.Namespace) ([]*blob.Blob, error) { + return nil, nil +} +func (m *mockBlobAPI) GetProof(ctx context.Context, height uint64, namespace share.Namespace, commitment blob.Commitment) (*blob.Proof, error) { + return nil, nil +} +func (m *mockBlobAPI) Included(ctx context.Context, height uint64, namespace share.Namespace, proof *blob.Proof, commitment blob.Commitment) (bool, error) { + return false, nil +} + +func newRetrySubmitter(api *mockBlobAPI) *DASubmitter { + cfg := config.Config{} + cfg.DA.BlockTime.Duration = 1 * time.Millisecond + cfg.DA.MaxSubmitAttempts = 3 + cfg.DA.SubmitOptions = "opts" + cfg.DA.Namespace = "ns" + cfg.DA.DataNamespace = "ns" + + client := da.NewClient(da.Config{ + BlobAPI: api, + Logger: zerolog.Nop(), + Namespace: cfg.DA.Namespace, + DataNamespace: cfg.DA.DataNamespace, + }) + + return NewDASubmitter(client, cfg, genesis.Genesis{}, common.BlockOptions{}, common.NopMetrics(), zerolog.Nop()) +} + +func TestSubmitToDA_MempoolRetry(t *testing.T) { + api := &mockBlobAPI{} + ns := share.MustNewV0Namespace([]byte("ns")).Bytes() + opts := []byte("{}") + + api.On("Submit", mock.Anything, mock.Anything, mock.AnythingOfType("*blob.SubmitOptions")). + Return(uint64(0), datypes.ErrTxTimedOut).Once() + api.On("Submit", mock.Anything, mock.Anything, mock.AnythingOfType("*blob.SubmitOptions")). + Return(uint64(1), nil).Once() + + s := newRetrySubmitter(api) + err := submitToDA[string](s, context.Background(), []string{"a", "b"}, func(s string) ([]byte, error) { return []byte(s), nil }, func([]string, *datypes.ResultSubmit) {}, "item", ns, opts, nil) + assert.NoError(t, err) + api.AssertNumberOfCalls(t, "Submit", 2) +} + +func TestSubmitToDA_UnknownError_RetryThenSuccess(t *testing.T) { + api := &mockBlobAPI{} + ns := share.MustNewV0Namespace([]byte("ns")).Bytes() + opts := []byte("{}") + + api.On("Submit", mock.Anything, mock.Anything, mock.AnythingOfType("*blob.SubmitOptions")). + Return(uint64(0), errors.New("boom")).Once() + api.On("Submit", mock.Anything, mock.Anything, mock.AnythingOfType("*blob.SubmitOptions")). + Return(uint64(1), nil).Once() + + s := newRetrySubmitter(api) + err := submitToDA[string](s, context.Background(), []string{"x"}, func(s string) ([]byte, error) { return []byte(s), nil }, func([]string, *datypes.ResultSubmit) {}, "item", ns, opts, nil) + assert.NoError(t, err) + api.AssertNumberOfCalls(t, "Submit", 2) +} + +func TestSubmitToDA_TooBig_SplitsAndSucceeds(t *testing.T) { + api := &mockBlobAPI{} + ns := share.MustNewV0Namespace([]byte("ns")).Bytes() + opts := []byte("{}") + + api.On("Submit", mock.Anything, mock.Anything, mock.AnythingOfType("*blob.SubmitOptions")). + Return(uint64(0), datypes.ErrBlobSizeOverLimit).Once() + api.On("Submit", mock.Anything, mock.Anything, mock.AnythingOfType("*blob.SubmitOptions")). + Return(uint64(1), nil).Once() + + s := newRetrySubmitter(api) + err := submitToDA[string](s, context.Background(), []string{"a", "b", "c", "d"}, func(s string) ([]byte, error) { return []byte(s), nil }, func([]string, *datypes.ResultSubmit) {}, "item", ns, opts, nil) + assert.NoError(t, err) + api.AssertNumberOfCalls(t, "Submit", 2) +} diff --git a/block/internal/submitting/da_submitter_test.go b/block/internal/submitting/da_submitter_test.go index 214ab98db4..1d8377757f 100644 --- a/block/internal/submitting/da_submitter_test.go +++ b/block/internal/submitting/da_submitter_test.go @@ -1,3 +1,5 @@ +//go:build !ignore + package submitting import ( @@ -16,7 +18,6 @@ import ( "github.com/evstack/ev-node/block/internal/cache" "github.com/evstack/ev-node/block/internal/common" "github.com/evstack/ev-node/block/internal/da" - coreda "github.com/evstack/ev-node/core/da" "github.com/evstack/ev-node/pkg/config" "github.com/evstack/ev-node/pkg/genesis" "github.com/evstack/ev-node/pkg/rpc/server" @@ -26,22 +27,20 @@ import ( "github.com/evstack/ev-node/types" ) -func setupDASubmitterTest(t *testing.T) (*DASubmitter, store.Store, cache.Manager, coreda.DA, genesis.Genesis) { +func setupDASubmitterTest(t *testing.T) (*DASubmitter, store.Store, cache.Manager, genesis.Genesis) { t.Helper() // Create store and cache ds := sync.MutexWrap(datastore.NewMapDatastore()) st := store.New(ds) - cm, err := cache.NewManager(config.DefaultConfig(), st, zerolog.Nop()) - require.NoError(t, err) - - // Create dummy DA - dummyDA := coreda.NewDummyDA(10_000_000, 10*time.Millisecond) - - // Create config cfg := config.DefaultConfig() cfg.DA.Namespace = "test-headers" cfg.DA.DataNamespace = "test-data" + cm, err := cache.NewManager(cfg, st, zerolog.Nop()) + require.NoError(t, err) + + // Create config + blobAPI := da.NewLocalBlobAPI(common.DefaultMaxBlobSize) // Create genesis gen := genesis.Genesis{ @@ -53,10 +52,11 @@ func setupDASubmitterTest(t *testing.T) (*DASubmitter, store.Store, cache.Manage // Create DA submitter daClient := da.NewClient(da.Config{ - DA: dummyDA, - Logger: zerolog.Nop(), - Namespace: cfg.DA.Namespace, - DataNamespace: cfg.DA.DataNamespace, + BlobAPI: blobAPI, + Logger: zerolog.Nop(), + Namespace: cfg.DA.Namespace, + DataNamespace: cfg.DA.DataNamespace, + DefaultTimeout: 10 * time.Second, }) daSubmitter := NewDASubmitter( daClient, @@ -67,7 +67,7 @@ func setupDASubmitterTest(t *testing.T) (*DASubmitter, store.Store, cache.Manage zerolog.Nop(), ) - return daSubmitter, st, cm, dummyDA, gen + return daSubmitter, st, cm, gen } func createTestSigner(t *testing.T) ([]byte, crypto.PubKey, signer.Signer) { @@ -84,7 +84,7 @@ func createTestSigner(t *testing.T) ([]byte, crypto.PubKey, signer.Signer) { } func TestDASubmitter_NewDASubmitter(t *testing.T) { - submitter, _, _, _, _ := setupDASubmitterTest(t) + submitter, _, _, _ := setupDASubmitterTest(t) assert.NotNil(t, submitter) assert.NotNil(t, submitter.client) @@ -100,13 +100,12 @@ func TestNewDASubmitterSetsVisualizerWhenEnabled(t *testing.T) { cfg.RPC.EnableDAVisualization = true cfg.Node.Aggregator = true - dummyDA := coreda.NewDummyDA(10_000_000, 10*time.Millisecond) - daClient := da.NewClient(da.Config{ - DA: dummyDA, - Logger: zerolog.Nop(), - Namespace: cfg.DA.Namespace, - DataNamespace: cfg.DA.DataNamespace, + BlobAPI: da.NewLocalBlobAPI(common.DefaultMaxBlobSize), + Logger: zerolog.Nop(), + Namespace: cfg.DA.Namespace, + DataNamespace: cfg.DA.DataNamespace, + DefaultTimeout: 10 * time.Second, }) NewDASubmitter( daClient, @@ -121,7 +120,7 @@ func TestNewDASubmitterSetsVisualizerWhenEnabled(t *testing.T) { } func TestDASubmitter_SubmitHeaders_Success(t *testing.T) { - submitter, st, cm, _, gen := setupDASubmitterTest(t) + submitter, st, cm, gen := setupDASubmitterTest(t) ctx := context.Background() // Create test signer @@ -211,7 +210,7 @@ func TestDASubmitter_SubmitHeaders_Success(t *testing.T) { } func TestDASubmitter_SubmitHeaders_NoPendingHeaders(t *testing.T) { - submitter, _, cm, _, _ := setupDASubmitterTest(t) + submitter, _, cm, _ := setupDASubmitterTest(t) ctx := context.Background() // Submit headers when none are pending @@ -220,7 +219,7 @@ func TestDASubmitter_SubmitHeaders_NoPendingHeaders(t *testing.T) { } func TestDASubmitter_SubmitData_Success(t *testing.T) { - submitter, st, cm, _, gen := setupDASubmitterTest(t) + submitter, st, cm, gen := setupDASubmitterTest(t) ctx := context.Background() // Create test signer @@ -306,7 +305,7 @@ func TestDASubmitter_SubmitData_Success(t *testing.T) { } func TestDASubmitter_SubmitData_SkipsEmptyData(t *testing.T) { - submitter, st, cm, _, gen := setupDASubmitterTest(t) + submitter, st, cm, gen := setupDASubmitterTest(t) ctx := context.Background() // Create test signer @@ -355,7 +354,7 @@ func TestDASubmitter_SubmitData_SkipsEmptyData(t *testing.T) { } func TestDASubmitter_SubmitData_NoPendingData(t *testing.T) { - submitter, _, cm, _, gen := setupDASubmitterTest(t) + submitter, _, cm, gen := setupDASubmitterTest(t) ctx := context.Background() // Create test signer @@ -367,7 +366,7 @@ func TestDASubmitter_SubmitData_NoPendingData(t *testing.T) { } func TestDASubmitter_SubmitData_NilSigner(t *testing.T) { - submitter, st, cm, _, gen := setupDASubmitterTest(t) + submitter, st, cm, gen := setupDASubmitterTest(t) ctx := context.Background() // Create test data with transactions @@ -406,7 +405,7 @@ func TestDASubmitter_SubmitData_NilSigner(t *testing.T) { } func TestDASubmitter_CreateSignedData(t *testing.T) { - submitter, _, _, _, gen := setupDASubmitterTest(t) + submitter, _, _, gen := setupDASubmitterTest(t) // Create test signer addr, _, signer := createTestSigner(t) @@ -464,7 +463,7 @@ func TestDASubmitter_CreateSignedData(t *testing.T) { } func TestDASubmitter_CreateSignedData_NilSigner(t *testing.T) { - submitter, _, _, _, gen := setupDASubmitterTest(t) + submitter, _, _, gen := setupDASubmitterTest(t) // Create test data signedData := &types.SignedData{ diff --git a/block/internal/submitting/submitter.go b/block/internal/submitting/submitter.go index 8af2a76055..d6bbb16f13 100644 --- a/block/internal/submitting/submitter.go +++ b/block/internal/submitting/submitter.go @@ -381,10 +381,9 @@ func (s *Submitter) IsHeightDAIncluded(height uint64, header *types.SignedHeader headerHash := header.Hash().String() dataCommitment := data.DACommitment() - dataHash := dataCommitment.String() _, headerIncluded := s.cache.GetHeaderDAIncluded(headerHash) - _, dataIncluded := s.cache.GetDataDAIncluded(dataHash) + _, dataIncluded := s.cache.GetDataDAIncluded(dataCommitment.String()) dataIncluded = bytes.Equal(dataCommitment, common.DataHashForEmptyTxs) || dataIncluded diff --git a/block/internal/submitting/submitter_test.go b/block/internal/submitting/submitter_test.go index c1df11bf51..0fd7d565c9 100644 --- a/block/internal/submitting/submitter_test.go +++ b/block/internal/submitting/submitter_test.go @@ -1,3 +1,5 @@ +//go:build ignore + package submitting import ( diff --git a/block/internal/syncing/da_retriever.go b/block/internal/syncing/da_retriever.go index 9325d4d3bd..e2ac7688f4 100644 --- a/block/internal/syncing/da_retriever.go +++ b/block/internal/syncing/da_retriever.go @@ -12,7 +12,7 @@ import ( "github.com/evstack/ev-node/block/internal/cache" "github.com/evstack/ev-node/block/internal/common" "github.com/evstack/ev-node/block/internal/da" - coreda "github.com/evstack/ev-node/core/da" + datypes "github.com/evstack/ev-node/pkg/da/types" "github.com/evstack/ev-node/pkg/genesis" "github.com/evstack/ev-node/types" pb "github.com/evstack/ev-node/types/pb/evnode/v1" @@ -71,7 +71,7 @@ func (r *daRetriever) RetrieveFromDA(ctx context.Context, daHeight uint64) ([]co } // fetchBlobs retrieves blobs from both header and data namespaces -func (r *daRetriever) fetchBlobs(ctx context.Context, daHeight uint64) (coreda.ResultRetrieve, error) { +func (r *daRetriever) fetchBlobs(ctx context.Context, daHeight uint64) (datypes.ResultRetrieve, error) { // Retrieve from both namespaces using the DA client headerRes := r.client.RetrieveHeaders(ctx, daHeight) @@ -85,31 +85,31 @@ func (r *daRetriever) fetchBlobs(ctx context.Context, daHeight uint64) (coreda.R // Validate responses headerErr := r.validateBlobResponse(headerRes, daHeight) // ignoring error not found, as data can have data - if headerErr != nil && !errors.Is(headerErr, coreda.ErrBlobNotFound) { + if headerErr != nil && !errors.Is(headerErr, datypes.ErrBlobNotFound) { return headerRes, headerErr } dataErr := r.validateBlobResponse(dataRes, daHeight) // ignoring error not found, as header can have data - if dataErr != nil && !errors.Is(dataErr, coreda.ErrBlobNotFound) { + if dataErr != nil && !errors.Is(dataErr, datypes.ErrBlobNotFound) { return dataRes, dataErr } // Combine successful results - combinedResult := coreda.ResultRetrieve{ - BaseResult: coreda.BaseResult{ - Code: coreda.StatusSuccess, + combinedResult := datypes.ResultRetrieve{ + BaseResult: datypes.BaseResult{ + Code: datypes.StatusSuccess, Height: daHeight, }, Data: make([][]byte, 0), } - if headerRes.Code == coreda.StatusSuccess { + if headerRes.Code == datypes.StatusSuccess { combinedResult.Data = append(combinedResult.Data, headerRes.Data...) combinedResult.IDs = append(combinedResult.IDs, headerRes.IDs...) } - if dataRes.Code == coreda.StatusSuccess { + if dataRes.Code == datypes.StatusSuccess { combinedResult.Data = append(combinedResult.Data, dataRes.Data...) combinedResult.IDs = append(combinedResult.IDs, dataRes.IDs...) } @@ -117,9 +117,9 @@ func (r *daRetriever) fetchBlobs(ctx context.Context, daHeight uint64) (coreda.R // Re-throw error not found if both were not found. if len(combinedResult.Data) == 0 && len(combinedResult.IDs) == 0 { r.logger.Debug().Uint64("da_height", daHeight).Msg("no blob data found") - combinedResult.Code = coreda.StatusNotFound - combinedResult.Message = coreda.ErrBlobNotFound.Error() - return combinedResult, coreda.ErrBlobNotFound + combinedResult.Code = datypes.StatusNotFound + combinedResult.Message = datypes.ErrBlobNotFound.Error() + return combinedResult, datypes.ErrBlobNotFound } return combinedResult, nil @@ -127,15 +127,15 @@ func (r *daRetriever) fetchBlobs(ctx context.Context, daHeight uint64) (coreda.R // validateBlobResponse validates a blob response from DA layer // those are the only error code returned by da.RetrieveWithHelpers -func (r *daRetriever) validateBlobResponse(res coreda.ResultRetrieve, daHeight uint64) error { +func (r *daRetriever) validateBlobResponse(res datypes.ResultRetrieve, daHeight uint64) error { switch res.Code { - case coreda.StatusError: + case datypes.StatusError: return fmt.Errorf("DA retrieval failed: %s", res.Message) - case coreda.StatusHeightFromFuture: - return fmt.Errorf("%w: height from future", coreda.ErrHeightFromFuture) - case coreda.StatusNotFound: - return fmt.Errorf("%w: blob not found", coreda.ErrBlobNotFound) - case coreda.StatusSuccess: + case datypes.StatusHeightFromFuture: + return fmt.Errorf("%w: height from future", datypes.ErrHeightFromFuture) + case datypes.StatusNotFound: + return fmt.Errorf("%w: blob not found", datypes.ErrBlobNotFound) + case datypes.StatusSuccess: r.logger.Debug().Uint64("da_height", daHeight).Msg("successfully retrieved from DA") return nil default: diff --git a/block/internal/syncing/da_retriever_test.go b/block/internal/syncing/da_retriever_test.go index 4f43492737..de11952815 100644 --- a/block/internal/syncing/da_retriever_test.go +++ b/block/internal/syncing/da_retriever_test.go @@ -1,32 +1,33 @@ +//go:build !ignore + package syncing import ( - "bytes" "context" "errors" - "fmt" "testing" "time" + "github.com/celestiaorg/go-square/v3/share" "github.com/libp2p/go-libp2p/core/crypto" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/evstack/ev-node/block/internal/cache" "github.com/evstack/ev-node/block/internal/common" "github.com/evstack/ev-node/block/internal/da" - coreda "github.com/evstack/ev-node/core/da" + "github.com/evstack/ev-node/pkg/blob" "github.com/evstack/ev-node/pkg/config" + datypes "github.com/evstack/ev-node/pkg/da/types" "github.com/evstack/ev-node/pkg/genesis" + "github.com/evstack/ev-node/pkg/namespace" signerpkg "github.com/evstack/ev-node/pkg/signer" - testmocks "github.com/evstack/ev-node/test/mocks" "github.com/evstack/ev-node/types" ) // newTestDARetriever creates a DA retriever for testing with the given DA implementation -func newTestDARetriever(t *testing.T, mockDA coreda.DA, cfg config.Config, gen genesis.Genesis) *daRetriever { +func newTestDARetriever(t *testing.T, api da.BlobAPI, cfg config.Config, gen genesis.Genesis) *daRetriever { t.Helper() if cfg.DA.Namespace == "" { cfg.DA.Namespace = "test-ns" @@ -38,16 +39,56 @@ func newTestDARetriever(t *testing.T, mockDA coreda.DA, cfg config.Config, gen g cm, err := cache.NewCacheManager(cfg, zerolog.Nop()) require.NoError(t, err) + blobAPI := api + if blobAPI == nil { + blobAPI = da.NewLocalBlobAPI(common.DefaultMaxBlobSize) + } daClient := da.NewClient(da.Config{ - DA: mockDA, - Logger: zerolog.Nop(), - Namespace: cfg.DA.Namespace, - DataNamespace: cfg.DA.DataNamespace, + BlobAPI: blobAPI, + Logger: zerolog.Nop(), + Namespace: cfg.DA.Namespace, + DataNamespace: cfg.DA.DataNamespace, + DefaultTimeout: 10 * time.Second, }) return NewDARetriever(daClient, cm, gen, zerolog.Nop()) } +type stubBlobAPI struct { + submitFn func(ctx context.Context, blobs []*blob.Blob, opts *blob.SubmitOptions) (uint64, error) + getAllFn func(ctx context.Context, height uint64, namespaces []share.Namespace) ([]*blob.Blob, error) + getProofFn func(ctx context.Context, height uint64, namespace share.Namespace, commitment blob.Commitment) (*blob.Proof, error) + includedFn func(ctx context.Context, height uint64, namespace share.Namespace, proof *blob.Proof, commitment blob.Commitment) (bool, error) +} + +func (s stubBlobAPI) Submit(ctx context.Context, blobs []*blob.Blob, opts *blob.SubmitOptions) (uint64, error) { + if s.submitFn != nil { + return s.submitFn(ctx, blobs, opts) + } + return 0, nil +} + +func (s stubBlobAPI) GetAll(ctx context.Context, height uint64, namespaces []share.Namespace) ([]*blob.Blob, error) { + if s.getAllFn != nil { + return s.getAllFn(ctx, height, namespaces) + } + return []*blob.Blob{}, nil +} + +func (s stubBlobAPI) GetProof(ctx context.Context, height uint64, namespace share.Namespace, commitment blob.Commitment) (*blob.Proof, error) { + if s.getProofFn != nil { + return s.getProofFn(ctx, height, namespace, commitment) + } + return &blob.Proof{}, nil +} + +func (s stubBlobAPI) Included(ctx context.Context, height uint64, namespace share.Namespace, proof *blob.Proof, commitment blob.Commitment) (bool, error) { + if s.includedFn != nil { + return s.includedFn(ctx, height, namespace, proof, commitment) + } + return true, nil +} + // makeSignedDataBytes builds SignedData containing the provided Data and returns its binary encoding func makeSignedDataBytes(t *testing.T, chainID string, height uint64, proposer []byte, pub crypto.PubKey, signer signerpkg.Signer, txs int) ([]byte, *types.SignedData) { return makeSignedDataBytesWithTime(t, chainID, height, proposer, pub, signer, txs, uint64(time.Now().UnixNano())) @@ -73,51 +114,83 @@ func makeSignedDataBytesWithTime(t *testing.T, chainID string, height uint64, pr } func TestDARetriever_RetrieveFromDA_Invalid(t *testing.T) { - mockDA := testmocks.NewMockDA(t) - - mockDA.EXPECT().GetIDs(mock.Anything, mock.Anything, mock.Anything). - Return(nil, errors.New("just invalid")).Maybe() + api := stubBlobAPI{ + getAllFn: func(ctx context.Context, height uint64, namespaces []share.Namespace) ([]*blob.Blob, error) { + return nil, errors.New("just invalid") + }, + } - r := newTestDARetriever(t, mockDA, config.DefaultConfig(), genesis.Genesis{}) + r := newTestDARetriever(t, api, config.DefaultConfig(), genesis.Genesis{}) events, err := r.RetrieveFromDA(context.Background(), 42) assert.Error(t, err) assert.Len(t, events, 0) } func TestDARetriever_RetrieveFromDA_NotFound(t *testing.T) { - mockDA := testmocks.NewMockDA(t) - - // GetIDs returns ErrBlobNotFound -> helper maps to StatusNotFound - mockDA.EXPECT().GetIDs(mock.Anything, mock.Anything, mock.Anything). - Return(nil, fmt.Errorf("%s: whatever", coreda.ErrBlobNotFound.Error())).Maybe() + api := stubBlobAPI{ + getAllFn: func(ctx context.Context, height uint64, namespaces []share.Namespace) ([]*blob.Blob, error) { + return nil, errors.New(datypes.ErrBlobNotFound.Error()) + }, + } - r := newTestDARetriever(t, mockDA, config.DefaultConfig(), genesis.Genesis{}) + r := newTestDARetriever(t, api, config.DefaultConfig(), genesis.Genesis{}) events, err := r.RetrieveFromDA(context.Background(), 42) - assert.True(t, errors.Is(err, coreda.ErrBlobNotFound)) + assert.Error(t, err) + assert.Contains(t, err.Error(), "blob: not found") assert.Len(t, events, 0) } func TestDARetriever_RetrieveFromDA_HeightFromFuture(t *testing.T) { - mockDA := testmocks.NewMockDA(t) - // GetIDs returns ErrHeightFromFuture -> helper maps to StatusHeightFromFuture, fetchBlobs returns error - mockDA.EXPECT().GetIDs(mock.Anything, mock.Anything, mock.Anything). - Return(nil, fmt.Errorf("%s: later", coreda.ErrHeightFromFuture.Error())).Maybe() + api := stubBlobAPI{ + getAllFn: func(ctx context.Context, height uint64, namespaces []share.Namespace) ([]*blob.Blob, error) { + return nil, errors.New(datypes.ErrHeightFromFuture.Error()) + }, + } - r := newTestDARetriever(t, mockDA, config.DefaultConfig(), genesis.Genesis{}) + r := newTestDARetriever(t, api, config.DefaultConfig(), genesis.Genesis{}) events, derr := r.RetrieveFromDA(context.Background(), 1000) assert.Error(t, derr) - assert.True(t, errors.Is(derr, coreda.ErrHeightFromFuture)) + assert.Contains(t, derr.Error(), "height from future") assert.Nil(t, events) } +func TestDARetriever_RetrieveFromDA_Timeout(t *testing.T) { + t.Skip("Skipping flaky timeout test - timing is now controlled by DA client") + + api := stubBlobAPI{ + getAllFn: func(ctx context.Context, height uint64, namespaces []share.Namespace) ([]*blob.Blob, error) { + <-ctx.Done() + return nil, context.DeadlineExceeded + }, + } + + r := newTestDARetriever(t, api, config.DefaultConfig(), genesis.Genesis{}) + + start := time.Now() + events, err := r.RetrieveFromDA(context.Background(), 42) + duration := time.Since(start) + + // Verify error is returned and contains deadline exceeded information + require.Error(t, err) + assert.Contains(t, err.Error(), "DA retrieval failed") + assert.Contains(t, err.Error(), "context deadline exceeded") + assert.Len(t, events, 0) + + // Verify timeout occurred approximately at expected time (with some tolerance) + // DA client has a 30-second default timeout + assert.Greater(t, duration, 29*time.Second, "should timeout after approximately 30 seconds") + assert.Less(t, duration, 35*time.Second, "should not take much longer than timeout") +} + func TestDARetriever_RetrieveFromDA_TimeoutFast(t *testing.T) { - mockDA := testmocks.NewMockDA(t) - // Mock GetIDs to immediately return context deadline exceeded - mockDA.EXPECT().GetIDs(mock.Anything, mock.Anything, mock.Anything). - Return(nil, context.DeadlineExceeded).Maybe() + api := stubBlobAPI{ + getAllFn: func(ctx context.Context, height uint64, namespaces []share.Namespace) ([]*blob.Blob, error) { + return nil, context.DeadlineExceeded + }, + } - r := newTestDARetriever(t, mockDA, config.DefaultConfig(), genesis.Genesis{}) + r := newTestDARetriever(t, api, config.DefaultConfig(), genesis.Genesis{}) events, err := r.RetrieveFromDA(context.Background(), 42) @@ -213,15 +286,15 @@ func TestDARetriever_tryDecodeData_InvalidSignatureOrProposer(t *testing.T) { func TestDARetriever_validateBlobResponse(t *testing.T) { r := &daRetriever{logger: zerolog.Nop()} // StatusSuccess -> nil - err := r.validateBlobResponse(coreda.ResultRetrieve{BaseResult: coreda.BaseResult{Code: coreda.StatusSuccess}}, 1) + err := r.validateBlobResponse(datypes.ResultRetrieve{BaseResult: datypes.BaseResult{Code: datypes.StatusSuccess}}, 1) assert.NoError(t, err) // StatusError -> error - err = r.validateBlobResponse(coreda.ResultRetrieve{BaseResult: coreda.BaseResult{Code: coreda.StatusError, Message: "fail"}}, 1) + err = r.validateBlobResponse(datypes.ResultRetrieve{BaseResult: datypes.BaseResult{Code: datypes.StatusError, Message: "fail"}}, 1) assert.Error(t, err) // StatusHeightFromFuture -> specific error - err = r.validateBlobResponse(coreda.ResultRetrieve{BaseResult: coreda.BaseResult{Code: coreda.StatusHeightFromFuture}}, 1) + err = r.validateBlobResponse(datypes.ResultRetrieve{BaseResult: datypes.BaseResult{Code: datypes.StatusHeightFromFuture}}, 1) assert.Error(t, err) - assert.True(t, errors.Is(err, coreda.ErrHeightFromFuture)) + assert.True(t, errors.Is(err, datypes.ErrHeightFromFuture)) } func TestDARetriever_RetrieveFromDA_TwoNamespaces_Success(t *testing.T) { @@ -237,22 +310,35 @@ func TestDARetriever_RetrieveFromDA_TwoNamespaces_Success(t *testing.T) { cfg.DA.Namespace = "nsHdr" cfg.DA.DataNamespace = "nsData" - namespaceBz := coreda.NamespaceFromString(cfg.DA.GetNamespace()).Bytes() - namespaceDataBz := coreda.NamespaceFromString(cfg.DA.GetDataNamespace()).Bytes() + namespaceBz := namespace.NamespaceFromString(cfg.DA.GetNamespace()).Bytes() + namespaceDataBz := namespace.NamespaceFromString(cfg.DA.GetDataNamespace()).Bytes() + + hdrNS, err := share.NewNamespaceFromBytes(namespaceBz) + require.NoError(t, err) + dataNS, err := share.NewNamespaceFromBytes(namespaceDataBz) + require.NoError(t, err) - mockDA := testmocks.NewMockDA(t) - // Expect GetIDs for both namespaces - mockDA.EXPECT().GetIDs(mock.Anything, uint64(1234), mock.MatchedBy(func(ns []byte) bool { return bytes.Equal(ns, namespaceBz) })). - Return(&coreda.GetIDsResult{IDs: [][]byte{[]byte("h1")}, Timestamp: time.Now()}, nil).Once() - mockDA.EXPECT().Get(mock.Anything, mock.Anything, mock.MatchedBy(func(ns []byte) bool { return bytes.Equal(ns, namespaceBz) })). - Return([][]byte{hdrBin}, nil).Once() + hdrBlob, err := blob.NewBlobV0(hdrNS, hdrBin) + require.NoError(t, err) + dataBlob, err := blob.NewBlobV0(dataNS, dataBin) + require.NoError(t, err) - mockDA.EXPECT().GetIDs(mock.Anything, uint64(1234), mock.MatchedBy(func(ns []byte) bool { return bytes.Equal(ns, namespaceDataBz) })). - Return(&coreda.GetIDsResult{IDs: [][]byte{[]byte("d1")}, Timestamp: time.Now()}, nil).Once() - mockDA.EXPECT().Get(mock.Anything, mock.Anything, mock.MatchedBy(func(ns []byte) bool { return bytes.Equal(ns, namespaceDataBz) })). - Return([][]byte{dataBin}, nil).Once() + api := stubBlobAPI{ + getAllFn: func(ctx context.Context, height uint64, namespaces []share.Namespace) ([]*blob.Blob, error) { + require.Equal(t, uint64(1234), height) + require.Len(t, namespaces, 1) + switch string(namespaces[0].Bytes()) { + case string(hdrNS.Bytes()): + return []*blob.Blob{hdrBlob}, nil + case string(dataNS.Bytes()): + return []*blob.Blob{dataBlob}, nil + default: + return []*blob.Blob{}, nil + } + }, + } - r := newTestDARetriever(t, mockDA, cfg, gen) + r := newTestDARetriever(t, api, cfg, gen) events, derr := r.RetrieveFromDA(context.Background(), 1234) require.NoError(t, derr) diff --git a/block/internal/syncing/syncer.go b/block/internal/syncing/syncer.go index ee69edea7f..95dfb5b0dc 100644 --- a/block/internal/syncing/syncer.go +++ b/block/internal/syncing/syncer.go @@ -10,17 +10,16 @@ import ( "sync/atomic" "time" + coreexecutor "github.com/evstack/ev-node/core/execution" pubsub "github.com/libp2p/go-libp2p-pubsub" "github.com/rs/zerolog" "golang.org/x/sync/errgroup" - coreda "github.com/evstack/ev-node/core/da" - coreexecutor "github.com/evstack/ev-node/core/execution" - "github.com/evstack/ev-node/block/internal/cache" "github.com/evstack/ev-node/block/internal/common" "github.com/evstack/ev-node/block/internal/da" "github.com/evstack/ev-node/pkg/config" + datypes "github.com/evstack/ev-node/pkg/da/types" "github.com/evstack/ev-node/pkg/genesis" "github.com/evstack/ev-node/pkg/store" "github.com/evstack/ev-node/types" @@ -285,10 +284,10 @@ func (s *Syncer) fetchDAUntilCaughtUp() error { events, err := s.daRetriever.RetrieveFromDA(s.ctx, daHeight) if err != nil { switch { - case errors.Is(err, coreda.ErrBlobNotFound): + case errors.Is(err, datypes.ErrBlobNotFound): s.daRetrieverHeight.Store(daHeight + 1) continue // Fetch next height immediately - case errors.Is(err, coreda.ErrHeightFromFuture): + case errors.Is(err, datypes.ErrHeightFromFuture): s.logger.Debug().Err(err).Uint64("da_height", daHeight).Msg("DA is ahead of local target; backing off future height requests") return nil // Caught up default: diff --git a/block/internal/syncing/syncer_backoff_test.go b/block/internal/syncing/syncer_backoff_test.go index 65f2586966..6df2e577a0 100644 --- a/block/internal/syncing/syncer_backoff_test.go +++ b/block/internal/syncing/syncer_backoff_test.go @@ -15,9 +15,9 @@ import ( "github.com/evstack/ev-node/block/internal/cache" "github.com/evstack/ev-node/block/internal/common" - coreda "github.com/evstack/ev-node/core/da" "github.com/evstack/ev-node/core/execution" "github.com/evstack/ev-node/pkg/config" + datypes "github.com/evstack/ev-node/pkg/da/types" "github.com/evstack/ev-node/pkg/genesis" "github.com/evstack/ev-node/pkg/store" extmocks "github.com/evstack/ev-node/test/mocks/external" @@ -41,13 +41,13 @@ func TestSyncer_BackoffOnDAError(t *testing.T) { }, "height_from_future_triggers_backoff": { daBlockTime: 500 * time.Millisecond, - error: coreda.ErrHeightFromFuture, + error: datypes.ErrHeightFromFuture, expectsBackoff: true, description: "Height from future should trigger backoff", }, "blob_not_found_no_backoff": { daBlockTime: 1 * time.Second, - error: coreda.ErrBlobNotFound, + error: datypes.ErrBlobNotFound, expectsBackoff: false, description: "ErrBlobNotFound should not trigger backoff", }, @@ -111,7 +111,7 @@ func TestSyncer_BackoffOnDAError(t *testing.T) { // Cancel to end test cancel() }). - Return(nil, coreda.ErrBlobNotFound).Once() + Return(nil, datypes.ErrBlobNotFound).Once() } else { // For ErrBlobNotFound, DA height should increment daRetriever.On("RetrieveFromDA", mock.Anything, uint64(101)). @@ -120,7 +120,7 @@ func TestSyncer_BackoffOnDAError(t *testing.T) { callCount++ cancel() }). - Return(nil, coreda.ErrBlobNotFound).Once() + Return(nil, datypes.ErrBlobNotFound).Once() } // Run sync loop @@ -223,7 +223,7 @@ func TestSyncer_BackoffResetOnSuccess(t *testing.T) { callTimes = append(callTimes, time.Now()) cancel() }). - Return(nil, coreda.ErrBlobNotFound).Once() + Return(nil, datypes.ErrBlobNotFound).Once() // Start process loop to handle events go syncer.processLoop() @@ -292,7 +292,7 @@ func TestSyncer_BackoffBehaviorIntegration(t *testing.T) { Run(func(args mock.Arguments) { callTimes = append(callTimes, time.Now()) }). - Return(nil, coreda.ErrBlobNotFound).Once() + Return(nil, datypes.ErrBlobNotFound).Once() // Third call - should continue without delay (DA height incremented) daRetriever.On("RetrieveFromDA", mock.Anything, uint64(101)). @@ -300,7 +300,7 @@ func TestSyncer_BackoffBehaviorIntegration(t *testing.T) { callTimes = append(callTimes, time.Now()) cancel() }). - Return(nil, coreda.ErrBlobNotFound).Once() + Return(nil, datypes.ErrBlobNotFound).Once() go syncer.processLoop() syncer.startSyncWorkers() diff --git a/block/internal/syncing/syncer_test.go b/block/internal/syncing/syncer_test.go index 5c16da4435..47a1037ded 100644 --- a/block/internal/syncing/syncer_test.go +++ b/block/internal/syncing/syncer_test.go @@ -10,8 +10,8 @@ import ( "testing" "time" - coreda "github.com/evstack/ev-node/core/da" "github.com/evstack/ev-node/core/execution" + datypes "github.com/evstack/ev-node/pkg/da/types" "github.com/evstack/ev-node/pkg/genesis" signerpkg "github.com/evstack/ev-node/pkg/signer" "github.com/evstack/ev-node/pkg/signer/noop" @@ -412,7 +412,7 @@ func TestSyncLoopPersistState(t *testing.T) { }, 1*time.Second, 10*time.Millisecond) cancel() }). - Return(nil, coreda.ErrHeightFromFuture) + Return(nil, datypes.ErrHeightFromFuture) go syncerInst1.processLoop() syncerInst1.startSyncWorkers() diff --git a/block/public.go b/block/public.go index a5c105ed5f..c28d2bf085 100644 --- a/block/public.go +++ b/block/public.go @@ -1,9 +1,12 @@ package block import ( + "context" + "time" + "github.com/evstack/ev-node/block/internal/common" "github.com/evstack/ev-node/block/internal/da" - coreda "github.com/evstack/ev-node/core/da" + "github.com/evstack/ev-node/da/jsonrpc" "github.com/evstack/ev-node/pkg/config" "github.com/rs/zerolog" ) @@ -32,18 +35,44 @@ func NopMetrics() *Metrics { // DAClient is the interface representing the DA client for public use. type DAClient = da.Client -// NewDAClient creates a new DA client with configuration +// NewDAClient creates a new DA client with configuration. +// It always dials the blob RPC endpoint configured in config.DA. func NewDAClient( - daLayer coreda.DA, config config.Config, logger zerolog.Logger, ) DAClient { + var ( + blobClient da.BlobAPI + err error + ) + + blobClient, err = jsonrpc.NewClient(context.Background(), logger, config.DA.Address, config.DA.AuthToken, common.DefaultMaxBlobSize) + if err != nil { + logger.Warn().Err(err).Msg("failed to create blob jsonrpc client, falling back to local in-memory client") + blobClient = da.NewLocalBlobAPI(common.DefaultMaxBlobSize) + } + + return da.NewClient(da.Config{ + BlobAPI: blobClient, + Logger: logger, + DefaultTimeout: 10 * time.Second, + Namespace: config.DA.GetNamespace(), + DataNamespace: config.DA.GetDataNamespace(), + }) +} + +// NewLocalDAClient creates a new DA client using an in-memory LocalBlobAPI. +// This is useful for tests where multiple nodes need to share the same DA state. +func NewLocalDAClient( + config config.Config, + logger zerolog.Logger, +) DAClient { + blobClient := da.NewLocalBlobAPI(common.DefaultMaxBlobSize) return da.NewClient(da.Config{ - DA: daLayer, - Logger: logger, - Namespace: config.DA.GetNamespace(), - DefaultTimeout: config.DA.RequestTimeout.Duration, - DataNamespace: config.DA.GetDataNamespace(), - RetrieveBatchSize: config.DA.RetrieveBatchSize, + BlobAPI: blobClient, + Logger: logger, + DefaultTimeout: 10 * time.Second, + Namespace: config.DA.GetNamespace(), + DataNamespace: config.DA.GetDataNamespace(), }) } diff --git a/codecov.yml b/codecov.yml index b01d17dd04..a3fd53aacd 100644 --- a/codecov.yml +++ b/codecov.yml @@ -12,7 +12,6 @@ ignore: - "scripts/" - "pkg/rpc/example/" - "apps" - - "da/internal/mocks" - "da/cmd" - "execution/evm" # EVM is covered in e2e/integration tests - "**/metrics.go" diff --git a/core/README.md b/core/README.md index 5afbbef6fe..e8e5be5773 100644 --- a/core/README.md +++ b/core/README.md @@ -4,7 +4,7 @@ The `core` package is the zero-dependency foundation of Evolve. It provides the ## Purpose -The primary goal of the `core` package is to define common contracts (interfaces) for key components within Evolve, such as execution, sequencing, and data availability (DA). By having all other Evolve modules depend solely on `core`, we decouple their implementations. This allows each component to be compiled independently and avoids circular import issues, which can arise when components directly depend on each other. +The primary goal of the `core` package is to define common contracts (interfaces) for key components within Evolve, such as execution and sequencing. Data availability abstractions now live in the blob client (`block/internal/da`) to avoid pulling RPC dependencies into `core`. By having all other Evolve modules depend solely on `core`, we decouple their implementations. This allows each component to be compiled independently and avoids circular import issues, which can arise when components directly depend on each other. ## Key Interfaces @@ -51,55 +51,7 @@ type Sequencer interface { ### Data Availability (DA) -The `DA` interface specifies how data is submitted to and retrieved from the underlying data availability layer. - -```go -// core/da/da.go - -// DA defines the interface for the data availability layer. -type DA interface { - // MaxBlobSize returns the max blob size. - MaxBlobSize(ctx context.Context) (uint64, error) - // Get returns Blobs for given IDs. - Get(ctx context.Context, ids []ID, namespace []byte) ([]Blob, error) - // GetIDs returns IDs of Blobs at given height. - GetIDs(ctx context.Context, height uint64, namespace []byte) (*GetIDsResult, error) - // GetProofs returns inclusion proofs for Blobs specified by IDs at given height. - GetProofs(ctx context.Context, ids []ID, namespace []byte) ([]Proof, error) - // Commit creates commitments for Blobs. Submit uses Commit internally. - Commit(ctx context.Context, blobs []Blob, namespace []byte) ([]Commitment, error) - // Submit submits Blobs to DA layer. - Submit(ctx context.Context, blobs []Blob, gasPrice float64, namespace []byte) ([]ID, error) - // Validate validates Commitments against the corresponding Proofs. This should be possible without retrieving the Blobs. - Validate(ctx context.Context, ids []ID, proofs []Proof, namespace []byte) ([]bool, error) - // GasPrice returns the gas price. - GasPrice(ctx context.Context) (float64, error) - // GasMultiplier returns the gas multiplier. - GasMultiplier(ctx context.Context) (float64, error) -} -``` - -The `Client` interface provides a higher-level abstraction for interacting with the DA layer, often used by nodes. - -```go -// core/da/client.go - -// Client is the interface for the DA layer client. -type Client interface { - // Submit submits block data to DA layer. - Submit(ctx context.Context, data [][]byte, maxBlobSize uint64, gasPrice float64) ResultSubmit - // Retrieve retrieves block data from DA layer. - Retrieve(ctx context.Context, dataLayerHeight uint64) ResultRetrieve - // MaxBlobSize returns the maximum blob size for the DA layer. - MaxBlobSize(ctx context.Context) (uint64, error) - // GasPrice returns the gas price for the DA layer. - GasPrice(ctx context.Context) (float64, error) - // GasMultiplier returns the gas multiplier for the DA layer. - GasMultiplier(ctx context.Context) (float64, error) - // GetNamespace returns the namespace for the DA layer. - GetNamespace(ctx context.Context) ([]byte, error) -} -``` +The data availability interfaces have moved out of `core`. The blob RPC client and `BlobAPI` interface now live under [`block/internal/da`](https://github.com/evstack/ev-node/blob/main/block/internal/da/client.go), alongside the shared blob types in [`pkg/blob`](https://github.com/evstack/ev-node/tree/main/pkg/blob). This keeps `core` dependency-free while giving DA integrations direct access to the RPC layer utilities. ## Contributing diff --git a/core/da/da.go b/core/da/da.go deleted file mode 100644 index 4229f99879..0000000000 --- a/core/da/da.go +++ /dev/null @@ -1,126 +0,0 @@ -package da - -import ( - "context" - "encoding/binary" - "fmt" - "time" -) - -// DA defines very generic interface for interaction with Data Availability layers. -type DA interface { - // Get returns Blob for each given ID, or an error. - // - // Error should be returned if ID is not formatted properly, there is no Blob for given ID or any other client-level - // error occurred (dropped connection, timeout, etc). - Get(ctx context.Context, ids []ID, namespace []byte) ([]Blob, error) - - // GetIDs returns IDs of all Blobs located in DA at given height. - GetIDs(ctx context.Context, height uint64, namespace []byte) (*GetIDsResult, error) - - // GetProofs returns inclusion Proofs for Blobs specified by their IDs. - GetProofs(ctx context.Context, ids []ID, namespace []byte) ([]Proof, error) - - // Commit creates a Commitment for each given Blob. - Commit(ctx context.Context, blobs []Blob, namespace []byte) ([]Commitment, error) - - // Submit submits the Blobs to Data Availability layer. - // - // This method is synchronous. Upon successful submission to Data Availability layer, it returns the IDs identifying blobs - // in DA. - Submit(ctx context.Context, blobs []Blob, gasPrice float64, namespace []byte) ([]ID, error) - - // SubmitWithOptions submits the Blobs to Data Availability layer with additional options. - SubmitWithOptions(ctx context.Context, blobs []Blob, gasPrice float64, namespace []byte, options []byte) ([]ID, error) - - // Validate validates Commitments against the corresponding Proofs. This should be possible without retrieving the Blobs. - Validate(ctx context.Context, ids []ID, proofs []Proof, namespace []byte) ([]bool, error) -} - -// Blob is the data submitted/received from DA interface. -type Blob = []byte - -// ID should contain serialized data required by the implementation to find blob in Data Availability layer. -type ID = []byte - -// Commitment should contain serialized cryptographic commitment to Blob value. -type Commitment = []byte - -// Proof should contain serialized proof of inclusion (publication) of Blob in Data Availability layer. -type Proof = []byte - -// GetIDsResult holds the result of GetIDs call: IDs and timestamp of corresponding block. -type GetIDsResult struct { - IDs []ID - Timestamp time.Time -} - -// ResultSubmit contains information returned from DA layer after block headers/data submission. -type ResultSubmit struct { - BaseResult -} - -// ResultRetrieveHeaders contains batch of block headers returned from DA layer client. -type ResultRetrieve struct { - BaseResult - // Data is the block data retrieved from Data Availability Layer. - // If Code is not equal to StatusSuccess, it has to be nil. - Data [][]byte -} - -// StatusCode is a type for DA layer return status. -// TODO: define an enum of different non-happy-path cases -// that might need to be handled by Evolve independent of -// the underlying DA chain. -type StatusCode uint64 - -// Data Availability return codes. -const ( - StatusUnknown StatusCode = iota - StatusSuccess - StatusNotFound - StatusNotIncludedInBlock - StatusAlreadyInMempool - StatusTooBig - StatusContextDeadline - StatusError - StatusIncorrectAccountSequence - StatusContextCanceled - StatusHeightFromFuture -) - -// BaseResult contains basic information returned by DA layer. -type BaseResult struct { - // Code is to determine if the action succeeded. - Code StatusCode - // Message may contain DA layer specific information (like DA block height/hash, detailed error message, etc) - Message string - // Height is the height of the block on Data Availability Layer for given result. - Height uint64 - // SubmittedCount is the number of successfully submitted blocks. - SubmittedCount uint64 - // BlobSize is the size of the blob submitted. - BlobSize uint64 - // IDs is the list of IDs of the blobs submitted. - IDs [][]byte - // Timestamp is the timestamp of the posted data on Data Availability Layer. - Timestamp time.Time -} - -// makeID creates an ID from a height and a commitment. -func makeID(height uint64, commitment []byte) []byte { - id := make([]byte, len(commitment)+8) - binary.LittleEndian.PutUint64(id, height) - copy(id[8:], commitment) - return id -} - -// SplitID splits an ID into a height and a commitment. -// if len(id) <= 8, it returns 0 and nil. -func SplitID(id []byte) (uint64, []byte, error) { - if len(id) <= 8 { - return 0, nil, fmt.Errorf("invalid ID length: %d", len(id)) - } - commitment := id[8:] - return binary.LittleEndian.Uint64(id[:8]), commitment, nil -} diff --git a/core/da/dummy.go b/core/da/dummy.go deleted file mode 100644 index a66622bd77..0000000000 --- a/core/da/dummy.go +++ /dev/null @@ -1,243 +0,0 @@ -package da - -import ( - "bytes" - "context" - "crypto/sha256" - "errors" - "fmt" - "sync" - "time" -) - -var _ DA = (*DummyDA)(nil) - -// DummyDA is a simple in-memory implementation of the DA interface for testing purposes. -type DummyDA struct { - mu sync.RWMutex - blobs map[string]Blob - commitments map[string]Commitment - proofs map[string]Proof - blobsByHeight map[uint64][]ID - timestampsByHeight map[uint64]time.Time - namespaceByID map[string][]byte // Track namespace for each blob ID - maxBlobSize uint64 - - // DA height simulation - currentHeight uint64 - blockTime time.Duration - stopCh chan struct{} - - // Simulated failure support - submitShouldFail bool -} - -var ErrHeightFromFutureStr = fmt.Errorf("given height is from the future") - -// NewDummyDA creates a new instance of DummyDA with the specified maximum blob size and block time. -func NewDummyDA(maxBlobSize uint64, blockTime time.Duration) *DummyDA { - return &DummyDA{ - blobs: make(map[string]Blob), - commitments: make(map[string]Commitment), - proofs: make(map[string]Proof), - blobsByHeight: make(map[uint64][]ID), - timestampsByHeight: make(map[uint64]time.Time), - namespaceByID: make(map[string][]byte), - maxBlobSize: maxBlobSize, - blockTime: blockTime, - stopCh: make(chan struct{}), - currentHeight: 0, - } -} - -// StartHeightTicker starts a goroutine that increments currentHeight every blockTime. -func (d *DummyDA) StartHeightTicker() { - go func() { - ticker := time.NewTicker(d.blockTime) - defer ticker.Stop() - for { - select { - case <-ticker.C: - d.mu.Lock() - d.currentHeight++ - d.mu.Unlock() - case <-d.stopCh: - return - } - } - }() -} - -// StopHeightTicker stops the height ticker goroutine. -func (d *DummyDA) StopHeightTicker() { - close(d.stopCh) -} - -// Get returns blobs for the given IDs. -func (d *DummyDA) Get(ctx context.Context, ids []ID, namespace []byte) ([]Blob, error) { - d.mu.RLock() - defer d.mu.RUnlock() - - blobs := make([]Blob, 0, len(ids)) - for _, id := range ids { - blob, exists := d.blobs[string(id)] - if !exists { - return nil, ErrBlobNotFound // Use the specific error type - } - blobs = append(blobs, blob) - } - return blobs, nil -} - -// GetIDs returns IDs of all blobs at the given height. -func (d *DummyDA) GetIDs(ctx context.Context, height uint64, namespace []byte) (*GetIDsResult, error) { - d.mu.RLock() - defer d.mu.RUnlock() - - if height > d.currentHeight { - return nil, fmt.Errorf("%w: requested %d, current %d", ErrHeightFromFutureStr, height, d.currentHeight) - } - - ids, exists := d.blobsByHeight[height] - if !exists { - return &GetIDsResult{ - IDs: []ID{}, - Timestamp: time.Now(), - }, nil - } - - // Filter IDs by namespace - filteredIDs := make([]ID, 0) - for _, id := range ids { - idStr := string(id) - if ns, exists := d.namespaceByID[idStr]; exists && bytes.Equal(ns, namespace) { - filteredIDs = append(filteredIDs, id) - } - } - - return &GetIDsResult{ - IDs: filteredIDs, - Timestamp: d.timestampsByHeight[height], - }, nil -} - -// GetProofs returns proofs for the given IDs. -func (d *DummyDA) GetProofs(ctx context.Context, ids []ID, namespace []byte) ([]Proof, error) { - d.mu.RLock() - defer d.mu.RUnlock() - - proofs := make([]Proof, 0, len(ids)) - for _, id := range ids { - proof, exists := d.proofs[string(id)] - if !exists { - return nil, errors.New("proof not found") - } - proofs = append(proofs, proof) - } - return proofs, nil -} - -// Commit creates commitments for the given blobs. -func (d *DummyDA) Commit(ctx context.Context, blobs []Blob, namespace []byte) ([]Commitment, error) { - d.mu.Lock() - defer d.mu.Unlock() - - commitments := make([]Commitment, 0, len(blobs)) - for _, blob := range blobs { - // For simplicity, we use the blob itself as the commitment - commitment := blob - commitments = append(commitments, commitment) - } - return commitments, nil -} - -// Submit submits blobs to the DA layer. -func (d *DummyDA) Submit(ctx context.Context, blobs []Blob, gasPrice float64, namespace []byte) ([]ID, error) { - return d.SubmitWithOptions(ctx, blobs, gasPrice, namespace, nil) -} - -// SetSubmitFailure simulates DA layer going down by making Submit calls fail -func (d *DummyDA) SetSubmitFailure(shouldFail bool) { - d.mu.Lock() - defer d.mu.Unlock() - d.submitShouldFail = shouldFail -} - -// SubmitWithOptions submits blobs to the DA layer with additional options. -func (d *DummyDA) SubmitWithOptions(ctx context.Context, blobs []Blob, gasPrice float64, namespace []byte, options []byte) ([]ID, error) { - d.mu.Lock() - defer d.mu.Unlock() - - // Check if we should simulate failure - if d.submitShouldFail { - return nil, errors.New("simulated DA layer failure") - } - - height := d.currentHeight + 1 - ids := make([]ID, 0, len(blobs)) - var currentSize uint64 - - for _, blob := range blobs { // Use _ instead of i - blobLen := uint64(len(blob)) - // Check individual blob size first - if blobLen > d.maxBlobSize { - // Mimic DAClient behavior: if the first blob is too large, return error. - // Otherwise, we would have submitted the previous fitting blobs. - // Since DummyDA processes all at once, we return error if any *individual* blob is too large. - // A more complex dummy could simulate partial submission based on cumulative size. - // For now, error out if any single blob is too big. - return nil, ErrBlobSizeOverLimit // Use specific error type - } - - // Check cumulative batch size - if currentSize+blobLen > d.maxBlobSize { - // Stop processing blobs for this batch, return IDs collected so far - // d.logger.Info("DummyDA: Blob size limit reached for batch", "maxBlobSize", d.maxBlobSize, "index", i, "currentSize", currentSize, "nextBlobSize", blobLen) // Removed logger call - break - } - currentSize += blobLen - - // Create a commitment using SHA-256 hash - bz := sha256.Sum256(blob) - commitment := bz[:] - - // Create ID from height and commitment - id := makeID(height, commitment) - idStr := string(id) - - d.blobs[idStr] = blob - d.commitments[idStr] = commitment - d.proofs[idStr] = commitment // Simple proof - d.namespaceByID[idStr] = namespace // Store namespace for this blob - - ids = append(ids, id) - } - - // Add the IDs to the blobsByHeight map if they don't already exist - if existingIDs, exists := d.blobsByHeight[height]; exists { - d.blobsByHeight[height] = append(existingIDs, ids...) - } else { - d.blobsByHeight[height] = ids - } - d.timestampsByHeight[height] = time.Now() - - return ids, nil -} - -// Validate validates commitments against proofs. -func (d *DummyDA) Validate(ctx context.Context, ids []ID, proofs []Proof, namespace []byte) ([]bool, error) { - d.mu.RLock() - defer d.mu.RUnlock() - - if len(ids) != len(proofs) { - return nil, errors.New("number of IDs and proofs must match") - } - - results := make([]bool, len(ids)) - for i, id := range ids { - _, exists := d.blobs[string(id)] - results[i] = exists - } - - return results, nil -} diff --git a/core/da/dummy_test.go b/core/da/dummy_test.go deleted file mode 100644 index 9538aacc83..0000000000 --- a/core/da/dummy_test.go +++ /dev/null @@ -1,137 +0,0 @@ -package da - -import ( - "context" - "fmt" - "testing" - "time" -) - -func TestDummyDA(t *testing.T) { - testDABlockTime := 100 * time.Millisecond - // Create a new DummyDA instance with a max blob size of 1024 bytes - dummyDA := NewDummyDA(1024, testDABlockTime) - dummyDA.StartHeightTicker() - defer dummyDA.StopHeightTicker() - // Height is always 0 - ctx := context.Background() - - // Test Submit - blobs := []Blob{ - []byte("test blob 1"), - []byte("test blob 2"), - } - ids, err := dummyDA.Submit(ctx, blobs, 0, nil) - if err != nil { - t.Fatalf("Submit failed: %v", err) - } - err = waitForFirstDAHeight(ctx, dummyDA) // Wait for height to increment - if err != nil { - t.Fatalf("waitForFirstDAHeight failed: %v", err) - } - if len(ids) != len(blobs) { - t.Errorf("Expected %d IDs, got %d", len(blobs), len(ids)) - } - - // Test Get - retrievedBlobs, err := dummyDA.Get(ctx, ids, nil) - if err != nil { - t.Fatalf("Get failed: %v", err) - } - if len(retrievedBlobs) != len(blobs) { - t.Errorf("Expected %d blobs, got %d", len(blobs), len(retrievedBlobs)) - } - for i, blob := range blobs { - if string(retrievedBlobs[i]) != string(blob) { - t.Errorf("Expected blob %q, got %q", string(blob), string(retrievedBlobs[i])) - } - } - - // Test GetIDs - result, err := dummyDA.GetIDs(ctx, 1, nil) - if err != nil { - t.Fatalf("GetIDs failed: %v", err) - } - if len(result.IDs) != len(ids) { - t.Errorf("Expected %d IDs, got %d", len(ids), len(result.IDs)) - } - - // Test Commit - commitments, err := dummyDA.Commit(ctx, blobs, nil) - if err != nil { - t.Fatalf("Commit failed: %v", err) - } - if len(commitments) != len(blobs) { - t.Errorf("Expected %d commitments, got %d", len(blobs), len(commitments)) - } - - // Test GetProofs - proofs, err := dummyDA.GetProofs(ctx, ids, nil) - if err != nil { - t.Fatalf("GetProofs failed: %v", err) - } - if len(proofs) != len(ids) { - t.Errorf("Expected %d proofs, got %d", len(ids), len(proofs)) - } - - // Test Validate - validations, err := dummyDA.Validate(ctx, ids, proofs, nil) - if err != nil { - t.Fatalf("Validate failed: %v", err) - } - if len(validations) != len(ids) { - t.Errorf("Expected %d validations, got %d", len(ids), len(validations)) - } - for _, valid := range validations { - if !valid { - t.Errorf("Expected validation to be true") - } - } - - // Test error case: blob size exceeds maximum - largeBlob := make([]byte, 2048) // Larger than our max of 1024 - _, err = dummyDA.Submit(ctx, []Blob{largeBlob}, 0, nil) - if err == nil { - t.Errorf("Expected error for blob exceeding max size, got nil") - } -} - -func waitForFirstDAHeight(ctx context.Context, da *DummyDA) error { - return waitForAtLeastDAHeight(ctx, da, 1) -} - -// waitForAtLeastDAHeight waits for the DummyDA to reach at least the given height -func waitForAtLeastDAHeight(ctx context.Context, da *DummyDA, targetHeight uint64) error { - // Read current height at the start - da.mu.RLock() - current := da.currentHeight - da.mu.RUnlock() - - if current >= targetHeight { - return nil - } - - delta := targetHeight - current - - // Dynamically set pollInterval and timeout based on delta - pollInterval := da.blockTime / 2 - timeout := da.blockTime * time.Duration(delta+2) - - deadline := time.Now().Add(timeout) - for { - da.mu.RLock() - current = da.currentHeight - da.mu.RUnlock() - if current >= targetHeight { - return nil - } - if time.Now().After(deadline) { - return fmt.Errorf("timeout waiting for DA height %d, current %d", targetHeight, current) - } - select { - case <-ctx.Done(): - return ctx.Err() - case <-time.After(pollInterval): - } - } -} diff --git a/core/da/namespace.go b/core/da/namespace.go deleted file mode 100644 index 057bb29365..0000000000 --- a/core/da/namespace.go +++ /dev/null @@ -1,129 +0,0 @@ -package da - -import ( - "crypto/sha256" - "encoding/hex" - "fmt" - "strings" -) - -// Implemented in accordance to https://celestiaorg.github.io/celestia-app/namespace.html - -const ( - // NamespaceVersionIndex is the index of the namespace version in the byte slice - NamespaceVersionIndex = 0 - // 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[NamespaceVersionIndex] = n.Version - copy(result[NamespaceVersionSize:], 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 := range NamespaceVersionZeroPrefixSize { - 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[NamespaceVersionIndex], - } - copy(ns.ID[:], b[NamespaceVersionSize:]) - - // 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) -} diff --git a/da/cmd/local-da/README.md b/da/cmd/local-da/README.md index 3513f17a6e..f4987c6894 100644 --- a/da/cmd/local-da/README.md +++ b/da/cmd/local-da/README.md @@ -110,5 +110,5 @@ output: [2] [xh][xh] -[da]: https://github.com/evstack/ev-node/blob/main/core/da/da.go#L11 +[da]: https://github.com/evstack/ev-node/blob/main/block/internal/da/client.go#L41 [xh]: https://github.com/ducaale/xh diff --git a/da/cmd/local-da/local.go b/da/cmd/local-da/local.go index bd36393016..61d791ce28 100644 --- a/da/cmd/local-da/local.go +++ b/da/cmd/local-da/local.go @@ -3,48 +3,35 @@ package main import ( "bytes" "context" - "crypto/ed25519" - "crypto/rand" - "crypto/sha256" - "encoding/binary" - "encoding/hex" - "errors" "fmt" "sync" "time" - coreda "github.com/evstack/ev-node/core/da" + "github.com/celestiaorg/go-square/v3/share" "github.com/rs/zerolog" + + proxy "github.com/evstack/ev-node/da/jsonrpc" + "github.com/evstack/ev-node/pkg/blob" ) // DefaultMaxBlobSize is the default max blob size const DefaultMaxBlobSize uint64 = 2 * 1024 * 1024 // 2MB -// LocalDA is a simple implementation of in-memory DA. Not production ready! Intended only for testing! -// -// Data is stored in a map, where key is a serialized sequence number. This key is returned as ID. -// Commitments are simply hashes, and proofs are ED25519 signatures. +// LocalDA is a simple in-memory implementation of the blob API used for testing. type LocalDA struct { - mu *sync.Mutex // protects data and height - data map[uint64][]kvp + mu *sync.RWMutex + blobs map[uint64][]*blob.Blob timestamps map[uint64]time.Time maxBlobSize uint64 height uint64 - privKey ed25519.PrivateKey - pubKey ed25519.PublicKey - - logger zerolog.Logger -} - -type kvp struct { - key, value []byte + logger zerolog.Logger } -// NewLocalDA create new instance of DummyDA +// NewLocalDA creates a new LocalDA instance. func NewLocalDA(logger zerolog.Logger, opts ...func(*LocalDA) *LocalDA) *LocalDA { da := &LocalDA{ - mu: new(sync.Mutex), - data: make(map[uint64][]kvp), + mu: new(sync.RWMutex), + blobs: make(map[uint64][]*blob.Blob), timestamps: make(map[uint64]time.Time), maxBlobSize: DefaultMaxBlobSize, logger: logger, @@ -52,224 +39,117 @@ func NewLocalDA(logger zerolog.Logger, opts ...func(*LocalDA) *LocalDA) *LocalDA for _, f := range opts { da = f(da) } - da.pubKey, da.privKey, _ = ed25519.GenerateKey(rand.Reader) da.logger.Info().Msg("NewLocalDA: initialized LocalDA") return da } -var _ coreda.DA = &LocalDA{} +// Ensure LocalDA satisfies the blob API used by the JSON-RPC server. +var _ proxy.BlobAPI = (*LocalDA)(nil) -// 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)) +// Submit stores blobs at a new height and returns that height. +func (d *LocalDA) Submit(ctx context.Context, blobs []*blob.Blob, opts *blob.SubmitOptions) (uint64, error) { + for i, b := range blobs { + if uint64(len(b.Data())) > d.maxBlobSize { + d.logger.Error().Int("blobIndex", i).Int("blobSize", len(b.Data())).Uint64("maxBlobSize", d.maxBlobSize).Msg("Submit: blob size exceeds limit") + return 0, fmt.Errorf("blob size exceeds limit: %d > %d", len(b.Data()), d.maxBlobSize) + } } - 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") - return d.maxBlobSize, nil -} - -// Get returns Blobs for given IDs. -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() - blobs := make([]coreda.Blob, len(ids)) - for i, id := range ids { - if len(id) < 8 { - d.logger.Error().Interface("id", id).Msg("Get: invalid ID length") - return nil, errors.New("invalid ID") - } - height := binary.LittleEndian.Uint64(id) - found := false - for j := 0; !found && j < len(d.data[height]); j++ { - if bytes.Equal(d.data[height][j].key, id) { - blobs[i] = d.data[height][j].value - found = true - } - } - if !found { - d.logger.Warn().Interface("id", id).Uint64("height", height).Msg("Get: blob not found") - return nil, coreda.ErrBlobNotFound - } + + d.height++ + height := d.height + d.timestamps[height] = time.Now() + for _, b := range blobs { + d.blobs[height] = append(d.blobs[height], cloneBlob(b)) } - d.logger.Debug().Int("count", len(blobs)).Msg("Get successful") - return blobs, nil + d.logger.Info().Uint64("height", height).Int("count", len(blobs)).Msg("Submit successful") + return height, nil } -// GetIDs returns IDs of Blobs at given DA height. -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() +// GetAll returns blobs stored at the provided height filtered by namespaces. +func (d *LocalDA) GetAll(ctx context.Context, height uint64, namespaces []share.Namespace) ([]*blob.Blob, error) { + d.mu.RLock() + defer d.mu.RUnlock() + // Return error if requesting a height from the future if height > d.height { - d.logger.Error().Uint64("requested", height).Uint64("current", d.height).Msg("GetIDs: height in future") - return nil, fmt.Errorf("height %d is in the future: %w", height, coreda.ErrHeightFromFuture) + return nil, fmt.Errorf("height %d is in the future (current: %d): given height is from the future", height, d.height) } - kvps, ok := d.data[height] + entries, ok := d.blobs[height] if !ok { - d.logger.Debug().Uint64("height", height).Msg("GetIDs: no data for height") return nil, nil } - ids := make([]coreda.ID, len(kvps)) - for i, kv := range kvps { - ids[i] = kv.key + if len(namespaces) == 0 { + return cloneBlobs(entries), nil } - d.logger.Debug().Int("count", len(ids)).Msg("GetIDs successful") - return &coreda.GetIDsResult{IDs: ids, Timestamp: d.timestamps[height]}, nil -} -// GetProofs returns inclusion Proofs for all Blobs located in DA at given height. -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, ns) - if err != nil { - d.logger.Error().Err(err).Msg("GetProofs: failed to get blobs") - return nil, err + var filtered []*blob.Blob + for _, b := range entries { + if matchesNamespace(b.Namespace(), namespaces) { + filtered = append(filtered, cloneBlob(b)) + } } + return filtered, nil +} - d.mu.Lock() - defer d.mu.Unlock() - proofs := make([]coreda.Proof, len(blobs)) - for i, blob := range blobs { - proofs[i] = d.getProof(ids[i], blob) +// GetProof returns a placeholder proof if the blob exists at the height. +func (d *LocalDA) GetProof(ctx context.Context, height uint64, namespace share.Namespace, commitment blob.Commitment) (*blob.Proof, error) { + if !d.hasBlob(height, namespace, commitment) { + return nil, fmt.Errorf("blob not found") } - d.logger.Debug().Int("count", len(proofs)).Msg("GetProofs successful") - return proofs, nil + proof := blob.Proof{} + return &proof, nil } -// Commit returns cryptographic Commitments for given blobs. -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 { - commits[i] = d.getHash(blob) - } - d.logger.Debug().Int("count", len(commits)).Msg("Commit successful") - return commits, nil +// Included verifies that the blob exists at the height. The proof parameter is ignored. +func (d *LocalDA) Included(ctx context.Context, height uint64, namespace share.Namespace, proof *blob.Proof, commitment blob.Commitment) (bool, error) { + return d.hasBlob(height, namespace, commitment), nil } -// SubmitWithOptions stores blobs in DA layer (options are ignored). -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", hex.EncodeToString(ns)).Msg("SubmitWithOptions called") +func (d *LocalDA) hasBlob(height uint64, namespace share.Namespace, commitment blob.Commitment) bool { + d.mu.RLock() + defer d.mu.RUnlock() - // Validate blob sizes before processing - for i, blob := range blobs { - if uint64(len(blob)) > d.maxBlobSize { - d.logger.Error().Int("blobIndex", i).Int("blobSize", len(blob)).Uint64("maxBlobSize", d.maxBlobSize).Msg("SubmitWithOptions: blob size exceeds limit") - return nil, coreda.ErrBlobSizeOverLimit - } + entries, ok := d.blobs[height] + if !ok { + return false } - d.mu.Lock() - defer d.mu.Unlock() - ids := make([]coreda.ID, len(blobs)) - d.height += 1 - d.timestamps[d.height] = time.Now() - for i, blob := range blobs { - ids[i] = append(d.nextID(), d.getHash(blob)...) - - d.data[d.height] = append(d.data[d.height], kvp{ids[i], blob}) + for _, b := range entries { + if bytes.Equal(b.Namespace().Bytes(), namespace.Bytes()) && bytes.Equal(b.Commitment, commitment) { + return true + } } - d.logger.Info().Uint64("newHeight", d.height).Int("count", len(ids)).Msg("SubmitWithOptions successful") - return ids, nil + return false } -// Submit stores blobs in DA layer (options are ignored). -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 { - if uint64(len(blob)) > d.maxBlobSize { - d.logger.Error().Int("blobIndex", i).Int("blobSize", len(blob)).Uint64("maxBlobSize", d.maxBlobSize).Msg("Submit: blob size exceeds limit") - return nil, coreda.ErrBlobSizeOverLimit +func matchesNamespace(ns share.Namespace, targets []share.Namespace) bool { + for _, target := range targets { + if bytes.Equal(ns.Bytes(), target.Bytes()) { + return true } } - - d.mu.Lock() - defer d.mu.Unlock() - ids := make([]coreda.ID, len(blobs)) - d.height += 1 - d.timestamps[d.height] = time.Now() - for i, blob := range blobs { - ids[i] = append(d.nextID(), d.getHash(blob)...) - - d.data[d.height] = append(d.data[d.height], kvp{ids[i], blob}) - } - d.logger.Info().Uint64("newHeight", d.height).Int("count", len(ids)).Msg("Submit successful") - return ids, nil + return false } -// Validate checks the Proofs for given IDs. -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") - return nil, errors.New("number of IDs doesn't equal to number of proofs") +func cloneBlobs(blobs []*blob.Blob) []*blob.Blob { + out := make([]*blob.Blob, len(blobs)) + for i, b := range blobs { + out[i] = cloneBlob(b) } - results := make([]bool, len(ids)) - for i := 0; i < len(ids); i++ { - results[i] = ed25519.Verify(d.pubKey, ids[i][8:], proofs[i]) - d.logger.Debug().Interface("id", ids[i]).Bool("result", results[i]).Msg("Validate result") - } - d.logger.Debug().Interface("results", results).Msg("Validate finished") - return results, nil -} - -func (d *LocalDA) nextID() []byte { - return d.getID(d.height) + return out } -func (d *LocalDA) getID(cnt uint64) []byte { - id := make([]byte, 8) - binary.LittleEndian.PutUint64(id, cnt) - return id -} - -func (d *LocalDA) getHash(blob []byte) []byte { - sha := sha256.Sum256(blob) - return sha[:] -} - -func (d *LocalDA) getProof(id, blob []byte) []byte { - sign := ed25519.Sign(d.privKey, d.getHash(blob)) - return sign +func cloneBlob(b *blob.Blob) *blob.Blob { + if b == nil { + return nil + } + clone := *b + return &clone } // WithMaxBlobSize returns a function that sets the max blob size of LocalDA diff --git a/da/go.mod b/da/go.mod index 478488dfbc..9b4ea34126 100644 --- a/da/go.mod +++ b/da/go.mod @@ -1,32 +1,32 @@ module github.com/evstack/ev-node/da -go 1.24.1 +go 1.24.6 require ( - github.com/evstack/ev-node/core v1.0.0-beta.5 + github.com/celestiaorg/go-square/v3 v3.0.2 + github.com/evstack/ev-node v0.0.0-00010101000000-000000000000 github.com/filecoin-project/go-jsonrpc v0.9.0 github.com/rs/zerolog v1.34.0 - github.com/stretchr/testify v1.11.1 ) require ( - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/celestiaorg/go-square/merkle v0.0.0-20240627094109-7d01436067a3 // indirect + github.com/celestiaorg/nmt v0.24.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect - github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect - github.com/ipfs/go-log/v2 v2.0.8 // indirect - github.com/kr/pretty v0.3.1 // indirect + github.com/ipfs/go-log/v2 v2.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rogpeppe/go-internal v1.13.1 // indirect - github.com/stretchr/objx v0.5.2 // indirect 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/sys v0.31.0 // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + google.golang.org/protobuf v1.36.10 // indirect ) + +replace github.com/evstack/ev-node => ../ diff --git a/da/go.sum b/da/go.sum index cfa1dc32e2..63dcd0649a 100644 --- a/da/go.sum +++ b/da/go.sum @@ -1,10 +1,15 @@ 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/merkle v0.0.0-20240627094109-7d01436067a3 h1:wP84mtwOCVNOTfS3zErICjxKLnh74Z1uf+tdrlSFjVM= +github.com/celestiaorg/go-square/merkle v0.0.0-20240627094109-7d01436067a3/go.mod h1:86qIYnEhmn/hfW+xvw98NOI3zGaDEB3x8JGjYo2FqLs= +github.com/celestiaorg/go-square/v3 v3.0.2 h1:eSQOgNII8inK9IhiBZ+6GADQeWbRq4HYY72BOgcduA4= +github.com/celestiaorg/go-square/v3 v3.0.2/go.mod h1:oFReMLsSDMRs82ICFEeFQFCqNvwdsbIM1BzCcb0f7dM= +github.com/celestiaorg/nmt v0.24.2 h1:LlpJSPOd6/Lw1Ig6HUhZuqiINHLka/ZSRTBzlNJpchg= +github.com/celestiaorg/nmt v0.24.2/go.mod h1:vgLBpWBi8F5KLxTdXSwb7AU4NhiIQ1AQRGa+PzdcLEA= 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= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -13,11 +18,11 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evstack/ev-node/core v1.0.0-beta.5 h1:lgxE8XiF3U9pcFgh7xuKMgsOGvLBGRyd9kc9MR4WL0o= -github.com/evstack/ev-node/core v1.0.0-beta.5/go.mod h1:n2w/LhYQTPsi48m6lMj16YiIqsaQw6gxwjyJvR+B3sY= github.com/filecoin-project/go-jsonrpc v0.9.0 h1:G47qEF52w7GholpI21vPSTVBFvsrip6geIoqNiqyZtQ= github.com/filecoin-project/go-jsonrpc v0.9.0/go.mod h1:OG7kVBVh/AbDFHIwx7Kw0l9ARmKOS6gGOr0LbdBpbLc= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= @@ -40,87 +45,83 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/ipfs/go-log/v2 v2.0.8 h1:3b3YNopMHlj4AvyhWAx0pDxqSQWYi4/WuWO7yRV6/Qg= -github.com/ipfs/go-log/v2 v2.0.8/go.mod h1:eZs4Xt4ZUJQFM3DlanGhy7TkwwawCZcSByscwkWG+dw= +github.com/ipfs/go-log/v2 v2.9.0 h1:l4b06AwVXwldIzbVPZy5z7sKp9lHFTX0KWfTBCtHaOk= +github.com/ipfs/go-log/v2 v2.9.0/go.mod h1:UhIYAwMV7Nb4ZmihUxfIRM2Istw/y9cAk3xaK+4Zs2c= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/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-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= 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= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -128,8 +129,8 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -137,11 +138,13 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -163,15 +166,11 @@ 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.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/da/internal/mocks/da.go b/da/internal/mocks/da.go deleted file mode 100644 index bb3ad63391..0000000000 --- a/da/internal/mocks/da.go +++ /dev/null @@ -1,581 +0,0 @@ -// Code generated by mockery; DO NOT EDIT. -// github.com/vektra/mockery -// template: testify - -package mocks - -import ( - "context" - - "github.com/evstack/ev-node/core/da" - mock "github.com/stretchr/testify/mock" -) - -// NewMockDA creates a new instance of MockDA. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockDA(t interface { - mock.TestingT - Cleanup(func()) -}) *MockDA { - mock := &MockDA{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} - -// MockDA is an autogenerated mock type for the DA type -type MockDA struct { - mock.Mock -} - -type MockDA_Expecter struct { - mock *mock.Mock -} - -func (_m *MockDA) EXPECT() *MockDA_Expecter { - return &MockDA_Expecter{mock: &_m.Mock} -} - -// Commit provides a mock function for the type MockDA -func (_mock *MockDA) Commit(ctx context.Context, blobs []da.Blob, namespace []byte) ([]da.Commitment, error) { - ret := _mock.Called(ctx, blobs, namespace) - - if len(ret) == 0 { - panic("no return value specified for Commit") - } - - var r0 []da.Commitment - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, []da.Blob, []byte) ([]da.Commitment, error)); ok { - return returnFunc(ctx, blobs, namespace) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, []da.Blob, []byte) []da.Commitment); ok { - r0 = returnFunc(ctx, blobs, namespace) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]da.Commitment) - } - } - if returnFunc, ok := ret.Get(1).(func(context.Context, []da.Blob, []byte) error); ok { - r1 = returnFunc(ctx, blobs, namespace) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// MockDA_Commit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Commit' -type MockDA_Commit_Call struct { - *mock.Call -} - -// Commit is a helper method to define mock.On call -// - ctx context.Context -// - blobs []da.Blob -// - namespace []byte -func (_e *MockDA_Expecter) Commit(ctx interface{}, blobs interface{}, namespace interface{}) *MockDA_Commit_Call { - return &MockDA_Commit_Call{Call: _e.mock.On("Commit", ctx, blobs, namespace)} -} - -func (_c *MockDA_Commit_Call) Run(run func(ctx context.Context, blobs []da.Blob, namespace []byte)) *MockDA_Commit_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 []da.Blob - if args[1] != nil { - arg1 = args[1].([]da.Blob) - } - var arg2 []byte - if args[2] != nil { - arg2 = args[2].([]byte) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *MockDA_Commit_Call) Return(vs []da.Commitment, err error) *MockDA_Commit_Call { - _c.Call.Return(vs, err) - return _c -} - -func (_c *MockDA_Commit_Call) RunAndReturn(run func(ctx context.Context, blobs []da.Blob, namespace []byte) ([]da.Commitment, error)) *MockDA_Commit_Call { - _c.Call.Return(run) - return _c -} - -// Get provides a mock function for the type MockDA -func (_mock *MockDA) Get(ctx context.Context, ids []da.ID, namespace []byte) ([]da.Blob, error) { - ret := _mock.Called(ctx, ids, namespace) - - if len(ret) == 0 { - panic("no return value specified for Get") - } - - var r0 []da.Blob - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, []da.ID, []byte) ([]da.Blob, error)); ok { - return returnFunc(ctx, ids, namespace) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, []da.ID, []byte) []da.Blob); ok { - r0 = returnFunc(ctx, ids, namespace) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]da.Blob) - } - } - if returnFunc, ok := ret.Get(1).(func(context.Context, []da.ID, []byte) error); ok { - r1 = returnFunc(ctx, ids, namespace) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// MockDA_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' -type MockDA_Get_Call struct { - *mock.Call -} - -// Get is a helper method to define mock.On call -// - ctx context.Context -// - ids []da.ID -// - namespace []byte -func (_e *MockDA_Expecter) Get(ctx interface{}, ids interface{}, namespace interface{}) *MockDA_Get_Call { - return &MockDA_Get_Call{Call: _e.mock.On("Get", ctx, ids, namespace)} -} - -func (_c *MockDA_Get_Call) Run(run func(ctx context.Context, ids []da.ID, namespace []byte)) *MockDA_Get_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 []da.ID - if args[1] != nil { - arg1 = args[1].([]da.ID) - } - var arg2 []byte - if args[2] != nil { - arg2 = args[2].([]byte) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *MockDA_Get_Call) Return(vs []da.Blob, err error) *MockDA_Get_Call { - _c.Call.Return(vs, err) - return _c -} - -func (_c *MockDA_Get_Call) RunAndReturn(run func(ctx context.Context, ids []da.ID, namespace []byte) ([]da.Blob, error)) *MockDA_Get_Call { - _c.Call.Return(run) - return _c -} - -// GetIDs provides a mock function for the type MockDA -func (_mock *MockDA) GetIDs(ctx context.Context, height uint64, namespace []byte) (*da.GetIDsResult, error) { - ret := _mock.Called(ctx, height, namespace) - - if len(ret) == 0 { - panic("no return value specified for GetIDs") - } - - var r0 *da.GetIDsResult - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, uint64, []byte) (*da.GetIDsResult, error)); ok { - return returnFunc(ctx, height, namespace) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, uint64, []byte) *da.GetIDsResult); ok { - r0 = returnFunc(ctx, height, namespace) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*da.GetIDsResult) - } - } - if returnFunc, ok := ret.Get(1).(func(context.Context, uint64, []byte) error); ok { - r1 = returnFunc(ctx, height, namespace) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// MockDA_GetIDs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetIDs' -type MockDA_GetIDs_Call struct { - *mock.Call -} - -// GetIDs is a helper method to define mock.On call -// - ctx context.Context -// - height uint64 -// - namespace []byte -func (_e *MockDA_Expecter) GetIDs(ctx interface{}, height interface{}, namespace interface{}) *MockDA_GetIDs_Call { - return &MockDA_GetIDs_Call{Call: _e.mock.On("GetIDs", ctx, height, namespace)} -} - -func (_c *MockDA_GetIDs_Call) Run(run func(ctx context.Context, height uint64, namespace []byte)) *MockDA_GetIDs_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 uint64 - if args[1] != nil { - arg1 = args[1].(uint64) - } - var arg2 []byte - if args[2] != nil { - arg2 = args[2].([]byte) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *MockDA_GetIDs_Call) Return(getIDsResult *da.GetIDsResult, err error) *MockDA_GetIDs_Call { - _c.Call.Return(getIDsResult, err) - return _c -} - -func (_c *MockDA_GetIDs_Call) RunAndReturn(run func(ctx context.Context, height uint64, namespace []byte) (*da.GetIDsResult, error)) *MockDA_GetIDs_Call { - _c.Call.Return(run) - return _c -} - -// GetProofs provides a mock function for the type MockDA -func (_mock *MockDA) GetProofs(ctx context.Context, ids []da.ID, namespace []byte) ([]da.Proof, error) { - ret := _mock.Called(ctx, ids, namespace) - - if len(ret) == 0 { - panic("no return value specified for GetProofs") - } - - var r0 []da.Proof - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, []da.ID, []byte) ([]da.Proof, error)); ok { - return returnFunc(ctx, ids, namespace) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, []da.ID, []byte) []da.Proof); ok { - r0 = returnFunc(ctx, ids, namespace) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]da.Proof) - } - } - if returnFunc, ok := ret.Get(1).(func(context.Context, []da.ID, []byte) error); ok { - r1 = returnFunc(ctx, ids, namespace) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// MockDA_GetProofs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetProofs' -type MockDA_GetProofs_Call struct { - *mock.Call -} - -// GetProofs is a helper method to define mock.On call -// - ctx context.Context -// - ids []da.ID -// - namespace []byte -func (_e *MockDA_Expecter) GetProofs(ctx interface{}, ids interface{}, namespace interface{}) *MockDA_GetProofs_Call { - return &MockDA_GetProofs_Call{Call: _e.mock.On("GetProofs", ctx, ids, namespace)} -} - -func (_c *MockDA_GetProofs_Call) Run(run func(ctx context.Context, ids []da.ID, namespace []byte)) *MockDA_GetProofs_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 []da.ID - if args[1] != nil { - arg1 = args[1].([]da.ID) - } - var arg2 []byte - if args[2] != nil { - arg2 = args[2].([]byte) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *MockDA_GetProofs_Call) Return(vs []da.Proof, err error) *MockDA_GetProofs_Call { - _c.Call.Return(vs, err) - return _c -} - -func (_c *MockDA_GetProofs_Call) RunAndReturn(run func(ctx context.Context, ids []da.ID, namespace []byte) ([]da.Proof, error)) *MockDA_GetProofs_Call { - _c.Call.Return(run) - return _c -} - -// Submit provides a mock function for the type MockDA -func (_mock *MockDA) Submit(ctx context.Context, blobs []da.Blob, gasPrice float64, namespace []byte) ([]da.ID, error) { - ret := _mock.Called(ctx, blobs, gasPrice, namespace) - - if len(ret) == 0 { - panic("no return value specified for Submit") - } - - var r0 []da.ID - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, []da.Blob, float64, []byte) ([]da.ID, error)); ok { - return returnFunc(ctx, blobs, gasPrice, namespace) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, []da.Blob, float64, []byte) []da.ID); ok { - r0 = returnFunc(ctx, blobs, gasPrice, namespace) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]da.ID) - } - } - if returnFunc, ok := ret.Get(1).(func(context.Context, []da.Blob, float64, []byte) error); ok { - r1 = returnFunc(ctx, blobs, gasPrice, namespace) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// MockDA_Submit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Submit' -type MockDA_Submit_Call struct { - *mock.Call -} - -// Submit is a helper method to define mock.On call -// - ctx context.Context -// - blobs []da.Blob -// - gasPrice float64 -// - namespace []byte -func (_e *MockDA_Expecter) Submit(ctx interface{}, blobs interface{}, gasPrice interface{}, namespace interface{}) *MockDA_Submit_Call { - return &MockDA_Submit_Call{Call: _e.mock.On("Submit", ctx, blobs, gasPrice, namespace)} -} - -func (_c *MockDA_Submit_Call) Run(run func(ctx context.Context, blobs []da.Blob, gasPrice float64, namespace []byte)) *MockDA_Submit_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 []da.Blob - if args[1] != nil { - arg1 = args[1].([]da.Blob) - } - var arg2 float64 - if args[2] != nil { - arg2 = args[2].(float64) - } - var arg3 []byte - if args[3] != nil { - arg3 = args[3].([]byte) - } - run( - arg0, - arg1, - arg2, - arg3, - ) - }) - return _c -} - -func (_c *MockDA_Submit_Call) Return(vs []da.ID, err error) *MockDA_Submit_Call { - _c.Call.Return(vs, err) - return _c -} - -func (_c *MockDA_Submit_Call) RunAndReturn(run func(ctx context.Context, blobs []da.Blob, gasPrice float64, namespace []byte) ([]da.ID, error)) *MockDA_Submit_Call { - _c.Call.Return(run) - return _c -} - -// SubmitWithOptions provides a mock function for the type MockDA -func (_mock *MockDA) SubmitWithOptions(ctx context.Context, blobs []da.Blob, gasPrice float64, namespace []byte, options []byte) ([]da.ID, error) { - ret := _mock.Called(ctx, blobs, gasPrice, namespace, options) - - if len(ret) == 0 { - panic("no return value specified for SubmitWithOptions") - } - - var r0 []da.ID - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, []da.Blob, float64, []byte, []byte) ([]da.ID, error)); ok { - return returnFunc(ctx, blobs, gasPrice, namespace, options) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, []da.Blob, float64, []byte, []byte) []da.ID); ok { - r0 = returnFunc(ctx, blobs, gasPrice, namespace, options) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]da.ID) - } - } - if returnFunc, ok := ret.Get(1).(func(context.Context, []da.Blob, float64, []byte, []byte) error); ok { - r1 = returnFunc(ctx, blobs, gasPrice, namespace, options) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// MockDA_SubmitWithOptions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SubmitWithOptions' -type MockDA_SubmitWithOptions_Call struct { - *mock.Call -} - -// SubmitWithOptions is a helper method to define mock.On call -// - ctx context.Context -// - blobs []da.Blob -// - gasPrice float64 -// - namespace []byte -// - options []byte -func (_e *MockDA_Expecter) SubmitWithOptions(ctx interface{}, blobs interface{}, gasPrice interface{}, namespace interface{}, options interface{}) *MockDA_SubmitWithOptions_Call { - return &MockDA_SubmitWithOptions_Call{Call: _e.mock.On("SubmitWithOptions", ctx, blobs, gasPrice, namespace, options)} -} - -func (_c *MockDA_SubmitWithOptions_Call) Run(run func(ctx context.Context, blobs []da.Blob, gasPrice float64, namespace []byte, options []byte)) *MockDA_SubmitWithOptions_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 []da.Blob - if args[1] != nil { - arg1 = args[1].([]da.Blob) - } - var arg2 float64 - if args[2] != nil { - arg2 = args[2].(float64) - } - var arg3 []byte - if args[3] != nil { - arg3 = args[3].([]byte) - } - var arg4 []byte - if args[4] != nil { - arg4 = args[4].([]byte) - } - run( - arg0, - arg1, - arg2, - arg3, - arg4, - ) - }) - return _c -} - -func (_c *MockDA_SubmitWithOptions_Call) Return(vs []da.ID, err error) *MockDA_SubmitWithOptions_Call { - _c.Call.Return(vs, err) - return _c -} - -func (_c *MockDA_SubmitWithOptions_Call) RunAndReturn(run func(ctx context.Context, blobs []da.Blob, gasPrice float64, namespace []byte, options []byte) ([]da.ID, error)) *MockDA_SubmitWithOptions_Call { - _c.Call.Return(run) - return _c -} - -// Validate provides a mock function for the type MockDA -func (_mock *MockDA) Validate(ctx context.Context, ids []da.ID, proofs []da.Proof, namespace []byte) ([]bool, error) { - ret := _mock.Called(ctx, ids, proofs, namespace) - - if len(ret) == 0 { - panic("no return value specified for Validate") - } - - var r0 []bool - var r1 error - if returnFunc, ok := ret.Get(0).(func(context.Context, []da.ID, []da.Proof, []byte) ([]bool, error)); ok { - return returnFunc(ctx, ids, proofs, namespace) - } - if returnFunc, ok := ret.Get(0).(func(context.Context, []da.ID, []da.Proof, []byte) []bool); ok { - r0 = returnFunc(ctx, ids, proofs, namespace) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]bool) - } - } - if returnFunc, ok := ret.Get(1).(func(context.Context, []da.ID, []da.Proof, []byte) error); ok { - r1 = returnFunc(ctx, ids, proofs, namespace) - } else { - r1 = ret.Error(1) - } - return r0, r1 -} - -// MockDA_Validate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Validate' -type MockDA_Validate_Call struct { - *mock.Call -} - -// Validate is a helper method to define mock.On call -// - ctx context.Context -// - ids []da.ID -// - proofs []da.Proof -// - namespace []byte -func (_e *MockDA_Expecter) Validate(ctx interface{}, ids interface{}, proofs interface{}, namespace interface{}) *MockDA_Validate_Call { - return &MockDA_Validate_Call{Call: _e.mock.On("Validate", ctx, ids, proofs, namespace)} -} - -func (_c *MockDA_Validate_Call) Run(run func(ctx context.Context, ids []da.ID, proofs []da.Proof, namespace []byte)) *MockDA_Validate_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 []da.ID - if args[1] != nil { - arg1 = args[1].([]da.ID) - } - var arg2 []da.Proof - if args[2] != nil { - arg2 = args[2].([]da.Proof) - } - var arg3 []byte - if args[3] != nil { - arg3 = args[3].([]byte) - } - run( - arg0, - arg1, - arg2, - arg3, - ) - }) - return _c -} - -func (_c *MockDA_Validate_Call) Return(bools []bool, err error) *MockDA_Validate_Call { - _c.Call.Return(bools, err) - return _c -} - -func (_c *MockDA_Validate_Call) RunAndReturn(run func(ctx context.Context, ids []da.ID, proofs []da.Proof, namespace []byte) ([]bool, error)) *MockDA_Validate_Call { - _c.Call.Return(run) - return _c -} diff --git a/da/jsonrpc/client.go b/da/jsonrpc/client.go index 9803ebcd49..57cffd5bd6 100644 --- a/da/jsonrpc/client.go +++ b/da/jsonrpc/client.go @@ -2,184 +2,64 @@ package jsonrpc import ( "context" - "encoding/hex" "fmt" "net/http" - "strings" + "github.com/celestiaorg/go-square/v3/share" "github.com/filecoin-project/go-jsonrpc" "github.com/rs/zerolog" - "github.com/evstack/ev-node/core/da" + "github.com/evstack/ev-node/pkg/blob" ) -//go:generate mockgen -destination=mocks/api.go -package=mocks . Module -type Module interface { - da.DA -} - -// API defines the jsonrpc service module API +// API exposes the blob RPC methods used by the node. type API struct { Logger zerolog.Logger MaxBlobSize uint64 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"` - Commit func(ctx context.Context, blobs []da.Blob, ns []byte) ([]da.Commitment, error) `perm:"read"` - Validate func(context.Context, []da.ID, []da.Proof, []byte) ([]bool, error) `perm:"read"` - Submit func(context.Context, []da.Blob, float64, []byte) ([]da.ID, error) `perm:"write"` - SubmitWithOptions func(context.Context, []da.Blob, float64, []byte, []byte) ([]da.ID, error) `perm:"write"` + Submit func(context.Context, []*blob.Blob, *blob.SubmitOptions) (uint64, error) `perm:"write"` + GetAll func(context.Context, uint64, []share.Namespace) ([]*blob.Blob, error) `perm:"read"` + GetProof func(context.Context, uint64, share.Namespace, blob.Commitment) (*blob.Proof, error) `perm:"read"` + Included func(context.Context, uint64, share.Namespace, *blob.Proof, blob.Commitment) (bool, error) `perm:"read"` } } -// Get returns Blob for each given ID, or an error. -func (api *API) Get(ctx context.Context, ids []da.ID, ns []byte) ([]da.Blob, error) { - api.Logger.Debug().Str("method", "Get").Int("num_ids", len(ids)).Str("namespace", hex.EncodeToString(ns)).Msg("Making RPC call") - res, err := api.Internal.Get(ctx, ids, ns) - 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") - return res, context.Canceled - } - api.Logger.Error().Err(err).Str("method", "Get").Msg("RPC call failed") - // Wrap error for context, potentially using the translated error from the RPC library - return nil, fmt.Errorf("failed to get blobs: %w", err) - } - api.Logger.Debug().Str("method", "Get").Int("num_blobs_returned", len(res)).Msg("RPC call successful") - return res, nil +// Client is the jsonrpc client for the blob namespace implementing block/internal/da.BlobAPI. +type Client struct { + API API + closer multiClientCloser } -// GetIDs returns IDs of all Blobs located in DA at given height. -func (api *API) GetIDs(ctx context.Context, height uint64, ns []byte) (*da.GetIDsResult, error) { - api.Logger.Debug().Str("method", "GetIDs").Uint64("height", height).Str("namespace", hex.EncodeToString(ns)).Msg("Making RPC call") - res, err := api.Internal.GetIDs(ctx, height, ns) - 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 - if strings.Contains(err.Error(), da.ErrBlobNotFound.Error()) { // Use the error variable directly - api.Logger.Debug().Str("method", "GetIDs").Uint64("height", height).Msg("RPC call indicates blobs not found") - return nil, err // Return the specific ErrBlobNotFound - } - if strings.Contains(err.Error(), da.ErrHeightFromFuture.Error()) { - api.Logger.Debug().Str("method", "GetIDs").Uint64("height", height).Msg("RPC call indicates height from future") - return nil, err // Return the specific ErrHeightFromFuture - } - if strings.Contains(err.Error(), context.Canceled.Error()) { - api.Logger.Debug().Str("method", "GetIDs").Msg("RPC call canceled due to context cancellation") - return res, context.Canceled +// BlobAPI exposes the methods needed by block/internal/da. +func (c *Client) Submit(ctx context.Context, blobs []*blob.Blob, opts *blob.SubmitOptions) (uint64, error) { + if c.API.MaxBlobSize > 0 { + for i, b := range blobs { + if b == nil { + return 0, fmt.Errorf("blob %d is nil", i) + } + if uint64(len(b.Data())) > c.API.MaxBlobSize { + c.API.Logger.Warn(). + Int("index", i). + Int("size", len(b.Data())). + Uint64("max", c.API.MaxBlobSize). + Msg("blob rejected: size over limit") + return 0, fmt.Errorf("blob %d exceeds max blob size %d bytes", i, c.API.MaxBlobSize) + } } - api.Logger.Error().Err(err).Str("method", "GetIDs").Msg("RPC call failed") - return nil, err - } - - // Handle cases where the RPC call succeeds but returns no IDs - if res == nil || len(res.IDs) == 0 { - api.Logger.Debug().Str("method", "GetIDs").Uint64("height", height).Msg("RPC call successful but no IDs found") - return nil, da.ErrBlobNotFound // Return specific error for not found (use variable directly) } - - api.Logger.Debug().Str("method", "GetIDs").Msg("RPC call successful") - return res, nil + return c.API.Internal.Submit(ctx, blobs, opts) } -// GetProofs returns inclusion Proofs for Blobs specified by their IDs. -func (api *API) GetProofs(ctx context.Context, ids []da.ID, ns []byte) ([]da.Proof, error) { - api.Logger.Debug().Str("method", "GetProofs").Int("num_ids", len(ids)).Str("namespace", hex.EncodeToString(ns)).Msg("Making RPC call") - res, err := api.Internal.GetProofs(ctx, ids, ns) - if err != nil { - api.Logger.Error().Err(err).Str("method", "GetProofs").Msg("RPC call failed") - } else { - api.Logger.Debug().Str("method", "GetProofs").Int("num_proofs_returned", len(res)).Msg("RPC call successful") - } - return res, err +func (c *Client) GetAll(ctx context.Context, height uint64, namespaces []share.Namespace) ([]*blob.Blob, error) { + return c.API.Internal.GetAll(ctx, height, namespaces) } -// Commit creates a Commitment for each given Blob. -func (api *API) Commit(ctx context.Context, blobs []da.Blob, ns []byte) ([]da.Commitment, error) { - api.Logger.Debug().Str("method", "Commit").Int("num_blobs", len(blobs)).Str("namespace", hex.EncodeToString(ns)).Msg("Making RPC call") - res, err := api.Internal.Commit(ctx, blobs, ns) - if err != nil { - api.Logger.Error().Err(err).Str("method", "Commit").Msg("RPC call failed") - } else { - api.Logger.Debug().Str("method", "Commit").Int("num_commitments_returned", len(res)).Msg("RPC call successful") - } - return res, err +func (c *Client) GetProof(ctx context.Context, height uint64, namespace share.Namespace, commitment blob.Commitment) (*blob.Proof, error) { + return c.API.Internal.GetProof(ctx, height, namespace, commitment) } -// 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, ns []byte) ([]bool, error) { - api.Logger.Debug().Str("method", "Validate").Int("num_ids", len(ids)).Int("num_proofs", len(proofs)).Str("namespace", hex.EncodeToString(ns)).Msg("Making RPC call") - res, err := api.Internal.Validate(ctx, ids, proofs, ns) - if err != nil { - api.Logger.Error().Err(err).Str("method", "Validate").Msg("RPC call failed") - } else { - api.Logger.Debug().Str("method", "Validate").Int("num_results_returned", len(res)).Msg("RPC call successful") - } - return res, err -} - -// Submit submits the Blobs to Data Availability layer. -func (api *API) Submit(ctx context.Context, blobs []da.Blob, gasPrice float64, ns []byte) ([]da.ID, error) { - api.Logger.Debug().Str("method", "Submit").Int("num_blobs", len(blobs)).Float64("gas_price", gasPrice).Str("namespace", hex.EncodeToString(ns)).Msg("Making RPC call") - res, err := api.Internal.Submit(ctx, blobs, gasPrice, ns) - 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", ns).Msg("RPC call failed") - } else { - api.Logger.Debug().Str("method", "Submit").Int("num_ids_returned", len(res)).Msg("RPC call successful") - } - return res, err -} - -// 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, ns []byte, options []byte) ([]da.ID, error) { - maxBlobSize := api.MaxBlobSize - - if len(inputBlobs) == 0 { - return []da.ID{}, nil - } - - // Validate each blob individually and calculate total size - var totalSize uint64 - for i, blob := range inputBlobs { - blobLen := uint64(len(blob)) - if blobLen > maxBlobSize { - api.Logger.Warn().Int("index", i).Uint64("blobSize", blobLen).Uint64("maxBlobSize", maxBlobSize).Msg("Individual blob exceeds MaxBlobSize") - return nil, da.ErrBlobSizeOverLimit - } - totalSize += blobLen - } - - // Validate total batch size - if totalSize > maxBlobSize { - 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", hex.EncodeToString(ns)).Msg("Making RPC call") - res, err := api.Internal.SubmitWithOptions(ctx, inputBlobs, gasPrice, ns, 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") - return res, context.Canceled - } - api.Logger.Error().Err(err).Str("method", "SubmitWithOptions").Msg("RPC call failed") - } else { - api.Logger.Debug().Str("method", "SubmitWithOptions").Int("num_ids_returned", len(res)).Msg("RPC call successful") - } - - return res, err -} - -// Client is the jsonrpc client -type Client struct { - DA API - closer multiClientCloser +func (c *Client) Included(ctx context.Context, height uint64, namespace share.Namespace, proof *blob.Proof, commitment blob.Commitment) (bool, error) { + return c.API.Internal.Included(ctx, height, namespace, proof, commitment) } // multiClientCloser is a wrapper struct to close clients across multiple namespaces. @@ -204,38 +84,36 @@ func (c *Client) Close() { c.closer.closeAll() } -// NewClient creates a new Client with one connection per namespace with the -// given token as the authorization token. +// NewClient creates a new Client with one connection to the blob namespace. func NewClient(ctx context.Context, logger zerolog.Logger, addr, token string, maxBlobSize uint64) (*Client, error) { - authHeader := http.Header{"Authorization": []string{fmt.Sprintf("Bearer %s", token)}} + authHeader := http.Header{} + if token != "" { + authHeader.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + } return newClient(ctx, logger, addr, authHeader, maxBlobSize) } func newClient(ctx context.Context, logger zerolog.Logger, addr string, authHeader http.Header, maxBlobSize uint64) (*Client, error) { var multiCloser multiClientCloser var client Client - client.DA.Logger = logger - client.DA.MaxBlobSize = maxBlobSize + client.API.Logger = logger + client.API.MaxBlobSize = maxBlobSize - errs := getKnownErrorsMapping() for name, module := range moduleMap(&client) { - closer, err := jsonrpc.NewMergeClient(ctx, addr, name, []interface{}{module}, authHeader, jsonrpc.WithErrors(errs)) + closer, err := jsonrpc.NewMergeClient(ctx, addr, name, []interface{}{module}, authHeader) if err != nil { - // If an error occurs, close any previously opened connections multiCloser.closeAll() return nil, err } multiCloser.register(closer) } - client.closer = multiCloser // Assign the multiCloser to the client - + client.closer = multiCloser return &client, nil } func moduleMap(client *Client) map[string]interface{} { - // TODO: this duplication of strings many times across the codebase can be avoided with issue #1176 return map[string]interface{}{ - "da": &client.DA.Internal, + "blob": &client.API.Internal, } } diff --git a/da/jsonrpc/client_test.go b/da/jsonrpc/client_test.go deleted file mode 100644 index af32882ea9..0000000000 --- a/da/jsonrpc/client_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package jsonrpc - -import ( - "context" - "testing" - - "github.com/rs/zerolog" - "github.com/stretchr/testify/assert" - - "github.com/evstack/ev-node/core/da" -) - -// TestSubmitWithOptions_SizeValidation tests the corrected behavior of SubmitWithOptions -// where it validates the entire batch before submission and returns ErrBlobSizeOverLimit -// if the batch is too large, instead of silently dropping blobs. -func TestSubmitWithOptions_SizeValidation(t *testing.T) { - logger := zerolog.Nop() - - testCases := []struct { - name string - maxBlobSize uint64 - inputBlobs []da.Blob - expectError bool - expectedError error - description string - }{ - { - name: "Empty input", - maxBlobSize: 1000, - inputBlobs: []da.Blob{}, - expectError: false, - description: "Empty input should return empty result without error", - }, - { - name: "Single blob within limit", - maxBlobSize: 1000, - inputBlobs: []da.Blob{make([]byte, 500)}, - expectError: false, - description: "Single blob smaller than limit should succeed", - }, - { - name: "Single blob exceeds limit", - maxBlobSize: 1000, - inputBlobs: []da.Blob{make([]byte, 1500)}, - expectError: true, - expectedError: da.ErrBlobSizeOverLimit, - description: "Single blob larger than limit should fail", - }, - { - name: "Multiple blobs within limit", - maxBlobSize: 1000, - inputBlobs: []da.Blob{make([]byte, 300), make([]byte, 400), make([]byte, 200)}, - expectError: false, - description: "Multiple blobs totaling less than limit should succeed", - }, - { - name: "Multiple blobs exceed total limit", - maxBlobSize: 1000, - inputBlobs: []da.Blob{make([]byte, 400), make([]byte, 400), make([]byte, 400)}, - expectError: true, - expectedError: da.ErrBlobSizeOverLimit, - description: "Multiple blobs totaling more than limit should fail completely", - }, - { - name: "Mixed: some blobs fit, total exceeds limit", - maxBlobSize: 1000, - inputBlobs: []da.Blob{make([]byte, 100), make([]byte, 200), make([]byte, 800)}, - expectError: true, - expectedError: da.ErrBlobSizeOverLimit, - description: "Should fail completely, not partially submit blobs that fit", - }, - { - name: "One blob exceeds limit individually", - maxBlobSize: 1000, - inputBlobs: []da.Blob{make([]byte, 300), make([]byte, 1500), make([]byte, 200)}, - expectError: true, - expectedError: da.ErrBlobSizeOverLimit, - description: "Should fail if any individual blob exceeds limit", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // Create API with test configuration - api := &API{ - Logger: logger, - MaxBlobSize: tc.maxBlobSize, - } - - // Mock the Internal.SubmitWithOptions to always succeed if called - // This tests that our validation logic works before reaching the actual RPC call - mockCalled := false - api.Internal.SubmitWithOptions = func(ctx context.Context, blobs []da.Blob, gasPrice float64, namespace []byte, options []byte) ([]da.ID, error) { - mockCalled = true - // Return mock IDs for successful submissions - ids := make([]da.ID, len(blobs)) - for i := range blobs { - ids[i] = []byte{byte(i)} - } - return ids, nil - } - - // Call SubmitWithOptions - ctx := context.Background() - result, err := api.SubmitWithOptions(ctx, tc.inputBlobs, 1.0, []byte("test"), nil) - - // Verify expectations - if tc.expectError { - assert.Error(t, err, tc.description) - if tc.expectedError != nil { - assert.ErrorIs(t, err, tc.expectedError, tc.description) - } - assert.Nil(t, result, "Result should be nil on error") - assert.False(t, mockCalled, "Internal RPC should not be called when validation fails") - } else { - assert.NoError(t, err, tc.description) - assert.NotNil(t, result, "Result should not be nil on success") - if len(tc.inputBlobs) > 0 { - assert.True(t, mockCalled, "Internal RPC should be called for valid submissions") - assert.Len(t, result, len(tc.inputBlobs), "Should return IDs for all submitted blobs") - } - } - }) - } -} diff --git a/da/jsonrpc/errors.go b/da/jsonrpc/errors.go deleted file mode 100644 index c81040e899..0000000000 --- a/da/jsonrpc/errors.go +++ /dev/null @@ -1,21 +0,0 @@ -package jsonrpc - -import ( - "github.com/filecoin-project/go-jsonrpc" - - coreda "github.com/evstack/ev-node/core/da" -) - -// getKnownErrorsMapping returns a mapping of known error codes to their corresponding error types. -func getKnownErrorsMapping() jsonrpc.Errors { - errs := jsonrpc.NewErrors() - errs.Register(jsonrpc.ErrorCode(coreda.StatusNotFound), &coreda.ErrBlobNotFound) - errs.Register(jsonrpc.ErrorCode(coreda.StatusTooBig), &coreda.ErrBlobSizeOverLimit) - errs.Register(jsonrpc.ErrorCode(coreda.StatusContextDeadline), &coreda.ErrTxTimedOut) - errs.Register(jsonrpc.ErrorCode(coreda.StatusAlreadyInMempool), &coreda.ErrTxAlreadyInMempool) - errs.Register(jsonrpc.ErrorCode(coreda.StatusIncorrectAccountSequence), &coreda.ErrTxIncorrectAccountSequence) - errs.Register(jsonrpc.ErrorCode(coreda.StatusContextDeadline), &coreda.ErrContextDeadline) - errs.Register(jsonrpc.ErrorCode(coreda.StatusContextCanceled), &coreda.ErrContextCanceled) - errs.Register(jsonrpc.ErrorCode(coreda.StatusHeightFromFuture), &coreda.ErrHeightFromFuture) - return errs -} diff --git a/da/jsonrpc/proxy_test.go b/da/jsonrpc/proxy_test.go deleted file mode 100644 index 1ab623c037..0000000000 --- a/da/jsonrpc/proxy_test.go +++ /dev/null @@ -1,351 +0,0 @@ -package jsonrpc_test - -import ( - "bytes" - "context" - "errors" - "fmt" - "strings" - "sync" - "testing" - "time" - - "github.com/evstack/ev-node/da/internal/mocks" - proxy "github.com/evstack/ev-node/da/jsonrpc" - "github.com/rs/zerolog" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - coreda "github.com/evstack/ev-node/core/da" -) - -const ( - // ServerHost is the listen host for the test JSONRPC server - ServerHost = "localhost" - // ServerPort is the listen port for the test JSONRPC server - ServerPort = "3450" - // ClientURL is the url to dial for the test JSONRPC client - ClientURL = "http://localhost:3450" - - testMaxBlobSize = 100 - - DefaultMaxBlobSize = 2 * 1024 * 1024 // 2MB -) - -// 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 -// 3450 which is chosen to be sufficiently distinct from the default port - -func getTestDABlockTime() time.Duration { - return 100 * time.Millisecond -} - -func TestProxy(t *testing.T) { - dummy := coreda.NewDummyDA(100_000, getTestDABlockTime()) - dummy.StartHeightTicker() - logger := zerolog.Nop() - server := proxy.NewServer(logger, ServerHost, ServerPort, dummy) - err := server.Start(context.Background()) - require.NoError(t, err) - defer func() { - if err := server.Stop(context.Background()); err != nil { - require.NoError(t, err) - } - }() - - client, err := proxy.NewClient(context.Background(), logger, ClientURL, "74657374", DefaultMaxBlobSize) - require.NoError(t, err) - - t.Run("Basic DA test", func(t *testing.T) { - BasicDATest(t, &client.DA) - }) - t.Run("Get IDs and all data", func(t *testing.T) { - GetIDsTest(t, &client.DA) - }) - t.Run("Check Errors", func(t *testing.T) { - CheckErrors(t, &client.DA) - }) - t.Run("Concurrent read/write test", func(t *testing.T) { - ConcurrentReadWriteTest(t, &client.DA) - }) - t.Run("Given height is from the future", func(t *testing.T) { - HeightFromFutureTest(t, &client.DA) - }) - dummy.StopHeightTicker() -} - -// BasicDATest tests round trip of messages to DA and back. -func BasicDATest(t *testing.T, d coreda.DA) { - msg1 := []byte("message 1") - msg2 := []byte("message 2") - - ctx := t.Context() - id1, err := d.Submit(ctx, []coreda.Blob{msg1}, 0, testNamespace) - assert.NoError(t, err) - assert.NotEmpty(t, id1) - - id2, err := d.Submit(ctx, []coreda.Blob{msg2}, 0, testNamespace) - assert.NoError(t, err) - assert.NotEmpty(t, id2) - - time.Sleep(getTestDABlockTime()) - - id3, err := d.SubmitWithOptions(ctx, []coreda.Blob{msg1}, 0, testNamespace, []byte("random options")) - assert.NoError(t, err) - assert.NotEmpty(t, id3) - - assert.NotEqual(t, id1, id2) - assert.NotEqual(t, id1, id3) - - ret, err := d.Get(ctx, id1, testNamespace) - assert.NoError(t, err) - assert.Equal(t, []coreda.Blob{msg1}, ret) - - commitment1, err := d.Commit(ctx, []coreda.Blob{msg1}, []byte{}) - assert.NoError(t, err) - assert.NotEmpty(t, commitment1) - - commitment2, err := d.Commit(ctx, []coreda.Blob{msg2}, []byte{}) - assert.NoError(t, err) - assert.NotEmpty(t, commitment2) - - ids := []coreda.ID{id1[0], id2[0], id3[0]} - proofs, err := d.GetProofs(ctx, ids, testNamespace) - assert.NoError(t, err) - assert.NotEmpty(t, proofs) - oks, err := d.Validate(ctx, ids, proofs, testNamespace) - assert.NoError(t, err) - assert.NotEmpty(t, oks) - for _, ok := range oks { - assert.True(t, ok) - } -} - -// CheckErrors ensures that errors are handled properly by DA. -func CheckErrors(t *testing.T, d coreda.DA) { - ctx := t.Context() - blob, err := d.Get(ctx, []coreda.ID{[]byte("invalid blob id")}, testNamespace) - assert.Error(t, err) - assert.ErrorContains(t, err, coreda.ErrBlobNotFound.Error()) - assert.Empty(t, blob) -} - -// GetIDsTest tests iteration over DA -func GetIDsTest(t *testing.T, d coreda.DA) { - msgs := []coreda.Blob{[]byte("msg1"), []byte("msg2"), []byte("msg3")} - - ctx := t.Context() - ids, err := d.Submit(ctx, msgs, 0, testNamespace) - time.Sleep(getTestDABlockTime()) - assert.NoError(t, err) - assert.Len(t, ids, len(msgs)) - found := false - end := time.Now().Add(1 * time.Second) - - // To Keep It Simple: we assume working with DA used exclusively for this test (mock, devnet, etc) - // 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, testNamespace) - if err != nil { - if strings.Contains(err.Error(), coreda.ErrHeightFromFuture.Error()) { - break - } - t.Error("failed to get IDs:", err) - } - assert.NotNil(t, ret) - assert.NotZero(t, ret.Timestamp) - if len(ret.IDs) > 0 { - blobs, err := d.Get(ctx, ret.IDs, testNamespace) - assert.NoError(t, err) - - // Submit ensures atomicity of batch, so it makes sense to compare actual blobs (bodies) only when lengths - // of slices is the same. - if len(blobs) >= len(msgs) { - found = true - for _, msg := range msgs { - msgFound := false - for _, blob := range blobs { - if bytes.Equal(blob, msg) { - msgFound = true - break - } - } - if !msgFound { - found = false - break - } - } - } - } - } - - assert.True(t, found) -} - -// ConcurrentReadWriteTest tests the use of mutex lock in DummyDA by calling separate methods that use `d.data` and making sure there's no race conditions -func ConcurrentReadWriteTest(t *testing.T, d coreda.DA) { - var wg sync.WaitGroup - ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) - defer cancel() - - writeDone := make(chan struct{}) - - wg.Add(1) - go func() { - defer wg.Done() - for i := uint64(1); i <= 50; i++ { - _, err := d.Submit(ctx, []coreda.Blob{[]byte(fmt.Sprintf("test-%d", i))}, 0, []byte("test")) - assert.NoError(t, err) - } - close(writeDone) - }() - - wg.Add(1) - go func() { - defer wg.Done() - for { - select { - case <-writeDone: - return - default: - _, _ = d.GetIDs(ctx, 0, []byte("test")) - } - } - }() - - wg.Wait() -} - -// HeightFromFutureTest tests the case when the given height is from the future -func HeightFromFutureTest(t *testing.T, d coreda.DA) { - ctx := t.Context() - _, err := d.GetIDs(ctx, 999999999, []byte("test")) - assert.Error(t, err) - // Specifically check if the error contains the error message ErrHeightFromFuture - assert.ErrorContains(t, err, coreda.ErrHeightFromFuture.Error()) -} - -// TestSubmitWithOptions tests the SubmitWithOptions method with various scenarios -func TestSubmitWithOptions(t *testing.T) { - ctx := context.Background() - testNamespace := "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) - namespace := coreda.NamespaceFromString(testNamespace) - encodedNamespace := namespace.Bytes() - testOptions := []byte("test_options") - gasPrice := 0.0 - - // Helper function to create a client with a mocked internal API - createMockedClient := func(internalAPI *mocks.MockDA) *proxy.Client { - client := &proxy.Client{} - client.DA.Internal.SubmitWithOptions = internalAPI.SubmitWithOptions - client.DA.MaxBlobSize = testMaxBlobSize - client.DA.Logger = zerolog.Nop() - // Test verbosity no longer needed with Nop logger - return client - } - - t.Run("Happy Path - All blobs fit", func(t *testing.T) { - mockAPI := mocks.NewMockDA(t) - client := createMockedClient(mockAPI) - - blobs := []coreda.Blob{[]byte("blob1"), []byte("blob2")} - expectedIDs := []coreda.ID{[]byte("id1"), []byte("id2")} - - mockAPI.On("SubmitWithOptions", ctx, blobs, gasPrice, encodedNamespace, testOptions).Return(expectedIDs, nil).Once() - - ids, err := client.DA.SubmitWithOptions(ctx, blobs, gasPrice, encodedNamespace, testOptions) - - require.NoError(t, err) - assert.Equal(t, expectedIDs, ids) - mockAPI.AssertExpectations(t) - }) - - t.Run("Single Blob Too Large", func(t *testing.T) { - mockAPI := mocks.NewMockDA(t) - client := createMockedClient(mockAPI) - - largerBlob := make([]byte, testMaxBlobSize+1) - blobs := []coreda.Blob{largerBlob, []byte("this blob is definitely too large")} - - _, err := client.DA.SubmitWithOptions(ctx, blobs, gasPrice, encodedNamespace, testOptions) - - require.Error(t, err) - mockAPI.AssertExpectations(t) - }) - - t.Run("Total Size Exceeded", func(t *testing.T) { - mockAPI := mocks.NewMockDA(t) - client := createMockedClient(mockAPI) - - blobsizes := make([]byte, testMaxBlobSize/3) - blobsizesOver := make([]byte, testMaxBlobSize) - - blobs := []coreda.Blob{blobsizes, blobsizes, blobsizesOver} - - ids, err := client.DA.SubmitWithOptions(ctx, blobs, gasPrice, encodedNamespace, testOptions) - - require.Error(t, err) - assert.ErrorIs(t, err, coreda.ErrBlobSizeOverLimit) - assert.Nil(t, ids) - - // Should not call internal RPC when validation fails - mockAPI.AssertNotCalled(t, "SubmitWithOptions", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) - mockAPI.AssertExpectations(t) - }) - - t.Run("First Blob Too Large", func(t *testing.T) { - mockAPI := mocks.NewMockDA(t) - client := createMockedClient(mockAPI) - - largerBlob := make([]byte, testMaxBlobSize+1) - blobs := []coreda.Blob{largerBlob, []byte("small")} - - ids, err := client.DA.SubmitWithOptions(ctx, blobs, gasPrice, encodedNamespace, testOptions) - - require.Error(t, err) - assert.ErrorIs(t, err, coreda.ErrBlobSizeOverLimit) - assert.Nil(t, ids) - - mockAPI.AssertNotCalled(t, "SubmitWithOptions", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) - mockAPI.AssertExpectations(t) - }) - - t.Run("Empty Input Blobs", func(t *testing.T) { - mockAPI := mocks.NewMockDA(t) - client := createMockedClient(mockAPI) - - var blobs []coreda.Blob - - ids, err := client.DA.SubmitWithOptions(ctx, blobs, gasPrice, encodedNamespace, testOptions) - - require.NoError(t, err) - assert.Empty(t, ids) - - mockAPI.AssertNotCalled(t, "SubmitWithOptions", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) - mockAPI.AssertExpectations(t) - }) - - t.Run("Error During SubmitWithOptions RPC", func(t *testing.T) { - mockAPI := mocks.NewMockDA(t) - client := createMockedClient(mockAPI) - - blobs := []coreda.Blob{[]byte("blob1")} - expectedError := errors.New("rpc submit failed") - - mockAPI.On("SubmitWithOptions", ctx, blobs, gasPrice, encodedNamespace, testOptions).Return(nil, expectedError).Once() - - ids, err := client.DA.SubmitWithOptions(ctx, blobs, gasPrice, encodedNamespace, testOptions) - - require.Error(t, err) - assert.ErrorIs(t, err, expectedError) - assert.Nil(t, ids) - mockAPI.AssertExpectations(t) - }) -} diff --git a/da/jsonrpc/server.go b/da/jsonrpc/server.go index 456eefe908..46260ce077 100644 --- a/da/jsonrpc/server.go +++ b/da/jsonrpc/server.go @@ -7,78 +7,69 @@ import ( "sync/atomic" "time" + "github.com/celestiaorg/go-square/v3/share" "github.com/filecoin-project/go-jsonrpc" "github.com/rs/zerolog" - "github.com/evstack/ev-node/core/da" + "github.com/evstack/ev-node/pkg/blob" ) -// Server is a jsonrpc service that can serve the DA interface +// BlobAPI captures the blob RPC surface exposed over JSON-RPC. +type BlobAPI interface { + Submit(ctx context.Context, blobs []*blob.Blob, opts *blob.SubmitOptions) (uint64, error) + GetAll(ctx context.Context, height uint64, namespaces []share.Namespace) ([]*blob.Blob, error) + GetProof(ctx context.Context, height uint64, namespace share.Namespace, commitment blob.Commitment) (*blob.Proof, error) + Included(ctx context.Context, height uint64, namespace share.Namespace, proof *blob.Proof, commitment blob.Commitment) (bool, error) +} + +// Server is a jsonrpc service that serves the blob API surface type Server struct { logger zerolog.Logger srv *http.Server rpc *jsonrpc.RPCServer listener net.Listener - daImpl da.DA + blobAPI BlobAPI started atomic.Bool } // serverInternalAPI provides the actual RPC methods. type serverInternalAPI struct { - logger zerolog.Logger - daImpl da.DA -} - -// Get implements the RPC method. -func (s *serverInternalAPI) Get(ctx context.Context, ids []da.ID, ns []byte) ([]da.Blob, error) { - s.logger.Debug().Int("num_ids", len(ids)).Str("namespace", string(ns)).Msg("RPC server: Get called") - return s.daImpl.Get(ctx, ids, ns) -} - -// GetIDs implements the RPC method. -func (s *serverInternalAPI) GetIDs(ctx context.Context, height uint64, ns []byte) (*da.GetIDsResult, error) { - s.logger.Debug().Uint64("height", height).Str("namespace", string(ns)).Msg("RPC server: GetIDs called") - return s.daImpl.GetIDs(ctx, height, ns) -} - -// GetProofs implements the RPC method. -func (s *serverInternalAPI) GetProofs(ctx context.Context, ids []da.ID, ns []byte) ([]da.Proof, error) { - s.logger.Debug().Int("num_ids", len(ids)).Str("namespace", string(ns)).Msg("RPC server: GetProofs called") - return s.daImpl.GetProofs(ctx, ids, ns) + logger zerolog.Logger + blobAPI BlobAPI } -// Commit implements the RPC method. -func (s *serverInternalAPI) Commit(ctx context.Context, blobs []da.Blob, ns []byte) ([]da.Commitment, error) { - s.logger.Debug().Int("num_blobs", len(blobs)).Str("namespace", string(ns)).Msg("RPC server: Commit called") - return s.daImpl.Commit(ctx, blobs, ns) +// Submit implements the RPC submit method. +func (s *serverInternalAPI) Submit(ctx context.Context, blobs []*blob.Blob, opts *blob.SubmitOptions) (uint64, error) { + s.logger.Debug().Int("num_blobs", len(blobs)).Msg("RPC server: Submit called") + return s.blobAPI.Submit(ctx, blobs, opts) } -// Validate implements the RPC method. -func (s *serverInternalAPI) Validate(ctx context.Context, ids []da.ID, proofs []da.Proof, ns []byte) ([]bool, error) { - s.logger.Debug().Int("num_ids", len(ids)).Int("num_proofs", len(proofs)).Str("namespace", string(ns)).Msg("RPC server: Validate called") - return s.daImpl.Validate(ctx, ids, proofs, ns) +// GetAll implements the RPC method to fetch blobs by height/namespace. +func (s *serverInternalAPI) GetAll(ctx context.Context, height uint64, namespaces []share.Namespace) ([]*blob.Blob, error) { + s.logger.Debug().Uint64("height", height).Int("namespaces", len(namespaces)).Msg("RPC server: GetAll called") + return s.blobAPI.GetAll(ctx, height, namespaces) } -// Submit implements the RPC method. This is the primary submit method which includes options. -func (s *serverInternalAPI) Submit(ctx context.Context, blobs []da.Blob, gasPrice float64, ns []byte) ([]da.ID, error) { - s.logger.Debug().Int("num_blobs", len(blobs)).Float64("gas_price", gasPrice).Str("namespace", string(ns)).Msg("RPC server: Submit called") - return s.daImpl.Submit(ctx, blobs, gasPrice, ns) +// GetProof implements the RPC method to fetch a proof for a commitment. +func (s *serverInternalAPI) GetProof(ctx context.Context, height uint64, namespace share.Namespace, commitment blob.Commitment) (*blob.Proof, error) { + s.logger.Debug().Uint64("height", height).Msg("RPC server: GetProof called") + return s.blobAPI.GetProof(ctx, height, namespace, commitment) } -// SubmitWithOptions implements the RPC method. -func (s *serverInternalAPI) SubmitWithOptions(ctx context.Context, blobs []da.Blob, gasPrice float64, ns []byte, options []byte) ([]da.ID, error) { - s.logger.Debug().Int("num_blobs", len(blobs)).Float64("gas_price", gasPrice).Str("namespace", string(ns)).Str("options", string(options)).Msg("RPC server: SubmitWithOptions called") - return s.daImpl.SubmitWithOptions(ctx, blobs, gasPrice, ns, options) +// Included implements the RPC method to verify inclusion. +func (s *serverInternalAPI) Included(ctx context.Context, height uint64, namespace share.Namespace, proof *blob.Proof, commitment blob.Commitment) (bool, error) { + s.logger.Debug().Uint64("height", height).Msg("RPC server: Included called") + return s.blobAPI.Included(ctx, height, namespace, proof, commitment) } // NewServer accepts the host address port and the DA implementation to serve as a jsonrpc service -func NewServer(logger zerolog.Logger, address, port string, daImplementation da.DA) *Server { - rpc := jsonrpc.NewServer(jsonrpc.WithServerErrors(getKnownErrorsMapping())) +func NewServer(logger zerolog.Logger, address, port string, blobAPI BlobAPI) *Server { + rpc := jsonrpc.NewServer() srv := &Server{ - rpc: rpc, - logger: logger, - daImpl: daImplementation, + rpc: rpc, + logger: logger, + blobAPI: blobAPI, srv: &http.Server{ Addr: address + ":" + port, ReadHeaderTimeout: 2 * time.Second, @@ -87,11 +78,11 @@ func NewServer(logger zerolog.Logger, address, port string, daImplementation da. srv.srv.Handler = http.HandlerFunc(rpc.ServeHTTP) apiHandler := &serverInternalAPI{ - logger: logger, - daImpl: daImplementation, + logger: logger, + blobAPI: blobAPI, } - srv.rpc.Register("da", apiHandler) + srv.rpc.Register("blob", apiHandler) return srv } diff --git a/docs/learn/data-availability.md b/docs/learn/data-availability.md index 0a744ef945..da7e4fe598 100644 --- a/docs/learn/data-availability.md +++ b/docs/learn/data-availability.md @@ -17,7 +17,7 @@ Evolve is designed to be DA-agnostic, meaning it can integrate with different da - **External Data Availability Layer (DA Interface):** - Used for production and secure deployments. - - Evolve can post block data to any external DA layer that implements the Evolve [DA interface](https://github.com/evstack/ev-node/blob/main/core/da/da.go#L11) (e.g., Celestia). + - Evolve can post block data to any external DA layer that implements the blob [DA interface](https://github.com/evstack/ev-node/blob/main/block/internal/da/client.go#L41) (e.g., Celestia). - Anyone can verify that the data is available and reconstruct the chain state, depending on the guarantees of the chosen DA layer. ## Best Practices diff --git a/docs/learn/specs/da.md b/docs/learn/specs/da.md index d9f5ce5da7..64f31d9910 100644 --- a/docs/learn/specs/da.md +++ b/docs/learn/specs/da.md @@ -59,5 +59,5 @@ The separation of headers and data into different namespaces provides several ad [2] [jsonrpc][jsonrpc] -[da-interface]: https://github.com/evstack/ev-node/blob/main/core/da/da.go#L11 +[da-interface]: https://github.com/evstack/ev-node/blob/main/block/internal/da/client.go#L41 [jsonrpc]: https://github.com/evstack/ev-node/tree/main/da/jsonrpc diff --git a/docs/learn/specs/full_node.md b/docs/learn/specs/full_node.md index b6196ffd10..ce910db1b3 100644 --- a/docs/learn/specs/full_node.md +++ b/docs/learn/specs/full_node.md @@ -102,6 +102,6 @@ See [full node] [Store]: https://github.com/evstack/ev-node/blob/main/pkg/store/store.go [store interface]: https://github.com/evstack/ev-node/blob/main/pkg/store/types.go [Block Components]: https://github.com/evstack/ev-node/blob/main/block/components.go -[dalc]: https://github.com/evstack/ev-node/blob/main/core/da/da.go +[dalc]: https://github.com/evstack/ev-node/blob/main/block/internal/da/client.go [Header Sync Service]: https://github.com/evstack/ev-node/blob/main/pkg/sync/sync_service.go [Data Sync Service]: https://github.com/evstack/ev-node/blob/main/pkg/sync/sync_service.go diff --git a/execution/evm/test/execution_test.go b/execution/evm/test/execution_test.go index 34b2dfc67b..49dcecd25e 100644 --- a/execution/evm/test/execution_test.go +++ b/execution/evm/test/execution_test.go @@ -1,4 +1,5 @@ //go:build evm +// +build evm package test diff --git a/execution/evm/test/test_helpers.go b/execution/evm/test/test_helpers.go index e51a4e545b..fb7efeeb12 100644 --- a/execution/evm/test/test_helpers.go +++ b/execution/evm/test/test_helpers.go @@ -1,4 +1,5 @@ //go:build evm +// +build evm package test diff --git a/go.mod b/go.mod index cacb05d780..8b862f760f 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,20 @@ go 1.24.6 retract v0.12.0 // Published by accident +replace github.com/evstack/ev-node/core => ./core + +replace github.com/evstack/ev-node/da => ./da + require ( connectrpc.com/connect v1.19.1 connectrpc.com/grpcreflect v1.3.0 github.com/celestiaorg/go-header v0.7.4 + github.com/celestiaorg/go-square/merkle v0.0.0-20240627094109-7d01436067a3 github.com/celestiaorg/go-square/v3 v3.0.2 + github.com/celestiaorg/nmt v0.24.2 github.com/celestiaorg/utils v0.1.0 github.com/evstack/ev-node/core v1.0.0-beta.5 + github.com/evstack/ev-node/da v1.0.0-beta.6 github.com/go-kit/kit v0.13.0 github.com/goccy/go-yaml v1.19.0 github.com/ipfs/go-datastore v0.9.0 @@ -30,7 +37,6 @@ require ( golang.org/x/net v0.47.0 golang.org/x/sync v0.18.0 google.golang.org/protobuf v1.36.10 - gotest.tools/v3 v3.5.2 ) require ( @@ -45,6 +51,7 @@ require ( github.com/dgraph-io/ristretto/v2 v2.1.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/filecoin-project/go-clock v0.1.0 // indirect + github.com/filecoin-project/go-jsonrpc v0.9.0 // indirect github.com/flynn/noise v1.1.0 // indirect github.com/francoispqt/gojay v1.2.13 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect @@ -54,7 +61,6 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/flatbuffers v24.12.23+incompatible // indirect - github.com/google/go-cmp v0.7.0 // indirect github.com/google/gopacket v1.1.19 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gopherjs/gopherjs v0.0.0-20190812055157-5d271430af9f // indirect @@ -159,6 +165,7 @@ require ( golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.38.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gonum.org/v1/gonum v0.16.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.4.1 // indirect diff --git a/go.sum b/go.sum index 94ca45d02a..244dc8814a 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,12 @@ github.com/celestiaorg/go-header v0.7.4 h1:kQx3bVvKV+H2etxRi4IUuby5VQydBONx3giHF github.com/celestiaorg/go-header v0.7.4/go.mod h1:eX9iTSPthVEAlEDLux40ZT/olXPGhpxHd+mEzJeDhd0= 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/merkle v0.0.0-20240627094109-7d01436067a3 h1:wP84mtwOCVNOTfS3zErICjxKLnh74Z1uf+tdrlSFjVM= +github.com/celestiaorg/go-square/merkle v0.0.0-20240627094109-7d01436067a3/go.mod h1:86qIYnEhmn/hfW+xvw98NOI3zGaDEB3x8JGjYo2FqLs= github.com/celestiaorg/go-square/v3 v3.0.2 h1:eSQOgNII8inK9IhiBZ+6GADQeWbRq4HYY72BOgcduA4= github.com/celestiaorg/go-square/v3 v3.0.2/go.mod h1:oFReMLsSDMRs82ICFEeFQFCqNvwdsbIM1BzCcb0f7dM= +github.com/celestiaorg/nmt v0.24.2 h1:LlpJSPOd6/Lw1Ig6HUhZuqiINHLka/ZSRTBzlNJpchg= +github.com/celestiaorg/nmt v0.24.2/go.mod h1:vgLBpWBi8F5KLxTdXSwb7AU4NhiIQ1AQRGa+PzdcLEA= 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= @@ -62,10 +66,10 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evstack/ev-node/core v1.0.0-beta.5 h1:lgxE8XiF3U9pcFgh7xuKMgsOGvLBGRyd9kc9MR4WL0o= -github.com/evstack/ev-node/core v1.0.0-beta.5/go.mod h1:n2w/LhYQTPsi48m6lMj16YiIqsaQw6gxwjyJvR+B3sY= github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU= github.com/filecoin-project/go-clock v0.1.0/go.mod h1:4uB/O4PvOjlx1VCMdZ9MyDZXRm//gkj1ELEbxfI1AZs= +github.com/filecoin-project/go-jsonrpc v0.9.0 h1:G47qEF52w7GholpI21vPSTVBFvsrip6geIoqNiqyZtQ= +github.com/filecoin-project/go-jsonrpc v0.9.0/go.mod h1:OG7kVBVh/AbDFHIwx7Kw0l9ARmKOS6gGOr0LbdBpbLc= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= @@ -125,6 +129,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -430,6 +436,12 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= @@ -607,6 +619,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= @@ -653,8 +667,6 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= -gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/node/full.go b/node/full.go index 6d03a87c04..802458c8f4 100644 --- a/node/full.go +++ b/node/full.go @@ -17,8 +17,6 @@ import ( "github.com/rs/zerolog" "github.com/evstack/ev-node/block" - - coreda "github.com/evstack/ev-node/core/da" coreexecutor "github.com/evstack/ev-node/core/execution" coresequencer "github.com/evstack/ev-node/core/sequencer" "github.com/evstack/ev-node/pkg/config" @@ -53,8 +51,6 @@ type FullNode struct { nodeConfig config.Config - da coreda.DA - p2pClient *p2p.Client hSyncService *evsync.HeaderSyncService dSyncService *evsync.DataSyncService @@ -75,7 +71,7 @@ func newFullNode( database ds.Batching, exec coreexecutor.Executor, sequencer coresequencer.Sequencer, - da coreda.DA, + daClient block.DAClient, metricsProvider MetricsProvider, logger zerolog.Logger, nodeOpts NodeOptions, @@ -105,7 +101,7 @@ func newFullNode( rktStore, exec, sequencer, - da, + daClient, signer, headerSyncService, dataSyncService, @@ -119,7 +115,7 @@ func newFullNode( genesis, rktStore, exec, - da, + daClient, headerSyncService, dataSyncService, logger, @@ -136,7 +132,6 @@ func newFullNode( nodeConfig: nodeConfig, p2pClient: p2pClient, blockComponents: blockComponents, - da: da, Store: rktStore, hSyncService: headerSyncService, dSyncService: dataSyncService, diff --git a/node/full_node.md b/node/full_node.md index f1f96e6e31..8fc329129d 100644 --- a/node/full_node.md +++ b/node/full_node.md @@ -91,6 +91,6 @@ See [full node] [Store]: https://github.com/evstack/ev-node/blob/main/pkg/store/store.go [store interface]: https://github.com/evstack/ev-node/blob/main/pkg/store/types.go [Block Components]: https://github.com/evstack/ev-node/blob/main/block/components.go -[dalc]: https://github.com/evstack/ev-node/blob/main/core/da/da.go +[dalc]: https://github.com/evstack/ev-node/blob/main/block/internal/da/client.go [Header Sync Service]: https://github.com/evstack/ev-node/blob/main/pkg/sync/sync_service.go [Block Sync Service]: https://github.com/evstack/ev-node/blob/main/pkg/sync/sync_service.go diff --git a/node/helpers_test.go b/node/helpers_test.go index e77744a4ec..92d77d8988 100644 --- a/node/helpers_test.go +++ b/node/helpers_test.go @@ -11,16 +11,15 @@ import ( "time" testutils "github.com/celestiaorg/utils/test" + "github.com/evstack/ev-node/block" + coreexecutor "github.com/evstack/ev-node/core/execution" + coresequencer "github.com/evstack/ev-node/core/sequencer" "github.com/ipfs/go-datastore" dssync "github.com/ipfs/go-datastore/sync" "github.com/libp2p/go-libp2p/core/peer" "github.com/rs/zerolog" "github.com/stretchr/testify/require" - coreda "github.com/evstack/ev-node/core/da" - coreexecutor "github.com/evstack/ev-node/core/execution" - coresequencer "github.com/evstack/ev-node/core/sequencer" - evconfig "github.com/evstack/ev-node/pkg/config" "github.com/evstack/ev-node/pkg/p2p" "github.com/evstack/ev-node/pkg/p2p/key" @@ -43,15 +42,9 @@ const ( ) // createTestComponents creates test components for node initialization -func createTestComponents(t *testing.T, config evconfig.Config) (coreexecutor.Executor, coresequencer.Sequencer, coreda.DA, *p2p.Client, datastore.Batching, *key.NodeKey, func()) { +func createTestComponents(t *testing.T, config evconfig.Config) (coreexecutor.Executor, coresequencer.Sequencer, *p2p.Client, datastore.Batching, *key.NodeKey, func()) { executor := coreexecutor.NewDummyExecutor() sequencer := coresequencer.NewDummySequencer() - dummyDA := coreda.NewDummyDA(100_000, config.DA.BlockTime.Duration) - dummyDA.StartHeightTicker() - - stopDAHeightTicker := func() { - dummyDA.StopHeightTicker() - } // Create genesis and keys for P2P client _, genesisValidatorKey, _ := types.GetGenesisWithPrivkey("test-chain") @@ -65,7 +58,7 @@ func createTestComponents(t *testing.T, config evconfig.Config) (coreexecutor.Ex require.NotNil(t, p2pClient) ds := dssync.MutexWrap(datastore.NewMapDatastore()) - return executor, sequencer, dummyDA, p2pClient, ds, p2pKey, stopDAHeightTicker + return executor, sequencer, p2pClient, ds, p2pKey, func() {} } func getTestConfig(t *testing.T, n int) evconfig.Config { @@ -101,7 +94,7 @@ func newTestNode( config evconfig.Config, executor coreexecutor.Executor, sequencer coresequencer.Sequencer, - dac coreda.DA, + daClient block.DAClient, p2pClient *p2p.Client, ds datastore.Batching, stopDAHeightTicker func(), @@ -115,14 +108,13 @@ func newTestNode( config, executor, sequencer, - dac, remoteSigner, p2pClient, genesis, ds, DefaultMetricsProvider(evconfig.DefaultInstrumentationConfig()), zerolog.Nop(), - NodeOptions{}, + NodeOptions{DAClient: daClient}, ) require.NoError(t, err) @@ -136,8 +128,8 @@ func newTestNode( } func createNodeWithCleanup(t *testing.T, config evconfig.Config) (*FullNode, func()) { - executor, sequencer, dac, p2pClient, ds, _, stopDAHeightTicker := createTestComponents(t, config) - return newTestNode(t, config, executor, sequencer, dac, p2pClient, ds, stopDAHeightTicker) + executor, sequencer, p2pClient, ds, _, cleanup := createTestComponents(t, config) + return newTestNode(t, config, executor, sequencer, nil, p2pClient, ds, cleanup) } func createNodeWithCustomComponents( @@ -145,12 +137,18 @@ func createNodeWithCustomComponents( config evconfig.Config, executor coreexecutor.Executor, sequencer coresequencer.Sequencer, - dac coreda.DA, + daClient block.DAClient, p2pClient *p2p.Client, ds datastore.Batching, stopDAHeightTicker func(), ) (*FullNode, func()) { - return newTestNode(t, config, executor, sequencer, dac, p2pClient, ds, stopDAHeightTicker) + return newTestNode(t, config, executor, sequencer, daClient, p2pClient, ds, stopDAHeightTicker) +} + +// createTestDAClient creates a shared DA client for tests using LocalBlobAPI. +// All nodes in a test should share this client so they see the same DA state. +func createTestDAClient(config evconfig.Config, logger zerolog.Logger) block.DAClient { + return block.NewLocalDAClient(config, logger) } // Creates the given number of nodes the given nodes using the given wait group to synchronize them @@ -168,7 +166,7 @@ func createNodesWithCleanup(t *testing.T, num int, config evconfig.Config) ([]*F aggListenAddress := config.P2P.ListenAddress aggPeers := config.P2P.Peers - executor, sequencer, dac, p2pClient, ds, aggP2PKey, stopDAHeightTicker := createTestComponents(t, config) + executor, sequencer, p2pClient, ds, aggP2PKey, stopComponents := createTestComponents(t, config) aggPeerID, err := peer.IDFromPrivateKey(aggP2PKey.PrivKey) require.NoError(err) @@ -176,24 +174,27 @@ func createNodesWithCleanup(t *testing.T, num int, config evconfig.Config) ([]*F if testing.Verbose() { logger = zerolog.New(zerolog.NewTestWriter(t)) } + + // Create a shared DA client for all nodes + sharedDAClient := createTestDAClient(config, logger) + aggNode, err := NewNode( config, executor, sequencer, - dac, remoteSigner, p2pClient, genesis, ds, DefaultMetricsProvider(evconfig.DefaultInstrumentationConfig()), logger, - NodeOptions{}, + NodeOptions{DAClient: sharedDAClient}, ) require.NoError(err) // Update cleanup to cancel the context instead of calling Stop cleanup := func() { - stopDAHeightTicker() + stopComponents() } nodes[0], cleanups[0] = aggNode.(*FullNode), cleanup @@ -209,24 +210,23 @@ func createNodesWithCleanup(t *testing.T, num int, config evconfig.Config) ([]*F } config.P2P.ListenAddress = fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", 40001+i) config.RPC.Address = fmt.Sprintf("127.0.0.1:%d", 8001+i) - executor, sequencer, _, p2pClient, _, nodeP2PKey, stopDAHeightTicker := createTestComponents(t, config) + executor, sequencer, p2pClient, _, nodeP2PKey, stopComponents := createTestComponents(t, config) node, err := NewNode( config, executor, sequencer, - dac, nil, p2pClient, genesis, dssync.MutexWrap(datastore.NewMapDatastore()), DefaultMetricsProvider(evconfig.DefaultInstrumentationConfig()), logger, - NodeOptions{}, + NodeOptions{DAClient: sharedDAClient}, ) require.NoError(err) // Update cleanup to cancel the context instead of calling Stop cleanup := func() { - stopDAHeightTicker() + stopComponents() } nodes[i], cleanups[i] = node.(*FullNode), cleanup nodePeerID, err := peer.IDFromPrivateKey(nodeP2PKey.PrivKey) diff --git a/node/node.go b/node/node.go index 4d780035aa..eeeb303554 100644 --- a/node/node.go +++ b/node/node.go @@ -5,7 +5,6 @@ import ( "github.com/rs/zerolog" "github.com/evstack/ev-node/block" - coreda "github.com/evstack/ev-node/core/da" coreexecutor "github.com/evstack/ev-node/core/execution" coresequencer "github.com/evstack/ev-node/core/sequencer" "github.com/evstack/ev-node/pkg/config" @@ -24,6 +23,7 @@ type Node interface { type NodeOptions struct { BlockOptions block.BlockOptions + DAClient block.DAClient } // NewNode returns a new Full or Light Node based on the config @@ -33,7 +33,6 @@ func NewNode( conf config.Config, exec coreexecutor.Executor, sequencer coresequencer.Sequencer, - da coreda.DA, signer signer.Signer, p2pClient *p2p.Client, genesis genesis.Genesis, @@ -50,6 +49,11 @@ func NewNode( nodeOptions.BlockOptions = block.DefaultBlockOptions() } + daClient := nodeOptions.DAClient + if daClient == nil { + daClient = block.NewDAClient(conf, logger) + } + return newFullNode( conf, p2pClient, @@ -58,7 +62,7 @@ func NewNode( database, exec, sequencer, - da, + daClient, metricsProvider, logger, nodeOptions, diff --git a/node/single_sequencer_integration_test.go b/node/single_sequencer_integration_test.go index 22b2fd4506..7b069bb0b5 100644 --- a/node/single_sequencer_integration_test.go +++ b/node/single_sequencer_integration_test.go @@ -8,15 +8,16 @@ import ( "fmt" "net/http" "sync" + "sync/atomic" "testing" "time" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - coreda "github.com/evstack/ev-node/core/da" coreexecutor "github.com/evstack/ev-node/core/execution" evconfig "github.com/evstack/ev-node/pkg/config" + datypes "github.com/evstack/ev-node/pkg/da/types" ) // FullNodeTestSuite is a test suite for full node integration tests @@ -231,8 +232,8 @@ func TestStateRecovery(t *testing.T) { // Set up one sequencer config := getTestConfig(t, 1) - executor, sequencer, dac, p2pClient, ds, _, stopDAHeightTicker := createTestComponents(t, config) - node, cleanup := createNodeWithCustomComponents(t, config, executor, sequencer, dac, p2pClient, ds, stopDAHeightTicker) + executor, sequencer, p2pClient, ds, _, stopDAHeightTicker := createTestComponents(t, config) + node, cleanup := createNodeWithCustomComponents(t, config, executor, sequencer, nil, p2pClient, ds, stopDAHeightTicker) defer cleanup() var runningWg sync.WaitGroup @@ -256,8 +257,8 @@ func TestStateRecovery(t *testing.T) { shutdownAndWait(t, []context.CancelFunc{cancel}, &runningWg, 60*time.Second) // Create a new node instance using the same components - executor, sequencer, dac, p2pClient, _, _, stopDAHeightTicker = createTestComponents(t, config) - node, cleanup = createNodeWithCustomComponents(t, config, executor, sequencer, dac, p2pClient, ds, stopDAHeightTicker) + executor, sequencer, p2pClient, _, _, stopDAHeightTicker = createTestComponents(t, config) + node, cleanup = createNodeWithCustomComponents(t, config, executor, sequencer, nil, p2pClient, ds, stopDAHeightTicker) defer cleanup() // Verify state persistence @@ -313,19 +314,16 @@ func TestBatchQueueThrottlingWithDAFailure(t *testing.T) { config.DA.BlockTime = evconfig.DurationWrapper{Duration: 100 * time.Millisecond} // Longer DA time to ensure blocks are produced first // Create test components - executor, sequencer, dummyDA, p2pClient, ds, _, stopDAHeightTicker := createTestComponents(t, config) + executor, sequencer, p2pClient, ds, _, stopDAHeightTicker := createTestComponents(t, config) defer stopDAHeightTicker() + daClient := newControllableDAClient(config) // Cast executor to DummyExecutor so we can inject transactions dummyExecutor, ok := executor.(*coreexecutor.DummyExecutor) require.True(ok, "Expected DummyExecutor implementation") - // Cast dummyDA to our enhanced version so we can make it fail - dummyDAImpl, ok := dummyDA.(*coreda.DummyDA) - require.True(ok, "Expected DummyDA implementation") - // Create node with components - node, cleanup := createNodeWithCustomComponents(t, config, executor, sequencer, dummyDAImpl, p2pClient, ds, func() {}) + node, cleanup := createNodeWithCustomComponents(t, config, executor, sequencer, daClient, p2pClient, ds, func() {}) defer cleanup() ctx, cancel := context.WithCancel(t.Context()) @@ -352,7 +350,7 @@ func TestBatchQueueThrottlingWithDAFailure(t *testing.T) { // Simulate DA layer going down t.Log("Simulating DA layer failure") - dummyDAImpl.SetSubmitFailure(true) + daClient.SetSubmitFailure(true) // Continue injecting transactions - this tests the behavior when: // 1. DA layer is down (can't submit blocks to DA) @@ -461,3 +459,48 @@ func TestReadinessEndpointWhenBlockProductionStops(t *testing.T) { shutdownAndWait(t, []context.CancelFunc{cancel}, &runningWg, 10*time.Second) } + +// controllableDAClient is a lightweight DA client tailored for integration tests. It records +// submitted heights via an atomic counter and can simulate failures by toggling submitFailure. +// Retrieval methods are implemented to satisfy interfaces but return empty data to keep tests focused. +type controllableDAClient struct { + headerNamespace []byte + dataNamespace []byte + submitFailure atomic.Bool + latestHeight atomic.Uint64 +} + +func newControllableDAClient(cfg evconfig.Config) *controllableDAClient { + return &controllableDAClient{ + headerNamespace: []byte(cfg.DA.Namespace), + dataNamespace: []byte(cfg.DA.DataNamespace), + } +} + +// Submit adheres to block/internal/da.Client interface; it increments synthetic height for successful submissions. +func (c *controllableDAClient) Submit(ctx context.Context, data [][]byte, namespace []byte, options []byte) datypes.ResultSubmit { + if c.submitFailure.Load() { + return datypes.ResultSubmit{BaseResult: datypes.BaseResult{Code: datypes.StatusError, Message: "forced failure"}} + } + newHeight := c.latestHeight.Add(1) + return datypes.ResultSubmit{BaseResult: datypes.BaseResult{Code: datypes.StatusSuccess, Height: newHeight, SubmittedCount: uint64(len(data)), Timestamp: time.Now()}} +} + +func (c *controllableDAClient) Retrieve(ctx context.Context, height uint64, namespace []byte) datypes.ResultRetrieve { + return datypes.ResultRetrieve{BaseResult: datypes.BaseResult{Code: datypes.StatusNotFound}} +} + +func (c *controllableDAClient) RetrieveHeaders(ctx context.Context, height uint64) datypes.ResultRetrieve { + return c.Retrieve(ctx, height, c.headerNamespace) +} + +func (c *controllableDAClient) RetrieveData(ctx context.Context, height uint64) datypes.ResultRetrieve { + return c.Retrieve(ctx, height, c.dataNamespace) +} + +func (c *controllableDAClient) GetHeaderNamespace() []byte { return c.headerNamespace } +func (c *controllableDAClient) GetDataNamespace() []byte { return c.dataNamespace } + +func (c *controllableDAClient) SetSubmitFailure(val bool) { + c.submitFailure.Store(val) +} diff --git a/pkg/blob/blob.go b/pkg/blob/blob.go new file mode 100644 index 0000000000..728ca560a9 --- /dev/null +++ b/pkg/blob/blob.go @@ -0,0 +1,154 @@ +package blob + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "fmt" + + "github.com/celestiaorg/go-square/merkle" + "github.com/celestiaorg/go-square/v3/inclusion" + libshare "github.com/celestiaorg/go-square/v3/share" + "github.com/celestiaorg/nmt" +) + +// Commitment is the Merkle subtree commitment for a blob. +type Commitment []byte + +// Proof is a set of NMT proofs used to verify a blob inclusion. +// This mirrors celestia-node's blob.Proof shape. +type Proof []*nmt.Proof + +// DefaultMaxBlobSize is the default maximum blob size used by celestia-app (32 MiB). +const DefaultMaxBlobSize = 32 * 1_048_576 // bytes + +// subtreeRootThreshold is copied from celestia-app/v6 appconsts.SubtreeRootThreshold. +// It controls the branching factor when generating commitments. +const subtreeRootThreshold = 64 + +// Blob represents application-specific binary data that can be submitted to Celestia. +// It is intentionally compatible with celestia-node's blob.Blob JSON shape. +type Blob struct { + *libshare.Blob `json:"blob"` + + Commitment Commitment `json:"commitment"` + + // index is the index of the blob's first share in the EDS. + // Only blobs retrieved from the chain will have this set; default is -1. + index int +} + +// NewBlobV0 builds a version 0 blob (the only version we currently need). +func NewBlobV0(namespace libshare.Namespace, data []byte) (*Blob, error) { + return NewBlob(libshare.ShareVersionZero, namespace, data, nil) +} + +// NewBlob constructs a new blob from the provided namespace, data, signer, and share version. +// This is a lightly adapted copy of celestia-node/blob.NewBlob. +func NewBlob(shareVersion uint8, namespace libshare.Namespace, data, signer []byte) (*Blob, error) { + if err := namespace.ValidateForBlob(); err != nil { + return nil, fmt.Errorf("invalid namespace: %w", err) + } + + libBlob, err := libshare.NewBlob(namespace, data, shareVersion, signer) + if err != nil { + return nil, fmt.Errorf("build blob: %w", err) + } + + com, err := inclusion.CreateCommitment(libBlob, merkle.HashFromByteSlices, subtreeRootThreshold) + if err != nil { + return nil, fmt.Errorf("create commitment: %w", err) + } + + return &Blob{ + Blob: libBlob, + Commitment: com, + index: -1, + }, nil +} + +// Namespace returns the blob namespace. +func (b *Blob) Namespace() libshare.Namespace { + return b.Blob.Namespace() +} + +// Index returns the blob's first share index in the EDS (or -1 if unknown). +func (b *Blob) Index() int { + return b.index +} + +// MarshalJSON matches celestia-node's blob JSON encoding. +func (b *Blob) MarshalJSON() ([]byte, error) { + type jsonBlob struct { + Namespace []byte `json:"namespace"` + Data []byte `json:"data"` + ShareVersion uint8 `json:"share_version"` + Commitment Commitment `json:"commitment"` + Signer []byte `json:"signer,omitempty"` + Index int `json:"index"` + } + + jb := &jsonBlob{ + Namespace: b.Namespace().Bytes(), + Data: b.Data(), + ShareVersion: b.ShareVersion(), + Commitment: b.Commitment, + Signer: b.Signer(), + Index: b.index, + } + return json.Marshal(jb) +} + +// UnmarshalJSON matches celestia-node's blob JSON decoding. +func (b *Blob) UnmarshalJSON(data []byte) error { + type jsonBlob struct { + Namespace []byte `json:"namespace"` + Data []byte `json:"data"` + ShareVersion uint8 `json:"share_version"` + Commitment Commitment `json:"commitment"` + Signer []byte `json:"signer,omitempty"` + Index int `json:"index"` + } + + var jb jsonBlob + if err := json.Unmarshal(data, &jb); err != nil { + return err + } + + ns, err := libshare.NewNamespaceFromBytes(jb.Namespace) + if err != nil { + return err + } + + blob, err := NewBlob(jb.ShareVersion, ns, jb.Data, jb.Signer) + if err != nil { + return err + } + + blob.Commitment = jb.Commitment + blob.index = jb.Index + *b = *blob + return nil +} + +// MakeID constructs a blob ID by prefixing the commitment with the height (little endian). +func MakeID(height uint64, commitment Commitment) []byte { + id := make([]byte, 8+len(commitment)) + binary.LittleEndian.PutUint64(id, height) + copy(id[8:], commitment) + return id +} + +// SplitID splits a blob ID into height and commitment. +// If the ID is malformed, it returns height 0 and nil commitment. +func SplitID(id []byte) (uint64, Commitment) { + if len(id) <= 8 { + return 0, nil + } + return binary.LittleEndian.Uint64(id[:8]), id[8:] +} + +// EqualCommitment compares the blob's commitment with the provided one. +func (b *Blob) EqualCommitment(com Commitment) bool { + return bytes.Equal(b.Commitment, com) +} diff --git a/pkg/blob/blob_test.go b/pkg/blob/blob_test.go new file mode 100644 index 0000000000..2949fe4dd0 --- /dev/null +++ b/pkg/blob/blob_test.go @@ -0,0 +1,34 @@ +package blob + +import ( + "encoding/json" + "testing" + + libshare "github.com/celestiaorg/go-square/v3/share" + "github.com/stretchr/testify/require" +) + +func TestMakeAndSplitID(t *testing.T) { + id := MakeID(42, []byte{0x01, 0x02, 0x03}) + height, com := SplitID(id) + require.Equal(t, uint64(42), height) + require.Equal(t, []byte{0x01, 0x02, 0x03}, []byte(com)) +} + +func TestBlobJSONRoundTrip(t *testing.T) { + ns := libshare.MustNewV0Namespace([]byte("test-ids")) + + blob, err := NewBlobV0(ns, []byte("hello")) + require.NoError(t, err) + require.NotEmpty(t, blob.Commitment) + + encoded, err := json.Marshal(blob) + require.NoError(t, err) + + var decoded Blob + require.NoError(t, json.Unmarshal(encoded, &decoded)) + + require.Equal(t, blob.Namespace().Bytes(), decoded.Namespace().Bytes()) + require.Equal(t, blob.Data(), decoded.Data()) + require.Equal(t, blob.Commitment, decoded.Commitment) +} diff --git a/pkg/blob/submit_options.go b/pkg/blob/submit_options.go new file mode 100644 index 0000000000..844901db85 --- /dev/null +++ b/pkg/blob/submit_options.go @@ -0,0 +1,61 @@ +package blob + +import "encoding/json" + +// TxPriority mirrors celestia-node/state.TxPriority to preserve JSON compatibility. +type TxPriority int + +const ( + TxPriorityLow TxPriority = iota + 1 + TxPriorityMedium + TxPriorityHigh +) + +// SubmitOptions is a pared-down copy of celestia-node/state.TxConfig JSON shape. +// Only exported fields are marshalled to match the RPC expectation of the blob service. +type SubmitOptions struct { + GasPrice float64 `json:"gas_price,omitempty"` + IsGasPriceSet bool `json:"is_gas_price_set,omitempty"` + MaxGasPrice float64 `json:"max_gas_price,omitempty"` + Gas uint64 `json:"gas,omitempty"` + TxPriority TxPriority `json:"tx_priority,omitempty"` + KeyName string `json:"key_name,omitempty"` + SignerAddress string `json:"signer_address,omitempty"` + FeeGranterAddress string `json:"fee_granter_address,omitempty"` +} + +// MarshalJSON ensures we only emit the fields the remote RPC understands. +func (cfg SubmitOptions) MarshalJSON() ([]byte, error) { + type jsonSubmitOptions struct { + GasPrice float64 `json:"gas_price,omitempty"` + IsGasPriceSet bool `json:"is_gas_price_set,omitempty"` + MaxGasPrice float64 `json:"max_gas_price,omitempty"` + Gas uint64 `json:"gas,omitempty"` + TxPriority TxPriority `json:"tx_priority,omitempty"` + KeyName string `json:"key_name,omitempty"` + SignerAddress string `json:"signer_address,omitempty"` + FeeGranterAddress string `json:"fee_granter_address,omitempty"` + } + return json.Marshal(jsonSubmitOptions(cfg)) +} + +// UnmarshalJSON decodes the RPC shape back into SubmitOptions. +func (cfg *SubmitOptions) UnmarshalJSON(data []byte) error { + type jsonSubmitOptions struct { + GasPrice float64 `json:"gas_price,omitempty"` + IsGasPriceSet bool `json:"is_gas_price_set,omitempty"` + MaxGasPrice float64 `json:"max_gas_price,omitempty"` + Gas uint64 `json:"gas,omitempty"` + TxPriority TxPriority `json:"tx_priority,omitempty"` + KeyName string `json:"key_name,omitempty"` + SignerAddress string `json:"signer_address,omitempty"` + FeeGranterAddress string `json:"fee_granter_address,omitempty"` + } + + var j jsonSubmitOptions + if err := json.Unmarshal(data, &j); err != nil { + return err + } + *cfg = SubmitOptions(j) + return nil +} diff --git a/pkg/cmd/run_node.go b/pkg/cmd/run_node.go index fe42707f85..d682155849 100644 --- a/pkg/cmd/run_node.go +++ b/pkg/cmd/run_node.go @@ -16,7 +16,6 @@ import ( "github.com/rs/zerolog" "github.com/spf13/cobra" - coreda "github.com/evstack/ev-node/core/da" coreexecutor "github.com/evstack/ev-node/core/execution" coresequencer "github.com/evstack/ev-node/core/sequencer" "github.com/evstack/ev-node/node" @@ -82,7 +81,6 @@ func StartNode( cmd *cobra.Command, executor coreexecutor.Executor, sequencer coresequencer.Sequencer, - da coreda.DA, p2pClient *p2p.Client, datastore datastore.Batching, nodeConfig rollconf.Config, @@ -137,7 +135,6 @@ func StartNode( nodeConfig, executor, sequencer, - da, signer, p2pClient, genesis, diff --git a/pkg/cmd/run_node_test.go b/pkg/cmd/run_node_test.go index 16430ee450..a4400ae12e 100644 --- a/pkg/cmd/run_node_test.go +++ b/pkg/cmd/run_node_test.go @@ -8,12 +8,6 @@ import ( "testing" "time" - "github.com/ipfs/go-datastore" - "github.com/rs/zerolog" - "github.com/spf13/cobra" - "github.com/stretchr/testify/assert" - - coreda "github.com/evstack/ev-node/core/da" coreexecutor "github.com/evstack/ev-node/core/execution" coresequencer "github.com/evstack/ev-node/core/sequencer" "github.com/evstack/ev-node/node" @@ -22,18 +16,17 @@ import ( "github.com/evstack/ev-node/pkg/p2p" "github.com/evstack/ev-node/pkg/signer" filesigner "github.com/evstack/ev-node/pkg/signer/file" + "github.com/ipfs/go-datastore" + "github.com/rs/zerolog" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" ) const MockDANamespace = "test" -func createTestComponents(_ context.Context, t *testing.T) (coreexecutor.Executor, coresequencer.Sequencer, coreda.DA, signer.Signer, *p2p.Client, datastore.Batching, func()) { +func createTestComponents(_ context.Context, t *testing.T) (coreexecutor.Executor, coresequencer.Sequencer, signer.Signer, *p2p.Client, datastore.Batching, func()) { executor := coreexecutor.NewDummyExecutor() sequencer := coresequencer.NewDummySequencer() - dummyDA := coreda.NewDummyDA(100_000, 10*time.Second) - dummyDA.StartHeightTicker() - stopDAHeightTicker := func() { - dummyDA.StopHeightTicker() - } tmpDir := t.TempDir() keyProvider, err := filesigner.CreateFileSystemSigner(filepath.Join(tmpDir, "config"), []byte{}) if err != nil { @@ -43,7 +36,7 @@ func createTestComponents(_ context.Context, t *testing.T) (coreexecutor.Executo p2pClient := &p2p.Client{} ds := datastore.NewMapDatastore() - return executor, sequencer, dummyDA, keyProvider, p2pClient, ds, stopDAHeightTicker + return executor, sequencer, keyProvider, p2pClient, ds, func() {} } func TestParseFlags(t *testing.T) { @@ -78,19 +71,19 @@ func TestParseFlags(t *testing.T) { args := append([]string{"start"}, flags...) - executor, sequencer, dac, keyProvider, p2pClient, ds, stopDAHeightTicker := createTestComponents(context.Background(), t) - defer stopDAHeightTicker() + executor, sequencer, keyProvider, p2pClient, ds, cleanup := createTestComponents(context.Background(), t) + defer cleanup() nodeConfig := rollconf.DefaultConfig() nodeConfig.RootDir = t.TempDir() - newRunNodeCmd := newRunNodeCmd(t.Context(), executor, sequencer, dac, keyProvider, p2pClient, ds, nodeConfig) - _ = newRunNodeCmd.Flags().Set(rollconf.FlagRootDir, "custom/root/dir") - if err := newRunNodeCmd.ParseFlags(args); err != nil { + runCmd := buildRunNodeCmd(t.Context(), executor, sequencer, keyProvider, p2pClient, ds, nodeConfig) + _ = runCmd.Flags().Set(rollconf.FlagRootDir, "custom/root/dir") + if err := runCmd.ParseFlags(args); err != nil { t.Errorf("Error: %v", err) } - nodeConfig, err := ParseConfig(newRunNodeCmd) + nodeConfig, err := ParseConfig(runCmd) if err != nil { t.Errorf("Error: %v", err) } @@ -153,20 +146,20 @@ func TestAggregatorFlagInvariants(t *testing.T) { for i, flags := range flagVariants { args := append([]string{"start"}, flags...) - executor, sequencer, dac, keyProvider, p2pClient, ds, stopDAHeightTicker := createTestComponents(context.Background(), t) - defer stopDAHeightTicker() + executor, sequencer, keyProvider, p2pClient, ds, cleanup := createTestComponents(context.Background(), t) + defer cleanup() nodeConfig := rollconf.DefaultConfig() nodeConfig.RootDir = t.TempDir() - newRunNodeCmd := newRunNodeCmd(t.Context(), executor, sequencer, dac, keyProvider, p2pClient, ds, nodeConfig) - _ = newRunNodeCmd.Flags().Set(rollconf.FlagRootDir, "custom/root/dir") + runCmd := buildRunNodeCmd(t.Context(), executor, sequencer, keyProvider, p2pClient, ds, nodeConfig) + _ = runCmd.Flags().Set(rollconf.FlagRootDir, "custom/root/dir") - if err := newRunNodeCmd.ParseFlags(args); err != nil { + if err := runCmd.ParseFlags(args); err != nil { t.Errorf("Error: %v", err) } - nodeConfig, err := ParseConfig(newRunNodeCmd) + nodeConfig, err := ParseConfig(runCmd) if err != nil { t.Errorf("Error: %v", err) } @@ -189,14 +182,14 @@ func TestDefaultAggregatorValue(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - executor, sequencer, dac, keyProvider, p2pClient, ds, stopDAHeightTicker := createTestComponents(context.Background(), t) - defer stopDAHeightTicker() + executor, sequencer, keyProvider, p2pClient, ds, cleanup := createTestComponents(context.Background(), t) + defer cleanup() nodeConfig := rollconf.DefaultConfig() nodeConfig.RootDir = t.TempDir() - newRunNodeCmd := newRunNodeCmd(t.Context(), executor, sequencer, dac, keyProvider, p2pClient, ds, nodeConfig) - _ = newRunNodeCmd.Flags().Set(rollconf.FlagRootDir, "custom/root/dir") + runCmd := buildRunNodeCmd(t.Context(), executor, sequencer, keyProvider, p2pClient, ds, nodeConfig) + _ = runCmd.Flags().Set(rollconf.FlagRootDir, "custom/root/dir") // Create a new command without specifying any flags var args []string @@ -206,11 +199,11 @@ func TestDefaultAggregatorValue(t *testing.T) { args = []string{"start", "--rollkit.node.aggregator=false"} } - if err := newRunNodeCmd.ParseFlags(args); err != nil { + if err := runCmd.ParseFlags(args); err != nil { t.Errorf("Error parsing flags: %v", err) } - nodeConfig, err := ParseConfig(newRunNodeCmd) + nodeConfig, err := ParseConfig(runCmd) if err != nil { t.Errorf("Error parsing config: %v", err) } @@ -271,10 +264,10 @@ func TestCentralizedAddresses(t *testing.T) { "--rollkit.da.address=http://central-da:26657", } - executor, sequencer, dac, keyProvider, p2pClient, ds, stopDAHeightTicker := createTestComponents(context.Background(), t) - defer stopDAHeightTicker() + executor, sequencer, keyProvider, p2pClient, ds, cleanup := createTestComponents(context.Background(), t) + defer cleanup() - cmd := newRunNodeCmd(t.Context(), executor, sequencer, dac, keyProvider, p2pClient, ds, nodeConfig) + cmd := buildRunNodeCmd(t.Context(), executor, sequencer, keyProvider, p2pClient, ds, nodeConfig) _ = cmd.Flags().Set(rollconf.FlagRootDir, "custom/root/dir") if err := cmd.ParseFlags(args); err != nil { t.Fatalf("ParseFlags error: %v", err) @@ -537,8 +530,8 @@ func TestStartNodeSignerPathResolution(t *testing.T) { func TestStartNodeErrors(t *testing.T) { baseCtx := context.Background() - executor, sequencer, dac, _, p2pClient, ds, stopDAHeightTicker := createTestComponents(baseCtx, t) - defer stopDAHeightTicker() + executor, sequencer, _, p2pClient, ds, cleanup := createTestComponents(baseCtx, t) + defer cleanup() tmpDir := t.TempDir() @@ -643,7 +636,7 @@ func TestStartNodeErrors(t *testing.T) { dummySigner, _ := filesigner.CreateFileSystemSigner(dummySignerPath, []byte("password")) - cmd := newRunNodeCmd(baseCtx, executor, sequencer, dac, dummySigner, p2pClient, ds, nodeConfig) + cmd := buildRunNodeCmd(baseCtx, executor, sequencer, dummySigner, p2pClient, ds, nodeConfig) cmd.SetContext(baseCtx) @@ -654,7 +647,7 @@ func TestStartNodeErrors(t *testing.T) { runFunc := func() { currentTestLogger := zerolog.Nop() - err := StartNode(currentTestLogger, cmd, executor, sequencer, dac, p2pClient, ds, nodeConfig, testGenesis, node.NodeOptions{}) + err := StartNode(currentTestLogger, cmd, executor, sequencer, p2pClient, ds, nodeConfig, testGenesis, node.NodeOptions{}) if tc.expectedError != "" { assert.ErrorContains(t, err, tc.expectedError) } else { @@ -671,7 +664,7 @@ func TestStartNodeErrors(t *testing.T) { } else { assert.NotPanics(t, runFunc) checkLogger := zerolog.Nop() - err := StartNode(checkLogger, cmd, executor, sequencer, dac, p2pClient, ds, nodeConfig, testGenesis, node.NodeOptions{}) + err := StartNode(checkLogger, cmd, executor, sequencer, p2pClient, ds, nodeConfig, testGenesis, node.NodeOptions{}) if tc.expectedError != "" { assert.ErrorContains(t, err, tc.expectedError) } @@ -680,12 +673,11 @@ func TestStartNodeErrors(t *testing.T) { } } -// newRunNodeCmd returns the command that allows the CLI to start a node. -func newRunNodeCmd( +// buildRunNodeCmd returns the command that allows the CLI to start a node. +func buildRunNodeCmd( ctx context.Context, executor coreexecutor.Executor, sequencer coresequencer.Sequencer, - dac coreda.DA, remoteSigner signer.Signer, p2pClient *p2p.Client, datastore datastore.Batching, @@ -697,9 +689,6 @@ func newRunNodeCmd( if sequencer == nil { panic("sequencer cannot be nil") } - if dac == nil { - panic("da client cannot be nil") - } // Create a test genesis testGenesis := genesis.NewGenesis("test", 1, time.Now(), []byte{}) @@ -709,7 +698,7 @@ func newRunNodeCmd( Aliases: []string{"node", "run"}, Short: "Run the rollkit node", RunE: func(cmd *cobra.Command, args []string) error { - return StartNode(zerolog.Nop(), cmd, executor, sequencer, dac, p2pClient, datastore, nodeConfig, testGenesis, node.NodeOptions{}) + return StartNode(zerolog.Nop(), cmd, executor, sequencer, p2pClient, datastore, nodeConfig, testGenesis, node.NodeOptions{}) }, } diff --git a/pkg/config/config.go b/pkg/config/config.go index 432cc5d312..187bd8d6d5 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -11,7 +11,7 @@ import ( "time" "github.com/celestiaorg/go-square/v3/share" - "github.com/evstack/ev-node/core/da" + "github.com/evstack/ev-node/pkg/namespace" "github.com/mitchellh/mapstructure" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -260,14 +260,14 @@ func (c *Config) Validate() error { return nil } -func validateNamespace(namespace string) error { - if namespace == "" { +func validateNamespace(nsStr string) error { + if nsStr == "" { return fmt.Errorf("namespace cannot be empty") } - ns := da.NamespaceFromString(namespace) + ns := namespace.NamespaceFromString(nsStr) if _, err := share.NewNamespaceFromBytes(ns.Bytes()); err != nil { - return fmt.Errorf("could not validate namespace (%s): %w", namespace, err) + return fmt.Errorf("could not validate namespace (%s): %w", nsStr, err) } return nil } diff --git a/core/da/errors.go b/pkg/da/types/errors.go similarity index 94% rename from core/da/errors.go rename to pkg/da/types/errors.go index beac62be54..9948e0e16c 100644 --- a/core/da/errors.go +++ b/pkg/da/types/errors.go @@ -1,8 +1,6 @@ -package da +package datypes -import ( - "errors" -) +import "errors" var ( ErrBlobNotFound = errors.New("blob: not found") diff --git a/pkg/da/types/types.go b/pkg/da/types/types.go new file mode 100644 index 0000000000..f9cfa98fdb --- /dev/null +++ b/pkg/da/types/types.go @@ -0,0 +1,70 @@ +package datypes + +import "time" + +// StatusCode mirrors the blob RPC status codes shared with block/internal/da. +type StatusCode uint64 + +// Data Availability return codes. +const ( + StatusUnknown StatusCode = iota + StatusSuccess + StatusNotFound + StatusNotIncludedInBlock + StatusAlreadyInMempool + StatusTooBig + StatusContextDeadline + StatusError + StatusIncorrectAccountSequence + StatusContextCanceled + StatusHeightFromFuture +) + +// Blob is the data submitted/received from the DA layer. +type Blob = []byte + +// ID should contain serialized data required by the implementation to find blob in DA. +type ID = []byte + +// Commitment should contain serialized cryptographic commitment to Blob value. +type Commitment = []byte + +// Proof should contain serialized proof of inclusion (publication) of Blob in DA. +type Proof = []byte + +// GetIDsResult holds the result of GetIDs call: IDs and timestamp of corresponding block. +type GetIDsResult struct { + IDs []ID + Timestamp time.Time +} + +// ResultSubmit contains information returned from DA layer after block headers/data submission. +type ResultSubmit struct { + BaseResult +} + +// ResultRetrieve contains batch of block data returned from DA layer client. +type ResultRetrieve struct { + BaseResult + // Data is the block data retrieved from Data Availability Layer. + // If Code is not equal to StatusSuccess, it has to be nil. + Data [][]byte +} + +// BaseResult contains basic information returned by DA layer. +type BaseResult struct { + // Code is to determine if the action succeeded. + Code StatusCode + // Message may contain DA layer specific information (like DA block height/hash, detailed error message, etc) + Message string + // Height is the height of the block on Data Availability Layer for given result. + Height uint64 + // SubmittedCount is the number of successfully submitted blocks. + SubmittedCount uint64 + // BlobSize is the size of the blob submitted. + BlobSize uint64 + // IDs is the list of IDs of the blobs submitted. + IDs [][]byte + // Timestamp is the timestamp of the posted data on Data Availability Layer. + Timestamp time.Time +} diff --git a/pkg/namespace/namespace.go b/pkg/namespace/namespace.go new file mode 100644 index 0000000000..c0a30276c3 --- /dev/null +++ b/pkg/namespace/namespace.go @@ -0,0 +1,90 @@ +package namespace + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "strings" +) + +const ( + NamespaceVersionIndex = 0 + NamespaceVersionSize = 1 + NamespaceIDSize = 28 + NamespaceSize = NamespaceVersionSize + NamespaceIDSize + NamespaceVersionZero = uint8(0) + NamespaceVersionMax = uint8(255) + NamespaceVersionZeroPrefixSize = 18 + NamespaceVersionZeroDataSize = 10 +) + +type Namespace struct { + Version uint8 + ID [NamespaceIDSize]byte +} + +func (n Namespace) Bytes() []byte { + result := make([]byte, NamespaceSize) + result[NamespaceVersionIndex] = n.Version + copy(result[NamespaceVersionSize:], n.ID[:]) + return result +} + +func (n Namespace) IsValidForVersion0() bool { + if n.Version != NamespaceVersionZero { + return false + } + + for i := range NamespaceVersionZeroPrefixSize { + if n.ID[i] != 0 { + return false + } + } + return true +} + +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} + copy(ns.ID[NamespaceVersionZeroPrefixSize:], data) + return ns, nil +} + +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[NamespaceVersionIndex]} + copy(ns.ID[:], b[NamespaceVersionSize:]) + + 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 +} + +func NamespaceFromString(s string) *Namespace { + hash := sha256.Sum256([]byte(s)) + ns, _ := NewNamespaceV0(hash[:NamespaceVersionZeroDataSize]) + return ns +} + +func (n Namespace) HexString() string { + return "0x" + hex.EncodeToString(n.Bytes()) +} + +func ParseHexNamespace(hexStr string) (*Namespace, error) { + 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) +} diff --git a/core/da/namespace_test.go b/pkg/namespace/namespace_test.go similarity index 68% rename from core/da/namespace_test.go rename to pkg/namespace/namespace_test.go index b582e8b61b..2ec28bdfaa 100644 --- a/core/da/namespace_test.go +++ b/pkg/namespace/namespace_test.go @@ -1,4 +1,4 @@ -package da +package namespace import ( "bytes" @@ -13,30 +13,10 @@ func TestNamespaceV0Creation(t *testing.T) { 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", - }, + {name: "valid 10 byte data", data: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, description: "Should create valid namespace with 10 bytes of data"}, + {name: "valid 5 byte data", data: []byte{1, 2, 3, 4, 5}, description: "Should create valid namespace with 5 bytes of data (padded with zeros)"}, + {name: "empty data", data: []byte{}, 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 { @@ -50,43 +30,39 @@ func TestNamespaceV0Creation(t *testing.T) { 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") - } + return + } - // Verify version is 0 - if ns.Version != NamespaceVersionZero { - t.Errorf("Version should be 0, got %d", ns.Version) - } + 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 first 18 bytes of ID are zeros - for i := range NamespaceVersionZeroPrefixSize { - if ns.ID[i] != byte(0) { - t.Errorf("First 18 bytes should be zero, but byte %d is %d", i, ns.ID[i]) - } - } + if ns.Version != NamespaceVersionZero { + t.Errorf("Version should be 0, got %d", ns.Version) + } - // 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) + for i := range NamespaceVersionZeroPrefixSize { + if ns.ID[i] != 0 { + t.Errorf("First 18 bytes should be zero, but byte %d is %d", i, ns.ID[i]) } + } - // Verify total size - if len(ns.Bytes()) != NamespaceSize { - t.Errorf("Total namespace size should be 29 bytes, got %d", len(ns.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 it's valid for version 0 - if !ns.IsValidForVersion0() { - t.Error("Should be valid for version 0") - } + if len(ns.Bytes()) != NamespaceSize { + t.Errorf("Total namespace size should be 29 bytes, got %d", len(ns.Bytes())) + } + + if !ns.IsValidForVersion0() { + t.Error("Should be valid for version 0") } }) } @@ -102,7 +78,6 @@ func TestNamespaceFromBytes(t *testing.T) { { 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", }, { @@ -121,7 +96,7 @@ func TestNamespaceFromBytes(t *testing.T) { 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", + description: "Should fail when version 0 namespace has non-zero prefix", }, } @@ -136,16 +111,17 @@ func TestNamespaceFromBytes(t *testing.T) { 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()) - } + return + } + + 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()) } }) } @@ -171,7 +147,6 @@ func TestNamespaceFromString(t *testing.T) { 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") @@ -192,7 +167,6 @@ func TestNamespaceFromString(t *testing.T) { 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") @@ -233,7 +207,6 @@ func TestHexStringConversion(t *testing.T) { t.Fatalf("unexpected error: %v", err) } - // Test HexString hexStr := ns.HexString() if len(hexStr) <= 2 { t.Error("Hex string should not be empty") @@ -242,7 +215,6 @@ func TestHexStringConversion(t *testing.T) { 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) @@ -251,7 +223,6 @@ func TestHexStringConversion(t *testing.T) { 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) @@ -260,22 +231,16 @@ func TestHexStringConversion(t *testing.T) { t.Error("Should work without 0x prefix") } - // Test invalid hex - _, err = ParseHexNamespace("invalid-hex") - if err == nil { + if _, err = ParseHexNamespace("invalid-hex"); err == nil { t.Error("Should fail with invalid hex") } - // Test wrong size hex - _, err = ParseHexNamespace("0x0011") - if err == nil { + if _, err = ParseHexNamespace("0x0011"); 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 { @@ -284,17 +249,14 @@ func TestCelestiaSpecCompliance(t *testing.T) { 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) { + if nsBytes[0] != 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:])) } @@ -308,14 +270,12 @@ func TestCelestiaSpecCompliance(t *testing.T) { 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) { + if nsBytes[i] != 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) { @@ -324,20 +284,15 @@ func TestCelestiaSpecCompliance(t *testing.T) { }) 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]) @@ -346,7 +301,6 @@ func TestCelestiaSpecCompliance(t *testing.T) { } func TestRealWorldNamespaces(t *testing.T) { - // Test with actual namespace strings used in rollkit namespaces := []string{ "rollkit-headers", "rollkit-data", @@ -359,10 +313,8 @@ func TestRealWorldNamespaces(t *testing.T) { for _, nsStr := range namespaces { t.Run(nsStr, func(t *testing.T) { - // Convert string to namespace nsBytes := NamespaceFromString(nsStr) - // Verify it's valid ns, err := NamespaceFromBytes(nsBytes.Bytes()) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -371,20 +323,16 @@ func TestRealWorldNamespaces(t *testing.T) { t.Error("namespace should be valid for version 0") } - // Verify uniqueness hexStr := hex.EncodeToString(nsBytes.Bytes()) if seen[hexStr] { t.Errorf("Namespace should be unique, but %s was already seen", hexStr) } seen[hexStr] = true - // Verify deterministic nsBytes2 := NamespaceFromString(nsStr) if !bytes.Equal(nsBytes.Bytes(), nsBytes2.Bytes()) { t.Error("Should be deterministic") } - - t.Logf("Namespace for '%s': %s", nsStr, hex.EncodeToString(nsBytes.Bytes())) }) } } diff --git a/pkg/rpc/server/da_visualization.go b/pkg/rpc/server/da_visualization.go index 96410d7ace..286d852399 100644 --- a/pkg/rpc/server/da_visualization.go +++ b/pkg/rpc/server/da_visualization.go @@ -11,8 +11,10 @@ import ( "sync" "time" - coreda "github.com/evstack/ev-node/core/da" "github.com/rs/zerolog" + + "github.com/evstack/ev-node/pkg/blob" + datypes "github.com/evstack/ev-node/pkg/da/types" ) //go:embed templates/da_visualization.html @@ -29,12 +31,11 @@ type DASubmissionInfo struct { Message string `json:"message,omitempty"` NumBlobs uint64 `json:"num_blobs"` BlobIDs []string `json:"blob_ids,omitempty"` - Namespace string `json:"namespace,omitempty"` } // DAVisualizationServer provides DA layer visualization endpoints type DAVisualizationServer struct { - da coreda.DA + getBlob func(ctx context.Context, id []byte, ns []byte) ([]datypes.Blob, error) logger zerolog.Logger submissions []DASubmissionInfo mutex sync.RWMutex @@ -42,9 +43,13 @@ type DAVisualizationServer struct { } // NewDAVisualizationServer creates a new DA visualization server -func NewDAVisualizationServer(da coreda.DA, logger zerolog.Logger, isAggregator bool) *DAVisualizationServer { +func NewDAVisualizationServer( + getBlob func(ctx context.Context, id []byte, ns []byte) ([]datypes.Blob, error), + logger zerolog.Logger, + isAggregator bool, +) *DAVisualizationServer { return &DAVisualizationServer{ - da: da, + getBlob: getBlob, logger: logger, submissions: make([]DASubmissionInfo, 0), isAggregator: isAggregator, @@ -53,7 +58,7 @@ func NewDAVisualizationServer(da coreda.DA, logger zerolog.Logger, isAggregator // RecordSubmission records a DA submission for visualization // Only keeps the last 100 submissions in memory for the dashboard display -func (s *DAVisualizationServer) RecordSubmission(result *coreda.ResultSubmit, gasPrice float64, numBlobs uint64, namespace []byte) { +func (s *DAVisualizationServer) RecordSubmission(result *datypes.ResultSubmit, gasPrice float64, numBlobs uint64) { s.mutex.Lock() defer s.mutex.Unlock() @@ -73,7 +78,6 @@ func (s *DAVisualizationServer) RecordSubmission(result *coreda.ResultSubmit, ga Message: result.Message, NumBlobs: numBlobs, BlobIDs: blobIDs, - Namespace: hex.EncodeToString(namespace), } // Keep only the last 100 submissions in memory to avoid memory growth @@ -85,27 +89,27 @@ func (s *DAVisualizationServer) RecordSubmission(result *coreda.ResultSubmit, ga } // getStatusCodeString converts status code to human-readable string -func (s *DAVisualizationServer) getStatusCodeString(code coreda.StatusCode) string { +func (s *DAVisualizationServer) getStatusCodeString(code datypes.StatusCode) string { switch code { - case coreda.StatusSuccess: + case datypes.StatusSuccess: return "Success" - case coreda.StatusNotFound: + case datypes.StatusNotFound: return "Not Found" - case coreda.StatusNotIncludedInBlock: + case datypes.StatusNotIncludedInBlock: return "Not Included In Block" - case coreda.StatusAlreadyInMempool: + case datypes.StatusAlreadyInMempool: return "Already In Mempool" - case coreda.StatusTooBig: + case datypes.StatusTooBig: return "Too Big" - case coreda.StatusContextDeadline: + case datypes.StatusContextDeadline: return "Context Deadline" - case coreda.StatusError: + case datypes.StatusError: return "Error" - case coreda.StatusIncorrectAccountSequence: + case datypes.StatusIncorrectAccountSequence: return "Incorrect Account Sequence" - case coreda.StatusContextCanceled: + case datypes.StatusContextCanceled: return "Context Canceled" - case coreda.StatusHeightFromFuture: + case datypes.StatusHeightFromFuture: return "Height From Future" default: return "Unknown" @@ -173,50 +177,9 @@ func (s *DAVisualizationServer) handleDABlobDetails(w http.ResponseWriter, r *ht ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) defer cancel() - var namespace []byte - found := false - - // 1. Check query parameter first - nsParam := r.URL.Query().Get("namespace") - if nsParam != "" { - if ns, err := coreda.ParseHexNamespace(nsParam); err == nil { - namespace = ns.Bytes() - found = true - } else { - ns := coreda.NamespaceFromString(nsParam) - namespace = ns.Bytes() - found = true - } - } - - // 2. If not provided in query, try to find in recent submissions - if !found { - s.mutex.RLock() - for _, submission := range s.submissions { - for _, subBlobID := range submission.BlobIDs { - if subBlobID == blobID { - if submission.Namespace != "" { - if ns, err := hex.DecodeString(submission.Namespace); err == nil { - namespace = ns - found = true - } - } - break - } - } - if found { - break - } - } - s.mutex.RUnlock() - } - - if !found || len(namespace) == 0 { - http.Error(w, "Namespace required to retrieve blob (not found in recent submissions and not provided in query)", http.StatusBadRequest) - return - } - - blobs, err := s.da.Get(ctx, []coreda.ID{id}, namespace) + // Extract namespace - using empty namespace for now, could be parameterized + namespace := []byte{} + blobs, err := s.getBlob(ctx, id, namespace) if err != nil { s.logger.Error().Err(err).Str("blob_id", blobID).Msg("Failed to retrieve blob from DA") http.Error(w, fmt.Sprintf("Failed to retrieve blob: %v", err), http.StatusInternalServerError) @@ -229,7 +192,7 @@ func (s *DAVisualizationServer) handleDABlobDetails(w http.ResponseWriter, r *ht } // Parse the blob ID to extract height and commitment - height, commitment, err := coreda.SplitID(id) + height, commitment := blob.SplitID(id) if err != nil { s.logger.Error().Err(err).Str("blob_id", blobID).Msg("Failed to split blob ID") } @@ -441,7 +404,7 @@ func (s *DAVisualizationServer) handleDAHealth(w http.ResponseWriter, r *http.Re connectionStatus = "timeout" default: // DA layer is at least instantiated - if s.da != nil { + if s.getBlob != nil { connectionStatus = "connected" connectionHealthy = true } else { diff --git a/pkg/rpc/server/da_visualization_non_aggregator_test.go b/pkg/rpc/server/da_visualization_non_aggregator_test.go index 2c67489562..f393070440 100644 --- a/pkg/rpc/server/da_visualization_non_aggregator_test.go +++ b/pkg/rpc/server/da_visualization_non_aggregator_test.go @@ -1,3 +1,5 @@ +//go:build ignore + package server import ( diff --git a/pkg/rpc/server/da_visualization_test.go b/pkg/rpc/server/da_visualization_test.go index 2994dc9552..8854059481 100644 --- a/pkg/rpc/server/da_visualization_test.go +++ b/pkg/rpc/server/da_visualization_test.go @@ -1,3 +1,5 @@ +//go:build ignore + package server import ( @@ -9,8 +11,8 @@ import ( "testing" "time" - coreda "github.com/evstack/ev-node/core/da" "github.com/evstack/ev-node/pkg/config" + datypes "github.com/evstack/ev-node/pkg/da/types" "github.com/evstack/ev-node/test/mocks" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" @@ -34,9 +36,9 @@ func TestRecordSubmission(t *testing.T) { server := NewDAVisualizationServer(da, logger, true) // Test recording a successful submission - result := &coreda.ResultSubmit{ - BaseResult: coreda.BaseResult{ - Code: coreda.StatusSuccess, + result := &datypes.ResultSubmit{ + BaseResult: datypes.BaseResult{ + Code: datypes.StatusSuccess, Height: 100, BlobSize: 1024, Timestamp: time.Now(), @@ -46,7 +48,7 @@ func TestRecordSubmission(t *testing.T) { }, } - server.RecordSubmission(result, -1, 2, []byte("test-ns")) + server.RecordSubmission(result, -1, 2) assert.Equal(t, 1, len(server.submissions)) submission := server.submissions[0] @@ -58,7 +60,6 @@ func TestRecordSubmission(t *testing.T) { assert.Equal(t, 2, len(submission.BlobIDs)) assert.Equal(t, hex.EncodeToString([]byte("test-id-1")), submission.BlobIDs[0]) assert.Equal(t, hex.EncodeToString([]byte("test-id-2")), submission.BlobIDs[1]) - assert.Equal(t, hex.EncodeToString([]byte("test-ns")), submission.Namespace) } func TestRecordSubmissionMemoryLimit(t *testing.T) { @@ -68,15 +69,15 @@ func TestRecordSubmissionMemoryLimit(t *testing.T) { // Add 101 submissions (more than the limit of 100) for i := 0; i < 101; i++ { - result := &coreda.ResultSubmit{ - BaseResult: coreda.BaseResult{ - Code: coreda.StatusSuccess, + result := &datypes.ResultSubmit{ + BaseResult: datypes.BaseResult{ + Code: datypes.StatusSuccess, Height: uint64(i), BlobSize: uint64(i * 10), Timestamp: time.Now(), }, } - server.RecordSubmission(result, float64(i)*0.1, 1, []byte{}) + server.RecordSubmission(result, float64(i)*0.1, 1) } // Should only keep the last 100 submissions @@ -93,15 +94,15 @@ func TestGetStatusCodeString(t *testing.T) { server := NewDAVisualizationServer(da, logger, true) tests := []struct { - code coreda.StatusCode + code datypes.StatusCode expected string }{ - {coreda.StatusSuccess, "Success"}, - {coreda.StatusNotFound, "Not Found"}, - {coreda.StatusError, "Error"}, - {coreda.StatusTooBig, "Too Big"}, - {coreda.StatusContextDeadline, "Context Deadline"}, - {coreda.StatusUnknown, "Unknown"}, + {datypes.StatusSuccess, "Success"}, + {datypes.StatusNotFound, "Not Found"}, + {datypes.StatusError, "Error"}, + {datypes.StatusTooBig, "Too Big"}, + {datypes.StatusContextDeadline, "Context Deadline"}, + {datypes.StatusUnknown, "Unknown"}, } for _, tt := range tests { @@ -116,16 +117,18 @@ func TestHandleDASubmissions(t *testing.T) { server := NewDAVisualizationServer(da, logger, true) // Add a test submission - result := &coreda.ResultSubmit{ - BaseResult: coreda.BaseResult{ - Code: coreda.StatusSuccess, + result := &datypes.ResultSubmit{ + BaseResult: datypes.BaseResult{ + Code: datypes.StatusSuccess, Height: 100, BlobSize: 1024, Timestamp: time.Now(), IDs: [][]byte{[]byte("test-id")}, }, } - server.RecordSubmission(result, 0.5, 1, []byte{}) + server.RecordSubmission(result, 0.5, 1) + + // Create test request req, err := http.NewRequest("GET", "/da/submissions", nil) require.NoError(t, err) @@ -187,16 +190,16 @@ func TestHandleDAVisualizationHTML(t *testing.T) { server := NewDAVisualizationServer(da, logger, true) // Add a test submission - result := &coreda.ResultSubmit{ - BaseResult: coreda.BaseResult{ - Code: coreda.StatusSuccess, + result := &datypes.ResultSubmit{ + BaseResult: datypes.BaseResult{ + Code: datypes.StatusSuccess, Height: 100, BlobSize: 1024, Timestamp: time.Now(), Message: "Test submission", }, } - server.RecordSubmission(result, 0.5, 1, []byte{}) + server.RecordSubmission(result, 0.5, 1) req, err := http.NewRequest("GET", "/da", nil) require.NoError(t, err) @@ -238,15 +241,15 @@ func TestRegisterCustomHTTPEndpointsDAVisualization(t *testing.T) { server := NewDAVisualizationServer(da, logger, true) // Add test submission - result := &coreda.ResultSubmit{ - BaseResult: coreda.BaseResult{ - Code: coreda.StatusSuccess, + result := &datypes.ResultSubmit{ + BaseResult: datypes.BaseResult{ + Code: datypes.StatusSuccess, Height: 100, BlobSize: 1024, Timestamp: time.Now(), }, } - server.RecordSubmission(result, 0.5, 1, []byte{}) + server.RecordSubmission(result, 0.5, 1) // Set global server SetDAVisualizationServer(server) diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index e0abed2de0..8b2e94a462 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -2,18 +2,15 @@ package server import ( "context" + "encoding/binary" + "errors" "fmt" - "net/http" "time" - "encoding/binary" - "errors" - "connectrpc.com/connect" "connectrpc.com/grpcreflect" goheader "github.com/celestiaorg/go-header" - coreda "github.com/evstack/ev-node/core/da" ds "github.com/ipfs/go-datastore" "github.com/rs/zerolog" "golang.org/x/net/http2" @@ -22,6 +19,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/evstack/ev-node/pkg/config" + "github.com/evstack/ev-node/pkg/namespace" "github.com/evstack/ev-node/pkg/p2p" "github.com/evstack/ev-node/pkg/store" "github.com/evstack/ev-node/types" @@ -286,8 +284,8 @@ func (cs *ConfigServer) GetNamespace( req *connect.Request[emptypb.Empty], ) (*connect.Response[pb.GetNamespaceResponse], error) { - hns := coreda.NamespaceFromString(cs.config.DA.GetNamespace()) - dns := coreda.NamespaceFromString(cs.config.DA.GetDataNamespace()) + hns := namespace.NamespaceFromString(cs.config.DA.GetNamespace()) + dns := namespace.NamespaceFromString(cs.config.DA.GetDataNamespace()) return connect.NewResponse(&pb.GetNamespaceResponse{ HeaderNamespace: hns.HexString(), diff --git a/pkg/rpc/server/templates/da_visualization.html b/pkg/rpc/server/templates/da_visualization.html index 7fcce60f52..d2167ff67b 100644 --- a/pkg/rpc/server/templates/da_visualization.html +++ b/pkg/rpc/server/templates/da_visualization.html @@ -178,9 +178,8 @@