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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/evm/single/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ if [ -n "$DA_NAMESPACE" ]; then
default_flags="$default_flags --rollkit.da.namespace $DA_NAMESPACE"
fi

if [ -n "$DA_SIGNING_ADDRESSES" ]; then
default_flags="$default_flags --rollkit.da.signing_addresses $DA_SIGNING_ADDRESSES"
fi

# If no arguments passed, show help
if [ $# -eq 0 ]; then
exec evm-single
Expand Down
72 changes: 68 additions & 4 deletions block/internal/submitting/da_submitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package submitting
import (
"bytes"
"context"
"encoding/json"
"fmt"
"time"

Expand All @@ -13,6 +14,7 @@ import (
"github.com/evstack/ev-node/block/internal/common"
coreda "github.com/evstack/ev-node/core/da"
"github.com/evstack/ev-node/pkg/config"
pkgda "github.com/evstack/ev-node/pkg/da"
"github.com/evstack/ev-node/pkg/genesis"
"github.com/evstack/ev-node/pkg/rpc/server"
"github.com/evstack/ev-node/pkg/signer"
Expand Down Expand Up @@ -124,6 +126,9 @@ type DASubmitter struct {
// calculate namespaces bytes once and reuse them
namespaceBz []byte
namespaceDataBz []byte

// address selector for multi-account support
addressSelector pkgda.AddressSelector
}

// NewDASubmitter creates a new DA submitter
Expand All @@ -147,6 +152,17 @@ func NewDASubmitter(
metrics = common.NopMetrics()
}

// Create address selector based on configuration
var addressSelector pkgda.AddressSelector
if len(config.DA.SigningAddresses) > 0 {
addressSelector = pkgda.NewRoundRobinSelector(config.DA.SigningAddresses)
daSubmitterLogger.Info().
Int("num_addresses", len(config.DA.SigningAddresses)).
Msg("initialized round-robin address selector for multi-account DA submissions")
} else {
addressSelector = pkgda.NewNoOpSelector()
}

return &DASubmitter{
da: da,
config: config,
Expand All @@ -156,6 +172,7 @@ func NewDASubmitter(
logger: daSubmitterLogger,
namespaceBz: coreda.NamespaceFromString(config.DA.GetNamespace()).Bytes(),
namespaceDataBz: coreda.NamespaceFromString(config.DA.GetDataNamespace()).Bytes(),
addressSelector: addressSelector,
}
}

Expand Down Expand Up @@ -235,7 +252,6 @@ func (s *DASubmitter) SubmitHeaders(ctx context.Context, cache cache.Manager) er
"header",
s.namespaceBz,
[]byte(s.config.DA.SubmitOptions),
cache,
func() uint64 { return cache.NumPendingHeaders() },
)
}
Expand Down Expand Up @@ -279,7 +295,6 @@ func (s *DASubmitter) SubmitData(ctx context.Context, cache cache.Manager, signe
"data",
s.namespaceDataBz,
[]byte(s.config.DA.SubmitOptions),
cache,
func() uint64 { return cache.NumPendingData() },
)
}
Expand Down Expand Up @@ -340,6 +355,44 @@ func (s *DASubmitter) createSignedData(dataList []*types.SignedData, signer sign
return signedDataList, nil
}

// mergeSubmitOptions merges the base submit options with a signing address.
// If the base options are valid JSON, the signing address is added to the JSON object.
// Otherwise, a new JSON object is created with just the signing address.
// Returns the base options unchanged if no signing address is provided.
func mergeSubmitOptions(baseOptions []byte, signingAddress string) ([]byte, error) {
if signingAddress == "" {
return baseOptions, nil
}

var optionsMap map[string]interface{}

// If base options are provided, try to parse them as JSON
if len(baseOptions) > 0 {
// Try to unmarshal existing options, ignoring errors for non-JSON input
if err := json.Unmarshal(baseOptions, &optionsMap); err != nil {
// Not valid JSON - start with empty map
optionsMap = make(map[string]interface{})
}
}

// Ensure map is initialized even if unmarshal returned nil
if optionsMap == nil {
optionsMap = make(map[string]interface{})
}

// Add or override the signing address
// Note: Uses "signer_address" to match Celestia's TxConfig JSON schema
optionsMap["signer_address"] = signingAddress

// Marshal back to JSON
mergedOptions, err := json.Marshal(optionsMap)
if err != nil {
return nil, fmt.Errorf("failed to marshal submit options: %w", err)
}

return mergedOptions, nil
}

