diff --git a/cmd/createcluster.go b/cmd/createcluster.go index aa7ed12a9..131116587 100644 --- a/cmd/createcluster.go +++ b/cmd/createcluster.go @@ -171,12 +171,6 @@ func runCreateCluster(ctx context.Context, w io.Writer, conf clusterConfig) erro return err } - // Create validators - vals, err := getValidators(pubkeys, shareSets) - if err != nil { - return err - } - // Create operators ops, err := getOperators(numNodes, conf.ClusterDir) if err != nil { @@ -195,12 +189,26 @@ func runCreateCluster(ctx context.Context, w io.Writer, conf clusterConfig) erro } } + network, err := eth2util.ForkVersionToNetwork(def.ForkVersion) + if err != nil { + return err + } + + depositDatas, err := createDepositDatas(def.WithdrawalAddresses(), network, secrets) + if err != nil { + return err + } + // Write deposit-data file - if err = writeDepositData(def.WithdrawalAddresses(), conf.ClusterDir, def.ForkVersion, numNodes, secrets); err != nil { + if err = writeDepositData(depositDatas, network, conf.ClusterDir, numNodes); err != nil { + return err + } + + vals, err := getValidators(pubkeys, shareSets, depositDatas) + if err != nil { return err } - // Create cluster-lock lock := cluster.Lock{ Definition: def, Validators: vals, @@ -231,41 +239,47 @@ func runCreateCluster(ctx context.Context, w io.Writer, conf clusterConfig) erro } // signDepositDatas returns Distributed Validator pubkeys and deposit data signatures corresponding to each pubkey. -func signDepositDatas(secrets []tblsv2.PrivateKey, withdrawalAddresses []string, network string) ([]eth2p0.BLSPubKey, []eth2p0.BLSSignature, error) { +func signDepositDatas(secrets []tblsv2.PrivateKey, withdrawalAddresses []string, network string) ([]eth2p0.DepositData, error) { if len(secrets) != len(withdrawalAddresses) { - return nil, nil, errors.New("insufficient withdrawal addresses") + return nil, errors.New("insufficient withdrawal addresses") } - var ( - pubkeys []eth2p0.BLSPubKey - signatures []eth2p0.BLSSignature - ) + var datas []eth2p0.DepositData for i, secret := range secrets { withdrawalAddr, err := eth2util.ChecksumAddress(withdrawalAddresses[i]) if err != nil { - return nil, nil, err + return nil, err } pk, err := tblsv2.SecretToPublicKey(secret) if err != nil { - return nil, nil, errors.Wrap(err, "secret to pubkey") + return nil, errors.Wrap(err, "secret to pubkey") } - msgRoot, err := deposit.GetMessageSigningRoot(eth2p0.BLSPubKey(pk), withdrawalAddr, network) + msg, err := deposit.NewMessage(eth2p0.BLSPubKey(pk), withdrawalAddr) if err != nil { - return nil, nil, err + return nil, err } - sig, err := tblsv2.Sign(secret, msgRoot[:]) + sigRoot, err := deposit.GetMessageSigningRoot(msg, network) if err != nil { - return nil, nil, err + return nil, err + } + + sig, err := tblsv2.Sign(secret, sigRoot[:]) + if err != nil { + return nil, err } - pubkeys = append(pubkeys, eth2p0.BLSPubKey(pk)) - signatures = append(signatures, tblsconv2.SigToETH2(sig)) + datas = append(datas, eth2p0.DepositData{ + PublicKey: msg.PublicKey, + WithdrawalCredentials: msg.WithdrawalCredentials, + Amount: msg.Amount, + Signature: tblsconv2.SigToETH2(sig), + }) } - return pubkeys, signatures, nil + return datas, nil } // getTSSShares splits the secrets and returns the threshold key shares. @@ -335,25 +349,19 @@ func getKeys(splitKeys bool, splitKeysDir string, numDVs int) ([]tblsv2.PrivateK return secrets, nil } -// writeDepositData writes deposit data to disk for the DVs for all peers in a cluster. -func writeDepositData(withdrawalAddresses []string, clusterDir string, forkVersion []byte, numNodes int, secrets []tblsv2.PrivateKey) error { +// createDepositDatas creates a slice of deposit datas using the provided parameters and returns it. +func createDepositDatas(withdrawalAddresses []string, network string, secrets []tblsv2.PrivateKey) ([]eth2p0.DepositData, error) { if len(secrets) != len(withdrawalAddresses) { - return errors.New("insufficient withdrawal addresses") - } - - network, err := eth2util.ForkVersionToNetwork(forkVersion) - if err != nil { - return err + return nil, errors.New("insufficient withdrawal addresses") } - // Create deposit message signatures - pubkeys, sigs, err := signDepositDatas(secrets, withdrawalAddresses, network) - if err != nil { - return err - } + return signDepositDatas(secrets, withdrawalAddresses, network) +} +// writeDepositData writes deposit data to disk for the DVs for all peers in a cluster. +func writeDepositData(depositDatas []eth2p0.DepositData, network string, clusterDir string, numNodes int) error { // Serialize the deposit data into bytes - bytes, err := deposit.MarshalDepositData(pubkeys, sigs, withdrawalAddresses, network) + bytes, err := deposit.MarshalDepositData(depositDatas, network) if err != nil { return err } @@ -395,7 +403,7 @@ func writeLock(lock cluster.Lock, clusterDir string, numNodes int, shareSets [][ // getValidators returns distributed validators from the provided dv public keys and keyshares. // It creates new peers from the provided config and saves validator keys to disk for each peer. -func getValidators(dvsPubkeys []tblsv2.PublicKey, dvPrivShares [][]tblsv2.PrivateKey) ([]cluster.DistValidator, error) { +func getValidators(dvsPubkeys []tblsv2.PublicKey, dvPrivShares [][]tblsv2.PrivateKey, depositDatas []eth2p0.DepositData) ([]cluster.DistValidator, error) { var vals []cluster.DistValidator for idx, dv := range dvsPubkeys { dv := dv @@ -410,9 +418,28 @@ func getValidators(dvsPubkeys []tblsv2.PublicKey, dvPrivShares [][]tblsv2.Privat pubshares = append(pubshares, pubk[:]) } + depositIdx := -1 + for i, dd := range depositDatas { + if [48]byte(dd.PublicKey) != dv { + continue + } + depositIdx = i + + break + } + if depositIdx == -1 { + return nil, errors.New("deposit data not found") + } + vals = append(vals, cluster.DistValidator{ PubKey: dv[:], PubShares: pubshares, + DepositData: cluster.DepositData{ + PubKey: depositDatas[depositIdx].PublicKey[:], + WithdrawalCredentials: depositDatas[depositIdx].WithdrawalCredentials, + Amount: int(depositDatas[depositIdx].Amount), + Signature: depositDatas[depositIdx].Signature[:], + }, }) } diff --git a/dkg/disk.go b/dkg/disk.go index 9cb6074c4..bc2af59c1 100644 --- a/dkg/disk.go +++ b/dkg/disk.go @@ -32,7 +32,6 @@ import ( "github.com/obolnetwork/charon/app/log" "github.com/obolnetwork/charon/app/z" "github.com/obolnetwork/charon/cluster" - "github.com/obolnetwork/charon/eth2util" "github.com/obolnetwork/charon/eth2util/deposit" "github.com/obolnetwork/charon/eth2util/keymanager" "github.com/obolnetwork/charon/eth2util/keystore" @@ -160,21 +159,9 @@ func writeLock(datadir string, lock cluster.Lock) error { } // writeDepositData writes deposit data file to disk. -func writeDepositData(pubkeys []eth2p0.BLSPubKey, depositDataSigs []eth2p0.BLSSignature, withdrawalAddresses []string, network string, dataDir string) error { - if len(pubkeys) != len(withdrawalAddresses) { - return errors.New("insufficient withdrawal addresses") - } - - for i := 0; i < len(withdrawalAddresses); i++ { - var err error - withdrawalAddresses[i], err = eth2util.ChecksumAddress(withdrawalAddresses[i]) - if err != nil { - return err - } - } - +func writeDepositData(depositDatas []eth2p0.DepositData, network string, dataDir string) error { // Serialize the deposit data into bytes - bytes, err := deposit.MarshalDepositData(pubkeys, depositDataSigs, withdrawalAddresses, network) + bytes, err := deposit.MarshalDepositData(depositDatas, network) if err != nil { return err } diff --git a/dkg/dkg.go b/dkg/dkg.go index e8ba6f90e..15b1e4cdf 100644 --- a/dkg/dkg.go +++ b/dkg/dkg.go @@ -16,6 +16,7 @@ package dkg import ( + "bytes" "context" "fmt" "time" @@ -176,8 +177,16 @@ func Run(ctx context.Context, conf Config) (err error) { return errors.New("unsupported dkg algorithm") } + // Sign, exchange and aggregate Deposit Data + depositDatas, err := signAndAggDepositData(ctx, ex, shares, def.WithdrawalAddresses(), network, nodeIdx) + if err != nil { + return err + } + + log.Debug(ctx, "Aggregated deposit data signatures") + // Sign, exchange and aggregate Lock Hash signatures - lock, err := signAndAggLockHash(ctx, shares, def, nodeIdx, ex) + lock, err := signAndAggLockHash(ctx, shares, def, nodeIdx, ex, depositDatas) if err != nil { return err } @@ -188,13 +197,6 @@ func Run(ctx context.Context, conf Config) (err error) { } log.Debug(ctx, "Aggregated lock hash signatures") - // Sign, exchange and aggregate Deposit Data signatures - pubkeys, depositDataSigs, err := signAndAggDepositData(ctx, ex, shares, lock.WithdrawalAddresses(), network, nodeIdx) - if err != nil { - return err - } - log.Debug(ctx, "Aggregated deposit data signatures") - if err = stopSync(ctx); err != nil { return errors.Wrap(err, "sync shutdown") } @@ -225,7 +227,7 @@ func Run(ctx context.Context, conf Config) (err error) { } log.Debug(ctx, "Saved lock file to disk") - if err := writeDepositData(pubkeys, depositDataSigs, lock.WithdrawalAddresses(), network, conf.DataDir); err != nil { + if err := writeDepositData(depositDatas, network, conf.DataDir); err != nil { return err } log.Debug(ctx, "Saved deposit data file to disk") @@ -364,15 +366,19 @@ func startSyncProtocol(ctx context.Context, tcpNode host.Host, key *k1.PrivateKe } // signAndAggLockHash returns cluster lock file with aggregated signature after signing, exchange and aggregation of partial signatures. -func signAndAggLockHash(ctx context.Context, shares []share, def cluster.Definition, nodeIdx cluster.NodeIdx, ex *exchanger) (cluster.Lock, error) { - dvs := dvsFromShares(shares) +func signAndAggLockHash(ctx context.Context, shares []share, def cluster.Definition, + nodeIdx cluster.NodeIdx, ex *exchanger, depositDatas []eth2p0.DepositData, +) (cluster.Lock, error) { + vals, err := createDistValidators(shares, depositDatas) + if err != nil { + return cluster.Lock{}, err + } lock := cluster.Lock{ Definition: def, - Validators: dvs, + Validators: vals, } - - lock, err := lock.SetLockHash() + lock, err = lock.SetLockHash() if err != nil { return cluster.Lock{}, err } @@ -412,62 +418,21 @@ func signAndAggLockHash(ctx context.Context, shares []share, def cluster.Definit return lock, nil } -// signAndAggDepositData returns aggregated signatures per DV after signing, exchange and aggregation of partial signatures. -func signAndAggDepositData(ctx context.Context, ex *exchanger, shares []share, withdrawalAddresses []string, network string, nodeIdx cluster.NodeIdx) ([]eth2p0.BLSPubKey, []eth2p0.BLSSignature, error) { - parSig, msgs, err := signDepositData(shares, nodeIdx.ShareIdx, withdrawalAddresses, network) +// signAndAggDepositData returns the deposit datas for each DV after signing, exchange and aggregation of partial signatures. +func signAndAggDepositData(ctx context.Context, ex *exchanger, shares []share, withdrawalAddresses []string, + network string, nodeIdx cluster.NodeIdx, +) ([]eth2p0.DepositData, error) { + parSig, despositMsgs, err := signDepositMsgs(shares, nodeIdx.ShareIdx, withdrawalAddresses, network) if err != nil { - return nil, nil, err + return nil, err } peerSigs, err := ex.exchange(ctx, sigDepositData, parSig) if err != nil { - return nil, nil, err - } - - aggSigDepositData, err := aggDepositDataSigs(peerSigs, shares, msgs) - if err != nil { - return nil, nil, err - } - - for pk, sig := range aggSigDepositData { - pk := pk - sig := sig - pkb, err := pk.Bytes() - if err != nil { - return nil, nil, errors.Wrap(err, "core bytes marshaling failure") - } - - pubkey, err := tblsconv2.PubkeyFromBytes(pkb) - if err != nil { - return nil, nil, err - } - - err = tblsv2.Verify(pubkey, msgs[pk], sig) - if err != nil { - return nil, nil, errors.Wrap(err, "invalid deposit data aggregated signature") - } - } - - var ( - pubkeys []eth2p0.BLSPubKey - depositDataSigs []eth2p0.BLSSignature - ) - for _, sh := range shares { - eth2Pk, err := tblsconv2.PubkeyToETH2(sh.PubKey) - if err != nil { - return nil, nil, err - } - - corePk, err := core.PubKeyFromBytes(sh.PubKey[:]) - if err != nil { - return nil, nil, err - } - - pubkeys = append(pubkeys, eth2Pk) - depositDataSigs = append(depositDataSigs, tblsconv2.SigToETH2(aggSigDepositData[corePk])) + return nil, err } - return pubkeys, depositDataSigs, nil + return aggDepositData(peerSigs, shares, despositMsgs, network) } // aggLockHashSig returns the aggregated multi signature of the lock hash @@ -539,9 +504,9 @@ func signLockHash(shareIdx int, shares []share, hash []byte) (core.ParSignedData return set, nil } -// signDepositData returns a partially signed dataset containing signatures of the deposit data signing root. -func signDepositData(shares []share, shareIdx int, withdrawalAddresses []string, network string) (core.ParSignedDataSet, map[core.PubKey][]byte, error) { - msgs := make(map[core.PubKey][]byte) +// signDepositMsgs returns a partially signed dataset containing signatures of the deposit message signing root. +func signDepositMsgs(shares []share, shareIdx int, withdrawalAddresses []string, network string) (core.ParSignedDataSet, map[core.PubKey]eth2p0.DepositMessage, error) { + msgs := make(map[core.PubKey]eth2p0.DepositMessage) set := make(core.ParSignedDataSet) for i, share := range shares { withdrawalHex, err := eth2util.ChecksumAddress(withdrawalAddresses[i]) @@ -558,25 +523,36 @@ func signDepositData(shares []share, shareIdx int, withdrawalAddresses []string, return nil, nil, err } - msg, err := deposit.GetMessageSigningRoot(pubkey, withdrawalHex, network) + msg, err := deposit.NewMessage(pubkey, withdrawalHex) + if err != nil { + return nil, nil, err + } + + sigRoot, err := deposit.GetMessageSigningRoot(msg, network) if err != nil { return nil, nil, err } - msgs[pk] = msg[:] - sig, err := tblsv2.Sign(share.SecretShare, msg[:]) + sig, err := tblsv2.Sign(share.SecretShare, sigRoot[:]) if err != nil { return nil, nil, err } set[pk] = core.NewPartialSignature(tblsconv2.SigToCore(sig), shareIdx) + msgs[pk] = eth2p0.DepositMessage{ + PublicKey: msg.PublicKey, + WithdrawalCredentials: msg.WithdrawalCredentials, + Amount: msg.Amount, + } } return set, msgs, nil } -// aggDepositDataSigs returns the threshold aggregated signatures of the deposit data per DV. -func aggDepositDataSigs(data map[core.PubKey][]core.ParSignedData, shares []share, msgs map[core.PubKey][]byte) (map[core.PubKey]tblsv2.Signature, error) { +// aggDepositData returns the threshold aggregated deposit datas per DV. +func aggDepositData(data map[core.PubKey][]core.ParSignedData, shares []share, + msgs map[core.PubKey]eth2p0.DepositMessage, network string, +) ([]eth2p0.DepositData, error) { pubkeyToPubShares := make(map[core.PubKey]map[int]tblsv2.PublicKey) for _, sh := range shares { pk, err := core.PubKeyFromBytes(sh.PubKey[:]) @@ -587,11 +563,21 @@ func aggDepositDataSigs(data map[core.PubKey][]core.ParSignedData, shares []shar pubkeyToPubShares[pk] = sh.PublicShares } - resp := make(map[core.PubKey]tblsv2.Signature) + var resp []eth2p0.DepositData for pk, psigsData := range data { pk := pk psigsData := psigsData + + msg, ok := msgs[pk] + if !ok { + return nil, errors.New("deposit message not found") + } + sigRoot, err := deposit.GetMessageSigningRoot(msg, network) + if err != nil { + return nil, err + } + psigs := make(map[int]tblsv2.Signature) for _, s := range psigsData { sig, err := tblsconv2.SignatureFromBytes(s.Signature()) @@ -611,7 +597,7 @@ func aggDepositDataSigs(data map[core.PubKey][]core.ParSignedData, shares []shar return nil, errors.New("invalid pubshare") } - err = tblsv2.Verify(pubshare, msgs[pk], sig) + err = tblsv2.Verify(pubshare, sigRoot[:], sig) if err != nil { return nil, errors.New("invalid deposit data partial signature from peer", z.Int("peerIdx", s.ShareIdx-1), z.Str("pubkey", pk.String())) @@ -625,25 +611,61 @@ func aggDepositDataSigs(data map[core.PubKey][]core.ParSignedData, shares []shar if err != nil { return nil, err } - resp[pk] = asig + + pubkey, err := tblsconv2.PubkeyFromCore(pk) + if err != nil { + return nil, err + } + + err = tblsv2.Verify(pubkey, sigRoot[:], asig) + if err != nil { + return nil, errors.Wrap(err, "invalid deposit data aggregated signature") + } + + resp = append(resp, eth2p0.DepositData{ + PublicKey: msg.PublicKey, + WithdrawalCredentials: msg.WithdrawalCredentials, + Amount: msg.Amount, + Signature: tblsconv2.SigToETH2(asig), + }) } return resp, nil } -// dvsFromShares returns the shares as a slice of cluster distributed validator types. -func dvsFromShares(shares []share) []cluster.DistValidator { +// createDistValidators returns a slice of distributed validators from the provided +// shares and deposit datas. +func createDistValidators(shares []share, depositDatas []eth2p0.DepositData) ([]cluster.DistValidator, error) { var dvs []cluster.DistValidator for _, s := range shares { msg := msgFromShare(s) + ddIdx := -1 + for i, dd := range depositDatas { + if !bytes.Equal(msg.PubKey, dd.PublicKey[:]) { + continue + } + ddIdx = i + + break + } + if ddIdx == -1 { + return nil, errors.New("deposit data not found") + } + dvs = append(dvs, cluster.DistValidator{ PubKey: msg.PubKey, PubShares: msg.PubShares, + DepositData: cluster.DepositData{ + PubKey: depositDatas[ddIdx].PublicKey[:], + WithdrawalCredentials: depositDatas[ddIdx].WithdrawalCredentials, + Amount: int(depositDatas[ddIdx].Amount), + Signature: depositDatas[ddIdx].Signature[:], + }, }) } - return dvs + return dvs, nil } // writeLockToAPI posts the lock file to obol-api. diff --git a/dkg/dkg_internal_test.go b/dkg/dkg_internal_test.go index 4ebfd1fb4..e5c8aea59 100644 --- a/dkg/dkg_internal_test.go +++ b/dkg/dkg_internal_test.go @@ -18,11 +18,15 @@ package dkg import ( "testing" + eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/stretchr/testify/require" "github.com/obolnetwork/charon/core" + "github.com/obolnetwork/charon/eth2util" + "github.com/obolnetwork/charon/eth2util/deposit" tblsv2 "github.com/obolnetwork/charon/tbls/v2" tblsconv2 "github.com/obolnetwork/charon/tbls/v2/tblsconv" + "github.com/obolnetwork/charon/testutil" ) func TestInvalidSignatures(t *testing.T) { @@ -76,10 +80,14 @@ func TestInvalidSignatures(t *testing.T) { require.NoError(t, err) // Aggregate and verify deposit data signatures - depositDataMsg := []byte("deposit data msg") + msg := testutil.RandomDepositMsg(t) - _, err = aggDepositDataSigs(map[core.PubKey][]core.ParSignedData{corePubkey: getSigs(depositDataMsg)}, []share{shares}, - map[core.PubKey][]byte{corePubkey: depositDataMsg}) + _, err = aggDepositData( + map[core.PubKey][]core.ParSignedData{corePubkey: getSigs([]byte("any digest"))}, + []share{shares}, + map[core.PubKey]eth2p0.DepositMessage{corePubkey: msg}, + eth2util.Goerli.Name, + ) require.EqualError(t, err, "invalid deposit data partial signature from peer") // Aggregate and verify cluster lock hash signatures @@ -135,12 +143,23 @@ func TestValidSignatures(t *testing.T) { corePubkey, err := core.PubKeyFromBytes(pubkey[:]) require.NoError(t, err) + eth2Pubkey, err := corePubkey.ToETH2() + require.NoError(t, err) - // Aggregate and verify deposit data signatures - depositDataMsg := []byte("deposit data msg") + withdrawalAddr := testutil.RandomETHAddress() + network := eth2util.Goerli.Name - _, err = aggDepositDataSigs(map[core.PubKey][]core.ParSignedData{corePubkey: getSigs(depositDataMsg)}, []share{shares}, - map[core.PubKey][]byte{corePubkey: depositDataMsg}) + msg, err := deposit.NewMessage(eth2Pubkey, withdrawalAddr) + require.NoError(t, err) + sigRoot, err := deposit.GetMessageSigningRoot(msg, network) + require.NoError(t, err) + + _, err = aggDepositData( + map[core.PubKey][]core.ParSignedData{corePubkey: getSigs(sigRoot[:])}, + []share{shares}, + map[core.PubKey]eth2p0.DepositMessage{corePubkey: msg}, + network, + ) require.NoError(t, err) // Aggregate and verify cluster lock hash signatures diff --git a/dkg/dkg_test.go b/dkg/dkg_test.go index 292f53038..c35f76024 100644 --- a/dkg/dkg_test.go +++ b/dkg/dkg_test.go @@ -88,7 +88,8 @@ func TestDKG(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - lock, keys, _ := cluster.NewForT(t, vals, nodes, nodes, 0, withAlgo(test.dkgAlgo)) + version := cluster.WithVersion("v1.6.0") // TODO(corver): Remove this once v1.6 is released. + lock, keys, _ := cluster.NewForT(t, vals, nodes, nodes, 0, withAlgo(test.dkgAlgo), version) dir := t.TempDir() testDKG(t, lock.Definition, dir, keys, test.keymanager, test.publish) @@ -276,6 +277,11 @@ func verifyDKGResults(t *testing.T, def cluster.Definition, dir string) { require.NoError(t, json.Unmarshal(lockFile, &lock)) require.NoError(t, lock.VerifySignatures()) locks = append(locks, lock) + + for _, val := range lock.Validators { + require.EqualValues(t, val.PubKey, val.DepositData.PubKey) + require.EqualValues(t, 32_000_000_000, val.DepositData.Amount) + } } // Ensure locks hashes are identical. @@ -342,7 +348,8 @@ func TestSyncFlow(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - lock, keys, _ := cluster.NewForT(t, test.vals, test.nodes, test.nodes, 0) + version := cluster.WithVersion("v1.6.0") // TODO(corver): remove this once v1.6 released. + lock, keys, _ := cluster.NewForT(t, test.vals, test.nodes, test.nodes, 0, version) ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/eth2util/deposit/deposit.go b/eth2util/deposit/deposit.go index 87e3ad3cf..372a990cd 100644 --- a/eth2util/deposit/deposit.go +++ b/eth2util/deposit/deposit.go @@ -44,80 +44,63 @@ var ( depositCliVersion = "2.3.0" ) -// getMessageRoot returns a deposit message hash root created by the parameters. -func getMessageRoot(pubkey eth2p0.BLSPubKey, withdrawalAddr string) (eth2p0.Root, error) { +// NewMessage returns a deposit message created using the provided parameters. +func NewMessage(pubkey eth2p0.BLSPubKey, withdrawalAddr string) (eth2p0.DepositMessage, error) { creds, err := withdrawalCredsFromAddr(withdrawalAddr) if err != nil { - return eth2p0.Root{}, err + return eth2p0.DepositMessage{}, err } - dm := eth2p0.DepositMessage{ + return eth2p0.DepositMessage{ PublicKey: pubkey, WithdrawalCredentials: creds[:], Amount: validatorAmt, - } - hashRoot, err := dm.HashTreeRoot() - if err != nil { - return eth2p0.Root{}, errors.Wrap(err, "deposit message hash root") - } - - return hashRoot, nil + }, nil } // MarshalDepositData serializes a list of deposit data into a single file. -func MarshalDepositData(pubkeys []eth2p0.BLSPubKey, depositDataSigs []eth2p0.BLSSignature, withdrawalAddrs []string, network string) ([]byte, error) { - if len(pubkeys) != len(withdrawalAddrs) { - return nil, errors.New("insufficient withdrawal addresses") - } - +func MarshalDepositData(depositDatas []eth2p0.DepositData, network string) ([]byte, error) { forkVersion, err := eth2util.NetworkToForkVersion(network) if err != nil { return nil, err } var ddList []depositDataJSON - for i, sig := range depositDataSigs { - creds, err := withdrawalCredsFromAddr(withdrawalAddrs[i]) - if err != nil { - return nil, err + for _, depositData := range depositDatas { + msg := eth2p0.DepositMessage{ + PublicKey: depositData.PublicKey, + WithdrawalCredentials: depositData.WithdrawalCredentials, + Amount: depositData.Amount, } - - // calculate depositMessage root - msgRoot, err := getMessageRoot(pubkeys[i], withdrawalAddrs[i]) + msgRoot, err := msg.HashTreeRoot() if err != nil { return nil, err } // Verify deposit data signature - sigData, err := GetMessageSigningRoot(pubkeys[i], withdrawalAddrs[i], network) + sigData, err := GetMessageSigningRoot(msg, network) if err != nil { return nil, err } - blsSig := tblsv2.Signature(sig) - blsPubkey := tblsv2.PublicKey(pubkeys[i]) + blsSig := tblsv2.Signature(depositData.Signature) + blsPubkey := tblsv2.PublicKey(depositData.PublicKey) err = tblsv2.Verify(blsPubkey, sigData[:], blsSig) if err != nil { return nil, errors.Wrap(err, "invalid deposit data signature") } - dd := eth2p0.DepositData{ - PublicKey: pubkeys[i], - WithdrawalCredentials: creds[:], - Amount: validatorAmt, - Signature: sig, - } - dataRoot, err := dd.HashTreeRoot() + dataRoot, err := depositData.HashTreeRoot() if err != nil { return nil, errors.Wrap(err, "deposit data hash root") } ddList = append(ddList, depositDataJSON{ - PubKey: fmt.Sprintf("%x", pubkeys[i]), - WithdrawalCredentials: fmt.Sprintf("%x", creds), + PubKey: fmt.Sprintf("%x", depositData.PublicKey), + WithdrawalCredentials: fmt.Sprintf("%x", depositData.WithdrawalCredentials), Amount: uint64(validatorAmt), - Signature: fmt.Sprintf("%x", sig), + Signature: fmt.Sprintf("%x", depositData.Signature), DepositMessageRoot: fmt.Sprintf("%x", msgRoot), DepositDataRoot: fmt.Sprintf("%x", dataRoot), ForkVersion: strings.TrimPrefix(forkVersion, "0x"), @@ -157,10 +140,10 @@ func getDepositDomain(forkVersion eth2p0.Version) (eth2p0.Domain, error) { } // GetMessageSigningRoot returns the deposit message signing root created by the provided parameters. -func GetMessageSigningRoot(pubkey eth2p0.BLSPubKey, withdrawalAddr string, network string) ([32]byte, error) { - msgRoot, err := getMessageRoot(pubkey, withdrawalAddr) +func GetMessageSigningRoot(msg eth2p0.DepositMessage, network string) ([32]byte, error) { + msgRoot, err := msg.HashTreeRoot() if err != nil { - return [32]byte{}, err + return [32]byte{}, errors.Wrap(err, "deposit message root") } fv, err := eth2util.NetworkToForkVersionBytes(network) @@ -184,7 +167,7 @@ func GetMessageSigningRoot(pubkey eth2p0.BLSPubKey, withdrawalAddr string, netwo return resp, nil } -// WithdrawalCredsFromAddr returns the Withdrawal Credentials corresponding to a '0x01' Ethereum withdrawal address. +// withdrawalCredsFromAddr returns the Withdrawal Credentials corresponding to a '0x01' Ethereum withdrawal address. func withdrawalCredsFromAddr(addr string) ([32]byte, error) { // Check for validity of address. if _, err := eth2util.ChecksumAddress(addr); err != nil { diff --git a/eth2util/deposit/deposit_test.go b/eth2util/deposit/deposit_test.go index 64b4ff3f9..00b8ed879 100644 --- a/eth2util/deposit/deposit_test.go +++ b/eth2util/deposit/deposit_test.go @@ -46,23 +46,30 @@ func TestMarshalDepositData(t *testing.T) { } var ( - pubkeys []eth2p0.BLSPubKey - sigs []eth2p0.BLSSignature + datas []eth2p0.DepositData + network = eth2util.Goerli.Name ) for i := 0; i < len(privKeys); i++ { sk, pk := GetKeys(t, privKeys[i]) - msgRoot, err := deposit.GetMessageSigningRoot(pk, withdrawalAddrs[i], eth2util.Goerli.Name) + msg, err := deposit.NewMessage(pk, withdrawalAddrs[i]) require.NoError(t, err) - sig, err := tblsv2.Sign(sk, msgRoot[:]) + sigRoot, err := deposit.GetMessageSigningRoot(msg, network) require.NoError(t, err) - sigs = append(sigs, tblsconv2.SigToETH2(sig)) - pubkeys = append(pubkeys, pk) + sig, err := tblsv2.Sign(sk, sigRoot[:]) + require.NoError(t, err) + + datas = append(datas, eth2p0.DepositData{ + PublicKey: msg.PublicKey, + WithdrawalCredentials: msg.WithdrawalCredentials, + Amount: msg.Amount, + Signature: tblsconv2.SigToETH2(sig), + }) } - actual, err := deposit.MarshalDepositData(pubkeys, sigs, withdrawalAddrs, eth2util.Goerli.Name) + actual, err := deposit.MarshalDepositData(datas, network) require.NoError(t, err) testutil.RequireGoldenBytes(t, actual) diff --git a/tbls/v2/tblsconv/tblsconv.go b/tbls/v2/tblsconv/tblsconv.go index f48ca35cd..223ec724e 100644 --- a/tbls/v2/tblsconv/tblsconv.go +++ b/tbls/v2/tblsconv/tblsconv.go @@ -62,6 +62,17 @@ func PubkeyFromBytes(data []byte) (v2.PublicKey, error) { return *(*v2.PublicKey)(data), nil } +// PubkeyFromCore returns a v2.PublicKey from the given core public key. +// Returns an error if the data isn't of the expected length. +func PubkeyFromCore(pk core.PubKey) (v2.PublicKey, error) { + data, err := pk.Bytes() + if err != nil { + return v2.PublicKey{}, err + } + + return PubkeyFromBytes(data) +} + // SignatureFromBytes returns a v2.Signature from the given compressed signature bytes contained in data. // Returns an error if the data isn't of the expected length. func SignatureFromBytes(data []byte) (v2.Signature, error) { diff --git a/tbls/v2/tblsconv/tblsconv_test.go b/tbls/v2/tblsconv/tblsconv_test.go index 768880646..06c93115a 100644 --- a/tbls/v2/tblsconv/tblsconv_test.go +++ b/tbls/v2/tblsconv/tblsconv_test.go @@ -17,12 +17,15 @@ package tblsconv_test import ( "bytes" + "encoding/hex" + "strings" "testing" "github.com/stretchr/testify/require" v2 "github.com/obolnetwork/charon/tbls/v2" tblsconv2 "github.com/obolnetwork/charon/tbls/v2/tblsconv" + "github.com/obolnetwork/charon/testutil" ) func TestPrivkeyFromBytes(t *testing.T) { @@ -133,6 +136,17 @@ func TestPubkeyToETH2(t *testing.T) { require.Equal(t, pubkey[:], res[:]) } +func TestPubkeyFromCore(t *testing.T) { + pubkey := testutil.RandomCorePubKey(t) + + res, err := tblsconv2.PubkeyFromCore(pubkey) + require.NoError(t, err) + + expect, err := hex.DecodeString(strings.TrimPrefix(string(pubkey), "0x")) + require.NoError(t, err) + require.Equal(t, expect, res[:]) +} + func TestSignatureFromBytes(t *testing.T) { tests := []struct { name string diff --git a/testutil/random.go b/testutil/random.go index 267d27308..d9158c52f 100644 --- a/testutil/random.go +++ b/testutil/random.go @@ -864,6 +864,16 @@ func RandomCoreSignedRandao() core.SignedRandao { }} } +func RandomDepositMsg(t *testing.T) eth2p0.DepositMessage { + t.Helper() + + return eth2p0.DepositMessage{ + PublicKey: RandomEth2PubKey(t), + WithdrawalCredentials: RandomBytes32(), + Amount: RandomGwei(), + } +} + // GenerateInsecureK1Key returns a new deterministic insecure secp256k1 private using the provided seed for testing purposes only. // For random keys, rather use k1.GeneratePrivateKey(). func GenerateInsecureK1Key(t *testing.T, random io.Reader) *k1.PrivateKey {