forked from TrueCloudLab/neoneo-go
82c6ce218b
Part of #2767.
480 lines
31 KiB
Markdown
480 lines
31 KiB
Markdown
# NeoGo smart contract compiler
|
|
|
|
The neo-go compiler compiles Go programs to a bytecode that the NEO virtual machine can understand.
|
|
|
|
## Language compatibility
|
|
|
|
The compiler is mostly compatible with regular Go language specification. However,
|
|
there are some important deviations that you need to be aware of that make it
|
|
a dialect of Go rather than a complete port of the language:
|
|
* `new()` is not supported, most of the time you can substitute structs with composite literals
|
|
* `make()` is supported for maps and slices with elements of basic types
|
|
* `copy()` is supported only for byte slices because of the underlying `MEMCPY` opcode
|
|
* pointers are supported only for struct literals, one can't take an address
|
|
of an arbitrary variable
|
|
* there is no real distinction between different integer types, all of them
|
|
work as big.Int in Go with a limit of 256 bit in width; so you can use
|
|
`int` for just about anything. This is the way integers work in Neo VM and
|
|
adding proper Go types emulation is considered to be too costly.
|
|
* goroutines, channels and garbage collection are not supported and will
|
|
never be because emulating that aspects of Go runtime on top of Neo VM is
|
|
close to impossible
|
|
* `defer` and `recover` are supported except for the 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.
|
|
* lambdas are supported, but closures are not.
|
|
* maps are supported, but valid map keys are booleans, integers and strings with length <= 64
|
|
* converting value to interface type doesn't change the underlying type,
|
|
original value will always be used, therefore it never panics and always "succeeds";
|
|
it's up to the programmer whether it's a correct use of a value
|
|
* type assertion with two return values is not supported; single return value (of the desired type)
|
|
is supported; type assertion panics if value can't be asserted to the desired type, therefore
|
|
it's up to the programmer whether assert can be performed successfully.
|
|
|
|
## VM API (interop layer)
|
|
Compiler translates interop function calls into NEO VM syscalls or (for custom
|
|
functions) into NEO VM instructions. [Refer to
|
|
pkg.go.dev](https://pkg.go.dev/github.com/nspcc-dev/neo-go/pkg/interop)
|
|
for full API documentation. In general it provides the same level of
|
|
functionality as Neo .net Framework library.
|
|
|
|
Compiler provides some helpful builtins in `util`, `convert` and `math` packages.
|
|
Refer to them for detailed documentation.
|
|
|
|
`_deploy()` function has a special meaning and is executed when contract is deployed.
|
|
It should return no value and accept two arguments: the first one is `data` containing
|
|
all values `deploy` is aware of and able to make use of; the second one is a bool
|
|
argument which will be true on contract update.
|
|
`_deploy()` functions are called for every imported package in the same order as `init()`.
|
|
|
|
## Quick start
|
|
|
|
### Go setup
|
|
|
|
The compiler uses Go parser internally and depends on regular Go compiler
|
|
presence, so make sure you have it installed and set up. On some distributions
|
|
this requires you to set proper `GOROOT` environment variable, like
|
|
```
|
|
export GOROOT=/usr/lib64/go/1.15
|
|
```
|
|
|
|
The best way to create a new contract is to use `contract init` command. This will
|
|
create an example source file, a config file and `go.mod` with `github.com/nspcc-dev/neo-go/pkg/interop` dependency.
|
|
```
|
|
$ ./bin/neo-go contract init --name MyAwesomeContract
|
|
$ cd MyAwesomeContract
|
|
```
|
|
|
|
You'll also need to download dependency modules for your contract like this (in the
|
|
directory containing contract package):
|
|
```
|
|
$ go mod tidy
|
|
```
|
|
|
|
### Compiling
|
|
|
|
```
|
|
./bin/neo-go contract compile -i contract.go
|
|
```
|
|
|
|
By default, the filename will be the name of your .go file with the .nef
|
|
extension, the file will be located in the same directory with your Go contract.
|
|
If you want another location for your compiled contract:
|
|
|
|
```
|
|
./bin/neo-go contract compile -i contract.go --out /Users/foo/bar/contract.nef
|
|
```
|
|
|
|
If your contract is split across multiple files, you must provide a path
|
|
to the directory where package files are contained instead of a single Go file
|
|
(`out.nef` will be used as the default output file in this case):
|
|
```
|
|
./bin/neo-go contract compile -i ./path/to/contract
|
|
```
|
|
|
|
### Debugging
|
|
You can dump the opcodes generated by the compiler with the following command:
|
|
|
|
```
|
|
./bin/neo-go contract inspect -i contract.go -c
|
|
```
|
|
|
|
This will result in something like this:
|
|
|
|
```
|
|
INDEX OPCODE PARAMETER
|
|
0 INITSLOT 4 local, 2 arg <<
|
|
3 LDARG1
|
|
4 NOT
|
|
5 JMPIFNOT_L 151 (146/92000000)
|
|
10 SYSCALL System.Storage.GetContext (9bf667ce)
|
|
15 NOP
|
|
16 STLOC0
|
|
17 PUSHDATA1 53746f72616765206b6579206e6f7420796574207365742e2053657474696e6720746f2030 ("Storage key not yet set. Setting to 0")
|
|
56 CONVERT Buffer (30)
|
|
58 PUSH1
|
|
59 PACK
|
|
60 STLOC1
|
|
61 PUSHDATA1 696e666f ("info")
|
|
67 LDLOC1
|
|
68 SWAP
|
|
69 SYSCALL System.Runtime.Notify (95016f61)
|
|
74 NOP
|
|
75 PUSH0
|
|
76 STLOC2
|
|
77 LDLOC0
|
|
78 PUSHDATA1 746573742d73746f726167652d6b6579 ("test-storage-key")
|
|
96 LDLOC2
|
|
97 REVERSE3
|
|
98 SYSCALL System.Storage.Put (e63f1884)
|
|
103 NOP
|
|
104 PUSHDATA1 53746f72616765206b657920697320696e697469616c69736564 ("Storage key is initialised")
|
|
132 CONVERT Buffer (30)
|
|
134 PUSH1
|
|
135 PACK
|
|
136 STLOC3
|
|
137 PUSHDATA1 696e666f ("info")
|
|
143 LDLOC3
|
|
144 SWAP
|
|
145 SYSCALL System.Runtime.Notify (95016f61)
|
|
150 NOP
|
|
151 RET
|
|
152 INITSLOT 5 local, 0 arg
|
|
155 SYSCALL System.Storage.GetContext (9bf667ce)
|
|
160 NOP
|
|
161 STLOC0
|
|
162 LDLOC0
|
|
163 PUSHDATA1 746573742d73746f726167652d6b6579 ("test-storage-key")
|
|
181 SWAP
|
|
182 SYSCALL System.Storage.Get (925de831)
|
|
187 NOP
|
|
188 STLOC1
|
|
189 PUSHDATA1 56616c756520726561642066726f6d2073746f72616765 ("Value read from storage")
|
|
214 CONVERT Buffer (30)
|
|
216 PUSH1
|
|
217 PACK
|
|
218 STLOC2
|
|
219 PUSHDATA1 696e666f ("info")
|
|
225 LDLOC2
|
|
226 SWAP
|
|
227 SYSCALL System.Runtime.Notify (95016f61)
|
|
232 NOP
|
|
233 PUSHDATA1 53746f72616765206b657920616c7265616479207365742e20496e6372656d656e74696e672062792031 ("Storage key already set. Incrementing by 1")
|
|
277 CONVERT Buffer (30)
|
|
279 PUSH1
|
|
280 PACK
|
|
281 STLOC3
|
|
282 PUSHDATA1 696e666f ("info")
|
|
288 LDLOC3
|
|
289 SWAP
|
|
290 SYSCALL System.Runtime.Notify (95016f61)
|
|
295 NOP
|
|
296 LDLOC1
|
|
297 CONVERT Integer (21)
|
|
299 PUSH1
|
|
300 ADD
|
|
301 STLOC1
|
|
302 LDLOC0
|
|
303 PUSHDATA1 746573742d73746f726167652d6b6579 ("test-storage-key")
|
|
321 LDLOC1
|
|
322 REVERSE3
|
|
323 SYSCALL System.Storage.Put (e63f1884)
|
|
328 NOP
|
|
329 PUSHDATA1 4e65772076616c7565207772697474656e20696e746f2073746f72616765 ("New value written into storage")
|
|
361 CONVERT Buffer (30)
|
|
363 PUSH1
|
|
364 PACK
|
|
365 STLOC4
|
|
366 PUSHDATA1 696e666f ("info")
|
|
372 LDLOC4
|
|
373 SWAP
|
|
374 SYSCALL System.Runtime.Notify (95016f61)
|
|
379 NOP
|
|
380 LDLOC1
|
|
381 RET
|
|
```
|
|
|
|
#### Neo Smart Contract Debugger support
|
|
|
|
It's possible to debug contracts written in Go using standard [Neo Smart
|
|
Contract Debugger](https://github.com/neo-project/neo-debugger/) which is a
|
|
part of [Neo Blockchain
|
|
Toolkit](https://github.com/neo-project/neo-blockchain-toolkit/). To do that
|
|
you need to generate debug information using `--debug` option, like this:
|
|
|
|
```
|
|
$ ./bin/neo-go contract compile -i contract.go -c contract.yml -m contract.manifest.json -o contract.nef --debug contract.debug.json
|
|
```
|
|
|
|
This file can then be used by debugger and set up to work just like for any
|
|
other supported language.
|
|
|
|
### Deploying
|
|
|
|
Deploying a contract to blockchain with neo-go requires both NEF and JSON
|
|
manifest generated by the compiler from a configuration file provided in YAML
|
|
format. To create contract manifest, pass a YAML file with `-c` parameter and
|
|
specify the manifest output file with `-m`:
|
|
```
|
|
./bin/neo-go contract compile -i contract.go -c config.yml -m contract.manifest.json
|
|
```
|
|
|
|
Example of such YAML file contents:
|
|
```
|
|
name: Contract
|
|
safemethods: []
|
|
supportedstandards: []
|
|
events:
|
|
- name: info
|
|
parameters:
|
|
- name: message
|
|
type: String
|
|
```
|
|
|
|
Then, the manifest can be passed to the `deploy` command via `-m` option:
|
|
|
|
```
|
|
$ ./bin/neo-go contract deploy -i contract.nef -m contract.manifest.json -r http://localhost:20331 -w wallet.json
|
|
```
|
|
|
|
Deployment works via an RPC server, an address of which is passed via `-r`
|
|
option, and should be signed using a wallet from `-w` option. More details can
|
|
be found in `deploy` command help.
|
|
|
|
#### Config file
|
|
Configuration file contains following options:
|
|
|
|
| Parameter | Description | Example |
|
|
| --- | --- | --- |
|
|
| `name` | Contract name in the manifest. | `"My awesome contract"`
|
|
| `safemethods` | List of methods which don't change contract state, don't emit notifications and are available for anyone to call. | `["balanceOf", "decimals"]`
|
|
| `supportedstandards` | List of standards this contract implements. For example, `NEP-11` or `NEP-17` token standard. This will enable additional checks in compiler. The check can be disabled with `--no-standards` flag. | `["NEP-17"]`
|
|
| `events` | Notifications emitted by this contract. | See [Events](#Events). |
|
|
| `permissions` | Foreign calls allowed for this contract. | See [Permissions](#Permissions). |
|
|
| `overloads` | Custom method names for this contract. | See [Overloads](#Overloads). |
|
|
|
|
##### Events
|
|
Each event must have a name and 0 or more parameters. Parameters are specified using their name and type.
|
|
Both event and parameter names must be strings.
|
|
Parameter type can be one of the following:
|
|
|
|
Type in code | Type in config file
|
|
--- | ---
|
|
`bool` | `Boolean`
|
|
`int`, `int64` etc.| `Integer`
|
|
`[]byte` | `ByteArray`
|
|
`string` | `String`
|
|
Any non-byte slice `[]T`| `Array`
|
|
`map[K]V` | `Map`
|
|
`interop.Hash160` | `Hash160`
|
|
`interop.Hash256` | `Hash256`
|
|
`interop.Interface` | `InteropInterface`
|
|
`interop.PublicKey` | `PublicKey`
|
|
`interop.Signature` | `Signature`
|
|
anything else | `Any`
|
|
|
|
`interop.*` types are defined as aliases in `github.com/nspcc-dev/neo-go/pkg/interop` module
|
|
with the sole purpose of correct manifest generation.
|
|
|
|
As an example, consider `Transfer` event from `NEP-17` standard:
|
|
```
|
|
- name: Transfer
|
|
parameters:
|
|
- name: from
|
|
type: Hash160
|
|
- name: to
|
|
type: Hash160
|
|
- name: amount
|
|
type: Integer
|
|
```
|
|
|
|
By default, compiler performs some sanity checks. Most of the time
|
|
it will report missing events and/or parameter type mismatch.
|
|
It isn't prohibited to use a variable as an event name in code, but it will prevent
|
|
the compiler from analyzing the event. It is better to use either constant or string literal.
|
|
It isn't prohibited to use ellipsis expression as an event arguments, but it will also
|
|
prevent the compiler from analyzing the event. It is better to provide arguments directly
|
|
without `...`. The type conversion code will be emitted for checked events, it will cast
|
|
argument types to ones specified in the contract manifest. These checks and conversion can
|
|
be disabled with `--no-events` flag.
|
|
|
|
##### Permissions
|
|
Each permission specifies contracts and methods allowed for this permission.
|
|
If a contract is not specified in a rule, specified set of methods can be called on any contract.
|
|
By default, no calls are allowed. The simplest permission is to allow everything:
|
|
```
|
|
- methods: '*'
|
|
```
|
|
|
|
Another common case is to allow calling `onNEP17Payment`, which is necessary
|
|
for most of the NEP-17 token implementations:
|
|
```
|
|
- methods: ["onNEP17Payment"]
|
|
```
|
|
|
|
In addition to `methods`, permission can have one of these fields:
|
|
1. `hash` contains hash and restricts a set of contracts to a single contract.
|
|
2. `group` contains public key and restricts a set of contracts to those that
|
|
have the corresponding group in their manifest.
|
|
|
|
Consider an example:
|
|
```
|
|
- methods: ["onNEP17Payment"]
|
|
- hash: fffdc93764dbaddd97c48f252a53ea4643faa3fd
|
|
methods: ["start", "stop"]
|
|
- group: 03184b018d6b2bc093e535519732b3fd3f7551c8cffaf4621dd5a0b89482ca66c9
|
|
methods: ["update"]
|
|
```
|
|
|
|
This set of permissions allows calling:
|
|
- `onNEP17Payment` method of any contract
|
|
- `start` and `stop` methods of contract with hash `fffdc93764dbaddd97c48f252a53ea4643faa3fd`
|
|
- `update` method of contract in group with public key `03184b018d6b2bc093e535519732b3fd3f7551c8cffaf4621dd5a0b89482ca66c9`
|
|
|
|
Also note that a native contract must be included here too. For example, if your contract
|
|
transfers NEO/GAS or gets some info from the `Ledger` contract, all of these
|
|
calls must be allowed in permissions.
|
|
|
|
The compiler does its best to ensure that correct permissions are specified in the config.
|
|
Incorrect permissions will result in runtime invocation failures.
|
|
Using either constant or literal for contract hash and method will allow the compiler
|
|
to perform more extensive analysis.
|
|
This check can be disabled with `--no-permissions` flag.
|
|
|
|
##### Overloads
|
|
NeoVM allows a contract to have multiple methods with the same name
|
|
but different parameters number. Go lacks this feature, but this can be circumvented
|
|
with `overloads` section. Essentially, it is a mapping from default contract method names
|
|
to the new ones.
|
|
```
|
|
- overloads:
|
|
oldName1: newName
|
|
oldName2: newName
|
|
```
|
|
Since the use-case for this is to provide multiple implementations with the same ABI name,
|
|
`newName` is required to be already present in the compiled contract.
|
|
|
|
As an example, consider [`NEP-11` standard](https://github.com/neo-project/proposals/blob/master/nep-11.mediawiki#transfer).
|
|
It requires a divisible NFT contract to have 2 `transfer` methods. To achieve this, we might implement
|
|
`Transfer` and `TransferDivisible` and specify the emitted name in the config:
|
|
```
|
|
- overloads:
|
|
transferDivisible:transfer
|
|
```
|
|
|
|
|
|
#### Manifest file
|
|
Any contract can be included in a group identified by a public key which is used in [permissions](#Permissions).
|
|
This is achieved with `manifest add-group` command.
|
|
```
|
|
./bin/neo-go contract manifest add-group -n contract.nef -m contract.manifest.json --sender <sender> --wallet /path/to/wallet.json --account <account>
|
|
```
|
|
It accepts contract `.nef` and manifest files emitted by `compile` command as well as
|
|
sender and signer accounts. `--sender` is the account that will send deploy transaction later (not necessarily in wallet).
|
|
`--account` is the wallet account which signs contract hash using group private key.
|
|
|
|
#### Neo Express support
|
|
|
|
It's possible to deploy contracts written in Go using [Neo
|
|
Express](https://github.com/neo-project/neo-express), which is a part of [Neo
|
|
Blockchain
|
|
Toolkit](https://github.com/neo-project/neo-blockchain-toolkit/). To do that,
|
|
you need to generate a different metadata file using YAML written for
|
|
deployment with neo-go. It's done in the same step with compilation via
|
|
`--config` input parameter and `--abi` output parameter, combined with debug
|
|
support the command line will look like this:
|
|
|
|
```
|
|
$ ./bin/neo-go contract compile -i contract.go --config contract.yml -o contract.nef --debug contract.debug.json --abi contract.abi.json
|
|
```
|
|
|
|
This file can then be used by toolkit to deploy contract the same way
|
|
contracts in other languages are deployed.
|
|
|
|
|
|
### Invoking
|
|
You can import your contract into a standalone VM and run it there (see [VM
|
|
documentation](vm.md) for more info), but that only works for simple contracts
|
|
that don't use blockchain a lot. For more real contracts you need to deploy
|
|
them first and then do test invocations and regular invocations with `contract
|
|
testinvokefunction` and `contract invokefunction` commands (or their variants,
|
|
see `contract` command help for more details. They all work via RPC, so it's a
|
|
mandatory parameter.
|
|
|
|
Example call (contract `f84d6a337fbc3d3a201d41da99e86b479e7a2554` with method
|
|
`balanceOf` and method's parameter `AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y` using
|
|
given RPC server and wallet and paying 0.00001 extra GAS for this transaction):
|
|
|
|
```
|
|
$ ./bin/neo-go contract invokefunction -r http://localhost:20331 -w my_wallet.json -g 0.00001 f84d6a337fbc3d3a201d41da99e86b479e7a2554 balanceOf AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y
|
|
```
|
|
|
|
### Generating contract bindings
|
|
To be able to use deployed contract from another contract one needs to have
|
|
its interface definition (exported methods and hash). While it is possible to
|
|
use generic contract.Call interop interface, it's not very convenient and
|
|
efficient. NeoGo can autogenerate contract bindings in Go language for any
|
|
deployed contract based on its manifest, it creates a Go source file with all
|
|
of the contract's methods that then can be imported and used as a regular Go
|
|
package.
|
|
|
|
```
|
|
$ ./bin/neo-go contract generate-wrapper --manifest manifest.json --out wrapper.go --hash 0x1b4357bff5a01bdf2a6581247cf9ed1e24629176
|
|
```
|
|
|
|
Notice that some structured types can be omitted this way (when a function
|
|
returns some structure it's just an "Array" type in the manifest with no
|
|
internal details), but if the contract you're using is written in Go
|
|
originally you can create a specific configuration file during compilation
|
|
that will add this data for wrapper generator to use:
|
|
|
|
```
|
|
$ ./bin/neo-go contract compile -i contract.go --config contract.yml -o contract.nef --manifest manifest.json --bindings contract.bindings.yml
|
|
$ ./bin/neo-go contract generate-wrapper --manifest manifest.json --config contract.bindings.yml --out wrapper.go --hash 0x1b4357bff5a01bdf2a6581247cf9ed1e24629176
|
|
```
|
|
|
|
### Generating RPC contract bindings
|
|
To simplify interacting with the contract via RPC you can generate
|
|
contract-specific RPC bindings with the "generate-rpcwrapper" command. It
|
|
generates ContractReader structure for safe methods that accept appropriate
|
|
data for input and return things returned by the contract. State-changing
|
|
methods are contained in Contract structure with each contract method
|
|
represented by three wrapper methods that create/send transaction with a
|
|
script performing appropriate action. This script invokes contract method and
|
|
does not do anything else unless the method's returned value is of a boolean
|
|
type, in this case an ASSERT is added to script making it fail when the method
|
|
returns false.
|
|
|
|
```
|
|
$ ./bin/neo-go contract generate-rpcwrapper --manifest manifest.json --out rpcwrapper.go --hash 0x1b4357bff5a01bdf2a6581247cf9ed1e24629176
|
|
```
|
|
|
|
If your contract is NEP-11 or NEP-17 that's autodetected and an appropriate
|
|
package is included as well. Notice that the type data available in the
|
|
manifest is limited, so in some cases the interface generated may use generic
|
|
stackitem types. Any InteropInterface returned from a method is treated as
|
|
iterator and an appropriate unwrapper is used with UUID and iterator structure
|
|
result. This pair can then be used in Invoker `TraverseIterator` method to
|
|
retrieve actual resulting items.
|
|
|
|
Go contracts can also make use of additional type data from bindings
|
|
configuration file generated during compilation. At the moment it allows to
|
|
generate proper wrappers for simple array types, but doesn't cover structures:
|
|
|
|
```
|
|
$ ./bin/neo-go contract compile -i contract.go --config contract.yml -o contract.nef --manifest manifest.json --bindings contract.bindings.yml
|
|
$ ./bin/neo-go contract generate-rpcwrapper --manifest manifest.json --config contract.bindings.yml --out rpcwrapper.go --hash 0x1b4357bff5a01bdf2a6581247cf9ed1e24629176
|
|
```
|
|
|
|
## Smart contract examples
|
|
|
|
Some examples are provided in the [examples directory](../examples). For more
|
|
sophisticated real-world contracts written in Go check out [NeoFS
|
|
contracts](https://github.com/nspcc-dev/neofs-contract/).
|
|
|
|
## How to report compiler bugs
|
|
1. Make a proper testcase (example testcases can be found in the tests folder)
|
|
2. Create an issue on Github
|
|
3. Make a PR with a reference to the created issue, containing the testcase that proves the bug
|
|
4. Either you fix the bug yourself or wait for patch that solves the problem
|