Skip to content

Commit

Permalink
cluster: release v1.3 (#1289)
Browse files Browse the repository at this point in the history
Releases cluster definition version v1.3.

category: misc
ticket: #1204
  • Loading branch information
corverroos committed Oct 14, 2022
1 parent c9035b4 commit b5420b9
Show file tree
Hide file tree
Showing 13 changed files with 122 additions and 71 deletions.
8 changes: 1 addition & 7 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,17 +137,11 @@ func Run(ctx context.Context, conf Config) (err error) {
return err
}

lock, err := loadLock(conf)
lock, err := loadLock(ctx, conf)
if err != nil {
return err
}

if err := lock.VerifySignatures(); err != nil && !conf.NoVerify {
return errors.Wrap(err, "cluster lock signature verification failed. Run with --no-verify to bypass verification at own risk")
} else if err != nil && conf.NoVerify {
log.Warn(ctx, "Ignoring failed cluster lock signature verification due to --no-verify flag", err)
}

lockHashHex := hex.EncodeToString(lock.LockHash)[:7]

p2pKey := conf.TestConfig.P2PKey
Expand Down
22 changes: 18 additions & 4 deletions app/disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,17 @@
package app

import (
"context"
"encoding/json"
"os"

"github.com/obolnetwork/charon/app/errors"
"github.com/obolnetwork/charon/app/log"
"github.com/obolnetwork/charon/cluster"
)

// loadLock reads the cluster lock from the given file path.
func loadLock(conf Config) (cluster.Lock, error) {
func loadLock(ctx context.Context, conf Config) (cluster.Lock, error) {
if conf.TestConfig.Lock != nil {
return *conf.TestConfig.Lock, nil
}
Expand All @@ -34,11 +36,23 @@ func loadLock(conf Config) (cluster.Lock, error) {
return cluster.Lock{}, errors.Wrap(err, "read lock")
}

var res cluster.Lock
err = json.Unmarshal(buf, &res)
var lock cluster.Lock
err = json.Unmarshal(buf, &lock)
if err != nil {
return cluster.Lock{}, errors.Wrap(err, "unmarshal lock")
}

return res, nil
if err := lock.VerifyHashes(); err != nil && !conf.NoVerify {
return cluster.Lock{}, errors.Wrap(err, "cluster lock hash verification failed. Run with --no-verify to bypass verification at own risk")
} else if err != nil && conf.NoVerify {
log.Warn(ctx, "Ignoring failed cluster lock hash verification due to --no-verify flag", err)
}

if err := lock.VerifySignatures(); err != nil && !conf.NoVerify {
return cluster.Lock{}, errors.Wrap(err, "cluster lock signature verification failed. Run with --no-verify to bypass verification at own risk")
} else if err != nil && conf.NoVerify {
log.Warn(ctx, "Ignoring failed cluster lock signature verification due to --no-verify flag", err)
}

return lock, nil
}
2 changes: 1 addition & 1 deletion app/disk_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func TestLoadLock(t *testing.T) {
require.NoError(t, err)

conf := Config{LockFile: filename}
actual, err := loadLock(conf)
actual, err := loadLock(context.Background(), conf)
require.NoError(t, err)

b2, err := json.Marshal(actual)
Expand Down
8 changes: 1 addition & 7 deletions cluster/cluster_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,7 @@ func randomDefinition(t *testing.T, op0, op1 Operator) Definition {
rand.New(rand.NewSource(1)))
require.NoError(t, err)

// TODO(xenowits): Remove the line below when v1.3 is the current version.
definition.Version = v1_3

resp, err := definition.SetDefinitionHashes()
require.NoError(t, err)

return resp
return definition
}

func TestSupportEIP712Sigs(t *testing.T) {
Expand Down
7 changes: 4 additions & 3 deletions cluster/cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,12 @@ func TestEncode(t *testing.T) {
},
},
rand.New(rand.NewSource(0)),
func(d *cluster.Definition) {
d.Version = version
d.Timestamp = "2022-07-19T18:19:58+02:00" // Make deterministic
},
)
require.NoError(t, err)
definition.Version = version

// Definition version prior to v1.3.0 don't support EIP712 signatures.
if version == "v1.0.0" || version == "v1.1.0" || version == "v1.2.0" {
Expand All @@ -74,8 +77,6 @@ func TestEncode(t *testing.T) {
}
}

definition.Timestamp = "2022-07-19T18:19:58+02:00" // Make deterministic

t.Run("definition_json_"+vStr, func(t *testing.T) {
testutil.RequireGoldenJSON(t, definition,
testutil.WithFilename("cluster_definition_"+vStr+".json"))
Expand Down
9 changes: 7 additions & 2 deletions cluster/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ type NodeIdx struct {
}

// NewDefinition returns a new definition populated with the latest version, timestamp and UUID.
// The hashes are also populated accordingly. Note that the hashes need to be recalculated when any field is modified.
func NewDefinition(name string, numVals int, threshold int, feeRecipientAddress string, withdrawalAddress string,
forkVersionHex string, operators []Operator, random io.Reader,
forkVersionHex string, operators []Operator, random io.Reader, opts ...func(*Definition),
) (Definition, error) {
def := Definition{
Version: currentVersion,
Expand All @@ -64,7 +65,11 @@ func NewDefinition(name string, numVals int, threshold int, feeRecipientAddress
return Definition{}, err
}

return def, nil
for _, opt := range opts {
opt(&def)
}

return def.SetDefinitionHashes()
}

// Definition defines an intended charon cluster configuration excluding validators.
Expand Down
27 changes: 18 additions & 9 deletions cluster/lock.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,6 @@ func (l *Lock) UnmarshalJSON(data []byte) error {
return errors.New("unsupported version")
}

hash, err := hashLock(lock)
if err != nil {
return errors.Wrap(err, "hash lock")
}

if !bytes.Equal(lock.LockHash, hash[:]) {
return errors.New("invalid lock hash")
}

*l = lock

return nil
Expand All @@ -123,6 +114,24 @@ func (l Lock) SetLockHash() (Lock, error) {
return l, nil
}

// VerifyHashes returns an error if hashes populated from json object doesn't matches actual hashes.
func (l Lock) VerifyHashes() error {
if err := l.Definition.VerifyHashes(); err != nil {
return errors.Wrap(err, "invalid definition")
}

lockHash, err := hashLock(l)
if err != nil {
return err
}

if !bytes.Equal(l.LockHash, lockHash[:]) {
return errors.New("invalid lock hash")
}

return nil
}

// VerifySignatures returns true if all config signatures are fully populated and valid.
// A verified lock is ready for use in charon run.
func (l Lock) VerifySignatures() error {
Expand Down
13 changes: 5 additions & 8 deletions cluster/test_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,7 @@ func NewForT(t *testing.T, dv, k, n, seed int, opts ...func(*Definition)) (Lock,

def, err := NewDefinition("test cluster", dv, k,
testutil.RandomETHAddress(), testutil.RandomETHAddress(),
"0x00000000", ops, random)
require.NoError(t, err)

for _, opt := range opts {
opt(&def)
}

def, err = def.SetDefinitionHashes()
"0x00000000", ops, random, opts...)
require.NoError(t, err)

// Definition version prior to v1.3.0 don't support EIP712 signatures.
Expand All @@ -132,6 +125,10 @@ func NewForT(t *testing.T, dv, k, n, seed int, opts ...func(*Definition)) (Lock,
def.Operators[i], err = signOperator(p2pKeys[i], def, def.Operators[i])
require.NoError(t, err)
}

// Recalculate definition hash after adding signatures.
def, err = def.SetDefinitionHashes()
require.NoError(t, err)
}

lock := Lock{
Expand Down
30 changes: 30 additions & 0 deletions cluster/test_cluster_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright © 2022 Obol Labs Inc.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <http://www.gnu.org/licenses/>.

package cluster_test

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/obolnetwork/charon/cluster"
)

func TestNewCluster(t *testing.T) {
lock, _, _ := cluster.NewForT(t, 3, 3, 3, 0)
require.NoError(t, lock.VerifyHashes())
require.NoError(t, lock.VerifySignatures())
}
6 changes: 3 additions & 3 deletions cluster/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ package cluster
import "testing"

const (
currentVersion = v1_2
currentVersion = v1_3
dkgAlgo = "default"

v1_3 = "v1.3.0" // Draft
v1_2 = "v1.2.0" // Default
v1_3 = "v1.3.0" // Default
v1_2 = "v1.2.0"
v1_1 = "v1.1.0"
v1_0 = "v1.0.0"

Expand Down
2 changes: 1 addition & 1 deletion cmd/createcluster_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func testCreateCluster(t *testing.T, conf clusterConfig) {

var lock cluster.Lock
require.NoError(t, json.Unmarshal(b, &lock))

require.NoError(t, lock.VerifyHashes())
require.NoError(t, lock.VerifySignatures())
})
}
Expand Down
14 changes: 10 additions & 4 deletions cmd/createdkg.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,14 +141,20 @@ func runCreateDKG(ctx context.Context, conf createDKGConfig) (err error) {
return err
}

def, err := cluster.NewDefinition(conf.Name, conf.NumValidators, conf.Threshold, conf.FeeRecipient, conf.WithdrawalAddress,
forkVersion, operators, crand.Reader)
def, err := cluster.NewDefinition(
conf.Name, conf.NumValidators, conf.Threshold,
conf.FeeRecipient, conf.WithdrawalAddress,
forkVersion, operators, crand.Reader,
func(d *cluster.Definition) {
d.DKGAlgorithm = conf.DKGAlgo
})
if err != nil {
return err
}

def.DKGAlgorithm = conf.DKGAlgo

if err := def.VerifyHashes(); err != nil {
return err
}
if err := def.VerifySignatures(); err != nil {
return err
}
Expand Down
45 changes: 23 additions & 22 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,26 @@ The `charon create cluster` command combines both steps into one and just output
The schema of the `cluster-definition.json` is defined as:
```json
{
"name": "best cluster", // Optional cosmetic identifier
"operators": [
"name": "best cluster", // Cosmetic cluster name
"operators": [ // Operators of all Charon nodes in the cluster
{
"address": "0x123..abfc", // ETH1 address of the operator
"enr": "enr://abcdef...12345", // Charon node ENR
"config_signature": "0x123456...abcdef", // EIP712 Signature of config_hash by ETH1 address priv key
"enr_signature": "0x123654...abcedf" // EIP712 Signature of ENR by ETH1 address priv key
"config_signature": "0x123456...abcdef", // EIP712 Signature of config_hash by ETH1 address. Proves that the operator accepts the config.
"enr_signature": "0x123654...abcedf" // EIP712 Signature of ENR by ETH1 address. Allows this ENR to act on behalf of the operator.
}
],
"uuid": "1234-abcdef-1234-abcdef", // Random unique identifier.
"version": "v1.2.0", // Schema version
"version": "v1.3.0", // Schema version
"timestamp": "2022-01-01T12:00:00+00:00", // Creation timestamp
"num_validators": 100, // Number of distributed validators to be created in cluster.lock
"threshold": 3, // Optional threshold required for signature reconstruction
"fee_recipient_address":"0x123..abfc", // ETH1 fee_recipient address
"num_validators": 100, // Number of distributed validators (n*32ETH staked) to be created in cluster.lock
"threshold": 3, // Threshold required for signature reconstruction
"fee_recipient_address":"0x123..abfc", // ETH1 fee recipient address
"withdrawal_address": "0x123..abfc", // ETH1 withdrawal address
"dkg_algorithm": "foo_dkg_v1" , // Optional DKG algorithm for key generation
"fork_version": "0x00112233", // Chain/Network identifier
"config_hash": "0xabcfde...acbfed", // Hash of the static (non-changing) fields
"definition_hash": "0xabcdef...abcedef" // Final hash of all fields
"dkg_algorithm": "foo_dkg_v1" , // DKG algorithm for key generation
"fork_version": "0x00112233", // Chain/network identifier
"config_hash": "0xabcfde...acbfed", // Hash of the initial configuration fields excluding operator ENRs and signatures
"definition_hash": "0xabcdef...abcedef" // Final hash of all fields (after all operators have added ENRs and signatures)
}
```

Expand All @@ -45,21 +45,21 @@ config hash and definition hash are calculated.
The operator `config_signature` and `enr_signature` are [EIP712](https://eips.ethereum.org/EIPS/eip-712) signatures of **typed structured data** as opposed to just raw bytes. EIP712 enables users to see the object that they are signing in their wallet (ex: Metamask).
See [eip712sigs.go](../cluster/eip712sigs.go) for details on the EIP712 structure used to create these signatures.

The above `cluster-definition.json` is provided as input to the DKG which generates keys and the `cluster-lock.json` file.
The above `cluster-definition.json` is generated by the [dv-launchpad](https://launchpad.obol.dev/) provided as input to the `charon dkg` command which generates keys and the `cluster-lock.json` file.

The `cluster-lock.json` has the following schema:
```json
{
"cluster_definition": {...}, // Cluster definiition json, identical schema to above,
"distributed_validators": [ // Length equal to num_validators.
"distributed_validators": [ // Length equal to num_validators (n*32ETH staked).
{
"distributed_public_key": "0x123..abfc", // DV root pubkey
"public_shares": [ "0x123..abfc", "0x123..abfc"], // length of num_operators
"fee_recipient": "0x123..abfc" // Defaults to withdrawal address if not set, can be edited manually
"public_shares": [ "0x123..abfc", "0x123..abfc"], // The public share of each operator (length of num_operators)
"fee_recipient": "0x123..abfc" // Fee recipient address of this validator. Defaults to definition fee_recipient if empty.
}
],
"lock_hash": "0xabcdef...abcedef", // Config_hash plus distributed_validators
"signature_aggregate": "0xabcdef...abcedef" // BLS aggregate signature of the lock hash signed by all the public shares of all the distributed validators.
"lock_hash": "0xabcdef...abcedef", // Hash of the cluster definition and distributed validators. Uniquely identifies a cluster lock.
"signature_aggregate": "0xabcdef...abcedef" // BLS aggregate signature of the lock hash signed by all the key shares of all the distributed validators. Proves that the key shares exist and attested to being part of this cluster.
}
```

Expand All @@ -68,14 +68,15 @@ The `cluster-lock.json` has the following schema:
### Cluster Config Change Log

The following is the historical change log of the cluster config:
- `v1.3.0` **draft**:
- Refactored hash calculations by aligning with SSZ common types:
- `v1.3.0` **default**:
- Refactored `config_hash`, `definition_hash` and `lock_hash` calculations by aligning with SSZ common types:
- `ByteList[MaxN]`: Variable length with max limit for strings.
- `BytesN`: Fixed length byte.
- `Uint64`: numbers.
- Refactored definition operator signatures: `config_signature` and `enr_signature` to use updated EIP712 digest.
- Refactored definition operator signatures: `config_signature` and `enr_signature` to use updated EIP712 structured types.
- This version is compatible with [dv-launchpad](https://github.com/ObolNetwork/dv-launchpad) generated `cluster-definition.json`.
- See example [definition.json](../cluster/testdata/cluster_definition_v1_3_0.json) and [lock.json](../cluster/testdata/cluster_lock_v1_3_0.json)
- `v1.2.0` **default**:
- `v1.2.0`:
- Refactored all base64 fields to Ethereum's standard 0x prefixed hex.
- Refactored definition operator signatures: `config_signature` and `enr_signature`.
- Refactored definition fields: `config_hash` and `definition_hash`.
Expand Down

0 comments on commit b5420b9

Please sign in to comment.