Encryption
The confidential extension program makes use of a public key encryption scheme and an authenticated symmetric encryption scheme. For public key encryption, the program uses the twisted ElGamal encryption scheme. For symmetric encryption, it uses AES-GCM-SIV.
Twisted ElGamal Encryption
The twisted ElGamal encryption scheme is a simple variant of the standard ElGamal encryption scheme where a ciphertext is divided into two components:
- A Pedersen commitment of the encrypted message. This component is independent of the public key.
- A "decryption handle" that binds the encryption randomness with respect to a specific ElGamal public key. This component is independent of the actual encrypted message.
The structure of the twisted ElGamal ciphertexts simplifies their design of some zero-knowledge proof systems. Furthermore, since the encrypted messages are encoded as Pedersen commitments, many of the existing zero-knowledge proof systems that are designed to work specifically for Pedersen commitments can be directly used on the twisted ElGamal ciphertexts.
We provide the formal description of the twisted ElGamal encryption in the notes.
Ciphertext Decryption
One aspect that makes the use of the ElGamal encryption cumbersome in protocols
is the inefficiency of decryption. The decryption of an ElGamal ciphertext grows
exponentially with the size of the encrypted number. With modern hardware, the
decryption of 32-bit messages can be in the order of seconds, but it quickly
becomes infeasible as the message size grows. A standard Token account stores
general u64
balances, but an ElGamal ciphertext that encrypts large 64-bit
values are not decryptable. Therefore, extra care is put into the way the
balances and transfer amounts are encrypted and handled in the account state and
transfer data.
Account State
If the decryption of the twisted ElGamal encryption scheme were fast, then a confidential transfer account and a confidential instruction data could be modeled as follows:
struct ConfidentialTransferAccount {
/// `true` if this account has been approved for use. All confidential
/// transfer operations for
/// the account will fail until approval is granted.
approved: PodBool,
/// The public key associated with ElGamal encryption
encryption_pubkey: ElGamalPubkey,
/// The pending balance (encrypted by `encryption_pubkey`)
pending_balance: ElGamalCiphertext,
/// The available balance (encrypted by `encryption_pubkey`)
available_balance: ElGamalCiphertext,
}
// Actual cryptographic components are organized in `VerifyTransfer`
// instruction data
struct ConfidentialTransferInstructionData {
/// The transfer amount encrypted under the sender ElGamal public key
encrypted_amount_sender: ElGamalCiphertext,
/// The transfer amount encrypted under the receiver ElGamal public key
encrypted_amount_receiver: ElGamalCiphertext,
}
Upon receiving a transfer instruction, the Token program aggregates
encrypted_amount_receiver
into the account pending_balance
.
The actual structures of these two components are more involved. Since the
TransferInstructionData
requires zero-knowledge proof components, we defer the
discussion of its precise structure to the next subsection and focus on
ConfidentialTransferAccount
here. We start from the ideal
ConfidentialTransferAccount
structure above and incrementally modify it to
produce the final structure.
Available Balance
If the available balance is encrypted solely as general u64
values, then it
becomes infeasible for clients to decrypt and recover the exact balance in an
account. Therefore, in the Token program, the available balance is additionally
encrypted using an authenticated symmetric encryption scheme. The resulting
ciphertext is stored as the decryptable_balance
of an account and the
corresponding symmetric key should either be stored on the client side as an
independent key or be derived on-the-fly from the owner signing key.
struct ConfidentialTransferAccount {
/// `true` if this account has been approved for use. All confidential
/// transfer operations for
/// the account will fail until approval is granted.
approved: PodBool,
/// The public key associated with ElGamal encryption
encryption_pubkey: ElGamalPubkey,
/// The pending balance (encrypted by `encryption_pubkey`)
pending_balance: ElGamalCiphertext,
/// The available balance (encrypted by `encryption_pubkey`)
available_balance: ElGamalCiphertext,
/// The decryptable available balance
decryptable_available_balance: AeCiphertext,
}
Since decryptable_available_balance
is easily decryptable, clients should
generally use it to decrypt the available balance in an account. The
available_balance
ElGamal ciphertext should generally only be used to generate
zero-knowledge proofs when creating a transfer instruction.
The available_balance
and decryptable_available_balance
should encrypt the
same available balance that is associated with the account. The available
balance of an account can change only after an ApplyPendingBalance
instruction
and an outgoing Transfer
instruction. Both of these instructions require a
new_decryptable_available_balance
to be included as part of their instruction
data.
Pending Balance
Like in the case of the available balance, one can consider adding a
decryptable_pending_balance
to the pending balance. However, whereas the
available balance is always controlled by the owner of an account (via the
ApplyPendingBalance
and Transfer
instructions), the pending balance of an
account could constantly change with incoming transfers. Since the corresponding
decryption key of a decryptable balance ciphertext is only known to the owner of
an account, the sender of a Transfer
instruction cannot update the decryptable
balance of the receiver's account.
Therefore, for the case of the pending balance, the Token program stores two independent ElGamal ciphertexts, one encrypting the low bits of the 64-bit pending balance and one encrypting the high bits.
struct ConfidentialTransferAccount {
/// `true` if this account has been approved for use. All confidential
/// transfer operations for
/// the account will fail until approval is granted.
approved: PodBool,
/// The public key associated with ElGamal encryption
encryption_pubkey: ElGamalPubkey,
/// The low-bits of the pending balance (encrypted by `encryption_pubkey`)
pending_balance_lo: ElGamalCiphertext,
/// The high-bits of the pending balance (encrypted by `encryption_pubkey`)
pending_balance_hi: ElGamalCiphertext,
/// The available balance (encrypted by `encryption_pubkey`)
available_balance: ElGamalCiphertext,
/// The decryptable available balance
decryptable_available_balance: AeCiphertext,
}
We correspondingly divide the ciphertext that encrypts the transfer amount in the transfer instruction data as low and high bit encryptions.
// Actual cryptographic components are organized in `VerifyTransfer`
// instruction data
struct ConfidentialTransferInstructionData {
/// The transfer amount encrypted under the sender ElGamal public key
encrypted_amount_sender: ElGamalCiphertext,
/// The low-bits of the transfer amount encrypted under the receiver
/// ElGamal public key
encrypted_amount_lo_receiver: ElGamalCiphertext,
/// The high-bits of the transfer amount encrypted under the receiver
/// ElGamal public key
encrypted_amount_hi_receiver: ElGamalCiphertext,
}
Upon receiving a transfer instruction, the Token program aggregates
encrypted_amount_lo_receiver
in the instruction data to pending_balance_lo
in the account and encrypted_amount_hi_receiver
to pending_balance_hi
.
One natural way to divide the 64-bit pending balance and transfer amount in the structures above is to evenly split the number as low and high 32-bit numbers. Then since the amounts that are encrypted in each ciphertexts are 32-bit numbers, each of their decryption can be done efficiently.
The problem with this approach is that the 32-bit number that is encrypted as
pending_balance_lo
could easily overflow and grow larger than a 32-bit number.
For example, two transfers of the amount 2^32-1
to an account force the
pending_balance_lo
ciphertext in the account to 2^32
, a 33-bit number.
As the encrypted amount overflows, it becomes increasingly more difficult to
decrypt the ciphertext.
To cope with overflows, we add the following two components to the account state.
- The account state keeps track of the number of incoming transfers that it
received since the last
ApplyPendingBalance
instruction. - The account state stores a
maximum_pending_balance_credit_counter
which limits the number of incoming transfers that it can receive before anApplyPendingBalance
instruction is applied to the account. This upper bound can be configured with theConfigureAccount
and should typically be set to2^16
.
struct ConfidentialTransferAccount {
... // `approved`, `encryption_pubkey`, available balance fields omitted
/// The low bits of the pending balance (encrypted by `encryption_pubkey`)
pending_balance_lo: ElGamalCiphertext,
/// The high bits of the pending balance (encrypted by `encryption_pubkey`)
pending_balance_hi: ElGamalCiphertext,
/// The maximum number of `Deposit` and `Transfer` instructions that can credit
/// `pending_balance` before the `ApplyPendingBalance` instruction is executed
pub maximum_pending_balance_credit_counter: u64,
/// The number of incoming transfers since the `ApplyPendingBalance` instruction
/// was executed
pub pending_balance_credit_counter: u64,
}
For the case of the transfer instruction data, we make the following modifications:
- The transfer amount is restricted to be a 48-bit number.
- The transfer amount is divided into 16 and 32-bit numbers and is encrypted as
two ciphertexts
encrypted_amount_lo_receiver
andencrypted_amount_hi_receiver
.
// Actual cryptographic components are organized in `VerifyTransfer`
// instruction data
struct ConfidentialTransferInstructionData {
/// The transfer amount encrypted under the sender ElGamal public key
encrypted_amount_sender: ElGamalCiphertext,
/// The low *16-bits* of the transfer amount encrypted under the receiver
/// ElGamal public key
encrypted_amount_lo_receiver: ElGamalCiphertext,
/// The high *32-bits* of the transfer amount encrypted under the receiver
/// ElGamal public key
encrypted_amount_hi_receiver: ElGamalCiphertext,
}
The fields pending_balance_credit_counter
and
maximum_pending_balance_credit_counter
are used to limit amounts that are
encrypted in the pending balance ciphertexts pending_balance_lo
and
pending_balance_hi
. The choice of the limit on the transfer amount is
done to balance the efficiency of ElGamal decryption with the usability of a
confidential transfer.
Consider the case where maximum_pending_balance_credit_counter
is set to
2^16
.
-
The
encrypted_amount_lo_receiver
encrypts a number that is at most a 16-bit number. Therefore, even after2^16
incoming transfers, the ciphertextpending_balance_lo
in an account encrypts a balance that is at most a 32-bit number. This component of the pending balance can be decrypted efficiently. -
The
encrypted_amount_hi_receiver
encrypts a number that is at most a 32-bit number. Therefore, after2^16
incoming transfers, the ciphertextpending_balance_hi
encrypts a balance that is at most a 48-bit number.The decryption of a large 48-bit number is slow. However, for most applications, transfers of very high transaction amounts are relatively more rare. For an account to hold a pending balance of large 48-bit numbers, it must receive a large number of high transactions amounts. Clients that maintain accounts with high token balances can frequently submit the
ApplyPendingBalance
instruction to flush out the pending balance into the available balance to preventpending_balance_hi
from encrypting a number that is too large.