github.com/consensys/gnark-crypto@v0.14.0/ecc/bn254/fr/tensor-commitment/commitment.go (about)

     1  // Copyright 2020 ConsenSys Software Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package tensorcommitment
    16  
    17  import (
    18  	"bytes"
    19  	"errors"
    20  	"hash"
    21  	"math/big"
    22  
    23  	"github.com/consensys/gnark-crypto/ecc/bn254/fr"
    24  	"github.com/consensys/gnark-crypto/ecc/bn254/fr/fft"
    25  	"github.com/consensys/gnark-crypto/internal/parallel"
    26  )
    27  
    28  var (
    29  	ErrWrongSize           = errors.New("polynomial is too large")
    30  	ErrNotSquare           = errors.New("the size of the polynomial must be a square")
    31  	ErrProofFailedHash     = errors.New("hash of one of the columns is wrong")
    32  	ErrProofFailedEncoding = errors.New("inconsistency with the code word")
    33  	ErrProofFailedOob      = errors.New("the entry is out of bound")
    34  	ErrMaxNbColumns        = errors.New("the state is full")
    35  	ErrCommitmentNotDone   = errors.New("the proof cannot be built before the computation of the digest")
    36  )
    37  
    38  // commitment (TODO Merkle tree for that...)
    39  // The i-th entry is the hash of the i-th columns of P,
    40  // where P is written as a matrix √(m) x √(m)
    41  // (m = len(P)), and the ij-th entry of M is p[m*j + i].
    42  type Digest [][]byte
    43  
    44  // Proof that a commitment is correct
    45  // cf https://eprint.iacr.org/2021/1043.pdf page 10
    46  type Proof struct {
    47  
    48  	// list of entries of ̂{u} to query (see https://eprint.iacr.org/2021/1043.pdf for notations)
    49  	EntryList []int
    50  
    51  	// columns on against which the linear combination is checked
    52  	// (the i-th entry is the EntryList[i]-th column)
    53  	Columns [][]fr.Element
    54  
    55  	// Linear combination of the rows of the polynomial P written as a square matrix
    56  	LinearCombination []fr.Element
    57  
    58  	// small domain, to retrieve the canonical form of the linear combination
    59  	Domain *fft.Domain
    60  
    61  	// root of unity of the big domain
    62  	Generator fr.Element
    63  }
    64  
    65  // TcParams stores the public parameters of the tensor commitment
    66  type TcParams struct {
    67  	// NbColumns number of columns of the matrix storing the polynomials. The total size of
    68  	// the polynomials which are committed is NbColumns x NbRows.
    69  	// The Number of columns is a power of 2, it corresponds to the original size of the codewords
    70  	// of the Reed Solomon code.
    71  	NbColumns int
    72  
    73  	// NbRows number of rows of the matrix storing the polynomials. If a polynomial p is appended
    74  	// whose size if not 0 mod NbRows, it is padded as p' so that len(p')=0 mod NbRows.
    75  	NbRows int
    76  
    77  	// Domains[1] used for the Reed Solomon encoding
    78  	Domains [2]*fft.Domain
    79  
    80  	// Rho⁻¹, rate of the RS code ( > 1)
    81  	Rho int
    82  
    83  	// Function that returns a fresh hasher. The returned hash function is used for hashing the
    84  	// columns. We use this and not directly a hasher for threadsafety hasher. Indeed, if different
    85  	// thread share the same hasher, they will end up mixing hash inputs that should remain separate.
    86  	MakeHash func() hash.Hash
    87  }
    88  
    89  // TensorCommitment stores the data to use a tensor commitment
    90  type TensorCommitment struct {
    91  	// The public parameters of the tensor commitment
    92  	params *TcParams
    93  
    94  	// State contains the polynomials that have been appended so far.
    95  	// when we append a polynomial p, it is stored in the state like this:
    96  	// state[i][j] = p[j*nbRows + i]:
    97  	// p[0] 		| p[nbRows] 	| p[2*nbRows] 	...
    98  	// p[1] 		| p[nbRows+1]	| p[2*nbRows+1]
    99  	// p[2] 		| p[nbRows+2]	| p[2*nbRows+2]
   100  	// ..
   101  	// p[nbRows-1] 	| p[2*nbRows-1]	| p[3*nbRows-1] ..
   102  	State [][]fr.Element
   103  
   104  	// same content as state, but the polynomials are displayed as a matrix
   105  	// and the rows are encoded.
   106  	// encodedState = encodeRows(M_0 || .. || M_n)
   107  	// where M_i is the i-th polynomial laid out as a matrix, that is
   108  	// M_i_jk = p_i[i*m+j] where m = \sqrt(len(p)).
   109  	EncodedState [][]fr.Element
   110  
   111  	// boolean telling if the commitment has already been done.
   112  	// The method BuildProof cannot be called before Commit(),
   113  	// because it would allow to build a proof before giving the commitment
   114  	// to a verifier, making the workflow not secure.
   115  	isCommitted bool
   116  
   117  	// number of columns which have already been hashed (atomic)
   118  	NbColumnsHashed int
   119  
   120  	// counts the number of time `Append` was called (atomic).
   121  	NbAppendsSoFar int
   122  }
   123  
   124  // NewTensorCommitment returns a new TensorCommitment
   125  // * ρ rate of the code ( > 1)
   126  // * size size of the polynomial to be committed. The size of the commitment is
   127  // then ρ * √(m) where m² = size
   128  func NewTCParams(codeRate, NbColumns, NbRows int, makeHash func() hash.Hash) (*TcParams, error) {
   129  	var res TcParams
   130  
   131  	// domain[0]: domain to perform the FFT^-1, of size capacity * sqrt
   132  	// domain[1]: domain to perform FFT, of size rho * capacity * sqrt
   133  	res.Domains[0] = fft.NewDomain(uint64(NbColumns))
   134  	res.Domains[1] = fft.NewDomain(uint64(codeRate * NbColumns))
   135  
   136  	// size of the matrix
   137  	res.NbColumns = int(res.Domains[0].Cardinality)
   138  	res.NbRows = NbRows
   139  
   140  	// rate
   141  	res.Rho = codeRate
   142  
   143  	// Hash function
   144  	res.MakeHash = makeHash
   145  
   146  	return &res, nil
   147  }
   148  
   149  // Initializes an instance of tensor commitment that we can use start
   150  // appending value into it
   151  func NewTensorCommitment(params *TcParams) *TensorCommitment {
   152  	var res TensorCommitment
   153  
   154  	// create the state. It's the matrix containing the polynomials, the ij-th
   155  	// entry of the matrix is state[i][j]. The polynomials are split and stacked
   156  	// columns per column.
   157  	res.State = make([][]fr.Element, params.NbRows)
   158  	for i := 0; i < params.NbRows; i++ {
   159  		res.State[i] = make([]fr.Element, params.NbColumns)
   160  	}
   161  
   162  	// nothing has been committed...
   163  	res.isCommitted = false
   164  	res.params = params
   165  	return &res
   166  }
   167  
   168  // Append appends p to the state.
   169  // when we append a polynomial p, it is stored in the state like this:
   170  // state[i][j] = p[j*nbRows + i]:
   171  // p[0] 		| p[nbRows] 	| p[2*nbRows] 	...
   172  // p[1] 		| p[nbRows+1]	| p[2*nbRows+1]
   173  // p[2] 		| p[nbRows+2]	| p[2*nbRows+2]
   174  // ..
   175  // p[nbRows-1] 	| p[2*nbRows-1]	| p[3*nbRows-1] ..
   176  // If p doesn't fill a full submatrix it is padded with zeroes.
   177  func (tc *TensorCommitment) Append(ps ...[]fr.Element) ([][]byte, error) {
   178  
   179  	nbColumnsTakenByPs := make([]int, len(ps))
   180  	totalNumberOfColumnsTakenByPs := 0
   181  	// Short-hand to avoid writing `tc.params.NbRows` all over the places
   182  	numRows := tc.params.NbRows
   183  
   184  	/*
   185  		Precomputes the number of columns that will be taken by each colums
   186  	*/
   187  	for iPol, p := range ps {
   188  		// check if there is some room for p
   189  		nbColumnsTakenByP := len(p) / numRows
   190  		// Note, Alex. Really, if you want to not handle the padding and just
   191  		// panic whenever you receive "incomplete" columns this would be fine.
   192  		if len(p)%numRows != 0 {
   193  			// If the division has a remainder. Add an extra column
   194  			// Implicitly, it will be padded
   195  			nbColumnsTakenByP += 1
   196  		}
   197  
   198  		nbColumnsTakenByPs[iPol] = nbColumnsTakenByP
   199  		totalNumberOfColumnsTakenByPs += nbColumnsTakenByP
   200  	}
   201  
   202  	// Position at which we need to start inserting columns in the state
   203  	currentColumnToFill := int(tc.NbColumnsHashed)
   204  
   205  	// Check that we are not inserting more columns that we can handle
   206  	if currentColumnToFill+totalNumberOfColumnsTakenByPs > tc.params.NbColumns {
   207  		return nil, ErrMaxNbColumns
   208  	}
   209  
   210  	// Update the internal state variables to keep track of how many poly
   211  	// have been appended so far and how many columns.
   212  	tc.NbAppendsSoFar += len(ps)
   213  	tc.NbColumnsHashed += totalNumberOfColumnsTakenByPs
   214  
   215  	backupCurrentColumnToFill := currentColumnToFill
   216  
   217  	// put p in the state
   218  	for iPol, p := range ps {
   219  
   220  		pIsPadded := false
   221  		if len(p)%numRows != 0 {
   222  			pIsPadded = true
   223  		}
   224  
   225  		// Number of column taken by P, ignoring the last one if it is padded
   226  		nbFullColumnsTakenByP := nbColumnsTakenByPs[iPol]
   227  		if pIsPadded {
   228  			nbFullColumnsTakenByP--
   229  		}
   230  
   231  		// Insert the "full columns" in the state
   232  		for i := 0; i < nbFullColumnsTakenByP; i++ {
   233  			for j := 0; j < numRows; j++ {
   234  				tc.State[j][currentColumnToFill+i] = p[i*numRows+j]
   235  			}
   236  		}
   237  
   238  		// Insert the padded column in the state if any
   239  		currentColumnToFill += nbFullColumnsTakenByP
   240  		if pIsPadded {
   241  			offsetP := len(p) - len(p)%numRows
   242  			for j := offsetP; j < len(p); j++ {
   243  				tc.State[j-offsetP][currentColumnToFill] = p[j]
   244  			}
   245  			currentColumnToFill += 1
   246  		}
   247  	}
   248  
   249  	// Preallocate the result, and as well a buffer for the columns to hash
   250  	res := make([][]byte, totalNumberOfColumnsTakenByPs)
   251  
   252  	parallel.Execute(totalNumberOfColumnsTakenByPs, func(start, stop int) {
   253  		hasher := tc.params.MakeHash()
   254  		for i := start; i < stop; i++ {
   255  			hasher.Reset()
   256  			for j := 0; j < tc.params.NbRows; j++ {
   257  				hasher.Write(tc.State[j][i+backupCurrentColumnToFill].Marshal())
   258  			}
   259  			res[i] = hasher.Sum(nil)
   260  		}
   261  	})
   262  
   263  	return res, nil
   264  }
   265  
   266  // Commit to p. The commitment procedure is the following:
   267  // * Encode the rows of the state to get M'
   268  // * Hash the columns of M'
   269  func (tc *TensorCommitment) Commit() (Digest, error) {
   270  
   271  	// we encode the rows of p using Reed Solomon
   272  	// encodedState[i][:] = i-th line of M. It is of size domain[1].Cardinality
   273  	tc.EncodedState = make([][]fr.Element, tc.params.NbRows)
   274  	for i := 0; i < tc.params.NbRows; i++ { // we fill encodedState line by line
   275  		tc.EncodedState[i] = make([]fr.Element, tc.params.Domains[1].Cardinality) // size = NbRows*rho*capacity
   276  		for j := 0; j < tc.params.NbColumns; j++ {                                // for each polynomial
   277  			tc.EncodedState[i][j].Set(&tc.State[i][j])
   278  		}
   279  		tc.params.Domains[0].FFTInverse(tc.EncodedState[i][:tc.params.Domains[0].Cardinality], fft.DIF)
   280  		fft.BitReverse(tc.EncodedState[i][:tc.params.Domains[0].Cardinality])
   281  		tc.params.Domains[1].FFT(tc.EncodedState[i], fft.DIF)
   282  		fft.BitReverse(tc.EncodedState[i])
   283  	}
   284  
   285  	// now we hash each columns of _p
   286  	res := make([][]byte, tc.params.Domains[1].Cardinality)
   287  
   288  	parallel.Execute(int(tc.params.Domains[1].Cardinality), func(start, stop int) {
   289  		hasher := tc.params.MakeHash()
   290  		for i := start; i < stop; i++ {
   291  			hasher.Reset()
   292  			for j := 0; j < tc.params.NbRows; j++ {
   293  				hasher.Write(tc.EncodedState[j][i].Marshal())
   294  			}
   295  			res[i] = hasher.Sum(nil)
   296  		}
   297  	})
   298  
   299  	// records that the commitment has been built
   300  	tc.isCommitted = true
   301  
   302  	return res, nil
   303  
   304  }
   305  
   306  // BuildProofAtOnceForTest builds a proof to be tested against a previous commitment of a list of
   307  // polynomials.
   308  // * l the random linear coefficients used for the linear combination of size NbRows
   309  // * entryList list of columns to hash
   310  // l and entryList are supposed to be precomputed using Fiat Shamir
   311  //
   312  // The proof is the linear combination (using l) of the encoded rows of p written
   313  // as a matrix. Only the entries contained in entryList are kept.
   314  func (tc *TensorCommitment) BuildProofAtOnceForTest(l []fr.Element, entryList []int) (Proof, error) {
   315  	linComb, err := tc.ProverComputeLinComb(l)
   316  	if err != nil {
   317  		return Proof{}, err
   318  	}
   319  
   320  	openedColumns, err := tc.ProverOpenColumns(entryList)
   321  	if err != nil {
   322  		return Proof{}, err
   323  	}
   324  
   325  	return BuildProof(tc.params, linComb, entryList, openedColumns), nil
   326  }
   327  
   328  // func printVector(v []fr.Element) {
   329  // 	fmt.Printf("[")
   330  // 	for i := 0; i < len(v); i++ {
   331  // 		fmt.Printf("%s,", v[i].String())
   332  // 	}
   333  // 	fmt.Printf("]\n")
   334  // }
   335  
   336  // BuildProof builds a proof to be tested against a previous commitment of a list of
   337  // polynomials.
   338  // * l the random linear coefficients used for the linear combination of size NbRows
   339  // * entryList list of columns to hash
   340  // l and entryList are supposed to be precomputed using Fiat Shamir
   341  //
   342  // The proof is the linear combination (using l) of the encoded rows of p written
   343  // as a matrix. Only the entries contained in entryList are kept.
   344  func (tc *TensorCommitment) ProverComputeLinComb(l []fr.Element) ([]fr.Element, error) {
   345  
   346  	// check that the digest has been computed
   347  	if !tc.isCommitted {
   348  		return []fr.Element{}, ErrCommitmentNotDone
   349  	}
   350  
   351  	// since the digest has been computed, the encodedState is already stored.
   352  	// We use it to build the proof, without recomputing the ffts.
   353  
   354  	// linear combination of the rows of the state
   355  	linComb := make([]fr.Element, tc.params.NbColumns)
   356  	for i := 0; i < tc.params.NbColumns; i++ {
   357  		var tmp fr.Element
   358  		for j := 0; j < tc.params.NbRows; j++ {
   359  			tmp.Mul(&tc.State[j][i], &l[j])
   360  			linComb[i].Add(&linComb[i], &tmp)
   361  		}
   362  	}
   363  
   364  	return linComb, nil
   365  }
   366  
   367  func (tc *TensorCommitment) ProverOpenColumns(entryList []int) ([][]fr.Element, error) {
   368  
   369  	// check that the digest has been computed
   370  	if !tc.isCommitted {
   371  		return [][]fr.Element{}, ErrCommitmentNotDone
   372  	}
   373  
   374  	// columns of the state whose rows have been encoded, written as a matrix,
   375  	// corresponding to the indices in entryList (we will select the columns
   376  	// entryList[0], entryList[1], etc.
   377  	openedColumns := make([][]fr.Element, len(entryList))
   378  	for i := 0; i < len(entryList); i++ { // for each column (corresponding to an elmt in entryList)
   379  		openedColumns[i] = make([]fr.Element, tc.params.NbRows)
   380  		for j := 0; j < tc.params.NbRows; j++ {
   381  			openedColumns[i][j] = tc.EncodedState[j][entryList[i]]
   382  		}
   383  	}
   384  
   385  	return openedColumns, nil
   386  }
   387  
   388  /*
   389  Reconstruct the proof from the prover's outputs
   390  */
   391  func BuildProof(params *TcParams, linComb []fr.Element, entryList []int, openedCols [][]fr.Element) Proof {
   392  
   393  	var res Proof
   394  
   395  	// small domain to express the linear combination in canonical form
   396  	res.Domain = params.Domains[0]
   397  
   398  	// generator g of the biggest domain, used to evaluate the canonical form of
   399  	// the linear combination at some powers of g.
   400  	res.Generator.Set(&params.Domains[1].Generator)
   401  
   402  	res.Columns = openedCols
   403  	res.EntryList = entryList
   404  	res.LinearCombination = linComb
   405  
   406  	return res
   407  }
   408  
   409  // evalAtPower returns p(x**n) where p is interpreted as a polynomial
   410  // p[0] + p[1]X + .. p[len(p)-1]xˡᵉⁿ⁽ᵖ⁾⁻¹
   411  func evalAtPower(p []fr.Element, x fr.Element, n int) fr.Element {
   412  
   413  	var xexp fr.Element
   414  	xexp.Exp(x, big.NewInt(int64(n)))
   415  
   416  	var res fr.Element
   417  	for i := 0; i < len(p); i++ {
   418  		res.Mul(&res, &xexp)
   419  		res.Add(&p[len(p)-1-i], &res)
   420  	}
   421  
   422  	return res
   423  
   424  }
   425  
   426  // Verify a proof that digest is the hash of a  polynomial given a proof
   427  // proof: contains the linear combination of the non-encoded rows + the
   428  // digest: hash of the polynomial
   429  // l: random coefficients for the linear combination, chosen by the verifier
   430  // h: hash function that is used for hashing the columns of the polynomial
   431  // TODO make this function private and add a Verify function that derives
   432  // the randomness using Fiat Shamir
   433  //
   434  // Note (alex), A more convenient API would be to expose two functions,
   435  // one that does FS for you and what that let you do it for yourself. And likewise
   436  // for the prover.
   437  func Verify(proof Proof, digest Digest, l []fr.Element, h hash.Hash) error {
   438  
   439  	// for each entry in the list -> it corresponds to the sampling
   440  	// set on which we probabilistically check that
   441  	// Encoded(linear_combination) = linear_combination(encoded)
   442  	for i := 0; i < len(proof.EntryList); i++ {
   443  
   444  		// check that the hash of the columns correspond to what's in the digest
   445  		h.Reset()
   446  		for j := 0; j < len(proof.Columns[i]); j++ {
   447  			h.Write(proof.Columns[i][j].Marshal())
   448  		}
   449  		s := h.Sum(nil)
   450  		if !bytes.Equal(s, digest[proof.EntryList[i]]) {
   451  			return ErrProofFailedHash
   452  		}
   453  
   454  		if proof.EntryList[i] >= len(digest) {
   455  			return ErrProofFailedOob
   456  		}
   457  
   458  		// linear combination of the i-th column, whose entries
   459  		// are the entryList[i]-th entries of the encoded lines
   460  		// of p
   461  		var linCombEncoded, tmp fr.Element
   462  		for j := 0; j < len(proof.Columns[i]); j++ {
   463  
   464  			// linear combination of the encoded rows at column i
   465  			tmp.Mul(&proof.Columns[i][j], &l[j])
   466  			linCombEncoded.Add(&linCombEncoded, &tmp)
   467  		}
   468  
   469  		// entry i of the encoded linear combination
   470  		var encodedLinComb fr.Element
   471  		linCombCanonical := make([]fr.Element, proof.Domain.Cardinality)
   472  		copy(linCombCanonical, proof.LinearCombination)
   473  		proof.Domain.FFTInverse(linCombCanonical, fft.DIF)
   474  		fft.BitReverse(linCombCanonical)
   475  		encodedLinComb = evalAtPower(linCombCanonical, proof.Generator, proof.EntryList[i])
   476  
   477  		// compare both values
   478  		if !encodedLinComb.Equal(&linCombEncoded) {
   479  			return ErrProofFailedEncoding
   480  
   481  		}
   482  	}
   483  
   484  	return nil
   485  
   486  }