diff --git a/app/app.go b/app/app.go index d5f11e83bd..e6a206183f 100644 --- a/app/app.go +++ b/app/app.go @@ -8,7 +8,6 @@ import ( "bytes" "context" "encoding/hex" - "encoding/json" "net/http" "strings" "sync" @@ -42,9 +41,7 @@ import ( "github.com/obolnetwork/charon/app/tracer" "github.com/obolnetwork/charon/app/version" "github.com/obolnetwork/charon/app/z" - clusterpkg "github.com/obolnetwork/charon/cluster" - "github.com/obolnetwork/charon/cluster/manifest" - manifestpb "github.com/obolnetwork/charon/cluster/manifestpb/v1" + "github.com/obolnetwork/charon/cluster" "github.com/obolnetwork/charon/core" "github.com/obolnetwork/charon/core/aggsigdb" "github.com/obolnetwork/charon/core/bcast" @@ -73,8 +70,8 @@ type Config struct { P2P p2p.Config Log log.Config Feature featureset.Config - LockFile string ManifestFile string + LockFile string NoVerify bool PrivKeyFile string PrivKeyLocking bool @@ -118,7 +115,7 @@ type TestConfig struct { p2p.TestPingConfig // Lock provides the lock explicitly, skips loading from disk. - Lock *clusterpkg.Lock + Lock *cluster.Lock // P2PKey provides the p2p privkey explicitly, skips loading from keystore on disk. P2PKey *k1.PrivateKey // ParSigExFunc provides an in-memory partial signature exchange. @@ -177,19 +174,18 @@ func Run(ctx context.Context, conf Config) (err error) { eth1Cl := eth1wrap.NewDefaultEthClientRunner(conf.ExecutionEngineAddr) life.RegisterStart(lifecycle.AsyncAppCtx, lifecycle.StartEth1Client, lifecycle.HookFuncCtx(eth1Cl.Run)) - cluster, err := loadClusterManifest(ctx, conf, eth1Cl) + lock, err := loadClusterLock(ctx, conf, eth1Cl) if err != nil { return err } - clusterHash := cluster.GetInitialMutationHash() - core.SetClusterHash(clusterHash) + core.SetClusterHash(lock.LockHash) - if err := wireTracing(life, conf, clusterHash); err != nil { + if err := wireTracing(life, conf, lock.LockHash); err != nil { return err } - network, err := eth2util.ForkVersionToNetwork(cluster.GetForkVersion()) + network, err := eth2util.ForkVersionToNetwork(lock.ForkVersion) if err != nil { network = "unknown" } @@ -212,7 +208,7 @@ func Run(ctx context.Context, conf Config) (err error) { } } - peers, err := manifest.ClusterPeers(cluster) + peers, err := lock.Peers() if err != nil { return err } @@ -221,14 +217,14 @@ func Run(ctx context.Context, conf Config) (err error) { return err } - lockHashHex := Hex7(cluster.GetInitialMutationHash()) + lockHashHex := Hex7(lock.LockHash) - p2pNode, err := wireP2P(ctx, life, conf, cluster, p2pKey, lockHashHex) + p2pNode, err := wireP2P(ctx, life, conf, lock, p2pKey, lockHashHex, lock.UUID) if err != nil { return err } - nodeIdx, err := manifest.ClusterNodeIdx(cluster, p2pNode.ID()) + nodeIdx, err := lock.NodeIdx(p2pNode.ID()) if err != nil { return errors.Wrap(err, "private key not matching cluster manifest file") } @@ -242,16 +238,16 @@ func Run(ctx context.Context, conf Config) (err error) { z.Str("peer_name", p2p.PeerName(p2pNode.ID())), z.Str("nickname", conf.Nickname), z.Int("peer_index", nodeIdx.PeerIdx), - z.Str("cluster_name", cluster.GetName()), + z.Str("cluster_name", lock.Name), z.Str("cluster_hash", lockHashHex), - z.Str("cluster_hash_full", hex.EncodeToString(cluster.GetInitialMutationHash())), + z.Str("cluster_hash_full", hex.EncodeToString(lock.LockHash)), z.Str("enr", enrRec.String()), - z.Int("peers", len(cluster.GetOperators()))) + z.Int("peers", len(lock.Operators))) // Metric and logging labels. labels := map[string]string{ "cluster_hash": lockHashHex, - "cluster_name": cluster.GetName(), + "cluster_name": lock.Name, "cluster_peer": p2p.PeerName(p2pNode.ID()), "nickname": conf.Nickname, "cluster_network": network, @@ -264,9 +260,9 @@ func Run(ctx context.Context, conf Config) (err error) { return err } - initStartupMetrics(p2p.PeerName(p2pNode.ID()), int(cluster.GetThreshold()), len(cluster.GetOperators()), len(cluster.GetValidators()), network) + initStartupMetrics(p2p.PeerName(p2pNode.ID()), lock.Threshold, len(lock.Operators), len(lock.Validators), network) - eth2Cl, subEth2Cl, err := newETH2Client(ctx, conf, life, cluster, cluster.GetForkVersion(), conf.BeaconNodeTimeout, conf.BeaconNodeSubmitTimeout) + eth2Cl, subEth2Cl, err := newETH2Client(ctx, conf, life, lock, lock.ForkVersion, conf.BeaconNodeTimeout, conf.BeaconNodeSubmitTimeout) if err != nil { return err } @@ -276,7 +272,7 @@ func Run(ctx context.Context, conf Config) (err error) { return err } - peerIDs, err := manifest.ClusterPeerIDs(cluster) + peerIDs, err := lock.PeerIDs() if err != nil { return err } @@ -292,7 +288,7 @@ func Run(ctx context.Context, conf Config) (err error) { return errors.New("nickname cannot exceed 32 characters") } - wirePeerInfo(life, p2pNode, peerIDs, cluster.GetInitialMutationHash(), sender, conf.BuilderAPI, conf.Nickname) + wirePeerInfo(life, p2pNode, peerIDs, lock.LockHash, sender, conf.BuilderAPI, conf.Nickname) // seenPubkeys channel to send seen public keys from validatorapi to monitoringapi. seenPubkeys := make(chan core.PubKey) @@ -311,7 +307,7 @@ func Run(ctx context.Context, conf Config) (err error) { } } - pubkeys, err := getDVPubkeys(cluster) + pubkeys, err := getDVPubkeys(lock) if err != nil { return err } @@ -319,9 +315,9 @@ func Run(ctx context.Context, conf Config) (err error) { consensusDebugger := consensus.NewDebugger() wireMonitoringAPI(ctx, life, conf.MonitoringAddr, conf.DebugAddr, p2pNode, eth2Cl, peerIDs, - promRegistry, consensusDebugger, pubkeys, seenPubkeys, vapiCalls, len(cluster.GetValidators())) + promRegistry, consensusDebugger, pubkeys, seenPubkeys, vapiCalls, len(lock.Validators)) - err = wireCoreWorkflow(ctx, life, conf, cluster, nodeIdx, p2pNode, p2pKey, eth2Cl, subEth2Cl, + err = wireCoreWorkflow(ctx, life, conf, lock, nodeIdx, p2pNode, p2pKey, eth2Cl, subEth2Cl, peerIDs, sender, consensusDebugger, pubkeys, seenPubkeysFunc, sseListener, vapiCallsFunc) if err != nil { return err @@ -340,14 +336,14 @@ func wirePeerInfo(life *lifecycle.Manager, p2pNode host.Host, peers []peer.ID, l // wireP2P constructs the p2p tcp or udp (libp2p) nodes and registers it with the life cycle manager. func wireP2P(ctx context.Context, life *lifecycle.Manager, conf Config, - cluster *manifestpb.Cluster, p2pKey *k1.PrivateKey, lockHashHex string, + lock *cluster.Lock, p2pKey *k1.PrivateKey, lockHashHex, uuid string, ) (host.Host, error) { - peerIDs, err := manifest.ClusterPeerIDs(cluster) + peerIDs, err := lock.PeerIDs() if err != nil { return nil, err } - relays, err := p2p.NewRelays(ctx, conf.P2P.Relays, lockHashHex) + relays, err := p2p.NewRelays(ctx, conf.P2P.Relays, lockHashHex, uuid) if err != nil { return nil, err } @@ -398,34 +394,30 @@ func wireP2P(ctx context.Context, life *lifecycle.Manager, conf Config, // wireCoreWorkflow wires the core workflow components. func wireCoreWorkflow(ctx context.Context, life *lifecycle.Manager, conf Config, - cluster *manifestpb.Cluster, nodeIdx clusterpkg.NodeIdx, p2pNode host.Host, p2pKey *k1.PrivateKey, + lock *cluster.Lock, nodeIdx cluster.NodeIdx, p2pNode host.Host, p2pKey *k1.PrivateKey, eth2Cl, submissionEth2Cl eth2wrap.Client, peerIDs []peer.ID, sender *p2p.Sender, consensusDebugger consensus.Debugger, pubkeys []core.PubKey, seenPubkeys func(core.PubKey), sseListener sse.Listener, vapiCalls func(), ) error { // Convert and prep public keys and public shares var ( - builderRegistrations []clusterpkg.BuilderRegistration + builderRegistrations []cluster.BuilderRegistration eth2Pubkeys []eth2p0.BLSPubKey pubshares []eth2p0.BLSPubKey allPubSharesByKey = make(map[core.PubKey]map[int]tbls.PublicKey) // map[pubkey]map[shareIdx]pubshare feeRecipientAddrByCorePubkey = make(map[core.PubKey]string) + lockFeeRecipientAddresses = lock.FeeRecipientAddresses() ) - for _, val := range cluster.GetValidators() { - pubkey, err := manifest.ValidatorPublicKey(val) - if err != nil { - return err - } - - corePubkey, err := core.PubKeyFromBytes(pubkey[:]) + for vi, val := range lock.Validators { + corePubkey, err := core.PubKeyFromBytes(val.PubKey) if err != nil { return err } allPubShares := make(map[int]tbls.PublicKey) - for i, b := range val.GetPubShares() { + for i, b := range val.PubShares { pubshare, err := tblsconv.PubkeyFromBytes(b) if err != nil { return err @@ -435,29 +427,24 @@ func wireCoreWorkflow(ctx context.Context, life *lifecycle.Manager, conf Config, allPubShares[i+1] = pubshare } - pubShare, err := manifest.ValidatorPublicShare(val, nodeIdx.PeerIdx) + pubShare, err := val.PublicShare(nodeIdx.PeerIdx) if err != nil { return err } eth2Share := eth2p0.BLSPubKey(pubShare) - eth2Pubkey := eth2p0.BLSPubKey(pubkey) + eth2Pubkey := eth2p0.BLSPubKey(val.PubKey) eth2Pubkeys = append(eth2Pubkeys, eth2Pubkey) pubshares = append(pubshares, eth2Share) allPubSharesByKey[corePubkey] = allPubShares - feeRecipientAddrByCorePubkey[corePubkey] = val.GetFeeRecipientAddress() - - var builderRegistration clusterpkg.BuilderRegistration - if err := json.Unmarshal(val.GetBuilderRegistrationJson(), &builderRegistration); err != nil { - return errors.Wrap(err, "unmarshal builder registration") - } + feeRecipientAddrByCorePubkey[corePubkey] = lockFeeRecipientAddresses[vi] - builderRegistrations = append(builderRegistrations, builderRegistration) + builderRegistrations = append(builderRegistrations, val.BuilderRegistration) } - peers, err := manifest.ClusterPeers(cluster) + peers, err := lock.Peers() if err != nil { return err } @@ -566,7 +553,7 @@ func wireCoreWorkflow(ctx context.Context, life *lifecycle.Manager, conf Config, dutyDB := dutydb.NewMemDB(deadlinerFunc("dutydb")) - vapi, err := validatorapi.NewComponent(eth2Cl, allPubSharesByKey, nodeIdx.ShareIdx, feeRecipientFunc, conf.BuilderAPI, uint(cluster.GetTargetGasLimit()), seenPubkeys) + vapi, err := validatorapi.NewComponent(eth2Cl, allPubSharesByKey, nodeIdx.ShareIdx, feeRecipientFunc, conf.BuilderAPI, lock.TargetGasLimit, seenPubkeys) if err != nil { return err } @@ -575,7 +562,7 @@ func wireCoreWorkflow(ctx context.Context, life *lifecycle.Manager, conf Config, return err } - parSigDB := parsigdb.NewMemDB(int(cluster.GetThreshold()), deadlinerFunc("parsigdb")) + parSigDB := parsigdb.NewMemDB(lock.Threshold, deadlinerFunc("parsigdb")) var parSigEx core.ParSigEx if conf.TestConfig.ParSigExFunc != nil { @@ -589,7 +576,7 @@ func wireCoreWorkflow(ctx context.Context, life *lifecycle.Manager, conf Config, parSigEx = parsigex.NewParSigEx(p2pNode, sender.SendAsync, nodeIdx.PeerIdx, peerIDs, verifyFunc, gaterFunc) } - sigAgg, err := sigagg.New(int(cluster.GetThreshold()), sigagg.NewVerifier(eth2Cl)) + sigAgg, err := sigagg.New(lock.Threshold, sigagg.NewVerifier(eth2Cl)) if err != nil { return err } @@ -624,9 +611,9 @@ func wireCoreWorkflow(ctx context.Context, life *lifecycle.Manager, conf Config, coreConsensus := consensusController.CurrentConsensus() // initially points to DefaultConsensus() // Priority protocol always uses QBFTv2. - err = wirePrioritise(ctx, conf, life, p2pNode, peerIDs, int(cluster.GetThreshold()), + err = wirePrioritise(ctx, conf, life, p2pNode, peerIDs, lock.Threshold, sender.SendReceive, defaultConsensus, sched, p2pKey, deadlineFunc, - consensusController, cluster.GetConsensusProtocol()) + consensusController, lock.ConsensusProtocol) if err != nil { return err } @@ -812,29 +799,21 @@ func calculateTrackerDelay(ctx context.Context, cl eth2wrap.Client, now time.Tim } // eth2PubKeys returns a list of BLS pubkeys of validators in the cluster lock. -func eth2PubKeys(cluster *manifestpb.Cluster) ([]eth2p0.BLSPubKey, error) { +func eth2PubKeys(lock *cluster.Lock) []eth2p0.BLSPubKey { var pubkeys []eth2p0.BLSPubKey - for _, val := range cluster.GetValidators() { - pubkey, err := manifest.ValidatorPublicKey(val) - if err != nil { - return []eth2p0.BLSPubKey{}, err - } - - pk := eth2p0.BLSPubKey(pubkey) + for _, dv := range lock.Validators { + pk := eth2p0.BLSPubKey(dv.PubKey) pubkeys = append(pubkeys, pk) } - return pubkeys, nil + return pubkeys } // newETH2Client returns a new eth2client for the configured timeouts; it is either a beaconmock for // simnet or a multi http client to a real beacon node. -func newETH2Client(ctx context.Context, conf Config, life *lifecycle.Manager, cluster *manifestpb.Cluster, forkVersion []byte, bnTimeout time.Duration, submissionBnTimeout time.Duration) (eth2Cl eth2wrap.Client, submissionEth2Cl eth2wrap.Client, err error) { - pubkeys, err := eth2PubKeys(cluster) - if err != nil { - return nil, nil, err - } +func newETH2Client(ctx context.Context, conf Config, life *lifecycle.Manager, lock *cluster.Lock, forkVersion []byte, bnTimeout time.Duration, submissionBnTimeout time.Duration) (eth2Cl eth2wrap.Client, submissionEth2Cl eth2wrap.Client, err error) { + pubkeys := eth2PubKeys(lock) // Default to 1s slot duration if not set. if conf.SimnetSlotDuration == 0 { @@ -1124,16 +1103,11 @@ func setFeeRecipient(eth2Cl eth2wrap.Client, feeRecipientFunc func(core.PubKey) } // getDVPubkeys returns DV public keys from given cluster.Lock. -func getDVPubkeys(cluster *manifestpb.Cluster) ([]core.PubKey, error) { +func getDVPubkeys(lock *cluster.Lock) ([]core.PubKey, error) { var pubkeys []core.PubKey - for _, val := range cluster.GetValidators() { - pk, err := manifest.ValidatorPublicKey(val) - if err != nil { - return nil, err - } - - pubkey, err := core.PubKeyFromBytes(pk[:]) + for _, dv := range lock.Validators { + pubkey, err := core.PubKeyFromBytes(dv.PubKey) if err != nil { return nil, err } diff --git a/app/disk.go b/app/disk.go index 44f3c19418..4227d3bc0e 100644 --- a/app/disk.go +++ b/app/disk.go @@ -9,49 +9,17 @@ import ( "github.com/obolnetwork/charon/app/errors" "github.com/obolnetwork/charon/app/eth1wrap" - "github.com/obolnetwork/charon/app/log" "github.com/obolnetwork/charon/app/z" "github.com/obolnetwork/charon/cluster" - "github.com/obolnetwork/charon/cluster/manifest" - manifestpb "github.com/obolnetwork/charon/cluster/manifestpb/v1" ) -// loadClusterManifest returns the cluster manifest from the given file path. -func loadClusterManifest(ctx context.Context, conf Config, eth1Cl eth1wrap.EthClientRunner) (*manifestpb.Cluster, error) { +// loadClusterLock loads and verifies the cluster lock. +func loadClusterLock(ctx context.Context, conf Config, eth1Cl eth1wrap.EthClientRunner) (*cluster.Lock, error) { if conf.TestConfig.Lock != nil { - return manifest.NewClusterFromLockForT(nil, *conf.TestConfig.Lock) + return conf.TestConfig.Lock, nil } - verifyLock := func(lock cluster.Lock) error { - if conf.NoVerify { - if err := lock.VerifyHashes(); err != nil { - log.Warn(ctx, "Ignoring failed cluster lock hash verification due to --no-verify flag", err) - } - - if err := lock.VerifySignatures(eth1Cl); err != nil { - log.Warn(ctx, "Ignoring failed cluster lock signature verification due to --no-verify flag", err) - } - - return nil - } - - if err := lock.VerifyHashes(); err != nil { - return errors.Wrap(err, "cluster lock hash verification failed. Run with --no-verify to bypass verification at own risk") - } - - if err := lock.VerifySignatures(eth1Cl); err != nil { - return errors.Wrap(err, "cluster lock signature verification failed. Run with --no-verify to bypass verification at own risk") - } - - return nil - } - - cluster, err := manifest.LoadCluster(conf.ManifestFile, conf.LockFile, verifyLock) - if err != nil { - return nil, errors.Wrap(err, "load cluster manifest") - } - - return cluster, nil + return cluster.LoadClusterLock(ctx, conf.LockFile, conf.NoVerify, eth1Cl) } // FileExists checks if a file exists at the given path. diff --git a/app/disk_internal_test.go b/app/disk_internal_test.go index afdd3ca0c9..8d07e21252 100644 --- a/app/disk_internal_test.go +++ b/app/disk_internal_test.go @@ -21,10 +21,10 @@ func TestLoadClusterManifest(t *testing.T) { eth1Cl := eth1wrap.NewDefaultEthClientRunner("") go eth1Cl.Run(t.Context()) - cluster, err := loadClusterManifest(t.Context(), conf, eth1Cl) + cluster, err := loadClusterLock(t.Context(), conf, eth1Cl) require.NoError(t, err) require.NotNil(t, cluster) - require.Len(t, cluster.GetValidators(), 2) + require.Len(t, cluster.Validators, 2) } func TestFileExists(t *testing.T) { diff --git a/app/protonil/protonil_test.go b/app/protonil/protonil_test.go index 8043e3fc7f..1194736267 100644 --- a/app/protonil/protonil_test.go +++ b/app/protonil/protonil_test.go @@ -12,7 +12,6 @@ import ( "github.com/obolnetwork/charon/app/protonil" v1 "github.com/obolnetwork/charon/app/protonil/testdata/v1" - manifestpb "github.com/obolnetwork/charon/cluster/manifestpb/v1" corepb "github.com/obolnetwork/charon/core/corepb/v1" "github.com/obolnetwork/charon/testutil" ) @@ -136,10 +135,6 @@ func TestFuzz(t *testing.T) { new(v1.M2), new(v1.M3), new(v1.M4), - new(manifestpb.Cluster), - new(manifestpb.SignedMutation), - new(manifestpb.SignedMutationList), - new(manifestpb.LegacyLock), new(corepb.QBFTMsg), new(corepb.PriorityScoredResult), new(corepb.SniffedConsensusInstance), diff --git a/cluster/load.go b/cluster/load.go new file mode 100644 index 0000000000..bb6ec898b1 --- /dev/null +++ b/cluster/load.go @@ -0,0 +1,52 @@ +// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package cluster + +import ( + "context" + "encoding/json" + "os" + + "github.com/obolnetwork/charon/app/errors" + "github.com/obolnetwork/charon/app/eth1wrap" + "github.com/obolnetwork/charon/app/log" + "github.com/obolnetwork/charon/app/z" +) + +// LoadClusterLockAndVerify loads and verifies the cluster lock. Suitable for cmd tools. +func LoadClusterLockAndVerify(ctx context.Context, lockFilePath string) (*Lock, error) { + eth1Cl := eth1wrap.NewDefaultEthClientRunner("") + go eth1Cl.Run(ctx) + + return LoadClusterLock(ctx, lockFilePath, false, eth1Cl) +} + +// LoadClusterLock loads and verifies the cluster lock. +func LoadClusterLock(ctx context.Context, lockFilePath string, noVerify bool, eth1Cl eth1wrap.EthClientRunner) (*Lock, error) { + b, err := os.ReadFile(lockFilePath) + if err != nil { + return nil, errors.Wrap(err, "read cluster-lock.json", z.Str("path", lockFilePath)) + } + + var lock Lock + if err := json.Unmarshal(b, &lock); err != nil { + return nil, errors.Wrap(err, "unmarshal cluster-lock.json", z.Str("path", lockFilePath)) + } + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + if err := lock.VerifyHashes(); err != nil && !noVerify { + return nil, errors.Wrap(err, "verify cluster lock hashes (run with --no-verify to bypass verification at own risk)") + } else if err != nil && noVerify { + log.Warn(ctx, "Ignoring failed cluster lock hashes verification due to --no-verify flag", err) + } + + if err := lock.VerifySignatures(eth1Cl); err != nil && !noVerify { + return nil, errors.Wrap(err, "verify cluster lock signatures (run with --no-verify to bypass verification at own risk)") + } else if err != nil && noVerify { + log.Warn(ctx, "Ignoring failed cluster lock signature verification due to --no-verify flag", err) + } + + return &lock, nil +} diff --git a/cluster/load_test.go b/cluster/load_test.go new file mode 100644 index 0000000000..703e48781b --- /dev/null +++ b/cluster/load_test.go @@ -0,0 +1,37 @@ +// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package cluster_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/obolnetwork/charon/cluster" +) + +func TestLoadClusterLock(t *testing.T) { + ctx := t.Context() + + // Load the test lock file with noVerify=true to skip signature verification + lock, err := cluster.LoadClusterLock(ctx, "testdata/cluster_lock_v1_10_0.json", true, nil) + require.NoError(t, err) + require.NotNil(t, lock) + + // Verify basic fields from the test file + require.Equal(t, "test definition", lock.Name) + require.Equal(t, "v1.10.0", lock.Version) + require.Equal(t, "0194FDC2-FA2F-4CC0-81D3-FF12045B73C8", lock.UUID) + require.Equal(t, 2, lock.NumValidators) + require.Equal(t, 3, lock.Threshold) + require.Len(t, lock.Operators, 2) + require.Len(t, lock.Validators, 2) + + // Verify first operator + require.Equal(t, "0xe0255aa5b7d44bec40f84c892b9bffd43629b022", lock.Operators[0].Address) + require.Contains(t, lock.Operators[0].ENR, "enr:-HW4QNHE5aL5CrfBtTF9Hi2VrssawbiC4xuTUzuk0Mu3wRQSL2mA") + + // Verify first distributed validator + require.Equal(t, "0x6865fcf92b0c3a17c9028be9914eb7649c6c9347800979d1830356f2a54c3deab2a4b4475d63afbe8fb56987c77f5818", lock.Validators[0].PublicKeyHex()) + require.Len(t, lock.Validators[0].PubShares, 2) +} diff --git a/cluster/manifest/cluster.go b/cluster/manifest/cluster.go deleted file mode 100644 index d967da721f..0000000000 --- a/cluster/manifest/cluster.go +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 - -package manifest - -import ( - "github.com/libp2p/go-libp2p/core/peer" - - "github.com/obolnetwork/charon/app/errors" - "github.com/obolnetwork/charon/app/z" - "github.com/obolnetwork/charon/cluster" - manifestpb "github.com/obolnetwork/charon/cluster/manifestpb/v1" - "github.com/obolnetwork/charon/eth2util/enr" - "github.com/obolnetwork/charon/p2p" - "github.com/obolnetwork/charon/tbls" - "github.com/obolnetwork/charon/tbls/tblsconv" -) - -// ClusterPeers returns the cluster operators as a slice of p2p peers. -func ClusterPeers(c *manifestpb.Cluster) ([]p2p.Peer, error) { - if c == nil || len(c.GetOperators()) == 0 { - return nil, errors.New("invalid cluster") - } - - var resp []p2p.Peer - - dedup := make(map[string]bool) - for i, operator := range c.GetOperators() { - if dedup[operator.GetEnr()] { - return nil, errors.New("cluster contains duplicate peer enrs", z.Str("enr", operator.GetEnr())) - } - - dedup[operator.GetEnr()] = true - - record, err := enr.Parse(operator.GetEnr()) - if err != nil { - return nil, errors.Wrap(err, "decode enr", z.Str("enr", operator.GetEnr())) - } - - p, err := p2p.NewPeerFromENR(record, i) - if err != nil { - return nil, err - } - - resp = append(resp, p) - } - - return resp, nil -} - -// ClusterPeerIDs is a convenience function that returns the operators p2p peer IDs. -func ClusterPeerIDs(c *manifestpb.Cluster) ([]peer.ID, error) { - peers, err := ClusterPeers(c) - if err != nil { - return nil, err - } - - var resp []peer.ID - for _, p := range peers { - resp = append(resp, p.ID) - } - - return resp, nil -} - -// ClusterNodeIdx returns the node index for the peer in the cluster. -func ClusterNodeIdx(c *manifestpb.Cluster, pID peer.ID) (cluster.NodeIdx, error) { - peers, err := ClusterPeers(c) - if err != nil { - return cluster.NodeIdx{}, err - } - - for i, p := range peers { - if p.ID != pID { - continue - } - - return cluster.NodeIdx{ - PeerIdx: i, // 0-indexed - ShareIdx: i + 1, // 1-indexed - }, nil - } - - return cluster.NodeIdx{}, errors.New("peer not in definition") -} - -// ValidatorPublicKey returns the validator BLS group public key. -func ValidatorPublicKey(v *manifestpb.Validator) (tbls.PublicKey, error) { - return tblsconv.PubkeyFromBytes(v.GetPublicKey()) -} - -// ValidatorPublicKeyHex returns the validator hex group public key. -func ValidatorPublicKeyHex(v *manifestpb.Validator) string { - return to0xHex(v.GetPublicKey()) -} - -// ValidatorPublicShare returns the validator's peerIdx'th BLS public share. -func ValidatorPublicShare(v *manifestpb.Validator, peerIdx int) (tbls.PublicKey, error) { - return tblsconv.PubkeyFromBytes(v.GetPubShares()[peerIdx]) -} diff --git a/cluster/manifest/cluster_test.go b/cluster/manifest/cluster_test.go deleted file mode 100644 index aa8c101759..0000000000 --- a/cluster/manifest/cluster_test.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 - -package manifest_test - -import ( - "math/rand" - "slices" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/obolnetwork/charon/cluster" - "github.com/obolnetwork/charon/cluster/manifest" - manifestpb "github.com/obolnetwork/charon/cluster/manifestpb/v1" -) - -const ( - v1_10 = "v1.10.0" - v1_9 = "v1.9.0" - v1_8 = "v1.8.0" - v1_7 = "v1.7.0" - v1_6 = "v1.6.0" - v1_5 = "v1.5.0" - v1_4 = "v1.4.0" - v1_3 = "v1.3.0" - v1_2 = "v1.2.0" - v1_1 = "v1.1.0" - v1_0 = "v1.0.0" -) - -func isAnyVersion(version string, list ...string) bool { - return slices.Contains(list, version) -} - -func TestDuplicateENRs(t *testing.T) { - seed := 0 - random := rand.New(rand.NewSource(int64(seed))) - lock, _, _ := cluster.NewForT(t, 1, 3, 4, seed, random) - - _, err := manifest.ClusterPeers(&manifestpb.Cluster{Operators: []*manifestpb.Operator{ - {Enr: lock.Operators[0].ENR}, - {Enr: lock.Operators[0].ENR}, - }}) - require.ErrorContains(t, err, "duplicate peer enrs") -} diff --git a/cluster/manifest/helpers.go b/cluster/manifest/helpers.go deleted file mode 100644 index 398ad8aa58..0000000000 --- a/cluster/manifest/helpers.go +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 - -package manifest - -import ( - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "strings" - "testing" - - k1 "github.com/decred/dcrd/dcrec/secp256k1/v4" - "google.golang.org/protobuf/types/known/timestamppb" - - "github.com/obolnetwork/charon/app/errors" - "github.com/obolnetwork/charon/app/k1util" - "github.com/obolnetwork/charon/app/z" - "github.com/obolnetwork/charon/cluster" - manifestpb "github.com/obolnetwork/charon/cluster/manifestpb/v1" -) - -// hashLen is the length of a hash. -const hashLen = 32 - -// nowFunc is the time.Now function aliased for testing. -var nowFunc = timestamppb.Now - -// SetNowFuncForT sets the time.Now function for the duration of the test. -func SetNowFuncForT(t *testing.T, f func() *timestamppb.Timestamp) { - t.Helper() - - cached := nowFunc - - t.Cleanup(func() { - nowFunc = cached - }) - - nowFunc = f -} - -// hashSignedMutation returns the hash of a signed mutation. -func hashSignedMutation(signed *manifestpb.SignedMutation) ([]byte, error) { - if signed.GetMutation() == nil { - return nil, errors.New("invalid signed mutation") - } - - h := sha256.New() - - // Field 0: Mutation - b, err := hashMutation(signed.GetMutation()) - if err != nil { - return nil, errors.Wrap(err, "hash mutation") - } - - if _, err := h.Write(b); err != nil { - return nil, errors.Wrap(err, "hash mutation") - } - - // Field 1: Signer - if _, err := h.Write(signed.GetSigner()); err != nil { - return nil, errors.Wrap(err, "hash signer") - } - - // Field 2: Signature - if _, err := h.Write(signed.GetSignature()); err != nil { - return nil, errors.Wrap(err, "hash signature") - } - - return h.Sum(nil), nil -} - -// hashMutation returns the hash of a mutation. -func hashMutation(m *manifestpb.Mutation) ([]byte, error) { - if m.GetData() == nil { - return nil, errors.New("invalid mutation") - } - - h := sha256.New() - - // Field 0: Parent - if _, err := h.Write(m.GetParent()); err != nil { - return nil, errors.Wrap(err, "hash parent") - } - - // Field 1: Type - if _, err := h.Write([]byte(m.GetType())); err != nil { - return nil, errors.Wrap(err, "hash type") - } - - // Field 2: Data - if _, err := h.Write([]byte(m.GetData().GetTypeUrl())); err != nil { - return nil, errors.Wrap(err, "hash data type url") - } - - if _, err := h.Write(m.GetData().GetValue()); err != nil { - return nil, errors.Wrap(err, "hash data value") - } - - return h.Sum(nil), nil -} - -// verifyEmptySig verifies that the signed mutation isn't signed. -func verifyEmptySig(signed *manifestpb.SignedMutation) error { - if len(signed.GetSignature()) != 0 { - return errors.New("non-empty signature") - } - - if len(signed.GetSigner()) != 0 { - return errors.New("non-empty signer") - } - - return nil -} - -// SignK1 signs the mutation with the provided k1 secret. -func SignK1(m *manifestpb.Mutation, secret *k1.PrivateKey) (*manifestpb.SignedMutation, error) { - hash, err := hashMutation(m) - if err != nil { - return nil, errors.Wrap(err, "hash mutation") - } - - sig, err := k1util.Sign(secret, hash) - if err != nil { - return nil, errors.Wrap(err, "sign mutation") - } - - return &manifestpb.SignedMutation{ - Mutation: m, - Signer: secret.PubKey().SerializeCompressed(), - Signature: sig, - }, nil -} - -// verifyK1SignedMutation verifies that the signed mutation is signed by a k1 key. -func verifyK1SignedMutation(signed *manifestpb.SignedMutation) error { - pubkey, err := k1.ParsePubKey(signed.GetSigner()) - if err != nil { - return errors.Wrap(err, "parse signer pubkey") - } - - hash, err := hashMutation(signed.GetMutation()) - if err != nil { - return errors.Wrap(err, "hash mutation") - } - - if ok, err := k1util.Verify65(pubkey, hash, signed.GetSignature()); err != nil { - return errors.Wrap(err, "verify signature") - } else if !ok { - return errors.New("invalid mutation signature") - } - - return 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) -} - -// from0xHex returns bytes represented by the hex string. -// nolint: unparam -func from0xHex(s string, length int) ([]byte, error) { - if s == "" { - return nil, nil - } - - b, err := hex.DecodeString(strings.TrimPrefix(s, "0x")) - if err != nil { - return nil, errors.Wrap(err, "decode hex") - } else if len(b) != length { - return nil, errors.Wrap(err, "invalid hex length", z.Int("expect", length), z.Int("actual", len(b))) - } - - return b, nil -} - -// ValidatorToProto converts a legacy cluster validator to a protobuf validator. -func ValidatorToProto(val cluster.DistValidator, addrs cluster.ValidatorAddresses) (*manifestpb.Validator, error) { - var regJSON []byte - - if !val.ZeroRegistration() { - reg, err := val.Eth2Registration() - if err != nil { - return nil, errors.Wrap(err, "eth2 builder registration") - } - - regJSON, err = json.Marshal(reg) - if err != nil { - return nil, errors.Wrap(err, "marshal builder registration") - } - } - - return &manifestpb.Validator{ - PublicKey: val.PubKey, - PubShares: val.PubShares, - FeeRecipientAddress: addrs.FeeRecipientAddress, - WithdrawalAddress: addrs.WithdrawalAddress, - BuilderRegistrationJson: regJSON, - }, nil -} diff --git a/cluster/manifest/load.go b/cluster/manifest/load.go deleted file mode 100644 index 0c868e5a34..0000000000 --- a/cluster/manifest/load.go +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 - -package manifest - -import ( - "bytes" - "encoding/hex" - "encoding/json" - "os" - - "google.golang.org/protobuf/proto" - - "github.com/obolnetwork/charon/app/errors" - "github.com/obolnetwork/charon/app/z" - "github.com/obolnetwork/charon/cluster" - manifestpb "github.com/obolnetwork/charon/cluster/manifestpb/v1" -) - -// LoadCluster returns the current cluster state from disk by reading either from cluster manifest or legacy lock file. -// If both files are provided, both files are read and -// - If cluster hashes don't match, an error is returned -// - If cluster hashes match, the cluster loaded from the manifest file is returned -// -// It returns an error if the cluster can't be loaded from either file. -func LoadCluster(manifestFile, legacyLockFile string, lockCallback func(cluster.Lock) error) (*manifestpb.Cluster, error) { - dag, err := LoadDAG(manifestFile, legacyLockFile, lockCallback) - if err != nil { - return nil, errors.Wrap(err, "load dag from disk") - } - - cluster, err := Materialise(dag) - if err != nil { - return nil, errors.Wrap(err, "materialise dag") - } - - return cluster, nil -} - -// LoadDAG returns the raw cluster DAG from disk by reading either from cluster manifest or legacy lock file. -// If both files are provided, both files are read and -// - If cluster hashes don't match, an error is returned -// - If cluster hashes match, the DAG loaded from the manifest file is returned -// -// It returns an error if the DAG can't be loaded from either file. -func LoadDAG(manifestFile, legacyLockFile string, lockCallback func(cluster.Lock) error) (*manifestpb.SignedMutationList, error) { - dagManifest, errManifest := loadDAGFromManifest(manifestFile) - dagLegacy, errLegacy := loadDAGFromLegacyLock(legacyLockFile, lockCallback) - - switch { - case errManifest == nil && errLegacy == nil: - // Both files loaded successfully, check if cluster hashes match - if err := clusterHashesMatch(dagManifest, dagLegacy); err != nil { - return nil, err - } - - return dagManifest, nil - case errManifest == nil: - // Cluster manifest loaded successfully - return dagManifest, nil - case errLegacy == nil: - // Legacy cluster lock loaded successfully - return dagLegacy, nil - case errors.Is(errLegacy, os.ErrNotExist) && errors.Is(errManifest, os.ErrNotExist): - return nil, errors.New("no file found", z.Str("lock-file", legacyLockFile), z.Str("manifest-file", manifestFile)) - case !errors.Is(errLegacy, os.ErrNotExist): - // Return legacy lock error as it exists but failed to load. - return nil, errors.Wrap(errLegacy, "load cluster from legacy lock file") - default: - return nil, errors.Wrap(errManifest, "load cluster from manifest file") - } -} - -// loadDAGFromManifest returns the raw DAG from cluster manifest file on disk. -func loadDAGFromManifest(filename string) (*manifestpb.SignedMutationList, error) { - b, err := os.ReadFile(filename) - if err != nil { - return nil, errors.Wrap(err, "read manifest file", z.Str("file", filename)) - } - - rawDAG := new(manifestpb.SignedMutationList) - if err := proto.Unmarshal(b, rawDAG); err != nil { - return rawDAG, errors.Wrap(err, "unmarshal cluster dag", z.Str("file", filename)) - } - - return rawDAG, nil -} - -// loadDAGFromLegacyLock returns the raw DAG from legacy lock file on disk. -// It also accepts a callback that is called on the loaded lock. -func loadDAGFromLegacyLock(filename string, lockCallback func(cluster.Lock) error) (*manifestpb.SignedMutationList, error) { - b, err := os.ReadFile(filename) - if err != nil { - return nil, errors.Wrap(err, "read legacy lock file", z.Str("file", filename)) - } - - var lock cluster.Lock - - if err := json.Unmarshal(b, &lock); err != nil { - return nil, errors.Wrap(err, "unmarshal legacy lock", z.Str("file", filename)) - } - - if lockCallback != nil { - if err := lockCallback(lock); err != nil { - return nil, err - } - } - - legacy, err := NewRawLegacyLock(b) - if err != nil { - return nil, errors.Wrap(err, "create legacy lock") - } - - return &manifestpb.SignedMutationList{Mutations: []*manifestpb.SignedMutation{legacy}}, nil -} - -// clusterHashesMatch returns an error if the cluster hashes of the provided DAGs don't match. -func clusterHashesMatch(dagManifest, dagLegacy *manifestpb.SignedMutationList) error { - hashManifest, err := Hash(dagManifest.GetMutations()[0]) - if err != nil { - return errors.Wrap(err, "materialise dag") - } - - hashLegacy, err := Hash(dagLegacy.GetMutations()[0]) - if err != nil { - return errors.Wrap(err, "materialise dag") - } - - if !bytes.Equal(hashManifest, hashLegacy) { - return errors.New("manifest and legacy cluster hashes mismatch", - z.Str("manifest_hash", hex.EncodeToString(hashManifest)), - z.Str("legacy_hash", hex.EncodeToString(hashLegacy))) - } - - return nil -} diff --git a/cluster/manifest/load_test.go b/cluster/manifest/load_test.go deleted file mode 100644 index 3e81ce3169..0000000000 --- a/cluster/manifest/load_test.go +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 - -package manifest_test - -import ( - "encoding/hex" - "encoding/json" - "math/rand" - "os" - "path" - "testing" - - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - - "github.com/obolnetwork/charon/cluster" - "github.com/obolnetwork/charon/cluster/manifest" - manifestpb "github.com/obolnetwork/charon/cluster/manifestpb/v1" - "github.com/obolnetwork/charon/testutil" -) - -func TestLoadLegacy(t *testing.T) { - for _, version := range cluster.SupportedVersionsForT(t) { - t.Run(version, func(t *testing.T) { - testLoadLegacy(t, version) - }) - } -} - -func TestLoadManifest(t *testing.T) { - legacyLockFile := "testdata/lock.json" - lockJSON, err := os.ReadFile(legacyLockFile) - require.NoError(t, err) - - var lock cluster.Lock - testutil.RequireNoError(t, json.Unmarshal(lockJSON, &lock)) - - legacyLock, err := manifest.NewLegacyLockForT(t, lock) - require.NoError(t, err) - - dag := &manifestpb.SignedMutationList{Mutations: []*manifestpb.SignedMutation{legacyLock}} - cluster, err := manifest.Materialise(dag) - require.NoError(t, err) - - b, err := proto.Marshal(dag) - require.NoError(t, err) - - manifestFile := path.Join(t.TempDir(), "cluster-manifest.pb") - require.NoError(t, os.WriteFile(manifestFile, b, 0o644)) - - tests := []struct { - name string - manifestFile string - legacyLockFile string - errorMsg string - }{ - { - name: "no file", - errorMsg: "no file found", - }, - { - name: "only manifest", - manifestFile: manifestFile, - }, - { - name: "only legacy lock", - legacyLockFile: legacyLockFile, - }, - { - name: "both files", - manifestFile: manifestFile, - legacyLockFile: legacyLockFile, - }, - { - name: "mismatching cluster hashes", - manifestFile: manifestFile, - legacyLockFile: "testdata/lock2.json", - errorMsg: "manifest and legacy cluster hashes mismatch", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Load raw cluster DAG from disk - dag, err := manifest.LoadDAG(tt.manifestFile, tt.legacyLockFile, nil) - if tt.errorMsg != "" { - require.ErrorContains(t, err, tt.errorMsg) - return - } - - require.NoError(t, err) - - require.Len(t, dag.GetMutations(), 1) // The only mutation is the `legacy_lock` mutation - - clusterFromDAG, err := manifest.Materialise(dag) - require.NoError(t, err) - - // Load cluster manifest from disk - loaded, err := manifest.LoadCluster(tt.manifestFile, tt.legacyLockFile, nil) - require.NoError(t, err) - - require.True(t, proto.Equal(cluster, loaded)) - require.True(t, proto.Equal(cluster, clusterFromDAG)) - }) - } -} - -func testLoadLegacy(t *testing.T, version string) { - t.Helper() - - n := 4 + rand.Intn(6) - k := cluster.Threshold(n) - - var opts []func(*cluster.Definition) - - opts = append(opts, cluster.WithVersion(version)) - if isAnyVersion(version, v1_0, v1_1, v1_2, v1_3, v1_4) { - opts = append(opts, cluster.WithLegacyVAddrs(testutil.RandomETHAddress(), testutil.RandomETHAddress())) - } - - if isAnyVersion(version, v1_0, v1_1, v1_2, v1_3, v1_4, v1_5, v1_6, v1_7, v1_8, v1_9) { - opts = append(opts, func(d *cluster.Definition) { d.TargetGasLimit = 0 }) - } else { - opts = append(opts, func(d *cluster.Definition) { - d.TargetGasLimit = 36000000 - d.Compounding = true - }) - } - - seed := 0 - random := rand.New(rand.NewSource(int64(seed))) - lock, _, _ := cluster.NewForT(t, rand.Intn(10), k, n, seed, random, opts...) - - b, err := json.MarshalIndent(lock, "", " ") - require.NoError(t, err) - - file := path.Join(t.TempDir(), "lock.json") - - err = os.WriteFile(file, b, 0o644) - require.NoError(t, err) - - cluster, err := manifest.LoadCluster("", file, nil) - require.NoError(t, err) - - require.Equal(t, lock.LockHash, cluster.GetInitialMutationHash()) - require.Equal(t, lock.LockHash, cluster.GetLatestMutationHash()) - require.Equal(t, lock.Name, cluster.GetName()) - require.EqualValues(t, lock.Threshold, cluster.GetThreshold()) - require.Equal(t, lock.DKGAlgorithm, cluster.GetDkgAlgorithm()) - require.Equal(t, lock.ForkVersion, cluster.GetForkVersion()) - require.Len(t, cluster.GetValidators(), len(lock.Validators)) - require.Len(t, cluster.GetOperators(), len(lock.Operators)) - - for i, validator := range cluster.GetValidators() { - require.Equal(t, lock.Validators[i].PubKey, validator.GetPublicKey()) - require.Equal(t, lock.Validators[i].PubShares, validator.GetPubShares()) - require.Equal(t, lock.ValidatorAddresses[i].FeeRecipientAddress, validator.GetFeeRecipientAddress()) - require.Equal(t, lock.ValidatorAddresses[i].WithdrawalAddress, validator.GetWithdrawalAddress()) - } - - for i, operator := range cluster.GetOperators() { - require.Equal(t, lock.Operators[i].Address, operator.GetAddress()) - require.Equal(t, lock.Operators[i].ENR, operator.GetEnr()) - } -} - -// TestLoadModifiedLegacyLock ensure the incorrect hard-coded hash is used for -// legacy locks. This ensures the cluster hash doesn't change even if lock files -// were modified and run with --no-verify. -func TestLoadModifiedLegacyLock(t *testing.T) { - cluster, err := manifest.LoadCluster("", "testdata/lock3.json", nil) - require.NoError(t, err) - - hashHex := hex.EncodeToString(cluster.GetInitialMutationHash()) - require.Equal(t, "4073fe542", hashHex[:9]) -} diff --git a/cluster/manifest/materialise.go b/cluster/manifest/materialise.go deleted file mode 100644 index a1fcf33ed0..0000000000 --- a/cluster/manifest/materialise.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 - -package manifest - -import ( - "github.com/obolnetwork/charon/app/errors" - manifestpb "github.com/obolnetwork/charon/cluster/manifestpb/v1" -) - -// Materialise transforms a raw DAG and returns the resulting cluster manifest. -func Materialise(rawDAG *manifestpb.SignedMutationList) (*manifestpb.Cluster, error) { - if rawDAG == nil || len(rawDAG.GetMutations()) == 0 { - return nil, errors.New("empty raw DAG") - } - - var ( - cluster = new(manifestpb.Cluster) - err error - ) - for _, signed := range rawDAG.GetMutations() { - cluster, err = Transform(cluster, signed) - if err != nil { - return nil, err - } - } - - // InitialMutationHash is the hash of the first mutation. - cluster.InitialMutationHash, err = Hash(rawDAG.GetMutations()[0]) - if err != nil { - return nil, errors.Wrap(err, "calculate initial hash") - } - - // LatestMutationHash is the hash of the last mutation. - cluster.LatestMutationHash, err = Hash(rawDAG.GetMutations()[len(rawDAG.GetMutations())-1]) - if err != nil { - return nil, errors.Wrap(err, "calculate latest hash") - } - - return cluster, nil -} diff --git a/cluster/manifest/mutation.go b/cluster/manifest/mutation.go deleted file mode 100644 index 6c04f68b41..0000000000 --- a/cluster/manifest/mutation.go +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 - -package manifest - -import ( - manifestpb "github.com/obolnetwork/charon/cluster/manifestpb/v1" -) - -// 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) -} - -// Transform returns a transformed cluster manifest with the given mutation. -func (t MutationType) Transform(cluster *manifestpb.Cluster, signed *manifestpb.SignedMutation) (*manifestpb.Cluster, error) { - return mutationDefs[t].TransformFunc(cluster, signed) -} - -const ( - TypeUnknown MutationType = "" - TypeLegacyLock MutationType = "dv/legacy_lock/v0.0.1" - TypeNodeApproval MutationType = "dv/node_approval/v0.0.1" - TypeNodeApprovals MutationType = "dv/node_approvals/v0.0.1" - TypeGenValidators MutationType = "dv/gen_validators/v0.0.1" - TypeAddValidators MutationType = "dv/add_validators/v0.0.1" -) - -type mutationDef struct { - TransformFunc func(*manifestpb.Cluster, *manifestpb.SignedMutation) (*manifestpb.Cluster, error) -} - -var mutationDefs = make(map[MutationType]mutationDef) - -// init is required to populate the mutation definition map since -// static compile-time results in initialization cycle. -// -//nolint:gochecknoinits // required to avoid cycles -func init() { - mutationDefs[TypeLegacyLock] = mutationDef{ - TransformFunc: transformLegacyLock, - } - - mutationDefs[TypeNodeApproval] = mutationDef{ - TransformFunc: func(c *manifestpb.Cluster, signed *manifestpb.SignedMutation) (*manifestpb.Cluster, error) { - return c, verifyNodeApproval(signed) - }, - } - - mutationDefs[TypeNodeApprovals] = mutationDef{ - TransformFunc: transformNodeApprovals, - } - - mutationDefs[TypeGenValidators] = mutationDef{ - TransformFunc: transformGenValidators, - } - - mutationDefs[TypeAddValidators] = mutationDef{ - TransformFunc: transformAddValidators, - } -} diff --git a/cluster/manifest/mutationaddvalidator.go b/cluster/manifest/mutationaddvalidator.go deleted file mode 100644 index 0b66bfb57c..0000000000 --- a/cluster/manifest/mutationaddvalidator.go +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 - -package manifest - -import ( - "bytes" - - "google.golang.org/protobuf/types/known/anypb" - - "github.com/obolnetwork/charon/app/errors" - manifestpb "github.com/obolnetwork/charon/cluster/manifestpb/v1" -) - -// NewGenValidators creates a new generate validators mutation. -func NewGenValidators(parent []byte, validators []*manifestpb.Validator) (*manifestpb.SignedMutation, error) { - if err := verifyGenValidators(validators); err != nil { - return nil, errors.Wrap(err, "verify validators") - } - - if len(parent) != hashLen { - return nil, errors.New("invalid parent hash") - } - - valsAny, err := anypb.New(&manifestpb.ValidatorList{Validators: validators}) - if err != nil { - return nil, errors.Wrap(err, "marshal validators") - } - - return &manifestpb.SignedMutation{ - Mutation: &manifestpb.Mutation{ - Parent: parent, - Type: string(TypeGenValidators), - Data: valsAny, - }, - // No signer or signature. - }, nil -} - -// verifyGenValidators validates the GenValidators list, ensuring validators are populated with valid addresses. -func verifyGenValidators(vals []*manifestpb.Validator) error { - if len(vals) == 0 { - return errors.New("no validators") - } - - for _, validator := range vals { - if _, err := from0xHex(validator.GetFeeRecipientAddress(), 20); err != nil { - return errors.Wrap(err, "validate fee recipient address") - } - - if _, err := from0xHex(validator.GetWithdrawalAddress(), 20); err != nil { - return errors.Wrap(err, "validate withdrawal address") - } - } - - return nil -} - -func transformGenValidators(c *manifestpb.Cluster, signed *manifestpb.SignedMutation) (*manifestpb.Cluster, error) { - if err := verifyEmptySig(signed); err != nil { - return c, errors.Wrap(err, "verify empty sig") - } - - if MutationType(signed.GetMutation().GetType()) != TypeGenValidators { - return c, errors.New("invalid mutation type") - } - - vals := new(manifestpb.ValidatorList) - if err := signed.GetMutation().GetData().UnmarshalTo(vals); err != nil { - return c, errors.Wrap(err, "unmarshal validators") - } - - c.Validators = append(c.Validators, vals.GetValidators()...) - - return c, nil -} - -// NewAddValidators creates a new composite add validators mutation from the provided gen validators and node approvals. -func NewAddValidators(genValidators, nodeApprovals *manifestpb.SignedMutation) (*manifestpb.SignedMutation, error) { - if MutationType(genValidators.GetMutation().GetType()) != TypeGenValidators { - return nil, errors.New("invalid gen validators mutation type") - } - - if MutationType(nodeApprovals.GetMutation().GetType()) != TypeNodeApprovals { - return nil, errors.New("invalid node approvals mutation type") - } - - dataAny, err := anypb.New(&manifestpb.SignedMutationList{ - Mutations: []*manifestpb.SignedMutation{genValidators, nodeApprovals}, - }) - if err != nil { - return nil, errors.Wrap(err, "marshal signed mutation list") - } - - return &manifestpb.SignedMutation{ - Mutation: &manifestpb.Mutation{ - Parent: genValidators.GetMutation().GetParent(), - Type: string(TypeAddValidators), - Data: dataAny, - }, - // Composite mutations have no signer or signature. - }, nil -} - -func transformAddValidators(c *manifestpb.Cluster, signed *manifestpb.SignedMutation) (*manifestpb.Cluster, error) { - if err := verifyEmptySig(signed); err != nil { - return c, errors.Wrap(err, "verify empty sig") - } - - if MutationType(signed.GetMutation().GetType()) != TypeAddValidators { - return c, errors.New("invalid mutation type") - } - - list := new(manifestpb.SignedMutationList) - if err := signed.GetMutation().GetData().UnmarshalTo(list); err != nil { - return c, errors.Wrap(err, "unmarshal signed mutation list") - } else if len(list.GetMutations()) != 2 { - return c, errors.New("invalid mutation list length") - } - - genValidators := list.GetMutations()[0] - nodeApprovals := list.GetMutations()[1] - - if MutationType(genValidators.GetMutation().GetType()) != TypeGenValidators { - return c, errors.New("invalid gen validators mutation type") - } - - if !bytes.Equal(signed.GetMutation().GetParent(), genValidators.GetMutation().GetParent()) { - return c, errors.New("invalid gen validators parent") - } - - if MutationType(nodeApprovals.GetMutation().GetType()) != TypeNodeApprovals { - return c, errors.New("invalid node approvals mutation type") - } - - genHash, err := Hash(genValidators) - if err != nil { - return c, errors.Wrap(err, "hash gen validators") - } - - if !bytes.Equal(genHash, nodeApprovals.GetMutation().GetParent()) { - return c, errors.New("invalid node approvals parent") - } - - c, err = Transform(c, genValidators) - if err != nil { - return c, errors.Wrap(err, "transform gen validators") - } - - c, err = Transform(c, nodeApprovals) - if err != nil { - return c, errors.Wrap(err, "transform node approvals") - } - - return c, nil -} diff --git a/cluster/manifest/mutationaddvalidator_test.go b/cluster/manifest/mutationaddvalidator_test.go deleted file mode 100644 index e2c107c8aa..0000000000 --- a/cluster/manifest/mutationaddvalidator_test.go +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 - -package manifest_test - -import ( - "encoding/hex" - "encoding/json" - "math/rand" - "os" - "testing" - - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - - "github.com/obolnetwork/charon/cluster" - "github.com/obolnetwork/charon/cluster/manifest" - manifestpb "github.com/obolnetwork/charon/cluster/manifestpb/v1" - "github.com/obolnetwork/charon/testutil" -) - -//go:generate go test . -update - -func TestGenValidators(t *testing.T) { - setIncrementingTime(t) - - b, err := os.ReadFile("testdata/lock.json") - require.NoError(t, err) - - var lock cluster.Lock - require.NoError(t, json.Unmarshal(b, &lock)) - - // Convert validators into manifest.Validator - var vals []*manifestpb.Validator - - for i, validator := range lock.Validators { - val, err := manifest.ValidatorToProto(validator, lock.ValidatorAddresses[i]) - require.NoError(t, err) - - vals = append(vals, val) - } - - parent, err := hex.DecodeString("605ec6de4f1ae997dd3545513b934c335a833f4635dc9fad7758314f79ff0fae") - require.NoError(t, err) - signed, err := manifest.NewGenValidators(parent, vals) - require.NoError(t, err) - - t.Run("unmarshal", func(t *testing.T) { - b, err := json.Marshal(signed) - require.NoError(t, err) - - var signed2 *manifestpb.SignedMutation - require.NoError(t, json.Unmarshal(b, &signed2)) - - testutil.RequireProtoEqual(t, signed, signed2) - }) - - t.Run("transform", func(t *testing.T) { - cluster, err := manifest.Transform(new(manifestpb.Cluster), signed) - require.NoError(t, err) - - testutil.RequireProtosEqual(t, vals, cluster.GetValidators()) - }) - - t.Run("proto", func(t *testing.T) { - testutil.RequireGoldenProto(t, signed) - }) -} - -//go:generate go test . -update -run=TestAddValidators && go test . -run=TestAddValidators - -func TestAddValidators(t *testing.T) { - setIncrementingTime(t) - - nodes := 4 - seed := 1 - random := rand.New(rand.NewSource(int64(seed))) - lock, secrets, _ := cluster.NewForT(t, 3, 3, nodes, seed, random) - // Convert validators into manifest.Validator - var vals []*manifestpb.Validator - - for i, validator := range lock.Validators { - val, err := manifest.ValidatorToProto(validator, lock.ValidatorAddresses[i]) - require.NoError(t, err) - - vals = append(vals, val) - } - - genVals, err := manifest.NewGenValidators(testutil.RandomBytes32Seed(random), vals) - require.NoError(t, err) - genHash, err := manifest.Hash(genVals) - testutil.RequireNoError(t, err) - - var approvals []*manifestpb.SignedMutation - - for _, secret := range secrets { - approval, err := manifest.SignNodeApproval(genHash, secret) - require.NoError(t, err) - - approvals = append(approvals, approval) - } - - nodeApprovals, err := manifest.NewNodeApprovalsComposite(approvals) - require.NoError(t, err) - - addVals, err := manifest.NewAddValidators(genVals, nodeApprovals) - require.NoError(t, err) - - t.Run("proto", func(t *testing.T) { - testutil.RequireGoldenProto(t, addVals) - }) - - t.Run("unmarshal", func(t *testing.T) { - b, err := proto.Marshal(addVals) - require.NoError(t, err) - - addVals2 := new(manifestpb.SignedMutation) - require.NoError(t, proto.Unmarshal(b, addVals2)) - - testutil.RequireProtoEqual(t, addVals, addVals2) - }) - - t.Run("transform", func(t *testing.T) { - cluster, err := manifest.NewClusterFromLockForT(t, lock) - require.NoError(t, err) - - cluster.Validators = nil - - cluster, err = manifest.Transform(cluster, addVals) - require.NoError(t, err) - - testutil.RequireProtosEqual(t, vals, cluster.GetValidators()) - }) -} diff --git a/cluster/manifest/mutationlegacylock.go b/cluster/manifest/mutationlegacylock.go deleted file mode 100644 index fa05590bf9..0000000000 --- a/cluster/manifest/mutationlegacylock.go +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 - -package manifest - -import ( - "bytes" - "encoding/json" - "testing" - - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/anypb" - - "github.com/obolnetwork/charon/app/errors" - "github.com/obolnetwork/charon/cluster" - manifestpb "github.com/obolnetwork/charon/cluster/manifestpb/v1" -) - -// NewDAGFromLockForT returns a cluster DAG from the provided lock for use in tests. -func NewDAGFromLockForT(_ *testing.T, lock cluster.Lock) (*manifestpb.SignedMutationList, error) { - signed, err := NewLegacyLockForT(nil, lock) - if err != nil { - return nil, err - } - - return &manifestpb.SignedMutationList{Mutations: []*manifestpb.SignedMutation{signed}}, nil -} - -// NewClusterFromLockForT returns a cluster manifest from the provided lock for use in tests. -func NewClusterFromLockForT(_ *testing.T, lock cluster.Lock) (*manifestpb.Cluster, error) { - signed, err := NewLegacyLockForT(nil, lock) - if err != nil { - return nil, err - } - - return Materialise(&manifestpb.SignedMutationList{Mutations: []*manifestpb.SignedMutation{signed}}) -} - -// NewRawLegacyLock return a new legacy lock mutation from the provided raw json bytes. -func NewRawLegacyLock(b []byte) (*manifestpb.SignedMutation, error) { - // Verify that the bytes is a valid lock. - if err := json.Unmarshal(b, new(cluster.Lock)); err != nil { - return nil, errors.Wrap(err, "unmarshal lock") - } - - lockAny, err := anypb.New(&manifestpb.LegacyLock{Json: b}) - if err != nil { - return nil, errors.Wrap(err, "lock to any") - } - - var zeroParent [32]byte - - return &manifestpb.SignedMutation{ - Mutation: &manifestpb.Mutation{ - Parent: zeroParent[:], // Empty parent - Type: string(TypeLegacyLock), - Data: lockAny, - }, - // No signer or signature - }, nil -} - -// NewLegacyLockForT return a new legacy lock mutation from the provided lock. -func NewLegacyLockForT(_ *testing.T, lock cluster.Lock) (*manifestpb.SignedMutation, error) { - // Marshalling below re-calculates the lock hash, so ensure it matches. - lock2, err := lock.SetLockHash() - if err != nil { - return nil, errors.Wrap(err, "set lock hash") - } else if !bytes.Equal(lock2.LockHash, lock.LockHash) { - return nil, errors.New("this method only supports valid locks," + - " use NewRawLegacyLock for --no-verify support") - } - - b, err := json.Marshal(lock) - if err != nil { - return nil, errors.Wrap(err, "marshal lock") - } - - return NewRawLegacyLock(b) -} - -// verifyLegacyLock verifies that the signed mutation is a valid legacy lock. -func verifyLegacyLock(signed *manifestpb.SignedMutation) error { - if MutationType(signed.GetMutation().GetType()) != TypeLegacyLock { - return errors.New("invalid mutation type") - } - - if err := verifyEmptySig(signed); err != nil { - return errors.Wrap(err, "verify empty signature") - } - - legacyLock := new(manifestpb.LegacyLock) - if err := signed.GetMutation().GetData().UnmarshalTo(legacyLock); err != nil { - return errors.New("mutation data to legacy lock") - } - - var lock cluster.Lock - if err := json.Unmarshal(legacyLock.GetJson(), &lock); err != nil { - return errors.Wrap(err, "unmarshal lock") - } - // return lock.VerifySignatures() - - return nil -} - -// transformLegacyLock transforms the cluster manifest with the provided legacy lock mutation. -func transformLegacyLock(input *manifestpb.Cluster, signed *manifestpb.SignedMutation) (*manifestpb.Cluster, error) { - if !isZeroProto(input) { - return nil, errors.New("legacy lock not first mutation") - } - - if err := verifyLegacyLock(signed); err != nil { - return nil, errors.Wrap(err, "verify legacy lock") - } - - legacyLock := new(manifestpb.LegacyLock) - if err := signed.GetMutation().GetData().UnmarshalTo(legacyLock); err != nil { - return nil, errors.New("mutation data to legacy lock") - } - - var lock cluster.Lock - if err := json.Unmarshal(legacyLock.GetJson(), &lock); err != nil { - return nil, errors.Wrap(err, "unmarshal lock") - } - - var ops []*manifestpb.Operator - for _, operator := range lock.Operators { - ops = append(ops, &manifestpb.Operator{ - Address: operator.Address, - Enr: operator.ENR, - }) - } - - if len(lock.ValidatorAddresses) != len(lock.Validators) { - return nil, errors.New("validator addresses and validators length mismatch") - } - - var vals []*manifestpb.Validator - - for i, validator := range lock.Validators { - val, err := ValidatorToProto(validator, lock.ValidatorAddresses[i]) - if err != nil { - return nil, errors.Wrap(err, "validator to proto") - } - - vals = append(vals, val) - } - - return &manifestpb.Cluster{ - Name: lock.Name, - Threshold: int32(lock.Threshold), - DkgAlgorithm: lock.DKGAlgorithm, - ForkVersion: lock.ForkVersion, - ConsensusProtocol: lock.ConsensusProtocol, - TargetGasLimit: uint32(lock.TargetGasLimit), - Compounding: lock.Compounding, - Validators: vals, - Operators: ops, - }, nil -} - -// isZeroProto returns true if the provided proto message is zero. -// -// Note this function is inefficient for the negative case (i.e. when the message is not zero) -// as it copies the input argument. -func isZeroProto(m proto.Message) bool { - if m == nil { - return false - } - - clone := proto.Clone(m) - proto.Reset(clone) - - return proto.Equal(m, clone) -} diff --git a/cluster/manifest/mutationlegacylock_test.go b/cluster/manifest/mutationlegacylock_test.go deleted file mode 100644 index 030f2dad94..0000000000 --- a/cluster/manifest/mutationlegacylock_test.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 - -package manifest_test - -import ( - "encoding/json" - "os" - "path" - "testing" - - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - - "github.com/obolnetwork/charon/cluster" - "github.com/obolnetwork/charon/cluster/manifest" - manifestpb "github.com/obolnetwork/charon/cluster/manifestpb/v1" - "github.com/obolnetwork/charon/testutil" -) - -//go:generate go test . -update - -func TestZeroCluster(t *testing.T) { - _, err := manifest.TypeLegacyLock.Transform(&manifestpb.Cluster{Name: "foo"}, &manifestpb.SignedMutation{}) - require.ErrorContains(t, err, "legacy lock not first mutation") -} - -func TestLegacyLock(t *testing.T) { - lockJSON, err := os.ReadFile("testdata/lock.json") - require.NoError(t, err) - - var lock cluster.Lock - testutil.RequireNoError(t, json.Unmarshal(lockJSON, &lock)) - - legacyLock, err := manifest.NewLegacyLockForT(t, lock) - require.NoError(t, err) - - t.Run("proto", func(t *testing.T) { - testutil.RequireGoldenProto(t, legacyLock) - }) - - t.Run("cluster", func(t *testing.T) { - cluster, err := manifest.Materialise(&manifestpb.SignedMutationList{Mutations: []*manifestpb.SignedMutation{legacyLock}}) - require.NoError(t, err) - require.Equal(t, lock.LockHash, cluster.GetInitialMutationHash()) - require.Equal(t, lock.LockHash, cluster.GetLatestMutationHash()) - testutil.RequireGoldenProto(t, cluster, testutil.WithFilename("TestLegacyLock_cluster.golden")) - }) - - b, err := proto.Marshal(legacyLock) - require.NoError(t, err) - - signed2 := new(manifestpb.SignedMutation) - testutil.RequireNoError(t, proto.Unmarshal(b, signed2)) - - t.Run("proto again", func(t *testing.T) { - testutil.RequireGoldenProto(t, signed2, testutil.WithFilename("TestLegacyLock_proto.golden")) - }) - - t.Run("cluster loaded from lock", func(t *testing.T) { - cluster, err := manifest.LoadCluster("", "testdata/lock.json", nil) - require.NoError(t, err) - - testutil.RequireGoldenProto(t, cluster, testutil.WithFilename("TestLegacyLock_cluster.golden")) - }) - - t.Run("cluster loaded from manifest", func(t *testing.T) { - dag := &manifestpb.SignedMutationList{Mutations: []*manifestpb.SignedMutation{legacyLock}} - - b, err := proto.Marshal(dag) - require.NoError(t, err) - file := path.Join(t.TempDir(), "manifest.pb") - require.NoError(t, os.WriteFile(file, b, 0o644)) - - cluster, err := manifest.LoadCluster(file, "", nil) - require.NoError(t, err) - - testutil.RequireGoldenProto(t, cluster, testutil.WithFilename("TestLegacyLock_cluster.golden")) - }) -} diff --git a/cluster/manifest/mutationnodeapproval.go b/cluster/manifest/mutationnodeapproval.go deleted file mode 100644 index 977b7581b3..0000000000 --- a/cluster/manifest/mutationnodeapproval.go +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 - -package manifest - -import ( - "bytes" - - k1 "github.com/decred/dcrd/dcrec/secp256k1/v4" - "google.golang.org/protobuf/types/known/anypb" - "google.golang.org/protobuf/types/known/timestamppb" - - "github.com/obolnetwork/charon/app/errors" - "github.com/obolnetwork/charon/app/z" - manifestpb "github.com/obolnetwork/charon/cluster/manifestpb/v1" -) - -// SignNodeApproval signs a node approval mutation. -func SignNodeApproval(parent []byte, secret *k1.PrivateKey) (*manifestpb.SignedMutation, error) { - timestampAny, err := anypb.New(nowFunc()) - if err != nil { - return nil, errors.Wrap(err, "timestamp to any") - } - - if len(parent) != hashLen { - return nil, errors.New("invalid parent hash") - } - - return SignK1(&manifestpb.Mutation{ - Parent: parent, - Type: string(TypeNodeApproval), - Data: timestampAny, - }, secret) -} - -// NewNodeApprovalsComposite returns a new composite node approvals mutation. -// Note the approvals must be for all nodes in the cluster ordered by peer index. -func NewNodeApprovalsComposite(approvals []*manifestpb.SignedMutation) (*manifestpb.SignedMutation, error) { - if len(approvals) == 0 { - return nil, errors.New("empty node approvals") - } - - var parent []byte - for i, approval := range approvals { - if i == 0 { - parent = approval.GetMutation().GetParent() - } else if !bytes.Equal(parent, approval.GetMutation().GetParent()) { - return nil, errors.New("mismatching node approvals parent") - } - - if err := verifyNodeApproval(approval); err != nil { - return nil, errors.Wrap(err, "verify node approval", z.Int("index", i)) - } - } - - anyList, err := anypb.New(&manifestpb.SignedMutationList{ - Mutations: approvals, - }) - if err != nil { - return nil, errors.Wrap(err, "mutations to any") - } - - return &manifestpb.SignedMutation{ - Mutation: &manifestpb.Mutation{ - Parent: parent, - Type: string(TypeNodeApprovals), - Data: anyList, - }, - // Composite types do not have signatures - }, nil -} - -// verifyNodeApproval returns an error if the input signed mutation is not valid. -func verifyNodeApproval(signed *manifestpb.SignedMutation) error { - if MutationType(signed.GetMutation().GetType()) != TypeNodeApproval { - return errors.New("invalid mutation type") - } - - timestamp := new(timestamppb.Timestamp) - if err := signed.GetMutation().GetData().UnmarshalTo(timestamp); err != nil { - return errors.Wrap(err, "invalid node approval timestamp data") - } - - return verifyK1SignedMutation(signed) -} - -// transformNodeApprovals transforms the cluster manifest with the node approvals. -func transformNodeApprovals(c *manifestpb.Cluster, signed *manifestpb.SignedMutation) (*manifestpb.Cluster, error) { - if MutationType(signed.GetMutation().GetType()) != TypeNodeApprovals { - return c, errors.New("invalid mutation type") - } - - list := new(manifestpb.SignedMutationList) - if err := signed.GetMutation().GetData().UnmarshalTo(list); err != nil { - return c, errors.New("invalid node approval data") - } - - peers, err := ClusterPeers(c) - if err != nil { - return c, errors.Wrap(err, "get peers") - } - - if len(peers) != len(list.GetMutations()) { - return c, errors.New("invalid number of node approvals") - } - - var parent []byte - for i, approval := range list.GetMutations() { - if i == 0 { - parent = approval.GetMutation().GetParent() - } else if !bytes.Equal(parent, approval.GetMutation().GetParent()) { - return c, errors.New("mismatching node approvals parent") - } - - pubkey, err := peers[i].PublicKey() - if err != nil { - return c, errors.Wrap(err, "get peer public key") - } - - if !bytes.Equal(pubkey.SerializeCompressed(), approval.GetSigner()) { - return c, errors.New("invalid node approval signer") - } - - c, err = Transform(c, approval) - if err != nil { - return c, errors.Wrap(err, "transform node approval") - } - } - - return c, nil -} diff --git a/cluster/manifest/mutationnodeapproval_test.go b/cluster/manifest/mutationnodeapproval_test.go deleted file mode 100644 index 15f14451f0..0000000000 --- a/cluster/manifest/mutationnodeapproval_test.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 - -package manifest_test - -import ( - "math/rand" - "testing" - "time" - - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/timestamppb" - - "github.com/obolnetwork/charon/cluster" - "github.com/obolnetwork/charon/cluster/manifest" - manifestpb "github.com/obolnetwork/charon/cluster/manifestpb/v1" - "github.com/obolnetwork/charon/testutil" -) - -//go:generate go test . -update - -// setIncrementingTime sets the time function to an deterministic incrementing value -// for the duration of the test. -func setIncrementingTime(t *testing.T) { - t.Helper() - - ts := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) - - manifest.SetNowFuncForT(t, func() *timestamppb.Timestamp { - defer func() { - ts = ts.Add(time.Minute) - }() - - return timestamppb.New(ts) - }) -} - -func TestNodeApprovals(t *testing.T) { - setIncrementingTime(t) - - seed := 1 - random := rand.New(rand.NewSource(int64(seed))) - lock, secrets, _ := cluster.NewForT(t, 1, 3, 4, seed, random) - - parent := testutil.RandomBytes32Seed(random) - - var approvals []*manifestpb.SignedMutation - - for _, secret := range secrets { - approval, err := manifest.SignNodeApproval(parent, secret) - require.NoError(t, err) - - approvals = append(approvals, approval) - } - - composite, err := manifest.NewNodeApprovalsComposite(approvals) - testutil.RequireNoError(t, err) - - t.Run("proto", func(t *testing.T) { - testutil.RequireGoldenProto(t, composite) - }) - - t.Run("unmarshal", func(t *testing.T) { - b, err := proto.Marshal(composite) - require.NoError(t, err) - - composite2 := new(manifestpb.SignedMutation) - testutil.RequireNoError(t, proto.Unmarshal(b, composite2)) - testutil.RequireProtoEqual(t, composite, composite2) - }) - - t.Run("transform", func(t *testing.T) { - cluster, err := manifest.NewClusterFromLockForT(t, lock) - require.NoError(t, err) - - cluster2, err := manifest.Transform(cluster, composite) - require.NoError(t, err) - - testutil.RequireProtoEqual(t, cluster, cluster2) - }) -} diff --git a/cluster/manifest/testdata/TestAddValidators_proto.golden b/cluster/manifest/testdata/TestAddValidators_proto.golden deleted file mode 100644 index 4732ad1873..0000000000 --- a/cluster/manifest/testdata/TestAddValidators_proto.golden +++ /dev/null @@ -1,110 +0,0 @@ -mutation: { - parent: "k\xf8Lqt\xcbtv6L\xc3\xdb\xd9h\xb0\xf7\x17.\xd8W\x94\xbb5\x8b\x0c;R]\xa1xo\x9f" - type: "dv/add_validators/v0.0.1" - data: { - [type.googleapis.com/cluster.manifestpb.v1.SignedMutationList]: { - mutations: { - mutation: { - parent: "k\xf8Lqt\xcbtv6L\xc3\xdb\xd9h\xb0\xf7\x17.\xd8W\x94\xbb5\x8b\x0c;R]\xa1xo\x9f" - type: "dv/gen_validators/v0.0.1" - data: { - [type.googleapis.com/cluster.manifestpb.v1.ValidatorList]: { - validators: { - public_key: "\x96h/bc\xbeik\x02&6\xdf\xffFj\t=|H\xf6\x1f\x1ay\x1a\\n\x14n\xf6:\x83)\xb1\xf7\xfd\x93\xd7\n\t\xc0\xbb\x1a3\x9f\xe5w\x02q" - pub_shares: "\xb8ɱQ9\xea\x96n\x93ֈ\xacxġ\xf2\xb2\xd9\xd0\xcd\xe0d\x0fnG\xec'\xf1\x1f[\xb2\x98\xc3c|nJ\xb6\x1cõN*\xcb\xc2a\xb5\xb5" - pub_shares: "\xa0\xe4\xd16\xdb\x06\xddV5p\x8fx{\xd4\xc7\x1e\xa0\xe9o\xc4\x19\x99bm\x8c\x91<,w\xb9m\xec\xda\x12bʛ\x9e\xbb.\xf6\xea%\xe6\xc4\xf6\xb4\xc6" - pub_shares: "\xa17^\xce:\xff\x9cս\x1f\n\xcf\x18-\x9f\xa5\xeai\xadz\ta\x11\x18\x8d;\xc0s\xe5i4\xa2\xc4?H-B3\x955|\xd7Yf\xadT2i" - pub_shares: "\xb3W\xb8\x10\xbdh$\xc6\x04\xe4\xd8\xce\r\x94\r\x12\xfa5\xfcԥ\x1f\xdb\xe3\xe18\xb8\x1e/C\x8c\x18v\x0b\xca\xe5\xde~\n\xf4\xd1" - fee_recipient_address: "0x52fdfc072182654f163f5f0f9a621d729566c74d" - withdrawal_address: "0x81855ad8681d0d86d1e91e00167939cb6694d2c4" - builder_registration_json: "{\"version\":\"v1\",\"v1\":{\"message\":{\"fee_recipient\":\"0x52FDfc072182654f163F5F0f9A621D729566c74D\",\"gas_limit\":\"30000000\",\"timestamp\":\"1616508000\",\"pubkey\":\"0x96682f6263be696b022636dfff466a093d7c48f61f1a791a5c6e146ef63a8329b1f7fd93d70a09c0bb1a339fe5770271\"},\"signature\":\"0x83a4b59d0b44269dca0d1457673a4672d90881f1a5901774cfcd08c7ac3da736363355461f0f6786cf61880cd9a9417213b1ea152dba50b46b066d63fe23cb64ff7e2895d0339aa893a569d24a6183f75770af009dfa9231d242702b6a527f30\"}}" - } - validators: { - public_key: "\xa2\x97N\x9c\xa1\x7fZ\x98.\xe6R\x92\xf1a\xa63\x9esk\xef\xf4s\xf7\x1fvAw\r_x\x88&~džR\xa6\x86n͓+)\x06\xf2\xfc:\xa9" - pub_shares: "\x81\xcf)\x12\x06?ߔ\t\xa2\x08\x11\xc4\xf5\x85y\xeaC\x190\xe7`\n\x80\xfeZ\x1c\x95\x83\xc6d\x0f \xee1\x03\x97\xa0r\xfb\xe4\xc7;`\x07\x86$\x10" - pub_shares: "\xb3e\x99\xbbL\x84\"O)R\xfe\x8e\xf2\xed\x0c\x8b\xf4\xe5\xf5=\x1b\xb1!\x9bj>[5\xd2\x1bp*攘\x8f\xc4,|\xc0\x183N\x9b2u\xd3\x1c" - pub_shares: "\x88[\xf0\xa7\x8dF\xa5\xb7\xe7\x18\xbe47\xe9\x11\x87\xf6\xa6W3\x84\xbe^\xf6U\x86,\t\x97\xbe\x14z\x0b\xab0\xa4ū\xb5\x7f\xdc&;\xd7t\\\xd0\x1a" - pub_shares: "\xac\xfd\xa5^\x13\xd8\xdf\xdds\x89L\x97\x945\x85\xd3\xf1B\xd2=2\x89\x9a[~Q\xdc\x05II\xed\xbc\x96\xb4\xcbC5\xb9P\x80\xfdV\xbb{\xa2\x01\r(" - fee_recipient_address: "0xeb9d18a44784045d87f3c67cf22746e995af5a25" - withdrawal_address: "0x5fb90badb37c5821b6d95526a41a9504680b4e7c" - builder_registration_json: "{\"version\":\"v1\",\"v1\":{\"message\":{\"fee_recipient\":\"0xEb9D18A44784045d87F3C67Cf22746e995aF5a25\",\"gas_limit\":\"30000000\",\"timestamp\":\"1616508000\",\"pubkey\":\"0xa2974e9ca17f5a982ee65292f161a6339e736beff473f71f7641770d5f7888267ec78652a6866ecd932b2906f2fc3aa9\"},\"signature\":\"0xa6dbab26d933b280435ecda94be722c2f4528b6e1c1ac85f40cc8de38d2f01c2db5cfdedb6bcd07feb052106cb2c8485026ab017b22d818968384be718802b9dd2ab6b18cdea1724ea6289b7678fc33ecc733099bc0c3e34c670e1edd78e0ead\"}}" - } - validators: { - public_key: "\xae'\x88?\xfa\x0f\xff\x9d\xe0\x12\x8cX\xfe~b\xae\xc3-eQ\x9a<\xe2vy\xe1h\xd2^\x1dO\x04_\x0bz\xcf\xd1{\x0b\x87\x06'\xdczV\x06#l" - pub_shares: "\xac\xf8Y^;\x1e\xd3\xc2\\\x96؝\x90\xdb2\xef\x1a\xa4ML\xeb\xabB\xb0@Ru\x17|\xee\xb95\x83 ϊ#\xce\xc7\x11Ց޾=\x98V\xa7" - pub_shares: "\xb0c\xb7O\x82\xfeԫ\x80\n\xe1\xf7\xd4\xef\xe2ͮ\xd5+\x02\xf7\xa6\xe7K\xfe^\x7f\x1c\xcdm\xf8㾮\xccfr\xe4\xfc\xff\x05\x01h\xd3\r\xb7q\xdd" - pub_shares: "\x8a\x9e8\x15DY\x12P\xce+\x98\xb3e\xeeO\xf6M\xfa\xe5)\x02*\x16?\xb8\xbfd\xad\x98\xb4\xb1(t\xc4\x00\xa1u\x98\x10\xa1\x06\xb1J\x07\x82Ri\x86" - pub_shares: "\x8a\x8f\x165\x1d\xb5PŽV\xe3^\x84\xe3\x11]\r@\xa9\xd2r\xa4\x00~r˛\x83\xf8\xd3\xcd\x14\x80\xa9\x95\x07\x1d` \x06\x01\xcd\xe6\x8dh\x98T\xf0" - fee_recipient_address: "0x6325253fec738dd7a9e28bf921119c160f070244" - withdrawal_address: "0x0bf5059875921e668a5bdf2c7fc4844592d2572b" - builder_registration_json: "{\"version\":\"v1\",\"v1\":{\"message\":{\"fee_recipient\":\"0x6325253FEc738DD7a9E28bf921119c160F070244\",\"gas_limit\":\"30000000\",\"timestamp\":\"1616508000\",\"pubkey\":\"0xae27883ffa0fff9de0128c58fe7e62aec32d65519a3ce27679e168d25e1d4f045f0b7acfd17b0b870627dc7a5606236c\"},\"signature\":\"0x9316371ecc3ac0afcda10150535be1a0a9586f7005c0beee087d515a322b270945f1492989dccfdd64ec7135b9a08f0d16112947bdaeddd432a67952d20ab655d93257ec9341d7a94cb6281b39a976b9bb835707258ac154a78a6ec1971f406d\"}}" - } - } - } - } - } - mutations: { - mutation: { - parent: "\xe8\xbc \xeb\xafn4y\x96+z\xab\x1d\xcb:,s?\x99'\xce=\xb01\xed2tƁ\xa6\xd5m" - type: "dv/node_approvals/v0.0.1" - data: { - [type.googleapis.com/cluster.manifestpb.v1.SignedMutationList]: { - mutations: { - mutation: { - parent: "\xe8\xbc \xeb\xafn4y\x96+z\xab\x1d\xcb:,s?\x99'\xce=\xb01\xed2tƁ\xa6\xd5m" - type: "dv/node_approval/v0.0.1" - data: { - [type.googleapis.com/google.protobuf.Timestamp]: { - seconds: 1609459200 - } - } - } - signer: "\x02MKl\xd16\x102ʛҮ\xb9\xd9\x00\xaaME\xd9\xea\xd8\n\xc9B3t\xc4Q\xa7%M\x07f" - signature: "\xa5\x9d\x1dqOf\x12\x07\xb6\xdc\xf1-\x9c5\x8aA]\x84\xab\x1d\x84\xda\x1dU\xfba1\r\xcc\x05?\xb6A@\x14ߌ\xbd\x1a\x84U\x13\xa3\xe9\\\x97\xf72,J\x07\x16#5\x89\x01\xb6c\x08\xac\x8bW\xdc\x11\x01" - } - mutations: { - mutation: { - parent: "\xe8\xbc \xeb\xafn4y\x96+z\xab\x1d\xcb:,s?\x99'\xce=\xb01\xed2tƁ\xa6\xd5m" - type: "dv/node_approval/v0.0.1" - data: { - [type.googleapis.com/google.protobuf.Timestamp]: { - seconds: 1609459260 - } - } - } - signer: "\x02S\x1f\xe6\x06\x814P='#\x132'\xc8g\xac\x8f\xa6\xc87mI\xa3\x0bo\xb9\xaa\xecS\xd2e\xf5\xd1$\x8d]\xbd\x0f\x07JZS{\n\xc2RL\x88u\xd5\"\xe3-\xb8Dt\xf5$\x97\xbe" - pub_shares: "\xa3\xfe\xea0\xfa\x15\xc3\x10W\xf8\x85BBcYp(*\xe57!Z>%\r\xad\x08\xd40$RQ\x1c9]\x96H\xbe\xc8w\x14\xb4\xf3\x15uɶZ" - pub_shares: "\x8cd(\xb3\xf8\"\x08\xb6dj\xde\x1e̚ \xbe(\xf2\xf2h\x1b\x92\x8f\xbb\xfde\xef\xec\xfbB\x81?|\xcc\x1e;ϭ\x9e6\x1c)\xe3?\x12\xf5\x9c\x8a" - pub_shares: "\xa0\x8cě\xd4\xcbI\x8c1i\xf5,?\xd99\xc1m\x10\xd2\x12\x13\x92\x0c(2j\xda\x1a\x85|c\xec\xed\xf0\xf5\x96\xbe\xd9u\xa8\xbd\t\x90\xd6\xff\xb0s\xb4" - pub_shares: "\xa3\xf6\xce^\xed\xcex\xb8`|\xb6\xd8\xe3\xf2\xba \xb6t:\xads\xf0(\x10\xd9\xd0z\xc8\x16\x0c\xc1,\xb1\xa5\xf6\x9fhTz\x85\xcao\x08\xb4B\xfa\x0e\xc8" - fee_recipient_address: "0x4c7215a3b539eb1e5849c6077dbb5722f5717a28" - withdrawal_address: "0x0b4b373970115e82ed6f4125c8fa7311e4d7defa" - builder_registration_json: "{\"version\":\"v1\",\"v1\":{\"message\":{\"fee_recipient\":\"0x4c7215a3B539eb1e5849C6077Dbb5722F5717a28\",\"gas_limit\":\"30000000\",\"timestamp\":\"1616508000\",\"pubkey\":\"0xacff76ec0c853e376d49a30b6fb9aaec53d265f5d1248d5dbd0f074a5a537b0ac2524c8875d522e32db84474f52497be\"},\"signature\":\"0x8dde908a5e0e0ceaa9043d0460954c20114feef74c8f7ec2af582993c343aeb58006439ac9591bdb80b124f4eba2b3b70ffdb4460ebed8cc29461b67e08e83bd4075cf770bda7ef4c610602d1cd45b85822386dc13d4af5d24ad7d0ed316a3b4\"}}" - } - validators: { - public_key: "\x87\x9e\xcdt\x84\xe1ճ\xe7\x08\x06\x88\x15\x18$\x8e\"\xc3\x03O\xd3\n\x9b\xb0\x94\xb2\x9b" - pub_shares: "\xa8\x16\xa7}\xbb\x19\x90!\x00\xff\xb6\xe9\xae\xecI\x90z\x0b\xbb\xe1L\x1d}\xa7_\xfb\xba\x8a\xb9\xc9PT\x15\xab\xaaIA\xaa\xb4\xd3\xe2VO\xcb)\x13\xd5\xd8" - pub_shares: "\x98\xea\xeb\xe6\xe1\xf2K\xb7W#5]\xf5\xc37\xcd\x15B\x13\x13\x11\x02\xe5\x9c\x17\x82ʞu\xf2\xb7\xa3\x15\xd8\xecg\x84\xfd(\x98\xe0\xb0\xf1L\xfb\xc3U\xf6" - pub_shares: "\xb7*4\xd83\x9f&\xdbϦ\x80\xca\xcd\x05\x9b(Kq\x9aFV\x1c\xaa\xd8J\n\xa9\x14r\x13\x9e\xa9\x98`\xe06\xa5\x88\xe3\xf9\xd7\xd8\x1d4\xd1\x1b\x18\x08" - pub_shares: "\x80\xf1\x8a7\xae\x95\x1b\xc6e\x8b#\xb7B\x0eh\x97*Pq\xdf^\x08\xe3a\xe4ÿ/˿\xa5\xadtͪ7mI\xa3\x0bo\xb9\xaa\xecS\xd2e\xf5\xd1$\x8d]\xbd\x0f\x07JZS{\n\xc2RL\x88u\xd5\"\xe3-\xb8Dt\xf5$\x97\xbe" - pub_shares: "\xa3\xfe\xea0\xfa\x15\xc3\x10W\xf8\x85BBcYp(*\xe57!Z>%\r\xad\x08\xd40$RQ\x1c9]\x96H\xbe\xc8w\x14\xb4\xf3\x15uɶZ" - pub_shares: "\x8cd(\xb3\xf8\"\x08\xb6dj\xde\x1e̚ \xbe(\xf2\xf2h\x1b\x92\x8f\xbb\xfde\xef\xec\xfbB\x81?|\xcc\x1e;ϭ\x9e6\x1c)\xe3?\x12\xf5\x9c\x8a" - pub_shares: "\xa0\x8cě\xd4\xcbI\x8c1i\xf5,?\xd99\xc1m\x10\xd2\x12\x13\x92\x0c(2j\xda\x1a\x85|c\xec\xed\xf0\xf5\x96\xbe\xd9u\xa8\xbd\t\x90\xd6\xff\xb0s\xb4" - pub_shares: "\xa3\xf6\xce^\xed\xcex\xb8`|\xb6\xd8\xe3\xf2\xba \xb6t:\xads\xf0(\x10\xd9\xd0z\xc8\x16\x0c\xc1,\xb1\xa5\xf6\x9fhTz\x85\xcao\x08\xb4B\xfa\x0e\xc8" - fee_recipient_address: "0x4c7215a3b539eb1e5849c6077dbb5722f5717a28" - withdrawal_address: "0x0b4b373970115e82ed6f4125c8fa7311e4d7defa" - builder_registration_json: "{\"version\":\"v1\",\"v1\":{\"message\":{\"fee_recipient\":\"0x4c7215a3B539eb1e5849C6077Dbb5722F5717a28\",\"gas_limit\":\"30000000\",\"timestamp\":\"1616508000\",\"pubkey\":\"0xacff76ec0c853e376d49a30b6fb9aaec53d265f5d1248d5dbd0f074a5a537b0ac2524c8875d522e32db84474f52497be\"},\"signature\":\"0x8dde908a5e0e0ceaa9043d0460954c20114feef74c8f7ec2af582993c343aeb58006439ac9591bdb80b124f4eba2b3b70ffdb4460ebed8cc29461b67e08e83bd4075cf770bda7ef4c610602d1cd45b85822386dc13d4af5d24ad7d0ed316a3b4\"}}" -} -validators: { - public_key: "\x87\x9e\xcdt\x84\xe1ճ\xe7\x08\x06\x88\x15\x18$\x8e\"\xc3\x03O\xd3\n\x9b\xb0\x94\xb2\x9b" - pub_shares: "\xa8\x16\xa7}\xbb\x19\x90!\x00\xff\xb6\xe9\xae\xecI\x90z\x0b\xbb\xe1L\x1d}\xa7_\xfb\xba\x8a\xb9\xc9PT\x15\xab\xaaIA\xaa\xb4\xd3\xe2VO\xcb)\x13\xd5\xd8" - pub_shares: "\x98\xea\xeb\xe6\xe1\xf2K\xb7W#5]\xf5\xc37\xcd\x15B\x13\x13\x11\x02\xe5\x9c\x17\x82ʞu\xf2\xb7\xa3\x15\xd8\xecg\x84\xfd(\x98\xe0\xb0\xf1L\xfb\xc3U\xf6" - pub_shares: "\xb7*4\xd83\x9f&\xdbϦ\x80\xca\xcd\x05\x9b(Kq\x9aFV\x1c\xaa\xd8J\n\xa9\x14r\x13\x9e\xa9\x98`\xe06\xa5\x88\xe3\xf9\xd7\xd8\x1d4\xd1\x1b\x18\x08" - pub_shares: "\x80\xf1\x8a7\xae\x95\x1b\xc6e\x8b#\xb7B\x0eh\x97*Pq\xdf^\x08\xe3a\xe4ÿ/˿\xa5\xadtͪ cluster.manifestpb.v1.Operator - 5, // 1: cluster.manifestpb.v1.Cluster.validators:type_name -> cluster.manifestpb.v1.Validator - 9, // 2: cluster.manifestpb.v1.Mutation.data:type_name -> google.protobuf.Any - 1, // 3: cluster.manifestpb.v1.SignedMutation.mutation:type_name -> cluster.manifestpb.v1.Mutation - 2, // 4: cluster.manifestpb.v1.SignedMutationList.mutations:type_name -> cluster.manifestpb.v1.SignedMutation - 5, // 5: cluster.manifestpb.v1.ValidatorList.validators:type_name -> cluster.manifestpb.v1.Validator - 6, // [6:6] is the sub-list for method output_type - 6, // [6:6] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name -} - -func init() { file_cluster_manifestpb_v1_manifest_proto_init() } -func file_cluster_manifestpb_v1_manifest_proto_init() { - if File_cluster_manifestpb_v1_manifest_proto != nil { - return - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_cluster_manifestpb_v1_manifest_proto_rawDesc), len(file_cluster_manifestpb_v1_manifest_proto_rawDesc)), - NumEnums: 0, - NumMessages: 9, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_cluster_manifestpb_v1_manifest_proto_goTypes, - DependencyIndexes: file_cluster_manifestpb_v1_manifest_proto_depIdxs, - MessageInfos: file_cluster_manifestpb_v1_manifest_proto_msgTypes, - }.Build() - File_cluster_manifestpb_v1_manifest_proto = out.File - file_cluster_manifestpb_v1_manifest_proto_goTypes = nil - file_cluster_manifestpb_v1_manifest_proto_depIdxs = nil -} diff --git a/cluster/manifestpb/v1/manifest.proto b/cluster/manifestpb/v1/manifest.proto deleted file mode 100644 index 067af4fb80..0000000000 --- a/cluster/manifestpb/v1/manifest.proto +++ /dev/null @@ -1,70 +0,0 @@ -syntax = "proto3"; - -package cluster.manifestpb.v1; - -import "google/protobuf/any.proto"; - -option go_package = "github.com/obolnetwork/charon/cluster/manifestpb/v1"; - -// Cluster represents the manifest of a cluster after applying a sequence of mutations. -message Cluster { - bytes initial_mutation_hash = 1; // InitialMutationHash is the hash of first signed mutation, uniquely identifying cluster, aka "cluster hash". It must be 32 bytes. - bytes latest_mutation_hash = 2; // LatestMutationHash is the hash of last signed mutation, identifying this specific cluster iteration. It must be 32 bytes. - string name = 3; // Name is the name of the cluster. - int32 threshold = 4; // Threshold is the threshold of the cluster. - string dkg_algorithm = 5; // DKGAlgorithm is the DKG algorithm used to create the validator keys of the cluster. - bytes fork_version = 6; // ForkVersion is the fork version (network/chain) of the cluster. It must be 4 bytes. - repeated Operator operators = 7; // Operators is the list of operators of the cluster. - repeated Validator validators = 8; // Validators is the list of validators of the cluster. - string consensus_protocol = 9; // ConsensusProtocol is the consensus protocol name preferred by the cluster, e.g. "abft". - uint32 target_gas_limit = 10; // TargetGasLimit is the custom target gas limit for transactions. - bool compounding = 11; // Compounding is the flag to enable compounding rewards. -} - -// Mutation mutates the cluster manifest. -message Mutation { - bytes parent = 1; // Parent is the hash of the parent mutation. It must be 32 bytes. - string type = 2; // Type is the type of mutation. - google.protobuf.Any data = 3; // Data is the data of the mutation. Must be non-nil. -} - -// SignedMutation is a mutation signed by a signer. -message SignedMutation { - Mutation mutation = 1; // Mutation is the mutation. - bytes signer = 2; // Signer is the identity (public key) of the signer. - bytes signature = 3; // Signature is the signature of the mutation. -} - -// SignedMutationList is a list of signed mutations. -message SignedMutationList { - repeated SignedMutation mutations = 1; // Mutations is the list of mutations. -} - -// Operator represents the operator of a node in the cluster. -message Operator { - string address = 1; // Address is the operator's Ethereum address. - string enr = 2; // enr identifies the operator's charon node. -} - -// Validator represents a distributed validator managed by the DV cluster. -message Validator { - bytes public_key = 1; // PublicKey is the group public key of the validator. - repeated bytes pub_shares = 2; // PubShares is the ordered list of public shares of the validator. - string fee_recipient_address = 3; // FeeRecipientAddress is the fee recipient Ethereum address of the validator. - string withdrawal_address = 4; // WithdrawalAddress is the withdrawal Ethereum address of the validator. - bytes builder_registration_json = 5; // BuilderRegistration is the pre-generated json-formatted builder-API validator registration of the validator. -} - -// ValidatorList is a list of validators. -message ValidatorList { - repeated Validator validators = 1; // Validators is the list of validators. -} - -// LegacyLock represents a json formatted legacy cluster lock file. -message LegacyLock { - bytes json = 1; -} - -// Empty is an empty/noop message. -message Empty {} - diff --git a/cmd/combine/combine.go b/cmd/combine/combine.go index 277edeeb08..789e0811de 100644 --- a/cmd/combine/combine.go +++ b/cmd/combine/combine.go @@ -14,8 +14,6 @@ import ( "github.com/obolnetwork/charon/app/log" "github.com/obolnetwork/charon/app/z" "github.com/obolnetwork/charon/cluster" - "github.com/obolnetwork/charon/cluster/manifest" - manifestpb "github.com/obolnetwork/charon/cluster/manifestpb/v1" "github.com/obolnetwork/charon/eth2util" "github.com/obolnetwork/charon/eth2util/keystore" "github.com/obolnetwork/charon/tbls" @@ -69,7 +67,7 @@ func Combine(ctx context.Context, inputDir, outputDir string, force, noverify bo eth1Cl := eth1wrap.NewDefaultEthClientRunner(executionEngineAddr) go eth1Cl.Run(ctx) - cluster, possibleKeyPaths, err := loadManifest(ctx, inputDir, noverify, eth1Cl) + lock, possibleKeyPaths, err := loadManifest(ctx, inputDir, noverify, eth1Cl) if err != nil { return errors.Wrap(err, "open manifest file") } @@ -99,31 +97,31 @@ func Combine(ctx context.Context, inputDir, outputDir string, force, noverify bo for valIdx := range len(privkeys) { pkSet := privkeys[valIdx] - if len(pkSet) < int(cluster.GetThreshold()) { + if len(pkSet) < lock.Threshold { return errors.New( "insufficient private key shares found for validator", z.Int("validator_index", valIdx), - z.Int("expected", int(cluster.GetThreshold())), + z.Int("expected", lock.Threshold), z.Int("actual", len(pkSet)), ) } log.Info(ctx, "Recombining private key shares", z.Int("validator_index", valIdx)) - shares, err := shareIdxByPubkeys(cluster, pkSet, valIdx) + shares, err := shareIdxByPubkeys(lock, pkSet, valIdx) if err != nil { return err } - secret, err := tbls.RecoverSecret(shares, uint(len(cluster.GetOperators())), uint(cluster.GetThreshold())) + secret, err := tbls.RecoverSecret(shares, uint(len(lock.Operators)), uint(lock.Threshold)) if err != nil { return errors.Wrap(err, "recover private key share", z.Int("validator_index", valIdx)) } // require that the generated secret pubkey matches what's in the lockfile for the valIdx validator - val := cluster.GetValidators()[valIdx] + val := lock.Validators[valIdx] - valPk, err := tblsconv.PubkeyFromBytes(val.GetPublicKey()) + valPk, err := tblsconv.PubkeyFromBytes(val.PubKey) if err != nil { return errors.Wrap(err, "public key for validator from manifest", z.Int("validator_index", valIdx)) } @@ -181,11 +179,11 @@ func Combine(ctx context.Context, inputDir, outputDir string, force, noverify bo // shareIdxByPubkeys maps private keys to the valIndex validator public shares in the manifest file. // It preserves the order as found in the validator public share slice. -func shareIdxByPubkeys(cluster *manifestpb.Cluster, secrets []tbls.PrivateKey, valIndex int) (map[int]tbls.PrivateKey, error) { +func shareIdxByPubkeys(lock *cluster.Lock, secrets []tbls.PrivateKey, valIndex int) (map[int]tbls.PrivateKey, error) { pubkMap := make(map[tbls.PublicKey]int) - for peerIdx := range len(cluster.GetValidators()[valIndex].GetPubShares()) { - pubShareRaw := cluster.GetValidators()[valIndex].GetPubShares()[peerIdx] + for peerIdx := range len(lock.Validators[valIndex].PubShares) { + pubShareRaw := lock.Validators[valIndex].PubShares[peerIdx] pubShare, err := tblsconv.PubkeyFromBytes(pubShareRaw) if err != nil { @@ -236,7 +234,7 @@ type options struct { // loadManifest will fail if some of the directories contain a different set of manifest and lock file. // For example, if 3 out of 4 directories contain both manifest and lock file, and the fourth only contains lock, loadManifest will return error. // It returns the v1.Cluster read from the manifest, and a list of directories that possibly contains keys. -func loadManifest(ctx context.Context, dir string, noverify bool, eth1Cl eth1wrap.EthClientRunner) (*manifestpb.Cluster, []string, error) { +func loadManifest(ctx context.Context, dir string, noverify bool, eth1Cl eth1wrap.EthClientRunner) (*cluster.Lock, []string, error) { root, err := os.ReadDir(dir) if err != nil { return nil, nil, errors.Wrap(err, "read directory") @@ -244,7 +242,7 @@ func loadManifest(ctx context.Context, dir string, noverify bool, eth1Cl eth1wra var ( possibleValKeysDir []string - lastCluster *manifestpb.Cluster + lastCluster *cluster.Lock ) for _, sd := range root { @@ -262,17 +260,14 @@ func loadManifest(ctx context.Context, dir string, noverify bool, eth1Cl eth1wra // try opening the lock file lockFile := filepath.Join(dir, sd.Name(), "cluster-lock.json") - manifestFile := filepath.Join(dir, sd.Name(), "cluster-manifest.pb") - cl, err := manifest.LoadCluster(manifestFile, lockFile, func(lock cluster.Lock) error { - return verifyLock(ctx, lock, noverify, eth1Cl) - }) + cl, err := cluster.LoadClusterLock(ctx, lockFile, noverify, eth1Cl) if err != nil { return nil, nil, errors.Wrap(err, "manifest load error", z.Str("name", sd.Name())) } if !noverify { - if lastCluster != nil && !bytes.Equal(lastCluster.GetLatestMutationHash(), cl.GetLatestMutationHash()) { + if lastCluster != nil && !bytes.Equal(lastCluster.LockHash, cl.LockHash) { return nil, nil, errors.New("mismatching last mutation hash") } } @@ -288,19 +283,3 @@ func loadManifest(ctx context.Context, dir string, noverify bool, eth1Cl eth1wra return lastCluster, possibleValKeysDir, nil } - -func verifyLock(ctx context.Context, lock cluster.Lock, noverify bool, eth1Cl eth1wrap.EthClientRunner) error { - if err := lock.VerifyHashes(); err != nil && !noverify { - return errors.Wrap(err, "verify cluster lock hashes (run with --no-verify to bypass verification at own risk)") - } else if err != nil && noverify { - log.Warn(ctx, "Ignoring failed cluster lock hash verification due to --no-verify flag", err) - } - - if err := lock.VerifySignatures(eth1Cl); err != nil && !noverify { - return errors.Wrap(err, "verify cluster lock signatures (run with --no-verify to bypass verification at own risk)") - } else if err != nil && noverify { - log.Warn(ctx, "Ignoring failed cluster lock signature verification due to --no-verify flag", err) - } - - return nil -} diff --git a/cmd/combine/combine_test.go b/cmd/combine/combine_test.go index dfd429e953..3f1cb72458 100644 --- a/cmd/combine/combine_test.go +++ b/cmd/combine/combine_test.go @@ -14,11 +14,8 @@ import ( "testing" "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" "github.com/obolnetwork/charon/cluster" - "github.com/obolnetwork/charon/cluster/manifest" - manifestpb "github.com/obolnetwork/charon/cluster/manifestpb/v1" "github.com/obolnetwork/charon/cmd/combine" "github.com/obolnetwork/charon/eth2util" "github.com/obolnetwork/charon/eth2util/keystore" @@ -92,18 +89,6 @@ func TestCombineCannotLoadKeystore(t *testing.T) { require.ErrorContains(t, err, "insufficient private key shares found for validator") } -func TestCombineAllManifest(t *testing.T) { - seed := 0 - random := rand.New(rand.NewSource(int64(seed))) - lock, _, shares := cluster.NewForT(t, 100, 3, 4, seed, random) - combineTest(t, lock, shares, false, false, noLockModif, []manifestChoice{ - ManifestOnly, - ManifestOnly, - ManifestOnly, - ManifestOnly, - }, eth2util.Network{}) -} - func TestCombineCustomNetworkFork(t *testing.T) { seed := 0 random := rand.New(rand.NewSource(int64(seed))) @@ -120,31 +105,7 @@ func TestCombineCustomNetworkFork(t *testing.T) { lock, _, shares := cluster.NewForT(t, 100, 3, 4, seed, random, func(definition *cluster.Definition) { definition.ForkVersion = []byte{0xca, 0xfe, 0xba, 0xbe} }) - combineTest(t, lock, shares, false, false, noLockModif, nil, customNetwork) -} - -func TestCombineBothManifestAndLockForAll(t *testing.T) { - seed := 0 - random := rand.New(rand.NewSource(int64(seed))) - lock, _, shares := cluster.NewForT(t, 100, 3, 4, seed, random) - combineTest(t, lock, shares, false, false, noLockModif, []manifestChoice{ - Both, - Both, - Both, - Both, - }, eth2util.Network{}) -} - -func TestCombineBothManifestAndLockForSome(t *testing.T) { - seed := 0 - random := rand.New(rand.NewSource(int64(seed))) - lock, _, shares := cluster.NewForT(t, 100, 3, 4, seed, random) - combineTest(t, lock, shares, false, false, noLockModif, []manifestChoice{ - ManifestOnly, - Both, - Both, - LockOnly, - }, eth2util.Network{}) + combineTest(t, lock, shares, false, false, noLockModif, customNetwork) } // This test exists because of https://github.com/ObolNetwork/charon/issues/2151. @@ -152,21 +113,21 @@ func TestCombineLotsOfVals(t *testing.T) { seed := 0 random := rand.New(rand.NewSource(int64(seed))) lock, _, shares := cluster.NewForT(t, 100, 3, 4, seed, random) - combineTest(t, lock, shares, false, false, noLockModif, nil, eth2util.Network{}) + combineTest(t, lock, shares, false, false, noLockModif, eth2util.Network{}) } func TestCombine(t *testing.T) { seed := 0 random := rand.New(rand.NewSource(int64(seed))) lock, _, shares := cluster.NewForT(t, 2, 3, 4, seed, random) - combineTest(t, lock, shares, false, false, noLockModif, nil, eth2util.Network{}) + combineTest(t, lock, shares, false, false, noLockModif, eth2util.Network{}) } func TestCombineNoVerifyGoodLock(t *testing.T) { seed := 0 random := rand.New(rand.NewSource(int64(seed))) lock, _, shares := cluster.NewForT(t, 2, 3, 4, seed, random) - combineTest(t, lock, shares, true, false, noLockModif, nil, eth2util.Network{}) + combineTest(t, lock, shares, true, false, noLockModif, eth2util.Network{}) } func TestCombineNoVerifyBadLock(t *testing.T) { @@ -179,7 +140,7 @@ func TestCombineNoVerifyBadLock(t *testing.T) { } return src - }, nil, eth2util.Network{}) + }, eth2util.Network{}) } func TestCombineBadLock(t *testing.T) { @@ -192,7 +153,7 @@ func TestCombineBadLock(t *testing.T) { } return src - }, nil, eth2util.Network{}) + }, eth2util.Network{}) } func TestCombineNoVerifyDifferentValidatorData(t *testing.T) { @@ -205,33 +166,7 @@ func TestCombineNoVerifyDifferentValidatorData(t *testing.T) { } return src - }, nil, eth2util.Network{}) -} - -type manifestChoice int - -const ( - ManifestOnly manifestChoice = iota - LockOnly - Both -) - -func writeManifest( - t *testing.T, - valIdx int, - modifyLockFile func(valIndex int, src cluster.Lock) cluster.Lock, - path string, - lock cluster.Lock, -) { - t.Helper() - legacy, err := manifest.NewLegacyLockForT(t, modifyLockFile(valIdx, lock)) - require.NoError(t, err) - - dag := &manifestpb.SignedMutationList{Mutations: []*manifestpb.SignedMutation{legacy}} - data, err := proto.Marshal(dag) - require.NoError(t, err) - - require.NoError(t, os.WriteFile(filepath.Join(path, "cluster-manifest.pb"), data, 0o755)) + }, eth2util.Network{}) } func writeLock( @@ -256,7 +191,6 @@ func combineTest( noVerify bool, wantErr bool, modifyLockFile func(valIndex int, src cluster.Lock) cluster.Lock, - manifestOrLock []manifestChoice, testnetConfig eth2util.Network, ) { t.Helper() @@ -318,21 +252,7 @@ func combineTest( require.NoError(t, os.Mkdir(vk, 0o755)) require.NoError(t, keystore.StoreKeysInsecure(keys, vk, keystore.ConfirmInsecureKeys)) - if len(manifestOrLock) == 0 { - // default to lockfile - writeLock(t, idx, modifyLockFile, ep, lock) - continue - } - - switch manifestOrLock[idx] { - case ManifestOnly: - writeManifest(t, idx, modifyLockFile, ep, lock) - case LockOnly: - writeLock(t, idx, modifyLockFile, ep, lock) - case Both: - writeManifest(t, idx, modifyLockFile, ep, lock) - writeLock(t, idx, modifyLockFile, ep, lock) - } + writeLock(t, idx, modifyLockFile, ep, lock) } err := combine.Combine(context.Background(), dir, od, true, noVerify, "", testnetConfig, combine.WithInsecureKeysForT(t)) diff --git a/cmd/depositfetch.go b/cmd/depositfetch.go index 7f39c167ce..0c4d6d39e9 100644 --- a/cmd/depositfetch.go +++ b/cmd/depositfetch.go @@ -15,6 +15,7 @@ import ( "github.com/obolnetwork/charon/app/log" "github.com/obolnetwork/charon/app/obolapi" "github.com/obolnetwork/charon/app/z" + "github.com/obolnetwork/charon/cluster" "github.com/obolnetwork/charon/eth2util" "github.com/obolnetwork/charon/eth2util/deposit" ) @@ -56,7 +57,7 @@ func bindDepositFetchFlags(cmd *cobra.Command, config *depositFetchConfig) { } func runDepositFetch(ctx context.Context, config depositFetchConfig) error { - cl, err := loadClusterLock(config.LockFilePath) + cl, err := cluster.LoadClusterLockAndVerify(ctx, config.LockFilePath) if err != nil { return err } @@ -71,7 +72,7 @@ func runDepositFetch(ctx context.Context, config depositFetchConfig) error { for _, pubkey := range config.ValidatorPublicKeys { log.Info(ctx, "Fetching full deposit message", z.Str("validator_pubkey", pubkey)) - dd, err := oAPI.GetFullDeposit(ctx, pubkey, cl.GetInitialMutationHash(), int(cl.GetThreshold())) + dd, err := oAPI.GetFullDeposit(ctx, pubkey, cl.LockHash, cl.Threshold) if err != nil { return errors.Wrap(err, "fetch full deposit data from Obol API") } @@ -94,7 +95,7 @@ func runDepositFetch(ctx context.Context, config depositFetchConfig) error { return errors.Wrap(err, "create deposit data dir") } - network, err := eth2util.ForkVersionToNetwork(cl.GetForkVersion()) + network, err := eth2util.ForkVersionToNetwork(cl.ForkVersion) if err != nil { return err } diff --git a/cmd/depositfetch_internal_test.go b/cmd/depositfetch_internal_test.go index 9f8628600c..325937adb2 100644 --- a/cmd/depositfetch_internal_test.go +++ b/cmd/depositfetch_internal_test.go @@ -111,7 +111,7 @@ func TestDepositFetchCLI(t *testing.T) { }{ { name: "correct flags", - expectedErr: "load cluster lock: load dag from disk: no file found", + expectedErr: "read cluster-lock.json: open test: no such file or directory", flags: []string{ "--validator-public-keys=test", "--private-key-file=test", diff --git a/cmd/depositsign.go b/cmd/depositsign.go index 02cdd232a6..b83cff260a 100644 --- a/cmd/depositsign.go +++ b/cmd/depositsign.go @@ -16,6 +16,7 @@ import ( "github.com/obolnetwork/charon/app/log" "github.com/obolnetwork/charon/app/obolapi" "github.com/obolnetwork/charon/app/z" + "github.com/obolnetwork/charon/cluster" "github.com/obolnetwork/charon/core" "github.com/obolnetwork/charon/eth2util" "github.com/obolnetwork/charon/eth2util/deposit" @@ -67,7 +68,7 @@ func runDepositSign(ctx context.Context, config depositSignConfig) error { return errors.Wrap(err, "load identity key", z.Str("private_key_path", config.PrivateKeyPath)) } - cl, err := loadClusterLock(config.LockFilePath) + cl, err := cluster.LoadClusterLockAndVerify(ctx, config.LockFilePath) if err != nil { return err } @@ -77,7 +78,7 @@ func runDepositSign(ctx context.Context, config depositSignConfig) error { return errors.Wrap(err, "create Obol API client", z.Str("publish_address", config.PublishAddress)) } - shareIdx, err := keystore.ShareIdxForCluster(cl, *identityKey.PubKey()) + shareIdx, err := keystore.ShareIdxForCluster(*cl, *identityKey.PubKey()) if err != nil { return errors.Wrap(err, "determine operator index from cluster lock for supplied identity key") } @@ -100,7 +101,7 @@ func runDepositSign(ctx context.Context, config depositSignConfig) error { return errors.Wrap(err, "load keystore") } - shares, err := keystore.KeysharesToValidatorPubkey(cl, valKeys) + shares, err := keystore.KeysharesToValidatorPubkey(*cl, valKeys) if err != nil { return errors.Wrap(err, "match local validator key shares with their counterparty in cluster lock") } @@ -131,18 +132,18 @@ func runDepositSign(ctx context.Context, config depositSignConfig) error { depositDatas := []eth2p0.DepositData{} - network, err := eth2util.ForkVersionToNetwork(cl.GetForkVersion()) + network, err := eth2util.ForkVersionToNetwork(cl.ForkVersion) if err != nil { return err } for i, pubkey := range pubkeys { for _, amount := range config.DepositAmounts { - if !cl.GetCompounding() && (amount < 1 || amount > 32) { + if !cl.Compounding && (amount < 1 || amount > 32) { return errors.New("deposit amount must be between 1 and 32 ETH", z.U64("amount", uint64(amount))) } - if cl.GetCompounding() && (amount < 1 || amount > 2048) { + if cl.Compounding && (amount < 1 || amount > 2048) { return errors.New("deposit amount must be between 1 and 2048 ETH", z.U64("amount", uint64(amount))) } @@ -187,7 +188,7 @@ func runDepositSign(ctx context.Context, config depositSignConfig) error { log.Info(ctx, "Submitting partial deposit message") - err = oAPI.PostPartialDeposits(ctx, cl.GetInitialMutationHash(), shareIdx, depositDatas) + err = oAPI.PostPartialDeposits(ctx, cl.LockHash, shareIdx, depositDatas) if err != nil { return errors.Wrap(err, "submit partial deposit data to Obol API") } diff --git a/cmd/exit_broadcast.go b/cmd/exit_broadcast.go index 50841413a1..6fa8a298b6 100644 --- a/cmd/exit_broadcast.go +++ b/cmd/exit_broadcast.go @@ -23,7 +23,7 @@ import ( "github.com/obolnetwork/charon/app/log" "github.com/obolnetwork/charon/app/obolapi" "github.com/obolnetwork/charon/app/z" - manifestpb "github.com/obolnetwork/charon/cluster/manifestpb/v1" + "github.com/obolnetwork/charon/cluster" "github.com/obolnetwork/charon/core" "github.com/obolnetwork/charon/eth2util" "github.com/obolnetwork/charon/eth2util/keystore" @@ -124,7 +124,7 @@ func runBcastFullExit(ctx context.Context, config exitConfig) error { return errors.Wrap(err, "load identity key") } - cl, err := loadClusterLock(config.LockFilePath) + cl, err := cluster.LoadClusterLockAndVerify(ctx, config.LockFilePath) if err != nil { return err } @@ -134,7 +134,7 @@ func runBcastFullExit(ctx context.Context, config exitConfig) error { return err } - eth2Cl, err := eth2Client(ctx, config.FallbackBeaconNodeAddrs, beaconNodeHeaders, config.BeaconNodeEndpoints, config.BeaconNodeTimeout, [4]byte(cl.GetForkVersion())) + eth2Cl, err := eth2Client(ctx, config.FallbackBeaconNodeAddrs, beaconNodeHeaders, config.BeaconNodeEndpoints, config.BeaconNodeTimeout, [4]byte(cl.ForkVersion)) if err != nil { return errors.Wrap(err, "create eth2 client for specified beacon node(s)", z.Any("beacon_nodes_endpoints", config.BeaconNodeEndpoints)) } @@ -168,8 +168,8 @@ func runBcastFullExit(ctx context.Context, config exitConfig) error { fullExits[validatorPubKey] = exit } } else { - for _, validator := range cl.GetValidators() { - validatorPubKeyHex := fmt.Sprintf("0x%x", validator.GetPublicKey()) + for _, validator := range cl.Validators { + validatorPubKeyHex := validator.PublicKeyHex() valCtx := log.WithCtx(ctx, z.Str("validator_public_key", validatorPubKeyHex)) @@ -183,7 +183,7 @@ func runBcastFullExit(ctx context.Context, config exitConfig) error { return errors.Wrap(err, "fetch full exit for all validators from public key") } - validatorPubKey, err := core.PubKeyFromBytes(validator.GetPublicKey()) + validatorPubKey, err := core.PubKeyFromBytes(validator.PubKey) if err != nil { return errors.Wrap(err, "convert public key for validator") } @@ -233,7 +233,7 @@ func validatorPubKeyFromFileName(fileName string) (core.PubKey, error) { return validatorPubKey, nil } -func fetchFullExit(ctx context.Context, exitFilePath string, config exitConfig, cl *manifestpb.Cluster, identityKey *k1.PrivateKey, validatorPubKey string) (eth2p0.SignedVoluntaryExit, error) { +func fetchFullExit(ctx context.Context, exitFilePath string, config exitConfig, cl *cluster.Lock, identityKey *k1.PrivateKey, validatorPubKey string) (eth2p0.SignedVoluntaryExit, error) { var ( fullExit eth2p0.SignedVoluntaryExit err error @@ -299,18 +299,18 @@ func broadcastExitsToBeacon(ctx context.Context, eth2Cl eth2wrap.Client, exits m } // exitFromObolAPI fetches an eth2p0.SignedVoluntaryExit message from publishAddr for the given validatorPubkey. -func exitFromObolAPI(ctx context.Context, validatorPubkey, publishAddr string, publishTimeout time.Duration, cl *manifestpb.Cluster, identityKey *k1.PrivateKey) (eth2p0.SignedVoluntaryExit, error) { +func exitFromObolAPI(ctx context.Context, validatorPubkey, publishAddr string, publishTimeout time.Duration, cl *cluster.Lock, identityKey *k1.PrivateKey) (eth2p0.SignedVoluntaryExit, error) { oAPI, err := obolapi.New(publishAddr, obolapi.WithTimeout(publishTimeout)) if err != nil { return eth2p0.SignedVoluntaryExit{}, errors.Wrap(err, "create Obol API client", z.Str("publish_address", publishAddr)) } - shareIdx, err := keystore.ShareIdxForCluster(cl, *identityKey.PubKey()) + shareIdx, err := keystore.ShareIdxForCluster(*cl, *identityKey.PubKey()) if err != nil { return eth2p0.SignedVoluntaryExit{}, errors.Wrap(err, "determine operator index from cluster lock for supplied identity key") } - fullExit, err := oAPI.GetFullExit(ctx, validatorPubkey, cl.GetInitialMutationHash(), shareIdx, identityKey) + fullExit, err := oAPI.GetFullExit(ctx, validatorPubkey, cl.LockHash, shareIdx, identityKey) if err != nil { return eth2p0.SignedVoluntaryExit{}, errors.Wrap(err, "load full exit data from Obol API", z.Str("publish_address", publishAddr)) } diff --git a/cmd/exit_broadcast_internal_test.go b/cmd/exit_broadcast_internal_test.go index 6850d33628..f612011bf2 100644 --- a/cmd/exit_broadcast_internal_test.go +++ b/cmd/exit_broadcast_internal_test.go @@ -18,7 +18,6 @@ import ( "github.com/stretchr/testify/require" "github.com/obolnetwork/charon/cluster" - "github.com/obolnetwork/charon/cluster/manifest" "github.com/obolnetwork/charon/tbls" "github.com/obolnetwork/charon/testutil" "github.com/obolnetwork/charon/testutil/beaconmock" @@ -80,11 +79,6 @@ func testRunBcastFullExitCmdFlow(t *testing.T, fromFile bool, all bool) { } } - dag, err := manifest.NewDAGFromLockForT(t, lock) - require.NoError(t, err) - cl, err := manifest.Materialise(dag) - require.NoError(t, err) - mBytes, err := json.Marshal(lock) require.NoError(t, err) @@ -171,7 +165,7 @@ func testRunBcastFullExitCmdFlow(t *testing.T, fromFile bool, all bool) { if all { for _, validator := range lock.Validators { validatorPublicKey := validator.PublicKeyHex() - exit, err := exitFromObolAPI(ctx, validatorPublicKey, srv.URL, 10*time.Second, cl, enrs[0]) + exit, err := exitFromObolAPI(ctx, validatorPublicKey, srv.URL, 10*time.Second, &lock, enrs[0]) require.NoError(t, err) exitBytes, err := json.Marshal(exit) @@ -184,7 +178,7 @@ func testRunBcastFullExitCmdFlow(t *testing.T, fromFile bool, all bool) { config.ExitFromFileDir = baseDir } else { validatorPublicKey := lock.Validators[0].PublicKeyHex() - exit, err := exitFromObolAPI(ctx, validatorPublicKey, srv.URL, 10*time.Second, cl, enrs[0]) + exit, err := exitFromObolAPI(ctx, validatorPublicKey, srv.URL, 10*time.Second, &lock, enrs[0]) require.NoError(t, err) exitBytes, err := json.Marshal(exit) @@ -222,7 +216,7 @@ func Test_runBcastFullExitCmd_Config(t *testing.T) { { name: "No lock", noLock: true, - errData: "load cluster lock", + errData: "no such file or directory", }, { name: "Bad Obol API URL", diff --git a/cmd/exit_delete.go b/cmd/exit_delete.go index 6e09a822ae..f2486b7d3d 100644 --- a/cmd/exit_delete.go +++ b/cmd/exit_delete.go @@ -14,6 +14,7 @@ import ( "github.com/obolnetwork/charon/app/log" "github.com/obolnetwork/charon/app/obolapi" "github.com/obolnetwork/charon/app/z" + "github.com/obolnetwork/charon/cluster" "github.com/obolnetwork/charon/core" "github.com/obolnetwork/charon/eth2util" "github.com/obolnetwork/charon/eth2util/keystore" @@ -87,7 +88,7 @@ func runDeleteExit(ctx context.Context, config exitConfig) error { return errors.Wrap(err, "load identity key", z.Str("private_key_path", config.PrivateKeyPath)) } - cl, err := loadClusterLock(config.LockFilePath) + cl, err := cluster.LoadClusterLockAndVerify(ctx, config.LockFilePath) if err != nil { return err } @@ -97,20 +98,20 @@ func runDeleteExit(ctx context.Context, config exitConfig) error { return errors.Wrap(err, "create Obol API client", z.Str("publish_address", config.PublishAddress)) } - shareIdx, err := keystore.ShareIdxForCluster(cl, *identityKey.PubKey()) + shareIdx, err := keystore.ShareIdxForCluster(*cl, *identityKey.PubKey()) if err != nil { return errors.Wrap(err, "determine operator index from cluster lock for supplied identity key") } if config.All { - for _, validator := range cl.GetValidators() { - validatorPubKeyHex := fmt.Sprintf("0x%x", validator.GetPublicKey()) + for _, validator := range cl.Validators { + validatorPubKeyHex := validator.PublicKeyHex() valCtx := log.WithCtx(ctx, z.Str("validator", validatorPubKeyHex)) log.Info(ctx, "Deleting partial exit message") - err := oAPI.DeletePartialExit(valCtx, validatorPubKeyHex, cl.GetInitialMutationHash(), shareIdx, identityKey) + err := oAPI.DeletePartialExit(valCtx, validatorPubKeyHex, cl.LockHash, shareIdx, identityKey) if err != nil { if errors.Is(err, obolapi.ErrNoValue) { log.Warn(ctx, fmt.Sprintf("partial exit data from Obol API for validator %v not available (exit may not have been submitted)", validatorPubKeyHex), nil) @@ -130,7 +131,7 @@ func runDeleteExit(ctx context.Context, config exitConfig) error { log.Info(ctx, "Deleting partial exit message") - err := oAPI.DeletePartialExit(ctx, config.ValidatorPubkey, cl.GetInitialMutationHash(), shareIdx, identityKey) + err := oAPI.DeletePartialExit(ctx, config.ValidatorPubkey, cl.LockHash, shareIdx, identityKey) if err != nil { return errors.Wrap(err, "delete partial exit data from Obol API", z.Str("validator_public_key", config.ValidatorPubkey)) } diff --git a/cmd/exit_fetch.go b/cmd/exit_fetch.go index 7f3923b4b2..821762750d 100644 --- a/cmd/exit_fetch.go +++ b/cmd/exit_fetch.go @@ -17,6 +17,7 @@ import ( "github.com/obolnetwork/charon/app/log" "github.com/obolnetwork/charon/app/obolapi" "github.com/obolnetwork/charon/app/z" + "github.com/obolnetwork/charon/cluster" "github.com/obolnetwork/charon/core" "github.com/obolnetwork/charon/eth2util" "github.com/obolnetwork/charon/eth2util/keystore" @@ -104,7 +105,7 @@ func runFetchExit(ctx context.Context, config exitConfig) error { return errors.Wrap(err, "load identity key", z.Str("private_key_path", config.PrivateKeyPath)) } - cl, err := loadClusterLock(config.LockFilePath) + cl, err := cluster.LoadClusterLockAndVerify(ctx, config.LockFilePath) if err != nil { return err } @@ -114,20 +115,20 @@ func runFetchExit(ctx context.Context, config exitConfig) error { return errors.Wrap(err, "create Obol API client", z.Str("publish_address", config.PublishAddress)) } - shareIdx, err := keystore.ShareIdxForCluster(cl, *identityKey.PubKey()) + shareIdx, err := keystore.ShareIdxForCluster(*cl, *identityKey.PubKey()) if err != nil { return errors.Wrap(err, "determine operator index from cluster lock for supplied identity key") } if config.All { - for _, validator := range cl.GetValidators() { - validatorPubKeyHex := fmt.Sprintf("0x%x", validator.GetPublicKey()) + for _, validator := range cl.Validators { + validatorPubKeyHex := validator.PublicKeyHex() valCtx := log.WithCtx(ctx, z.Str("validator", validatorPubKeyHex)) log.Info(valCtx, "Retrieving full exit message") - fullExit, err := oAPI.GetFullExit(valCtx, validatorPubKeyHex, cl.GetInitialMutationHash(), shareIdx, identityKey) + fullExit, err := oAPI.GetFullExit(valCtx, validatorPubKeyHex, cl.LockHash, shareIdx, identityKey) if err != nil { if errors.Is(err, obolapi.ErrNoValue) { log.Warn(ctx, fmt.Sprintf("full exit data from Obol API for validator %v not available (validator may not be activated)", validatorPubKeyHex), nil) @@ -152,7 +153,7 @@ func runFetchExit(ctx context.Context, config exitConfig) error { log.Info(ctx, "Retrieving full exit message") - fullExit, err := oAPI.GetFullExit(ctx, config.ValidatorPubkey, cl.GetInitialMutationHash(), shareIdx, identityKey) + fullExit, err := oAPI.GetFullExit(ctx, config.ValidatorPubkey, cl.LockHash, shareIdx, identityKey) if err != nil { return errors.Wrap(err, "load full exit data from Obol API", z.Str("validator_public_key", config.ValidatorPubkey)) } diff --git a/cmd/exit_list.go b/cmd/exit_list.go index 11688c79dd..5c3f29aaeb 100644 --- a/cmd/exit_list.go +++ b/cmd/exit_list.go @@ -15,6 +15,7 @@ import ( "github.com/obolnetwork/charon/app/errors" "github.com/obolnetwork/charon/app/log" "github.com/obolnetwork/charon/app/z" + "github.com/obolnetwork/charon/cluster" "github.com/obolnetwork/charon/eth2util" ) @@ -89,7 +90,7 @@ func runListActiveValidatorsCmd(ctx context.Context, config exitConfig) error { } func listActiveVals(ctx context.Context, config exitConfig) ([]string, error) { - cl, err := loadClusterLock(config.LockFilePath) + cl, err := cluster.LoadClusterLockAndVerify(ctx, config.LockFilePath) if err != nil { return nil, err } @@ -106,8 +107,8 @@ func listActiveVals(ctx context.Context, config exitConfig) ([]string, error) { var allVals []eth2p0.BLSPubKey - for _, v := range cl.GetValidators() { - allVals = append(allVals, eth2p0.BLSPubKey(v.GetPublicKey())) + for _, v := range cl.Validators { + allVals = append(allVals, eth2p0.BLSPubKey(v.PubKey)) } valData, err := eth2Cl.Validators(ctx, ð2api.ValidatorsOpts{ diff --git a/cmd/exit_list_internal_test.go b/cmd/exit_list_internal_test.go index f42ddcacaa..f4448b6810 100644 --- a/cmd/exit_list_internal_test.go +++ b/cmd/exit_list_internal_test.go @@ -210,7 +210,7 @@ func TestExitListCLI(t *testing.T) { }{ { name: "check flags", - expectedErr: "load cluster lock: load dag from disk: no file found", + expectedErr: "read cluster-lock.json: open test: no such file or directory", flags: []string{ "--lock-file=test", "--beacon-node-endpoints=test1,test2", diff --git a/cmd/exit_sign.go b/cmd/exit_sign.go index f3c1e59322..04ea5224ce 100644 --- a/cmd/exit_sign.go +++ b/cmd/exit_sign.go @@ -18,6 +18,7 @@ import ( "github.com/obolnetwork/charon/app/log" "github.com/obolnetwork/charon/app/obolapi" "github.com/obolnetwork/charon/app/z" + "github.com/obolnetwork/charon/cluster" "github.com/obolnetwork/charon/core" "github.com/obolnetwork/charon/eth2util" "github.com/obolnetwork/charon/eth2util/keystore" @@ -107,7 +108,7 @@ func runSignPartialExit(ctx context.Context, config exitConfig) error { return errors.Wrap(err, "load identity key", z.Str("private_key_path", config.PrivateKeyPath)) } - cl, err := loadClusterLock(config.LockFilePath) + cl, err := cluster.LoadClusterLockAndVerify(ctx, config.LockFilePath) if err != nil { return err } @@ -122,12 +123,12 @@ func runSignPartialExit(ctx context.Context, config exitConfig) error { return errors.Wrap(err, "load keystore") } - shares, err := keystore.KeysharesToValidatorPubkey(cl, valKeys) + shares, err := keystore.KeysharesToValidatorPubkey(*cl, valKeys) if err != nil { return errors.Wrap(err, "match local validator key shares with their counterparty in cluster lock") } - shareIdx, err := keystore.ShareIdxForCluster(cl, *identityKey.PubKey()) + shareIdx, err := keystore.ShareIdxForCluster(*cl, *identityKey.PubKey()) if err != nil { return errors.Wrap(err, "determine operator index from cluster lock for supplied identity key") } @@ -142,7 +143,7 @@ func runSignPartialExit(ctx context.Context, config exitConfig) error { return err } - eth2Cl, err := eth2Client(ctx, config.FallbackBeaconNodeAddrs, beaconNodeHeaders, config.BeaconNodeEndpoints, config.BeaconNodeTimeout, [4]byte(cl.GetForkVersion())) + eth2Cl, err := eth2Client(ctx, config.FallbackBeaconNodeAddrs, beaconNodeHeaders, config.BeaconNodeEndpoints, config.BeaconNodeTimeout, [4]byte(cl.ForkVersion)) if err != nil { return errors.Wrap(err, "create eth2 client for specified beacon node(s)", z.Any("beacon_nodes_endpoints", config.BeaconNodeEndpoints)) } @@ -172,7 +173,7 @@ func runSignPartialExit(ctx context.Context, config exitConfig) error { } } - if err := oAPI.PostPartialExits(ctx, cl.GetInitialMutationHash(), shareIdx, identityKey, exitBlobs...); err != nil { + if err := oAPI.PostPartialExits(ctx, cl.LockHash, shareIdx, identityKey, exitBlobs...); err != nil { return errors.Wrap(err, "http POST partial exit message to Obol API") } diff --git a/cmd/exit_sign_internal_test.go b/cmd/exit_sign_internal_test.go index eb545e233d..ed085377d6 100644 --- a/cmd/exit_sign_internal_test.go +++ b/cmd/exit_sign_internal_test.go @@ -266,7 +266,7 @@ func Test_runSubmitPartialExit_Config(t *testing.T) { { name: "No cluster lock", noLock: true, - errData: "load cluster lock", + errData: "no such file or directory", }, { name: "No keystore", diff --git a/cmd/manifest_tools.go b/cmd/manifest_tools.go deleted file mode 100644 index b8df9993ea..0000000000 --- a/cmd/manifest_tools.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 - -package cmd - -import ( - "github.com/obolnetwork/charon/app/errors" - "github.com/obolnetwork/charon/app/z" - "github.com/obolnetwork/charon/cluster" - "github.com/obolnetwork/charon/cluster/manifest" - manifestpb "github.com/obolnetwork/charon/cluster/manifestpb/v1" -) - -// loadClusterLock loads cluster lock from disk. -func loadClusterLock(lockFilePath string) (*manifestpb.Cluster, error) { - verifyLock := func(lock cluster.Lock) error { - if err := lock.VerifyHashes(); err != nil { - return errors.Wrap(err, "verify cluster lock hashes") - } - - if err := lock.VerifySignatures(nil); err != nil { - return errors.Wrap(err, "verify cluster lock signatures") - } - - return nil - } - - cluster, err := manifest.LoadCluster("", lockFilePath, verifyLock) - if err != nil { - return nil, errors.Wrap(err, "load cluster lock", z.Str("path", lockFilePath)) - } - - return cluster, nil -} diff --git a/cmd/run.go b/cmd/run.go index c1921558bd..22e9eed5a3 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -75,7 +75,7 @@ func bindNoVerifyFlag(flags *pflag.FlagSet, config *bool) { func bindRunFlags(cmd *cobra.Command, config *app.Config) { cmd.Flags().StringVar(&config.LockFile, "lock-file", ".charon/cluster-lock.json", "The path to the cluster lock file defining the distributed validator cluster. If both cluster manifest and cluster lock files are provided, the cluster manifest file takes precedence.") - cmd.Flags().StringVar(&config.ManifestFile, "manifest-file", ".charon/cluster-manifest.pb", "The path to the cluster manifest file. If both cluster manifest and cluster lock files are provided, the cluster manifest file takes precedence.") + cmd.Flags().StringVar(&config.ManifestFile, "manifest-file", ".charon/cluster-manifest.pb", "[DEPRECATED] The path to the cluster manifest file. If both cluster manifest and cluster lock files are provided, the cluster manifest file takes precedence.") cmd.Flags().StringSliceVar(&config.BeaconNodeAddrs, "beacon-node-endpoints", nil, "Comma separated list of one or more beacon node endpoint URLs.") cmd.Flags().DurationVar(&config.BeaconNodeTimeout, "beacon-node-timeout", eth2ClientTimeout, "Timeout for the HTTP requests Charon makes to the configured beacon nodes.") cmd.Flags().DurationVar(&config.BeaconNodeSubmitTimeout, "beacon-node-submit-timeout", eth2ClientTimeout, "Timeout for the submission-related HTTP requests Charon makes to the configured beacon nodes.") diff --git a/cmd/testmev.go b/cmd/testmev.go index b63d7f7d9c..cb4573e219 100644 --- a/cmd/testmev.go +++ b/cmd/testmev.go @@ -445,6 +445,7 @@ func getBlockHeader(ctx context.Context, target string, headers map[string]strin for k, v := range headers { req.Header.Set(k, v) } + req.Header.Set("Date-Milliseconds", strconv.FormatInt(time.Now().UnixMilli(), 10)) resp, err := http.DefaultTransport.RoundTrip(req) diff --git a/cmd/testpeers.go b/cmd/testpeers.go index 9415495415..e07ae3980e 100644 --- a/cmd/testpeers.go +++ b/cmd/testpeers.go @@ -21,6 +21,7 @@ import ( "time" k1 "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/google/uuid" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" @@ -821,8 +822,9 @@ func startTCPNode(ctx context.Context, conf testPeersConfig) (host.Host, func(), slices.Sort(allENRs) allENRsString := strings.Join(allENRs, ",") allENRsHash := sha256.Sum256([]byte(allENRsString)) + allUUID := uuid.UUID(allENRsHash[:16]) - return setupP2P(ctx, p2pPrivKey, conf.P2P, peers, allENRsHash[:]) + return setupP2P(ctx, p2pPrivKey, conf.P2P, peers, allENRsHash[:], allUUID.String()) } func pingPeerOnce(ctx context.Context, p2pNode host.Host, peer p2p.Peer) (ping.Result, error) { @@ -886,7 +888,7 @@ func dialLibp2pTCPIP(ctx context.Context, address string) error { return nil } -func setupP2P(ctx context.Context, privKey *k1.PrivateKey, conf p2p.Config, peers []p2p.Peer, enrsHash []byte) (host.Host, func(), error) { +func setupP2P(ctx context.Context, privKey *k1.PrivateKey, conf p2p.Config, peers []p2p.Peer, enrsHash []byte, uuid string) (host.Host, func(), error) { var peerIDs []peer.ID for _, peer := range peers { peerIDs = append(peerIDs, peer.ID) @@ -896,7 +898,7 @@ func setupP2P(ctx context.Context, privKey *k1.PrivateKey, conf p2p.Config, peer return nil, nil, err } - relays, err := p2p.NewRelays(ctx, conf.Relays, hex.EncodeToString(enrsHash)) + relays, err := p2p.NewRelays(ctx, conf.Relays, hex.EncodeToString(enrsHash), uuid) if err != nil { return nil, nil, err } diff --git a/cmd/testpeers_internal_test.go b/cmd/testpeers_internal_test.go index ba5e24e683..bb61a908cf 100644 --- a/cmd/testpeers_internal_test.go +++ b/cmd/testpeers_internal_test.go @@ -526,7 +526,7 @@ func startPeer(t *testing.T, ctx context.Context, conf testPeersConfig, peerPriv freeTCPAddr := testutil.AvailableAddr(t) peerConf.P2P.TCPAddrs = []string{fmt.Sprintf("127.0.0.1:%v", freeTCPAddr.Port)} - relays, err := p2p.NewRelays(ctx, peerConf.P2P.Relays, "test") + relays, err := p2p.NewRelays(ctx, peerConf.P2P.Relays, "test", "test") require.NoError(t, err) hostPrivKey, err := k1util.Load(conf.PrivateKeyFile) diff --git a/core/fetcher/fetcher.go b/core/fetcher/fetcher.go index 6897c01804..a0f5562df5 100644 --- a/core/fetcher/fetcher.go +++ b/core/fetcher/fetcher.go @@ -310,6 +310,7 @@ func (f *Fetcher) fetchProposerData(ctx context.Context, slot uint64, defSet cor if proposal.Blinded { blinded = 1 } + proposalBlindedGauge.Set(blinded) resp[pubkey] = coreProposal diff --git a/core/fetcher/metrics.go b/core/fetcher/metrics.go index 9f2d9d7d67..6f0f3235d6 100644 --- a/core/fetcher/metrics.go +++ b/core/fetcher/metrics.go @@ -8,11 +8,9 @@ import ( "github.com/obolnetwork/charon/app/promauto" ) -var ( - proposalBlindedGauge = promauto.NewGauge(prometheus.GaugeOpts{ - Namespace: "core", - Subsystem: "fetcher", - Name: "proposal_blinded", - Help: "Whether the fetched proposal was blinded (1) or local (0)", - }) -) +var proposalBlindedGauge = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "core", + Subsystem: "fetcher", + Name: "proposal_blinded", + Help: "Whether the fetched proposal was blinded (1) or local (0)", +}) diff --git a/dkg/dkg.go b/dkg/dkg.go index cd2e12e0c4..f208362155 100644 --- a/dkg/dkg.go +++ b/dkg/dkg.go @@ -225,7 +225,7 @@ func Run(ctx context.Context, conf Config) (err error) { logPeerSummary(ctx, pID, peers, def.Operators) - p2pNode, shutdown, err := setupP2P(ctx, key, conf, peers, def.DefinitionHash) + p2pNode, shutdown, err := setupP2P(ctx, key, conf, peers, def.DefinitionHash, def.UUID) if err != nil { return err } @@ -1261,7 +1261,7 @@ func setRegistrationSignature(reg core.VersionedSignedValidatorRegistration, sig } // setupP2P returns a started libp2p tcp node and a shutdown function. -func setupP2P(ctx context.Context, key *k1.PrivateKey, conf Config, peers []p2p.Peer, defHash []byte) (host.Host, func(), error) { +func setupP2P(ctx context.Context, key *k1.PrivateKey, conf Config, peers []p2p.Peer, defHash []byte, uuid string) (host.Host, func(), error) { var peerIDs []peer.ID for _, p := range peers { peerIDs = append(peerIDs, p.ID) @@ -1271,7 +1271,7 @@ func setupP2P(ctx context.Context, key *k1.PrivateKey, conf Config, peers []p2p. return nil, nil, err } - relays, err := p2p.NewRelays(ctx, conf.P2P.Relays, hex.EncodeToString(defHash)) + relays, err := p2p.NewRelays(ctx, conf.P2P.Relays, hex.EncodeToString(defHash), uuid) if err != nil { return nil, nil, err } diff --git a/dkg/protocol.go b/dkg/protocol.go index ebc607618c..2ad97b12c9 100644 --- a/dkg/protocol.go +++ b/dkg/protocol.go @@ -4,7 +4,6 @@ package dkg import ( "context" - "encoding/json" "os" "time" @@ -142,7 +141,7 @@ func RunProtocol(ctx context.Context, protocol Protocol, lockFilePath, privateKe return err } - thisNode, shutdown, err := setupP2P(ctx, enrPrivateKey, config, peers, lock.DefinitionHash) + thisNode, shutdown, err := setupP2P(ctx, enrPrivateKey, config, peers, lock.DefinitionHash, lock.UUID) if err != nil { return err } @@ -187,35 +186,13 @@ func RunProtocol(ctx context.Context, protocol Protocol, lockFilePath, privateKe // LoadAndVerifyClusterLock loads the cluster lock from disk and verifies its hashes and signatures. func LoadAndVerifyClusterLock(ctx context.Context, lockFilePath, executionEngineAddr string, noVerify bool) (*cluster.Lock, error) { - b, err := os.ReadFile(lockFilePath) - if err != nil { - return nil, errors.Wrap(err, "read cluster-lock.json", z.Str("path", lockFilePath)) - } - - var lock cluster.Lock - if err := json.Unmarshal(b, &lock); err != nil { - return nil, errors.Wrap(err, "unmarshal cluster-lock.json", z.Str("path", lockFilePath)) - } - ctx, cancel := context.WithCancel(ctx) defer cancel() eth1Cl := eth1wrap.NewDefaultEthClientRunner(executionEngineAddr) go eth1Cl.Run(ctx) - if err := lock.VerifyHashes(); err != nil && !noVerify { - return nil, errors.Wrap(err, "verify cluster lock hashes (run with --no-verify to bypass verification at own risk)") - } else if err != nil && noVerify { - log.Warn(ctx, "Ignoring failed cluster lock hashes verification due to --no-verify flag", err) - } - - if err := lock.VerifySignatures(eth1Cl); err != nil && !noVerify { - return nil, errors.Wrap(err, "verify cluster lock signatures (run with --no-verify to bypass verification at own risk)") - } else if err != nil && noVerify { - log.Warn(ctx, "Ignoring failed cluster lock signature verification due to --no-verify flag", err) - } - - return &lock, nil + return cluster.LoadClusterLock(ctx, lockFilePath, noVerify, eth1Cl) } // LoadSecrets loads the private key shares from the validator keys subdirectory in the given data directory. diff --git a/docs/configuration.md b/docs/configuration.md index f7979c29ba..559d6425c1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -181,7 +181,7 @@ Flags: --log-output-path string Path in which to write on-disk logs. --loki-addresses strings Enables sending of logfmt structured logs to these Loki log aggregation server addresses. This is in addition to normal stderr logs. --loki-service string Service label sent with logs to Loki. (default "charon") - --manifest-file string The path to the cluster manifest file. If both cluster manifest and cluster lock files are provided, the cluster manifest file takes precedence. (default ".charon/cluster-manifest.pb") + --manifest-file string [DEPRECATED] The path to the cluster manifest file. If both cluster manifest and cluster lock files are provided, the cluster manifest file takes precedence. (default ".charon/cluster-manifest.pb") --monitoring-address string Listening address (ip and port) for the monitoring API (prometheus). (default "127.0.0.1:3620") --nickname string Human friendly peer nickname. Maximum 32 characters. --no-verify Disables cluster definition and lock file verification. diff --git a/eth2util/keystore/keystore.go b/eth2util/keystore/keystore.go index fca7b7cf14..a45f4f37c4 100644 --- a/eth2util/keystore/keystore.go +++ b/eth2util/keystore/keystore.go @@ -24,8 +24,7 @@ import ( "github.com/obolnetwork/charon/app/errors" "github.com/obolnetwork/charon/app/forkjoin" "github.com/obolnetwork/charon/app/z" - "github.com/obolnetwork/charon/cluster/manifest" - manifestpb "github.com/obolnetwork/charon/cluster/manifestpb/v1" + "github.com/obolnetwork/charon/cluster" "github.com/obolnetwork/charon/core" "github.com/obolnetwork/charon/tbls" "github.com/obolnetwork/charon/tbls/tblsconv" @@ -246,7 +245,7 @@ func checkDir(dir string) error { // KeysharesToValidatorPubkey maps each share in cl to the associated validator private key. // It returns an error if a keyshare does not appear in cl, or if there's a validator public key associated to no // keyshare. -func KeysharesToValidatorPubkey(cl *manifestpb.Cluster, shares []tbls.PrivateKey) (ValidatorShares, error) { +func KeysharesToValidatorPubkey(lock cluster.Lock, shares []tbls.PrivateKey) (ValidatorShares, error) { ret := make(map[core.PubKey]IndexedKeyShare) var pubShares []tbls.PublicKey @@ -261,11 +260,11 @@ func KeysharesToValidatorPubkey(cl *manifestpb.Cluster, shares []tbls.PrivateKey } // this is sadly a O(n^2) search - for _, validator := range cl.GetValidators() { - valHex := fmt.Sprintf("0x%x", validator.GetPublicKey()) + for _, validator := range lock.Validators { + valHex := validator.PublicKeyHex() valPubShares := make(map[tbls.PublicKey]struct{}) - for _, valShare := range validator.GetPubShares() { + for _, valShare := range validator.PubShares { valPubShares[tbls.PublicKey(valShare)] = struct{}{} } @@ -290,7 +289,7 @@ func KeysharesToValidatorPubkey(cl *manifestpb.Cluster, shares []tbls.PrivateKey } } - if len(ret) != len(cl.GetValidators()) { + if len(ret) != len(lock.Validators) { return nil, errors.New("key shares and validator public keys count mismatch") } @@ -298,8 +297,8 @@ func KeysharesToValidatorPubkey(cl *manifestpb.Cluster, shares []tbls.PrivateKey } // ShareIdxForCluster returns the share index for the Charon cluster's ENR identity key, given a *manifestpb.Cluster. -func ShareIdxForCluster(cl *manifestpb.Cluster, identityKey k1.PublicKey) (uint64, error) { - pids, err := manifest.ClusterPeerIDs(cl) +func ShareIdxForCluster(lock cluster.Lock, identityKey k1.PublicKey) (uint64, error) { + pids, err := lock.PeerIDs() if err != nil { return 0, errors.Wrap(err, "cluster peer ids") } @@ -313,7 +312,7 @@ func ShareIdxForCluster(cl *manifestpb.Cluster, identityKey k1.PublicKey) (uint6 continue } - nIdx, err := manifest.ClusterNodeIdx(cl, pid) + nIdx, err := lock.NodeIdx(pid) if err != nil { return 0, errors.Wrap(err, "cluster node idx") } diff --git a/eth2util/keystore/keystore_test.go b/eth2util/keystore/keystore_test.go index a77172de37..6b1bca57a9 100644 --- a/eth2util/keystore/keystore_test.go +++ b/eth2util/keystore/keystore_test.go @@ -17,8 +17,6 @@ import ( "github.com/stretchr/testify/require" "github.com/obolnetwork/charon/cluster" - "github.com/obolnetwork/charon/cluster/manifest" - manifestpb "github.com/obolnetwork/charon/cluster/manifestpb/v1" "github.com/obolnetwork/charon/eth2util/keystore" "github.com/obolnetwork/charon/tbls" "github.com/obolnetwork/charon/tbls/tblsconv" @@ -377,14 +375,14 @@ func TestKeyshareToValidatorPubkey(t *testing.T) { privateShares := make([]tbls.PrivateKey, valAmt) - cl := &manifestpb.Cluster{} + lock := cluster.Lock{} for valIdx := range valAmt { valPubk, err := tblsconv.PubkeyFromCore(testutil.RandomCorePubKey(t)) require.NoError(t, err) - validator := &manifestpb.Validator{ - PublicKey: valPubk[:], + validator := cluster.DistValidator{ + PubKey: valPubk[:], } randomShareSelected := false @@ -404,14 +402,14 @@ func TestKeyshareToValidatorPubkey(t *testing.T) { validator.PubShares = append(validator.PubShares, sharePub[:]) } - rand.Shuffle(len(validator.GetPubShares()), func(i, j int) { - validator.PubShares[i], validator.PubShares[j] = validator.GetPubShares()[j], validator.GetPubShares()[i] + rand.Shuffle(len(validator.PubShares), func(i, j int) { + validator.PubShares[i], validator.PubShares[j] = validator.PubShares[j], validator.PubShares[i] }) - cl.Validators = append(cl.Validators, validator) + lock.Validators = append(lock.Validators, validator) } - ret, err := keystore.KeysharesToValidatorPubkey(cl, privateShares) + ret, err := keystore.KeysharesToValidatorPubkey(lock, privateShares) require.NoError(t, err) require.Len(t, ret, 4) @@ -420,8 +418,8 @@ func TestKeyshareToValidatorPubkey(t *testing.T) { valFound := false sharePrivKeyFound := false - for _, val := range cl.GetValidators() { - if string(valPubKey) == fmt.Sprintf("0x%x", val.GetPublicKey()) { + for _, val := range lock.Validators { + if string(valPubKey) == val.PublicKeyHex() { valFound = true break } @@ -454,15 +452,9 @@ func TestShareIdxForCluster(t *testing.T) { random, ) - dag, err := manifest.NewDAGFromLockForT(t, lock) - require.NoError(t, err) - - cl, err := manifest.Materialise(dag) - require.NoError(t, err) - pubkey := enrs[0].PubKey() - res, err := keystore.ShareIdxForCluster(cl, *pubkey) + res, err := keystore.ShareIdxForCluster(lock, *pubkey) require.NoError(t, err) require.Equal(t, uint64(1), res) } diff --git a/p2p/bootnode.go b/p2p/bootnode.go index 99a4c125d4..3acb109834 100644 --- a/p2p/bootnode.go +++ b/p2p/bootnode.go @@ -24,7 +24,7 @@ import ( ) // NewRelays returns the libp2p relays from the provided addresses. -func NewRelays(ctx context.Context, relayAddrs []string, lockHashHex string, +func NewRelays(ctx context.Context, relayAddrs []string, lockHashHex, uuid string, ) ([]*MutablePeer, error) { var resp []*MutablePeer @@ -35,7 +35,7 @@ func NewRelays(ctx context.Context, relayAddrs []string, lockHashHex string, } mutable := new(MutablePeer) - go resolveRelay(ctx, relayAddr, lockHashHex, mutable.Set) + go resolveRelay(ctx, relayAddr, lockHashHex, uuid, mutable.Set) resp = append(resp, mutable) @@ -89,13 +89,13 @@ func NewRelays(ctx context.Context, relayAddrs []string, lockHashHex string, // resolveRelay continuously resolves the relay multiaddrs from the HTTP url and returns // the new Peer when it changes via the callback. -func resolveRelay(ctx context.Context, rawURL, lockHashHex string, callback func(Peer)) { +func resolveRelay(ctx context.Context, rawURL, lockHashHex, uuid string, callback func(Peer)) { var ( prevAddrs string backoff, reset = expbackoff.NewWithReset(ctx, expbackoff.WithFastConfig()) // Fast retries mostly for unit tests. ) for ctx.Err() == nil { - addrs, err := queryRelayAddrs(ctx, rawURL, backoff, lockHashHex) + addrs, err := queryRelayAddrs(ctx, rawURL, backoff, lockHashHex, uuid) if err != nil { log.Error(ctx, "Failed to resolve relay addresses from URL. Check that the URL is correct, the relay service is running, and network connectivity is available", err, z.Str("url", rawURL)) return @@ -138,7 +138,7 @@ func resolveRelay(ctx context.Context, rawURL, lockHashHex string, callback func // when relays are deployed in docker compose or kubernetes // // It retries until the context is cancelled. -func queryRelayAddrs(ctx context.Context, relayURL string, backoff func(), lockHashHex string) ([]ma.Multiaddr, error) { +func queryRelayAddrs(ctx context.Context, relayURL string, backoff func(), lockHashHex, uuid string) ([]ma.Multiaddr, error) { parsedURL, err := url.ParseRequestURI(relayURL) if err != nil { return nil, errors.Wrap(err, "parse relay url") @@ -163,6 +163,7 @@ func queryRelayAddrs(ctx context.Context, relayURL string, backoff func(), lockH } req.Header.Set("Charon-Cluster", lockHashHex) + req.Header.Set("Cluster-Uuid", uuid) resp, err := client.Do(req) if err != nil {