Skip to content

feat(contract): implement immutable payment privacy semantics - #135

Merged
DioChuks merged 24 commits into
BuidlZone-Labs:mainfrom
Depo-dev:payment-privacy-v2
Jul 23, 2026
Merged

feat(contract): implement immutable payment privacy semantics#135
DioChuks merged 24 commits into
BuidlZone-Labs:mainfrom
Depo-dev:payment-privacy-v2

Conversation

@Depo-dev

@Depo-dev Depo-dev commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements enforceable on-chain semantics for the three payment privacy levels from the spec (Anonymous → Private → Standard), so the privacy slider maps to real contract behaviour instead of UI-only state. Rebased onto the latest main with all merge conflicts resolved.

Each PaymentRecord / Ticket now stores exactly one identity representation, chosen by its privacy level:

Level Stored on-chain On-chain refund
Standard raw payer address yes
Private salted wallet hash + one-time stealth delivery key no (off-chain via stealth key)
Anonymous nullifier commitment only no (off-chain via commitment)

Key behaviours

  • Immutable — the privacy level is fixed at purchase; there is no on-chain mutation path.
  • Field exclusivity — supplying material for a different level (e.g. a commitment on a Standard payment) is rejected with PrivacyLevelMismatch.
  • No wallet linkage for Anonymous — nonce replay-protection and per-user ticket keys derive from the nullifier commitment, never a payer hash; commitment uniqueness blocks reuse.
  • Privacy-aware events — payment / ticket / refund events emit a masked identity (full / hashed / commitment) taken from the payment's own privacy level, never a raw address for Private/Anonymous.
  • Refunds preserve privacy — Anonymous/Private payments carry no on-chain address, so on-chain refund paths return RefundNotAllowed; settlement happens off-chain.
  • Cross-contract registrationregister_for_event enforces the event's configured payment privacy rather than silently downgrading Private/Anonymous events to Standard.

