Skip to content
6 changes: 5 additions & 1 deletion cli/GENERATE_CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ The `generate-config` command allows you to generate a YAML configuration file b
| `--operator-private-key` | string | | Secret key for the operator. |
| `--metrics-api-port` | int | `0` | Port number for the Metrics API (set to `0` to disable). |
| `--ssv-domain` | string | Derived from local testnet config | Hex-encoded domain type (prefixed with `0x`). |
| `--ssv-next-domain` | string | Derived from local testnet config | Hex-encoded next domain type (prefixed with `0x`). |
| `--ssv-registry-sync-offset` | uint64 | Derived from local testnet config | Registry sync offset for the network. |
| `--ssv-registry-contract-addr` | string | Derived from local testnet config | Ethereum address of the network registry contract (e.g., `0xYourAddress`). |
| `--ssv-bootnodes` | string | Derived from local testnet config | Comma-separated list of network bootnodes. |
Expand Down Expand Up @@ -89,6 +90,7 @@ ssvnode generate-config \
--consensus-client "http://consensus.example.com:9000" \
--execution-client "http://execution.example.com:8545" \
--ssv-domain "0x12345678" \
--ssv-next-domain "0x9abcdef0" \
--ssv-registry-sync-offset 50 \
--ssv-registry-contract-addr "0xYourRegistryContractAddress" \
--ssv-bootnodes "enode://bootnode1@127.0.0.1:30303,enode://bootnode2@127.0.0.1:30304" \
Expand Down Expand Up @@ -117,6 +119,7 @@ ssv:
Network: LocalTestnetSSV
CustomNetwork:
DomainType: "0x12345678"
NextDomainType: "0x9abcdef0"
RegistrySyncOffset: 0
RegistryContractAddr: "0xYourRegistryContractAddress"
Bootnodes:
Expand Down Expand Up @@ -148,6 +151,7 @@ MetricsAPIPort: 8080
- `Network`: Name of the network.
- `CustomNetwork`: Contains custom network parameters.
- `DomainType`: Hex-encoded domain type (prefixed with `0x`).
- `NextDomainType`: Hex-encoded next domain type (prefixed with `0x`).
- `RegistrySyncOffset`: Registry sync offset for the network.
- `RegistryContractAddr`: Ethereum address of the network registry contract.
- `Bootnodes`: List of network bootnodes.
Expand All @@ -157,4 +161,4 @@ MetricsAPIPort: 8080
- `OperatorPrivateKey`: Secret key for the operator.

