Go 이더리움 주소들 무결성 검사

박재훈·2023년 7월 2일
0

GO

목록 보기
17/19

Go에서 여러 이더리움 주소들을 다룰 때 무결성 확인을 했던 것에 대한 기록.

여러 주소들을 xor를 통해 진행하였다. xor를 하기 위해 big.Intcommon.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 연산을 하는 식으로 순서를 보장할 수는 있겠지만 연산 비트가 적어진 만큼 무결성에 영향이 가는 건 어쩔 수 없다.

profile
코딩 좋아합니다

0개의 댓글