diff --git a/field/field.go b/field/field.go index 4116696..799ee51 100644 --- a/field/field.go +++ b/field/field.go @@ -1,6 +1,8 @@ package field import ( + "math/big" + "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/std/math/emulated" @@ -28,3 +30,9 @@ func NewFieldAPI(api frontend.API) frontend.API { } return field } + +var r EmulatedField + +func EmulatedFieldModulus() *big.Int { + return r.Modulus() +} diff --git a/plonky2_verifier/fri.go b/plonky2_verifier/fri.go index bdc57bd..cb673fe 100644 --- a/plonky2_verifier/fri.go +++ b/plonky2_verifier/fri.go @@ -1,9 +1,9 @@ package plonky2_verifier import ( - "fmt" "gnark-ed25519/field" . "gnark-ed25519/field" + "gnark-ed25519/poseidon" "math" "github.com/consensys/gnark/frontend" @@ -34,16 +34,19 @@ type FriChip struct { field frontend.API qe *QuadraticExtensionAPI + poseidonChip *poseidon.PoseidonChip + friParams *FriParams verifierOnlyCircuitData *VerifierOnlyCircuitData } -func NewFriChip(api frontend.API, field frontend.API, qe *QuadraticExtensionAPI, friParams *FriParams) *FriChip { +func NewFriChip(api frontend.API, field frontend.API, qe *QuadraticExtensionAPI, poseidonChip *poseidon.PoseidonChip, friParams *FriParams) *FriChip { return &FriChip{ - api: api, - field: field, - qe: qe, - friParams: friParams, + api: api, + field: field, + qe: qe, + poseidonChip: poseidonChip, + friParams: friParams, } } @@ -52,9 +55,6 @@ func (f *FriChip) assertLeadingZeros(powWitness F, friConfig FriConfig) { // Note that this is assuming that the Goldilocks field is being used. Specfically that the // field is 64 bits long maxPowWitness := uint64(math.Pow(2, float64(64-friConfig.ProofOfWorkBits))) - 1 - f.field.Println(powWitness) - fmt.Println(maxPowWitness) - fmt.Println(friConfig.ProofOfWorkBits) f.field.AssertIsLessOrEqual(powWitness, field.NewFieldElement(maxPowWitness)) } @@ -70,10 +70,63 @@ func (f *FriChip) fromOpeningsAndAlpha(openings *FriOpenings, alpha QuadraticExt return reducedOpenings } -func (f *FriChip) verifyMerkleProofToCap(leafData []F, leafIndex F, merkleCap MerkleCap, proof *MerkleProof) { +func (f *FriChip) hashOrNoop(data []F) Hash { + var elements Hash + if len(data) <= 4 { + // Pad the data to have a size of 4 + for i, inputElement := range data { + elements[i] = inputElement + } + for i := len(data); i < 4; i++ { + elements[i] = f.qe.ZERO_F + } + + return elements + } else { + hashOutput := f.poseidonChip.HashNToMNoPad(data, 4) + + if len(hashOutput) != len(elements) { + panic("The length of hashOutput and elements is different") + } + + for i, hashField := range hashOutput { + elements[i] = hashField + } + + return elements + } } -func (f *FriChip) verifyInitialProof(xIndex F, proof *FriInitialTreeProof, initialMerkleCaps []MerkleCap) { +func (f *FriChip) verifyMerkleProofToCapWithCapIndex(leafData []F, leafIndexBits []frontend.Variable, capIndex F, merkleCap MerkleCap, proof *MerkleProof) { + currentDigest := f.hashOrNoop(leafData) + + if len(leafIndexBits) != len(proof.Siblings) { + panic("len(leafIndexBits) != len(proof.Siblings)") + } + + fourZeros := [4]F{f.qe.ZERO_F, f.qe.ZERO_F, f.qe.ZERO_F, f.qe.ZERO_F} + for i, bit := range leafIndexBits { + sibling := proof.Siblings[i] + + var leftSiblingState poseidon.PoseidonState + copy(leftSiblingState[0:4], sibling[0:4]) + copy(leftSiblingState[4:8], currentDigest[0:4]) + copy(leftSiblingState[8:12], fourZeros[0:4]) + leftHash := f.poseidonChip.Poseidon(leftSiblingState) + leftHashCompress := leftHash[0:4] + + var rightSiblingState poseidon.PoseidonState + copy(rightSiblingState[0:4], currentDigest[0:4]) + copy(rightSiblingState[4:8], sibling[0:4]) + copy(rightSiblingState[8:12], fourZeros[0:4]) + rightHash := f.poseidonChip.Poseidon(rightSiblingState) + rightHashCompress := rightHash[0:4] + + currentDigest = f.api.Select(bit, leftHashCompress, rightHashCompress).(Hash) + } +} + +func (f *FriChip) verifyInitialProof(xIndexBits []frontend.Variable, proof *FriInitialTreeProof, initialMerkleCaps []MerkleCap, capIndex F) { if len(proof.EvalsProofs) != len(initialMerkleCaps) { panic("length of eval proofs in fri proof should equal length of initial merkle caps") } @@ -82,7 +135,28 @@ func (f *FriChip) verifyInitialProof(xIndex F, proof *FriInitialTreeProof, initi evals := proof.EvalsProofs[i].Elements merkleProof := proof.EvalsProofs[i].MerkleProof cap := initialMerkleCaps[i] - f.verifyMerkleProofToCap(evals, xIndex, cap, &merkleProof) + f.verifyMerkleProofToCapWithCapIndex(evals, xIndexBits, capIndex, cap, &merkleProof) + } +} + +// / We decompose FRI query indices into bits without verifying that the decomposition given by +// / the prover is the canonical one. In particular, if `x_index < 2^field_bits - p`, then the +// / prover could supply the binary encoding of either `x_index` or `x_index + p`, since they are +// / congruent mod `p`. However, this only occurs with probability +// / p_ambiguous = (2^field_bits - p) / p +// / which is small for the field that we use in practice. +// / +// / In particular, the soundness error of one FRI query is roughly the codeword rate, which +// / is much larger than this ambiguous-element probability given any reasonable parameters. +// / Thus ambiguous elements contribute a negligible amount to soundness error. +// / +// / Here we compare the probabilities as a sanity check, to verify the claim above. +func (f *FriChip) assertNoncanonicalIndicesOK() { + numAmbiguousElems := uint64(math.MaxUint64) - EmulatedFieldModulus().Uint64() + 1 + queryError := f.friParams.Config.rate() + pAmbiguous := float64(numAmbiguousElems) / float64(EmulatedFieldModulus().Uint64()) + if pAmbiguous < queryError*1e-5 { + panic("A non-negligible portion of field elements are in the range that permits non-canonical encodings. Need to do more analysis or enforce canonical encodings.") } } @@ -95,11 +169,17 @@ func (f *FriChip) verifyQueryRound( n uint64, roundProof *FriQueryRound, ) { - f.verifyInitialProof(xIndex, &roundProof.InitialTreesProof, initialMerkleCaps) + nLog := log2Strict(uint(n)) + + f.assertNoncanonicalIndicesOK() + xIndexBits := f.qe.field.ToBinary(xIndex, nLog) + capIndex := f.qe.field.FromBinary(xIndexBits[len(xIndexBits)-int(f.friParams.Config.CapHeight):]...).(F) + + f.verifyInitialProof(xIndexBits, &roundProof.InitialTreesProof, initialMerkleCaps, capIndex) } func (f *FriChip) VerifyFriProof( - openings *FriOpenings, + openings FriOpenings, friChallenges *FriChallenges, initialMerkleCaps []MerkleCap, friProof *FriProof, @@ -116,9 +196,9 @@ func (f *FriChip) VerifyFriProof( ); */ // Check POW - f.assertLeadingZeros(friProof.PowWitness, f.friParams.Config) + f.assertLeadingZeros(friChallenges.FriPowResponse, f.friParams.Config) - precomputedReducedEvals := f.fromOpeningsAndAlpha(openings, friChallenges.FriAlpha) + precomputedReducedEvals := f.fromOpeningsAndAlpha(&openings, friChallenges.FriAlpha) // Size of the LDE domain. n := uint64(math.Pow(2, float64(f.friParams.DegreeBits+f.friParams.Config.RateBits))) diff --git a/plonky2_verifier/fri_test.go b/plonky2_verifier/fri_test.go index e7b1a9a..cc3bcf3 100644 --- a/plonky2_verifier/fri_test.go +++ b/plonky2_verifier/fri_test.go @@ -2,6 +2,7 @@ package plonky2_verifier import ( . "gnark-ed25519/field" + "gnark-ed25519/poseidon" "testing" "github.com/consensys/gnark/frontend" @@ -13,11 +14,66 @@ type TestFriCircuit struct{} func (circuit *TestFriCircuit) Define(api frontend.API) error { proofWithPis := DeserializeProofWithPublicInputs("./data/fibonacci/proof_with_public_inputs.json") commonCircuitData := DeserializeCommonCircuitData("./data/fibonacci/common_circuit_data.json") + verifierOnlyCircuitData := DeserializeVerifierOnlyCircuitData("./data/fibonacci/verifier_only_circuit_data.json") field := NewFieldAPI(api) + qe := NewQuadraticExtensionAPI(field, commonCircuitData.DegreeBits) + poseidonChip := poseidon.NewPoseidonChip(api, field) + friChip := NewFriChip(api, field, qe, poseidonChip, &commonCircuitData.FriParams) + + friChallenges := FriChallenges{ + FriAlpha: QuadraticExtension{ + NewFieldElementFromString("14641715242626918707"), + NewFieldElementFromString("10574243340537902930"), + }, + FriBetas: []QuadraticExtension{}, + FriPowResponse: NewFieldElement(82451580476419), + FriQueryIndicies: []F{ + NewFieldElement(6790812084677375942), + NewFieldElement(12394212020331474798), + NewFieldElement(16457600747000998582), + NewFieldElement(1543271328932331916), + NewFieldElement(12115726870906958644), + NewFieldElement(6775897107605342797), + NewFieldElement(15989401564746021030), + NewFieldElement(10691676456016926845), + NewFieldElement(1632499470630032007), + NewFieldElement(1317292355445098328), + NewFieldElement(18391440812534384252), + NewFieldElement(17321705613231354333), + NewFieldElement(6176487551308859603), + NewFieldElement(7119835651572002873), + NewFieldElement(3903019169623116693), + NewFieldElement(4886491111111487546), + NewFieldElement(4087641893164620518), + NewFieldElement(13801643080324181364), + NewFieldElement(16993775312274189321), + NewFieldElement(9268202926222765679), + NewFieldElement(10683001302406181735), + NewFieldElement(13359465725531647963), + NewFieldElement(4523327590105620849), + NewFieldElement(4883588003760409588), + NewFieldElement(187699146998097671), + NewFieldElement(14489263557623716717), + NewFieldElement(11748359318238148146), + NewFieldElement(13636347200053048758), + }, + } + + initialMerkleCaps := []MerkleCap{ + verifierOnlyCircuitData.ConstantSigmasCap, + proofWithPis.Proof.WiresCap, + proofWithPis.Proof.PlonkZsPartialProductsCap, + proofWithPis.Proof.QuotientPolysCap, + } + + friChip.VerifyFriProof( + proofWithPis.Proof.Openings.ToFriOpenings(), + &friChallenges, + initialMerkleCaps, + &proofWithPis.Proof.OpeningProof, + ) - friChip := NewFriChip(api, field, commonCircuitData.Config.FriConfig) - friChip.VerifyFriProof(&proofWithPis.Proof.OpeningProof) return nil } diff --git a/plonky2_verifier/structs.go b/plonky2_verifier/structs.go index 14944e5..2cf02ca 100644 --- a/plonky2_verifier/structs.go +++ b/plonky2_verifier/structs.go @@ -75,6 +75,10 @@ type FriConfig struct { // TODO: add FriReductionStrategy } +func (fc *FriConfig) rate() float64 { + return 1.0 / float64((uint64(1) << fc.RateBits)) +} + type FriParams struct { Config FriConfig Hiding bool diff --git a/plonky2_verifier/utils.go b/plonky2_verifier/utils.go index 3578def..26d920b 100644 --- a/plonky2_verifier/utils.go +++ b/plonky2_verifier/utils.go @@ -1,7 +1,9 @@ package plonky2_verifier import ( + "fmt" . "gnark-ed25519/field" + "math/bits" ) func reduceWithPowers(qe *QuadraticExtensionAPI, terms []QuadraticExtension, scalar QuadraticExtension) QuadraticExtension { @@ -19,3 +21,12 @@ func reduceWithPowers(qe *QuadraticExtensionAPI, terms []QuadraticExtension, sca return sum } + +// Computes `log_2(n)`, panicking if `n` is not a power of two. +func log2Strict(n uint) int { + res := bits.TrailingZeros(n) + if n>>res != 1 { + panic(fmt.Sprintf("Not a power of two: %d", n)) + } + return res +}