- **MetricsAPIPort**
- `MetricsAPIPort`: Port number for the Metrics API.
- `MetricsAPIPort`: Port number for the Metrics API.
8 changes: 8 additions & 0 deletions cli/generate_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ var (
operatorPrivateKey string
metricsAPIPort int
ssvDomain string
ssvNextDomain string
ssvRegistrySyncOffset uint64
ssvRegistryContractAddr string
ssvBootnodes string
Expand Down Expand Up @@ -79,6 +80,10 @@ var generateConfigCmd = &cobra.Command{
if err != nil {
log.Fatalf("Failed to decode network domain: %v", err)
}
parsedNextDomain, err := hex.DecodeString(strings.TrimPrefix(ssvNextDomain, "0x"))
if err != nil {
log.Fatalf("Failed to decode next network domain: %v", err)
}

parsedDiscoveryProtocolID, err := hex.DecodeString(strings.TrimPrefix(ssvDiscoveryProtocolID, "0x"))
if err != nil {
Expand All @@ -105,6 +110,7 @@ var generateConfigCmd = &cobra.Command{
config.MetricsAPIPort = metricsAPIPort
config.SSV.CustomNetwork = &networkconfig.SSV{
DomainType: spectypes.DomainType(parsedDomain),
NextDomainType: spectypes.DomainType(parsedNextDomain),
RegistrySyncOffset: new(big.Int).SetUint64(ssvRegistrySyncOffset),
RegistryContractAddr: ethcommon.HexToAddress(ssvRegistryContractAddr),
Bootnodes: bootnodes,
Expand Down Expand Up @@ -143,6 +149,8 @@ func init() {

ssvDomainDefault := "0x" + hex.EncodeToString(defaultNetwork.DomainType[:])
generateConfigCmd.Flags().StringVar(&ssvDomain, "ssv-domain", ssvDomainDefault, "SSV domain type")
ssvNextDomainDefault := "0x" + hex.EncodeToString(defaultNetwork.NextDomainType[:])
generateConfigCmd.Flags().StringVar(&ssvNextDomain, "ssv-next-domain", ssvNextDomainDefault, "SSV next domain type")
generateConfigCmd.Flags().Uint64Var(&ssvRegistrySyncOffset, "ssv-registry-sync-offset", defaultNetwork.RegistrySyncOffset.Uint64(), "SSV registry sync offset")
generateConfigCmd.Flags().StringVar(&ssvRegistryContractAddr, "ssv-registry-contract-addr", defaultNetwork.RegistryContractAddr.String(), "SSV registry contract addr")
generateConfigCmd.Flags().StringVar(&ssvBootnodes, "ssv-bootnodes", strings.Join(defaultNetwork.Bootnodes, sliceSeparator), "SSV bootnodes (comma-separated)")
Expand Down
27 changes: 2 additions & 25 deletions cli/operator/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import (
"bytes"
"context"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"fmt"
"log"
"math/big"
Expand Down Expand Up @@ -466,8 +464,8 @@ var StartNodeCmd = &cobra.Command{
ws := exporterapi.NewWsServer(cmd.Context(), logger, nil, http.NewServeMux(), cfg.WithPing)
cfg.SSVOptions.WS = ws
cfg.SSVOptions.WsAPIPort = cfg.WsAPIPort
cfg.SSVOptions.ValidatorOptions.NewDecidedHandler = decided.NewStreamPublisher(logger, networkConfig.DomainType, ws)
decidedStreamPublisherFn = decided.NewDecidedListener(logger, networkConfig.DomainType, ws, nodeStorage.ValidatorStore())
cfg.SSVOptions.ValidatorOptions.NewDecidedHandler = decided.NewStreamPublisher(logger, networkConfig, ws)
decidedStreamPublisherFn = decided.NewDecidedListener(logger, networkConfig, ws, nodeStorage.ValidatorStore())
}

cfg.SSVOptions.ValidatorOptions.DutyRoles = []spectypes.BeaconRole{spectypes.BNRoleAttester} // TODO could be better to set in other place
Expand Down Expand Up @@ -1025,27 +1023,6 @@ func setupSSVNetwork(logger *zap.Logger) (*networkconfig.SSV, error) {
)
}

if cfg.SSVOptions.CustomDomainType != "" {
if !strings.HasPrefix(cfg.SSVOptions.CustomDomainType, "0x") {
return nil, errors.New("custom domain type must be a hex string")
}
domainBytes, err := hex.DecodeString(cfg.SSVOptions.CustomDomainType[2:])
if err != nil {
return nil, errors.Wrap(err, "failed to decode custom domain type")
}
if len(domainBytes) != 4 {
return nil, errors.New("custom domain type must be 4 bytes")
}

// https://github.com/ssvlabs/ssv/pull/1808 incremented the post-fork domain type by 1, so we have to maintain the compatibility.
postForkDomain := binary.BigEndian.Uint32(domainBytes) + 1
binary.BigEndian.PutUint32(ssvConfig.DomainType[:], postForkDomain)

logger.Warn("running with custom domain type; it's deprecated, consider using custom network instead",
fields.Domain(ssvConfig.DomainType),
)
}

nodeType := "light"
if cfg.SSVOptions.ValidatorOptions.FullNode {
nodeType = "full"
Expand Down
1 change: 1 addition & 0 deletions eth/eventhandler/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ func (eh *EventHandler) validatorAddedEventToShare(
encryptedKey = encryptedKeys[i]
}

// Legacy field kept for storage/spec compatibility; runtime domain selection is slot-based elsewhere.
validatorShare.DomainType = eh.networkConfig.DomainType
validatorShare.Committee = shareMembers

Expand Down
10 changes: 5 additions & 5 deletions exporter/api/decided/stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import (
"time"

"github.com/patrickmn/go-cache"
spectypes "github.com/ssvlabs/ssv-spec/types"
"go.uber.org/zap"

"github.com/ssvlabs/ssv/exporter/api"
qbftstorage "github.com/ssvlabs/ssv/ibft/storage"
"github.com/ssvlabs/ssv/networkconfig"
"github.com/ssvlabs/ssv/observability/log/fields"
dutytracer "github.com/ssvlabs/ssv/operator/dutytracer"
"github.com/ssvlabs/ssv/protocol/v2/qbft/controller"
Expand All @@ -18,7 +18,7 @@ import (

// NewStreamPublisher handles incoming newly decided messages.
// it forward messages to websocket stream, where messages are cached (1m TTL) to avoid flooding
func NewStreamPublisher(logger *zap.Logger, domainType spectypes.DomainType, ws api.WebSocketServer) controller.NewDecidedHandler {
func NewStreamPublisher(logger *zap.Logger, netCfg *networkconfig.Network, ws api.WebSocketServer) controller.NewDecidedHandler {
c := cache.New(time.Minute, time.Minute*3/2)
feed := ws.BroadcastFeed()
return func(msg qbftstorage.Participation) {
Expand All @@ -30,13 +30,13 @@ func NewStreamPublisher(logger *zap.Logger, domainType spectypes.DomainType, ws
c.SetDefault(key, true)

logger.Debug("broadcast decided stream", fields.PubKey(msg.PubKey[:]), fields.Slot(msg.Slot))
feed.Send(api.NewParticipantsAPIMsg(domainType, msg))
feed.Send(api.NewParticipantsAPIMsg(netCfg, msg))
}
}

// NewDecidedListener handles incoming newly decided messages.
// it forward messages to websocket stream, where messages are cached (1m TTL) to avoid flooding
func NewDecidedListener(logger *zap.Logger, domainType spectypes.DomainType, ws api.WebSocketServer, validators registrystorage.ValidatorStore) func(dutytracer.DecidedInfo) {
func NewDecidedListener(logger *zap.Logger, netCfg *networkconfig.Network, ws api.WebSocketServer, validators registrystorage.ValidatorStore) func(dutytracer.DecidedInfo) {
feed := ws.BroadcastFeed()
cache := cache.New(time.Minute, 90*time.Second) // 1m TTL, 1.5m eviction to avoid flooding ws stream

Expand Down Expand Up @@ -65,6 +65,6 @@ func NewDecidedListener(logger *zap.Logger, domainType spectypes.DomainType, ws
return
}
cache.SetDefault(key, true)
feed.Send(api.NewParticipantsAPIMsg(domainType, participation))
feed.Send(api.NewParticipantsAPIMsg(netCfg, participation))
}
}
8 changes: 5 additions & 3 deletions exporter/api/msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
spectypes "github.com/ssvlabs/ssv-spec/types"

qbftstorage "github.com/ssvlabs/ssv/ibft/storage"
"github.com/ssvlabs/ssv/networkconfig"
)

// Message represents an exporter message
Expand All @@ -33,8 +34,8 @@ type ParticipantsAPI struct {
}

// NewParticipantsAPIMsg creates a new message in a new format from the given message.
func NewParticipantsAPIMsg(domainType spectypes.DomainType, msg qbftstorage.Participation) Message {
data, err := ParticipantsAPIData(domainType, msg)
func NewParticipantsAPIMsg(netCfg *networkconfig.Network, msg qbftstorage.Participation) Message {
data, err := ParticipantsAPIData(netCfg, msg)
if err != nil {
return Message{
Type: TypeParticipants,
Expand All @@ -55,13 +56,14 @@ func NewParticipantsAPIMsg(domainType spectypes.DomainType, msg qbftstorage.Part
}

// ParticipantsAPIData creates a new message from the given message in a new format.
func ParticipantsAPIData(domainType spectypes.DomainType, msgs ...qbftstorage.Participation) (any, error) {
func ParticipantsAPIData(netCfg *networkconfig.Network, msgs ...qbftstorage.Participation) (any, error) {
if len(msgs) == 0 {
return nil, errors.New("no messages")
}

apiMsgs := make([]*ParticipantsAPI, 0)
for _, msg := range msgs {
domainType := netCfg.DomainTypeAtSlot(msg.Slot)
var msgID = legacyNewMsgID(domainType, msg.PubKey[:], msg.Role)
blsPubKey := phase0.BLSPubKey{}
copy(blsPubKey[:], msg.PubKey[:])
Expand Down
5 changes: 3 additions & 2 deletions exporter/api/query_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"go.uber.org/zap"

"github.com/ssvlabs/ssv/ibft/storage"
"github.com/ssvlabs/ssv/networkconfig"
"github.com/ssvlabs/ssv/observability/log/fields"
"github.com/ssvlabs/ssv/protocol/v2/message"
)
Expand Down Expand Up @@ -56,7 +57,7 @@ func (h *Handler) HandleUnknownQuery(nm *NetworkMessage) {
}

// HandleParticipantsQuery handles TypeParticipants queries.
func (h *Handler) HandleParticipantsQuery(store *storage.ParticipantStores, nm *NetworkMessage, domain spectypes.DomainType) {
func (h *Handler) HandleParticipantsQuery(store *storage.ParticipantStores, nm *NetworkMessage, netCfg *networkconfig.Network) {
h.logger.Debug("handles query request",
zap.Uint64("from", nm.Msg.Filter.From),
zap.Uint64("to", nm.Msg.Filter.To),
Expand Down Expand Up @@ -103,7 +104,7 @@ func (h *Handler) HandleParticipantsQuery(store *storage.ParticipantStores, nm *
res.Data = []string{"internal error - could not get participants messages"}
} else {
participations := toParticipations(role, spectypes.ValidatorPK(pkRaw), participantsList)
data, err := ParticipantsAPIData(domain, participations...)
data, err := ParticipantsAPIData(netCfg, participations...)
if err != nil {
res.Data = []string{err.Error()}
} else {
Expand Down
11 changes: 5 additions & 6 deletions exporter/api/query_handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ func TestHandleDecidedQuery(t *testing.T) {

for _, role := range roles {
pk := sks[1].GetPublicKey()
ssvConfig := networkconfig.TestNetwork.SSV
decided250Seq, err := qbftstorage.CreateMultipleStoredInstances(rsaKeys, specqbft.Height(0), specqbft.Height(250), func(height specqbft.Height) ([]spectypes.OperatorID, *specqbft.Message) {
return oids, &specqbft.Message{
MsgType: specqbft.CommitMsgType,
Expand All @@ -131,7 +130,7 @@ func TestHandleDecidedQuery(t *testing.T) {
t.Run("valid range", func(t *testing.T) {
nm := newParticipantsAPIMsg(pk.SerializeToHexStr(), spectypes.BNRoleAttester, 0, 250)
h := NewHandler(l)
h.HandleParticipantsQuery(ibftStorage, nm, ssvConfig.DomainType)
h.HandleParticipantsQuery(ibftStorage, nm, networkconfig.TestNetwork)
require.NotNil(t, nm.Msg.Data)
msgs, ok := nm.Msg.Data.([]*ParticipantsAPI)

Expand All @@ -142,7 +141,7 @@ func TestHandleDecidedQuery(t *testing.T) {
t.Run("invalid range", func(t *testing.T) {
nm := newParticipantsAPIMsg(pk.SerializeToHexStr(), spectypes.BNRoleAttester, 400, 404)
h := NewHandler(l)
h.HandleParticipantsQuery(ibftStorage, nm, ssvConfig.DomainType)
h.HandleParticipantsQuery(ibftStorage, nm, networkconfig.TestNetwork)
require.NotNil(t, nm.Msg.Data)
data, ok := nm.Msg.Data.([]string)
require.True(t, ok)
Expand All @@ -152,7 +151,7 @@ func TestHandleDecidedQuery(t *testing.T) {
t.Run("non-existing validator", func(t *testing.T) {
nm := newParticipantsAPIMsg("xxx", spectypes.BNRoleAttester, 400, 404)
h := NewHandler(l)
h.HandleParticipantsQuery(ibftStorage, nm, ssvConfig.DomainType)
h.HandleParticipantsQuery(ibftStorage, nm, networkconfig.TestNetwork)
require.NotNil(t, nm.Msg.Data)
errs, ok := nm.Msg.Data.([]string)
require.True(t, ok)
Expand All @@ -162,7 +161,7 @@ func TestHandleDecidedQuery(t *testing.T) {
t.Run("non-existing role", func(t *testing.T) {
nm := newParticipantsAPIMsg(pk.SerializeToHexStr(), math.MaxUint64, 0, 250)
h := NewHandler(l)
h.HandleParticipantsQuery(ibftStorage, nm, ssvConfig.DomainType)
h.HandleParticipantsQuery(ibftStorage, nm, networkconfig.TestNetwork)
require.NotNil(t, nm.Msg.Data)
errs, ok := nm.Msg.Data.([]string)
require.True(t, ok)
Expand All @@ -172,7 +171,7 @@ func TestHandleDecidedQuery(t *testing.T) {
t.Run("non-existing storage", func(t *testing.T) {
nm := newParticipantsAPIMsg(pk.SerializeToHexStr(), spectypes.BNRoleSyncCommitteeContribution, 0, 250)
h := NewHandler(l)
h.HandleParticipantsQuery(ibftStorage, nm, ssvConfig.DomainType)
h.HandleParticipantsQuery(ibftStorage, nm, networkconfig.TestNetwork)
require.NotNil(t, nm.Msg.Data)
errs, ok := nm.Msg.Data.([]string)
require.True(t, ok)
Expand Down
3 changes: 3 additions & 0 deletions message/validation/consensus_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ func (mv *messageValidator) validateConsensusMessage(
if err := mv.validateTopicAtSlot(committeeInfo, topic, phase0.Slot(consensusMessage.Height)); err != nil {
return consensusMessage, err
}
if err := mv.validateDomainAtSlot(ssvMessage.GetID(), phase0.Slot(consensusMessage.Height)); err != nil {
return consensusMessage, err
}

if err := mv.validateConsensusMessageSemantics(signedSSVMessage, consensusMessage, committeeInfo.committee); err != nil {
return consensusMessage, err
Expand Down
3 changes: 3 additions & 0 deletions message/validation/partial_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ func (mv *messageValidator) validatePartialSignatureMessage(
if err := mv.validateTopicAtSlot(committeeInfo, topic, partialSignatureMessages.Slot); err != nil {
return partialSignatureMessages, err
}
if err := mv.validateDomainAtSlot(ssvMessage.GetID(), partialSignatureMessages.Slot); err != nil {
return partialSignatureMessages, err
}

if err := mv.validatePartialSignatureMessageSemantics(signedSSVMessage, partialSignatureMessages, committeeInfo.validatorIndices); err != nil {
return partialSignatureMessages, err
Expand Down
11 changes: 0 additions & 11 deletions message/validation/signed_ssv_message.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package validation

import (
"bytes"
"encoding/hex"
"fmt"
"slices"

Expand Down Expand Up @@ -121,15 +119,6 @@ func (mv *messageValidator) validateSSVMessage(ssvMessage *spectypes.SSVMessage)
return e
}

// Rule: If domain is different then self domain
domain := mv.netCfg.DomainType
if !bytes.Equal(ssvMessage.GetID().GetDomain(), domain[:]) {
err := ErrWrongDomain
err.got = hex.EncodeToString(ssvMessage.MsgID.GetDomain())
err.want = hex.EncodeToString(domain[:])
return err
}

return nil
}

Expand Down
12 changes: 12 additions & 0 deletions message/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package validation
// validator.go contains main code for validation and most of the rule checks.

import (
"bytes"
"context"
"encoding/hex"
"fmt"
Expand Down Expand Up @@ -237,6 +238,17 @@ func (mv *messageValidator) validateTopicAtSlot(committeeInfo CommitteeInfo, top
return nil
}

func (mv *messageValidator) validateDomainAtSlot(msgID spectypes.MessageID, slot phase0.Slot) error {
expectedDomain := mv.netCfg.DomainTypeAtSlot(slot)
if msgDomain := msgID.GetDomain(); !bytes.Equal(msgDomain, expectedDomain[:]) {
err := ErrWrongDomain
err.got = hex.EncodeToString(msgDomain)
err.want = hex.EncodeToString(expectedDomain[:])
return err
}
return nil
}

func (mv *messageValidator) getValidationLock(key spectypes.MessageID) *sync.Mutex {
lock, _, _ := mv.validationLocksInflight.Do(key, func() (*sync.Mutex, error) {
cachedLock := mv.validationLockCache.Get(key)
Expand Down
Loading
Loading