Skip to content

Commit

Permalink
app/eth2wrap: sign domain for VOLUNTARY_EXIT (#3035)
Browse files Browse the repository at this point in the history
Instead of returning the latest Domain available when the DomainName requested is VOLUNTARY_EXIT, returns the Capella one as detailed in EIP-7044.

Doing so fixes both `exit` commands generating wrong signatures and voluntary exits sent over the VC.

category: bug
ticket: #2981

Closes #2981.
  • Loading branch information
gsora authored Apr 12, 2024
1 parent 098f970 commit cc3c003
Show file tree
Hide file tree
Showing 9 changed files with 389 additions and 5 deletions.
4 changes: 3 additions & 1 deletion app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,7 @@ func newETH2Client(ctx context.Context, conf Config, life *lifecycle.Manager,

if conf.SimnetBMockFuzz {
log.Info(ctx, "Beaconmock fuzz configured!")
bmock, err := beaconmock.New(beaconmock.WithBeaconMockFuzzer())
bmock, err := beaconmock.New(beaconmock.WithBeaconMockFuzzer(), beaconmock.WithForkVersion([4]byte(forkVersion)))
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -826,6 +826,8 @@ func newETH2Client(ctx context.Context, conf Config, life *lifecycle.Manager,
return nil, errors.Wrap(err, "new eth2 http client")
}

eth2Cl.SetForkVersion([4]byte(forkVersion))

if conf.SyntheticBlockProposals {
log.Info(ctx, "Synthetic block proposals enabled")
eth2Cl = eth2wrap.WithSyntheticDuties(eth2Cl)
Expand Down
6 changes: 6 additions & 0 deletions app/eth2wrap/eth2wrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ func (m multi) SetValidatorCache(valCache func(context.Context) (ActiveValidator
}
}

func (m multi) SetForkVersion(forkVersion [4]byte) {
for _, c := range m.clients {
c.SetForkVersion(forkVersion)
}
}

func (m multi) ActiveValidators(ctx context.Context) (ActiveValidators, error) {
const label = "active_validators"
// No latency since this is a cached endpoint.
Expand Down
2 changes: 2 additions & 0 deletions app/eth2wrap/eth2wrap_gen.go

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

3 changes: 3 additions & 0 deletions app/eth2wrap/genwrap/genwrap.go

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

31 changes: 27 additions & 4 deletions app/eth2wrap/httpwrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package eth2wrap
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"fmt"
"io"
Expand All @@ -19,6 +20,7 @@ import (

"github.com/obolnetwork/charon/app/errors"
"github.com/obolnetwork/charon/app/z"
"github.com/obolnetwork/charon/eth2util"
"github.com/obolnetwork/charon/eth2util/eth2exp"
)

Expand Down Expand Up @@ -62,10 +64,15 @@ func newHTTPAdapter(ethSvc *eth2http.Service, address string, timeout time.Durat
// - experimental interfaces
type httpAdapter struct {
*eth2http.Service
address string
timeout time.Duration
valCacheMu sync.RWMutex
valCache func(context.Context) (ActiveValidators, error)
address string
timeout time.Duration
valCacheMu sync.RWMutex
valCache func(context.Context) (ActiveValidators, error)
forkVersion [4]byte
}

func (h *httpAdapter) SetForkVersion(forkVersion [4]byte) {
h.forkVersion = forkVersion
}

func (h *httpAdapter) SetValidatorCache(valCache func(context.Context) (ActiveValidators, error)) {
Expand Down Expand Up @@ -182,6 +189,22 @@ func (h *httpAdapter) NodePeerCount(ctx context.Context) (int, error) {
return resp.Data.Connected, nil
}

// Domain returns the signing domain for a given domain type.
// After EIP-7044, the VOLUNTARY_EXIT domain must always return a domain relative to the Capella hardfork.
// This method returns just that for that domain type, otherwise follows the standard go-eth2-client flow.
func (h *httpAdapter) Domain(ctx context.Context, domainType eth2p0.DomainType, epoch eth2p0.Epoch) (eth2p0.Domain, error) {
if domainType == (eth2p0.DomainType{0x04, 0x00, 0x00, 0x00}) { // voluntary exit domain
domain, err := eth2util.CapellaDomain(ctx, "0x"+hex.EncodeToString(h.forkVersion[:]), h.Service, h.Service)
if err != nil {
return eth2p0.Domain{}, errors.Wrap(err, "get domain")
}

return domain, nil
}

return h.Service.Domain(ctx, domainType, epoch)
}

type submitBeaconCommitteeSelectionsJSON struct {
Data []*eth2exp.BeaconCommitteeSelection `json:"data"`
}
Expand Down
9 changes: 9 additions & 0 deletions app/eth2wrap/lazy.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ func (l *lazy) getOrCreateClient(ctx context.Context) (Client, error) {
return cl, err
}

func (l *lazy) SetForkVersion(forkVersion [4]byte) {
cl, ok := l.getClient()
if !ok {
return
}

cl.SetForkVersion(forkVersion)
}

func (l *lazy) Name() string {
cl, ok := l.getClient()
if !ok {
Expand Down
134 changes: 134 additions & 0 deletions eth2util/helper_capella.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright © 2022-2024 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1

package eth2util

import (
"context"
"encoding/hex"
"strings"

eth2client "github.com/attestantio/go-eth2-client"
eth2api "github.com/attestantio/go-eth2-client/api"
eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
ssz "github.com/ferranbt/fastssz"

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

var capellaForkMap = map[string]string{
"0x00000000": "0x03000000",
"0x00001020": "0x03001020",
"0x00000064": "0x03000064",
"0x90000069": "0x90000072",
"0x01017000": "0x04017000",
}

// CapellaFork maps generic fork hashes to their specific Capella hardfork
// values.
func CapellaFork(forkHash string) (string, error) {
d, ok := capellaForkMap[forkHash]
if !ok {
return "", errors.New("no capella fork for specified fork")
}

return d, nil
}

type forkDataType struct {
CurrentVersion [4]byte
GenesisValidatorsRoot [32]byte
}

func (e forkDataType) GetTree() (*ssz.Node, error) {
node, err := ssz.ProofTree(e)
if err != nil {
return nil, errors.Wrap(err, "proof tree")
}

return node, nil
}

func (e forkDataType) HashTreeRoot() ([32]byte, error) {
hash, err := ssz.HashWithDefaultHasher(e)
if err != nil {
return [32]byte{}, errors.Wrap(err, "hash with default hasher")
}

return hash, nil
}

func (e forkDataType) HashTreeRootWith(hh ssz.HashWalker) error {
indx := hh.Index()

// Field (0) 'CurrentVersion'
hh.PutBytes(e.CurrentVersion[:])

// Field (1) 'GenesisValidatorsRoot'
hh.PutBytes(e.GenesisValidatorsRoot[:])

hh.Merkleize(indx)

return nil
}

// ComputeDomain computes the domain for a given domainType, genesisValidatorRoot at the specified fork hash.
func ComputeDomain(forkHash string, domainType eth2p0.DomainType, genesisValidatorRoot eth2p0.Root) (eth2p0.Domain, error) {
fb, err := hex.DecodeString(strings.TrimPrefix(forkHash, "0x"))
if err != nil {
return eth2p0.Domain{}, errors.Wrap(err, "fork hash hex")
}

_, err = CapellaFork(forkHash)
if err != nil {
return eth2p0.Domain{}, errors.Wrap(err, "invalid fork hash")
}

rawFdt := forkDataType{GenesisValidatorsRoot: genesisValidatorRoot, CurrentVersion: [4]byte(fb)}
fdt, err := rawFdt.HashTreeRoot()
if err != nil {
return eth2p0.Domain{}, errors.Wrap(err, "fork data type hash tree root")
}

var domain []byte
domain = append(domain, domainType[:]...)
domain = append(domain, fdt[:28]...)

return eth2p0.Domain(domain), nil
}

// CapellaDomain returns the Capella signature domain, calculating it given the fork hash string.
func CapellaDomain(
ctx context.Context,
forkHash string,
specProvider eth2client.SpecProvider,
genesisProvider eth2client.GenesisProvider,
) (eth2p0.Domain, error) {
rawSpec, err := specProvider.Spec(ctx, &eth2api.SpecOpts{})
if err != nil {
return eth2p0.Domain{}, errors.Wrap(err, "fetch spec")
}

genesis, err := genesisProvider.Genesis(ctx, &eth2api.GenesisOpts{})
if err != nil {
return eth2p0.Domain{}, errors.Wrap(err, "fetch genesis")
}

spec := rawSpec.Data

domainType, ok := spec["DOMAIN_VOLUNTARY_EXIT"]
if !ok {
return eth2p0.Domain{}, errors.New("domain type not found in spec")
}

domainTyped, ok := domainType.(eth2p0.DomainType)
if !ok {
return [32]byte{}, errors.New("invalid domain type")
}

domain, err := ComputeDomain(forkHash, domainTyped, genesis.Data.GenesisValidatorsRoot)
if err != nil {
return eth2p0.Domain{}, errors.Wrap(err, "compute domain")
}

return domain, nil
}
Loading

0 comments on commit cc3c003

Please sign in to comment.