diff --git a/app/eth2wrap/eth2wrap_test.go b/app/eth2wrap/eth2wrap_test.go index eb843a0b7..c0aefd579 100644 --- a/app/eth2wrap/eth2wrap_test.go +++ b/app/eth2wrap/eth2wrap_test.go @@ -19,6 +19,7 @@ import ( eth2api "github.com/attestantio/go-eth2-client/api" eth2v1 "github.com/attestantio/go-eth2-client/api/v1" eth2spec "github.com/attestantio/go-eth2-client/spec" + eth2e "github.com/attestantio/go-eth2-client/spec/electra" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -358,37 +359,108 @@ func TestCtxCancel(t *testing.T) { } } -func TestBlockAttestations(t *testing.T) { - atts := []*eth2spec.VersionedAttestation{ - testutil.RandomDenebVersionedAttestation(), - testutil.RandomDenebVersionedAttestation(), - } +func TestBlockAttestationsV2(t *testing.T) { + phase0Att1 := testutil.RandomPhase0Attestation() + phase0Att2 := testutil.RandomPhase0Attestation() + electraAtt1 := testutil.RandomElectraAttestation() + electraAtt2 := testutil.RandomElectraAttestation() - statusCode := http.StatusOK - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - require.Equal(t, "/eth/v2/beacon/blocks/head/attestations", r.URL.Path) - b, err := json.Marshal(struct { - Data []*eth2p0.Attestation - }{ - Data: []*eth2p0.Attestation{atts[0].Deneb, atts[1].Deneb}, - }) - require.NoError(t, err) + tests := []struct { + version string + attestations []*eth2spec.VersionedAttestation + serverJSONStruct any + expErr string + }{ + { + version: "electra", + attestations: []*eth2spec.VersionedAttestation{ + {Version: eth2spec.DataVersionElectra, Electra: electraAtt1}, + {Version: eth2spec.DataVersionElectra, Electra: electraAtt2}, + }, + serverJSONStruct: struct{ Data []*eth2e.Attestation }{Data: []*eth2e.Attestation{electraAtt1, electraAtt2}}, + expErr: "", + }, + { + version: "deneb", + attestations: []*eth2spec.VersionedAttestation{ + {Version: eth2spec.DataVersionDeneb, Deneb: phase0Att1}, + {Version: eth2spec.DataVersionDeneb, Deneb: phase0Att2}, + }, + serverJSONStruct: struct{ Data []*eth2p0.Attestation }{Data: []*eth2p0.Attestation{phase0Att1, phase0Att2}}, + expErr: "", + }, + { + version: "capella", + attestations: []*eth2spec.VersionedAttestation{ + {Version: eth2spec.DataVersionCapella, Capella: phase0Att1}, + {Version: eth2spec.DataVersionCapella, Capella: phase0Att2}, + }, + serverJSONStruct: struct{ Data []*eth2p0.Attestation }{Data: []*eth2p0.Attestation{phase0Att1, phase0Att2}}, + expErr: "", + }, + { + version: "bellatrix", + attestations: []*eth2spec.VersionedAttestation{ + {Version: eth2spec.DataVersionBellatrix, Bellatrix: phase0Att1}, + {Version: eth2spec.DataVersionBellatrix, Bellatrix: phase0Att2}, + }, + serverJSONStruct: struct{ Data []*eth2p0.Attestation }{Data: []*eth2p0.Attestation{phase0Att1, phase0Att2}}, + expErr: "", + }, + { + version: "altair", + attestations: []*eth2spec.VersionedAttestation{ + {Version: eth2spec.DataVersionAltair, Altair: phase0Att1}, + {Version: eth2spec.DataVersionAltair, Altair: phase0Att2}, + }, + serverJSONStruct: struct{ Data []*eth2p0.Attestation }{Data: []*eth2p0.Attestation{phase0Att1, phase0Att2}}, + expErr: "", + }, + { + version: "phase0", + attestations: []*eth2spec.VersionedAttestation{ + {Version: eth2spec.DataVersionPhase0, Phase0: phase0Att1}, + {Version: eth2spec.DataVersionPhase0, Phase0: phase0Att2}, + }, + serverJSONStruct: struct{ Data []*eth2p0.Attestation }{Data: []*eth2p0.Attestation{phase0Att1, phase0Att2}}, + expErr: "", + }, + { + version: "unknown version", + attestations: nil, + serverJSONStruct: struct{ Data []*eth2p0.Attestation }{Data: []*eth2p0.Attestation{phase0Att1, phase0Att2}}, + expErr: "failed to get consensus version", + }, + } + for _, test := range tests { + t.Run(test.version, func(t *testing.T) { + statusCode := http.StatusOK + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + require.Equal(t, "/eth/v2/beacon/blocks/head/attestations", r.URL.Path) + b, err := json.Marshal(test.serverJSONStruct) + require.NoError(t, err) - w.Header().Add("Eth-Consensus-Version", "deneb") - w.WriteHeader(statusCode) - _, _ = w.Write(b) - })) + w.Header().Add("Eth-Consensus-Version", test.version) + w.WriteHeader(statusCode) + _, _ = w.Write(b) + })) - cl := eth2wrap.NewHTTPAdapterForT(t, srv.URL, time.Hour) - resp, err := cl.BlockAttestationsV2(context.Background(), "head") - require.NoError(t, err) - require.Equal(t, atts, resp) + cl := eth2wrap.NewHTTPAdapterForT(t, srv.URL, time.Hour) + resp, err := cl.BlockAttestationsV2(context.Background(), "head") + if test.expErr != "" { + require.ErrorContains(t, err, test.expErr) + } else { + require.NoError(t, err) + } + require.Equal(t, test.attestations, resp) - statusCode = http.StatusNotFound - resp, err = cl.BlockAttestationsV2(context.Background(), "head") - require.NoError(t, err) - require.Empty(t, resp) + statusCode = http.StatusNotFound + resp, err = cl.BlockAttestationsV2(context.Background(), "head") + require.NoError(t, err) + require.Empty(t, resp) + }) + } } // TestOneError tests the case where one of the servers returns errors. diff --git a/app/eth2wrap/synthproposer.go b/app/eth2wrap/synthproposer.go index 6ffe96336..70c449d5e 100644 --- a/app/eth2wrap/synthproposer.go +++ b/app/eth2wrap/synthproposer.go @@ -14,6 +14,7 @@ import ( eth2api "github.com/attestantio/go-eth2-client/api" eth2v1 "github.com/attestantio/go-eth2-client/api/v1" eth2deneb "github.com/attestantio/go-eth2-client/api/v1/deneb" + eth2electra "github.com/attestantio/go-eth2-client/api/v1/electra" eth2spec "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/bellatrix" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" @@ -174,6 +175,14 @@ func (h *synthWrapper) syntheticProposal(ctx context.Context, slot eth2p0.Slot, proposal.Deneb.Block.ProposerIndex = vIdx proposal.Deneb.Block.Body.ExecutionPayload.FeeRecipient = feeRecipient proposal.Deneb.Block.Body.ExecutionPayload.Transactions = fraction(proposal.Deneb.Block.Body.ExecutionPayload.Transactions) + case eth2spec.DataVersionElectra: + proposal.Electra = ð2electra.BlockContents{} + proposal.Electra.Block = signedBlock.Electra.Message + proposal.Electra.Block.Body.Graffiti = GetSyntheticGraffiti() + proposal.Electra.Block.Slot = slot + proposal.Electra.Block.ProposerIndex = vIdx + proposal.Electra.Block.Body.ExecutionPayload.FeeRecipient = feeRecipient + proposal.Electra.Block.Body.ExecutionPayload.Transactions = fraction(proposal.Electra.Block.Body.ExecutionPayload.Transactions) default: return nil, errors.New("unsupported proposal version") } @@ -225,6 +234,8 @@ func IsSyntheticBlindedBlock(block *eth2api.VersionedSignedBlindedProposal) bool graffiti = block.Capella.Message.Body.Graffiti case eth2spec.DataVersionDeneb: graffiti = block.Deneb.Message.Body.Graffiti + case eth2spec.DataVersionElectra: + graffiti = block.Electra.Message.Body.Graffiti default: return false } @@ -246,6 +257,8 @@ func IsSyntheticProposal(block *eth2api.VersionedSignedProposal) bool { graffiti = block.Capella.Message.Body.Graffiti case eth2spec.DataVersionDeneb: graffiti = block.Deneb.SignedBlock.Message.Body.Graffiti + case eth2spec.DataVersionElectra: + graffiti = block.Electra.SignedBlock.Message.Body.Graffiti default: return false } diff --git a/app/eth2wrap/synthproposer_test.go b/app/eth2wrap/synthproposer_test.go index 0ce27ae29..e55e4806a 100644 --- a/app/eth2wrap/synthproposer_test.go +++ b/app/eth2wrap/synthproposer_test.go @@ -5,12 +5,15 @@ package eth2wrap_test import ( "context" "fmt" + "math/big" "net/http" "testing" eth2api "github.com/attestantio/go-eth2-client/api" eth2v1 "github.com/attestantio/go-eth2-client/api/v1" eth2capella "github.com/attestantio/go-eth2-client/api/v1/capella" + eth2deneb "github.com/attestantio/go-eth2-client/api/v1/deneb" + eth2electra "github.com/attestantio/go-eth2-client/api/v1/electra" eth2spec "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/bellatrix" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" @@ -24,143 +27,286 @@ import ( func TestSynthProposer(t *testing.T) { ctx := context.Background() - var ( - set = beaconmock.ValidatorSetA - feeRecipient = bellatrix.ExecutionAddress{0x00, 0x01, 0x02} - slotsPerEpoch = 16 - epoch eth2p0.Epoch = 100 - realBlockSlot = eth2p0.Slot(slotsPerEpoch) * eth2p0.Slot(epoch) - done = make(chan struct{}) - activeVals = 0 - ) + tests := []struct { + version eth2spec.DataVersion + versionedSignedBlock *eth2spec.VersionedSignedBeaconBlock + beaconMockProposalFunc func(context.Context, *eth2api.ProposalOpts) (*eth2api.VersionedProposal, error) + populateBlockFunc func(*eth2api.VersionedProposal) *eth2api.VersionedSignedBlindedProposal + createVersionedSignedProposal func(*eth2api.VersionedProposal) *eth2api.VersionedSignedProposal + }{ + { + version: eth2spec.DataVersionElectra, + versionedSignedBlock: testutil.RandomElectraVersionedSignedBeaconBlock(), + beaconMockProposalFunc: func(_ context.Context, opts *eth2api.ProposalOpts) (*eth2api.VersionedProposal, error) { + var block *eth2api.VersionedProposal + if opts.BuilderBoostFactor == nil || *opts.BuilderBoostFactor == 0 { + block = testutil.RandomElectraVersionedProposal() + block.Electra.Block.Slot = opts.Slot + block.Electra.Block.Body.RANDAOReveal = opts.RandaoReveal + block.Electra.Block.Body.Graffiti = opts.Graffiti + block.ExecutionValue = big.NewInt(1) + block.ConsensusValue = big.NewInt(1) + } else { + block = ð2api.VersionedProposal{ + Version: eth2spec.DataVersionElectra, + ElectraBlinded: testutil.RandomElectraBlindedBeaconBlock(), + } + block.ElectraBlinded.Slot = opts.Slot + block.ElectraBlinded.Body.RANDAOReveal = opts.RandaoReveal + block.ElectraBlinded.Body.Graffiti = opts.Graffiti + block.ExecutionValue = big.NewInt(1) + block.ConsensusValue = big.NewInt(1) + block.Blinded = true + } + + return block, nil + }, + populateBlockFunc: func(block *eth2api.VersionedProposal) *eth2api.VersionedSignedBlindedProposal { + return ð2api.VersionedSignedBlindedProposal{ + Version: eth2spec.DataVersionElectra, + Electra: ð2electra.SignedBlindedBeaconBlock{ + Message: block.ElectraBlinded, + Signature: testutil.RandomEth2Signature(), + }, + } + }, + createVersionedSignedProposal: func(block *eth2api.VersionedProposal) *eth2api.VersionedSignedProposal { + signed := testutil.RandomElectraVersionedSignedProposal() + signed.Electra.SignedBlock.Message = block.Electra.Block - bmock, err := beaconmock.New(beaconmock.WithValidatorSet(set), beaconmock.WithSlotsPerEpoch(slotsPerEpoch)) - require.NoError(t, err) + return signed + }, + }, + { + version: eth2spec.DataVersionDeneb, + versionedSignedBlock: testutil.RandomDenebVersionedSignedBeaconBlock(), + beaconMockProposalFunc: func(_ context.Context, opts *eth2api.ProposalOpts) (*eth2api.VersionedProposal, error) { + var block *eth2api.VersionedProposal + if opts.BuilderBoostFactor == nil || *opts.BuilderBoostFactor == 0 { + block = testutil.RandomDenebVersionedProposal() + block.Deneb.Block.Slot = opts.Slot + block.Deneb.Block.Body.RANDAOReveal = opts.RandaoReveal + block.Deneb.Block.Body.Graffiti = opts.Graffiti + block.ExecutionValue = big.NewInt(1) + block.ConsensusValue = big.NewInt(1) + } else { + block = ð2api.VersionedProposal{ + Version: eth2spec.DataVersionDeneb, + DenebBlinded: testutil.RandomDenebBlindedBeaconBlock(), + } + block.DenebBlinded.Slot = opts.Slot + block.DenebBlinded.Body.RANDAOReveal = opts.RandaoReveal + block.DenebBlinded.Body.Graffiti = opts.Graffiti + block.ExecutionValue = big.NewInt(1) + block.ConsensusValue = big.NewInt(1) + block.Blinded = true + } + + return block, nil + }, + populateBlockFunc: func(block *eth2api.VersionedProposal) *eth2api.VersionedSignedBlindedProposal { + return ð2api.VersionedSignedBlindedProposal{ + Version: eth2spec.DataVersionDeneb, + Deneb: ð2deneb.SignedBlindedBeaconBlock{ + Message: block.DenebBlinded, + Signature: testutil.RandomEth2Signature(), + }, + } + }, + createVersionedSignedProposal: func(block *eth2api.VersionedProposal) *eth2api.VersionedSignedProposal { + signed := testutil.RandomDenebVersionedSignedProposal() + signed.Deneb.SignedBlock.Message = block.Deneb.Block - bmock.SubmitProposalFunc = func(ctx context.Context, opts *eth2api.SubmitProposalOpts) error { - require.Equal(t, realBlockSlot, opts.Proposal.Capella.Message.Slot) - close(done) + return signed + }, + }, + { + version: eth2spec.DataVersionCapella, + versionedSignedBlock: testutil.RandomCapellaVersionedSignedBeaconBlock(), + beaconMockProposalFunc: func(_ context.Context, opts *eth2api.ProposalOpts) (*eth2api.VersionedProposal, error) { + var block *eth2api.VersionedProposal + if opts.BuilderBoostFactor == nil || *opts.BuilderBoostFactor == 0 { + block = testutil.RandomCapellaVersionedProposal() + block.Capella.Slot = opts.Slot + block.Capella.Body.RANDAOReveal = opts.RandaoReveal + block.Capella.Body.Graffiti = opts.Graffiti + block.ExecutionValue = big.NewInt(1) + block.ConsensusValue = big.NewInt(1) + } else { + block = ð2api.VersionedProposal{ + Version: eth2spec.DataVersionCapella, + CapellaBlinded: testutil.RandomCapellaBlindedBeaconBlock(), + } + block.CapellaBlinded.Slot = opts.Slot + block.CapellaBlinded.Body.RANDAOReveal = opts.RandaoReveal + block.CapellaBlinded.Body.Graffiti = opts.Graffiti + block.ExecutionValue = big.NewInt(1) + block.ConsensusValue = big.NewInt(1) + block.Blinded = true + } + + return block, nil + }, + populateBlockFunc: func(block *eth2api.VersionedProposal) *eth2api.VersionedSignedBlindedProposal { + return ð2api.VersionedSignedBlindedProposal{ + Version: eth2spec.DataVersionCapella, + Capella: ð2capella.SignedBlindedBeaconBlock{ + Message: block.CapellaBlinded, + Signature: testutil.RandomEth2Signature(), + }, + } + }, + createVersionedSignedProposal: func(block *eth2api.VersionedProposal) *eth2api.VersionedSignedProposal { + signed := testutil.RandomCapellaVersionedSignedProposal() + signed.Capella.Message = block.Capella - return nil + return signed + }, + }, } - bmock.SubmitBlindedProposalFunc = func(ctx context.Context, opts *eth2api.SubmitBlindedProposalOpts) error { - require.Equal(t, realBlockSlot, opts.Proposal.Capella.Message.Slot) - close(done) + for _, test := range tests { + t.Run(test.version.String(), func(t *testing.T) { + var ( + set = beaconmock.ValidatorSetA + feeRecipient = bellatrix.ExecutionAddress{0x00, 0x01, 0x02} + slotsPerEpoch = 16 + epoch eth2p0.Epoch = 100 + realBlockSlot = eth2p0.Slot(slotsPerEpoch) * eth2p0.Slot(epoch) + done = make(chan struct{}) + activeVals = 0 + ) + + bmock, err := beaconmock.New(beaconmock.WithValidatorSet(set), beaconmock.WithSlotsPerEpoch(slotsPerEpoch)) + require.NoError(t, err) - return nil - } + bmock.SubmitProposalFunc = func(ctx context.Context, opts *eth2api.SubmitProposalOpts) error { + slot, err := opts.Proposal.Slot() + require.NoError(t, err) + require.Equal(t, realBlockSlot, slot) + close(done) - bmock.ProposerDutiesFunc = func(ctx context.Context, e eth2p0.Epoch, indices []eth2p0.ValidatorIndex) ([]*eth2v1.ProposerDuty, error) { - require.Equal(t, int(epoch), int(e)) + return nil + } - return []*eth2v1.ProposerDuty{ // First validator is the proposer for first slot in the epoch. - { - PubKey: set[1].Validator.PublicKey, - Slot: realBlockSlot, - ValidatorIndex: set[1].Index, - }, - }, nil - } - cached := bmock.CachedValidatorsFunc - bmock.CachedValidatorsFunc = func(ctx context.Context) (eth2wrap.ActiveValidators, eth2wrap.CompleteValidators, error) { - activeVals++ - return cached(ctx) - } - bmock.SignedBeaconBlockFunc = func(ctx context.Context, blockID string) (*eth2spec.VersionedSignedBeaconBlock, error) { - resp := testutil.RandomCapellaVersionedSignedBeaconBlock() + bmock.SubmitBlindedProposalFunc = func(ctx context.Context, opts *eth2api.SubmitBlindedProposalOpts) error { + slot, err := opts.Proposal.Slot() + require.NoError(t, err) + require.Equal(t, realBlockSlot, slot) + close(done) - return resp, nil - } + return nil + } - eth2Cl := eth2wrap.WithSyntheticDuties(bmock) + bmock.ProposerDutiesFunc = func(ctx context.Context, e eth2p0.Epoch, indices []eth2p0.ValidatorIndex) ([]*eth2v1.ProposerDuty, error) { + require.Equal(t, int(epoch), int(e)) - var preps []*eth2v1.ProposalPreparation - for vIdx := range set { - preps = append(preps, ð2v1.ProposalPreparation{ - ValidatorIndex: vIdx, - FeeRecipient: feeRecipient, - }) - } - require.NoError(t, eth2Cl.SubmitProposalPreparations(ctx, preps)) + return []*eth2v1.ProposerDuty{ // First validator is the proposer for first slot in the epoch. + { + PubKey: set[1].Validator.PublicKey, + Slot: realBlockSlot, + ValidatorIndex: set[1].Index, + }, + }, nil + } + cached := bmock.CachedValidatorsFunc + bmock.CachedValidatorsFunc = func(ctx context.Context) (eth2wrap.ActiveValidators, eth2wrap.CompleteValidators, error) { + activeVals++ + return cached(ctx) + } + bmock.SignedBeaconBlockFunc = func(ctx context.Context, blockID string) (*eth2spec.VersionedSignedBeaconBlock, error) { + resp := test.versionedSignedBlock - // Get synthetic duties - opts := ð2api.ProposerDutiesOpts{ - Epoch: epoch, - Indices: nil, - } - resp1, err := eth2Cl.ProposerDuties(ctx, opts) - require.NoError(t, err) - duties := resp1.Data - require.Len(t, duties, len(set)) - require.Equal(t, 1, activeVals) + return resp, nil + } - // Get synthetic duties again - resp2, err := eth2Cl.ProposerDuties(ctx, opts) - require.NoError(t, err) - duties2 := resp2.Data - require.Equal(t, duties, duties2) // Identical - require.Equal(t, 1, activeVals) // Cached + bmock.ProposalFunc = test.beaconMockProposalFunc - // Submit blocks - for _, duty := range duties { - var bbf uint64 = 100 - var graff [32]byte - copy(graff[:], "test") - opts1 := ð2api.ProposalOpts{ - Slot: duty.Slot, - RandaoReveal: testutil.RandomEth2Signature(), - Graffiti: graff, - BuilderBoostFactor: &bbf, - } - resp, err := eth2Cl.Proposal(ctx, opts1) - require.NoError(t, err) - - if resp.Data.Blinded { - block := resp.Data - if duty.Slot == realBlockSlot { - require.NotContains(t, string(block.CapellaBlinded.Body.Graffiti[:]), "DO NOT SUBMIT") - require.NotEqual(t, feeRecipient, block.CapellaBlinded.Body.ExecutionPayloadHeader.FeeRecipient) - } else { - require.Equal(t, feeRecipient, block.CapellaBlinded.Body.ExecutionPayloadHeader.FeeRecipient) + eth2Cl := eth2wrap.WithSyntheticDuties(bmock) + + var preps []*eth2v1.ProposalPreparation + for vIdx := range set { + preps = append(preps, ð2v1.ProposalPreparation{ + ValidatorIndex: vIdx, + FeeRecipient: feeRecipient, + }) } - require.Equal(t, eth2spec.DataVersionCapella, block.Version) - - signed := ð2api.VersionedSignedBlindedProposal{ - Version: eth2spec.DataVersionCapella, - Capella: ð2capella.SignedBlindedBeaconBlock{ - Message: block.CapellaBlinded, - Signature: testutil.RandomEth2Signature(), - }, + require.NoError(t, eth2Cl.SubmitProposalPreparations(ctx, preps)) + + // Get synthetic duties + opts := ð2api.ProposerDutiesOpts{ + Epoch: epoch, + Indices: nil, } - err = eth2Cl.SubmitBlindedProposal(ctx, ð2api.SubmitBlindedProposalOpts{ - Proposal: signed, - }) + resp1, err := eth2Cl.ProposerDuties(ctx, opts) require.NoError(t, err) - } else { - block := resp.Data - - if duty.Slot == realBlockSlot { - require.NotContains(t, string(block.Capella.Body.Graffiti[:]), "DO NOT SUBMIT") - require.NotEqual(t, feeRecipient, block.Capella.Body.ExecutionPayload.FeeRecipient) - } else { - require.Contains(t, string(block.Capella.Body.Graffiti[:]), "DO NOT SUBMIT") - require.Equal(t, feeRecipient, block.Capella.Body.ExecutionPayload.FeeRecipient) + duties := resp1.Data + require.Len(t, duties, len(set)) + require.Equal(t, 1, activeVals) - continue + // Get synthetic duties again + resp2, err := eth2Cl.ProposerDuties(ctx, opts) + require.NoError(t, err) + duties2 := resp2.Data + require.Equal(t, duties, duties2) // Identical + require.Equal(t, 1, activeVals) // Cached + + // Submit blocks + for _, duty := range duties { + var bbf uint64 = 100 + var graff [32]byte + copy(graff[:], "test") + opts1 := ð2api.ProposalOpts{ + Slot: duty.Slot, + RandaoReveal: testutil.RandomEth2Signature(), + Graffiti: graff, + BuilderBoostFactor: &bbf, + } + resp, err := eth2Cl.Proposal(ctx, opts1) + require.NoError(t, err) + + block := resp.Data + graffitiInBlock, err := block.Graffiti() + require.NoError(t, err) + feeRecipientInBlock, err := block.FeeRecipient() + require.NoError(t, err) + if resp.Data.Blinded { + if duty.Slot == realBlockSlot { + require.NotContains(t, string(graffitiInBlock[:]), "DO NOT SUBMIT") + require.NotEqual(t, feeRecipient, feeRecipientInBlock) + } else { + require.Equal(t, feeRecipient, feeRecipientInBlock) + } + require.Equal(t, test.version, block.Version) + + signed := test.populateBlockFunc(block) + err = eth2Cl.SubmitBlindedProposal(ctx, ð2api.SubmitBlindedProposalOpts{ + Proposal: signed, + }) + require.NoError(t, err) + } else { + if duty.Slot == realBlockSlot { + require.NotContains(t, string(graffitiInBlock[:]), "DO NOT SUBMIT") + require.NotEqual(t, feeRecipient, feeRecipientInBlock) + } else { + require.Contains(t, string(graffitiInBlock[:]), "DO NOT SUBMIT") + require.Equal(t, feeRecipient, feeRecipientInBlock) + + continue + } + require.Equal(t, test.version, block.Version) + + signed := test.createVersionedSignedProposal(block) + err = eth2Cl.SubmitProposal(ctx, ð2api.SubmitProposalOpts{ + Proposal: signed, + }) + } + require.NoError(t, err) } - require.Equal(t, eth2spec.DataVersionCapella, block.Version) - signed := testutil.RandomCapellaVersionedSignedProposal() - signed.Capella.Message = block.Capella - err = eth2Cl.SubmitProposal(ctx, ð2api.SubmitProposalOpts{ - Proposal: signed, - }) - } - require.NoError(t, err) + <-done + }) } - - <-done } func TestSynthProposerBlockNotFound(t *testing.T) { diff --git a/core/fetcher/fetcher_internal_test.go b/core/fetcher/fetcher_internal_test.go index c62c9e033..81ee62c9d 100644 --- a/core/fetcher/fetcher_internal_test.go +++ b/core/fetcher/fetcher_internal_test.go @@ -67,6 +67,21 @@ func TestVerifyFeeRecipient(t *testing.T) { Blinded: true, }, }, + { + name: "electra", + proposal: eth2api.VersionedProposal{ + Version: eth2spec.DataVersionElectra, + Electra: testutil.RandomElectraVersionedProposal().Electra, + }, + }, + { + name: "electra blinded", + proposal: eth2api.VersionedProposal{ + Version: eth2spec.DataVersionElectra, + ElectraBlinded: testutil.RandomElectraBlindedBeaconBlock(), + Blinded: true, + }, + }, } for _, test := range tests { diff --git a/core/parsigex/parsigex_test.go b/core/parsigex/parsigex_test.go index 4740ec419..80e66ec27 100644 --- a/core/parsigex/parsigex_test.go +++ b/core/parsigex/parsigex_test.go @@ -261,7 +261,7 @@ func TestParSigExVerifier(t *testing.T) { Deneb: ð2p0.SignedAggregateAndProof{ Message: ð2p0.AggregateAndProof{ AggregatorIndex: 0, - Aggregate: testutil.RandomAttestation(), + Aggregate: testutil.RandomPhase0Attestation(), SelectionProof: testutil.RandomEth2Signature(), }, }, diff --git a/core/signeddata_test.go b/core/signeddata_test.go index 780249e51..5b66497d2 100644 --- a/core/signeddata_test.go +++ b/core/signeddata_test.go @@ -11,10 +11,13 @@ import ( eth2bellatrix "github.com/attestantio/go-eth2-client/api/v1/bellatrix" eth2capella "github.com/attestantio/go-eth2-client/api/v1/capella" eth2deneb "github.com/attestantio/go-eth2-client/api/v1/deneb" + eth2electra "github.com/attestantio/go-eth2-client/api/v1/electra" eth2spec "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/bellatrix" "github.com/attestantio/go-eth2-client/spec/capella" + "github.com/attestantio/go-eth2-client/spec/deneb" + "github.com/attestantio/go-eth2-client/spec/electra" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/stretchr/testify/require" @@ -31,7 +34,31 @@ func TestSignedDataSetSignature(t *testing.T) { data core.SignedData }{ { - name: "versioned signed proposal", + name: "versioned signed proposal phase0", + data: core.VersionedSignedProposal{ + VersionedSignedProposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionPhase0, + Phase0: ð2p0.SignedBeaconBlock{ + Message: testutil.RandomPhase0BeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + }, + { + name: "versioned signed proposal altair", + data: core.VersionedSignedProposal{ + VersionedSignedProposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionAltair, + Altair: &altair.SignedBeaconBlock{ + Message: testutil.RandomAltairBeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + }, + { + name: "versioned signed proposal bellatrix", data: core.VersionedSignedProposal{ VersionedSignedProposal: eth2api.VersionedSignedProposal{ Version: eth2spec.DataVersionBellatrix, @@ -42,19 +69,179 @@ func TestSignedDataSetSignature(t *testing.T) { }, }, }, + { + name: "versioned signed proposal bellatrix blinded", + data: core.VersionedSignedProposal{ + VersionedSignedProposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionBellatrix, + BellatrixBlinded: ð2bellatrix.SignedBlindedBeaconBlock{ + Message: testutil.RandomBellatrixBlindedBeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + Blinded: true, + }, + }, + }, + { + name: "versioned signed proposal capella", + data: core.VersionedSignedProposal{ + VersionedSignedProposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionCapella, + Capella: &capella.SignedBeaconBlock{ + Message: testutil.RandomCapellaBeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + }, + { + name: "versioned signed proposal capella blinded", + data: core.VersionedSignedProposal{ + VersionedSignedProposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionCapella, + CapellaBlinded: ð2capella.SignedBlindedBeaconBlock{ + Message: testutil.RandomCapellaBlindedBeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + Blinded: true, + }, + }, + }, + { + name: "versioned signed proposal deneb", + data: core.VersionedSignedProposal{ + VersionedSignedProposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionDeneb, + Deneb: ð2deneb.SignedBlockContents{ + SignedBlock: &deneb.SignedBeaconBlock{ + Message: testutil.RandomDenebBeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + KZGProofs: []deneb.KZGProof{}, + Blobs: []deneb.Blob{}, + }, + }, + }, + }, + { + name: "versioned signed proposal deneb blinded", + data: core.VersionedSignedProposal{ + VersionedSignedProposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionDeneb, + DenebBlinded: ð2deneb.SignedBlindedBeaconBlock{ + Message: testutil.RandomDenebBlindedBeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + Blinded: true, + }, + }, + }, + { + name: "versioned signed proposal electra", + data: core.VersionedSignedProposal{ + VersionedSignedProposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionElectra, + Electra: ð2electra.SignedBlockContents{ + SignedBlock: &electra.SignedBeaconBlock{ + Message: testutil.RandomElectraBeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + KZGProofs: []deneb.KZGProof{}, + Blobs: []deneb.Blob{}, + }, + }, + }, + }, + { + name: "versioned signed proposal electra blinded", + data: core.VersionedSignedProposal{ + VersionedSignedProposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionElectra, + ElectraBlinded: ð2electra.SignedBlindedBeaconBlock{ + Message: testutil.RandomElectraBlindedBeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + Blinded: true, + }, + }, + }, { name: "signed beacon committee selection", data: testutil.RandomCoreBeaconCommitteeSelection(), }, { - name: "signed aggregate and proof", + name: "signed aggregate and proof phase0", + data: core.VersionedSignedAggregateAndProof{ + VersionedSignedAggregateAndProof: eth2spec.VersionedSignedAggregateAndProof{ + Version: eth2spec.DataVersionPhase0, + Phase0: ð2p0.SignedAggregateAndProof{ + Message: ð2p0.AggregateAndProof{ + AggregatorIndex: 0, + Aggregate: testutil.RandomPhase0Attestation(), + SelectionProof: testutil.RandomEth2Signature(), + }, + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + }, + { + name: "signed aggregate and proof altair", + data: core.VersionedSignedAggregateAndProof{ + VersionedSignedAggregateAndProof: eth2spec.VersionedSignedAggregateAndProof{ + Version: eth2spec.DataVersionAltair, + Altair: ð2p0.SignedAggregateAndProof{ + Message: ð2p0.AggregateAndProof{ + AggregatorIndex: 0, + Aggregate: testutil.RandomPhase0Attestation(), + SelectionProof: testutil.RandomEth2Signature(), + }, + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + }, + { + name: "signed aggregate and proof bellatrix", + data: core.VersionedSignedAggregateAndProof{ + VersionedSignedAggregateAndProof: eth2spec.VersionedSignedAggregateAndProof{ + Version: eth2spec.DataVersionBellatrix, + Bellatrix: ð2p0.SignedAggregateAndProof{ + Message: ð2p0.AggregateAndProof{ + AggregatorIndex: 0, + Aggregate: testutil.RandomPhase0Attestation(), + SelectionProof: testutil.RandomEth2Signature(), + }, + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + }, + { + name: "signed aggregate and proof capella", + data: core.VersionedSignedAggregateAndProof{ + VersionedSignedAggregateAndProof: eth2spec.VersionedSignedAggregateAndProof{ + Version: eth2spec.DataVersionCapella, + Capella: ð2p0.SignedAggregateAndProof{ + Message: ð2p0.AggregateAndProof{ + AggregatorIndex: 0, + Aggregate: testutil.RandomPhase0Attestation(), + SelectionProof: testutil.RandomEth2Signature(), + }, + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + }, + { + name: "signed aggregate and proof deneb", data: core.VersionedSignedAggregateAndProof{ VersionedSignedAggregateAndProof: eth2spec.VersionedSignedAggregateAndProof{ Version: eth2spec.DataVersionDeneb, Deneb: ð2p0.SignedAggregateAndProof{ Message: ð2p0.AggregateAndProof{ AggregatorIndex: 0, - Aggregate: testutil.RandomAttestation(), + Aggregate: testutil.RandomPhase0Attestation(), SelectionProof: testutil.RandomEth2Signature(), }, Signature: testutil.RandomEth2Signature(), @@ -62,6 +249,101 @@ func TestSignedDataSetSignature(t *testing.T) { }, }, }, + { + name: "signed aggregate and proof electra", + data: core.VersionedSignedAggregateAndProof{ + VersionedSignedAggregateAndProof: eth2spec.VersionedSignedAggregateAndProof{ + Version: eth2spec.DataVersionElectra, + Electra: &electra.SignedAggregateAndProof{ + Message: &electra.AggregateAndProof{ + AggregatorIndex: 0, + Aggregate: testutil.RandomElectraAttestation(), + SelectionProof: testutil.RandomEth2Signature(), + }, + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + }, + { + name: "signed attestation phase0", + data: core.VersionedAttestation{ + VersionedAttestation: eth2spec.VersionedAttestation{ + Version: eth2spec.DataVersionPhase0, + Phase0: ð2p0.Attestation{ + AggregationBits: testutil.RandomBitList(1), + Data: testutil.RandomAttestationData(), + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + }, + { + name: "signed attestation altair", + data: core.VersionedAttestation{ + VersionedAttestation: eth2spec.VersionedAttestation{ + Version: eth2spec.DataVersionAltair, + Altair: ð2p0.Attestation{ + AggregationBits: testutil.RandomBitList(1), + Data: testutil.RandomAttestationData(), + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + }, + { + name: "signed attestation bellatrix", + data: core.VersionedAttestation{ + VersionedAttestation: eth2spec.VersionedAttestation{ + Version: eth2spec.DataVersionBellatrix, + Bellatrix: ð2p0.Attestation{ + AggregationBits: testutil.RandomBitList(1), + Data: testutil.RandomAttestationData(), + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + }, + { + name: "signed attestation capella", + data: core.VersionedAttestation{ + VersionedAttestation: eth2spec.VersionedAttestation{ + Version: eth2spec.DataVersionCapella, + Capella: ð2p0.Attestation{ + AggregationBits: testutil.RandomBitList(1), + Data: testutil.RandomAttestationData(), + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + }, + { + name: "signed attestation deneb", + data: core.VersionedAttestation{ + VersionedAttestation: eth2spec.VersionedAttestation{ + Version: eth2spec.DataVersionDeneb, + Deneb: ð2p0.Attestation{ + AggregationBits: testutil.RandomBitList(1), + Data: testutil.RandomAttestationData(), + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + }, + { + name: "signed attestation electra", + data: core.VersionedAttestation{ + VersionedAttestation: eth2spec.VersionedAttestation{ + Version: eth2spec.DataVersionElectra, + Electra: &electra.Attestation{ + AggregationBits: testutil.RandomBitList(1), + Data: testutil.RandomAttestationData(), + Signature: testutil.RandomEth2Signature(), + CommitteeBits: testutil.RandomBitVec64(), + }, + }, + }, + }, { name: "signed sync committee message", data: core.SignedSyncMessage{ @@ -179,6 +461,10 @@ func TestNewVersionedSignedProposal(t *testing.T) { error: "no deneb proposal", version: eth2spec.DataVersionDeneb, }, + { + error: "no electra proposal", + version: eth2spec.DataVersionElectra, + }, { error: "no bellatrix blinded proposal", version: eth2spec.DataVersionBellatrix, @@ -194,6 +480,11 @@ func TestNewVersionedSignedProposal(t *testing.T) { version: eth2spec.DataVersionDeneb, blinded: true, }, + { + error: "no electra blinded proposal", + version: eth2spec.DataVersionElectra, + blinded: true, + }, } for _, test := range tests { @@ -225,6 +516,92 @@ func TestNewPartialVersionedSignedProposal(t *testing.T) { require.Equal(t, 3, psd.ShareIdx) } +func TestNewVersionedSignedProposalFromBlindedProposal(t *testing.T) { + proposal, err := testutil.RandomElectraVersionedSignedBlindedProposal().ToBlinded() + require.NoError(t, err) + + pvsp, err := core.NewVersionedSignedProposalFromBlindedProposal(&proposal) + + require.NoError(t, err) + require.NotNil(t, pvsp.ElectraBlinded) +} + +func TestNewPartialVersionedSignedBlindedProposal(t *testing.T) { + proposal, err := testutil.RandomElectraVersionedSignedBlindedProposal().ToBlinded() + require.NoError(t, err) + + pvsp, err := core.NewPartialVersionedSignedBlindedProposal(&proposal, 3) + + require.NoError(t, err) + require.NotNil(t, pvsp.SignedData) + require.Equal(t, 3, pvsp.ShareIdx) +} + +func TestNewVersionedAttestation(t *testing.T) { + type testCase struct { + error string + version eth2spec.DataVersion + } + + tests := []testCase{ + { + error: "unknown version", + version: eth2spec.DataVersion(999), + }, + { + error: "no phase0 attestation", + version: eth2spec.DataVersionPhase0, + }, + { + error: "no altair attestation", + version: eth2spec.DataVersionAltair, + }, + { + error: "no bellatrix attestation", + version: eth2spec.DataVersionBellatrix, + }, + { + error: "no capella attestation", + version: eth2spec.DataVersionCapella, + }, + { + error: "no deneb attestation", + version: eth2spec.DataVersionDeneb, + }, + { + error: "no electra attestation", + version: eth2spec.DataVersionElectra, + }, + } + + for _, test := range tests { + t.Run(test.error, func(t *testing.T) { + _, err := core.NewVersionedAttestation(ð2spec.VersionedAttestation{ + Version: test.version, + }) + require.ErrorContains(t, err, test.error) + }) + } + + t.Run("happy path", func(t *testing.T) { + attestation := testutil.RandomElectraCoreVersionedAttestation() + + p, err := core.NewVersionedAttestation(&attestation.VersionedAttestation) + require.NoError(t, err) + require.Equal(t, attestation, p) + }) +} + +func TestNewPartialVersionedAttestation(t *testing.T) { + attestation := testutil.RandomElectraVersionedAttestation() + + pva, err := core.NewPartialVersionedAttestation(attestation, 3) + + require.NoError(t, err) + require.NotNil(t, pva.SignedData) + require.Equal(t, 3, pva.ShareIdx) +} + func TestVersionedSignedProposal(t *testing.T) { type testCase struct { name string @@ -312,6 +689,24 @@ func TestVersionedSignedProposal(t *testing.T) { Blinded: true, }, }, + { + name: "electra", + proposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionElectra, + Electra: testutil.RandomElectraVersionedSignedProposal().Electra, + }, + }, + { + name: "electra blinded", + proposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionElectra, + ElectraBlinded: ð2electra.SignedBlindedBeaconBlock{ + Message: testutil.RandomElectraBlindedBeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + Blinded: true, + }, + }, } for _, test := range tests { @@ -351,6 +746,140 @@ func TestVersionedSignedProposal(t *testing.T) { } } +func TestVersionedSignedAggregateAndProofUtilFunctions(t *testing.T) { + data := testutil.RandomAttestationData() + aggregationBits := testutil.RandomBitList(64) + type testCase struct { + name string + aggregateAndProof core.VersionedSignedAggregateAndProof + } + + tests := []testCase{ + { + name: "phase0", + aggregateAndProof: core.VersionedSignedAggregateAndProof{ + VersionedSignedAggregateAndProof: eth2spec.VersionedSignedAggregateAndProof{ + Version: eth2spec.DataVersionPhase0, + Phase0: ð2p0.SignedAggregateAndProof{ + Message: ð2p0.AggregateAndProof{ + AggregatorIndex: testutil.RandomVIdx(), + Aggregate: ð2p0.Attestation{ + AggregationBits: aggregationBits, + Data: data, + Signature: testutil.RandomEth2Signature(), + }, + SelectionProof: testutil.RandomEth2Signature(), + }, + }, + }, + }, + }, + { + name: "altair", + aggregateAndProof: core.VersionedSignedAggregateAndProof{ + VersionedSignedAggregateAndProof: eth2spec.VersionedSignedAggregateAndProof{ + Version: eth2spec.DataVersionAltair, + Altair: ð2p0.SignedAggregateAndProof{ + Message: ð2p0.AggregateAndProof{ + AggregatorIndex: testutil.RandomVIdx(), + Aggregate: ð2p0.Attestation{ + AggregationBits: aggregationBits, + Data: data, + Signature: testutil.RandomEth2Signature(), + }, + SelectionProof: testutil.RandomEth2Signature(), + }, + }, + }, + }, + }, + { + name: "bellatrix", + aggregateAndProof: core.VersionedSignedAggregateAndProof{ + VersionedSignedAggregateAndProof: eth2spec.VersionedSignedAggregateAndProof{ + Version: eth2spec.DataVersionBellatrix, + Bellatrix: ð2p0.SignedAggregateAndProof{ + Message: ð2p0.AggregateAndProof{ + AggregatorIndex: testutil.RandomVIdx(), + Aggregate: ð2p0.Attestation{ + AggregationBits: aggregationBits, + Data: data, + Signature: testutil.RandomEth2Signature(), + }, + SelectionProof: testutil.RandomEth2Signature(), + }, + }, + }, + }, + }, + { + name: "capella", + aggregateAndProof: core.VersionedSignedAggregateAndProof{ + VersionedSignedAggregateAndProof: eth2spec.VersionedSignedAggregateAndProof{ + Version: eth2spec.DataVersionCapella, + Capella: ð2p0.SignedAggregateAndProof{ + Message: ð2p0.AggregateAndProof{ + AggregatorIndex: testutil.RandomVIdx(), + Aggregate: ð2p0.Attestation{ + AggregationBits: aggregationBits, + Data: data, + Signature: testutil.RandomEth2Signature(), + }, + SelectionProof: testutil.RandomEth2Signature(), + }, + }, + }, + }, + }, + { + name: "deneb", + aggregateAndProof: core.VersionedSignedAggregateAndProof{ + VersionedSignedAggregateAndProof: eth2spec.VersionedSignedAggregateAndProof{ + Version: eth2spec.DataVersionDeneb, + Deneb: ð2p0.SignedAggregateAndProof{ + Message: ð2p0.AggregateAndProof{ + AggregatorIndex: testutil.RandomVIdx(), + Aggregate: ð2p0.Attestation{ + AggregationBits: aggregationBits, + Data: data, + Signature: testutil.RandomEth2Signature(), + }, + SelectionProof: testutil.RandomEth2Signature(), + }, + }, + }, + }, + }, + { + name: "electra", + aggregateAndProof: core.VersionedSignedAggregateAndProof{ + VersionedSignedAggregateAndProof: eth2spec.VersionedSignedAggregateAndProof{ + Version: eth2spec.DataVersionElectra, + Electra: &electra.SignedAggregateAndProof{ + Message: &electra.AggregateAndProof{ + AggregatorIndex: testutil.RandomVIdx(), + Aggregate: &electra.Attestation{ + AggregationBits: aggregationBits, + Data: data, + Signature: testutil.RandomEth2Signature(), + CommitteeBits: testutil.RandomBitVec64(), + }, + SelectionProof: testutil.RandomEth2Signature(), + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, data, test.aggregateAndProof.Data()) + require.Equal(t, aggregationBits, test.aggregateAndProof.AggregationBits()) + }) + } +} + // func TestGnosisProposals(t *testing.T) { // baseProposal := eth2api.VersionedSignedProposal{ // Version: eth2spec.DataVersionDeneb, diff --git a/core/ssz_test.go b/core/ssz_test.go index 69e7ac2d7..3e7cb1ccc 100644 --- a/core/ssz_test.go +++ b/core/ssz_test.go @@ -14,6 +14,7 @@ import ( eth2bellatrix "github.com/attestantio/go-eth2-client/api/v1/bellatrix" eth2capella "github.com/attestantio/go-eth2-client/api/v1/capella" eth2deneb "github.com/attestantio/go-eth2-client/api/v1/deneb" + eth2electra "github.com/attestantio/go-eth2-client/api/v1/electra" eth2spec "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/bellatrix" @@ -335,6 +336,24 @@ func TestV3SignedProposalSSZSerialisation(t *testing.T) { Blinded: true, }, }, + { + name: "electra", + proposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionElectra, + Electra: testutil.RandomElectraVersionedSignedProposal().Electra, + }, + }, + { + name: "electra blinded", + proposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionElectra, + ElectraBlinded: ð2electra.SignedBlindedBeaconBlock{ + Message: testutil.RandomElectraBlindedBeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + Blinded: true, + }, + }, } for _, test := range tests { diff --git a/core/tracker/inclusion_internal_test.go b/core/tracker/inclusion_internal_test.go index 7a94221ea..4eef23b31 100644 --- a/core/tracker/inclusion_internal_test.go +++ b/core/tracker/inclusion_internal_test.go @@ -8,6 +8,7 @@ import ( "testing" eth2spec "github.com/attestantio/go-eth2-client/spec" + "github.com/attestantio/go-eth2-client/spec/electra" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/prysmaticlabs/go-bitfield" "github.com/stretchr/testify/require" @@ -21,57 +22,129 @@ import ( func TestDuplicateAttData(t *testing.T) { ctx := context.Background() - bmock, err := beaconmock.New() - require.NoError(t, err) - - // Mock 3 attestations, with same data but different aggregation bits. - bits1 := testutil.RandomBitList(8) - bits2 := testutil.RandomBitList(8) - bits3 := testutil.RandomBitList(8) - attData := testutil.RandomAttestationData() - - bmock.BlockAttestationsV2Func = func(_ context.Context, _ string) ([]*eth2spec.VersionedAttestation, error) { - return []*eth2spec.VersionedAttestation{ - {Version: eth2spec.DataVersionDeneb, Deneb: ð2p0.Attestation{AggregationBits: bits1, Data: attData}}, - {Version: eth2spec.DataVersionDeneb, Deneb: ð2p0.Attestation{AggregationBits: bits2, Data: attData}}, - {Version: eth2spec.DataVersionDeneb, Deneb: ð2p0.Attestation{AggregationBits: bits3, Data: attData}}, - }, nil + tests := []struct { + name string + attestationsFunc func(*eth2p0.AttestationData, bitfield.Bitlist, bitfield.Bitlist, bitfield.Bitlist) []*eth2spec.VersionedAttestation + }{ + { + name: "phase0", + attestationsFunc: func(attData *eth2p0.AttestationData, bits1 bitfield.Bitlist, bits2 bitfield.Bitlist, bits3 bitfield.Bitlist) []*eth2spec.VersionedAttestation { + return []*eth2spec.VersionedAttestation{ + {Version: eth2spec.DataVersionPhase0, Phase0: ð2p0.Attestation{AggregationBits: bits1, Data: attData}}, + {Version: eth2spec.DataVersionPhase0, Phase0: ð2p0.Attestation{AggregationBits: bits2, Data: attData}}, + {Version: eth2spec.DataVersionPhase0, Phase0: ð2p0.Attestation{AggregationBits: bits3, Data: attData}}, + } + }, + }, + { + name: "altair", + attestationsFunc: func(attData *eth2p0.AttestationData, bits1 bitfield.Bitlist, bits2 bitfield.Bitlist, bits3 bitfield.Bitlist) []*eth2spec.VersionedAttestation { + return []*eth2spec.VersionedAttestation{ + {Version: eth2spec.DataVersionAltair, Altair: ð2p0.Attestation{AggregationBits: bits1, Data: attData}}, + {Version: eth2spec.DataVersionAltair, Altair: ð2p0.Attestation{AggregationBits: bits2, Data: attData}}, + {Version: eth2spec.DataVersionAltair, Altair: ð2p0.Attestation{AggregationBits: bits3, Data: attData}}, + } + }, + }, + { + name: "bellatrix", + attestationsFunc: func(attData *eth2p0.AttestationData, bits1 bitfield.Bitlist, bits2 bitfield.Bitlist, bits3 bitfield.Bitlist) []*eth2spec.VersionedAttestation { + return []*eth2spec.VersionedAttestation{ + {Version: eth2spec.DataVersionBellatrix, Bellatrix: ð2p0.Attestation{AggregationBits: bits1, Data: attData}}, + {Version: eth2spec.DataVersionBellatrix, Bellatrix: ð2p0.Attestation{AggregationBits: bits2, Data: attData}}, + {Version: eth2spec.DataVersionBellatrix, Bellatrix: ð2p0.Attestation{AggregationBits: bits3, Data: attData}}, + } + }, + }, + { + name: "capella", + attestationsFunc: func(attData *eth2p0.AttestationData, bits1 bitfield.Bitlist, bits2 bitfield.Bitlist, bits3 bitfield.Bitlist) []*eth2spec.VersionedAttestation { + return []*eth2spec.VersionedAttestation{ + {Version: eth2spec.DataVersionCapella, Capella: ð2p0.Attestation{AggregationBits: bits1, Data: attData}}, + {Version: eth2spec.DataVersionCapella, Capella: ð2p0.Attestation{AggregationBits: bits2, Data: attData}}, + {Version: eth2spec.DataVersionCapella, Capella: ð2p0.Attestation{AggregationBits: bits3, Data: attData}}, + } + }, + }, + { + name: "deneb", + attestationsFunc: func(attData *eth2p0.AttestationData, bits1 bitfield.Bitlist, bits2 bitfield.Bitlist, bits3 bitfield.Bitlist) []*eth2spec.VersionedAttestation { + return []*eth2spec.VersionedAttestation{ + {Version: eth2spec.DataVersionDeneb, Deneb: ð2p0.Attestation{AggregationBits: bits1, Data: attData}}, + {Version: eth2spec.DataVersionDeneb, Deneb: ð2p0.Attestation{AggregationBits: bits2, Data: attData}}, + {Version: eth2spec.DataVersionDeneb, Deneb: ð2p0.Attestation{AggregationBits: bits3, Data: attData}}, + } + }, + }, + { + name: "electra", + attestationsFunc: func(attData *eth2p0.AttestationData, bits1 bitfield.Bitlist, bits2 bitfield.Bitlist, bits3 bitfield.Bitlist) []*eth2spec.VersionedAttestation { + return []*eth2spec.VersionedAttestation{ + {Version: eth2spec.DataVersionElectra, Electra: &electra.Attestation{AggregationBits: bits1, Data: attData}}, + {Version: eth2spec.DataVersionElectra, Electra: &electra.Attestation{AggregationBits: bits2, Data: attData}}, + {Version: eth2spec.DataVersionElectra, Electra: &electra.Attestation{AggregationBits: bits3, Data: attData}}, + } + }, + }, } - noopTrackerInclFunc := func(duty core.Duty, key core.PubKey, data core.SignedData, err error) {} - - incl, err := NewInclusion(ctx, bmock, noopTrackerInclFunc) - require.NoError(t, err) - - done := make(chan struct{}) - attDataRoot, err := attData.HashTreeRoot() - require.NoError(t, err) - - // Assert that the block to check contains all bitlists above. - incl.checkBlockFunc = func(ctx context.Context, block block) { - require.Len(t, block.AttestationsByDataRoot, 1) - att, ok := block.AttestationsByDataRoot[attDataRoot] - require.True(t, ok) - - ok, err := att.Deneb.AggregationBits.Contains(bits1) - require.NoError(t, err) - require.True(t, ok) - - ok, err = att.Deneb.AggregationBits.Contains(bits2) - require.NoError(t, err) - require.True(t, ok) - - ok, err = att.Deneb.AggregationBits.Contains(bits3) - require.NoError(t, err) - require.True(t, ok) - - close(done) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + bmock, err := beaconmock.New() + require.NoError(t, err) + + // Mock 3 attestations, with same data but different aggregation bits. + bits1 := testutil.RandomBitList(8) + bits2 := testutil.RandomBitList(8) + bits3 := testutil.RandomBitList(8) + attData := testutil.RandomAttestationData() + + bmock.BlockAttestationsV2Func = func(_ context.Context, _ string) ([]*eth2spec.VersionedAttestation, error) { + return test.attestationsFunc(attData, bits1, bits2, bits3), nil + } + + noopTrackerInclFunc := func(duty core.Duty, key core.PubKey, data core.SignedData, err error) {} + + incl, err := NewInclusion(ctx, bmock, noopTrackerInclFunc) + require.NoError(t, err) + + done := make(chan struct{}) + attDataRoot, err := attData.HashTreeRoot() + require.NoError(t, err) + + // Assert that the block to check contains all bitlists above. + incl.checkBlockFunc = func(ctx context.Context, block block) { + require.Len(t, block.AttestationsByDataRoot, 1) + att, ok := block.AttestationsByDataRoot[attDataRoot] + require.True(t, ok) + + aggBits1, err := att.AggregationBits() + require.NoError(t, err) + ok, err = aggBits1.Contains(bits1) + require.NoError(t, err) + require.True(t, ok) + + aggBits2, err := att.AggregationBits() + require.NoError(t, err) + ok, err = aggBits2.Contains(bits2) + require.NoError(t, err) + require.True(t, ok) + + aggBits3, err := att.AggregationBits() + require.NoError(t, err) + ok, err = aggBits3.Contains(bits3) + require.NoError(t, err) + require.True(t, ok) + + close(done) + } + + err = incl.checkBlock(ctx, uint64(attData.Slot)) + require.NoError(t, err) + + <-done + }) } - - err = incl.checkBlock(ctx, uint64(attData.Slot)) - require.NoError(t, err) - - <-done } func TestInclusion(t *testing.T) { diff --git a/core/unsigneddata_test.go b/core/unsigneddata_test.go index 764cc3dda..f9cc5816e 100644 --- a/core/unsigneddata_test.go +++ b/core/unsigneddata_test.go @@ -8,6 +8,8 @@ import ( eth2api "github.com/attestantio/go-eth2-client/api" eth2spec "github.com/attestantio/go-eth2-client/spec" + "github.com/attestantio/go-eth2-client/spec/electra" + eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/stretchr/testify/require" "github.com/obolnetwork/charon/core" @@ -90,6 +92,10 @@ func TestNewVersionedProposal(t *testing.T) { error: "no deneb block", version: eth2spec.DataVersionDeneb, }, + { + error: "no electra block", + version: eth2spec.DataVersionElectra, + }, { error: "no bellatrix blinded block", version: eth2spec.DataVersionBellatrix, @@ -105,6 +111,11 @@ func TestNewVersionedProposal(t *testing.T) { version: eth2spec.DataVersionDeneb, blinded: true, }, + { + error: "no electra blinded block", + version: eth2spec.DataVersionElectra, + blinded: true, + }, } for _, test := range tests { @@ -126,6 +137,164 @@ func TestNewVersionedProposal(t *testing.T) { }) } +func TestNewVersionedAggregatedAttestation(t *testing.T) { + type testCase struct { + error string + version eth2spec.DataVersion + } + + tests := []testCase{ + { + error: "unknown version", + version: eth2spec.DataVersion(999), + }, + { + error: "no phase0 attestation", + version: eth2spec.DataVersionPhase0, + }, + { + error: "no altair attestation", + version: eth2spec.DataVersionAltair, + }, + { + error: "no bellatrix attestation", + version: eth2spec.DataVersionBellatrix, + }, + { + error: "no capella attestation", + version: eth2spec.DataVersionCapella, + }, + { + error: "no deneb attestation", + version: eth2spec.DataVersionDeneb, + }, + { + error: "no electra attestation", + version: eth2spec.DataVersionElectra, + }, + } + + for _, test := range tests { + t.Run(test.error, func(t *testing.T) { + _, err := core.NewVersionedAggregatedAttestation(ð2spec.VersionedAttestation{ + Version: test.version, + }) + require.ErrorContains(t, err, test.error) + }) + } + + t.Run("happy path", func(t *testing.T) { + attestation := testutil.RandomElectraCoreVersionedAttestation() + + p, err := core.NewVersionedAggregatedAttestation(&attestation.VersionedAttestation) + require.NoError(t, err) + require.Equal(t, attestation.VersionedAttestation, p.VersionedAttestation) + }) +} + +func TestVersionedAggregatedAttestationUtilFunctions(t *testing.T) { + data := testutil.RandomAttestationData() + aggregationBits := testutil.RandomBitList(64) + type testCase struct { + name string + versionedAttestation core.VersionedAggregatedAttestation + } + + tests := []testCase{ + { + name: "phase0", + versionedAttestation: core.VersionedAggregatedAttestation{ + VersionedAttestation: eth2spec.VersionedAttestation{ + Version: eth2spec.DataVersionPhase0, + Phase0: ð2p0.Attestation{ + AggregationBits: aggregationBits, + Data: data, + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + }, + { + name: "altair", + versionedAttestation: core.VersionedAggregatedAttestation{ + VersionedAttestation: eth2spec.VersionedAttestation{ + Version: eth2spec.DataVersionAltair, + Altair: ð2p0.Attestation{ + AggregationBits: aggregationBits, + Data: data, + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + }, + { + name: "bellatrix", + versionedAttestation: core.VersionedAggregatedAttestation{ + VersionedAttestation: eth2spec.VersionedAttestation{ + Version: eth2spec.DataVersionBellatrix, + Bellatrix: ð2p0.Attestation{ + AggregationBits: aggregationBits, + Data: data, + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + }, + { + name: "capella", + versionedAttestation: core.VersionedAggregatedAttestation{ + VersionedAttestation: eth2spec.VersionedAttestation{ + Version: eth2spec.DataVersionCapella, + Capella: ð2p0.Attestation{ + AggregationBits: aggregationBits, + Data: data, + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + }, + { + name: "deneb", + versionedAttestation: core.VersionedAggregatedAttestation{ + VersionedAttestation: eth2spec.VersionedAttestation{ + Version: eth2spec.DataVersionDeneb, + Deneb: ð2p0.Attestation{ + AggregationBits: aggregationBits, + Data: data, + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + }, + { + name: "electra", + versionedAttestation: core.VersionedAggregatedAttestation{ + VersionedAttestation: eth2spec.VersionedAttestation{ + Version: eth2spec.DataVersionElectra, + Electra: &electra.Attestation{ + AggregationBits: aggregationBits, + Data: data, + Signature: testutil.RandomEth2Signature(), + CommitteeBits: testutil.RandomBitVec64(), + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + hash, err := test.versionedAttestation.HashTreeRoot() + require.NoError(t, err) + require.NotNil(t, hash) + + attJSON, err := test.versionedAttestation.MarshalJSON() + require.NoError(t, err) + require.NotNil(t, attJSON) + }) + } +} + func TestVersionedProposal(t *testing.T) { type testCase struct { name string @@ -218,6 +387,21 @@ func TestVersionedProposal(t *testing.T) { Blinded: true, }, }, + { + name: "electra", + proposal: eth2api.VersionedProposal{ + Version: eth2spec.DataVersionElectra, + Electra: testutil.RandomElectraVersionedProposal().Electra, + }, + }, + { + name: "electra blinded", + proposal: eth2api.VersionedProposal{ + Version: eth2spec.DataVersionElectra, + ElectraBlinded: testutil.RandomElectraBlindedBeaconBlock(), + Blinded: true, + }, + }, } for _, test := range tests { diff --git a/core/validatorapi/router.go b/core/validatorapi/router.go index 25842f904..c896e8295 100644 --- a/core/validatorapi/router.go +++ b/core/validatorapi/router.go @@ -26,6 +26,7 @@ import ( eth2bellatrix "github.com/attestantio/go-eth2-client/api/v1/bellatrix" eth2capella "github.com/attestantio/go-eth2-client/api/v1/capella" eth2deneb "github.com/attestantio/go-eth2-client/api/v1/deneb" + eth2electra "github.com/attestantio/go-eth2-client/api/v1/electra" eth2spec "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/bellatrix" @@ -462,16 +463,15 @@ func submitAttestations(p eth2client.AttestationsSubmitter) handlerFunc { return func(ctx context.Context, _ map[string]string, _ url.Values, typ contentType, body []byte) (any, http.Header, error) { atts := []*eth2spec.VersionedAttestation{} - denebAtts := new([]eth2p0.Attestation) - err := unmarshal(typ, body, denebAtts) + electraAtts := new([]electra.Attestation) + err := unmarshal(typ, body, electraAtts) if err == nil { - for _, att := range *denebAtts { - // TODO: Data version is not Deneb, it might be anything between Phase0 and Deneb - versionedAgg := eth2spec.VersionedAttestation{ - Version: eth2spec.DataVersionDeneb, - Deneb: &att, + for _, att := range *electraAtts { + versionedAtt := eth2spec.VersionedAttestation{ + Version: eth2spec.DataVersionElectra, + Electra: &att, } - atts = append(atts, &versionedAgg) + atts = append(atts, &versionedAtt) } return nil, nil, p.SubmitAttestations(ctx, ð2api.SubmitAttestationsOpts{ @@ -479,15 +479,16 @@ func submitAttestations(p eth2client.AttestationsSubmitter) handlerFunc { }) } - electraAtts := new([]electra.Attestation) - err = unmarshal(typ, body, electraAtts) + denebAtts := new([]eth2p0.Attestation) + err = unmarshal(typ, body, denebAtts) if err == nil { - for _, att := range *electraAtts { - versionedAtt := eth2spec.VersionedAttestation{ - Version: eth2spec.DataVersionElectra, - Electra: &att, + for _, att := range *denebAtts { + // TODO(kalo): Data version is not Deneb, it might be anything between Phase0 and Deneb + versionedAgg := eth2spec.VersionedAttestation{ + Version: eth2spec.DataVersionDeneb, + Deneb: &att, } - atts = append(atts, &versionedAtt) + atts = append(atts, &versionedAgg) } return nil, nil, p.SubmitAttestations(ctx, ð2api.SubmitAttestationsOpts{ @@ -842,8 +843,21 @@ func createProposeBlockResponse(proposal *eth2api.VersionedProposal) (*proposeBl func submitProposal(p eth2client.ProposalSubmitter) handlerFunc { return func(ctx context.Context, _ map[string]string, _ url.Values, typ contentType, body []byte) (any, http.Header, error) { + electraBlock := new(eth2electra.SignedBlockContents) + err := unmarshal(typ, body, electraBlock) + if err == nil { + block := ð2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionElectra, + Electra: electraBlock, + } + + return nil, nil, p.SubmitProposal(ctx, ð2api.SubmitProposalOpts{ + Proposal: block, + }) + } + denebBlock := new(eth2deneb.SignedBlockContents) - err := unmarshal(typ, body, denebBlock) + err = unmarshal(typ, body, denebBlock) if err == nil { block := ð2api.VersionedSignedProposal{ Version: eth2spec.DataVersionDeneb, @@ -913,9 +927,22 @@ func submitProposal(p eth2client.ProposalSubmitter) handlerFunc { func submitBlindedBlock(p eth2client.BlindedProposalSubmitter) handlerFunc { return func(ctx context.Context, _ map[string]string, _ url.Values, typ contentType, body []byte) (any, http.Header, error) { - // The blinded block maybe either bellatrix, capella or deneb. + // The blinded block maybe either bellatrix, capella, deneb or electra. + electraBlock := new(eth2electra.SignedBlindedBeaconBlock) + err := unmarshal(typ, body, electraBlock) + if err == nil { + block := ð2api.VersionedSignedBlindedProposal{ + Version: eth2spec.DataVersionElectra, + Electra: electraBlock, + } + + return nil, nil, p.SubmitBlindedProposal(ctx, ð2api.SubmitBlindedProposalOpts{ + Proposal: block, + }) + } + denebBlock := new(eth2deneb.SignedBlindedBeaconBlock) - err := unmarshal(typ, body, denebBlock) + err = unmarshal(typ, body, denebBlock) if err == nil { block := ð2api.VersionedSignedBlindedProposal{ Version: eth2spec.DataVersionDeneb, @@ -1068,14 +1095,13 @@ func submitAggregateAttestations(s eth2client.AggregateAttestationsSubmitter) ha return func(ctx context.Context, _ map[string]string, _ url.Values, typ contentType, body []byte) (any, http.Header, error) { aggs := []*eth2spec.VersionedSignedAggregateAndProof{} - denebAggs := new([]eth2p0.SignedAggregateAndProof) - err := unmarshal(typ, body, denebAggs) + electraAggs := new([]electra.SignedAggregateAndProof) + err := unmarshal(typ, body, electraAggs) if err == nil { - for _, agg := range *denebAggs { - // TODO: Data version is not Deneb, it might be anything between Phase0 and Deneb + for _, agg := range *electraAggs { versionedAgg := eth2spec.VersionedSignedAggregateAndProof{ - Version: eth2spec.DataVersionDeneb, - Deneb: &agg, + Version: eth2spec.DataVersionElectra, + Electra: &agg, } aggs = append(aggs, &versionedAgg) } @@ -1085,13 +1111,14 @@ func submitAggregateAttestations(s eth2client.AggregateAttestationsSubmitter) ha }) } - electraAggs := new([]electra.SignedAggregateAndProof) - err = unmarshal(typ, body, electraAggs) + denebAggs := new([]eth2p0.SignedAggregateAndProof) + err = unmarshal(typ, body, denebAggs) if err == nil { - for _, agg := range *electraAggs { + for _, agg := range *denebAggs { + // TODO(kalo): Data version is not Deneb, it might be anything between Phase0 and Deneb versionedAgg := eth2spec.VersionedSignedAggregateAndProof{ - Version: eth2spec.DataVersionElectra, - Electra: &agg, + Version: eth2spec.DataVersionDeneb, + Deneb: &agg, } aggs = append(aggs, &versionedAgg) } diff --git a/core/validatorapi/router_internal_test.go b/core/validatorapi/router_internal_test.go index 39756deb8..c898ccd62 100644 --- a/core/validatorapi/router_internal_test.go +++ b/core/validatorapi/router_internal_test.go @@ -25,13 +25,15 @@ import ( eth2v1 "github.com/attestantio/go-eth2-client/api/v1" eth2bellatrix "github.com/attestantio/go-eth2-client/api/v1/bellatrix" eth2capella "github.com/attestantio/go-eth2-client/api/v1/capella" - "github.com/attestantio/go-eth2-client/api/v1/deneb" + eth2deneb "github.com/attestantio/go-eth2-client/api/v1/deneb" + eth2electra "github.com/attestantio/go-eth2-client/api/v1/electra" eth2http "github.com/attestantio/go-eth2-client/http" eth2mock "github.com/attestantio/go-eth2-client/mock" eth2spec "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/bellatrix" "github.com/attestantio/go-eth2-client/spec/capella" + "github.com/attestantio/go-eth2-client/spec/electra" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" ssz "github.com/ferranbt/fastssz" "github.com/stretchr/testify/require" @@ -547,6 +549,38 @@ func TestRawRouter(t *testing.T) { require.True(t, done.Load()) }) + t.Run("submit electra ssz beacon block", func(t *testing.T) { + var done atomic.Bool + coreBlock := testutil.RandomElectraCoreVersionedSignedProposal() + proposal := &coreBlock.VersionedSignedProposal + + handler := testHandler{ + SubmitProposalFunc: func(ctx context.Context, actual *eth2api.SubmitProposalOpts) error { + require.Equal(t, proposal, actual.Proposal) + done.Store(true) + + return nil + }, + } + + callback := func(ctx context.Context, baseURL string) { + b, err := ssz.MarshalSSZ(proposal.Electra) + require.NoError(t, err) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, + baseURL+"/eth/v2/beacon/blocks", bytes.NewReader(b)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/octet-stream") + + resp, err := new(http.Client).Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + } + + testRawRouter(t, handler, callback) + require.True(t, done.Load()) + }) + t.Run("get response header for block proposal v3", func(t *testing.T) { block := ð2api.VersionedProposal{ Version: eth2spec.DataVersionCapella, @@ -1203,7 +1237,7 @@ func TestRouter(t *testing.T) { t.Run("submit blinded block deneb", func(t *testing.T) { block1 := ð2api.VersionedSignedBlindedProposal{ Version: eth2spec.DataVersionDeneb, - Deneb: &deneb.SignedBlindedBeaconBlock{ + Deneb: ð2deneb.SignedBlindedBeaconBlock{ Message: testutil.RandomDenebBlindedBeaconBlock(), Signature: testutil.RandomEth2Signature(), }, @@ -1225,6 +1259,31 @@ func TestRouter(t *testing.T) { testRouter(t, handler, callback) }) + t.Run("submit blinded block electra", func(t *testing.T) { + block1 := ð2api.VersionedSignedBlindedProposal{ + Version: eth2spec.DataVersionElectra, + Electra: ð2electra.SignedBlindedBeaconBlock{ + Message: testutil.RandomElectraBlindedBeaconBlock(), + Signature: testutil.RandomEth2Signature(), + }, + } + handler := testHandler{ + SubmitBlindedProposalFunc: func(ctx context.Context, block *eth2api.SubmitBlindedProposalOpts) error { + require.Equal(t, block1, block.Proposal) + return nil + }, + } + + callback := func(ctx context.Context, cl *eth2http.Service) { + err := cl.SubmitBlindedProposal(ctx, ð2api.SubmitBlindedProposalOpts{ + Proposal: block1, + }) + require.NoError(t, err) + } + + testRouter(t, handler, callback) + }) + t.Run("submit validator registration", func(t *testing.T) { expect := []*eth2api.VersionedSignedValidatorRegistration{ { @@ -1422,49 +1481,133 @@ func TestBeaconCommitteeSelections(t *testing.T) { } func TestSubmitAggregateAttestations(t *testing.T) { - ctx := context.Background() - const vIdx = 1 - agg := ð2spec.VersionedSignedAggregateAndProof{ - Version: eth2spec.DataVersionDeneb, - Deneb: ð2p0.SignedAggregateAndProof{ - Message: ð2p0.AggregateAndProof{ - AggregatorIndex: vIdx, - Aggregate: testutil.RandomAttestation(), - SelectionProof: testutil.RandomEth2Signature(), + tests := []struct { + version eth2spec.DataVersion + versionedSignedAggregateAndProof *eth2spec.VersionedSignedAggregateAndProof + }{ + { + version: eth2spec.DataVersionDeneb, + versionedSignedAggregateAndProof: ð2spec.VersionedSignedAggregateAndProof{ + Version: eth2spec.DataVersionDeneb, + Deneb: ð2p0.SignedAggregateAndProof{ + Message: ð2p0.AggregateAndProof{ + AggregatorIndex: vIdx, + Aggregate: testutil.RandomPhase0Attestation(), + SelectionProof: testutil.RandomEth2Signature(), + }, + Signature: testutil.RandomEth2Signature(), + }, + }, + }, + { + version: eth2spec.DataVersionElectra, + versionedSignedAggregateAndProof: ð2spec.VersionedSignedAggregateAndProof{ + Version: eth2spec.DataVersionElectra, + Electra: &electra.SignedAggregateAndProof{ + Message: &electra.AggregateAndProof{ + AggregatorIndex: vIdx, + Aggregate: testutil.RandomElectraAttestation(), + SelectionProof: testutil.RandomEth2Signature(), + }, + Signature: testutil.RandomEth2Signature(), + }, }, - Signature: testutil.RandomEth2Signature(), }, } + for _, test := range tests { + t.Run(test.version.String(), func(t *testing.T) { + ctx := context.Background() - handler := testHandler{ - SubmitAggregateAttestationsFunc: func(_ context.Context, aggregateAndProofs *eth2api.SubmitAggregateAttestationsOpts) error { - require.Equal(t, agg, aggregateAndProofs.SignedAggregateAndProofs[0]) + agg := test.versionedSignedAggregateAndProof + + handler := testHandler{ + SubmitAggregateAttestationsFunc: func(_ context.Context, aggregateAndProofs *eth2api.SubmitAggregateAttestationsOpts) error { + require.Equal(t, agg, aggregateAndProofs.SignedAggregateAndProofs[0]) - return nil + return nil + }, + } + + proxy := httptest.NewServer(handler.newBeaconHandler(t)) + defer proxy.Close() + + r, err := NewRouter(ctx, handler, testBeaconAddr{addr: proxy.URL}, true) + require.NoError(t, err) + + server := httptest.NewServer(r) + defer server.Close() + + var eth2Svc eth2client.Service + eth2Svc, err = eth2http.New(ctx, + eth2http.WithLogLevel(1), + eth2http.WithAddress(server.URL), + ) + require.NoError(t, err) + + eth2Cl := eth2wrap.AdaptEth2HTTP(eth2Svc.(*eth2http.Service), time.Second) + err = eth2Cl.SubmitAggregateAttestations(ctx, ð2api.SubmitAggregateAttestationsOpts{SignedAggregateAndProofs: []*eth2spec.VersionedSignedAggregateAndProof{agg}}) + require.NoError(t, err) + }) + } +} + +func TestSubmitAttestations(t *testing.T) { + tests := []struct { + version eth2spec.DataVersion + versionedAttestation *eth2spec.VersionedAttestation + }{ + { + version: eth2spec.DataVersionDeneb, + versionedAttestation: ð2spec.VersionedAttestation{ + Version: eth2spec.DataVersionDeneb, + Deneb: testutil.RandomPhase0Attestation(), + }, + }, + { + version: eth2spec.DataVersionElectra, + versionedAttestation: ð2spec.VersionedAttestation{ + Version: eth2spec.DataVersionElectra, + Electra: testutil.RandomElectraAttestation(), + }, }, } + for _, test := range tests { + t.Run(test.version.String(), func(t *testing.T) { + ctx := context.Background() - proxy := httptest.NewServer(handler.newBeaconHandler(t)) - defer proxy.Close() + att := test.versionedAttestation - r, err := NewRouter(ctx, handler, testBeaconAddr{addr: proxy.URL}, true) - require.NoError(t, err) + handler := testHandler{ + SubmitAttestationsFunc: func(_ context.Context, attestations *eth2api.SubmitAttestationsOpts) error { + require.Equal(t, att, attestations.Attestations[0]) - server := httptest.NewServer(r) - defer server.Close() + return nil + }, + } - var eth2Svc eth2client.Service - eth2Svc, err = eth2http.New(ctx, - eth2http.WithLogLevel(1), - eth2http.WithAddress(server.URL), - ) - require.NoError(t, err) + proxy := httptest.NewServer(handler.newBeaconHandler(t)) + defer proxy.Close() - eth2Cl := eth2wrap.AdaptEth2HTTP(eth2Svc.(*eth2http.Service), time.Second) - err = eth2Cl.SubmitAggregateAttestations(ctx, ð2api.SubmitAggregateAttestationsOpts{SignedAggregateAndProofs: []*eth2spec.VersionedSignedAggregateAndProof{agg}}) - require.NoError(t, err) + r, err := NewRouter(ctx, handler, testBeaconAddr{addr: proxy.URL}, true) + require.NoError(t, err) + + server := httptest.NewServer(r) + defer server.Close() + + var eth2Svc eth2client.Service + eth2Svc, err = eth2http.New(ctx, + eth2http.WithLogLevel(1), + eth2http.WithAddress(server.URL), + ) + require.NoError(t, err) + + eth2Cl := eth2wrap.AdaptEth2HTTP(eth2Svc.(*eth2http.Service), time.Second) + err = eth2Cl.SubmitAttestations(ctx, ð2api.SubmitAttestationsOpts{Attestations: []*eth2spec.VersionedAttestation{att}}) + require.NoError(t, err) + }) + } } func TestGetExecutionOptimisticFromMetadata(t *testing.T) { @@ -1613,6 +1756,30 @@ func TestCreateProposeBlindedBlockResponse(t *testing.T) { }) require.ErrorContains(t, err, "no deneb blinded block") }) + + t.Run("electra", func(t *testing.T) { + p := ð2api.VersionedProposal{ + Version: eth2spec.DataVersionElectra, + ElectraBlinded: testutil.RandomElectraBlindedBeaconBlock(), + Blinded: true, + ConsensusValue: big.NewInt(123), + ExecutionValue: big.NewInt(456), + } + + pp, err := createProposeBlockResponse(p) + require.NoError(t, err) + require.NotNil(t, pp) + require.Equal(t, p.Version.String(), pp.Version) + require.Equal(t, p.ElectraBlinded, pp.Data) + require.Equal(t, p.ConsensusValue.String(), pp.ConsensusBlockValue) + require.Equal(t, p.ExecutionValue.String(), pp.ExecutionPayloadValue) + + _, err = createProposeBlockResponse(ð2api.VersionedProposal{ + Version: eth2spec.DataVersionElectra, + Blinded: true, + }) + require.ErrorContains(t, err, "no electra blinded block") + }) } func TestCreateProposeBlockResponse(t *testing.T) { @@ -1788,6 +1955,7 @@ type testHandler struct { AggregateSyncCommitteeSelectionsFunc func(ctx context.Context, partialSelections []*eth2exp.SyncCommitteeSelection) ([]*eth2exp.SyncCommitteeSelection, error) AttestationDataFunc func(ctx context.Context, opts *eth2api.AttestationDataOpts) (*eth2api.Response[*eth2p0.AttestationData], error) AttesterDutiesFunc func(ctx context.Context, opts *eth2api.AttesterDutiesOpts) (*eth2api.Response[[]*eth2v1.AttesterDuty], error) + SubmitAttestationsFunc func(ctx context.Context, opts *eth2api.SubmitAttestationsOpts) error ProposalFunc func(ctx context.Context, opts *eth2api.ProposalOpts) (*eth2api.Response[*eth2api.VersionedProposal], error) SubmitProposalFunc func(ctx context.Context, proposal *eth2api.SubmitProposalOpts) error SubmitBlindedProposalFunc func(ctx context.Context, proposal *eth2api.SubmitBlindedProposalOpts) error @@ -1813,6 +1981,10 @@ func (h testHandler) AttesterDuties(ctx context.Context, opts *eth2api.AttesterD return h.AttesterDutiesFunc(ctx, opts) } +func (h testHandler) SubmitAttestations(ctx context.Context, opts *eth2api.SubmitAttestationsOpts) error { + return h.SubmitAttestationsFunc(ctx, opts) +} + func (h testHandler) Proposal(ctx context.Context, opts *eth2api.ProposalOpts) (*eth2api.Response[*eth2api.VersionedProposal], error) { return h.ProposalFunc(ctx, opts) } diff --git a/core/validatorapi/validatorapi.go b/core/validatorapi/validatorapi.go index f05ceccc1..fbf83d86a 100644 --- a/core/validatorapi/validatorapi.go +++ b/core/validatorapi/validatorapi.go @@ -589,6 +589,7 @@ func (c Component) SubmitBlindedProposal(ctx context.Context, opts *eth2api.Subm BellatrixBlinded: opts.Proposal.Bellatrix, CapellaBlinded: opts.Proposal.Capella, DenebBlinded: opts.Proposal.Deneb, + ElectraBlinded: opts.Proposal.Electra, }, BroadcastValidation: opts.BroadcastValidation, }, prop); err != nil { diff --git a/core/validatorapi/validatorapi_test.go b/core/validatorapi/validatorapi_test.go index 27cb7b9e6..698820ecb 100644 --- a/core/validatorapi/validatorapi_test.go +++ b/core/validatorapi/validatorapi_test.go @@ -17,10 +17,13 @@ import ( eth2bellatrix "github.com/attestantio/go-eth2-client/api/v1/bellatrix" eth2capella "github.com/attestantio/go-eth2-client/api/v1/capella" eth2deneb "github.com/attestantio/go-eth2-client/api/v1/deneb" + eth2electra "github.com/attestantio/go-eth2-client/api/v1/electra" eth2spec "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/bellatrix" "github.com/attestantio/go-eth2-client/spec/capella" + "github.com/attestantio/go-eth2-client/spec/deneb" + "github.com/attestantio/go-eth2-client/spec/electra" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/prysmaticlabs/go-bitfield" "github.com/stretchr/testify/require" @@ -520,13 +523,6 @@ func TestComponent_SubmitProposalsWithWrongVCData(t *testing.T) { func TestComponent_SubmitProposal(t *testing.T) { ctx := context.Background() - // Create keys (just use normal keys, not split tbls) - secret, err := tbls.GenerateSecretKey() - require.NoError(t, err) - - pubkey, err := tbls.SecretToPublicKey(secret) - require.NoError(t, err) - const ( vIdx = 1 shareIdx = 1 @@ -534,78 +530,189 @@ func TestComponent_SubmitProposal(t *testing.T) { epoch = eth2p0.Epoch(3) ) - // Convert pubkey - corePubKey, err := core.PubKeyFromBytes(pubkey[:]) - require.NoError(t, err) - allPubSharesByKey := map[core.PubKey]map[int]tbls.PublicKey{corePubKey: {shareIdx: pubkey}} // Maps self to self since not tbls - - // Configure beacon mock - bmock, err := beaconmock.New() - require.NoError(t, err) + tests := []struct { + version eth2spec.DataVersion + versionedSignedBlock *eth2spec.VersionedSignedBeaconBlock + unsignedBlockFunc func(eth2p0.BLSSignature) *eth2spec.VersionedBeaconBlock + signedBlockFunc func(*eth2spec.VersionedBeaconBlock, [96]byte) *eth2api.VersionedSignedProposal + awaitBlockFunc func(*eth2spec.VersionedBeaconBlock, *eth2api.VersionedSignedProposal) *eth2api.VersionedProposal + }{ + { + version: eth2spec.DataVersionCapella, + versionedSignedBlock: testutil.RandomCapellaVersionedSignedBeaconBlock(), + unsignedBlockFunc: func(randao eth2p0.BLSSignature) *eth2spec.VersionedBeaconBlock { + unsignedBlock := ð2spec.VersionedBeaconBlock{ + Version: eth2spec.DataVersionCapella, + Capella: testutil.RandomCapellaBeaconBlock(), + } + unsignedBlock.Capella.Body.RANDAOReveal = randao + unsignedBlock.Capella.Slot = slot + unsignedBlock.Capella.ProposerIndex = vIdx - // Construct the validator api component - vapi, err := validatorapi.NewComponent(bmock, allPubSharesByKey, shareIdx, nil, false, 30000000, nil) - require.NoError(t, err) + return unsignedBlock + }, + signedBlockFunc: func(unsignedBlock *eth2spec.VersionedBeaconBlock, s [96]byte) *eth2api.VersionedSignedProposal { + return ð2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionCapella, + Capella: &capella.SignedBeaconBlock{ + Message: unsignedBlock.Capella, + Signature: eth2p0.BLSSignature(s), + }, + } + }, + awaitBlockFunc: func(unsignedBlock *eth2spec.VersionedBeaconBlock, signedBlock *eth2api.VersionedSignedProposal) *eth2api.VersionedProposal { + return ð2api.VersionedProposal{ + Version: signedBlock.Version, + Capella: unsignedBlock.Capella, + } + }, + }, + { + version: eth2spec.DataVersionDeneb, + versionedSignedBlock: testutil.RandomDenebVersionedSignedBeaconBlock(), + unsignedBlockFunc: func(randao eth2p0.BLSSignature) *eth2spec.VersionedBeaconBlock { + unsignedBlock := ð2spec.VersionedBeaconBlock{ + Version: eth2spec.DataVersionDeneb, + Deneb: testutil.RandomDenebBeaconBlock(), + } + unsignedBlock.Deneb.Body.RANDAOReveal = randao + unsignedBlock.Deneb.Slot = slot + unsignedBlock.Deneb.ProposerIndex = vIdx - // Prepare unsigned beacon block - msg := []byte("randao reveal") - sig, err := tbls.Sign(secret, msg) - require.NoError(t, err) + return unsignedBlock + }, + signedBlockFunc: func(unsignedBlock *eth2spec.VersionedBeaconBlock, s [96]byte) *eth2api.VersionedSignedProposal { + return ð2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionDeneb, + Deneb: ð2deneb.SignedBlockContents{ + SignedBlock: &deneb.SignedBeaconBlock{ + Message: unsignedBlock.Deneb, + Signature: eth2p0.BLSSignature(s), + }, + KZGProofs: []deneb.KZGProof{}, + Blobs: []deneb.Blob{}, + }, + } + }, + awaitBlockFunc: func(unsignedBlock *eth2spec.VersionedBeaconBlock, signedBlock *eth2api.VersionedSignedProposal) *eth2api.VersionedProposal { + return ð2api.VersionedProposal{ + Version: signedBlock.Version, + Deneb: ð2deneb.BlockContents{ + Block: unsignedBlock.Deneb, + KZGProofs: []deneb.KZGProof{}, + Blobs: []deneb.Blob{}, + }, + } + }, + }, + { + version: eth2spec.DataVersionElectra, + versionedSignedBlock: testutil.RandomElectraVersionedSignedBeaconBlock(), + unsignedBlockFunc: func(randao eth2p0.BLSSignature) *eth2spec.VersionedBeaconBlock { + unsignedBlock := ð2spec.VersionedBeaconBlock{ + Version: eth2spec.DataVersionElectra, + Electra: testutil.RandomElectraBeaconBlock(), + } + unsignedBlock.Electra.Body.RANDAOReveal = randao + unsignedBlock.Electra.Slot = slot + unsignedBlock.Electra.ProposerIndex = vIdx - randao := eth2p0.BLSSignature(sig) - unsignedBlock := ð2spec.VersionedBeaconBlock{ - Version: eth2spec.DataVersionCapella, - Capella: testutil.RandomCapellaBeaconBlock(), + return unsignedBlock + }, + signedBlockFunc: func(unsignedBlock *eth2spec.VersionedBeaconBlock, s [96]byte) *eth2api.VersionedSignedProposal { + return ð2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionElectra, + Electra: ð2electra.SignedBlockContents{ + SignedBlock: &electra.SignedBeaconBlock{ + Message: unsignedBlock.Electra, + Signature: eth2p0.BLSSignature(s), + }, + KZGProofs: []deneb.KZGProof{}, + Blobs: []deneb.Blob{}, + }, + } + }, + awaitBlockFunc: func(unsignedBlock *eth2spec.VersionedBeaconBlock, signedBlock *eth2api.VersionedSignedProposal) *eth2api.VersionedProposal { + return ð2api.VersionedProposal{ + Version: signedBlock.Version, + Electra: ð2electra.BlockContents{ + Block: unsignedBlock.Electra, + KZGProofs: []deneb.KZGProof{}, + Blobs: []deneb.Blob{}, + }, + } + }, + }, } - unsignedBlock.Capella.Body.RANDAOReveal = randao - unsignedBlock.Capella.Slot = slot - unsignedBlock.Capella.ProposerIndex = vIdx - vapi.RegisterGetDutyDefinition(func(ctx context.Context, duty core.Duty) (core.DutyDefinitionSet, error) { - return core.DutyDefinitionSet{corePubKey: nil}, nil - }) + for _, test := range tests { + t.Run(test.version.String(), func(t *testing.T) { + // Create keys (just use normal keys, not split tbls) + secret, err := tbls.GenerateSecretKey() + require.NoError(t, err) - // Sign beacon block - sigRoot, err := unsignedBlock.Root() - require.NoError(t, err) + pubkey, err := tbls.SecretToPublicKey(secret) + require.NoError(t, err) - domain, err := signing.GetDomain(ctx, bmock, signing.DomainBeaconProposer, epoch) - require.NoError(t, err) + // Convert pubkey + corePubKey, err := core.PubKeyFromBytes(pubkey[:]) + require.NoError(t, err) + allPubSharesByKey := map[core.PubKey]map[int]tbls.PublicKey{corePubKey: {shareIdx: pubkey}} // Maps self to self since not tbls - sigData, err := (ð2p0.SigningData{ObjectRoot: sigRoot, Domain: domain}).HashTreeRoot() - require.NoError(t, err) + // Configure beacon mock + bmock, err := beaconmock.New() + require.NoError(t, err) - s, err := tbls.Sign(secret, sigData[:]) - require.NoError(t, err) + // Construct the validator api component + vapi, err := validatorapi.NewComponent(bmock, allPubSharesByKey, shareIdx, nil, false, 30000000, nil) + require.NoError(t, err) - signedBlock := ð2api.VersionedSignedProposal{ - Version: eth2spec.DataVersionCapella, - Capella: &capella.SignedBeaconBlock{ - Message: unsignedBlock.Capella, - Signature: eth2p0.BLSSignature(s), - }, - } + // Prepare unsigned beacon block + msg := []byte("randao reveal") + sig, err := tbls.Sign(secret, msg) + require.NoError(t, err) - vapi.RegisterAwaitProposal(func(ctx context.Context, slot uint64) (*eth2api.VersionedProposal, error) { - return ð2api.VersionedProposal{ - Version: signedBlock.Version, - Capella: unsignedBlock.Capella, - }, nil - }) + randao := eth2p0.BLSSignature(sig) + unsignedBlock := test.unsignedBlockFunc(randao) - // Register subscriber - vapi.Subscribe(func(ctx context.Context, duty core.Duty, set core.ParSignedDataSet) error { - block, ok := set[corePubKey].SignedData.(core.VersionedSignedProposal) - require.True(t, ok) - require.Equal(t, *signedBlock, block.VersionedSignedProposal) + vapi.RegisterGetDutyDefinition(func(ctx context.Context, duty core.Duty) (core.DutyDefinitionSet, error) { + return core.DutyDefinitionSet{corePubKey: nil}, nil + }) - return nil - }) + // Sign beacon block + sigRoot, err := unsignedBlock.Root() + require.NoError(t, err) - err = vapi.SubmitProposal(ctx, ð2api.SubmitProposalOpts{ - Proposal: signedBlock, - }) - require.NoError(t, err) + domain, err := signing.GetDomain(ctx, bmock, signing.DomainBeaconProposer, epoch) + require.NoError(t, err) + + sigData, err := (ð2p0.SigningData{ObjectRoot: sigRoot, Domain: domain}).HashTreeRoot() + require.NoError(t, err) + + s, err := tbls.Sign(secret, sigData[:]) + require.NoError(t, err) + + signedBlock := test.signedBlockFunc(unsignedBlock, s) + + vapi.RegisterAwaitProposal(func(ctx context.Context, slot uint64) (*eth2api.VersionedProposal, error) { + return test.awaitBlockFunc(unsignedBlock, signedBlock), nil + }) + + // Register subscriber + vapi.Subscribe(func(ctx context.Context, duty core.Duty, set core.ParSignedDataSet) error { + block, ok := set[corePubKey].SignedData.(core.VersionedSignedProposal) + require.True(t, ok) + require.Equal(t, *signedBlock, block.VersionedSignedProposal) + + return nil + }) + + err = vapi.SubmitProposal(ctx, ð2api.SubmitProposalOpts{ + Proposal: signedBlock, + }) + require.NoError(t, err) + }) + } } // func TestComponent_SubmitProposal_Gnosis(t *testing.T) { @@ -905,13 +1012,6 @@ func TestComponent_SubmitProposalInvalidBlock(t *testing.T) { func TestComponent_SubmitBlindedProposal(t *testing.T) { ctx := context.Background() - // Create keys (just use normal keys, not split tbls) - secret, err := tbls.GenerateSecretKey() - require.NoError(t, err) - - pubkey, err := tbls.SecretToPublicKey(secret) - require.NoError(t, err) - const ( vIdx = 1 shareIdx = 1 @@ -919,78 +1019,170 @@ func TestComponent_SubmitBlindedProposal(t *testing.T) { epoch = eth2p0.Epoch(3) ) - // Convert pubkey - corePubKey, err := core.PubKeyFromBytes(pubkey[:]) - require.NoError(t, err) - allPubSharesByKey := map[core.PubKey]map[int]tbls.PublicKey{corePubKey: {shareIdx: pubkey}} // Maps self to self since not tbls + tests := []struct { + version eth2spec.DataVersion + versionedSignedBlock *eth2spec.VersionedSignedBeaconBlock + unsignedBlockSigRootFunc func(tbls.Signature) (any, [32]byte, error) + signedBlockFunc func(any, [96]byte) *eth2api.VersionedSignedBlindedProposal + awaitBlockFunc func(*eth2api.VersionedSignedBlindedProposal) *eth2api.VersionedProposal + }{ + { + version: eth2spec.DataVersionCapella, + unsignedBlockSigRootFunc: func(sig tbls.Signature) (any, [32]byte, error) { + unsignedBlindedBlock := testutil.RandomCapellaBlindedBeaconBlock() + unsignedBlindedBlock.Body.RANDAOReveal = eth2p0.BLSSignature(sig) + unsignedBlindedBlock.Slot = slot + unsignedBlindedBlock.ProposerIndex = vIdx + hash, err := unsignedBlindedBlock.HashTreeRoot() + + return unsignedBlindedBlock, hash, err + }, + signedBlockFunc: func(unsignedBlock any, s [96]byte) *eth2api.VersionedSignedBlindedProposal { + parsed := unsignedBlock.(*eth2capella.BlindedBeaconBlock) + return ð2api.VersionedSignedBlindedProposal{ + Version: eth2spec.DataVersionCapella, + Capella: ð2capella.SignedBlindedBeaconBlock{ + Message: parsed, + Signature: eth2p0.BLSSignature(s), + }, + } + }, + awaitBlockFunc: func(signedBlock *eth2api.VersionedSignedBlindedProposal) *eth2api.VersionedProposal { + return ð2api.VersionedProposal{ + Version: signedBlock.Version, + Blinded: true, + CapellaBlinded: signedBlock.Capella.Message, + } + }, + }, + { + version: eth2spec.DataVersionDeneb, + unsignedBlockSigRootFunc: func(sig tbls.Signature) (any, [32]byte, error) { + unsignedBlindedBlock := testutil.RandomDenebBlindedBeaconBlock() + unsignedBlindedBlock.Body.RANDAOReveal = eth2p0.BLSSignature(sig) + unsignedBlindedBlock.Slot = slot + unsignedBlindedBlock.ProposerIndex = vIdx + hash, err := unsignedBlindedBlock.HashTreeRoot() + + return unsignedBlindedBlock, hash, err + }, + signedBlockFunc: func(unsignedBlock any, s [96]byte) *eth2api.VersionedSignedBlindedProposal { + parsed := unsignedBlock.(*eth2deneb.BlindedBeaconBlock) + return ð2api.VersionedSignedBlindedProposal{ + Version: eth2spec.DataVersionDeneb, + Deneb: ð2deneb.SignedBlindedBeaconBlock{ + Message: parsed, + Signature: eth2p0.BLSSignature(s), + }, + } + }, + awaitBlockFunc: func(signedBlock *eth2api.VersionedSignedBlindedProposal) *eth2api.VersionedProposal { + return ð2api.VersionedProposal{ + Version: signedBlock.Version, + Blinded: true, + DenebBlinded: signedBlock.Deneb.Message, + } + }, + }, + { + version: eth2spec.DataVersionElectra, + unsignedBlockSigRootFunc: func(sig tbls.Signature) (any, [32]byte, error) { + unsignedBlindedBlock := testutil.RandomElectraBlindedBeaconBlock() + unsignedBlindedBlock.Body.RANDAOReveal = eth2p0.BLSSignature(sig) + unsignedBlindedBlock.Slot = slot + unsignedBlindedBlock.ProposerIndex = vIdx + hash, err := unsignedBlindedBlock.HashTreeRoot() + + return unsignedBlindedBlock, hash, err + }, + signedBlockFunc: func(unsignedBlock any, s [96]byte) *eth2api.VersionedSignedBlindedProposal { + parsed := unsignedBlock.(*eth2electra.BlindedBeaconBlock) + return ð2api.VersionedSignedBlindedProposal{ + Version: eth2spec.DataVersionElectra, + Electra: ð2electra.SignedBlindedBeaconBlock{ + Message: parsed, + Signature: eth2p0.BLSSignature(s), + }, + } + }, + awaitBlockFunc: func(signedBlock *eth2api.VersionedSignedBlindedProposal) *eth2api.VersionedProposal { + return ð2api.VersionedProposal{ + Version: signedBlock.Version, + Blinded: true, + ElectraBlinded: signedBlock.Electra.Message, + } + }, + }, + } - // Configure beacon mock - bmock, err := beaconmock.New() - require.NoError(t, err) + for _, test := range tests { + t.Run(test.version.String(), func(t *testing.T) { + // Create keys (just use normal keys, not split tbls) + secret, err := tbls.GenerateSecretKey() + require.NoError(t, err) - // Construct the validator api component - vapi, err := validatorapi.NewComponent(bmock, allPubSharesByKey, shareIdx, nil, true, 30000000, nil) - require.NoError(t, err) + pubkey, err := tbls.SecretToPublicKey(secret) + require.NoError(t, err) - // Prepare unsigned beacon block - msg := []byte("randao reveal") - sig, err := tbls.Sign(secret, msg) - require.NoError(t, err) + // Convert pubkey + corePubKey, err := core.PubKeyFromBytes(pubkey[:]) + require.NoError(t, err) + allPubSharesByKey := map[core.PubKey]map[int]tbls.PublicKey{corePubKey: {shareIdx: pubkey}} // Maps self to self since not tbls - unsignedBlindedBlock := testutil.RandomCapellaBlindedBeaconBlock() - unsignedBlindedBlock.Body.RANDAOReveal = eth2p0.BLSSignature(sig) - unsignedBlindedBlock.Slot = slot - unsignedBlindedBlock.ProposerIndex = vIdx + // Configure beacon mock + bmock, err := beaconmock.New() + require.NoError(t, err) - vapi.RegisterGetDutyDefinition(func(ctx context.Context, duty core.Duty) (core.DutyDefinitionSet, error) { - return core.DutyDefinitionSet{corePubKey: nil}, nil - }) + // Construct the validator api component + vapi, err := validatorapi.NewComponent(bmock, allPubSharesByKey, shareIdx, nil, true, 30000000, nil) + require.NoError(t, err) - // Sign blinded beacon block - sigRoot, err := unsignedBlindedBlock.HashTreeRoot() - require.NoError(t, err) + // Prepare unsigned beacon block + msg := []byte("randao reveal") + sig, err := tbls.Sign(secret, msg) + require.NoError(t, err) - domain, err := signing.GetDomain(ctx, bmock, signing.DomainBeaconProposer, epoch) - require.NoError(t, err) + vapi.RegisterGetDutyDefinition(func(ctx context.Context, duty core.Duty) (core.DutyDefinitionSet, error) { + return core.DutyDefinitionSet{corePubKey: nil}, nil + }) - sigData, err := (ð2p0.SigningData{ObjectRoot: sigRoot, Domain: domain}).HashTreeRoot() - require.NoError(t, err) + // Sign blinded beacon block + unsignedBlindedBlock, sigRoot, err := test.unsignedBlockSigRootFunc(sig) + require.NoError(t, err) - s, err := tbls.Sign(secret, sigData[:]) - require.NoError(t, err) + domain, err := signing.GetDomain(ctx, bmock, signing.DomainBeaconProposer, epoch) + require.NoError(t, err) - signedBlindedBlock := ð2api.VersionedSignedBlindedProposal{ - Version: eth2spec.DataVersionCapella, - Capella: ð2capella.SignedBlindedBeaconBlock{ - Message: unsignedBlindedBlock, - Signature: eth2p0.BLSSignature(s), - }, - } + sigData, err := (ð2p0.SigningData{ObjectRoot: sigRoot, Domain: domain}).HashTreeRoot() + require.NoError(t, err) - vapi.RegisterAwaitProposal(func(ctx context.Context, slot uint64) (*eth2api.VersionedProposal, error) { - return ð2api.VersionedProposal{ - Version: signedBlindedBlock.Version, - Blinded: true, - CapellaBlinded: signedBlindedBlock.Capella.Message, - }, nil - }) + s, err := tbls.Sign(secret, sigData[:]) + require.NoError(t, err) - // Register subscriber - vapi.Subscribe(func(ctx context.Context, duty core.Duty, set core.ParSignedDataSet) error { - block, ok := set[corePubKey].SignedData.(core.VersionedSignedProposal) - require.True(t, ok) + signedBlindedBlock := test.signedBlockFunc(unsignedBlindedBlock, s) - blindedBlock, err := block.ToBlinded() - require.NoError(t, err) - require.Equal(t, *signedBlindedBlock, blindedBlock) + vapi.RegisterAwaitProposal(func(ctx context.Context, slot uint64) (*eth2api.VersionedProposal, error) { + return test.awaitBlockFunc(signedBlindedBlock), nil + }) - return nil - }) + // Register subscriber + vapi.Subscribe(func(ctx context.Context, duty core.Duty, set core.ParSignedDataSet) error { + block, ok := set[corePubKey].SignedData.(core.VersionedSignedProposal) + require.True(t, ok) - err = vapi.SubmitBlindedProposal(ctx, ð2api.SubmitBlindedProposalOpts{ - Proposal: signedBlindedBlock, - }) - require.NoError(t, err) + blindedBlock, err := block.ToBlinded() + require.NoError(t, err) + require.Equal(t, *signedBlindedBlock, blindedBlock) + + return nil + }) + + err = vapi.SubmitBlindedProposal(ctx, ð2api.SubmitBlindedProposalOpts{ + Proposal: signedBlindedBlock, + }) + require.NoError(t, err) + }) + } } func TestComponent_SubmitBlindedProposalInvalidSignature(t *testing.T) { @@ -1671,7 +1863,7 @@ func TestComponent_SubmitAggregateAttestations(t *testing.T) { Deneb: ð2p0.SignedAggregateAndProof{ Message: ð2p0.AggregateAndProof{ AggregatorIndex: vIdx, - Aggregate: testutil.RandomAttestation(), + Aggregate: testutil.RandomPhase0Attestation(), SelectionProof: testutil.RandomEth2Signature(), }, Signature: testutil.RandomEth2Signature(), @@ -1731,7 +1923,7 @@ func TestComponent_SubmitAggregateAttestationVerify(t *testing.T) { slot := eth2p0.Slot(99) aggProof := ð2p0.AggregateAndProof{ AggregatorIndex: val.Index, - Aggregate: testutil.RandomAttestation(), + Aggregate: testutil.RandomPhase0Attestation(), } aggProof.Aggregate.Data.Slot = slot aggProof.SelectionProof = signBeaconSelection(t, bmock, secret, slot) diff --git a/eth2util/types_test.go b/eth2util/types_test.go index c52ba3a60..ca6a424ce 100644 --- a/eth2util/types_test.go +++ b/eth2util/types_test.go @@ -9,6 +9,7 @@ import ( "math/rand" "testing" + eth2spec "github.com/attestantio/go-eth2-client/spec" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/stretchr/testify/require" @@ -53,3 +54,93 @@ func TestUnmarshallingSignedEpoch(t *testing.T) { require.Equal(t, sig, e2.Signature[:]) require.Equal(t, epoch, e2.Epoch) } + +func TestToETH2(t *testing.T) { + tests := []struct { + version eth2util.DataVersion + expectedVersion eth2spec.DataVersion + }{ + { + version: eth2util.DataVersionUnknown, + expectedVersion: eth2spec.DataVersionUnknown, + }, + { + version: eth2util.DataVersionPhase0, + expectedVersion: eth2spec.DataVersionPhase0, + }, + { + version: eth2util.DataVersionAltair, + expectedVersion: eth2spec.DataVersionAltair, + }, + { + version: eth2util.DataVersionBellatrix, + expectedVersion: eth2spec.DataVersionBellatrix, + }, + { + version: eth2util.DataVersionCapella, + expectedVersion: eth2spec.DataVersionCapella, + }, + { + version: eth2util.DataVersionDeneb, + expectedVersion: eth2spec.DataVersionDeneb, + }, + { + version: eth2util.DataVersionElectra, + expectedVersion: eth2spec.DataVersionElectra, + }, + } + for _, test := range tests { + t.Run(test.version.String(), func(t *testing.T) { + require.Equal(t, test.expectedVersion, test.version.ToETH2()) + }, + ) + } +} + +func TestDataVersionFromETH2(t *testing.T) { + tests := []struct { + version eth2spec.DataVersion + expectedVersion eth2util.DataVersion + expectedErr string + }{ + { + version: eth2spec.DataVersionUnknown, + expectedVersion: eth2util.DataVersionUnknown, + expectedErr: "unknown data version", + }, + { + version: eth2spec.DataVersionPhase0, + expectedVersion: eth2util.DataVersionPhase0, + }, + { + version: eth2spec.DataVersionAltair, + expectedVersion: eth2util.DataVersionAltair, + }, + { + version: eth2spec.DataVersionBellatrix, + expectedVersion: eth2util.DataVersionBellatrix, + }, + { + version: eth2spec.DataVersionCapella, + expectedVersion: eth2util.DataVersionCapella, + }, + { + version: eth2spec.DataVersionDeneb, + expectedVersion: eth2util.DataVersionDeneb, + }, + { + version: eth2spec.DataVersionElectra, + expectedVersion: eth2util.DataVersionElectra, + }, + } + for _, test := range tests { + t.Run(test.expectedVersion.String(), func(t *testing.T) { + actual, err := eth2util.DataVersionFromETH2(test.version) + if test.expectedErr != "" { + require.ErrorContains(t, err, test.expectedErr) + } + require.Equal(t, test.expectedVersion, actual) + }, + ) + } +} diff --git a/testutil/random.go b/testutil/random.go index 300e9f3e4..b38818937 100644 --- a/testutil/random.go +++ b/testutil/random.go @@ -19,11 +19,13 @@ import ( eth2bellatrix "github.com/attestantio/go-eth2-client/api/v1/bellatrix" eth2capella "github.com/attestantio/go-eth2-client/api/v1/capella" eth2deneb "github.com/attestantio/go-eth2-client/api/v1/deneb" + eth2electra "github.com/attestantio/go-eth2-client/api/v1/electra" eth2spec "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/bellatrix" "github.com/attestantio/go-eth2-client/spec/capella" "github.com/attestantio/go-eth2-client/spec/deneb" + "github.com/attestantio/go-eth2-client/spec/electra" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" k1 "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/holiman/uint256" @@ -117,7 +119,7 @@ func RandomValidatorSet(t *testing.T, vals int) map[eth2p0.ValidatorIndex]*eth2v return resp } -func RandomAttestation() *eth2p0.Attestation { +func RandomPhase0Attestation() *eth2p0.Attestation { return ð2p0.Attestation{ AggregationBits: RandomBitList(1), Data: RandomAttestationData(), @@ -125,11 +127,29 @@ func RandomAttestation() *eth2p0.Attestation { } } +func RandomElectraAttestation() *electra.Attestation { + return &electra.Attestation{ + AggregationBits: RandomBitList(1), + Data: RandomAttestationData(), + Signature: RandomEth2Signature(), + CommitteeBits: RandomBitVec64(), + } +} + func RandomDenebCoreVersionedAttestation() core.VersionedAttestation { return core.VersionedAttestation{ VersionedAttestation: eth2spec.VersionedAttestation{ Version: eth2spec.DataVersionDeneb, - Deneb: RandomAttestation(), + Deneb: RandomPhase0Attestation(), + }, + } +} + +func RandomElectraCoreVersionedAttestation() core.VersionedAttestation { + return core.VersionedAttestation{ + VersionedAttestation: eth2spec.VersionedAttestation{ + Version: eth2spec.DataVersionElectra, + Electra: RandomElectraAttestation(), }, } } @@ -137,7 +157,14 @@ func RandomDenebCoreVersionedAttestation() core.VersionedAttestation { func RandomDenebVersionedAttestation() *eth2spec.VersionedAttestation { return ð2spec.VersionedAttestation{ Version: eth2spec.DataVersionDeneb, - Deneb: RandomAttestation(), + Deneb: RandomPhase0Attestation(), + } +} + +func RandomElectraVersionedAttestation() *eth2spec.VersionedAttestation { + return ð2spec.VersionedAttestation{ + Version: eth2spec.DataVersionElectra, + Electra: RandomElectraAttestation(), } } @@ -197,7 +224,7 @@ func RandomPhase0BeaconBlockBody() *eth2p0.BeaconBlockBody { Graffiti: RandomArray32(), ProposerSlashings: []*eth2p0.ProposerSlashing{}, AttesterSlashings: []*eth2p0.AttesterSlashing{}, - Attestations: []*eth2p0.Attestation{RandomAttestation(), RandomAttestation()}, + Attestations: []*eth2p0.Attestation{RandomPhase0Attestation(), RandomPhase0Attestation()}, Deposits: []*eth2p0.Deposit{}, VoluntaryExits: []*eth2p0.SignedVoluntaryExit{}, } @@ -224,7 +251,7 @@ func RandomAltairBeaconBlockBody() *altair.BeaconBlockBody { Graffiti: RandomArray32(), ProposerSlashings: []*eth2p0.ProposerSlashing{}, AttesterSlashings: []*eth2p0.AttesterSlashing{}, - Attestations: []*eth2p0.Attestation{RandomAttestation(), RandomAttestation()}, + Attestations: []*eth2p0.Attestation{RandomPhase0Attestation(), RandomPhase0Attestation()}, Deposits: []*eth2p0.Deposit{}, VoluntaryExits: []*eth2p0.SignedVoluntaryExit{}, SyncAggregate: RandomSyncAggregate(), @@ -259,7 +286,7 @@ func RandomBellatrixBeaconBlockBody() *bellatrix.BeaconBlockBody { Graffiti: RandomArray32(), ProposerSlashings: []*eth2p0.ProposerSlashing{}, AttesterSlashings: []*eth2p0.AttesterSlashing{}, - Attestations: []*eth2p0.Attestation{RandomAttestation(), RandomAttestation()}, + Attestations: []*eth2p0.Attestation{RandomPhase0Attestation(), RandomPhase0Attestation()}, Deposits: []*eth2p0.Deposit{}, VoluntaryExits: []*eth2p0.SignedVoluntaryExit{}, SyncAggregate: RandomSyncAggregate(), @@ -285,7 +312,7 @@ func RandomCapellaBeaconBlockBody() *capella.BeaconBlockBody { Graffiti: RandomArray32(), ProposerSlashings: []*eth2p0.ProposerSlashing{}, AttesterSlashings: []*eth2p0.AttesterSlashing{}, - Attestations: []*eth2p0.Attestation{RandomAttestation(), RandomAttestation()}, + Attestations: []*eth2p0.Attestation{RandomPhase0Attestation(), RandomPhase0Attestation()}, Deposits: []*eth2p0.Deposit{}, VoluntaryExits: []*eth2p0.SignedVoluntaryExit{}, SyncAggregate: RandomSyncAggregate(), @@ -366,6 +393,22 @@ func RandomDenebCoreVersionedSignedProposal() core.VersionedSignedProposal { } } +func RandomElectraCoreVersionedSignedProposal() core.VersionedSignedProposal { + return core.VersionedSignedProposal{ + VersionedSignedProposal: eth2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionElectra, + Electra: ð2electra.SignedBlockContents{ + SignedBlock: &electra.SignedBeaconBlock{ + Message: RandomElectraBeaconBlock(), + Signature: RandomEth2Signature(), + }, + KZGProofs: []deneb.KZGProof{}, + Blobs: []deneb.Blob{}, + }, + }, + } +} + // RandomCapellaVersionedSignedBeaconBlock returns a random signed capella beacon block. func RandomCapellaVersionedSignedBeaconBlock() *eth2spec.VersionedSignedBeaconBlock { return ð2spec.VersionedSignedBeaconBlock{ @@ -388,6 +431,17 @@ func RandomDenebVersionedSignedBeaconBlock() *eth2spec.VersionedSignedBeaconBloc } } +// RandomElectraVersionedSignedBeaconBlock returns a random signed electra beacon block. +func RandomElectraVersionedSignedBeaconBlock() *eth2spec.VersionedSignedBeaconBlock { + return ð2spec.VersionedSignedBeaconBlock{ + Version: eth2spec.DataVersionElectra, + Electra: &electra.SignedBeaconBlock{ + Message: RandomElectraBeaconBlock(), + Signature: RandomEth2Signature(), + }, + } +} + // RandomCapellaVersionedSignedProposal returns a random versioned signed proposal containing capella beacon block. func RandomCapellaVersionedSignedProposal() *eth2api.VersionedSignedProposal { return ð2api.VersionedSignedProposal{ @@ -414,6 +468,21 @@ func RandomDenebVersionedSignedProposal() *eth2api.VersionedSignedProposal { } } +// RandomElectraVersionedSignedProposal returns a random versioned signed proposal containing electra beacon block. +func RandomElectraVersionedSignedProposal() *eth2api.VersionedSignedProposal { + return ð2api.VersionedSignedProposal{ + Version: eth2spec.DataVersionElectra, + Electra: ð2electra.SignedBlockContents{ + SignedBlock: &electra.SignedBeaconBlock{ + Message: RandomElectraBeaconBlock(), + Signature: RandomEth2Signature(), + }, + KZGProofs: []deneb.KZGProof{}, + Blobs: []deneb.Blob{}, + }, + } +} + // RandomCapellaVersionedProposal returns a random versioned proposal containing capella beacon block. func RandomCapellaVersionedProposal() *eth2api.VersionedProposal { return ð2api.VersionedProposal{ @@ -434,6 +503,18 @@ func RandomDenebVersionedProposal() *eth2api.VersionedProposal { } } +// RandomElectraVersionedProposal returns a random versioned proposal containing electra beacon block. +func RandomElectraVersionedProposal() *eth2api.VersionedProposal { + return ð2api.VersionedProposal{ + Version: eth2spec.DataVersionElectra, + Electra: ð2electra.BlockContents{ + Block: RandomElectraBeaconBlock(), + KZGProofs: []deneb.KZGProof{}, + Blobs: []deneb.Blob{}, + }, + } +} + func RandomBellatrixBlindedBeaconBlock() *eth2bellatrix.BlindedBeaconBlock { return ð2bellatrix.BlindedBeaconBlock{ Slot: RandomSlot(), @@ -455,7 +536,7 @@ func RandomBellatrixBlindedBeaconBlockBody() *eth2bellatrix.BlindedBeaconBlockBo Graffiti: RandomArray32(), ProposerSlashings: []*eth2p0.ProposerSlashing{}, AttesterSlashings: []*eth2p0.AttesterSlashing{}, - Attestations: []*eth2p0.Attestation{RandomAttestation(), RandomAttestation()}, + Attestations: []*eth2p0.Attestation{RandomPhase0Attestation(), RandomPhase0Attestation()}, Deposits: []*eth2p0.Deposit{}, VoluntaryExits: []*eth2p0.SignedVoluntaryExit{}, SyncAggregate: RandomSyncAggregate(), @@ -484,7 +565,7 @@ func RandomCapellaBlindedBeaconBlockBody() *eth2capella.BlindedBeaconBlockBody { Graffiti: RandomArray32(), ProposerSlashings: []*eth2p0.ProposerSlashing{}, AttesterSlashings: []*eth2p0.AttesterSlashing{}, - Attestations: []*eth2p0.Attestation{RandomAttestation(), RandomAttestation()}, + Attestations: []*eth2p0.Attestation{RandomPhase0Attestation(), RandomPhase0Attestation()}, Deposits: []*eth2p0.Deposit{}, VoluntaryExits: []*eth2p0.SignedVoluntaryExit{}, SyncAggregate: RandomSyncAggregate(), @@ -552,6 +633,19 @@ func RandomDenebVersionedSignedBlindedProposal() core.VersionedSignedProposal { } } +func RandomElectraVersionedSignedBlindedProposal() core.VersionedSignedProposal { + return core.VersionedSignedProposal{ + VersionedSignedProposal: eth2api.VersionedSignedProposal{ + Blinded: true, + Version: eth2spec.DataVersionElectra, + ElectraBlinded: ð2electra.SignedBlindedBeaconBlock{ + Message: RandomElectraBlindedBeaconBlock(), + Signature: RandomEth2Signature(), + }, + }, + } +} + func RandomDenebBeaconBlock() *deneb.BeaconBlock { return &deneb.BeaconBlock{ Slot: RandomSlot(), @@ -562,6 +656,16 @@ func RandomDenebBeaconBlock() *deneb.BeaconBlock { } } +func RandomElectraBeaconBlock() *electra.BeaconBlock { + return &electra.BeaconBlock{ + Slot: RandomSlot(), + ProposerIndex: RandomVIdx(), + ParentRoot: RandomRoot(), + StateRoot: RandomRoot(), + Body: RandomElectraBeaconBlockBody(), + } +} + func RandomDenebBlindedBeaconBlock() *eth2deneb.BlindedBeaconBlock { return ð2deneb.BlindedBeaconBlock{ Slot: RandomSlot(), @@ -572,6 +676,16 @@ func RandomDenebBlindedBeaconBlock() *eth2deneb.BlindedBeaconBlock { } } +func RandomElectraBlindedBeaconBlock() *eth2electra.BlindedBeaconBlock { + return ð2electra.BlindedBeaconBlock{ + Slot: RandomSlot(), + ProposerIndex: RandomVIdx(), + ParentRoot: RandomRoot(), + StateRoot: RandomRoot(), + Body: RandomElectraBlindedBeaconBlockBody(), + } +} + func RandomDenebBeaconBlockBody() *deneb.BeaconBlockBody { return &deneb.BeaconBlockBody{ RANDAOReveal: RandomEth2Signature(), @@ -583,7 +697,7 @@ func RandomDenebBeaconBlockBody() *deneb.BeaconBlockBody { Graffiti: RandomArray32(), ProposerSlashings: []*eth2p0.ProposerSlashing{}, AttesterSlashings: []*eth2p0.AttesterSlashing{}, - Attestations: []*eth2p0.Attestation{RandomAttestation(), RandomAttestation()}, + Attestations: []*eth2p0.Attestation{RandomPhase0Attestation(), RandomPhase0Attestation()}, Deposits: []*eth2p0.Deposit{}, VoluntaryExits: []*eth2p0.SignedVoluntaryExit{}, SyncAggregate: RandomSyncAggregate(), @@ -593,6 +707,32 @@ func RandomDenebBeaconBlockBody() *deneb.BeaconBlockBody { } } +func RandomElectraBeaconBlockBody() *electra.BeaconBlockBody { + return &electra.BeaconBlockBody{ + RANDAOReveal: RandomEth2Signature(), + ETH1Data: ð2p0.ETH1Data{ + DepositRoot: RandomRoot(), + DepositCount: 0, + BlockHash: RandomBytes32(), + }, + Graffiti: RandomArray32(), + ProposerSlashings: []*eth2p0.ProposerSlashing{}, + AttesterSlashings: []*electra.AttesterSlashing{}, + Attestations: []*electra.Attestation{RandomElectraAttestation(), RandomElectraAttestation()}, + Deposits: []*eth2p0.Deposit{}, + VoluntaryExits: []*eth2p0.SignedVoluntaryExit{}, + SyncAggregate: RandomSyncAggregate(), + ExecutionPayload: RandomDenebExecutionPayload(), + BLSToExecutionChanges: []*capella.SignedBLSToExecutionChange{}, + BlobKZGCommitments: []deneb.KZGCommitment{}, + ExecutionRequests: &electra.ExecutionRequests{ + Deposits: []*electra.DepositRequest{}, + Withdrawals: []*electra.WithdrawalRequest{}, + Consolidations: []*electra.ConsolidationRequest{}, + }, + } +} + func RandomDenebBlindedBeaconBlockBody() *eth2deneb.BlindedBeaconBlockBody { return ð2deneb.BlindedBeaconBlockBody{ RANDAOReveal: RandomEth2Signature(), @@ -604,13 +744,39 @@ func RandomDenebBlindedBeaconBlockBody() *eth2deneb.BlindedBeaconBlockBody { Graffiti: RandomArray32(), ProposerSlashings: []*eth2p0.ProposerSlashing{}, AttesterSlashings: []*eth2p0.AttesterSlashing{}, - Attestations: []*eth2p0.Attestation{RandomAttestation(), RandomAttestation()}, + Attestations: []*eth2p0.Attestation{RandomPhase0Attestation(), RandomPhase0Attestation()}, + Deposits: []*eth2p0.Deposit{}, + VoluntaryExits: []*eth2p0.SignedVoluntaryExit{}, + SyncAggregate: RandomSyncAggregate(), + ExecutionPayloadHeader: RandomDenebExecutionPayloadHeader(), + BLSToExecutionChanges: []*capella.SignedBLSToExecutionChange{}, + BlobKZGCommitments: []deneb.KZGCommitment{}, + } +} + +func RandomElectraBlindedBeaconBlockBody() *eth2electra.BlindedBeaconBlockBody { + return ð2electra.BlindedBeaconBlockBody{ + RANDAOReveal: RandomEth2Signature(), + ETH1Data: ð2p0.ETH1Data{ + DepositRoot: RandomRoot(), + DepositCount: 0, + BlockHash: RandomBytes32(), + }, + Graffiti: RandomArray32(), + ProposerSlashings: []*eth2p0.ProposerSlashing{}, + AttesterSlashings: []*electra.AttesterSlashing{}, + Attestations: []*electra.Attestation{RandomElectraAttestation(), RandomElectraAttestation()}, Deposits: []*eth2p0.Deposit{}, VoluntaryExits: []*eth2p0.SignedVoluntaryExit{}, SyncAggregate: RandomSyncAggregate(), ExecutionPayloadHeader: RandomDenebExecutionPayloadHeader(), BLSToExecutionChanges: []*capella.SignedBLSToExecutionChange{}, BlobKZGCommitments: []deneb.KZGCommitment{}, + ExecutionRequests: &electra.ExecutionRequests{ + Deposits: []*electra.DepositRequest{}, + Withdrawals: []*electra.WithdrawalRequest{}, + Consolidations: []*electra.ConsolidationRequest{}, + }, } } @@ -720,7 +886,7 @@ func RandomSyncCommitteeContribution() *altair.SyncCommitteeContribution { Slot: RandomSlot(), BeaconBlockRoot: RandomRoot(), SubcommitteeIndex: rand.Uint64(), - AggregationBits: RandomBitVec(), + AggregationBits: RandomBitVec128(), Signature: RandomEth2Signature(), } } @@ -1110,7 +1276,7 @@ func RandomBitList(length int) bitfield.Bitlist { return resp } -func RandomBitVec() bitfield.Bitvector128 { +func RandomBitVec128() bitfield.Bitvector128 { size := 128 index := rand.Intn(size) resp := bitfield.NewBitvector128() @@ -1128,6 +1294,15 @@ func RandomBitVec4() bitfield.Bitvector4 { return resp } +func RandomBitVec64() bitfield.Bitvector64 { + size := 64 + index := rand.Intn(size) + resp := bitfield.NewBitvector64() + resp.SetBitAt(uint64(index), true) + + return resp +} + // RandomSecp256k1Signature returns a random byte slice of length 65 with the last byte set to 0, 1, 27 or 28. func RandomSecp256k1Signature() []byte { return RandomSecp256k1SignatureSeed(NewSeedRand()) diff --git a/testutil/validatormock/propose_test.go b/testutil/validatormock/propose_test.go index 0f30e578b..084994382 100644 --- a/testutil/validatormock/propose_test.go +++ b/testutil/validatormock/propose_test.go @@ -4,6 +4,8 @@ package validatormock_test import ( "context" + "fmt" + "math/big" "net/http" "net/http/httptest" "sort" @@ -123,49 +125,196 @@ func TestAttest(t *testing.T) { func TestProposeBlock(t *testing.T) { ctx := context.Background() - // Configure beacon mock - valSet := beaconmock.ValidatorSetA - beaconMock, err := beaconmock.New( - beaconmock.WithValidatorSet(valSet), - beaconmock.WithDeterministicProposerDuties(0), - ) - require.NoError(t, err) + tests := []struct { + version string + jsonBeaconBlock func(slotsPerEpoch uint64) []byte + beaconMockProposalFunc func(context.Context, *eth2api.ProposalOpts) (*eth2api.VersionedProposal, error) + }{ + { + version: "capella", + jsonBeaconBlock: func(slotsPerEpoch uint64) []byte { + block := testutil.RandomCapellaBeaconBlock() + block.Slot = eth2p0.Slot(slotsPerEpoch) + blockJSON, err := block.MarshalJSON() + require.NoError(t, err) + + return blockJSON + }, + beaconMockProposalFunc: func(_ context.Context, opts *eth2api.ProposalOpts) (*eth2api.VersionedProposal, error) { + block := testutil.RandomCapellaVersionedProposal() + block.Capella.Slot = opts.Slot + block.Capella.Body.RANDAOReveal = opts.RandaoReveal + block.Capella.Body.Graffiti = opts.Graffiti + block.ExecutionValue = big.NewInt(1) + block.ConsensusValue = big.NewInt(1) + + return block, nil + }, + }, + { + version: "deneb", + jsonBeaconBlock: func(slotsPerEpoch uint64) []byte { + block := testutil.RandomDenebBeaconBlock() + block.Slot = eth2p0.Slot(slotsPerEpoch) + blockJSON, err := block.MarshalJSON() + require.NoError(t, err) + + return blockJSON + }, + beaconMockProposalFunc: func(_ context.Context, opts *eth2api.ProposalOpts) (*eth2api.VersionedProposal, error) { + block := testutil.RandomDenebVersionedProposal() + block.Deneb.Block.Slot = opts.Slot + block.Deneb.Block.Body.RANDAOReveal = opts.RandaoReveal + block.Deneb.Block.Body.Graffiti = opts.Graffiti + block.ExecutionValue = big.NewInt(1) + block.ConsensusValue = big.NewInt(1) + + return block, nil + }, + }, + { + version: "electra", + jsonBeaconBlock: func(slotsPerEpoch uint64) []byte { + block := testutil.RandomElectraBeaconBlock() + block.Slot = eth2p0.Slot(slotsPerEpoch) + blockJSON, err := block.MarshalJSON() + require.NoError(t, err) + + return blockJSON + }, + beaconMockProposalFunc: func(_ context.Context, opts *eth2api.ProposalOpts) (*eth2api.VersionedProposal, error) { + block := testutil.RandomElectraVersionedProposal() + block.Electra.Block.Slot = opts.Slot + block.Electra.Block.Body.RANDAOReveal = opts.RandaoReveal + block.Electra.Block.Body.Graffiti = opts.Graffiti + block.ExecutionValue = big.NewInt(1) + block.ConsensusValue = big.NewInt(1) + + return block, nil + }, + }, + { + version: "capella blinded", + jsonBeaconBlock: func(slotsPerEpoch uint64) []byte { + block := testutil.RandomCapellaBlindedBeaconBlock() + block.Slot = eth2p0.Slot(slotsPerEpoch) + blockJSON, err := block.MarshalJSON() + require.NoError(t, err) + + return blockJSON + }, + beaconMockProposalFunc: func(_ context.Context, opts *eth2api.ProposalOpts) (*eth2api.VersionedProposal, error) { + block := ð2api.VersionedProposal{ + Version: eth2spec.DataVersionCapella, + CapellaBlinded: testutil.RandomCapellaBlindedBeaconBlock(), + } + block.CapellaBlinded.Slot = opts.Slot + block.CapellaBlinded.Body.RANDAOReveal = opts.RandaoReveal + block.CapellaBlinded.Body.Graffiti = opts.Graffiti + block.ExecutionValue = big.NewInt(1) + block.ConsensusValue = big.NewInt(1) + block.Blinded = true + + return block, nil + }, + }, + { + version: "deneb blinded", + jsonBeaconBlock: func(slotsPerEpoch uint64) []byte { + block := testutil.RandomDenebBeaconBlock() + block.Slot = eth2p0.Slot(slotsPerEpoch) + blockJSON, err := block.MarshalJSON() + require.NoError(t, err) + + return blockJSON + }, + beaconMockProposalFunc: func(_ context.Context, opts *eth2api.ProposalOpts) (*eth2api.VersionedProposal, error) { + block := ð2api.VersionedProposal{ + Version: eth2spec.DataVersionDeneb, + DenebBlinded: testutil.RandomDenebBlindedBeaconBlock(), + } + block.DenebBlinded.Slot = opts.Slot + block.DenebBlinded.Body.RANDAOReveal = opts.RandaoReveal + block.DenebBlinded.Body.Graffiti = opts.Graffiti + block.ExecutionValue = big.NewInt(1) + block.ConsensusValue = big.NewInt(1) + block.Blinded = true + + return block, nil + }, + }, + { + version: "electra blinded", + jsonBeaconBlock: func(slotsPerEpoch uint64) []byte { + block := testutil.RandomElectraBeaconBlock() + block.Slot = eth2p0.Slot(slotsPerEpoch) + blockJSON, err := block.MarshalJSON() + require.NoError(t, err) + + return blockJSON + }, + beaconMockProposalFunc: func(_ context.Context, opts *eth2api.ProposalOpts) (*eth2api.VersionedProposal, error) { + block := ð2api.VersionedProposal{ + Version: eth2spec.DataVersionElectra, + ElectraBlinded: testutil.RandomElectraBlindedBeaconBlock(), + } + block.ElectraBlinded.Slot = opts.Slot + block.ElectraBlinded.Body.RANDAOReveal = opts.RandaoReveal + block.ElectraBlinded.Body.Graffiti = opts.Graffiti + block.ExecutionValue = big.NewInt(1) + block.ConsensusValue = big.NewInt(1) + block.Blinded = true + + return block, nil + }, + }, + } - // Signature stub function - signFunc := func(key eth2p0.BLSPubKey, _ []byte) (eth2p0.BLSSignature, error) { - var sig eth2p0.BLSSignature - copy(sig[:], key[:]) + for _, test := range tests { + t.Run(test.version, func(t *testing.T) { + // Configure beacon mock + valSet := beaconmock.ValidatorSetA + beaconMock, err := beaconmock.New( + beaconmock.WithValidatorSet(valSet), + beaconmock.WithDeterministicProposerDuties(0), + ) + require.NoError(t, err) - return sig, nil - } + beaconMock.ProposalFunc = test.beaconMockProposalFunc - slotsPerEpoch, err := beaconMock.SlotsPerEpoch(ctx) - require.NoError(t, err) + // Signature stub function + signFunc := func(key eth2p0.BLSPubKey, _ []byte) (eth2p0.BLSSignature, error) { + var sig eth2p0.BLSSignature + copy(sig[:], key[:]) - block := testutil.RandomPhase0BeaconBlock() - block.Slot = eth2p0.Slot(slotsPerEpoch) + return sig, nil + } - mockVAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - testResponse := []byte(`{"version":"phase0","data":`) - blockJSON, err := block.MarshalJSON() - require.NoError(t, err) + slotsPerEpoch, err := beaconMock.SlotsPerEpoch(ctx) + require.NoError(t, err) - testResponse = append(testResponse, blockJSON...) - testResponse = append(testResponse, []byte(`}`)...) - require.NoError(t, err) + mockVAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + testResponse := []byte(fmt.Sprintf(`{"version":"%v","data":`, test.version)) + blockJSON := test.jsonBeaconBlock(slotsPerEpoch) - _, _ = w.Write(testResponse) - })) - defer mockVAPI.Close() + testResponse = append(testResponse, blockJSON...) + testResponse = append(testResponse, []byte(`}`)...) + require.NoError(t, err) - provider := addrWrap{ - Client: beaconMock, - addr: mockVAPI.URL, - } + _, _ = w.Write(testResponse) + })) + defer mockVAPI.Close() - // Call propose block function - err = validatormock.ProposeBlock(ctx, provider, signFunc, eth2p0.Slot(slotsPerEpoch)) - require.NoError(t, err) + provider := addrWrap{ + Client: beaconMock, + addr: mockVAPI.URL, + } + + // Call propose block function + err = validatormock.ProposeBlock(ctx, provider, signFunc, eth2p0.Slot(slotsPerEpoch)) + require.NoError(t, err) + }) + } } func TestProposeBlindedBlock(t *testing.T) {