mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-11-30 09:33:36 +00:00
7758378d28
Signed-off-by: Evgeniy Stratonikov <evgeniy@nspcc.ru>
400 lines
26 KiB
Markdown
400 lines
26 KiB
Markdown
# NeoGo smart contract compiler
|
|
|
|
The neo-go compiler compiles Go programs to bytecode that the NEO virtual machine can understand.
|
|
|
|
## Language compatibility
|
|
|
|
The compiler is mostly compatible with regular Go language specification, but
|
|
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 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 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
|
|
|
|
## 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
|
|
```
|
|
|
|
### 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 where your Go contract
|
|
is. 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 configuration file provided in YAML
|
|
format. To create contract manifest pass YAML file with `-c` parameter and
|
|
specify manifest output file with `-m`:
|
|
```
|
|
./bin/neo-go contract compile -i contract.go -c config.yml -m contract.manifest.json
|
|
```
|
|
|
|
Example 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.
|
|
Using variable as an event name in code isn't prohibited but will prevent
|
|
compiler from analyzing an event. It is better to use either constant or string literal.
|
|
The check can be disabled with `--no-events` flag.
|
|
|
|
##### Permissions
|
|
Each permission specifies contracts and methods allowed for this permission.
|
|
If contract is not specified in a rule, specified set of methods can be called on any contract.
|
|
By default, no calls are allowed. 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 set of contracts to a single contract.
|
|
2. `group` contains public key and restricts set of contracts to those who
|
|
have 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 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.
|
|
|
|
Compiler does its best to ensure correct permissions are specified in config.
|
|
Incorrect permissions will result in runtime invocation failures.
|
|
Using either constant or literal for contract hash and method will allow 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
|
|
```
|
|
Because 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 divisible NFT contract to have 2 `transfer` methods. To achieve this we might implement
|
|
`Tranfer` and `TransferDivisible` and specify emitted name in 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 who 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 languagues are deployed.
|
|
|
|
|
|
### Invoking
|
|
You can import your contract into the 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
|
|
```
|
|
|
|
## 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
|