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

app: sign with Capella domain #65

Merged
merged 5 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
repos:
# First run code formatters
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
rev: v4.1.0
hooks:
- id: trailing-whitespace # trims trailing whitespace
exclude: "testdata"
Expand All @@ -12,10 +12,10 @@ repos:
- id: no-commit-to-branch # Protect specific branches (default: main/master) from direct checkins

- repo: https://github.com/ObolNetwork/go-pre-commit-hooks
rev: v0.0.2
rev: v0.0.3
hooks:
- id: check-go-version
args: [ -v=go1.20 ] # Only check minor version locally
args: [ -v=go1.22 ] # Only check minor version locally
pass_filenames: false
additional_dependencies: [ packaging ]
- id: check-licence-header
Expand All @@ -28,21 +28,21 @@ repos:
- id: go-fiximports # format imports for go source files

- repo: https://github.com/tekwizely/pre-commit-golang
rev: v1.0.0-rc.1 # cannot use master as it is a mutable reference and is not supported
rev: v1.0.0-beta.5 # cannot use master as it is a mutable reference and is not supported
hooks:
- id: go-mod-tidy
files: go.mod

- repo: https://github.com/dnephin/pre-commit-golang
rev: v0.5.1
rev: v0.4.0
hooks:
- id: go-fmt
args: [ -w, -s ] # simplify code and write result to (source) file instead of stdout

# Then run code validators (on the formatted code)

- repo: https://github.com/golangci/golangci-lint # See .golangci.yml for config
rev: v1.54.2
rev: v1.56.2
hooks:
- id: golangci-lint

Expand Down
95 changes: 81 additions & 14 deletions app/app.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Copyright © 2022-2024 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1

// Copyright © 2022-2023 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1

