#430 Feature: proof-of-uniqueness leaderboard-safe attestation for ai… - #497
Open
felladaniel36-hash wants to merge 2 commits into
Conversation
|
@felladaniel36-hash Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
…n for airdrops/quotas FIXED
felladaniel36-hash
force-pushed
the
#430-Feature--proof-of-uniqueness-leaderboard-safe-attestation-for-airdrops/quotas-FIX
branch
from
September 1, 2026 17:49
b74ee8d to
b4ee387
Compare
Author
|
@Psalmuel01 PLEASE REVIEW |
…aderboard-safe-attestation-for-airdrops/quotas-FIX
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat(contracts,sdk): verified-human-once claims for airdrops/quotas FIXED
What does this PR do?
Turns the per-app nullifier idea into a working, plug-and-play anti-Sybil distribution
pattern — and supplies the primitive it was supposed to build on, which did not actually
exist in the repo yet (
grep -ri nullifierreturned zero hits; every gate wasaddress-scoped, so one person could pass the same credential from N wallets N times).
ProofRegistrynow records the proof's identity commitment (public-input field 0 —wallet-independent, stable per human) under a new additive
DataKey::Subjectkey andexposes
identity_commitment()andapp_nullifier() = sha256(commitment ‖ app_scope),which returns
Noneunless the claim is currently valid. On top of that sitscontracts/human_airdrop, a small reference distribution contract: campaigns carry ascope, credential rule, threshold, issuer allowlist, amount, budget, claim cap and window,
and a
Spent(campaign, nullifier)entry enforces one claim per human per campaign.Distributors integrate either by calling
claim()(reference payout) orconsume()—which burns the human's one-shot claim under the consumer contract's own auth and returns
the nullifier, leaving payout economics entirely to the caller. Ships with the
createHumanClaim()SDK helper (plus a pure, isomorphicderiveNullifier()pinned to thecontract by a shared sha256 test vector), a worked HumanDrop demo in the apps gallery
at
/apps/humandrop, anddocs/ANTI_SYBIL.mddocumenting the guarantees and theirlimits.
Closes #430
Type of change
Merge requirements
cargo test(contracts) 112 passed / 0 failed;pnpm tsc --noEmit(frontend + SDK) clean;pnpm build(frontend) green, 20/20 static pages;cargo clippy --workspace --all-targets -- -D warningsclean;cargo build --release --target wasm32v1-noneproduceshuman_airdrop.wasm(32 742 B) andproof_registry.wasm(46 338 B); frontendvitest run325 passed / 0 failedfixtures/<type>/artifacts updated — N/A, no circuit changed. No new artifacts needed; the new tests consume the existingfixtures/kyc/andfixtures/funds/proofs unmodifiedNEXT_PUBLIC_prefix on server-only env vars — new SDK config (contractId,registryId) is passed as an argument tocreateHumanClaim(); no new env var is introduced anywhereprehash:falsepreserved on any issuer signing path touched — no issuer signing path touched (packages/issueruntouched)airdrop.tsandHumanDropPanel.tsxare read-only/simulation and derivation only, the SDK never signs or submits✅ Merge requirements
Notes for reviewers
1. The
ProofRegistrychange is deliberately additive — please sanity-check that call.The commitment goes into a new
DataKey::Subject(Address, Symbol)entry rather than a7th field on
ProofRecord. Adding a field would break decoding of every deployed recordand force another
migrate_recordpath; a separate key means existing records decodeunchanged, no migration runs, and no existing function signature moves. Cost: one extra
persistent write per
submit_proof(TTL-bumped in lockstep with the proof entry).revoke_proof/revoke_allnow also delete theSubjectentry for storage hygiene —harmless for the gate, since a claim already requires a live proof.
2. Why sha256 and not Poseidon2 for the nullifier.
sha256is a host function onSoroban (cheap, no in-contract circuit arithmetic) and is trivially reproducible in the
browser via WebCrypto, which is what makes the SDK's offline
deriveNullifier()possible.The nullifier is a domain-separated hash of an already-hiding commitment, so it needs
preimage resistance, not algebraic friendliness. If you'd prefer Poseidon2 for
circuit-composability later (e.g. proving nullifier correctness in-circuit), say so — it's
a one-function change, but it would break the current off-chain derivation.
3. Contract ⇄ SDK derivation is pinned by a shared test vector. The same
sha256(commitment ‖ scope)valueac90ac63…93a091(derived from the realfixtures/kyccommitment) is asserted independently in
contracts/human_airdrop/src/test.rs,contracts/proof_registry/src/test.rs,frontend/packages/sdk/src/airdrop.test.ts, andrendered by the gallery demo. If either side of the derivation drifts, at least two suites
go red rather than the two silently disagreeing in production.
4.
consume()auth model.consumer.require_auth()means only the contract beingconsumed for can burn a claim — a third party can't grief a human by spending their
nullifier. Covered by
consume_requires_consumer_authand by the in-testDemoDistributorcontract, which is the exact integration shape documented inANTI_SYBIL.md. Worth a second pair of eyes on whether we also want an admin allowlist ofconsumer addresses; I left it out because
require_authalready binds the burn to thecaller and an allowlist adds admin surface without adding a guarantee.
5. Read the limits table before approving the claim in the title. The pattern is
one-claim-per-credential; "per human" is inherited from the issuer's own de-duplication.
docs/ANTI_SYBIL.md§4 enumerates this honestly: multiple registered issuers, saltedre-issuance, weak KYC, credential sale/transfer, and issuer key compromise each break
uniqueness, with mitigations (
trusted_issuersnarrowing, deterministic salts, campaignwindows, key rotation). I'd rather ship the caveats loudly than let an integrator assume
proof-of-personhood.
6. Demo is a faithful simulation, not a live deployment.
HumanDropPanelderives realnullifiers with the SDK (same bytes the contract computes) but keeps the "spent" set in
React state, so
/apps/humandropworks against any deployment or none. Note the secondwallet's Claim button stays enabled on purpose — the point of the demo is watching the
rejection happen (
Error #8 AlreadyClaimed), not hiding it behind a disabled control.Contract IDs land in
DEPLOYMENTS.md(placeholder rows added) once deployed.7. Balance ledger is a demo, flagged as such.
claim()credits an internal i128 ledgerexactly like
gated_pool, not a token transfer. Production integrators should useconsume()from their own distributor, or swapset_balancefor atoken::Clienttransfer — called out in the module docs and in
ANTI_SYBIL.md§4.8. Pre-existing failures I did not touch.
packages/sdk/test/integration.test.tshas2 failing tests (
buildBadgeEmbedCode is not defined— a missing import in that test file).Verified via
git stashthat they fail identically on a clean checkout; out of scope here,happy to fix in a follow-up. Likewise
cargo fmt --all --checkreports diffs in files thisPR doesn't touch (the repo isn't fmt-clean at the pinned 1.93.1 toolchain); all new and
changed code is fmt-clean.
9. Test snapshots. 19 new
human_airdropsnapshots plus refreshed snapshots forproof_registry/gated_pool/credential_verifier— the refreshes are the expectedconsequence of the new
Subjectledger entry, all machine-generated bycargo test, nonehand-edited.
Files
Created (9 hand-written, 2 659 LOC):
contracts/human_airdrop/{Cargo.toml,Makefile,src/lib.rs,src/test.rs},docs/ANTI_SYBIL.md,frontend/packages/sdk/src/{airdrop.ts,airdrop.test.ts},frontend/components/{HumanDropPanel.tsx,HumanDropPanel.test.tsx}Modified (12, +512/−8):
contracts/proof_registry/src/{lib.rs,test.rs},frontend/packages/sdk/{src/index.ts,README.md},frontend/lib/protocols.tsx,frontend/app/apps/[id]/page.tsx,README.md,DEPLOYMENTS.md,docs/EVENTS.md,docs/contract-error-codes.md,.github/workflows/reproducible-build.yml,Cargo.lock