// // Copyright Coinbase, Inc. All Rights Reserved. // // SPDX-License-Identifier: Apache-2.0 // // Package bulletproof implements the zero knowledge protocol bulletproofs as defined in https://eprint.iacr.org/2017/1066.pdf package bulletproof import ( crand "crypto/rand" "math/big" "github.com/gtank/merlin" "github.com/pkg/errors" "source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/core/curves" ) // RangeProver is the struct used to create RangeProofs // It specifies which curve to use and holds precomputed generators // See NewRangeProver() for prover initialization. type RangeProver struct { curve curves.Curve generators *ippGenerators ippProver *InnerProductProver } // RangeProof is the struct used to hold a range proof // capA is a commitment to a_L and a_R using randomness alpha // capS is a commitment to s_L and s_R using randomness rho // capTau1,2 are commitments to t1,t2 respectively using randomness tau_1,2 // tHat represents t(X) as defined on page 19 // taux is the blinding factor for tHat // ipp is the inner product proof used for compacting the transfer of l,r (See 4.2 on pg20). type RangeProof struct { capA, capS, capT1, capT2 curves.Point taux, mu, tHat curves.Scalar ipp *InnerProductProof curve *curves.Curve } type RangeProofGenerators struct { g, h, u curves.Point } // NewRangeProver initializes a new prover // It uses the specified domain to generate generators for vectors of at most maxVectorLength // A prover can be used to construct range proofs for vectors of length less than or equal to maxVectorLength // A prover is defined by an explicit curve. func NewRangeProver(maxVectorLength int, rangeDomain, ippDomain []byte, curve curves.Curve) (*RangeProver, error) { generators, err := getGeneratorPoints(maxVectorLength, rangeDomain, curve) if err != nil { return nil, errors.Wrap(err, "range NewRangeProver") } ippProver, err := NewInnerProductProver(maxVectorLength, ippDomain, curve) if err != nil { return nil, errors.Wrap(err, "range NewRangeProver") } return &RangeProver{curve: curve, generators: generators, ippProver: ippProver}, nil } // NewRangeProof initializes a new RangeProof for a specified curve // This should be used in tandem with UnmarshalBinary() to convert a marshaled proof into the struct. func NewRangeProof(curve *curves.Curve) *RangeProof { out := RangeProof{ capA: nil, capS: nil, capT1: nil, capT2: nil, taux: nil, mu: nil, tHat: nil, ipp: NewInnerProductProof(curve), curve: curve, } return &out } // Prove uses the range prover to prove that some value v is within the range [0, 2^n] // It implements the protocol defined on pgs 19,20 in https://eprint.iacr.org/2017/1066.pdf // v is the value of which to prove the range // n is the power that specifies the upper bound of the range, ie. 2^n // gamma is a scalar used for as a blinding factor // g, h, u are unique points used as generators for the blinding factor // transcript is a merlin transcript to be used for the fiat shamir heuristic. func (prover *RangeProver) Prove(v, gamma curves.Scalar, n int, proofGenerators RangeProofGenerators, transcript *merlin.Transcript) (*RangeProof, error) { // n must be less than or equal to the number of generators generated if n > len(prover.generators.G) { return nil, errors.New("ipp vector length must be less than or equal to maxVectorLength") } // In case where len(a) is less than number of generators precomputed by prover, trim to length proofG := prover.generators.G[0:n] proofH := prover.generators.H[0:n] // Check that v is in range [0, 2^n] if bigZero := big.NewInt(0); v.BigInt().Cmp(bigZero) == -1 { return nil, errors.New("v is less than 0") } bigTwo := big.NewInt(2) if n < 0 { return nil, errors.New("n cannot be less than 0") } bigN := big.NewInt(int64(n)) var bigTwoToN big.Int bigTwoToN.Exp(bigTwo, bigN, nil) if v.BigInt().Cmp(&bigTwoToN) == 1 { return nil, errors.New("v is greater than 2^n") } // L40 on pg19 aL, err := getaL(v, n, prover.curve) if err != nil { return nil, errors.Wrap(err, "rangeproof prove") } onen := get1nVector(n, prover.curve) // L41 on pg19 aR, err := subtractPairwiseScalarVectors(aL, onen) if err != nil { return nil, errors.Wrap(err, "rangeproof prove") } alpha := prover.curve.Scalar.Random(crand.Reader) // Calc A (L44, pg19) halpha := proofGenerators.h.Mul(alpha) gaL := prover.curve.Point.SumOfProducts(proofG, aL) haR := prover.curve.Point.SumOfProducts(proofH, aR) capA := halpha.Add(gaL).Add(haR) // L45, 46, pg19 sL := getBlindingVector(n, prover.curve) sR := getBlindingVector(n, prover.curve) rho := prover.curve.Scalar.Random(crand.Reader) // Calc S (L47, pg19) hrho := proofGenerators.h.Mul(rho) gsL := prover.curve.Point.SumOfProducts(proofG, sL) hsR := prover.curve.Point.SumOfProducts(proofH, sR) capS := hrho.Add(gsL).Add(hsR) // Fiat Shamir for y,z (L49, pg19) capV := getcapV(v, gamma, proofGenerators.g, proofGenerators.h) y, z, err := calcyz(capV, capA, capS, transcript, prover.curve) if err != nil { return nil, errors.Wrap(err, "rangeproof prove") } // Calc t_1, t_2 // See the l(X), r(X), t(X) equations on pg 19 // Use l(X)'s and r(X)'s constant and linear terms to derive t_1 and t_2 // (a_l - z*1^n) zonen := multiplyScalarToScalarVector(z, onen) constantTerml, err := subtractPairwiseScalarVectors(aL, zonen) if err != nil { return nil, errors.Wrap(err, "rangeproof prove") } linearTerml := sL // z^2 * 2^N twoN := get2nVector(n, prover.curve) zSquareTwon := multiplyScalarToScalarVector(z.Square(), twoN) // a_r + z*1^n aRPluszonen, err := addPairwiseScalarVectors(aR, zonen) if err != nil { return nil, errors.Wrap(err, "rangeproof prove") } yn := getknVector(y, n, prover.curve) hadamard, err := multiplyPairwiseScalarVectors(yn, aRPluszonen) if err != nil { return nil, errors.Wrap(err, "rangeproof prove") } constantTermr, err := addPairwiseScalarVectors(hadamard, zSquareTwon) if err != nil { return nil, errors.Wrap(err, "rangeproof prove") } linearTermr, err := multiplyPairwiseScalarVectors(yn, sR) if err != nil { return nil, errors.Wrap(err, "rangeproof prove") } // t_1 (as the linear coefficient) is the sum of the dot products of l(X)'s linear term dot r(X)'s constant term // and r(X)'s linear term dot l(X)'s constant term t1FirstTerm, err := innerProduct(linearTerml, constantTermr) if err != nil { return nil, errors.Wrap(err, "rangeproof prove") } t1SecondTerm, err := innerProduct(linearTermr, constantTerml) if err != nil { return nil, errors.Wrap(err, "rangeproof prove") } t1 := t1FirstTerm.Add(t1SecondTerm) // t_2 (as the quadratic coefficient) is the dot product of l(X)'s and r(X)'s linear terms t2, err := innerProduct(linearTerml, linearTermr) if err != nil { return nil, errors.Wrap(err, "rangeproof prove") } // L52, pg20 tau1 := prover.curve.Scalar.Random(crand.Reader) tau2 := prover.curve.Scalar.Random(crand.Reader) // T_1, T_2 (L53, pg20) capT1 := proofGenerators.g.Mul(t1).Add(proofGenerators.h.Mul(tau1)) capT2 := proofGenerators.g.Mul(t2).Add(proofGenerators.h.Mul(tau2)) // Fiat shamir for x (L55, pg20) x, err := calcx(capT1, capT2, transcript, prover.curve) if err != nil { return nil, errors.Wrap(err, "rangeproof prove") } // Calc l (L58, pg20) // Instead of using the expression in the line, evaluate l() at x sLx := multiplyScalarToScalarVector(x, linearTerml) l, err := addPairwiseScalarVectors(constantTerml, sLx) if err != nil { return nil, errors.Wrap(err, "rangeproof prove") } // Calc r (L59, pg20) // Instead of using the expression in the line, evaluate r() at x ynsRx := multiplyScalarToScalarVector(x, linearTermr) r, err := addPairwiseScalarVectors(constantTermr, ynsRx) if err != nil { return nil, errors.Wrap(err, "rangeproof prove") } // Calc t hat (L60, pg20) // For efficiency, instead of calculating the dot product, evaluate t() at x deltayz, err := deltayz(y, z, n, prover.curve) if err != nil { return nil, errors.Wrap(err, "rangeproof prove") } t0 := v.Mul(z.Square()).Add(deltayz) tLinear := t1.Mul(x) tQuadratic := t2.Mul(x.Square()) tHat := t0.Add(tLinear).Add(tQuadratic) // Calc tau_x (L61, pg20) tau2xsquare := tau2.Mul(x.Square()) tau1x := tau1.Mul(x) zsquaregamma := z.Square().Mul(gamma) taux := tau2xsquare.Add(tau1x).Add(zsquaregamma) // Calc mu (L62, pg20) mu := alpha.Add(rho.Mul(x)) // Calc IPP (See section 4.2) hPrime, err := gethPrime(proofH, y, prover.curve) if err != nil { return nil, errors.Wrap(err, "rangeproof prove") } capPhmu, err := getPhmu(proofG, hPrime, proofGenerators.h, capA, capS, x, y, z, mu, n, prover.curve) if err != nil { return nil, errors.Wrap(err, "rangeproof prove") } wBytes := transcript.ExtractBytes([]byte("getw"), 64) w, err := prover.curve.NewScalar().SetBytesWide(wBytes) if err != nil { return nil, errors.Wrap(err, "rangeproof prove") } ipp, err := prover.ippProver.rangeToIPP(proofG, hPrime, l, r, tHat, capPhmu, proofGenerators.u.Mul(w), transcript) if err != nil { return nil, errors.Wrap(err, "rangeproof prove") } out := &RangeProof{ capA: capA, capS: capS, capT1: capT1, capT2: capT2, taux: taux, mu: mu, tHat: tHat, ipp: ipp, curve: &prover.curve, } return out, nil } // MarshalBinary takes a range proof and marshals into bytes. func (proof *RangeProof) MarshalBinary() []byte { var out []byte out = append(out, proof.capA.ToAffineCompressed()...) out = append(out, proof.capS.ToAffineCompressed()...) out = append(out, proof.capT1.ToAffineCompressed()...) out = append(out, proof.capT2.ToAffineCompressed()...) out = append(out, proof.taux.Bytes()...) out = append(out, proof.mu.Bytes()...) out = append(out, proof.tHat.Bytes()...) out = append(out, proof.ipp.MarshalBinary()...) return out } // UnmarshalBinary takes bytes of a marshaled proof and writes them into a range proof // The range proof used should be from the output of NewRangeProof(). func (proof *RangeProof) UnmarshalBinary(data []byte) error { scalarLen := len(proof.curve.NewScalar().Bytes()) pointLen := len(proof.curve.NewGeneratorPoint().ToAffineCompressed()) ptr := 0 // Get points capA, err := proof.curve.Point.FromAffineCompressed(data[ptr : ptr+pointLen]) if err != nil { return errors.New("rangeProof UnmarshalBinary FromAffineCompressed") } proof.capA = capA ptr += pointLen capS, err := proof.curve.Point.FromAffineCompressed(data[ptr : ptr+pointLen]) if err != nil { return errors.New("rangeProof UnmarshalBinary FromAffineCompressed") } proof.capS = capS ptr += pointLen capT1, err := proof.curve.Point.FromAffineCompressed(data[ptr : ptr+pointLen]) if err != nil { return errors.New("rangeProof UnmarshalBinary FromAffineCompressed") } proof.capT1 = capT1 ptr += pointLen capT2, err := proof.curve.Point.FromAffineCompressed(data[ptr : ptr+pointLen]) if err != nil { return errors.New("rangeProof UnmarshalBinary FromAffineCompressed") } proof.capT2 = capT2 ptr += pointLen // Get scalars taux, err := proof.curve.NewScalar().SetBytes(data[ptr : ptr+scalarLen]) if err != nil { return errors.New("rangeProof UnmarshalBinary SetBytes") } proof.taux = taux ptr += scalarLen mu, err := proof.curve.NewScalar().SetBytes(data[ptr : ptr+scalarLen]) if err != nil { return errors.New("rangeProof UnmarshalBinary SetBytes") } proof.mu = mu ptr += scalarLen tHat, err := proof.curve.NewScalar().SetBytes(data[ptr : ptr+scalarLen]) if err != nil { return errors.New("rangeProof UnmarshalBinary SetBytes") } proof.tHat = tHat ptr += scalarLen // Get IPP err = proof.ipp.UnmarshalBinary(data[ptr:]) if err != nil { return errors.New("rangeProof UnmarshalBinary") } return nil } // checkRange validates whether some scalar v is within the range [0, 2^n - 1] // It will return an error if v is less than 0 or greater than 2^n - 1 // Otherwise it will return nil. func checkRange(v curves.Scalar, n int) error { bigOne := big.NewInt(1) if n < 0 { return errors.New("n cannot be less than 0") } var bigTwoToN big.Int bigTwoToN.Lsh(bigOne, uint(n)) if v.BigInt().Cmp(&bigTwoToN) == 1 { return errors.New("v is greater than 2^n") } return nil } // getBlindingVector returns a vector of scalars used as blinding factors for commitments. func getBlindingVector(length int, curve curves.Curve) []curves.Scalar { vec := make([]curves.Scalar, length) for i := 0; i < length; i++ { vec[i] = curve.Scalar.Random(crand.Reader) } return vec } // getcapV returns a commitment to v using blinding factor gamma. func getcapV(v, gamma curves.Scalar, g, h curves.Point) curves.Point { return h.Mul(gamma).Add(g.Mul(v)) } // getaL obtains the bit vector representation of v // See the a_L definition towards the bottom of pg 17 of https://eprint.iacr.org/2017/1066.pdf func getaL(v curves.Scalar, n int, curve curves.Curve) ([]curves.Scalar, error) { var err error vBytes := v.Bytes() zero := curve.Scalar.Zero() one := curve.Scalar.One() aL := make([]curves.Scalar, n) for j := 0; j < len(aL); j++ { aL[j] = zero } for i := 0; i < n; i++ { ithBit := vBytes[i>>3] >> (i & 0x07) & 0x01 aL[i], err = cmoveScalar(zero, one, int(ithBit), curve) if err != nil { return nil, errors.Wrap(err, "getaL") } } return aL, nil } // cmoveScalar provides a constant time operation that returns x if which is 0 and returns y if which is 1. func cmoveScalar(x, y curves.Scalar, which int, curve curves.Curve) (curves.Scalar, error) { if which != 0 && which != 1 { return nil, errors.New("cmoveScalar which must be 0 or 1") } mask := -byte(which) xBytes := x.Bytes() yBytes := y.Bytes() for i, xByte := range xBytes { xBytes[i] ^= (xByte ^ yBytes[i]) & mask } out, err := curve.NewScalar().SetBytes(xBytes) if err != nil { return nil, errors.Wrap(err, "cmoveScalar SetBytes") } return out, nil } // calcyz uses a merlin transcript for Fiat Shamir // It takes the current state of the transcript and appends the newly calculated capA and capS values // Two new scalars are then read from the transcript // See section 4.4 pg22 of https://eprint.iacr.org/2017/1066.pdf func calcyz(capV, capA, capS curves.Point, transcript *merlin.Transcript, curve curves.Curve) (curves.Scalar, curves.Scalar, error) { // Add the A,S values to transcript transcript.AppendMessage([]byte("addV"), capV.ToAffineUncompressed()) transcript.AppendMessage([]byte("addcapA"), capA.ToAffineUncompressed()) transcript.AppendMessage([]byte("addcapS"), capS.ToAffineUncompressed()) // Read 64 bytes twice from, set to scalar for y and z yBytes := transcript.ExtractBytes([]byte("gety"), 64) y, err := curve.NewScalar().SetBytesWide(yBytes) if err != nil { return nil, nil, errors.Wrap(err, "calcyz NewScalar SetBytesWide") } zBytes := transcript.ExtractBytes([]byte("getz"), 64) z, err := curve.NewScalar().SetBytesWide(zBytes) if err != nil { return nil, nil, errors.Wrap(err, "calcyz NewScalar SetBytesWide") } return y, z, nil } // calcx uses a merlin transcript for Fiat Shamir // It takes the current state of the transcript and appends the newly calculated capT1 and capT2 values // A new scalar is then read from the transcript // See section 4.4 pg22 of https://eprint.iacr.org/2017/1066.pdf func calcx(capT1, capT2 curves.Point, transcript *merlin.Transcript, curve curves.Curve) (curves.Scalar, error) { // Add the Tau1,2 values to transcript transcript.AppendMessage([]byte("addcapT1"), capT1.ToAffineUncompressed()) transcript.AppendMessage([]byte("addcapT2"), capT2.ToAffineUncompressed()) // Read 64 bytes from, set to scalar outBytes := transcript.ExtractBytes([]byte("getx"), 64) x, err := curve.NewScalar().SetBytesWide(outBytes) if err != nil { return nil, errors.Wrap(err, "calcx NewScalar SetBytesWide") } return x, nil }