Go에서 여러 이더리움 주소들을 다룰 때 무결성 확인을 했던 것에 대한 기록.
여러 주소들을 xor를 통해 진행하였다. xor를 하기 위해 big.Int
와 common.Address
를 이용하는 2가지 방법을 생각해보았다.
각 타입들로 변환하여 xor 수행 후 target
과 비교를 하게 된다.
테스트에 사용한 주소들은 아래와 같으며, 세폴리아 이더스캔에서 무작위로 가져왔다.
var addrs = []string{
"0xbeef32ca5b9a198d27B4e02F4c70439fE60356Cf",
"0xa2A6d93439144FFE4D27c9E088dCD8b783946263",
"0x098d4D36073cB7Fb9388C2eDBdF09d153021D778",
"0xd7d76c58b3a519e9fA6Cc4D22dC017259BC49F1E",
"0x25c4a76E7d118705e7Ea2e9b7d8C59930d8aCD3b",
"0xb21c33DE1FAb3FA15499c62B59fe0cC3250020d1",
"0x10F5d45854e038071485AC9e402308cF80D2d2fE",
"0x14d0908CdF097C8ce8B74407327bA174cAf68D53",
}
먼저 big.Int
를 이용하는 방법은 각 주소들을 []byte
타입으로 변환 후 그걸 이용해 big.Int
에 적용하여 xor에 이용하는 방식이다.
func BenchmarkBigIntXor(b *testing.B) {
as := make([][]byte, 0, len(addrs))
for _, addr := range addrs {
x, _ := hex.DecodeString(addr[2:])
as = append(as, x)
}
var target = big.NewInt(0)
for i := 0; i < b.N; i++ {
res := big.NewInt(0)
for _, a := range as {
res.Xor(res, new(big.Int).SetBytes(a))
}
res.Cmp(target)
}
}
common.Address
를 이용하는 방식에서는 common.HexToAddress()
를 통해 변환한 뒤 xor를 수행하였다.
func BenchmarkBytesXor(b *testing.B) {
as := make([]common.Address, 0, len(addrs))
for _, addr := range addrs {
as = append(as, common.HexToAddress(addr))
}
var target = common.Address{}
for i := 0; i < b.N; i++ {
res := common.Address{}
for _, a := range as {
for j := 0; j < common.AddressLength; j++ {
res[j] ^= a[j]
}
}
bytes.Equal(res[:], target[:])
}
}
코드 작성 후 벤치마크를 통해 결과를 확인하였다.
$ go test -bench=. -benchtime=10000000x -benchmem -count 5
goos: darwin
goarch: amd64
pkg: golang-test
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkBigIntXor-12 10000000 476.4 ns/op 576 B/op 9 allocs/op
BenchmarkBigIntXor-12 10000000 446.2 ns/op 576 B/op 9 allocs/op
BenchmarkBigIntXor-12 10000000 413.6 ns/op 576 B/op 9 allocs/op
BenchmarkBigIntXor-12 10000000 413.7 ns/op 576 B/op 9 allocs/op
BenchmarkBigIntXor-12 10000000 415.6 ns/op 576 B/op 9 allocs/op
BenchmarkBytesXor-12 10000000 111.9 ns/op 0 B/op 0 allocs/op
BenchmarkBytesXor-12 10000000 105.1 ns/op 0 B/op 0 allocs/op
BenchmarkBytesXor-12 10000000 102.2 ns/op 0 B/op 0 allocs/op
BenchmarkBytesXor-12 10000000 104.1 ns/op 0 B/op 0 allocs/op
BenchmarkBytesXor-12 10000000 109.3 ns/op 0 B/op 0 allocs/op
PASS
ok golang-test 27.337s
big.Int
는 4~500ns, common.Address
는 100 초반대의 ns가 나왔다. common.Address
가 4배 이상 빠른 것이다.
그렇다면, big.Int
대신 uint64
를 쓰면 훨씬 빠르지 않을까?
그걸 하기 위해서는 일단 이더리움 주소를 분할할 필요가 있다. 40자리의 16진수는 uint64
에 담을 수 없기 때문에 10자리씩 쪼갰다. 그렇게 1개의 이더리움 주소를 4개 원소를 가진 uint64
슬라이스로 만든 뒤, 전체 원소들을 xor 하는 방식으로 진행하였다.
만약 xor 결과값이 의미가 있는 것이라면 다른 방법을 생각해야 했겠지만 단순히 무결성 검사이므로 이렇게 하였다.
func BenchmarkUint64Xor(b *testing.B) {
as := make([]uint64, 0, len(addrs)*4)
for _, addr := range addrs {
i1, _ := strconv.ParseUint(addr[2:12], 16, 64)
i2, _ := strconv.ParseUint(addr[12:22], 16, 64)
i3, _ := strconv.ParseUint(addr[22:32], 16, 64)
i4, _ := strconv.ParseUint(addr[32:], 16, 64)
as = append(as, i1, i2, i3, i4)
}
var target uint64 = 0
for i := 0; i < b.N; i++ {
x := uint64(0)
for _, a := range as {
x ^= a
}
_ = target == x
}
}
결과는... 압도적이었다. common.Address
에 비해서도 10배 가량 빠른 속도를 보여주었다.
$ go test -bench=. -benchtime=10000000x -benchmem -count 5
goos: darwin
goarch: amd64
pkg: golang-test
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkBigIntXor-12 10000000 449.2 ns/op 576 B/op 9 allocs/op
BenchmarkBigIntXor-12 10000000 518.4 ns/op 576 B/op 9 allocs/op
BenchmarkBigIntXor-12 10000000 416.6 ns/op 576 B/op 9 allocs/op
BenchmarkBigIntXor-12 10000000 412.4 ns/op 576 B/op 9 allocs/op
BenchmarkBigIntXor-12 10000000 410.8 ns/op 576 B/op 9 allocs/op
BenchmarkBytesXor-12 10000000 112.7 ns/op 0 B/op 0 allocs/op
BenchmarkBytesXor-12 10000000 102.6 ns/op 0 B/op 0 allocs/op
BenchmarkBytesXor-12 10000000 103.6 ns/op 0 B/op 0 allocs/op
BenchmarkBytesXor-12 10000000 109.9 ns/op 0 B/op 0 allocs/op
BenchmarkBytesXor-12 10000000 106.5 ns/op 0 B/op 0 allocs/op
BenchmarkUint64Xor-12 10000000 9.791 ns/op 0 B/op 0 allocs/op
BenchmarkUint64Xor-12 10000000 9.913 ns/op 0 B/op 0 allocs/op
BenchmarkUint64Xor-12 10000000 10.30 ns/op 0 B/op 0 allocs/op
BenchmarkUint64Xor-12 10000000 9.829 ns/op 0 B/op 0 allocs/op
BenchmarkUint64Xor-12 10000000 9.744 ns/op 0 B/op 0 allocs/op
PASS
ok golang-test 28.110s
하지만 주소를 잘라서 진행했기 때문에 무결성에 문제가 생길 수 있다. 자른 부분들끼리 순서가 바뀌어있는 주소에 대해서는 동일한 결과가 나오게 된다.
예를 들어 0xbeef32ca5b9a198d27B4e02F4c70439fE60356Cf
주소와 0xe02F4c70439fE60356Cfbeef32ca5b9a198d27B4
주소는 동일한 것처럼 취급될 수 있다.
하나의 주소에 대해서는 순서가 적용되는 연산을 한 뒤 각 주소들끼리 xor 연산을 하는 식으로 순서를 보장할 수는 있겠지만 연산 비트가 적어진 만큼 무결성에 영향이 가는 건 어쩔 수 없다.