In the previous Road to Neo3 article, we gave a general overview of the importance of exception handling in Neo smart contracts and studied some of the preliminary development discussion. Before we continue more in depth on the implementation, we will follow the lifecycle of a Neo smart contract in order to better understand how the new mechanism for exception handling is applied.
Source code to bytecode
Opcodes are instructions that can be understood by NeoVM and correspond to a specific operation to be performed. For example, to perform a simple addition calculation in NeoVM, we might use the “PUSH1” opcode to push the number 1 onto the stack, then “PUSH2” to put the number 2 on top of the stack, and then finally use the “ADD” opcode to add the top two values on the stack together.
This conversion process is handled by a compiler, which takes the contract’s source code and converts it into the bytecode ready to be executed by NeoVM. In the Neo ecosystem, there are compilers available for multiple languages, such as the core C# compiler Neon, neo-boa for Python, and neo-go for Go.
As we noted in the last article, creation of the new exception handling system required several new opcodes, bringing with them the new logic needed to catch and handle exceptions in NeoVM. These include the three instructions required for the mechanism itself; TRY, ENDTRY, and ENDFINALLY, along with three instructions for throwing errors or faulting the VM as required; ABORT, ASSERT, and THROW.
Bytecode into the VM
After producing the bytecode version of a smart contract, the next step is to load it into NeoVM, usually achieved by deploying the contract to the Neo blockchain using a transaction. This stores the contract on each node on the network, where it can be invoked as required.
Invocation is handled by the appropriately named InvocationStack, a key component of the NeoVM execution engine. When a smart contract is invoked, the contract bytecode and any other relevant parameters are loaded into the VM, creating a running execution context. This execution context can be thought of as an isolated environment for carrying out the relevant operations.
Starting from the initial “NONE” state, NeoVM will then begin working its way through one operation at a time, stopping only when the InvocationStack is empty (entering the “HALT” state) or after an error occurs (resulting in the “FAULT” state).
Once the VM has reached FAULT, the state cannot be reverted. This is the case whether the FAULT was encountered due to an error in the contract, caused by an invalid transaction, or intentionally thrown in the code for another reason.
As a result, to add exception handling capabilities to Neo smart contracts, it was necessary to devise a solution that would allow code segments to be executed (and fail) without faulting the whole invocation.
Handling exceptions during execution
To explain the design, Chuan Lu, protocol group leader at Neo Global Development Shanghai and initial author of the mechanism, provided a written overview of the initial version in a Neo Column article. Though not fully up to date with the finished implementation, Chuan Lu introduces two important parts of the system; the TryStack and ExceptionHandlingContext.
An ExceptionHandlingContext is created whenever NeoVM executes the TRY opcode, with the new context being added to the TryStack. The context includes important information such as pointers to the instructions to be performed when either catching an exception or to be executed last as part of the optional finally segment. The other two opcodes, ENDTRY and ENDFINALLY, are used to guide NeoVM to the next instructions to follow, ensuring that code is executed as intended.
Whenever an exception is encountered, the TryStack can be searched for a try-catch that can handle the exception. Essentially this means that execution of a contract can be paused while different conditions are checked in the catch segment, allowing smart contracts on Neo to define their own catchable exceptions to preemptively avoid invocation faults (or possibly to force a fault in the event that unwanted behaviour is detected).
For a Neo contract developer, the arrangement of these various opcodes is handled by the compiler, meaning they can focus on adding exception handling to their contracts at the source code level. As an example, a Python developer would be able to use a try and except code block in their code, which would be interpreted by the neo-boa compiler to add the same catch conditions to the final smart contract.
One notable benefit to the design of this mechanism is its support for try-catch nesting, allowing a try-catch to be performed within another try-catch. If the first try-catch does not catch an exception, it is thrown upward until caught or a fault is triggered, allowing for greater flexibility.
The implementation of exception handling for Neo3 smart contracts provides yet another powerful tool for dApp developers, continuing in Neo’s wider vision to be the most developer friendly platform.