forked from protolambda/eth2-shuffle
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathshuffle.go
272 lines (248 loc) · 10.8 KB
/
shuffle.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
package eth2_shuffle
import "encoding/binary"
type HashFn func(input []byte) []byte
const hSeedSize = int8(32)
const hRoundSize = int8(1)
const hPositionWindowSize = int8(4)
const hPivotViewSize = hSeedSize + hRoundSize
const hTotalSize = hSeedSize + hRoundSize + hPositionWindowSize
// To make it completely clear:
// Memory layout hash input:
// | 32 bytes for seed ... || 1 byte, round || 4 bytes for position window ...|
// | <---------------------- for pivot hash ----- ... --------> || ignored for pivot hash |
// | <---------------------------------------- for source hash ---------------------------------------------> |
/*
Return `p(index)` in a pseudorandom permutation `p` of `0...list_size-1` with ``seed`` as entropy.
Utilizes 'swap or not' shuffling found in
https://link.springer.com/content/pdf/10.1007%2F978-3-642-32009-5_1.pdf
See the 'generalized domain' algorithm on page 3.
Eth 2.0 spec implementation here:
https://github.com/ethereum/eth2.0-specs/blob/dev/specs/core/0_beacon-chain.md#get_permuted_index
*/
// Permute index, i.e. shuffle an individual list item without allocating a complete list.
// Returns the index in the would-be shuffled list.
func PermuteIndex(hashFn HashFn, rounds uint8, index uint64, listSize uint64, seed [32]byte) uint64 {
return innerPermuteIndex(hashFn, rounds, index, listSize, seed, true)
}
// Inverse of PermuteIndex, returns original index when given the same shuffling context parameters and permuted index.
func UnpermuteIndex(hashFn HashFn, rounds uint8, index uint64, listSize uint64, seed [32]byte) uint64 {
return innerPermuteIndex(hashFn, rounds, index, listSize, seed, false)
}
func innerPermuteIndex(hashFn HashFn, rounds uint8, index uint64, listSize uint64, seed [32]byte, dir bool) uint64 {
if rounds == 0 {
return index
}
buf := make([]byte, hTotalSize, hTotalSize)
r := uint8(0)
if !dir {
// Start at last round.
// Iterating through the rounds in reverse, un-swaps everything, effectively un-shuffling the list.
r = rounds - 1
}
// Seed is always the first 32 bytes of the hash input, we never have to change this part of the buffer.
copy(buf[:hSeedSize], seed[:])
for {
// spec: pivot = bytes_to_int(hash(seed + int_to_bytes1(round))[0:8]) % list_size
// This is the "int_to_bytes1(round)", appended to the seed.
buf[hSeedSize] = r
// Seed is already in place, now just hash the correct part of the buffer, and take a uint64 from it,
// and modulo it to get a pivot within range.
pivot := binary.LittleEndian.Uint64(hashFn(buf[:hPivotViewSize])[:8]) % listSize
// spec: flip = (pivot - index) % list_size
// Add extra list_size to prevent underflows.
// "flip" will be the other side of the pair
flip := (pivot + (listSize - index)) % listSize
// spec: position = max(index, flip)
// Why? Don't do double work: we consider every pair only once.
// (Otherwise we would swap it back in place)
// Pick the highest index of the pair as position to retrieve randomness with.
position := index
if flip > position {
position = flip
}
// spec: source = hash(seed + int_to_bytes1(round) + int_to_bytes4(position // 256))
// - seed is still in 0:32 (excl., 32 bytes)
// - round number is still in 32
// - mix in the position for randomness, except the last byte of it,
// which will be used later to select a bit from the resulting hash.
binary.LittleEndian.PutUint32(buf[hPivotViewSize:], uint32(position>>8))
source := hashFn(buf)
// spec: byte = source[(position % 256) // 8]
// Effectively keep the first 5 bits of the byte value of the position,
// and use it to retrieve one of the 32 (= 2^5) bytes of the hash.
byteV := source[(position&0xff)>>3]
// Using the last 3 bits of the position-byte, determine which bit to get from the hash-byte (8 bits, = 2^3)
// spec: bit = (byte >> (position % 8)) % 2
bitV := (byteV >> (position & 0x7)) & 0x1
// Now that we have our "coin-flip", swap index, or don't.
// If bitV, flip.
if bitV == 1 {
index = flip
}
// go forwards?
if dir {
// -> shuffle
r++
if r == rounds {
break
}
} else {
if r == 0 {
break
}
// -> un-shuffle
r--
}
}
return index
}
/*
def shuffle(list_size, seed):
indices = list(range(list_size))
for round in range(90):
hash_bytes = b''.join([
hash(seed + round.to_bytes(1, 'little') + (i).to_bytes(4, 'little'))
for i in range((list_size + 255) // 256)
])
pivot = int.from_bytes(hash(seed + round.to_bytes(1, 'little')), 'little') % list_size
powers_of_two = [1, 2, 4, 8, 16, 32, 64, 128]
for i, index in enumerate(indices):
flip = (pivot - index) % list_size
hash_pos = index if index > flip else flip
byte = hash_bytes[hash_pos // 8]
if byte & powers_of_two[hash_pos % 8]:
indices[i] = flip
return indices
Heavily-optimized version of the set-shuffling algorithm proposed by Vitalik to shuffle all items in a list together.
Original here:
https://github.com/ethereum/eth2.0-specs/pull/576#issue-250741806
Main differences, implemented by @protolambda:
- User can supply input slice to shuffle, simple provide [0,1,2,3,4, ...] to get a list of cleanly shuffled indices.
- Input slice is shuffled (hence no return value), no new array is allocated
- Allocations as minimal as possible: only a very minimal buffer for hashing
(this should be allocated on the stack, compiler will find it with escape analysis).
This is not bigger than what's used for shuffling a single index!
As opposed to larger allocations (size O(n) instead of O(1)) made in the original.
- Replaced pseudocode/python workarounds with bit-logic.
- User can provide their own hash-function (as long as it outputs a 32 len byte slice)
*/
// Shuffles the list
func ShuffleList(hashFn HashFn, input []uint64, rounds uint8, seed [32]byte) {
innerShuffleList(hashFn, input, rounds, seed, true)
}
// Un-shuffles the list
func UnshuffleList(hashFn HashFn, input []uint64, rounds uint8, seed [32]byte) {
innerShuffleList(hashFn, input, rounds, seed, false)
}
// Shuffles or unshuffles, depending on the `dir` (true for shuffling, false for unshuffling
func innerShuffleList(hashFn HashFn, input []uint64, rounds uint8, seed [32]byte, dir bool) {
if len(input) <= 1 {
// nothing to (un)shuffle
return
}
if rounds == 0 {
return
}
listSize := uint64(len(input))
buf := make([]byte, hTotalSize, hTotalSize)
r := uint8(0)
if !dir {
// Start at last round.
// Iterating through the rounds in reverse, un-swaps everything, effectively un-shuffling the list.
r = rounds - 1
}
// Seed is always the first 32 bytes of the hash input, we never have to change this part of the buffer.
copy(buf[:hSeedSize], seed[:])
for {
// spec: pivot = bytes_to_int(hash(seed + int_to_bytes1(round))[0:8]) % list_size
// This is the "int_to_bytes1(round)", appended to the seed.
buf[hSeedSize] = r
// Seed is already in place, now just hash the correct part of the buffer, and take a uint64 from it,
// and modulo it to get a pivot within range.
pivot := binary.LittleEndian.Uint64(hashFn(buf[:hPivotViewSize])[:8]) % listSize
// Split up the for-loop in two:
// 1. Handle the part from 0 (incl) to pivot (incl). This is mirrored around (pivot / 2)
// 2. Handle the part from pivot (excl) to N (excl). This is mirrored around ((pivot / 2) + (size/2))
// The pivot defines a split in the array, with each of the splits mirroring their data within the split.
// Print out some example even/odd sized index lists, with some even/odd pivots,
// and you can deduce how the mirroring works exactly.
// Note that the mirror is strict enough to not consider swapping the index @mirror with itself.
mirror := (pivot + 1) >> 1
// Since we are iterating through the "positions" in order, we can just repeat the hash every 256th position.
// No need to pre-compute every possible hash for efficiency like in the example code.
// We only need it consecutively (we are going through each in reverse order however, but same thing)
//
// spec: source = hash(seed + int_to_bytes1(round) + int_to_bytes4(position // 256))
// - seed is still in 0:32 (excl., 32 bytes)
// - round number is still in 32
// - mix in the position for randomness, except the last byte of it,
// which will be used later to select a bit from the resulting hash.
// We start from the pivot position, and work back to the mirror position (of the part left to the pivot).
// This makes us process each pear exactly once (instead of unnecessarily twice, like in the spec)
binary.LittleEndian.PutUint32(buf[hPivotViewSize:], uint32(pivot>>8))
source := hashFn(buf)
byteV := source[(pivot&0xff)>>3]
for i, j := uint64(0), pivot; i < mirror; i, j = i+1, j-1 {
// The pair is i,j. With j being the bigger of the two, hence the "position" identifier of the pair.
// Every 256th bit (aligned to j).
if j&0xff == 0xff {
// just overwrite the last part of the buffer, reuse the start (seed, round)
binary.LittleEndian.PutUint32(buf[hPivotViewSize:], uint32(j>>8))
source = hashFn(buf)
}
// Same trick with byte retrieval. Only every 8th.
if j&0x7 == 0x7 {
byteV = source[(j&0xff)>>3]
}
bitV := (byteV >> (j & 0x7)) & 0x1
if bitV == 1 {
// swap the pair items
input[i], input[j] = input[j], input[i]
}
}
// Now repeat, but for the part after the pivot.
mirror = (pivot + listSize + 1) >> 1
end := listSize - 1
// Again, seed and round input is in place, just update the position.
// We start at the end, and work back to the mirror point.
// This makes us process each pear exactly once (instead of unnecessarily twice, like in the spec)
binary.LittleEndian.PutUint32(buf[hPivotViewSize:], uint32(end>>8))
source = hashFn(buf)
byteV = source[(end&0xff)>>3]
for i, j := pivot+1, end; i < mirror; i, j = i+1, j-1 {
// Exact same thing (copy of above loop body)
//--------------------------------------------
// The pair is i,j. With j being the bigger of the two, hence the "position" identifier of the pair.
// Every 256th bit (aligned to j).
if j&0xff == 0xff {
// just overwrite the last part of the buffer, reuse the start (seed, round)
binary.LittleEndian.PutUint32(buf[hPivotViewSize:], uint32(j>>8))
source = hashFn(buf)
}
// Same trick with byte retrieval. Only every 8th.
if j&0x7 == 0x7 {
byteV = source[(j&0xff)>>3]
}
bitV := (byteV >> (j & 0x7)) & 0x1
if bitV == 1 {
// swap the pair items
input[i], input[j] = input[j], input[i]
}
//--------------------------------------------
}
// go forwards?
if dir {
// -> shuffle
r++
if r == rounds {
break
}
} else {
if r == 0 {
break
}
// -> un-shuffle
r--
}
}
}