package multiproof import ( "encoding/binary" "errors" "fmt" "io" "runtime" "github.com/crate-crypto/go-ipa/bandersnatch/fr" "github.com/crate-crypto/go-ipa/banderwagon" "github.com/crate-crypto/go-ipa/common" "github.com/crate-crypto/go-ipa/ipa" ) // The following are unexported labels to be used in Fiat-Shamir during the // multiproof protocol. // // The following is a short description on how they're used in the protocol: // 1. Append the domain separator. (labelDomainSep) // 2. For each opening, we append to the transcript: // a. The polynomial commitment (labelC). // b. The evaluation point (labelZ). // c. The evaluation result (labelY). // 3. Pull a scalar-field element from the transcript to be used for // the random linear combination of openings. (labelR) // 4. Append point D which is sum(r^i * (f_i(x)-y_i)/(x-z_i)). (labelD) // 5. Pull a random scalar-field to be used as a random evaluation point. (labelT) // 5. Append point E which is sum(r^i * f_i(x)/(t-z_i)). (labelE) // 7. Create the IPA proof for (E-D) at point `t`. See the `ipa` package for the FS description. // // Note: this package must not mutate these label values, nor pass them to // parts of the code that would mutate them. var ( labelC = []byte("C") labelZ = []byte("z") labelY = []byte("y") labelD = []byte("D") labelE = []byte("E") labelT = []byte("t") labelR = []byte("r") labelDomainSep = []byte("multiproof") ) // MultiProof is a multi-proof for several polynomials in evaluation form. type MultiProof struct { IPA ipa.IPAProof D banderwagon.Element } // CreateMultiProof creates a multi-proof for several polynomials in evaluation form. // The list of triplets (C, Fs, Z) represents each polynomial commitment, evaluations in the domain, and evaluation // point respectively. func CreateMultiProof(transcript *common.Transcript, ipaConf *ipa.IPAConfig, Cs []*banderwagon.Element, fs [][]fr.Element, zs []uint8) (*MultiProof, error) { transcript.DomainSep(labelDomainSep) for _, f := range fs { if len(f) != common.VectorLength { return nil, fmt.Errorf("polynomial length = %d, while expected length = %d", len(f), common.VectorLength) } } if len(Cs) != len(fs) { return nil, fmt.Errorf("number of commitments = %d, while number of functions = %d", len(Cs), len(fs)) } if len(Cs) != len(zs) { return nil, fmt.Errorf("number of commitments = %d, while number of points = %d", len(Cs), len(zs)) } num_queries := len(Cs) if num_queries == 0 { return nil, errors.New("cannot create a multiproof with 0 queries") } if err := banderwagon.BatchNormalize(Cs); err != nil { return nil, fmt.Errorf("could not batch normalize commitments: %w", err) } for i := 0; i < num_queries; i++ { transcript.AppendPoint(Cs[i], labelC) var z = domainToFr(zs[i]) transcript.AppendScalar(&z, labelZ) // get the `y` value f := fs[i] y := f[zs[i]] transcript.AppendScalar(&y, labelY) } r := transcript.ChallengeScalar(labelR) powersOfR := common.PowersOf(r, num_queries) // Compute g(x) // We first compute the polynomials in lagrange form grouped by evaluation point, and // then we compute g(X). This limit the numbers of DivideOnDomain() calls up to // the domain size. groupedFs := groupPolynomialsByEvaluationPoint(fs, powersOfR, zs) g_x := make([]fr.Element, common.VectorLength) for index, f := range groupedFs { // If there is no polynomial for this evaluation point, we skip it. if len(f) == 0 { continue } quotient := ipaConf.PrecomputedWeights.DivideOnDomain(uint8(index), f) for j := 0; j < common.VectorLength; j++ { g_x[j].Add(&g_x[j], "ient[j]) } } D := ipaConf.Commit(g_x) transcript.AppendPoint(&D, labelD) t := transcript.ChallengeScalar(labelT) // Calculate the denominator inverses only for referenced evaluation points. den_inv := make([]fr.Element, 0, common.VectorLength) for z, f := range groupedFs { if len(f) == 0 { continue } var z = domainToFr(uint8(z)) var den fr.Element den.Sub(&t, &z) den_inv = append(den_inv, den) } den_inv = fr.BatchInvert(den_inv) // Compute h(X) = g_1(X) h_x := make([]fr.Element, common.VectorLength) denInvIdx := 0 for _, f := range groupedFs { if len(f) == 0 { continue } for k := 0; k < common.VectorLength; k++ { var tmp fr.Element tmp.Mul(&f[k], &den_inv[denInvIdx]) h_x[k].Add(&h_x[k], &tmp) } denInvIdx++ } h_minus_g := make([]fr.Element, common.VectorLength) for i := 0; i < common.VectorLength; i++ { h_minus_g[i].Sub(&h_x[i], &g_x[i]) } E := ipaConf.Commit(h_x) transcript.AppendPoint(&E, labelE) var EminusD banderwagon.Element EminusD.Sub(&E, &D) ipaProof, err := ipa.CreateIPAProof(transcript, ipaConf, EminusD, h_minus_g, t) if err != nil { return nil, fmt.Errorf("could not create IPA proof: %w", err) } return &MultiProof{ IPA: ipaProof, D: D, }, nil } // CheckMultiProof verifies a multi-proof for several polynomials in evaluation form. // The list of triplets (C, Y, Z) represents each polynomial commitment, evaluation // result, and evaluation point in the domain. func CheckMultiProof(transcript *common.Transcript, ipaConf *ipa.IPAConfig, proof *MultiProof, Cs []*banderwagon.Element, ys []*fr.Element, zs []uint8) (bool, error) { transcript.DomainSep(labelDomainSep) if len(Cs) != len(ys) { return false, fmt.Errorf("number of commitments = %d, while number of output points = %d", len(Cs), len(ys)) } if len(Cs) != len(zs) { return false, fmt.Errorf("number of commitments = %d, while number of input points = %d", len(Cs), len(zs)) } num_queries := len(Cs) if num_queries == 0 { return false, errors.New("number of queries is zero") } for i := 0; i < num_queries; i++ { transcript.AppendPoint(Cs[i], labelC) var z = domainToFr(zs[i]) transcript.AppendScalar(&z, labelZ) transcript.AppendScalar(ys[i], labelY) } r := transcript.ChallengeScalar(labelR) powers_of_r := common.PowersOf(r, num_queries) transcript.AppendPoint(&proof.D, labelD) t := transcript.ChallengeScalar(labelT) // Compute the polynomials in lagrange form grouped by evaluation point, and // the needed helper scalars. groupedEvals := make([]fr.Element, common.VectorLength) for i := 0; i < num_queries; i++ { z := zs[i] // r * y_i r := powers_of_r[i] var scaledEvaluation fr.Element scaledEvaluation.Mul(&r, ys[i]) groupedEvals[z].Add(&groupedEvals[z], &scaledEvaluation) } // Compute helper_scalar_den. This is 1 / t - z_i helper_scalar_den := make([]fr.Element, common.VectorLength) for i := 0; i < common.VectorLength; i++ { // (t - z_i) var z = domainToFr(uint8(i)) helper_scalar_den[i].Sub(&t, &z) } helper_scalar_den = fr.BatchInvert(helper_scalar_den) // Compute g_2(t) = SUM (y_i * r^i) / (t - z_i) = SUM (y_i * r) * helper_scalars_den g_2_t := fr.Zero() for i := 0; i < common.VectorLength; i++ { if groupedEvals[i].IsZero() { continue } var tmp fr.Element tmp.Mul(&groupedEvals[i], &helper_scalar_den[i]) g_2_t.Add(&g_2_t, &tmp) } // Compute E = SUM C_i * (r^i / t - z_i) = SUM C_i * msm_scalars msm_scalars := make([]fr.Element, len(Cs)) Csnp := make([]banderwagon.Element, len(Cs)) for i := 0; i < len(Cs); i++ { Csnp[i] = *Cs[i] msm_scalars[i].Mul(&powers_of_r[i], &helper_scalar_den[zs[i]]) } E, err := ipa.MultiScalar(Csnp, msm_scalars) if err != nil { return false, fmt.Errorf("could not compute E: %w", err) } transcript.AppendPoint(&E, labelE) var E_minus_D banderwagon.Element E_minus_D.Sub(&E, &proof.D) ok, err := ipa.CheckIPAProof(transcript, ipaConf, E_minus_D, proof.IPA, t, g_2_t) if err != nil { return false, fmt.Errorf("could not check IPA proof: %w", err) } return ok, nil } func domainToFr(in uint8) fr.Element { var x fr.Element x.SetUint64(uint64(in)) return x } // Write serializes a multi-proof to a writer. func (mp *MultiProof) Write(w io.Writer) error { if err := binary.Write(w, binary.BigEndian, mp.D.Bytes()); err != nil { return fmt.Errorf("failed to write D: %w", err) } if err := mp.IPA.Write(w); err != nil { return fmt.Errorf("failed to write IPA proof: %w", err) } return nil } // Read deserializes a multi-proof from a reader. func (mp *MultiProof) Read(r io.Reader) error { D, err := common.ReadPoint(r) if err != nil { return fmt.Errorf("failed to read D: %w", err) } mp.D = *D if err := mp.IPA.Read(r); err != nil { return fmt.Errorf("failed to read IPA proof: %w", err) } // Check that the next read is EOF. var buf [1]byte if _, err := r.Read(buf[:]); err != io.EOF { return errors.New("expected EOF") } return nil } // Equal checks if two multi-proofs are equal. func (mp MultiProof) Equal(other MultiProof) bool { if !mp.IPA.Equal(other.IPA) { return false } return mp.D.Equal(&other.D) } func groupPolynomialsByEvaluationPoint(fs [][]fr.Element, powersOfR []fr.Element, zs []uint8) [common.VectorLength][]fr.Element { workersAggregations := make(chan [common.VectorLength][]fr.Element) numWorkers := runtime.NumCPU() batchSize := (len(fs) + numWorkers - 1) / numWorkers for i := 0; i < numWorkers; i++ { go func(start, end int) { if end > len(fs) { end = len(fs) } var groupedFs [common.VectorLength][]fr.Element for i := start; i < end; i++ { z := zs[i] if len(groupedFs[z]) == 0 { groupedFs[z] = make([]fr.Element, common.VectorLength) } for j := 0; j < common.VectorLength; j++ { var scaledEvaluation fr.Element scaledEvaluation.Mul(&powersOfR[i], &fs[i][j]) groupedFs[z][j].Add(&groupedFs[z][j], &scaledEvaluation) } } workersAggregations <- groupedFs }(i*batchSize, (i+1)*batchSize) } // Each worker has computed its own aggregation. Now we aggregate the results. // This is bounded to reducing a `numWorkers` sized array of `common.VectorLength` sized arrays. var groupedFs [common.VectorLength][]fr.Element for i := 0; i < numWorkers; i++ { workerAggregation := <-workersAggregations for z := range workerAggregation { if len(workerAggregation[z]) == 0 { continue } // If this is the first time we see this evaluation point, we initialize it // reusing the worker result. if groupedFs[z] == nil { groupedFs[z] = workerAggregation[z] continue } // If not, we aggregate the worker result with the previous result for this evaluation. for j := 0; j < common.VectorLength; j++ { groupedFs[z][j].Add(&groupedFs[z][j], &workerAggregation[z][j]) } } } return groupedFs }