// submitToDA is a generic helper for submitting items to the DA layer with retry, backoff, and gas price logic.
func submitToDA[T any](
s *DASubmitter,
Expand All @@ -350,7 +403,6 @@ func submitToDA[T any](
itemType string,
namespace []byte,
options []byte,
cache cache.Manager,
getTotalPendingFn func() uint64,
) error {
marshaled, err := marshalItems(ctx, items, marshalFn, itemType)
Expand Down Expand Up @@ -397,12 +449,24 @@ func submitToDA[T any](
return err
}

// Select signing address and merge with options
signingAddress := s.addressSelector.Next()
mergedOptions, err := mergeSubmitOptions(options, signingAddress)
if err != nil {
s.logger.Error().Err(err).Msg("failed to merge submit options with signing address")
return fmt.Errorf("failed to merge submit options: %w", err)
}

if signingAddress != "" {
s.logger.Debug().Str("signingAddress", signingAddress).Msg("using signing address for DA submission")
}

submitCtx, cancel := context.WithTimeout(ctx, submissionTimeout)
defer cancel()

// Perform submission
start := time.Now()
res := types.SubmitWithHelpers(submitCtx, s.da, s.logger, marshaled, rs.GasPrice, namespace, options)
res := types.SubmitWithHelpers(submitCtx, s.da, s.logger, marshaled, rs.GasPrice, 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
Expand Down
5 changes: 0 additions & 5 deletions block/internal/submitting/da_submitter_mocks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ func TestSubmitToDA_MempoolRetry_IncreasesGasAndSucceeds(t *testing.T) {
nsBz,
opts,
nil,
nil,
)
assert.NoError(t, err)

Expand Down Expand Up @@ -138,7 +137,6 @@ func TestSubmitToDA_UnknownError_RetriesSameGasThenSucceeds(t *testing.T) {
nsBz,
opts,
nil,
nil,
)
assert.NoError(t, err)
assert.Equal(t, []float64{5.5, 5.5}, usedGas)
Expand Down Expand Up @@ -195,7 +193,6 @@ func TestSubmitToDA_TooBig_HalvesBatch(t *testing.T) {
nsBz,
opts,
nil,
nil,
)
assert.NoError(t, err)
assert.Equal(t, []int{4, 2}, batchSizes)
Expand Down Expand Up @@ -245,7 +242,6 @@ func TestSubmitToDA_SentinelNoGas_PreservesGasAcrossRetries(t *testing.T) {
nsBz,
opts,
nil,
nil,
)
assert.NoError(t, err)
assert.Equal(t, []float64{-1, -1}, usedGas)
Expand Down Expand Up @@ -286,7 +282,6 @@ func TestSubmitToDA_PartialSuccess_AdvancesWindow(t *testing.T) {
nsBz,
opts,
nil,
nil,
)
assert.NoError(t, err)
assert.Equal(t, 3, totalSubmitted)
Expand Down
132 changes: 132 additions & 0 deletions block/internal/submitting/da_submitter_options_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package submitting

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestMergeSubmitOptions_NoSigningAddress(t *testing.T) {
baseOptions := []byte(`{"key":"value"}`)

result, err := mergeSubmitOptions(baseOptions, "")
require.NoError(t, err)
assert.Equal(t, baseOptions, result, "should return unchanged options when no signing address")
}

func TestMergeSubmitOptions_EmptyBaseOptions(t *testing.T) {
signingAddress := "celestia1abc123"

result, err := mergeSubmitOptions([]byte{}, signingAddress)
require.NoError(t, err)

var resultMap map[string]interface{}
err = json.Unmarshal(result, &resultMap)
require.NoError(t, err)

assert.Equal(t, signingAddress, resultMap["signer_address"])
}

func TestMergeSubmitOptions_ValidJSON(t *testing.T) {
baseOptions := []byte(`{"existing":"option","number":42}`)
signingAddress := "celestia1def456"

result, err := mergeSubmitOptions(baseOptions, signingAddress)
require.NoError(t, err)

var resultMap map[string]interface{}
err = json.Unmarshal(result, &resultMap)
require.NoError(t, err)

assert.Equal(t, "option", resultMap["existing"])
assert.Equal(t, float64(42), resultMap["number"]) // JSON numbers are float64
assert.Equal(t, signingAddress, resultMap["signer_address"])
}

func TestMergeSubmitOptions_InvalidJSON(t *testing.T) {
baseOptions := []byte(`not-json-content`)
signingAddress := "celestia1ghi789"

result, err := mergeSubmitOptions(baseOptions, signingAddress)
require.NoError(t, err)

var resultMap map[string]interface{}
err = json.Unmarshal(result, &resultMap)
require.NoError(t, err)

// Should create new JSON object with just the signing address
assert.Equal(t, signingAddress, resultMap["signer_address"])
assert.Len(t, resultMap, 1, "should only contain signing address when base options are invalid JSON")
}

func TestMergeSubmitOptions_OverrideExistingAddress(t *testing.T) {
baseOptions := []byte(`{"signer_address":"old-address","other":"data"}`)
newAddress := "celestia1new456"

result, err := mergeSubmitOptions(baseOptions, newAddress)
require.NoError(t, err)

var resultMap map[string]interface{}
err = json.Unmarshal(result, &resultMap)
require.NoError(t, err)

assert.Equal(t, newAddress, resultMap["signer_address"], "should override existing signing address")
assert.Equal(t, "data", resultMap["other"])
}

func TestMergeSubmitOptions_NilBaseOptions(t *testing.T) {
signingAddress := "celestia1jkl012"

result, err := mergeSubmitOptions(nil, signingAddress)
require.NoError(t, err)

var resultMap map[string]interface{}
err = json.Unmarshal(result, &resultMap)
require.NoError(t, err)

assert.Equal(t, signingAddress, resultMap["signer_address"])
}

func TestMergeSubmitOptions_ComplexJSON(t *testing.T) {
baseOptions := []byte(`{
"nested": {
"key": "value"
},
"array": [1, 2, 3],
"bool": true
}`)
signingAddress := "celestia1complex"

result, err := mergeSubmitOptions(baseOptions, signingAddress)
require.NoError(t, err)

var resultMap map[string]interface{}
err = json.Unmarshal(result, &resultMap)
require.NoError(t, err)

// Check nested structure is preserved
nested, ok := resultMap["nested"].(map[string]interface{})
require.True(t, ok)
assert.Equal(t, "value", nested["key"])

// Check array is preserved
array, ok := resultMap["array"].([]interface{})
require.True(t, ok)
assert.Len(t, array, 3)

// Check bool is preserved
assert.Equal(t, true, resultMap["bool"])

// Check signing address was added
assert.Equal(t, signingAddress, resultMap["signer_address"])
}

func TestMergeSubmitOptions_NullJSON(t *testing.T) {
base := []byte("null")
merged, err := mergeSubmitOptions(base, `{"signer_address": "abc"}`)
require.NoError(t, err)
require.NotNil(t, merged)
require.Contains(t, string(merged), "signer_address")
}
2 changes: 1 addition & 1 deletion core/da/dummy.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ func (d *DummyDA) SubmitWithOptions(ctx context.Context, blobs []Blob, gasPrice

d.blobs[idStr] = blob
d.commitments[idStr] = commitment
d.proofs[idStr] = commitment // Simple proof
d.proofs[idStr] = commitment // Simple proof
d.namespaceByID[idStr] = namespace // Store namespace for this blob

ids = append(ids, id)
Expand Down
Loading
Loading