Skip to content

VerificationList stored in instance storage is an unbounded, corruption-prone queue that breaks production indexing #61

Description

@cybermax4200

Labels: type: bug, type: security, difficulty: advanced, area: reward-engine, area: storage

Why this matters now

Phase 4 (testnet deployment + backend integration) requires the off-chain oracle and
backend to reliably paginate pending verifications. The current implementation makes
that impossible at scale, and worse, it introduces a state-corruption window on
every resolve call. This must be fixed before any live oracle integration.

Problem / What

VerificationList — the list of VerificationKey structs that get_pending_verifications
and get_pending_verifications_paged iterate over — is stored entirely in
instance storage (e.storage().instance()), which has a hard size limit on Stellar
(instance storage is serialised as a single SCVal entry). As the list grows, two
problems compound:

  1. Unbounded write amplification on every mutation. Every call to
    push_verification_key and remove_verification_key deserialises the entire list,
    appends/filters it in-memory, then re-serialises and writes the whole thing back.
    On a production network with thousands of submissions this will hit Stellar's contract
    write size limits and start panicking, taking the engine down entirely.

  2. remove_verification_key is O(n) linear scan with no deduplication guard.
    If an adversary submits proofs for a large number of (task, user) pairs and the oracle
    resolves them, every resolve call degrades proportionally to the total historical
    submission count. There is no cap. This is a gas-exhaustion / liveness attack vector.

  3. Cursor-based pagination in collect_pending is offset-based into the global
    verification log
    , not into a "pending only" index. After a large number of
    resolutions, get_pending_verifications_paged(0, 10) must scan resolved entries
    until it accumulates 10 pending ones — defeating pagination entirely and making
    oracle polling unpredictably expensive.

  4. The UserVerifications list has the same problem — stored in persistent
    storage (push_user_verification_key), it grows forever and is read in full on
    every get_verifications_by_user call.

Affected code:

  • contracts/reward-engine/src/storage.rspush_verification_key,
    remove_verification_key, read_verification_keys, push_user_verification_key,
    read_user_verification_tasks
  • contracts/reward-engine/src/verification.rscollect_pending,
    get_pending_verifications_paged, get_verifications_by_user

Key Challenges

  • Storage class constraint. The fix cannot simply move VerificationList to
    persistent storage without a migration strategy; it also cannot remain in instance
    storage. The correct architecture is a sparse pending-count counter + per-user
    persistent index keyed by (task_id, user), removing the need for a global list
    altogether. An alternative is a doubly-linked list with head/tail pointers in
    instance storage, but this is more complex.
  • Backward compatibility. Any change to the storage layout of DataKey::VerificationList
    is a breaking change on an already-deployed contract. The implementation must either
    be migration-aware or clearly documented as requiring a fresh deployment (testnet
    only, acceptable).
  • Pagination contract change. Switching from "offset into the full log" to
    "offset into the pending-only set" changes the semantics of cursor for callers.
    The new interface must be clearly documented and the integration tests in
    tests/reward_integration_test.rs updated to match.
  • collect_pending correctness. The replacement must preserve the guarantee that
    a verification in Disputed or Rejected status is not returned as pending —
    this is currently tested in test_resolved_not_in_pending_list and
    test_get_pending_verifications_paged_skips_resolved.

Acceptance Criteria

  • VerificationList is no longer stored as a single growing Vec in instance
    storage. The pending set must be navigable in O(1) per item regardless of total
    historical verification count.
  • push_verification_key / remove_verification_key do not deserialise a
    growing list; each call is bounded to a fixed number of storage operations.
  • get_pending_verifications_paged(cursor, limit) returns exactly limit
    pending verifications starting from a stable cursor, skipping no more than
    O(1) entries per result regardless of how many verifications have been resolved.
  • All 51 existing reward-engine tests pass. test_get_pending_verifications_paged,
    test_get_pending_verifications_paged_skips_resolved, and
    test_resolved_not_in_pending_list must still pass with their current
    semantics.
  • A new stress test submits ≥ 200 proofs, resolves ≥ 180 of them, and asserts
    that get_pending_verifications_paged(0, 20) returns exactly 20 entries in a
    single call without scanning resolved records.
  • No change to the Verification struct layout in persistent storage (existing
    verification records remain readable).

Relevant files / functions

File Symbol
contracts/reward-engine/src/storage.rs push_verification_key, remove_verification_key, read_verification_keys, push_user_verification_key, read_user_verification_tasks, DataKey
contracts/reward-engine/src/verification.rs collect_pending, get_pending_verifications_paged, get_verifications_by_user, submit_proof, approve_proof, reject_proof, dispute_proof
contracts/reward-engine/src/verification.rs (tests) test_get_pending_verifications_paged, test_get_pending_verifications_paged_skips_resolved, test_resolved_not_in_pending_list

Out of scope

  • Changes to Verification or VerificationStatus struct definitions
  • Changes to get_verification(task_id, user) — that read path is fine
  • Any changes to eco-token or task-registry
  • Mainnet migration tooling

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions