Skip to content

feat: index MerkleDistributor pools, roots, and claims#186

Open
zer0stars wants to merge 11 commits into
mainfrom
feature/merkle-claims-indexing
Open

feat: index MerkleDistributor pools, roots, and claims#186
zer0stars wants to merge 11 commits into
mainfrom
feature/merkle-claims-indexing

Conversation

@zer0stars

Copy link
Copy Markdown
Member

Summary

  • Indexes the new MerkleDistributor contract events (PoolCreated/RootSet/Claimed/Funded/Swept/WeeklyLimitSet) — pool-agnostic, so future third-party pools appear with zero config.
  • On RootSet: fetches the weekly tree file from proofsURI (HTTPS-only, host allowlist, redirect-guarded, 50MB cap), recomputes the merkle root from leaves and verifies it byte-equal to the on-chain root before serving anything, then upserts all leaves+proofs in one transaction (claim state preserved on root overwrite).
  • On Claimed: idempotent claim marking with SELECT FOR UPDATE (Kafka redelivery safe).
  • New public GraphQL: merklePools, merklePool(poolId), merkleRewards(account, poolId, claimed) — Relay connections; serves everything a client needs to call claim()/claimBatch().
  • pkg/merkletree copied from rewards-api (swap to module import once published).

Companion PRs: dimo-rewards (contract), rewards-api (producer), contract-event-processor (config), transactions (SDK), dimo-driver (UI).

Test plan

  • Handler tests: happy path, tampered file, event-root mismatch, claim idempotency, overflow guards, fetcher limits incl. redirect refusal
  • Repository tests: filters, bidirectional cursor pagination
  • Full go test ./... exit 0
  • Set MERKLE_DISTRIBUTOR_ADDR + tree host after deployment (placeholders in chart values)

- Guard the HTTP tree fetcher against SSRF via redirects by re-validating
  every redirect target against the HTTPS scheme and host allowlist
- Apply both after and before cursors in GetMerkleRewards instead of
  dropping before when after is set
- Reject MerkleDistributor event poolId/week arguments that overflow int64
- Put the MERKLE_DISTRIBUTOR_ADDR TODO comment on its own line in
  settings.sample.yaml
@zer0stars zer0stars requested a review from elffjs as a code owner June 10, 2026 14:30
Review fixes for the Merkle claims indexing feature:

- Replace the per-leaf claim Upsert loop in handleRootSet with a single
  INSERT ... SELECT unnest(...) ON CONFLICT statement; claimed_at and
  claim_tx are still preserved on conflict since they are not in the
  SET list. Covered by a new 150-leaf handler test that also exercises
  redelivery through the conflict path.
- Replace the single-column merkle_claims account index with a
  composite (account, pool_id, epoch DESC) index matching the
  merkleRewards query and cursor order, and add a partial index on
  account WHERE claimed_at IS NULL for the unclaimed-by-account hot
  path. Indexes do not affect SQLBoiler models, so no regen needed.
- Resolve MerkleReward.pool through a new MerklePoolByID dataloader
  instead of one query per reward row.
- Log fetchDurationMs and dbWriteDurationMs when a root is set, and
  document why the tree-file/event root mismatch path intentionally
  returns an error (Kafka redelivery is the operator alert).
- Hoist the tree fetcher's 10s timeout into merkle.FetchTimeout with a
  comment noting it spans the full body read of up to 50 MiB.
- Document and pin the RootSet root wire format: even as an indexed
  parameter, bytes32 decodes via abi.ParseTopicsIntoMap/toGoType to a
  [32]byte and serializes as a JSON number array (verified against
  go-ethereum v1.15.11, contract-event-processor's version), so the
  Root field stays [32]byte and the consumer needs no wire change.
rewards(user).totalTokens now merges legacy per-device reward sums with
merkle_claims amounts for the account, so user-level earnings keep
growing after the merkle cutover. Merkle amounts count as earned at
RootSet, claimed or not. History edges and totalCount stay legacy-only;
per-epoch merkle data lives in the merkleRewards query.
@zer0stars

Copy link
Copy Markdown
Member Author

Added in latest push: rewards(user).totalTokens now includes merkle-era claims (earned-at-RootSet semantics, claimed or not) so LifetimeEarnings keeps working after cutover. History edges stay legacy-only — new-era weekly data comes from merkleRewards. Per-vehicle earnings remain historical after cutover (per-vehicle split doesn't exist on-chain in the merkle era — it lives in rewards-api's DB).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant