Lightweight Contracts
Introduction to Lightweight Contracts
Lightweight Contracts represent a framework for developing a layer of automation on top of the existing Ardor APIs.
Contracts are developed by implementing predefined interfaces. The contract code is deployed to the blockchain as a cloud data transaction, which stores the code itself, and a contract reference transaction which associates a specific account with an existing deployed contract and configures the contract setup parameters for this specific account.
Learn more about the concepts behind the lightweight contracts using these resources:
Prerequisites
Contract developers are expected to master the Java programming language or any other language which uses the Java JVM as its runtime environment.
Some prior knowledge of blockchain in general, Ardor specifically. Familiarity with concepts like account, passphrase, transaction, and block are important but not mandatory as you can always review the sample contracts supplied with the product as reference implementation.
Getting Started
Installation
As of Ardor 2.2.3 when installing Ardor using the Windows or Mac installer a fully functional JDK is installed into the jdk folder under the product installation. We recommend that you use this JDK version when setting up your IntelliJ project. For all other configurations install Java SE JDK 8 or higher manually.
Install the latest version of the IntelliJ IDE, the free community edition is supported. You can develop contracts using any other Java IDE but the project setup and all examples assume using the IntelliJ IDE.
Install the latest version of Ardor and make sure to leave the dev.tools option checked during installation.
Contract Runner
Unlike many other contract development frameworks, lightweight contracts are not executed by every node on the blockchain, instead node operators have to configure their node to run contracts by registering the contract runner addon. The contract runner monitors every new block for trigger transactions. When found it triggers the respective contract after loading it from the blockchain. The contract runner also supports contracts which execute on every block and provides APIs for triggering contracts directly or using a transaction voucher.
Contract Runner fees and funds
The contract runner account needs funds to pay the fees for any submitted transaction. Please ensure that the account has enough balance on every chain where the contracts will submit transactions. Alternatively, you can setup a bundler to sponsor the fees and let the contract runner submit zero fee transactions.
The contract runner uses the reference transaction mechanism to link any submitted transaction to the transaction that triggered the contract. This ensures that the confirmation of the new transaction depends on the confirmation of the triggering transaction. This requires a deposit of 10 ARDR for every submitted transaction that is refunded when both transactions are confirmed or the submitted transaction expires. So please make sure the contract runner account has enough ARDR balance to cover this deposit.
How to configure the Contract Runner
First and foremost, to activate the contract runner for your node add the property
nxt.addOns=nxt.addons.ContractRunner
to your node's nxt.properties file.
You have now several ways to configure the contract runner. You only need to use one of the options.
- The easiest and recommended way is to use the Node Processes UI to set up an encrypted configuration file stored on the node. You only need to save the file once and then just start the file using an encryption password on each node start. The Save modal popup even creates a basic working configuration for you that is fully functional so most of the times it is just a matter of entering the passphrase and an encryption password and you are good to go.
- Another option is to keep the configuration on your local computer and upload it as a json file using the provided button on the contract runner status page accessible via the Settings menu.
- You can also add the configuration parameters directly on the nxt.properties file so they will be used right away as the node starts. You need to prefix every parameter with
addon.contractRunner.
so they become like this:addon.contractRunner.secretPhrase=IWontTellYou
Please be aware that there are several parameters that shall remain secret like the accounts’ secret phrases or some contract setup parameters. Thenxt.properties
file is stored in clear text.
Contract Runner General Configuration Settings
The following properties control the Contract Runner execution.
"secretPhrase" - specify the secret phrase of the contract runner account i.e. the account from which the contract runner will submit transactions. This passphrase is never submitted to any remote node. Be careful if you decide to store this data in the nxt.properties file as it is stored in clear text.
"accountRS" - alternatively if you only want your contract runner to verify transactions submitted by another contract runner, specify the account of the contract runner you would like to follow. This way your contract runner can repeat the contract calculations and verify transactions submitted by another contract but not submit its own transactions.
"autoFeeRate" – if set to true (default: false), the contract runner will calculate the best fee available based on the current bundlers. This should be equivalent to the Calculate Fee button on the Wallet UI.
"minBundlerBalanceFXT" – this optional parameter allows filtering out bundlers when autoFeeRate is activated. Bundlers with an effective balance below the property minBundlerBalanceFXT are not considered for the best fee calculation. The default for this property is to use the same value as the global property nxt.minBundlerBalanceFXT.
"minBundlerFeeLimitFQT" - this optional parameter allows filtering out bundlers when autoFeeRate is activated. Bundlers with a fee limit currently below the property minBundlerFeeLimitFQT are not considered for the best fee calculation. Again, the default for this property is to use the same value as the equivalent global property.
"feeRateNQTPerFXT.<Chain Name>" - when autoFeeRate is set to false or the best fee cannot be obtained the contract runner will use this value specified in NQT to calculate the child chain fee to submit. For each chain your contract runner is expected to submit transactions, you can specify a different value. Example: to set the rate to 2.5 IGNIS per ARDR, set this property to 250000000. i.e. 2.5 IGNIS specified in NQT.
"validator" - if set to true (default: false), the contract runner will watch for transactions submitted by other contract runners and will try to verify that the other contract runner indeed runs the contract stored on the blockchain.
"validatorSecretPhrase" - when validator is set to true, and another contract runner is using an account under account control, specify here the secret phrase of the controlling account. Your contract runner will intercept the transactions submitted by the other contract runner, repeat the calculations, and if it receives the same result, will submit an approval transaction for the transaction submitted by the other contract runner.
"catchUpInterval" - a timeout value specified in seconds (default: 3600 i.e. one hour). During blockchain download, the contract runner will only submit transactions when downloading a block with a timestamp later than the current time minus the defined catchUpInterval. The purpose of this setting is to prevent a contract runner from flooding the unconfirmed transaction pool with duplicate transactions during blockchain download.
"seed" - supply random seed to the contract runner formatted as hex string (default: the public key of the contract account which is useless from randomness perspective). Convert any random value to hex string using the hexConvert API and specify the resulting hex string value as a seed. A seed of less than 16 bytes can be easily brute-forced so make sure your seed is longer. Keep your seed secret, it can be used in the future to validate your contract execution.
Contract Runner Contract Specific Settings
Note that in most cases there is no need to specify these parameters. Always prefer using contract setup parameters that are stored on the blockchain itself except when a contract parameter has to remain secret (for example, a passphrase or some secret credentials to a 3rd party service).
In the rare cases when this configuration is needed it is recommended to use the Node Processes UI.
Alternatively, specify the configuration file used by the contract runner to load contract specific setup parameters by adding the property addon.contractRunner.configFile to nxt.properties, for example addon.contractRunner.configFile=./conf/contracts.json
, the specified path is relative to the user directory.
A sample contract configuration file is provided in the ./addons/resources/contracts.json file under the installation folder. Copy this file to the conf folder under your project's user directory and configure it as follows.
"params" - specify contract setup parameters per contract. Use this setting only for private setup parameters that you don't like to submit to the blockchain such as passwords, API keys, etc. Otherwise specify the setup parameters in the contract reference transaction as explained in the contract manager configuration. The contract parameters configured in the params object are accessed by the contract code using the context method
context.getContractRunnerConfigParams(getClass().getSimpleName())
Alternatively, create an inner interface decorated with the @ContractParametersProvider
annotation inside your contract and define a method with the annotation @ContractRunnerParameter
using the same name as the corresponding parameter in the contract runner configuration file.
For example see the secretPhrases()
method of the LeaseRenewal
sample contract which returns the array of passphrases specified in contracts.json
{ "params": { "LeaseRenewal": { "secretPhrases": [ ... ] }}}
Monitoring the contract runner
To verify that the contract runner has started, look for the message "ContractRunner Started" in the node log file.
To check the status of the contract runner and deployed contracts, invoke the getSupportedContracts API.
In response, you'll receive an array of contracts supported by the specific contract runner and their properties, and the contractAccount parameter which specifies the owner account of the contract runner, which is based on the passphrase or account id you specified in the contract runner configuration.
In case the contract runner has failed to start, the getSupportedContracts API will return a descriptive error message.
As of version 2.2.1 when your wallet is connected to a contract runner node, you can choose "Contracts" from the "Settings" menu to view information about the contract runner and the deployed contracts. Including information about invocation parameters for the contracts and validation used by the contract.
Contract Manager
The contract manager is a command-line utility that manages the contract lifecycle. Its primary usage is to deploy contracts to the blockchain, use it also to add or change a reference to an existing contract, to update contract setup parameters, to remove old contract references, and to verify that a specific contract was compiled from a specific Java source file.
Contract Manager Configuration
The settings which control the operation of the contract manager are defined in the nxt.properties file, their documentation is specified in the ./conf/nxt-default.properties file under the installation folder in the "#### CONTRACT MANAGER ####" section.
You should define the secret phrase for transactions submitted by the contract manager using the property contract.manager.secretPhrase=[Secret Phrase]
The contract manager connects to an Ardor node to use the Ardor APIs and submit transactions. By default, it connects to a node running on localhost. Use the contract.manager.serverAddress=[Server Address]
property to connect the contract manager to a remote node. The contract manager will never submit its passphrase to a remote node.
Use the optional contract.manager.uploadParamsFile=[Path to configuration file]
property, to configure the contract manager upload parameters configuration file.
Upload Parameters Configuration File
The upload parameters file contains a json array of contract definitions named "contracts".
Note: as of version 2.2, the contract upload configuration file is optional. Every setting in this file can be configured using a matching property in nxt.properties file.
For each contract specify the following parameters:
"className": the name of the class without package name, this name represents the class name loaded by the contract runner from the data cloud and the name of the contract reference.
"packageName": [optional, default to package specified as command line parameter] the Java package name of the contract class as specified in the package directive inside the source code.
"filePath": only in case the contract is deployed as a Jar file. Specify the path to the Jar file which will be deployed to the cloud data. If no filePath is specified the contract manager will attempt to load the class file specified by the className attribute from the classpath.
"params": a Json object representing the contract setup parameters. These parameters are accessed by the contract through its context object or using the @ContractParametersProvider
interface methods decorated with @ContractSetupParameter
annotation.
The upload parameters for the sample contracts available out of the box are defined in the ./addons/resources/contract.uploader.json file under the installation folder. This file is automatically copied to the ./conf folder under the user directory in case it does not exist yet.
Alternatively, every setting in the contract uploader file can be defined in nxt.properties using the following format:
Contract Manager settings for a specific contract - contract.[contractName].[setting]
Contract setup parameters submitted to the blockchain - contract.[contractName].param.[setting]
For example, to define the "frequency" setup parameter of the "AllForOnePayment" contract use the following property:
contract.AllForOnePayment.param.frequency=6
Setting Up the Development Environment
Assuming all dependencies were installed, we can now open the Ardor contracts project inside IntelliJ and start developing contracts.
The IntelliJ project structure is provided as part of the "Contract Development Tools" package you selected when installing Ardor so there is no need to create a new project.
If you installed Ardor into a read-only folder you will need to copy it to a folder for which you have write permissions.
Windows - copy "c:\Program Files\Ardor"
to a writable location such as c:\Users\<Your user>\Documents
Mac - copy /Applications/ardor.app
to ardor.app
in your home folder (use cp -R from the terminal for recursive copy on Mac)
Linux - this should not be an issue as long as you installed Ardor into a folder you own.
Alternatively, if you decided to develop your project in the same location where you installed Ardor, a future upgrade may override some of the project files so make a backup of your project before upgrading.
Start IntelliJ, from the top menu choose "File->Open", in the resulting "Open file or project" dialog select the Ardor folder itself.
Select the Ardor module from the left pane by clicking the "1:Project" label.
The Ardor module contains the sample contracts provided with installation.
Open the Ardor module settings by pressing F4 while the module itself is in focus or right click and choose "Open Module Settings".
Select the "Project" link from the left pane and set the "Project SDK" to the Java JDK you installed earlier and set the "Project Language Level" to 8 and click "Ok".
Build the Ardor module by selecting the "rebuild Module 'Ardor'" from the IntelliJ "Build" menu (Ctrl+Shift+F9)
Restart IntelliJ and reopen the project.
Under the Ardor module, there are two Java source roots, the first contains the sample contracts, the second contains the unit tests for the sample contracts.
For example, this is the HelloWorld sample contract.
Your IntelliJ contracts project contains the following launchers, visible in the selection box on the top right.
Ardor Local
- runs the node using the existing node configuration
ContractRunnerSuite
- invokes the automatic tests for all contracts
You are now ready to develop your first contract
Contract Development
A contract is composed of one or more Java class files possibly packaged into a Jar and possibly relying on other Jar files.
The main contract class has to extend the nxt.addons.AbstractContract class and implement one or more of the provided callback methods which are invoked by the Contract Runner in response to specific events as shown as follows:
Trigger: Trigger transaction
Callback method:
JO processTransaction(TransactionContext context)
Description: Invoked when a trigger transaction is applied by the blockchain i.e. the callback is invoked for phased transactions when confirmed, and for normal transactions once they are included in a block. Special case are transactions phased by hashed secret which are also invoked when included in a block.
Trigger: Every block
Callback method:
JO processBlock(BlockContext context)
Description: Invoked every block
Trigger: triggerContractByRequest API call
Callback method:
JO processRequest(RequestContext context)
throws NxtException
Description: Invoked by calling the triggerContractByRequest API
Trigger: triggerContractByVoucher API call
Callback method:
JO processVoucher(VoucherContext context)
Description: Invoked by calling the triggerContractByVoucher API
Trigger: Call from another contract
Callback method:
ReturnedData processInvocation(DelegatedContext context,
InvocationData data)
Description: Invoked when another contract calls this contract
A contract should implement one or more of these callback methods.
Context Objects
Each of the above callback methods, receive a context object, the context object provides various services to the contract and represents the interface between the contract and the blockchain.
Always prefer using the context object over calling directly to an internal blockchain method since every method provided by the context is guaranteed to maintain backward compatibility and not break your contract code when a new Ardor release is deployed. The services provided by the context object depend on the type of callback, these services are documented in the Javadoc for the context class.
API callers
For each of the Ardor public APIs there is a specific Java caller class named the same as the API with a capital letter at the beginning and a "Call" prefix.
For example the call object for the getBlockchainStatus API is GetBlockchainStatusCall
.
API invocation always follows the same pattern, create a caller instance, possibly set some API parameters, then invoke the call() method to receive a Json response as a JO object you can then work with.
For example: to invoke the getExecutedTransactions API from inside the contract:
GetExecutedTransactionsCall request = GetExecutedTransactionsCall.create(2).height(height).type(0).subtype(0).recipient(context.getConfig().getAccount());
JO getExecutedTransactionsResponse = request.call();
Always use the API callers to access information stored on the blockchain since these API callers represent the interface between your contract and the data stored on the blockchain. The API callers will remain compatible in future releases.
Submitting Transactions
In most cases, contracts should not save any internal state between contract invocations. To make changes to the blockchain state, contracts should submit transactions. To submit a transaction, invoke the API caller for the specific transaction type, then use the context.createTransaction()
method to submit the transaction to the blockchain.
Internally the createTransaction method implements local signing, fee calculation, and other complex processing that you as contract developer don't have to deal with.
Example:
SendMoneyCall sendMoneyCall = SendMoneyCall.create(context.getChainOfTransaction().getId()).recipient(recipient).amountNQT(returnAmount);
context.createTransaction(sendMoneyCall);
Contract Parameters
For contracts triggered by a transaction use
context.getRuntimeParams()
to load the specific invocation parameters or preferably define the parameter as a method of an inner interface decorated with @ContractParametersProvider annotation. Name the method the same as the parameter name and decorate it with the @ContractInvocationParameter annotation. To load the contract setup parameters specified in the contract reference transaction use
context.getContractSetupParameters()
or preferably define the parameter as a method of an inner interface decorated with @ContractParametersProvider annotation. Name the method the same as the parameter name and decorate it with the @ContractSetupParameter annotation. To load the contract runner parameters specified in the contract runner configuration file for the specific contract use
context.getContractRunnerConfigParams(getClass().getSimpleName())
or preferably define the parameter as a method of an inner interface decorated with @ContractParametersProvider annotation. Name the method the same as the parameter name and decorate it with the @ContractRunnerParameter annotation.
All the parameters getter methods return a JO object.
For usage example of the @ContractParametersProvider
decorated inner interface see the HelloWorldForwarder
and LeaseRenewal
sample contracts.
Working with Json
All API callers and configuration parameters return a JO object which represents a Json object.
Use the JO object to query the Json object. Specifically use the getArray() method to obtain a JA object representing a Json Array.
There are plenty of usage examples in the sample contracts.
Accessing External Resources
Unlike most smart contract frameworks, lightweight contracts support access to external resources. Your contract can communicate as client of any external service which provides a Java client API library, or Http, XML, Json, Soap, ldap, or a similar interface.
If necessary your contract can access code from 3rd party Jar files as long as these Jar files are included in the classpath of the contract runner node. You may add external Jar files to the classpath of the node by copying the files to the ./addons/lib folder under the installation folder.
Learn mode about Oracle Contracts
Lightweight Contracts Design
To learn more about the internal design of the lightweight contracts framework see this article
Best Practices
Single Source File Multiple Classes
To simplify contract development always attempt to concentrate all your source code into a single source file which defines the contract class itself and possibly additional inner classes. When deploying the contract to the blockchain the contract manager loads the main contract class and checks if it has inner classes. If there are no inner classes, the contract class file is deployed to the blockchain, otherwise, the contract manager packs the contract and all its inner classes into a Jar file and deploys the Jar file to the blockchain.
Parameter Validation
Always start your contract code with input validity checks. For example, if your contract is triggered by a sendMoney transaction, make sure the payment recipient is the contract account and that the payment uses the expected chain, that the payment amount is correct, etc.
Validate that all invocation parameters represent the data type you expect and are within their valid boundaries.
For your convenience you can decorate the contract processTransaction
and processVoucher
methods with one of the following predefined annotations:
@ValidateTransactionType
validates that the trigger transaction type is of one of the accepted types and is not of one of the rejected types.
@ValidateContractRunnerIsRecipient
validates that the contract runner account is the recipient of the trigger transaction.
@ValidateChain
validates that the chain of the trigger transaction is one of the accepted chains and is not one of the excluded chains.
See usage example in the RandomPayment
contract.
Stateless Contracts
In most cases, contracts should only store state information in the blockchain itself or as setup parameters. If you are using member variables in the contract class itself you might be doing something wrong. Bare in mind that the contract runner may restart at any moment and that your contract callbacks may execute in parallel with other contracts. Learn more about stateless contracts
Internal Blockchain Functionality
Do not invoke public methods of the blockchain directly. Always attempt to use the context object and the caller APIs.
If you are missing some essential function or service, let us know and we will add it in the next release.
Random Data
If your contract requires random data, make sure to define a secret random seed in the contract runner configuration. See the following article about our approach to random number generation
Contract Deployment
As explained above, contract deployment is a two-step process, first the contract code is deployed using a cloud data transaction then a contract reference transaction is submitted to provide an entry point to the correct version of the contract. While this deployment process can be accomplished manually using the Ardor APIs, it is much simpler to perform the deployment using the contract manager utility.
First configure the contract manager then use either the contract manager IntelliJ plugin (recommended) or invoke the command line utility (ContractManager.bat or contractManager.sh) from the command line to view the available options.
In both configurations, the contract manager connects to an existing full node, preferably running on your local workstation. Make sure your node is properly configured and fully synchronized with the blockchain.
Remember that the contract manager deploys transactions to the blockchain. For the transactions to confirm you need to wait for the next block.
Contract Manager IntelliJ Plugin
The contract manager plugin provides a simple configuration dialog to invoke the contract manager utility from inside the IntelliJ IDE. You first need to install the contract manager plugin into your IDE, this process has to be repeated after every Ardor version upgrade.
Install the Plugin
From the "File" menu choose the "Settings" option.
From the resulting "Settings" dialog select the plugins page.
IntelliJ 2018.3 and higher
Select the cogwheel menu on the right and click "Install plugin from disk" menu item. Using the resulting file dialog select the ContractManagerPlugin.zip file from the Ardor installation folder.
IntelliJ 2018.2 and lower
Click the "Install plugin from disk" button at the bottom. Using the resulting file dialog select the ContractManagerPlugin.zip file from the Ardor installation folder.
You should now see an "Ardor Contract Manager" entry listed in the plugins list.
Restart IntelliJ to complete the installation.
The contract manager plugin is now installed.
Repeat this process whenever you upgrade Ardor to install a new version of the plugin.
Use the Plugin
To use the contract manager plugin you must first start your Ardor node. One simple way to do so is to run the node using the "Ardor Local" launcher from inside the IntelliJ IDE. If your node is offline while running the contract manager, the contract manager will display the message "Cannot connect to http://<address>:<port>/nxt make sure the node is running" and terminate.
Edit the runtime configurations.
Add a new configuration of type "Ardor Contract Manager"
Name the configuration and define the contract manager options
Run the configuration as usual
Review diagnostic messages logged to the IntelliJ console at the bottom of the screen.
Contract Manager Command Line Utility
Examples
Deploy a new version of the ForgingReward contract:
contractManager.bat -u -n ForgingReward -p com.jelurida.ardor.contracts
Response:
... Contract class com.jelurida.ardor.contracts.ForgingReward uploaded 5f4afbead4f7f887082a36dd97df72f6ed7f980c1b3e99eccb7d67bb150c9748 ... Contract reference registered ForgingReward={"rewardArdor":"true","interval":5,"rewardNxt":"true","rewardChain":"IGNIS","rewardAmountNQT":150000000} for contract chain: IGNIS, full hash: 5f4afbead4f7f887082a36dd97df72f6ed7f980c1b3e99eccb7d67bb150c9748
List deployed contracts for a specific account:
contractManager.bat -l -a ARDOR-XK4R-7VJU-6EQG-7R335
Response:
{"contract":{"chain":2,"transactionFullHash":"ff374385044519900147c145beeaa07520f0a9850b95d84b835c88ff1ce81015"},"name":"DistributedRandomNumberGenerator","id":"10806939862175194746","params":"{}"}
{"contract":{"chain":2,"transactionFullHash":"5f4afbead4f7f887082a36dd97df72f6ed7f980c1b3e99eccb7d67bb150c9748"},"name":"ForgingReward","id":"10360831005888949167","params":"{\"rewardArdor\":\"true\",\"interval\":5,\"rewardNxt\":\"true\",\"rewardChain\":\"IGNIS\",\"rewardAmountNQT\":150000000}"}
{"contract":{"chain":2,"transactionFullHash":"8292a5e3a9bd209a85b3a865be8175d1c762393dac33e84a3113e19a7efc0e39"},"name":"NewAccountFaucet","id":"4615352619232966745","params":"{\"thresholdAmountNQT\":72000000000,\"chain\":2,\"thresholdBlocks\":1440,\"faucetAmountNQT\":500000000}"}
Delete contract reference for a specific account (the contract itself is not deleted)
contractManager.bat -d -a ARDOR-XK4R-7VJU-6EQG-7R335 -n ForgingReward
Response:
Contract reference 10360831005888949167 deleted for account ARDOR-XK4R-7VJU-6EQG-7R335 contract name ForgingReward
Add or update contract reference
contractManager.bat -r -h 5f4afbead4f7f887082a36dd97df72f6ed7f980c1b3e99eccb7d67bb150c9748 -n ForgingReward
(the hash parameter -h is the full hash of the cloud data transaction storing the contract version you would like to reference)
Response:
Contract reference registered ForgingReward={"rewardArdor":"true","interval":5,"rewardNxt":"true","rewardChain":"IGNIS","rewardAmountNQT":150000000} for contract chain: IGNIS, full hash: 5f4afbead4f7f887082a36dd97df72f6ed7f980c1b3e99eccb7d67bb150c9748
Verify that a contract class in the blockchain was compiled from a specific source file, note that this action has to run using a Java JDK and not using the Java JRE provided with some of the installations.
Invoke the command below from the Ardor installation directory. The verification action has to run using an Oracle Java JDK.
"c:\Program Files\Java\jdk-10.0.2\bin\java.exe" -Djava.security.manager -Djava.security.policy=contractManager.policy -cp classes;lib\*;conf nxt.tools.ContractManager -v -h 4a8cd1c3fc86ad1cfbce4957142ff86c5d908af633bc6e38fddbf0be2418ff05 -s c:\Users\liory\ardor\addons\src\java\com\jelurida\ardor\contracts\HelloWorld.java
Response:
Verification succeeded - class files are identical
Contract Security
The contract runner relies on the Java JAAS framework to ensure that malicious contracts downloaded from the blockchain cannot attack the contract runner local workstation or the blockchain node operations. To achieve this, contracts are loaded using their own class loader and run in their own protection domain which forms a sandbox that limits the permissions assigned to contract code.
Most contracts should be able to run using default the permissions. However, if you need to make changes to security permissions, read on.
The main configuration files for controlling the contract security are ardor.policy and ardordesktop.policy, both files are standard Java policy files. ardor.policy is the active policy file when running in command line mode and when running unit tests. ardordesktop.policy is a slightly more permissive policy file active when running the desktop wallet. In this document, we will always use ardor.policy, everything explained is also applicable to ardordesktop.policy.
By default, contracts are assigned the permissions defined in the virtual untrustedContractCode protection domain.
grant codeBase "file://untrustedContractCode" {
permission java.io.FilePermission "${java.io.tmpdir}*", "read,write,delete";
permission java.util.PropertyPermission "java.io.tmpdir", "read";
};
The defined permissions grant the contract code access to the temporary folder on the contract runner workstation. The contract can freely read, write, delete files in this folder.
In addition, contracts are always allowed to connect to any internet address.
See the AllowedActions
contract for code samples permitted by the policy file and the ForbiddenActions
for code samples not permitted by default. Neither of these samples cover the full list of permitted and non-permitted actions.
Granting Additional Contract Permissions
At the moment all sample contracts are designed to run using the default untrusted contract permissions, however, developing contracts with elevated permissions is supported. A contract runner node operator can grant additional permissions by adding these permissions to the untrustedContractCode protection domain of ardor.policy, but this is discouraged since it will grant these permissions to all the contracts run by this contract runner. Instead, we recommend elevating permissions per contract signer account or per specific contract.
To grant additional permissions for contracts signed by a specific account, define the public key of the account that submitted the contract to the blockchain, in a new protection domain entry using the "signedBy" token. The public key should be specified in hex string format.
For example the following ardor.policy section grants additional permissions for contracts submitted to the blockchain by the account whose public key is the following:
112e0c5748b5ea610a44a09b1ad0d2bddc945a6ef5edc7551b80576249ba585b
grant codeBase "file://untrustedContractCode" signedBy "112e0c5748b5ea610a44a09b1ad0d2bddc945a6ef5edc7551b80576249ba585b" {
permission nxt.util.security.BlockchainPermission "db";
permission nxt.util.security.BlockchainPermission "getBlockchain";
};
Similarly to grant additional permissions to a specific contract use the "principal nxt.util.security.TransactionPrincipal" setting followed by the contract's transaction full hash or the hash of the whole tagged data as saved in the blockchain or the hash of the contract class/jar itself.
grant codeBase "file://untrustedContractCode" principal nxt.util.security.TransactionPrincipal "df15278b53c5c24ccb179302834608b50b94c3e91c97ffa0510357e35fec919b" {
permission nxt.util.security.BlockchainPermission "db";
};
To monitor the permissions assigned to your contract in runtime, use the following Java command line flag -Djava.security.debug="access"
. Additional information about protection domains can be obtained using -Djava.security.debug="access,domain"
but this creates very verbose output.
Look in the generated log for permissions which are marked as "denied" and use the diagnostic information to learn the reason for denial.
To configure the ardor.policy file you can use any text editor or IntelliJ IDE but be warned that the syntax of this file is very strict, therefore every small typo will render the file useless and remove all permissions to all code resulting in errors when starting the node.
Alternatively use the graphical policytool provided with the Java JRE for making changes to the policy file without the risk of creating syntax errors.
To temporarily disable all permission checks add the following property to the nxt.properties file:
nxt.disableSecurityPolicy=true
Checking Contract Permissions
A contract that requires elevated permissions from the contract runner should start by checking that these permissions were granted, if not, it should fail gracefully. Use the context.isPermissionGranted()
API to check if a specific permission is granted. See the DatabaseAccess sample contract for example how to check for permissions. If a required permission is not granted, log an error message and return.
Sample Contracts
There are multiple sample contracts included with source code as part of the product installation. The sample contracts are documented using javadoc and internal comments. Always use the sample contracts as reference to understand correct coding techniques and best practices.
In the next sub-sections it will be explained several sample contracts. All the examples can be found in the contracts folder of the official repository.
Trading bot (CoinExchangeTradingBot.java)
Trading bot CoinExchangeTradingBot.java
is a contract which performs market making on the coin exchange and asset exchange. Contract runners can define the holdings (coin or asset) on which to submit buy and sell orders and the percentage difference from a base exchange rate loaded from coinmarketcap, which serves as a sample data source. The bot can be configured to compete with other traders or issue orders according to the base exchange rate to provider liquidity.
Account Balance Notifier (AccountBalanceNotifier.java)
The account balance notifier AccountBalanceNotifier.java
monitors the account balances of configured holdings and raises an alert in case their holding balance drops below some minimum limit. The notifier can be used to monitor an account performing market making to alert the operator in case a holding balance is exhausted.
Chain Monitor Contract (ChainMonitor.java)
The chain monitor contract ChainMonitor.java
monitors specific aspects of the blockchain operations. It submits an alert to one or more notification services in case a problem with the blockchain operation is detected.
Notification service contracts (SlackNotifier.java, TelegramNotifier)
Notification service contracts SlackNotifier.java, TelegramNotifier
are utility contracts used for posting messages from other contracts to Slack and Telegram respectively.
Contract Trigger Transaction
To trigger the execution of a deployed contract, any account can submit a transaction with a special message attachment. The message should be prunable (leave "Message is Never Deleted" unchecked in the wallet) and should be formatted as Json, can be either encrypted or plain text, should identify the contract to execute and optionally provide contract specific invocation parameters.
Examples
Trigger the HelloWorld contract by sending a message transaction to the contract runner account with the text:
{“contract”:”HelloWorld”}
Trigger the SplitPayment contract and describe how to split the payment, by sending a payment transaction to the contract and add the following attached message:
{"contract":"SplitPayment","params":{"ARDOR-SZKV-J8TH-GSM9-9LKV6":"0.2","ARDOR-9KZM-KNYY-QBXZ-5TD8V":"0.5","ARDOR-E93F-7E8Z-BHJ8-A65RG":"0.3"}}
Note that the "params" token in the message is a Json object by itself, the data in the params object is contract specific. Consult the contract author as to which params need to be submitted and in which format.
When a trigger transaction is submitted, all active contract runners will process this transaction. Whether or not the contract will actually process the transaction depends on the contract itself.
For example, most contracts will only process payments submitted to their contract runner account and ignore payments made to other accounts but this depends on the contract logic. Contracts can choose to process certain transaction types, certain chains, and certain recipient accounts based on their own internal logic.
Oracle Contracts - Interface with External Systems
One of the powerful features of Lightweight Contracts is their ability to interface with external systems to load data and register it on the blockchain and to save data to an external system based on blockchain data. Interfacing with external systems is a trade-off between decentralization and utility. There are cases where a specific contract runner can load data from an external system such as exchange rate or personal data that no other contract runner can validate. There is no choice for users but to trust the contract runner not to manipulate this data the same way users trust a centralized system. Still, once the data is registered on the blockchain, it is digitally signed and timestamped by the contract runner account and can no longer change. So as long as the contract runner is a trusted entity information provided by it can be used safely.
However, there are few steps contract developers need to take to increase the reliability of Oracle contracts, as a developer, you should consider that contracts should be designed to be idempotent (i.e. can run multiple times on the same data and produce the same result). For example, a contract can run once when a trigger transaction is received, then again if the node switches to a better fork and the trigger transaction is included in a different fork, and again in case the contract runner is re-downloading the blockchain. Ideally, in all these cases the contract should reproduce the exact same output transactions. The blockchain itself protects against including duplicate transactions in the blockchain.
However, when relying on external data there is no way to guarantee that transaction data won't change between invocations. Therefore developers of Oracle contracts should always look for duplicate transactions generated by the contract based on the same trigger transaction but with different data (if the data is the same, duplicate transactions will be discarded by the blockchain itself).
Contract developers need to override the following method:
public <T extends TransactionResponse> boolean isDuplicate(T myTransaction, List<T> existingUnconfirmedTransactions) { ... }
The method accepts the transaction currently submitted by the contract and a list of transactions already waiting to be included in the blockchain. The contract should implement contract specific policy to identify if the new transaction duplicates an existing transaction. If it does, the method should return true value to instruct the contract runner not to submit the new transaction to the blockchain.
Example for Oracle contracts
The IgnisArdorRates
contract uses the Bittrex exchange APIs to calculate the market rate between Ardor and Ignis and compare it to the decentralized coin exchange market rate. For this contract duplicates are no a real risk, at worst, there will be slightly different exchange rates registered on the blockchain on the same block.
The LiberlandCitizenRegistry
contract uses the Liberland APIs to load citizen data. There is a small risk that the citizen data will change between invocations of this contract so a duplicate check makes sense before submitting a transaction.
Testing and Debugging your Contracts
Testing and debugging your contract is supported by the IntelliJ IDE using the standard tools used to test and debug any Java program.
Unit Tests
For each of the sample contracts provided with the product, there is also a matching unit test class with the same name followed by a "Test" postfix. For example contract HelloWorld
has a unit test named HelloWorldTest
.
Unit tests rely on the junit framework, each unit test starts from a clean blockchain copy, which only includes the Genesis block, it then deploys a contract, perform some actions on the contract and tests the output transactions.
All sample unit tests are grouped into the ContractRunnerSuite, the IntelliJ project provided by the installation includes a predefined launcher which runs this test suite. The first run will generate the unit tests blockchain database. Subsequent runs should work faster.
Our recommendation is that whenever you design a new contract you always design a matching unit test class to test it. Whenever you make changes to the contract make sure that all unit tests for the contract pass without errors. Unit tests also enable you to design your contract without deploying it to the blockchain where it is visible to others.
Debugging your Contracts
You can easily debug your contracts by placing a breakpoint inside the contract code. You can then run the unit test which test your contract in debug mode and debug the contract. Another option is to deploy your contract to the testnet then run your node using the "Ardor Local" launcher in debug mode. This will let you debug your contract exactly as it is executed in runtime. Since contracts are simply Java programs you can use any common debugging technique available by IntelliJ such as conditional breakpoints etc.
Obtaining more Diagnostic information
The contract runner and the contracts it runs, log information about their operation in the normal Ardor log.