diff --git a/pkg/core/native/oracle_types.go b/pkg/core/native/oracle_types.go new file mode 100644 index 000000000..075eab3a3 --- /dev/null +++ b/pkg/core/native/oracle_types.go @@ -0,0 +1,225 @@ +package native + +import ( + "crypto/elliptic" + "errors" + "math/big" + "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/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" +) + +// IDList is a list of oracle request IDs. +type IDList []uint64 + +// NodeList represents list or oracle nodes. +type NodeList keys.PublicKeys + +// OracleRequest represents oracle request. +type OracleRequest struct { + OriginalTxID util.Uint256 + GasForResponse uint64 + URL string + Filter *string + CallbackContract util.Uint160 + CallbackMethod string + UserData []byte +} + +// Bytes return l serizalized to a byte-slice. +func (l IDList) Bytes() []byte { + w := io.NewBufBinWriter() + l.EncodeBinary(w.BinWriter) + return w.Bytes() +} + +// EncodeBinary implements io.Serializable. +func (l IDList) EncodeBinary(w *io.BinWriter) { + stackitem.EncodeBinaryStackItem(l.toStackItem(), w) +} + +// DecodeBinary implements io.Serializable. +func (l *IDList) DecodeBinary(r *io.BinReader) { + item := stackitem.DecodeBinaryStackItem(r) + if r.Err != nil || item == nil { + return + } + r.Err = l.fromStackItem(item) +} + +func (l IDList) toStackItem() stackitem.Item { + arr := make([]stackitem.Item, len(l)) + for i := range l { + arr[i] = stackitem.NewBigInteger(new(big.Int).SetUint64(l[i])) + } + return stackitem.NewArray(arr) +} + +func (l *IDList) fromStackItem(it stackitem.Item) error { + arr, ok := it.Value().([]stackitem.Item) + if !ok { + return errors.New("not an array") + } + *l = make(IDList, len(arr)) + for i := range arr { + bi, err := arr[i].TryInteger() + if err != nil { + return err + } + (*l)[i] = bi.Uint64() + } + return nil +} + +// Bytes return l serizalized to a byte-slice. +func (l NodeList) Bytes() []byte { + w := io.NewBufBinWriter() + l.EncodeBinary(w.BinWriter) + return w.Bytes() +} + +// EncodeBinary implements io.Serializable. +func (l NodeList) EncodeBinary(w *io.BinWriter) { + stackitem.EncodeBinaryStackItem(l.toStackItem(), w) +} + +// DecodeBinary implements io.Serializable. +func (l *NodeList) DecodeBinary(r *io.BinReader) { + item := stackitem.DecodeBinaryStackItem(r) + if r.Err != nil || item == nil { + return + } + r.Err = l.fromStackItem(item) +} + +func (l NodeList) toStackItem() stackitem.Item { + arr := make([]stackitem.Item, len(l)) + for i := range l { + arr[i] = stackitem.NewByteArray(l[i].Bytes()) + } + return stackitem.NewArray(arr) +} + +func (l *NodeList) fromStackItem(it stackitem.Item) error { + arr, ok := it.Value().([]stackitem.Item) + if !ok { + return errors.New("not an array") + } + *l = make(NodeList, len(arr)) + for i := range arr { + bs, err := arr[i].TryBytes() + if err != nil { + return err + } + pub, err := keys.NewPublicKeyFromBytes(bs, elliptic.P256()) + if err != nil { + return err + } + (*l)[i] = pub + } + return nil +} + +// Bytes return o serizalized to a byte-slice. +func (o *OracleRequest) Bytes() []byte { + w := io.NewBufBinWriter() + o.EncodeBinary(w.BinWriter) + return w.Bytes() +} + +// EncodeBinary implements io.Serializable. +func (o *OracleRequest) EncodeBinary(w *io.BinWriter) { + stackitem.EncodeBinaryStackItem(o.toStackItem(), w) +} + +// DecodeBinary implements io.Serializable. +func (o *OracleRequest) DecodeBinary(r *io.BinReader) { + item := stackitem.DecodeBinaryStackItem(r) + if r.Err != nil || item == nil { + return + } + r.Err = o.fromStackItem(item) +} + +func (o *OracleRequest) toStackItem() stackitem.Item { + filter := stackitem.Item(stackitem.Null{}) + if o.Filter != nil { + filter = stackitem.Make(*o.Filter) + } + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(o.OriginalTxID.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(o.GasForResponse)), + stackitem.Make(o.URL), + filter, + stackitem.NewByteArray(o.CallbackContract.BytesBE()), + stackitem.Make(o.CallbackMethod), + stackitem.NewByteArray(o.UserData), + }) +} + +func (o *OracleRequest) fromStackItem(it stackitem.Item) error { + arr, ok := it.Value().([]stackitem.Item) + if !ok || len(arr) < 7 { + return errors.New("not an array of needed length") + } + bs, err := arr[0].TryBytes() + if err != nil { + return err + } + o.OriginalTxID, err = util.Uint256DecodeBytesBE(bs) + if err != nil { + return err + } + + gas, err := arr[1].TryInteger() + if err != nil { + return err + } + o.GasForResponse = gas.Uint64() + + s, isNull, ok := itemToString(arr[2]) + if !ok || isNull { + return errors.New("invalid URL") + } + o.URL = s + + s, isNull, ok = itemToString(arr[3]) + if !ok { + return errors.New("invalid filter") + } else if !isNull { + filter := s + o.Filter = &filter + } + + bs, err = arr[4].TryBytes() + if err != nil { + return err + } + o.CallbackContract, err = util.Uint160DecodeBytesBE(bs) + if err != nil { + return err + } + + o.CallbackMethod, isNull, ok = itemToString(arr[5]) + if !ok || isNull { + return errors.New("invalid callback method") + } + + o.UserData, err = arr[6].TryBytes() + return err +} + +func itemToString(it stackitem.Item) (string, bool, bool) { + _, ok := it.(stackitem.Null) + if ok { + return "", true, true + } + bs, err := it.TryBytes() + if err != nil || !utf8.Valid(bs) { + return "", false, false + } + return string(bs), false, true +} diff --git a/pkg/core/native/oracle_types_test.go b/pkg/core/native/oracle_types_test.go new file mode 100644 index 000000000..1f0b70630 --- /dev/null +++ b/pkg/core/native/oracle_types_test.go @@ -0,0 +1,125 @@ +package native + +import ( + "math/big" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/internal/random" + "github.com/nspcc-dev/neo-go/pkg/internal/testserdes" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/stretchr/testify/require" +) + +func getInvalidTestFunc(actual io.Serializable, value interface{}) func(t *testing.T) { + return func(t *testing.T) { + w := io.NewBufBinWriter() + it := stackitem.Make(value) + stackitem.EncodeBinaryStackItem(it, w.BinWriter) + require.NoError(t, w.Err) + require.Error(t, testserdes.DecodeBinary(w.Bytes(), actual)) + } +} + +func TestIDList_EncodeBinary(t *testing.T) { + t.Run("Valid", func(t *testing.T) { + l := &IDList{1, 4, 5} + testserdes.EncodeDecodeBinary(t, l, new(IDList)) + }) + t.Run("Invalid", func(t *testing.T) { + t.Run("NotArray", getInvalidTestFunc(new(IDList), []byte{})) + t.Run("InvalidElement", getInvalidTestFunc(new(IDList), []stackitem.Item{stackitem.Null{}})) + t.Run("NotStackItem", func(t *testing.T) { + require.Error(t, testserdes.DecodeBinary([]byte{0x77}, new(IDList))) + }) + }) +} + +func TestNodeList_EncodeBinary(t *testing.T) { + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + pub := priv.PublicKey() + + t.Run("Valid", func(t *testing.T) { + l := &NodeList{pub} + testserdes.EncodeDecodeBinary(t, l, new(NodeList)) + }) + t.Run("Invalid", func(t *testing.T) { + t.Run("NotArray", getInvalidTestFunc(new(NodeList), []byte{})) + t.Run("InvalidElement", getInvalidTestFunc(new(NodeList), []stackitem.Item{stackitem.Null{}})) + t.Run("InvalidKey", getInvalidTestFunc(new(NodeList), + []stackitem.Item{stackitem.NewByteArray([]byte{0x9})})) + t.Run("NotStackItem", func(t *testing.T) { + require.Error(t, testserdes.DecodeBinary([]byte{0x77}, new(NodeList))) + }) + }) +} + +func TestOracleRequest_EncodeBinary(t *testing.T) { + t.Run("Valid", func(t *testing.T) { + r := &OracleRequest{ + OriginalTxID: random.Uint256(), + GasForResponse: 12345, + URL: "https://get.value", + CallbackContract: random.Uint160(), + CallbackMethod: "method", + UserData: []byte{1, 2, 3}, + } + testserdes.EncodeDecodeBinary(t, r, new(OracleRequest)) + + t.Run("WithFilter", func(t *testing.T) { + s := "filter" + r.Filter = &s + testserdes.EncodeDecodeBinary(t, r, new(OracleRequest)) + }) + }) + t.Run("Invalid", func(t *testing.T) { + w := io.NewBufBinWriter() + t.Run("NotArray", func(t *testing.T) { + w.Reset() + it := stackitem.NewByteArray([]byte{}) + stackitem.EncodeBinaryStackItem(it, w.BinWriter) + require.Error(t, testserdes.DecodeBinary(w.Bytes(), new(OracleRequest))) + }) + t.Run("NotStackItem", func(t *testing.T) { + w.Reset() + require.Error(t, testserdes.DecodeBinary([]byte{0x77}, new(OracleRequest))) + }) + + items := []stackitem.Item{ + stackitem.NewByteArray(random.Uint256().BytesBE()), + stackitem.NewBigInteger(big.NewInt(123)), + stackitem.Make("url"), + stackitem.Null{}, + stackitem.NewByteArray(random.Uint160().BytesBE()), + stackitem.Make("method"), + stackitem.NewByteArray([]byte{1, 2, 3}), + } + arrItem := stackitem.NewArray(items) + runInvalid := func(i int, elem stackitem.Item) func(t *testing.T) { + return func(t *testing.T) { + w.Reset() + before := items[i] + items[i] = elem + stackitem.EncodeBinaryStackItem(arrItem, w.BinWriter) + items[i] = before + require.Error(t, testserdes.DecodeBinary(w.Bytes(), new(OracleRequest))) + } + } + t.Run("TxID", func(t *testing.T) { + t.Run("Type", runInvalid(0, stackitem.NewMap())) + t.Run("Length", runInvalid(0, stackitem.NewByteArray([]byte{0, 1, 2}))) + }) + t.Run("Gas", runInvalid(1, stackitem.NewMap())) + t.Run("URL", runInvalid(2, stackitem.NewMap())) + t.Run("Filter", runInvalid(3, stackitem.NewMap())) + t.Run("Contract", func(t *testing.T) { + t.Run("Type", runInvalid(4, stackitem.NewMap())) + t.Run("Length", runInvalid(4, stackitem.NewByteArray([]byte{0, 1, 2}))) + }) + t.Run("Method", runInvalid(5, stackitem.NewMap())) + t.Run("UserData", runInvalid(6, stackitem.NewMap())) + }) + +}