Skip to content

Commit

Permalink
statetrie: nibbles (algorand#5759)
Browse files Browse the repository at this point in the history
  • Loading branch information
bbroder-uji authored Nov 16, 2023
1 parent 7040500 commit ff0ee44
Show file tree
Hide file tree
Showing 2 changed files with 379 additions and 0 deletions.
161 changes: 161 additions & 0 deletions crypto/statetrie/nibbles/nibbles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright (C) 2019-2023 Algorand, Inc.
// This file is part of go-algorand
//
// go-algorand is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// go-algorand is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with go-algorand. If not, see <https://www.gnu.org/licenses/>.

package nibbles

import (
"bytes"
"errors"
)

// Nibbles are 4-bit values stored in an 8-bit byte arrays
type Nibbles []byte

const (
// oddIndicator for serialization when the last nibble in a byte array
// is not part of the nibble array.
oddIndicator = 0x01
// evenIndicator for when it is.
evenIndicator = 0x03
)

// Pack the nibble array into a byte array.
// Return the byte array and a bool indicating if the last byte is a full byte or
// only the high 4 bits are part of the encoding
// the last four bits of a oddLength byte encoding will always be zero.
// Allocates a new byte slice.
//
// [0x1, 0x2, 0x3] -> [0x12, 0x30], true
// [0x1, 0x2, 0x3, 0x4] -> [0x12, 0x34], false
// [0x1] -> [0x10], true
// [] -> [], false
func Pack(nyb Nibbles) ([]byte, bool) {
length := len(nyb)
data := make([]byte, length/2+length%2, length/2+length%2+1)
for i := 0; i < length; i++ {
if i%2 == 0 {
data[i/2] = nyb[i] << 4
} else {
data[i/2] = data[i/2] | nyb[i]
}
}

return data, length%2 != 0
}

// Equal returns true if the two nibble arrays are equal
// [0x1, 0x2, 0x3], [0x1, 0x2, 0x3] -> true
// [0x1, 0x2, 0x3], [0x1, 0x2, 0x4] -> false
// [0x1, 0x2, 0x3], [0x1] -> false
// [0x1, 0x2, 0x3], [0x1, 0x2, 0x3, 0x4] -> false
// [], [] -> true
// [], [0x1] -> false
func Equal(nyb1 Nibbles, nyb2 Nibbles) bool {
return bytes.Equal(nyb1, nyb2)
}

// ShiftLeft returns a slice of nyb1 that contains the Nibbles after the first
// numNibbles
func ShiftLeft(nyb1 Nibbles, numNibbles int) Nibbles {
if numNibbles <= 0 {
return nyb1
}
if numNibbles > len(nyb1) {
return nyb1[:0]
}

return nyb1[numNibbles:]
}

// SharedPrefix returns a slice from nyb1 that contains the shared prefix
// between nyb1 and nyb2
func SharedPrefix(nyb1 Nibbles, nyb2 Nibbles) Nibbles {
minLength := len(nyb1)
if len(nyb2) < minLength {
minLength = len(nyb2)
}
for i := 0; i < minLength; i++ {
if nyb1[i] != nyb2[i] {
return nyb1[:i]
}
}
return nyb1[:minLength]
}

// Serialize returns a byte array that represents the Nibbles
// an empty nibble array is serialized as a single byte with value 0x3
// as the empty nibble is considered to be full width
//
// [0x1, 0x2, 0x3] -> [0x12, 0x30, 0x01]
// [0x1, 0x2, 0x3, 0x4] -> [0x12, 0x34, 0x03]
// [] -> [0x03]
func Serialize(nyb Nibbles) (data []byte) {
p, h := Pack(nyb)
if h {
// 0x01 is the odd length indicator
return append(p, oddIndicator)
}
// 0x03 is the even length indicator
return append(p, evenIndicator)
}

// Deserialize returns a nibble array from the byte array.
func Deserialize(encoding []byte) (Nibbles, error) {
var ns Nibbles
length := len(encoding)
if length == 0 {
return nil, errors.New("invalid encoding")
}
if encoding[length-1] == oddIndicator {
if length == 1 {
return nil, errors.New("invalid encoding")
}
ns = makeNibbles(encoding[:length-1], true)
} else if encoding[length-1] == evenIndicator {
ns = makeNibbles(encoding[:length-1], false)
} else {
return nil, errors.New("invalid encoding")
}
return ns, nil
}

// makeNibbles returns a nibble array from the byte array. If oddLength is true,
// the last 4 bits of the last byte of the array are ignored.
//
// [0x12, 0x30], true -> [0x1, 0x2, 0x3]
// [0x12, 0x34], false -> [0x1, 0x2, 0x3, 0x4]
// [0x12, 0x34], true -> [0x1, 0x2, 0x3] <-- last byte last 4 bits ignored
// [], false -> []
// never to be called with [], true
// Allocates a new byte slice.
func makeNibbles(data []byte, oddLength bool) Nibbles {
length := len(data) * 2
if oddLength {
length = length - 1
}
ns := make([]byte, length)

j := 0
for i := 0; i < length; i++ {
if i%2 == 0 {
ns[i] = data[j] >> 4
} else {
ns[i] = data[j] & 0x0f
j++
}
}
return ns
}
218 changes: 218 additions & 0 deletions crypto/statetrie/nibbles/nibbles_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
// Copyright (C) 2019-2023 Algorand, Inc.
// This file is part of go-algorand
//
// go-algorand is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// go-algorand is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with go-algorand. If not, see <https://www.gnu.org/licenses/>.

package nibbles

import (
"bytes"
"fmt"
"math/rand"
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/algorand/go-algorand/test/partitiontest"
)

func TestNibblesRandom(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()

seed := time.Now().UnixNano()
localRand := rand.New(rand.NewSource(seed))
defer func() {
if t.Failed() {
t.Logf("The seed was %d", seed)
}
}()

for i := 0; i < 1_000; i++ {
length := localRand.Intn(8192) + 1
data := make([]byte, length)
localRand.Read(data)
half := localRand.Intn(2) == 0 // half of the time, we have an odd number of nibbles
if half && localRand.Intn(2) == 0 {
data[len(data)-1] &= 0xf0 // sometimes clear the last nibble, sometimes do not
}
nibbles := makeNibbles(data, half)

data2 := Serialize(nibbles)
nibbles2, err := Deserialize(data2)
require.NoError(t, err)
require.Equal(t, nibbles, nibbles2)

if half {
data[len(data)-1] &= 0xf0 // clear last nibble
}
packed, odd := Pack(nibbles)
require.Equal(t, odd, half)
require.Equal(t, packed, data)
unpacked := makeNibbles(packed, odd)
require.Equal(t, nibbles, unpacked)

packed, odd = Pack(nibbles2)
require.Equal(t, odd, half)
require.Equal(t, packed, data)
unpacked = makeNibbles(packed, odd)
require.Equal(t, nibbles2, unpacked)
}
}

func TestNibblesDeserialize(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()
enc := []byte{0x01}
_, err := Deserialize(enc)
require.Error(t, err, "should return invalid encoding error")
}

func TestNibbles(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()

sampleNibbles := []Nibbles{
{0x0, 0x1, 0x2, 0x3, 0x4},
{0x4, 0x1, 0x2, 0x3, 0x4},
{0x0, 0x0, 0x2, 0x3, 0x5},
{0x0, 0x1, 0x2, 0x3, 0x4, 0x5},
{},
{0x1},
}

sampleNibblesPacked := [][]byte{
{0x01, 0x23, 0x40},
{0x41, 0x23, 0x40},
{0x00, 0x23, 0x50},
{0x01, 0x23, 0x45},
{},
{0x10},
}

sampleNibblesShifted1 := []Nibbles{
{0x1, 0x2, 0x3, 0x4},
{0x1, 0x2, 0x3, 0x4},
{0x0, 0x2, 0x3, 0x5},
{0x1, 0x2, 0x3, 0x4, 0x5},
{},
{},
}

sampleNibblesShifted2 := []Nibbles{
{0x2, 0x3, 0x4},
{0x2, 0x3, 0x4},
{0x2, 0x3, 0x5},
{0x2, 0x3, 0x4, 0x5},
{},
{},
}

for i, n := range sampleNibbles {
b, oddLength := Pack(n)
if oddLength {
// require that oddLength packs returns a byte slice with the last nibble set to 0x0
require.Equal(t, b[len(b)-1]&0x0f == 0x00, true)
}

require.Equal(t, oddLength == (len(n)%2 == 1), true)
require.Equal(t, bytes.Equal(b, sampleNibblesPacked[i]), true)

unp := makeNibbles(b, oddLength)
require.Equal(t, bytes.Equal(unp, n), true)

}
for i, n := range sampleNibbles {
require.Equal(t, bytes.Equal(ShiftLeft(n, -2), sampleNibbles[i]), true)
require.Equal(t, bytes.Equal(ShiftLeft(n, -1), sampleNibbles[i]), true)
require.Equal(t, bytes.Equal(ShiftLeft(n, 0), sampleNibbles[i]), true)
require.Equal(t, bytes.Equal(ShiftLeft(n, 1), sampleNibblesShifted1[i]), true)
require.Equal(t, bytes.Equal(ShiftLeft(n, 2), sampleNibblesShifted2[i]), true)
}

sampleSharedNibbles := [][]Nibbles{
{{0x0, 0x1, 0x2, 0x9, 0x2}, {0x0, 0x1, 0x2}},
{{0x4, 0x1}, {0x4, 0x1}},
{{0x9, 0x2, 0x3}, {}},
{{0x0}, {0x0}},
{{}, {}},
}
for i, n := range sampleSharedNibbles {
shared := SharedPrefix(n[0], sampleNibbles[i])
require.Equal(t, bytes.Equal(shared, n[1]), true)
shared = SharedPrefix(sampleNibbles[i], n[0])
require.Equal(t, bytes.Equal(shared, n[1]), true)
}

sampleSerialization := []Nibbles{
{0x0, 0x1, 0x2, 0x9, 0x2},
{0x4, 0x1},
{0x4, 0x1, 0x4, 0xf},
{0x4, 0x1, 0x4, 0xf, 0x0},
{0x9, 0x2, 0x3},
{},
{0x05},
{},
}

for _, n := range sampleSerialization {
nbytes := Serialize(n)
n2, err := Deserialize(nbytes)
require.NoError(t, err)
require.True(t, bytes.Equal(n, n2))
require.Equal(t, len(nbytes), len(n)/2+len(n)%2+1, fmt.Sprintf("nbytes: %v, n: %v", nbytes, n))
if len(n)%2 == 0 {
require.Equal(t, nbytes[len(nbytes)-1], uint8(evenIndicator))
} else {
require.Equal(t, nbytes[len(nbytes)-1], uint8(oddIndicator))
require.Equal(t, nbytes[len(nbytes)-2]&0x0F, uint8(0))
}
}

makeNibblesTestExpected := Nibbles{0x0, 0x1, 0x2, 0x9, 0x2}
makeNibblesTestData := []byte{0x01, 0x29, 0x20}
mntr := makeNibbles(makeNibblesTestData, true)
require.Equal(t, bytes.Equal(mntr, makeNibblesTestExpected), true)
makeNibblesTestExpectedFW := Nibbles{0x0, 0x1, 0x2, 0x9, 0x2, 0x0}
mntr2 := makeNibbles(makeNibblesTestData, false)
require.Equal(t, bytes.Equal(mntr2, makeNibblesTestExpectedFW), true)

sampleEqualFalse := [][]Nibbles{
{{0x0, 0x1, 0x2, 0x9, 0x2}, {0x0, 0x1, 0x2, 0x9}},
{{0x0, 0x1, 0x2, 0x9}, {0x0, 0x1, 0x2, 0x9, 0x2}},
{{0x0, 0x1, 0x2, 0x9, 0x2}, {}},
{{}, {0x0, 0x1, 0x2, 0x9, 0x2}},
{{0x0}, {}},
{{}, {0x0}},
{{}, {0x1}},
}
for _, n := range sampleEqualFalse {
ds := Serialize(n[0])
us, e := Deserialize(ds)
require.NoError(t, e)
require.Equal(t, Equal(n[0], us), true)
require.Equal(t, Equal(n[0], n[0]), true)
require.Equal(t, Equal(us, n[0]), true)
require.Equal(t, Equal(n[0], n[1]), false)
require.Equal(t, Equal(us, n[1]), false)
require.Equal(t, Equal(n[1], n[0]), false)
require.Equal(t, Equal(n[1], us), false)
}

_, e := Deserialize([]byte{})
require.Error(t, e)
_, e = Deserialize([]byte{0x02})
require.Error(t, e)
}

0 comments on commit ff0ee44

Please sign in to comment.