Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dkg: generate deposit data #491

Merged
merged 12 commits into from
May 9, 2022
Merged

dkg: generate deposit data #491

merged 12 commits into from
May 9, 2022

Conversation

xenowits
Copy link
Contributor

@xenowits xenowits commented May 5, 2022

  • Add types for DepositData and DepositMessage.
  • Add HashTreeRoot methods on above types.
  • Implement marshalling DepositData

category: feature
ticket: #481
feature_set: alpha

@xenowits xenowits marked this pull request as draft May 5, 2022 19:39
@@ -0,0 +1,79 @@
package eth2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xenowits why is this in testutil? isn't it used in the normal DKG process?

Copy link
Contributor

@corverroos corverroos May 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked him to put it here for now, next to keystore, we need to move them both and some other "eth2" related utils somewhere else but I'm not sure where. Waiting for a pattern to emerge.

@xenowits please rename package to deposits

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@corverroos don't you think we should avoid the eth2 term as it has become unofficial? Perhaps instead of using eth2 we could use ECL?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as discussed, we are sticking with eth2 for now

Copy link
Contributor

@corverroos corverroos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commented

@@ -0,0 +1,79 @@
package eth2
Copy link
Contributor

@corverroos corverroos May 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked him to put it here for now, next to keystore, we need to move them both and some other "eth2" related utils somewhere else but I'm not sure where. Waiting for a pattern to emerge.

@xenowits please rename package to deposits

go.mod Outdated
@@ -22,6 +22,7 @@ require (
github.com/multiformats/go-multiaddr v0.5.0
github.com/prometheus/client_golang v1.12.1
github.com/prysmaticlabs/go-bitfield v0.0.0-20210809151128-385d8c5e3fb7
github.com/prysmaticlabs/prysm/v2 v2.0.1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would avoid importing prysm, it is a HUGE dependency. And we do not need it actually.

}

domainType := [4]byte{1, 2, 3, 4}
domain, err := signing.ComputeDomain(domainType, nil, nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we already have this logic, see validatorapi.GetDomain()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you might beed to dig into how eth2client does it and copy inner logic since genesis and forkversion is nil in this case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is forkversion nil in this case?

Mainnet started with genesis_fork_version of 0x00000000, but I think some testnets started with a non-zero genesis fork version, if that's where the nil is coming from.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is how prysm does it, probably in die spec somewhere. Will confirm

}

