feat(mpp/charge): account-setup helper + TransferWithFee bundle#190
feat(mpp/charge): account-setup helper + TransferWithFee bundle#190lgalabru wants to merge 3 commits into
Conversation
Two additions to the Token-2022 confidential-transfer support: - `derive_confidential_account_setup(signer, mint)` (protocol/confidential.rs): returns the owner's associated token account plus ElGamal public key and AES key (as raw bytes), ready to hand to a test cheatcode — e.g. surfpool's `surfnet_setTokenAccount` — to fabricate a configured confidential account without the real configure/deposit/apply-pending-balance flow. - TransferWithFee bundle support (client/confidential.rs): mints that carry the `ConfidentialTransferFeeConfig` extension (e.g. USDPT) require the fee-bearing transfer variant. `build_confidential_transfer_with_fee_bundle` generates the five split proofs (equality, transfer-amount validity, fee sigma / percentage-with-cap, fee validity, U256 range) and emits `inner_transfer_with_fee`. The builder reads the mint's transfer-fee config and branches to this path; non-fee mints keep the lean 3-proof `Transfer` path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR expands confidential Token-2022 transfer support in the MPP kit. The main changes are:
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (3): Last reviewed commit: "chore: merge main into feat/confidential..." | Re-trigger Greptile |
Addresses review (Greptile "Fee Epoch Can Drift"): the fee parameters are read for the current epoch and baked into the fee proofs. A mismatch with the on-chain TransferWithFee check is only possible when the mint has a *scheduled* fee-schedule change and the bundle is built in the last epoch before it but lands after the rollover. That fails closed (transfer rejected, never mis-settled) and is retriable; mints without a pending change have no drift. Matches standard spl-token tooling. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| // there is no drift. This matches standard spl-token tooling, which likewise reads | ||
| // the current epoch's fee at build time. | ||
| let epoch = rpc | ||
| .get_epoch_info() |
There was a problem hiding this comment.
Epoch Drift Still Fails The fee parameters are still read from
get_epoch_info() when the bundle is built, and the returned bundle has no freshness check before the final TransferWithFee transaction runs. If the bundle is built in the last epoch before a scheduled transfer-fee change, the proof transactions can create contexts for the old fee schedule, then the final transfer can land after rollover and be verified by Token-2022 against the new fee schedule. That rejects the payment after the earlier bundle transactions have already run, so documenting the narrow case does not prevent the failed transfer.
EfeDurmaz16
left a comment
There was a problem hiding this comment.
Review summary — adversarial line-by-line pass
Verdict: no approve-blocking findings. Feature-gated (confidential) test-env helper plus the fee-bearing confidential-transfer builder; both sound.
derive_confidential_account_setupis safely test-scoped. Keys are deterministically re-derived from the owner's own wallet signature over the ATA seed (spl-token convention), never separately stored, and it exports only the owner's own ElGamal pubkey + AES key for a test cheatcode to fabricate a decryptable balance the same owner later spends. No third-party secret leaves the owner, and the whole path is behind theconfidentialfeature.- TransferWithFee / fee-epoch handling is correct. Fee params are read for the current epoch and baked into the fee proofs; the on-chain
TransferWithFeere-derives at execution. The single drift window (a scheduled fee-schedule change where the bundle is built pre-rollover and lands post-rollover) fails closed and is retriable by rebuild, andolder == newermints (USDPT) never drift. Pre-flight checks (credits/approval/balance) run before proof generation, and the litesvm tests verify the proofs against the real ZK program plus recipient/auditor recovery.
Blockers: none. Follow-ups: none.
EfeDurmaz16
left a comment
There was a problem hiding this comment.
Approve
Ready. Re-checked for completeness after the first pass:
- CI is 41/41 green; no
TODO/unimplemented!/stubs. build_confidential_transfer_bundle(the TransferWithFee bundle) is wired into the production confidential-charge path (confidential_charge_payload, client/confidential.rs:749) and exported fromclient/mod.rs— not orphaned. litesvm tests verify the proofs against the real ZK program plus recipient/auditor recovery.- Key material stays owner-scoped and behind the
confidentialfeature (verified earlier).
Optional follow-up (non-blocking): derive_confidential_account_setup has no in-repo caller and no direct unit test — by design its consumer is an out-of-repo test cheatcode (surfpool surfnet_setTokenAccount). A small smoke test asserting the derived ATA/keys round-trip with what the client re-derives would close the loop, but it's a documented primitive built from already-tested pieces, so not blocking.
LGTM.
|
@copilot resolve the merge conflicts in this pull request |
Merged |
Summary
Two additions to the Token-2022 confidential-transfer support in
solana-mpp(confidentialfeature), needed to drive a confidential transfer end-to-end against a fee-bearing mint (USDPT) from a test environment.1.
derive_confidential_account_setup(signer, mint)—crates/mpp/src/protocol/confidential.rsReturns everything a test cheatcode needs to stand up a configured confidential account for an owner: the associated token account (Token-2022) plus the owner's ElGamal public key and AES key as raw bytes.
The keys are derived deterministically from the owner's wallet signature (via the existing
derive_confidential_keys), so whatever fabricates the account encrypts to keys the owner can later re-derive and spend. This is what surfpool's newsurfnet_setTokenAccountconfidential option consumes (see references) and what thepayCLI'sconfidential setupcalls.2.
TransferWithFeebundle support —crates/mpp/src/client/confidential.rsA mint that carries the
ConfidentialTransferFeeConfigextension (e.g. USDPT) requires the fee-bearing transfer variant — a plain confidentialTransferis rejected by the Token-2022 program (surfaces asInvalidInstructionData), even when the fee rate is 0%.build_confidential_transfer_with_fee_bundleimplements that path:TransferFeeConfig(current-epoch rate + cap) andConfidentialTransferFeeConfig(withdraw-withheld-authority ElGamal pubkey);transfer_with_fee_split_proof_data— equality, transfer-amount validity, fee sigma (percentage-with-cap), fee validity, and a U256 range proof (vs three for the plain transfer);inner_transfer_with_feereferencing the five proof-context accounts.build_confidential_transfer_bundlenow branches on whether the mint has a transfer-fee config; non-fee mints keep the lean 3-proofTransferpath unchanged.Why
Without (1) there's no ergonomic way to bootstrap a confidential account for tests (the real flow is
ConfigureAccount → Deposit → ApplyPendingBalance, 3+ txs needing the ZK ElGamal Proof program and owner signatures). Without (2), confidential transfers simply fail on any fee-bearing mint — which includes USDPT, the flagship confidential stablecoin.Verified end-to-end on a local stack (surfpool forking the real USDPT mint + a local pay-api): a confidential
TransferWithFeesettled to afinalizedsignature with the recipient'spendingBalanceCreditCounterincrementing.Notes
cargo build -p solana-mpp --features confidential✓;cargo fmt --check✓.Related work (cross-repo)
surfnet_setTokenAccountconfidential cheatcode that consumesderive_confidential_account_setup's output: solana-foundation/surfpool#705 (its description is a full ZK/confidential-transfer onboarding). Follow-up DX leads: surfpool#706. Docs: surfpool-web-ui#30.pay confidential setup+ confidential-aware send, on branchfeat/confidential-send(depends on this PR).localnet) not the config network key (sandbox), and the SOL/USD price oracle should be fetched lazily (skipped for confidential sends). Local-only so far; the cluster fix likely affects the hosted sandbox.🤖 Generated with Claude Code