From a1f98f92fe01f263311f74493f520d878a6f129e Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 16 Jul 2020 12:39:54 +0300 Subject: [PATCH 1/9] compiler: add ConvertResultToStruct flag Part of #1055. There'll be a lot of interops which result with a struct on stack instead of interop interface, and sometimes their names are the same, so it's unrelyable to take into account interop name only and don't pay attention to it's API (package). Also sort syscalls by package and name. --- pkg/compiler/codegen.go | 9 ++- pkg/compiler/syscall.go | 129 +++++++++++++++++++++------------------- 2 files changed, 71 insertions(+), 67 deletions(-) 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..88926e5fd 100644 --- a/pkg/compiler/syscall.go +++ b/pkg/compiler/syscall.go @@ -1,73 +1,78 @@ 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": { + "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", false}, + "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", false}, + "CreateStandardAccount": {"System.Contract.CreateStandardAccount", false}, + "Destroy": {"System.Contract.Destroy", false}, + "IsStandard": {"System.Contract.IsStandard", 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}, }, } From 74ffde83674f6a1af196ce7376816030c75bd0c5 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 15 Jul 2020 13:05:29 +0300 Subject: [PATCH 2/9] core: adjust System.Blockchain.GetTransactionFromBlock interop To match C# implementation, we should pick all arguments from stack first. --- pkg/core/interop_system.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/core/interop_system.go b/pkg/core/interop_system.go index 003c6acda..3d84b7d24 100644 --- a/pkg/core/interop_system.go +++ b/pkg/core/interop_system.go @@ -155,12 +155,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") } From d2f452c2400f8fdabb8a87d9297ac602edfd0374 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 15 Jul 2020 13:53:24 +0300 Subject: [PATCH 3/9] core: adjust System.Contract.CallEx interop Part of #1055 --- pkg/core/interop_system.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/core/interop_system.go b/pkg/core/interop_system.go index 3d84b7d24..06632c813 100644 --- a/pkg/core/interop_system.go +++ b/pkg/core/interop_system.go @@ -422,6 +422,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) } From 1a5fb01e61ee85cecf81ea172fb974b75f2b3260 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 16 Jul 2020 09:56:57 +0300 Subject: [PATCH 4/9] core: adjust System.Contract.IsStandard interop Part of #1055. It should check not only stored contracts, but also interop context script container in case if it's a transaction. --- pkg/core/interop_system.go | 13 +++++++++++-- pkg/core/interop_system_test.go | 34 +++++++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/pkg/core/interop_system.go b/pkg/core/interop_system.go index 06632c813..de84623aa 100644 --- a/pkg/core/interop_system.go +++ b/pkg/core/interop_system.go @@ -486,8 +486,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 diff --git a/pkg/core/interop_system_test.go b/pkg/core/interop_system_test.go index a47894946..e17b3ffe2 100644 --- a/pkg/core/interop_system_test.go +++ b/pkg/core/interop_system_test.go @@ -5,8 +5,10 @@ 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/util" @@ -157,7 +159,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 +199,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})) From d2ec0fed3d57d6b4fc7a82282472d0f5a9b9ecc0 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 15 Jul 2020 12:53:09 +0300 Subject: [PATCH 5/9] core: adjust System.Blockchain.GetContract interop Part of #1055. It should put on stack an array instead of interop interface. --- pkg/compiler/syscall.go | 2 +- pkg/core/interop_system.go | 22 ++++++++++++++++++++-- pkg/core/interop_system_test.go | 28 ++++++++++++++++++++++++++++ pkg/interop/blockchain/blockchain.go | 16 ++++++++++------ 4 files changed, 59 insertions(+), 9 deletions(-) diff --git a/pkg/compiler/syscall.go b/pkg/compiler/syscall.go index 88926e5fd..6f96c5f79 100644 --- a/pkg/compiler/syscall.go +++ b/pkg/compiler/syscall.go @@ -14,7 +14,7 @@ var syscalls = map[string]map[string]Syscall{ }, "blockchain": { "GetBlock": {"System.Blockchain.GetBlock", true}, - "GetContract": {"System.Blockchain.GetContract", false}, + "GetContract": {"System.Blockchain.GetContract", true}, "GetHeight": {"System.Blockchain.GetHeight", false}, "GetTransaction": {"System.Blockchain.GetTransaction", true}, "GetTransactionFromBlock": {"System.Blockchain.GetTransactionFromBlock", false}, diff --git a/pkg/core/interop_system.go b/pkg/core/interop_system.go index de84623aa..65f99db65 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 } diff --git a/pkg/core/interop_system_test.go b/pkg/core/interop_system_test.go index e17b3ffe2..e5ecf893e 100644 --- a/pkg/core/interop_system_test.go +++ b/pkg/core/interop_system_test.go @@ -292,3 +292,31 @@ 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)) + + expectedManifest, err := cs.Manifest.MarshalJSON() + require.NoError(t, err) + actual := v.Estack().Pop().Array() + require.Equal(t, 4, len(actual)) + require.Equal(t, cs.Script, actual[0].Value().([]byte)) + require.Equal(t, expectedManifest, actual[1].Value().([]byte)) + require.Equal(t, cs.HasStorage(), actual[2].Bool()) + require.Equal(t, cs.IsPayable(), actual[3].Bool()) + }) + + 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) + }) +} diff --git a/pkg/interop/blockchain/blockchain.go b/pkg/interop/blockchain/blockchain.go index 417cd9d76..c8e582a80 100644 --- a/pkg/interop/blockchain/blockchain.go +++ b/pkg/interop/blockchain/blockchain.go @@ -3,10 +3,6 @@ Package blockchain provides functions to access various blockchain data. */ package blockchain -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. type Transaction struct { @@ -57,6 +53,14 @@ type Block struct { TransactionsLength int } +// Contract represents a Neo contract and is used in interop functions. +type Contract struct { + Script []byte + Manifest []byte + HasStorage bool + IsPayable bool +} + // GetHeight returns current block height (index of the last accepted block). // Note that when transaction is being run as a part of new block this block is // considered as not yet accepted (persisted) and thus you'll get an index of @@ -99,6 +103,6 @@ func GetTransactionHeight(hash []byte) int { // format represented as a slice of 20 bytes). Refer to the `contract` package // for details on how to use the returned structure. This function uses // `System.Blockchain.GetContract` syscall. -func GetContract(scriptHash []byte) contract.Contract { - return contract.Contract{} +func GetContract(scriptHash []byte) Contract { + return Contract{} } From fddad0b47548c3d20d5cb5ad7de28c60b3eec5ff Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 15 Jul 2020 14:39:20 +0300 Subject: [PATCH 6/9] core: adjust System.Contract.Create interop Part of #1055. It should check given scripthash against manifest groups and return the contract state as a struct (not interop interface). --- pkg/compiler/syscall.go | 2 +- pkg/core/interop_neo.go | 11 +++- pkg/core/interop_system_test.go | 60 ++++++++++++++++++--- pkg/interop/blockchain/blockchain.go | 14 ++--- pkg/interop/contract/contract.go | 9 +++- pkg/smartcontract/manifest/manifest.go | 14 +++++ pkg/smartcontract/manifest/manifest_test.go | 47 ++++++++++++++++ pkg/smartcontract/manifest/method.go | 7 +++ 8 files changed, 141 insertions(+), 23 deletions(-) diff --git a/pkg/compiler/syscall.go b/pkg/compiler/syscall.go index 6f96c5f79..cc2668c43 100644 --- a/pkg/compiler/syscall.go +++ b/pkg/compiler/syscall.go @@ -21,7 +21,7 @@ var syscalls = map[string]map[string]Syscall{ "GetTransactionHeight": {"System.Blockchain.GetTransactionHeight", false}, }, "contract": { - "Create": {"System.Contract.Create", false}, + "Create": {"System.Contract.Create", true}, "CreateStandardAccount": {"System.Contract.CreateStandardAccount", false}, "Destroy": {"System.Contract.Destroy", false}, "IsStandard": {"System.Contract.IsStandard", false}, diff --git a/pkg/core/interop_neo.go b/pkg/core/interop_neo.go index 47cff726f..26d652431 100644 --- a/pkg/core/interop_neo.go +++ b/pkg/core/interop_neo.go @@ -72,7 +72,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,10 +95,17 @@ 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 } diff --git a/pkg/core/interop_system_test.go b/pkg/core/interop_system_test.go index e5ecf893e..b8636ba2d 100644 --- a/pkg/core/interop_system_test.go +++ b/pkg/core/interop_system_test.go @@ -302,14 +302,8 @@ func TestBlockchainGetContractState(t *testing.T) { v.Estack().PushVal(cs.ScriptHash().BytesBE()) require.NoError(t, bcGetContract(ic, v)) - expectedManifest, err := cs.Manifest.MarshalJSON() - require.NoError(t, err) - actual := v.Estack().Pop().Array() - require.Equal(t, 4, len(actual)) - require.Equal(t, cs.Script, actual[0].Value().([]byte)) - require.Equal(t, expectedManifest, actual[1].Value().([]byte)) - require.Equal(t, cs.HasStorage(), actual[2].Bool()) - require.Equal(t, cs.IsPayable(), actual[3].Bool()) + actual := v.Estack().Pop().Item() + compareContractStates(t, cs, actual) }) t.Run("uncknown contract state", func(t *testing.T) { @@ -320,3 +314,53 @@ func TestBlockchainGetContractState(t *testing.T) { 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()) +} diff --git a/pkg/interop/blockchain/blockchain.go b/pkg/interop/blockchain/blockchain.go index c8e582a80..22ecb64d7 100644 --- a/pkg/interop/blockchain/blockchain.go +++ b/pkg/interop/blockchain/blockchain.go @@ -3,6 +3,8 @@ Package blockchain provides functions to access various blockchain data. */ package blockchain +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. type Transaction struct { @@ -53,14 +55,6 @@ type Block struct { TransactionsLength int } -// Contract represents a Neo contract and is used in interop functions. -type Contract struct { - Script []byte - Manifest []byte - HasStorage bool - IsPayable bool -} - // GetHeight returns current block height (index of the last accepted block). // Note that when transaction is being run as a part of new block this block is // considered as not yet accepted (persisted) and thus you'll get an index of @@ -103,6 +97,6 @@ func GetTransactionHeight(hash []byte) int { // format represented as a slice of 20 bytes). Refer to the `contract` package // for details on how to use the returned structure. This function uses // `System.Blockchain.GetContract` syscall. -func GetContract(scriptHash []byte) Contract { - return Contract{} +func GetContract(scriptHash []byte) contract.Contract { + return contract.Contract{} } diff --git a/pkg/interop/contract/contract.go b/pkg/interop/contract/contract.go index 12e530bc9..4ef212b34 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) diff --git a/pkg/smartcontract/manifest/manifest.go b/pkg/smartcontract/manifest/manifest.go index e5b355498..d37b0f8af 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 f0869e946..be2e06217 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 4a3ada56a..6c761a4aa 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{ From 842feb2533f1e7cf2e52f409cba15b45b9a4b7a8 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 16 Jul 2020 11:32:32 +0300 Subject: [PATCH 7/9] core: adjust System.Contract.Update interop Part of #1055. We should check contract scripthash against the one provided in manifest and manifest groups. We shouldn't put on stack anything after return. And ofcourse, we mast not destroy the old contract at the end, as `contractDestroy` removes all storage items associated with the old contract ID (which equals to the new contract ID). We just remove old contract state - it's enough. --- pkg/core/interop_neo.go | 77 +++++++---- pkg/core/interop_system_test.go | 218 +++++++++++++++++++++++++++++++ pkg/interop/contract/contract.go | 4 +- 3 files changed, 275 insertions(+), 24 deletions(-) diff --git a/pkg/core/interop_neo.go b/pkg/core/interop_neo.go index 26d652431..721d8ddc0 100644 --- a/pkg/core/interop_neo.go +++ b/pkg/core/interop_neo.go @@ -8,6 +8,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/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" @@ -109,45 +110,77 @@ func contractCreate(ic *interop.Context, v *vm.VM) error { 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. diff --git a/pkg/core/interop_system_test.go b/pkg/core/interop_system_test.go index b8636ba2d..37f8fd462 100644 --- a/pkg/core/interop_system_test.go +++ b/pkg/core/interop_system_test.go @@ -11,6 +11,8 @@ import ( "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" @@ -364,3 +366,219 @@ func compareContractStates(t *testing.T, expected *state.Contract, actual stacki 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) + }) +} diff --git a/pkg/interop/contract/contract.go b/pkg/interop/contract/contract.go index 4ef212b34..c6ce760aa 100644 --- a/pkg/interop/contract/contract.go +++ b/pkg/interop/contract/contract.go @@ -28,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 From f31ce9289db5ac526b351ed85141bf883b0a3a0f Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 16 Jul 2020 12:13:55 +0300 Subject: [PATCH 8/9] core: add System.Contract.GetCallFlags interop Part of #1055. It returns calling flags of the current context. --- pkg/compiler/syscall.go | 1 + pkg/core/interop_system.go | 6 ++++++ pkg/core/interop_system_test.go | 9 +++++++++ pkg/core/interops.go | 1 + pkg/interop/contract/contract.go | 6 ++++++ 5 files changed, 23 insertions(+) diff --git a/pkg/compiler/syscall.go b/pkg/compiler/syscall.go index cc2668c43..c88a0d928 100644 --- a/pkg/compiler/syscall.go +++ b/pkg/compiler/syscall.go @@ -25,6 +25,7 @@ var syscalls = map[string]map[string]Syscall{ "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": { diff --git a/pkg/core/interop_system.go b/pkg/core/interop_system.go index 65f99db65..54c77a315 100644 --- a/pkg/core/interop_system.go +++ b/pkg/core/interop_system.go @@ -530,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 37f8fd462..b115b8f68 100644 --- a/pkg/core/interop_system_test.go +++ b/pkg/core/interop_system_test.go @@ -582,3 +582,12 @@ func TestContractUpdate(t *testing.T) { 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 e9a6da597..d2dcd6958 100644 --- a/pkg/core/interops.go +++ b/pkg/core/interops.go @@ -93,6 +93,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/contract/contract.go b/pkg/interop/contract/contract.go index c6ce760aa..4e237f6d9 100644 --- a/pkg/interop/contract/contract.go +++ b/pkg/interop/contract/contract.go @@ -49,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 +} From 74d2f437f4b75f0a5db373a5617d9376a4cbb40a Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 15 Jul 2020 11:49:24 +0300 Subject: [PATCH 9/9] core: add System.Binary.Base64Encode(Decode) interops Part of #1055 --- pkg/compiler/syscall.go | 6 ++++-- pkg/core/interop_neo.go | 20 ++++++++++++++++++++ pkg/core/interop_neo_test.go | 34 ++++++++++++++++++++++++++++++++++ pkg/core/interops.go | 2 ++ pkg/interop/binary/binary.go | 12 ++++++++++++ 5 files changed, 72 insertions(+), 2 deletions(-) diff --git a/pkg/compiler/syscall.go b/pkg/compiler/syscall.go index c88a0d928..276acb26b 100644 --- a/pkg/compiler/syscall.go +++ b/pkg/compiler/syscall.go @@ -9,8 +9,10 @@ type Syscall struct { // All lists are sorted, keep 'em this way, please. var syscalls = map[string]map[string]Syscall{ "binary": { - "Deserialize": {"System.Binary.Deserialize", false}, - "Serialize": {"System.Binary.Serialize", false}, + "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", true}, diff --git a/pkg/core/interop_neo.go b/pkg/core/interop_neo.go index 721d8ddc0..cf41306c4 100644 --- a/pkg/core/interop_neo.go +++ b/pkg/core/interop_neo.go @@ -2,6 +2,7 @@ package core import ( "bytes" + "encoding/base64" "errors" "fmt" "sort" @@ -192,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/interops.go b/pkg/core/interops.go index d2dcd6958..2e9ebed86 100644 --- a/pkg/core/interops.go +++ b/pkg/core/interops.go @@ -69,6 +69,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, 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 +}