Solana ZK Part1
Solana ZK Part1
Solana Labs
• Confidentiality: This property refers to the privacy of the amount associated with a
transaction. Suppose that Alice initiates a confidential transfer of 10 tokens from her account
to Bob’s account. Such a transfer may reveal that some transfer did take place between Alice
and Bob, but it does not reveal that 10 tokens were transfered between the two parties.
• Anonymity: This property refers to the privacy of the identities of the sender and receiver of
a transaction. Using the same example as above, an anonymous transfer of 10 tokens between
Alice and Bob may reveal that a transfer of 10 tokens have taken place, but it does not reveal
that Alice and Bob were the two parties involved in such a transfer.
1
SPL tokens are Solana’s analogue of Ethereum’s ERC20 tokens.
1
Our current focus at Solana Labs is enabling confidential transactions of SPL tokens. For this,
we are developing the ZK-Token program that extends the regular SPL Token program to enable
confidential transfer of funds.
SPL ZK-Tokens. The ZK-Token program is based on a protocol that was developed in-house at
Solana and was designed to interoperate with the existing SPL Token program. ZK-Tokens can be
enabled for any existing Token mint. Once enabled, a ZK-Token account can be created from any
Token account: the ZK-Token account address is derived as a PDA from the linked Token account
and mint. Once the accounts are created, funds can be transferred between regular Token accounts
and ZK-Token accounts. Between ZK-Token accounts, funds can be transferred confidentially.2
Regarding the underlying cryptography, Solana Labs designed the ZK-Token protocol to serve as
a thin layer of encryption on top of SPL tokens. It relies on two lightweight cryptographic objects: an
additively-homomorphic encryption scheme and a corresponding rangeproof system. For encryption,
we use a slight variant of the standard ElGamal encryption scheme. For rangeproofs, we use an
adaptation of the popular Bulletproofs system [3] to work with our variant of ElGamal encryption.
Both of these objects are lightweight in comparison to more advanced cryptographic objects such
as general-purpose SNARKs, and they can be implemented on a pairing-free elliptic curve such
as Curve25519 [2]. As Solana already uses Curve25519 for cryptographic signatures with highly
optimized signature verification, we plan to similarly optimize for fast, parallel proof verification.
Reader guide. This document is divided into two parts. Part 1 is geared towards developers that
wish to understand the underlying cryptographic protocol for the ZK-Token program. We do assume
that readers have some basic familiarity with cryptographic concepts such as cryptographic keys,
encryption, and signatures.
Part 2 of the document is written for cryptographers and academics. For these readers, we still
recommend skimming through the overview in Section 2. Some of our treatment of the protocol
may appear quite verbose, but it summarizes the main features of the ZK-Token protocol. Then,
the readers can immediately jump into Part 2 of the document for a more formal treatment of the
protocol and security proofs.
2 Protocol Overview
In this section, we provide the main intuition behind the underlying cryptography that is used in
the ZK-Token program. We start with a minimal description of the Token API in Section 2.1. Then,
in each of the subsequent subsections, we incrementally add cryptograhpic features to the Token
program towards the ZK-Token program.
2
Mint:
Address: The address of the mint information.
Authority: The address of the “owner” of the mint.
Supply: The total supply of tokens.
The Account data structure is used to store the token balance of a user:
Account:
Address: The address of the account information.
Mint: The address of the mint that is associated with this account.
Owner: The address of the owner account that owns this account.
Balance: The balance of tokens that this account holds.
A user can initialize an account data structure by submitting an InitializeAccount instruction. The
InitializeAccount instruction specifies an account address for the account data, an address for the
associated mint, and an owner address.
With these two data structures, users can interact with the token program on the Solana
blockchain with the following set of instructions:
• InitializeMint(addr, auth): Creates a new token mint by initializing a Mint data structure at
the address addr and with a specified mint authority auth.
• MintTo(addr, amt, σmint-auth ): Mints the specified amount amt of tokens to the account at the
address addr.
The token program processes this instruction only if the specified signature σmint-auth verifies
under the token mint-authority’s public key.
• Transfer(source, destination, amt, σsender ): Transfers the amount amt of tokens from the specified
source account to the specified destination account.
The token program processes this instruction only if the specified signature σsender verifies
under the source account’s owner public key.3
Encryption. Since the Account data structure is stored on chain, any user can look up the balance
associated with other user accounts. In the ZK-Token program, we use the most basic way to hide
the balances: keep them in encrypted form. Consider the following example of the Account data
structure:
3
Throughout this section, we use sender and receiver to refer to the source account owner and destination account
owner respectively in a transfer instruction.
3
Account:
Address: EzMCP3PVVkZD4YFXhJNKhgwMihuxxeFqH4F6uPxFxFAe
Mint: Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB
Owner: 5vBrLAPeMjJr9UfssGbjUaBmWtrXTg2vZuMN6L4c8HE6
Balance: 50
To hide the account balance, we can encrypt the balance under the account owner’s public key
before storing it on chain:
Account:
Address: EzMCP3PVVkZD4YFXhJNKhgwMihuxxeFqH4F6uPxFxFAe
Mint: Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB
Owner: 5vBrLAPeMjJr9UfssGbjUaBmWtrXTg2vZuMN6L4c8HE6
Balance: Encrypt(pkowner , 50)
We can similarly use encryption to hide transfer amounts in a transaction. Consider the following
example of a transfer instruction:
To hide the transaction amount, we can encrypt the amount under the sender’s public key before
submitting it to the chain:
By simply encrypting account balances and transfer amounts, we can add confidentiality to the
Token program.
Linear homomorphism. One problem with this simple approach is that the token program
cannot deduct or add transaction amounts to accounts as they are all in encrypted form. One way
to resolve this issue is to use a class of encryption schemes that are linearly homomorphic such as
the ElGamal encryption scheme.4 An encryption scheme is linearly homomorphic if for any two
numbers x0 , x1 ∈ Zp and their encryptions ct0 = Encrypt(pk, x0 ), ct1 = Encrypt(pk, x1 ) under the
same public key, there exist ciphertext-specific add and subtract operations such that
4
In other words, a linearly homomorphic encryption scheme allows numbers to be added and
subtracted in encrypted form. The sum and the difference of the individual encryptions of x0 , x1
result in a ciphertext that is equivalent to an encryption of the sum and the difference of the
numbers x0 , x1 .
By using a linearly homomorphic encryption scheme to encrypt balances and transfer amounts,
we can allow the token program to process balances and transfer amounts in encrypted form. As
linear homomorphism holds only when ciphertexts are encrypted under the same public keys, we
require that a transfer amount to be encrypted under both the sender and receiver public keys:
Transfer(source, destination, amt, σsender ):
source: EzMCP3PVVkZD4YFXhJNKhgwMihuxxeFqH4F6uPxFxFAe
destination: 0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7
amt: Encrypt(pksender , 10), Encrypt(pkreceiver , 10)
Then, upon receiving a transfer instruction of this form, the token program can subtract and add
ciphertexts to the source and destination accounts accordingly:
Accountsource :
Address: EzMCP3PVVkZD4YFXhJNKhgwMihuxxeFqH4F6uPxFxFAe
Mint: Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB
Owner: 5vBrLAPeMjJr9UfssGbjUaBmWtrXTg2vZuMN6L4c8HE6
Balance: Encrypt(pksender , 50) − Encrypt(pksender , 10) ≈ Encrypt(pksender , 40)
Accountdestination :
Address: EzMCP3PVVkZD4YFXhJNKhgwMihuxxeFqH4F6uPxFxFAe
Mint: Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB
Owner: 5vBrLAPeMjJr9UfssGbjUaBmWtrXTg2vZuMN6L4c8HE6
Balance: Encrypt(pkreceiver , 50) + Encrypt(pkreceiver , 10) ≈ Encrypt(pkreceiver , 60)
Zero-knowledge proofs. Another problem with encrypting account balances and transfer amounts
is that the token program cannot check the validity of a transfer amount. For instance, a user
with an account balance of 50 tokens should not be able to transfer 70 tokens to another account.
For regular SPL tokens, the token program can easily detect that there are not enough funds in
the user’s account. If account balances and transfer amounts are encrypted, then these values are
hidden to the token program itself, preventing it from verifying the validity of a transaction.
Therefore, in the ZK-Token program, we require that transfer instructions include zero-knowledge
proofs that validate the correctness of the transfer.5 There are two key components to the proofs
that are required for a transfer instruction.6
• Range Proof : Range proofs are special types of zero-knowledge proof systems that allow users
to generate a proof πrange that a ciphertext ct encrypts a value x ∈ Z that falls in a specified
range `, u ∈ Z:
Verify`,u (ct, πrange ) = 1 ⇐⇒ ct = Encrypt(pk, x) and ` ≤ x < u.
5
Technically speaking, all the cryptographic proofs that are used in the ZK-Token program are cryptographic arguments
as opposed to proofs. To keep the presentation as simple as possible, we still refer to these cryptographic objects as
“proofs” throughout the text.
6
We later remove the need for equality proofs in transfer instructions (see Section 2.4.)
5
The zero-knowledge property guarantees that πrange does not reveal the actual value of x, but
only the fact that ` ≤ x < u.
In the ZK-Tokens program, we require that a transfer instruction contains a range proof that
certify the following:
– The proof should certify that there are enough funds in the source account. Specifically,
let ctsource be the encrypted balance of a source account and cttrans-amt be the encrypted
amount of a transfer. Then we require that the proof certifies that
By verifying the proof, the token program can check that there are enough funds in the
source account.
– The proof should certify that the transfer amount itself is a positive 64-bit number. Let
cttrans-amt be the encrypted amount of a transfer. Then we require that the proof certifies
that
cttrans-amt = Encrypt(pksender , x) such that 0 ≤ x < 264 .
By verifying the proof, the SPL program can check that the transfer amount is a positive
number and therefore, cannot create a negative transfer.
• Equality Proof : Recall that a transfer instruction contains two ciphertexts of the transfer
value x: a ciphertext under the sender public key ctsender = Encrypt(pksender , x) and one under
the receiver public key ctreceiver = Encrypt(pkreceiver , x). However, a malicious user can encrypt
two different values for ctsender and ctreceiver . Therefore, we require that a transfer instruction
contains an equality proof πeq certifying that the two ciphertexts encrypt the same value:
The zero-knowledge property guarantees that πeq does not reveal the actual values of x0 , x1 ,
but only the fact that x0 = x1 .
We formally specifiy the proof generation and verification algorithms in Part 2 of the document.
Encryption key. In our overview of the ZK-Token program in the previous subsection, we used
the public key (signing key) of the account owner to encrypt the balance of an account. In our
actual implementation of the ZK-Tokens program, we use a separate account-specific encryption
key to encrypt the account balances.
Account:
Address: EzMCP3PVVkZD4YFXhJNKhgwMihuxxeFqH4F6uPxFxFAe
Mint: Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB
Owner: 5vBrLAPeMjJr9UfssGbjUaBmWtrXTg2vZuMN6L4c8HE6
EncryptionKey: ek
Balance: Encrypt(ek, 50)
6
The ElGamal public key ek can be set by the owner of the account by including it as part of an
InitializeAccount(ek) instruction. A corresponding decryption key dk can be stored privately on a
client-side wallet.
In general, the use of a single public-secret key pair for both signatures and encryption is
discouraged for potential security vulnerabilities and therefore, it makes sense to use separate
dedicated key pairs for just encryption. More importantly, the ZK-Tokens API is designed to be as
general as possible for application developers. Separate dedicated keys for signing transactions and
decrypting transaction amounts allow for a more flexible API.
In a potential application, the decryption key dk can be shared among multiple users (i.e.
regulators) that should have access to an account balance. Although these users can decrypt account
balances, only the owner of the account that have access to the signing key sk can sign a transaction
that initiates a transfer of tokens. The ZK-Tokens protocol also allows the owner of an account
to submit a new encryption key ek0 via the UpdateAccountPk instruction to replace the associated
encryption key. This instruction can, for instance, be used to revoke another user’s access to an
account balance.
Global Auditor. As separate decryption keys are associated with each user accounts, users can
provide read access to specific account balances to potential auditors. The ZK-Token program also
supports global auditor feature that can be optionally enabled for mints. Specifically, for each
SPL-Token mint, the ZK-Token program maintains a corresponding TransferAuditor account that
maintains a global auditor encryption key. This auditor encryption key can be specified when
the ZK-Token program is first enabled for a specific SPL-Token mint and can be subsequently
updated via the UpdateTransferAuditor instruction. If the transfer auditor feature is enabled, then
any transfer instruction must additionally contain an encryption of the transfer amount under the
auditor’s encryption key.
This allows any entity with a corresponding auditor decryption key to be able to decrypt any transfer
amounts for a particular mint.
Similarly to how a sender can encrypt inconsistent transfer amounts under the source and
destination encryption keys, it can encrypt inconsistent transfer amounts for the auditor ciphertext.
Therefore, if the global auditor feature is enabled, the ZK-Token program requires that a transfer
instruction contains an additional zero-knowledge proof that the transfer amount is encrypted
properly under the auditor’s public key.
Pending and available balance. One way an attacker can compromise the usability of a ZK-Token
account is by taking advantage of the “front-running” problem. Zero-knowledge proofs are verified
with respect to the encrypted balance of an account. Suppose that a user Alice generates a proof
with respect to her current encrypted account balance. If another user Bob transfers some tokens to
Alice, and Bob’s transaction is processed first, then Alice’s transaction will be rejected by the token
program as the proof will not verify with respect to the newly updated ciphertext.
Under normal circumstances, upon a rejection by the program, Alice can simply look up the newly
updated ciphertext and submit a new transaction. However, if a malicious attacker continuously
7
floods the network with a transfer to Alice’s account, then the account may theoretically become
unusable.7 To prevent this type of attack, we modify the account data structure such that the
encrypted balance of an account is divided into two separate components: the pending balance and
the available balance.
Account:
Address: EzMCP3PVVkZD4YFXhJNKhgwMihuxxeFqH4F6uPxFxFAe
Mint: Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB
Owner: 5vBrLAPeMjJr9UfssGbjUaBmWtrXTg2vZuMN6L4c8HE6
EncryptionKey: ek
PendingBalance: Encrypt(ek, 10)
AvailableBalance: Encrypt(ek, 50)
Any outgoing funds from an account is subtracted from its available balance. Any incoming funds
to an account is added to its pending balance.
As an example, consider a transfer instruction that moves 10 tokens from a sender’s account to
a receiver’s account.
Upon receiving this a transaction and after verifying its validity, the token program subtracts
the encrypted amount from the sender’s available balance and adds the encrypted amount to the
receiver’s pending balance.
Accountsource :
Address: EzMCP3PVVkZD4YFXhJNKhgwMihuxxeFqH4F6uPxFxFAe
Mint: Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB
Owner: 5vBrLAPeMjJr9UfssGbjUaBmWtrXTg2vZuMN6L4c8HE6
EncryptionKey: eksender
PendingBalance: Encrypt(eksender , 10)
AvailableBalance: Encrypt(eksender , 50) − Encrypt(eksender , 10) ≈ Encrypt(eksender , 40)
Accountdestination :
Address: EzMCP3PVVkZD4YFXhJNKhgwMihuxxeFqH4F6uPxFxFAe
Mint: Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB
Owner: 5vBrLAPeMjJr9UfssGbjUaBmWtrXTg2vZuMN6L4c8HE6
EncryptionKey: ekreceiver
PendingBalance: Encrypt(ekreceiver ,10) + Encrypt(ekreceiver , 10) ≈ Encrypt(ekreceiver , 20)
AvailableBalance: Encrypt(ekreceiver , 10)
7
We do believe that such an attack on the Solana network is unlikely to actually occur in practice. With current
transaction throughput and latency, it will be extremely costly for an attack to forcibly disable an account for any
extended period of time due to transaction costs.
8
This modification removes the sender’s ability to change the receiver’s available balance of a source
account. As range proofs are generated and verified with respect to the available balance, this
prevents a user’s transaction from being invalidated due to a transaction that is generated by
another user.
An account’s pending balance can be merged into its available balance via the ApplyPendingBalance
instruction, which only the owner of the account can authorize. Upon receiving this instruction and
after verifying that the owner of the account signed the transaction, the token program adds the
pending balance into the available balance.
Account:
Address: EzMCP3PVVkZD4YFXhJNKhgwMihuxxeFqH4F6uPxFxFAe
Mint: Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB
Owner: 5vBrLAPeMjJr9UfssGbjUaBmWtrXTg2vZuMN6L4c8HE6
EncryptionKey: ek
PendingBalance: Encrypt(ek, 0)
AvailableBalance: Encrypt(ek, 50available ) + Encrypt(ek, 10pending ) ≈ Encrypt(ek, 60)
To fix this problem in a transfer instruction, we require that a 64-bit transfer amount xtx-amt be
divided into two 32-bit numbers xlo , xhi such that xtx-amt = xlo + 232 · xhi and then encrypted
separately:
9
source: EzMCP3PVVkZD4YFXhJNKhgwMihuxxeFqH4F6uPxFxFAe
destination: 0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7
amt:
Encrypt(eksender , xlo ), Encrypt(eksender , xhi )
Encrypt(ekreceiver , xlo ), Encrypt(ekreceiver , xhi )
Encrypt(ekauditor , xlo ), Encrypt(ekauditor , xhi )
Since the encrypted values of ElGamal ciphertexts are all 32-bit numbers, they can all be decrypted
and recovered by the receiver and the auditor (See Section 3.3). Naturally, instead of requiring that
the range proof associated with a transfer instruction certify that ciphertexts encrypt 64-bit transfer
amounts, we require that it certifies multiple 32-bit transfer amounts.
Even if the transfer amount xtx-amt is encrypted as two different ciphertexts, they can be
homomorphically merged into a single encryption of xtx-amt :
Upon receiving a transfer instruction, the token program merges the corresponding ciphertexts, and
then subtracts and adds them to the source and destination account balances respectively.
Twisted ElGamal encryption A key challenge in designing any private payment system is
minimizing the size of a transaction. In the ZK-Tokens program, we make a number of optimizations
that reduces transaction size. Among these optimizations, a significant amount of savings stem
from the use of the twisted ElGamal encryption, which was formulated in the work of [4] as a
simple variant of the standard ElGamal encryption scheme. In the twisted ElGamal encryption, a
ciphertext is divided into two components:
• A Pedersen commitment of the encrypted message, which is independent of the public key.
• A decryption handle that encodes the encryption randomness with respect to a specific public
key, and is independent of the encrypted message.
10
Transfer(source, destination, amt, σsender ):
source: EzMCP3PVVkZD4YFXhJNKhgwMihuxxeFqH4F6uPxFxFAe
destination: 0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7
amt:
Encrypt(eksender , xlo ), Encrypt(eksender , xhi )
Encrypt(ekreceiver , xlo ), Encrypt(ekreceiver , xhi )
Encrypt(ekauditor , xlo ), Encrypt(ekauditor , xhi )
Instead of including six ciphertexts (64 bytes each) on a single instruction, one can generate a single
commitment for each xlo , xhi and then include decryption handles for each of these two commitments:
Each decryption handle can be combined with the corresponding Pedersen commitments to form
a complete ElGamal ciphertext. For example, the auditor can use its decryption key dkauditor to
decrypt the pairs
As Pedersen commitments and decryption handles are 32 bytes each, this way of encrypting
transaction amounts result in savings in the total ciphertext size associated with a transfer instruction.
More importantly, since Pedersen commitments are shared among multiple ciphertexts, there is
no need for equality proofs to be included in a transfer instruction. The instruction still requires
proofs that each decryption handles are generated correctly, but these proofs are much smaller in
size compared to equality proofs. We provide the details on the proof requirements for transfer
instructions in Part 2 of the document.
11
G as points. Also, we use the standard pk and sk to denote the public encryption key and secret
decryption key respectively. This is in contrast to Section 2.3 where we used ek and dk for the
encryption and decryption keys to distinguish them from a user’s public and signing key.
It is well-known that the Pedersen commitment scheme over any cryptographically robust group G
satisfies both the security properties of binding and hiding.
12
• KeyGen() → (pk, sk): The key generation algorithm generates a random scalar s ←R Zp . It
computes P = s−1 · H and sets
pk = P, sk = s.
1. Pedersen commitment: C = r · H + x · G,
2. Decryption handle: D = r · P .
• Decrypt(sk, ct) → x: The decryption algorithm takes in a secret key sk = s and a ciphertext
ct = (C, D) as input. It computes
C =C −s·D ∈ G,
and then solves the discrete log problem to recover x ∈ Zp for which x · G = P .
Correctness. The correctness of the decryption algorithm can be easily verified. Let pk = P ,
sk = s be a public-secret key pair and let ct = (C = r · H + x · G, D = r · P ) be a properly encrypted
ciphertext of a scalar message x ∈ Zp . Then, a proper decryption of the ciphertext produces:
Assuming that the discrete log is solved correctly, the decryption algorithm recovers the original
message x ∈ Zp .
x · G = C.
The discrete log problem is the computational task of recovering x given a generator G and a
target C:
13
If C is an arbitrary random element in the group G, then this problem is well-known to be hard to
compute. However, if there is a guarantee that the unique scalar x ∈ Zp for which C = x · G is a
small number (e.g. 0 ≤ x < 232 ), then the problem can be solved efficiently.
Suppose that x is a 32-bit number. The simplest way to solve the discrete log problem is to
enumerate through all possible values 0 ≤ x < 232 and test for the relation C = x · G. Although
this brute-force way of computing discrete log is in the realm of feasibility with modern hardware,
we can solve the problem much more efficiently by relying on the space-time tradeoff. Let xlo , xhi be
two numbers represented by the least and most significant 16-bits of x such that x = xlo + 216 · xhi .
Then the discrete log relation can be represented as
C =x·G
= (xlo + 216 · xhi ) · G
= xlo · G + 216 · xhi · G
| {z } | {z }
online offline
Using this relation, one can solve the discrete log problem by first computing all possible values of
216 · xhi · G for xhi = 0, 1, . . . , 216 − 1 and then solving for xlo :
ComputeDiscreteLog(G, C) :
1. Instantiate a look-up table (e.g. HashMap). For xhi = 0, . . . 216 − 1, store the following
key-value pairs
• key: 216 · xhi · G
• value: xhi
2. For xlo = 0, . . . , 216 − 1, check whether a key C − xlo · G exists in the look-up table. If it
exists, then take the table entry xhi and return x = xlo + 216 · xhi .
We note that the look-up table in step 1 above is independent of the discrete log target C ∈ G.
Therefore, the look-up table can be pre-computed once offline and can be re-used for multiple
instances of discrete log with a fixed generator G ∈ G. The look-up table stores 216 entries of 16-bit
numbers, which translates to around 130 kilobytes. With this amount of pre-computation, any
discrete log challenge can be solved in a matter of seconds.
We note that the algorithm above divides the 32-bit number x specifically into two 16-bit
numbers xlo and xhi . This choice of numbers is purely for simplicity; the numbers xlo and xhi can
be chosen to be arbitrary `lo and `hi -bit numbers for which `lo + `hi = 32. The numbers `lo and
`hi define a space-time trade-off in computing the discrete log problem. For instance, by setting
`lo smaller and `hi larger, the discrete log problem can be solved in less than a second with larger
pre-computed data.
14
4 Instructions
Coming soon!
15
References
[1] Solana Program Library Docs. https://spl.solana.com. Accessed: 2021-08-18.
[2] Bernstein, D. J. Curve25519: New diffie-hellman speed records. In Public Key Cryptography -
PKC 2006, 9th International Conference on Theory and Practice of Public-Key Cryptography,
New York, NY, USA, April 24-26, 2006, Proceedings (2006), M. Yung, Y. Dodis, A. Kiayias,
and T. Malkin, Eds., vol. 3958 of Lecture Notes in Computer Science, Springer, pp. 207–228.
[3] Bünz, B., Bootle, J., Boneh, D., Poelstra, A., Wuille, P., and Maxwell, G.
Bulletproofs: Short proofs for confidential transactions and more. In 2018 IEEE Symposium on
Security and Privacy (SP) (2018), IEEE, pp. 315–334.
[4] Chen, Y., Ma, X., Tang, C., and Au, M. H. Pgc: Decentralized confidential payment
system with auditability. In European Symposium on Research in Computer Security (2020),
Springer, pp. 591–610.
16