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),
}
}