From 877b8ece63eca48e2a8d63949d9db3f05562f0ab Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 21 Sep 2020 17:00:33 +0300 Subject: [PATCH 1/9] core: move contractCall* to a separate package --- pkg/core/interop/contract/call.go | 89 +++++++++++++++++++++++++++++++ pkg/core/interop_system.go | 79 --------------------------- pkg/core/interop_system_test.go | 15 +++--- pkg/core/interops.go | 5 +- pkg/core/native_contract_test.go | 5 +- 5 files changed, 103 insertions(+), 90 deletions(-) create mode 100644 pkg/core/interop/contract/call.go diff --git a/pkg/core/interop/contract/call.go b/pkg/core/interop/contract/call.go new file mode 100644 index 000000000..86adf6df8 --- /dev/null +++ b/pkg/core/interop/contract/call.go @@ -0,0 +1,89 @@ +package contract + +import ( + "errors" + "fmt" + "strings" + + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" +) + +// Call calls a contract. +func Call(ic *interop.Context) error { + h := ic.VM.Estack().Pop().Bytes() + method := ic.VM.Estack().Pop().String() + args := ic.VM.Estack().Pop().Array() + return callExInternal(ic, h, method, args, smartcontract.All) +} + +// CallEx calls a contract with flags. +func CallEx(ic *interop.Context) error { + h := ic.VM.Estack().Pop().Bytes() + method := ic.VM.Estack().Pop().String() + args := ic.VM.Estack().Pop().Array() + flags := smartcontract.CallFlag(int32(ic.VM.Estack().Pop().BigInt().Int64())) + if flags&^smartcontract.All != 0 { + return errors.New("call flags out of range") + } + return callExInternal(ic, h, method, args, flags) +} + +func callExInternal(ic *interop.Context, h []byte, name string, args []stackitem.Item, f smartcontract.CallFlag) error { + u, err := util.Uint160DecodeBytesBE(h) + if err != nil { + return errors.New("invalid contract hash") + } + cs, err := ic.DAO.GetContractState(u) + if err != nil { + return errors.New("contract not found") + } + if strings.HasPrefix(name, "_") { + return errors.New("invalid method name (starts with '_')") + } + md := cs.Manifest.ABI.GetMethod(name) + if md == nil { + return fmt.Errorf("method '%s' not found", name) + } + curr, err := ic.DAO.GetContractState(ic.VM.GetCurrentScriptHash()) + if err == nil { + if !curr.Manifest.CanCall(&cs.Manifest, name) { + return errors.New("disallowed method call") + } + } + + if len(args) != len(md.Parameters) { + return fmt.Errorf("invalid argument count: %d (expected %d)", len(args), len(md.Parameters)) + } + + ic.Invocations[u]++ + ic.VM.LoadScriptWithHash(cs.Script, u, ic.VM.Context().GetCallFlags()&f) + var isNative bool + for i := range ic.Natives { + if ic.Natives[i].Metadata().Hash.Equals(u) { + isNative = true + break + } + } + if isNative { + ic.VM.Estack().PushVal(args) + ic.VM.Estack().PushVal(name) + } else { + for i := len(args) - 1; i >= 0; i-- { + ic.VM.Estack().PushVal(args[i]) + } + // use Jump not Call here because context was loaded in LoadScript above. + ic.VM.Jump(ic.VM.Context(), md.Offset) + ic.VM.Context().CheckReturn = true + } + + md = cs.Manifest.ABI.GetMethod(manifest.MethodInit) + if md != nil { + ic.VM.Call(ic.VM.Context(), md.Offset) + } + + return nil +} diff --git a/pkg/core/interop_system.go b/pkg/core/interop_system.go index 300f59556..efffc3fdb 100644 --- a/pkg/core/interop_system.go +++ b/pkg/core/interop_system.go @@ -6,7 +6,6 @@ import ( "fmt" "math" "math/big" - "strings" "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/blockchainer" @@ -15,8 +14,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" - "github.com/nspcc-dev/neo-go/pkg/smartcontract" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" @@ -454,82 +451,6 @@ func storageContextAsReadOnly(ic *interop.Context) error { return nil } -// contractCall calls a contract. -func contractCall(ic *interop.Context) error { - h := ic.VM.Estack().Pop().Bytes() - method := ic.VM.Estack().Pop().String() - args := ic.VM.Estack().Pop().Array() - return contractCallExInternal(ic, h, method, args, smartcontract.All) -} - -// contractCallEx calls a contract with flags. -func contractCallEx(ic *interop.Context) error { - h := ic.VM.Estack().Pop().Bytes() - method := ic.VM.Estack().Pop().String() - args := ic.VM.Estack().Pop().Array() - flags := smartcontract.CallFlag(int32(ic.VM.Estack().Pop().BigInt().Int64())) - if flags&^smartcontract.All != 0 { - return errors.New("call flags out of range") - } - return contractCallExInternal(ic, h, method, args, flags) -} - -func contractCallExInternal(ic *interop.Context, h []byte, name string, args []stackitem.Item, f smartcontract.CallFlag) error { - u, err := util.Uint160DecodeBytesBE(h) - if err != nil { - return errors.New("invalid contract hash") - } - cs, err := ic.DAO.GetContractState(u) - if err != nil { - return errors.New("contract not found") - } - if strings.HasPrefix(name, "_") { - return errors.New("invalid method name (starts with '_')") - } - md := cs.Manifest.ABI.GetMethod(name) - if md == nil { - return fmt.Errorf("method '%s' not found", name) - } - curr, err := ic.DAO.GetContractState(ic.VM.GetCurrentScriptHash()) - if err == nil { - if !curr.Manifest.CanCall(&cs.Manifest, name) { - return errors.New("disallowed method call") - } - } - - if len(args) != len(md.Parameters) { - return fmt.Errorf("invalid argument count: %d (expected %d)", len(args), len(md.Parameters)) - } - - ic.Invocations[u]++ - ic.VM.LoadScriptWithHash(cs.Script, u, ic.VM.Context().GetCallFlags()&f) - var isNative bool - for i := range ic.Natives { - if ic.Natives[i].Metadata().Hash.Equals(u) { - isNative = true - break - } - } - if isNative { - ic.VM.Estack().PushVal(args) - ic.VM.Estack().PushVal(name) - } else { - for i := len(args) - 1; i >= 0; i-- { - ic.VM.Estack().PushVal(args[i]) - } - // use Jump not Call here because context was loaded in LoadScript above. - ic.VM.Jump(ic.VM.Context(), md.Offset) - ic.VM.Context().CheckReturn = true - } - - md = cs.Manifest.ABI.GetMethod(manifest.MethodInit) - if md != nil { - ic.VM.Call(ic.VM.Context(), md.Offset) - } - - return nil -} - // contractDestroy destroys a contract. func contractDestroy(ic *interop.Context) error { hash := ic.VM.GetCurrentScriptHash() diff --git a/pkg/core/interop_system_test.go b/pkg/core/interop_system_test.go index 56eb18ade..eac5a680c 100644 --- a/pkg/core/interop_system_test.go +++ b/pkg/core/interop_system_test.go @@ -8,6 +8,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/core/interop" "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/runtime" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/transaction" @@ -441,7 +442,7 @@ func TestContractCall(t *testing.T) { ic.VM.Estack().PushVal(addArgs) ic.VM.Estack().PushVal("add") ic.VM.Estack().PushVal(h.BytesBE()) - require.NoError(t, contractCall(ic)) + require.NoError(t, contract.Call(ic)) require.NoError(t, ic.VM.Run()) require.Equal(t, 2, ic.VM.Estack().Len()) require.Equal(t, big.NewInt(3), ic.VM.Estack().Pop().Value()) @@ -454,7 +455,7 @@ func TestContractCall(t *testing.T) { ic.VM.Estack().PushVal(addArgs) ic.VM.Estack().PushVal("add") ic.VM.Estack().PushVal(h.BytesBE()) - require.Error(t, contractCallEx(ic)) + require.Error(t, contract.CallEx(ic)) }) runInvalid := func(args ...interface{}) func(t *testing.T) { @@ -466,7 +467,7 @@ func TestContractCall(t *testing.T) { // interops can both return error and panic, // we don't care which kind of error has occurred require.Panics(t, func() { - err := contractCall(ic) + err := contract.Call(ic) if err != nil { panic(err) } @@ -491,7 +492,7 @@ func TestContractCall(t *testing.T) { ic.VM.Estack().PushVal(stackitem.NewArray(nil)) ic.VM.Estack().PushVal("invalidReturn") ic.VM.Estack().PushVal(h.BytesBE()) - require.NoError(t, contractCall(ic)) + require.NoError(t, contract.Call(ic)) require.Error(t, ic.VM.Run()) }) t.Run("Void", func(t *testing.T) { @@ -499,7 +500,7 @@ func TestContractCall(t *testing.T) { ic.VM.Estack().PushVal(stackitem.NewArray(nil)) ic.VM.Estack().PushVal("justReturn") ic.VM.Estack().PushVal(h.BytesBE()) - require.NoError(t, contractCall(ic)) + require.NoError(t, contract.Call(ic)) require.NoError(t, ic.VM.Run()) require.Equal(t, 2, ic.VM.Estack().Len()) require.Equal(t, stackitem.Null{}, ic.VM.Estack().Pop().Item()) @@ -512,7 +513,7 @@ func TestContractCall(t *testing.T) { ic.VM.Estack().PushVal(stackitem.NewArray(nil)) ic.VM.Estack().PushVal("drop") ic.VM.Estack().PushVal(h.BytesBE()) - require.NoError(t, contractCall(ic)) + require.NoError(t, contract.Call(ic)) require.Error(t, ic.VM.Run()) }) @@ -523,7 +524,7 @@ func TestContractCall(t *testing.T) { ic.VM.Estack().PushVal(stackitem.NewArray([]stackitem.Item{stackitem.Make(5)})) ic.VM.Estack().PushVal("add3") ic.VM.Estack().PushVal(h.BytesBE()) - require.NoError(t, contractCall(ic)) + require.NoError(t, contract.Call(ic)) require.NoError(t, ic.VM.Run()) require.Equal(t, 2, ic.VM.Estack().Len()) require.Equal(t, big.NewInt(8), ic.VM.Estack().Pop().Value()) diff --git a/pkg/core/interops.go b/pkg/core/interops.go index 4c6ab5af8..0d44457cf 100644 --- a/pkg/core/interops.go +++ b/pkg/core/interops.go @@ -10,6 +10,7 @@ package core import ( "github.com/nspcc-dev/neo-go/pkg/core/interop" "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" "github.com/nspcc-dev/neo-go/pkg/core/interop/enumerator" "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" @@ -53,9 +54,9 @@ var systemInterops = []interop.Function{ {Name: interopnames.SystemCallbackCreateFromMethod, Func: callback.CreateFromMethod, Price: 1000000, ParamCount: 2, DisallowCallback: true}, {Name: interopnames.SystemCallbackCreateFromSyscall, Func: callback.CreateFromSyscall, Price: 400, ParamCount: 1, DisallowCallback: true}, {Name: interopnames.SystemCallbackInvoke, Func: callback.Invoke, Price: 1000000, ParamCount: 2, DisallowCallback: true}, - {Name: interopnames.SystemContractCall, Func: contractCall, Price: 1000000, + {Name: interopnames.SystemContractCall, Func: contract.Call, Price: 1000000, RequiredFlags: smartcontract.AllowCall, ParamCount: 3, DisallowCallback: true}, - {Name: interopnames.SystemContractCallEx, Func: contractCallEx, Price: 1000000, + {Name: interopnames.SystemContractCallEx, Func: contract.CallEx, Price: 1000000, RequiredFlags: smartcontract.AllowCall, ParamCount: 4, DisallowCallback: true}, {Name: interopnames.SystemContractCreate, Func: contractCreate, Price: 0, RequiredFlags: smartcontract.AllowModifyStates, ParamCount: 2, DisallowCallback: true}, diff --git a/pkg/core/native_contract_test.go b/pkg/core/native_contract_test.go index 222af4c4d..b3c5d545d 100644 --- a/pkg/core/native_contract_test.go +++ b/pkg/core/native_contract_test.go @@ -7,6 +7,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/core/interop/contract" "github.com/nspcc-dev/neo-go/pkg/core/native" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/storage" @@ -131,7 +132,7 @@ func (tn *testNative) callOtherContractWithoutArgs(ic *interop.Context, args []s vm.Estack().PushVal(stackitem.NewArray([]stackitem.Item{})) // no args vm.Estack().PushVal(args[1]) // method vm.Estack().PushVal(args[0]) // contract hash - err := contractCall(ic) + err := contract.Call(ic) if err != nil { return stackitem.NewBigInteger(big.NewInt(-1)) } @@ -147,7 +148,7 @@ func (tn *testNative) callOtherContractWithArg(ic *interop.Context, args []stack vm.Estack().PushVal(stackitem.NewArray([]stackitem.Item{args[2]})) // arg vm.Estack().PushVal(args[1]) // method vm.Estack().PushVal(args[0]) // contract hash - err := contractCall(ic) + err := contract.Call(ic) if err != nil { return stackitem.NewBigInteger(big.NewInt(-1)) } From 30526046e7ec0fd24e2d3accf80d98d5dcadfb8d Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 21 Sep 2020 17:34:40 +0300 Subject: [PATCH 2/9] native: don't push void result in Call This was done in https://github.com/neo-project/neo/pull/1693 for native calls. `OnPersist` script still uses `DROP` though as value is pushed via `CheckReturn` logic for regular calls. --- pkg/core/interop/context.go | 2 ++ pkg/core/interop/contract/call.go | 2 +- pkg/core/native/interop.go | 5 ++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/core/interop/context.go b/pkg/core/interop/context.go index e1db69a20..61ab10961 100644 --- a/pkg/core/interop/context.go +++ b/pkg/core/interop/context.go @@ -82,6 +82,7 @@ type Method = func(ic *Context, args []stackitem.Item) stackitem.Item // MethodAndPrice is a native-contract method descriptor. type MethodAndPrice struct { Func Method + MD *manifest.Method Price int64 RequiredFlags smartcontract.CallFlag } @@ -123,6 +124,7 @@ func NewContractMD(name string) *ContractMD { // AddMethod adds new method to a native contract. func (c *ContractMD) AddMethod(md *MethodAndPrice, desc *manifest.Method, safe bool) { c.Manifest.ABI.Methods = append(c.Manifest.ABI.Methods, *desc) + md.MD = desc c.Methods[desc.Name] = *md if safe { c.Manifest.SafeMethods.Add(desc.Name) diff --git a/pkg/core/interop/contract/call.go b/pkg/core/interop/contract/call.go index 86adf6df8..6560dd2c7 100644 --- a/pkg/core/interop/contract/call.go +++ b/pkg/core/interop/contract/call.go @@ -77,8 +77,8 @@ func callExInternal(ic *interop.Context, h []byte, name string, args []stackitem } // use Jump not Call here because context was loaded in LoadScript above. ic.VM.Jump(ic.VM.Context(), md.Offset) - ic.VM.Context().CheckReturn = true } + ic.VM.Context().CheckReturn = true md = cs.Manifest.ABI.GetMethod(manifest.MethodInit) if md != nil { diff --git a/pkg/core/native/interop.go b/pkg/core/native/interop.go index 0c752f8e2..05ee8afcb 100644 --- a/pkg/core/native/interop.go +++ b/pkg/core/native/interop.go @@ -6,6 +6,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" ) // Deploy deploys native contract. @@ -62,6 +63,8 @@ func Call(ic *interop.Context) error { return errors.New("gas limit exceeded") } result := m.Func(ic, args) - ic.VM.Estack().PushVal(result) + if m.MD.ReturnType != smartcontract.VoidType { + ic.VM.Estack().PushVal(result) + } return nil } From ca1b8a7df017a1581010cd685306d32303b6c6a4 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 17 Sep 2020 11:42:40 +0300 Subject: [PATCH 3/9] state: implement marshalers for oracle-related state --- pkg/core/native/oracle_types.go | 225 +++++++++++++++++++++++++++ pkg/core/native/oracle_types_test.go | 125 +++++++++++++++ 2 files changed, 350 insertions(+) create mode 100644 pkg/core/native/oracle_types.go create mode 100644 pkg/core/native/oracle_types_test.go 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())) + }) + +} From 5b205ffa7dc8e0800c50bfd9c383eecc113b0dd7 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 24 Sep 2020 15:36:14 +0300 Subject: [PATCH 4/9] native: cache committee script hash --- pkg/core/native/native_neo.go | 29 +++++++++++++++++------------ pkg/core/native_neo_test.go | 8 ++------ 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index e3868cdf1..f922c5f25 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -35,6 +35,8 @@ type NEO struct { // (every 28 blocks for mainnet). It's value // is always equal to value stored by `prefixCommittee`. committee atomic.Value + // committeeHash contains script hash of the committee. + committeeHash atomic.Value } // keyWithVotes is a serialized key with votes balance. It's not deserialized @@ -99,6 +101,7 @@ func NewNEO() *NEO { n.nextValidators.Store(keys.PublicKeys(nil)) n.validators.Store(keys.PublicKeys(nil)) n.committee.Store(keys.PublicKeys(nil)) + n.committeeHash.Store(util.Uint160{}) onp := n.Methods["onPersist"] onp.Func = getOnPersistWrapper(n.onPersist) @@ -166,9 +169,14 @@ func (n *NEO) Initialize(ic *interop.Context) error { committee := ic.Chain.GetStandByCommittee() n.committee.Store(committee) + script, err := smartcontract.CreateMajorityMultiSigRedeemScript(committee.Copy()) + if err != nil { + return err + } + n.committeeHash.Store(hash.Hash160(script)) n.updateNextValidators(committee, ic.Chain) - err := ic.DAO.PutStorageItem(n.ContractID, prefixCommittee, &state.StorageItem{Value: committee.Bytes()}) + err = ic.DAO.PutStorageItem(n.ContractID, prefixCommittee, &state.StorageItem{Value: committee.Bytes()}) if err != nil { return err } @@ -212,6 +220,11 @@ func (n *NEO) updateCommittee(ic *interop.Context) error { return err } n.committee.Store(committee) + script, err := smartcontract.CreateMajorityMultiSigRedeemScript(committee.Copy()) + if err != nil { + return err + } + n.committeeHash.Store(hash.Hash160(script)) n.updateNextValidators(committee, ic.Chain) n.votesChanged.Store(false) si := &state.StorageItem{Value: committee.Bytes()} @@ -332,13 +345,8 @@ func (n *NEO) GetGASPerBlock(ic *interop.Context, index uint32) (*big.Int, error } // GetCommitteeAddress returns address of the committee. -func (n *NEO) GetCommitteeAddress(bc blockchainer.Blockchainer, d dao.DAO) (util.Uint160, error) { - pubs := n.GetCommitteeMembers() - script, err := smartcontract.CreateMajorityMultiSigRedeemScript(pubs) - if err != nil { - return util.Uint160{}, err - } - return hash.Hash160(script), nil +func (n *NEO) GetCommitteeAddress() util.Uint160 { + return n.committeeHash.Load().(util.Uint160) } func (n *NEO) setGASPerBlock(ic *interop.Context, args []stackitem.Item) stackitem.Item { @@ -355,10 +363,7 @@ func (n *NEO) SetGASPerBlock(ic *interop.Context, index uint32, gas *big.Int) (b if gas.Sign() == -1 || gas.Cmp(big.NewInt(10*GASFactor)) == 1 { return false, errors.New("invalid value for GASPerBlock") } - h, err := n.GetCommitteeAddress(ic.Chain, ic.DAO) - if err != nil { - return false, err - } + h := n.GetCommitteeAddress() ok, err := runtime.CheckHashedWitness(ic, h) if err != nil || !ok { return ok, err diff --git a/pkg/core/native_neo_test.go b/pkg/core/native_neo_test.go index 0a6bd7e24..4c200c0fc 100644 --- a/pkg/core/native_neo_test.go +++ b/pkg/core/native_neo_test.go @@ -128,9 +128,7 @@ func TestNEO_SetGasPerBlock(t *testing.T) { ic := bc.newInteropContext(trigger.System, bc.dao, nil, tx) ic.VM = vm.New() - h, err := neo.GetCommitteeAddress(bc, bc.dao) - require.NoError(t, err) - + h := neo.GetCommitteeAddress() t.Run("Default", func(t *testing.T) { g, err := neo.GetGASPerBlock(ic, 0) require.NoError(t, err) @@ -189,9 +187,7 @@ func TestNEO_CalculateBonus(t *testing.T) { require.EqualValues(t, 0, res.Int64()) }) t.Run("ManyBlocks", func(t *testing.T) { - h, err := neo.GetCommitteeAddress(bc, bc.dao) - require.NoError(t, err) - setSigner(tx, h) + setSigner(tx, neo.GetCommitteeAddress()) ok, err := neo.SetGASPerBlock(ic, 10, big.NewInt(1*native.GASFactor)) require.NoError(t, err) require.True(t, ok) From ef7c0dbd788131d48366866b50d4730277cca780 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 25 Sep 2020 17:01:58 +0300 Subject: [PATCH 5/9] core: use `GetCommitteeAddress` to get committee address --- pkg/core/blockchain.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 16fc98460..5465a9b94 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1243,12 +1243,7 @@ func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error { for i := range tx.Attributes { switch tx.Attributes[i].Type { case transaction.HighPriority: - pubs := bc.contracts.NEO.GetCommitteeMembers() - s, err := smartcontract.CreateMajorityMultiSigRedeemScript(pubs) - if err != nil { - return err - } - h := hash.Hash160(s) + h := bc.contracts.NEO.GetCommitteeAddress() for i := range tx.Signers { if tx.Signers[i].Account.Equals(h) { return nil From 141d6e325f0a1f5b56c2a6082c93d2c8b2ba8c9a Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 18 Sep 2020 16:26:36 +0300 Subject: [PATCH 6/9] native: implement basis for oracle contract --- pkg/core/native/contract.go | 9 +- pkg/core/native/oracle.go | 412 +++++++++++++++++++++++++++ pkg/core/native/oracle_types.go | 14 + pkg/core/native/oracle_types_test.go | 16 ++ pkg/core/native_oracle_test.go | 246 ++++++++++++++++ 5 files changed, 696 insertions(+), 1 deletion(-) create mode 100644 pkg/core/native/oracle.go create mode 100644 pkg/core/native_oracle_test.go diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go index 1dbcd5517..45490b7f7 100644 --- a/pkg/core/native/contract.go +++ b/pkg/core/native/contract.go @@ -16,6 +16,7 @@ type Contracts struct { NEO *NEO GAS *GAS Policy *Policy + Oracle *Oracle Contracts []interop.Contract // persistScript is vm script which executes "onPersist" method of every native contract. persistScript []byte @@ -51,6 +52,12 @@ func NewContracts() *Contracts { policy := newPolicy() cs.Policy = policy cs.Contracts = append(cs.Contracts, policy) + + oracle := newOracle() + oracle.GAS = gas + oracle.NEO = neo + cs.Oracle = oracle + cs.Contracts = append(cs.Contracts, oracle) return cs } @@ -64,7 +71,7 @@ func (cs *Contracts) GetPersistScript() []byte { md := cs.Contracts[i].Metadata() // Not every contract is persisted: // https://github.com/neo-project/neo/blob/master/src/neo/Ledger/Blockchain.cs#L90 - if md.ContractID == policyContractID { + if md.ContractID == policyContractID || md.ContractID == oracleContractID { continue } emit.Int(w.BinWriter, 0) diff --git a/pkg/core/native/oracle.go b/pkg/core/native/oracle.go new file mode 100644 index 000000000..fbbdb8859 --- /dev/null +++ b/pkg/core/native/oracle.go @@ -0,0 +1,412 @@ +package native + +import ( + "encoding/binary" + "errors" + "math/big" + "sort" + + "github.com/nspcc-dev/neo-go/pkg/core/dao" + "github.com/nspcc-dev/neo-go/pkg/core/interop" + "github.com/nspcc-dev/neo-go/pkg/core/interop/contract" + "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" + "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/core/storage" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" +) + +// Oracle represents Oracle native contract. +type Oracle struct { + interop.ContractMD + GAS *GAS + NEO *NEO +} + +const ( + oracleContractID = -4 + oracleName = "Oracle" +) + +const ( + maxURLLength = 256 + maxFilterLength = 128 + maxCallbackLength = 32 + maxUserDataLength = 512 + + oracleRequestPrice = 5000_0000 +) + +var oracleScript []byte + +func init() { + w := io.NewBufBinWriter() + emit.String(w.BinWriter, oracleName) + emit.Syscall(w.BinWriter, interopnames.NeoNativeCall) + h := hash.Hash160(w.Bytes()) + + w.Reset() + emit.Int(w.BinWriter, 0) + emit.Opcode(w.BinWriter, opcode.NEWARRAY) + emit.String(w.BinWriter, "finish") + emit.Bytes(w.BinWriter, h.BytesBE()) + emit.Syscall(w.BinWriter, interopnames.SystemContractCall) + oracleScript = w.Bytes() +} + +var ( + prefixIDList = []byte{6} + prefixRequest = []byte{7} + prefixNodeList = []byte{8} + prefixRequestID = []byte{9} +) + +// Various validation errors. +var ( + ErrBigArgument = errors.New("some of the arguments are invalid") + ErrEmptyNodeList = errors.New("oracle nodes list is empty") + ErrInvalidWitness = errors.New("witness check failed") + ErrNotEnoughGas = errors.New("gas limit exceeded") + ErrRequestNotFound = errors.New("oracle request not found") + ErrResponseNotFound = errors.New("oracle response not found") +) + +// GetOracleResponseScript returns script for transaction with oracle response. +func GetOracleResponseScript() []byte { + b := make([]byte, len(oracleScript)) + copy(b, oracleScript) + return b +} + +func newOracle() *Oracle { + o := &Oracle{ContractMD: *interop.NewContractMD(oracleName)} + o.ContractID = oracleContractID + o.Manifest.Features = smartcontract.HasStorage + + desc := newDescriptor("request", smartcontract.VoidType, + manifest.NewParameter("url", smartcontract.StringType), + manifest.NewParameter("filter", smartcontract.StringType), + manifest.NewParameter("callback", smartcontract.StringType), + manifest.NewParameter("userData", smartcontract.AnyType), + manifest.NewParameter("gasForResponse", smartcontract.IntegerType)) + md := newMethodAndPrice(o.request, oracleRequestPrice, smartcontract.AllowModifyStates) + o.AddMethod(md, desc, false) + + desc = newDescriptor("finish", smartcontract.VoidType) + md = newMethodAndPrice(o.finish, 0, smartcontract.AllowModifyStates) + o.AddMethod(md, desc, false) + + desc = newDescriptor("getOracleNodes", smartcontract.ArrayType) + md = newMethodAndPrice(o.getOracleNodes, 100_0000, smartcontract.AllowStates) + o.AddMethod(md, desc, true) + + desc = newDescriptor("setOracleNodes", smartcontract.VoidType) + md = newMethodAndPrice(o.setOracleNodes, 0, smartcontract.AllowModifyStates) + o.AddMethod(md, desc, false) + + desc = newDescriptor("verify", smartcontract.BoolType) + md = newMethodAndPrice(o.verify, 100_0000, smartcontract.NoneFlag) + o.AddMethod(md, desc, false) + + pp := chainOnPersist(postPersistBase, o.PostPersist) + desc = newDescriptor("postPersist", smartcontract.VoidType) + md = newMethodAndPrice(getOnPersistWrapper(pp), 0, smartcontract.AllowModifyStates) + o.AddMethod(md, desc, false) + + return o +} + +// PostPersist represents `postPersist` method. +func (o *Oracle) PostPersist(ic *interop.Context) error { + var nodes keys.PublicKeys + var reward []big.Int + single := new(big.Int).SetUint64(oracleRequestPrice) + for _, tx := range ic.Block.Transactions { + resp := getResponse(tx) + if resp == nil { + continue + } + reqKey := makeRequestKey(resp.ID) + req := new(OracleRequest) + if err := o.getSerializableFromDAO(ic.DAO, reqKey, req); err != nil { + return err + } + if err := ic.DAO.DeleteStorageItem(o.ContractID, reqKey); err != nil { + return err + } + + idKey := makeIDListKey(req.URL) + idList := new(IDList) + if err := o.getSerializableFromDAO(ic.DAO, idKey, idList); err != nil { + return err + } + if !idList.Remove(resp.ID) { + return errors.New("response ID wasn't found") + } + + var err error + if len(*idList) == 0 { + err = ic.DAO.DeleteStorageItem(o.ContractID, idKey) + } else { + si := &state.StorageItem{Value: idList.Bytes()} + err = ic.DAO.PutStorageItem(o.ContractID, idKey, si) + } + if err != nil { + return err + } + + if nodes == nil { + nodes, err = o.GetOracleNodes(ic.DAO) + if err != nil { + return err + } + reward = make([]big.Int, len(nodes)) + } + + if len(reward) > 0 { + index := resp.ID % uint64(len(nodes)) + reward[index].Add(&reward[index], single) + } + } + for i := range reward { + o.GAS.mint(ic, nodes[i].GetScriptHash(), &reward[i]) + } + return nil +} + +// Metadata returns contract metadata. +func (o *Oracle) Metadata() *interop.ContractMD { + return &o.ContractMD +} + +// Initialize initializes Oracle contract. +func (o *Oracle) Initialize(ic *interop.Context) error { + si := &state.StorageItem{Value: NodeList{}.Bytes()} + if err := ic.DAO.PutStorageItem(o.ContractID, prefixNodeList, si); err != nil { + return err + } + si = &state.StorageItem{Value: make([]byte, 8)} // uint64(0) LE + return ic.DAO.PutStorageItem(o.ContractID, prefixRequestID, si) +} + +func getResponse(tx *transaction.Transaction) *transaction.OracleResponse { + for i := range tx.Attributes { + if tx.Attributes[i].Type == transaction.OracleResponseT { + return tx.Attributes[i].Value.(*transaction.OracleResponse) + } + } + return nil +} + +func (o *Oracle) finish(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + err := o.FinishInternal(ic) + if err != nil { + panic(err) + } + return stackitem.Null{} +} + +// FinishInternal processes oracle response. +func (o *Oracle) FinishInternal(ic *interop.Context) error { + resp := getResponse(ic.Tx) + if resp == nil { + return ErrResponseNotFound + } + req, err := o.GetRequestInternal(ic.DAO, resp.ID) + if err != nil { + return ErrRequestNotFound + } + + r := io.NewBinReaderFromBuf(req.UserData) + userData := stackitem.DecodeBinaryStackItem(r) + args := stackitem.NewArray([]stackitem.Item{ + stackitem.Make(req.URL), + stackitem.Make(userData), + stackitem.Make(resp.Code), + stackitem.Make(resp.Result), + }) + ic.VM.Estack().PushVal(args) + ic.VM.Estack().PushVal(req.CallbackMethod) + ic.VM.Estack().PushVal(req.CallbackContract.BytesBE()) + return contract.Call(ic) +} + +func (o *Oracle) request(ic *interop.Context, args []stackitem.Item) stackitem.Item { + url, err := stackitem.ToString(args[0]) + if err != nil { + panic(err) + } + filter, err := stackitem.ToString(args[1]) + if err != nil { + panic(err) + } + cb, err := stackitem.ToString(args[2]) + if err != nil { + panic(err) + } + userData := args[3] + gas, err := args[4].TryInteger() + if err != nil { + panic(err) + } + if err := o.RequestInternal(ic, url, filter, cb, userData, gas); err != nil { + panic(err) + } + return stackitem.Null{} +} + +// RequestInternal processes oracle request. +func (o *Oracle) RequestInternal(ic *interop.Context, url, filter, cb string, userData stackitem.Item, gas *big.Int) error { + if len(url) > maxURLLength || len(filter) > maxFilterLength || len(cb) > maxCallbackLength || gas.Uint64() < 1000_0000 { + return ErrBigArgument + } + + if !ic.VM.AddGas(gas.Int64()) { + return ErrNotEnoughGas + } + o.GAS.mint(ic, o.Hash, gas) + si := ic.DAO.GetStorageItem(o.ContractID, prefixRequestID) + id := binary.LittleEndian.Uint64(si.Value) + 1 + binary.LittleEndian.PutUint64(si.Value, id) + if err := ic.DAO.PutStorageItem(o.ContractID, prefixRequestID, si); err != nil { + return err + } + + // Should be executed from contract. + _, err := ic.DAO.GetContractState(ic.VM.GetCallingScriptHash()) + if err != nil { + return err + } + + w := io.NewBufBinWriter() + stackitem.EncodeBinaryStackItem(userData, w.BinWriter) + if w.Err != nil { + return w.Err + } + data := w.Bytes() + if len(data) > maxUserDataLength { + return ErrBigArgument + } + + req := &OracleRequest{ + OriginalTxID: o.getOriginalTxID(ic.DAO, ic.Tx), + GasForResponse: gas.Uint64(), + URL: url, + Filter: &filter, + CallbackContract: ic.VM.GetCallingScriptHash(), + CallbackMethod: cb, + UserData: data, + } + reqItem := &state.StorageItem{Value: req.Bytes()} + reqKey := makeRequestKey(id) + if err = ic.DAO.PutStorageItem(o.ContractID, reqKey, reqItem); err != nil { + return err + } + + // Add request ID to the id list. + lst := new(IDList) + key := makeIDListKey(url) + if err := o.getSerializableFromDAO(ic.DAO, key, lst); err != nil && !errors.Is(err, storage.ErrKeyNotFound) { + return err + } + *lst = append(*lst, id) + si = &state.StorageItem{Value: lst.Bytes()} + return ic.DAO.PutStorageItem(o.ContractID, key, si) +} + +func (o *Oracle) getOracleNodes(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + pubs, err := o.GetOracleNodes(ic.DAO) + if err != nil { + panic(err) + } + return pubsToArray(pubs) +} + +// GetOracleNodes returns public keys of oracle nodes. +func (o *Oracle) GetOracleNodes(d dao.DAO) (keys.PublicKeys, error) { + ns := new(NodeList) + return keys.PublicKeys(*ns), o.getSerializableFromDAO(d, prefixNodeList, ns) +} + +func (o *Oracle) setOracleNodes(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + var pubs keys.PublicKeys + err := o.SetOracleNodes(ic, pubs) + if err != nil { + panic(err) + } + return pubsToArray(pubs) +} + +// SetOracleNodes sets oracle node public keys to pubs. +func (o *Oracle) SetOracleNodes(ic *interop.Context, pubs keys.PublicKeys) error { + if len(pubs) == 0 { + return ErrEmptyNodeList + } + h := o.NEO.GetCommitteeAddress() + if ok, err := runtime.CheckHashedWitness(ic, h); err != nil || !ok { + return ErrInvalidWitness + } + + sort.Sort(pubs) + si := &state.StorageItem{Value: NodeList(pubs).Bytes()} + return ic.DAO.PutStorageItem(o.ContractID, prefixNodeList, si) +} + +// GetRequestInternal returns request by ID and key under which it is stored. +func (o *Oracle) GetRequestInternal(d dao.DAO, id uint64) (*OracleRequest, error) { + key := makeRequestKey(id) + req := new(OracleRequest) + return req, o.getSerializableFromDAO(d, key, req) +} + +// GetIDListInternal returns request by ID and key under which it is stored. +func (o *Oracle) GetIDListInternal(d dao.DAO, url string) (*IDList, error) { + key := makeIDListKey(url) + idList := new(IDList) + return idList, o.getSerializableFromDAO(d, key, idList) +} + +func (o *Oracle) verify(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + return stackitem.NewBool(ic.Tx.HasAttribute(transaction.OracleResponseT)) +} + +func (o *Oracle) getOriginalTxID(d dao.DAO, tx *transaction.Transaction) util.Uint256 { + for i := range tx.Attributes { + if tx.Attributes[i].Type == transaction.OracleResponseT { + id := tx.Attributes[i].Value.(*transaction.OracleResponse).ID + req, _ := o.GetRequestInternal(d, id) + return req.OriginalTxID + } + } + return tx.Hash() +} + +func makeRequestKey(id uint64) []byte { + k := make([]byte, 9) + k[0] = prefixRequest[0] + binary.LittleEndian.PutUint64(k[1:], id) + return k +} + +func makeIDListKey(url string) []byte { + return append(prefixIDList, hash.Hash160([]byte(url)).BytesBE()...) +} + +func (o *Oracle) getSerializableFromDAO(d dao.DAO, key []byte, item io.Serializable) error { + si := d.GetStorageItem(o.ContractID, key) + if si == nil { + return storage.ErrKeyNotFound + } + r := io.NewBinReaderFromBuf(si.Value) + item.DecodeBinary(r) + return r.Err +} diff --git a/pkg/core/native/oracle_types.go b/pkg/core/native/oracle_types.go index 075eab3a3..af66068e2 100644 --- a/pkg/core/native/oracle_types.go +++ b/pkg/core/native/oracle_types.go @@ -74,6 +74,20 @@ func (l *IDList) fromStackItem(it stackitem.Item) error { return nil } +// Remove removes id from list. +func (l *IDList) Remove(id uint64) bool { + for i := range *l { + if id == (*l)[i] { + if i < len(*l) { + copy((*l)[i:], (*l)[i+1:]) + } + *l = (*l)[:len(*l)-1] + return true + } + } + return false +} + // Bytes return l serizalized to a byte-slice. func (l NodeList) Bytes() []byte { w := io.NewBufBinWriter() diff --git a/pkg/core/native/oracle_types_test.go b/pkg/core/native/oracle_types_test.go index 1f0b70630..85cc7ee10 100644 --- a/pkg/core/native/oracle_types_test.go +++ b/pkg/core/native/oracle_types_test.go @@ -36,6 +36,22 @@ func TestIDList_EncodeBinary(t *testing.T) { }) } +func TestIDList_Remove(t *testing.T) { + l := IDList{1, 4, 5} + + // missing + require.False(t, l.Remove(2)) + require.Equal(t, IDList{1, 4, 5}, l) + + // middle + require.True(t, l.Remove(4)) + require.Equal(t, IDList{1, 5}, l) + + // last + require.True(t, l.Remove(5)) + require.Equal(t, IDList{1}, l) +} + func TestNodeList_EncodeBinary(t *testing.T) { priv, err := keys.NewPrivateKey() require.NoError(t, err) diff --git a/pkg/core/native_oracle_test.go b/pkg/core/native_oracle_test.go new file mode 100644 index 000000000..7c4ad3164 --- /dev/null +++ b/pkg/core/native_oracle_test.go @@ -0,0 +1,246 @@ +package core + +import ( + "errors" + "math/big" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" + "github.com/nspcc-dev/neo-go/pkg/core/native" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/internal/testchain" + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/stretchr/testify/require" +) + +// getTestContractState returns test contract which uses oracles. +func getOracleContractState(h util.Uint160) *state.Contract { + w := io.NewBufBinWriter() + emit.Int(w.BinWriter, 5) + emit.Opcode(w.BinWriter, opcode.PACK) + emit.String(w.BinWriter, "request") + emit.Bytes(w.BinWriter, h.BytesBE()) + emit.Syscall(w.BinWriter, interopnames.SystemContractCall) + emit.Opcode(w.BinWriter, opcode.RET) + + // `handle` method aborts if len(userData) == 2 + offset := w.Len() + emit.Opcode(w.BinWriter, opcode.OVER) + emit.Opcode(w.BinWriter, opcode.SIZE) + emit.Int(w.BinWriter, 2) + emit.Instruction(w.BinWriter, opcode.JMPNE, []byte{3}) + emit.Opcode(w.BinWriter, opcode.ABORT) + emit.Int(w.BinWriter, 4) // url, userData, code, result + emit.Opcode(w.BinWriter, opcode.PACK) + emit.Syscall(w.BinWriter, interopnames.SystemBinarySerialize) + emit.String(w.BinWriter, "lastOracleResponse") + emit.Syscall(w.BinWriter, interopnames.SystemStorageGetContext) + emit.Syscall(w.BinWriter, interopnames.SystemStoragePut) + emit.Opcode(w.BinWriter, opcode.RET) + + m := manifest.NewManifest(h) + m.Features = smartcontract.HasStorage + m.ABI.Methods = []manifest.Method{ + { + Name: "requestURL", + Offset: 0, + Parameters: []manifest.Parameter{ + manifest.NewParameter("url", smartcontract.StringType), + manifest.NewParameter("filter", smartcontract.StringType), + manifest.NewParameter("callback", smartcontract.StringType), + manifest.NewParameter("userData", smartcontract.AnyType), + manifest.NewParameter("gasForResponse", smartcontract.IntegerType), + }, + ReturnType: smartcontract.VoidType, + }, + { + Name: "handle", + Offset: offset, + Parameters: []manifest.Parameter{ + manifest.NewParameter("url", smartcontract.StringType), + manifest.NewParameter("userData", smartcontract.AnyType), + manifest.NewParameter("code", smartcontract.IntegerType), + manifest.NewParameter("result", smartcontract.ByteArrayType), + }, + ReturnType: smartcontract.VoidType, + }, + } + + perm := manifest.NewPermission(manifest.PermissionHash, h) + perm.Methods.Add("request") + m.Permissions = append(m.Permissions, *perm) + + return &state.Contract{ + Script: w.Bytes(), + Manifest: *m, + ID: 42, + } +} + +func putOracleRequest(t *testing.T, h util.Uint160, bc *Blockchain, + url, filter string, userData []byte, gas int64) util.Uint256 { + w := io.NewBufBinWriter() + emit.AppCallWithOperationAndArgs(w.BinWriter, h, "requestURL", + url, filter, "handle", userData, gas) + require.NoError(t, w.Err) + + gas += 50_000_000 + 5_000_000 // request + contract call with args + tx := transaction.New(netmode.UnitTestNet, w.Bytes(), gas) + tx.ValidUntilBlock = bc.BlockHeight() + 1 + tx.NetworkFee = 1_000_000 + setSigner(tx, testchain.MultisigScriptHash()) + require.NoError(t, signTx(bc, tx)) + require.NoError(t, bc.AddBlock(bc.newBlock(tx))) + return tx.Hash() +} + +func TestOracle_Request(t *testing.T) { + bc := newTestChain(t) + defer bc.Close() + + orc := bc.contracts.Oracle + cs := getOracleContractState(orc.Hash) + require.NoError(t, bc.dao.PutContractState(cs)) + + gasForResponse := int64(2000_1234) + userData := []byte("custom info") + txHash := putOracleRequest(t, cs.ScriptHash(), bc, "url", "flt", userData, gasForResponse) + + req, err := orc.GetRequestInternal(bc.dao, 1) + require.NotNil(t, req) + require.NoError(t, err) + require.Equal(t, txHash, req.OriginalTxID) + require.Equal(t, "url", req.URL) + require.Equal(t, "flt", *req.Filter) + require.Equal(t, cs.ScriptHash(), req.CallbackContract) + require.Equal(t, "handle", req.CallbackMethod) + require.Equal(t, uint64(gasForResponse), req.GasForResponse) + + idList, err := orc.GetIDListInternal(bc.dao, "url") + require.NoError(t, err) + require.Equal(t, &native.IDList{1}, idList) + + // Finish. + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + pub := priv.PublicKey() + + tx := transaction.New(netmode.UnitTestNet, []byte{}, 0) + setSigner(tx, testchain.CommitteeScriptHash()) + ic := bc.newInteropContext(trigger.Application, bc.dao, nil, tx) + err = orc.SetOracleNodes(ic, keys.PublicKeys{pub}) + require.NoError(t, err) + + tx = transaction.New(netmode.UnitTestNet, native.GetOracleResponseScript(), 0) + ic.Tx = tx + ic.Block = bc.newBlock(tx) + + err = orc.FinishInternal(ic) + require.True(t, errors.Is(err, native.ErrResponseNotFound), "got: %v", err) + + resp := &transaction.OracleResponse{ + ID: 13, + Code: transaction.Success, + Result: []byte{4, 8, 15, 16, 23, 42}, + } + tx.Attributes = []transaction.Attribute{{ + Type: transaction.OracleResponseT, + Value: resp, + }} + err = orc.FinishInternal(ic) + require.True(t, errors.Is(err, native.ErrRequestNotFound), "got: %v", err) + + // We need to ensure that callback is called thus, executing full script is necessary. + resp.ID = 1 + ic.VM = ic.SpawnVM() + ic.VM.LoadScriptWithFlags(tx.Script, smartcontract.All) + require.NoError(t, ic.VM.Run()) + + si := ic.DAO.GetStorageItem(cs.ID, []byte("lastOracleResponse")) + require.NotNil(t, si) + item, err := stackitem.DeserializeItem(si.Value) + require.NoError(t, err) + arr, ok := item.Value().([]stackitem.Item) + require.True(t, ok) + require.Equal(t, []byte("url"), arr[0].Value()) + require.Equal(t, userData, arr[1].Value()) + require.Equal(t, big.NewInt(int64(resp.Code)), arr[2].Value()) + require.Equal(t, resp.Result, arr[3].Value()) + + // Check that processed request is removed during `postPersist`. + _, err = orc.GetRequestInternal(ic.DAO, 1) + require.NoError(t, err) + + require.NoError(t, orc.PostPersist(ic)) + _, err = orc.GetRequestInternal(ic.DAO, 1) + require.Error(t, err) + + t.Run("ErrorOnFinish", func(t *testing.T) { + const reqID = 2 + + putOracleRequest(t, cs.ScriptHash(), bc, "url", "flt", []byte{1, 2}, gasForResponse) + _, err := orc.GetRequestInternal(bc.dao, reqID) // ensure ID is 2 + require.NoError(t, err) + + tx = transaction.New(netmode.UnitTestNet, native.GetOracleResponseScript(), 0) + tx.Attributes = []transaction.Attribute{{ + Type: transaction.OracleResponseT, + Value: &transaction.OracleResponse{ + ID: reqID, + Code: transaction.Success, + Result: []byte{4, 8, 15, 16, 23, 42}, + }, + }} + ic := bc.newInteropContext(trigger.Application, bc.dao, bc.newBlock(tx), tx) + ic.VM = ic.SpawnVM() + ic.VM.LoadScriptWithFlags(tx.Script, smartcontract.All) + require.Error(t, ic.VM.Run()) + + // Request is cleaned up even if callback failed. + require.NoError(t, orc.PostPersist(ic)) + _, err = orc.GetRequestInternal(ic.DAO, reqID) + require.Error(t, err) + }) +} + +func TestOracle_SetOracleNodes(t *testing.T) { + bc := newTestChain(t) + defer bc.Close() + + orc := bc.contracts.Oracle + tx := transaction.New(netmode.UnitTestNet, []byte{}, 0) + ic := bc.newInteropContext(trigger.System, bc.dao, nil, tx) + ic.VM = vm.New() + + pubs, err := orc.GetOracleNodes(ic.DAO) + require.NoError(t, err) + require.Equal(t, 0, len(pubs)) + + err = orc.SetOracleNodes(ic, keys.PublicKeys{}) + require.True(t, errors.Is(err, native.ErrEmptyNodeList), "got: %v", err) + + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + + pub := priv.PublicKey() + err = orc.SetOracleNodes(ic, keys.PublicKeys{pub}) + require.True(t, errors.Is(err, native.ErrInvalidWitness), "got: %v", err) + + setSigner(tx, testchain.CommitteeScriptHash()) + require.NoError(t, orc.SetOracleNodes(ic, keys.PublicKeys{pub})) + + pubs, err = orc.GetOracleNodes(ic.DAO) + require.NoError(t, err) + require.Equal(t, keys.PublicKeys{pub}, pubs) +} From bd1790bddf59c53cee574c0e00b77528849850c6 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 24 Sep 2020 15:46:31 +0300 Subject: [PATCH 7/9] native: cache oracle nodes --- pkg/core/blockchain.go | 1 + pkg/core/native/oracle.go | 37 ++++++++++++++++++++++++---------- pkg/core/native_oracle_test.go | 10 ++++----- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 5465a9b94..8865b207f 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -650,6 +650,7 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error return err } bc.contracts.Policy.OnPersistEnd(bc.dao) + bc.contracts.Oracle.OnPersistEnd(bc.dao) bc.dao.MPT.Flush() // Every persist cycle we also compact our in-memory MPT. persistedHeight := atomic.LoadUint32(&bc.persistedHeight) diff --git a/pkg/core/native/oracle.go b/pkg/core/native/oracle.go index fbbdb8859..a0008f314 100644 --- a/pkg/core/native/oracle.go +++ b/pkg/core/native/oracle.go @@ -5,6 +5,7 @@ import ( "errors" "math/big" "sort" + "sync/atomic" "github.com/nspcc-dev/neo-go/pkg/core/dao" "github.com/nspcc-dev/neo-go/pkg/core/interop" @@ -30,6 +31,11 @@ type Oracle struct { interop.ContractMD GAS *GAS NEO *NEO + + // nodesChanged is true if `SetOracleNodes` was called. + nodesChanged atomic.Value + // nodes contains cached list of oracle nodes. + nodes atomic.Value } const ( @@ -165,10 +171,7 @@ func (o *Oracle) PostPersist(ic *interop.Context) error { } if nodes == nil { - nodes, err = o.GetOracleNodes(ic.DAO) - if err != nil { - return err - } + nodes = o.GetOracleNodes() reward = make([]big.Int, len(nodes)) } @@ -194,6 +197,8 @@ func (o *Oracle) Initialize(ic *interop.Context) error { if err := ic.DAO.PutStorageItem(o.ContractID, prefixNodeList, si); err != nil { return err } + o.nodes.Store(keys.PublicKeys(nil)) + o.nodesChanged.Store(false) si = &state.StorageItem{Value: make([]byte, 8)} // uint64(0) LE return ic.DAO.PutStorageItem(o.ContractID, prefixRequestID, si) } @@ -324,17 +329,13 @@ func (o *Oracle) RequestInternal(ic *interop.Context, url, filter, cb string, us } func (o *Oracle) getOracleNodes(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - pubs, err := o.GetOracleNodes(ic.DAO) - if err != nil { - panic(err) - } + pubs := o.GetOracleNodes() return pubsToArray(pubs) } // GetOracleNodes returns public keys of oracle nodes. -func (o *Oracle) GetOracleNodes(d dao.DAO) (keys.PublicKeys, error) { - ns := new(NodeList) - return keys.PublicKeys(*ns), o.getSerializableFromDAO(d, prefixNodeList, ns) +func (o *Oracle) GetOracleNodes() keys.PublicKeys { + return o.nodes.Load().(keys.PublicKeys).Copy() } func (o *Oracle) setOracleNodes(ic *interop.Context, _ []stackitem.Item) stackitem.Item { @@ -357,6 +358,7 @@ func (o *Oracle) SetOracleNodes(ic *interop.Context, pubs keys.PublicKeys) error } sort.Sort(pubs) + o.nodesChanged.Store(true) si := &state.StorageItem{Value: NodeList(pubs).Bytes()} return ic.DAO.PutStorageItem(o.ContractID, prefixNodeList, si) } @@ -410,3 +412,16 @@ func (o *Oracle) getSerializableFromDAO(d dao.DAO, key []byte, item io.Serializa item.DecodeBinary(r) return r.Err } + +// OnPersistEnd updates cached Oracle values if they've been changed +func (o *Oracle) OnPersistEnd(d dao.DAO) { + if !o.nodesChanged.Load().(bool) { + return + } + + ns := new(NodeList) + _ = o.getSerializableFromDAO(d, prefixNodeList, ns) + o.nodes.Store(keys.PublicKeys(*ns)) + o.nodesChanged.Store(false) + return +} diff --git a/pkg/core/native_oracle_test.go b/pkg/core/native_oracle_test.go index 7c4ad3164..7f2304e75 100644 --- a/pkg/core/native_oracle_test.go +++ b/pkg/core/native_oracle_test.go @@ -141,6 +141,7 @@ func TestOracle_Request(t *testing.T) { ic := bc.newInteropContext(trigger.Application, bc.dao, nil, tx) err = orc.SetOracleNodes(ic, keys.PublicKeys{pub}) require.NoError(t, err) + orc.OnPersistEnd(ic.DAO) tx = transaction.New(netmode.UnitTestNet, native.GetOracleResponseScript(), 0) ic.Tx = tx @@ -223,11 +224,10 @@ func TestOracle_SetOracleNodes(t *testing.T) { ic := bc.newInteropContext(trigger.System, bc.dao, nil, tx) ic.VM = vm.New() - pubs, err := orc.GetOracleNodes(ic.DAO) - require.NoError(t, err) + pubs := orc.GetOracleNodes() require.Equal(t, 0, len(pubs)) - err = orc.SetOracleNodes(ic, keys.PublicKeys{}) + err := orc.SetOracleNodes(ic, keys.PublicKeys{}) require.True(t, errors.Is(err, native.ErrEmptyNodeList), "got: %v", err) priv, err := keys.NewPrivateKey() @@ -239,8 +239,8 @@ func TestOracle_SetOracleNodes(t *testing.T) { setSigner(tx, testchain.CommitteeScriptHash()) require.NoError(t, orc.SetOracleNodes(ic, keys.PublicKeys{pub})) + orc.OnPersistEnd(ic.DAO) - pubs, err = orc.GetOracleNodes(ic.DAO) - require.NoError(t, err) + pubs = orc.GetOracleNodes() require.Equal(t, keys.PublicKeys{pub}, pubs) } From f084acc339c6878f7463a9c3c6524ed7240ce451 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 24 Sep 2020 15:50:52 +0300 Subject: [PATCH 8/9] native: cache oracle script hash --- pkg/core/native/oracle.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/core/native/oracle.go b/pkg/core/native/oracle.go index a0008f314..10338362d 100644 --- a/pkg/core/native/oracle.go +++ b/pkg/core/native/oracle.go @@ -36,6 +36,8 @@ type Oracle struct { nodesChanged atomic.Value // nodes contains cached list of oracle nodes. nodes atomic.Value + // oracleHash contains cached oracle script hash. + oracleHash atomic.Value } const ( @@ -338,6 +340,15 @@ func (o *Oracle) GetOracleNodes() keys.PublicKeys { return o.nodes.Load().(keys.PublicKeys).Copy() } +// GetScriptHash returns script hash or oracle nodes. +func (o *Oracle) GetScriptHash() (util.Uint160, error) { + h := o.oracleHash.Load() + if h == nil { + return util.Uint160{}, storage.ErrKeyNotFound + } + return h.(util.Uint160), nil +} + func (o *Oracle) setOracleNodes(ic *interop.Context, _ []stackitem.Item) stackitem.Item { var pubs keys.PublicKeys err := o.SetOracleNodes(ic, pubs) @@ -422,6 +433,8 @@ func (o *Oracle) OnPersistEnd(d dao.DAO) { ns := new(NodeList) _ = o.getSerializableFromDAO(d, prefixNodeList, ns) o.nodes.Store(keys.PublicKeys(*ns)) + script, _ := smartcontract.CreateMajorityMultiSigRedeemScript(keys.PublicKeys(*ns).Copy()) + o.oracleHash.Store(hash.Hash160(script)) o.nodesChanged.Store(false) return } From e91d13c615d96dde134eb4f0acabfcf50fbbb226 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Thu, 24 Sep 2020 16:33:40 +0300 Subject: [PATCH 9/9] core: implement oracle tx verification --- pkg/core/blockchain.go | 29 +++++++++++ pkg/core/blockchain_test.go | 100 +++++++++++++++++++++++++++++++++++- pkg/core/native/oracle.go | 15 ++++-- 3 files changed, 137 insertions(+), 7 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 8865b207f..91652d5c4 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1,6 +1,7 @@ package core import ( + "bytes" "errors" "fmt" "math/big" @@ -1251,6 +1252,34 @@ func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error { } } return fmt.Errorf("%w: high priority tx is not signed by committee", ErrInvalidAttribute) + case transaction.OracleResponseT: + h, err := bc.contracts.Oracle.GetScriptHash() + if err != nil { + return fmt.Errorf("%w: %v", ErrInvalidAttribute, err) + } + hasOracle := false + for i := range tx.Signers { + if tx.Signers[i].Scopes != transaction.FeeOnly { + return fmt.Errorf("%w: oracle tx has invalid signer scope", ErrInvalidAttribute) + } + if tx.Signers[i].Account.Equals(h) { + hasOracle = true + } + } + if !hasOracle { + return fmt.Errorf("%w: oracle tx is not signed by oracle nodes", ErrInvalidAttribute) + } + if !bytes.Equal(tx.Script, native.GetOracleResponseScript()) { + return fmt.Errorf("%w: oracle tx has invalid script", ErrInvalidAttribute) + } + resp := tx.Attributes[i].Value.(*transaction.OracleResponse) + req, err := bc.contracts.Oracle.GetRequestInternal(bc.dao, resp.ID) + if err != nil { + return fmt.Errorf("%w: oracle tx points to invalid request: %v", ErrInvalidAttribute, err) + } + if uint64(tx.NetworkFee+tx.SystemFee) < req.GasForResponse { + return fmt.Errorf("%w: oracle tx has insufficient gas", ErrInvalidAttribute) + } } } return nil diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index 1524ff6a2..101cc6c6b 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -11,12 +11,15 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" "github.com/nspcc-dev/neo-go/pkg/core/mempool" + "github.com/nspcc-dev/neo-go/pkg/core/native" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/internal/testchain" "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" @@ -212,13 +215,17 @@ func TestVerifyTx(t *testing.T) { bc := newTestChain(t) defer bc.Close() - accs := make([]*wallet.Account, 2) + accs := make([]*wallet.Account, 3) for i := range accs { var err error accs[i], err = wallet.NewAccount() require.NoError(t, err) } + oracleAcc := accs[2] + oraclePubs := keys.PublicKeys{oracleAcc.PrivateKey().PublicKey()} + require.NoError(t, oracleAcc.ConvertMultisig(1, oraclePubs)) + neoHash := bc.contracts.NEO.Hash gasHash := bc.contracts.GAS.Hash w := io.NewBufBinWriter() @@ -229,7 +236,7 @@ func TestVerifyTx(t *testing.T) { amount = 1_000_000_000 } emit.AppCallWithOperationAndArgs(w.BinWriter, sc, "transfer", - neoOwner, a.PrivateKey().GetScriptHash(), amount) + neoOwner, a.Contract.ScriptHash(), amount) emit.Opcode(w.BinWriter, opcode.ASSERT) } } @@ -376,6 +383,95 @@ func TestVerifyTx(t *testing.T) { }} require.NoError(t, bc.VerifyTx(tx)) }) + t.Run("Oracle", func(t *testing.T) { + orc := bc.contracts.Oracle + req := &native.OracleRequest{GasForResponse: 1000_0000} + require.NoError(t, orc.PutRequestInternal(1, req, bc.dao)) + + oracleScript, err := smartcontract.CreateMajorityMultiSigRedeemScript(oraclePubs) + require.NoError(t, err) + oracleHash := hash.Hash160(oracleScript) + + // We need to create new transaction, + // because hashes are cached after signing. + getOracleTx := func(t *testing.T) *transaction.Transaction { + tx := bc.newTestTx(h, native.GetOracleResponseScript()) + resp := &transaction.OracleResponse{ + ID: 1, + Code: transaction.Success, + Result: []byte{1, 2, 3}, + } + tx.Attributes = []transaction.Attribute{{ + Type: transaction.OracleResponseT, + Value: resp, + }} + tx.NetworkFee += 4_000_000 // multisig check + tx.SystemFee = int64(req.GasForResponse - uint64(tx.NetworkFee)) + tx.Signers = []transaction.Signer{{ + Account: oracleHash, + Scopes: transaction.FeeOnly, + }} + size := io.GetVarSize(tx) + netFee, sizeDelta := CalculateNetworkFee(oracleScript) + tx.NetworkFee += netFee + tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte() + return tx + } + + t.Run("NoOracleNodes", func(t *testing.T) { + tx := getOracleTx(t) + require.NoError(t, oracleAcc.SignTx(tx)) + checkErr(t, ErrInvalidAttribute, tx) + }) + + txSetOracle := transaction.New(netmode.UnitTestNet, []byte{}, 0) + setSigner(txSetOracle, testchain.CommitteeScriptHash()) + txSetOracle.Scripts = []transaction.Witness{{ + InvocationScript: testchain.SignCommittee(txSetOracle.GetSignedPart()), + VerificationScript: testchain.CommitteeVerificationScript(), + }} + ic := bc.newInteropContext(trigger.All, bc.dao, nil, txSetOracle) + require.NoError(t, bc.contracts.Oracle.SetOracleNodes(ic, oraclePubs)) + bc.contracts.Oracle.OnPersistEnd(ic.DAO) + _, err = ic.DAO.Persist() + require.NoError(t, err) + + t.Run("Valid", func(t *testing.T) { + tx := getOracleTx(t) + require.NoError(t, oracleAcc.SignTx(tx)) + require.NoError(t, bc.VerifyTx(tx)) + }) + t.Run("InvalidRequestID", func(t *testing.T) { + tx := getOracleTx(t) + tx.Attributes[0].Value.(*transaction.OracleResponse).ID = 2 + require.NoError(t, oracleAcc.SignTx(tx)) + checkErr(t, ErrInvalidAttribute, tx) + }) + t.Run("InvalidScope", func(t *testing.T) { + tx := getOracleTx(t) + tx.Signers[0].Scopes = transaction.Global + require.NoError(t, oracleAcc.SignTx(tx)) + checkErr(t, ErrInvalidAttribute, tx) + }) + t.Run("InvalidScript", func(t *testing.T) { + tx := getOracleTx(t) + tx.Script[0] = ^tx.Script[0] + require.NoError(t, oracleAcc.SignTx(tx)) + checkErr(t, ErrInvalidAttribute, tx) + }) + t.Run("InvalidSigner", func(t *testing.T) { + tx := getOracleTx(t) + tx.Signers[0].Account = accs[0].Contract.ScriptHash() + require.NoError(t, accs[0].SignTx(tx)) + checkErr(t, ErrInvalidAttribute, tx) + }) + t.Run("SmallFee", func(t *testing.T) { + tx := getOracleTx(t) + tx.SystemFee = 0 + require.NoError(t, oracleAcc.SignTx(tx)) + checkErr(t, ErrInvalidAttribute, tx) + }) + }) }) } diff --git a/pkg/core/native/oracle.go b/pkg/core/native/oracle.go index 10338362d..5d3bd5a6e 100644 --- a/pkg/core/native/oracle.go +++ b/pkg/core/native/oracle.go @@ -313,21 +313,26 @@ func (o *Oracle) RequestInternal(ic *interop.Context, url, filter, cb string, us CallbackMethod: cb, UserData: data, } + return o.PutRequestInternal(id, req, ic.DAO) +} + +// PutRequestInternal puts oracle request with the specified id to d. +func (o *Oracle) PutRequestInternal(id uint64, req *OracleRequest, d dao.DAO) error { reqItem := &state.StorageItem{Value: req.Bytes()} reqKey := makeRequestKey(id) - if err = ic.DAO.PutStorageItem(o.ContractID, reqKey, reqItem); err != nil { + if err := d.PutStorageItem(o.ContractID, reqKey, reqItem); err != nil { return err } // Add request ID to the id list. lst := new(IDList) - key := makeIDListKey(url) - if err := o.getSerializableFromDAO(ic.DAO, key, lst); err != nil && !errors.Is(err, storage.ErrKeyNotFound) { + key := makeIDListKey(req.URL) + if err := o.getSerializableFromDAO(d, key, lst); err != nil && !errors.Is(err, storage.ErrKeyNotFound) { return err } *lst = append(*lst, id) - si = &state.StorageItem{Value: lst.Bytes()} - return ic.DAO.PutStorageItem(o.ContractID, key, si) + si := &state.StorageItem{Value: lst.Bytes()} + return d.PutStorageItem(o.ContractID, key, si) } func (o *Oracle) getOracleNodes(ic *interop.Context, _ []stackitem.Item) stackitem.Item {