Skip to content
Open
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
2 changes: 2 additions & 0 deletions app/monitoringapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,13 +284,15 @@ func beaconNodeVersionMetric(ctx context.Context, eth2Cl eth2wrap.Client, beacon
if err != nil {
log.Warn(ctx, "Failed to fetch beacon node version", err,
z.Str("beacon_node_address", addr))

continue
}

response, err := scopedClient.NodeIdentity(ctx, &eth2api.NodeIdentityOpts{})
if err != nil {
log.Warn(ctx, "Failed to fetch beacon node identity", err,
z.Str("beacon_node_address", addr))

continue
}

Expand Down
84 changes: 82 additions & 2 deletions cluster/cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
//go:generate go test . -v -update -clean

const (
v1_11 = "v1.11.0"
v1_10 = "v1.10.0"
v1_9 = "v1.9.0"
v1_8 = "v1.8.0"
Expand Down Expand Up @@ -64,12 +65,12 @@ func TestEncode(t *testing.T) {
}

var partialAmounts []int
if isAnyVersion(version, v1_8, v1_9, v1_10) {
if isAnyVersion(version, v1_8, v1_9, v1_10, v1_11) {
partialAmounts = []int{16, 16}
}

targetGasLimit := uint(0)
if isAnyVersion(version, v1_10) {
if isAnyVersion(version, v1_10, v1_11) {
targetGasLimit = 30000000
}

Expand Down Expand Up @@ -316,6 +317,85 @@ func TestDefinitionPeers(t *testing.T) {
}
}

// TestV1x11SafeSignatures tests that v1.11 supports variable-length signatures (Safe multisig).
func TestV1x11SafeSignatures(t *testing.T) {
r := rand.New(rand.NewSource(1))

// Create 130-byte signatures (Safe threshold=2: 2 × 65 bytes)
safeSignature130 := make([]byte, 130)
_, _ = r.Read(safeSignature130)

// Create 195-byte signatures (Safe threshold=3: 3 × 65 bytes)
safeSignature195 := make([]byte, 195)
_, _ = r.Read(safeSignature195)

// Create 65-byte EOA signature
eoaSignature65 := testutil.RandomSecp256k1SignatureSeed(r)

_, enr1 := testutil.RandomENR(t, 1)
_, enr2 := testutil.RandomENR(t, 2)

def, err := cluster.NewDefinition(
"Safe multisig cluster",
1,
2,
[]string{testutil.RandomETHAddressSeed(r)},
[]string{testutil.RandomETHAddressSeed(r)},
eth2util.Sepolia.GenesisForkVersionHex,
cluster.Creator{
Address: testutil.RandomETHAddressSeed(r),
ConfigSignature: safeSignature130, // Safe threshold=2
},
[]cluster.Operator{
{
Address: testutil.RandomETHAddressSeed(r),
ENR: enr1.String(),
ConfigSignature: safeSignature195, // Safe threshold=3
ENRSignature: safeSignature130, // Safe threshold=2
},
{
Address: testutil.RandomETHAddressSeed(r),
ENR: enr2.String(),
ConfigSignature: eoaSignature65, // EOA (65 bytes)
ENRSignature: eoaSignature65, // EOA (65 bytes)
},
},
[]int{32}, // Deposit amounts (32 ETH)
"abft", // Consensus protocol
30000000, // Target gas limit
false, // Compounding
r, // Random reader
func(d *cluster.Definition) {
d.Version = v1_11
d.Timestamp = "2024-01-01T00:00:00Z"
},
)
require.NoError(t, err)

// Test SetDefinitionHashes with variable-length signatures
defWithHashes, err := def.SetDefinitionHashes()
require.NoError(t, err, "SetDefinitionHashes should succeed with Safe signatures")
require.NotEmpty(t, defWithHashes.ConfigHash, "ConfigHash should be computed")
require.NotEmpty(t, defWithHashes.DefinitionHash, "DefinitionHash should be computed")

// Test JSON marshaling/unmarshaling
jsonBytes, err := json.Marshal(defWithHashes)
require.NoError(t, err, "JSON marshal should succeed")

var unmarshaled cluster.Definition
err = json.Unmarshal(jsonBytes, &unmarshaled)
require.NoError(t, err, "JSON unmarshal should succeed")

// Verify signatures are preserved
require.Equal(t, 130, len(unmarshaled.Creator.ConfigSignature), "Creator Safe signature length")
require.Equal(t, 195, len(unmarshaled.Operators[0].ConfigSignature), "Operator 0 config Safe signature length")
require.Equal(t, 130, len(unmarshaled.Operators[0].ENRSignature), "Operator 0 ENR Safe signature length")
require.Equal(t, 65, len(unmarshaled.Operators[1].ConfigSignature), "Operator 1 EOA signature length")

// Test VerifyHashes
require.NoError(t, defWithHashes.VerifyHashes(), "VerifyHashes should succeed")
}

func isAnyVersion(version string, list ...string) bool {
return slices.Contains(list, version)
}
94 changes: 79 additions & 15 deletions cluster/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,10 @@ func (d Definition) VerifySignatures(eth1 eth1wrap.EthClientRunner) error {
return errors.New("older version signatures not supported")
}

if err := d.validateCreatorSignatureLength(); err != nil {
return err
}

// Check valid operator config signature for each operator.
operatorConfigHashDigest, err := digestEIP712(getOperatorEIP712Type(d.Version), d, Operator{})
if err != nil {
Expand All @@ -255,6 +259,10 @@ func (d Definition) VerifySignatures(eth1 eth1wrap.EthClientRunner) error {
return errors.New("empty operator config signature", z.Any("operator_address", o.Address))
}

if err := d.validateOperatorSignatureLengths(o); err != nil {
return err
}

// Check that we have a valid config signature for each operator.
if ok, err := verifySig(o.Address, operatorConfigHashDigest, o.ConfigSignature); err != nil {
return err
Expand Down Expand Up @@ -320,6 +328,62 @@ func (d Definition) VerifySignatures(eth1 eth1wrap.EthClientRunner) error {
return nil
}

// validateCreatorSignatureLength validates creator signature length for v1.11.0+.
func (d Definition) validateCreatorSignatureLength() error {
if !isAnyVersion(d.Version, v1_11) {
return nil // Skip validation for older versions
}

if err := validateSignatureLength(d.Creator.ConfigSignature, "creator config signature"); err != nil {
return errors.Wrap(err, "invalid signature length", z.Str("address", d.Creator.Address))
}

return nil
}

// validateOperatorSignatureLengths validates a single operator's signature lengths for v1.11.0+.
func (d Definition) validateOperatorSignatureLengths(op Operator) error {
if !isAnyVersion(d.Version, v1_11) {
return nil // Skip validation for older versions
}

if err := validateSignatureLength(op.ConfigSignature, "operator config signature"); err != nil {
return errors.Wrap(err, "invalid signature length", z.Str("address", op.Address))
}
if err := validateSignatureLength(op.ENRSignature, "operator enr signature"); err != nil {
return errors.Wrap(err, "invalid signature length", z.Str("address", op.Address))
}

return nil
}

// validateSignatureLength validates that a signature is either empty or a multiple of 65 bytes.
// Safe/Gnosis Safe multisig signatures are concatenated ECDSA signatures: threshold × 65 bytes.
func validateSignatureLength(sig []byte, fieldName string) error {
if len(sig) == 0 {
return nil // Empty signatures are valid
}

if len(sig)%65 != 0 {
return errors.New("signature must be multiple of 65 bytes",
z.Str("field", fieldName),
z.Int("length", len(sig)),
z.Int("remainder", len(sig)%65),
)
}

if len(sig) > sszMaxSignature {
return errors.New("signature exceeds maximum length",
z.Str("field", fieldName),
z.Int("length", len(sig)),
z.Int("max", sszMaxSignature),
z.Int("max_threshold", sszMaxSignature/65),
)
}

return nil
}

// Peers returns the operators as a slice of p2p peers.
func (d Definition) Peers() ([]p2p.Peer, error) {
var resp []p2p.Peer
Expand Down Expand Up @@ -439,8 +503,8 @@ func (d Definition) MarshalJSON() ([]byte, error) {
return marshalDefinitionV1x8(d2)
case isAnyVersion(d2.Version, v1_9):
return marshalDefinitionV1x9(d2)
case isAnyVersion(d2.Version, v1_10):
return marshalDefinitionV1x10(d2)
case isAnyVersion(d2.Version, v1_10, v1_11):
return marshalDefinitionV1x10to11(d2)
default:
return nil, errors.New("unsupported version")
}
Expand Down Expand Up @@ -496,8 +560,8 @@ func (d *Definition) UnmarshalJSON(data []byte) error {
if err != nil {
return err
}
case isAnyVersion(version.Version, v1_10):
def, err = unmarshalDefinitionV1x10(data)
case isAnyVersion(version.Version, v1_10, v1_11):
def, err = unmarshalDefinitionV1x10to11(data)
if err != nil {
return err
}
Expand Down Expand Up @@ -623,7 +687,7 @@ func marshalDefinitionV1x4(def Definition) ([]byte, error) {
}

func marshalDefinitionV1x5to7(def Definition) ([]byte, error) {
resp, err := json.Marshal(definitionJSONv1x5{
resp, err := json.Marshal(definitionJSONv1x5to7{
Name: def.Name,
UUID: def.UUID,
Version: def.Version,
Expand Down Expand Up @@ -703,8 +767,8 @@ func marshalDefinitionV1x9(def Definition) ([]byte, error) {
return resp, nil
}

func marshalDefinitionV1x10(def Definition) ([]byte, error) {
resp, err := json.Marshal(definitionJSONv1x10{
func marshalDefinitionV1x10to11(def Definition) ([]byte, error) {
resp, err := json.Marshal(definitionJSONv1x10to11{
Name: def.Name,
UUID: def.UUID,
Version: def.Version,
Expand Down Expand Up @@ -832,7 +896,7 @@ func unmarshalDefinitionV1x4(data []byte) (def Definition, err error) {
}

func unmarshalDefinitionV1x5to7(data []byte) (def Definition, err error) {
var defJSON definitionJSONv1x5
var defJSON definitionJSONv1x5to7
if err := json.Unmarshal(data, &defJSON); err != nil {
return Definition{}, errors.Wrap(err, "unmarshal definition v1_5")
}
Expand Down Expand Up @@ -932,10 +996,10 @@ func unmarshalDefinitionV1x9(data []byte) (def Definition, err error) {
}, nil
}

func unmarshalDefinitionV1x10(data []byte) (def Definition, err error) {
var defJSON definitionJSONv1x10
func unmarshalDefinitionV1x10to11(data []byte) (def Definition, err error) {
var defJSON definitionJSONv1x10to11
if err := json.Unmarshal(data, &defJSON); err != nil {
return Definition{}, errors.Wrap(err, "unmarshal definition v1_10")
return Definition{}, errors.Wrap(err, "unmarshal definition v1_10 to v1_11")
}

if len(defJSON.ValidatorAddresses) != defJSON.NumValidators {
Expand Down Expand Up @@ -1053,8 +1117,8 @@ type definitionJSONv1x4 struct {
DefinitionHash ethHex `json:"definition_hash"`
}

// definitionJSONv1x5 is the json formatter of Definition for versions v1.5 to v1.7.
type definitionJSONv1x5 struct {
// definitionJSONv1x5to7 is the json formatter of Definition for versions v1.5 to v1.7.
type definitionJSONv1x5to7 struct {
Name string `json:"name,omitempty"`
Creator creatorJSON `json:"creator"`
Operators []operatorJSONv1x2orLater `json:"operators"`
Expand Down Expand Up @@ -1107,8 +1171,8 @@ type definitionJSONv1x9 struct {
DefinitionHash ethHex `json:"definition_hash"`
}

// definitionJSONv1x10 is the json formatter of Definition for versions v1.10 or later.
type definitionJSONv1x10 struct {
// definitionJSONv1x10to11 is the json formatter of Definition for versions v1.10 and v1.11.
type definitionJSONv1x10to11 struct {
Name string `json:"name,omitempty"`
Creator creatorJSON `json:"creator"`
Operators []operatorJSONv1x2orLater `json:"operators"`
Expand Down
4 changes: 2 additions & 2 deletions cluster/lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func (l Lock) MarshalJSON() ([]byte, error) {
return marshalLockV1x6(l, lockHash)
case isAnyVersion(l.Version, v1_7):
return marshalLockV1x7(l, lockHash)
case isAnyVersion(l.Version, v1_8, v1_9, v1_10):
case isAnyVersion(l.Version, v1_8, v1_9, v1_10, v1_11):
return marshalLockV1x8OrLater(l, lockHash)
default:
return nil, errors.New("unsupported version")
Expand Down Expand Up @@ -104,7 +104,7 @@ func (l *Lock) UnmarshalJSON(data []byte) error {
if err != nil {
return err
}
case isAnyVersion(version.Definition.Version, v1_8, v1_9, v1_10):
case isAnyVersion(version.Definition.Version, v1_8, v1_9, v1_10, v1_11):
lock, err = unmarshalLockV1x8OrLater(data)
if err != nil {
return err
Expand Down
4 changes: 2 additions & 2 deletions cluster/operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ type Operator struct {
ENR string `config_hash:"-" definition_hash:"1" json:"enr" ssz:"ByteList[1024]"`

// ConfigSignature is an EIP712 signature of the config_hash using privkey corresponding to operator Ethereum Address.
ConfigSignature []byte `json:"config_signature,0xhex" ssz:"Bytes65" config_hash:"-" definition_hash:"2"`
ConfigSignature []byte `json:"config_signature,0xhex" ssz:"ByteList[384]" config_hash:"-" definition_hash:"2"`

// ENRSignature is a EIP712 signature of the ENR by the Address, authorising the charon node to act on behalf of the operator in the cluster.
ENRSignature []byte `json:"enr_signature,0xhex" ssz:"Bytes65" config_hash:"-" definition_hash:"3"`
ENRSignature []byte `json:"enr_signature,0xhex" ssz:"ByteList[384]" config_hash:"-" definition_hash:"3"`
}

// operatorJSONv1x1 is the json formatter of Operator for versions v1.0.0 and v1.1.0.
Expand Down
Loading
Loading