Skip to content

Commit b48dae0

Browse files
authored
Support ARM architecture (#60)
When converting between slices of bytes and uint64, we can use unsafe casting on x86-64 architecture because the byte alignment is guaranteed. However, on ARM architecture there is a possibility this will cause issues. Testing on an ARM VM didn't cause problems but it is best to provide an option without unsafe casting. This commit adds build directives where necessary to support the two versions. Where needed, the shared functionality remains in the original filename (bit.go). Functions which rely on unsafe casting are in a file appended with `_amd64.go` (`bits_amdt64.go`). Support without unsafe casting is in a file appended with `_gen.go` (`bits_gen.go`). The version without unsafe casting is not as performant. Use `-tags=generic` if running on AMD64 and you want to test the performance of the functions that do not rely on unsafe casting.
1 parent 3521638 commit b48dae0

12 files changed

Lines changed: 419 additions & 193 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![Go Report Card](https://goreportcard.com/badge/github.com/optable/match)](https://goreportcard.com/report/github.com/optable/match)
44
[![GoDoc](https://godoc.org/github.com/optable/match?status.svg)](https://godoc.org/github.com/optable/match)
55

6-
An open-source set intersection protocols library written in golang. Currently only compatible with **x86-64**.
6+
An open-source set intersection protocols library written in golang.
77

88
The goal of the match library is to provide production level implementations of various set intersection protocols. Protocols will typically tradeoff security for performance. For example, a private set intersection (PSI) protocol provides cryptographic guarantees to participants concerning their private and non-intersecting data records, and is suitable for scenarios where participants trust each other to be honest in adhering to the protocol, but still want to protect their private data while performing the intersection operation.
99

benchmark/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Benchmarks
22

3-
The following scatter plot shows the results of benchmarking match attempts using different PSI algorithms on Google Cloud n2-standard-64 [general-purpose virtual machines (VMs)](https://cloud.google.com/compute/docs/general-purpose-machines#n2_machines). For each benchmark, the sender and the receiver use the same type of VM. The plot shows runtime for various PSI algorithms when the sender and receiver have an equal number of records. The BPSI used for these experiments has a false positive rate fixed at 1e-6. All the match attempts performed have an intersection size of 50m (million). [Detailed benchmarks of the KKRT protocol can be found here](KKRT.md).
3+
The following scatter plot shows the results of benchmarking match attempts using different PSI algorithms on Google Cloud n2-standard-64 [general-purpose virtual machines (VMs)](https://cloud.google.com/compute/docs/general-purpose-machines#n2_machines) (x84-64 architecture). For each benchmark, the sender and the receiver use the same type of VM. The plot shows runtime for various PSI algorithms when the sender and receiver have an equal number of records. The BPSI used for these experiments has a false positive rate fixed at 1e-6. All the match attempts performed have an intersection size of 50m (million). [Detailed benchmarks of the KKRT protocol can be found here](KKRT.md).
44

55
<p align="center">
66
<img src="scatter_equal_sets.png"/>

internal/crypto/cipher.go

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,10 @@
11
package crypto
22

33
import (
4-
"crypto/aes"
5-
"crypto/cipher"
6-
7-
"github.com/alecthomas/unsafeslice"
84
"github.com/optable/match/internal/util"
9-
"github.com/twmb/murmur3"
105
"github.com/zeebo/blake3"
116
)
127

13-
// PseudorandomCode is implemented as follows:
14-
// C(x) = AES(1||h(x)[:15]) ||
15-
// AES(2||h(x)[:15]) ||
16-
// AES(3||h(x)[:15]) ||
17-
// AES(4||h(x)[:15])
18-
// where h() is the Murmur3 hashing function.
19-
// PseudorandomCode is passed the src as well as the associated hash
20-
// index. It also requires an AES block cipher.
21-
// The full pseudorandom code consists of four 16 byte encrypted AES
22-
// blocks that are encoded into a slice of 64 bytes. The hash function is
23-
// constructed with the hash index as its two seeds. It is fed the full
24-
// ID source. It returns two uint64s which are cast to a slice of bytes.
25-
// The output is shifted right to allow prepending of the block index.
26-
// For each block, the prepended value is changed to indicate the block
27-
// index (1, 2, 3, 4) before being used as the source for the AES encode.
28-
func PseudorandomCode(aesBlock cipher.Block, src []byte, hIdx byte) []byte {
29-
// prepare destination
30-
dst := make([]byte, aes.BlockSize*4)
31-
32-
// hash id and the hash index
33-
lo, hi := murmur3.SeedSum128(uint64(hIdx), uint64(hIdx), src)
34-
35-
// store in scratch slice
36-
s := unsafeslice.ByteSliceFromUint64Slice([]uint64{lo, hi})
37-
copy(s[1:], s) // shift for prepending
38-
39-
// encrypt
40-
s[0] = 1
41-
aesBlock.Encrypt(dst[:aes.BlockSize], s)
42-
s[0] = 2
43-
aesBlock.Encrypt(dst[aes.BlockSize:aes.BlockSize*2], s)
44-
s[0] = 3
45-
aesBlock.Encrypt(dst[aes.BlockSize*2:aes.BlockSize*3], s)
46-
s[0] = 4
47-
aesBlock.Encrypt(dst[aes.BlockSize*3:], s)
48-
return dst
49-
}
50-
518
// XorCipherWithBlake3 uses the output of Blake3 XOF as pseudorandom
529
// bytes to perform a XOR cipher.
5310
func XorCipherWithBlake3(key []byte, ind byte, src []byte) []byte {

internal/crypto/cipher_amd64.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// +build amd64,!generic
2+
3+
package crypto
4+
5+
import (
6+
"crypto/aes"
7+
"crypto/cipher"
8+
9+
"github.com/alecthomas/unsafeslice"
10+
"github.com/twmb/murmur3"
11+
)
12+
13+
// PseudorandomCode is implemented as follows:
14+
// C(x) = AES(1||h(x)[:15]) ||
15+
// AES(2||h(x)[:15]) ||
16+
// AES(3||h(x)[:15]) ||
17+
// AES(4||h(x)[:15])
18+
// where h() is the Murmur3 hashing function.
19+
// PseudorandomCode is passed the src as well as the associated hash
20+
// index. It also requires an AES block cipher.
21+
// The full pseudorandom code consists of four 16 byte encrypted AES
22+
// blocks that are encoded into a slice of 64 bytes. The hash function is
23+
// constructed with the hash index as its two seeds. It is fed the full
24+
// ID source. It returns two uint64s which are cast to a slice of bytes.
25+
// The output is shifted right to allow prepending of the block index.
26+
// For each block, the prepended value is changed to indicate the block
27+
// index (1, 2, 3, 4) before being used as the source for the AES encode.
28+
func PseudorandomCode(aesBlock cipher.Block, src []byte, hIdx byte) []byte {
29+
// prepare destination
30+
dst := make([]byte, aes.BlockSize*4)
31+
32+
// hash id and the hash index
33+
lo, hi := murmur3.SeedSum128(uint64(hIdx), uint64(hIdx), src)
34+
35+
// store in scratch slice
36+
s := unsafeslice.ByteSliceFromUint64Slice([]uint64{lo, hi})
37+
copy(s[1:], s) // shift for prepending
38+
39+
// encrypt
40+
s[0] = 1
41+
aesBlock.Encrypt(dst[:aes.BlockSize], s)
42+
s[0] = 2
43+
aesBlock.Encrypt(dst[aes.BlockSize:aes.BlockSize*2], s)
44+
s[0] = 3
45+
aesBlock.Encrypt(dst[aes.BlockSize*2:aes.BlockSize*3], s)
46+
s[0] = 4
47+
aesBlock.Encrypt(dst[aes.BlockSize*3:], s)
48+
return dst
49+
}

internal/crypto/cipher_generic.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// +build !amd64 generic
2+
3+
package crypto
4+
5+
import (
6+
"crypto/aes"
7+
"crypto/cipher"
8+
"encoding/binary"
9+
10+
"github.com/twmb/murmur3"
11+
)
12+
13+
// PseudorandomCode is implemented as follows:
14+
// C(x) = AES(1||h(x)[:15]) ||
15+
// AES(2||h(x)[:15]) ||
16+
// AES(3||h(x)[:15]) ||
17+
// AES(4||h(x)[:15])
18+
// where h() is the Murmur3 hashing function.
19+
// PseudorandomCode is passed the src as well as the associated hash
20+
// index. It also requires an AES block cipher.
21+
// The full pseudorandom code consists of four 16 byte encrypted AES
22+
// blocks that are encoded into a slice of 64 bytes. The hash function is
23+
// constructed with the hash index as its two seeds. It is fed the full
24+
// ID source. It returns two uint64s which are cast to a slice of bytes.
25+
// The output is shifted right to allow prepending of the block index.
26+
// For each block, the prepended value is changed to indicate the block
27+
// index (1, 2, 3, 4) before being used as the source for the AES encode.
28+
func PseudorandomCode(aesBlock cipher.Block, src []byte, hIdx byte) []byte {
29+
// prepare destination
30+
dst := make([]byte, aes.BlockSize*4)
31+
32+
// hash id and the hash index
33+
lo, hi := murmur3.SeedSum128(uint64(hIdx), uint64(hIdx), src)
34+
35+
// store in scratch slice - shifted for prepending later
36+
s := make([]byte, 1+aes.BlockSize)
37+
binary.LittleEndian.PutUint64(s[1:], lo)
38+
binary.LittleEndian.PutUint64(s[9:], hi)
39+
40+
// encrypt
41+
s[0] = 1
42+
aesBlock.Encrypt(dst[:aes.BlockSize], s)
43+
s[0] = 2
44+
aesBlock.Encrypt(dst[aes.BlockSize:aes.BlockSize*2], s)
45+
s[0] = 3
46+
aesBlock.Encrypt(dst[aes.BlockSize*2:aes.BlockSize*3], s)
47+
s[0] = 4
48+
aesBlock.Encrypt(dst[aes.BlockSize*3:], s)
49+
return dst
50+
}

internal/hash/hash_test.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
package hash
22

33
import (
4+
"crypto/aes"
45
"crypto/rand"
6+
"encoding/binary"
57
"fmt"
68
"testing"
79

8-
"github.com/alecthomas/unsafeslice"
910
"github.com/twmb/murmur3"
1011
)
1112

@@ -41,11 +42,13 @@ func BenchmarkMetro(b *testing.B) {
4142
}
4243
}
4344

44-
func BenchmarkMurmur316Unsafe(b *testing.B) {
45+
func BenchmarkMurmur316(b *testing.B) {
4546
src := make([]byte, 66)
4647
b.ResetTimer()
4748
for i := 0; i < b.N; i++ {
4849
hi, lo := murmur3.SeedSum128(0, 2, src)
49-
unsafeslice.ByteSliceFromUint64Slice([]uint64{hi, lo})
50+
h := make([]byte, aes.BlockSize)
51+
binary.LittleEndian.PutUint64(h, lo)
52+
binary.LittleEndian.PutUint64(h[8:], hi)
5053
}
5154
}

internal/util/bits.go

Lines changed: 0 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -5,119 +5,10 @@ import (
55
"fmt"
66
"runtime"
77
"sync"
8-
9-
"github.com/alecthomas/unsafeslice"
108
)
119

1210
var ErrByteLengthMissMatch = fmt.Errorf("provided bytes do not have the same length for bit operations")
1311

14-
// Xor casts the first part of the byte slices (length divisible
15-
// by 8) into uint64 and then performs XOR on the slices of uint64.
16-
// The excess elements that could not be cast are XORed conventionally.
17-
// The whole operation is performed in place. Panic if a and dst do
18-
// not have the same length.
19-
// Only tested on x86-64.
20-
func Xor(dst, a []byte) {
21-
if len(dst) != len(a) {
22-
panic(ErrByteLengthMissMatch)
23-
}
24-
25-
castDst := unsafeslice.Uint64SliceFromByteSlice(dst)
26-
castA := unsafeslice.Uint64SliceFromByteSlice(a)
27-
28-
for i := range castDst {
29-
castDst[i] ^= castA[i]
30-
}
31-
32-
// deal with excess bytes which could not be cast to uint64
33-
// in the conventional manner
34-
for j := 0; j < len(dst)%8; j++ {
35-
dst[len(dst)-j-1] ^= a[len(a)-j-1]
36-
}
37-
}
38-
39-
// And casts the first part of the byte slices (length divisible
40-
// by 8) into uint64 and then performs AND on the slices of uint64.
41-
// The excess elements that could not be cast are ANDed conventionally.
42-
// The whole operation is performed in place. Panic if a and dst do
43-
// not have the same length.
44-
// Only tested on x86-64.
45-
func And(dst, a []byte) {
46-
if len(dst) != len(a) {
47-
panic(ErrByteLengthMissMatch)
48-
}
49-
50-
castDst := unsafeslice.Uint64SliceFromByteSlice(dst)
51-
castA := unsafeslice.Uint64SliceFromByteSlice(a)
52-
53-
for i := range castDst {
54-
castDst[i] &= castA[i]
55-
}
56-
57-
// deal with excess bytes which could not be cast to uint64
58-
// in the conventional manner
59-
for j := 0; j < len(dst)%8; j++ {
60-
dst[len(dst)-j-1] &= a[len(a)-j-1]
61-
}
62-
}
63-
64-
// DoubleXor casts the first part of the byte slices (length divisible
65-
// by 8) into uint64 and then performs XOR on the slices of uint64
66-
// (first with a and then with b). The excess elements that could not
67-
// be cast are XORed conventionally. The whole operation is performed
68-
// in place. Panic if a, b and dst do not have the same length.
69-
// Only tested on x86-64.
70-
func DoubleXor(dst, a, b []byte) {
71-
if len(dst) != len(a) || len(dst) != len(b) {
72-
panic(ErrByteLengthMissMatch)
73-
}
74-
75-
castDst := unsafeslice.Uint64SliceFromByteSlice(dst)
76-
castA := unsafeslice.Uint64SliceFromByteSlice(a)
77-
castB := unsafeslice.Uint64SliceFromByteSlice(b)
78-
79-
for i := range castDst {
80-
castDst[i] ^= castA[i]
81-
castDst[i] ^= castB[i]
82-
}
83-
84-
// deal with excess bytes which could not be cast to uint64
85-
// in the conventional manner
86-
for j := 0; j < len(dst)%8; j++ {
87-
dst[len(dst)-j-1] ^= a[len(a)-j-1]
88-
dst[len(dst)-j-1] ^= b[len(b)-j-1]
89-
}
90-
}
91-
92-
// AndXor casts the first part of the byte slices (length divisible
93-
// by 8) into uint64 and then performs AND on the slices of uint64
94-
// (with a) and then performs XOR (with b). The excess elements
95-
// that could not be cast are operated on conventionally. The whole
96-
// operation is performed in place. Panic if a, b and dst do not
97-
// have the same length.
98-
// Only tested on x86-64.
99-
func AndXor(dst, a, b []byte) {
100-
if len(dst) != len(a) || len(dst) != len(b) {
101-
panic(ErrByteLengthMissMatch)
102-
}
103-
104-
castDst := unsafeslice.Uint64SliceFromByteSlice(dst)
105-
castA := unsafeslice.Uint64SliceFromByteSlice(a)
106-
castB := unsafeslice.Uint64SliceFromByteSlice(b)
107-
108-
for i := range castDst {
109-
castDst[i] &= castA[i]
110-
castDst[i] ^= castB[i]
111-
}
112-
113-
// deal with excess bytes which could not be cast to uint64
114-
// in the conventional manner
115-
for j := 0; j < len(dst)%8; j++ {
116-
dst[len(dst)-j-1] &= a[len(a)-j-1]
117-
dst[len(dst)-j-1] ^= b[len(b)-j-1]
118-
}
119-
}
120-
12112
// ConcurrentBitOp performs an in-place bitwise operation, f, on each
12213
// byte from a with dst if they are both the same length.
12314
func ConcurrentBitOp(f func([]byte, []byte), dst, a []byte) {

0 commit comments

Comments
 (0)