Skip to content

feat: confidential rewards for continuous pools (Token-2022 CT)#17

Closed
dev-jodee wants to merge 9 commits intomainfrom
feat/confidential-rewards-continuous
Closed

feat: confidential rewards for continuous pools (Token-2022 CT)#17
dev-jodee wants to merge 9 commits intomainfrom
feat/confidential-rewards-continuous

Conversation

@dev-jodee
Copy link
Collaborator

Summary

Adds opt-in confidential reward distribution to continuous reward pools using the Token-2022 ConfidentialTransfer extension.

  • Pool creators set confidential_rewards: u8 flag on CreateContinuousPool — no change to RewardPool::DATA_LEN (occupies an existing padding byte)
  • ClaimContinuous and ContinuousOptOut perform CT transfers using client-supplied ZK proof context accounts (equality + validity + range proofs)
  • DistributeContinuousReward issues a ConfidentialDeposit CPI after TransferChecked for confidential pools
  • ContinuousOptIn validates that the user's token account has CT configured when the pool is confidential
  • New confidential_transfer.rs CPI builder in the program
  • New confidential_helpers.rs in the Rust client: ConfidentialVaultState, configure_account(), apply_pending_balance()

ZK-ops Token-2022 binary

LiteSVM ships Token-2022 compiled without --features zk-ops, causing ConfidentialDeposit/ConfidentialTransfer to return InvalidInstructionData. Tests load a ZK-ops-enabled build sourced from Surfpool's public zk-edge cluster (Apache-2.0, 619 KB) committed as tests/integration-tests/src/programs/spl_token_2022_zk.so.

Note: The ZK ElGamal proof program is currently disabled on mainnet/public devnet (post-audit). Full CT integration tests require a ZK-enabled validator (Surfpool zk-edge or internal test nodes). LiteSVM's default FeatureSet already activates zk_elgamal_proof_program_enabled so the tests pass locally with the binary above.

Tests

cargo fmt --all                          → clean
cargo clippy -D warnings                → clean
cargo test -p rewards-program           → 352 passed
cargo test -p tests-rewards-program     → 301 passed

5 new CT integration tests + ZK ElGamal probe test.

Files changed

Area Files
Program instructions claim, create_pool, distribute_reward, opt_in, opt_out (accounts + data + processor)
Program state reward_pool.rsconfidential_rewards: u8 field
Program utils confidential_transfer.rs (new), continuous_utils.rs, mod.rs
Rust client lib.rs, confidential_helpers.rs (new)
Integration tests fixtures, 5 new test files, setup.rs, spl_token_2022_zk.so
IDL / docs rewards_program.json, CU_BENCHMARKS.md

@amilz amilz self-requested a review March 13, 2026 18:55
Adds opt-in confidential reward distribution to continuous reward pools
using Token-2022 ConfidentialTransfer. Pool creators set `confidential_rewards`
flag; claim/opt-out perform CT transfers with client-supplied ZK proofs;
distribute does a CT deposit after the normal TransferChecked CPI.

Includes ZK-ops-enabled Token-2022 binary fixture for integration tests
(619 KB, Apache-2.0, from Surfpool zk-edge cluster) since LiteSVM's
bundled binary lacks `--features zk-ops`.

- RewardPool gains `confidential_rewards: u8` (occupies existing padding byte, DATA_LEN unchanged)
- New `confidential_transfer.rs` CPI builder
- `ConfidentialVaultState` + `configure_account`/`apply_pending_balance` client helpers
- 5 new CT integration tests + ZK ElGamal probe test (301 tests total pass)
- Add `#[cfg(feature = "confidential")]` to `builder_extensions.rs`
  so it is consistent with `confidential_helpers.rs`
- `cargo fmt --all` cleanup
- distribute: add expected_pending_balance_credit_counter to instruction data
  (hardcoded 0 would fail in production when deposit increments counter)
