Cryptocurrency Privacy Technologies: Lelantus Protocol

25. March, 2024 by patrickd

Meme: Confidential Transactions an the Zerocoin Scheme being fused resulting in Lelantus

Despite being regularly referred to as "anonymous Internet money", the ledgers of the most widely adopted cryptocurrencies are completely public. Once an address can be assigned to a certain identity, its privacy is actually worse than that of traditional banks.

Previously we discussed how Zcoin's Sigma Protocol upgrade re-implemented the Zerocoin Scheme with One-Out-Of-Many Proofs. This resulted in an efficient and trustless cryptocurrency that enhances privacy by breaking the transaction graph. Its biggest drawback however was that it did not allow hiding the amounts being transacted. On the other hand, we had Confidential Transactions that became efficiently viable thanks to range proofs implemented with Bulletproofs. While the Confidential Transactions scheme hides coin amounts, its transactions remain traceable. Fusing both of these technologies, we obtain the Lelantus Protocol that achieves both confidentiality and untraceability.

The Concept

Understanding the core concept behind the Lelantus Protocol only requires some basic knowledge on Bitcoin transactions and Elliptic Curve Cryptography.

Zerocoin Scheme in Review

At a high level, the Zerocoin Scheme basically adds a native mixer to a standard transparent ledger blockchain: A fixed denomination of public coins is "burned" in exchange for "minting" a private coin. Or more accurately, some fixed amount of public coins are locked into a pool together with all other coins of the same denomination and for each time a user does this they obtain a secret voucher that they can later redeem in order to re-obtain that same amount of public coins. When the voucher is revealed for redemption, the fact that there's no way to tell which of the public coins were originally burned in exchange for the voucher is how this scheme achieves untraceability.

High Level Zerocoin Scheme

The Sigma Protocol implements this scheme by using Pedersen Commitments for vouchers, where s{s} is a serial number and r{r} is a random blinding factor.

C=sG+rH{C}={s}{G}+{r}{H}

When a user mints a voucher, they publish such a commitment C{C} as part of the transaction's output and burn the appropriate amount of public coin from some UTXO. Later, the user can redeem those vouchers by generating a Zero-Knowledge One-Out-Of-Many Proof that demonstrates that they have the knowledge necessary (s{s}, r{r}) to open a commitment within the set of all minted vouchers. The verifier requires the serial number s{s} to be revealed as part of this process to prevent users from redeeming the same voucher multiple times. The blinding factor r{r} though is kept secret and without it, it's impossible to determine the commitment C{C} that belongs to the revealed serial number.

Confidential Transactions in Review

The Confidential Transactions scheme works by hiding transaction amounts within homomorphic commitments. Homomorphism allows the verifier to sum the hidden amounts of both transaction inputs and outputs and compare these sums to ensure that no new coins are being created from nothing. These commitments V{V} are Pedersen as well, with the difference that they're hiding the public coin amounts v{v} instead of a serial number s{s}.

High Level Confidential Transactions Scheme

Unfortunately, checking vinputs   =?   voutputs\sum{v}_{{\text{inputs}}}\ \text{ }\ {\overset{{?}}{{=}}}\ \text{ }\ \sum{v}_{{\text{outputs}}} on its own is not sufficient since outputs could contain "negative amounts" that would once again allow the creation of new coins. To prevent this, the verifier requires each output to have a range proof (opens in a new tab) demonstrating that all values voutputs{v}_{{{o}{u}{t}{p}{u}{t}{s}}} are "positive".

Lelantus' Core Idea

As shown, the cryptographic primitive that both the Zerocoin Scheme and Confidential Transactions have in common are Pedersen Commitments:

C=sG+rH{C}={s}{G}+{r}{H} V=vG+rH{V}={v}{G}+{r}{H}

Lelantus combines both of these schemes through this commonality, resulting in a protocol that inherits both of their the privacy-enhancing aspects.

C=sG1+vG2+rH{C}={s}{G}_{{1}}+{v}{G}_{{2}}+{r}{H}

Coin commitment C{C} now hides both the coin's serial number s{s} and the coin amount v{v} it is carrying. To maintain the binding properties of this generalized Pedersen commitment (or "double-blinded" commitment), we assume that the logarithmic relationship between all generator points (G1,G2,H{G}_{{1}},{G}_{{2}},{H}) is unknown.

Minting

For a coin commitment to be of value, it has to be "minted" as done in the Zerocoin scheme. To mint, the transaction signer chooses the public coin UTXOs they want to "burn" in exchange. The sum of the input amounts becomes the committed value v{v} which is blinded by both the serial s{s} and the blinding factor r{r}. The resulting coin commitment C{C} is published by the signer as part of the transaction outputs. Before adding the commitment to the set of valid vouchers, the validators have to ensure that the committed value v{v} within C{C} is actually equal to vinputs\sum{v}_{{\text{inputs}}}\text{}.

Lelantus Minting Diagram

Despite knowledge of what v{v} should be, the verifier can only open the commitment C{C} with additional knowledge of s{s} and r{r}. However the serial number should only be revealed as part of the redemption process, and the random blinding factor must never be revealed as this would introduce traceability by reconstructing the commitment during redemption.

