Skip to content

Commit

Permalink
cmd: proper exit Capella domain handling (#3055)
Browse files Browse the repository at this point in the history
Handle capella domain calculation for exits the correct way.

Fixes issues noticed during the offsite, that is, not being able to exit through API.

category: bug
ticket: none
  • Loading branch information
gsora authored Apr 30, 2024
1 parent b3c54f1 commit 43b7ab1
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 60 deletions.
20 changes: 10 additions & 10 deletions app/obolapi/exit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,6 @@ const exitEpoch = eth2p0.Epoch(162304)
func TestAPIFlow(t *testing.T) {
kn := 4

handler, addLockFiles := obolapimock.MockServer(false)
srv := httptest.NewServer(handler)

defer srv.Close()

beaconMock, err := beaconmock.New()
require.NoError(t, err)
defer func() {
Expand All @@ -42,6 +37,11 @@ func TestAPIFlow(t *testing.T) {

mockEth2Cl := eth2Client(t, context.Background(), beaconMock.Address())

handler, addLockFiles := obolapimock.MockServer(false, mockEth2Cl)
srv := httptest.NewServer(handler)

defer srv.Close()

random := rand.New(rand.NewSource(int64(0)))

lock, identityKeys, shares := cluster.NewForT(
Expand Down Expand Up @@ -120,11 +120,6 @@ func TestAPIFlow(t *testing.T) {
func TestAPIFlowMissingSig(t *testing.T) {
kn := 4

handler, addLockFiles := obolapimock.MockServer(true)
srv := httptest.NewServer(handler)

defer srv.Close()

beaconMock, err := beaconmock.New()
require.NoError(t, err)
defer func() {
Expand All @@ -133,6 +128,11 @@ func TestAPIFlowMissingSig(t *testing.T) {

mockEth2Cl := eth2Client(t, context.Background(), beaconMock.Address())

handler, addLockFiles := obolapimock.MockServer(true, mockEth2Cl)
srv := httptest.NewServer(handler)

defer srv.Close()

random := rand.New(rand.NewSource(int64(0)))

lock, identityKeys, shares := cluster.NewForT(
Expand Down
2 changes: 2 additions & 0 deletions cmd/exit_broadcast.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ func runBcastFullExit(ctx context.Context, config exitConfig) error {
return errors.Wrap(err, "cannot create eth2 client for specified beacon node")
}

eth2Cl.SetForkVersion([4]byte(cl.GetForkVersion()))

var fullExit eth2p0.SignedVoluntaryExit
maybeExitFilePath := strings.TrimSpace(config.ExitFromFilePath)

Expand Down
15 changes: 10 additions & 5 deletions cmd/exit_broadcast_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,6 @@ func testRunBcastFullExitCmdFlow(t *testing.T, fromFile bool) {
mBytes, err := json.Marshal(lock)
require.NoError(t, err)

handler, addLockFiles := obolapimock.MockServer(false)
srv := httptest.NewServer(handler)
addLockFiles(lock)
defer srv.Close()

validatorSet := beaconmock.ValidatorSet{}

for idx, v := range lock.Validators {
Expand All @@ -104,6 +99,16 @@ func testRunBcastFullExitCmdFlow(t *testing.T, fromFile bool) {
require.NoError(t, beaconMock.Close())
}()

eth2Cl, err := eth2Client(ctx, beaconMock.Address(), 10*time.Second)
require.NoError(t, err)

eth2Cl.SetForkVersion([4]byte(lock.ForkVersion))

handler, addLockFiles := obolapimock.MockServer(false, eth2Cl)
srv := httptest.NewServer(handler)
addLockFiles(lock)
defer srv.Close()

writeAllLockData(t, root, operatorAmt, enrs, operatorShares, mBytes)

for idx := 0; idx < operatorAmt; idx++ {
Expand Down
15 changes: 10 additions & 5 deletions cmd/exit_fetch_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,6 @@ func Test_runFetchExitFullFlow(t *testing.T) {
mBytes, err := json.Marshal(lock)
require.NoError(t, err)

handler, addLockFiles := obolapimock.MockServer(false)
srv := httptest.NewServer(handler)
addLockFiles(lock)
defer srv.Close()

validatorSet := beaconmock.ValidatorSet{}

for idx, v := range lock.Validators {
Expand All @@ -88,6 +83,16 @@ func Test_runFetchExitFullFlow(t *testing.T) {
require.NoError(t, beaconMock.Close())
}()

eth2Cl, err := eth2Client(ctx, beaconMock.Address(), 10*time.Second)
require.NoError(t, err)

eth2Cl.SetForkVersion([4]byte(lock.ForkVersion))

handler, addLockFiles := obolapimock.MockServer(false, eth2Cl)
srv := httptest.NewServer(handler)
addLockFiles(lock)
defer srv.Close()

writeAllLockData(t, root, operatorAmt, enrs, operatorShares, mBytes)

for idx := 0; idx < operatorAmt; idx++ {
Expand Down
2 changes: 2 additions & 0 deletions cmd/exit_sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ func runSignPartialExit(ctx context.Context, config exitConfig) error {
return errors.Wrap(err, "cannot create eth2 client for specified beacon node")
}

eth2Cl.SetForkVersion([4]byte(cl.GetForkVersion()))

oAPI, err := obolapi.New(config.PublishAddress)
if err != nil {
return errors.Wrap(err, "could not create obol api client")
Expand Down
15 changes: 10 additions & 5 deletions cmd/exit_sign_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,6 @@ func Test_runSubmitPartialExitFlow(t *testing.T) {
mBytes, err := json.Marshal(lock)
require.NoError(t, err)

handler, addLockFiles := obolapimock.MockServer(false)
srv := httptest.NewServer(handler)
addLockFiles(lock)
defer srv.Close()

validatorSet := beaconmock.ValidatorSet{}

for idx, v := range lock.Validators {
Expand All @@ -116,6 +111,16 @@ func Test_runSubmitPartialExitFlow(t *testing.T) {
require.NoError(t, beaconMock.Close())
}()

eth2Cl, err := eth2Client(ctx, beaconMock.Address(), 10*time.Second)
require.NoError(t, err)

eth2Cl.SetForkVersion([4]byte(lock.ForkVersion))

handler, addLockFiles := obolapimock.MockServer(false, eth2Cl)
srv := httptest.NewServer(handler)
addLockFiles(lock)
defer srv.Close()

writeAllLockData(t, root, operatorAmt, enrs, operatorShares, mBytes)

baseDir := filepath.Join(root, fmt.Sprintf("op%d", 0))
Expand Down
13 changes: 9 additions & 4 deletions eth2util/helper_capella.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,22 @@ func (e forkDataType) HashTreeRootWith(hh ssz.HashWalker) error {

// 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"))
_, err := hex.DecodeString(strings.TrimPrefix(forkHash, "0x"))
if err != nil {
return eth2p0.Domain{}, errors.Wrap(err, "fork hash hex")
return eth2p0.Domain{}, errors.Wrap(err, "malformed fork hash")
}

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

rawFdt := forkDataType{GenesisValidatorsRoot: genesisValidatorRoot, CurrentVersion: [4]byte(fb)}
cforkHex, err := hex.DecodeString(strings.TrimPrefix(cfork, "0x"))
if err != nil {
return eth2p0.Domain{}, errors.Wrap(err, "capella fork hash hex")
}

rawFdt := forkDataType{GenesisValidatorsRoot: genesisValidatorRoot, CurrentVersion: [4]byte(cforkHex)}
fdt, err := rawFdt.HashTreeRoot()
if err != nil {
return eth2p0.Domain{}, errors.Wrap(err, "fork data type hash tree root")
Expand Down
82 changes: 51 additions & 31 deletions testutil/obolapimock/obolapi_exit.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,26 @@ import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"sync"
"time"

eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/gorilla/mux"

"github.com/obolnetwork/charon/app/errors"
"github.com/obolnetwork/charon/app/eth2wrap"
"github.com/obolnetwork/charon/app/k1util"
"github.com/obolnetwork/charon/app/log"
"github.com/obolnetwork/charon/app/obolapi"
"github.com/obolnetwork/charon/app/z"
"github.com/obolnetwork/charon/cluster"
"github.com/obolnetwork/charon/eth2util/enr"
"github.com/obolnetwork/charon/eth2util/signing"
"github.com/obolnetwork/charon/tbls"
)

const (
Expand Down Expand Up @@ -74,6 +78,9 @@ type testServer struct {

// drop one partial signature when returning the full set
dropOnePsig bool

// Beacon node client, needed to verify exits.
beacon eth2wrap.Client
}

// addLockFiles adds a set of lock files to ts.
Expand Down Expand Up @@ -128,17 +135,33 @@ func (ts *testServer) HandlePartialExit(writer http.ResponseWriter, request *htt

for _, exit := range data.PartialExits {
exit := exit
valFound := false
var validatorFound bool
var partialPubkey []byte

for _, lockVal := range lock.Validators {
if strings.EqualFold(exit.PublicKey, lockVal.PublicKeyHex()) {
valFound = true
valHex := lockVal.PublicKeyHex()
if strings.EqualFold(exit.PublicKey, valHex) {
partialPubkey = lockVal.PubShares[data.ShareIdx-1]
validatorFound = true

break
}
}

if !valFound {
continue
if !validatorFound {
writeErr(writer, http.StatusBadRequest, fmt.Sprintf("could not find validator %s in lock file", exit.PublicKey))
return
}

exitSigData, err := sigDataForExit(request.Context(), *exit.SignedExitMessage.Message, ts.beacon, exit.SignedExitMessage.Message.Epoch)
if err != nil {
writeErr(writer, http.StatusInternalServerError, err.Error())
return
}

if err := tbls.Verify(tbls.PublicKey(partialPubkey), exitSigData[:], tbls.Signature(exit.SignedExitMessage.Signature)); err != nil {
writeErr(writer, http.StatusBadRequest, err.Error())
return
}

// check that the last partial exit's data is the same as the new one
Expand Down Expand Up @@ -294,12 +317,13 @@ func cleanTmpl(tmpl string) string {

// MockServer returns a obol API mock test server.
// It returns a http.Handler to be served over HTTP, and a function to add cluster lock files to its database.
func MockServer(dropOnePsig bool) (http.Handler, func(lock cluster.Lock)) {
func MockServer(dropOnePsig bool, beacon eth2wrap.Client) (http.Handler, func(lock cluster.Lock)) {
ts := testServer{
lock: sync.Mutex{},
partialExits: map[string][]exitBlob{},
lockFiles: map[string]cluster.Lock{},
dropOnePsig: dropOnePsig,
beacon: beacon,
}

router := mux.NewRouter()
Expand All @@ -313,30 +337,6 @@ func MockServer(dropOnePsig bool) (http.Handler, func(lock cluster.Lock)) {
return router, ts.addLockFiles
}

// Run runs obol api mock on the provided bind port.
func Run(_ context.Context, bind string, locks []cluster.Lock, dropOnePsig bool) error {
ms, addLock := MockServer(dropOnePsig)

for _, lock := range locks {
addLock(lock)
}

srv := http.Server{
ReadTimeout: 1 * time.Second,
WriteTimeout: 1 * time.Second,
IdleTimeout: 30 * time.Second,
ReadHeaderTimeout: 2 * time.Second,
Handler: ms,
Addr: bind,
}

if err := srv.ListenAndServe(); err != nil {
return errors.Wrap(err, "obol api mock error")
}

return nil
}

func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
bearer := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer")
Expand Down Expand Up @@ -379,3 +379,23 @@ func from0x(data string, length int) ([]byte, error) {

return b, 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) {
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)
if err != nil {
return [32]byte{}, errors.Wrap(err, "get domain")
}

sigData, err := (&eth2p0.SigningData{ObjectRoot: sigRoot, Domain: domain}).HashTreeRoot()
if err != nil {
return [32]byte{}, errors.Wrap(err, "signing data hash tree root")
}

return sigData, nil
}

0 comments on commit 43b7ab1

Please sign in to comment.