- revoke_user: add CT transfer path (TransferChecked fails on CT vaults)
- close_pool: guard against closing with unclaimed CT rewards
- claim/opt_out: validate proof context accounts are owned by ZK ElGamal program
- opt_in: check CT ATA size >= 460 bytes before accepting
- confidential_transfer.rs: replace MaybeUninit with zero-initialized arrays
- confidential_helpers: clarify effective_amount vs raw amount in docstring
@dev-jodee dev-jodee force-pushed the feat/confidential-rewards-continuous branch from 38fbf87 to d1b9f5e Compare March 16, 2026 15:02
…ousReward

The program's data.rs already required 16 bytes (amount + counter) but
definition.rs and the generated client only serialised 8 bytes (amount),
causing InvalidInstructionData on every distribute call.

- Add expected_pending_balance_credit_counter: u64 to DistributeContinuousReward
  in definition.rs; regenerate IDL and client
- Pass counter=0 in all non-confidential test builders
- Pass counter=1 in confidential test builders (one ConfidentialDeposit fires
  in the same tx, incrementing the vault counter from 0 to 1)
- Raise fake Token-2022 ATA size in lifecycle test from 300 to 460 bytes to
  satisfy the updated CT opt-in threshold (>= 460)
- Refactor CT branching into shared utils: transfer_reward_tokens and
  maybe_confidential_deposit consolidate the if/else blocks that were
  duplicated across Claim, OptOut, RevokeContinuousUser, and Distribute
- Add ConfidentialWithdraw and ConfidentialEmptyAccount CPIs to enable
  CloseContinuousPool on CT pools with unclaimed rewards:
  Withdraw converts CT balance to plaintext, EmptyAccount zeros the
  ciphertext bytes (required for CloseAccount), TransferChecked sweeps
- close_pool accepts optional trailing data (new_decryptable) and three
  ZK proof context accounts (equality, range, zero_ciphertext) when
  pool.confidential_rewards != 0 and total_distributed != total_claimed
- Add CloseContinuousPoolBuilder::confidential_close and
  ConfidentialVaultState::prepare_close to the client helpers
- 336 integration tests pass, 377 unit tests pass
pub reward_token_program: &'a AccountView,
pub event_authority: &'a AccountView,
pub program: &'a AccountView,
/// Required when `pool.confidential_rewards != 0`.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Util to have those extra 3 accounts or macro, since it's used across all the different "accounts.rs" for each instruction that supports it

tracked_token_program,
)?;

let (equality_proof_context, ciphertext_validity_proof_context, range_proof_context) = if accounts.len() >= 15 {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

shared utils since its used by all "accounts.rs"

let amount = u64::from_le_bytes(data[0..8].try_into().map_err(|_| ProgramError::InvalidInstructionData)?);

Ok(Self { amount })
let confidential_transfer_bytes = if data.len() >= Self::LEN + CONFIDENTIAL_TRANSFER_DATA_LEN {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

shared util that takes expected len, since used across instructions supporting conf balance

// For confidential pools with unclaimed rewards: convert the vault's CT available
// balance back to plaintext so the existing TransferChecked sweep can handle it.
if pool.confidential_rewards != 0 && pool.total_distributed != pool.total_claimed {
let withdrawal_amount =
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

if pool confidential, should have all the conf balance utils in its own util file for confidential logic, since it might be reused across the code

/// where `counter` is the vault's current `pending_balance_credit_counter`
/// read off-chain before building the transaction (one `ConfidentialDeposit`
/// increments it in the same tx).
pub expected_pending_balance_credit_counter: u64,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Wondering should we have instructions for conf balances specifically? Feels like we're adding a lot of option<> to code that most of the time might not need it

// extension (base 165 bytes + CT extension ~295 bytes = ~460 bytes minimum). Accounts
// smaller than this cannot have had ConfigureAccount called on them.
verify_owned_by(reward_ata, &pinocchio_token_2022::ID)?;
if reward_ata.data_len() < 460 {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

should be util in conf util

@@ -0,0 +1,86 @@
/// Probe: does LiteSVM's ZK ElGamal proof program work end-to-end?
///
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this should be removed completely (the file)

@dev-jodee
Copy link
Collaborator Author

Closing, will most likely become it's own program at some point instead.

@dev-jodee dev-jodee closed this Mar 20, 2026
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