@@ -3,6 +3,7 @@ package submitting
33import (
44 "bytes"
55 "context"
6+ "encoding/json"
67 "fmt"
78 "time"
89
@@ -13,6 +14,7 @@ import (
1314 "github.com/evstack/ev-node/block/internal/common"
1415 coreda "github.com/evstack/ev-node/core/da"
1516 "github.com/evstack/ev-node/pkg/config"
17+ pkgda "github.com/evstack/ev-node/pkg/da"
1618 "github.com/evstack/ev-node/pkg/genesis"
1719 "github.com/evstack/ev-node/pkg/rpc/server"
1820 "github.com/evstack/ev-node/pkg/signer"
@@ -124,6 +126,9 @@ type DASubmitter struct {
124126 // calculate namespaces bytes once and reuse them
125127 namespaceBz []byte
126128 namespaceDataBz []byte
129+
130+ // address selector for multi-account support
131+ addressSelector pkgda.AddressSelector
127132}
128133
129134// NewDASubmitter creates a new DA submitter
@@ -147,6 +152,17 @@ func NewDASubmitter(
147152 metrics = common .NopMetrics ()
148153 }
149154
155+ // Create address selector based on configuration
156+ var addressSelector pkgda.AddressSelector
157+ if len (config .DA .SigningAddresses ) > 0 {
158+ addressSelector = pkgda .NewRoundRobinSelector (config .DA .SigningAddresses )
159+ daSubmitterLogger .Info ().
160+ Int ("num_addresses" , len (config .DA .SigningAddresses )).
161+ Msg ("initialized round-robin address selector for multi-account DA submissions" )
162+ } else {
163+ addressSelector = pkgda .NewNoOpSelector ()
164+ }
165+
150166 return & DASubmitter {
151167 da : da ,
152168 config : config ,
@@ -156,6 +172,7 @@ func NewDASubmitter(
156172 logger : daSubmitterLogger ,
157173 namespaceBz : coreda .NamespaceFromString (config .DA .GetNamespace ()).Bytes (),
158174 namespaceDataBz : coreda .NamespaceFromString (config .DA .GetDataNamespace ()).Bytes (),
175+ addressSelector : addressSelector ,
159176 }
160177}
161178
@@ -235,7 +252,6 @@ func (s *DASubmitter) SubmitHeaders(ctx context.Context, cache cache.Manager) er
235252 "header" ,
236253 s .namespaceBz ,
237254 []byte (s .config .DA .SubmitOptions ),
238- cache ,
239255 func () uint64 { return cache .NumPendingHeaders () },
240256 )
241257}
@@ -279,7 +295,6 @@ func (s *DASubmitter) SubmitData(ctx context.Context, cache cache.Manager, signe
279295 "data" ,
280296 s .namespaceDataBz ,
281297 []byte (s .config .DA .SubmitOptions ),
282- cache ,
283298 func () uint64 { return cache .NumPendingData () },
284299 )
285300}
@@ -340,6 +355,44 @@ func (s *DASubmitter) createSignedData(dataList []*types.SignedData, signer sign
340355 return signedDataList , nil
341356}
342357
358+ // mergeSubmitOptions merges the base submit options with a signing address.
359+ // If the base options are valid JSON, the signing address is added to the JSON object.
360+ // Otherwise, a new JSON object is created with just the signing address.
361+ // Returns the base options unchanged if no signing address is provided.
362+ func mergeSubmitOptions (baseOptions []byte , signingAddress string ) ([]byte , error ) {
363+ if signingAddress == "" {
364+ return baseOptions , nil
365+ }
366+
367+ var optionsMap map [string ]interface {}
368+
369+ // If base options are provided, try to parse them as JSON
370+ if len (baseOptions ) > 0 {
371+ // Try to unmarshal existing options, ignoring errors for non-JSON input
372+ if err := json .Unmarshal (baseOptions , & optionsMap ); err != nil {
373+ // Not valid JSON - start with empty map
374+ optionsMap = make (map [string ]interface {})
375+ }
376+ }
377+
378+ // Ensure map is initialized even if unmarshal returned nil
379+ if optionsMap == nil {
380+ optionsMap = make (map [string ]interface {})
381+ }
382+
383+ // Add or override the signing address
384+ // Note: Uses "signer_address" to match Celestia's TxConfig JSON schema
385+ optionsMap ["signer_address" ] = signingAddress
386+
387+ // Marshal back to JSON
388+ mergedOptions , err := json .Marshal (optionsMap )
389+ if err != nil {
390+ return nil , fmt .Errorf ("failed to marshal submit options: %w" , err )
391+ }
392+
393+ return mergedOptions , nil
394+ }
395+
343396// submitToDA is a generic helper for submitting items to the DA layer with retry, backoff, and gas price logic.
344397func submitToDA [T any ](
345398 s * DASubmitter ,
@@ -350,7 +403,6 @@ func submitToDA[T any](
350403 itemType string ,
351404 namespace []byte ,
352405 options []byte ,
353- cache cache.Manager ,
354406 getTotalPendingFn func () uint64 ,
355407) error {
356408 marshaled , err := marshalItems (ctx , items , marshalFn , itemType )
@@ -397,12 +449,24 @@ func submitToDA[T any](
397449 return err
398450 }
399451
452+ // Select signing address and merge with options
453+ signingAddress := s .addressSelector .Next ()
454+ mergedOptions , err := mergeSubmitOptions (options , signingAddress )
455+ if err != nil {
456+ s .logger .Error ().Err (err ).Msg ("failed to merge submit options with signing address" )
457+ return fmt .Errorf ("failed to merge submit options: %w" , err )
458+ }
459+
460+ if signingAddress != "" {
461+ s .logger .Debug ().Str ("signingAddress" , signingAddress ).Msg ("using signing address for DA submission" )
462+ }
463+
400464 submitCtx , cancel := context .WithTimeout (ctx , submissionTimeout )
401465 defer cancel ()
402466
403467 // Perform submission
404468 start := time .Now ()
405- res := types .SubmitWithHelpers (submitCtx , s .da , s .logger , marshaled , rs .GasPrice , namespace , options )
469+ res := types .SubmitWithHelpers (submitCtx , s .da , s .logger , marshaled , rs .GasPrice , namespace , mergedOptions )
406470 s .logger .Debug ().Int ("attempts" , rs .Attempt ).Dur ("elapsed" , time .Since (start )).Uint64 ("code" , uint64 (res .Code )).Msg ("got SubmitWithHelpers response from celestia" )
407471
408472 // Record submission result for observability
0 commit comments