diff --git a/std/math/uints/bytes.go b/std/math/uints/bytes.go index 77b520cb5e..ede48838bf 100644 --- a/std/math/uints/bytes.go +++ b/std/math/uints/bytes.go @@ -6,6 +6,7 @@ import ( "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/internal/kvstore" "github.com/consensys/gnark/std/internal/logderivprecomp" + stdbits "github.com/consensys/gnark/std/math/bits" "github.com/consensys/gnark/std/rangecheck" ) @@ -103,6 +104,22 @@ func (bf *Bytes) packInternal(val frontend.Variable) U8 { return U8{Val: val, internal: true} } +// ToBits decomposes a byte into little-endian bits and constrains the byte to +// equal the recomposition of these bits. +func (bf *Bytes) ToBits(a U8) []frontend.Variable { + return bf.api.ToBinary(a.Val, 8) +} + +// FromBits packs eight little-endian bits into a byte. The input bits are +// constrained to be boolean unless they are already marked as boolean by the +// compiler. +func (bf *Bytes) FromBits(bits ...frontend.Variable) U8 { + if len(bits) != 8 { + panic("expected exactly 8 bits") + } + return bf.packInternal(stdbits.FromBinary(bf.api, bits)) +} + // ValueOf returns a constrainted [U8] variable. For a constant value, use // [NewU8] instead. func (bf *Bytes) ValueOf(a frontend.Variable) U8 { diff --git a/std/math/uints/uint8.go b/std/math/uints/uint8.go index 5cc44ad5b3..e3cfef8906 100644 --- a/std/math/uints/uint8.go +++ b/std/math/uints/uint8.go @@ -138,6 +138,11 @@ func (bf *BinaryField[T]) zero() T { return res } +// API returns the underlying frontend API. +func (bf *BinaryField[T]) API() frontend.API { + return bf.Bytes.api +} + // ByteValueOf converts a frontend.Variable into a single byte. If the input // doesn't fit into a byte then solver fails. func (bf *BinaryField[T]) ByteValueOf(a frontend.Variable) U8 { @@ -196,6 +201,29 @@ func (bf *BinaryField[T]) PackLSB(a ...U8) T { return ret } +// ToBits decomposes a long integer into little-endian bits and constrains each +// byte to equal the recomposition of its bits. +func (bf *BinaryField[T]) ToBits(a T) []frontend.Variable { + ret := make([]frontend.Variable, 0, bf.lenBts()*8) + for i := 0; i < bf.lenBts(); i++ { + ret = append(ret, bf.Bytes.ToBits(a[i])...) + } + return ret +} + +// FromBits packs little-endian bits into a long integer. The number of bits +// must match the width of T. +func (bf *BinaryField[T]) FromBits(bits ...frontend.Variable) T { + if len(bits) != bf.lenBts()*8 { + panic("incorrect number of bits") + } + var ret T + for i := 0; i < bf.lenBts(); i++ { + ret[i] = bf.Bytes.FromBits(bits[8*i : 8*(i+1)]...) + } + return ret +} + // UnpackMSB unpacks a long integer T into bytes assuming most significant // byte first order. // For example, UnpackMSB(0x12345678) = (0x12, 0x34, 0x56, 0x78) diff --git a/std/math/uints/uint8_test.go b/std/math/uints/uint8_test.go index 7e8d50a8c6..685fa583dc 100644 --- a/std/math/uints/uint8_test.go +++ b/std/math/uints/uint8_test.go @@ -86,6 +86,26 @@ func TestValueOf(t *testing.T) { assert.CheckCircuit(&valueOfCircuit[U32]{}, test.WithInvalidAssignment(&valueOfCircuit[U32]{In: 0x1234567812345678, Expected: [4]U8{NewU8(0x78), NewU8(0x56), NewU8(0x34), NewU8(0x12)}})) } +type bitsRoundTripCircuit struct { + In, Expected U64 +} + +func (c *bitsRoundTripCircuit) Define(api frontend.API) error { + uapi, err := New[U64](api) + if err != nil { + return err + } + bits := uapi.ToBits(c.In) + res := uapi.FromBits(bits...) + uapi.AssertEq(res, c.Expected) + return nil +} + +func TestBitsRoundTrip(t *testing.T) { + assert := test.NewAssert(t) + assert.CheckCircuit(&bitsRoundTripCircuit{}, test.WithValidAssignment(&bitsRoundTripCircuit{In: NewU64(0x0123456789abcdef), Expected: NewU64(0x0123456789abcdef)})) +} + type addCircuit struct { In []U32 Expected U32 diff --git a/std/permutation/keccakf/keccak_test.go b/std/permutation/keccakf/keccak_test.go index 84e970ad4f..526dabe032 100644 --- a/std/permutation/keccakf/keccak_test.go +++ b/std/permutation/keccakf/keccak_test.go @@ -8,6 +8,8 @@ import ( "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark/backend" "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/frontend/cs/r1cs" + "github.com/consensys/gnark/frontend/cs/scs" "github.com/consensys/gnark/std/permutation/keccakf" "github.com/consensys/gnark/test" ) @@ -35,6 +37,33 @@ func (c *keccakfCircuit) Define(api frontend.API) error { return nil } +type keccakfCountCircuit struct { + In [25]uints.U64 + Expected [25]uints.U64 `gnark:",public"` +} + +func (c *keccakfCountCircuit) Define(api frontend.API) error { + uapi, err := uints.New[uints.U64](api) + if err != nil { + return err + } + res := keccakf.Permute(uapi, c.In) + for i := range res { + uapi.AssertEq(res[i], c.Expected[i]) + } + return nil +} + +func TestKeccakfCount(t *testing.T) { + assert := test.NewAssert(t) + ccs, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, &keccakfCountCircuit{}) + assert.NoError(err) + t.Log("KeccakF-1600 r1cs constraints =", ccs.GetNbConstraints(), "instructions =", ccs.GetNbInstructions()) + ccs, err = frontend.Compile(ecc.BN254.ScalarField(), scs.NewBuilder, &keccakfCountCircuit{}) + assert.NoError(err) + t.Log("KeccakF-1600 scs constraints =", ccs.GetNbConstraints(), "instructions =", ccs.GetNbInstructions()) +} + func TestKeccakf(t *testing.T) { var nativeIn [25]uint64 var res [25]uint64 diff --git a/std/permutation/keccakf/keccakf.go b/std/permutation/keccakf/keccakf.go index 48aded9131..b3e8ca56ab 100644 --- a/std/permutation/keccakf/keccakf.go +++ b/std/permutation/keccakf/keccakf.go @@ -6,39 +6,50 @@ // package. // // The cost for a single application of permutation is: -// - 193650 constraints in Groth16 -// - 292032 constraints in Plonk +// - 94160 constraints in Groth16 +// - 158486 constraints in Plonk package keccakf import ( + "errors" + "math/big" + + "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/kvstore" "github.com/consensys/gnark/std/math/uints" ) -var rc = [24]uints.U64{ - uints.NewU64(0x0000000000000001), - uints.NewU64(0x0000000000008082), - uints.NewU64(0x800000000000808A), - uints.NewU64(0x8000000080008000), - uints.NewU64(0x000000000000808B), - uints.NewU64(0x0000000080000001), - uints.NewU64(0x8000000080008081), - uints.NewU64(0x8000000000008009), - uints.NewU64(0x000000000000008A), - uints.NewU64(0x0000000000000088), - uints.NewU64(0x0000000080008009), - uints.NewU64(0x000000008000000A), - uints.NewU64(0x000000008000808B), - uints.NewU64(0x800000000000008B), - uints.NewU64(0x8000000000008089), - uints.NewU64(0x8000000000008003), - uints.NewU64(0x8000000000008002), - uints.NewU64(0x8000000000000080), - uints.NewU64(0x000000000000800A), - uints.NewU64(0x800000008000000A), - uints.NewU64(0x8000000080008081), - uints.NewU64(0x8000000000008080), - uints.NewU64(0x0000000080000001), - uints.NewU64(0x8000000080008008), +func init() { + solver.RegisterHint(xor3Hint, chiHint) +} + +var rc = [24]uint64{ + 0x0000000000000001, + 0x0000000000008082, + 0x800000000000808A, + 0x8000000080008000, + 0x000000000000808B, + 0x0000000080000001, + 0x8000000080008081, + 0x8000000000008009, + 0x000000000000008A, + 0x0000000000000088, + 0x0000000080008009, + 0x000000008000000A, + 0x000000008000808B, + 0x800000000000008B, + 0x8000000000008089, + 0x8000000000008003, + 0x8000000000008002, + 0x8000000000000080, + 0x000000000000800A, + 0x800000008000000A, + 0x8000000080008081, + 0x8000000000008080, + 0x0000000080000001, + 0x8000000080008008, } var rotc = [24]int{ 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, @@ -52,32 +63,110 @@ var piln = [24]int{ // Permute applies Keccak-F permutation on the input and returns the permuted vector. // Original input is not modified. func Permute(uapi *uints.BinaryField[uints.U64], input [25]uints.U64) [25]uints.U64 { - var state [25]uints.U64 - copy(state[:], input[:]) - return permute(uapi, state) + var state [25][64]frontend.Variable + for i := range input { + bits := uapi.ToBits(input[i]) + copy(state[i][:], bits) + } + state = permuteBits(uapi.API(), state) + + var ret [25]uints.U64 + for i := range ret { + ret[i] = uapi.FromBits(state[i][:]...) + } + return ret } -func permute(uapi *uints.BinaryField[uints.U64], st [25]uints.U64) [25]uints.U64 { - var t uints.U64 - var bc [5]uints.U64 +func permuteBits(api frontend.API, st [25][64]frontend.Variable) [25][64]frontend.Variable { + if isR1CS(api) { + return permuteBitsR1CS(api, st) + } + return permuteBitsGeneric(api, st) +} + +func isR1CS(api frontend.API) bool { + if api.Compiler().Field().Cmp(big.NewInt(3)) <= 0 { + return false + } + _, ok := api.Compiler().ToCanonicalVariable(0).(constraint.LinearExpression) + return ok +} + +func permuteBitsGeneric(api frontend.API, st [25][64]frontend.Variable) [25][64]frontend.Variable { + var bc [5][64]frontend.Variable for r := 0; r < 24; r++ { // theta for i := 0; i < 5; i++ { - bc[i] = uapi.Xor(st[i], st[i+5], st[i+10], st[i+15], st[i+20]) + for z := 0; z < 64; z++ { + bc[i][z] = xor(api, st[i][z], st[i+5][z], st[i+10][z], st[i+15][z], st[i+20][z]) + } + } + for i := 0; i < 5; i++ { + rot := lrot(bc[(i+1)%5], 1) + for z := 0; z < 64; z++ { + d := api.Xor(bc[(i+4)%5][z], rot[z]) + for j := 0; j < 25; j += 5 { + st[j+i][z] = api.Xor(st[j+i][z], d) + } + } + } + // rho pi + t := st[1] + for i := 0; i < 24; i++ { + j := piln[i] + bc0 := st[j] + st[j] = lrot(t, rotc[i]) + t = bc0 + } + + // chi + for j := 0; j < 25; j += 5 { + for i := 0; i < 5; i++ { + bc[i] = st[j+i] + } + for i := 0; i < 5; i++ { + for z := 0; z < 64; z++ { + st[j+i][z] = api.Xor(st[j+i][z], andNot(api, bc[(i+1)%5][z], bc[(i+2)%5][z])) + } + } + } + // iota + for z := 0; z < 64; z++ { + if (rc[r]>>z)&1 == 1 { + st[0][z] = api.Xor(st[0][z], 1) + } + } + } + return st +} + +func permuteBitsR1CS(api frontend.API, st [25][64]frontend.Variable) [25][64]frontend.Variable { + var bc [5][64]frontend.Variable + for r := 0; r < 24; r++ { + // theta: C[x] is a five-bit parity using two one-row XOR3s. D[x] is + // folded into A[x,y] as XOR3(A[x,y], C[x-1], ROT(C[x+1], 1)). + for i := 0; i < 5; i++ { + for z := 0; z < 64; z++ { + t := xor3R1CS(api, st[i][z], st[i+5][z], st[i+10][z]) + bc[i][z] = xor3R1CS(api, t, st[i+15][z], st[i+20][z]) + } } for i := 0; i < 5; i++ { - t = uapi.Xor(bc[(i+4)%5], uapi.Lrot(bc[(i+1)%5], 1)) - for j := 0; j < 25; j += 5 { - st[j+i] = uapi.Xor(st[j+i], t) + rot := lrot(bc[(i+1)%5], 1) + for z := 0; z < 64; z++ { + for j := 0; j < 25; j += 5 { + st[j+i][z] = xor3R1CS(api, st[j+i][z], bc[(i+4)%5][z], rot[z]) + } } } + // rho pi - t = st[1] + t := st[1] for i := 0; i < 24; i++ { j := piln[i] - bc[0] = st[j] - st[j] = uapi.Lrot(t, rotc[i]) - t = bc[0] + bc0 := st[j] + st[j] = lrot(t, rotc[i]) + t = bc0 } // chi @@ -86,11 +175,133 @@ func permute(uapi *uints.BinaryField[uints.U64], st [25]uints.U64) [25]uints.U64 bc[i] = st[j+i] } for i := 0; i < 5; i++ { - st[j+i] = uapi.Xor(st[j+i], uapi.And(uapi.Not(bc[(i+1)%5]), bc[(i+2)%5])) + for z := 0; z < 64; z++ { + st[j+i][z] = chiR1CS(api, bc[i][z], bc[(i+1)%5][z], bc[(i+2)%5][z]) + } } } + // iota - st[0] = uapi.Xor(st[0], rc[r]) + for z := 0; z < 64; z++ { + if (rc[r]>>z)&1 == 1 { + st[0][z] = api.Sub(1, st[0][z]) + api.Compiler().MarkBoolean(st[0][z]) + } + } } return st } + +func xor(api frontend.API, xs ...frontend.Variable) frontend.Variable { + ret := xs[0] + for _, x := range xs[1:] { + ret = api.Xor(ret, x) + } + return ret +} + +func andNot(api frontend.API, b, c frontend.Variable) frontend.Variable { + plonkAPI, ok := api.Compiler().(frontend.PlonkAPI) + if !ok { + return api.And(api.Xor(b, 1), c) + } + // z = (NOT b) AND c = c - bc, with b and c already boolean. + z := plonkAPI.EvaluatePlonkExpression(b, c, 0, 1, -1, 0) + api.Compiler().MarkBoolean(z) + return z +} + +func lrot(a [64]frontend.Variable, c int) [64]frontend.Variable { + var ret [64]frontend.Variable + for i := range a { + ret[(i+c)%64] = a[i] + } + return ret +} + +type r1cBlueprintKey struct{} + +func r1cBlueprintID(api frontend.API) constraint.BlueprintID { + kv, ok := api.Compiler().(kvstore.Store) + if !ok { + panic("compiler does not implement kvstore.Store") + } + if id := kv.GetKeyValue(r1cBlueprintKey{}); id != nil { + return id.(constraint.BlueprintID) + } + id := api.Compiler().AddBlueprint(&constraint.BlueprintGenericR1C{}) + kv.SetKeyValue(r1cBlueprintKey{}, id) + return id +} + +func addR1C(api frontend.API, left, right, output frontend.Variable) { + l, ok := api.Compiler().ToCanonicalVariable(left).(constraint.LinearExpression) + if !ok { + panic("expected R1CS linear expression") + } + r, ok := api.Compiler().ToCanonicalVariable(right).(constraint.LinearExpression) + if !ok { + panic("expected R1CS linear expression") + } + o, ok := api.Compiler().ToCanonicalVariable(output).(constraint.LinearExpression) + if !ok { + panic("expected R1CS linear expression") + } + + r1c := constraint.R1C{L: l, R: r, O: o} + var blueprint constraint.BlueprintGenericR1C + calldata := make([]uint32, 0) + blueprint.CompressR1C(&r1c, &calldata) + api.Compiler().AddInstruction(r1cBlueprintID(api), calldata) +} + +func xor3R1CS(api frontend.API, a, b, c frontend.Variable) frontend.Variable { + out, err := api.Compiler().NewHint(xor3Hint, 1, a, b, c) + if err != nil { + panic(err) + } + z := out[0] + // For boolean a,b,c and char > 3, the multiplier is never zero and this + // row uniquely pins z = a XOR b XOR c. + left := api.Add(z, api.Mul(2, a), api.Mul(2, b), api.Mul(7, c)) + right := api.Add(a, b, api.Mul(-4, c), 1) + output := api.Add(api.Mul(6, a), api.Mul(6, b), api.Mul(-24, c)) + addR1C(api, left, right, output) + api.Compiler().MarkBoolean(z) + return z +} + +func chiR1CS(api frontend.API, a, b, c frontend.Variable) frontend.Variable { + out, err := api.Compiler().NewHint(chiHint, 1, a, b, c) + if err != nil { + panic(err) + } + z := out[0] + // For boolean a,b,c and char > 3, the multiplier is never zero and this + // row uniquely pins z = a XOR ((NOT b) AND c). + left := api.Add(z, api.Mul(3, a), api.Neg(b), api.Neg(c)) + right := api.Add(api.Mul(4, a), b, c, -3) + output := api.Add(api.Mul(4, a), api.Mul(2, b)) + addR1C(api, left, right, output) + api.Compiler().MarkBoolean(z) + return z +} + +func xor3Hint(_ *big.Int, inputs, outputs []*big.Int) error { + if len(inputs) != 3 || len(outputs) != 1 { + return errors.New("expecting three inputs and one output") + } + outputs[0].SetUint64(uint64(inputs[0].Bit(0) ^ inputs[1].Bit(0) ^ inputs[2].Bit(0))) + return nil +} + +func chiHint(_ *big.Int, inputs, outputs []*big.Int) error { + if len(inputs) != 3 || len(outputs) != 1 { + return errors.New("expecting three inputs and one output") + } + a := inputs[0].Bit(0) + b := inputs[1].Bit(0) + c := inputs[2].Bit(0) + outputs[0].SetUint64(uint64(a ^ ((1 ^ b) & c))) + return nil +}