Merge pull request #2413 from nspcc-dev/smartcontract-big-numbers

smartcontract: allow to use `*big.Int` numbers for integers
This commit is contained in:
Roman Khimov 2022-04-01 10:38:33 +03:00 committed by GitHub
commit 2d60d4021b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 86 additions and 286 deletions

View file

@ -5,6 +5,7 @@ package client
import (
"errors"
"fmt"
"math/big"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
@ -55,11 +56,11 @@ func (c *Client) GetDesignatedByRole(role noderoles.Role, index uint32) (keys.Pu
result, err := c.InvokeFunction(rmHash, "getDesignatedByRole", []smartcontract.Parameter{
{
Type: smartcontract.IntegerType,
Value: int64(role),
Value: big.NewInt(int64(role)),
},
{
Type: smartcontract.IntegerType,
Value: int64(index),
Value: big.NewInt(int64(index)),
},
}, nil)
if err != nil {
@ -84,7 +85,7 @@ func (c *Client) NNSResolve(nnsHash util.Uint160, name string, typ nns.RecordTyp
},
{
Type: smartcontract.IntegerType,
Value: int64(typ),
Value: big.NewInt(int64(typ)),
},
}, nil)
if err != nil {

View file

@ -1,72 +0,0 @@
package smartcontract
import (
"fmt"
"math/big"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
// ParameterFromStackItem converts stackitem.Item to Parameter.
func ParameterFromStackItem(i stackitem.Item, seen map[stackitem.Item]bool) Parameter {
switch t := i.(type) {
case stackitem.Null, *stackitem.Pointer:
return NewParameter(AnyType)
case *stackitem.BigInteger:
return Parameter{
Type: IntegerType,
Value: i.Value().(*big.Int).Int64(),
}
case stackitem.Bool:
return Parameter{
Type: BoolType,
Value: i.Value().(bool),
}
case *stackitem.ByteArray:
return Parameter{
Type: ByteArrayType,
Value: i.Value().([]byte),
}
case *stackitem.Interop:
return Parameter{
Type: InteropInterfaceType,
Value: nil,
}
case *stackitem.Buffer:
return Parameter{
Type: ByteArrayType,
Value: i.Value().([]byte),
}
case *stackitem.Struct, *stackitem.Array:
var value []Parameter
if !seen[i] {
seen[i] = true
for _, stackItem := range i.Value().([]stackitem.Item) {
parameter := ParameterFromStackItem(stackItem, seen)
value = append(value, parameter)
}
}
return Parameter{
Type: ArrayType,
Value: value,
}
case *stackitem.Map:
value := make([]ParameterPair, 0)
if !seen[i] {
seen[i] = true
for _, element := range i.Value().([]stackitem.MapElement) {
value = append(value, ParameterPair{
Key: ParameterFromStackItem(element.Key, seen),
Value: ParameterFromStackItem(element.Value, seen),
})
}
}
return Parameter{
Type: MapType,
Value: value,
}
default:
panic(fmt.Sprintf("unknown stack item type: %v", t))
}
}

View file

@ -1,79 +0,0 @@
package smartcontract
import (
"math/big"
"testing"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/assert"
)
var toContractParameterTestCases = []struct {
input stackitem.Item
result Parameter
}{
{
input: stackitem.NewStruct([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(1)),
stackitem.NewBool(true),
}),
result: Parameter{Type: ArrayType, Value: []Parameter{
{Type: IntegerType, Value: int64(1)},
{Type: BoolType, Value: true},
}},
},
{
input: stackitem.NewBool(false),
result: Parameter{Type: BoolType, Value: false},
},
{
input: stackitem.NewByteArray([]byte{0x01, 0x02, 0x03}),
result: Parameter{Type: ByteArrayType, Value: []byte{0x01, 0x02, 0x03}},
},
{
input: stackitem.NewBuffer([]byte{0x01, 0x02, 0x03}),
result: Parameter{Type: ByteArrayType, Value: []byte{0x01, 0x02, 0x03}},
},
{
input: stackitem.NewArray([]stackitem.Item{stackitem.NewBigInteger(big.NewInt(2)), stackitem.NewBool(true)}),
result: Parameter{Type: ArrayType, Value: []Parameter{
{Type: IntegerType, Value: int64(2)},
{Type: BoolType, Value: true},
}},
},
{
input: stackitem.NewInterop(nil),
result: Parameter{Type: InteropInterfaceType, Value: nil},
},
{
input: stackitem.NewMapWithValue([]stackitem.MapElement{
{Key: stackitem.NewBigInteger(big.NewInt(1)), Value: stackitem.NewBool(true)},
{Key: stackitem.NewByteArray([]byte("qwerty")), Value: stackitem.NewBigInteger(big.NewInt(3))},
{Key: stackitem.NewBool(true), Value: stackitem.NewBool(false)},
}),
result: Parameter{
Type: MapType,
Value: []ParameterPair{
{
Key: Parameter{Type: IntegerType, Value: int64(1)},
Value: Parameter{Type: BoolType, Value: true},
}, {
Key: Parameter{Type: ByteArrayType, Value: []byte("qwerty")},
Value: Parameter{Type: IntegerType, Value: int64(3)},
}, {
Key: Parameter{Type: BoolType, Value: true},
Value: Parameter{Type: BoolType, Value: false},
},
},
},
},
}
func TestToContractParameter(t *testing.T) {
for _, tc := range toContractParameterTestCases {
seen := make(map[stackitem.Item]bool)
res := ParameterFromStackItem(tc.input, seen)
assert.Equal(t, res, tc.result)
}
}

View file

@ -5,13 +5,14 @@ import (
"encoding/json"
"errors"
"fmt"
"strconv"
"math/big"
"strings"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
// ParamType represents the Type of the smart contract parameter.
@ -208,7 +209,11 @@ func adjustValToType(typ ParamType, val string) (interface{}, error) {
return nil, errors.New("invalid boolean value")
}
case IntegerType:
return strconv.ParseInt(val, 10, 64)
bi, ok := new(big.Int).SetString(val, 10)
if !ok || stackitem.CheckIntegerSize(bi) != nil {
return nil, errors.New("invalid integer value")
}
return bi, nil
case Hash160Type:
u, err := address.StringToUint160(val)
if err == nil {
@ -250,8 +255,8 @@ func adjustValToType(typ ParamType, val string) (interface{}, error) {
func inferParamType(val string) ParamType {
var err error
_, err = strconv.Atoi(val)
if err == nil {
bi, ok := new(big.Int).SetString(val, 10)
if ok && stackitem.CheckIntegerSize(bi) == nil {
return IntegerType
}

View file

@ -2,9 +2,11 @@ package smartcontract
import (
"encoding/hex"
"math/big"
"testing"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -76,6 +78,7 @@ func TestParseParamType(t *testing.T) {
}
func TestInferParamType(t *testing.T) {
bi := new(big.Int).Lsh(big.NewInt(1), stackitem.MaxBigIntegerSizeBits-2)
var inouts = []struct {
in string
out ParamType
@ -88,6 +91,15 @@ func TestInferParamType(t *testing.T) {
}, {
in: "0",
out: IntegerType,
}, {
in: "8765432187654321111",
out: IntegerType,
}, {
in: bi.String(),
out: IntegerType,
}, {
in: bi.String() + "0", // big for Integer but is still a valid hex
out: ByteArrayType,
}, {
in: "2e10",
out: ByteArrayType,
@ -150,6 +162,8 @@ func TestInferParamType(t *testing.T) {
}
func TestAdjustValToType(t *testing.T) {
bi := big.NewInt(1).Lsh(big.NewInt(1), stackitem.MaxBigIntegerSizeBits-2)
var inouts = []struct {
typ ParamType
val string
@ -190,15 +204,23 @@ func TestAdjustValToType(t *testing.T) {
}, {
typ: IntegerType,
val: "0",
out: int64(0),
out: big.NewInt(0),
}, {
typ: IntegerType,
val: "42",
out: int64(42),
out: big.NewInt(42),
}, {
typ: IntegerType,
val: "-42",
out: int64(-42),
out: big.NewInt(-42),
}, {
typ: IntegerType,
val: bi.String(),
out: bi,
}, {
typ: IntegerType,
val: bi.String() + "0",
err: true,
}, {
typ: IntegerType,
val: "q",

View file

@ -8,16 +8,16 @@ import (
"encoding/json"
"errors"
"fmt"
"math"
"math/big"
"math/bits"
"os"
"strconv"
"strings"
"unicode/utf8"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
// Parameter represents a smart contract parameter.
@ -65,13 +65,12 @@ func (p Parameter) MarshalJSON() ([]byte, error) {
case BoolType, StringType, Hash160Type, Hash256Type:
resultRawValue, resultErr = json.Marshal(p.Value)
case IntegerType:
val, ok := p.Value.(int64)
val, ok := p.Value.(*big.Int)
if !ok {
resultErr = errors.New("invalid integer value")
break
}
valStr := strconv.FormatInt(val, 10)
resultRawValue = json.RawMessage(`"` + valStr + `"`)
resultRawValue = json.RawMessage(`"` + val.String() + `"`)
case PublicKeyType, ByteArrayType, SignatureType:
if p.Type == PublicKeyType {
resultRawValue, resultErr = json.Marshal(hex.EncodeToString(p.Value.([]byte)))
@ -145,17 +144,22 @@ func (p *Parameter) UnmarshalJSON(data []byte) (err error) {
p.Value = s
case IntegerType:
if err = json.Unmarshal(r.Value, &i); err == nil {
p.Value = i
p.Value = big.NewInt(i)
return
}
// sometimes integer comes as string
if err = json.Unmarshal(r.Value, &s); err != nil {
return
if jErr := json.Unmarshal(r.Value, &s); jErr != nil {
return jErr
}
if i, err = strconv.ParseInt(s, 10, 64); err != nil {
return
bi, ok := new(big.Int).SetString(s, 10)
if !ok {
// In this case previous err should mean string contains non-digit characters.
return err
}
err = stackitem.CheckIntegerSize(bi)
if err == nil {
p.Value = bi
}
p.Value = i
case ArrayType:
// https://github.com/neo-project/neo/blob/3d59ecca5a8deb057bdad94b3028a6d5e25ac088/neo/Network/RPC/RpcServer.cs#L67
var rs []Parameter
@ -190,87 +194,6 @@ func (p *Parameter) UnmarshalJSON(data []byte) (err error) {
return
}
// EncodeBinary implements io.Serializable interface.
func (p *Parameter) EncodeBinary(w *io.BinWriter) {
w.WriteB(byte(p.Type))
switch p.Type {
case BoolType:
w.WriteBool(p.Value.(bool))
case ByteArrayType, PublicKeyType, SignatureType:
if p.Value == nil {
w.WriteVarUint(math.MaxUint64)
} else {
w.WriteVarBytes(p.Value.([]byte))
}
case StringType:
w.WriteString(p.Value.(string))
case IntegerType:
w.WriteU64LE(uint64(p.Value.(int64)))
case ArrayType:
w.WriteArray(p.Value.([]Parameter))
case MapType:
w.WriteArray(p.Value.([]ParameterPair))
case Hash160Type:
w.WriteBytes(p.Value.(util.Uint160).BytesBE())
case Hash256Type:
w.WriteBytes(p.Value.(util.Uint256).BytesBE())
case InteropInterfaceType, AnyType:
default:
w.Err = fmt.Errorf("unknown type: %x", p.Type)
}
}
// DecodeBinary implements io.Serializable interface.
func (p *Parameter) DecodeBinary(r *io.BinReader) {
p.Type = ParamType(r.ReadB())
switch p.Type {
case BoolType:
p.Value = r.ReadBool()
case ByteArrayType, PublicKeyType, SignatureType:
ln := r.ReadVarUint()
if ln != math.MaxUint64 {
b := make([]byte, ln)
r.ReadBytes(b)
p.Value = b
}
case StringType:
p.Value = r.ReadString()
case IntegerType:
p.Value = int64(r.ReadU64LE())
case ArrayType:
ps := []Parameter{}
r.ReadArray(&ps)
p.Value = ps
case MapType:
ps := []ParameterPair{}
r.ReadArray(&ps)
p.Value = ps
case Hash160Type:
var u util.Uint160
r.ReadBytes(u[:])
p.Value = u
case Hash256Type:
var u util.Uint256
r.ReadBytes(u[:])
p.Value = u
case InteropInterfaceType, AnyType:
default:
r.Err = fmt.Errorf("unknown type: %x", p.Type)
}
}
// EncodeBinary implements io.Serializable interface.
func (p *ParameterPair) EncodeBinary(w *io.BinWriter) {
p.Key.EncodeBinary(w)
p.Value.EncodeBinary(w)
}
// DecodeBinary implements io.Serializable interface.
func (p *ParameterPair) DecodeBinary(r *io.BinReader) {
p.Key.DecodeBinary(r)
p.Value.DecodeBinary(r)
}
// Params is an array of Parameter (TODO: drop it?).
type Params []Parameter
@ -318,6 +241,9 @@ func (p Parameter) TryParse(dest interface{}) error {
return err
}
return nil
case **big.Int:
*dest = bigint.FromBytes(data)
return nil
case *int64, *int32, *int16, *int8, *int, *uint64, *uint32, *uint16, *uint8, *uint:
var size int
switch dest.(type) {

View file

@ -5,10 +5,11 @@ import (
"encoding/hex"
"encoding/json"
"math"
"math/big"
"reflect"
"strings"
"testing"
"github.com/nspcc-dev/neo-go/internal/testserdes"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/util"
@ -22,9 +23,13 @@ var marshalJSONTestCases = []struct {
result string
}{
{
input: Parameter{Type: IntegerType, Value: int64(12345)},
input: Parameter{Type: IntegerType, Value: big.NewInt(12345)},
result: `{"type":"Integer","value":12345}`,
},
{
input: Parameter{Type: IntegerType, Value: new(big.Int).Lsh(big.NewInt(1), 254)},
result: `{"type":"Integer","value":"` + new(big.Int).Lsh(big.NewInt(1), 254).String() + `"}`,
},
{
input: Parameter{Type: StringType, Value: "Some string"},
result: `{"type":"String","value":"Some string"}`,
@ -57,7 +62,7 @@ var marshalJSONTestCases = []struct {
Type: ArrayType,
Value: []Parameter{
{Type: StringType, Value: "str 1"},
{Type: IntegerType, Value: int64(2)},
{Type: IntegerType, Value: big.NewInt(2)},
},
},
result: `{"type":"Array","value":[{"type":"String","value":"str 1"},{"type":"Integer","value":2}]}`,
@ -84,7 +89,7 @@ var marshalJSONTestCases = []struct {
Value: []ParameterPair{
{
Key: Parameter{Type: StringType, Value: "key1"},
Value: Parameter{Type: IntegerType, Value: int64(1)},
Value: Parameter{Type: IntegerType, Value: big.NewInt(1)},
},
{
Key: Parameter{Type: StringType, Value: "key2"},
@ -102,7 +107,7 @@ var marshalJSONTestCases = []struct {
Key: Parameter{Type: StringType, Value: "key1"},
Value: Parameter{Type: ArrayType, Value: []Parameter{
{Type: StringType, Value: "str 1"},
{Type: IntegerType, Value: int64(2)},
{Type: IntegerType, Value: big.NewInt(2)},
}},
},
},
@ -185,11 +190,11 @@ var unmarshalJSONTestCases = []struct {
},
{
input: `{"type":"Integer","value":12345}`,
result: Parameter{Type: IntegerType, Value: int64(12345)},
result: Parameter{Type: IntegerType, Value: big.NewInt(12345)},
},
{
input: `{"type":"Integer","value":"12345"}`,
result: Parameter{Type: IntegerType, Value: int64(12345)},
result: Parameter{Type: IntegerType, Value: big.NewInt(12345)},
},
{
input: `{"type":"ByteString","value":"` + hexToBase64("010203") + `"}`,
@ -215,7 +220,7 @@ var unmarshalJSONTestCases = []struct {
Type: ArrayType,
Value: []Parameter{
{Type: StringType, Value: "str 1"},
{Type: IntegerType, Value: int64(2)},
{Type: IntegerType, Value: big.NewInt(2)},
},
},
},
@ -248,7 +253,7 @@ var unmarshalJSONTestCases = []struct {
Value: []ParameterPair{
{
Key: Parameter{Type: StringType, Value: "key1"},
Value: Parameter{Type: IntegerType, Value: int64(1)},
Value: Parameter{Type: IntegerType, Value: big.NewInt(1)},
},
{
Key: Parameter{Type: StringType, Value: "key2"},
@ -266,7 +271,7 @@ var unmarshalJSONTestCases = []struct {
Key: Parameter{Type: StringType, Value: "key1"},
Value: Parameter{Type: ArrayType, Value: []Parameter{
{Type: StringType, Value: "str 1"},
{Type: IntegerType, Value: int64(2)},
{Type: IntegerType, Value: big.NewInt(2)},
}},
},
},
@ -310,6 +315,8 @@ var unmarshalJSONErrorCases = []string{
`{"type": "String","value":1}`, // incorrect Value
`{"type": "Integer","value": "nn"}`, // incorrect Integer value
`{"type": "Integer","value": []}`, // incorrect Integer value
`{"type": "Integer","value":"` +
strings.Repeat("9", 100) + `"}`, // too big Integer
`{"type": "Array","value": 123}`, // incorrect Array value
`{"type": "Hash160","value": "0bcd"}`, // incorrect Uint160 value
`{"type": "Hash256","value": "0bcd"}`, // incorrect Uint256 value
@ -330,7 +337,7 @@ func TestParam_UnmarshalJSON(t *testing.T) {
}
for _, input := range unmarshalJSONErrorCases {
assert.Error(t, json.Unmarshal([]byte(input), &s))
assert.Error(t, json.Unmarshal([]byte(input), &s), input)
}
}
@ -370,6 +377,10 @@ var tryParseTestCases = []struct {
input: []byte{0x63, 0x78, 0x29, 0xcd, 0x0b},
expected: int64(50686687331),
},
{
input: []byte{0x63, 0x78, 0x29, 0xcd, 0x0b},
expected: big.NewInt(50686687331),
},
{
input: []byte("this is a test string"),
expected: "this is a test string",
@ -450,13 +461,13 @@ func TestNewParameterFromString(t *testing.T) {
out: Parameter{StringType, "qwerty"},
}, {
in: "42",
out: Parameter{IntegerType, int64(42)},
out: Parameter{IntegerType, big.NewInt(42)},
}, {
in: "Hello, 世界",
out: Parameter{StringType, "Hello, 世界"},
}, {
in: `\4\2`,
out: Parameter{IntegerType, int64(42)},
out: Parameter{IntegerType, big.NewInt(42)},
}, {
in: `\\4\2`,
out: Parameter{StringType, `\42`},
@ -465,7 +476,7 @@ func TestNewParameterFromString(t *testing.T) {
out: Parameter{StringType, `\42`},
}, {
in: "int:42",
out: Parameter{IntegerType, int64(42)},
out: Parameter{IntegerType, big.NewInt(42)},
}, {
in: "true",
out: Parameter{BoolType, true},
@ -514,20 +525,6 @@ func TestNewParameterFromString(t *testing.T) {
}
}
func TestEncodeDecodeBinary(t *testing.T) {
for _, tc := range marshalJSONTestCases {
testserdes.EncodeDecodeBinary(t, &tc.input, new(Parameter))
}
t.Run("unknown", func(t *testing.T) {
p := Parameter{Type: UnknownType}
_, err := testserdes.EncodeBinary(&p)
require.Error(t, err)
require.Error(t, testserdes.DecodeBinary([]byte{0xAA}, &p))
})
}
func hexToBase64(s string) string {
b, _ := hex.DecodeString(s)
return base64.StdEncoding.EncodeToString(b)
@ -544,8 +541,8 @@ func TestExpandParameterToEmitable(t *testing.T) {
Expected: true,
},
{
In: Parameter{Type: IntegerType, Value: int64(123)},
Expected: int64(123),
In: Parameter{Type: IntegerType, Value: big.NewInt(123)},
Expected: big.NewInt(123),
},
{
In: Parameter{Type: ByteArrayType, Value: []byte{1, 2, 3}},
@ -575,7 +572,7 @@ func TestExpandParameterToEmitable(t *testing.T) {
In: Parameter{Type: ArrayType, Value: []Parameter{
{
Type: IntegerType,
Value: int64(123),
Value: big.NewInt(123),
},
{
Type: ByteArrayType,
@ -591,7 +588,7 @@ func TestExpandParameterToEmitable(t *testing.T) {
},
},
}},
Expected: []interface{}{int64(123), []byte{1, 2, 3}, []interface{}{true}},
Expected: []interface{}{big.NewInt(123), []byte{1, 2, 3}, []interface{}{true}},
},
}
bw := io.NewBufBinWriter()