Skip to content

Commit

Permalink
eth2util/signing: add NewVerifyFunc (#912)
Browse files Browse the repository at this point in the history
Adds NewVerifyFunc in eth2util/signing.

category: refactor
ticket: #217
  • Loading branch information
dB2510 authored Aug 4, 2022
1 parent 2c4a704 commit b7d5237
Show file tree
Hide file tree
Showing 2 changed files with 318 additions and 5 deletions.
141 changes: 136 additions & 5 deletions eth2util/signing/signing.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import (
eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"

"github.com/obolnetwork/charon/app/errors"
"github.com/obolnetwork/charon/core"
"github.com/obolnetwork/charon/eth2util"
"github.com/obolnetwork/charon/tbls"
"github.com/obolnetwork/charon/tbls/tblsconv"
)

// DomainName as defined in eth2 spec.
Expand All @@ -42,14 +46,15 @@ const (
// DomainContributionAndProof DomainName = "DOMAIN_CONTRIBUTION_AND_PROOF".
)

// Eth2DomainProvider is the subset of eth2 beacon api provider required to get a signing domain.
type Eth2DomainProvider interface {
eth2client.SpecProvider
// Eth2Provider is the subset of eth2 beacon api provider required to get a signing domain.
type Eth2Provider interface {
eth2client.DomainProvider
eth2client.SlotsPerEpochProvider
eth2client.SpecProvider
}

// GetDomain returns the beacon domain for the provided type.
func GetDomain(ctx context.Context, eth2Cl Eth2DomainProvider, name DomainName, epoch eth2p0.Epoch) (eth2p0.Domain, error) {
func GetDomain(ctx context.Context, eth2Cl Eth2Provider, name DomainName, epoch eth2p0.Epoch) (eth2p0.Domain, error) {
// TODO(corver): Remove once https://github.com/attestantio/go-eth2-client/pull/23 is released
if name == DomainApplicationBuilder {
// See https://github.com/ethereum/builder-specs/blob/main/specs/builder.md#domain-types
Expand All @@ -76,7 +81,7 @@ func GetDomain(ctx context.Context, eth2Cl Eth2DomainProvider, name DomainName,

// GetDataRoot wraps the signing root with the domain and returns signing data hash tree root.
// The result should be identical to what was signed by the VC.
func GetDataRoot(ctx context.Context, eth2Cl Eth2DomainProvider, name DomainName, epoch eth2p0.Epoch, root eth2p0.Root) ([32]byte, error) {
func GetDataRoot(ctx context.Context, eth2Cl Eth2Provider, name DomainName, epoch eth2p0.Epoch, root eth2p0.Root) ([32]byte, error) {
domain, err := GetDomain(ctx, eth2Cl, name, epoch)
if err != nil {
return [32]byte{}, err
Expand All @@ -89,3 +94,129 @@ func GetDataRoot(ctx context.Context, eth2Cl Eth2DomainProvider, name DomainName

return msg, nil
}

// NewVerifyFunc returns partial signature verification function which verifies given ParSignedData according to Duty Type.
//nolint:gocognit
func NewVerifyFunc(eth2Cl Eth2Provider) func(context.Context, core.Duty, core.PubKey, core.ParSignedData) error {
return func(ctx context.Context, duty core.Duty, pubkey core.PubKey, parSig core.ParSignedData) error {
switch duty.Type {
case core.DutyAttester:
att, ok := parSig.SignedData.(core.Attestation)
if !ok {
return errors.New("invalid attestation")
}

sigRoot, err := att.Data.HashTreeRoot()
if err != nil {
return errors.Wrap(err, "hash attestation data")
}

return verify(ctx, eth2Cl, DomainBeaconAttester, att.Data.Target.Epoch, sigRoot, att.Attestation.Signature, pubkey)
case core.DutyProposer:
block, ok := parSig.SignedData.(core.VersionedSignedBeaconBlock)
if !ok {
return errors.New("invalid block")
}

// Calculate slot epoch
epoch, err := epochFromSlot(ctx, eth2Cl, eth2p0.Slot(duty.Slot))
if err != nil {
return err
}

sigRoot, err := block.Root()
if err != nil {
return err
}

return verify(ctx, eth2Cl, DomainBeaconProposer, epoch, sigRoot, block.Signature().ToETH2(), pubkey)
case core.DutyBuilderProposer:
blindedBlock, ok := parSig.SignedData.(core.VersionedSignedBlindedBeaconBlock)
if !ok {
return errors.New("invalid blinded block")
}

// Calculate slot epoch
epoch, err := epochFromSlot(ctx, eth2Cl, eth2p0.Slot(duty.Slot))
if err != nil {
return err
}

sigRoot, err := blindedBlock.Root()
if err != nil {
return err
}

return verify(ctx, eth2Cl, DomainBeaconProposer, epoch, sigRoot, blindedBlock.Signature().ToETH2(), pubkey)
case core.DutyRandao:
randao, ok := parSig.SignedData.(core.Signature)
if !ok {
return errors.New("invalid randao")
}

// Calculate slot epoch
epoch, err := epochFromSlot(ctx, eth2Cl, eth2p0.Slot(duty.Slot))
if err != nil {
return err
}

sigRoot, err := eth2util.EpochHashRoot(epoch)
if err != nil {
return err
}

return verify(ctx, eth2Cl, DomainRandao, epoch, sigRoot, randao.ToETH2(), pubkey)
case core.DutyExit:
exit, ok := parSig.SignedData.(core.SignedVoluntaryExit)
if !ok {
return errors.New("invalid voluntary exit")
}

sigRoot, err := exit.Message.HashTreeRoot()
if err != nil {
return err
}

return verify(ctx, eth2Cl, DomainExit, exit.Message.Epoch, sigRoot, exit.SignedVoluntaryExit.Signature, pubkey)
default:
return errors.New("invalid duty type")
}
}
}

func verify(ctx context.Context, eth2Cl Eth2Provider, domain DomainName, epoch eth2p0.Epoch, sigRoot [32]byte, sig eth2p0.BLSSignature, pubkey core.PubKey) error {
sigData, err := GetDataRoot(ctx, eth2Cl, domain, epoch, sigRoot)
if err != nil {
return err
}

// Convert the signature
s, err := tblsconv.SigFromETH2(sig)
if err != nil {
return errors.Wrap(err, "convert signature")
}

// Verify using public share
pubshare, err := tblsconv.KeyFromCore(pubkey)
if err != nil {
return err
}

ok, err := tbls.Verify(pubshare, sigData[:], s)
if err != nil {
return err
} else if !ok {
return errors.New("invalid signature")
}

return nil
}

func epochFromSlot(ctx context.Context, eth2Cl Eth2Provider, slot eth2p0.Slot) (eth2p0.Epoch, error) {
slotsPerEpoch, err := eth2Cl.SlotsPerEpoch(ctx)
if err != nil {
return 0, errors.Wrap(err, "getting slots per epoch")
}

return eth2p0.Epoch(uint64(slot) / slotsPerEpoch), nil
}
182 changes: 182 additions & 0 deletions eth2util/signing/signing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Copyright © 2022 Obol Labs Inc.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <http://www.gnu.org/licenses/>.

package signing_test

import (
"context"
"testing"

eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/coinbase/kryptology/pkg/signatures/bls/bls_sig"
"github.com/stretchr/testify/require"

"github.com/obolnetwork/charon/core"
"github.com/obolnetwork/charon/eth2util"
"github.com/obolnetwork/charon/eth2util/signing"
"github.com/obolnetwork/charon/tbls"
"github.com/obolnetwork/charon/tbls/tblsconv"
"github.com/obolnetwork/charon/testutil"
"github.com/obolnetwork/charon/testutil/beaconmock"
)

func TestVerifyAttestation(t *testing.T) {
att := testutil.RandomAttestation()
duty := core.NewAttesterDuty(int64(att.Data.Slot))

bmock, err := beaconmock.New()
require.NoError(t, err)

root, err := att.Data.HashTreeRoot()
require.NoError(t, err)

sigData, err := signing.GetDataRoot(context.Background(), bmock, signing.DomainBeaconAttester, att.Data.Target.Epoch, root)
require.NoError(t, err)

sig, pubkey := sign(t, sigData[:])
att.Signature = tblsconv.SigToETH2(sig)
psig := core.NewPartialAttestation(att, 0)

verifyFunc := signing.NewVerifyFunc(bmock)
require.NoError(t, verifyFunc(context.Background(), duty, pubkey, psig))
}

func TestVerifyBeaconBlock(t *testing.T) {
block := testutil.RandomCoreVersionSignedBeaconBlock(t)

slot, err := block.Slot()
require.NoError(t, err)

duty := core.NewProposerDuty(int64(slot))

bmock, err := beaconmock.New()
require.NoError(t, err)

root, err := block.Root()
require.NoError(t, err)

slotsPerEpoch, err := bmock.SlotsPerEpoch(context.Background())
require.NoError(t, err)
epoch := eth2p0.Epoch(uint64(slot) / slotsPerEpoch)

sigData, err := signing.GetDataRoot(context.Background(), bmock, signing.DomainBeaconProposer, epoch, root)
require.NoError(t, err)

sig, pubkey := sign(t, sigData[:])
data, err := block.SetSignature(tblsconv.SigToCore(sig))
require.NoError(t, err)

psig := core.ParSignedData{SignedData: data, ShareIdx: 0}
verifyFunc := signing.NewVerifyFunc(bmock)
require.NoError(t, verifyFunc(context.Background(), duty, pubkey, psig))
}

func TestVerifyDutyRandao(t *testing.T) {
duty := core.NewRandaoDuty(123)

bmock, err := beaconmock.New()
require.NoError(t, err)

slotsPerEpoch, err := bmock.SlotsPerEpoch(context.Background())
require.NoError(t, err)

epoch := eth2p0.Epoch(uint64(duty.Slot) / slotsPerEpoch)
sigRoot, err := eth2util.EpochHashRoot(epoch)
require.NoError(t, err)

sigData, err := signing.GetDataRoot(context.Background(), bmock, signing.DomainRandao, epoch, sigRoot)
require.NoError(t, err)

sig, pubkey := sign(t, sigData[:])
psig := core.NewPartialSignature(tblsconv.SigToCore(sig), 0)

verifyFunc := signing.NewVerifyFunc(bmock)
require.NoError(t, verifyFunc(context.Background(), duty, pubkey, psig))
}

func TestVerifyVoluntaryExit(t *testing.T) {
duty := core.NewVoluntaryExit(123)
exit := testutil.RandomExit()

bmock, err := beaconmock.New()
require.NoError(t, err)

sigRoot, err := exit.Message.HashTreeRoot()
require.NoError(t, err)

sigData, err := signing.GetDataRoot(context.Background(), bmock, signing.DomainExit, exit.Message.Epoch, sigRoot)
require.NoError(t, err)

sig, pubkey := sign(t, sigData[:])
exit.Signature = tblsconv.SigToETH2(sig)
psig := core.NewPartialSignedVoluntaryExit(exit, 0)

verifyFunc := signing.NewVerifyFunc(bmock)
require.NoError(t, verifyFunc(context.Background(), duty, pubkey, psig))
}

func TestVerifyBlindedBeaconBlock(t *testing.T) {
duty := core.NewBuilderProposerDuty(123)
blindedBlock := testutil.RandomCoreVersionSignedBlindedBeaconBlock(t)

bmock, err := beaconmock.New()
require.NoError(t, err)

sigRoot, err := blindedBlock.Root()
require.NoError(t, err)

slotsPerEpoch, err := bmock.SlotsPerEpoch(context.Background())
require.NoError(t, err)

epoch := eth2p0.Epoch(uint64(duty.Slot) / slotsPerEpoch)
sigData, err := signing.GetDataRoot(context.Background(), bmock, signing.DomainBeaconProposer, epoch, sigRoot)
require.NoError(t, err)

sig, pubkey := sign(t, sigData[:])
data, err := blindedBlock.SetSignature(tblsconv.SigToCore(sig))
require.NoError(t, err)

psig := core.ParSignedData{SignedData: data, ShareIdx: 0}
verifyFunc := signing.NewVerifyFunc(bmock)
require.NoError(t, verifyFunc(context.Background(), duty, pubkey, psig))
}

func TestVerifyInvalidDuty(t *testing.T) {
duty := core.NewBuilderRegistrationDuty(123)

bmock, err := beaconmock.New()
require.NoError(t, err)

pubkey := testutil.RandomCorePubKey(t)
psig := core.ParSignedData{}

verifyFunc := signing.NewVerifyFunc(bmock)
require.EqualError(t, verifyFunc(context.Background(), duty, pubkey, psig), "invalid duty type")
}

func sign(t *testing.T, data []byte) (*bls_sig.Signature, core.PubKey) {
t.Helper()

blsPubkey, secret, err := tbls.Keygen()
require.NoError(t, err)

sig, err := tbls.Sign(secret, data)
require.NoError(t, err)

pk, err := tblsconv.KeyToCore(blsPubkey)
require.NoError(t, err)

return sig, pk
}

0 comments on commit b7d5237

Please sign in to comment.