From 453ecc050446cf77acdbe78b8532bc17e2dc2040 Mon Sep 17 00:00:00 2001 From: arnaucube Date: Mon, 8 Feb 2021 16:08:13 +0100 Subject: [PATCH] Add Float40 methods This commit adds Float40 related methods, and keeps the Float16 version which will be deleted in a near future once the Float40 migration is ready. --- common/float16.go | 1 + common/float16_test.go | 10 ++-- common/float40.go | 102 +++++++++++++++++++++++++++++++++++++++++ common/float40_test.go | 95 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 203 insertions(+), 5 deletions(-) create mode 100644 common/float40.go create mode 100644 common/float40_test.go diff --git a/common/float16.go b/common/float16.go index d1bcfdb..4a33c49 100644 --- a/common/float16.go +++ b/common/float16.go @@ -30,6 +30,7 @@ func (f16 Float16) Bytes() []byte { // Float16FromBytes returns a Float16 from a byte array of 2 bytes. func Float16FromBytes(b []byte) *Float16 { + // WARNING b[:2] for a b where len(b)<2 can break f16 := Float16(binary.BigEndian.Uint16(b[:2])) return &f16 } diff --git a/common/float16_test.go b/common/float16_test.go index 9726d0e..1ba7052 100644 --- a/common/float16_test.go +++ b/common/float16_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestConversions(t *testing.T) { +func TestConversionsFloat16(t *testing.T) { testVector := map[Float16]string{ 0x307B: "123000000", 0x1DC6: "454500", @@ -32,14 +32,14 @@ func TestConversions(t *testing.T) { bi.SetString(testVector[test], 10) fl, err := NewFloat16(bi) - assert.Equal(t, nil, err) + assert.NoError(t, err) fx2 := fl.BigInt() assert.Equal(t, fx2.String(), testVector[test]) } } -func TestFloorFix2Float(t *testing.T) { +func TestFloorFix2FloatFloat16(t *testing.T) { testVector := map[string]Float16{ "87999990000000000": 0x776f, "87950000000000001": 0x776f, @@ -57,10 +57,10 @@ func TestFloorFix2Float(t *testing.T) { } } -func TestConversionLosses(t *testing.T) { +func TestConversionLossesFloat16(t *testing.T) { a := big.NewInt(1000) b, err := NewFloat16(a) - assert.Equal(t, nil, err) + assert.NoError(t, err) c := b.BigInt() assert.Equal(t, c, a) diff --git a/common/float40.go b/common/float40.go new file mode 100644 index 0000000..ca7bafd --- /dev/null +++ b/common/float40.go @@ -0,0 +1,102 @@ +// Package common Float40 provides methods to work with Hermez custom half +// float precision, 40 bits, codification internally called Float40 has been +// adopted to encode large integers. This is done in order to save bits when L2 +// transactions are published. +//nolint:gomnd +package common + +import ( + "bytes" + "encoding/binary" + "errors" + "math/big" + + "github.com/hermeznetwork/tracerr" +) + +const ( + // maxFloat40Value is the maximum value that the Float40 can have + // (40 bits: maxFloat40Value=2**40-1) + maxFloat40Value = 0xffffffffff +) + +var ( + // ErrFloat40Overflow is used when a given nonce overflows the maximum + // capacity of the Float40 (2**40-1) + ErrFloat40Overflow = errors.New("Float40 overflow, max value: 2**40 -1") + // ErrFloat40E31 is used when the e > 31 when trying to convert a + // *big.Int to Float40 + ErrFloat40E31 = errors.New("Float40 error, e > 31") + // ErrFloat40NotEnoughPrecission is used when the given *big.Int can + // not be represented as Float40 due not enough precission + ErrFloat40NotEnoughPrecission = errors.New("Float40 error, not enough precission") +) + +// Float40 represents a float in a 64 bit format +type Float40 uint64 + +// Bytes return a byte array of length 5 with the Float40 value encoded in +// BigEndian +func (f40 Float40) Bytes() ([]byte, error) { + if f40 > maxFloat40Value { + return []byte{}, tracerr.Wrap(ErrFloat40Overflow) + } + + var f40Bytes [8]byte + binary.BigEndian.PutUint64(f40Bytes[:], uint64(f40)) + var b [5]byte + copy(b[:], f40Bytes[3:]) + return b[:], nil +} + +// Float40FromBytes returns a Float40 from a byte array of 5 bytes in Bigendian +// representation. +func Float40FromBytes(b []byte) Float40 { + var f40Bytes [8]byte + copy(f40Bytes[3:], b[:]) + f40 := binary.BigEndian.Uint64(f40Bytes[:]) + return Float40(f40) +} + +// BigInt converts the Float40 to a *big.Int v, where v = m * 10^e, being: +// [ e | m ] +// [ 5 bits | 35 bits ] +func (f40 Float40) BigInt() (*big.Int, error) { + // take the 5 used bytes (FF * 5) + var f40Uint64 uint64 = uint64(f40) & 0x00_00_00_FF_FF_FF_FF_FF + f40Bytes, err := f40.Bytes() + if err != nil { + return nil, err + } + + e := f40Bytes[0] & 0xF8 >> 3 // take first 5 bits + m := f40Uint64 & 0x07_FF_FF_FF_FF // take the others 35 bits + + exp := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(e)), nil) + r := new(big.Int).Mul(big.NewInt(int64(m)), exp) + return r, nil +} + +// NewFloat40 encodes a *big.Int integer as a Float40, returning error in case +// of loss during the encoding. +func NewFloat40(f *big.Int) (Float40, error) { + m := f + e := big.NewInt(0) + zero := big.NewInt(0) + ten := big.NewInt(10) + thres := big.NewInt(0x08_00_00_00_00) + for bytes.Equal(zero.Bytes(), new(big.Int).Mod(m, ten).Bytes()) && + !bytes.Equal(zero.Bytes(), new(big.Int).Div(m, thres).Bytes()) { + m = new(big.Int).Div(m, ten) + e = new(big.Int).Add(e, big.NewInt(1)) + } + if e.Int64() > 31 { + return 0, ErrFloat40E31 + } + if !bytes.Equal(zero.Bytes(), new(big.Int).Div(m, thres).Bytes()) { + return 0, ErrFloat40NotEnoughPrecission + } + r := new(big.Int).Add(m, + new(big.Int).Mul(e, thres)) + return Float40(r.Uint64()), nil +} diff --git a/common/float40_test.go b/common/float40_test.go new file mode 100644 index 0000000..50f2c71 --- /dev/null +++ b/common/float40_test.go @@ -0,0 +1,95 @@ +package common + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConversionsFloat40(t *testing.T) { + testVector := map[Float40]string{ + 6*0x800000000 + 123: "123000000", + 2*0x800000000 + 4545: "454500", + 30*0x800000000 + 10235: "10235000000000000000000000000000000", + 0x000000000: "0", + 0x800000000: "0", + 0x0001: "1", + 0x0401: "1025", + 0x800000000 + 1: "10", + 0xFFFFFFFFFF: "343597383670000000000000000000000000000000", + } + + for test := range testVector { + fix, err := test.BigInt() + require.NoError(t, err) + assert.Equal(t, fix.String(), testVector[test]) + + bi, ok := new(big.Int).SetString(testVector[test], 10) + require.True(t, ok) + + fl, err := NewFloat40(bi) + assert.NoError(t, err) + + fx2, err := fl.BigInt() + require.NoError(t, err) + assert.Equal(t, fx2.String(), testVector[test]) + } +} + +func TestExpectError(t *testing.T) { + testVector := map[string]error{ + "9922334455000000000000000000000000000000": nil, + "9922334455000000000000000000000000000001": ErrFloat40NotEnoughPrecission, + "9922334454999999999999999999999999999999": ErrFloat40NotEnoughPrecission, + "42949672950000000000000000000000000000000": nil, + "99223344556573838487575": ErrFloat40NotEnoughPrecission, + "992233445500000000000000000000000000000000": ErrFloat40E31, + "343597383670000000000000000000000000000000": nil, + "343597383680000000000000000000000000000000": ErrFloat40NotEnoughPrecission, + "343597383690000000000000000000000000000000": ErrFloat40NotEnoughPrecission, + "343597383700000000000000000000000000000000": ErrFloat40E31, + } + for test := range testVector { + bi, ok := new(big.Int).SetString(test, 10) + require.True(t, ok) + _, err := NewFloat40(bi) + assert.Equal(t, testVector[test], err) + } +} + +func BenchmarkFloat40(b *testing.B) { + newBigInt := func(s string) *big.Int { + bigInt, ok := new(big.Int).SetString(s, 10) + if !ok { + panic("Can not convert string to *big.Int") + } + return bigInt + } + type pair struct { + Float40 Float40 + BigInt *big.Int + } + testVector := []pair{ + {6*0x800000000 + 123, newBigInt("123000000")}, + {2*0x800000000 + 4545, newBigInt("454500")}, + {30*0x800000000 + 10235, newBigInt("10235000000000000000000000000000000")}, + {0x000000000, newBigInt("0")}, + {0x800000000, newBigInt("0")}, + {0x0001, newBigInt("1")}, + {0x0401, newBigInt("1025")}, + {0x800000000 + 1, newBigInt("10")}, + {0xFFFFFFFFFF, newBigInt("343597383670000000000000000000000000000000")}, + } + b.Run("NewFloat40()", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = NewFloat40(testVector[i%len(testVector)].BigInt) + } + }) + b.Run("Float40.BigInt()", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = testVector[i%len(testVector)].Float40.BigInt() + } + }) +}