From a5979f6d640706293fae3f44806c383130c498d5 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 23 Mar 2022 11:58:07 +0300 Subject: [PATCH 01/14] examples: add defer/recover to OnNEP17Payment handlers Transaction must be ABORTed, exceptions are not sufficient. --- examples/nft-d/nft.go | 6 ++++++ examples/nft-nd/nft.go | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/examples/nft-d/nft.go b/examples/nft-d/nft.go index ed238f528..6257adce5 100644 --- a/examples/nft-d/nft.go +++ b/examples/nft-d/nft.go @@ -351,6 +351,12 @@ func removeOwner(ctx storage.Context, token []byte, holder interop.Hash160) { // this method directly, instead it's called by GAS contract when you transfer // GAS from your address to the address of this NFT contract. func OnNEP17Payment(from interop.Hash160, amount int, data interface{}) { + defer func() { + if r := recover(); r != nil { + runtime.Log(r.(string)) + util.Abort() + } + }() if string(runtime.GetCallingScriptHash()) != gas.Hash { panic("only GAS is accepted") } diff --git a/examples/nft-nd/nft.go b/examples/nft-nd/nft.go index d54443802..b8f8a7faf 100644 --- a/examples/nft-nd/nft.go +++ b/examples/nft-nd/nft.go @@ -210,6 +210,12 @@ func postTransfer(from interop.Hash160, to interop.Hash160, token []byte, data i // this method directly, instead it's called by GAS contract when you transfer // GAS from your address to the address of this NFT contract. func OnNEP17Payment(from interop.Hash160, amount int, data interface{}) { + defer func() { + if r := recover(); r != nil { + runtime.Log(r.(string)) + util.Abort() + } + }() if string(runtime.GetCallingScriptHash()) != gas.Hash { panic("only GAS is accepted") } From 26c3a6c1619121a18cdfc6d80714f69dc50f5fda Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 23 Mar 2022 18:18:03 +0300 Subject: [PATCH 02/14] examples: drop token-sale contract It makes zero sense, it's not what it says it is. AmountPerNEO/MaxExchangeLimitRound are not used at all to affect minting and allowance/transferFrom features are largely obsolete. We have proper minting implemented in NFT contracts and there is a nice NEP-17 available also. --- examples/README.md | 1 - examples/token-sale/go.mod | 5 - examples/token-sale/go.sum | 2 - examples/token-sale/token_sale.go | 279 ----------------------------- examples/token-sale/token_sale.yml | 13 -- 5 files changed, 300 deletions(-) delete mode 100644 examples/token-sale/go.mod delete mode 100644 examples/token-sale/go.sum delete mode 100644 examples/token-sale/token_sale.go delete mode 100644 examples/token-sale/token_sale.yml diff --git a/examples/README.md b/examples/README.md index c24eff814..c42cd66ba 100644 --- a/examples/README.md +++ b/examples/README.md @@ -33,7 +33,6 @@ See the table below for the detailed examples description. | [storage](storage) | The contract implements API for basic operations with a contract storage. It shows hos to use `storage` interop package. See the `storage` [package documentation](../pkg/interop/storage/storage.go). | | [timer](timer) | The idea of the contract is to count `tick` method invocations and destroy itself after the third invocation. It shows how to use `contract.Call` interop function to call, update (migrate) and destroy the contract. Please, refer to the `contract.Call` [function documentation](../pkg/interop/contract/contract.go) | | [token](token) | This contract implements NEP-17 token standard (like NEO and GAS tokens) with all required methods and operations. See the NEP-17 token standard [specification](https://github.com/neo-project/proposals/pull/126) for details. | -| [token-sale](token-sale) | The contract represents a token with `allowance`. It means that the token owner should approve token withdrawing before the transfer. The contract demonstrates how interop packages can be combined to work together. | ## Compile diff --git a/examples/token-sale/go.mod b/examples/token-sale/go.mod deleted file mode 100644 index 58e2026c0..000000000 --- a/examples/token-sale/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/nspcc-dev/neo-go/examples/token-sale - -go 1.16 - -require github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20220321144137-d5a9af5860af diff --git a/examples/token-sale/go.sum b/examples/token-sale/go.sum deleted file mode 100644 index d6c936018..000000000 --- a/examples/token-sale/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20220321144137-d5a9af5860af h1:QO3pU/jSYyX3EHBX8BPO01oRkVhGBXPrQaQEhn+4fv8= -github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20220321144137-d5a9af5860af/go.mod h1:QBE0I30F2kOAISNpT5oks82yF4wkkUq3SCfI3Hqgx/Y= diff --git a/examples/token-sale/token_sale.go b/examples/token-sale/token_sale.go deleted file mode 100644 index 8593e9a04..000000000 --- a/examples/token-sale/token_sale.go +++ /dev/null @@ -1,279 +0,0 @@ -package tokensale - -import ( - "github.com/nspcc-dev/neo-go/pkg/interop" - "github.com/nspcc-dev/neo-go/pkg/interop/runtime" - "github.com/nspcc-dev/neo-go/pkg/interop/storage" - "github.com/nspcc-dev/neo-go/pkg/interop/util" -) - -const ( - decimals = 8 - multiplier = decimals * 10 -) - -var ( - owner = util.FromAddress("NbrUYaZgyhSkNoRo9ugRyEMdUZxrhkNaWB") - trigger byte - token TokenConfig - ctx storage.Context -) - -// TokenConfig holds information about the token we want to use for the sale. -type TokenConfig struct { - // Name of the token. - Name string - // 3 letter abbreviation of the token. - Symbol string - // How decimals this token will have. - Decimals int - // Address of the token owner. This is the Uint160 hash. - Owner []byte - // The total amount of tokens created. Notice that we need to multiply the - // amount by 100000000. (10^8) - TotalSupply int - // Initial amount is number of tokens that are available for the token sale. - InitialAmount int - // How many NEO will be worth 1 token. For example: - // Lets say 1 euro per token, where 1 NEO is 60 euro. This means buyers - // will get (60 * 10^8) tokens for 1 NEO. - AmountPerNEO int - // How many Gas will be worth 1 token. This is the same calculation as - // for the AmountPerNEO, except Gas price will have a different value. - AmountPerGas int - // The maximum amount you can mint in the limited round. For example: - // 500 NEO/buyer * 60 tokens/NEO * 10^8 - MaxExchangeLimitRound int - // When to start the token sale. - SaleStart int - // When to end the initial limited round if there is one. For example: - // SaleStart + 10000 - LimitRoundEnd int - // The prefix used to store how many tokens there are in circulation. - CirculationKey []byte - // The prefix used to store how many tokens there are in the limited round. - LimitRoundKey []byte - // The prefix used to store the addresses that are registered with KYC. - KYCKey []byte -} - -// newTokenConfig returns the initialized TokenConfig. -func newTokenConfig() TokenConfig { - return TokenConfig{ - Name: "My awesome token", - Symbol: "MAT", - Decimals: decimals, - Owner: owner, - TotalSupply: 10000000 * multiplier, - InitialAmount: 5000000 * multiplier, - AmountPerNEO: 60 * multiplier, - AmountPerGas: 40 * multiplier, - MaxExchangeLimitRound: 500 * 60 * multiplier, - SaleStart: 75500, - LimitRoundEnd: 75500 + 10000, - CirculationKey: []byte("in_circulation"), - LimitRoundKey: []byte("r1"), - KYCKey: []byte("kyc_ok"), - } -} - -// getIntFromDB is a helper that checks for nil result of storage.Get and returns -// zero as the default value. -func getIntFromDB(ctx storage.Context, key []byte) int { - var res int - val := storage.Get(ctx, key) - if val != nil { - res = val.(int) - } - return res -} - -// InCirculation returns the amount of total tokens that are in circulation. -func InCirculation() int { - return getIntFromDB(ctx, token.CirculationKey) -} - -// addToCirculation sets the given amount as "in circulation" in the storage. -func addToCirculation(amount int) bool { - if amount < 0 { - return false - } - supply := getIntFromDB(ctx, token.CirculationKey) - supply += amount - if supply > token.TotalSupply { - return false - } - storage.Put(ctx, token.CirculationKey, supply) - return true -} - -// AvailableAmount returns the total amount of available tokens left -// to be distributed. -func AvailableAmount() int { - inCirc := getIntFromDB(ctx, token.CirculationKey) - return token.TotalSupply - inCirc -} - -// init initializes runtime trigger, TokenConfig and storage context before any -// other contract method is called -func init() { - trigger = runtime.GetTrigger() - token = newTokenConfig() - ctx = storage.GetContext() -} - -// checkOwnerWitness is a helper function which checks whether the invoker is the -// owner of the contract. -func checkOwnerWitness() bool { - // This is used to verify if a transfer of system assets (NEO and Gas) - // involving this contract's address can proceed. - if trigger == runtime.Application { - // Check if the invoker is the owner of the contract. - return runtime.CheckWitness(token.Owner) - } - return false -} - -// Decimals returns the token decimals -func Decimals() int { - if trigger != runtime.Application { - panic("invalid trigger") - } - return token.Decimals -} - -// Symbol returns the token symbol -func Symbol() string { - if trigger != runtime.Application { - panic("invalid trigger") - } - return token.Symbol -} - -// TotalSupply returns the token total supply value -func TotalSupply() int { - if trigger != runtime.Application { - panic("invalid trigger") - } - return getIntFromDB(ctx, token.CirculationKey) -} - -// BalanceOf returns the amount of token on the specified address -func BalanceOf(holder interop.Hash160) int { - if trigger != runtime.Application { - panic("invalid trigger") - } - return getIntFromDB(ctx, holder) -} - -// Transfer transfers specified amount of token from one user to another -func Transfer(from, to interop.Hash160, amount int, _ interface{}) bool { - if trigger != runtime.Application { - return false - } - if amount <= 0 || len(to) != 20 || !runtime.CheckWitness(from) { - return false - } - amountFrom := getIntFromDB(ctx, from) - if amountFrom < amount { - return false - } - if amountFrom == amount { - storage.Delete(ctx, from) - } else { - diff := amountFrom - amount - storage.Put(ctx, from, diff) - } - amountTo := getIntFromDB(ctx, to) - totalAmountTo := amountTo + amount - if totalAmountTo != 0 { - storage.Put(ctx, to, totalAmountTo) - } - return true -} - -// TransferFrom transfers specified amount of token from one user to another. -// It differs from Transfer in that it use allowance value to store the amount -// of token available to transfer. -func TransferFrom(from, to []byte, amount int) bool { - if trigger != runtime.Application { - return false - } - if amount <= 0 { - return false - } - availableKey := append(from, to...) - if len(availableKey) != 40 { - return false - } - availableTo := getIntFromDB(ctx, availableKey) - if availableTo < amount { - return false - } - fromBalance := getIntFromDB(ctx, from) - if fromBalance < amount { - return false - } - toBalance := getIntFromDB(ctx, to) - newFromBalance := fromBalance - amount - newToBalance := toBalance + amount - storage.Put(ctx, to, newToBalance) - storage.Put(ctx, from, newFromBalance) - - newAllowance := availableTo - amount - if newAllowance == 0 { - storage.Delete(ctx, availableKey) - } else { - storage.Put(ctx, availableKey, newAllowance) - } - return true -} - -// Approve stores token transfer data if the owner has enough token to send. -func Approve(owner, spender []byte, amount int) bool { - if !checkOwnerWitness() || amount < 0 { - return false - } - if len(spender) != 20 { - return false - } - toSpend := getIntFromDB(ctx, owner) - if toSpend < amount { - return false - } - approvalKey := append(owner, spender...) - if amount == 0 { - storage.Delete(ctx, approvalKey) - } else { - storage.Put(ctx, approvalKey, amount) - } - return true -} - -// Allowance returns allowance value for specified sender and receiver. -func Allowance(from, to []byte) interface{} { - if trigger != runtime.Application { - return false - } - key := append(from, to...) - return getIntFromDB(ctx, key) -} - -// Mint initial supply of tokens -func Mint(to []byte) bool { - if trigger != runtime.Application { - return false - } - if !checkOwnerWitness() { - return false - } - minted := storage.Get(ctx, []byte("minted")) - if minted != nil && minted.(bool) == true { - return false - } - - storage.Put(ctx, to, token.TotalSupply) - storage.Put(ctx, []byte("minted"), true) - addToCirculation(token.TotalSupply) - return true -} diff --git a/examples/token-sale/token_sale.yml b/examples/token-sale/token_sale.yml deleted file mode 100644 index 979bbc4a2..000000000 --- a/examples/token-sale/token_sale.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: "My awesome token" -sourceurl: https://github.com/nspcc-dev/neo-go/ -supportedstandards: ["NEP-17"] -safemethods: ["balanceOf", "decimals", "symbol", "totalSupply"] -events: -- name: Transfer - parameters: - - name: from - type: Hash160 - - name: to - type: Hash160 - - name: amount - type: Integer From 68c76a33077ee17784c47dba87a4ad02fadf99f1 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 23 Mar 2022 18:22:23 +0300 Subject: [PATCH 03/14] docs: #2293 was fixed some time ago --- docs/compiler.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/compiler.md b/docs/compiler.md index 4a0760b51..9c60bb7ee 100644 --- a/docs/compiler.md +++ b/docs/compiler.md @@ -22,8 +22,7 @@ a dialect of Go rather than a complete port of the language: * `defer` and `recover` are supported except for cases where panic occurs in `return` statement, because this complicates implementation and imposes runtime overhead for all contracts. This can easily be mitigated by first storing values - in variables and returning the result. `defer` can't be used in - conditional code (#2293). + in variables and returning the result. * lambdas are supported, but closures are not. * maps are supported, but valid map keys are booleans, integers and strings with length <= 64 From 479eda6786193c3a5738b579b06bd79bf58bdf64 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 23 Mar 2022 18:26:02 +0300 Subject: [PATCH 04/14] interop: add LICENSE.md into the package Otherwise pkg.go.dev refuses to display the documentation. --- pkg/interop/LICENSE.md | 1 + 1 file changed, 1 insertion(+) create mode 120000 pkg/interop/LICENSE.md diff --git a/pkg/interop/LICENSE.md b/pkg/interop/LICENSE.md new file mode 120000 index 000000000..f0608a63a --- /dev/null +++ b/pkg/interop/LICENSE.md @@ -0,0 +1 @@ +../../LICENSE.md \ No newline at end of file From 4c4e9c03717c7c3d866d55873f91801af742c9d6 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 23 Mar 2022 22:02:19 +0300 Subject: [PATCH 05/14] examples: add something to the storage for iterator example --- examples/iterator/iterator.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/iterator/iterator.go b/examples/iterator/iterator.go index 03ffccee9..4c830cf6f 100644 --- a/examples/iterator/iterator.go +++ b/examples/iterator/iterator.go @@ -6,6 +6,14 @@ import ( "github.com/nspcc-dev/neo-go/pkg/interop/storage" ) +// _deploy primes contract's storage with some data to be used later. +func _deploy(_ interface{}, _ bool) { + ctx := storage.GetContext() + storage.Put(ctx, "foo1", "1") + storage.Put(ctx, "foo2", "2") + storage.Put(ctx, "foo3", "3") +} + // NotifyKeysAndValues sends notification with `foo` storage keys and values func NotifyKeysAndValues() bool { iter := storage.Find(storage.GetContext(), []byte("foo"), storage.None) From 1d1b4d4c18db357f725f93566f5f6feeb21cd2d0 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 23 Mar 2022 22:04:17 +0300 Subject: [PATCH 06/14] examples: extend iterator with more storage options --- examples/iterator/iterator.go | 9 +++++++++ examples/iterator/iterator.yml | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/examples/iterator/iterator.go b/examples/iterator/iterator.go index 4c830cf6f..a30233fb5 100644 --- a/examples/iterator/iterator.go +++ b/examples/iterator/iterator.go @@ -22,3 +22,12 @@ func NotifyKeysAndValues() bool { } return true } + +// NotifyValues sends notification with `foo` storage values. +func NotifyValues() bool { + iter := storage.Find(storage.GetContext(), []byte("foo"), storage.ValuesOnly) + for iterator.Next(iter) { + runtime.Notify("Value", iterator.Value(iter)) + } + return true +} diff --git a/examples/iterator/iterator.yml b/examples/iterator/iterator.yml index 1c6477104..ddd62c869 100644 --- a/examples/iterator/iterator.yml +++ b/examples/iterator/iterator.yml @@ -6,3 +6,7 @@ events: parameters: - name: value type: Any + - name: Value + parameters: + - name: value + type: Any From 3070c2e7fceec95fbc6b00d8b99c58ac3ad6d4ce Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 23 Mar 2022 22:05:40 +0300 Subject: [PATCH 07/14] examples: polish iterator Use read only context where appropriate, shorten notifications, fix comment. --- examples/iterator/iterator.go | 10 +++++----- examples/iterator/iterator.yml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/iterator/iterator.go b/examples/iterator/iterator.go index a30233fb5..fe53881b8 100644 --- a/examples/iterator/iterator.go +++ b/examples/iterator/iterator.go @@ -8,24 +8,24 @@ import ( // _deploy primes contract's storage with some data to be used later. func _deploy(_ interface{}, _ bool) { - ctx := storage.GetContext() + ctx := storage.GetContext() // RW context. storage.Put(ctx, "foo1", "1") storage.Put(ctx, "foo2", "2") storage.Put(ctx, "foo3", "3") } -// NotifyKeysAndValues sends notification with `foo` storage keys and values +// NotifyKeysAndValues sends notification with `foo` storage keys and values. func NotifyKeysAndValues() bool { - iter := storage.Find(storage.GetContext(), []byte("foo"), storage.None) + iter := storage.Find(storage.GetReadOnlyContext(), []byte("foo"), storage.None) for iterator.Next(iter) { - runtime.Notify("found storage key-value pair", iterator.Value(iter)) + runtime.Notify("Key-Value", iterator.Value(iter)) } return true } // NotifyValues sends notification with `foo` storage values. func NotifyValues() bool { - iter := storage.Find(storage.GetContext(), []byte("foo"), storage.ValuesOnly) + iter := storage.Find(storage.GetReadOnlyContext(), []byte("foo"), storage.ValuesOnly) for iterator.Next(iter) { runtime.Notify("Value", iterator.Value(iter)) } diff --git a/examples/iterator/iterator.yml b/examples/iterator/iterator.yml index ddd62c869..61170428c 100644 --- a/examples/iterator/iterator.yml +++ b/examples/iterator/iterator.yml @@ -2,7 +2,7 @@ name: "Iterator example" sourceurl: https://github.com/nspcc-dev/neo-go/ supportedstandards: [] events: - - name: found storage key-value pair + - name: Key-Value parameters: - name: value type: Any From 2c70f41e514a274e67444c0cda4ad598f489c9d7 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 23 Mar 2022 22:21:07 +0300 Subject: [PATCH 08/14] examples: move nft-nd-nns test into the same package Which is the way Go tests are normally stored. --- .../{tests/nonnative_name_service_test.go => nns_test.go} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename examples/nft-nd-nns/{tests/nonnative_name_service_test.go => nns_test.go} (99%) diff --git a/examples/nft-nd-nns/tests/nonnative_name_service_test.go b/examples/nft-nd-nns/nns_test.go similarity index 99% rename from examples/nft-nd-nns/tests/nonnative_name_service_test.go rename to examples/nft-nd-nns/nns_test.go index ecd602786..45219a0e8 100644 --- a/examples/nft-nd-nns/tests/nonnative_name_service_test.go +++ b/examples/nft-nd-nns/nns_test.go @@ -1,4 +1,4 @@ -package tests +package nns_test import ( "strings" @@ -17,7 +17,7 @@ import ( func newNSClient(t *testing.T) *neotest.ContractInvoker { bc, acc := chain.NewSingle(t) e := neotest.NewExecutor(t, bc, acc, acc) - c := neotest.CompileFile(t, e.CommitteeHash, "..", "../nns.yml") + c := neotest.CompileFile(t, e.CommitteeHash, ".", "nns.yml") e.DeployContract(t, c, nil) return e.CommitteeInvoker(c.Hash) From ab6fe44cabf2fbbf507a6b5d8b7d244516c11b28 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 23 Mar 2022 22:23:58 +0300 Subject: [PATCH 09/14] examples: add a note on tests for nft-nd-nns --- examples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/README.md b/examples/README.md index c42cd66ba..e548a7f20 100644 --- a/examples/README.md +++ b/examples/README.md @@ -27,7 +27,7 @@ See the table below for the detailed examples description. | [iterator](iterator) | This example describes a way to work with NEO iterators. Please, refer to the `iterator` [package documentation](../pkg/interop/iterator/iterator.go) for details. | | [nft-d](nft-d) | NEP-11 divisible NFT. See NEP-11 token standard [specification](https://github.com/neo-project/proposals/blob/master/nep-11.mediawiki) for details. | | [nft-nd](nft-nd) | NEP-11 non-divisible NFT. See NEP-11 token standard [specification](https://github.com/neo-project/proposals/blob/master/nep-11.mediawiki) for details. | -| [nft-nd-nns](nft-nd-nns) | Neo Name Service contract which is NEP-11 non-divisible NFT. The contract implements methods for Neo domain name system managing such as domains registration/transferring, records addition and names resolving. | +| [nft-nd-nns](nft-nd-nns) | Neo Name Service contract which is NEP-11 non-divisible NFT. The contract implements methods for Neo domain name system managing such as domains registration/transferring, records addition and names resolving. The package also contains tests implemented with [neotest](https://pkg.go.dev/github.com/nspcc-dev/neo-go/pkg/neotest). | | [oracle](oracle) | Oracle demo contract exposing two methods that you can use to process URLs. It uses oracle native contract, see [interop package documentation](../pkg/interop/native/oracle/oracle.go) also. | | [runtime](runtime) | This contract demonstrates how to use special `_initialize` and `_deploy` methods. See the [compiler documentation](../docs/compiler.md#vm-api-interop-layer ) for methods details. It also shows the pattern for checking owner witness inside the contract with the help of `runtime.CheckWitness` interop [function](../pkg/interop/runtime/runtime.go). | | [storage](storage) | The contract implements API for basic operations with a contract storage. It shows hos to use `storage` interop package. See the `storage` [package documentation](../pkg/interop/storage/storage.go). | From 208e38803c5507a7814b65e099dc81ca92390b09 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 23 Mar 2022 22:48:41 +0300 Subject: [PATCH 10/14] neotest: add some high-level documentation --- pkg/neotest/chain/doc.go | 7 +++++++ pkg/neotest/doc.go | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 pkg/neotest/chain/doc.go create mode 100644 pkg/neotest/doc.go diff --git a/pkg/neotest/chain/doc.go b/pkg/neotest/chain/doc.go new file mode 100644 index 000000000..1b0d6f286 --- /dev/null +++ b/pkg/neotest/chain/doc.go @@ -0,0 +1,7 @@ +/* +Package chain contains functions creating new test blockchain instances. +Different configurations can be used, but all chains created here use +well-known keys. Most of the time single-node chain is the best choice to use +unless you specifically need multiple validators and large committee. +*/ +package chain diff --git a/pkg/neotest/doc.go b/pkg/neotest/doc.go new file mode 100644 index 000000000..a86dc4615 --- /dev/null +++ b/pkg/neotest/doc.go @@ -0,0 +1,18 @@ +/* +Package neotest contains framework for automated contract testing. +It can be used to implement unit-tests for contracts in Go using regular Go +conventions. + +Usually it's used like this: + * an instance of blockchain is created using chain subpackage + * target contract is compiled using one of Compile* functions + * and Executor is created for blockchain + * it's used to deploy contract with DeployContract + * CommitteeInvoker and/or ValidatorInvoker are then created to perform test invocations + * if needed NewAccount is used to create appropriate number of accounts for the test + +Higher-order methods provided in Executor and ContractInvoker hide the details +of transaction creation for the most part, but there are lower-level methods as +well that can be used for specific tasks. +*/ +package neotest From f6094c8c4efc7a49cf2e97fdb86e5918bb35635d Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 23 Mar 2022 23:06:34 +0300 Subject: [PATCH 11/14] neotest: use public constants for default configuration And be more consistent about MaxTraceableBlocks use. --- pkg/neotest/chain/chain.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/pkg/neotest/chain/chain.go b/pkg/neotest/chain/chain.go index b6fbdb2e6..d3da291b1 100644 --- a/pkg/neotest/chain/chain.go +++ b/pkg/neotest/chain/chain.go @@ -17,6 +17,16 @@ import ( "go.uber.org/zap/zaptest" ) +const ( + // MaxTraceableBlocks is the default MaxTraceableBlocks setting used for test chains. + // We don't need a lot of traceable blocks for tests. + MaxTraceableBlocks = 1000 + + // SecondsPerBlock is the default SecondsPerBlock setting used for test chains. + // Usually blocks are created by tests bypassing this setting. + SecondsPerBlock = 1 +) + const singleValidatorWIF = "KxyjQ8eUa4FHt3Gvioyt1Wz29cTUrE4eTqX3yFSk1YFCsPL8uNsY" // committeeWIFs is a list of unencrypted WIFs sorted by public key. @@ -121,8 +131,8 @@ func NewSingleWithCustomConfig(t *testing.T, f func(*config.ProtocolConfiguratio func NewSingleWithCustomConfigAndStore(t *testing.T, f func(cfg *config.ProtocolConfiguration), st storage.Store, run bool) (*core.Blockchain, neotest.Signer) { protoCfg := config.ProtocolConfiguration{ Magic: netmode.UnitTestNet, - MaxTraceableBlocks: 1000, // We don't need a lot of traceable blocks for tests. - SecondsPerBlock: 1, + MaxTraceableBlocks: MaxTraceableBlocks, + SecondsPerBlock: SecondsPerBlock, StandbyCommittee: []string{hex.EncodeToString(committeeAcc.PrivateKey().PublicKey().Bytes())}, ValidatorsCount: 1, VerifyBlocks: true, @@ -153,7 +163,8 @@ func NewMulti(t *testing.T) (*core.Blockchain, neotest.Signer, neotest.Signer) { func NewMultiWithCustomConfig(t *testing.T, f func(*config.ProtocolConfiguration)) (*core.Blockchain, neotest.Signer, neotest.Signer) { protoCfg := config.ProtocolConfiguration{ Magic: netmode.UnitTestNet, - SecondsPerBlock: 1, + MaxTraceableBlocks: MaxTraceableBlocks, + SecondsPerBlock: SecondsPerBlock, StandbyCommittee: standByCommittee, ValidatorsCount: 4, VerifyBlocks: true, From 7fe462a65de1a619705301e83cec0d11d82710cc Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 23 Mar 2022 23:07:02 +0300 Subject: [PATCH 12/14] neotest/chain: improve comments --- pkg/neotest/chain/chain.go | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/pkg/neotest/chain/chain.go b/pkg/neotest/chain/chain.go index d3da291b1..5cbec396b 100644 --- a/pkg/neotest/chain/chain.go +++ b/pkg/neotest/chain/chain.go @@ -116,18 +116,27 @@ func init() { } // NewSingle creates new blockchain instance with a single validator and -// setups cleanup functions. +// setups cleanup functions. The configuration used is with netmode.UnitTestNet +// magic, and SecondsPerBlock/MaxTraceableBlocks options defined by constants in +// this package. MemoryStore is used as the backend storage, so all of the chain +// contents is always in RAM. The Signer returned is validator (and committee at +// the same time). func NewSingle(t *testing.T) (*core.Blockchain, neotest.Signer) { return NewSingleWithCustomConfig(t, nil) } -// NewSingleWithCustomConfig creates new blockchain instance with custom protocol -// configuration and a single validator. It also setups cleanup functions. +// NewSingleWithCustomConfig is similar to NewSingle, but allows to override the +// default configuration. func NewSingleWithCustomConfig(t *testing.T, f func(*config.ProtocolConfiguration)) (*core.Blockchain, neotest.Signer) { st := storage.NewMemoryStore() return NewSingleWithCustomConfigAndStore(t, f, st, true) } +// NewSingleWithCustomConfigAndStore is similar to NewSingleWithCustomConfig, but +// also allows to override backend Store being used. The last parameter controls if +// Run method is called on the Blockchain instance, if not then it's caller's +// responsibility to do that before using the chain and its caller's responsibility +// also to properly Close the chain when done. func NewSingleWithCustomConfigAndStore(t *testing.T, f func(cfg *config.ProtocolConfiguration), st storage.Store, run bool) (*core.Blockchain, neotest.Signer) { protoCfg := config.ProtocolConfiguration{ Magic: netmode.UnitTestNet, @@ -151,15 +160,15 @@ func NewSingleWithCustomConfigAndStore(t *testing.T, f func(cfg *config.Protocol return bc, neotest.NewMultiSigner(committeeAcc) } -// NewMulti creates new blockchain instance with 4 validators and 6 committee members. -// Second return value is for validator signer, third -- for committee. +// NewMulti creates new blockchain instance with four validators and six +// committee members, otherwise not differring much from NewSingle. The +// second value returned contains validators Signer, the third -- committee one. func NewMulti(t *testing.T) (*core.Blockchain, neotest.Signer, neotest.Signer) { return NewMultiWithCustomConfig(t, nil) } -// NewMultiWithCustomConfig creates new blockchain instance with custom protocol -// configuration, 4 validators and 6 committee members. Second return value is -// for validator signer, third -- for committee. +// NewMultiWithCustomConfig is similar to NewMulti except it allows to override the +// default configuration. func NewMultiWithCustomConfig(t *testing.T, f func(*config.ProtocolConfiguration)) (*core.Blockchain, neotest.Signer, neotest.Signer) { protoCfg := config.ProtocolConfiguration{ Magic: netmode.UnitTestNet, From be7527409c4cf37a986b6ccd016d1914207088c9 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 24 Mar 2022 16:34:18 +0300 Subject: [PATCH 13/14] compiler: fix panic in notification check Options is a pointer, so it can be nil: --- FAIL: TestCompiler (0.23s) --- FAIL: TestCompiler/TestCompile (0.21s) panic: runtime error: invalid memory address or nil pointer dereference [recovered] panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x40 pc=0x960374] goroutine 8861 [running]: testing.tRunner.func1.2({0xa604c0, 0x112c230}) /usr/lib64/go/1.18/src/testing/testing.go:1389 +0x24e testing.tRunner.func1() /usr/lib64/go/1.18/src/testing/testing.go:1392 +0x39f panic({0xa604c0, 0x112c230}) /usr/lib64/go/1.18/src/runtime/panic.go:838 +0x207 github.com/nspcc-dev/neo-go/pkg/compiler.(*codegen).processNotify(0xc0000eba40, 0xc000233ae0?, {0xc00044ae90, 0x1, 0x1}) /home/rik/dev/neo-go/pkg/compiler/inline.go:134 +0xd4 github.com/nspcc-dev/neo-go/pkg/compiler.(*codegen).processStdlibCall(0xc0000eba40?, 0xc000233ae0, {0xc00044ae90, 0x1, 0x1}) /home/rik/dev/neo-go/pkg/compiler/inline.go:124 +0xda github.com/nspcc-dev/neo-go/pkg/compiler.(*codegen).inlineCall(0xc0000eba40, 0xc000233ae0, 0xc0001fe5c0) /home/rik/dev/neo-go/pkg/compiler/inline.go:35 +0x1fa github.com/nspcc-dev/neo-go/pkg/compiler.(*codegen).Visit(0xc0000eba40, {0xd36bf8?, 0xc0001fe5c0?}) /home/rik/dev/neo-go/pkg/compiler/codegen.go:932 +0x152c go/ast.Walk({0xd348e0?, 0xc0000eba40?}, {0xd36bf8?, 0xc0001fe5c0?}) /usr/lib64/go/1.18/src/go/ast/walk.go:52 +0x62 go/ast.Walk({0xd348e0?, 0xc0000eba40?}, {0xd36db0?, 0xc00044aeb0?}) /usr/lib64/go/1.18/src/go/ast/walk.go:207 +0x1154 github.com/nspcc-dev/neo-go/pkg/compiler.(*codegen).Visit(0xc0000eba40, {0xd36ba8?, 0xc0001cce70?}) /home/rik/dev/neo-go/pkg/compiler/codegen.go:1155 +0x54cd go/ast.Walk({0xd348e0?, 0xc0000eba40?}, {0xd36ba8?, 0xc0001cce70?}) /usr/lib64/go/1.18/src/go/ast/walk.go:52 +0x62 github.com/nspcc-dev/neo-go/pkg/compiler.(*codegen).Visit(0xc0000eba40, {0xd36f68?, 0xc0001fe600?}) /home/rik/dev/neo-go/pkg/compiler/codegen.go:733 +0x2e30 go/ast.Walk({0xd348e0?, 0xc0000eba40?}, {0xd36f68?, 0xc0001fe600?}) /usr/lib64/go/1.18/src/go/ast/walk.go:52 +0x62 github.com/nspcc-dev/neo-go/pkg/compiler.(*codegen).Visit(0xc0000eba40, {0xd36ba8?, 0xc0001ccea0?}) /home/rik/dev/neo-go/pkg/compiler/codegen.go:1155 +0x54cd go/ast.Walk({0xd348e0?, 0xc0000eba40?}, {0xd36ba8?, 0xc0001ccea0?}) /usr/lib64/go/1.18/src/go/ast/walk.go:52 +0x62 github.com/nspcc-dev/neo-go/pkg/compiler.(*codegen).convertFuncDecl(0xc0000eba40, {0xd36e28, 0xc000256700}, 0xc0001cced0, 0xc00041b68c?) /home/rik/dev/neo-go/pkg/compiler/codegen.go:502 +0x97d github.com/nspcc-dev/neo-go/pkg/compiler.(*codegen).compile.func2(0xc000256700, 0xc0001c64b0) /home/rik/dev/neo-go/pkg/compiler/codegen.go:2129 +0x1f9 github.com/nspcc-dev/neo-go/pkg/compiler.(*codegen).ForEachFile.func1(0xc000254a00) /home/rik/dev/neo-go/pkg/compiler/compiler.go:102 +0x96 github.com/nspcc-dev/neo-go/pkg/compiler.(*codegen).ForEachPackage(0xc0000eba40, 0xc0002f3c68) /home/rik/dev/neo-go/pkg/compiler/compiler.go:93 +0xdb github.com/nspcc-dev/neo-go/pkg/compiler.(*codegen).ForEachFile(0xa1f700?, 0xc000448f90?) /home/rik/dev/neo-go/pkg/compiler/compiler.go:99 +0x4d github.com/nspcc-dev/neo-go/pkg/compiler.(*codegen).compile(0xc0000eba40, 0xc00022c390, 0x1?) /home/rik/dev/neo-go/pkg/compiler/codegen.go:2116 +0x3d6 github.com/nspcc-dev/neo-go/pkg/compiler.codeGen(0xc00022c390) /home/rik/dev/neo-go/pkg/compiler/codegen.go:2167 +0x373 github.com/nspcc-dev/neo-go/pkg/compiler.CompileWithOptions({0xc000c525a0?, 0x16?}, {0x0?, 0x0?}, 0x0) /home/rik/dev/neo-go/pkg/compiler/compiler.go:218 +0x7d github.com/nspcc-dev/neo-go/pkg/compiler.Compile({0xc000c525a0?, 0xc000304340?}, {0x0?, 0x0?}) /home/rik/dev/neo-go/pkg/compiler/compiler.go:203 +0x34 github.com/nspcc-dev/neo-go/pkg/compiler_test.compileFile(...) /home/rik/dev/neo-go/pkg/compiler/compiler_test.go:89 github.com/nspcc-dev/neo-go/pkg/compiler_test.TestCompiler.func2(0x6a2a89?) /home/rik/dev/neo-go/pkg/compiler/compiler_test.go:61 +0x133 testing.tRunner(0xc000304340, 0xc7f5b0) /usr/lib64/go/1.18/src/testing/testing.go:1439 +0x102 created by testing.(*T).Run /usr/lib64/go/1.18/src/testing/testing.go:1486 +0x35f --- pkg/compiler/inline.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/compiler/inline.go b/pkg/compiler/inline.go index a80f85487..74e33c190 100644 --- a/pkg/compiler/inline.go +++ b/pkg/compiler/inline.go @@ -131,7 +131,7 @@ func (c *codegen) processStdlibCall(f *funcScope, args []ast.Expr) { func (c *codegen) processNotify(f *funcScope, args []ast.Expr) { if c.scope != nil && c.isVerifyFunc(c.scope.decl) && - c.scope.pkg == c.mainPkg.Types && !c.buildInfo.options.NoEventsCheck { + c.scope.pkg == c.mainPkg.Types && (c.buildInfo.options == nil || !c.buildInfo.options.NoEventsCheck) { c.prog.Err = fmt.Errorf("runtime.%s is not allowed in `Verify`", f.name) return } From fe27ae9b96bfc81fc85307c52345abf59b3667e4 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 24 Mar 2022 16:44:15 +0300 Subject: [PATCH 14/14] examples: completely rework runtime example Make it more useful, triggers are largely obsolete (2.x thing). --- examples/runtime/runtime.go | 72 ++++++++++++++++++++++++------------ examples/runtime/runtime.yml | 3 ++ 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/examples/runtime/runtime.go b/examples/runtime/runtime.go index 5acb63f0c..3536bc1ea 100644 --- a/examples/runtime/runtime.go +++ b/examples/runtime/runtime.go @@ -1,55 +1,81 @@ package runtimecontract import ( + "github.com/nspcc-dev/neo-go/pkg/interop/native/management" "github.com/nspcc-dev/neo-go/pkg/interop/runtime" "github.com/nspcc-dev/neo-go/pkg/interop/util" ) var ( // Check if the invoker of the contract is the specified owner - owner = util.FromAddress("NbrUYaZgyhSkNoRo9ugRyEMdUZxrhkNaWB") - trigger byte + owner = util.FromAddress("NbrUYaZgyhSkNoRo9ugRyEMdUZxrhkNaWB") ) -// init initializes trigger before any other contract method is called +// init is transformed into _initialize method that is called whenever contract +// is being loaded (so you'll see this log entry with every invocation). func init() { - trigger = runtime.GetTrigger() + // No events and logging allowed in verification context. + if runtime.GetTrigger() != runtime.Verification { + runtime.Log("init called") + } } +// _deploy is called after contract deployment or update, it'll be called +// in deployment transaction and if call update method of this contract. func _deploy(_ interface{}, isUpdate bool) { if isUpdate { - Log("_deploy method called before contract update") + Log("_deploy method called after contract update") return } - Log("_deploy method called before contract creation") + Log("_deploy method called after contract creation") } -// CheckWitness checks owner's witness +// CheckWitness checks owner's witness. It returns true if invoked by the owner +// and false otherwise. func CheckWitness() bool { - // Log owner upon Verification trigger - if trigger != runtime.Verification { - return false - } if runtime.CheckWitness(owner) { runtime.Log("Verified Owner") + return true } - return true + return false } -// Log logs given message -func Log(message string) bool { - if trigger != runtime.Application { - return false - } +// Log logs given message. +func Log(message string) { runtime.Log(message) - return true } -// Notify notifies about given message -func Notify(event interface{}) bool { - if trigger != runtime.Application { +// Notify emits an event with the specified data. +func Notify(event interface{}) { + runtime.Notify("Event", event) +} + +// Verify method is used when contract is being used as a signer of transaction, +// it can have parameters (that then need to be present in invocation script) +// and it returns simple pass/fail result. This implementation just checks for +// owner's signature presence. +func Verify() bool { + // Technically this restriction is not needed, but you can see the difference + // between invokefunction and invokecontractverify RPC methods with it. + if runtime.GetTrigger() != runtime.Verification { return false } - runtime.Notify("Event", event) - return true + return CheckWitness() +} + +// Destroy destroys the contract, only owner can do that. +func Destroy() { + if !Verify() { + panic("only owner can destroy") + } + management.Destroy() +} + +// Update updates the contract, only owner can do that. _deploy will be called +// after update. +func Update(nef, manifest []byte) { + if !Verify() { + panic("only owner can update") + } + management.Update(nef, manifest) } diff --git a/examples/runtime/runtime.yml b/examples/runtime/runtime.yml index b152cbc0a..0ed32855c 100644 --- a/examples/runtime/runtime.yml +++ b/examples/runtime/runtime.yml @@ -6,3 +6,6 @@ events: parameters: - name: event type: Any +permissions: + - hash: fffdc93764dbaddd97c48f252a53ea4643faa3fd + methods: ["update", "destroy"]