Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
eef6fc5
add minLen to FixedLengthSum in sha3
ggq89 Mar 12, 2025
602387d
add minLen to FixedLengthSum in sha2
ggq89 Mar 12, 2025
cd0f4f7
add sha3FixedLengthSumWithMinLenCircuit test
ggq89 Mar 12, 2025
7232801
add Test_SHA3FixedLengthSum_WithMinLen_VS_Zero
ggq89 Mar 12, 2025
5432869
add comment for FixedLengthSum when < minLen
ggq89 Mar 13, 2025
32a21a6
optimize FixedLengthSum in sha2
ggq89 Mar 13, 2025
116a36f
update length from minLen to maxLen in TestSHA2FixedLengthSum
ggq89 Mar 13, 2025
41d1c0a
optimize resultState select times of absorbingFixedWidth in sha3
ggq89 Mar 13, 2025
6f1a0b2
optimize FixedLengthSum when == minNbOfBlocks
ggq89 Mar 13, 2025
65a1960
optimize padding 0x80 in FixedLengthSum of sh2
ggq89 Mar 13, 2025
14166d5
add unpackU8digest in sha2
ggq89 Mar 14, 2025
df2dd95
revert to previous order in FixedLengthSum of sha2
ggq89 Mar 14, 2025
c72e469
update data to padded in sha2 FixedLengthSum
ggq89 Mar 14, 2025
6357df3
update reachEnd to isPaddingStartPos in sha3 paddingFixedWidth
ggq89 Mar 14, 2025
6fe09e3
revert to data in sha2 and reachEnd in sha3
ggq89 Mar 14, 2025
b2d701b
refactor FixedLengthSum interface for backward compatibility
ggq89 Mar 18, 2025
df6c166
update some fn name in sha2
ggq89 Mar 18, 2025
43733d0
Revert "update some fn name in sha2"
ivokub Mar 19, 2025
eef9083
Revert "refactor FixedLengthSum interface for backward compatibility"
ivokub Mar 19, 2025
4a2073d
chore: revert FixedBinaryLengthHasher interface
ivokub Mar 18, 2025
fa7393c
feat: introduce hasher options
ivokub Mar 18, 2025
513ba1c
chore: use option for minimal sha2 fixed length
ivokub Mar 18, 2025
d07b93b
test: test fixed length hasher for different lenghts
ivokub Mar 18, 2025
a958184
chore: take minimum length as option instead
ivokub Mar 18, 2025
d5d87e6
refactor: use internal minimal length
ivokub Mar 19, 2025
ec3909e
test: remove separate circuit for bounded sha3
ivokub Mar 19, 2025
17a99c9
test: remove compile test
ivokub Mar 19, 2025
ba47bb9
test: run bounded sha3 tests as subtests
ivokub Mar 19, 2025
0816543
test: refactor sha2 test
ivokub Mar 19, 2025
e152050
chore: reintroduce sha2 fn name change
ivokub Mar 19, 2025
c574d89
chore: fix suggested typo
ivokub Mar 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion std/hash/hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,34 @@ type BinaryHasher interface {
// the length of the input is the total number of bytes written.
type BinaryFixedLengthHasher interface {
BinaryHasher
// FixedLengthSum returns digest of the first length bytes.
// FixedLengthSum returns digest of the first length bytes. See the
// [WithMinimalLength] option for setting lower bound on length.
FixedLengthSum(length frontend.Variable) []uints.U8
}

// HasherConfig allows to configure the behavior of the hash constructors. Do
// not initialize the configuration directly but rather use the [Option]
// functions which perform correct initializations. This configuration is
// exported for importing in hash implementations.
type HasherConfig struct {
MinimalLength int
}

// Option allows configuring the hash functions.
type Option func(*HasherConfig) error

// WithMinimalLength hints the minimal length of the input to the hash function.
// This allows to optimize the constraint count when calling
// [BinaryFixedLengthHasher.FixedLengthSum] as we can avoid selecting between
// the dummy padding and actual padding. If this option is not provided, then we
// assume the minimal length is 0.
func WithMinimalLength(minimalLength int) Option {
return func(cfg *HasherConfig) error {
cfg.MinimalLength = minimalLength
return nil
}
}

// Compressor is a 2-1 one-way function. It takes two inputs and compresses
// them into one output.
//
Expand Down
67 changes: 44 additions & 23 deletions std/hash/sha2/sha2.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package sha2

import (
"encoding/binary"
"fmt"
"math/big"

"github.com/consensys/gnark/frontend"
Expand All @@ -25,14 +26,22 @@ type digest struct {
api frontend.API
uapi *uints.BinaryField[uints.U32]
in []uints.U8

minimalLength int
}

func New(api frontend.API) (hash.BinaryFixedLengthHasher, error) {
func New(api frontend.API, opts ...hash.Option) (hash.BinaryFixedLengthHasher, error) {
cfg := new(hash.HasherConfig)
for _, opt := range opts {
if err := opt(cfg); err != nil {
return nil, fmt.Errorf("applying option: %w", err)
}
}
uapi, err := uints.New[uints.U32](api)
if err != nil {
return nil, err
return nil, fmt.Errorf("initializing uints: %w", err)
}
return &digest{api: api, uapi: uapi}, nil
return &digest{api: api, uapi: uapi, minimalLength: cfg.MinimalLength}, nil
}

func (d *digest) Write(data []uints.U8) {
Expand Down Expand Up @@ -68,9 +77,14 @@ func (d *digest) Sum() []uints.U8 {
copy(buf[:], padded[i*64:(i+1)*64])
runningDigest = sha2.Permute(d.uapi, runningDigest, buf)
}

return d.unpackU8Digest(runningDigest)
}

func (d *digest) unpackU8Digest(digest [8]uints.U32) []uints.U8 {
var ret []uints.U8
for i := range runningDigest {
ret = append(ret, d.uapi.UnpackMSB(runningDigest[i])...)
for i := range digest {
ret = append(ret, d.uapi.UnpackMSB(digest[i])...)
}
return ret
}
Expand All @@ -85,15 +99,18 @@ func (d *digest) FixedLengthSum(length frontend.Variable) []uints.U8 {
// idea - have a mask for blocks where 1 is only for the block we want to
// use.

data := make([]uints.U8, len(d.in))
copy(data, d.in)

comparator := cmp.NewBoundedComparator(d.api, big.NewInt(int64(len(data)+64+8)), false)

for i := 0; i < 64+8; i++ {
data = append(data, uints.NewU8(0))
maxLen := len(d.in)
comparator := cmp.NewBoundedComparator(d.api, big.NewInt(int64(maxLen+64+8)), false)
// when minimal length is 0 (i.e. not set), then we can skip the check as it holds naturally (all field elements are non-negative)
if d.minimalLength > 0 {
// we use comparator as [frontend.API] doesn't have a fast path for case API.AssertIsLessOrEqual(constant, variable)
comparator.AssertIsLessEq(d.minimalLength, length)
}

data := make([]uints.U8, maxLen)
copy(data, d.in)
data = append(data, uints.NewU8Array(make([]uint8, 64+8))...)

lenMod64 := d.mod64(length)
lenMod64Less56 := comparator.IsLess(lenMod64, 56)

Expand All @@ -106,16 +123,18 @@ func (d *digest) FixedLengthSum(length frontend.Variable) []uints.U8 {
var dataLenBtyes [8]frontend.Variable
d.bigEndianPutUint64(dataLenBtyes[:], d.api.Mul(length, 8))

for i := range data {
isPaddingStartPos := d.api.IsZero(d.api.Sub(i, length))
// When i < minLen or i > maxLen, padding 1 0r 0 is completely unnecessary
for i := d.minimalLength; i <= maxLen; i++ {
isPaddingStartPos := cmp.IsEqual(d.api, i, length)
data[i].Val = d.api.Select(isPaddingStartPos, 0x80, data[i].Val)

isPaddingPos := comparator.IsLess(length, i)
data[i].Val = d.api.Select(isPaddingPos, 0, data[i].Val)
}

for i := range data {
isLast8BytesPos := d.api.IsZero(d.api.Sub(i, last8BytesPos))
// When i <= minLen, padding length is completely unnecessary
for i := d.minimalLength + 1; i < len(data); i++ {
isLast8BytesPos := cmp.IsEqual(d.api, i, last8BytesPos)
for j := 0; j < 8; j++ {
if i+j < len(data) {
data[i+j].Val = d.api.Select(isLast8BytesPos, dataLenBtyes[j], data[i+j].Val)
Expand All @@ -127,26 +146,28 @@ func (d *digest) FixedLengthSum(length frontend.Variable) []uints.U8 {
var resultDigest [8]uints.U32
var buf [64]uints.U8
copy(runningDigest[:], _seed)
copy(resultDigest[:], _seed)

for i := 0; i < len(data)/64; i++ {
copy(buf[:], data[i*64:(i+1)*64])
runningDigest = sha2.Permute(d.uapi, runningDigest, buf)

isInRange := comparator.IsLess(i*64, totalLen)
// When i < minLen/64, runningDigest cannot be resultDigest, and proceed to the next loop directly
if i < d.minimalLength/64 {
continue
} else if i == d.minimalLength/64 { // init resultDigests
copy(resultDigest[:], runningDigest[:])
continue
}

isInRange := comparator.IsLess(i*64, totalLen)
for j := 0; j < 8; j++ {
for k := 0; k < 4; k++ {
resultDigest[j][k].Val = d.api.Select(isInRange, runningDigest[j][k].Val, resultDigest[j][k].Val)
}
}
}

var ret []uints.U8
for i := range resultDigest {
ret = append(ret, d.uapi.UnpackMSB(resultDigest[i])...)
}
return ret
return d.unpackU8Digest(resultDigest)
}

func (d *digest) Reset() {
Expand Down
43 changes: 31 additions & 12 deletions std/hash/sha2/sha2_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package sha2

import (
"crypto/rand"
"crypto/sha256"
"fmt"
"testing"

"github.com/consensys/gnark-crypto/ecc"
"github.com/consensys/gnark/frontend"
"github.com/consensys/gnark/std/hash"
"github.com/consensys/gnark/std/math/uints"
"github.com/consensys/gnark/test"
)
Expand Down Expand Up @@ -53,10 +55,13 @@ type sha2FixedLengthCircuit struct {
In []uints.U8
Length frontend.Variable
Expected [32]uints.U8

// minimal length of the input is the circuit parameter
minimalLength int
}

func (c *sha2FixedLengthCircuit) Define(api frontend.API) error {
h, err := New(api)
h, err := New(api, hash.WithMinimalLength(c.minimalLength))
if err != nil {
return err
}
Expand All @@ -76,16 +81,30 @@ func (c *sha2FixedLengthCircuit) Define(api frontend.API) error {
}

func TestSHA2FixedLengthSum(t *testing.T) {
bts := make([]byte, 144)
length := 56
dgst := sha256.Sum256(bts[:length])
witness := sha2FixedLengthCircuit{
In: uints.NewU8Array(bts),
Length: length,
}
copy(witness.Expected[:], uints.NewU8Array(dgst[:]))
err := test.IsSolved(&sha2FixedLengthCircuit{In: make([]uints.U8, len(bts))}, &witness, ecc.BN254.ScalarField())
if err != nil {
t.Fatal(err)
const maxLen = 144
assert := test.NewAssert(t)
bts := make([]byte, maxLen)
_, err := rand.Reader.Read(bts)
assert.NoError(err)

for _, lengthBound := range []int{0, 1, 63, 64, 65, len(bts)} {
circuit := &sha2FixedLengthCircuit{In: make([]uints.U8, len(bts)), minimalLength: lengthBound}
for _, length := range []int{0, 1, 63, 64, 65, len(bts)} {
assert.Run(func(assert *test.Assert) {
dgst := sha256.Sum256(bts[:length])
witness := &sha2FixedLengthCircuit{
In: uints.NewU8Array(bts),
Length: length,
Expected: [32]uints.U8(uints.NewU8Array(dgst[:])),
}

err = test.IsSolved(circuit, witness, ecc.BN254.ScalarField())
if length >= lengthBound {
assert.NoError(err)
} else if length < lengthBound {
assert.Error(err, "expected error for length < lengthBound")
}
}, fmt.Sprintf("bound=%d/length=%d", lengthBound, length))
}
}
}
96 changes: 33 additions & 63 deletions std/hash/sha3/hashes.go
Original file line number Diff line number Diff line change
@@ -1,99 +1,69 @@
package sha3

import (
"fmt"

"github.com/consensys/gnark/frontend"
"github.com/consensys/gnark/std/hash"
"github.com/consensys/gnark/std/math/uints"
)

// New256 creates a new SHA3-256 hash.
// Its generic security strength is 256 bits against preimage attacks,
// and 128 bits against collision attacks.
func New256(api frontend.API) (hash.BinaryFixedLengthHasher, error) {
// newHash is a helper function to create a new SHA3 hash.
func newHash(api frontend.API, dsByte byte, rate, outputLen int, opts ...hash.Option) (hash.BinaryFixedLengthHasher, error) {
cfg := new(hash.HasherConfig)
for _, opt := range opts {
if err := opt(cfg); err != nil {
return nil, fmt.Errorf("applying option: %w", err)
}
}
uapi, err := uints.New[uints.U64](api)
if err != nil {
return nil, err
return nil, fmt.Errorf("initializing uints: %w", err)
}
return &digest{
api: api,
uapi: uapi,
state: newState(),
dsbyte: 0x06,
rate: 136,
outputLen: 32,
api: api,
uapi: uapi,
state: newState(),
dsbyte: dsByte,
rate: rate,
outputLen: outputLen,
minimalLength: cfg.MinimalLength,
}, nil
}

// New256 creates a new SHA3-256 hash.
// Its generic security strength is 256 bits against preimage attacks,
// and 128 bits against collision attacks.
func New256(api frontend.API, opts ...hash.Option) (hash.BinaryFixedLengthHasher, error) {
return newHash(api, 0x06, 136, 32, opts...)
}

// New384 creates a new SHA3-384 hash.
// Its generic security strength is 384 bits against preimage attacks,
// and 192 bits against collision attacks.
func New384(api frontend.API) (hash.BinaryFixedLengthHasher, error) {
uapi, err := uints.New[uints.U64](api)
if err != nil {
return nil, err
}
return &digest{
api: api,
uapi: uapi,
state: newState(),
dsbyte: 0x06,
rate: 104,
outputLen: 48,
}, nil
func New384(api frontend.API, opts ...hash.Option) (hash.BinaryFixedLengthHasher, error) {
return newHash(api, 0x06, 104, 48, opts...)
}

// New512 creates a new SHA3-512 hash.
// Its generic security strength is 512 bits against preimage attacks,
// and 256 bits against collision attacks.
func New512(api frontend.API) (hash.BinaryFixedLengthHasher, error) {
uapi, err := uints.New[uints.U64](api)
if err != nil {
return nil, err
}
return &digest{
api: api,
uapi: uapi,
state: newState(),
dsbyte: 0x06,
rate: 72,
outputLen: 64,
}, nil
func New512(api frontend.API, opts ...hash.Option) (hash.BinaryFixedLengthHasher, error) {
return newHash(api, 0x06, 72, 64, opts...)
}

// NewLegacyKeccak256 creates a new Keccak-256 hash.
//
// Only use this function if you require compatibility with an existing cryptosystem
// that uses non-standard padding. All other users should use New256 instead.
func NewLegacyKeccak256(api frontend.API) (hash.BinaryFixedLengthHasher, error) {
uapi, err := uints.New[uints.U64](api)
if err != nil {
return nil, err
}
return &digest{
api: api,
uapi: uapi,
state: newState(),
dsbyte: 0x01,
rate: 136,
outputLen: 32,
}, nil
func NewLegacyKeccak256(api frontend.API, opts ...hash.Option) (hash.BinaryFixedLengthHasher, error) {
return newHash(api, 0x01, 136, 32, opts...)
}

// NewLegacyKeccak512 creates a new Keccak-512 hash.
//
// Only use this function if you require compatibility with an existing cryptosystem
// that uses non-standard padding. All other users should use New512 instead.
func NewLegacyKeccak512(api frontend.API) (hash.BinaryFixedLengthHasher, error) {
uapi, err := uints.New[uints.U64](api)
if err != nil {
return nil, err
}
return &digest{
api: api,
uapi: uapi,
state: newState(),
dsbyte: 0x01,
rate: 72,
outputLen: 64,
}, nil
func NewLegacyKeccak512(api frontend.API, opts ...hash.Option) (hash.BinaryFixedLengthHasher, error) {
return newHash(api, 0x01, 72, 64, opts...)
}
Loading
Loading