diff --git a/core/consensus/component.go b/core/consensus/component.go index 800505aaf..3c971a8f1 100644 --- a/core/consensus/component.go +++ b/core/consensus/component.go @@ -75,7 +75,11 @@ func New(tcpNode host.Host, sender *p2p.Sender, peers []p2p.Peer, p2pKey *ecdsa. // Decide sends consensus output to subscribers. Decide: func(ctx context.Context, duty core.Duty, _ [32]byte, qcommit []qbft.Msg[core.Duty, [32]byte]) { - set := core.UnsignedDataSetFromProto(qcommit[0].(msg).msg.Value) + set, err := core.UnsignedDataSetFromProto(duty.Type, qcommit[0].(msg).msg.Value) + if err != nil { + log.Error(ctx, "Unmarshal decided value", err) + return + } for _, sub := range c.subs { if err := sub(ctx, duty, set); err != nil { log.Warn(ctx, "Subscriber error", err) @@ -146,7 +150,11 @@ func (c *Component) Propose(ctx context.Context, duty core.Duty, data core.Unsig log.Debug(ctx, "Starting qbft consensus instance", z.Any("duty", duty)) // Hash the proposed data, since qbft ony supports simple comparable values. - value := core.UnsignedDataSetToProto(data) + value, err := core.UnsignedDataSetToProto(data) + if err != nil { + return err + } + hash, err := hashProto(value) if err != nil { return err diff --git a/core/consensus/component_test.go b/core/consensus/component_test.go index 695206f94..1285d3acc 100644 --- a/core/consensus/component_test.go +++ b/core/consensus/component_test.go @@ -106,7 +106,7 @@ func TestComponent(t *testing.T) { runErrs <- c.Propose( log.WithCtx(ctx, z.Int("node", i)), core.Duty{Type: core.DutyAttester}, - core.UnsignedDataSet{pubkey: core.UnsignedData{byte(i)}}, + core.UnsignedDataSet{pubkey: testutil.RandomCoreAttestationData(t)}, ) }(ctx, i, c) } @@ -118,7 +118,6 @@ func TestComponent(t *testing.T) { for { select { case err := <-runErrs: - log.Error(ctx, "", err) require.NoError(t, err) case res := <-results: t.Logf("Got result: %#v", res) diff --git a/core/consensus/msg_internal_test.go b/core/consensus/msg_internal_test.go index 56567ec9b..a6d4fda58 100644 --- a/core/consensus/msg_internal_test.go +++ b/core/consensus/msg_internal_test.go @@ -35,12 +35,13 @@ func TestHashProto(t *testing.T) { set := testutil.RandomUnsignedDataSet(t) testutil.RequireGoldenJSON(t, set) - setPB := core.UnsignedDataSetToProto(set) + setPB, err := core.UnsignedDataSetToProto(set) + require.NoError(t, err) hash, err := hashProto(setPB) require.NoError(t, err) require.Equal(t, - "2629f0aaf0f78c37ad7aeae4cc3ee0ff05741a9b341e0002c03b257d62b2e237", + "39933362de95b6dabf0b6512bc19a43826debf8cb71936d99e251b053ad8846d", hex.EncodeToString(hash[:]), ) } @@ -49,14 +50,19 @@ func TestSigning(t *testing.T) { privkey, err := crypto.GenerateKey() require.NoError(t, err) + v, err := core.UnsignedDataSetToProto(testutil.RandomUnsignedDataSet(t)) + require.NoError(t, err) + pv, err := core.UnsignedDataSetToProto(testutil.RandomUnsignedDataSet(t)) + require.NoError(t, err) + msg := &pbv1.QBFTMsg{ Type: rand.Int63(), Duty: core.DutyToProto(core.Duty{Type: core.DutyType(rand.Int()), Slot: rand.Int63()}), PeerIdx: rand.Int63(), Round: rand.Int63(), - Value: core.UnsignedDataSetToProto(testutil.RandomUnsignedDataSet(t)), + Value: v, PreparedRound: rand.Int63(), - PreparedValue: core.UnsignedDataSetToProto(testutil.RandomUnsignedDataSet(t)), + PreparedValue: pv, Signature: nil, } diff --git a/core/consensus/testdata/TestHashProto.golden b/core/consensus/testdata/TestHashProto.golden index eb9f3a31c..93363800c 100644 --- a/core/consensus/testdata/TestHashProto.golden +++ b/core/consensus/testdata/TestHashProto.golden @@ -1,3 +1,26 @@ { - "0xabdb58472a254161accbb62d14ecf92951cac6b37ad08b9ca4c36635fd7c88f803a08e7f3e8940d6b68d0ba7175a4f22": "eyJEYXRhIjp7InNsb3QiOiIxMTg5MzM1Nzc2OTI0NzkwMTg3MSIsImluZGV4IjoiMTc3NDkzMjg5MTI4Njk4MDE1MyIsImJlYWNvbl9ibG9ja19yb290IjoiMHg3ODg5MmVlMjg1ZWNlMTUxMTQ1NTc4MDg3NWQ2NGVlMmQzZDBkMGRlNmJmOGY5YjQ0Y2U4NWZmMDQ0YzZiMWY4Iiwic291cmNlIjp7ImVwb2NoIjoiMTYxMjMxNTcyODU4NTI5NjMxIiwicm9vdCI6IjB4NWU3YTgxYmZkZTI3YzM1NGYzZWRlMmQ2YmVjYzRlYTNhZTVlODg1MjZhOWY0YTU3OGJjYjllZjJkNGE2NTMxNCJ9LCJ0YXJnZXQiOnsiZXBvY2giOiIxMTg1MDQxMDc3NzEzOTU4MjU3NSIsInJvb3QiOiIweDYwN2JlMDYzNzEwNDVjM2YwMDBmOGE3OTZiY2U2YzUxMmMzODAxYWFjYWVlZGZhZDViNTA2NjY0ZThjMGU0YTcifX0sIkR1dHkiOnsicHVia2V5IjoiMHg5NGZmMDY5MTBkNTA1MGM0ZTAxZDY5NDZkMGY4ZWE1MGFlMDc0NmM5YjRhODE0ZDkyZjhkYjAwZGIyZjU0MjI5ZDAzOWI0YTQ2YzcyZjM2ZjVlMThiMjQzNzBmMjdlMDEiLCJzbG90IjoiMjI1OTQwNDExNzcwNDM5MzE1MiIsInZhbGlkYXRvcl9pbmRleCI6IjYwNTAxMjg2NzM4MDI5OTU4MjciLCJjb21taXR0ZWVfaW5kZXgiOiI5NzI0NjA1NDg3MzkzOTczNjAyIiwiY29tbWl0dGVlX2xlbmd0aCI6IjI1NiIsImNvbW1pdHRlZXNfYXRfc2xvdCI6IjI1NiIsInZhbGlkYXRvcl9jb21taXR0ZWVfaW5kZXgiOiIyNTEifX0=" + "0x94ff06910d5050c4e01d6946d0f8ea50ae0746c9b4a814d92f8db00db2f54229d039b4a46c72f36f5e18b24370f27e01": { + "attestation_data": { + "slot": "1774932891286980153", + "index": "15267744271532198264", + "beacon_block_root": "0x511455780875d64ee2d3d0d0de6bf8f9b44ce85ff044c6b1f83b8e883bbf857a", + "source": { + "epoch": "16482847956365694147", + "root": "0x67cfe242cf3ccc4ea3ae5e88526a9f4a578bcb9ef2d4a65314768d6d299761ea" + }, + "target": { + "epoch": "6303220950515014660", + "root": "0xddcdd01d756bce6c512c3801aacaeedfad5b506664e8c0e4a771ece0b8b7c196" + } + }, + "attestation_duty": { + "pubkey": "0x8af7b15d89bb85ee4d8de40b0d811eb821ea31e406c84d0cb4387994a0cfbcd0ed27da8fcd309abfde5902543119abbc", + "slot": "6050128673802995827", + "validator_index": "9724605487393973602", + "committee_index": "12613765599614152010", + "committee_length": "256", + "committees_at_slot": "256", + "validator_committee_index": "224" + } + } } \ No newline at end of file diff --git a/core/dutydb/memory.go b/core/dutydb/memory.go index f36e4979e..69e252275 100644 --- a/core/dutydb/memory.go +++ b/core/dutydb/memory.go @@ -16,7 +16,6 @@ package dutydb import ( - "bytes" "context" "sync" @@ -155,11 +154,16 @@ func (db *MemDB) PubKeyByAttestation(_ context.Context, slot, commIdx, valCommId // storeAttestationUnsafe stores the unsigned attestation. It is unsafe since it assumes the lock is held. func (db *MemDB) storeAttestationUnsafe(pubkey core.PubKey, unsignedData core.UnsignedData) error { - attData, err := core.DecodeAttesterUnsignedData(unsignedData) + cloned, err := unsignedData.Clone() // Clone before storing. if err != nil { return err } + attData, ok := cloned.(core.AttestationData) + if !ok { + return errors.New("invalid unsigned attestation data") + } + // Store key and value for PubKeyByAttestation pKey := pkKey{ Slot: int64(attData.Data.Slot), @@ -193,27 +197,37 @@ func (db *MemDB) storeAttestationUnsafe(pubkey core.PubKey, unsignedData core.Un // storeBeaconBlockUnsafe stores the unsigned BeaconBlock. It is unsafe since it assumes the lock is held. func (db *MemDB) storeBeaconBlockUnsafe(unsignedData core.UnsignedData) error { - block, err := core.DecodeProposerUnsignedData(unsignedData) + cloned, err := unsignedData.Clone() // Clone before storing. if err != nil { return err } + block, ok := cloned.(core.VersionedBeaconBlock) + if !ok { + return errors.New("invalid unsigned block") + } + slot, err := block.Slot() if err != nil { return err } - if actualBlock, ok := db.proDuties[int64(slot)]; ok { - actualData, err := core.EncodeProposerUnsignedData(actualBlock) + if existing, ok := db.proDuties[int64(slot)]; ok { + existingRoot, err := existing.Root() + if err != nil { + return errors.Wrap(err, "block root") + } + + providedRoot, err := block.Root() if err != nil { - return errors.Wrap(err, "marshalling block") + return errors.Wrap(err, "block root") } - if !bytes.Equal(actualData, unsignedData) { + if existingRoot != providedRoot { return errors.New("clashing blocks") } } else { - db.proDuties[int64(slot)] = block + db.proDuties[int64(slot)] = &block.VersionedBeaconBlock } return nil diff --git a/core/dutydb/memory_test.go b/core/dutydb/memory_test.go index c55794aba..8f9b16550 100644 --- a/core/dutydb/memory_test.go +++ b/core/dutydb/memory_test.go @@ -95,24 +95,22 @@ func TestMemDB(t *testing.T) { duty := core.Duty{Slot: slot, Type: core.DutyAttester} // The two validators have similar unsigned data, just the ValidatorCommitteeIndex is different. - unsignedA, err := core.EncodeAttesterUnsignedData(&core.AttestationData{ + unsignedA := core.AttestationData{ Data: attData, Duty: eth2v1.AttesterDuty{ CommitteeLength: commLen, ValidatorCommitteeIndex: valCommIdxA, CommitteesAtSlot: notZero, }, - }) - require.NoError(t, err) - unsignedB, err := core.EncodeAttesterUnsignedData(&core.AttestationData{ + } + unsignedB := core.AttestationData{ Data: attData, Duty: eth2v1.AttesterDuty{ CommitteeLength: commLen, ValidatorCommitteeIndex: valCommIdxB, CommitteesAtSlot: notZero, }, - }) - require.NoError(t, err) + } // Store it err = db.Store(ctx, duty, core.UnsignedDataSet{pubkeysByIdx[vIdxA]: unsignedA, pubkeysByIdx[vIdxB]: unsignedB}) @@ -172,7 +170,7 @@ func TestMemDBProposer(t *testing.T) { // Store the Blocks for i := 0; i < queries; i++ { - unsigned, err := core.EncodeProposerUnsignedData(blocks[i]) + unsigned, err := core.NewVersionedBeaconBlock(blocks[i]) require.NoError(t, err) duty := core.Duty{Slot: slots[i], Type: core.DutyProposer} @@ -207,10 +205,10 @@ func TestMemDBClashingBlocks(t *testing.T) { pubkey := testutil.RandomCorePubKey(t) // Encode the Blocks - unsigned1, err := core.EncodeProposerUnsignedData(block1) + unsigned1, err := core.NewVersionedBeaconBlock(block1) require.NoError(t, err) - unsigned2, err := core.EncodeProposerUnsignedData(block2) + unsigned2, err := core.NewVersionedBeaconBlock(block2) require.NoError(t, err) // Store the Blocks @@ -240,7 +238,7 @@ func TestMemDBClashProposer(t *testing.T) { pubkey := testutil.RandomCorePubKey(t) // Encode the block - unsigned, err := core.EncodeProposerUnsignedData(block) + unsigned, err := core.NewVersionedBeaconBlock(block) require.NoError(t, err) // Store the Blocks @@ -258,7 +256,7 @@ func TestMemDBClashProposer(t *testing.T) { // Store a different block for the same slot block.Phase0.ProposerIndex++ - unsignedB, err := core.EncodeProposerUnsignedData(block) + unsignedB, err := core.NewVersionedBeaconBlock(block) require.NoError(t, err) err = db.Store(ctx, duty, core.UnsignedDataSet{ pubkey: unsignedB, diff --git a/core/encode.go b/core/encode.go index e5f2092d2..71335953a 100644 --- a/core/encode.go +++ b/core/encode.go @@ -19,7 +19,6 @@ import ( "encoding/json" eth2v1 "github.com/attestantio/go-eth2-client/api/v1" - "github.com/attestantio/go-eth2-client/spec" "github.com/obolnetwork/charon/app/errors" ) @@ -65,45 +64,3 @@ func EncodeProposerFetchArg(proDuty *eth2v1.ProposerDuty) (FetchArg, error) { return b, nil } - -// DecodeAttesterUnsignedData returns the attestation data from the encoded UnsignedData. -func DecodeAttesterUnsignedData(unsignedData UnsignedData) (*AttestationData, error) { - attData := new(AttestationData) - err := json.Unmarshal(unsignedData, attData) - if err != nil { - return nil, errors.Wrap(err, "unmarshal attestation data") - } - - return attData, nil -} - -// EncodeAttesterUnsignedData returns the attestation data as an encoded UnsignedData. -func EncodeAttesterUnsignedData(attData *AttestationData) (UnsignedData, error) { - b, err := json.Marshal(attData) - if err != nil { - return nil, errors.Wrap(err, "marshal attestation data") - } - - return b, nil -} - -// EncodeProposerUnsignedData returns the proposer data as an encoded UnsignedData. -func EncodeProposerUnsignedData(proData *spec.VersionedBeaconBlock) (UnsignedData, error) { - b, err := json.Marshal(proData) - if err != nil { - return nil, errors.Wrap(err, "marshal proposer data") - } - - return b, nil -} - -// DecodeProposerUnsignedData returns the proposer data from the encoded UnsignedData. -func DecodeProposerUnsignedData(unsignedData UnsignedData) (*spec.VersionedBeaconBlock, error) { - proData := new(spec.VersionedBeaconBlock) - err := json.Unmarshal(unsignedData, proData) - if err != nil { - return nil, errors.Wrap(err, "unmarshal proposer data") - } - - return proData, nil -} diff --git a/core/encode_test.go b/core/encode_test.go index 325b182ed..39be699f3 100644 --- a/core/encode_test.go +++ b/core/encode_test.go @@ -18,7 +18,6 @@ package core_test import ( "testing" - "github.com/attestantio/go-eth2-client/spec" "github.com/stretchr/testify/require" "github.com/obolnetwork/charon/core" @@ -41,25 +40,6 @@ func TestEncodeAttesterFetchArg(t *testing.T) { require.Equal(t, arg1, arg2) } -func TestEncodeAttesterUnsignedData(t *testing.T) { - attData1 := &core.AttestationData{ - Data: *testutil.RandomAttestationData(), - Duty: *testutil.RandomAttestationDuty(t), - } - - data1, err := core.EncodeAttesterUnsignedData(attData1) - require.NoError(t, err) - - attData2, err := core.DecodeAttesterUnsignedData(data1) - require.NoError(t, err) - - data2, err := core.EncodeAttesterUnsignedData(attData2) - require.NoError(t, err) - - require.Equal(t, attData1, attData2) - require.Equal(t, data1, data2) -} - func TestEncodeProposerFetchArg(t *testing.T) { proDuty1 := testutil.RandomProposerDuty(t) @@ -75,22 +55,3 @@ func TestEncodeProposerFetchArg(t *testing.T) { require.Equal(t, arg1, arg2) require.Equal(t, proDuty1, proDuty2) } - -func TestEncodeProposerUnsignedData(t *testing.T) { - proData1 := &spec.VersionedBeaconBlock{ - Version: spec.DataVersionPhase0, - Phase0: testutil.RandomPhase0BeaconBlock(), - } - - data1, err := core.EncodeProposerUnsignedData(proData1) - require.NoError(t, err) - - proData2, err := core.DecodeProposerUnsignedData(data1) - require.NoError(t, err) - - data2, err := core.EncodeProposerUnsignedData(proData2) - require.NoError(t, err) - - require.Equal(t, proData1, proData2) - require.Equal(t, data1, data2) -} diff --git a/core/fetcher/fetcher.go b/core/fetcher/fetcher.go index e8155b29e..6acc44684 100644 --- a/core/fetcher/fetcher.go +++ b/core/fetcher/fetcher.go @@ -80,10 +80,14 @@ func (f *Fetcher) Fetch(ctx context.Context, duty core.Duty, argSet core.FetchAr } for _, sub := range f.subs { - err := sub(ctx, duty, unsignedSet) + clone, err := unsignedSet.Clone() // Clone before calling each subscriber. if err != nil { return err } + + if err := sub(ctx, duty, clone); err != nil { + return err + } } return nil @@ -118,17 +122,12 @@ func (f *Fetcher) fetchAttesterData(ctx context.Context, slot int64, argSet core dataByCommIdx[attDuty.CommitteeIndex] = eth2AttData } - attData := &core.AttestationData{ + attData := core.AttestationData{ Data: *eth2AttData, Duty: *attDuty, } - dutyData, err := core.EncodeAttesterUnsignedData(attData) - if err != nil { - return nil, errors.Wrap(err, "unmarhsal json") - } - - resp[pubkey] = dutyData + resp[pubkey] = attData } return resp, nil @@ -157,12 +156,12 @@ func (f *Fetcher) fetchProposerData(ctx context.Context, slot int64, argSet core return nil, err } - dutyData, err := core.EncodeProposerUnsignedData(block) + coreBlock, err := core.NewVersionedBeaconBlock(block) if err != nil { - return nil, errors.Wrap(err, "encode proposer data") + return nil, errors.Wrap(err, "new block") } - resp[pubkey] = dutyData + resp[pubkey] = coreBlock } return resp, nil diff --git a/core/fetcher/fetcher_test.go b/core/fetcher/fetcher_test.go index 5c970fc42..b6d03a701 100644 --- a/core/fetcher/fetcher_test.go +++ b/core/fetcher/fetcher_test.go @@ -80,16 +80,12 @@ func TestFetchAttester(t *testing.T) { require.Equal(t, duty, resDuty) require.Len(t, resDataSet, 2) - dataA := resDataSet[pubkeysByIdx[vIdxA]] - dutyDataA, err := core.DecodeAttesterUnsignedData(dataA) - require.NoError(t, err) + dutyDataA := resDataSet[pubkeysByIdx[vIdxA]].(core.AttestationData) require.EqualValues(t, slot, dutyDataA.Data.Slot) require.EqualValues(t, vIdxA, dutyDataA.Data.Index) require.EqualValues(t, dutyA, dutyDataA.Duty) - dataB := resDataSet[pubkeysByIdx[vIdxB]] - dutyDataB, err := core.DecodeAttesterUnsignedData(dataB) - require.NoError(t, err) + dutyDataB := resDataSet[pubkeysByIdx[vIdxB]].(core.AttestationData) require.EqualValues(t, slot, dutyDataB.Data.Slot) require.EqualValues(t, vIdxB, dutyDataB.Data.Index) require.EqualValues(t, dutyB, dutyDataB.Duty) @@ -155,19 +151,13 @@ func TestFetchProposer(t *testing.T) { require.Equal(t, duty, resDuty) require.Len(t, resDataSet, 2) - dataA := resDataSet[pubkeysByIdx[vIdxA]] - dutyDataA, err := core.DecodeProposerUnsignedData(dataA) - require.NoError(t, err) - + dutyDataA := resDataSet[pubkeysByIdx[vIdxA]].(core.VersionedBeaconBlock) slotA, err := dutyDataA.Slot() require.NoError(t, err) require.EqualValues(t, slot, slotA) assertRandao(t, randaoByPubKey[pubkeysByIdx[vIdxA]].Signature().ToETH2(), dutyDataA) - dataB := resDataSet[pubkeysByIdx[vIdxB]] - dutyDataB, err := core.DecodeProposerUnsignedData(dataB) - require.NoError(t, err) - + dutyDataB := resDataSet[pubkeysByIdx[vIdxB]].(core.VersionedBeaconBlock) slotB, err := dutyDataB.Slot() require.NoError(t, err) require.EqualValues(t, slot, slotB) @@ -180,7 +170,7 @@ func TestFetchProposer(t *testing.T) { require.NoError(t, err) } -func assertRandao(t *testing.T, randao eth2p0.BLSSignature, block *spec.VersionedBeaconBlock) { +func assertRandao(t *testing.T, randao eth2p0.BLSSignature, block core.VersionedBeaconBlock) { t.Helper() switch block.Version { diff --git a/core/leadercast/transport.go b/core/leadercast/transport.go index 563fe89fb..08bb7e93a 100644 --- a/core/leadercast/transport.go +++ b/core/leadercast/transport.go @@ -152,6 +152,61 @@ type p2pMsg struct { Data core.UnsignedDataSet } +func (m p2pMsg) MarshalJSON() ([]byte, error) { + data := make(map[core.PubKey]json.RawMessage) + for key, val := range m.Data { + b, err := val.MarshalJSON() + if err != nil { + return nil, errors.Wrap(err, "marshal unsigned data") + } + data[key] = b + } + + b, err := json.Marshal(p2pMsgJSON{ + Idx: m.Idx, + FromIdx: m.FromIdx, + Duty: m.Duty, + Data: data, + }) + if err != nil { + return nil, errors.Wrap(err, "marshal p2p msg") + } + + return b, nil +} + +func (m *p2pMsg) UnmarshalJSON(input []byte) error { + var msg p2pMsgJSON + if err := json.Unmarshal(input, &msg); err != nil { + return errors.Wrap(err, "unmarshal p2p msg") + } + + set := make(core.UnsignedDataSet) + for key, val := range msg.Data { + data, err := core.UnmarshalUnsignedData(msg.Duty.Type, val) + if err != nil { + return errors.Wrap(err, "marshal unsigned data") + } + set[key] = data + } + + *m = p2pMsg{ + Idx: msg.Idx, + FromIdx: msg.FromIdx, + Duty: msg.Duty, + Data: set, + } + + return nil +} + +type p2pMsgJSON struct { + Idx int + FromIdx int + Duty core.Duty + Data map[core.PubKey]json.RawMessage +} + // NewMemTransportFunc returns a function that itself returns in-memory // transport instances that communicate with each other. // It stops processing messages when the context is closed. diff --git a/core/leadercast/transport_test.go b/core/leadercast/transport_test.go index df65ca445..41c96c7dd 100644 --- a/core/leadercast/transport_test.go +++ b/core/leadercast/transport_test.go @@ -73,16 +73,14 @@ func TestMemTransport(t *testing.T) { duty := core.Duty{Slot: int64(i)} data := core.UnsignedDataSet{} for j := 0; j < n; j++ { - unsignedData, err := core.EncodeAttesterUnsignedData(&core.AttestationData{ + unsignedData := core.AttestationData{ Data: *testutil.RandomAttestationData(), Duty: eth2v1.AttesterDuty{ CommitteeLength: commLen, ValidatorCommitteeIndex: uint64(j), CommitteesAtSlot: notZero, }, - }) - require.NoError(t, err) - + } data[pubkeysByIdx[j]] = unsignedData } @@ -107,9 +105,13 @@ func TestMemTransport(t *testing.T) { for _, resolved := range actual { for j := 0; j < n; j++ { a := resolved[pubkeysByIdx[j]] + ab, err := a.MarshalJSON() + require.NoError(t, err) b := expect[pubkeysByIdx[j]] + bb, err := b.MarshalJSON() + require.NoError(t, err) // increase count if all the bytes are equal in between expected and actual data - if bytes.Equal(a, b) { + if bytes.Equal(ab, bb) { count++ } } @@ -184,18 +186,17 @@ func TestP2PTransport(t *testing.T) { // propose attestation for each slot var expected []core.UnsignedDataSet for i := 0; i < slots; i++ { - duty := core.Duty{Slot: int64(i)} + duty := core.NewAttesterDuty(int64(i)) data := core.UnsignedDataSet{} for j := 0; j < n; j++ { - unsignedData, err := core.EncodeAttesterUnsignedData(&core.AttestationData{ + unsignedData := core.AttestationData{ Data: *testutil.RandomAttestationData(), Duty: eth2v1.AttesterDuty{ CommitteeLength: commLen, ValidatorCommitteeIndex: uint64(j), CommitteesAtSlot: notZero, }, - }) - require.NoError(t, err) + } data[pubkeysByIdx[j]] = unsignedData } @@ -221,9 +222,13 @@ func TestP2PTransport(t *testing.T) { for _, resolved := range actual { for j := 0; j < n; j++ { a := resolved[pubkeysByIdx[j]] + ab, err := a.MarshalJSON() + require.NoError(t, err) b := expect[pubkeysByIdx[j]] + bb, err := b.MarshalJSON() + require.NoError(t, err) // increase count if all the bytes are equal in between expected and actual data - if bytes.Equal(a, b) { + if bytes.Equal(ab, bb) { count++ } } diff --git a/core/proto.go b/core/proto.go index 280007e60..738168ffe 100644 --- a/core/proto.go +++ b/core/proto.go @@ -123,23 +123,31 @@ func ParSignedDataSetFromProto(typ DutyType, set *pbv1.ParSignedDataSet) (ParSig } // UnsignedDataSetToProto returns the set as a protobuf. -func UnsignedDataSetToProto(set UnsignedDataSet) *pbv1.UnsignedDataSet { +func UnsignedDataSetToProto(set UnsignedDataSet) (*pbv1.UnsignedDataSet, error) { inner := make(map[string][]byte) for pubkey, data := range set { - inner[string(pubkey)] = data + var err error + inner[string(pubkey)], err = data.MarshalJSON() + if err != nil { + return nil, err + } } return &pbv1.UnsignedDataSet{ Set: inner, - } + }, nil } // UnsignedDataSetFromProto returns the set from a protobuf. -func UnsignedDataSetFromProto(set *pbv1.UnsignedDataSet) UnsignedDataSet { +func UnsignedDataSetFromProto(typ DutyType, set *pbv1.UnsignedDataSet) (UnsignedDataSet, error) { resp := make(UnsignedDataSet) for pubkey, data := range set.Set { - resp[PubKey(pubkey)] = data + var err error + resp[PubKey(pubkey)], err = UnmarshalUnsignedData(typ, data) + if err != nil { + return nil, err + } } - return resp + return resp, nil } diff --git a/core/proto_test.go b/core/proto_test.go index c24761b7d..486d2864a 100644 --- a/core/proto_test.go +++ b/core/proto_test.go @@ -94,24 +94,6 @@ func TestParSignedDataSetProto(t *testing.T) { } } -func TestMarshal(t *testing.T) { - att := core.Attestation{Attestation: *testutil.RandomAttestation()} - - b, err := json.Marshal(att) - require.NoError(t, err) - - b2, err := att.MarshalJSON() - require.NoError(t, err) - require.Equal(t, b, b2) - - var a core.SignedData - a = &core.Attestation{} - err = json.Unmarshal(b, a) - require.NoError(t, err) - - require.Equal(t, &att, a) -} - func TestSetBlockSig(t *testing.T) { block := core.VersionedSignedBeaconBlock{ VersionedSignedBeaconBlock: spec.VersionedSignedBeaconBlock{ @@ -128,6 +110,48 @@ func TestSetBlockSig(t *testing.T) { require.NotEqual(t, clone.Signature(), block.Signature()) } +func TestUnsignedDataToProto(t *testing.T) { + tests := []struct { + Type core.DutyType + Data core.UnsignedData + }{ + { + Type: core.DutyAttester, + Data: testutil.RandomCoreAttestationData(t), + }, + { + Type: core.DutyProposer, + Data: testutil.RandomCoreVersionBeaconBlock(t), + }, + } + + for _, test := range tests { + t.Run(test.Type.String(), func(t *testing.T) { + set1 := core.UnsignedDataSet{ + testutil.RandomCorePubKey(t): test.Data, + } + + pb1, err := core.UnsignedDataSetToProto(set1) + require.NoError(t, err) + set2, err := core.UnsignedDataSetFromProto(test.Type, pb1) + require.NoError(t, err) + pb2, err := core.UnsignedDataSetToProto(set2) + require.NoError(t, err) + require.Equal(t, set1, set2) + require.Equal(t, pb1, pb2) + + b, err := proto.Marshal(pb1) + require.NoError(t, err) + + pb3 := new(pbv1.UnsignedDataSet) + err = proto.Unmarshal(b, pb3) + require.NoError(t, err) + + require.True(t, proto.Equal(pb1, pb3)) + }) + } +} + func TestParSignedData(t *testing.T) { for typ, signedData := range randomSignedData(t) { t.Run(typ.String(), func(t *testing.T) { diff --git a/core/signeddata.go b/core/signeddata.go index 36a0d0c83..1b260517a 100644 --- a/core/signeddata.go +++ b/core/signeddata.go @@ -151,7 +151,12 @@ func (b VersionedSignedBeaconBlock) Clone() (SignedData, error) { //nolint:revive // similar method names. func (b VersionedSignedBeaconBlock) clone() (VersionedSignedBeaconBlock, error) { var resp VersionedSignedBeaconBlock - return resp, cloneSignedData(b, &resp) + err := cloneJSONMarshaler(b, &resp) + if err != nil { + return VersionedSignedBeaconBlock{}, errors.Wrap(err, "clone block") + } + + return resp, nil } func (b VersionedSignedBeaconBlock) Signature() Signature { @@ -208,7 +213,7 @@ func (b VersionedSignedBeaconBlock) MarshalJSON() ([]byte, error) { return nil, errors.Wrap(err, "marshal block") } - resp, err := json.Marshal(versionedSignedBeaconBlockJSON{ + resp, err := json.Marshal(versionedRawBlockJSON{ Version: int(b.Version), Block: block, }) @@ -220,7 +225,7 @@ func (b VersionedSignedBeaconBlock) MarshalJSON() ([]byte, error) { } func (b *VersionedSignedBeaconBlock) UnmarshalJSON(input []byte) error { - var raw versionedSignedBeaconBlockJSON + var raw versionedRawBlockJSON if err := json.Unmarshal(input, &raw); err != nil { return errors.Wrap(err, "unmarshal block") } @@ -254,8 +259,8 @@ func (b *VersionedSignedBeaconBlock) UnmarshalJSON(input []byte) error { return nil } -// versionedSignedBeaconBlockJSON is a custom VersionedSignedBeaconBlock serialiser. -type versionedSignedBeaconBlockJSON struct { +// versionedRawBlockJSON is a custom VersionedSignedBeaconBlock serialiser. +type versionedRawBlockJSON struct { Version int `json:"version"` Block json.RawMessage `json:"block"` } @@ -287,7 +292,12 @@ func (a Attestation) Clone() (SignedData, error) { //nolint:revive // similar method names. func (a Attestation) clone() (Attestation, error) { var resp Attestation - return resp, cloneSignedData(a, &resp) + err := cloneJSONMarshaler(a, &resp) + if err != nil { + return Attestation{}, errors.Wrap(err, "clone attestation") + } + + return resp, nil } func (a Attestation) Signature() Signature { @@ -339,7 +349,12 @@ func (e SignedVoluntaryExit) Clone() (SignedData, error) { //nolint:revive // similar method names. func (e SignedVoluntaryExit) clone() (SignedVoluntaryExit, error) { var resp SignedVoluntaryExit - return resp, cloneSignedData(e, &resp) + err := cloneJSONMarshaler(e, &resp) + if err != nil { + return SignedVoluntaryExit{}, errors.Wrap(err, "clone exit") + } + + return resp, nil } func (e SignedVoluntaryExit) Signature() Signature { @@ -365,17 +380,17 @@ func (e *SignedVoluntaryExit) UnmarshalJSON(b []byte) error { return e.SignedVoluntaryExit.UnmarshalJSON(b) } -// cloneSignedData clones the signed data by serialising to-from json +// cloneJSONMarshaler clones the marshaler by serialising to-from json // since eth2 types contains pointers. The result is stored // in the value pointed to by v. -func cloneSignedData(data SignedData, v any) error { +func cloneJSONMarshaler(data json.Marshaler, v any) error { bytes, err := data.MarshalJSON() if err != nil { - return errors.Wrap(err, "marshal signed data") + return errors.Wrap(err, "marshal data") } if err := json.Unmarshal(bytes, v); err != nil { - return errors.Wrap(err, "unmarshal signed data") + return errors.Wrap(err, "unmarshal data") } return nil diff --git a/core/types.go b/core/types.go index 9195fe54d..4fff1fd1e 100644 --- a/core/types.go +++ b/core/types.go @@ -20,7 +20,6 @@ import ( "encoding/json" "fmt" - eth2v1 "github.com/attestantio/go-eth2-client/api/v1" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/obolnetwork/charon/app/errors" @@ -177,17 +176,30 @@ type FetchArg []byte type FetchArgSet map[PubKey]FetchArg // UnsignedData represents an unsigned duty data object. -type UnsignedData []byte +type UnsignedData interface { + // Clone returns a cloned copy of the UnsignedData. For an immutable core workflow architecture, + // remember to clone data when it leaves the current scope (sharing, storing, returning, etc). + Clone() (UnsignedData, error) + // Marshaler returns the json serialised unsigned duty data. + json.Marshaler +} // UnsignedDataSet is a set of unsigned duty data objects, one per validator. type UnsignedDataSet map[PubKey]UnsignedData -// AttestationData wraps the eth2 attestation data and adds the original duty. -// The original duty allows mapping the partial signed response from the VC -// backed to the validator pubkey via the aggregation bits field. -type AttestationData struct { - Data eth2p0.AttestationData - Duty eth2v1.AttesterDuty +// Clone returns a cloned copy of the UnsignedDataSet. For an immutable core workflow architecture, +// remember to clone data when it leaves the current scope (sharing, storing, returning, etc). +func (s UnsignedDataSet) Clone() (UnsignedDataSet, error) { + resp := make(UnsignedDataSet, len(s)) + for key, data := range s { + var err error + resp[key], err = data.Clone() + if err != nil { + return nil, err + } + } + + return resp, nil } // SignedData is a signed duty data. diff --git a/core/unsigneddata.go b/core/unsigneddata.go new file mode 100644 index 000000000..b882585db --- /dev/null +++ b/core/unsigneddata.go @@ -0,0 +1,204 @@ +// 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 ( + "encoding/json" + + eth2v1 "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/spec" + "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/obolnetwork/charon/app/errors" +) + +var ( + _ UnsignedData = AttestationData{} + _ UnsignedData = VersionedBeaconBlock{} +) + +// AttestationData wraps the eth2 attestation data and adds the original duty. +// The original duty allows mapping the partial signed response from the VC +// backed to the validator pubkey via the aggregation bits field. +type AttestationData struct { + Data eth2p0.AttestationData + Duty eth2v1.AttesterDuty +} + +func (a AttestationData) Clone() (UnsignedData, error) { + var resp AttestationData + err := cloneJSONMarshaler(a, &resp) + if err != nil { + return nil, errors.Wrap(err, "clone attestation") + } + + return resp, nil +} + +func (a AttestationData) MarshalJSON() ([]byte, error) { + resp, err := json.Marshal(attestationDataJSON{ + Data: &a.Data, + Duty: &a.Duty, + }) + if err != nil { + return nil, errors.Wrap(err, "marshal attestation") + } + + return resp, nil +} + +func (a *AttestationData) UnmarshalJSON(data []byte) error { + var att attestationDataJSON + if err := json.Unmarshal(data, &att); err != nil { + return errors.Wrap(err, "unmarshal attestation") + } + + a.Data = *att.Data + a.Duty = *att.Duty + + return nil +} + +type attestationDataJSON struct { + Data *eth2p0.AttestationData `json:"attestation_data"` + Duty *eth2v1.AttesterDuty `json:"attestation_duty"` +} + +// NewVersionedBeaconBlock validates and returns a new wrapped VersionedBeaconBlock. +func NewVersionedBeaconBlock(block *spec.VersionedBeaconBlock) (VersionedBeaconBlock, error) { + switch block.Version { + case spec.DataVersionPhase0: + if block.Phase0 == nil { + return VersionedBeaconBlock{}, errors.New("no phase0 block") + } + case spec.DataVersionAltair: + if block.Altair == nil { + return VersionedBeaconBlock{}, errors.New("no altair block") + } + case spec.DataVersionBellatrix: + if block.Bellatrix == nil { + return VersionedBeaconBlock{}, errors.New("no bellatrix block") + } + default: + return VersionedBeaconBlock{}, errors.New("unknown version") + } + + return VersionedBeaconBlock{VersionedBeaconBlock: *block}, nil +} + +type VersionedBeaconBlock struct { + spec.VersionedBeaconBlock +} + +func (b VersionedBeaconBlock) Clone() (UnsignedData, error) { + var resp VersionedBeaconBlock + err := cloneJSONMarshaler(b, &resp) + if err != nil { + return nil, errors.Wrap(err, "clone block") + } + + return resp, nil +} + +func (b VersionedBeaconBlock) MarshalJSON() ([]byte, error) { + var marshaller json.Marshaler + switch b.Version { + // No block nil checks since `NewVersionedSignedBeaconBlock` assumed. + case spec.DataVersionPhase0: + marshaller = b.Phase0 + case spec.DataVersionAltair: + marshaller = b.Altair + case spec.DataVersionBellatrix: + marshaller = b.Bellatrix + default: + return nil, errors.New("unknown version") + } + + block, err := marshaller.MarshalJSON() + if err != nil { + return nil, errors.Wrap(err, "marshal block") + } + + resp, err := json.Marshal(versionedRawBlockJSON{ + Version: int(b.Version), + Block: block, + }) + if err != nil { + return nil, errors.Wrap(err, "marshal wrapper") + } + + return resp, nil +} + +func (b *VersionedBeaconBlock) UnmarshalJSON(input []byte) error { + var raw versionedRawBlockJSON + if err := json.Unmarshal(input, &raw); err != nil { + return errors.Wrap(err, "unmarshal block") + } + + resp := spec.VersionedBeaconBlock{Version: spec.DataVersion(raw.Version)} + switch resp.Version { + case spec.DataVersionPhase0: + block := new(eth2p0.BeaconBlock) + if err := json.Unmarshal(raw.Block, &block); err != nil { + return errors.Wrap(err, "unmarshal phase0") + } + resp.Phase0 = block + case spec.DataVersionAltair: + block := new(altair.BeaconBlock) + if err := json.Unmarshal(raw.Block, &block); err != nil { + return errors.Wrap(err, "unmarshal altair") + } + resp.Altair = block + case spec.DataVersionBellatrix: + block := new(bellatrix.BeaconBlock) + if err := json.Unmarshal(raw.Block, &block); err != nil { + return errors.Wrap(err, "unmarshal bellatrix") + } + resp.Bellatrix = block + default: + return errors.New("unknown version") + } + + *b = VersionedBeaconBlock{VersionedBeaconBlock: resp} + + return nil +} + +// UnmarshalUnsignedData returns an instantiated unsigned data based on the duty type. +// TODO(corver): Unexport once leadercast is removed or uses protobufs. +func UnmarshalUnsignedData(typ DutyType, data []byte) (UnsignedData, error) { + switch typ { + case DutyAttester: + var resp AttestationData + if err := json.Unmarshal(data, &resp); err != nil { + return nil, errors.Wrap(err, "unmarshal attestation data") + } + + return resp, nil + case DutyProposer: + var resp VersionedBeaconBlock + if err := json.Unmarshal(data, &resp); err != nil { + return nil, errors.Wrap(err, "unmarshal block") + } + + return resp, nil + default: + return nil, errors.New("unsupported unsigned data duty type") + } +} diff --git a/core/unsigneddata_test.go b/core/unsigneddata_test.go new file mode 100644 index 000000000..26b35758a --- /dev/null +++ b/core/unsigneddata_test.go @@ -0,0 +1,39 @@ +// 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 ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/obolnetwork/charon/core" + "github.com/obolnetwork/charon/testutil" +) + +func TestCloneVersionedBeaconBlock(t *testing.T) { + block := testutil.RandomCoreVersionBeaconBlock(t) + slot1, err := block.Slot() + require.NoError(t, err) + + clone, err := block.Clone() + require.NoError(t, err) + block2 := clone.(core.VersionedBeaconBlock) + slot2, err := block2.Slot() + require.NoError(t, err) + + require.Equal(t, slot1, slot2) +} diff --git a/testutil/random.go b/testutil/random.go index add97156e..8f2aea5de 100644 --- a/testutil/random.go +++ b/testutil/random.go @@ -27,6 +27,7 @@ import ( "testing" eth2v1 "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/spec" "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" @@ -182,6 +183,17 @@ func RandomBellatrixBeaconBlockBody(t *testing.T) *bellatrix.BeaconBlockBody { } } +func RandomCoreVersionBeaconBlock(t *testing.T) core.VersionedBeaconBlock { + t.Helper() + + return core.VersionedBeaconBlock{ + VersionedBeaconBlock: spec.VersionedBeaconBlock{ + Version: spec.DataVersionBellatrix, + Bellatrix: RandomBellatrixBeaconBlock(t), + }, + } +} + func RandomSyncAggregate(t *testing.T) *altair.SyncAggregate { t.Helper() @@ -373,19 +385,23 @@ func RandomENR(t *testing.T, random io.Reader) (*ecdsa.PrivateKey, enr.Record) { return p2pKey, r } -func RandomUnsignedDataSet(t *testing.T) core.UnsignedDataSet { +func RandomCoreAttestationData(t *testing.T) core.AttestationData { t.Helper() duty := RandomAttestationDuty(t) data := RandomAttestationData() - unsigned, err := core.EncodeAttesterUnsignedData(&core.AttestationData{ + + return core.AttestationData{ Data: *data, Duty: *duty, - }) - require.NoError(t, err) + } +} + +func RandomUnsignedDataSet(t *testing.T) core.UnsignedDataSet { + t.Helper() return core.UnsignedDataSet{ - RandomCorePubKey(t): unsigned, + RandomCorePubKey(t): RandomCoreAttestationData(t), } }