Skip to content

[ciphertext-arithmetic, program, interface] Add add_to_with_offset and update tests#1116

Merged
samkim-crypto merged 3 commits intosolana-program:mainfrom
samkim-crypto:ctxt-arith
Apr 16, 2026
Merged

[ciphertext-arithmetic, program, interface] Add add_to_with_offset and update tests#1116
samkim-crypto merged 3 commits intosolana-program:mainfrom
samkim-crypto:ctxt-arith

Conversation

@samkim-crypto
Copy link
Copy Markdown
Contributor

@samkim-crypto samkim-crypto commented Apr 16, 2026

Problem

The new solana-zk-sdk v5 and newer ships with extra safety checks on sigma proof verification (solana-program/zk-elgamal-proof#199). Specifically, when the inputs like ElGamal public key, ciphertexts and Pedersen commitments to the zk statement to be proved are all-zero values, then the proof rejects for extra safety.

This is nice and fine, but it does have some implications for confidential transfer extensions:

  • If a user deposits funds and immediately withdraws their entire balance without receiving or sending tokens, their remaining encrypted balance evaluates to an all-zero ciphertext. Because this is the identity point, the SDK rejects the ciphertext-commitment equality proof required for the withdrawal, causing the transaction to fail.
  • The EmptyAccount instruction will fail if the confidential transfer account is already emptied
  • The WithdrawWithheldTokensFromMint will fail if the encrypted fees in the mint is an all zero ciphertext
  • The RotateSupplyElGamalPubkey will fail if the encrypted supply is an all zero ciphertext

Summary of Changes

  • Fixed the Withdraw issue via ciphertext offsets: Modified the Deposit logic to homomorphically add a fixed cryptographic offset (an encryption of 0 with a Pedersen randomness of 1) to the user's balance. This ensures that even when a user's numeric token balance drops to zero after a full withdrawal, the underlying ciphertext retains randomness and does not evaluate to the identity point, allowing the zero-knowledge proofs to succeed seamlessly.

The issue with WithdrawWithheldTokensFromMint and RotateSupplyElGamalPubkey are pretty niche cases, but the case of EmptyAccount can occur in practice, so I added a note on this.

I also adjusted the test suite to align with the new SDK constraints and validate the new Deposit offset behavior.

The CI is failing with cargo audit, so I will rebase after #1115 is merged.

@samkim-crypto samkim-crypto marked this pull request as ready for review April 16, 2026 03:16
@samkim-crypto samkim-crypto requested a review from joncinque April 16, 2026 03:16
Copy link
Copy Markdown
Contributor

@joncinque joncinque left a comment

Choose a reason for hiding this comment

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

Just a couple of layperson questions, likely nothing stopping this from going in.

Comment on lines +17 to +20
const H: PodRistrettoPoint = PodRistrettoPoint([
140, 146, 64, 180, 86, 169, 230, 220, 101, 195, 119, 161, 4, 141, 116, 95, 148, 160, 140, 219,
127, 68, 203, 205, 123, 70, 243, 64, 72, 135, 17, 52,
]);
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.

This is my layperson brain here -- is H typically the term for 0? If so, all good! If not, can you add a comment to that effect? (Assuming I've correctly understood this as 0)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah I can definitely add a clarifying comment here (but let me do it in a follow-up 🙏 ).

H actually isn't 0. In Pedersen commitments, G and H are the two standard generator points on the curve. G is used for the token amount, and H is used for the randomness (blinding factor).

A commitment is mathematically calculated as (amount & G) + (randomness * H). Because we want to create a dummy ciphertext that encrypts an amount of 0 with a fixed randomness of 1 (so the resulting ciphertext isn't an all-zero identity point), the commitment part evaluates to 0 * G + 1 * H, which just leaves us with exactly H.

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.

Thanks for the explanation!

Comment on lines +184 to +188
/// Convert a `PodElGamalPubkey` into `PodRistrettoPoint`
fn elgamal_pubkey_to_ristretto(pubkey: &PodElGamalPubkey) -> PodRistrettoPoint {
let bytes = bytes_of(pubkey);
PodRistrettoPoint(bytes.try_into().unwrap())
}
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.

Layperson here again: I've never seen a conversion from the encryption key to a ristretto point in our code before -- is the idea to use that as the randomness since it'll never be 0?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's right. A Twisted ElGamal ciphertext consists of two halves, the commitment (H from above) and a decryption handle. The decryption handle is calculated as randomness * Pubkey.

Here, we want to intentionally use a fixed randomness of 1. So whenever we deposit, we want to add 1 * H to the commitment and 1 * Pubkey to the decryption handle.

The representation that the addition syscall expects is PodRistrettoPoint, so I added a elgamal_pubkey_to_ristretto function to convert the PodElGamalPubkey as PodRistrettoPoint.

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.

Thanks!

@samkim-crypto samkim-crypto merged commit 51437ee into solana-program:main Apr 16, 2026
40 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.

2 participants