Skip to content

Commit

Permalink
cluster/state: legacy lock json and transform (#2182)
Browse files Browse the repository at this point in the history
Add legacy lock serialisation and transform logic. 

category: feature
ticket: #1886
  • Loading branch information
corverroos authored May 12, 2023
1 parent 57408a7 commit 0ee68ed
Show file tree
Hide file tree
Showing 10 changed files with 508 additions and 3 deletions.
21 changes: 20 additions & 1 deletion cluster/state/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,24 @@ package state

// Cluster represents the state of a cluster after applying a sequence of mutations.
type Cluster struct {
// TODO(corver): Implement
Name string
Threshold int
DKGAlgorithm string
ForkVersion []byte
Operators []Operator
Validators []Validator
}

// Operator represents the operator of a node in the cluster.
type Operator struct {
Address string
ENR string
}

// Validator represents a validator in the cluster.
type Validator struct {
PubKey []byte
PubShares [][]byte
FeeRecipientAddress string
WithdrawalAddress string
}
58 changes: 58 additions & 0 deletions cluster/state/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,34 @@
package state

import (
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"testing"
"time"

k1 "github.com/decred/dcrd/dcrec/secp256k1/v4"
ssz "github.com/ferranbt/fastssz"

"github.com/obolnetwork/charon/app/errors"
"github.com/obolnetwork/charon/app/k1util"
)

// nowFunc is the time.Now function aliased for testing.
var nowFunc = time.Now

// SetNowFuncForT sets the time.Now function for the duration of the test.
func SetNowFuncForT(t *testing.T, f func() time.Time) {
t.Helper()
cached := nowFunc
t.Cleanup(func() {
nowFunc = cached
})

nowFunc = f
}

// hashRoot hashes a ssz root hasher object.
func hashRoot(hasher rootHasher) ([32]byte, error) {
hw := ssz.DefaultHasherPool.Get()
Expand Down Expand Up @@ -66,3 +87,40 @@ func verifyK1SignedMutation(signed SignedMutation) error {

return nil
}

// ethHex represents a byte slice that is json formatted as 0x prefixed hex.
type ethHex []byte

func (h *ethHex) UnmarshalJSON(data []byte) error {
var strHex string
if err := json.Unmarshal(data, &strHex); err != nil {
return errors.Wrap(err, "unmarshal hex string")
}

resp, err := hex.DecodeString(strings.TrimPrefix(strHex, "0x"))
if err != nil {
return errors.Wrap(err, "unmarshal hex")
}

*h = resp

return nil
}

func (h ethHex) MarshalJSON() ([]byte, error) {
resp, err := json.Marshal(to0xHex(h))
if err != nil {
return nil, errors.Wrap(err, "marshal hex")
}

return resp, nil
}

// to0xHex returns the bytes as a 0x prefixed hex string.
func to0xHex(b []byte) string {
if len(b) == 0 {
return ""
}

return fmt.Sprintf("%#x", b)
}
29 changes: 29 additions & 0 deletions cluster/state/mutation.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,33 @@

package state

import (
"encoding/json"

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

// MutationType represents the type of a mutation.
type MutationType string

// Valid returns true if the mutation type is valid.
func (t MutationType) Valid() bool {
_, ok := mutationDefs[t]
return ok
}

// String returns the name of the mutation type.
func (t MutationType) String() string {
return string(t)
}

// Unmarshal returns a new unmarshalled mutation data from the input bytes.
func (t MutationType) Unmarshal(input []byte) (MutationData, error) {
return mutationDefs[t].UnmarshalFunc(input)
}

// Transform returns a transformed cluster state with the given mutation.
func (t MutationType) Transform(cluster Cluster, signed SignedMutation) (Cluster, error) {
// TODO(corver): Verify signature

Expand All @@ -23,9 +43,18 @@ const (
)

var mutationDefs = map[MutationType]struct {
UnmarshalFunc func(input []byte) (MutationData, error)
TransformFunc func(Cluster, SignedMutation) (Cluster, error)
}{
TypeLegacyLock: {
UnmarshalFunc: func(input []byte) (MutationData, error) {
var lock cluster.Lock
if err := json.Unmarshal(input, &lock); err != nil {
return nil, errors.Wrap(err, "unmarshal lock")
}

return lockWrapper{lock}, nil
},
TransformFunc: transformLegacyLock,
},
}
37 changes: 35 additions & 2 deletions cluster/state/mutationlegacylock.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ func verifyLegacyLock(signed SignedMutation) error {
return errors.New("invalid mutation type")
}

if _, ok := signed.Mutation.Data.(lockWrapper); !ok {
return errors.New("invalid mutation data")
}

if err := verifyEmptySig(signed); err != nil {
return errors.Wrap(err, "verify empty signature")
}
Expand All @@ -85,7 +89,36 @@ func transformLegacyLock(_ Cluster, signed SignedMutation) (Cluster, error) {
return Cluster{}, errors.Wrap(err, "verify legacy lock")
}

// TODO(corver): Implement legacy lock transform.
lock := signed.Mutation.Data.(lockWrapper) // Can just cast, already verified data is a lock

return Cluster{}, nil
var ops []Operator
for _, operator := range lock.Operators {
ops = append(ops, Operator{
Address: operator.Address,
ENR: operator.ENR,
})
}

if len(lock.ValidatorAddresses) != len(lock.Validators) {
return Cluster{}, errors.New("validator addresses and validators length mismatch")
}

var vals []Validator
for i, validator := range lock.Validators {
vals = append(vals, Validator{
PubKey: validator.PubKey,
PubShares: validator.PubShares,
FeeRecipientAddress: lock.ValidatorAddresses[i].FeeRecipientAddress,
WithdrawalAddress: lock.ValidatorAddresses[i].WithdrawalAddress,
})
}

return Cluster{
Name: lock.Name,
Threshold: lock.Threshold,
DKGAlgorithm: lock.DKGAlgorithm,
ForkVersion: lock.ForkVersion,
Validators: vals,
Operators: ops,
}, nil
}
48 changes: 48 additions & 0 deletions cluster/state/mutationlegacylock_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright © 2022-2023 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1

package state_test

import (
"encoding/json"
"os"
"testing"

"github.com/stretchr/testify/require"

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

//go:generate go test . -update

func TestLegacyLock(t *testing.T) {
lockJON, err := os.ReadFile("testdata/lock.json")
require.NoError(t, err)

var lock cluster.Lock
testutil.RequireNoError(t, json.Unmarshal(lockJON, &lock))

signed, err := state.NewLegacyLock(lock)
require.NoError(t, err)

t.Run("json", func(t *testing.T) {
testutil.RequireGoldenJSON(t, signed)
})

t.Run("cluster", func(t *testing.T) {
cluster, err := signed.Mutation.Type.Transform(state.Cluster{}, signed)
require.NoError(t, err)
testutil.RequireGoldenJSON(t, cluster)
})

b, err := json.MarshalIndent(signed, "", " ")
require.NoError(t, err)

var signed2 state.SignedMutation
testutil.RequireNoError(t, json.Unmarshal(b, &signed2))

t.Run("json again", func(t *testing.T) {
testutil.RequireGoldenJSON(t, signed2, testutil.WithFilename("TestLegacyLock_json.golden"))
})
}
34 changes: 34 additions & 0 deletions cluster/state/ssz_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions cluster/state/testdata/TestLegacyLock_cluster.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"Name": "test cluster",
"Threshold": 3,
"DKGAlgorithm": "default",
"ForkVersion": "AAAQIA==",
"Operators": [
{
"Address": "0x5050A4F4b3f9338C3472dcC01A87C76A144b3c9c",
"ENR": "enr:-HW4QIHPUOMb34YoizKGhz7nsDNQ7hCaiuwyscmeaOQ04awdH05gDnGrZhxDfzcfHssCDeB-esi99A2RoZia6UaYBCuAgmlkgnY0iXNlY3AyNTZrMaECTUts0TYQMsqb0q652QCqTUXZ6tgKyUIzdMRRpyVNB2Y"
},
{
"Address": "0x3325a78425F17a7E487Eb5666b2bFd93aBb06c70",
"ENR": "enr:-HW4QDztNDqgEPAgJoHkcF4LfXyjXUo1r_xYoNv48H0PFItwYx-OnviqgfHxEz51RDOGvUMiTpXyo0HBjK5ZZ8YxS9WAgmlkgnY0iXNlY3AyNTZrMaECUx_mBoE0UD0nIxMyJ8hnrI-myDxTfppEw8W9vcsf4zc"
},
{
"Address": "0xc48B812bB43401392c037381AcA934F4069C0517",
"ENR": "enr:-HW4QGSS-HN3zRfCJGISFmDT59Cpo-daC4U2vSjqPZWegHVSJklFsDs0f1fF_E7X4q8NUbR3bWDlX7IifsjQ_Xrm7QuAgmlkgnY0iXNlY3AyNTZrMaEDRid5rUqtOVFGFHUacQhfLxDhx6WT5OAw77W4chzlWws"
},
{
"Address": "0xd09Ad14080d4b257a819a4f579b8485Be88f086c",
"ENR": "enr:-HW4QGFxPElPQZLydQ9Ach--g-jHJ0N4LO6uuIvyfw-Tg2K_R-R6iMCfzGryG80gmdPQwz9asajtn3CF88-rpu38YoKAgmlkgnY0iXNlY3AyNTZrMaEDYsCgRtrM6G3dA0PG08fHnCIIug2cnPJKbQRtIdIfkPc"
}
],
"Validators": [
{
"PubKey": "o4nJ3v/2qlWFp2YNVor4JRQ7ytkRk36qbuIWfSJuEMIMjixY+q9f1ddJh5hErNcG",
"PubShares": [
"ifSPWSihJJpTvdqoofhUO9JZM9myk1/5qiiy+Tc8J4AbRPRGNwfBfFfn/Lt8Yk5Z",
"mer0HxEq9eL5/zSAMf0p/TvU9ww/lFzC1leR2RdFa9PrVFPtBZUPFcHP6AYjByOe",
"s3D3UnDkGWDvETZrOEf1DHhgB31vZGPf7PlTW7jpL+sRSdmQbg4XAa1MGTxBrioK",
"kAP3hEt7fLeRpLUi7TJngPArg+7wyf8HMDAV0o/CKcHXiw/REnwh71QfIk6+ZTqU"
],
"FeeRecipientAddress": "0x52fdfc072182654f163f5f0f9a621d729566c74d",
"WithdrawalAddress": "0x81855ad8681d0d86d1e91e00167939cb6694d2c4"
}
]
}
Loading

0 comments on commit 0ee68ed

Please sign in to comment.