CodeRabbit review items addressed

  • Enforce privacy-material exclusivity in build_payment_record.
  • Stop persisting payer-derived keys for Anonymous nonce / ticket counters.
  • Reject (don't silently downgrade) Private/Anonymous events in register_for_event.
  • Event tests assert the emitted MaskedAddress, not just stored state.
  • Rejected-purchase tests assert balances are unchanged (no silent debit).

Tests

New test_privacy_semantics.rs plus updates covering field exclusivity, missing-field rejection, nullifier reuse, refund guards, immutability, emitted-event identity, and balance-safety on failed purchases. Full workspace suite green; cargo fmt and cargo clippy -D warnings clean.

Known limitation

require_auth() runs for every privacy level because the token transfer needs wallet authorization, so the submitting account is visible in the Stellar transaction envelope regardless of level. Anonymous/Private privacy applies at the contract storage and event layer only; transaction-level anonymity would require a relayer / meta-transaction model, which is out of scope for this issue.

Closes #117

Summary by CodeRabbit

  • New Features
    • Added end-to-end payment privacy handling across Standard, Private, and Anonymous flows, including privacy-appropriate identity in emitted payment/ticket events.
    • Enhanced privacy checks for event registration and zk attendance, rejecting unsupported private/anonymous settlement paths.
  • Bug Fixes
    • Added new error reporting for privacy/settlement and payment privacy validation mismatches.
    • Updated refunds/resale/payment identity rules to preserve privacy level and avoid exposing unavailable payer data.
  • Tests
    • Added and updated privacy semantics and registration rejection coverage.

Depo-dev added 21 commits July 22, 2026 00:55
MissingNullifierCommitment, MissingStealthDeliveryKey and PrivacyLevelMismatch
for the enforceable payment-privacy semantics.
Store exactly one identity representation per privacy level: optional payer
(Standard), hashed_wallet + stealth_delivery_key (Private), nullifier_commitment
(Anonymous).
ProcessedNonceHash, UserEventTicketsHash and SpentNullifier keys plus the
nonce-hash and nullifier helper functions.
Payment, ticket and refund events now emit a masked identity (full address,
wallet hash or nullifier commitment) taken from the payment's own privacy
level, never a raw address for Private/Anonymous.
PaymentParams carries the per-level privacy inputs; build_payment_record maps
each level to exactly one identity representation and rejects cross-level
material with PrivacyLevelMismatch; build_ticket and private_wallet_hash added.
Standard keys by raw address, Private by a wallet hash, Anonymous by its
nullifier commitment so no wallet-linked value is read from a ledger key.
Persist the built payment/ticket via the privacy-aware paths, enforce nullifier
uniqueness for Anonymous payments, and key nonce/ticket writes without ever
storing a payer-derived value for Anonymous.
…ints

pay_for_ticket accepts nullifier_commitment and stealth_delivery_key; the
commitment and options variants pass the appropriate None values.
Refunds require an on-chain payer, so Anonymous/Private payments return
RefundNotAllowed; the refund event derives its masked identity from the stored
payment.
Resolve the ticket owner and refund recipient as optional and return
RefundNotAllowed when no on-chain address exists.
…paths

bind_email_commitment, list_ticket_for_resale and buy_resale_ticket compare
against the now-optional payer/owner; only address-bound (Standard) records are
eligible.
Signals that the cross-contract registration path cannot settle a Private or
Anonymous payment.
Derive the event's configured privacy and reject Private/Anonymous paid
registrations instead of silently settling them as Standard and storing the
raw attendee. Addresses CodeRabbit review.
Match the extended pay_for_ticket signature; the event contract is the payer,
so Standard with no privacy material is correct.
Covers per-level storage, field exclusivity, nullifier reuse, refund guards,
immutability, emitted masked identity (asserted from env.events()), and
balance-safety on rejected purchases (issue BuidlZone-Labs#117).
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Depo-dev, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 48 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1f870c50-283f-426b-b496-6612e3874e68

📥 Commits

Reviewing files that changed from the base of the PR and between d35c5db and 6c4d771.

📒 Files selected for processing (1)
  • contracts/event/src/test_claims.rs
📝 Walkthrough

Walkthrough

Payment privacy semantics now flow through payment records, tickets, storage, events, refunds, resale, and event registration. Standard, Private, and Anonymous payments use distinct identity material and validation rules.

Changes

Payment privacy semantics

Layer / File(s) Summary
Privacy contracts and storage
contracts/payments/src/types.rs, contracts/payments/src/errors.rs, contracts/payments/src/storage.rs
Payment and ticket identities are optional, with privacy-specific errors and hashed nonce, ticket-counter, and nullifier storage.
Privacy-aware payment creation and events
contracts/payments/src/lib.rs, contracts/payments/src/events.rs
Payment creation validates privacy inputs, stores mode-specific identity data, applies privacy-aware replay and ticket limits, and emits masked identities from stored records.
Refund, resale, and event integration
contracts/payments/src/lib.rs, contracts/event/src/lib.rs, contracts/event/src/errors.rs
Private and Anonymous refunds are rejected, resale and authorization checks use optional owners/payers, and cross-contract event registration rejects unsupported privacy settlement.
Privacy behavior and API validation tests
contracts/payments/src/test_privacy_semantics.rs, contracts/payments/src/test.rs, contracts/payments/src/*_test.rs, contracts/event/src/integration_tests.rs, contracts/event/src/test.rs
Tests cover privacy storage, event masking, validation, nullifier reuse, refund behavior, updated API calls, and optional identity assertions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant PaymentsContract
  participant PaymentStorage
  participant PaymentEvents
  Client->>PaymentsContract: pay_for_ticket with privacy inputs
  PaymentsContract->>PaymentStorage: validate and store privacy-specific identity
  PaymentsContract->>PaymentEvents: emit masked payment and ticket events
Loading

Possibly related PRs

Suggested reviewers: diochuks, codebestia, codeze-us

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is useful but misses most required template sections, including storage impact, cross-contract impact, privacy checklist, and acceptance criteria. Rewrite the PR description using the repository template and fill in the mandatory sections with concrete details for #117.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately describes the main change: immutable payment privacy semantics in the contracts.
Linked Issues check ✅ Passed The changes implement #117’s required semantics: anonymous uses nullifiers, private uses hashed wallets and stealth keys, standard keeps raw addresses, and privacy is immutable.
Out of Scope Changes check ✅ Passed No clearly unrelated code changes are evidenced in the summary; the visible changes align with the privacy semantics issue.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Depo-dev
Depo-dev marked this pull request as ready for review July 22, 2026 00:06
ethnum 1.5.2 fails to compile with E0512 (invalid mem::transmute) on newer
stable rustc, breaking CI Build/Clippy/Test. 1.5.3 replaces the transmutes with
safe construction. Lock-only change; semver-compatible with soroban-env-common.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
contracts/payments/src/lib.rs (1)

335-355: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Token transfer executes before the new privacy-material validation and nullifier-uniqueness check.

try_transfer (unchanged, lines ~337-341) runs before build_payment_record's MissingNullifierCommitment/MissingStealthDeliveryKey/PrivacyLevelMismatch checks and before the has_nullifier dedup check. Every other validation in this function (nonce, amount, event status, ticket cap) already runs before the transfer — this is the one new check ordering that doesn't follow that pattern. Because Soroban reverts all state on an Err return, this isn't a fund-safety bug (confirmed by test_anonymous_requires_commitment/test_private_requires_stealth_key asserting unchanged balances), but it means a cross-contract token transfer call is executed and paid for even on invocations that are guaranteed to fail on privacy-material shape alone.

Consider validating the privacy-material shape (the checks currently inside build_payment_record) before the try_transfer call, and moving the nullifier-uniqueness check up alongside the nonce dedup check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contracts/payments/src/lib.rs` around lines 335 - 355, Reorder validation in
the payment flow so privacy-material checks currently performed by
build_payment_record and the nullifier uniqueness check occur before
token_client.try_transfer. Preserve the existing validation behavior and ensure
payment construction/storage still uses the validated values after the transfer
succeeds.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contracts/event/src/lib.rs`:
- Around line 845-855: Enforce the configured event privacy before any
registration, ticket, or payment state mutation: in contracts/event/src/lib.rs
lines 845-855, move the PrivacyLevel::Private/Anonymous rejection outside the
tier.price > 0 branch so free registrations are rejected too; in
contracts/event/src/lib.rs lines 1150-1151, add the same privacy gate before the
Standard payment/ticket flow in verify_and_attend. Keep Standard events
proceeding through their existing paths.

---

Nitpick comments:
In `@contracts/payments/src/lib.rs`:
- Around line 335-355: Reorder validation in the payment flow so
privacy-material checks currently performed by build_payment_record and the
nullifier uniqueness check occur before token_client.try_transfer. Preserve the
existing validation behavior and ensure payment construction/storage still uses
the validated values after the transfer succeeds.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2dd33783-b85b-4d41-9463-d0387fcc56fd

📥 Commits

Reviewing files that changed from the base of the PR and between 0a267c6 and d64ca04.

📒 Files selected for processing (13)
  • contracts/event/src/errors.rs
  • contracts/event/src/integration_tests.rs
  • contracts/event/src/lib.rs
  • contracts/payments/src/errors.rs
  • contracts/payments/src/events.rs
  • contracts/payments/src/lib.rs
  • contracts/payments/src/multi_token_test.rs
  • contracts/payments/src/receipt_commitment_test.rs
  • contracts/payments/src/revenue_split_test.rs
  • contracts/payments/src/storage.rs
  • contracts/payments/src/test.rs
  • contracts/payments/src/test_privacy_semantics.rs
  • contracts/payments/src/types.rs

Comment thread contracts/event/src/lib.rs Outdated
@DioChuks
DioChuks self-requested a review July 23, 2026 15:55
@DioChuks

Copy link
Copy Markdown
Contributor

@Depo-dev kindly resolve the major security & privacy concern

The privacy gate previously ran only for paid tiers in register_for_event, so
free Private/Anonymous registrations still stored the raw attendee, and
verify_and_attend had no gate at all. Extract a shared require_settleable_privacy
helper and call it before any registration, ticket, or payment state is mutated
in both flows. Addresses CodeRabbit review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contracts/event/src/test.rs`:
- Around line 742-754: The existing test only exercises paid tier 0 and does not
cover the free-registration privacy bypass. Add a separate Private or Anonymous
event scenario using the free-tier registration path, such as the appropriate
free-tier amount or configuration, and assert that registration is rejected with
EventError::PaymentPrivacyUnsupported without storing attendee or ticket state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cbd1541c-2cf7-42d7-81ff-f24475d9aa64

📥 Commits

Reviewing files that changed from the base of the PR and between d64ca04 and d35c5db.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • contracts/event/src/lib.rs
  • contracts/event/src/test.rs

Comment thread contracts/event/src/test.rs
@DioChuks

Copy link
Copy Markdown
Contributor

@Depo-dev let's work on some maintainability & code quality, pls address the minor change

Add a price-0 Private-event case proving the free-registration path is rejected
too; the existing paid-tier test would pass even with the old paid-only gate.
Addresses CodeRabbit review.

@DioChuks DioChuks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well done 👍

@DioChuks
DioChuks merged commit e4bdf79 into BuidlZone-Labs:main Jul 23, 2026
5 checks passed
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.

Define on-chain semantics for Anonymous vs Private vs Standard payment privacy levels

2 participants