Skip to content

Commit 5573c9c

Browse files
[Delegations prereq 2] Add hash bins helpers (#192)
* [Delegations prereq] Add hash bins helpers Splitting up #175 * Respond to comments * Comments and iterator refactor * Use indexes instead of iterator * Fix typo
1 parent 470b5ab commit 5573c9c

File tree

2 files changed

+232
-0
lines changed

2 files changed

+232
-0
lines changed

internal/targets/hash_bins.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package targets
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"strings"
7+
)
8+
9+
const MinDelegationHashPrefixBitLen = 1
10+
const MaxDelegationHashPrefixBitLen = 32
11+
12+
// hexEncode formats x as a hex string. The hex string is left padded with
13+
// zeros to padWidth, if necessary.
14+
func hexEncode(x uint64, padWidth int) string {
15+
// Benchmarked to be more than 10x faster than padding with Sprintf.
16+
s := strconv.FormatUint(x, 16)
17+
if len(s) >= padWidth {
18+
return s
19+
}
20+
return strings.Repeat("0", padWidth-len(s)) + s
21+
}
22+
23+
const bitsPerHexDigit = 4
24+
25+
// numHexDigits returns the number of hex digits required to encode the given
26+
// number of bits.
27+
func numHexDigits(numBits int) int {
28+
// ceil(numBits / bitsPerHexDigit)
29+
return ((numBits - 1) / bitsPerHexDigit) + 1
30+
}
31+
32+
// HashBins represents an ordered list of hash bin target roles, which together
33+
// partition the space of target path hashes equal-sized buckets based on path
34+
// has prefix.
35+
type HashBins struct {
36+
rolePrefix string
37+
bitLen int
38+
hexDigitLen int
39+
40+
numBins uint64
41+
numPrefixesPerBin uint64
42+
}
43+
44+
// NewHashBins creates a HashBins partitioning with 2^bitLen buckets.
45+
func NewHashBins(rolePrefix string, bitLen int) (*HashBins, error) {
46+
if bitLen < MinDelegationHashPrefixBitLen || bitLen > MaxDelegationHashPrefixBitLen {
47+
return nil, fmt.Errorf("bitLen is out of bounds, should be between %v and %v inclusive", MinDelegationHashPrefixBitLen, MaxDelegationHashPrefixBitLen)
48+
}
49+
50+
hexDigitLen := numHexDigits(bitLen)
51+
numBins := uint64(1) << bitLen
52+
53+
numPrefixesTotal := uint64(1) << (bitsPerHexDigit * hexDigitLen)
54+
numPrefixesPerBin := numPrefixesTotal / numBins
55+
56+
return &HashBins{
57+
rolePrefix: rolePrefix,
58+
bitLen: bitLen,
59+
hexDigitLen: hexDigitLen,
60+
numBins: numBins,
61+
numPrefixesPerBin: numPrefixesPerBin,
62+
}, nil
63+
}
64+
65+
// NumBins returns the number of hash bin partitions.
66+
func (hb *HashBins) NumBins() uint64 {
67+
return hb.numBins
68+
}
69+
70+
// GetBin returns the HashBin at index i, or nil if i is out of bounds.
71+
func (hb *HashBins) GetBin(i uint64) *HashBin {
72+
if i >= hb.numBins {
73+
return nil
74+
}
75+
76+
return &HashBin{
77+
rolePrefix: hb.rolePrefix,
78+
hexDigitLen: hb.hexDigitLen,
79+
first: i * hb.numPrefixesPerBin,
80+
last: ((i + 1) * hb.numPrefixesPerBin) - 1,
81+
}
82+
}
83+
84+
// HashBin represents a hex prefix range. First should be less than Last.
85+
type HashBin struct {
86+
rolePrefix string
87+
hexDigitLen int
88+
first uint64
89+
last uint64
90+
}
91+
92+
// RoleName returns the name of the role that signs for the HashBin.
93+
func (b *HashBin) RoleName() string {
94+
if b.first == b.last {
95+
return b.rolePrefix + hexEncode(b.first, b.hexDigitLen)
96+
}
97+
98+
return b.rolePrefix + hexEncode(b.first, b.hexDigitLen) + "-" + hexEncode(b.last, b.hexDigitLen)
99+
}
100+
101+
// HashPrefixes returns a slice of all hash prefixes in the bin.
102+
func (b *HashBin) HashPrefixes() []string {
103+
n := int(b.last - b.first + 1)
104+
ret := make([]string, int(n))
105+
106+
x := b.first
107+
for i := 0; i < n; i++ {
108+
ret[i] = hexEncode(x, b.hexDigitLen)
109+
x++
110+
}
111+
112+
return ret
113+
}