package app
Expand Down Expand Up @@ -25,7 +27,6 @@ import (
"github.com/obolnetwork/charon/app/log"
"github.com/obolnetwork/charon/app/z"
manifestpb "github.com/obolnetwork/charon/cluster/manifestpb/v1"
"github.com/obolnetwork/charon/eth2util/signing"
"github.com/obolnetwork/charon/p2p"
"github.com/obolnetwork/charon/tbls"
"github.com/obolnetwork/charon/tbls/tblsconv"
Expand Down Expand Up @@ -115,6 +116,16 @@ func Run(ctx context.Context, config Config) error {
return errors.Wrap(err, "cannot fetch genesis spec")
}

genesis, err := bnClient.Genesis(ctx, &eth2api.GenesisOpts{})
if err != nil {
return errors.Wrap(err, "cannot fetch genesis")
gsora marked this conversation as resolved.
Show resolved Hide resolved
}

capellaForkHash, err := bnapi.CapellaFork("0x" + hex.EncodeToString(genesis.Data.GenesisForkVersion[:]))
if err != nil {
return errors.Wrap(err, "fork hash conversion")
}

rawSlotsPerEpoch, ok := specResp.Data["SLOTS_PER_EPOCH"]
if !ok {
return errors.Wrap(err, "spec field SLOTS_PER_EPOCH not found in spec")
Expand Down Expand Up @@ -144,14 +155,16 @@ func Run(ctx context.Context, config Config) error {
if len(fetchedSignedExits) != len(signedExits) {
writeAllFullExits(
ctx,
bnClient,
oAPI,
cl,
signedExits,
fetchedSignedExits,
config.EjectorExitPath,
shareIdx,
identityKey,
genesis.Data.GenesisValidatorsRoot,
capellaForkHash,
specResp.Data,
gsora marked this conversation as resolved.
Show resolved Hide resolved
)

continue
Expand Down Expand Up @@ -219,7 +232,14 @@ func Run(ctx context.Context, config Config) error {
}

// sign exit
exit, err := signExit(ctx, bnClient, valIndex, valKeyShare.Share, eth2p0.Epoch(config.ExitEpoch))
exit, err := signExit(
valIndex,
valKeyShare.Share,
eth2p0.Epoch(config.ExitEpoch),
genesis.Data.GenesisValidatorsRoot,
capellaForkHash,
specResp.Data,
)
if err != nil {
log.Error(ctx, "Cannot sign exit", err)
continue
Expand Down Expand Up @@ -247,14 +267,16 @@ func Run(ctx context.Context, config Config) error {

writeAllFullExits(
ctx,
bnClient,
oAPI,
cl,
signedExits,
fetchedSignedExits,
config.EjectorExitPath,
shareIdx,
identityKey,
genesis.Data.GenesisValidatorsRoot,
capellaForkHash,
specResp.Data,
)
}

Expand All @@ -266,14 +288,16 @@ func Run(ctx context.Context, config Config) error {
// writeAllFullExits fetches and writes signedExits to disk.
func writeAllFullExits(
ctx context.Context,
eth2Cl eth2wrap.Client,
oAPI obolapi.Client,
cl *manifestpb.Cluster,
signedExits []obolapi.ExitBlob,
alreadySignedExits map[string]struct{},
ejectorExitPath string,
shareIndex uint64,
identityKey *k1.PrivateKey,
genesisValidatorRoot eth2p0.Root,
forkHash string,
spec map[string]any,
) {
for _, signedExit := range signedExits {
if _, ok := alreadySignedExits[signedExit.PublicKey]; ok {
Expand All @@ -282,7 +306,18 @@ func writeAllFullExits(

exitFSPath := filepath.Join(ejectorExitPath, fmt.Sprintf("validator-exit-%s.json", signedExit.PublicKey))

if !fetchFullExit(ctx, eth2Cl, oAPI, cl.InitialMutationHash, signedExit.PublicKey, exitFSPath, shareIndex, identityKey) {
if !fetchFullExit(
ctx,
oAPI,
cl.GetInitialMutationHash(),
signedExit.PublicKey,
exitFSPath,
shareIndex,
identityKey,
genesisValidatorRoot,
forkHash,
spec,
) {
log.Debug(ctx, "Could not fetch full exit for validator", z.Str("validator", signedExit.PublicKey))
continue
}
Expand All @@ -293,7 +328,17 @@ func writeAllFullExits(

// fetchFullExit returns true if a full exit was received from the Obol API, and was written in exitFSPath.
// Each HTTP request has a default timeout.
func fetchFullExit(ctx context.Context, eth2Cl eth2wrap.Client, oAPI obolapi.Client, lockHash []byte, validatorPubkey, exitFSPath string, shareIndex uint64, identityKey *k1.PrivateKey) bool {
func fetchFullExit(
ctx context.Context,
oAPI obolapi.Client,
lockHash []byte,
validatorPubkey, exitFSPath string,
shareIndex uint64,
identityKey *k1.PrivateKey,
genesisValidatorRoot eth2p0.Root,
forkHash string,
spec map[string]any,
) bool {
ctx, cancel := context.WithTimeout(ctx, obolAPITimeout)
defer cancel()

Expand Down Expand Up @@ -336,7 +381,7 @@ func fetchFullExit(ctx context.Context, eth2Cl eth2wrap.Client, oAPI obolapi.Cli
return false
}

exitRoot, err := sigDataForExit(ctx, *fullExit.SignedExitMessage.Message, eth2Cl, fullExit.SignedExitMessage.Message.Epoch)
exitRoot, err := sigDataForExit(*fullExit.SignedExitMessage.Message, genesisValidatorRoot, forkHash, spec)
if err != nil {
log.Error(ctx, "Cannot calculate hash tree root for exit message for verification", err)

Expand Down Expand Up @@ -378,13 +423,20 @@ func shouldProcessValidator(v *eth2v1.Validator) bool {

// signExit signs a voluntary exit message for valIdx with the given keyShare.
// Adapted from charon.
func signExit(ctx context.Context, eth2Cl eth2wrap.Client, valIdx eth2p0.ValidatorIndex, keyShare tbls.PrivateKey, exitEpoch eth2p0.Epoch) (eth2p0.SignedVoluntaryExit, error) {
func signExit(
valIdx eth2p0.ValidatorIndex,
keyShare tbls.PrivateKey,
exitEpoch eth2p0.Epoch,
genesisValidatorRoot eth2p0.Root,
forkHash string,
spec map[string]any,
) (eth2p0.SignedVoluntaryExit, error) {
exit := &eth2p0.VoluntaryExit{
Epoch: exitEpoch,
ValidatorIndex: valIdx,
}

sigData, err := sigDataForExit(ctx, *exit, eth2Cl, exitEpoch)
sigData, err := sigDataForExit(*exit, genesisValidatorRoot, forkHash, spec)
if err != nil {
return eth2p0.SignedVoluntaryExit{}, errors.Wrap(err, "exit hash tree root")
}
Expand All @@ -400,16 +452,31 @@ func signExit(ctx context.Context, eth2Cl eth2wrap.Client, valIdx eth2p0.Validat
}, nil
}

// sigDataForExit returns the hash tree root for the given exit message, at the given exit epoch.
func sigDataForExit(ctx context.Context, exit eth2p0.VoluntaryExit, eth2Cl eth2wrap.Client, exitEpoch eth2p0.Epoch) ([32]byte, error) {
// sigDataForExit returns the hash tree root for the given exit message.
func sigDataForExit(
exit eth2p0.VoluntaryExit,
genesisValidatorRoot eth2p0.Root,
forkHash string,
spec map[string]any,
) ([32]byte, error) {
sigRoot, err := exit.HashTreeRoot()
if err != nil {
return [32]byte{}, errors.Wrap(err, "exit hash tree root")
}

domain, err := signing.GetDomain(ctx, eth2Cl, signing.DomainExit, exitEpoch)
domainType, ok := spec["DOMAIN_VOLUNTARY_EXIT"]
gsora marked this conversation as resolved.
Show resolved Hide resolved
if !ok {
return [32]byte{}, errors.New("domain type not found")
}

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

domain, err := bnapi.ComputeDomain(forkHash, domainTyped, genesisValidatorRoot)
if err != nil {
return [32]byte{}, errors.Wrap(err, "get domain")
return [32]byte{}, err
}

sigData, err := (&eth2p0.SigningData{ObjectRoot: sigRoot, Domain: domain}).HashTreeRoot()
Expand Down
29 changes: 25 additions & 4 deletions app/app_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
// Copyright © 2022-2024 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1

// Copyright © 2022-2023 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1

package app_test

import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
"testing"

eth2api "github.com/attestantio/go-eth2-client/api"
eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
k1 "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/obolnetwork/charon/app/errors"
Expand All @@ -19,18 +23,16 @@ import (
"github.com/obolnetwork/charon/cluster"
"github.com/obolnetwork/charon/cluster/manifest"
ckeystore "github.com/obolnetwork/charon/eth2util/keystore"
"github.com/obolnetwork/charon/eth2util/signing"
"github.com/obolnetwork/charon/tbls"
"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"
"google.golang.org/protobuf/proto"

"github.com/ObolNetwork/lido-dv-exit/app"
"github.com/ObolNetwork/lido-dv-exit/app/bnapi"
"github.com/ObolNetwork/lido-dv-exit/app/util/testutil"
)

const exitEpoch = eth2p0.Epoch(194048)

func Test_NormalFlow(t *testing.T) {
valAmt := 100
operatorAmt := 4
Expand Down Expand Up @@ -272,6 +274,19 @@ func run(

mockEth2Cl := servers.Eth2Client(t, context.Background())

rawSpec, err := mockEth2Cl.Spec(ctx, &eth2api.SpecOpts{})
require.NoError(t, err)

spec := rawSpec.Data

genesis, err := mockEth2Cl.Genesis(ctx, &eth2api.GenesisOpts{})
require.NoError(t, err)

forkHash, err := bnapi.CapellaFork("0x" + hex.EncodeToString(genesis.Data.GenesisForkVersion[:]))
require.NoError(t, err)

genesisValidatorRoot := genesis.Data.GenesisValidatorsRoot

// check that all produced exit messages are signed by all partial keys for all operators
for opIdx := 0; opIdx < operatorAmt; opIdx++ {
opID := fmt.Sprintf("op%d", opIdx)
Expand All @@ -290,7 +305,13 @@ func run(
sigRoot, err := exit.Message.HashTreeRoot()
require.NoError(t, err)

domain, err := signing.GetDomain(context.Background(), mockEth2Cl, signing.DomainExit, exitEpoch)
domainType, ok := spec["DOMAIN_VOLUNTARY_EXIT"]
require.True(t, ok)

domainTyped, ok := domainType.(eth2p0.DomainType)
require.True(t, ok)

domain, err := bnapi.ComputeDomain(forkHash, domainTyped, genesisValidatorRoot)
require.NoError(t, err)

sigData, err := (&eth2p0.SigningData{ObjectRoot: sigRoot, Domain: domain}).HashTreeRoot()
Expand Down
Loading
Loading