// GenerateDepositData generates a deposit data object by populating the required fields.
func GenerateDepositData(def cluster.Definition, pubkey string) (DepositData, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pass in only what you need. You only use withdrawal address and forkversion from the definition, rather just pass them in directly.

case "00000064":
return "gnosis"
default:
return "mainnet"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure defaulting to mainnet is correct. Rather default to "unknown"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not sure either but sure will default to unknown

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No no! Jim McDonald and others pushed to change defaults to mainnet instead of testnet, to reduce the chance of an invalid deposit on mainnet. An unknown/non-mainnet default could blow up lots of money. Sending a mainnet deposit data to testnet blows up monopoly money. (I once blew up 120 testnet ether when Jim made this change. Before then ethdo pulled fork_version from connected beacon node, now it defaults to mainnet and you have to explicitly tell it to do something else.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok sure, mainnet default


domainType := [4]byte{1, 2, 3, 4}
domain, err := signing.ComputeDomain(domainType, nil, nil)
depositMessageRoot, err := signing.ComputeSigningRoot(depositMessage, domain)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we also alreadyhave this, see validatorapi.prepSigningData. You can just copy it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please, don't copy/paste code if it is possible

// GenerateDepositData generates a deposit data object by populating the required fields.
func GenerateDepositData(def cluster.Definition, pubkey string) (DepositData, error) {
amount := uint64(32000000000) // 32 Ether in Gwei
depositMessage := DepositMessage{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggest making DepositMessage unexported, and adding a HashTreeRoot() function to it. See prysm, cluster.Lock and cluster.Defintion how to implement HashTreeRoot

return DepositData{}, errors.Wrap(err, "compute domain")
}

depositDataRoot := ""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here depositDataRoot needs to be calcualted by a method you need to add to DepositData called HashTreeRoot, see prsym which fields and what order to hash them.

@corverroos corverroos added the do not merge Indicate to bulldozer bot that this PR should not be merged label May 6, 2022
}

// GenerateDepositData generates a deposit data object by populating the required fields.
func GenerateDepositData(def cluster.Definition, pubkey string) (DepositData, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

always use proper types, use eth2p0.PublicKey not string

deposit_data := DepositData{
PubKey: pubkey,
Amount: amount,
WithdrawalCredentials: def.WithdrawalAddress,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note that withdrawal credentials isn't an address, is looks different to me

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, they need to be slightly converted

See the spec here https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/validator.md#eth1_address_withdrawal_prefix

And an example here https://discord.com/channels/930779837218562078/932001790763864094/970934579563204688

We should only support type 0x01 for creation, because they are the only type that exits will be supported for (split-keys shouldn't care/know what the withdrawal creds are)

}

depositDataRoot := ""
signature := "" // sign the depositMsgRoot with the bls key
Copy link
Contributor

@corverroos corverroos May 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

signing in a distributed validator is not trivial, so we need to restructure this logic, you cannot sign "inside a function".

Suggest splitting into two functions:

  • deposits.GetMessageRoot(ctx, eth2Cl, pubkey, withdrawalPubKey) eth2p0.Root, error
  • We will then threshold sign this sig root, and aggregate it to get a the signature, and only then make deposit data.
  • deposits.GetDepositData(pubkey, withdrawalPubKey, forkversion, messageRoot, messageSig) DepositData, error

idx := hh.Index()

// Field 0 'PubKey`
hh.PutBytes([]byte(d.PubKey))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is going to fail, since the type of pubkey is a string, so it is really unclear what format should be. Rather make it []byte then we know it is a standard public key bytes format

return b, nil
}

func (d DepositMessage) HashTreeRootWith(hh *ssz.Hasher) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add tests for this, try to find reference data in prysm. Ie. given a message with values X,Y,Z, we know the hash should be something

hh.PutUint64(d.Amount)

// Field 2 'WithdrawalCredentials'
hh.PutBytes([]byte(d.WithdrawalCredentials))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, this should be bytes, also please reference the format spec.

"github.com/obolnetwork/charon/app/z"
)

const ETH1_ADDRESS_WITHDRAWAL_PREFIX = "01"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not export types/interfaces/variables/constants by default, always start unexported


withdrawalCreds := make([]byte, 0, 32)

prefix, err := hex.DecodeString(ETH1_ADDRESS_WITHDRAWAL_PREFIX)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rather make the constant a []byte, then no need to convert

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok. made it a single byte: byte(1)

return WithdrawalCredentials{}, errors.New("invalid withdrawal address", z.Str("address", addr))
}

withdrawalCreds := make([]byte, 0, 32)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

introduces variables just before they are used

canonical slice instantiation: var creds []byte

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoid micro optimisations (adding length to slice). It draws attention ... to nothing in this case.

Only do micro optimisations if really required, like in some hot path.

// Append the single byte ETH1_ADDRESS_WITHDRAWAL_PREFIX as prefix.
withdrawalCreds = append(withdrawalCreds, prefix...)

zeroes, err := hex.DecodeString("0000000000000000000000")
Copy link
Contributor

@corverroos corverroos May 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, make this a global var elevenZeros = []byte{0x00,0x00, ...}

withdrawalCreds = append(withdrawalCreds, zeroes...)

// Finally, append 20 bytes of ethereum address.
withdrawalCreds = append(withdrawalCreds, addr...)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are appending a hex string to a byte slice...

@codecov
Copy link

codecov bot commented May 8, 2022

Codecov Report

Merging #491 (f74eedd) into main (419dbe5) will increase coverage by 0.57%.
The diff coverage is n/a.

@@            Coverage Diff             @@
##             main     #491      +/-   ##
==========================================
+ Coverage   55.87%   56.44%   +0.57%     
==========================================
  Files          84       87       +3     
  Lines        7578     7979     +401     
==========================================
+ Hits         4234     4504     +270     
- Misses       2770     2874     +104     
- Partials      574      601      +27     
Impacted Files Coverage Δ
app/app.go 63.31% <0.00%> (-2.08%) ⬇️
core/qbft/uponrule_string.go 40.00% <0.00%> (ø)
core/qbft/qbft.go 69.74% <0.00%> (ø)
core/qbft/msgtype_string.go 50.00% <0.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 419dbe5...f74eedd. Read the comment docs.

@xenowits xenowits marked this pull request as ready for review May 8, 2022 17:00
var version eth2p0.Version
copy(version[:], forkVersion)

depositData := DepositData{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inline this: return DepositData{...}, nil

}

// GenerateDepositData generates a deposit data object by populating the required fields.
func GenerateDepositData(addr string, forkVersion string, pubkey eth2p0.BLSPubKey) (DepositData, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: NewDepositData

Also add signature as parameter

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another idea is to skip the DepositData struct and go stright to raw json bytes:

// MarshalDepositData returns the serialised json deposit data bytes to be written to disk.
func MarshalDepositData(pubkey eth2p0.BLSPubKey, withdrawalAddr string, forkVersion string, signature []byte) []byte, error

Copy link
Contributor Author

@xenowits xenowits May 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the idea to skip the structs obviously seems very simple. But, i think that'd be oversimplifying it. We're not doing anything complex here so we can keep the types for the time being. (Pro tip: makes tree hashing easier)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can get rid of the unmarshal functions though since we only need to marshal and save to disk


// WithdrawalCredentials is the 0x01 withdrawal credentials. See spec:
// https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/validator.md#withdrawal-credentials
type WithdrawalCredentials [32]byte
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type not required, can just use [32]byte directly

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't oppose a type, and maybe an assertion that the first byte is 01, along with maybe some 0x prepend handling.

Overkill though?

// Append 11 bytes of 0.
withdrawalCreds = append(withdrawalCreds, elevenZeroes...)

addrBytes, err := hex.DecodeString(addr)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might have "0x" prefix, trim it first: addr = strings.TrimPrefix(addr, "0x")

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this appending or prepending the zeros? Because it should be at the start of the string.

E.g. 0x01 + 00000000000 + %s

return string(credentials[12:]), nil
}

func (d depositMessage) HashTreeRoot() ([32]byte, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

colocate type and its methods

}

// ddFmt is the json formatter for DepositData.
type ddFmt struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: ddJSON


// ddFmt is the json formatter for DepositData.
type ddFmt struct {
PubKey []byte `json:"pubkey"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[]byte is encoded as base64, so this needs to be string

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all hex fields should be string

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other option is to create your own wrapper type called type hex []byte

Then implement json.Marshaller and json.Unmarshaller to does hex conversion.

Then just wrap the byte:

json.Marshal(ddJson{
  Pubkey: hex(pkBytes),
  Signature: hex(sig), 
})

if err != nil {
return DepositData{}, err
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO(): verify the signature

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

verify sig using: pubkey == crypto.SigToPub(depositMessageRoot[:], sig)

@OisinKyne OisinKyne added this to the Devnet 1 milestone May 9, 2022
}

// MarshalDepositData returns the json serialised deposit data bytes to be written to disk.
func MarshalDepositData(pubkey eth2p0.BLSPubKey, msgRoot eth2p0.Root, sig eth2p0.BLSSignature, withdrawalAddr, forkVersion string) ([]byte, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe rather remove msgRoot, since you can calculate it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm thinking along the lines of if we have a sig (that's calculated on msgRoot), we already have a msgRoot. So, we could have that as an input parameter.

hh.PutUint64(uint64(d.Amount))

// Field 2 'Eth1WithdrawalAddress'
hh.PutBytes([]byte(d.Eth1WithdrawalAddress))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should probably be credentials, not address

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct, also changed the ordering of fields according to spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#depositdata


// construct DepositData and then calculate the hash.
var version eth2p0.Version
copy(version[:], forkVersion)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forkVersion is hex string, you need to convert to bytes first

const depositAmt = 32000000000

// DepositData contains all the information required for activating validators on the Ethereum Network.
type DepositData struct { //nolint:revive
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unexport this

@xenowits xenowits added merge when ready Indicates bulldozer bot may merge when all checks pass and removed do not merge Indicate to bulldozer bot that this PR should not be merged labels May 9, 2022
@obol-bulldozer obol-bulldozer bot merged commit 660e8a2 into main May 9, 2022
@obol-bulldozer obol-bulldozer bot deleted the xenowits/add-deposit-data branch May 9, 2022 16:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
merge when ready Indicates bulldozer bot may merge when all checks pass
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants