Skip to content

Commit

Permalink
cluster: definition v1.8 (#2823)
Browse files Browse the repository at this point in the history
Introducing cluster definition v1.8 with partial deposits.

category: feature
ticket: none
  • Loading branch information
pinebit authored Jan 29, 2024
1 parent f929f59 commit 15ca030
Show file tree
Hide file tree
Showing 14 changed files with 721 additions and 90 deletions.
30 changes: 26 additions & 4 deletions cluster/cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strings"
"testing"

eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/stretchr/testify/require"

"github.com/obolnetwork/charon/cluster"
Expand All @@ -21,6 +22,7 @@ import (
//go:generate go test . -v -update -clean

const (
v1_8 = "v1.8.0"
v1_7 = "v1.7.0"
v1_6 = "v1.6.0"
v1_5 = "v1.5.0"
Expand All @@ -47,6 +49,10 @@ func TestEncode(t *testing.T) {
func(d *cluster.Definition) {
d.Version = version
d.Timestamp = "2022-07-19T18:19:58+02:00" // Make deterministic
d.DepositAmounts = []eth2p0.Gwei{
eth2p0.Gwei(16000000000),
eth2p0.Gwei(16000000000),
}
},
}
// Definition version prior to v1.5 don't support multiple validator addresses.
Expand Down Expand Up @@ -104,6 +110,11 @@ func TestEncode(t *testing.T) {
definition.Creator = cluster.Creator{}
}

// Definition version prior to v1.8.0 don't support DepositAmounts.
if isAnyVersion(version, v1_0, v1_1, v1_2, v1_3, v1_4, v1_5, v1_6, v1_7) {
definition.DepositAmounts = nil
}

t.Run("definition_json_"+vStr, func(t *testing.T) {
testutil.RequireGoldenJSON(t, definition,
testutil.WithFilename("cluster_definition_"+vStr+".json"))
Expand Down Expand Up @@ -135,15 +146,15 @@ func TestEncode(t *testing.T) {
testutil.RandomBytes48(),
testutil.RandomBytes48(),
},
DepositData: cluster.RandomDepositData(),
PartialDepositData: []cluster.DepositData{cluster.RandomDepositData()},
BuilderRegistration: cluster.RandomRegistration(t, eth2util.Sepolia.Name),
}, {
PubKey: testutil.RandomBytes48(),
PubShares: [][]byte{
testutil.RandomBytes48(),
testutil.RandomBytes48(),
},
DepositData: cluster.RandomDepositData(),
PartialDepositData: []cluster.DepositData{cluster.RandomDepositData()},
BuilderRegistration: cluster.RandomRegistration(t, eth2util.Sepolia.Name),
},
},
Expand All @@ -155,14 +166,16 @@ func TestEncode(t *testing.T) {

// Make sure all the pubkeys are same.
for i := range lock.Validators {
lock.Validators[i].DepositData.PubKey = lock.Validators[i].PubKey
for j := range lock.Validators[i].PartialDepositData {
lock.Validators[i].PartialDepositData[j].PubKey = lock.Validators[i].PubKey
}
lock.Validators[i].BuilderRegistration.Message.PubKey = lock.Validators[i].PubKey
}

// Lock version prior to v1.6.0 don't support DepositData.
if isAnyVersion(version, v1_0, v1_1, v1_2, v1_3, v1_4, v1_5) {
for i := range lock.Validators {
lock.Validators[i].DepositData = cluster.DepositData{}
lock.Validators[i].PartialDepositData = nil
}
}

Expand All @@ -175,6 +188,15 @@ func TestEncode(t *testing.T) {
lock.NodeSignatures = nil
}

// Lock version v1.8.0 supports multiple PartialDepositData.
if isAnyVersion(version, v1_8) {
for i := range lock.Validators {
dd := cluster.RandomDepositData()
dd.PubKey = lock.Validators[i].PubKey
lock.Validators[i].PartialDepositData = append(lock.Validators[i].PartialDepositData, dd)
}
}

t.Run("lock_json_"+vStr, func(t *testing.T) {
testutil.RequireGoldenJSON(t, lock,
testutil.WithFilename("cluster_lock_"+vStr+".json"))
Expand Down
109 changes: 98 additions & 11 deletions cluster/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io"
"time"

eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/libp2p/go-libp2p/core/peer"

"github.com/obolnetwork/charon/app/errors"
Expand Down Expand Up @@ -138,8 +139,11 @@ type Definition struct {
// ValidatorAddresses define addresses of each validator.
ValidatorAddresses []ValidatorAddresses `config_hash:"10" definition_hash:"10" json:"validators" ssz:"CompositeList[65536]"`

// DepositAmounts specifies partial deposit amounts that sum up to 32ETH.
DepositAmounts []eth2p0.Gwei `config_hash:"11" definition_hash:"11" json:"deposit_amounts" ssz:"uint64[256]"`

// ConfigHash uniquely identifies a cluster definition excluding operator ENRs and signatures.
ConfigHash []byte `json:"config_hash,0xhex" ssz:"Bytes32" config_hash:"-" definition_hash:"11"`
ConfigHash []byte `json:"config_hash,0xhex" ssz:"Bytes32" config_hash:"-" definition_hash:"12"`

// DefinitionHash uniquely identifies a cluster definition including operator ENRs and signatures.
DefinitionHash []byte `json:"definition_hash,0xhex" ssz:"Bytes32" config_hash:"-" definition_hash:"-"`
Expand Down Expand Up @@ -365,7 +369,9 @@ func (d Definition) MarshalJSON() ([]byte, error) {
case isAnyVersion(d2.Version, v1_4):
return marshalDefinitionV1x4(d2)
case isAnyVersion(d2.Version, v1_5, v1_6, v1_7):
return marshalDefinitionV1x5(d2)
return marshalDefinitionV1x5to7(d2)
case isAnyVersion(d2.Version, v1_8):
return marshalDefinitionV1x8(d2)
default:
return nil, errors.New("unsupported version")
}
Expand Down Expand Up @@ -406,7 +412,12 @@ func (d *Definition) UnmarshalJSON(data []byte) error {
return err
}
case isAnyVersion(version.Version, v1_5, v1_6, v1_7):
def, err = unmarshalDefinitionV1x5(data)
def, err = unmarshalDefinitionV1x5to7(data)
if err != nil {
return err
}
case isAnyVersion(version.Version, v1_8):
def, err = unmarshalDefinitionV1x8(data)
if err != nil {
return err
}
Expand Down Expand Up @@ -465,7 +476,7 @@ func marshalDefinitionV1x0or1(def Definition) ([]byte, error) {
DefinitionHash: def.DefinitionHash,
})
if err != nil {
return nil, errors.Wrap(err, "marshal definition")
return nil, errors.Wrap(err, "marshal definition", z.Str("version", def.Version))
}

return resp, nil
Expand Down Expand Up @@ -493,7 +504,7 @@ func marshalDefinitionV1x2or3(def Definition) ([]byte, error) {
DefinitionHash: def.DefinitionHash,
})
if err != nil {
return nil, errors.Wrap(err, "marshal definition")
return nil, errors.Wrap(err, "marshal definition", z.Str("version", def.Version))
}

return resp, nil
Expand Down Expand Up @@ -525,13 +536,13 @@ func marshalDefinitionV1x4(def Definition) ([]byte, error) {
},
})
if err != nil {
return nil, errors.Wrap(err, "marshal definition")
return nil, errors.Wrap(err, "marshal definition", z.Str("version", def.Version))
}

return resp, nil
}

func marshalDefinitionV1x5(def Definition) ([]byte, error) {
func marshalDefinitionV1x5to7(def Definition) ([]byte, error) {
resp, err := json.Marshal(definitionJSONv1x5{
Name: def.Name,
UUID: def.UUID,
Expand All @@ -551,7 +562,34 @@ func marshalDefinitionV1x5(def Definition) ([]byte, error) {
},
})
if err != nil {
return nil, errors.Wrap(err, "marshal definition")
return nil, errors.Wrap(err, "marshal definition", z.Str("version", def.Version))
}

return resp, nil
}

func marshalDefinitionV1x8(def Definition) ([]byte, error) {
resp, err := json.Marshal(definitionJSONv1x8{
Name: def.Name,
UUID: def.UUID,
Version: def.Version,
Timestamp: def.Timestamp,
NumValidators: def.NumValidators,
Threshold: def.Threshold,
DKGAlgorithm: def.DKGAlgorithm,
ValidatorAddresses: validatorAddressesToJSON(def.ValidatorAddresses),
ForkVersion: def.ForkVersion,
ConfigHash: def.ConfigHash,
DefinitionHash: def.DefinitionHash,
Operators: operatorsToV1x2orLater(def.Operators),
Creator: creatorJSON{
Address: def.Creator.Address,
ConfigSignature: def.Creator.ConfigSignature,
},
DepositAmounts: def.DepositAmounts,
})
if err != nil {
return nil, errors.Wrap(err, "marshal definition", z.Str("version", def.Version))
}

return resp, nil
Expand Down Expand Up @@ -655,7 +693,7 @@ func unmarshalDefinitionV1x4(data []byte) (def Definition, err error) {
}, nil
}

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

func unmarshalDefinitionV1x8(data []byte) (def Definition, err error) {
var defJSON definitionJSONv1x8
if err := json.Unmarshal(data, &defJSON); err != nil {
return Definition{}, errors.Wrap(err, "unmarshal definition v1_8")
}

if len(defJSON.ValidatorAddresses) != defJSON.NumValidators {
return Definition{}, errors.New("num_validators not matching validators length")
}

return Definition{
Name: defJSON.Name,
UUID: defJSON.UUID,
Version: defJSON.Version,
Timestamp: defJSON.Timestamp,
NumValidators: defJSON.NumValidators,
Threshold: defJSON.Threshold,
DKGAlgorithm: defJSON.DKGAlgorithm,
ForkVersion: defJSON.ForkVersion,
ConfigHash: defJSON.ConfigHash,
DefinitionHash: defJSON.DefinitionHash,
Operators: operatorsFromV1x2orLater(defJSON.Operators),
ValidatorAddresses: validatorAddressesFromJSON(defJSON.ValidatorAddresses),
Creator: Creator{
Address: defJSON.Creator.Address,
ConfigSignature: defJSON.Creator.ConfigSignature,
},
DepositAmounts: defJSON.DepositAmounts,
}, nil
}

// supportEIP712Sigs returns true if the provided definition version supports EIP712 signatures.
// Note that Definition versions prior to v1.3.0 don't support EIP712 signatures.
func supportEIP712Sigs(version string) bool {
Expand Down Expand Up @@ -735,7 +804,7 @@ type definitionJSONv1x2or3 struct {
DefinitionHash ethHex `json:"definition_hash"`
}

// definitionJSONv1x4 is the json formatter of Definition for versions v1.4.
// definitionJSONv1x4 is the json formatter of Definition for version v1.4.
type definitionJSONv1x4 struct {
Name string `json:"name,omitempty"`
Creator creatorJSON `json:"creator"`
Expand All @@ -753,7 +822,7 @@ type definitionJSONv1x4 struct {
DefinitionHash ethHex `json:"definition_hash"`
}

// definitionJSONv1x5 is the json formatter of Definition for versions v1.5.
// definitionJSONv1x5 is the json formatter of Definition for versions v1.5 to v1.7.
type definitionJSONv1x5 struct {
Name string `json:"name,omitempty"`
Creator creatorJSON `json:"creator"`
Expand All @@ -770,6 +839,24 @@ type definitionJSONv1x5 struct {
DefinitionHash ethHex `json:"definition_hash"`
}

// definitionJSONv1x8 is the json formatter of Definition for versions v1.8 or later.
type definitionJSONv1x8 struct {
Name string `json:"name,omitempty"`
Creator creatorJSON `json:"creator"`
Operators []operatorJSONv1x2orLater `json:"operators"`
UUID string `json:"uuid"`
Version string `json:"version"`
Timestamp string `json:"timestamp,omitempty"`
NumValidators int `json:"num_validators"`
Threshold int `json:"threshold"`
ValidatorAddresses []validatorAddressesJSON `json:"validators"`
DKGAlgorithm string `json:"dkg_algorithm"`
ForkVersion ethHex `json:"fork_version"`
DepositAmounts []eth2p0.Gwei `json:"deposit_amounts"`
ConfigHash ethHex `json:"config_hash"`
DefinitionHash ethHex `json:"definition_hash"`
}

// Creator identifies the creator of a cluster definition.
// Note the following struct tag meanings:
// - json: json field name. Suffix 0xhex indicates bytes are formatted as 0x prefixed hex strings.
Expand Down
40 changes: 39 additions & 1 deletion cluster/deposit.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type DepositData struct {
// WithdrawalCredentials included in the deposit.
WithdrawalCredentials []byte `json:"withdrawal_credentials" lock_hash:"1" ssz:"Bytes32"`

// Amount is the amount in Gwei to be deposited.
// Amount is the amount in Gwei to be deposited [1ETH..32ETH].
Amount int `json:"amount" lock_hash:"2" ssz:"uint64"`

// Signature is the BLS signature of the deposit message (above three fields).
Expand All @@ -26,6 +26,16 @@ type depositDataJSON struct {
Signature ethHex `json:"signature"`
}

// firstDepositDataOrDefault returns the first DepositData element or a default instance.
// For backward compatibility with versions prior to v1.8.
func firstDepositDataOrDefault(dd []DepositData) DepositData {
if len(dd) == 0 {
return DepositData{}
}

return dd[0]
}

// depositDataToJSON converts DepositData to depositDataJSON.
func depositDataToJSON(d DepositData) depositDataJSON {
return depositDataJSON{
Expand All @@ -45,3 +55,31 @@ func depositDataFromJSON(d depositDataJSON) DepositData {
Signature: d.Signature,
}
}

// depositDataArrayToJSON converts []DepositData to []depositDataJSON.
func depositDataArrayToJSON(dd []DepositData) []depositDataJSON {
if dd == nil {
return nil
}

array := make([]depositDataJSON, len(dd))
for i, d := range dd {
array[i] = depositDataToJSON(d)
}

return array
}

// depositDataArrayFromJSON converts []depositDataJSON to []DepositData.
func depositDataArrayFromJSON(dd []depositDataJSON) []DepositData {
if dd == nil {
return nil
}

array := make([]DepositData, len(dd))
for i, d := range dd {
array[i] = depositDataFromJSON(d)
}

return array
}
18 changes: 18 additions & 0 deletions cluster/deposit_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,21 @@ func TestDepositJSON(t *testing.T) {

require.Equal(t, b1, b2)
}

func TestDepositArrayJSON(t *testing.T) {
dd := []DepositData{
RandomDepositData(),
RandomDepositData(),
RandomDepositData(),
}

json := depositDataArrayToJSON(dd)
dd2 := depositDataArrayFromJSON(json)

require.Equal(t, dd, dd2)

t.Run("nil", func(t *testing.T) {
require.Nil(t, depositDataArrayToJSON(nil))
require.Nil(t, depositDataArrayFromJSON(nil))
})
}
Loading

0 comments on commit 15ca030

Please sign in to comment.