Skip to content

Commit 09af95a

Browse files
Update the limb decomposition of the SIS (#389)
* Update limb decomposition to unblock the self-recursion * skip the limb decomposition test with 32 bits
1 parent da59459 commit 09af95a

File tree

4 files changed

+111114
-110974
lines changed

4 files changed

+111114
-110974
lines changed

ecc/bn254/fr/sis/sis.go

+92-45
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,16 @@ func NewRSis(seed int64, logTwoDegree, logTwoBound, maxNbElementsToHash int) (*R
9191
// capacity == [degree * n * logTwoBound] / 8
9292
// n == (capacity*8)/(degree*logTwoBound)
9393

94-
n := capacity * 8 / logTwoBound // number of coefficients
94+
// First n <- #limbs to represent a single field element
95+
n := (fr.Bytes * 8) / logTwoBound
96+
if n*logTwoBound < fr.Bytes*8 {
97+
n++
98+
}
99+
100+
// Then multiply by the number of field elements
101+
n *= maxNbElementsToHash
102+
103+
// And divide (+ ceil) to get the number of polynomials
95104
if n%degree == 0 {
96105
n /= degree
97106
} else {
@@ -160,53 +169,11 @@ func (r *RSis) Sum(b []byte) []byte {
160169
}
161170

162171
// clear the buffers of the instance.
163-
defer func() {
164-
r.bufMValues.ClearAll()
165-
for i := 0; i < len(r.bufM); i++ {
166-
r.bufM[i].SetZero()
167-
}
168-
for i := 0; i < len(r.bufRes); i++ {
169-
r.bufRes[i].SetZero()
170-
}
171-
}()
172+
defer r.cleanupBuffers()
172173

173-
// bitwise decomposition of the buffer, in order to build m (the vector to hash)
174-
// as a list of polynomials, whose coefficients are less than r.B bits long.
175-
// Say buf=[0xbe,0x0f]. As a stream of bits it is interpreted like this:
176-
// 10111110 00001111. BitAt(0)=1 (=leftmost bit), bitAt(1)=0 (=second leftmost bit), etc.
177-
nbBits := len(buf) * 8
178-
bitAt := func(i int) uint8 {
179-
k := i / 8
180-
if k >= len(buf) {
181-
return 0
182-
}
183-
b := buf[k]
184-
j := i % 8
185-
return b >> (7 - j) & 1
186-
}
187-
188-
// now we can construct m. The input to hash consists of the polynomials
189-
// m[k*r.Degree:(k+1)*r.Degree]
190174
m := r.bufM
191-
192-
// mark blocks m[i*r.Degree : (i+1)*r.Degree] != [0...0]
193175
mValues := r.bufMValues
194-
195-
// we process the input buffer by blocks of r.LogTwoBound bits
196-
// each of these block (<< 64bits) are interpreted as a coefficient
197-
mPos := 0
198-
for i := 0; i < nbBits; mPos++ {
199-
for j := 0; j < r.LogTwoBound; j++ {
200-
// r.LogTwoBound < 64; we just use the first word of our element here,
201-
// and set the bits from LSB to MSB.
202-
m[mPos][0] |= uint64(bitAt(i) << j)
203-
i++
204-
}
205-
if m[mPos][0] == 0 {
206-
continue
207-
}
208-
mValues.Set(uint(mPos / r.Degree))
209-
}
176+
limbDecomposeBytes(buf, m, r.LogTwoBound, r.Degree, mValues)
210177

211178
// we can hash now.
212179
res := r.bufRes
@@ -322,3 +289,83 @@ func (r *RSis) CopyWithFreshBuffer() RSis {
322289
res.buffer = bytes.Buffer{}
323290
return res
324291
}
292+
293+
// Cleanup the buffers of the RSis instance
294+
func (r *RSis) cleanupBuffers() {
295+
r.bufMValues.ClearAll()
296+
for i := 0; i < len(r.bufM); i++ {
297+
r.bufM[i].SetZero()
298+
}
299+
for i := 0; i < len(r.bufRes); i++ {
300+
r.bufRes[i].SetZero()
301+
}
302+
}
303+
304+
// Split an slice of bytes representing an array of serialized field element in
305+
// big-endian form into an array of limbs representing the same field elements
306+
// in little-endian form. Namely, if our field is reprented with 64 bits and we
307+
// have the following field element 0x0123456789abcdef (0 being the most significant
308+
// character and and f being the least significant one) and our log norm bound is
309+
// 16 (so 1 hex character = 1 limb). The function assigns the values of m to [f, e,
310+
// d, c, b, a, ..., 3, 2, 1, 0]. m should be preallocated and zeroized. Additionally,
311+
// we have the guarantee that 2 bits contributing to different field elements cannot
312+
// be part of the same limb.
313+
func LimbDecomposeBytes(buf []byte, m fr.Vector, logTwoBound int) {
314+
limbDecomposeBytes(buf, m, logTwoBound, 0, nil)
315+
}
316+
317+
// Split an slice of bytes representing an array of serialized field element in
318+
// big-endian form into an array of limbs representing the same field elements
319+
// in little-endian form. Namely, if our field is reprented with 64 bits and we
320+
// have the following field element 0x0123456789abcdef (0 being the most significant
321+
// character and and f being the least significant one) and our log norm bound is
322+
// 16 (so 1 hex character = 1 limb). The function assigns the values of m to [f, e,
323+
// d, c, b, a, ..., 3, 2, 1, 0]. m should be preallocated and zeroized. mValues is
324+
// an optional bitSet. If provided, it must be empty. The function will set bit "i"
325+
// to indicate the that i-th SIS input polynomial should be non-zero. Recall, that a
326+
// SIS polynomial corresponds to a chunk of limbs of size `degree`. Additionally,
327+
// we have the guarantee that 2 bits contributing to different field elements cannot
328+
// be part of the same limb.
329+
func limbDecomposeBytes(buf []byte, m fr.Vector, logTwoBound, degree int, mValues *bitset.BitSet) {
330+
331+
// bitwise decomposition of the buffer, in order to build m (the vector to hash)
332+
// as a list of polynomials, whose coefficients are less than r.B bits long.
333+
// Say buf=[0xbe,0x0f]. As a stream of bits it is interpreted like this:
334+
// 10111110 00001111. BitAt(0)=1 (=leftmost bit), bitAt(1)=0 (=second leftmost bit), etc.
335+
nbBits := len(buf) * 8
336+
bitAt := func(i int) uint8 {
337+
k := i / 8
338+
if k >= len(buf) {
339+
return 0
340+
}
341+
b := buf[k]
342+
j := i % 8
343+
return b >> (7 - j) & 1
344+
}
345+
346+
// we process the input buffer by blocks of r.LogTwoBound bits
347+
// each of these block (<< 64bits) are interpreted as a coefficient
348+
mPos := 0
349+
for fieldStart := 0; fieldStart < nbBits; {
350+
for bitInField := 0; bitInField < fr.Bytes*8; {
351+
352+
j := bitInField % logTwoBound
353+
354+
// r.LogTwoBound < 64; we just use the first word of our element here,
355+
// and set the bits from LSB to MSB.
356+
at := fieldStart + fr.Bytes*8 - bitInField - 1
357+
m[mPos][0] |= uint64(bitAt(at) << j)
358+
bitInField++
359+
360+
// Check if mPos is zero and mark as non-zero in the bitset if not
361+
if m[mPos][0] > 0 && mValues != nil {
362+
mValues.Set(uint(mPos / degree))
363+
}
364+
365+
if j == logTwoBound-1 || bitInField == fr.Bytes*8 {
366+
mPos++
367+
}
368+
}
369+
fieldStart += fr.Bytes * 8
370+
}
371+
}

ecc/bn254/fr/sis/sis.sage

+27-5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import json
66
# BN254 Fr
77
r = 21888242871839275222246405745257275088548364400416034343698204186575808495617
88
frByteSize = 32
9+
countToDeath = int(5)
910
gfr = GF(r)
1011
Fr = GF(r)
1112
Fr.<x> = Fr[]
@@ -78,16 +79,37 @@ def splitCoeffs(b, logTwoBound):
7879
7980
Returns:
8081
an array of coeffs, each coeff being the i-th chunk of logTwoBounds bits of b.
82+
The coeffs are formed as follow. The input byte string is implicitly parsed as
83+
a slice of field elements of 32 bytes each in bigendian-natural form. the outputs
84+
are in a little-endian form. That is, each chunk of size 256 / logTwoBounds of the
85+
output can be seen as a polynomial, such that, when evaluated at 2 we get the original
86+
field element.
8187
"""
8288
nbBits = len(b)*8
8389
res = []
8490
i = 0
85-
while i < nbBits:
91+
92+
if len(b) % frByteSize != 0:
93+
exit("the length of b should divide the field size")
94+
95+
# The number of fields that we are parsing. In case we have that
96+
# logTwoBound does not divide the number of bits to represent a
97+
# field element, we do not merge them.
98+
nbField = len(b) / 32
99+
nbBitsInField = int(frByteSize * 8)
100+
101+
for fieldID in range(nbField):
102+
fieldStart = fieldID * 256
86103
e = 0
87-
for j in range(logTwoBound):
88-
e += bitAt(i, b) << j
89-
i += 1
90-
res.append(e)
104+
for bitInField in range(nbBitsInField):
105+
j = bitInField % logTwoBound
106+
at = fieldStart + nbBitsInField - 1 - bitInField
107+
e |= bitAt(at, b) << j
108+
# Switch to a new limb
109+
if j == logTwoBound - 1 or bitInField == frByteSize * 8 - 1:
110+
res.append(e)
111+
e = 0
112+
91113
# careful Montgomery constant...
92114
return [Fr(e)*rr**-1 for e in res]
93115

ecc/bn254/fr/sis/sis_test.go

+73-2
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func TestReference(t *testing.T) {
6969
err = json.Unmarshal(data, &testCases)
7070
assert.NoError(err, "reading test cases failed")
7171

72-
for _, testCase := range testCases.Entries {
72+
for testCaseID, testCase := range testCases.Entries {
7373
// create the SIS instance
7474
sis, err := NewRSis(testCase.Params.Seed, testCase.Params.LogTwoDegree, testCase.Params.LogTwoBound, testCase.Params.MaxNbElementsToHash)
7575
assert.NoError(err)
@@ -88,7 +88,11 @@ func TestReference(t *testing.T) {
8888
assert.True(e.IsZero(), "mismatch between reference test and computed value")
8989
}
9090
} else {
91-
assert.EqualValues(testCase.Expected[i], got, "mismatch between reference test and computed value")
91+
assert.EqualValues(
92+
testCase.Expected[i], got,
93+
"mismatch between reference test and computed value (testcase %v - input n° %v)",
94+
testCaseID, i,
95+
)
9296
}
9397

9498
// ensure max nb elements to hash has no incidence on result.
@@ -155,6 +159,73 @@ func TestMulMod(t *testing.T) {
155159

156160
}
157161

162+
// Test the fact that the limb decomposition allows obtaining the original
163+
// field element by evaluating the polynomial whose the coeffiients are the
164+
// limbs.
165+
func TestLimbDecomposition(t *testing.T) {
166+
167+
// Skipping the test for 32 bits
168+
if bits.UintSize == 32 {
169+
t.Skip("skipping this test in 32bit.")
170+
}
171+
172+
sis, _ := NewRSis(0, 4, 4, 3)
173+
174+
testcases := []fr.Vector{
175+
{fr.One()},
176+
{fr.NewElement(2)},
177+
{fr.NewElement(1 << 32), fr.NewElement(2), fr.NewElement(1)},
178+
}
179+
180+
for _, testcase := range testcases {
181+
182+
// clean the sis hasher
183+
sis.bufMValues.ClearAll()
184+
for i := 0; i < len(sis.bufM); i++ {
185+
sis.bufM[i].SetZero()
186+
}
187+
for i := 0; i < len(sis.bufRes); i++ {
188+
sis.bufRes[i].SetZero()
189+
}
190+
191+
buf := bytes.Buffer{}
192+
for _, x := range testcase {
193+
xBytes := x.Bytes()
194+
buf.Write(xBytes[:])
195+
}
196+
limbDecomposeBytes(buf.Bytes(), sis.bufM, sis.LogTwoBound, sis.Degree, sis.bufMValues)
197+
198+
// Just to test, this does not return panic
199+
dummyBuffer := make(fr.Vector, 192)
200+
LimbDecomposeBytes(buf.Bytes(), dummyBuffer, sis.LogTwoBound)
201+
202+
// b is a field element representing the max norm bound
203+
// used for limb splitting the input field elements.
204+
b := fr.NewElement(1 << sis.LogTwoBound)
205+
numLimbsPerField := fr.Bytes * 8 / sis.LogTwoBound
206+
207+
// Compute r (corresponds to the Montgommery constant)
208+
var r fr.Element
209+
r.SetString("6350874878119819312338956282401532410528162663560392320966563075034087161851")
210+
211+
// Attempt to recompose the entry #i in the test-case
212+
for i := range testcase {
213+
// allegedly corresponds to the limbs of the entry i
214+
subRes := sis.bufM[i*numLimbsPerField : (i+1)*numLimbsPerField]
215+
216+
// performs a Horner evaluation of subres by b
217+
var y fr.Element
218+
for j := numLimbsPerField - 1; j >= 0; j-- {
219+
y.Mul(&y, &b)
220+
y.Add(&y, &subRes[j])
221+
}
222+
223+
y.Mul(&y, &r)
224+
require.Equal(t, testcase[i].String(), y.String(), "the subRes was %v", subRes)
225+
}
226+
}
227+
}
228+
158229
func makeKeyDeterminitic(t *testing.T, sis *RSis, _seed int64) {
159230
t.Helper()
160231
// generate the key deterministically, the same way

0 commit comments

Comments
 (0)