SSZ support for Attestation APIs Part 1 (#15377)

* ssz support for validator/GetAttestationData V1

* ssz support for validator/GetAggregateAttestation V2

* changelog

* remove duplicate header
This commit is contained in:
Bastin
2025-06-05 15:39:52 +02:00
committed by GitHub
parent 79b064a6cc
commit 265d84569c
4 changed files with 402 additions and 3 deletions

View File

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

View File

@@ -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", &ethpbalpha.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", &ethpbalpha.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),

View File

@@ -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 := &ethpbalpha.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 := &ethpbalpha.AttestationData{
Slot: slot,
BeaconBlockRoot: blockRoot[:],
CommitteeIndex: 0,
Source: &ethpbalpha.Checkpoint{
Epoch: 2,
Root: justifiedRoot[:],
},
Target: &ethpbalpha.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 := &ethpbalpha.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 := &ethpbalpha.AttestationData{
Slot: slot,
BeaconBlockRoot: blockRoot[:],
CommitteeIndex: 0,
Source: &ethpbalpha.Checkpoint{
Epoch: 0,
Root: justifiedRoot[:],
},
Target: &ethpbalpha.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 := &ethpbalpha.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 := &ethpbalpha.AttestationData{
Slot: slot,
BeaconBlockRoot: blockRoot[:],
CommitteeIndex: 0,
Source: &ethpbalpha.Checkpoint{
Epoch: slots.ToEpoch(1500),
Root: justifiedBlockRoot[:],
},
Target: &ethpbalpha.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) {

View File

@@ -0,0 +1,4 @@
### Added
- Add SSZ support for two attestation APIs: `/eth/v1/validator/attestation_data` and
`/eth/v2/validator/aggregate_attestation`.