diff --git a/pkg/compiler/codegen.go b/pkg/compiler/codegen.go index 1b71e3a41..c63499bd1 100644 --- a/pkg/compiler/codegen.go +++ b/pkg/compiler/codegen.go @@ -1139,14 +1139,13 @@ func (c *codegen) getByteArray(expr ast.Expr) []byte { } func (c *codegen) convertSyscall(expr *ast.CallExpr, api, name string) { - api, ok := syscalls[api][name] + syscall, ok := syscalls[api][name] if !ok { - c.prog.Err = fmt.Errorf("unknown VM syscall api: %s", name) + c.prog.Err = fmt.Errorf("unknown VM syscall api: %s.%s", api, name) return } - emit.Syscall(c.prog.BinWriter, api) - switch name { - case "GetTransaction", "GetBlock", "GetScriptContainer": + emit.Syscall(c.prog.BinWriter, syscall.API) + if syscall.ConvertResultToStruct { c.emitConvert(stackitem.StructT) } diff --git a/pkg/compiler/syscall.go b/pkg/compiler/syscall.go index e47a9ca75..276acb26b 100644 --- a/pkg/compiler/syscall.go +++ b/pkg/compiler/syscall.go @@ -1,73 +1,81 @@ package compiler -var syscalls = map[string]map[string]string{ - "binary": { - "Serialize": "System.Binary.Serialize", - "Deserialize": "System.Binary.Deserialize", - }, - "crypto": { - "ECDsaSecp256r1Verify": "Neo.Crypto.VerifyWithECDsaSecp256r1", - "ECDsaSecp256k1Verify": "Neo.Crypto.VerifyWithECDsaSecp256k1", - "ECDSASecp256r1CheckMultisig": "Neo.Crypto.CheckMultisigWithECDsaSecp256r1", - "ECDSASecp256k1CheckMultisig": "Neo.Crypto.CheckMultisigWithECDsaSecp256k1", - }, - "enumerator": { - "Concat": "System.Enumerator.Concat", - "Create": "System.Enumerator.Create", - "Next": "System.Enumerator.Next", - "Value": "System.Enumerator.Value", - }, - "json": { - "Serialize": "System.Json.Serialize", - "Deserialize": "System.Json.Deserialize", - }, - "storage": { - "ConvertContextToReadOnly": "System.Storage.AsReadOnly", - "Delete": "System.Storage.Delete", - "Find": "System.Storage.Find", - "Get": "System.Storage.Get", - "GetContext": "System.Storage.GetContext", - "GetReadOnlyContext": "System.Storage.GetReadOnlyContext", - "Put": "System.Storage.Put", - }, - "runtime": { - "GetScriptContainer": "System.Runtime.GetScriptContainer", - "GetCallingScriptHash": "System.Runtime.GetCallingScriptHash", - "GetEntryScriptHash": "System.Runtime.GetEntryScriptHash", - "GetExecutingScriptHash": "System.Runtime.GetExecutingScriptHash", - "GetNotifications": "System.Runtime.GetNotifications", - "GetInvocationCounter": "System.Runtime.GetInvocationCounter", +// Syscall represents NEO or System syscall API with flag for proper AVM generation +type Syscall struct { + API string + ConvertResultToStruct bool +} - "GasLeft": "System.Runtime.GasLeft", - "GetTrigger": "System.Runtime.GetTrigger", - "CheckWitness": "System.Runtime.CheckWitness", - "Notify": "System.Runtime.Notify", - "Log": "System.Runtime.Log", - "GetTime": "System.Runtime.GetTime", +// All lists are sorted, keep 'em this way, please. +var syscalls = map[string]map[string]Syscall{ + "binary": { + "Base64Decode": {"System.Binary.Base64Decode", false}, + "Base64Encode": {"System.Binary.Base64Encode", false}, + "Deserialize": {"System.Binary.Deserialize", false}, + "Serialize": {"System.Binary.Serialize", false}, }, "blockchain": { - "GetBlock": "System.Blockchain.GetBlock", - "GetContract": "System.Blockchain.GetContract", - "GetHeight": "System.Blockchain.GetHeight", - "GetTransaction": "System.Blockchain.GetTransaction", - "GetTransactionFromBlock": "System.Blockchain.GetTransactionFromBlock", - "GetTransactionHeight": "System.Blockchain.GetTransactionHeight", + "GetBlock": {"System.Blockchain.GetBlock", true}, + "GetContract": {"System.Blockchain.GetContract", true}, + "GetHeight": {"System.Blockchain.GetHeight", false}, + "GetTransaction": {"System.Blockchain.GetTransaction", true}, + "GetTransactionFromBlock": {"System.Blockchain.GetTransactionFromBlock", false}, + "GetTransactionHeight": {"System.Blockchain.GetTransactionHeight", false}, }, "contract": { - "Create": "System.Contract.Create", - "Destroy": "System.Contract.Destroy", - "Update": "System.Contract.Update", - - "IsStandard": "System.Contract.IsStandard", - "CreateStandardAccount": "System.Contract.CreateStandardAccount", + "Create": {"System.Contract.Create", true}, + "CreateStandardAccount": {"System.Contract.CreateStandardAccount", false}, + "Destroy": {"System.Contract.Destroy", false}, + "IsStandard": {"System.Contract.IsStandard", false}, + "GetCallFlags": {"System.Contract.GetCallFlags", false}, + "Update": {"System.Contract.Update", false}, + }, + "crypto": { + "ECDsaSecp256k1Verify": {"Neo.Crypto.VerifyWithECDsaSecp256k1", false}, + "ECDSASecp256k1CheckMultisig": {"Neo.Crypto.CheckMultisigWithECDsaSecp256k1", false}, + "ECDsaSecp256r1Verify": {"Neo.Crypto.VerifyWithECDsaSecp256r1", false}, + "ECDSASecp256r1CheckMultisig": {"Neo.Crypto.CheckMultisigWithECDsaSecp256r1", false}, + }, + "enumerator": { + "Concat": {"System.Enumerator.Concat", false}, + "Create": {"System.Enumerator.Create", false}, + "Next": {"System.Enumerator.Next", false}, + "Value": {"System.Enumerator.Value", false}, }, "iterator": { - "Concat": "System.Iterator.Concat", - "Create": "System.Iterator.Create", - "Key": "System.Iterator.Key", - "Keys": "System.Iterator.Keys", - "Next": "System.Enumerator.Next", - "Value": "System.Enumerator.Value", - "Values": "System.Iterator.Values", + "Concat": {"System.Iterator.Concat", false}, + "Create": {"System.Iterator.Create", false}, + "Key": {"System.Iterator.Key", false}, + "Keys": {"System.Iterator.Keys", false}, + "Next": {"System.Enumerator.Next", false}, + "Value": {"System.Enumerator.Value", false}, + "Values": {"System.Iterator.Values", false}, + }, + "json": { + "Deserialize": {"System.Json.Deserialize", false}, + "Serialize": {"System.Json.Serialize", false}, + }, + "runtime": { + "GasLeft": {"System.Runtime.GasLeft", false}, + "GetInvocationCounter": {"System.Runtime.GetInvocationCounter", false}, + "GetCallingScriptHash": {"System.Runtime.GetCallingScriptHash", false}, + "GetEntryScriptHash": {"System.Runtime.GetEntryScriptHash", false}, + "GetExecutingScriptHash": {"System.Runtime.GetExecutingScriptHash", false}, + "GetNotifications": {"System.Runtime.GetNotifications", false}, + "GetScriptContainer": {"System.Runtime.GetScriptContainer", true}, + "GetTime": {"System.Runtime.GetTime", false}, + "GetTrigger": {"System.Runtime.GetTrigger", false}, + "CheckWitness": {"System.Runtime.CheckWitness", false}, + "Log": {"System.Runtime.Log", false}, + "Notify": {"System.Runtime.Notify", false}, + }, + "storage": { + "ConvertContextToReadOnly": {"System.Storage.AsReadOnly", false}, + "Delete": {"System.Storage.Delete", false}, + "Find": {"System.Storage.Find", false}, + "Get": {"System.Storage.Get", false}, + "GetContext": {"System.Storage.GetContext", false}, + "GetReadOnlyContext": {"System.Storage.GetReadOnlyContext", false}, + "Put": {"System.Storage.Put", false}, }, } diff --git a/pkg/core/interop_neo.go b/pkg/core/interop_neo.go index 47cff726f..cf41306c4 100644 --- a/pkg/core/interop_neo.go +++ b/pkg/core/interop_neo.go @@ -2,12 +2,14 @@ package core import ( "bytes" + "encoding/base64" "errors" "fmt" "sort" "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/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" @@ -72,7 +74,7 @@ func createContractStateFromVM(ic *interop.Context, v *vm.VM) (*state.Contract, var m manifest.Manifest err := m.UnmarshalJSON(manifestBytes) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to retrieve manifest from stack: %v", err) } return &state.Contract{ Script: script, @@ -95,52 +97,91 @@ func contractCreate(ic *interop.Context, v *vm.VM) error { return err } newcontract.ID = id + if !newcontract.Manifest.IsValid(newcontract.ScriptHash()) { + return errors.New("failed to check contract script hash against manifest") + } if err := ic.DAO.PutContractState(newcontract); err != nil { return err } - v.Estack().PushVal(stackitem.NewInterop(newcontract)) + cs, err := contractToStackItem(newcontract) + if err != nil { + return fmt.Errorf("cannot convert contract to stack item: %v", err) + } + v.Estack().PushVal(cs) return nil } -// contractUpdate migrates a contract. +// contractUpdate migrates a contract. This method assumes that Manifest and Script +// of the contract can be updated independently. func contractUpdate(ic *interop.Context, v *vm.VM) error { - contract, err := ic.DAO.GetContractState(v.GetCurrentScriptHash()) + contract, _ := ic.DAO.GetContractState(v.GetCurrentScriptHash()) if contract == nil { return errors.New("contract doesn't exist") } - newcontract, err := createContractStateFromVM(ic, v) - if err != nil { - return err + script := v.Estack().Pop().Bytes() + if len(script) > MaxContractScriptSize { + return errors.New("the script is too big") } - if newcontract.Script != nil { - if l := len(newcontract.Script); l == 0 || l > MaxContractScriptSize { + manifestBytes := v.Estack().Pop().Bytes() + if len(manifestBytes) > manifest.MaxManifestSize { + return errors.New("manifest is too big") + } + if !v.AddGas(int64(StoragePrice * (len(script) + len(manifestBytes)))) { + return errGasLimitExceeded + } + // if script was provided, update the old contract script and Manifest.ABI hash + if l := len(script); l > 0 { + if l > MaxContractScriptSize { return errors.New("invalid script len") } - h := newcontract.ScriptHash() - if h.Equals(contract.ScriptHash()) { + newHash := hash.Hash160(script) + if newHash.Equals(contract.ScriptHash()) { return errors.New("the script is the same") - } else if _, err := ic.DAO.GetContractState(h); err == nil { + } else if _, err := ic.DAO.GetContractState(newHash); err == nil { return errors.New("contract already exists") } - newcontract.ID = contract.ID - if err := ic.DAO.PutContractState(newcontract); err != nil { - return err + oldHash := contract.ScriptHash() + // re-write existing contract variable, as we need it to be up-to-date during manifest update + contract = &state.Contract{ + ID: contract.ID, + Script: script, + Manifest: contract.Manifest, } - if err := ic.DAO.DeleteContractState(contract.ScriptHash()); err != nil { - return err + contract.Manifest.ABI.Hash = newHash + if err := ic.DAO.PutContractState(contract); err != nil { + return fmt.Errorf("failed to update script: %v", err) + } + if err := ic.DAO.DeleteContractState(oldHash); err != nil { + return fmt.Errorf("failed to update script: %v", err) } } - if !newcontract.HasStorage() { - siMap, err := ic.DAO.GetStorageItems(contract.ID) + // if manifest was provided, update the old contract manifest and check associated + // storage items if needed + if len(manifestBytes) > 0 { + var newManifest manifest.Manifest + err := newManifest.UnmarshalJSON(manifestBytes) if err != nil { - return err + return fmt.Errorf("unable to retrieve manifest from stack: %v", err) } - if len(siMap) != 0 { - return errors.New("old contract shouldn't have storage") + // we don't have to perform `GetContractState` one more time as it's already up-to-date + contract.Manifest = newManifest + if !contract.Manifest.IsValid(contract.ScriptHash()) { + return errors.New("failed to check contract script hash against new manifest") + } + if !contract.HasStorage() { + siMap, err := ic.DAO.GetStorageItems(contract.ID) + if err != nil { + return fmt.Errorf("failed to update manifest: %v", err) + } + if len(siMap) != 0 { + return errors.New("old contract shouldn't have storage") + } + } + if err := ic.DAO.PutContractState(contract); err != nil { + return fmt.Errorf("failed to update manifest: %v", err) } } - v.Estack().PushVal(stackitem.NewInterop(contract)) - return contractDestroy(ic, v) + return nil } // runtimeSerialize serializes top stack item into a ByteArray. @@ -152,3 +193,22 @@ func runtimeSerialize(_ *interop.Context, v *vm.VM) error { func runtimeDeserialize(_ *interop.Context, v *vm.VM) error { return vm.RuntimeDeserialize(v) } + +// runtimeEncode encodes top stack item into a base64 string. +func runtimeEncode(_ *interop.Context, v *vm.VM) error { + src := v.Estack().Pop().Bytes() + result := base64.StdEncoding.EncodeToString(src) + v.Estack().PushVal([]byte(result)) + return nil +} + +// runtimeDecode decodes top stack item from base64 string to byte array. +func runtimeDecode(_ *interop.Context, v *vm.VM) error { + src := string(v.Estack().Pop().Bytes()) + result, err := base64.StdEncoding.DecodeString(src) + if err != nil { + return err + } + v.Estack().PushVal(result) + return nil +} diff --git a/pkg/core/interop_neo_test.go b/pkg/core/interop_neo_test.go index 9d26b88b0..2336fed8a 100644 --- a/pkg/core/interop_neo_test.go +++ b/pkg/core/interop_neo_test.go @@ -1,6 +1,7 @@ package core import ( + "encoding/base64" "fmt" "testing" @@ -214,6 +215,39 @@ func TestECDSAVerify(t *testing.T) { }) } +func TestRuntimeEncode(t *testing.T) { + str := []byte("my pretty string") + v, ic, bc := createVM(t) + defer bc.Close() + + v.Estack().PushVal(str) + require.NoError(t, runtimeEncode(ic, v)) + + expected := []byte(base64.StdEncoding.EncodeToString(str)) + actual := v.Estack().Pop().Bytes() + require.Equal(t, expected, actual) +} + +func TestRuntimeDecode(t *testing.T) { + expected := []byte("my pretty string") + str := base64.StdEncoding.EncodeToString(expected) + v, ic, bc := createVM(t) + defer bc.Close() + + t.Run("positive", func(t *testing.T) { + v.Estack().PushVal(str) + require.NoError(t, runtimeDecode(ic, v)) + + actual := v.Estack().Pop().Bytes() + require.Equal(t, expected, actual) + }) + + t.Run("error", func(t *testing.T) { + v.Estack().PushVal(str + "%") + require.Error(t, runtimeDecode(ic, v)) + }) +} + // Helper functions to create VM, InteropContext, TX, Account, Contract. func createVM(t *testing.T) (*vm.VM, *interop.Context, *Blockchain) { diff --git a/pkg/core/interop_system.go b/pkg/core/interop_system.go index 003c6acda..54c77a315 100644 --- a/pkg/core/interop_system.go +++ b/pkg/core/interop_system.go @@ -83,6 +83,20 @@ func bcGetBlock(ic *interop.Context, v *vm.VM) error { return nil } +// contractToStackItem converts state.Contract to stackitem.Item +func contractToStackItem(cs *state.Contract) (stackitem.Item, error) { + manifest, err := cs.Manifest.MarshalJSON() + if err != nil { + return nil, err + } + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(cs.Script), + stackitem.NewByteArray(manifest), + stackitem.NewBool(cs.HasStorage()), + stackitem.NewBool(cs.IsPayable()), + }), nil +} + // bcGetContract returns contract. func bcGetContract(ic *interop.Context, v *vm.VM) error { hashbytes := v.Estack().Pop().Bytes() @@ -92,9 +106,13 @@ func bcGetContract(ic *interop.Context, v *vm.VM) error { } cs, err := ic.DAO.GetContractState(hash) if err != nil { - v.Estack().PushVal([]byte{}) + v.Estack().PushVal(stackitem.Null{}) } else { - v.Estack().PushVal(stackitem.NewInterop(cs)) + item, err := contractToStackItem(cs) + if err != nil { + return err + } + v.Estack().PushVal(item) } return nil } @@ -155,12 +173,12 @@ func bcGetTransactionFromBlock(ic *interop.Context, v *vm.VM) error { if err != nil { return err } + index := v.Estack().Pop().BigInt().Int64() block, err := ic.DAO.GetBlock(hash) if err != nil || !isTraceableBlock(ic, block.Index) { v.Estack().PushVal(stackitem.Null{}) return nil } - index := v.Estack().Pop().BigInt().Int64() if index < 0 || index >= int64(len(block.Transactions)) { return errors.New("wrong transaction index") } @@ -422,6 +440,9 @@ func contractCallEx(ic *interop.Context, v *vm.VM) error { method := v.Estack().Pop().Item() args := v.Estack().Pop().Item() flags := smartcontract.CallFlag(int32(v.Estack().Pop().BigInt().Int64())) + if flags&^smartcontract.All != 0 { + return errors.New("call flags out of range") + } return contractCallExInternal(ic, v, h, method, args, flags) } @@ -483,8 +504,17 @@ func contractIsStandard(ic *interop.Context, v *vm.VM) error { } var result bool cs, _ := ic.DAO.GetContractState(u) - if cs == nil || vm.IsStandardContract(cs.Script) { - result = true + if cs != nil { + result = vm.IsStandardContract(cs.Script) + } else { + if tx, ok := ic.Container.(*transaction.Transaction); ok { + for _, witness := range tx.Scripts { + if witness.ScriptHash() == u { + result = vm.IsStandardContract(witness.VerificationScript) + break + } + } + } } v.Estack().PushVal(result) return nil @@ -500,3 +530,9 @@ func contractCreateStandardAccount(ic *interop.Context, v *vm.VM) error { v.Estack().PushVal(p.GetScriptHash().BytesBE()) return nil } + +// contractGetCallFlags returns current context calling flags. +func contractGetCallFlags(_ *interop.Context, v *vm.VM) error { + v.Estack().PushVal(v.Context().GetCallFlags()) + return nil +} diff --git a/pkg/core/interop_system_test.go b/pkg/core/interop_system_test.go index a47894946..b115b8f68 100644 --- a/pkg/core/interop_system_test.go +++ b/pkg/core/interop_system_test.go @@ -5,10 +5,14 @@ import ( "testing" "github.com/nspcc-dev/dbft/crypto" + "github.com/nspcc-dev/neo-go/pkg/config/netmode" "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" "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/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/opcode" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" @@ -157,7 +161,35 @@ func TestContractIsStandard(t *testing.T) { v, ic, chain := createVM(t) defer chain.Close() - t.Run("True", func(t *testing.T) { + t.Run("contract not stored", func(t *testing.T) { + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + + pub := priv.PublicKey() + tx := transaction.New(netmode.TestNet, []byte{1, 2, 3}, 1) + tx.Scripts = []transaction.Witness{ + { + InvocationScript: []byte{1, 2, 3}, + VerificationScript: pub.GetVerificationScript(), + }, + } + ic.Container = tx + + t.Run("true", func(t *testing.T) { + v.Estack().PushVal(pub.GetScriptHash().BytesBE()) + require.NoError(t, contractIsStandard(ic, v)) + require.True(t, v.Estack().Pop().Bool()) + }) + + t.Run("false", func(t *testing.T) { + tx.Scripts[0].VerificationScript = []byte{9, 8, 7} + v.Estack().PushVal(pub.GetScriptHash().BytesBE()) + require.NoError(t, contractIsStandard(ic, v)) + require.False(t, v.Estack().Pop().Bool()) + }) + }) + + t.Run("contract stored, true", func(t *testing.T) { priv, err := keys.NewPrivateKey() require.NoError(t, err) @@ -169,7 +201,7 @@ func TestContractIsStandard(t *testing.T) { require.NoError(t, contractIsStandard(ic, v)) require.True(t, v.Estack().Pop().Bool()) }) - t.Run("False", func(t *testing.T) { + t.Run("contract stored, false", func(t *testing.T) { script := []byte{byte(opcode.PUSHT)} require.NoError(t, ic.DAO.PutContractState(&state.Contract{ID: 24, Script: script})) @@ -262,3 +294,300 @@ func TestRuntimeGetInvocationCounter(t *testing.T) { require.EqualValues(t, 42, v.Estack().Pop().BigInt().Int64()) }) } + +func TestBlockchainGetContractState(t *testing.T) { + v, cs, ic, bc := createVMAndContractState(t) + defer bc.Close() + require.NoError(t, ic.DAO.PutContractState(cs)) + + t.Run("positive", func(t *testing.T) { + v.Estack().PushVal(cs.ScriptHash().BytesBE()) + require.NoError(t, bcGetContract(ic, v)) + + actual := v.Estack().Pop().Item() + compareContractStates(t, cs, actual) + }) + + t.Run("uncknown contract state", func(t *testing.T) { + v.Estack().PushVal(util.Uint160{1, 2, 3}.BytesBE()) + require.NoError(t, bcGetContract(ic, v)) + + actual := v.Estack().Pop().Item() + require.Equal(t, stackitem.Null{}, actual) + }) +} + +func TestContractCreate(t *testing.T) { + v, cs, ic, bc := createVMAndContractState(t) + v.GasLimit = -1 + defer bc.Close() + + putArgsOnStack := func() { + manifest, err := cs.Manifest.MarshalJSON() + require.NoError(t, err) + v.Estack().PushVal(manifest) + v.Estack().PushVal(cs.Script) + } + + t.Run("positive", func(t *testing.T) { + putArgsOnStack() + + require.NoError(t, contractCreate(ic, v)) + actual := v.Estack().Pop().Item() + compareContractStates(t, cs, actual) + }) + + t.Run("invalid scripthash", func(t *testing.T) { + cs.Script = append(cs.Script, 0x01) + putArgsOnStack() + + require.Error(t, contractCreate(ic, v)) + }) + + t.Run("contract already exists", func(t *testing.T) { + cs.Script = cs.Script[:len(cs.Script)-1] + require.NoError(t, ic.DAO.PutContractState(cs)) + putArgsOnStack() + + require.Error(t, contractCreate(ic, v)) + }) +} + +func compareContractStates(t *testing.T, expected *state.Contract, actual stackitem.Item) { + act, ok := actual.Value().([]stackitem.Item) + require.True(t, ok) + + expectedManifest, err := expected.Manifest.MarshalJSON() + require.NoError(t, err) + + require.Equal(t, 4, len(act)) + require.Equal(t, expected.Script, act[0].Value().([]byte)) + require.Equal(t, expectedManifest, act[1].Value().([]byte)) + require.Equal(t, expected.HasStorage(), act[2].Bool()) + require.Equal(t, expected.IsPayable(), act[3].Bool()) +} + +func TestContractUpdate(t *testing.T) { + v, cs, ic, bc := createVMAndContractState(t) + defer bc.Close() + v.GasLimit = -1 + + putArgsOnStack := func(script, manifest []byte) { + v.Estack().PushVal(manifest) + v.Estack().PushVal(script) + } + + t.Run("no args", func(t *testing.T) { + require.NoError(t, ic.DAO.PutContractState(cs)) + v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.ScriptHash(), smartcontract.All) + putArgsOnStack(nil, nil) + require.NoError(t, contractUpdate(ic, v)) + }) + + t.Run("no contract", func(t *testing.T) { + require.NoError(t, ic.DAO.PutContractState(cs)) + v.LoadScriptWithHash([]byte{byte(opcode.RET)}, util.Uint160{8, 9, 7}, smartcontract.All) + require.Error(t, contractUpdate(ic, v)) + }) + + t.Run("too large script", func(t *testing.T) { + require.NoError(t, ic.DAO.PutContractState(cs)) + v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.ScriptHash(), smartcontract.All) + putArgsOnStack(make([]byte, MaxContractScriptSize+1), nil) + require.Error(t, contractUpdate(ic, v)) + }) + + t.Run("too large manifest", func(t *testing.T) { + require.NoError(t, ic.DAO.PutContractState(cs)) + v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.ScriptHash(), smartcontract.All) + putArgsOnStack(nil, make([]byte, manifest.MaxManifestSize+1)) + require.Error(t, contractUpdate(ic, v)) + }) + + t.Run("gas limit exceeded", func(t *testing.T) { + require.NoError(t, ic.DAO.PutContractState(cs)) + v.GasLimit = 0 + v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.ScriptHash(), smartcontract.All) + putArgsOnStack([]byte{1}, []byte{2}) + require.Error(t, contractUpdate(ic, v)) + }) + + t.Run("update script, the same script", func(t *testing.T) { + require.NoError(t, ic.DAO.PutContractState(cs)) + v.GasLimit = -1 + v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.ScriptHash(), smartcontract.All) + putArgsOnStack(cs.Script, nil) + + require.Error(t, contractUpdate(ic, v)) + }) + + t.Run("update script, already exists", func(t *testing.T) { + require.NoError(t, ic.DAO.PutContractState(cs)) + duplicateScript := []byte{byte(opcode.PUSHDATA4)} + require.NoError(t, ic.DAO.PutContractState(&state.Contract{ + ID: 95, + Script: duplicateScript, + Manifest: manifest.Manifest{ + ABI: manifest.ABI{ + Hash: hash.Hash160(duplicateScript), + }, + }, + })) + v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.ScriptHash(), smartcontract.All) + putArgsOnStack(duplicateScript, nil) + + require.Error(t, contractUpdate(ic, v)) + }) + + t.Run("update script, positive", func(t *testing.T) { + require.NoError(t, ic.DAO.PutContractState(cs)) + v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.ScriptHash(), smartcontract.All) + newScript := []byte{9, 8, 7, 6, 5} + putArgsOnStack(newScript, nil) + + require.NoError(t, contractUpdate(ic, v)) + + // updated contract should have new scripthash + actual, err := ic.DAO.GetContractState(hash.Hash160(newScript)) + require.NoError(t, err) + expected := &state.Contract{ + ID: cs.ID, + Script: newScript, + Manifest: cs.Manifest, + } + expected.Manifest.ABI.Hash = hash.Hash160(newScript) + _ = expected.ScriptHash() + require.Equal(t, expected, actual) + + // old contract should be deleted + _, err = ic.DAO.GetContractState(cs.ScriptHash()) + require.Error(t, err) + }) + + t.Run("update manifest, bad manifest", func(t *testing.T) { + require.NoError(t, ic.DAO.PutContractState(cs)) + v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.ScriptHash(), smartcontract.All) + putArgsOnStack(nil, []byte{1, 2, 3}) + + require.Error(t, contractUpdate(ic, v)) + }) + + t.Run("update manifest, bad contract hash", func(t *testing.T) { + require.NoError(t, ic.DAO.PutContractState(cs)) + v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.ScriptHash(), smartcontract.All) + manifest := &manifest.Manifest{ + ABI: manifest.ABI{ + Hash: util.Uint160{4, 5, 6}, + }, + } + manifestBytes, err := manifest.MarshalJSON() + require.NoError(t, err) + putArgsOnStack(nil, manifestBytes) + + require.Error(t, contractUpdate(ic, v)) + }) + + t.Run("update manifest, old contract shouldn't have storage", func(t *testing.T) { + cs.Manifest.Features |= smartcontract.HasStorage + require.NoError(t, ic.DAO.PutContractState(cs)) + require.NoError(t, ic.DAO.PutStorageItem(cs.ID, []byte("my_item"), &state.StorageItem{ + Value: []byte{1, 2, 3}, + IsConst: false, + })) + v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.ScriptHash(), smartcontract.All) + manifest := &manifest.Manifest{ + ABI: manifest.ABI{ + Hash: cs.ScriptHash(), + }, + } + manifestBytes, err := manifest.MarshalJSON() + require.NoError(t, err) + putArgsOnStack(nil, manifestBytes) + + require.Error(t, contractUpdate(ic, v)) + }) + + t.Run("update manifest, positive", func(t *testing.T) { + cs.Manifest.Features = smartcontract.NoProperties + require.NoError(t, ic.DAO.PutContractState(cs)) + v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.ScriptHash(), smartcontract.All) + manifest := &manifest.Manifest{ + ABI: manifest.ABI{ + Hash: cs.ScriptHash(), + EntryPoint: manifest.Method{ + Name: "Main", + Parameters: []manifest.Parameter{ + manifest.NewParameter("NewParameter", smartcontract.IntegerType), + }, + ReturnType: smartcontract.StringType, + }, + }, + Features: smartcontract.HasStorage, + } + manifestBytes, err := manifest.MarshalJSON() + require.NoError(t, err) + putArgsOnStack(nil, manifestBytes) + + require.NoError(t, contractUpdate(ic, v)) + + // updated contract should have new scripthash + actual, err := ic.DAO.GetContractState(cs.ScriptHash()) + expected := &state.Contract{ + ID: cs.ID, + Script: cs.Script, + Manifest: *manifest, + } + _ = expected.ScriptHash() + require.Equal(t, expected, actual) + }) + + t.Run("update both script and manifest", func(t *testing.T) { + require.NoError(t, ic.DAO.PutContractState(cs)) + v.LoadScriptWithHash([]byte{byte(opcode.RET)}, cs.ScriptHash(), smartcontract.All) + newScript := []byte{12, 13, 14} + newManifest := manifest.Manifest{ + ABI: manifest.ABI{ + Hash: hash.Hash160(newScript), + EntryPoint: manifest.Method{ + Name: "Main", + Parameters: []manifest.Parameter{ + manifest.NewParameter("VeryNewParameter", smartcontract.IntegerType), + }, + ReturnType: smartcontract.StringType, + }, + }, + Features: smartcontract.HasStorage, + } + newManifestBytes, err := newManifest.MarshalJSON() + require.NoError(t, err) + + putArgsOnStack(newScript, newManifestBytes) + + require.NoError(t, contractUpdate(ic, v)) + + // updated contract should have new script and manifest + actual, err := ic.DAO.GetContractState(hash.Hash160(newScript)) + require.NoError(t, err) + expected := &state.Contract{ + ID: cs.ID, + Script: newScript, + Manifest: newManifest, + } + expected.Manifest.ABI.Hash = hash.Hash160(newScript) + _ = expected.ScriptHash() + require.Equal(t, expected, actual) + + // old contract should be deleted + _, err = ic.DAO.GetContractState(cs.ScriptHash()) + require.Error(t, err) + }) +} + +func TestContractGetCallFlags(t *testing.T) { + v, ic, bc := createVM(t) + defer bc.Close() + + v.LoadScriptWithHash([]byte{byte(opcode.RET)}, util.Uint160{1, 2, 3}, smartcontract.All) + require.NoError(t, contractGetCallFlags(ic, v)) + require.Equal(t, int64(smartcontract.All), v.Estack().Pop().Value().(*big.Int).Int64()) +} diff --git a/pkg/core/interops.go b/pkg/core/interops.go index bf4f830c9..750713ce5 100644 --- a/pkg/core/interops.go +++ b/pkg/core/interops.go @@ -70,6 +70,8 @@ func getInteropFromSlice(ic *interop.Context, slice []interop.Function) func(uin // All lists are sorted, keep 'em this way, please. var systemInterops = []interop.Function{ + {Name: "System.Binary.Base64Decode", Func: runtimeDecode, Price: 100000}, + {Name: "System.Binary.Base64Encode", Func: runtimeEncode, Price: 100000}, {Name: "System.Binary.Deserialize", Func: runtimeDeserialize, Price: 500000}, {Name: "System.Binary.Serialize", Func: runtimeSerialize, Price: 100000}, {Name: "System.Blockchain.GetBlock", Func: bcGetBlock, Price: 2500000, @@ -94,6 +96,7 @@ var systemInterops = []interop.Function{ {Name: "System.Contract.Destroy", Func: contractDestroy, Price: 1000000, AllowedTriggers: trigger.Application, RequiredFlags: smartcontract.AllowModifyStates}, {Name: "System.Contract.IsStandard", Func: contractIsStandard, Price: 30000}, + {Name: "System.Contract.GetCallFlags", Func: contractGetCallFlags, Price: 30000}, {Name: "System.Contract.Update", Func: contractUpdate, Price: 0, AllowedTriggers: trigger.Application, RequiredFlags: smartcontract.AllowModifyStates}, {Name: "System.Enumerator.Concat", Func: enumerator.Concat, Price: 400}, diff --git a/pkg/interop/binary/binary.go b/pkg/interop/binary/binary.go index 8a847e47c..647e4d19f 100644 --- a/pkg/interop/binary/binary.go +++ b/pkg/interop/binary/binary.go @@ -16,3 +16,15 @@ func Serialize(item interface{}) []byte { func Deserialize(b []byte) interface{} { return nil } + +// Base64Encode encodes given byte slice into a base64 string and returns byte +// representation of this string. It uses `System.Binary.Base64Encode` interop. +func Base64Encode(b []byte) []byte { + return nil +} + +// Base64Decode decodes given base64 string represented as a byte slice into +// byte slice. It uses `System.Binary.Base64Decode` interop. +func Base64Decode(b []byte) []byte { + return nil +} diff --git a/pkg/interop/blockchain/blockchain.go b/pkg/interop/blockchain/blockchain.go index 417cd9d76..22ecb64d7 100644 --- a/pkg/interop/blockchain/blockchain.go +++ b/pkg/interop/blockchain/blockchain.go @@ -3,9 +3,7 @@ Package blockchain provides functions to access various blockchain data. */ package blockchain -import ( - "github.com/nspcc-dev/neo-go/pkg/interop/contract" -) +import "github.com/nspcc-dev/neo-go/pkg/interop/contract" // Transaction represents a NEO transaction. It's similar to Transaction class // in Neo .net framework. diff --git a/pkg/interop/contract/contract.go b/pkg/interop/contract/contract.go index 12e530bc9..4e237f6d9 100644 --- a/pkg/interop/contract/contract.go +++ b/pkg/interop/contract/contract.go @@ -4,10 +4,15 @@ Package contract provides functions to work with contracts. package contract // Contract represents a Neo contract and is used in interop functions. It's -// an opaque data structure that you can manipulate with using functions from +// a data structure that you can manipulate with using functions from // this package. It's similar in function to the Contract class in the Neo .net // framework. -type Contract struct{} +type Contract struct { + Script []byte + Manifest []byte + HasStorage bool + IsPayable bool +} // Create creates a new contract using a set of input parameters: // script contract's bytecode (limited in length by 1M) @@ -23,8 +28,8 @@ func Create(script []byte, manifest []byte) Contract { // Create. The old contract will be deleted by this call, if it has any storage // associated it will be migrated to the new contract. New contract is returned. // This function uses `System.Contract.Update` syscall. -func Update(script []byte, manifest []byte) Contract { - return Contract{} +func Update(script []byte, manifest []byte) { + return } // Destroy deletes calling contract (the one that calls Destroy) from the @@ -44,3 +49,9 @@ func IsStandard(h []byte) bool { func CreateStandardAccount(pub []byte) []byte { return nil } + +// GetCallFlags returns calling flags which execution context was created with. +// This function uses `System.Contract.GetCallFlags` syscall. +func GetCallFlags() int64 { + return 0 +} diff --git a/pkg/smartcontract/manifest/manifest.go b/pkg/smartcontract/manifest/manifest.go index 4989fc9b4..1b1c1bbee 100644 --- a/pkg/smartcontract/manifest/manifest.go +++ b/pkg/smartcontract/manifest/manifest.go @@ -85,6 +85,20 @@ func (m *Manifest) CanCall(toCall *Manifest, method string) bool { return false } +// IsValid checks whether the given hash is the one specified in manifest and +// verifies it against all the keys in manifest groups. +func (m *Manifest) IsValid(hash util.Uint160) bool { + if m.ABI.Hash != hash { + return false + } + for _, g := range m.Groups { + if !g.IsValid(hash) { + return false + } + } + return true +} + // MarshalJSON implements json.Marshaler interface. func (m *Manifest) MarshalJSON() ([]byte, error) { features := make(map[string]bool) diff --git a/pkg/smartcontract/manifest/manifest_test.go b/pkg/smartcontract/manifest/manifest_test.go index a5aec9b32..c012829a4 100644 --- a/pkg/smartcontract/manifest/manifest_test.go +++ b/pkg/smartcontract/manifest/manifest_test.go @@ -119,3 +119,50 @@ func TestPermission_IsAllowed(t *testing.T) { require.False(t, perm.IsAllowed(manifest, "AAA")) }) } + +func TestIsValid(t *testing.T) { + contractHash := util.Uint160{1, 2, 3} + m := NewManifest(contractHash) + + t.Run("valid, no groups", func(t *testing.T) { + require.True(t, m.IsValid(contractHash)) + }) + + t.Run("invalid, no groups", func(t *testing.T) { + require.False(t, m.IsValid(util.Uint160{9, 8, 7})) + }) + + t.Run("with groups", func(t *testing.T) { + m.Groups = make([]Group, 3) + pks := make([]*keys.PrivateKey, 3) + for i := range pks { + pk, err := keys.NewPrivateKey() + require.NoError(t, err) + pks[i] = pk + m.Groups[i] = Group{ + PublicKey: pk.PublicKey(), + Signature: pk.Sign(contractHash.BytesBE()), + } + } + + t.Run("valid", func(t *testing.T) { + require.True(t, m.IsValid(contractHash)) + }) + + t.Run("invalid, wrong contract hash", func(t *testing.T) { + require.False(t, m.IsValid(util.Uint160{4, 5, 6})) + }) + + t.Run("invalid, wrong group signature", func(t *testing.T) { + pk, err := keys.NewPrivateKey() + require.NoError(t, err) + m.Groups = append(m.Groups, Group{ + PublicKey: pk.PublicKey(), + // actually, there shouldn't be such situation, as Signature is always the signature + // of the contract hash. + Signature: pk.Sign([]byte{1, 2, 3}), + }) + require.False(t, m.IsValid(contractHash)) + }) + }) +} diff --git a/pkg/smartcontract/manifest/method.go b/pkg/smartcontract/manifest/method.go index 25e44bd21..d4ae4ecfd 100644 --- a/pkg/smartcontract/manifest/method.go +++ b/pkg/smartcontract/manifest/method.go @@ -4,8 +4,10 @@ import ( "encoding/hex" "encoding/json" + "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/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" ) // Parameter represents smartcontract's parameter's definition. @@ -60,6 +62,11 @@ func DefaultEntryPoint() *Method { } } +// IsValid checks whether group's signature corresponds to the given hash. +func (g *Group) IsValid(h util.Uint160) bool { + return g.PublicKey.Verify(g.Signature, hash.Sha256(h.BytesBE()).BytesBE()) +} + // MarshalJSON implements json.Marshaler interface. func (g *Group) MarshalJSON() ([]byte, error) { aux := &groupAux{