Files
ethdo/cmd/validator/depositdata/process.go
2025-01-31 17:04:50 +00:00

221 lines
7.2 KiB
Go

// Copyright © 2019-2021 Weald Technology Limited.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package depositdata
import (
"context"
"encoding/hex"
"fmt"
"sort"
"strconv"
"strings"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/wealdtech/ethdo/signing"
ethdoutil "github.com/wealdtech/ethdo/util"
e2types "github.com/wealdtech/go-eth2-types/v2"
util "github.com/wealdtech/go-eth2-util"
e2wtypes "github.com/wealdtech/go-eth2-wallet-types/v2"
)
func process(data *dataIn) ([]*dataOut, error) {
if data == nil {
return nil, errors.New("no data")
}
results := make([]*dataOut, 0)
withdrawalCredentials, err := createWithdrawalCredentials(data)
if err != nil {
return nil, err
}
for _, validatorAccount := range data.validatorAccounts {
validatorPubKey, err := ethdoutil.BestPublicKey(validatorAccount)
if err != nil {
return nil, errors.Wrap(err, "validator account does not provide a public key")
}
var pubKey spec.BLSPubKey
copy(pubKey[:], validatorPubKey.Marshal())
depositMessage := &spec.DepositMessage{
PublicKey: pubKey,
WithdrawalCredentials: withdrawalCredentials,
Amount: data.amount,
}
root, err := depositMessage.HashTreeRoot()
if err != nil {
return nil, errors.Wrap(err, "failed to generate deposit message root")
}
var depositMessageRoot spec.Root
copy(depositMessageRoot[:], root[:])
sig, err := signing.SignRoot(context.Background(), validatorAccount, data.passphrases, depositMessageRoot, *data.domain)
if err != nil {
return nil, errors.Wrap(err, "failed to sign deposit message")
}
depositData := &spec.DepositData{
PublicKey: pubKey,
WithdrawalCredentials: withdrawalCredentials,
Amount: data.amount,
Signature: sig,
}
root, err = depositData.HashTreeRoot()
if err != nil {
return nil, errors.Wrap(err, "failed to generate deposit data root")
}
var depositDataRoot spec.Root
copy(depositDataRoot[:], root[:])
validatorWallet := validatorAccount.(e2wtypes.AccountWalletProvider).Wallet()
result := &dataOut{
format: data.format,
account: fmt.Sprintf("%s/%s", validatorWallet.Name(), validatorAccount.Name()),
validatorPubKey: &pubKey,
withdrawalCredentials: withdrawalCredentials,
amount: data.amount,
signature: &sig,
forkVersion: data.forkVersion,
depositMessageRoot: &depositMessageRoot,
depositDataRoot: &depositDataRoot,
}
if pathProvider, isPathProvider := validatorAccount.(e2wtypes.AccountPathProvider); isPathProvider {
result.path = pathProvider.Path()
}
results = append(results, result)
}
if len(results) == 0 {
return results, nil
}
// Order the results
if results[0].path != "" {
// Order accounts by their path components.
sort.Slice(results, func(i int, j int) bool {
iBits := strings.Split(results[i].path, "/")
jBits := strings.Split(results[j].path, "/")
for index := range iBits {
if iBits[index] == "m" && jBits[index] == "m" {
continue
}
if len(jBits) <= index {
return false
}
iBit, err := strconv.ParseUint(iBits[index], 10, 64)
if err != nil {
return true
}
jBit, err := strconv.ParseUint(jBits[index], 10, 64)
if err != nil {
return false
}
if iBit < jBit {
return true
}
if iBit > jBit {
return false
}
}
return len(jBits) > len(iBits)
})
} else {
// Order accounts by their name.
sort.Slice(results, func(i int, j int) bool {
return strings.Compare(results[i].account, results[j].account) < 0
})
}
return results, nil
}
// createWithdrawalCredentials creates withdrawal credentials given an account, public key or Ethereum 1 address.
func createWithdrawalCredentials(data *dataIn) ([]byte, error) {
var withdrawalCredentials []byte
switch {
case data.withdrawalAccount != "":
ctx, cancel := context.WithTimeout(context.Background(), data.timeout)
defer cancel()
_, withdrawalAccount, err := ethdoutil.WalletAndAccountFromPath(ctx, data.withdrawalAccount)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain withdrawal account")
}
pubKey, err := ethdoutil.BestPublicKey(withdrawalAccount)
if err != nil {
return nil, errors.Wrap(err, "failed to obtain public key for withdrawal account")
}
withdrawalCredentials = util.SHA256(pubKey.Marshal())
// This is hard-coded, to allow deposit data to be generated without a connection to the beacon node.
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
case data.withdrawalPubKey != "":
withdrawalPubKeyBytes, err := hex.DecodeString(strings.TrimPrefix(data.withdrawalPubKey, "0x"))
if err != nil {
return nil, errors.Wrap(err, "failed to decode withdrawal public key")
}
if len(withdrawalPubKeyBytes) != 48 {
return nil, errors.New("withdrawal public key must be exactly 48 bytes in length")
}
pubKey, err := e2types.BLSPublicKeyFromBytes(withdrawalPubKeyBytes)
if err != nil {
return nil, errors.Wrap(err, "withdrawal public key is not valid")
}
withdrawalCredentials = util.SHA256(pubKey.Marshal())
// This is hard-coded, to allow deposit data to be generated without a connection to the beacon node.
withdrawalCredentials[0] = byte(0) // BLS_WITHDRAWAL_PREFIX
case data.withdrawalAddress != "":
withdrawalAddressBytes, err := hex.DecodeString(strings.TrimPrefix(data.withdrawalAddress, "0x"))
if err != nil {
return nil, errors.Wrap(err, "failed to decode withdrawal address")
}
if len(withdrawalAddressBytes) != 20 {
return nil, errors.New("withdrawal address must be exactly 20 bytes in length")
}
// Ensure the address is properly checksummed.
checksummedAddress := addressBytesToEIP55(withdrawalAddressBytes)
if checksummedAddress != data.withdrawalAddress {
return nil, fmt.Errorf("withdrawal address checksum does not match (expected %s)", checksummedAddress)
}
withdrawalCredentials = make([]byte, 32)
copy(withdrawalCredentials[12:32], withdrawalAddressBytes)
// This is hard-coded, to allow deposit data to be generated without a connection to the beacon node.
withdrawalCredentials[0] = byte(1) // ETH1_ADDRESS_WITHDRAWAL_PREFIX
default:
return nil, errors.New("withdrawal account, public key or address is required")
}
return withdrawalCredentials, nil
}
// addressBytesToEIP55 converts a byte array in to an EIP-55 string format.
func addressBytesToEIP55(address []byte) string {
bytes := []byte(hex.EncodeToString(address))
hash := util.Keccak256(bytes)
for i := 0; i < len(bytes); i++ {
hashByte := hash[i/2]
if i%2 == 0 {
hashByte >>= 4
} else {
hashByte &= 0xf
}
if bytes[i] > '9' && hashByte > 7 {
bytes[i] -= 32
}
}
return fmt.Sprintf("0x%s", string(bytes))
}