From e4bf531e3e1ad0e3c8972913cc289fe5d811a728 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 9 Nov 2020 17:26:23 +0300 Subject: [PATCH] core: implement `System.Binary.Atoi/Itoa` syscalls They follow C# conversion rules, but differ from our `bigint` module conversions: 1. String must be big-endian. 2. Sign extension is 4-bit in size (single hex character) and not 8-byte. --- pkg/core/interop/binary/itoa.go | 91 ++++++++++++++++++++++++ pkg/core/interop/binary/itoa_test.go | 98 ++++++++++++++++++++++++++ pkg/core/interop/interopnames/names.go | 4 ++ pkg/core/interops.go | 3 + 4 files changed, 196 insertions(+) create mode 100644 pkg/core/interop/binary/itoa.go create mode 100644 pkg/core/interop/binary/itoa_test.go diff --git a/pkg/core/interop/binary/itoa.go b/pkg/core/interop/binary/itoa.go new file mode 100644 index 000000000..b31918836 --- /dev/null +++ b/pkg/core/interop/binary/itoa.go @@ -0,0 +1,91 @@ +package binary + +import ( + "encoding/hex" + "errors" + "math/big" + "strings" + + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/encoding/bigint" +) + +var ( + // ErrInvalidBase is returned when base is invalid. + ErrInvalidBase = errors.New("invalid base") + // ErrInvalidFormat is returned when string is not a number. + ErrInvalidFormat = errors.New("invalid format") +) + +// Itoa converts number to string. +func Itoa(ic *interop.Context) error { + num := ic.VM.Estack().Pop().BigInt() + base := ic.VM.Estack().Pop().BigInt() + if !base.IsInt64() { + return ErrInvalidBase + } + var s string + switch b := base.Int64(); b { + case 10: + s = num.Text(10) + case 16: + if num.Sign() == 0 { + s = "0" + break + } + bs := bigint.ToBytes(num) + reverse(bs) + s = hex.EncodeToString(bs) + if pad := bs[0] & 0xF8; pad == 0 || pad == 0xF8 { + s = s[1:] + } + s = strings.ToUpper(s) + default: + return ErrInvalidBase + } + ic.VM.Estack().PushVal(s) + return nil +} + +// Atoi converts string to number. +func Atoi(ic *interop.Context) error { + num := ic.VM.Estack().Pop().String() + base := ic.VM.Estack().Pop().BigInt() + if !base.IsInt64() { + return ErrInvalidBase + } + var bi *big.Int + switch b := base.Int64(); b { + case 10: + var ok bool + bi, ok = new(big.Int).SetString(num, int(b)) + if !ok { + return ErrInvalidFormat + } + case 16: + changed := len(num)%2 != 0 + if changed { + num = "0" + num + } + bs, err := hex.DecodeString(num) + if err != nil { + return ErrInvalidFormat + } + if changed && bs[0]&0x8 != 0 { + bs[0] |= 0xF0 + } + reverse(bs) + bi = bigint.FromBytes(bs) + default: + return ErrInvalidBase + } + ic.VM.Estack().PushVal(bi) + return nil +} + +func reverse(b []byte) { + l := len(b) + for i := 0; i < l/2; i++ { + b[i], b[l-i-1] = b[l-i-1], b[i] + } +} diff --git a/pkg/core/interop/binary/itoa_test.go b/pkg/core/interop/binary/itoa_test.go new file mode 100644 index 000000000..faf189076 --- /dev/null +++ b/pkg/core/interop/binary/itoa_test.go @@ -0,0 +1,98 @@ +package binary + +import ( + "errors" + "math" + "math/big" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/stretchr/testify/require" +) + +func TestItoa(t *testing.T) { + var testCases = []struct { + num *big.Int + base *big.Int + result string + }{ + {big.NewInt(0), big.NewInt(10), "0"}, + {big.NewInt(0), big.NewInt(16), "0"}, + {big.NewInt(1), big.NewInt(10), "1"}, + {big.NewInt(-1), big.NewInt(10), "-1"}, + {big.NewInt(1), big.NewInt(16), "1"}, + {big.NewInt(7), big.NewInt(16), "7"}, + {big.NewInt(8), big.NewInt(16), "08"}, + {big.NewInt(65535), big.NewInt(16), "0FFFF"}, + {big.NewInt(15), big.NewInt(16), "0F"}, + {big.NewInt(-1), big.NewInt(16), "F"}, + } + + for _, tc := range testCases { + ic := &interop.Context{VM: vm.New()} + ic.VM.Estack().PushVal(tc.base) + ic.VM.Estack().PushVal(tc.num) + require.NoError(t, Itoa(ic)) + require.Equal(t, tc.result, ic.VM.Estack().Pop().String()) + + ic = &interop.Context{VM: vm.New()} + ic.VM.Estack().PushVal(tc.base) + ic.VM.Estack().PushVal(tc.result) + + require.NoError(t, Atoi(ic)) + require.Equal(t, tc.num, ic.VM.Estack().Pop().BigInt()) + } + + t.Run("-1", func(t *testing.T) { + for _, s := range []string{"FF", "FFF", "FFFF"} { + ic := &interop.Context{VM: vm.New()} + ic.VM.Estack().PushVal(16) + ic.VM.Estack().PushVal(s) + + require.NoError(t, Atoi(ic)) + require.Equal(t, big.NewInt(-1), ic.VM.Estack().Pop().BigInt()) + } + }) +} + +func TestItoaError(t *testing.T) { + var testCases = []struct { + num *big.Int + base *big.Int + err error + }{ + {big.NewInt(1), big.NewInt(13), ErrInvalidBase}, + {big.NewInt(-1), new(big.Int).Add(big.NewInt(math.MaxInt64), big.NewInt(10)), ErrInvalidBase}, + } + + for _, tc := range testCases { + ic := &interop.Context{VM: vm.New()} + ic.VM.Estack().PushVal(tc.base) + ic.VM.Estack().PushVal(tc.num) + err := Itoa(ic) + require.True(t, errors.Is(err, tc.err), "got: %v", err) + } +} + +func TestAtoiError(t *testing.T) { + var testCases = []struct { + num string + base *big.Int + err error + }{ + {"1", big.NewInt(13), ErrInvalidBase}, + {"1", new(big.Int).Add(big.NewInt(math.MaxInt64), big.NewInt(16)), ErrInvalidBase}, + {"1_000", big.NewInt(10), ErrInvalidFormat}, + {"FE", big.NewInt(10), ErrInvalidFormat}, + {"XD", big.NewInt(16), ErrInvalidFormat}, + } + + for _, tc := range testCases { + ic := &interop.Context{VM: vm.New()} + ic.VM.Estack().PushVal(tc.base) + ic.VM.Estack().PushVal(tc.num) + err := Atoi(ic) + require.True(t, errors.Is(err, tc.err), "got: %v", err) + } +} diff --git a/pkg/core/interop/interopnames/names.go b/pkg/core/interop/interopnames/names.go index 93fc94366..c3021014d 100644 --- a/pkg/core/interop/interopnames/names.go +++ b/pkg/core/interop/interopnames/names.go @@ -2,11 +2,13 @@ package interopnames // Names of all used interops. const ( + SystemBinaryAtoi = "System.Binary.Atoi" SystemBinaryBase58Decode = "System.Binary.Base58Decode" SystemBinaryBase58Encode = "System.Binary.Base58Encode" SystemBinaryBase64Decode = "System.Binary.Base64Decode" SystemBinaryBase64Encode = "System.Binary.Base64Encode" SystemBinaryDeserialize = "System.Binary.Deserialize" + SystemBinaryItoa = "System.Binary.Itoa" SystemBinarySerialize = "System.Binary.Serialize" SystemBlockchainGetBlock = "System.Blockchain.GetBlock" SystemBlockchainGetContract = "System.Blockchain.GetContract" @@ -69,11 +71,13 @@ const ( ) var names = []string{ + SystemBinaryAtoi, SystemBinaryBase58Decode, SystemBinaryBase58Encode, SystemBinaryBase64Decode, SystemBinaryBase64Encode, SystemBinaryDeserialize, + SystemBinaryItoa, SystemBinarySerialize, SystemBlockchainGetBlock, SystemBlockchainGetContract, diff --git a/pkg/core/interops.go b/pkg/core/interops.go index 0d44457cf..b60dbaa54 100644 --- a/pkg/core/interops.go +++ b/pkg/core/interops.go @@ -9,6 +9,7 @@ package core import ( "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/core/interop/binary" "github.com/nspcc-dev/neo-go/pkg/core/interop/callback" "github.com/nspcc-dev/neo-go/pkg/core/interop/contract" "github.com/nspcc-dev/neo-go/pkg/core/interop/crypto" @@ -32,11 +33,13 @@ func SpawnVM(ic *interop.Context) *vm.VM { // All lists are sorted, keep 'em this way, please. var systemInterops = []interop.Function{ + {Name: interopnames.SystemBinaryAtoi, Func: binary.Atoi, Price: 100000, ParamCount: 2}, {Name: interopnames.SystemBinaryBase58Decode, Func: runtimeDecodeBase58, Price: 100000, ParamCount: 1}, {Name: interopnames.SystemBinaryBase58Encode, Func: runtimeEncodeBase58, Price: 100000, ParamCount: 1}, {Name: interopnames.SystemBinaryBase64Decode, Func: runtimeDecodeBase64, Price: 100000, ParamCount: 1}, {Name: interopnames.SystemBinaryBase64Encode, Func: runtimeEncodeBase64, Price: 100000, ParamCount: 1}, {Name: interopnames.SystemBinaryDeserialize, Func: runtimeDeserialize, Price: 500000, ParamCount: 1}, + {Name: interopnames.SystemBinaryItoa, Func: binary.Itoa, Price: 100000, ParamCount: 2}, {Name: interopnames.SystemBinarySerialize, Func: runtimeSerialize, Price: 100000, ParamCount: 1}, {Name: interopnames.SystemBlockchainGetBlock, Func: bcGetBlock, Price: 2500000, RequiredFlags: smartcontract.AllowStates, ParamCount: 1},