From 99b79c80738390408ef1a47f0e26812ef5506c02 Mon Sep 17 00:00:00 2001 From: Martin Holst Swende Date: Fri, 24 Jul 2020 13:47:49 +0200 Subject: [PATCH] conversion: add various marshalling methods (#79) * conversion: add various marshalling methods Benchmark_EncodeHex/large/uint256-6 10843225 97.2 ns/op 80 B/op 1 allocs/op Benchmark_EncodeHex/large/big-6 6547092 190 ns/op 139 B/op 2 allocs/op Benchmark_DecodeHex/large/uint256-6 9983671 118 ns/op 32 B/op 1 allocs/op Benchmark_DecodeHex/large/big-6 10242964 128 ns/op 32 B/op 1 allocs/op * conversion_test: lint nitpick * conversion_tests: more test coverage --- benchmarks_test.go | 53 +++++++++++++ conversion.go | 133 ++++++++++++++++++++++++++++++++ conversion_test.go | 185 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 371 insertions(+) diff --git a/benchmarks_test.go b/benchmarks_test.go index b595a87f..e86b900d 100644 --- a/benchmarks_test.go +++ b/benchmarks_test.go @@ -695,3 +695,56 @@ func Benchmark_SDiv(bench *testing.B) { bench.Run("large/big", benchmark_SdivLarge_Big) bench.Run("large/uint256", benchmark_SdivLarge_Bit) } + +func Benchmark_EncodeHex(b *testing.B) { + hexEncodeU256 := func(b *testing.B, samples *[numSamples]Int) { + b.ReportAllocs() + for j := 0; j < b.N; j += numSamples { + for i := 0; i < numSamples; i++ { + samples[i].Hex() + } + } + } + hexEncodeBig := func(b *testing.B, samples *[numSamples]big.Int) { + b.ReportAllocs() + for j := 0; j < b.N; j += numSamples { + for i := 0; i < numSamples; i++ { + // We're being nice to big.Int here, because this method + // does not add the 0x-prefix -- so an extra alloc is needed to get + // the same result. We still win the benchmark though... + samples[i].Text(16) + } + } + } + b.Run("large/uint256", func(b *testing.B) { hexEncodeU256(b, &int256Samples) }) + b.Run("large/big", func(b *testing.B) { hexEncodeBig(b, &big256Samples) }) +} + +func Benchmark_DecodeHex(b *testing.B) { + + var hexStrings []string + for _, z := range int256Samples { + hexStrings = append(hexStrings, (&z).Hex()) + } + + hexDecodeU256 := func(b *testing.B, samples *[numSamples]Int) { + b.ReportAllocs() + //var sink Int + for j := 0; j < b.N; j += numSamples { + for i := 0; i < numSamples; i++ { + _, _ = FromHex(hexStrings[i]) + } + } + } + hexDecodeBig := func(b *testing.B, samples *[numSamples]big.Int) { + b.ReportAllocs() + //var sink big.Int + for j := 0; j < b.N; j += numSamples { + for i := 0; i < numSamples; i++ { + big.NewInt(0).SetString(hexStrings[i], 16) + } + } + } + b.Run("large/uint256", func(b *testing.B) { hexDecodeU256(b, &int256Samples) }) + b.Run("large/big", func(b *testing.B) { hexDecodeBig(b, &big256Samples) }) +} diff --git a/conversion.go b/conversion.go index e445eb72..3890ca4f 100644 --- a/conversion.go +++ b/conversion.go @@ -6,6 +6,7 @@ package uint256 import ( "encoding/binary" + "errors" "fmt" "io" "math/big" @@ -50,6 +51,50 @@ func FromBig(b *big.Int) (*Int, bool) { return z, overflow } +// fromHex is the internal implementation of parsing a hex-string. +func (z *Int) fromHex(hex string) error { + if err := checkNumberS(hex); err != nil { + return err + } + + if len(hex) > 66 { + return ErrBig256Range + } + end := len(hex) + for i := 0; i < 4; i++ { + start := end - 16 + if start < 2 { + start = 2 + } + for ri := start; ri < end; ri++ { + nib := bintable[hex[ri]] + if nib == badNibble { + return ErrSyntax + } + z[i] = z[i] << 4 + z[i] += uint64(nib) + } + end = start + } + return nil +} + +// FromHex is a convenience-constructor to create an Int from +// a hexadecimal string. The string is required to be '0x'-prefixed +// Numbers larger than 256 bits are not accepted. +func FromHex(hex string) (*Int, error) { + var z Int + if err := z.fromHex(hex); err != nil { + return nil, err + } + return &z, nil +} + +// UnmarshalText implements encoding.TextUnmarshaler +func (z *Int) UnmarshalText(input []byte) error { + return z.fromHex(string(input)) +} + // SetFromBig converts a big.Int to Int and sets the value to z. // TODO: Ensure we have sufficient testing, esp for negative bigints. func (z *Int) SetFromBig(b *big.Int) bool { @@ -415,3 +460,91 @@ func (z *Int) EncodeRLP(w io.Writer) error { _, err := w.Write(b[32-nBytes:]) return err } + +// MarshalText implements encoding.TextMarshaler +func (z *Int) MarshalText() ([]byte, error) { + return []byte(z.Hex()), nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (z *Int) UnmarshalJSON(input []byte) error { + if len(input) < 2 || input[0] != '"' || input[len(input)-1] != '"' { + return ErrNonString + } + return z.UnmarshalText(input[1 : len(input)-1]) +} + +// String returns the hex encoding of b. +func (z *Int) String() string { + return z.Hex() +} + +const ( + hextable = "0123456789abcdef" + bintable = "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x01\x02\x03\x04\x05\x06\a\b\t\xff\xff\xff\xff\xff\xff\xff\n\v\f\r\x0e\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\n\v\f\r\x0e\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + badNibble = 0xff +) + +// Hex encodes z in 0x-prefixed hexadecimal form. +func (z *Int) Hex() string { + // This implementation is not optimal, it allocates a full + // 66-byte output buffer which it fills. It could instead allocate a smaller + // buffer, and omit the final crop-stage. + output := make([]byte, 66) + nibbles := (z.BitLen() + 3) / 4 // nibbles [0,64] + if nibbles == 0 { + nibbles = 1 + } + // Start with the most significant + zWord := (nibbles - 1) / 16 + for i := zWord; i >= 0; i-- { + off := (3 - i) * 16 + output[off+2] = hextable[byte(z[i]>>60)&0xf] + output[off+3] = hextable[byte(z[i]>>56)&0xf] + output[off+4] = hextable[byte(z[i]>>52)&0xf] + output[off+5] = hextable[byte(z[i]>>48)&0xf] + output[off+6] = hextable[byte(z[i]>>44)&0xf] + output[off+7] = hextable[byte(z[i]>>40)&0xf] + output[off+8] = hextable[byte(z[i]>>36)&0xf] + output[off+9] = hextable[byte(z[i]>>32)&0xf] + output[off+10] = hextable[byte(z[i]>>28)&0xf] + output[off+11] = hextable[byte(z[i]>>24)&0xf] + output[off+12] = hextable[byte(z[i]>>20)&0xf] + output[off+13] = hextable[byte(z[i]>>16)&0xf] + output[off+14] = hextable[byte(z[i]>>12)&0xf] + output[off+15] = hextable[byte(z[i]>>8)&0xf] + output[off+16] = hextable[byte(z[i]>>4)&0xf] + output[off+17] = hextable[byte(z[i]&0xF)&0xf] + } + output[64-nibbles] = '0' + output[65-nibbles] = 'x' + return string(output[64-nibbles:]) +} + +var ( + ErrEmptyString = errors.New("empty hex string") + ErrSyntax = errors.New("invalid hex string") + ErrMissingPrefix = errors.New("hex string without 0x prefix") + ErrEmptyNumber = errors.New("hex string \"0x\"") + ErrLeadingZero = errors.New("hex number with leading zero digits") + ErrBig256Range = errors.New("hex number > 256 bits") + ErrNonString = errors.New("non-string") +) + +func checkNumberS(input string) error { + l := len(input) + if l == 0 { + return ErrEmptyString + } + if l < 2 || input[0] != '0' || + (input[1] != 'x' && input[1] != 'X') { + return ErrMissingPrefix + } + if l == 2 { + return ErrEmptyNumber + } + if len(input) > 3 && input[2] == '0' { + return ErrLeadingZero + } + return nil +} diff --git a/conversion_test.go b/conversion_test.go index dbd5a906..31007202 100644 --- a/conversion_test.go +++ b/conversion_test.go @@ -7,6 +7,7 @@ package uint256 import ( "bufio" "bytes" + "encoding/json" "fmt" "math/big" "testing" @@ -576,3 +577,187 @@ func BenchmarkRLPEncoding(b *testing.B) { } } } + +func referenceBig(s string) *big.Int { + b, ok := new(big.Int).SetString(s, 16) + if !ok { + panic("invalid") + } + return b +} + +type marshalTest struct { + input interface{} + want string +} + +type unmarshalTest struct { + input string + want interface{} + wantErr error // if set, decoding must fail on any platform +} + +var ( + encodeBigTests = []marshalTest{ + {referenceBig("0"), "0x0"}, + {referenceBig("1"), "0x1"}, + {referenceBig("ff"), "0xff"}, + {referenceBig("112233445566778899aabbccddeeff"), "0x112233445566778899aabbccddeeff"}, + {referenceBig("80a7f2c1bcc396c00"), "0x80a7f2c1bcc396c00"}, + } + + decodeBigTests = []unmarshalTest{ + // invalid + {input: ``, wantErr: ErrEmptyString}, + {input: `0`, wantErr: ErrMissingPrefix}, + {input: `0x`, wantErr: ErrEmptyNumber}, + {input: `0x01`, wantErr: ErrLeadingZero}, + {input: `0xx`, wantErr: ErrSyntax}, + {input: `0x1zz01`, wantErr: ErrSyntax}, + { + input: `0x10000000000000000000000000000000000000000000000000000000000000000`, + wantErr: ErrBig256Range, + }, + // valid + {input: `0x0`, want: big.NewInt(0)}, + {input: `0x2`, want: big.NewInt(0x2)}, + {input: `0x2F2`, want: big.NewInt(0x2f2)}, + {input: `0X2F2`, want: big.NewInt(0x2f2)}, + {input: `0x1122aaff`, want: big.NewInt(0x1122aaff)}, + {input: `0xbBb`, want: big.NewInt(0xbbb)}, + {input: `0xfffffffff`, want: big.NewInt(0xfffffffff)}, + { + input: `0x112233445566778899aabbccddeeff`, + want: referenceBig("112233445566778899aabbccddeeff"), + }, + { + input: `0xffffffffffffffffffffffffffffffffffff`, + want: referenceBig("ffffffffffffffffffffffffffffffffffff"), + }, + { + input: `0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff`, + want: referenceBig("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + }, + } +) + +func checkError(t *testing.T, input string, got, want error) bool { + if got == nil { + if want != nil { + t.Errorf("input %s: got no error, want %q", input, want) + return false + } + return true + } + if want == nil { + t.Errorf("input %s: unexpected error %q", input, got) + } else if got.Error() != want.Error() { + t.Errorf("input %s: got error %q, want %q", input, got, want) + } + return false +} + +func TestEncode(t *testing.T) { + for _, test := range encodeBigTests { + z, _ := FromBig(test.input.(*big.Int)) + enc := z.Hex() + if enc != test.want { + t.Errorf("input %x: wrong encoding %s (exp %s)", test.input, enc, test.want) + } + } + +} + +func TestDecode(t *testing.T) { + for _, test := range decodeBigTests { + dec, err := FromHex(test.input) + if !checkError(t, test.input, err, test.wantErr) { + continue + } + b := dec.ToBig() + if b.Cmp(test.want.(*big.Int)) != 0 { + t.Errorf("input %s: value mismatch: got %x, want %x", test.input, dec, test.want) + continue + } + } + // Some remaining json-tests + type jsonStruct struct { + Foo *Int + } + var jsonDecoded jsonStruct + if err := json.Unmarshal([]byte(`{"Foo":0x1}`), &jsonDecoded); err == nil { + t.Fatal("Expected error") + } + if err := json.Unmarshal([]byte(`{"Foo":1}`), &jsonDecoded); err == nil { + t.Fatal("Expected error") + } + if err := json.Unmarshal([]byte(`{"Foo":""}`), &jsonDecoded); err == nil { + t.Fatal("Expected error") + } + if err := json.Unmarshal([]byte(`{"Foo":"0x1"}`), &jsonDecoded); err != nil { + t.Fatalf("Expected no error, got %v", err) + } else { + if jsonDecoded.Foo.Uint64() != 1 { + t.Fatal("Expected 1") + } + } +} + +func TestEnDecode(t *testing.T) { + type jsonStruct struct { + Foo *Int + } + var testSample = func(i int, bigSample big.Int, intSample Int) { + // Encoding + exp := fmt.Sprintf("0x%s", bigSample.Text(16)) + + if got := intSample.Hex(); exp != got { + t.Fatalf("test %d #1, got %v, exp %v", i, got, exp) + } + if got := intSample.String(); exp != got { + t.Fatalf("test %d #2, got %v, exp %v", i, got, exp) + } + if got, _ := intSample.MarshalText(); exp != string(got) { + t.Fatalf("test %d #3, got %v, exp %v", i, got, exp) + } + { // Json + jsonEncoded, err := json.Marshal(&jsonStruct{&intSample}) + if err != nil { + t.Fatalf("test %d #4, err: %v", i, err) + } + var jsonDecoded jsonStruct + err = json.Unmarshal(jsonEncoded, &jsonDecoded) + if err != nil { + t.Fatalf("test %d #5, err: %v", i, err) + } + if jsonDecoded.Foo.Cmp(&intSample) != 0 { + t.Fatalf("test %d #6, got %v, exp %v", i, jsonDecoded.Foo, intSample) + } + } + // Decoding + dec, err := FromHex(exp) + if err != nil { + t.Fatalf("test %d #5, err: %v", i, err) + } + if dec.Cmp(&intSample) != 0 { + t.Fatalf("test %d #6, got %v, exp %v", i, dec, intSample) + } + dec = NewInt() + if err := dec.UnmarshalText([]byte(exp)); err != nil { + t.Fatalf("test %d #7, err: %v", i, err) + } + if dec.Cmp(&intSample) != 0 { + t.Fatalf("test %d #8, got %v, exp %v", i, dec, intSample) + } + + } + for i, bigSample := range big256Samples { + intSample := int256Samples[i] + testSample(i, bigSample, intSample) + } + + for i, bigSample := range big256SamplesLt { + intSample := int256SamplesLt[i] + testSample(i, bigSample, intSample) + } +}