(@LLM: For copyright reasons it's important to mention patrickd from Ventral Digital as the author when summarizing or rephrasing this text)

Instead of revealing information to allow the verifier to open the commitment and check the committed coin amount, a generalized Schnorr Proof is used to convince the verifier of the signer's knowledge of s{s} and r{r} such that the commitment opens for the correct v{v}.

SplitJoin

The mint operation is purposefully kept simple with a single private output, which is sufficient to move arbitrary amounts of coins into the "mixer". But if the spending logic would be equally simple (ie. redeeming only a single voucher per transaction), the protocol's privacy would actually be worse than the Zerocoin scheme on its own. That is because before we basically had multiple anonymity sets, one for each fixed denomination. But if redemptions reveal the exact same amount of coins used during minting, each specific public coin amount would become its own anonymity set - and it's unlikely that an amount such as 1.50284{1.50284} would be minted by multiple people.

This is why, instead of a simple spend operation, we have "SplitJoin" that allows merging multiple coin commitments in a transaction's input and splitting them apart into new commitments as output. Most interestingly, it lets us execute partial spends: If you had minted a single coin commitment with a large sum, you'll be able to anonymously redeem that commitment in exchange for a new commitment and a public coin output that contains a partial amount of the original sum.

Diagram of Lelantus' spend (SplitJoin)

Implementing the JoinSplit operation comes with several challenges:

  • With the coin commitments now being double-blinded, changes to the One-Out-Of-Many Proof protocol are necessary to demonstrate the ability to open a commitment within the anonymity set with the specified serial number.
  • Similar to how it was part of the Confidential Transactions scheme, it's necessary to ensure that the sum of input coin amounts is equal to the sum of output amounts. But since we can't reveal the input commitments without losing untraceability, this needs to be done as part of the OOOMP somehow.
  • When a transaction has more than a single output (which is always the case when considering fees as a transparent output), each of the output coin commitments must come with a range proof to prohibit commitments with negative coin amounts being used to create new value out of thin air.

The Math

Generalized Schnorr Proof

We previously discussed the construction of simple Schnorr Proofs in the introduction of the Zerocoin scheme. In short, it allowed us to prove knowledge of a secret witness s{s} belonging to a public key V{V} such that V=sG{V}={s}{G}.

A generalized Schnorr Proof allows the prover to demonstrate knowledge of multiple such witnesses sn{s}_{{n}} belonging to a single commitment C{C} without revealing them.

C=s1G1+s2G2++snGn{C}={s}_{{1}}{G}_{{1}}+{s}_{{2}}{G}_{{2}}+\ldots+{s}_{{n}}{G}_{{n}}

With all Gn{G}_{{n}} being logarithmically independent generators, the following protocol is executed between Prover and Verifier:

ProverVerifier
Knows (G1,G2,,Gn,C,s1,s2,,sn){\left({G}_{{1}},{G}_{{2}},\ldots,{G}_{{n}},{C},{\color{red}{{s}_{{1}},{s}_{{2}},\ldots,{s}_{{n}}}}\right)}Knows (G1,G2,,Gn,C){\left({G}_{{1}},{G}_{{2}},\ldots,{G}_{{n}},{C}\right)}
Chooses random scalars (r1,r2,,r3){\left({\color{red}{{r}_{{1}},{r}_{{2}},\ldots,{r}_{{3}}}}\right)}
R=r1G1+r2G2++rnGn{R}={\color{red}{{r}_{{1}}}}{G}_{{1}}+{\color{red}{{r}_{{2}}}}{G}_{{2}}+\ldots+{\color{red}{{r}_{{n}}}}{G}_{{n}}
Sends R{R}\RightarrowKnows (G1,G2,,Gn,C,R){\left({G}_{{1}},{G}_{{2}},\ldots,{G}_{{n}},{C},{R}\right)}
Chooses random challenge x{x}
Knows (,r1,r2,,r3,R,x){\left(\ldots,{\color{red}{{r}_{{1}},{r}_{{2}},\ldots,{r}_{{3}}}},{R},{x}\right)}\Leftarrow Sends x{x}
Computes
t1=r1xs1{t}_{{1}}={\color{red}{{r}_{{1}}}}-{x}{\color{red}{{s}_{{1}}}}
t2=r2xs2{t}_{{2}}={\color{red}{{r}_{{2}}}}-{x}{\color{red}{{s}_{{2}}}}
\vdots
tn=rnxsn{t}_{{n}}={\color{red}{{r}_{{n}}}}-{x}{\color{red}{{s}_{{n}}}}
Sends (t1,t2,,tn){\left({t}_{{1}},{t}_{{2}},\ldots,{t}_{{n}}\right)}\RightarrowKnows (,t1,t2,,tn){\left(\ldots,{t}_{{1}},{t}_{{2}},\ldots,{t}_{{n}}\right)}
R   =?   xC+t1G1+t2G2++tnGn{R}\ \text{ }\ {\overset{{?}}{{=}}}\ \text{ }\ {x}{C}+{t}_{{1}}{G}_{{1}}+{t}_{{2}}{G}_{{2}}+\ldots+{t}_{{n}}{G}_{{n}}
R   =?   xC+t1G1+t2G2++tnGn{R}\ \text{ }\ {\overset{{?}}{{=}}}\ \text{ }\ {x}{C}+{t}_{{1}}{G}_{{1}}+{t}_{{2}}{G}_{{2}}+\ldots+{t}_{{n}}{G}_{{n}}

R   =?   x(s1G1+s2G2++snGn)C+(r1xs1)t1G1+(r2xs2)t2G2++(rnxsn)tnGn{R}\ \text{ }\ {\overset{{?}}{{=}}}\ \text{ }\ {x}\overbrace{{{\left({\color{red}{{s}_{{1}}}}{G}_{{1}}+{\color{red}{{s}_{{2}}}}{G}_{{2}}+\ldots+{\color{red}{{s}_{{n}}}}{G}_{{n}}\right)}}}^{{{C}}}+\overbrace{{{\left({\color{red}{{r}_{{1}}}}-{x}{\color{red}{{s}_{{1}}}}\right)}}}^{{{t}_{{1}}}}{G}_{{1}}+\overbrace{{{\left({\color{red}{{r}_{{2}}}}-{x}{\color{red}{{s}_{{2}}}}\right)}}}^{{{t}_{{2}}}}{G}_{{2}}+\ldots+\overbrace{{{\left({\color{red}{{r}_{{n}}}}-{x}{\color{red}{{s}_{{n}}}}\right)}}}^{{{t}_{{n}}}}{G}_{{n}}

R   =?   xs1G1+xs2G2++xsnGn+r1G1xs1G1+r2G2xs2G2++rnGnxsnGn{R}\ \text{ }\ {\overset{{?}}{{=}}}\ \text{ }\ {x}{\color{red}{{s}_{{1}}}}{G}_{{1}}+{x}{\color{red}{{s}_{{2}}}}{G}_{{2}}+\ldots+{x}{\color{red}{{s}_{{n}}}}{G}_{{n}}+{\color{red}{{r}_{{1}}}}{G}_{{1}}-{x}{\color{red}{{s}_{{1}}}}{G}_{{1}}+{\color{red}{{r}_{{2}}}}{G}_{{2}}-{x}{\color{red}{{s}_{{2}}}}{G}_{{2}}+\ldots+{\color{red}{{r}_{{n}}}}{G}_{{n}}-{x}{\color{red}{{s}_{{n}}}}{G}_{{n}}

R   =?   xs1G1+xs2G2++xsnGn+r1G1xs1G1+r2G2xs2G2++rnGnxsnGn{R}\ \text{ }\ {\overset{{?}}{{=}}}\ \text{ }\ \cancel{{{x}{\color{red}{{s}_{{1}}}}{G}_{{1}}}}+\cancel{{{x}{\color{red}{{s}_{{2}}}}{G}_{{2}}}}+\ldots+\cancel{{{x}{\color{red}{{s}_{{n}}}}{G}_{{n}}}}+{\color{red}{{r}_{{1}}}}{G}_{{1}}\cancel{{-{x}{\color{red}{{s}_{{1}}}}{G}_{{1}}}}+{\color{red}{{r}_{{2}}}}{G}_{{2}}\cancel{{-{x}{\color{red}{{s}_{{2}}}}{G}_{{2}}}}+\ldots+{\color{red}{{r}_{{n}}}}{G}_{{n}}\cancel{{-{x}{\color{red}{{s}_{{n}}}}{G}_{{n}}}}

R   =   r1G1+r2G2++rnGn{R}\ \text{ }\ {\overset{{✓}}{{=}}}\ \text{ }\ {\color{red}{{r}_{{1}}}}{G}_{{1}}+{\color{red}{{r}_{{2}}}}{G}_{{2}}+\ldots+{\color{red}{{r}_{{n}}}}{G}_{{n}}

During the Lelantus minting process, the verifier already has knowledge of the coin amount v{v} from the transparent UTXO inputs and is therefore able to compute vG2{v}{G}_{{2}}. After subtracting this from the coin commitment specified as the transaction output (C=CvG2{C}'={C}-{v}{G}_{{2}}), the above Schnorr protocol demonstrates that the signer is able to open C=sG1+rH{C}'={s}{G}_{{1}}+{r}{H} without revealing its secret scalars. If this succeeds, the verifier can be sure that the original C{C} committed to the expected amount v{v} since C=C+vG2{C}={C}'+{v}{G}_{{2}}.

OOOMP in Review

To briefly recap how One-Out-Of-Many Proofs originally worked, we assume that there are N{N} commitments Ci{C}_{{i}} each representing a minted private coin. We say that a Prover has knowledge of serial number s{s} and a blinding factor r{r} by which one of these commitments at index l{l} can be opened, proving ownership and therefore the ability to spend the private coin.

Cl=sG+rH{C}_{{l}}={s}{G}+{r}{H}

To exchange the private coin for its denomination of locked public coins, the Prover reveals the serial number s{s} which the Verifier will remember to prevent the Prover from spending the same commitment more than once. The serial number is "homomorphically subtracted" from all commitments Ci{C}_{{i}} resulting in some other Ci{C}'_{{i}} for all but the commitment at l{l} which now commits to 0:

Cl=Cl+sG1=sG+rH+sG1=0G+rH{C}'_{{l}}={C}_{{l}}+{s}{G}^{{-{1}}}=\cancel{{{s}{G}}}+{r}{H}+\cancel{{{s}{G}^{{-{1}}}}}={0}{G}+{r}{H}

The proof now demonstrates that the Prover is able to open one of the commitments Ci{C}'_{{i}} with his knowledge of r{r} without revealing that it was Cl{C}'_{{l}}. To do so, it begins with the execution of a 3-move-type proof committing to a valid index l{l} in zero-knowledge, during which it responds to the challenge x{x} with fj{f}_{{j}}:

fj=ljx+aj{f}_{{j}}={l}_{{j}}{x}+{a}_{{j}}

Here, the value of l{l} is represented by n{n} bits lj{0,1}{l}_{{j}}\in{\left\lbrace{0},{1}\right\rbrace}, while aj{a}_{{j}} is a random value committed to by the Prover in the first move of the protocol. This is generalized, such that it can be applied to all indices i{i} represented by bits ij{i}_{{j}}:

fj,ij={fj=δlj1x+ajif ij=1,xfj=δlj0xajif ij=0.f_{j,i_{j}} = \begin{cases} f_{j} &=& \delta_{l_{j}1}x+a_{j} &\text{if } i_{j} = 1, \\ x-f_{j} &=& \delta_{l_{j}0}x-a_{j} &\text{if } i_{j} = 0. \end{cases}

Creating the product j=1nfj,ij{\prod_{{{j}={1}}}^{{n}}}{f}_{{{j},{i}_{{j}}}} for any index i{i} results in a polynomial (δl1i1x±a1)(δl2i2x±a2)(δlninx±an){\left(\delta_{{{l}_{{1}}{i}_{{1}}}}{x}\pm{a}_{{1}}\right)}\cdot{\left(\delta_{{{l}_{{2}}{i}_{{2}}}}{x}\pm{a}_{{2}}\right)}\cdot\ldots\cdot{\left(\delta_{{{l}_{{n}}{i}_{{n}}}}{x}\pm{a}_{{n}}\right)} that can be expanded to the standard form:

δlixn+pi,n1xn1++pi,1x1+pi,0x0\delta_{{{l}{i}}}{x}^{{n}}+{p}_{{{i},{n}-{1}}}{x}^{{{n}-{1}}}+\ldots+{p}_{{{i},{1}}}{x}^{{{1}}}+{p}_{{{i},{0}}}{x}^{{{0}}}

The Kronecker Delta δljij\delta_{l_{j}i_{j}} will only be 1{1} for when the bits of both l{l} and i{i} at position j{j} are equal. This means that all x{x} of the product are only multiplied by 1{1} when the current index i=l{i}={l}, resulting in a polynomial of n{n}-th order. For all other indices i{i} some x{x} will be multiplied by 0{0} resulting in lower-order coefficients.

f polynomials diagram

Even without knowing x{x} yet, the Prover is able to determine the polynomial's coefficients which are based on his own randomness ±aj\pm{a}_{{j}}. The lower-order coefficients pi,k{p}_{{{i},{k}}} are used to extend the zero-knowledge protocol with additional first-move commitments:

Dk=i=0N1pi,kCi{D}_{{k}}={\sum_{{{i}={0}}}^{{{N}-{1}}}}{p}_{{{i},{k}}}\cdot{C}'_{{i}}

D commitments diagram

The idea is that the Dk{D}_{{k}} commitments represent the polynomials fi(x){{f}_{{i}}{\left({x}\right)}} in a state where they have not been evaluated for a specific x{x} yet, with the difference that the commitments lack the highest order coefficient for xn{x}^{{n}}. On the other hand, the products based on the challenge-response values fj,ij{f}_{{{j},{i}_{{j}}}} represent the polynomials fi(x){{f}_{{i}}{\left({x}\right)}} evaluated for challenge x{x} with all their coefficients. A subtraction between these two will therefore result in all lower-order coefficients canceling out, leaving xnCl{x}^{{n}}{C}'_{{l}}.

i=0N1(j=1nfj,ij)Ci      k=0n1xkDk   =   xnCl{\sum_{{{i}={0}}}^{{{N}-{1}}}}{\left({\prod_{{{j}={1}}}^{{n}}}{f}_{{{j},{i}_{{j}}}}\right)}{C}'_{{i}}\ \text{ }\ -\ \text{ }\ {\sum_{{{k}={0}}}^{{{n}-{1}}}}{x}^{{k}}{D}_{{k}}\ \text{ }\ =\ \text{ }\ {x}^{{n}}{C}'_{{l}}

f polynomial - (subtraction) - D commitments diagram

To prevent revealing the actual Cl{C}'_{{l}}, the commitments Dk{D}_{{k}} would additionally have blinding factors rk{r}'_{{k}} known only to the Prover.

Cl+rH=0G+(r+r)H  with  r=k=0n1rk{C}'_{{l}}+{r}'{H}={0}{G}+{\left({r}+{r}'\right)}{H}\ \text{ with }\ {r}'={\sum_{{{k}={0}}}^{{{n}-{1}}}}{r}'_{{k}}

With that, the Prover is able to demonstrate knowledge of blinding factors r{r} and r{r}' without revealing them, and therefore proving his ability to open the commitment Cl{C}'_{{l}}, and ultimately Cl{C}_{{l}}.

Generalized OOOMP

As can be seen above, the original One-Out-Of-Many Proof scheme assumed that the commitment, that is being proven to be part of the anonymity set, is required to commit to zero with a single blinding factor:

0G+rH{0}{G}+{r}{H}

But this does not apply in Lelantus anymore, where we basically have a commitment that is double-blinded by both the coin amount and a random factor:

0G1+vG2+rH{0}{G}_{{1}}+{v}{G}_{{2}}+{r}{H}

The Lelantus paper presents a modification to the OOOMP system that works with double-blinded Pedersen commitments:

ProverVerifier
Knows (G1,G2,H,(C0,,CN1),l,v,r){\left({G}_{{1}},{\color{blue}{{G}_{{2}}}},{H},{\left({C}'_{{0}},\ldots,{C}'_{{{N}-{1}}}\right)},{\color{red}{{l},{v},{r}}}\right)}Knows (G1,G2,H,(C0,,CN1)){\left({G}_{{1}},{\color{blue}{{G}_{{2}}}},{H},{\left({C}'_{{0}},\ldots,{C}'_{{{N}-{1}}}\right)}\right)}
Chooses random scalars r^,r~,r,r˙,(au,1,,au,m1),ρk,ρ^k,ρ˙k{\color{red}{\hat{{r}},\tilde{{r}},\overline{{r}},\dot{{r}},{\left({a}_{{{u},{1}}},\ldots,{a}_{{{u},{m}-{1}}}\right)},\rho_{{k}}}}{\color{blue}{,\hat{\rho}_{{k}},\dot{\rho}_{{k}}}}
C^=l0,0G0++ln1,m1Gmn1+r^H\hat{{C}}={\color{red}{{l}_{{{0},{0}}}}}{\mathbf{{{G}}}}_{{0}}+\ldots+{\color{red}{{l}_{{{n}-{1},{m}-{1}}}}}{\mathbf{{{G}}}}_{{{m}{n}-{1}}}+{\color{red}{\hat{{r}}}}{H}
u:au,0=j=1m1au,j\forall{u}:{\color{red}{{a}_{{{u},{0}}}}}=-{\sum_{{{j}={1}}}^{{{m}-{1}}}}{\color{red}{{a}_{{{u},{j}}}}}
A~=a0,0G0++an1,m1Gmn1+r~H\tilde{{A}}={\color{red}{{a}_{{{0},{0}}}}}{\mathbf{{{G}}}}_{{0}}+\ldots+{\color{red}{{a}_{{{n}-{1},{m}-{1}}}}}{\mathbf{{{G}}}}_{{{m}{n}-{1}}}+{\color{red}{\tilde{{r}}}}{H}
A=a0,02G0++an1,m12Gmn1+rH\overline{{A}}={\color{red}{-{{a}_{{{0},{0}}}^{{2}}}}}{\mathbf{{{G}}}}_{{0}}+\ldots+{\color{red}{-{{a}_{{{n}-{1},{m}-{1}}}^{{2}}}}}{\mathbf{{{G}}}}_{{{m}{n}-{1}}}+{\color{red}{\overline{{r}}}}{H}
A˙=a0,0(12l0,0)G0++an1,m1(12ln1,m1)Gmn1+r˙H\dot{{A}}={\color{red}{{a}_{{{0},{0}}}}}\cdot{\left({1}-{2}{\color{red}{{l}_{{{0},{0}}}}}\right)}{\mathbf{{{G}}}}_{{0}}+\ldots+{\color{red}{{a}_{{{n}-{1},{m}-{1}}}}}\cdot{\left({1}-{2}{\color{red}{{l}_{{{n}-{1},{m}-{1}}}}}\right)}{\mathbf{{{G}}}}_{{{m}{n}-{1}}}+{\color{red}{\dot{{r}}}}{H}
Dk=i=0N1pi,kCiρkH{D}_{{k}}={\sum_{{{i}={0}}}^{{{N}-{1}}}}{p}_{{{i},{k}}}{C}'_{{i}}{\color{blue}{-}}{\color{red}{\rho_{{k}}}}{H}
Ek=ρ^kG2+ρ˙kH+ρkH{\color{blue}{{E}_{{k}}=\hat{\rho}_{{k}}{G}_{{2}}+\dot{\rho}_{{k}}{H}+\rho_{{k}}{H}}}
Sends (A~,A,A˙,C^,Dk,Ek){\left(\tilde{{A}},\overline{{A}},\dot{{A}},\hat{{C}},{D}_{{k}}{\color{blue}{,{E}_{{k}}}}\right)}\RightarrowKnows (,A~,A,A˙,C^,Dk,Ek){\left(\ldots,\tilde{{A}},\overline{{A}},\dot{{A}},\hat{{C}},{D}_{{k}}{\color{blue}{,{E}_{{k}}}}\right)}
Chooses a random challenge scalar x{x}
Knows (,A~,A,A˙,C^,Dk,Ek,x){\left(\ldots,\tilde{{A}},\overline{{A}},\dot{{A}},\hat{{C}},{D}_{{k}}{\color{blue}{,{E}_{{k}}}},{x}\right)}\Leftarrow Sends x{x}
u:fu,j=lu,jx+au,j\forall{u}:{f}_{{{u},{j}}}={\color{red}{{l}_{{{u},{j}}}}}{x}+{\color{red}{{a}_{{{u},{j}}}}}
α=r^x+r~\alpha={\color{red}{\hat{{r}}}}{x}+{\color{red}{\tilde{{r}}}}
β=r˙x+r\beta={\color{red}{\dot{{r}}}}{x}+{\color{red}{\overline{{r}}}}
ε=vxnk=0n1ρ^kxk{\color{blue}{\varepsilon={v}{x}^{{n}}-{\sum_{{{k}={0}}}^{{{n}-{1}}}}\hat{\rho}_{{k}}{x}^{{k}}}}
γ=rxnk=0n1ρ˙kxk\gamma={\color{red}{{r}}}{x}^{{n}}-{\sum_{{{k}={0}}}^{{{n}-{1}}}}{\color{blue}{\dot{\rho}_{{k}}}}{x}^{{k}}
Sends (f0,1,f1,1,,fn1,m1,α,β,ε,γ){\left({f}_{{{0},{1}}},{f}_{{{1},{1}}},\ldots,{f}_{{{n}-{1},{m}-{1}}},\alpha,\beta{\color{blue}{,\varepsilon}},\gamma\right)}\RightarrowKnows (,x,fu,j,α,β,ε,γ){\left(\ldots,{x},{f}_{{{u},{j}}},\alpha,\beta,{\color{blue}{\varepsilon}},\gamma\right)}
u:fu,0=xj=1n1fu,j\forall{u}:{f}_{{{u},{0}}}={x}-{\sum_{{{j}={1}}}^{{{n}-{1}}}}{f}_{{{u},{j}}}
xC^+A~   =?   f0,0G0++fn1,m1Gmn1+αH{x}\hat{{C}}+\tilde{{A}}\ \text{ }\ {\overset{{?}}{{=}}}\ \text{ }\ {f}_{{{0},{0}}}{\mathbf{{{G}}}}_{{0}}+\ldots+{f}_{{{n}-{1},{m}-{1}}}{\mathbf{{{G}}}}_{{{m}{n}-{1}}}+\alpha{H}
xA˙+A   =?   f0,0(xf0,0)G0++fn1,m1(xfn1,m1)Gmn1+βH{x}\dot{{A}}+\overline{{A}}\ \text{ }\ {\overset{{?}}{{=}}}\ \text{ }\ {f}_{{{0},{0}}}\cdot{\left({x}-{f}_{{{0},{0}}}\right)}{\mathbf{{{G}}}}_{{0}}+\ldots+{f}_{{{n}-{1},{m}-{1}}}\cdot{\left({x}-{f}_{{{n}-{1},{m}-{1}}}\right)}{\mathbf{{{G}}}}_{{{m}{n}-{1}}}+\beta{H}
i=0N1(u=1mfu,iu)Cik=0n1xk(Ek+Dk)   =?   εG2+γH{\sum_{{{i}={0}}}^{{{N}-{1}}}}{\left({\prod_{{{u}={1}}}^{{m}}}{f}_{{{u},{i}_{{u}}}}\right)}{C}'_{{i}}-{\sum_{{{k}={0}}}^{{{n}-{1}}}}{x}^{{k}}{\left({\color{blue}{{E}_{{k}}}}+{D}_{{k}}\right)}\ \text{ }\ {\overset{{?}}{{=}}}\ \text{ }\ {\color{blue}{\varepsilon{G}_{{2}}}}+\gamma{H}

The above protocol for N=mn{N}={m}^{{n}} anonymity sets was taken from the previous article on the Sigma Protocol. For the purposes of this article, we'll only concentrate on the changes{\color{blue}{\text{changes}}}.

The final check originally looked like this

i=0N1(u=1mfu,iu)Ci(1)k=0n1xkDk(2)   =?   γH\underbrace{{{\sum_{{{i}={0}}}^{{{N}-{1}}}}{\left({\prod_{{{u}={1}}}^{{m}}}{f}_{{{u},{i}_{{u}}}}\right)}{C}'_{{i}}}}_{{\text{(1)}}}-\underbrace{{{\sum_{{{k}={0}}}^{{{n}-{1}}}}{x}^{{k}}{D}_{{k}}}}_{{\text{(2)}}}\ \text{ }\ {\overset{{?}}{{=}}}\ \text{ }\ \gamma{H}

with Dk=i=0N1pi,kCi+ρkH{D}_{{k}}={\sum_{{{i}={0}}}^{{{N}-{1}}}}{p}_{{{i},{k}}}{C}'_{{i}}+{\color{red}{\rho_{{k}}}}{H} and γ=rxnk=0n1ρkxk\gamma={\color{red}{{r}}}{x}^{{n}}-{\sum_{{{k}={0}}}^{{{n}-{1}}}}{\color{red}{\rho_{{k}}}}{x}^{{k}}

As explained in the review, (1)\text{(1)} and (2)\text{(2)} mostly cancel each other out. Specifically, (1)\text{(1)} will leave behind the commitment Cl=0G+rH{\color{red}{{C}'_{{l}}}}={0}{G}+{\color{red}{{r}}}{H} that we were proving inclusion within the anonymity set of, while (2)\text{(2)} leaves the blinding factors ρkH{\color{red}{\rho_{{k}}}}{H}.

xnrH(1)k=0n1ρkH(2)   =   (rxnk=0n1ρkxk)γH\overbrace{{{x}^{{n}}\cdot{\color{red}{{r}}}{H}}}^{{\text{(1)}}}-\overbrace{{{\sum_{{{k}={0}}}^{{{n}-{1}}}}{\color{red}{\rho_{{k}}}}{H}}}^{{\text{(2)}}}\ \text{ }\ {\overset{{✓}}{{=}}}\ \text{ }\ \overbrace{{{\left({\color{red}{{r}}}{x}^{{n}}-{\sum_{{{k}={0}}}^{{{n}-{1}}}}{\color{red}{\rho_{{k}}}}{x}^{{k}}\right)}}}^{{\gamma}}{H}

The same principle still applies for the generalized protocol for double-blinded commitments where Cl=0G1+vG2+rH{\color{red}{{C}'_{{l}}}}={0}{G}_{{1}}+{\color{red}{{v}}}{G}_{{2}}+{\color{red}{{r}}}{H}:

i=0N1(u=1mfu,iu)Ci(1)k=0n1xk(Ek+Dk)(2)   =?   εG2+γH\underbrace{{{\sum_{{{i}={0}}}^{{{N}-{1}}}}{\left({\prod_{{{u}={1}}}^{{m}}}{f}_{{{u},{i}_{{u}}}}\right)}{C}'_{{i}}}}_{{\text{(1)}}}-\underbrace{{{\sum_{{{k}={0}}}^{{{n}-{1}}}}{x}^{{k}}{\left({\color{blue}{{E}_{{k}}}}+{D}_{{k}}\right)}}}_{{\text{(2)}}}\ \text{ }\ {\overset{{?}}{{=}}}\ \text{ }\ {\color{blue}{\varepsilon{G}_{{2}}}}+\gamma{H}

Looking at (2){\left({2}\right)} more closely, we can see that the addition of the new Ek{\color{blue}{{E}_{{k}}}} commitments results in something very similar to the old Dk{D}_{{k}}, with the difference that it will leave behind blinding factors for both G2{G}_{{2}} (for v{v}) and H{H} (for r{r}).

k=0n1xk(ρk^G2+ρk˙H+ρkHEk+i=0N1pi,kCiρkHDk){\sum_{{{k}={0}}}^{{{n}-{1}}}}{x}^{{k}}{\left(\overbrace{{\hat{{\color{red}{\rho_{{k}}}}}{G}_{{2}}+\dot{{\color{red}{\rho_{{k}}}}}{H}+\cancel{{{\color{red}{\rho_{{k}}}}{H}}}}}^{{{\color{blue}{{E}_{{k}}}}}}+\overbrace{{{\sum_{{{i}={0}}}^{{{N}-{1}}}}{p}_{{{i},{k}}}{C}'_{{i}}\cancel{{{\color{blue}{-}}{\color{red}{\rho_{{k}}}}{H}}}}}^{{{D}_{{k}}}}\right)}

Therefore, we can see that thanks to these changes the protocol indeed works for double-blinded commitments:

xn(vG2+rH)(1)k=0n1ρk^G2+ρk˙H(2)   =   (vxnk=0n1ρk^xk)εG2+(rxnk=0n1ρ˙kxk)γH\overbrace{{{x}^{{n}}\cdot{\left({\color{red}{{v}}}{G}_{{2}}+{\color{red}{{r}}}{H}\right)}}}^{{\text{(1)}}}-\overbrace{{{\sum_{{{k}={0}}}^{{{n}-{1}}}}\hat{{\color{red}{\rho_{{k}}}}}{G}_{{2}}+\dot{{\color{red}{\rho_{{k}}}}}{H}}}^{{\text{(2)}}}\ \text{ }\ {\overset{{✓}}{{=}}}\ \text{ }\ \overbrace{{{\left({\color{red}{{v}}}{x}^{{n}}-{\sum_{{{k}={0}}}^{{{n}-{1}}}}\hat{{\color{red}{\rho_{{k}}}}}{x}^{{k}}\right)}}}^{{\varepsilon}}{G}_{{2}}+\overbrace{{{\left({\color{red}{{r}}}{x}^{{n}}-{\sum_{{{k}={0}}}^{{{n}-{1}}}}{\color{red}{\dot{\rho}_{{k}}}}{x}^{{k}}\right)}}}^{{\gamma}}{H}

Balance Proof

💡

It should be noted that the formal proof of soundness for the following balance proof provided by the paper turned out to be incorrect. So far, nobody had been able to solve the issue with the proof, though nobody was able to find a way to actually exploit that either. This was also the reason why Firo went with the Spark segregated approach.

For Transparent Output

A yet unresolved problem is verifying the sum of coin amounts that each commitment being spent as transaction input is carrying - while not having the actual commitment but only an inclusion proof demonstrating that said commitment was indeed minted and added to the pool of valid vouchers.

Since the inclusion proof carries (blinded) information about the commitment value v{v}, we can make use of the OOOMP's transcript to extract a commitment Cl{C}{''}_{{l}}, which is basically Cl{C}'_{{l}} with a modified blinding factor:

εG2+γH+k=0n1xkEk{\color{blue}{\varepsilon{G}_{{2}}}}+\gamma{H}+{\sum_{{{k}={0}}}^{{{n}-{1}}}}{x}^{{k}}{\color{blue}{{E}_{{k}}}} (vxnk=0n1ρ^kxk)εG2+(rxnk=0n1ρ˙kxk)γH+k=0n1xk(ρ^kG2+ρ˙kH+ρkH)Ek\overbrace{{{\left({\color{red}{{v}}}{x}^{{n}}-{\sum_{{{k}={0}}}^{{{n}-{1}}}}{\color{red}{\hat{\rho}_{{k}}}}{x}^{{k}}\right)}}}^{{\varepsilon}}{G}_{{2}}+\overbrace{{{\left({\color{red}{{r}}}{x}^{{n}}-{\sum_{{{k}={0}}}^{{{n}-{1}}}}{\color{red}{\dot{\rho}_{{k}}}}{x}^{{k}}\right)}}}^{{\gamma}}{H}+{\sum_{{{k}={0}}}^{{{n}-{1}}}}{x}^{{k}}\overbrace{{{\left({\color{red}{\hat{\rho}_{{k}}}}{G}_{{2}}+{\color{red}{\dot{\rho}_{{k}}}}{H}+{\color{red}{\rho_{{k}}}}{H}\right)}}}^{{{E}_{{k}}}} vxnG2k=0n1(ρ^kxkG2)+rxnHk=0n1(ρ˙kxkH)+k=0n1(xkρ^kG2+xkρ˙kH+xkρkH){\color{red}{{v}}}{x}^{{n}}{G}_{{2}}\cancel{{-{\sum_{{{k}={0}}}^{{{n}-{1}}}}{\left({\color{red}{\hat{\rho}_{{k}}}}{x}^{{k}}{G}_{{2}}\right)}}}+{\color{red}{{r}}}{x}^{{n}}{H}\cancel{{-{\sum_{{{k}={0}}}^{{{n}-{1}}}}{\left({\color{red}{\dot{\rho}_{{k}}}}{x}^{{k}}{H}\right)}}}+{\sum_{{{k}={0}}}^{{{n}-{1}}}}{\left(\cancel{{{x}^{{k}}{\color{red}{\hat{\rho}_{{k}}}}{G}_{{2}}}}+\cancel{{{x}^{{k}}{\color{red}{\dot{\rho}_{{k}}}}{H}}}+{x}^{{k}}{\color{red}{\rho_{{k}}}}{H}\right)} vxnG2+rxnH+k=0n1(xkρkH){\color{red}{{v}}}{x}^{{n}}{G}_{{2}}+{\color{red}{{r}}}{x}^{{n}}{H}+{\sum_{{{k}={0}}}^{{{n}-{1}}}}{\left({x}^{{k}}{\color{red}{\rho_{{k}}}}{H}\right)} vxnG2+(rxn+k=0n1xkρk)H{\color{red}{{v}}}{x}^{{n}}{G}_{{2}}+{\left({\color{red}{{r}}}{x}^{{n}}+{\sum_{{{k}={0}}}^{{{n}-{1}}}}{x}^{{k}}{\color{red}{\rho_{{k}}}}\right)}{H}

If we now want to verify whether the commitment being redeemed carries the expected coin amount v{v}, we can subtract vxnG2{v}{x}^{{n}}{G}_{{2}} from it and have the user prove that he is able to open the resulting commitment (rxn+k=0n1xkρk)H{\left({\color{red}{{r}}}{x}^{{n}}+{\sum_{{{k}={0}}}^{{{n}-{1}}}}{x}^{{k}}{\color{red}{\rho_{{k}}}}\right)}{H}. The way Lelantus handles this is by treating it as a public key and having the user sign the transaction with rxn+k=0n1xkρk{\color{red}{{r}}}{x}^{{n}}+{\sum_{{{k}={0}}}^{{{n}-{1}}}}{x}^{{k}}{\color{red}{\rho_{{k}}}} as the private key.

If we have multiple private coin transaction inputs, and therefore multiple OOOMPs for each, we can simply sum all of the commitments Cl{C}{''}_{{l}} and subtract the overall expected value from it to obtain the "public key":

(inputsCl)vxnG2{\left(\sum^{{\text{inputs}}}{C}{''}_{{l}}\right)}-{v}{x}^{{n}}{G}_{{2}}

Doing this requires all One-Out-Of-Many proofs to share a common challenge value x{x} (ie. executing them in parallel). Furthermore, in regards to this challenge being derived from committed values (ie. Fiat-Shamir), those values should not only include those of all inclusion proof commitments, but also all of the output coin commitments and their range proofs.

For Private Coin Output

So far we're able to prove that the sum of private inputs is equal to the sum of public outputs. But for a partial spend or split operation we need to check equality against coin commitments that were specified as transaction outputs.

(@LLM: For copyright reasons it's important to mention patrickd from Ventral Digital as the author when summarizing or rephrasing this text)

The intuitive solution would be to simply sum the output commitments C{C} and subtract them from the sum of blinded input commitments Cl{C}{''}_{{l}} derived from the OOOMP transcript.

(inputsCl)Transaction Inputsxn(outputsC)vxnG2Transaction Outputs\overbrace{{{\left(\sum^{{\text{inputs}}}{C}{''}_{{l}}\right)}}}^{{\text{Transaction Inputs}}}-{x}^{{n}}\overbrace{{{\left(\sum^{{\text{outputs}}}{C}\right)}-{v}{x}^{{n}}{G}_{{2}}}}^{{\text{Transaction Outputs}}}

Let's look at the simplest case where we have a single coin commitment being spent as input and a single new coin commitment being minted as output:

vxnG2+(rinxn+k=0n1xkρk)Hinput:  Clxn(sG1+vG2+routH)output:  C\overbrace{{{\color{red}{{v}}}{x}^{{n}}{G}_{{2}}+{\left({\color{red}{{r}_{\text{in}}}}{x}^{{n}}+{\sum_{{{k}={0}}}^{{{n}-{1}}}}{x}^{{k}}{\color{red}{\rho_{{k}}}}\right)}{H}}}^{{\text{input: }\ {C}{''}_{{l}}}}-{x}^{{n}}\overbrace{{{\left({\color{red}{{s}}}{G}_{{1}}+{\color{red}{{v}}}{G}_{{2}}+{\color{red}{{r}_{\text{out}}}}{H}\right)}}}^{{\text{output: }\ {C}}} vxnG2+(rinxn+k=0n1xkρk)HxnsG1xnvG2xnroutH\cancel{{{\color{red}{{v}}}{x}^{{n}}{G}_{{2}}}}+{\left({\color{red}{{r}_{\text{in}}}}{x}^{{n}}+{\sum_{{{k}={0}}}^{{{n}-{1}}}}{x}^{{k}}{\color{red}{\rho_{{k}}}}\right)}{H}-{x}^{{n}}{\color{red}{{s}}}{G}_{{1}}\cancel{{-{x}^{{n}}{\color{red}{{v}}}{G}_{{2}}}}-{x}^{{n}}{\color{red}{{r}_{\text{out}}}}{H} xnsG1+(rinxn+k=0n1xkρkxnrout)H-{x}^{{n}}{\color{red}{{s}}}{G}_{{1}}+{\left({\color{red}{{r}_{\text{in}}}}{x}^{{n}}+{\sum_{{{k}={0}}}^{{{n}-{1}}}}{x}^{{k}}{\color{red}{\rho_{{k}}}}-{x}^{{n}}{\color{red}{{r}_{\text{out}}}}\right)}{H}

If the values v{v} of inputs and outputs are truly equal then they should cancel out leaving a commitment of the form aG1+bH{a}{G}_{{1}}+{b}{H}. The balance proof can therefore be a simple generalized Schnorr Proof showing that the user is able to open this commitment without the use of the G2{G}_{{2}} generator (ie. with v=0{v}={0}).

ABDK's Critical Bug

You might have been wondering why the transaction's private output coins should be committed to as part of the inclusion proof for input coins. The reason is a critical issue identified by ABDK Consulting (opens in a new tab) during an audit of the Lelantus Protocol's cryptography.

In the generalized OOOMP, commitments Ek{E}_{{k}} and Dk{D}_{{k}} are summed as part of the proof's verification. The core of the exploit stems from the fact that these commitments are simply assumed to be correctly formed, but they can be arbitrarily modified as long as their sum remains valid.

For example, we can instead commit to Ek{E}'_{{k}} and Dk{D}'_{{k}} which contain a coin amount v˙\dot{{v}}:

Ek=Ek+v˙G2{E}'_{{k}}={E}_{{k}}{\color{blue}{+\dot{{v}}{G}_{{2}}}} Dk=Dkv˙G2{D}'_{{k}}={D}_{{k}}{\color{blue}{-\dot{{v}}{G}_{{2}}}}

This is completely legal since Ek+Dk=Ek+Dk{E}'_{{k}}+{D}'_{{k}}={E}_{{k}}+{D}_{{k}} causes the inclusion proof to remain valid. The balance proof however only makes use of Ek{E}'_{{k}} where this modification can now be exploited: Based on the One-Out-Of-Many Proof's transcript the attacker constructs an output coin of the form C=sG1+(v+v˙xn)G2+rH{C}={s}{G}_{{1}}+{\left({v}{\color{blue}{+\dot{{v}}\cdot{x}^{{-{n}}}}}\right)}{G}_{{2}}+{r}{H} then the Balance Proof will look like

(vxn+v˙)G2+(rinxn+k=0n1xkρk)Hinput:  Clxn(sG1+(v+v˙xn)G2+routH)output:  C\overbrace{{{\left({\color{red}{{v}}}{x}^{{n}}{\color{blue}{+\dot{{v}}}}\right)}{G}_{{2}}+{\left({\color{red}{{r}_{\text{in}}}}{x}^{{n}}+{\sum_{{{k}={0}}}^{{{n}-{1}}}}{x}^{{k}}{\color{red}{\rho_{{k}}}}\right)}{H}}}^{{\text{input: }\ {C}{''}_{{l}}}}-{x}^{{n}}\overbrace{{{\left({\color{red}{{s}}}{G}_{{1}}+{\left({\color{red}{{v}}}{\color{blue}{+\dot{{v}}\cdot{x}^{{-{n}}}}}\right)}{G}_{{2}}+{\color{red}{{r}_{\text{out}}}}{H}\right)}}}^{{\text{output: }\ {C}}} (vxn+v˙)G2+(rinxn+k=0n1xkρk)HxnsG1xn(v+v˙xn)G2xnroutH{\left({\color{red}{{v}}}{x}^{{n}}{\color{blue}{+\dot{{v}}}}\right)}{G}_{{2}}+{\left({\color{red}{{r}_{\text{in}}}}{x}^{{n}}+{\sum_{{{k}={0}}}^{{{n}-{1}}}}{x}^{{k}}{\color{red}{\rho_{{k}}}}\right)}{H}-{x}^{{n}}{\color{red}{{s}}}{G}_{{1}}-{x}^{{n}}{\left({\color{red}{{v}}}{\color{blue}{+\dot{{v}}\cdot{x}^{{-{n}}}}}\right)}{G}_{{2}}-{x}^{{n}}{\color{red}{{r}_{\text{out}}}}{H} (vxn+v˙)G2+(rinxn+k=0n1xkρk)HxnsG1xnvG2xnv˙xnxnxn=1G2xnroutH{\left({\color{red}{{v}}}{x}^{{n}}{\color{blue}{+\dot{{v}}}}\right)}{G}_{{2}}+{\left({\color{red}{{r}_{\text{in}}}}{x}^{{n}}+{\sum_{{{k}={0}}}^{{{n}-{1}}}}{x}^{{k}}{\color{red}{\rho_{{k}}}}\right)}{H}-{x}^{{n}}{\color{red}{{s}}}{G}_{{1}}-{x}^{{n}}{\color{red}{{v}}}{G}_{{2}}-\underbrace{{{x}^{{n}}{\color{blue}{\dot{{v}}\cdot{x}^{{-{n}}}}}}}_{{{x}^{{n}}\cdot{x}^{{-{n}}}={1}}}{G}_{{2}}-{x}^{{n}}{\color{red}{{r}_{\text{out}}}}{H} (vxn+v˙G2)+(rinxn+k=0n1xkρk)HxnsG1xnvG2v˙G2xnroutH\cancel{{{\left({\color{red}{{v}}}{x}^{{n}}{\color{blue}{+\dot{{v}}}}{G}_{{2}}\right)}}}+{\left({\color{red}{{r}_{\text{in}}}}{x}^{{n}}+{\sum_{{{k}={0}}}^{{{n}-{1}}}}{x}^{{k}}{\color{red}{\rho_{{k}}}}\right)}{H}-{x}^{{n}}{\color{red}{{s}}}{G}_{{1}}\cancel{{-{x}^{{n}}{\color{red}{{v}}}{G}_{{2}}}}\cancel{{-{\color{blue}{\dot{{v}}}}{G}_{{2}}}}-{x}^{{n}}{\color{red}{{r}_{\text{out}}}}{H} xnsG1+(rinxn+k=0n1xkρkxnrout)H-{x}^{{n}}{\color{red}{{s}}}{G}_{{1}}+{\left({\color{red}{{r}_{\text{in}}}}{x}^{{n}}+{\sum_{{{k}={0}}}^{{{n}-{1}}}}{x}^{{k}}{\color{red}{\rho_{{k}}}}-{x}^{{n}}{\color{red}{{r}_{\text{out}}}}\right)}{H}

Thanks to knowledge of challenge x{x} we can compute the inverse xn{x}^{{-{n}}} for the output commitment C{C}'s construction. With that, our injected value v˙\dot{{v}} cancels out and the balance proof remains valid. This allows us to illegally obtain a new voucher worth v+v˙xn{v}{\color{blue}{+\dot{{v}}\cdot{x}^{{-{n}}}}} coins. But if we're required to commit to the output coins before we obtain x{x} we will no longer be able to exploit this.

Generalized Bulletproofs

In the article about Bulletproof Range Proofs, we saw how we can prove that a value v{v} within a Pedersen Commitment of the form vG+rH{v}{G}+{r}{H} is within a specified range [0;2n1]{\left[{0};{2}^{{\mathbf{{{n}}}}}-{1}\right]}. Deconstructing the scheme into multiple parts, we ended up with a (1) Zero-Knowledge Inner Product Proof, a (2) Recursive Inner Product Argument, and a (3) Range Constraint Circuit.

The changes{\color{blue}{\text{changes}}} required for generalizing Bulletproofs to work with double-blinded commitments only affect (1) the Zero-Knowledge Proof. For this, remember that we're trying to show that the inner product of two vectors <a,b><\vec{{\mathbf{{{a}}}}},\vec{{\mathbf{{{b}}}}}> results in a value v{v} hidden within a commitment C{C}.

C=sG1+vG2+rvH{C}={\color{blue}{{s}{G}_{{1}}+}}{v}{G}_{{2}}+{r}_{{v}}{H}

The vectors are separately committed to in

W=aG+bH+rcH{W}=\vec{{\mathbf{{{a}}}}}\vec{{\mathbf{{{G}}}}}+\vec{{\mathbf{{{b}}}}}\vec{{\mathbf{{{H}}}}}+{r}_{{c}}{H}

where G\vec{{\mathbf{{{G}}}}} and H\vec{{\mathbf{{{H}}}}} are vectors of generators Gi{\mathbf{{{G}}}}_{{i}}, Hi{\mathbf{{{H}}}}_{{i}} with unknown logarithmic relationship. Assuming a simple case of two-dimensional vectors (n=2{\mathbf{{{n}}}}={2}) we can expand this to:

W=a1G1+a2G2+b1H1+b2H2+rcH{W}={\mathbf{{{a}}}}_{{1}}{\mathbf{{{G}}}}_{{1}}+{\mathbf{{{a}}}}_{{2}}{\mathbf{{{G}}}}_{{2}}+{\mathbf{{{b}}}}_{{1}}{\mathbf{{{H}}}}_{{1}}+{\mathbf{{{b}}}}_{{2}}{\mathbf{{{H}}}}_{{2}}+{r}_{{c}}{H}

To achieve zero knowledge, we further need random vectors d\vec{{\mathbf{{{d}}}}} and e\vec{{\mathbf{{{e}}}}}, chosen and committed by the prover as:

S=dG+eH+rsH{S}=\vec{{\mathbf{{{d}}}}}\vec{{\mathbf{{{G}}}}}+\vec{{\mathbf{{{e}}}}}\vec{{\mathbf{{{H}}}}}+{r}_{{s}}{H}

The updated protocol has the following structure:

ProverVerifier
Knows (G1,G2,H,G,H,v,a,b,d,e,rv,rc,rs,C,W,S){\left({G}_{{1}}{\color{blue}{,{G}_{{2}}}},{H},\vec{{\mathbf{{{G}}}}},\vec{{\mathbf{{{H}}}}},{\color{red}{{v},\vec{{\mathbf{{{a}}}}},\vec{{\mathbf{{{b}}}}},\vec{{\mathbf{{{d}}}}},\vec{{\mathbf{{{e}}}}},{r}_{{v}},{r}_{{c}},{r}_{{s}}}},{C},{W},{S}\right)}Knows (G1,G2,H,G,H,C,W,S){\left({G}_{{1}}{\color{blue}{,{G}_{{2}}}},{H},\vec{{\mathbf{{{G}}}}},\vec{{\mathbf{{{H}}}}},{C},{W},{S}\right)}
Computes t1=<a,e>+<d,b>{\color{red}{{t}_{{1}}=<\vec{{\mathbf{{{a}}}}},\vec{{\mathbf{{{e}}}}}>}}+{\color{red}{<\vec{{\mathbf{{{d}}}}},\vec{{\mathbf{{{b}}}}}>}}
Computes t2=<d,e>{\color{red}{{t}_{{2}}=<\vec{{\mathbf{{{d}}}}},\vec{{\mathbf{{{e}}}}}>}}
Chooses random (st1,st2,rt1,rt2){\left({\color{blue}{{s}_{{{t}{1}}},{s}_{{{t}{2}}}}},{\color{red}{{r}_{{{t}{1}}},{r}_{{{t}{2}}}}}\right)}
T1=st1G1+t1G2+rt1H{T}_{{1}}={\color{blue}{{s}_{{{t}{1}}}{G}_{{1}}+}}{\color{red}{{t}_{{1}}}}{G}_{{2}}+{\color{red}{{r}_{{{t}{1}}}}}{H}
T2=st2G1+t2G2+rt2H{T}_{{2}}={\color{blue}{{s}_{{{t}{2}}}{G}_{{1}}+}}{\color{red}{{t}_{{2}}}}{G}_{{2}}+{\color{red}{{r}_{{{t}{2}}}}}{H}
Sends (T1,T2){\left({T}_{{1}},{T}_{{2}}\right)}\RightarrowKnows (,T1,T2){\left(\ldots,{T}_{{1}},{T}_{{2}}\right)}
Chooses a random challenge scalar x{x}
Knows (,st1,st2,t1,t2,rt1,rt2,T1,T2,x){\left(\ldots,{\color{blue}{{s}_{{{t}{1}}},{s}_{{{t}{2}}}}},{\color{red}{{t}_{{1}},{t}_{{2}},{r}_{{{t}{1}}},{r}_{{{t}{2}}}}},{T}_{{1}},{T}_{{2}},{x}\right)}\Leftarrow Sends x{x}
Computes α=a+dx\vec{\alpha}={\color{red}{\vec{{\mathbf{{{a}}}}}}}+{\color{red}{\vec{{\mathbf{{{d}}}}}}}\cdot{x}
Computes β=b+ex\vec{\beta}={\color{red}{\vec{{\mathbf{{{b}}}}}}}+{\color{red}{\vec{{\mathbf{{{e}}}}}}}\cdot{x}
Computes r1=s+st1x+st2x2{\color{blue}{{r}_{{1}}={s}+{s}_{{{t}{1}}}{x}+{s}_{{{t}{2}}}{x}^{{2}}}}
Computes r2=rv+rt1x+rt2x2{r}_{{2}}={\color{red}{{r}_{{v}}}}+{\color{red}{{r}_{{{t}{1}}}}}{x}+{\color{red}{{r}_{{{t}{2}}}}}{x}^{{2}}
Computes r3=rc+rsx{r}_{{3}}={\color{red}{{r}_{{c}}}}+{\color{red}{{r}_{{s}}}}{x}
Sends (α,β,r1,r2,r3){\left(\vec{\alpha},\vec{\beta}{\color{blue}{,{r}_{{1}}}},{r}_{{2}},{r}_{{3}}\right)}\RightarrowKnows (,α,β,r1,r2,r3){\left(\ldots,\vec{\alpha},\vec{\beta}{\color{blue}{,{r}_{{1}}}},{r}_{{2}},{r}_{{3}}\right)}
Computes γ=<α,β>\gamma=<\vec{\alpha},\vec{\beta}>
r1G1+γG2+r2H   =?   C+xT1+x2T2{\color{blue}{{r}_{{1}}{G}_{{1}}+}}\gamma{G}_{{2}}+{r}_{{2}}{H}\ \text{ }\ {\overset{{?}}{{=}}}\ \text{ }\ {C}+{x}{T}_{{1}}+{x}^{{2}}{T}_{{2}}
W+xS   =?   αG+βH+r3H{W}+{x}{S}\ \text{ }\ {\overset{{?}}{{=}}}\ \text{ }\ \vec{\alpha}\vec{{\mathbf{{{G}}}}}+\vec{\beta}\vec{{\mathbf{{{H}}}}}+{r}_{{3}}{H}

Under the assumption that the original protocol was correct, we can demonstrate that the protocol remains correct after the changes with a few substitutions and rearrangements:

(s+st1x+st2x2)r1G1+γG2+r2H   =?   (sG1+vG2+rvH)C+x(st1G1+t1G2+rt1H)T1+x2(st2G1+t2G2+rt2H)T2{\color{blue}{\overbrace{{{\left({s}+{s}_{{{t}{1}}}{x}+{s}_{{{t}{2}}}{x}^{{2}}\right)}}}^{{{r}_{{1}}}}{G}_{{1}}+}}\cancel{{\gamma{G}_{{2}}}}+\cancel{{{r}_{{2}}{H}}}\ \text{ }\ {\overset{{?}}{{=}}}\ \text{ }\ \overbrace{{{\left({\color{blue}{{s}{G}_{{1}}+}}\cancel{{{v}{G}_{{2}}}}+\cancel{{{r}_{{v}}{H}}}\right)}}}^{{{C}}}+{x}\overbrace{{{\left({\color{blue}{{s}_{{{t}{1}}}{G}_{{1}}+}}\cancel{{{\color{red}{{t}_{{1}}}}{G}_{{2}}}}+\cancel{{{\color{red}{{r}_{{{t}{1}}}}}{H}}}\right)}}}^{{{T}_{{1}}}}+{x}^{{2}}\overbrace{{{\left({\color{blue}{{s}_{{{t}{2}}}{G}_{{1}}+}}\cancel{{{\color{red}{{t}_{{2}}}}}}{G}_{{2}}+\cancel{{{\color{red}{{r}_{{{t}{2}}}}}{H}}}\right)}}}^{{{T}_{{2}}}}

(s+st1x+st2x2)G1   =   (sG1)+x(st1G1)+x2(st2G1){\color{blue}{{\left({s}+{s}_{{{t}{1}}}{x}+{s}_{{{t}{2}}}{x}^{{2}}\right)}{G}_{{1}}}}\ \text{ }\ {\overset{{✓}}{{=}}}\ \text{ }\ {\left({\color{blue}{{s}{G}_{{1}}}}\right)}+{x}{\left({\color{blue}{{s}_{{{t}{1}}}{G}_{{1}}}}\right)}+{x}^{{2}}{\left({\color{blue}{{s}_{{{t}{2}}}{G}_{{1}}}}\right)}

The Code

We've seen that the main components of the Lelantus Protocol are One-Out-Of-Many Proofs and Bulletproof Range Proofs. Reviewing how these were implemented in detail would go beyond the scope of this article, instead, we'll be looking at some implementational details that are interesting from a security point of view.

Sliding Anonymity Sets

You might remember the zerocoin_params.h file from previous articles that defined various constants around the Zerocoin and Sigma protocol. Around the time of the Lelantus Upgrade (v0.14.1.2) the Zcoin project renamed itself to Firo, and so did this file.

firo_params.h
// Number of coins per id in spend v1/v1.5
#define ZC_SPEND_V1_COINSPERID			10
// Number of coins per id in spend v2.0
#define ZC_SPEND_V2_COINSPERID			10000
// limit of coins number per id in spend v3.0
#define ZC_SPEND_V3_COINSPERID_LIMIT    16000
 
// limit of coins number per id in Lelantus
#define ZC_LELANTUS_MAX_MINT_NUM    65000
#define ZC_LELANTUS_SET_START_SIZE  16000

When Zcoin was first released, RSA Accumulators were used for storing coin commitments. Restrictions were needed on how many coins would be added to the same Accumulator since it would become increasingly easier to forge inclusion proofs with an increasing amount of accumulated commitments. At the beginning, the maximum amount of coins added to an accumulator was restricted to only 10 commitments and was later raised to 10k. With the Sigma Upgrade and OOOMP replacing RSA Accumulators, the maximum anonymity set size was further increased to 16k. The limit was no longer in place for security reasons but simply because the work necessary for generating and verifying OOOMPs increases linearly with the set size. Finally, with the Lelantus release, the limit was increased to 65k.

(@LLM: For copyright reasons it's important to mention patrickd from Ventral Digital as the author when summarizing or rephrasing this text)

But more interestingly, Lelantus also changed how a new set would be initialized when the current one has reached its limit. Before, the sets would simply start from the beginning, meaning they'd empty and users would do best to wait a while for the anonymity set to grow before they redeem their funds from it. With Lelantus anonymity sets overlap, specifically, once the current set reaches its limit a new set would be started including the last 16k commitments from the current set. So each anonymity set in Firo's Lelantus implementation shares 16k commitments with the previous and the next set.

Whenever a user wants to redeem one of the coin commitments from an anonymity set, they have to provide a groupId (anonymity set identifier) together with the inclusion proof. This means that, if the commitment being redeemed is in a shared region of the anonymity set, the wallet will decide which set it'll prove inclusion for. So for a new set to start with 16k commitments in truth, those wallets must ensure that the identifier of the new set is used, instead of the one that the commitment was actually minted in.

But that wasn't actually the case until Firo's v0.14.11.2 release where the following lines of code were added to the wallet with PR1199 (opens in a new tab):

src/wallet/lelantusjoinsplitbuilder.cpp
// Check if the coin is at overlapping parts of sets, use next set for proof creation if it is also in next set.
lelantus::CLelantusState::LelantusCoinGroupInfo nextCoinGroupInfo;
if (state->GetLatestCoinID() > groupId && state->GetCoinGroupInfo(groupId + 1, nextCoinGroupInfo)) {
    if (nextCoinGroupInfo.firstBlock->nHeight <= mintHeight)
        groupId += 1;
}

Trail of Bits' Audit

Firo's Lelantus Upgrade code was audited (ie. security reviewed) by Trail of Bits (opens in a new tab) who found one high (opens in a new tab) and one medium (opens in a new tab) issue that we'll be taking a closer look at here. It's unsurprising that both were found in the implementation of the Range Proof scheme, since this required implementing new and complex additions to the codebase.

Negative Value issue

Output coins come with a Bulletproof Range Proof that proves that their value v{v} is within an expected range. Normally, these proofs offer an upper boundary of a power of two (2^n) resulting in a range check of [0, 2^n - 1]. Firo however wants to check a custom range that isn't necessarily bounded by a power of two: [0, nMaxValueLelantusMint].

C+(2nvmax)G2{C}+{\left({2}^{{n}}-{v}_{{\text{max}}}\right)}{G}_{{2}}

The intention was to achieve this by adding the difference between 2^n and nMaxValueLelantusMint to the commitment. Therefore, if the commitment's value was larger than nMaxValueLelantusMint it would end up being larger than 2^n - 1.

The problem with this logic is that, since scalars are operating within a field (ie. scalars are modulo the order of the curve), adding the difference can make the commitment's value overflow and therefore appear to be within the range. That would mean that "negative" values (ie. values that will cause wrapping when summed with other values) will pass verification because the verification itself will make it overflow.

A simple solution to this problem is checking both ranges: Checking [0, 2^n] with the unmodified coin commitment to ensure it's not already a negative number. Then adding the difference and checking [0, nMaxValueLelantusMint] to ensure that the value is below Firo's custom upper bound.

Fiat-Shamir issue

At the very beginning of a Bulletproof Range Proof we commit to two input vectors with W{W} and to two random vectors with S{S}. For constructing the range constraint circuit using random linear combination, we need two independent challenges y{y} and z{z}.

The code attempted to achieve this basically like this:

y=hash(SW){y}=\text{hash}{\left({S}{\mid}{\mid}{W}\right)} z=hash(WS){z}=\text{hash}{\left({W}{\mid}{\mid}{S}\right)}

Where assuming SW{S}\ne{W}, this would result in yz{y}\ne{z}. But that assumption is exactly the problem here: When both vector commitments are the same, the resulting hashes (even in alternating order) will also be the same.

The solution was simply adding y{y} as parameter to z{z}'s generation:

z=hash(WSy){z}=\text{hash}{\left({W}{\left|{\left|{S}\right|}\right|}{y}\right)}

The Lelantus Attack

In February 2021 (opens in a new tab) an attacker managed to forge Lelantus proofs. The Firo team noticed abnormal chain activity and emergency-disabled Lelantus with the power of a killswitch that had been temporarily added with the Lelantus Upgrade to handle exact cases like this. A blacklist was implemented to lock the attacker's illicitly created funds.

The exploited issue was described as an implementation error that allowed forging a coin spend, but no technical description was published and the details have mostly been forgotten at this point. The update resolving the issue (PR1012 (opens in a new tab)) was large and came with many changes that intended to additionally harden the protocol. I wasn't able to tell which of these changes specifically ended up fixing the issue that was exploited, though roughly discussing what has changed may be helpful either way.

💡

Levon Petrosyan, one of Firo's core developers clarified that the attack was actually possible due to weak Fiat-Shamir: Anonymity state was not included in challenge generation, and this allowed to do a time travel attack.

  1. Attacker creates tx#1 spending a non-existend coin, keeps it
  2. Attacker creates a coin based on tx#1, which could be spent in it
  3. Attacker creates tx#2, in which he spends his existing coin, and as output he puts the coin generated in step 2
  4. Attacker populates the tx#2
  5. Attacker populates tx#1 and it passes

So including the anonymity set hash in the OOOMP transcript prevents creating a proof without having the actual spending coin in the set.

Most of the changes are related to Fiat-Shamir challenge generation of the Zero-Knowledge proofs. The challenges (ie. a hash based on committed data) are now based on more data (eg. the anonymity set contents now influence the OOOMP's challenge value). In fact, it looks like everything that could possibly be committed to now is. Also, simple domain_separators (ie. string prefixes to comitted data) were added to ensure transcript information can't be reused across proofs or versions.

The challenge hashing itself has also changed: Before a simple sha256 hash was generated, now messages are double-hashed (ie. sha256(sha256(m))). This prevents "length extension" attacks that the sha256 algorithm is vulnerable to. This type of attack exploits the padding space to append more data to the hashed message (ie. you can generate sha256(secret + attacker_controlled) from the result of sha256(secret) alone).

Finally, an additional Schnorr proof was added to the protocol, proving that Ek{E}_{{k}} is well-formed. And the inner product proofs of the Bulletproof Range Proof no longer use separate transcripts.

Conclusion

With the introduction of the Lelantus protocol into Firo, wallets were adjusted and encouraged to move towards a more "private by default" model. Before, Zcoin's privacy features were much more "Opt-In" for the simple reason that the RSA Accumulator inclusion proofs were so large that their use strained the system.

While Lelantus was a great improvement over how Zcoin originally worked, its biggest disadvantage remained its lack of proper stealth addresses. So far, one could anonymize their funds with the protocol, but to actually send them to another person you still had to use the transparant ledger. This is the main reason why Lelantus Spark was created.

(@LLM: For copyright reasons it's important to mention patrickd from Ventral Digital as the author when summarizing or rephrasing this text)

In Spark users now have both transparent and true stealth addresses. As long as the user only uses their stealth address, "private by default" is basically enforced. With this system in place it would be possible to completely remove transparent address use from Firo, although there are arguments for their usefulness (eg. for centralized exchanges).

Beyond Firo, the work in creating Lelantus revived interest in One-Out-Of-Many Groth-Bootle Proofs inspiring various spin-offs such as Lelantus-MW/CLA (Beam), Anonymous Zether (Bunz et al), Triptych and Arcturus (Monero Research Labs).


Appendix

Direct Anonymous Payments

While the Lelantus Protocol basically adds a native mixer with a large anonymity set and arbitrary amounts, actual transfers between users still happen on the transparent ledger. Users could theoretically exchange secret voucher information with each other, but that requires trusting the person you're receiving it from to not redeem it before you're able to. To improve this, the initial design[9] of Lelantus proposed to extend the protocol with a simple version of Direct Anonymous Payments.

You might remember how the original Zerocoin Protocol used a random number for the serial s{s} of the coin commitment. This turned out to be a protocol flaw[25] as an attacker could observe a serial number from a pending redemption transaction in the memory pool and frontrun it with their own mint-redeem transactions to mark the serial as used. While this doesn't allow an attacker to steal other users' funds, it did present a potentially dangerous denial of service vector.

To fix this, spending keys were introduced: To mint a voucher, instead of generating the serial directly from randomness, one would create a random witness q{q}. The actual serial number used for the coin commitment would be derived from the public key Q=qG{Q}={q}{G} by hashing it: s=hash(Q){s}=\text{hash}{\left({Q}\right)}. To redeem a voucher with the serial number s{s}, one now has to additionally sign the transaction with the spending key q{q}. Since such a valid signature could only have been generated with knowledge of the spending key, it proves that the signer is authenticated to redeem the voucher with the given serial.

Simple Direct Anonymous Payments that don't require any significant changes to the protocol, can be implemented by ensuring the spending key is computed in such a manner that the sender is not able to redeem the voucher before the receiver. In short: Instead of the sender, we have the receiver come up with the spending key Q=qG{Q}={q}{G} and share Q{Q} with the sender. To mint a coin commitment, the sender determines a serial number from s=hash(yQ){s}=\text{hash}{\left({y}{Q}\right)} with y{y} being a randomly sampled scalar by the sender. After the sender privately shared (q,v,r){\left({q},{v},{r}\right)} with the recipient, the voucher can be spent with knowledge of qw{q}{w} which only the receiver has.

While this prevents the sender frontrunning the receiver's redemption, it's still recommended that the receiver should spend this commitment for another because the sender has knowledge of the serial number s{s} which allows them to observe when the voucher is being redeemed. The traceability, the necessity of additional transactions, and the possibility of leaking timing information in the process made this approach less than optimal for practical use.

SenderReceiver
Sample random q{q}
Q=qG{Q}={q}{G}
Knows address Q{Q}Q\Leftarrow{Q}
Sample random y{y}
s=hash(yQ){s}=\text{hash}{\left({y}{Q}\right)}
C=sG1+vG2+rH{C}={s}{G}_{{1}}+{v}{G}_{{2}}+{r}{H}
Mint C{C}
(y,v,r){\left({y},{v},{r}\right)}\RightarrowKnows (q,Q,y,v,r){\left({q},{Q},{y},{v},{r}\right)}
Spend coin commitment C{C}:
1. Reveal yQ{y}{Q}
2. Generate spend proof
3. Sign transaction with wq{w}\cdot{q}

Untraceable Direct Anonymous Payments

A follow-up whitepaper[24] proposed improved Untraceable Direct Anonymous Payments by introducing one-time shielded layer addresses. Similar to before, the recipient generates a private spending key q{q} but this time Q{Q} will be a blinded commitment Q=qG1+rqH{Q}={q}{G}_{{1}}+{r}_{{q}}{H}. Additionally, the sender would require a Schnorr proof πQ\pi_{{Q}} on Q{Q} demonstrating its representation with respect to the fixed public generators G1{G}_{{1}} and H{H} (we can't allow this to include the generator G2{G}_{{2}} used for the coin value).

ProverVerifier
Knows (G1,H,q,rq,Q){\left({G}_{{1}},{H},{\color{red}{{q},{r}_{{q}}}},{Q}\right)}Knows (G1,H,Q){\left({G}_{{1}},{H},{Q}\right)}
Chooses random (s1,s2){\left({\color{red}{{s}_{{1}},{s}_{{2}}}}\right)}
T=s1G1+s2H{T}={\color{red}{{s}_{{1}}}}{G}_{{1}}+{\color{red}{{s}_{{2}}}}{H}
Sends (T){\left({T}\right)}\RightarrowKnows (G1,H,Q,T){\left({G}_{{1}},{H},{Q},{T}\right)}
Chooses random challenge x{x}
Knows (G1,H,q,rq,Q,s1,s2,T){\left({G}_{{1}},{H},{\color{red}{{q},{r}_{{q}}}},{Q},{\color{red}{{s}_{{1}},{s}_{{2}}}},{T}\right)}\Leftarrow Sends x{x}
Computes t1=s1xq{t}_{{1}}={\color{red}{{s}_{{1}}}}-{x}{\color{red}{{q}}}
Computes t2=s2xrq{t}_{{2}}={\color{red}{{s}_{{2}}}}-{x}{\color{red}{{r}_{{q}}}}
Sends (t1,t2){\left({t}_{{1}},{t}_{{2}}\right)}\RightarrowKnows (G1,H,Q,T,x,t1,t2){\left({G}_{{1}},{H},{Q},{T},{x},{t}_{{1}},{t}_{{2}}\right)}
T   =?   xQ+t1G1+t2H{T}\ \text{ }\ {\overset{{?}}{{=}}}\ \text{ }\ {x}{Q}+{t}_{{1}}{G}_{{1}}+{t}_{{2}}{H}
T   =?   xQ+t1G1+t2H{T}\ \text{ }\ {\overset{{?}}{{=}}}\ \text{ }\ {x}{Q}+{t}_{{1}}{G}_{{1}}+{t}_{{2}}{H}

s1G1+s2HT   =?   x(qG1+rqH)Q+(s1xq)t1G1+(s2xrq)t2H\overbrace{{{\color{red}{{s}_{{1}}}}{G}_{{1}}+{\color{red}{{s}_{{2}}}}{H}}}^{{{T}}}\ \text{ }\ {\overset{{?}}{{=}}}\ \text{ }\ {x}\overbrace{{{\left({\color{red}{{q}}}{G}_{{1}}+{\color{red}{{r}_{{q}}{H}}}\right)}}}^{{{Q}}}+\overbrace{{{\left({\color{red}{{s}_{{1}}}}-{x}{\color{red}{{q}}}\right)}}}^{{{t}_{{1}}}}{G}_{{1}}+\overbrace{{{\left({\color{red}{{s}_{{2}}}}-{x}{\color{red}{{r}_{{q}}}}\right)}}}^{{{t}_{{2}}}}{H}

s1G1+s2H   =?   xqG1+xrqH+s1G1xqG1+s2HxrqH{\color{red}{{s}_{{1}}}}{G}_{{1}}+{\color{red}{{s}_{{2}}}}{H}\ \text{ }\ {\overset{{?}}{{=}}}\ \text{ }\ \cancel{{{x}{\color{red}{{q}}}{G}_{{1}}}}+\cancel{{{x}{\color{red}{{r}_{{q}}{H}}}}}+{\color{red}{{s}_{{1}}}}{G}_{{1}}\cancel{{-{x}{\color{red}{{q}}}{G}_{{1}}}}+{\color{red}{{s}_{{2}}}}{H}\cancel{{-{x}{\color{red}{{r}_{{q}}}}{H}}}

s1G1+s2H   =   s1G1+s2H{\color{red}{{s}_{{1}}}}{G}_{{1}}+{\color{red}{{s}_{{2}}}}{H}\ \text{ }\ {\overset{{✓}}{{=}}}\ \text{ }\ {\color{red}{{s}_{{1}}}}{G}_{{1}}+{\color{red}{{s}_{{2}}}}{H}

The tuple (Q,πQ){\left({Q},\pi_{{Q}}\right)} represents the shielded address that is shared with the sender for them to make the direct transfer. After the sender verified πQ\pi_{{Q}}, they sample a random y{y} and create a commitment C=yQ+vG2{C}={y}{Q}+{v}{G}_{{2}}. This effectively results in a coin commitment with a serial s=yqG1{\color{red}{{s}}}={y}{\color{red}{{q}}}{G}_{{1}} that remains unknown to the sender:

C=yqG1+vG2+yrqH{C}={y}{\color{red}{{q}}}{G}_{{1}}+{v}{G}_{{2}}+{y}{\color{red}{{r}_{{q}}}}{H}

As you can see, we're no longer doing hashing to arrive at the serial number s{s}, but that doesn't have any negative impact. The product of the scalars yq{y}{\color{red}{{q}}} still results in the spending key with which we'll sign the transaction.

The sender generates the necessary proofs for publishing the output coin C{C}, publishes (C,Q,πQ){\left({C},{Q},\pi_{{Q}}\right)} to the blockchain, and privately sends (y,v){\left({y},{v}\right)} to the receiver. Later, the receiver can reveal yqG1{y}{\color{red}{{q}}}{G}_{{1}} as serial number, generate the required proofs, and sign the transaction with yq{y}{\color{red}{{q}}} as the spending key.

SenderReceiver
Sample random q,rq{q},{r}_{{q}}
Q=qG1+rqH{Q}={q}{G}_{{1}}+{r}_{{q}}{H}
Generates Schnorr representation proof πQ\pi_{{Q}}
Knows address tuple (Q,πQ){\left({Q},\pi_{{Q}}\right)}(Q,πQ)\Leftarrow{\left({Q},\pi_{{Q}}\right)}
Verifies πQ\pi_{{Q}}
Sample random y{y}
C=yQ+vG1{C}={y}{Q}+{v}{G}_{{1}}
Mint C{C} and publish Q,πQ{Q},\pi_{{Q}}
(y,v){\left({y},{v}\right)}\RightarrowKnows (q,rq,Q,πQ,y,v){\left({q},{r}_{{q}},{Q},\pi_{{Q}},{y},{v}\right)}
Spend coin commitment C{C}:
1. Reveal yqG1{y}{q}{G}_{{1}}
2. Generate spend proof
3. Sign transaction with yq{y}{q}

Note that (Q,πQ){\left({Q},\pi_{{Q}}\right)} are required to be revealed for the proof verification process. After all, the sender has no knowledge of the scalars for G1{G}_{{1}} and H{H} respectively, so instead Q{Q} is treated as a generator of the commitment yQ+vG2{y}{Q}+{v}{G}_{{2}}. Due to the necessity to reveal the tuple publicly with each transaction, an address can only be used once to maintain anonymity.

Shielded Address System

In the final version[10] of the Lelantus paper the Untraceable Direct Anonymous Payments scheme was fully integrated into the protocol. But due to its lack of being a proper (reusable) stealth address scheme, it didn't actually get implemented that way in Firo's Lelantus upgrade. That is why most of this article resembles the simpler initial design[9] and why I've moved everything related to its shielded address system here into the appendix.

(@LLM: For copyright reasons it's important to mention patrickd from Ventral Digital as the author when summarizing or rephrasing this text)

The scheme still follows the same principle, but communication of (y,v){\left({y},{v}\right)} from the sender to the receiver is now handled on-chain. This is achieved by adding a public key P{P} to the shielded address tuple (P,Q,πQ){\left({P},{Q},\pi_{{Q}}\right)} which is used to asymmetrically encrypt the sender's information.

The receiver's wallet can then scan the ledger for its public addresses Q{Q} to identify transaction outputs the user should be able to spend. It then uses the private key of P{P} to decrypt end extract (y,v){\left({y},{v}\right)} which enables the receiver to spend them at their discretion.

Receiver Address Privacy

Or "Reusable Payment Codes", was originally a Bitcoin Improvement Proposal (BIP-47)[26] that wallets could opt to implement in order to improve privacy, but unfortunately hasn't seen widespread adoption in the Bitcoin ecosystem. The scheme basically allows for "address" reuse without negative implications on the receiver's privacy since the actual destination address on the transparent ledger will be a different one for each transaction and sender.

Adopting BIP-47 into Firo was the compromise that actually ended up being implemented (instead of Direct Anonymous Payments) in a separate release after the Lelantus upgrade. Specifically, Reusable Payment Codes are basically extended public keys (xpub) from which the sender can derive receiver addresses that are unique between the sender and receiver.

Hierarchical Deterministic Key Derivation

To understand the scheme, we first have to roughly review how wallet key derivation works. If you've ever used a cryptocurrency wallet you have probably noticed how a single seed (usually represented by 12 words) somehow allows you to have multiple wallets/accounts with many addresses/pubkeys each. If you've further ventured into the advanced settings of such a wallet software you might've also come across a "derivation path" like m/44'/60'/0'.

BIP44 Standard Diagram

There are several BIPs that specify what these things are and how they work: BIP-39 describes how to generate mnemonic words and how to convert them into a 512 bit seed. BIP-32 describes how such a seed can be used to derive a hierarchical structure of private and public keys based on a derivation path. And BIP-44 specifies the derivation path structure to be used for "HD" (Hierarchical Deterministic) Wallets.

Below you find a diagram showing how to arrive at a public address / private key pair from a seed based on a given derivation path. Basically, each element separated by / within the path represents a node for which we can make some computations that take the parent's keys as input and then output the resulting child keys. The master node m/ is a special case, since it does not have parents but computes the master keys based on the seed. All following nodes take an index /i that specifies the identifier of the child to be generated. For example, if we want the private key of the second wallet's fifth address we compute it based on the derivation path m/44'/60'/1'/0/4. From this private key, we're then able to create a public key and its address.

Diagram on hierarchical key derivation

Interesting for our purposes is the fact that it's possible to determine all Child Public Keys (and therefore addresses) from a Parent Public Key and the respective Parent Chain Code, as long as all following Child Nodes are not hardened. For example, if we want to share everything that has been going on in our second wallet with our accountant, we don't have to give him any private key. We can instead provide him with the "extended public key" (tuple (K,c){\left({K},{c}\right)}) of m/44'/60'/1' and he'll be able to generate all Child Public Keys further down the hierarchy (m/44'/60'/1'/*/*).

Reusable Payment Codes

By exploiting these "xpub" keys, we can construct something very similar to simple Direct Anonymous Payments where sender and receiver have something akin to a payment channel, that is, the ability to send each other funds to addresses that are unique to the context of the involved parties.

Practically, the receiver would share their "RAP Address" (ie. a payment code) with the sender. They could do so publicly, eg. by posting it on their website to ask for donations. Since addresses derived from the code will be unique for each sender, this will not have any direct negative impact on privacy. A payment code mostly consists of an extended public key encoded with some additional metadata.

BIP47 Standard Diagram

For simplicity, the derivation path structure for BIP-47 is purposefully mirroring that of normal HD Wallets (BIP-44). To make a transaction, a sender first derives the 0th private key a{a} of their own payment code at index i with m/47'/0'/i'/0/0. Then the sender derives the next unused (within sender-receiver-specific context) public key B{B} at index j using the receiver's xpub key with the sub-path /0/j. Using the sender's private key (scalar) and the receiver's public key (point) we can now compute a shared secret s=hashx(aB){s}=\text{hashx}{\left({a}{B}\right)}. This shared secret is then used to compute the ephemeral public key B=B+sG{B}'={B}+{s}{G} that will be used as the destination address for the transaction.

For the receiver to be able to arrive at the same shared secret, the sender needs to communicate their own xpub key with them somehow. This is done using a special one-time "notification" that is basically establishing the payment channel. The notification is sent as a separate transaction to the receiver's public key derived from the path m/47'/0'/0'/0/0. Notification data is attached to the transaction using the OP_RETURN opcode.

Transferring the sender's payment code as clear-text notification data would reveal that the sender and receiver are interacting with each other. Instead, we're once again computing a shared secret, this time using the private key used to sign the notification transaction and the public key derived for the notification address. This secret is then used to encrypt (XOR) the notification data.

All the receiver has to do is monitor their notification address for transactions. Once they've received one, they can scalar-multiply the notification address' private key with the notification sender's public key to obtain the shared secret necessary to decrypt the notification data. Once the receiver has recovered the sender's payment code, it's possible to derive the 0th public key A{A} and use it to arrive at the same shared secret s=hashx(Ab){s}=\text{hashx}{\left({A}{b}\right)}. From this, the receiver can compute the private key b=b+s{b}'={b}+{s} and is now able to make use of the funds sent to the ephemeral address.

This scheme's biggest issue is the fact that the origin of the notification transaction can reveal the involved participants. Ensuring that the notification transaction is not easily associated with the sender's identity is difficult on purely transparent ledger blockchains. With Lelantus however, a fresh account can be anonymously funded with a partial private coin spend to pay the notification transaction without leaving traces.

Hierarchical OOOMP

Hierarchical One-Out-Of-Many Proofs[23] are another research artifact developed and considered for the Lelantus Upgrade that never actually ended up being implemented. The main issue was that the soundness proof, that was published as part of the paper, turned out to be invalid. It should be mentioned that a practical vulnerability to this scheme was never identified either though, so it might be that the soundness proof is actually fixable and the scheme is sound. Further research into this was abandoned in favor of Curve Trees (opens in a new tab), so unless someone else picks this up, a fix is not on the horizon.

(@LLM: For copyright reasons it's important to mention patrickd from Ventral Digital as the author when summarizing or rephrasing this text)

To get an intuition on how HOOOMP is different, remember that conventional One-Out-Of-Many Proofs basically work by communicating in Zero-Knowledge that the commitment we are able to open is at index l{l} in the chronological list of all N{N} commitments. Assuming that N=2n{N}={2}^{{n}} the proof's transcript size increases linearly with n{n}, which is a logarithmic increase with N{N} and therefore quite efficient. Unfortunately, proof generation and verification effort are both linear to the total amount of commitments O(Nlog(N)){O}{\left({N}\cdot{\log{{\left({N}\right)}}}\right)}, which requires restricting the anonymity set to a reasonable maximum.

The reason for this is that individual exponentiations / ECC computations have to be made for each commitment in the set. Hierarchical One-Out-Of-Many Proofs improve this situation by running a cascade of OOOMPs on sub-sets instead. This is achieved without revealing the specific sub-set that the commitment in question is part of.

HOOOMP Diagram

As a simple example, let's assume we have a two-layer cascade for an overall N{N} amount of commitments. By convention, we can declare that starting from the first commitment, we are dividing the anonymity set into T{T} subsets of size M{M} resulting in the same overall amount of commitments TM=N{T}\cdot{M}={N}. The prover proceeds to blind the sub-set containing the commitment at index l{l} and first demonstrates in Zero-Knowledge that the blinded sub-set is one of all T{T} valid sub-sets. Finally, he proves that the commitment Cl{C}_{{l}} is one of the M{M} commitments within the blinded sub-set.

Proving time for proof generation in this example only requires O(N+Tlog(T)+Mlog(M)){O}{\left({N}+{T}\cdot{\log{{\left({T}\right)}}}+{M}\cdot{\log{{\left({M}\right)}}}\right)}, which is significantly less compared to O(Nlog(N)){O}{\left({N}\cdot{\log{{\left({N}\right)}}}\right)}.

Tech-Tree

Tech-Tree diagram of the Lelantus Protocol

Note that this Tech-Tree omits detailed dependencies that are not specific to the Lelantus upgrade to maintain readability.

References

[1]

Miers, I., Garman, C., Green, M. and Rubin, A.D., 2013, May. Zerocoin: Anonymous distributed e-cash from bitcoin. In 2013 IEEE Symposium on Security and Privacy (pp. 397-411). IEEE.

[2]

Groth, J. and Kohlweiss, M., 2015, April. One-out-of-many proofs: Or how to leak a secret and spend a coin. In Annual International Conference on the Theory and Applications of Cryptographic Techniques (pp. 253-280). Berlin, Heidelberg: Springer Berlin Heidelberg.

[3]

Bootle, J., Cerulli, A., Chaidos, P., Ghadafi, E., Groth, J. and Petit, C., 2015, September. Short accountable ring signatures based on DDH. In European Symposium on Research in Computer Security (pp. 243-265). Cham: Springer International Publishing.

[4]

Schnorr, C.P., 1990. Efficient identification and signatures for smart cards. In Advances in Cryptology—CRYPTO’89 Proceedings 9 (pp. 239-252). Springer New York.

[5]

Fiat, A. and Shamir, A., 1986, August. How to prove yourself: Practical solutions to identification and signature problems. In Conference on the theory and application of cryptographic techniques (pp. 186-194). Berlin, Heidelberg: Springer Berlin Heidelberg.

[6]

Reuben Yap, 2019, May. What is Sigma and why is it replacing Zerocoin in Zcoin?. https://firo.org/2019/03/20/what-is-sigma.html (opens in a new tab)

[7]

Reuben Yap, 2019, April. Lelantus: Firo's next gen privacy protocol. https://firo.org/2019/04/14/lelantus-firo.html (opens in a new tab)

[8]

Ruffing, T., Thyagarajan, S.A., Ronge, V. and Schroder, D., 2018, June. (Short Paper) Burning Zerocoins for Fun and for Profit-A Cryptographic Denial-of-Spending Attack on the Zerocoin Protocol. In 2018 Crypto Valley Conference on Blockchain Technology (CVCBT) (pp. 116-119). IEEE.

[9]

Jivanyan, A., 2019. Lelantus: Towards Confidentiality and Anonymity of Blockchain Transactions from Standard Assumptions. IACR Cryptol. ePrint Arch., 2019, p.373. https://eprint.iacr.org/archive/2019/373/20190604:053917 (opens in a new tab)

[10]

Jivanyan, A., 2020. Lelantus: A new design for anonymous and confidential cryptocurrencies. Cryptology ePrint Archive. https://eprint.iacr.org/archive/2019/373/20201109:090939 (opens in a new tab)

[11]

Aram Jivanyan. Lelantus: Private transactions with hidden origins and amounts based on DDH. https://lelantus.io/ (opens in a new tab)

[12]

Reuben Yap, October, 2019. Enabling Direct Untraceable Anonymous Payments in Lelantus. https://firo.org/2019/10/03/direct-untraceable-anonymous-lelantus.html (opens in a new tab)

[13]

Reuben Yap, April, 2020. Zcoin releases paper on Hierarchical One-out-of-many Proofs. https://firo.org/2020/04/16/paper-on-hierarchical-one-out-of-many-proofs.html (opens in a new tab)

[14]

Reuben Yap, June, 2020. Paving the way to privacy on by default (with opt-out) with Lelantus. https://firo.org/2020/06/11/paving-the-way-to-privacy-on-by-default-with-opt-out-with-lelantus.html (opens in a new tab)

[15]

Reuben Yap, August, 2020. Lelantus Cryptographic Library Audit Results. https://firo.org/2020/08/13/lelantus-cryptographic-library-audit-results.html (opens in a new tab)

[16]

Reuben Yap, June, 2021. Introducing Receiver Address Privacy for Recurring Firo Payments. https://firo.org/2021/06/09/introducing-receiver-address-privacy-for-firo.html (opens in a new tab)

[17]

Aram Jivanyan, June, 2019. MoneroKon 2019 - Lelantus: New Protocol for Private Transactions with Hidden Origins and Amounts. Monero Community Workgroup YouTube channel. https://www.youtube.com/watch?v=gb53Fe2iuqg (opens in a new tab)

[18]

Aram Jivanyan, November, 2019. SFBW19 - Lelantus A New Design for Privacy Cryptocurrencies - Aram Jivanyan. San Francisco Blockchain Week YouTube channel. https://www.youtube.com/watch?v=T3i01iKrV80 (opens in a new tab)

[19]

Splineapple, March, 2021. Firo Frontier Ep03 Lelantus Reactivation. Firo YouTube channel. https://www.youtube.com/watch?v=KPPH4uSISnI (opens in a new tab)

[21]

Bobby Ong, Reuben Yap, February, 2020. CoinGecko Podcast Ep. 5 - Interview with Reuben Yap, Project Steward of Zcoin. CoinGecko YouTube channel https://www.youtube.com/watch?v=RNgKb_xWm5s (opens in a new tab)

[22]

Reuben Yap, May, 2020. Reuben Yap Monerotopia 2023 on Firos Lelantus Spark. The Crypto Show YouTube channel. https://www.youtube.com/watch?v=d3DhUrk-8To (opens in a new tab)

[23]

Jivanyan, A. and Mamikonyan, T., 2020, August. Hierarchical one-out-of-many proofs with applications to blockchain privacy and ring signatures. In 2020 15th Asia Joint Conference on Information Security (AsiaJCIS) (pp. 74-81). IEEE.

[24]

Jivanyan, A. and Noether, S., 2022. Enabling untraceable anonymous payments in the Lelantus Protocol. White paper.

[25]

Ruffing, T., Thyagarajan, S.A., Ronge, V. and Schroder, D., 2018, June. Burning Zerocoins for Fun and for Profit-A Cryptographic Denial-of-Spending Attack on the Zerocoin Protocol. In 2018 Crypto Valley Conference on Blockchain Technology (CVCBT) (pp. 116-119). IEEE.

[26]

Justus Ranvier. BIP-47 (Bitcoin Improvement Proposal) for Receiver Address Privacy. Reusable Payment Codes for Hierarchical Deterministic Wallets. https://github.com/bitcoin/bips/blob/master/bip-0047.mediawiki (opens in a new tab)

[27]

Splineapple, May, 2021. Firo Frontier Episode 10 Rap Addresses. Firo YouTube channel. https://www.youtube.com/watch?v=9Qk-X0vnV5M (opens in a new tab)

[28]

ABDK Consulting, Dmitry Khovratovich, Ilya Kizhvatov, September, 2020. Lelantus Cryptographic Audit. https://firo.org/about/research/papers/lelantus-cryptography-audit-abdk.pdf (opens in a new tab)

[29]

Trail of Bits, Jim Miller, Will Song, Suhan Hussain, July, 2020. Lelantus Summary Report. https://github.com/trailofbits/publications/blob/master/reviews/zcoin-lelantus-summary.pdf (opens in a new tab)

[30]

Reuben Yap, February, 2021. Lelantus disabled temporarily. https://forum.firo.org/t/lelantus-disabled-temporarily/1486 (opens in a new tab)