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

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).

zer0stars added a commit that referenced this pull request Sep 14, 2026
Brings feature/merkle-claims-indexing (cf6065c, 11 commits) into
r2-device-definitions with its history intact.

Conflict resolution:
- settings.sample.yaml was the only textual conflict. Kept r2's
  DEFINITIONS_CATALOG_URL in place of TABLELAND_API_GATEWAY (that setting
  no longer exists in config.Settings), kept ETHEREUM_RPC_URL, and
  appended #186's MERKLE_DISTRIBUTOR_ADDR and MERKLE_TREE_ALLOWED_HOST
  with their comments.

Files changed on both sides that git merged cleanly, checked by hand:
- internal/config/settings.go: DefinitionsCatalogURL and
  DefinitionsMinCount from r2, MerkleDistributorAddr and
  MerkleTreeAllowedHost from #186.
- internal/services/contracts_events_consumer.go: r2's connection SACD
  and PermissionsRenounced handling alongside #186's merkle handler, the
  shared HTTP client (merkle.FetchTimeout, still 10s) and the
  MerkleDistributor case in the contract address switch.
- graph/resolver.go: connectionsacd and merkle repositories both wired.
- gqlgen.yml: Connection.sacds resolver and the MerklePool/MerkleReward
  resolvers both present.
- models/boil_table_names.go: ConnectionSacds plus MerkleClaims,
  MerklePools and MerkleRoots.
- charts/identity-api/values.yaml and values-prod.yaml: r2's image tags
  and DEFINITIONS_CATALOG_URL/DEFINITIONS_MIN_COUNT plus #186's
  MERKLE_DISTRIBUTOR_ADDR/MERKLE_TREE_ALLOWED_HOST.

Regeneration:
- go run github.com/99designs/gqlgen generate (v0.17.89, pinned in
  go.mod) reproduced graph/generated.go, graph/model/models_gen.go and
  the resolver files byte for byte, so they are committed as merged.
- go -C graph run github.com/DIMO-Network/server-garage/cmd/mcpgen
  -schema ./schema/ -prefix identity -out mcp_tools_gen.go -package graph
  (the go:generate directive in graph/resolver.go), and the go mod tidy
  that gqlgen runs, produce changes r2-device-definitions already needed
  before this merge. Those land in the next commit.
- sqlboiler models are not regenerated (needs a live database);
  boil_table_names.go lists both sides' tables.

Migration check:
- r2-device-definitions carries migrations/00049_create_connection_sacds.sql
  (#185, on main, released in v0.6.6 and v0.6.7). #186 adds
  migrations/00049_create_merkle_tables.sql, which was never built from
  main. goose rejects duplicate versions, so a follow-up commit renumbers
  the merkle migration to 00050.
zer0stars added a commit that referenced this pull request Sep 14, 2026
- graph/mcp_tools_gen.go: rerun mcpgen (go:generate in graph/resolver.go).
  CondensedSchema now includes Connection.sacds and
  VehiclesFilter.connection, which this branch added to the schema
  without regenerating. The merkle additions were already present.
- go.mod, go.sum: go mod tidy, which gqlgen generate runs. Drops goqu,
  httpmock, retry-go and x/time, which nothing imports since the
  Tableland client was removed, and records go-sqlmock as indirect.
  go mod tidy on this branch before the merge gives the same diff.
- internal/config/settings.go: gofmt -s. The Settings struct has been
  misaligned since DefinitionsMinCount was added.
@zer0stars

Copy link
Copy Markdown
Member Author

Folded into #188 so identity-api has one open PR for this work. The 11 commits are preserved there through merge commit 0876169. The Merkle tables migration is renumbered to 00050, because 00049 is already the connection SACDs migration on main.

@zer0stars

Copy link
Copy Markdown
Member Author

Reopened. Folding this into #188 was a mistake: #188 is scoped to device definitions and has been reset to its pre-merge commit, so none of this PR's code is in it. This branch is unchanged. Before merging, note that migrations/00049_create_merkle_tables.sql collides with 00049_create_connection_sacds.sql, which is now on main, so it needs renumbering to 00050.

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