Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 5 additions & 28 deletions decentralized-api/internal/server/public/poc_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,34 +406,11 @@ func (s *Server) preparePocProofRequest(

reqCount := uint32(req.Count)

// Quota on distinct snapshot counts per validator (after signature
// verification, so only authenticated requests consume quota). Honest
// validation touches at most the early and final snapshots; cycling
// through more counts is a rebuild-DoS probe, not a legitimate pattern.
if s.pocSnapshotLimiter != nil {
allowed, distinct := s.pocSnapshotLimiter.Allow(req.ValidatorAddress, int64(req.PocStageStartBlockHeight), req.ModelId, reqCount)
if !allowed {
logging.Warn("PoC proofs snapshot-count quota exceeded", types.Validation,
"validatorAddress", req.ValidatorAddress,
"pocStageStartBlockHeight", req.PocStageStartBlockHeight,
"modelId", req.ModelId,
"requestedCount", reqCount,
"distinctCounts", distinct)
return nil, 0, nil, echo.NewHTTPError(http.StatusTooManyRequests, "too many distinct snapshot counts requested")
}
// Two distinct counts (early + final) is the expected honest ceiling.
// A third is tolerated but anomalous: surface it so operators can
// spot validators whose early capture diverged (or probing) before
// the quota ever trips.
if distinct > 2 {
logging.Warn("PoC proofs validator exceeded expected two snapshot counts", types.Validation,
"validatorAddress", req.ValidatorAddress,
"pocStageStartBlockHeight", req.PocStageStartBlockHeight,
"modelId", req.ModelId,
"requestedCount", reqCount,
"distinctCounts", distinct)
}
}
// No per-validator snapshot-count quota is needed: committed counts are
// served from retained copy-on-write snapshots in O(depth), and any
// non-committed count is rejected outright (its root can never match the
// on-chain commitment), so a proof request can no longer trigger an O(N)
// rebuild — the rebuild-DoS surface the quota guarded against is gone.
storeRoot, err := stageStore.GetRootAt(reqCount)
if err != nil {
logging.Warn("Snapshot count not servable", types.Validation,
Expand Down
84 changes: 0 additions & 84 deletions decentralized-api/internal/server/public/poc_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -494,87 +494,3 @@ func TestPostPocProofsByNonce_UsesModelScopedStore(t *testing.T) {
assert.Equal(t, uint32(0), resp.Proofs[0].LeafIndex)
mockRecorder.AssertExpectations(t)
}

func TestPostPocProofs_DistinctSnapshotCountQuota(t *testing.T) {
store := artifacts.NewManagedArtifactStore(t.TempDir(), 3)
defer store.Close()

// Four flush boundaries: counts 1..4, each a legitimately servable snapshot.
modelStore, err := store.GetOrCreateStore(100, "model-a")
assert.NoError(t, err)
for i := int32(1); i <= 4; i++ {
assert.NoError(t, modelStore.AddWithNode(i, []byte{byte(i), 2, 3, 4}, ""))
assert.NoError(t, modelStore.Flush())
}

privKey := secp256k1.GenPrivKey()
pubKeyB64 := base64.StdEncoding.EncodeToString(privKey.PubKey().Bytes())
queryClient, cleanup := newProofQueryClient(t, &proofQueryServer{pubkeyB64: pubKeyB64})
defer cleanup()

mockRecorder := &cosmosclient.MockCosmosMessageClient{}
mockRecorder.On("NewInferenceQueryClient").Return(queryClient)
mockRecorder.On("GetSignerAddress").Return("participant-signer")
mockRecorder.On("SignBytes", mock.Anything).Return([]byte("response-signature"), nil)

server := &Server{
artifactStore: store,
recorder: mockRecorder,
authzCache: authzcache.NewAuthzCache(mockRecorder),
pocSnapshotLimiter: newSnapshotCountLimiter(),
}
e := echo.New()

post := func(validator string, count uint32) (int, error) {
root, err := modelStore.GetRootAt(count)
assert.NoError(t, err)
reqBody := &PocProofsRequest{
PocStageStartBlockHeight: 100,
ModelId: "model-a",
RootHash: base64.StdEncoding.EncodeToString(root),
Count: StringUint32(count),
LeafIndices: []StringUint32{0},
ValidatorAddress: validator,
ValidatorSignerAddress: validator,
Timestamp: StringInt64(time.Now().UnixNano()),
}
signature, err := privKey.Sign(buildPocProofsSignPayload(reqBody, root))
assert.NoError(t, err)
reqBody.Signature = base64.StdEncoding.EncodeToString(signature)

body, err := json.Marshal(reqBody)
assert.NoError(t, err)
req := httptest.NewRequest(http.MethodPost, "/v1/poc/proofs", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
if err := server.postPocProofs(e.NewContext(req, rec)); err != nil {
var httpErr *echo.HTTPError
if assert.ErrorAs(t, err, &httpErr) {
return httpErr.Code, err
}
return 0, err
}
return rec.Code, nil
}

// First three distinct snapshot counts are served.
for count := uint32(1); count <= 3; count++ {
code, err := post("validator-address", count)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, code)
}

// Fourth distinct count trips the quota, even though the snapshot is valid.
code, _ := post("validator-address", 4)
assert.Equal(t, http.StatusTooManyRequests, code)

// Repeats of an already-seen count are still served.
code, err = post("validator-address", 2)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, code)

// A different validator has an independent quota.
code, err = post("other-validator", 4)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, code)
}
103 changes: 0 additions & 103 deletions decentralized-api/internal/server/public/poc_snapshot_limiter.go

This file was deleted.

This file was deleted.

2 changes: 0 additions & 2 deletions decentralized-api/internal/server/public/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ type Server struct {
authzCache *authzcache.AuthzCache
httpClient *http.Client
statsStorage statsstorage.StatsStorage
pocSnapshotLimiter *snapshotCountLimiter
}

// ServerOption configures optional Server dependencies.
Expand Down Expand Up @@ -90,7 +89,6 @@ func NewServer(
epochGroupDataCache: internal.NewEpochGroupDataCache(recorder),
authzCache: authzcache.NewAuthzCache(recorder),
httpClient: NewNoRedirectClient(httpClientTimeout),
pocSnapshotLimiter: newSnapshotCountLimiter(),
}

for _, opt := range opts {
Expand Down
Loading
Loading