internal/targets/hash_bins_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package targets
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func BenchmarkHexEncode1(b *testing.B) {
10+
for n := 0; n <= b.N; n++ {
11+
for x := uint64(0); x <= 0xf; x += 1 {
12+
hexEncode(x, 1)
13+
}
14+
}
15+
}
16+
17+
func BenchmarkHexEncode4(b *testing.B) {
18+
for n := 0; n <= b.N; n++ {
19+
for x := uint64(0); x <= 0xffff; x += 1 {
20+
hexEncode(x, 4)
21+
}
22+
}
23+
}
24+
25+
func TestHashBin(t *testing.T) {
26+
tcs := []struct {
27+
hb *HashBin
28+
roleName string
29+
hashPrefixes []string
30+
}{
31+
{
32+
hb: &HashBin{
33+
rolePrefix: "abc_",
34+
hexDigitLen: 1,
35+
first: 0x0,
36+
last: 0x7,
37+
},
38+
roleName: "abc_0-7",
39+
hashPrefixes: []string{
40+
"0", "1", "2", "3", "4", "5", "6", "7",
41+
},
42+
},
43+
{
44+
hb: &HashBin{
45+
rolePrefix: "abc_",
46+
hexDigitLen: 2,
47+
first: 0x0,
48+
last: 0xf,
49+
},
50+
roleName: "abc_00-0f",
51+
hashPrefixes: []string{
52+
"00", "01", "02", "03", "04", "05", "06", "07",
53+
"08", "09", "0a", "0b", "0c", "0d", "0e", "0f",
54+
},
55+
},
56+
{
57+
hb: &HashBin{
58+
rolePrefix: "cba_",
59+
hexDigitLen: 4,
60+
first: 0xcd,
61+
last: 0xcf,
62+
},
63+
roleName: "cba_00cd-00cf",
64+
hashPrefixes: []string{"00cd", "00ce", "00cf"},
65+
},
66+
{
67+
hb: &HashBin{
68+
rolePrefix: "cba_",
69+
hexDigitLen: 3,
70+
first: 0xc1,
71+
last: 0xc1,
72+
},
73+
roleName: "cba_0c1",
74+
hashPrefixes: []string{"0c1"},
75+
},
76+
}
77+
78+
for i, tc := range tcs {
79+
assert.Equalf(t, tc.roleName, tc.hb.RoleName(), "test case %v: RoleName()", i)
80+
assert.Equalf(t, tc.hashPrefixes, tc.hb.HashPrefixes(), "test case %v: HashPrefixes()", i)
81+
}
82+
}
83+
84+
func TestHashBins(t *testing.T) {
85+
tcs := []struct {
86+
bitLen int
87+
roleNames []string
88+
}{
89+
{1, []string{"0-7", "8-f"}},
90+
{2, []string{"0-3", "4-7", "8-b", "c-f"}},
91+
{3, []string{"0-1", "2-3", "4-5", "6-7", "8-9", "a-b", "c-d", "e-f"}},
92+
{4, []string{
93+
"0", "1", "2", "3", "4", "5", "6", "7",
94+
"8", "9", "a", "b", "c", "d", "e", "f",
95+
}},
96+
{5, []string{
97+
"00-07", "08-0f", "10-17", "18-1f", "20-27", "28-2f", "30-37", "38-3f",
98+
"40-47", "48-4f", "50-57", "58-5f", "60-67", "68-6f", "70-77", "78-7f",
99+
"80-87", "88-8f", "90-97", "98-9f", "a0-a7", "a8-af", "b0-b7", "b8-bf",
100+
"c0-c7", "c8-cf", "d0-d7", "d8-df", "e0-e7", "e8-ef", "f0-f7", "f8-ff",
101+
}},
102+
}
103+
for i, tc := range tcs {
104+
got := []string{}
105+
hbs, err := NewHashBins("", tc.bitLen)
106+
assert.NoError(t, err)
107+
n := hbs.NumBins()
108+
for i := uint64(0); i < n; i += 1 {
109+
hb := hbs.GetBin(i)
110+
got = append(got, hb.RoleName())
111+
}
112+
assert.Equalf(t, tc.roleNames, got, "test case %v", i)
113+
}
114+
115+
_, err := NewHashBins("", 0)
116+
assert.Error(t, err)
117+
_, err = NewHashBins("", 33)
118+
assert.Error(t, err)
119+
}

0 commit comments

Comments
 (0)