mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-02-13 22:44:57 -05:00
Compare commits
16 Commits
calculate-
...
ptc-duty-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a22ae9803 | ||
|
|
3eb4f7bdba | ||
|
|
b83907cdf3 | ||
|
|
fd98fbddd5 | ||
|
|
5d528f8a69 | ||
|
|
d130778def | ||
|
|
7529f9c6cb | ||
|
|
3c9881a675 | ||
|
|
4b42fd5fc9 | ||
|
|
11aeed325e | ||
|
|
55ad19a512 | ||
|
|
bd70298e94 | ||
|
|
b330a0571a | ||
|
|
f73073cfdf | ||
|
|
0076fc78bd | ||
|
|
af2a6c29a6 |
@@ -74,6 +74,18 @@ type SyncCommitteeDuty struct {
|
||||
ValidatorSyncCommitteeIndices []string `json:"validator_sync_committee_indices"`
|
||||
}
|
||||
|
||||
type GetPTCDutiesResponse struct {
|
||||
DependentRoot string `json:"dependent_root"`
|
||||
ExecutionOptimistic bool `json:"execution_optimistic"`
|
||||
Data []*PTCDuty `json:"data"`
|
||||
}
|
||||
|
||||
type PTCDuty struct {
|
||||
Pubkey string `json:"pubkey"`
|
||||
ValidatorIndex string `json:"validator_index"`
|
||||
Slot string `json:"slot"`
|
||||
}
|
||||
|
||||
// ProduceBlockV3Response is a wrapper json object for the returned block from the ProduceBlockV3 endpoint
|
||||
type ProduceBlockV3Response struct {
|
||||
Version string `json:"version"`
|
||||
|
||||
@@ -156,6 +156,65 @@ func PayloadCommittee(ctx context.Context, st state.ReadOnlyBeaconState, slot pr
|
||||
return selected, nil
|
||||
}
|
||||
|
||||
// PTCDuty represents a validator's PTC assignment for a slot.
|
||||
type PTCDuty struct {
|
||||
ValidatorIndex primitives.ValidatorIndex
|
||||
Slot primitives.Slot
|
||||
}
|
||||
|
||||
// PTCDuties returns PTC slot assignments for the requested validators in the epoch derived from the state's slot.
|
||||
// It's optimized for batch lookups with early exit once all validators are found.
|
||||
// Validators not in any PTC for the epoch will not appear in the result.
|
||||
func PTCDuties(
|
||||
ctx context.Context,
|
||||
st state.ReadOnlyBeaconState,
|
||||
validators map[primitives.ValidatorIndex]struct{},
|
||||
) ([]PTCDuty, error) {
|
||||
if len(validators) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
epoch := slots.ToEpoch(st.Slot())
|
||||
startSlot, err := slots.EpochStart(epoch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Track remaining validators to find.
|
||||
remaining := make(map[primitives.ValidatorIndex]struct{}, len(validators))
|
||||
for v := range validators {
|
||||
remaining[v] = struct{}{}
|
||||
}
|
||||
|
||||
var duties []PTCDuty
|
||||
endSlot := startSlot + params.BeaconConfig().SlotsPerEpoch
|
||||
|
||||
for slot := startSlot; slot < endSlot && len(remaining) > 0; slot++ {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
// Compute PTC for this slot.
|
||||
ptc, err := PayloadCommittee(ctx, st, slot)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get PTC for slot %d", slot)
|
||||
}
|
||||
|
||||
// Check which remaining validators are in this PTC.
|
||||
for _, valIdx := range ptc {
|
||||
if _, ok := remaining[valIdx]; ok {
|
||||
duties = append(duties, PTCDuty{
|
||||
ValidatorIndex: valIdx,
|
||||
Slot: slot,
|
||||
})
|
||||
delete(remaining, valIdx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return duties, nil
|
||||
}
|
||||
|
||||
// ptcSeed computes the seed for the payload timeliness committee.
|
||||
func ptcSeed(st state.ReadOnlyBeaconState, epoch primitives.Epoch, slot primitives.Slot) ([32]byte, error) {
|
||||
seed, err := helpers.Seed(st, epoch, params.BeaconConfig().DomainPTCAttester)
|
||||
|
||||
@@ -291,6 +291,95 @@ func signAttestation(t *testing.T, st state.ReadOnlyBeaconState, data *eth.Paylo
|
||||
return agg.Marshal()
|
||||
}
|
||||
|
||||
func TestPTCDuties(t *testing.T) {
|
||||
helpers.ClearCache()
|
||||
setupTestConfig(t)
|
||||
|
||||
// Create state with enough validators.
|
||||
numVals := 100
|
||||
vals := make([]*eth.Validator, numVals)
|
||||
for i := range numVals {
|
||||
_, pk := newKey(t)
|
||||
vals[i] = activeValidator(pk)
|
||||
}
|
||||
st := newTestState(t, vals, 0)
|
||||
|
||||
t.Run("returns duties for validators in PTC", func(t *testing.T) {
|
||||
// Request duties for all validators.
|
||||
requested := make(map[primitives.ValidatorIndex]struct{})
|
||||
for i := range numVals {
|
||||
requested[primitives.ValidatorIndex(i)] = struct{}{}
|
||||
}
|
||||
|
||||
duties, err := gloas.PTCDuties(t.Context(), st, requested)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, duties, "Should return some duties")
|
||||
|
||||
// Verify all returned duties are for requested validators.
|
||||
for _, duty := range duties {
|
||||
_, ok := requested[duty.ValidatorIndex]
|
||||
require.Equal(t, true, ok, "Returned validator should be in requested set")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns empty for empty request", func(t *testing.T) {
|
||||
requested := make(map[primitives.ValidatorIndex]struct{})
|
||||
duties, err := gloas.PTCDuties(t.Context(), st, requested)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, len(duties), "Should return no duties for empty request")
|
||||
})
|
||||
|
||||
t.Run("returns empty for validators not in any PTC", func(t *testing.T) {
|
||||
// Request duties for validators that don't exist.
|
||||
requested := make(map[primitives.ValidatorIndex]struct{})
|
||||
for i := 1000000; i < 1000010; i++ {
|
||||
requested[primitives.ValidatorIndex(i)] = struct{}{}
|
||||
}
|
||||
|
||||
duties, err := gloas.PTCDuties(t.Context(), st, requested)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, len(duties), "Non-existent validators should have no duties")
|
||||
})
|
||||
|
||||
t.Run("each validator has at most one duty per epoch", func(t *testing.T) {
|
||||
requested := make(map[primitives.ValidatorIndex]struct{})
|
||||
for i := range numVals {
|
||||
requested[primitives.ValidatorIndex(i)] = struct{}{}
|
||||
}
|
||||
|
||||
duties, err := gloas.PTCDuties(t.Context(), st, requested)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check for duplicates.
|
||||
seen := make(map[primitives.ValidatorIndex]bool)
|
||||
for _, duty := range duties {
|
||||
if seen[duty.ValidatorIndex] {
|
||||
t.Errorf("Validator %d appears in duties multiple times", duty.ValidatorIndex)
|
||||
}
|
||||
seen[duty.ValidatorIndex] = true
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("duties are deterministic", func(t *testing.T) {
|
||||
requested := make(map[primitives.ValidatorIndex]struct{})
|
||||
for i := range 50 {
|
||||
requested[primitives.ValidatorIndex(i)] = struct{}{}
|
||||
}
|
||||
|
||||
duties1, err := gloas.PTCDuties(t.Context(), st, requested)
|
||||
require.NoError(t, err)
|
||||
|
||||
duties2, err := gloas.PTCDuties(t.Context(), st, requested)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, len(duties1), len(duties2), "Should return same number of duties")
|
||||
for i := range duties1 {
|
||||
require.Equal(t, duties1[i].ValidatorIndex, duties2[i].ValidatorIndex)
|
||||
require.Equal(t, duties1[i].Slot, duties2[i].Slot)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type validatorLookupErrState struct {
|
||||
state.BeaconState
|
||||
errIndex primitives.ValidatorIndex
|
||||
|
||||
@@ -340,6 +340,17 @@ func (s *Service) validatorEndpoints(
|
||||
handler: server.GetSyncCommitteeDuties,
|
||||
methods: []string{http.MethodPost},
|
||||
},
|
||||
{
|
||||
template: "/eth/v1/validator/duties/ptc/{epoch}",
|
||||
name: namespace + ".GetPTCDuties",
|
||||
middleware: []middleware.Middleware{
|
||||
middleware.ContentTypeHandler([]string{api.JsonMediaType}),
|
||||
middleware.AcceptHeaderHandler([]string{api.JsonMediaType}),
|
||||
middleware.AcceptEncodingHeaderHandler(),
|
||||
},
|
||||
handler: server.GetPTCDuties,
|
||||
methods: []string{http.MethodPost},
|
||||
},
|
||||
{
|
||||
template: "/eth/v1/validator/prepare_beacon_proposer",
|
||||
name: namespace + ".PrepareBeaconProposer",
|
||||
|
||||
@@ -94,6 +94,7 @@ func Test_endpoints(t *testing.T) {
|
||||
"/eth/v1/validator/duties/attester/{epoch}": {http.MethodPost},
|
||||
"/eth/v1/validator/duties/proposer/{epoch}": {http.MethodGet},
|
||||
"/eth/v1/validator/duties/sync/{epoch}": {http.MethodPost},
|
||||
"/eth/v1/validator/duties/ptc/{epoch}": {http.MethodPost},
|
||||
"/eth/v3/validator/blocks/{slot}": {http.MethodGet},
|
||||
"/eth/v1/validator/attestation_data": {http.MethodGet},
|
||||
"/eth/v2/validator/aggregate_attestation": {http.MethodGet},
|
||||
|
||||
@@ -18,6 +18,7 @@ go_library(
|
||||
"//beacon-chain/builder:go_default_library",
|
||||
"//beacon-chain/cache:go_default_library",
|
||||
"//beacon-chain/core/feed/operation:go_default_library",
|
||||
"//beacon-chain/core/gloas:go_default_library",
|
||||
"//beacon-chain/core/helpers:go_default_library",
|
||||
"//beacon-chain/db:go_default_library",
|
||||
"//beacon-chain/operations/attestations:go_default_library",
|
||||
@@ -67,6 +68,7 @@ go_test(
|
||||
"//beacon-chain/blockchain/testing:go_default_library",
|
||||
"//beacon-chain/builder/testing:go_default_library",
|
||||
"//beacon-chain/cache:go_default_library",
|
||||
"//beacon-chain/core/gloas:go_default_library",
|
||||
"//beacon-chain/core/helpers:go_default_library",
|
||||
"//beacon-chain/core/transition:go_default_library",
|
||||
"//beacon-chain/db/testing:go_default_library",
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/OffchainLabs/prysm/v7/api/server/structs"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/builder"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/cache"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/gloas"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/core"
|
||||
rpchelpers "github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/eth/helpers"
|
||||
@@ -1212,6 +1213,147 @@ func (s *Server) GetSyncCommitteeDuties(w http.ResponseWriter, r *http.Request)
|
||||
httputil.WriteJson(w, resp)
|
||||
}
|
||||
|
||||
// GetPTCDuties retrieves the payload timeliness committee (PTC) duties for the requested epoch.
|
||||
// The PTC is responsible for attesting to payload timeliness in ePBS (Gloas fork and later).
|
||||
func (s *Server) GetPTCDuties(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, span := trace.StartSpan(r.Context(), "validator.GetPTCDuties")
|
||||
defer span.End()
|
||||
|
||||
if shared.IsSyncing(ctx, w, s.SyncChecker, s.HeadFetcher, s.TimeFetcher, s.OptimisticModeFetcher) {
|
||||
return
|
||||
}
|
||||
|
||||
_, requestedEpochUint, ok := shared.UintFromRoute(w, r, "epoch")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
requestedEpoch := primitives.Epoch(requestedEpochUint)
|
||||
|
||||
// PTC duties are only available from Gloas fork onwards.
|
||||
if requestedEpoch < params.BeaconConfig().GloasForkEpoch {
|
||||
httputil.HandleError(w, "PTC duties are not available before Gloas fork", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var indices []string
|
||||
err := json.NewDecoder(r.Body).Decode(&indices)
|
||||
switch {
|
||||
case errors.Is(err, io.EOF):
|
||||
httputil.HandleError(w, "No data submitted", http.StatusBadRequest)
|
||||
return
|
||||
case err != nil:
|
||||
httputil.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if len(indices) == 0 {
|
||||
httputil.HandleError(w, "No data submitted", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
requestedValIndices := make([]primitives.ValidatorIndex, len(indices))
|
||||
for i, ix := range indices {
|
||||
valIx, valid := shared.ValidateUint(w, fmt.Sprintf("ValidatorIndices[%d]", i), ix)
|
||||
if !valid {
|
||||
return
|
||||
}
|
||||
requestedValIndices[i] = primitives.ValidatorIndex(valIx)
|
||||
}
|
||||
|
||||
// Limit how far in the future we can query (current + 1 epoch).
|
||||
cs := s.TimeFetcher.CurrentSlot()
|
||||
currentEpoch := slots.ToEpoch(cs)
|
||||
nextEpoch := currentEpoch + 1
|
||||
if requestedEpoch > nextEpoch {
|
||||
httputil.HandleError(w,
|
||||
fmt.Sprintf("Request epoch %d can not be greater than next epoch %d", requestedEpoch, nextEpoch),
|
||||
http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// For next epoch requests, we use the current epoch's state since PTC
|
||||
// assignments for next epoch can be computed from current epoch's state.
|
||||
// This mirrors the spec's get_ptc_assignment which asserts epoch <= next_epoch
|
||||
// and uses the current state to compute assignments.
|
||||
epochForState := requestedEpoch
|
||||
if requestedEpoch == nextEpoch {
|
||||
epochForState = currentEpoch
|
||||
}
|
||||
st, err := s.Stater.StateByEpoch(ctx, epochForState)
|
||||
if err != nil {
|
||||
shared.WriteStateFetchError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Build a set of requested validators (also deduplicates per spec's uniqueItems requirement).
|
||||
// Validate that each index exists in the validator registry.
|
||||
requestedSet := make(map[primitives.ValidatorIndex]struct{}, len(requestedValIndices))
|
||||
var zeroPubkey [fieldparams.BLSPubkeyLength]byte
|
||||
for _, idx := range requestedValIndices {
|
||||
// Skip duplicates.
|
||||
if _, exists := requestedSet[idx]; exists {
|
||||
continue
|
||||
}
|
||||
// Validate index exists.
|
||||
pubkey := st.PubkeyAtIndex(idx)
|
||||
if bytes.Equal(pubkey[:], zeroPubkey[:]) {
|
||||
httputil.HandleError(w, fmt.Sprintf("Invalid validator index %d", idx), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
requestedSet[idx] = struct{}{}
|
||||
}
|
||||
|
||||
// Compute PTC duties using the optimized batch function.
|
||||
// This exits early once all requested validators are found.
|
||||
ptcDuties, err := gloas.PTCDuties(ctx, st, requestedSet)
|
||||
if err != nil {
|
||||
httputil.HandleError(w, "Could not compute PTC duties: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to response format.
|
||||
duties := make([]*structs.PTCDuty, 0, len(ptcDuties))
|
||||
for _, duty := range ptcDuties {
|
||||
pubkey := st.PubkeyAtIndex(duty.ValidatorIndex)
|
||||
duties = append(duties, &structs.PTCDuty{
|
||||
Pubkey: hexutil.Encode(pubkey[:]),
|
||||
ValidatorIndex: strconv.FormatUint(uint64(duty.ValidatorIndex), 10),
|
||||
Slot: strconv.FormatUint(uint64(duty.Slot), 10),
|
||||
})
|
||||
}
|
||||
|
||||
// Get dependent root. Per the spec, dependent_root is:
|
||||
// get_block_root_at_slot(state, compute_start_slot_at_epoch(epoch - 1) - 1)
|
||||
// or the genesis block root in the case of underflow.
|
||||
var dependentRoot []byte
|
||||
if requestedEpoch <= 1 {
|
||||
r, err := s.BeaconDB.GenesisBlockRoot(ctx)
|
||||
if err != nil {
|
||||
httputil.HandleError(w, "Could not get genesis block root: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
dependentRoot = r[:]
|
||||
} else {
|
||||
dependentRoot, err = attestationDependentRoot(st, requestedEpoch)
|
||||
if err != nil {
|
||||
httputil.HandleError(w, "Could not get dependent root: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
isOptimistic, err := s.OptimisticModeFetcher.IsOptimistic(ctx)
|
||||
if err != nil {
|
||||
httputil.HandleError(w, "Could not check optimistic status: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
resp := &structs.GetPTCDutiesResponse{
|
||||
DependentRoot: hexutil.Encode(dependentRoot),
|
||||
ExecutionOptimistic: isOptimistic,
|
||||
Data: duties,
|
||||
}
|
||||
httputil.WriteJson(w, resp)
|
||||
}
|
||||
|
||||
// GetLiveness requests the beacon node to indicate if a validator has been observed to be live in a given epoch.
|
||||
// The beacon node might detect liveness by observing messages from the validator on the network,
|
||||
// in the beacon chain, from its API or from any other source.
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
mockChain "github.com/OffchainLabs/prysm/v7/beacon-chain/blockchain/testing"
|
||||
builderTest "github.com/OffchainLabs/prysm/v7/beacon-chain/builder/testing"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/cache"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/gloas"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
|
||||
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/transition"
|
||||
dbutil "github.com/OffchainLabs/prysm/v7/beacon-chain/db/testing"
|
||||
@@ -2959,6 +2960,250 @@ func TestGetSyncCommitteeDuties(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetPTCDuties(t *testing.T) {
|
||||
helpers.ClearCache()
|
||||
params.SetupTestConfigCleanup(t)
|
||||
cfg := params.BeaconConfig()
|
||||
cfg.GloasForkEpoch = 0
|
||||
params.OverrideBeaconConfig(cfg)
|
||||
|
||||
// Use fixed slot 0 for deterministic tests.
|
||||
slot := primitives.Slot(0)
|
||||
genesisTime := time.Now()
|
||||
// Need enough validators for PTC selection (PTC_SIZE is 512 on mainnet, 2 on minimal)
|
||||
numVals := uint64(fieldparams.PTCSize * 2)
|
||||
st, _ := util.DeterministicGenesisStateFulu(t, numVals)
|
||||
require.NoError(t, st.SetGenesisTime(genesisTime))
|
||||
// Initialize the committee cache for epoch 0.
|
||||
require.NoError(t, helpers.UpdateCommitteeCache(t.Context(), st, 0))
|
||||
|
||||
// Set up a genesis block root for dependent_root calculation.
|
||||
genesisRoot := [32]byte{1, 2, 3}
|
||||
db := dbutil.SetupDB(t)
|
||||
require.NoError(t, db.SaveGenesisBlockRoot(t.Context(), genesisRoot))
|
||||
|
||||
mockChainService := &mockChain.ChainService{Genesis: genesisTime, State: st, Slot: &slot}
|
||||
s := &Server{
|
||||
Stater: &testutil.MockStater{BeaconState: st},
|
||||
SyncChecker: &mockSync.Sync{IsSyncing: false},
|
||||
TimeFetcher: mockChainService,
|
||||
HeadFetcher: mockChainService,
|
||||
OptimisticModeFetcher: mockChainService,
|
||||
BeaconDB: db,
|
||||
}
|
||||
|
||||
t.Run("single validator in PTC", func(t *testing.T) {
|
||||
// Request duties for validator index 0
|
||||
var body bytes.Buffer
|
||||
_, err := body.WriteString("[\"0\"]")
|
||||
require.NoError(t, err)
|
||||
request := httptest.NewRequest(http.MethodPost, "http://www.example.com/eth/v1/validator/duties/ptc/{epoch}", &body)
|
||||
request.SetPathValue("epoch", "0")
|
||||
writer := httptest.NewRecorder()
|
||||
writer.Body = &bytes.Buffer{}
|
||||
|
||||
s.GetPTCDuties(writer, request)
|
||||
assert.Equal(t, http.StatusOK, writer.Code)
|
||||
resp := &structs.GetPTCDutiesResponse{}
|
||||
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp))
|
||||
assert.NotEmpty(t, resp.DependentRoot)
|
||||
})
|
||||
|
||||
t.Run("verifies PTC duties correctness", func(t *testing.T) {
|
||||
// Request duties for a range of validators.
|
||||
// Some will be in the PTC, some won't.
|
||||
var indices []string
|
||||
requestedSet := make(map[primitives.ValidatorIndex]struct{})
|
||||
for i := range 100 {
|
||||
indices = append(indices, strconv.Itoa(i))
|
||||
requestedSet[primitives.ValidatorIndex(i)] = struct{}{}
|
||||
}
|
||||
|
||||
// Test PTCDuties directly.
|
||||
directDuties, err := gloas.PTCDuties(t.Context(), st, requestedSet)
|
||||
require.NoError(t, err)
|
||||
// Should return some duties (not necessarily all 100, depends on PTC selection).
|
||||
require.NotEmpty(t, directDuties, "Should return at least some duties")
|
||||
|
||||
// All returned duties should be for slots within epoch 0.
|
||||
slotsPerEpoch := params.BeaconConfig().SlotsPerEpoch
|
||||
for _, duty := range directDuties {
|
||||
if uint64(duty.Slot) >= uint64(slotsPerEpoch) {
|
||||
t.Errorf("Duty slot %d should be within epoch 0 (< %d)", duty.Slot, slotsPerEpoch)
|
||||
}
|
||||
// Verify returned validator was in the request.
|
||||
_, ok := requestedSet[duty.ValidatorIndex]
|
||||
if !ok {
|
||||
t.Errorf("Returned validator %d should be in requested set", duty.ValidatorIndex)
|
||||
}
|
||||
}
|
||||
|
||||
// Test via HTTP - should return same count.
|
||||
indicesJSON, err := json.Marshal(indices)
|
||||
require.NoError(t, err)
|
||||
|
||||
request := httptest.NewRequest(http.MethodPost, "http://www.example.com/eth/v1/validator/duties/ptc/{epoch}", bytes.NewReader(indicesJSON))
|
||||
request.SetPathValue("epoch", "0")
|
||||
writer := httptest.NewRecorder()
|
||||
writer.Body = &bytes.Buffer{}
|
||||
|
||||
s.GetPTCDuties(writer, request)
|
||||
assert.Equal(t, http.StatusOK, writer.Code)
|
||||
resp := &structs.GetPTCDutiesResponse{}
|
||||
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp))
|
||||
|
||||
// HTTP response should match direct call.
|
||||
assert.Equal(t, len(directDuties), len(resp.Data), "HTTP response should match direct PTCDuties call")
|
||||
})
|
||||
|
||||
t.Run("multiple validators", func(t *testing.T) {
|
||||
var body bytes.Buffer
|
||||
_, err := body.WriteString("[\"0\",\"1\",\"2\",\"3\",\"4\"]")
|
||||
require.NoError(t, err)
|
||||
request := httptest.NewRequest(http.MethodPost, "http://www.example.com/eth/v1/validator/duties/ptc/{epoch}", &body)
|
||||
request.SetPathValue("epoch", "0")
|
||||
writer := httptest.NewRecorder()
|
||||
writer.Body = &bytes.Buffer{}
|
||||
|
||||
s.GetPTCDuties(writer, request)
|
||||
assert.Equal(t, http.StatusOK, writer.Code)
|
||||
resp := &structs.GetPTCDutiesResponse{}
|
||||
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp))
|
||||
assert.NotEmpty(t, resp.DependentRoot)
|
||||
// Verify any returned duties have correct structure
|
||||
for _, duty := range resp.Data {
|
||||
assert.NotEmpty(t, duty.Pubkey)
|
||||
assert.NotEmpty(t, duty.ValidatorIndex)
|
||||
assert.NotEmpty(t, duty.Slot)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("duplicate validator indices are deduplicated", func(t *testing.T) {
|
||||
// Request the same validator multiple times - should be deduplicated.
|
||||
var body bytes.Buffer
|
||||
_, err := body.WriteString("[\"0\",\"0\",\"0\",\"1\",\"1\"]")
|
||||
require.NoError(t, err)
|
||||
request := httptest.NewRequest(http.MethodPost, "http://www.example.com/eth/v1/validator/duties/ptc/{epoch}", &body)
|
||||
request.SetPathValue("epoch", "0")
|
||||
writer := httptest.NewRecorder()
|
||||
writer.Body = &bytes.Buffer{}
|
||||
|
||||
s.GetPTCDuties(writer, request)
|
||||
assert.Equal(t, http.StatusOK, writer.Code)
|
||||
resp := &structs.GetPTCDutiesResponse{}
|
||||
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), resp))
|
||||
// Each validator should appear at most once in the response.
|
||||
seen := make(map[string]bool)
|
||||
for _, duty := range resp.Data {
|
||||
if seen[duty.ValidatorIndex] {
|
||||
t.Errorf("Validator %s appears multiple times in response", duty.ValidatorIndex)
|
||||
}
|
||||
seen[duty.ValidatorIndex] = true
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("pre-Gloas epoch returns error", func(t *testing.T) {
|
||||
// Temporarily set GloasForkEpoch to 10
|
||||
cfg := params.BeaconConfig()
|
||||
cfg.GloasForkEpoch = 10
|
||||
params.OverrideBeaconConfig(cfg)
|
||||
defer func() {
|
||||
cfg.GloasForkEpoch = 0
|
||||
params.OverrideBeaconConfig(cfg)
|
||||
}()
|
||||
|
||||
var body bytes.Buffer
|
||||
_, err := body.WriteString("[\"0\"]")
|
||||
require.NoError(t, err)
|
||||
request := httptest.NewRequest(http.MethodPost, "http://www.example.com/eth/v1/validator/duties/ptc/{epoch}", &body)
|
||||
request.SetPathValue("epoch", "0")
|
||||
writer := httptest.NewRecorder()
|
||||
writer.Body = &bytes.Buffer{}
|
||||
|
||||
s.GetPTCDuties(writer, request)
|
||||
assert.Equal(t, http.StatusBadRequest, writer.Code)
|
||||
e := &httputil.DefaultJsonError{}
|
||||
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e))
|
||||
assert.StringContains(t, "PTC duties are not available before Gloas fork", e.Message)
|
||||
})
|
||||
|
||||
t.Run("no body", func(t *testing.T) {
|
||||
request := httptest.NewRequest(http.MethodPost, "http://www.example.com/eth/v1/validator/duties/ptc/{epoch}", nil)
|
||||
request.SetPathValue("epoch", "0")
|
||||
writer := httptest.NewRecorder()
|
||||
writer.Body = &bytes.Buffer{}
|
||||
|
||||
s.GetPTCDuties(writer, request)
|
||||
assert.Equal(t, http.StatusBadRequest, writer.Code)
|
||||
e := &httputil.DefaultJsonError{}
|
||||
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e))
|
||||
assert.StringContains(t, "No data submitted", e.Message)
|
||||
})
|
||||
|
||||
t.Run("empty body", func(t *testing.T) {
|
||||
var body bytes.Buffer
|
||||
_, err := body.WriteString("[]")
|
||||
require.NoError(t, err)
|
||||
request := httptest.NewRequest(http.MethodPost, "http://www.example.com/eth/v1/validator/duties/ptc/{epoch}", &body)
|
||||
request.SetPathValue("epoch", "0")
|
||||
writer := httptest.NewRecorder()
|
||||
writer.Body = &bytes.Buffer{}
|
||||
|
||||
s.GetPTCDuties(writer, request)
|
||||
assert.Equal(t, http.StatusBadRequest, writer.Code)
|
||||
e := &httputil.DefaultJsonError{}
|
||||
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e))
|
||||
assert.StringContains(t, "No data submitted", e.Message)
|
||||
})
|
||||
|
||||
t.Run("invalid validator index string", func(t *testing.T) {
|
||||
var body bytes.Buffer
|
||||
_, err := body.WriteString("[\"foo\"]")
|
||||
require.NoError(t, err)
|
||||
request := httptest.NewRequest(http.MethodPost, "http://www.example.com/eth/v1/validator/duties/ptc/{epoch}", &body)
|
||||
request.SetPathValue("epoch", "0")
|
||||
writer := httptest.NewRecorder()
|
||||
writer.Body = &bytes.Buffer{}
|
||||
|
||||
s.GetPTCDuties(writer, request)
|
||||
assert.Equal(t, http.StatusBadRequest, writer.Code)
|
||||
})
|
||||
|
||||
t.Run("out of bounds validator index", func(t *testing.T) {
|
||||
// Request a validator index that's way beyond the number of validators.
|
||||
var body bytes.Buffer
|
||||
_, err := body.WriteString("[\"999999999\"]")
|
||||
require.NoError(t, err)
|
||||
request := httptest.NewRequest(http.MethodPost, "http://www.example.com/eth/v1/validator/duties/ptc/{epoch}", &body)
|
||||
request.SetPathValue("epoch", "0")
|
||||
writer := httptest.NewRecorder()
|
||||
writer.Body = &bytes.Buffer{}
|
||||
|
||||
s.GetPTCDuties(writer, request)
|
||||
// Invalid validator index should return 400, matching attester duties behavior.
|
||||
assert.Equal(t, http.StatusBadRequest, writer.Code)
|
||||
e := &httputil.DefaultJsonError{}
|
||||
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e))
|
||||
assert.StringContains(t, "Invalid validator index", e.Message)
|
||||
})
|
||||
|
||||
t.Run("epoch too far in future", func(t *testing.T) {
|
||||
var body bytes.Buffer
|
||||
_, err := body.WriteString("[\"0\"]")
|
||||
require.NoError(t, err)
|
||||
request := httptest.NewRequest(http.MethodPost, "http://www.example.com/eth/v1/validator/duties/ptc/{epoch}", &body)
|
||||
request.SetPathValue("epoch", "100") // Far future epoch
|
||||
writer := httptest.NewRecorder()
|
||||
writer.Body = &bytes.Buffer{}
|
||||
|
||||
s.GetPTCDuties(writer, request)
|
||||
assert.Equal(t, http.StatusBadRequest, writer.Code)
|
||||
e := &httputil.DefaultJsonError{}
|
||||
require.NoError(t, json.Unmarshal(writer.Body.Bytes(), e))
|
||||
assert.StringContains(t, "can not be greater than next epoch", e.Message)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPrepareBeaconProposer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
3
changelog/satushh_ptc-duties-endpoint.md
Normal file
3
changelog/satushh_ptc-duties-endpoint.md
Normal file
@@ -0,0 +1,3 @@
|
||||
### Added
|
||||
|
||||
- PTC (Payload Timeliness Committee) duties endpoint: POST /eth/v1/validator/duties/ptc/{epoch} for ePBS (Gloas fork).
|
||||
Reference in New Issue
Block a user