Merge pull request #2356 from nspcc-dev/rpc-big-numbers

Allow to use big integers in RPC
This commit is contained in:
Roman Khimov 2022-02-11 13:24:35 +03:00 committed by GitHub
commit 075fd05bfc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 263 additions and 56 deletions

View file

@ -7,6 +7,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"math/big"
"strconv" "strconv"
"strings" "strings"
@ -115,7 +116,7 @@ func (p *Param) GetString() (string, error) {
if err == nil { if err == nil {
p.cache = s p.cache = s
} else { } else {
var i int var i int64
err = json.Unmarshal(p.RawMessage, &i) err = json.Unmarshal(p.RawMessage, &i)
if err == nil { if err == nil {
p.cache = i p.cache = i
@ -133,8 +134,8 @@ func (p *Param) GetString() (string, error) {
switch t := p.cache.(type) { switch t := p.cache.(type) {
case string: case string:
return t, nil return t, nil
case int: case int64:
return strconv.Itoa(t), nil return strconv.FormatInt(t, 10), nil
case bool: case bool:
if t { if t {
return "true", nil return "true", nil
@ -180,7 +181,7 @@ func (p *Param) GetBoolean() (bool, error) {
if err == nil { if err == nil {
p.cache = s p.cache = s
} else { } else {
var i int var i int64
err = json.Unmarshal(p.RawMessage, &i) err = json.Unmarshal(p.RawMessage, &i)
if err == nil { if err == nil {
p.cache = i p.cache = i
@ -195,7 +196,7 @@ func (p *Param) GetBoolean() (bool, error) {
return t, nil return t, nil
case string: case string:
return t != "", nil return t != "", nil
case int: case int64:
return t != 0, nil return t != 0, nil
default: default:
return false, errNotABool return false, errNotABool
@ -210,20 +211,46 @@ func (p *Param) GetIntStrict() (int, error) {
if p.IsNull() { if p.IsNull() {
return 0, errNotAnInt return 0, errNotAnInt
} }
if p.cache == nil { value, err := p.fillIntCache()
var i int if err != nil {
err := json.Unmarshal(p.RawMessage, &i) return 0, err
if err != nil {
return i, errNotAnInt
}
p.cache = i
} }
if i, ok := p.cache.(int); ok { if i, ok := value.(int64); ok && i == int64(int(i)) {
return i, nil return int(i), nil
} }
return 0, errNotAnInt return 0, errNotAnInt
} }
func (p *Param) fillIntCache() (interface{}, error) {
if p.cache != nil {
return p.cache, nil
}
// We could also try unmarshalling to uint64, but JSON reliably supports numbers
// up to 53 bits in size.
var i int64
err := json.Unmarshal(p.RawMessage, &i)
if err == nil {
p.cache = i
return i, nil
}
var s string
err = json.Unmarshal(p.RawMessage, &s)
if err == nil {
p.cache = s
return s, nil
}
var b bool
err = json.Unmarshal(p.RawMessage, &b)
if err == nil {
p.cache = b
return b, nil
}
return nil, errNotAnInt
}
// GetInt returns int value of the parameter or tries to cast parameter to an int value. // GetInt returns int value of the parameter or tries to cast parameter to an int value.
func (p *Param) GetInt() (int, error) { func (p *Param) GetInt() (int, error) {
if p == nil { if p == nil {
@ -232,30 +259,16 @@ func (p *Param) GetInt() (int, error) {
if p.IsNull() { if p.IsNull() {
return 0, errNotAnInt return 0, errNotAnInt
} }
if p.cache == nil { value, err := p.fillIntCache()
var i int if err != nil {
err := json.Unmarshal(p.RawMessage, &i) return 0, err
if err == nil {
p.cache = i
} else {
var s string
err = json.Unmarshal(p.RawMessage, &s)
if err == nil {
p.cache = s
} else {
var b bool
err = json.Unmarshal(p.RawMessage, &b)
if err == nil {
p.cache = b
} else {
return 0, errNotAnInt
}
}
}
} }
switch t := p.cache.(type) { switch t := value.(type) {
case int: case int64:
return t, nil if t == int64(int(t)) {
return int(t), nil
}
return 0, errNotAnInt
case string: case string:
return strconv.Atoi(t) return strconv.Atoi(t)
case bool: case bool:
@ -264,7 +277,38 @@ func (p *Param) GetInt() (int, error) {
} }
return 0, nil return 0, nil
default: default:
return 0, errNotAnInt panic("unreachable")
}
}
// GetBigInt returns big-interer value of the parameter.
func (p *Param) GetBigInt() (*big.Int, error) {
if p == nil {
return nil, errMissingParameter
}
if p.IsNull() {
return nil, errNotAnInt
}
value, err := p.fillIntCache()
if err != nil {
return nil, err
}
switch t := value.(type) {
case int64:
return big.NewInt(t), nil
case string:
bi, ok := new(big.Int).SetString(t, 10)
if !ok {
return nil, errNotAnInt
}
return bi, nil
case bool:
if t {
return big.NewInt(1), nil
}
return new(big.Int), nil
default:
panic("unreachable")
} }
} }

View file

@ -5,6 +5,8 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"math"
"math/big"
"testing" "testing"
"github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/core/transaction"
@ -190,6 +192,48 @@ func TestParam_UnmarshalJSON(t *testing.T) {
} }
} }
func TestGetBigInt(t *testing.T) {
maxUint64 := new(big.Int).SetUint64(math.MaxUint64)
minInt64 := big.NewInt(math.MinInt64)
testCases := []struct {
raw string
expected *big.Int
}{
{"true", big.NewInt(1)},
{"false", new(big.Int)},
{"42", big.NewInt(42)},
{`"` + minInt64.String() + `"`, minInt64},
{`"` + maxUint64.String() + `"`, maxUint64},
{`"` + minInt64.String() + `000"`, new(big.Int).Mul(minInt64, big.NewInt(1000))},
{`"` + maxUint64.String() + `000"`, new(big.Int).Mul(maxUint64, big.NewInt(1000))},
{`"abc"`, nil},
{`[]`, nil},
{`null`, nil},
}
for _, tc := range testCases {
var p Param
require.NoError(t, json.Unmarshal([]byte(tc.raw), &p))
actual, err := p.GetBigInt()
if tc.expected == nil {
require.Error(t, err)
continue
}
require.NoError(t, err)
require.Equal(t, tc.expected, actual)
expected := tc.expected.Int64()
actualInt, err := p.GetInt()
if !actual.IsInt64() || int64(int(expected)) != expected {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, int(expected), actualInt)
}
}
}
func TestGetWitness(t *testing.T) { func TestGetWitness(t *testing.T) {
accountHash, err := util.Uint160DecodeStringLE("cadb3dc2faa3ef14a13b619c9a43124755aa2569") accountHash, err := util.Uint160DecodeStringLE("cadb3dc2faa3ef14a13b619c9a43124755aa2569")
require.NoError(t, err) require.NoError(t, err)

View file

@ -63,11 +63,11 @@ func ExpandArrayIntoScript(script *io.BinWriter, slice []Param) error {
} }
emit.Bytes(script, key.Bytes()) emit.Bytes(script, key.Bytes())
case smartcontract.IntegerType: case smartcontract.IntegerType:
val, err := fp.Value.GetInt() bi, err := fp.Value.GetBigInt()
if err != nil { if err != nil {
return err return err
} }
emit.Int(script, int64(val)) emit.BigInt(script, bi)
case smartcontract.BoolType: case smartcontract.BoolType:
val, err := fp.Value.GetBoolean() // not GetBooleanStrict(), because that's the way C# code works val, err := fp.Value.GetBoolean() // not GetBooleanStrict(), because that's the way C# code works
if err != nil { if err != nil {
@ -97,7 +97,7 @@ func ExpandArrayIntoScript(script *io.BinWriter, slice []Param) error {
return fmt.Errorf("parameter type %v is not supported", fp.Type) return fmt.Errorf("parameter type %v is not supported", fp.Type)
} }
} }
return nil return script.Err
} }
// CreateFunctionInvocationScript creates a script to invoke given contract with // CreateFunctionInvocationScript creates a script to invoke given contract with

View file

@ -3,6 +3,7 @@ package request
import ( import (
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"math/big"
"testing" "testing"
"github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/io"
@ -100,6 +101,10 @@ func TestInvocationScriptCreationBad(t *testing.T) {
} }
func TestExpandArrayIntoScript(t *testing.T) { func TestExpandArrayIntoScript(t *testing.T) {
bi := new(big.Int).Lsh(big.NewInt(1), 254)
rawInt := make([]byte, 32)
rawInt[31] = 0x40
testCases := []struct { testCases := []struct {
Input []Param Input []Param
Expected []byte Expected []byte
@ -112,6 +117,10 @@ func TestExpandArrayIntoScript(t *testing.T) {
Input: []Param{{RawMessage: []byte(`{"type": "Array", "value": [{"type": "String", "value": "a"}]}`)}}, Input: []Param{{RawMessage: []byte(`{"type": "Array", "value": [{"type": "String", "value": "a"}]}`)}},
Expected: []byte{byte(opcode.PUSHDATA1), 1, byte('a'), byte(opcode.PUSH1), byte(opcode.PACK)}, Expected: []byte{byte(opcode.PUSHDATA1), 1, byte('a'), byte(opcode.PUSH1), byte(opcode.PACK)},
}, },
{
Input: []Param{{RawMessage: []byte(`{"type": "Integer", "value": "` + bi.String() + `"}`)}},
Expected: append([]byte{byte(opcode.PUSHINT256)}, rawInt...),
},
} }
for _, c := range testCases { for _, c := range testCases {
script := io.NewBufBinWriter() script := io.NewBufBinWriter()
@ -126,6 +135,10 @@ func TestExpandArrayIntoScript(t *testing.T) {
{ {
{RawMessage: []byte(`{"type": "Array", "value": null}`)}, {RawMessage: []byte(`{"type": "Array", "value": null}`)},
}, },
{
{RawMessage: []byte(`{"type": "Integer", "value": "` +
new(big.Int).Lsh(big.NewInt(1), 255).String() + `"}`)},
},
} }
for _, c := range errorCases { for _, c := range errorCases {
script := io.NewBufBinWriter() script := io.NewBufBinWriter()

View file

@ -52,18 +52,43 @@ func padRight(s int, buf []byte) []byte {
// Int emits a int type to the given buffer. // Int emits a int type to the given buffer.
func Int(w *io.BinWriter, i int64) { func Int(w *io.BinWriter, i int64) {
if smallInt(w, i) {
return
}
bigInt(w, big.NewInt(i), false)
}
// BigInt emits big-integer to the given buffer.
func BigInt(w *io.BinWriter, n *big.Int) {
bigInt(w, n, true)
}
func smallInt(w *io.BinWriter, i int64) bool {
switch { switch {
case i == -1: case i == -1:
Opcodes(w, opcode.PUSHM1) Opcodes(w, opcode.PUSHM1)
case i >= 0 && i < 16: case i >= 0 && i < 16:
val := opcode.Opcode(int(opcode.PUSH1) - 1 + int(i)) val := opcode.Opcode(int(opcode.PUSH0) + int(i))
Opcodes(w, val) Opcodes(w, val)
default: default:
bigInt(w, big.NewInt(i)) return false
} }
return true
} }
func bigInt(w *io.BinWriter, n *big.Int) { func bigInt(w *io.BinWriter, n *big.Int, trySmall bool) {
if w.Err != nil {
return
}
if trySmall && n.IsInt64() && smallInt(w, n.Int64()) {
return
}
if err := stackitem.CheckIntegerSize(n); err != nil {
w.Err = err
return
}
buf := bigint.ToPreallocatedBytes(n, make([]byte, 0, 32)) buf := bigint.ToPreallocatedBytes(n, make([]byte, 0, 32))
if len(buf) == 0 { if len(buf) == 0 {
Opcodes(w, opcode.PUSH0) Opcodes(w, opcode.PUSH0)
@ -101,7 +126,7 @@ func Array(w *io.BinWriter, es ...interface{}) {
case int: case int:
Int(w, int64(e)) Int(w, int64(e))
case *big.Int: case *big.Int:
bigInt(w, e) BigInt(w, e)
case string: case string:
String(w, e) String(w, e)
case util.Uint160: case util.Uint160:

View file

@ -86,6 +86,76 @@ func TestEmitInt(t *testing.T) {
}) })
} }
func TestEmitBigInt(t *testing.T) {
t.Run("biggest positive number", func(t *testing.T) {
buf := io.NewBufBinWriter()
bi := big.NewInt(1)
bi.Lsh(bi, 255)
bi.Sub(bi, big.NewInt(1))
// sanity check
require.NotPanics(t, func() { stackitem.NewBigInteger(bi) })
BigInt(buf.BinWriter, bi)
require.NoError(t, buf.Err)
expected := make([]byte, 33)
expected[0] = byte(opcode.PUSHINT256)
for i := 1; i < 32; i++ {
expected[i] = 0xFF
}
expected[32] = 0x7F
require.Equal(t, expected, buf.Bytes())
})
t.Run("smallest negative number", func(t *testing.T) {
buf := io.NewBufBinWriter()
bi := big.NewInt(-1)
bi.Lsh(bi, 255)
// sanity check
require.NotPanics(t, func() { stackitem.NewBigInteger(bi) })
BigInt(buf.BinWriter, bi)
require.NoError(t, buf.Err)
expected := make([]byte, 33)
expected[0] = byte(opcode.PUSHINT256)
expected[32] = 0x80
require.Equal(t, expected, buf.Bytes())
})
t.Run("biggest positive number plus 1", func(t *testing.T) {
buf := io.NewBufBinWriter()
bi := big.NewInt(1)
bi.Lsh(bi, 255)
// sanity check
require.Panics(t, func() { stackitem.NewBigInteger(bi) })
BigInt(buf.BinWriter, bi)
require.Error(t, buf.Err)
t.Run("do not clear previous error", func(t *testing.T) {
buf.Reset()
expected := errors.New("expected")
buf.Err = expected
BigInt(buf.BinWriter, bi)
require.Equal(t, expected, buf.Err)
})
})
t.Run("smallest negative number minus 1", func(t *testing.T) {
buf := io.NewBufBinWriter()
bi := big.NewInt(-1)
bi.Lsh(bi, 255)
bi.Sub(bi, big.NewInt(1))
// sanity check
require.Panics(t, func() { stackitem.NewBigInteger(bi) })
BigInt(buf.BinWriter, bi)
require.Error(t, buf.Err)
})
}
func getSlice(n int) []byte { func getSlice(n int) []byte {
data := make([]byte, n) data := make([]byte, n)
for i := range data { for i := range data {

View file

@ -384,21 +384,32 @@ type BigInteger big.Int
// NewBigInteger returns an new BigInteger object. // NewBigInteger returns an new BigInteger object.
func NewBigInteger(value *big.Int) *BigInteger { func NewBigInteger(value *big.Int) *BigInteger {
// There are 2 cases, when `BitLen` differs from actual size: if err := CheckIntegerSize(value); err != nil {
// 1. Positive integer with highest bit on byte boundary = 1. panic(err)
// 2. Negative integer with highest bit on byte boundary = 1
// minus some value. (-0x80 -> 0x80, -0x7F -> 0x81, -0x81 -> 0x7FFF).
sz := value.BitLen()
if sz > MaxBigIntegerSizeBits {
panic(errTooBigInteger)
} else if sz == MaxBigIntegerSizeBits {
if value.Sign() == 1 || value.TrailingZeroBits() != MaxBigIntegerSizeBits-1 {
panic(errTooBigInteger)
}
} }
return (*BigInteger)(value) return (*BigInteger)(value)
} }
// CheckIntegerSize checks that value size doesn't exceed VM limit for Interer.
func CheckIntegerSize(value *big.Int) error {
// There are 2 cases, when `BitLen` differs from actual size:
// 1. Positive integer with the highest bit on byte boundary = 1.
// 2. Negative integer with the highest bit on byte boundary = 1
// minus some value. (-0x80 -> 0x80, -0x7F -> 0x81, -0x81 -> 0x7FFF).
sz := value.BitLen()
// This check is not required, just an optimization for the common case.
if sz < MaxBigIntegerSizeBits {
return nil
}
if sz > MaxBigIntegerSizeBits {
return errTooBigInteger
}
if value.Sign() == 1 || value.TrailingZeroBits() != MaxBigIntegerSizeBits-1 {
return errTooBigInteger
}
return nil
}
// Big casts i to the big.Int type. // Big casts i to the big.Int type.
func (i *BigInteger) Big() *big.Int { func (i *BigInteger) Big() *big.Int {
return (*big.Int)(i) return (*big.Int)(i)