diff --git a/beacon-chain/rpc/endpoints.go b/beacon-chain/rpc/endpoints.go index 1607ef5919..ac34b18209 100644 --- a/beacon-chain/rpc/endpoints.go +++ b/beacon-chain/rpc/endpoints.go @@ -245,7 +245,7 @@ func (s *Service) validatorEndpoints( template: "/eth/v2/validator/aggregate_attestation", name: namespace + ".GetAggregateAttestationV2", middleware: []middleware.Middleware{ - middleware.AcceptHeaderHandler([]string{api.JsonMediaType}), + middleware.AcceptHeaderHandler([]string{api.JsonMediaType, api.OctetStreamMediaType}), }, handler: server.GetAggregateAttestationV2, methods: []string{http.MethodGet}, @@ -314,7 +314,7 @@ func (s *Service) validatorEndpoints( template: "/eth/v1/validator/attestation_data", name: namespace + ".GetAttestationData", middleware: []middleware.Middleware{ - middleware.AcceptHeaderHandler([]string{api.JsonMediaType}), + middleware.AcceptHeaderHandler([]string{api.JsonMediaType, api.OctetStreamMediaType}), }, handler: server.GetAttestationData, methods: []string{http.MethodGet}, diff --git a/beacon-chain/rpc/eth/validator/handlers.go b/beacon-chain/rpc/eth/validator/handlers.go index 9cd05a4564..ede89f7dd9 100644 --- a/beacon-chain/rpc/eth/validator/handlers.go +++ b/beacon-chain/rpc/eth/validator/handlers.go @@ -98,6 +98,39 @@ func (s *Server) GetAggregateAttestationV2(w http.ResponseWriter, r *http.Reques if agg == nil { return } + + if httputil.RespondWithSsz(r) { + var data []byte + var err error + if v >= version.Electra { + typedAgg, ok := agg.(*ethpbalpha.AttestationElectra) + if !ok { + httputil.HandleError(w, fmt.Sprintf("Attestation is not of type %T", ðpbalpha.AttestationElectra{}), http.StatusInternalServerError) + return + } + data, err = typedAgg.MarshalSSZ() + if err != nil { + httputil.HandleError(w, "Could not marshal attestation: "+err.Error(), http.StatusInternalServerError) + return + } + } else { + typedAgg, ok := agg.(*ethpbalpha.Attestation) + if !ok { + httputil.HandleError(w, fmt.Sprintf("Attestation is not of type %T", ðpbalpha.Attestation{}), http.StatusInternalServerError) + return + } + data, err = typedAgg.MarshalSSZ() + if err != nil { + httputil.HandleError(w, "Could not marshal attestation: "+err.Error(), http.StatusInternalServerError) + return + } + } + + w.Header().Set(api.VersionHeader, version.String(v)) + httputil.WriteSsz(w, data) + return + } + resp := &structs.AggregateAttestationResponse{ Version: version.String(v), } @@ -610,6 +643,16 @@ func (s *Server) GetAttestationData(w http.ResponseWriter, r *http.Request) { return } + if httputil.RespondWithSsz(r) { + data, err := attestationData.MarshalSSZ() + if err != nil { + httputil.HandleError(w, "Could not marshal attestation data: "+err.Error(), http.StatusInternalServerError) + return + } + httputil.WriteSsz(w, data) + return + } + response := &structs.GetAttestationDataResponse{ Data: &structs.AttestationData{ Slot: strconv.FormatUint(uint64(attestationData.Slot), 10), diff --git a/beacon-chain/rpc/eth/validator/handlers_test.go b/beacon-chain/rpc/eth/validator/handlers_test.go index 3d39152980..1d3f9eea04 100644 --- a/beacon-chain/rpc/eth/validator/handlers_test.go +++ b/beacon-chain/rpc/eth/validator/handlers_test.go @@ -307,6 +307,23 @@ func TestGetAggregateAttestation(t *testing.T) { compareResult(t, attestation, "2", hexutil.Encode(aggSlot2.AggregationBits), root1, sig.Marshal()) }) + t.Run("1 matching aggregated attestation - SSZ", func(t *testing.T) { + reqRoot, err := aggSlot2.Data.HashTreeRoot() + require.NoError(t, err, "Failed to generate attestation data hash tree root") + attDataRoot := hexutil.Encode(reqRoot[:]) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=2" + "&committee_index=0" + request := httptest.NewRequest(http.MethodGet, url, nil) + request.Header.Add("Accept", "application/octet-stream") + writer := httptest.NewRecorder() + + s.GetAggregateAttestationV2(writer, request) + require.Equal(t, http.StatusOK, writer.Code, "Expected HTTP status OK") + + var resp ethpbalpha.Attestation + require.NoError(t, resp.UnmarshalSSZ(writer.Body.Bytes())) + + compareResult(t, *structs.AttFromConsensus(&resp), "2", hexutil.Encode(aggSlot2.AggregationBits), root1, sig.Marshal()) + }) t.Run("multiple matching aggregated attestations - return the one with most bits", func(t *testing.T) { reqRoot, err := aggSlot1_Root1_1.Data.HashTreeRoot() require.NoError(t, err, "Failed to generate attestation data hash tree root") @@ -327,6 +344,23 @@ func TestGetAggregateAttestation(t *testing.T) { compareResult(t, attestation, "1", hexutil.Encode(aggSlot1_Root1_2.AggregationBits), root1, sig.Marshal()) }) + t.Run("multiple matching aggregated attestations - return the one with most bits - SSZ", func(t *testing.T) { + reqRoot, err := aggSlot1_Root1_1.Data.HashTreeRoot() + require.NoError(t, err, "Failed to generate attestation data hash tree root") + attDataRoot := hexutil.Encode(reqRoot[:]) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=1" + "&committee_index=0" + request := httptest.NewRequest(http.MethodGet, url, nil) + request.Header.Add("Accept", "application/octet-stream") + writer := httptest.NewRecorder() + + s.GetAggregateAttestationV2(writer, request) + require.Equal(t, http.StatusOK, writer.Code, "Expected HTTP status OK") + + var resp ethpbalpha.Attestation + require.NoError(t, resp.UnmarshalSSZ(writer.Body.Bytes())) + + compareResult(t, *structs.AttFromConsensus(&resp), "1", hexutil.Encode(aggSlot1_Root1_2.AggregationBits), root1, sig.Marshal()) + }) }) t.Run("post-electra", func(t *testing.T) { aggSlot1_Root1_1 := createAttestationElectra(1, bitfield.Bitlist{0b11100}, root1) @@ -421,6 +455,23 @@ func TestGetAggregateAttestation(t *testing.T) { compareResult(t, attestation, "2", hexutil.Encode(aggSlot2.AggregationBits), root1, sig.Marshal(), hexutil.Encode(aggSlot2.CommitteeBits)) }) + t.Run("1 matching aggregated attestation - SSZ", func(t *testing.T) { + reqRoot, err := aggSlot2.Data.HashTreeRoot() + require.NoError(t, err, "Failed to generate attestation data hash tree root") + attDataRoot := hexutil.Encode(reqRoot[:]) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=2" + "&committee_index=0" + request := httptest.NewRequest(http.MethodGet, url, nil) + request.Header.Add("Accept", "application/octet-stream") + writer := httptest.NewRecorder() + + s.GetAggregateAttestationV2(writer, request) + require.Equal(t, http.StatusOK, writer.Code, "Expected HTTP status OK") + + var resp ethpbalpha.AttestationElectra + require.NoError(t, resp.UnmarshalSSZ(writer.Body.Bytes())) + + compareResult(t, *structs.AttElectraFromConsensus(&resp), "2", hexutil.Encode(aggSlot2.AggregationBits), root1, sig.Marshal(), hexutil.Encode(aggSlot2.CommitteeBits)) + }) t.Run("multiple matching aggregated attestations - return the one with most bits", func(t *testing.T) { reqRoot, err := aggSlot1_Root1_1.Data.HashTreeRoot() require.NoError(t, err, "Failed to generate attestation data hash tree root") @@ -441,6 +492,23 @@ func TestGetAggregateAttestation(t *testing.T) { compareResult(t, attestation, "1", hexutil.Encode(aggSlot1_Root1_2.AggregationBits), root1, sig.Marshal(), hexutil.Encode(aggSlot1_Root1_1.CommitteeBits)) }) + t.Run("multiple matching aggregated attestations - return the one with most bits - SSZ", func(t *testing.T) { + reqRoot, err := aggSlot1_Root1_1.Data.HashTreeRoot() + require.NoError(t, err, "Failed to generate attestation data hash tree root") + attDataRoot := hexutil.Encode(reqRoot[:]) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=1" + "&committee_index=0" + request := httptest.NewRequest(http.MethodGet, url, nil) + request.Header.Add("Accept", "application/octet-stream") + writer := httptest.NewRecorder() + + s.GetAggregateAttestationV2(writer, request) + require.Equal(t, http.StatusOK, writer.Code, "Expected HTTP status OK") + + var resp ethpbalpha.AttestationElectra + require.NoError(t, resp.UnmarshalSSZ(writer.Body.Bytes())) + + compareResult(t, *structs.AttElectraFromConsensus(&resp), "1", hexutil.Encode(aggSlot1_Root1_2.AggregationBits), root1, sig.Marshal(), hexutil.Encode(aggSlot1_Root1_1.CommitteeBits)) + }) t.Run("1 matching unaggregated attestation", func(t *testing.T) { reqRoot, err := unaggSlot4.Data.HashTreeRoot() require.NoError(t, err, "Failed to generate attestation data hash tree root") @@ -460,6 +528,23 @@ func TestGetAggregateAttestation(t *testing.T) { require.NoError(t, json.Unmarshal(resp.Data, &attestation), "Failed to unmarshal attestation data") compareResult(t, attestation, "4", hexutil.Encode(unaggSlot4.AggregationBits), root1, sig.Marshal(), hexutil.Encode(unaggSlot4.CommitteeBits)) }) + t.Run("1 matching unaggregated attestation - SSZ", func(t *testing.T) { + reqRoot, err := unaggSlot4.Data.HashTreeRoot() + require.NoError(t, err, "Failed to generate attestation data hash tree root") + attDataRoot := hexutil.Encode(reqRoot[:]) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=4" + "&committee_index=0" + request := httptest.NewRequest(http.MethodGet, url, nil) + request.Header.Add("Accept", "application/octet-stream") + writer := httptest.NewRecorder() + + s.GetAggregateAttestationV2(writer, request) + require.Equal(t, http.StatusOK, writer.Code, "Expected HTTP status OK") + + var resp ethpbalpha.AttestationElectra + require.NoError(t, resp.UnmarshalSSZ(writer.Body.Bytes())) + + compareResult(t, *structs.AttElectraFromConsensus(&resp), "4", hexutil.Encode(unaggSlot4.AggregationBits), root1, sig.Marshal(), hexutil.Encode(unaggSlot4.CommitteeBits)) + }) t.Run("multiple matching unaggregated attestations - their aggregate is returned", func(t *testing.T) { reqRoot, err := unaggSlot3_Root1_1.Data.HashTreeRoot() require.NoError(t, err, "Failed to generate attestation data hash tree root") @@ -484,12 +569,33 @@ func TestGetAggregateAttestation(t *testing.T) { expectedSig := bls.AggregateSignatures([]common.Signature{sig1, sig2}) compareResult(t, attestation, "3", hexutil.Encode(bitfield.Bitlist{0b11100}), root1, expectedSig.Marshal(), hexutil.Encode(unaggSlot3_Root1_1.CommitteeBits)) }) + t.Run("multiple matching unaggregated attestations - their aggregate is returned - SSZ", func(t *testing.T) { + reqRoot, err := unaggSlot3_Root1_1.Data.HashTreeRoot() + require.NoError(t, err, "Failed to generate attestation data hash tree root") + attDataRoot := hexutil.Encode(reqRoot[:]) + url := "http://example.com?attestation_data_root=" + attDataRoot + "&slot=3" + "&committee_index=0" + request := httptest.NewRequest(http.MethodGet, url, nil) + request.Header.Add("Accept", "application/octet-stream") + writer := httptest.NewRecorder() + + s.GetAggregateAttestationV2(writer, request) + require.Equal(t, http.StatusOK, writer.Code, "Expected HTTP status OK") + + var resp ethpbalpha.AttestationElectra + require.NoError(t, resp.UnmarshalSSZ(writer.Body.Bytes())) + + sig1, err := bls.SignatureFromBytes(unaggSlot3_Root1_1.Signature) + require.NoError(t, err) + sig2, err := bls.SignatureFromBytes(unaggSlot3_Root1_2.Signature) + require.NoError(t, err) + expectedSig := bls.AggregateSignatures([]common.Signature{sig1, sig2}) + compareResult(t, *structs.AttElectraFromConsensus(&resp), "3", hexutil.Encode(bitfield.Bitlist{0b11100}), root1, expectedSig.Marshal(), hexutil.Encode(unaggSlot3_Root1_1.CommitteeBits)) + }) t.Run("pre-electra attestation is ignored", func(t *testing.T) { }) }) }) - } func createAttestationData(slot primitives.Slot, committeeIndex primitives.CommitteeIndex, root []byte) *ethpbalpha.AttestationData { @@ -1293,6 +1399,81 @@ func TestGetAttestationData(t *testing.T) { assert.DeepEqual(t, expectedResponse, resp) }) + t.Run("ok SSZ", func(t *testing.T) { + block := util.NewBeaconBlock() + block.Block.Slot = 3*params.BeaconConfig().SlotsPerEpoch + 1 + targetBlock := util.NewBeaconBlock() + targetBlock.Block.Slot = 1 * params.BeaconConfig().SlotsPerEpoch + justifiedBlock := util.NewBeaconBlock() + justifiedBlock.Block.Slot = 2 * params.BeaconConfig().SlotsPerEpoch + blockRoot, err := block.Block.HashTreeRoot() + require.NoError(t, err, "Could not hash beacon block") + justifiedRoot, err := justifiedBlock.Block.HashTreeRoot() + require.NoError(t, err, "Could not get signing root for justified block") + slot := 3*params.BeaconConfig().SlotsPerEpoch + 1 + beaconState, err := util.NewBeaconState() + require.NoError(t, err) + require.NoError(t, beaconState.SetSlot(slot)) + justifiedCheckpoint := ðpbalpha.Checkpoint{ + Epoch: 2, + Root: justifiedRoot[:], + } + require.NoError(t, beaconState.SetCurrentJustifiedCheckpoint(justifiedCheckpoint)) + offset := int64(slot.Mul(params.BeaconConfig().SecondsPerSlot)) + chain := &mockChain.ChainService{ + Optimistic: false, + Genesis: time.Now().Add(time.Duration(-1*offset) * time.Second), + Root: blockRoot[:], + CurrentJustifiedCheckPoint: justifiedCheckpoint, + TargetRoot: blockRoot, + State: beaconState, + } + + s := &Server{ + SyncChecker: &mockSync.Sync{IsSyncing: false}, + HeadFetcher: chain, + TimeFetcher: chain, + OptimisticModeFetcher: chain, + CoreService: &core.Service{ + HeadFetcher: chain, + GenesisTimeFetcher: chain, + FinalizedFetcher: chain, + AttestationCache: cache.NewAttestationDataCache(), + OptimisticModeFetcher: chain, + }, + } + + expectedAttData := ðpbalpha.AttestationData{ + Slot: slot, + BeaconBlockRoot: blockRoot[:], + CommitteeIndex: 0, + Source: ðpbalpha.Checkpoint{ + Epoch: 2, + Root: justifiedRoot[:], + }, + Target: ðpbalpha.Checkpoint{ + Epoch: 3, + Root: blockRoot[:], + }, + } + + expectedAttDataSSZ, err := expectedAttData.MarshalSSZ() + require.NoError(t, err, "Could not marshal expected attestation data to SSZ") + + url := fmt.Sprintf("http://example.com?slot=%d&committee_index=%d", slot, 0) + request := httptest.NewRequest(http.MethodGet, url, nil) + request.Header.Add("Accept", "application/octet-stream") + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.GetAttestationData(writer, request) + + assert.Equal(t, http.StatusOK, writer.Code) + assert.DeepSSZEqual(t, expectedAttDataSSZ, writer.Body.Bytes()) + var att ethpbalpha.AttestationData + require.NoError(t, att.UnmarshalSSZ(writer.Body.Bytes())) + }) + t.Run("syncing", func(t *testing.T) { beaconState, err := util.NewBeaconState() require.NoError(t, err) @@ -1536,6 +1717,82 @@ func TestGetAttestationData(t *testing.T) { assert.DeepEqual(t, expectedResponse, resp) }) + t.Run("succeeds in first epoch SSZ", func(t *testing.T) { + slot := primitives.Slot(5) + block := util.NewBeaconBlock() + block.Block.Slot = slot + targetBlock := util.NewBeaconBlock() + targetBlock.Block.Slot = 0 + justifiedBlock := util.NewBeaconBlock() + justifiedBlock.Block.Slot = 0 + blockRoot, err := block.Block.HashTreeRoot() + require.NoError(t, err, "Could not hash beacon block") + justifiedRoot, err := justifiedBlock.Block.HashTreeRoot() + require.NoError(t, err, "Could not get signing root for justified block") + + beaconState, err := util.NewBeaconState() + require.NoError(t, err) + require.NoError(t, beaconState.SetSlot(slot)) + justifiedCheckpt := ðpbalpha.Checkpoint{ + Epoch: 0, + Root: justifiedRoot[:], + } + require.NoError(t, beaconState.SetCurrentJustifiedCheckpoint(justifiedCheckpt)) + require.NoError(t, err) + offset := int64(slot.Mul(params.BeaconConfig().SecondsPerSlot)) + chain := &mockChain.ChainService{ + Root: blockRoot[:], + Genesis: time.Now().Add(time.Duration(-1*offset) * time.Second), + CurrentJustifiedCheckPoint: justifiedCheckpt, + TargetRoot: blockRoot, + State: beaconState, + } + + s := &Server{ + SyncChecker: &mockSync.Sync{IsSyncing: false}, + HeadFetcher: chain, + TimeFetcher: chain, + OptimisticModeFetcher: chain, + CoreService: &core.Service{ + AttestationCache: cache.NewAttestationDataCache(), + OptimisticModeFetcher: chain, + HeadFetcher: chain, + GenesisTimeFetcher: chain, + FinalizedFetcher: chain, + }, + } + + expectedAttData := ðpbalpha.AttestationData{ + Slot: slot, + BeaconBlockRoot: blockRoot[:], + CommitteeIndex: 0, + Source: ðpbalpha.Checkpoint{ + Epoch: 0, + Root: justifiedRoot[:], + }, + Target: ðpbalpha.Checkpoint{ + Epoch: 0, + Root: blockRoot[:], + }, + } + + expectedAttDataSSZ, err := expectedAttData.MarshalSSZ() + require.NoError(t, err, "Could not marshal expected attestation data to SSZ") + + url := fmt.Sprintf("http://example.com?slot=%d&committee_index=%d", slot, 0) + request := httptest.NewRequest(http.MethodGet, url, nil) + request.Header.Add("Accept", "application/octet-stream") + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.GetAttestationData(writer, request) + + assert.Equal(t, http.StatusOK, writer.Code) + assert.DeepSSZEqual(t, expectedAttDataSSZ, writer.Body.Bytes()) + var att ethpbalpha.AttestationData + require.NoError(t, att.UnmarshalSSZ(writer.Body.Bytes())) + }) + t.Run("handles far away justified epoch", func(t *testing.T) { // Scenario: // @@ -1629,6 +1886,101 @@ func TestGetAttestationData(t *testing.T) { require.NotNil(t, resp) assert.DeepEqual(t, expectedResponse, resp) }) + + t.Run("handles far away justified epoch SSZ", func(t *testing.T) { + // Scenario: + // + // State slot = 10000 + // Last justified slot = epoch start of 1500 + // HistoricalRootsLimit = 8192 + // + // More background: https://github.com/prysmaticlabs/prysm/issues/2153 + // This test breaks if it doesn't use mainnet config + + // Ensure HistoricalRootsLimit matches scenario + params.SetupTestConfigCleanup(t) + cfg := params.MainnetConfig() + cfg.HistoricalRootsLimit = 8192 + params.OverrideBeaconConfig(cfg) + + block := util.NewBeaconBlock() + block.Block.Slot = 10000 + epochBoundaryBlock := util.NewBeaconBlock() + var err error + epochBoundaryBlock.Block.Slot, err = slots.EpochStart(slots.ToEpoch(10000)) + require.NoError(t, err) + justifiedBlock := util.NewBeaconBlock() + justifiedBlock.Block.Slot, err = slots.EpochStart(slots.ToEpoch(1500)) + require.NoError(t, err) + justifiedBlock.Block.Slot -= 2 // Imagine two skip block + blockRoot, err := block.Block.HashTreeRoot() + require.NoError(t, err, "Could not hash beacon block") + justifiedBlockRoot, err := justifiedBlock.Block.HashTreeRoot() + require.NoError(t, err, "Could not hash justified block") + + slot := primitives.Slot(10000) + beaconState, err := util.NewBeaconState() + require.NoError(t, err) + require.NoError(t, beaconState.SetSlot(slot)) + justifiedCheckpt := ðpbalpha.Checkpoint{ + Epoch: slots.ToEpoch(1500), + Root: justifiedBlockRoot[:], + } + require.NoError(t, beaconState.SetCurrentJustifiedCheckpoint(justifiedCheckpt)) + + offset := int64(slot.Mul(params.BeaconConfig().SecondsPerSlot)) + chain := &mockChain.ChainService{ + Root: blockRoot[:], + Genesis: time.Now().Add(time.Duration(-1*offset) * time.Second), + CurrentJustifiedCheckPoint: justifiedCheckpt, + TargetRoot: blockRoot, + State: beaconState, + } + + s := &Server{ + SyncChecker: &mockSync.Sync{IsSyncing: false}, + HeadFetcher: chain, + TimeFetcher: chain, + OptimisticModeFetcher: chain, + CoreService: &core.Service{ + AttestationCache: cache.NewAttestationDataCache(), + OptimisticModeFetcher: chain, + HeadFetcher: chain, + GenesisTimeFetcher: chain, + FinalizedFetcher: chain, + }, + } + + expectedAttData := ðpbalpha.AttestationData{ + Slot: slot, + BeaconBlockRoot: blockRoot[:], + CommitteeIndex: 0, + Source: ðpbalpha.Checkpoint{ + Epoch: slots.ToEpoch(1500), + Root: justifiedBlockRoot[:], + }, + Target: ðpbalpha.Checkpoint{ + Epoch: 312, + Root: blockRoot[:], + }, + } + + expectedAttDataSSZ, err := expectedAttData.MarshalSSZ() + require.NoError(t, err, "Could not marshal expected attestation data to SSZ") + + url := fmt.Sprintf("http://example.com?slot=%d&committee_index=%d", slot, 0) + request := httptest.NewRequest(http.MethodGet, url, nil) + request.Header.Add("Accept", "application/octet-stream") + writer := httptest.NewRecorder() + writer.Body = &bytes.Buffer{} + + s.GetAttestationData(writer, request) + + assert.Equal(t, http.StatusOK, writer.Code) + assert.DeepSSZEqual(t, expectedAttDataSSZ, writer.Body.Bytes()) + var att ethpbalpha.AttestationData + require.NoError(t, att.UnmarshalSSZ(writer.Body.Bytes())) + }) } func TestProduceSyncCommitteeContribution(t *testing.T) { diff --git a/changelog/bastin_attestation-api-ssz.md b/changelog/bastin_attestation-api-ssz.md new file mode 100644 index 0000000000..451cf0d78c --- /dev/null +++ b/changelog/bastin_attestation-api-ssz.md @@ -0,0 +1,4 @@ +### Added + +- Add SSZ support for two attestation APIs: `/eth/v1/validator/attestation_data` and + `/eth/v2/validator/aggregate_attestation`. \ No newline at end of file