diff --git a/db/rocksdb.go b/db/rocksdb.go index 885b57d1..127b0cd8 100644 --- a/db/rocksdb.go +++ b/db/rocksdb.go @@ -6,6 +6,7 @@ import ( "bytes" "encoding/binary" "encoding/hex" + "math/big" "os" "path/filepath" "time" @@ -1072,3 +1073,62 @@ func unpackVarint(buf []byte) (int32, int) { i, ofs := vlq.Int(buf) return int32(i), ofs } + +const ( + // number of bits in a big.Word + wordBits = 32 << (uint64(^big.Word(0)) >> 63) + // number of bytes in a big.Word + wordBytes = wordBits / 8 + // max packed bigint words + maxPackedBigintWords = (256 - wordBytes) / wordBytes +) + +// big int is packed in BigEndian order without memory allocation as 1 byte length followed by bytes of big int +// number of written bytes is returned +// limitation: bigints longer than 248 bytes are truncated to 248 bytes +// caution: buffer must be big enough to hold the packed big int, buffer 249 bytes big is always safe +func packBigint(bi *big.Int, buf []byte) int { + w := bi.Bits() + lw := len(w) + // zero returns only one byte - zero length + if lw == 0 { + buf[0] = 0 + return 1 + } + // pack the most significant word in a special way - skip leading zeros + w0 := w[lw-1] + fb := 8 + mask := big.Word(0xff) << (wordBits - 8) + for w0&mask == 0 { + fb-- + mask >>= 8 + } + for i := fb; i > 0; i-- { + buf[i] = byte(w0) + w0 >>= 8 + } + // if the big int is too big (> 2^1984), the number of bytes would not fit to 1 byte + // in this case, truncate the number, it is not expected to work with this big numbers as amounts + s := 0 + if lw > maxPackedBigintWords { + s = lw - maxPackedBigintWords + } + // pack the rest of the words in reverse order + for j := lw - 2; j >= s; j-- { + d := w[j] + for i := fb + wordBytes; i > fb; i-- { + buf[i] = byte(d) + d >>= 8 + } + fb += wordBytes + } + buf[0] = byte(fb) + return fb + 1 +} + +func unpackBigint(buf []byte) (big.Int, int) { + var r big.Int + l := int(buf[0]) + 1 + r.SetBytes(buf[1:l]) + return r, l +} diff --git a/db/rocksdb_test.go b/db/rocksdb_test.go index 52be1c5d..f34ecefb 100644 --- a/db/rocksdb_test.go +++ b/db/rocksdb_test.go @@ -1,4 +1,4 @@ -// +build unittest +// build unittest package db @@ -8,6 +8,7 @@ import ( "encoding/hex" "fmt" "io/ioutil" + "math/big" "os" "reflect" "sort" @@ -723,3 +724,93 @@ func Test_unpackBlockAddresses(t *testing.T) { }) } } + +func Test_packBigint_unpackBigint(t *testing.T) { + bigbig1, _ := big.NewInt(0).SetString("123456789123456789012345", 10) + bigbig2, _ := big.NewInt(0).SetString("12345678912345678901234512389012345123456789123456789012345123456789123456789012345", 10) + bigbigbig := big.NewInt(0) + bigbigbig.Mul(bigbig2, bigbig2) + bigbigbig.Mul(bigbigbig, bigbigbig) + bigbigbig.Mul(bigbigbig, bigbigbig) + tests := []struct { + name string + bi *big.Int + buf []byte + toobiglen int + }{ + { + name: "0", + bi: big.NewInt(0), + buf: make([]byte, 249), + }, + { + name: "1", + bi: big.NewInt(1), + buf: make([]byte, 249), + }, + { + name: "54321", + bi: big.NewInt(54321), + buf: make([]byte, 249), + }, + { + name: "12345678", + bi: big.NewInt(12345678), + buf: make([]byte, 249), + }, + { + name: "123456789123456789", + bi: big.NewInt(123456789123456789), + buf: make([]byte, 249), + }, + { + name: "bigbig1", + bi: bigbig1, + buf: make([]byte, 249), + }, + { + name: "bigbig2", + bi: bigbig2, + buf: make([]byte, 249), + }, + { + name: "bigbigbig", + bi: bigbigbig, + buf: make([]byte, 249), + toobiglen: 242, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // packBigint + got := packBigint(tt.bi, tt.buf) + if tt.toobiglen == 0 { + // create buffer that we expect + bb := tt.bi.Bytes() + want := append([]byte(nil), byte(len(bb))) + want = append(want, bb...) + if got != len(want) { + t.Errorf("packBigint() = %v, want %v", got, len(want)) + } + for i := 0; i < got; i++ { + if tt.buf[i] != want[i] { + t.Errorf("packBigint() buf = %v, want %v", tt.buf[:got], want) + break + } + } + // unpackBigint + got1, got2 := unpackBigint(tt.buf) + if got2 != len(want) { + t.Errorf("unpackBigint() = %v, want %v", got2, len(want)) + } + if tt.bi.Cmp(&got1) != 0 { + t.Errorf("unpackBigint() = %v, want %v", got1, tt.bi) + } + } else { + if got != tt.toobiglen { + t.Errorf("packBigint() = %v, want toobiglen %v", got, tt.toobiglen) + } + } + }) + } +}