Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update the limb decomposition of the SIS #389

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
137 changes: 92 additions & 45 deletions ecc/bn254/fr/sis/sis.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,16 @@ func NewRSis(seed int64, logTwoDegree, logTwoBound, maxNbElementsToHash int) (*R
// capacity == [degree * n * logTwoBound] / 8
// n == (capacity*8)/(degree*logTwoBound)

n := capacity * 8 / logTwoBound // number of coefficients
// First n <- #limbs to represent a single field element
n := (fr.Bytes * 8) / logTwoBound
if n*logTwoBound < fr.Bytes*8 {
n++
}

// Then multiply by the number of field elements
n *= maxNbElementsToHash

// And divide (+ ceil) to get the number of polynomials
if n%degree == 0 {
n /= degree
} else {
Expand Down Expand Up @@ -160,53 +169,11 @@ func (r *RSis) Sum(b []byte) []byte {
}

// clear the buffers of the instance.
defer func() {
r.bufMValues.ClearAll()
for i := 0; i < len(r.bufM); i++ {
r.bufM[i].SetZero()
}
for i := 0; i < len(r.bufRes); i++ {
r.bufRes[i].SetZero()
}
}()
defer r.cleanupBuffers()

// bitwise decomposition of the buffer, in order to build m (the vector to hash)
// as a list of polynomials, whose coefficients are less than r.B bits long.
// Say buf=[0xbe,0x0f]. As a stream of bits it is interpreted like this:
// 10111110 00001111. BitAt(0)=1 (=leftmost bit), bitAt(1)=0 (=second leftmost bit), etc.
nbBits := len(buf) * 8
bitAt := func(i int) uint8 {
k := i / 8
if k >= len(buf) {
return 0
}
b := buf[k]
j := i % 8
return b >> (7 - j) & 1
}

// now we can construct m. The input to hash consists of the polynomials
// m[k*r.Degree:(k+1)*r.Degree]
m := r.bufM

// mark blocks m[i*r.Degree : (i+1)*r.Degree] != [0...0]
mValues := r.bufMValues

// we process the input buffer by blocks of r.LogTwoBound bits
// each of these block (<< 64bits) are interpreted as a coefficient
mPos := 0
for i := 0; i < nbBits; mPos++ {
for j := 0; j < r.LogTwoBound; j++ {
// r.LogTwoBound < 64; we just use the first word of our element here,
// and set the bits from LSB to MSB.
m[mPos][0] |= uint64(bitAt(i) << j)
i++
}
if m[mPos][0] == 0 {
continue
}
mValues.Set(uint(mPos / r.Degree))
}
limbDecomposeBytes(buf, m, r.LogTwoBound, r.Degree, mValues)

// we can hash now.
res := r.bufRes
Expand Down Expand Up @@ -322,3 +289,83 @@ func (r *RSis) CopyWithFreshBuffer() RSis {
res.buffer = bytes.Buffer{}
return res
}

// Cleanup the buffers of the RSis instance
func (r *RSis) cleanupBuffers() {
r.bufMValues.ClearAll()
for i := 0; i < len(r.bufM); i++ {
r.bufM[i].SetZero()
}
for i := 0; i < len(r.bufRes); i++ {
r.bufRes[i].SetZero()
}
}

// Split an slice of bytes representing an array of serialized field element in
// big-endian form into an array of limbs representing the same field elements
// in little-endian form. Namely, if our field is reprented with 64 bits and we
// have the following field element 0x0123456789abcdef (0 being the most significant
// character and and f being the least significant one) and our log norm bound is
// 16 (so 1 hex character = 1 limb). The function assigns the values of m to [f, e,
// d, c, b, a, ..., 3, 2, 1, 0]. m should be preallocated and zeroized. Additionally,
// we have the guarantee that 2 bits contributing to different field elements cannot
// be part of the same limb.
func LimbDecomposeBytes(buf []byte, m fr.Vector, logTwoBound int) {
limbDecomposeBytes(buf, m, logTwoBound, 0, nil)
}

// Split an slice of bytes representing an array of serialized field element in
// big-endian form into an array of limbs representing the same field elements
// in little-endian form. Namely, if our field is reprented with 64 bits and we
// have the following field element 0x0123456789abcdef (0 being the most significant
// character and and f being the least significant one) and our log norm bound is
// 16 (so 1 hex character = 1 limb). The function assigns the values of m to [f, e,
// d, c, b, a, ..., 3, 2, 1, 0]. m should be preallocated and zeroized. mValues is
// an optional bitSet. If provided, it must be empty. The function will set bit "i"
// to indicate the that i-th SIS input polynomial should be non-zero. Recall, that a
// SIS polynomial corresponds to a chunk of limbs of size `degree`. Additionally,
// we have the guarantee that 2 bits contributing to different field elements cannot
// be part of the same limb.
func limbDecomposeBytes(buf []byte, m fr.Vector, logTwoBound, degree int, mValues *bitset.BitSet) {

// bitwise decomposition of the buffer, in order to build m (the vector to hash)
// as a list of polynomials, whose coefficients are less than r.B bits long.
// Say buf=[0xbe,0x0f]. As a stream of bits it is interpreted like this:
// 10111110 00001111. BitAt(0)=1 (=leftmost bit), bitAt(1)=0 (=second leftmost bit), etc.
nbBits := len(buf) * 8
bitAt := func(i int) uint8 {
k := i / 8
if k >= len(buf) {
return 0
}
b := buf[k]
j := i % 8
return b >> (7 - j) & 1
}

// we process the input buffer by blocks of r.LogTwoBound bits
// each of these block (<< 64bits) are interpreted as a coefficient
mPos := 0
for fieldStart := 0; fieldStart < nbBits; {
for bitInField := 0; bitInField < fr.Bytes*8; {

j := bitInField % logTwoBound

// r.LogTwoBound < 64; we just use the first word of our element here,
// and set the bits from LSB to MSB.
at := fieldStart + fr.Bytes*8 - bitInField - 1
m[mPos][0] |= uint64(bitAt(at) << j)
bitInField++

// Check if mPos is zero and mark as non-zero in the bitset if not
if m[mPos][0] > 0 && mValues != nil {
mValues.Set(uint(mPos / degree))
}

if j == logTwoBound-1 || bitInField == fr.Bytes*8 {
mPos++
}
}
fieldStart += fr.Bytes * 8
}
}
32 changes: 27 additions & 5 deletions ecc/bn254/fr/sis/sis.sage
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import json
# BN254 Fr
r = 21888242871839275222246405745257275088548364400416034343698204186575808495617
frByteSize = 32
countToDeath = int(5)
gfr = GF(r)
Fr = GF(r)
Fr.<x> = Fr[]
Expand Down Expand Up @@ -78,16 +79,37 @@ def splitCoeffs(b, logTwoBound):

Returns:
an array of coeffs, each coeff being the i-th chunk of logTwoBounds bits of b.
The coeffs are formed as follow. The input byte string is implicitly parsed as
a slice of field elements of 32 bytes each in bigendian-natural form. the outputs
are in a little-endian form. That is, each chunk of size 256 / logTwoBounds of the
output can be seen as a polynomial, such that, when evaluated at 2 we get the original
field element.
"""
nbBits = len(b)*8
res = []
i = 0
while i < nbBits:

if len(b) % frByteSize != 0:
exit("the length of b should divide the field size")

# The number of fields that we are parsing. In case we have that
# logTwoBound does not divide the number of bits to represent a
# field element, we do not merge them.
nbField = len(b) / 32
nbBitsInField = int(frByteSize * 8)

for fieldID in range(nbField):
fieldStart = fieldID * 256
e = 0
for j in range(logTwoBound):
e += bitAt(i, b) << j
i += 1
res.append(e)
for bitInField in range(nbBitsInField):
j = bitInField % logTwoBound
at = fieldStart + nbBitsInField - 1 - bitInField
e |= bitAt(at, b) << j
# Switch to a new limb
if j == logTwoBound - 1 or bitInField == frByteSize * 8 - 1:
res.append(e)
e = 0

# careful Montgomery constant...
return [Fr(e)*rr**-1 for e in res]

Expand Down
75 changes: 73 additions & 2 deletions ecc/bn254/fr/sis/sis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func TestReference(t *testing.T) {
err = json.Unmarshal(data, &testCases)
assert.NoError(err, "reading test cases failed")

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

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

}

// Test the fact that the limb decomposition allows obtaining the original
// field element by evaluating the polynomial whose the coeffiients are the
// limbs.
func TestLimbDecomposition(t *testing.T) {

// Skipping the test for 32 bits
if bits.UintSize == 32 {
t.Skip("skipping this test in 32bit.")
}

sis, _ := NewRSis(0, 4, 4, 3)

testcases := []fr.Vector{
{fr.One()},
{fr.NewElement(2)},
{fr.NewElement(1 << 32), fr.NewElement(2), fr.NewElement(1)},
}

for _, testcase := range testcases {

// clean the sis hasher
sis.bufMValues.ClearAll()
for i := 0; i < len(sis.bufM); i++ {
sis.bufM[i].SetZero()
}
for i := 0; i < len(sis.bufRes); i++ {
sis.bufRes[i].SetZero()
}

buf := bytes.Buffer{}
for _, x := range testcase {
xBytes := x.Bytes()
buf.Write(xBytes[:])
}
limbDecomposeBytes(buf.Bytes(), sis.bufM, sis.LogTwoBound, sis.Degree, sis.bufMValues)

// Just to test, this does not return panic
dummyBuffer := make(fr.Vector, 192)
LimbDecomposeBytes(buf.Bytes(), dummyBuffer, sis.LogTwoBound)

// b is a field element representing the max norm bound
// used for limb splitting the input field elements.
b := fr.NewElement(1 << sis.LogTwoBound)
numLimbsPerField := fr.Bytes * 8 / sis.LogTwoBound

// Compute r (corresponds to the Montgommery constant)
var r fr.Element
r.SetString("6350874878119819312338956282401532410528162663560392320966563075034087161851")

// Attempt to recompose the entry #i in the test-case
for i := range testcase {
// allegedly corresponds to the limbs of the entry i
subRes := sis.bufM[i*numLimbsPerField : (i+1)*numLimbsPerField]

// performs a Horner evaluation of subres by b
var y fr.Element
for j := numLimbsPerField - 1; j >= 0; j-- {
y.Mul(&y, &b)
y.Add(&y, &subRes[j])
}

y.Mul(&y, &r)
require.Equal(t, testcase[i].String(), y.String(), "the subRes was %v", subRes)
}
}
}

func makeKeyDeterminitic(t *testing.T, sis *RSis, _seed int64) {
t.Helper()
// generate the key deterministically, the same way
Expand Down
Loading