diff --git a/core/eth2signeddata.go b/core/eth2signeddata.go new file mode 100644 index 000000000..6fb7b8d08 --- /dev/null +++ b/core/eth2signeddata.go @@ -0,0 +1,153 @@ +// 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 . + +package core + +import ( + "context" + + eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/coinbase/kryptology/pkg/signatures/bls/bls_sig" + + "github.com/obolnetwork/charon/app/eth2wrap" + "github.com/obolnetwork/charon/eth2util" + "github.com/obolnetwork/charon/eth2util/signing" +) + +// VerifyEth2SignedData verifies signature associated with given Eth2SignedData. +func VerifyEth2SignedData(ctx context.Context, eth2Cl eth2wrap.Client, data Eth2SignedData, pubkey *bls_sig.PublicKey) error { + epoch, err := data.Epoch(ctx, eth2Cl) + if err != nil { + return err + } + + sigRoot, err := data.MessageRoot() + if err != nil { + return err + } + + return signing.Verify(ctx, eth2Cl, data.DomainName(), epoch, sigRoot, data.Signature().ToETH2(), pubkey) +} + +// Implement Eth2SignedData for VersionedSignedBeaconBlock. + +func (VersionedSignedBeaconBlock) DomainName() signing.DomainName { + return signing.DomainBeaconProposer +} + +func (b VersionedSignedBeaconBlock) Epoch(ctx context.Context, eth2Cl eth2wrap.Client) (eth2p0.Epoch, error) { + slot, err := b.VersionedSignedBeaconBlock.Slot() + if err != nil { + return 0, err + } + + return eth2util.EpochFromSlot(ctx, eth2Cl, slot) +} + +// Implement Eth2SignedData for VersionedSignedBlindedBeaconBlock. + +func (VersionedSignedBlindedBeaconBlock) DomainName() signing.DomainName { + return signing.DomainBeaconProposer +} + +func (b VersionedSignedBlindedBeaconBlock) Epoch(ctx context.Context, eth2Cl eth2wrap.Client) (eth2p0.Epoch, error) { + slot, err := b.VersionedSignedBlindedBeaconBlock.Slot() + if err != nil { + return 0, err + } + + return eth2util.EpochFromSlot(ctx, eth2Cl, slot) +} + +// Implement Eth2SignedData for Attestation. + +func (Attestation) DomainName() signing.DomainName { + return signing.DomainBeaconAttester +} + +func (a Attestation) Epoch(_ context.Context, _ eth2wrap.Client) (eth2p0.Epoch, error) { + return a.Attestation.Data.Target.Epoch, nil +} + +// Implement Eth2SignedData for SignedVoluntaryExit. + +func (SignedVoluntaryExit) DomainName() signing.DomainName { + return signing.DomainExit +} + +func (e SignedVoluntaryExit) Epoch(_ context.Context, _ eth2wrap.Client) (eth2p0.Epoch, error) { + return e.Message.Epoch, nil +} + +// Implement Eth2SignedData for VersionedSignedValidatorRegistration. + +func (VersionedSignedValidatorRegistration) DomainName() signing.DomainName { + return signing.DomainApplicationBuilder +} + +func (VersionedSignedValidatorRegistration) Epoch(context.Context, eth2wrap.Client) (eth2p0.Epoch, error) { + // Always use epoch 0 for DomainApplicationBuilder. + return 0, nil +} + +// Implement Eth2SignedData for SignedRandao. + +func (SignedRandao) DomainName() signing.DomainName { + return signing.DomainRandao +} + +func (s SignedRandao) Epoch(_ context.Context, _ eth2wrap.Client) (eth2p0.Epoch, error) { + return s.SignedEpoch.Epoch, nil +} + +// Implement Eth2SignedData for BeaconCommitteeSelection. + +func (BeaconCommitteeSelection) DomainName() signing.DomainName { + return signing.DomainSelectionProof +} + +func (s BeaconCommitteeSelection) Epoch(ctx context.Context, eth2Cl eth2wrap.Client) (eth2p0.Epoch, error) { + return eth2util.EpochFromSlot(ctx, eth2Cl, s.Slot) +} + +// Implement Eth2SignedData for SignedAggregateAndProof. + +func (SignedAggregateAndProof) DomainName() signing.DomainName { + return signing.DomainAggregateAndProof +} + +func (s SignedAggregateAndProof) Epoch(ctx context.Context, eth2Cl eth2wrap.Client) (eth2p0.Epoch, error) { + return eth2util.EpochFromSlot(ctx, eth2Cl, s.Message.Aggregate.Data.Slot) +} + +// Implement Eth2SignedData for SignedSyncMessage. + +func (SignedSyncMessage) DomainName() signing.DomainName { + return signing.DomainSyncCommittee +} + +func (s SignedSyncMessage) Epoch(ctx context.Context, eth2Cl eth2wrap.Client) (eth2p0.Epoch, error) { + return eth2util.EpochFromSlot(ctx, eth2Cl, s.Slot) +} + +// Implement Eth2SignedData for SignedSyncContributionAndProof. + +func (SignedSyncContributionAndProof) DomainName() signing.DomainName { + return signing.DomainContributionAndProof +} + +func (s SignedSyncContributionAndProof) Epoch(ctx context.Context, eth2Cl eth2wrap.Client) (eth2p0.Epoch, error) { + return eth2util.EpochFromSlot(ctx, eth2Cl, s.Message.Contribution.Slot) +} diff --git a/core/eth2signeddata_test.go b/core/eth2signeddata_test.go new file mode 100644 index 000000000..ca6569d11 --- /dev/null +++ b/core/eth2signeddata_test.go @@ -0,0 +1,122 @@ +// 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 . + +package core_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/signing" + "github.com/obolnetwork/charon/tbls" + "github.com/obolnetwork/charon/tbls/tblsconv" + "github.com/obolnetwork/charon/testutil" + "github.com/obolnetwork/charon/testutil/beaconmock" +) + +func TestVerifyEth2SignedData(t *testing.T) { + tests := []struct { + name string + data core.Eth2SignedData + }{ + { + name: "verify attestation", + data: core.NewAttestation(testutil.RandomAttestation()), + }, + { + name: "verify beacon block", + data: testutil.RandomCoreVersionSignedBeaconBlock(t), + }, + { + name: "verify blinded beacon block", + data: testutil.RandomCoreVersionSignedBlindedBeaconBlock(t), + }, + { + name: "verify randao", + data: testutil.RandomCoreSignedRandao(), + }, + { + name: "verify voluntary exit", + data: core.NewSignedVoluntaryExit(testutil.RandomExit()), + }, + { + name: "verify registration", + data: testutil.RandomCoreVersionedSignedValidatorRegistration(t), + }, + { + name: "verify beacon committee selection", + data: testutil.RandomCoreBeaconCommitteeSelection(), + }, + { + name: "verify attestation aggregate and proof", + data: core.SignedAggregateAndProof{ + SignedAggregateAndProof: eth2p0.SignedAggregateAndProof{ + Message: testutil.RandomAggregateAndProof(), + }, + }, + }, + { + name: "verify sync committee message", + data: core.NewSignedSyncMessage(testutil.RandomSyncCommitteeMessage()), + }, + { + name: "verify sync committee contribution and proof", + data: core.NewSignedSyncContributionAndProof(testutil.RandomSignedSyncContributionAndProof()), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + bmock, err := beaconmock.New() + require.NoError(t, err) + + epoch, err := test.data.Epoch(context.Background(), bmock) + require.NoError(t, err) + + root, err := test.data.MessageRoot() + require.NoError(t, err) + + sigData, err := signing.GetDataRoot(context.Background(), bmock, test.data.DomainName(), epoch, root) + require.NoError(t, err) + + sig, pubkey := sign(t, sigData[:]) + + s, err := test.data.SetSignature(sig) + require.NoError(t, err) + + eth2Signed, ok := s.(core.Eth2SignedData) + require.True(t, ok) + + require.NoError(t, core.VerifyEth2SignedData(context.Background(), bmock, eth2Signed, pubkey)) + }) + } +} + +func sign(t *testing.T, data []byte) (core.Signature, *bls_sig.PublicKey) { + t.Helper() + + pk, secret, err := tbls.Keygen() + require.NoError(t, err) + + sig, err := tbls.Sign(secret, data) + require.NoError(t, err) + + return tblsconv.SigToCore(sig), pk +} diff --git a/core/signeddata.go b/core/signeddata.go index 5c34852bd..2692ac340 100644 --- a/core/signeddata.go +++ b/core/signeddata.go @@ -16,7 +16,6 @@ package core import ( - "context" "encoding/json" eth2api "github.com/attestantio/go-eth2-client/api" @@ -25,13 +24,10 @@ import ( "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/bellatrix" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" - "github.com/coinbase/kryptology/pkg/signatures/bls/bls_sig" "github.com/obolnetwork/charon/app/errors" - "github.com/obolnetwork/charon/app/eth2wrap" "github.com/obolnetwork/charon/eth2util" "github.com/obolnetwork/charon/eth2util/eth2exp" - "github.com/obolnetwork/charon/eth2util/signing" ) var ( @@ -292,19 +288,6 @@ func (b *VersionedSignedBeaconBlock) UnmarshalJSON(input []byte) error { return nil } -func (VersionedSignedBeaconBlock) DomainName() signing.DomainName { - return signing.DomainBeaconProposer -} - -func (b VersionedSignedBeaconBlock) Epoch(ctx context.Context, eth2Cl eth2wrap.Client) (eth2p0.Epoch, error) { - slot, err := b.VersionedSignedBeaconBlock.Slot() - if err != nil { - return 0, err - } - - return eth2util.EpochFromSlot(ctx, eth2Cl, slot) -} - // NewVersionedSignedBlindedBeaconBlock validates and returns a new wrapped VersionedSignedBlindedBeaconBlock. func NewVersionedSignedBlindedBeaconBlock(block *eth2api.VersionedSignedBlindedBeaconBlock) (VersionedSignedBlindedBeaconBlock, error) { switch block.Version { @@ -440,19 +423,6 @@ func (b *VersionedSignedBlindedBeaconBlock) UnmarshalJSON(input []byte) error { return nil } -func (VersionedSignedBlindedBeaconBlock) DomainName() signing.DomainName { - return signing.DomainBeaconProposer -} - -func (b VersionedSignedBlindedBeaconBlock) Epoch(ctx context.Context, eth2Cl eth2wrap.Client) (eth2p0.Epoch, error) { - slot, err := b.VersionedSignedBlindedBeaconBlock.Slot() - if err != nil { - return 0, err - } - - return eth2util.EpochFromSlot(ctx, eth2Cl, slot) -} - // versionedRawBlockJSON is a custom VersionedSignedBeaconBlock or VersionedSignedBlindedBeaconBlock serialiser. type versionedRawBlockJSON struct { Version int `json:"version"` @@ -521,14 +491,6 @@ func (a *Attestation) UnmarshalJSON(b []byte) error { return a.Attestation.UnmarshalJSON(b) } -func (Attestation) DomainName() signing.DomainName { - return signing.DomainBeaconAttester -} - -func (a Attestation) Epoch(_ context.Context, _ eth2wrap.Client) (eth2p0.Epoch, error) { - return a.Attestation.Data.Target.Epoch, nil -} - // NewSignedVoluntaryExit is a convenience function that returns a new signed voluntary exit. func NewSignedVoluntaryExit(exit *eth2p0.SignedVoluntaryExit) SignedVoluntaryExit { return SignedVoluntaryExit{SignedVoluntaryExit: *exit} @@ -590,14 +552,6 @@ func (e *SignedVoluntaryExit) UnmarshalJSON(b []byte) error { return e.SignedVoluntaryExit.UnmarshalJSON(b) } -func (SignedVoluntaryExit) DomainName() signing.DomainName { - return signing.DomainExit -} - -func (e SignedVoluntaryExit) Epoch(_ context.Context, _ eth2wrap.Client) (eth2p0.Epoch, error) { - return e.Message.Epoch, nil -} - // versionedRawValidatorRegistrationJSON is a custom VersionedSignedValidator serialiser. type versionedRawValidatorRegistrationJSON struct { Version int `json:"version"` @@ -735,15 +689,6 @@ func (r *VersionedSignedValidatorRegistration) UnmarshalJSON(input []byte) error return nil } -func (VersionedSignedValidatorRegistration) DomainName() signing.DomainName { - return signing.DomainApplicationBuilder -} - -func (VersionedSignedValidatorRegistration) Epoch(context.Context, eth2wrap.Client) (eth2p0.Epoch, error) { - // Always use epoch 0 for DomainApplicationBuilder. - return 0, nil -} - // NewPartialSignedRandao is a convenience function that returns a new partially signed Randao Reveal. func NewPartialSignedRandao(epoch eth2p0.Epoch, randao eth2p0.BLSSignature, shareIdx int) ParSignedData { return ParSignedData{ @@ -801,14 +746,6 @@ func (s SignedRandao) clone() (SignedRandao, error) { return resp, nil } -func (SignedRandao) DomainName() signing.DomainName { - return signing.DomainRandao -} - -func (s SignedRandao) Epoch(_ context.Context, _ eth2wrap.Client) (eth2p0.Epoch, error) { - return s.SignedEpoch.Epoch, nil -} - // NewBeaconCommitteeSelection is a convenience function which returns new signed BeaconCommitteeSelection. func NewBeaconCommitteeSelection(selection *eth2exp.BeaconCommitteeSelection) BeaconCommitteeSelection { return BeaconCommitteeSelection{ @@ -870,14 +807,6 @@ func (s *BeaconCommitteeSelection) UnmarshalJSON(input []byte) error { return s.BeaconCommitteeSelection.UnmarshalJSON(input) } -func (BeaconCommitteeSelection) DomainName() signing.DomainName { - return signing.DomainSelectionProof -} - -func (s BeaconCommitteeSelection) Epoch(ctx context.Context, eth2Cl eth2wrap.Client) (eth2p0.Epoch, error) { - return eth2util.EpochFromSlot(ctx, eth2Cl, s.Slot) -} - // SyncCommitteeSelection wraps an eth2exp.SyncCommitteeSelection and implements SignedData. type SyncCommitteeSelection struct { eth2exp.SyncCommitteeSelection @@ -1003,14 +932,6 @@ func (s *SignedAggregateAndProof) UnmarshalJSON(input []byte) error { return s.SignedAggregateAndProof.UnmarshalJSON(input) } -func (SignedAggregateAndProof) DomainName() signing.DomainName { - return signing.DomainAggregateAndProof -} - -func (s SignedAggregateAndProof) Epoch(ctx context.Context, eth2Cl eth2wrap.Client) (eth2p0.Epoch, error) { - return eth2util.EpochFromSlot(ctx, eth2Cl, s.Message.Aggregate.Data.Slot) -} - // NewSignedSyncMessage is a convenience function which returns new signed SignedSyncMessage. func NewSignedSyncMessage(data *altair.SyncCommitteeMessage) SignedSyncMessage { return SignedSyncMessage{SyncCommitteeMessage: *data} @@ -1070,14 +991,6 @@ func (s *SignedSyncMessage) UnmarshalJSON(input []byte) error { return s.SyncCommitteeMessage.UnmarshalJSON(input) } -func (SignedSyncMessage) DomainName() signing.DomainName { - return signing.DomainSyncCommittee -} - -func (s SignedSyncMessage) Epoch(ctx context.Context, eth2Cl eth2wrap.Client) (eth2p0.Epoch, error) { - return eth2util.EpochFromSlot(ctx, eth2Cl, s.Slot) -} - func NewSignedSyncContributionAndProof(proof *altair.SignedContributionAndProof) SignedSyncContributionAndProof { return SignedSyncContributionAndProof{SignedContributionAndProof: *proof} } @@ -1128,14 +1041,6 @@ func (s *SignedSyncContributionAndProof) UnmarshalJSON(input []byte) error { return s.SignedContributionAndProof.UnmarshalJSON(input) } -func (SignedSyncContributionAndProof) DomainName() signing.DomainName { - return signing.DomainContributionAndProof -} - -func (s SignedSyncContributionAndProof) Epoch(ctx context.Context, eth2Cl eth2wrap.Client) (eth2p0.Epoch, error) { - return eth2util.EpochFromSlot(ctx, eth2Cl, s.Message.Contribution.Slot) -} - // cloneJSONMarshaler clones the marshaler by serialising to-from json // since eth2 types contain pointers. The result is stored in the value pointed to by v. func cloneJSONMarshaler(data json.Marshaler, v any) error { @@ -1150,18 +1055,3 @@ func cloneJSONMarshaler(data json.Marshaler, v any) error { return nil } - -// VerifyEth2SignedData verifies signature associated with given Eth2SignedData. -func VerifyEth2SignedData(ctx context.Context, eth2Cl eth2wrap.Client, data Eth2SignedData, pubkey *bls_sig.PublicKey) error { - epoch, err := data.Epoch(ctx, eth2Cl) - if err != nil { - return err - } - - sigRoot, err := data.MessageRoot() - if err != nil { - return err - } - - return signing.Verify(ctx, eth2Cl, data.DomainName(), epoch, sigRoot, data.Signature().ToETH2(), pubkey) -} diff --git a/eth2util/signing/signing.go b/eth2util/signing/signing.go index 329656036..fcb4e9c3e 100644 --- a/eth2util/signing/signing.go +++ b/eth2util/signing/signing.go @@ -99,7 +99,7 @@ func VerifyAggregateAndProofSelection(ctx context.Context, eth2Cl eth2wrap.Clien // Verify returns an error if the signature doesn't match the eth2 domain signed root. func Verify(ctx context.Context, eth2Cl eth2wrap.Client, domain DomainName, epoch eth2p0.Epoch, sigRoot eth2p0.Root, - signature eth2p0.BLSSignature, pubshare *bls_sig.PublicKey, + signature eth2p0.BLSSignature, pubkey *bls_sig.PublicKey, ) error { ctx, span := tracer.Start(ctx, "eth2util.Verify") defer span.End() @@ -121,7 +121,7 @@ func Verify(ctx context.Context, eth2Cl eth2wrap.Client, domain DomainName, epoc } span.AddEvent("tbls.Verify") - ok, err := tbls.Verify(pubshare, sigData[:], s) + ok, err := tbls.Verify(pubkey, sigData[:], s) if err != nil { return err } else if !ok { diff --git a/eth2util/signing/signing_test.go b/eth2util/signing/signing_test.go index 55bc9d2c1..c2e72c5f2 100644 --- a/eth2util/signing/signing_test.go +++ b/eth2util/signing/signing_test.go @@ -23,96 +23,14 @@ import ( "testing" eth2v1 "github.com/attestantio/go-eth2-client/api/v1" - 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/signing" "github.com/obolnetwork/charon/tbls" "github.com/obolnetwork/charon/tbls/tblsconv" - "github.com/obolnetwork/charon/testutil" "github.com/obolnetwork/charon/testutil/beaconmock" ) -func TestVerifySignedData(t *testing.T) { - tests := []struct { - name string - data core.Eth2SignedData - domain signing.DomainName - }{ - { - name: "verify attestation", - data: core.NewAttestation(testutil.RandomAttestation()), - domain: signing.DomainBeaconAttester, - }, - { - name: "verify beacon block", - data: testutil.RandomCoreVersionSignedBeaconBlock(t), - domain: signing.DomainBeaconProposer, - }, - { - name: "verify blinded beacon block", - data: testutil.RandomCoreVersionSignedBlindedBeaconBlock(t), - domain: signing.DomainBeaconProposer, - }, - { - name: "verify randao", - data: testutil.RandomCoreSignedRandao(), - domain: signing.DomainRandao, - }, - { - name: "verify voluntary exit", - data: core.NewSignedVoluntaryExit(testutil.RandomExit()), - domain: signing.DomainExit, - }, - { - name: "verify registration", - data: testutil.RandomCoreVersionedSignedValidatorRegistration(t), - domain: signing.DomainApplicationBuilder, - }, - { - name: "verify beacon committee selection", - data: testutil.RandomCoreBeaconCommitteeSelection(), - domain: signing.DomainSelectionProof, - }, - { - name: "verify attestation aggregate and proof", - data: core.SignedAggregateAndProof{ - SignedAggregateAndProof: eth2p0.SignedAggregateAndProof{ - Message: testutil.RandomAggregateAndProof(), - }, - }, - domain: signing.DomainAggregateAndProof, - }, - { - name: "verify sync committee message", - data: core.NewSignedSyncMessage(testutil.RandomSyncCommitteeMessage()), - domain: signing.DomainSyncCommittee, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - bmock, err := beaconmock.New() - require.NoError(t, err) - - epoch, err := test.data.Epoch(context.Background(), bmock) - require.NoError(t, err) - - root, err := test.data.MessageRoot() - require.NoError(t, err) - - sigData, err := signing.GetDataRoot(context.Background(), bmock, test.domain, epoch, root) - require.NoError(t, err) - - sig, pubkey := sign(t, sigData[:]) - - require.NoError(t, signing.Verify(context.Background(), bmock, test.domain, epoch, root, sig, pubkey)) - }) - } -} - func TestVerifyRegistrationReference(t *testing.T) { bmock, err := beaconmock.New() require.NoError(t, err) @@ -154,16 +72,10 @@ func TestVerifyRegistrationReference(t *testing.T) { fmt.Sprintf("%x", registration.Signature), fmt.Sprintf("%x", sigEth2), ) -} -func sign(t *testing.T, data []byte) (eth2p0.BLSSignature, *bls_sig.PublicKey) { - t.Helper() - - pk, secret, err := tbls.Keygen() + pubkey, err := secretShare.GetPublicKey() require.NoError(t, err) - sig, err := tbls.Sign(secret, data) + err = signing.Verify(context.Background(), bmock, signing.DomainApplicationBuilder, 0, sigRoot, tblsconv.SigToETH2(sig), pubkey) require.NoError(t, err) - - return tblsconv.SigToETH2(sig), pk }