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(¶ms.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 }