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.
This commit is contained in:
Evgenii Stratonikov 2020-11-09 17:26:23 +03:00
parent ede2b05f29
commit e4bf531e3e
4 changed files with 196 additions and 0 deletions

View file

@ -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]
}
}

View file

@ -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)
}
}

View file

@ -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,

View file

@ -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},