diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 982886bcc..45d124d85 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1245,8 +1245,8 @@ func (bc *Blockchain) GetScriptHashesForVerifying(t *transaction.Transaction) ([ } // GetTestVM returns a VM and a Store setup for a test run of some sort of code. -func (bc *Blockchain) GetTestVM() *vm.VM { - systemInterop := bc.newInteropContext(trigger.Application, bc.dao, nil, nil) +func (bc *Blockchain) GetTestVM(tx *transaction.Transaction) *vm.VM { + systemInterop := bc.newInteropContext(trigger.Application, bc.dao, nil, tx) vm := SpawnVM(systemInterop) vm.SetPriceGetter(getPrice) return vm diff --git a/pkg/core/blockchainer/blockchainer.go b/pkg/core/blockchainer/blockchainer.go index 5b84826b4..2a9664246 100644 --- a/pkg/core/blockchainer/blockchainer.go +++ b/pkg/core/blockchainer/blockchainer.go @@ -41,7 +41,7 @@ type Blockchainer interface { GetScriptHashesForVerifying(*transaction.Transaction) ([]util.Uint160, error) GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem GetStorageItems(hash util.Uint160) (map[string]*state.StorageItem, error) - GetTestVM() *vm.VM + GetTestVM(tx *transaction.Transaction) *vm.VM GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error) mempool.Feer // fee interface PoolTx(*transaction.Transaction) error diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index fe60dac6d..70ad94bb8 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -97,7 +97,7 @@ func (chain testChain) GetScriptHashesForVerifying(*transaction.Transaction) ([] func (chain testChain) GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem { panic("TODO") } -func (chain testChain) GetTestVM() *vm.VM { +func (chain testChain) GetTestVM(tx *transaction.Transaction) *vm.VM { panic("TODO") } func (chain testChain) GetStorageItems(hash util.Uint160) (map[string]*state.StorageItem, error) { diff --git a/pkg/rpc/request/param.go b/pkg/rpc/request/param.go index 3f5f9acfa..87af906fc 100644 --- a/pkg/rpc/request/param.go +++ b/pkg/rpc/request/param.go @@ -7,6 +7,7 @@ import ( "fmt" "strconv" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" @@ -65,6 +66,7 @@ const ( TxFilterT NotificationFilterT ExecutionFilterT + Cosigner ) func (p Param) String() string { @@ -154,6 +156,47 @@ func (p Param) GetBytesHex() ([]byte, error) { return hex.DecodeString(s) } +// GetCosigner returns transaction.Cosigner value of the parameter. +func (p Param) GetCosigner() (transaction.Cosigner, error) { + c, ok := p.Value.(transaction.Cosigner) + if !ok { + return transaction.Cosigner{}, errors.New("not a cosigner") + } + return c, nil +} + +// GetCosigners returns a slice of transaction.Cosigner with global scope from +// array of Uint160 or array of serialized transaction.Cosigner stored in the +// parameter. +func (p Param) GetCosigners() ([]transaction.Cosigner, error) { + hashes, err := p.GetArray() + if err != nil { + return nil, err + } + cosigners := make([]transaction.Cosigner, len(hashes)) + // try to extract hashes first + for i, h := range hashes { + var u util.Uint160 + u, err = h.GetUint160FromHex() + if err != nil { + break + } + cosigners[i] = transaction.Cosigner{ + Account: u, + Scopes: transaction.Global, + } + } + if err != nil { + for i, h := range hashes { + cosigners[i], err = h.GetCosigner() + if err != nil { + return nil, err + } + } + } + return cosigners, nil +} + // UnmarshalJSON implements json.Unmarshaler interface. func (p *Param) UnmarshalJSON(data []byte) error { var s string @@ -167,6 +210,7 @@ func (p *Param) UnmarshalJSON(data []byte) error { {TxFilterT, &TxFilter{}}, {NotificationFilterT, &NotificationFilter{}}, {ExecutionFilterT, &ExecutionFilter{}}, + {Cosigner, &transaction.Cosigner{}}, {ArrayT, &[]Param{}}, } @@ -196,6 +240,8 @@ func (p *Param) UnmarshalJSON(data []byte) error { } else { continue } + case *transaction.Cosigner: + p.Value = *val case *[]Param: p.Value = *val } diff --git a/pkg/rpc/request/param_test.go b/pkg/rpc/request/param_test.go index 032173446..e6ada2ced 100644 --- a/pkg/rpc/request/param_test.go +++ b/pkg/rpc/request/param_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "testing" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" @@ -19,9 +20,13 @@ func TestParam_UnmarshalJSON(t *testing.T) { {"cosigner": "f84d6a337fbc3d3a201d41da99e86b479e7a2554"}, {"sender": "f84d6a337fbc3d3a201d41da99e86b479e7a2554", "cosigner": "f84d6a337fbc3d3a201d41da99e86b479e7a2554"}, {"contract": "f84d6a337fbc3d3a201d41da99e86b479e7a2554"}, - {"state": "HALT"}]` + {"state": "HALT"}, + {"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": 0}, + [{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": 0}]]` contr, err := util.Uint160DecodeStringLE("f84d6a337fbc3d3a201d41da99e86b479e7a2554") require.NoError(t, err) + accountHash, err := util.Uint160DecodeStringLE("cadb3dc2faa3ef14a13b619c9a43124755aa2569") + require.NoError(t, err) expected := Params{ { Type: StringT, @@ -83,6 +88,25 @@ func TestParam_UnmarshalJSON(t *testing.T) { Type: ExecutionFilterT, Value: ExecutionFilter{State: "HALT"}, }, + { + Type: Cosigner, + Value: transaction.Cosigner{ + Account: accountHash, + Scopes: transaction.Global, + }, + }, + { + Type: ArrayT, + Value: []Param{ + { + Type: Cosigner, + Value: transaction.Cosigner{ + Account: accountHash, + Scopes: transaction.Global, + }, + }, + }, + }, } var ps Params @@ -214,3 +238,67 @@ func TestParamGetBytesHex(t *testing.T) { _, err = p.GetBytesHex() require.NotNil(t, err) } + +func TestParamGetCosigner(t *testing.T) { + c := transaction.Cosigner{ + Account: util.Uint160{1, 2, 3, 4}, + Scopes: transaction.Global, + } + p := Param{Type: Cosigner, Value: c} + actual, err := p.GetCosigner() + require.NoError(t, err) + require.Equal(t, c, actual) + + p = Param{Type: Cosigner, Value: `{"account": "0xcadb3dc2faa3ef14a13b619c9a43124755aa2569", "scopes": 0}`} + _, err = p.GetCosigner() + require.Error(t, err) +} + +func TestParamGetCosigners(t *testing.T) { + u1 := util.Uint160{1, 2, 3, 4} + u2 := util.Uint160{5, 6, 7, 8} + t.Run("from hashes", func(t *testing.T) { + p := Param{ArrayT, []Param{ + {Type: StringT, Value: u1.StringLE()}, + {Type: StringT, Value: u2.StringLE()}, + }} + actual, err := p.GetCosigners() + require.NoError(t, err) + require.Equal(t, 2, len(actual)) + require.True(t, u1.Equals(actual[0].Account)) + require.True(t, u2.Equals(actual[1].Account)) + }) + + t.Run("from cosigners", func(t *testing.T) { + c1 := transaction.Cosigner{ + Account: u1, + Scopes: transaction.Global, + } + c2 := transaction.Cosigner{ + Account: u2, + Scopes: transaction.CustomContracts, + AllowedContracts: []util.Uint160{ + {1, 2, 3}, + {4, 5, 6}, + }, + } + p := Param{ArrayT, []Param{ + {Type: Cosigner, Value: c1}, + {Type: Cosigner, Value: c2}, + }} + actual, err := p.GetCosigners() + require.NoError(t, err) + require.Equal(t, 2, len(actual)) + require.Equal(t, c1, actual[0]) + require.Equal(t, c2, actual[1]) + }) + + t.Run("bad format", func(t *testing.T) { + p := Param{ArrayT, []Param{ + {Type: StringT, Value: u1.StringLE()}, + {Type: StringT, Value: "bla"}, + }} + _, err := p.GetCosigners() + require.Error(t, err) + }) +} diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 1068c2a2d..5cbda8f88 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -618,7 +618,7 @@ func (s *Server) getDecimals(h util.Uint160, cache map[util.Uint160]int64) (int6 if err != nil { return 0, response.NewInternalServerError("Can't create script", err) } - res := s.runScriptInVM(script) + res := s.runScriptInVM(script, nil) if res == nil || res.State != "HALT" || len(res.Stack) == 0 { return 0, response.NewInternalServerError("execution error", errors.New("no result")) } @@ -864,11 +864,21 @@ func (s *Server) invokeFunction(reqParams request.Params) (interface{}, *respons if err != nil { return nil, response.ErrInvalidParams } - script, err := request.CreateFunctionInvocationScript(scriptHash, reqParams[1:]) + tx := &transaction.Transaction{} + checkWitnessHashesIndex := len(reqParams) + if checkWitnessHashesIndex > 3 { + cosigners, err := reqParams[3].GetCosigners() + if err != nil { + return nil, response.ErrInvalidParams + } + tx.Cosigners = cosigners + checkWitnessHashesIndex-- + } + script, err := request.CreateFunctionInvocationScript(scriptHash, reqParams[1:checkWitnessHashesIndex]) if err != nil { return nil, response.NewInternalServerError("can't create invocation script", err) } - return s.runScriptInVM(script), nil + return s.runScriptInVM(script, tx), nil } // invokescript implements the `invokescript` RPC call. @@ -882,13 +892,21 @@ func (s *Server) invokescript(reqParams request.Params) (interface{}, *response. return nil, response.ErrInvalidParams } - return s.runScriptInVM(script), nil + tx := &transaction.Transaction{} + if len(reqParams) > 1 { + cosigners, err := reqParams[1].GetCosigners() + if err != nil { + return nil, response.ErrInvalidParams + } + tx.Cosigners = cosigners + } + return s.runScriptInVM(script, tx), nil } // runScriptInVM runs given script in a new test VM and returns the invocation // result. -func (s *Server) runScriptInVM(script []byte) *result.Invoke { - vm := s.chain.GetTestVM() +func (s *Server) runScriptInVM(script []byte, tx *transaction.Transaction) *result.Invoke { + vm := s.chain.GetTestVM(tx) vm.SetGasLimit(s.config.MaxGasInvoke) vm.LoadScript(script) _ = vm.Run() diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 2560f786c..72e9a28fa 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -620,6 +620,55 @@ var rpcTestCases = map[string][]rpcTestCase{ assert.NotEqual(t, 0, res.GasConsumed) }, }, + { + name: "positive, good witness", + // script is hex-encoded `test_verify.avm` representation, hashes are hex-encoded LE bytes of hashes used in the contract with `0x` prefix + params: `["5707000c14010c030e05060c0d020e0f0d030e070900000000db307068115541f827ec8c21aa270700000011400c140d0f03020900020103070304050201000e060c09db307169115541f827ec8c21aa270700000012401340",["0x0000000009070e030d0f0e020d0c06050e030c01","0x090c060e00010205040307030102000902030f0d"]]`, + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Equal(t, "HALT", res.State) + require.Equal(t, 1, len(res.Stack)) + require.Equal(t, int64(3), res.Stack[0].Value) + }, + }, + { + name: "positive, bad witness of second hash", + params: `["5707000c14010c030e05060c0d020e0f0d030e070900000000db307068115541f827ec8c21aa270700000011400c140d0f03020900020103070304050201000e060c09db307169115541f827ec8c21aa270700000012401340",["0x0000000009070e030d0f0e020d0c06050e030c01"]]`, + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Equal(t, "HALT", res.State) + require.Equal(t, 1, len(res.Stack)) + require.Equal(t, int64(2), res.Stack[0].Value) + }, + }, + { + name: "positive, no good hashes", + params: `["5707000c14010c030e05060c0d020e0f0d030e070900000000db307068115541f827ec8c21aa270700000011400c140d0f03020900020103070304050201000e060c09db307169115541f827ec8c21aa270700000012401340"]`, + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Equal(t, "HALT", res.State) + require.Equal(t, 1, len(res.Stack)) + require.Equal(t, int64(1), res.Stack[0].Value) + }, + }, + { + name: "positive, bad hashes witness", + params: `["5707000c14010c030e05060c0d020e0f0d030e070900000000db307068115541f827ec8c21aa270700000011400c140d0f03020900020103070304050201000e060c09db307169115541f827ec8c21aa270700000012401340",["0x0000000009070e030d0f0e020d0c06050e030c02"]]`, + result: func(e *executor) interface{} { return &result.Invoke{} }, + check: func(t *testing.T, e *executor, inv interface{}) { + res, ok := inv.(*result.Invoke) + require.True(t, ok) + assert.Equal(t, "HALT", res.State) + assert.Equal(t, 1, len(res.Stack)) + assert.Equal(t, int64(1), res.Stack[0].Value) + }, + }, { name: "no params", params: `[]`, diff --git a/pkg/rpc/server/testdata/test_verify.avm b/pkg/rpc/server/testdata/test_verify.avm new file mode 100755 index 000000000..100b20621 Binary files /dev/null and b/pkg/rpc/server/testdata/test_verify.avm differ diff --git a/pkg/rpc/server/testdata/test_verify.go b/pkg/rpc/server/testdata/test_verify.go new file mode 100644 index 000000000..1587e14da --- /dev/null +++ b/pkg/rpc/server/testdata/test_verify.go @@ -0,0 +1,17 @@ +package testdata + +import "github.com/nspcc-dev/neo-go/pkg/interop/runtime" + +// This contract is used to test `invokescript` and `invokefunction` RPC-calls +func Main() int { + // h1 and h2 are just random uint160 hashes + h1 := []byte{1, 12, 3, 14, 5, 6, 12, 13, 2, 14, 15, 13, 3, 14, 7, 9, 0, 0, 0, 0} + if !runtime.CheckWitness(h1) { + return 1 + } + h2 := []byte{13, 15, 3, 2, 9, 0, 2, 1, 3, 7, 3, 4, 5, 2, 1, 0, 14, 6, 12, 9} + if !runtime.CheckWitness(h2) { + return 2 + } + return 3 +}