This guide will provide a short overview on how to write programs that can work on the kindelia network, how to deploy them and how to interact with them after they have been installed on the network.
It doesn't cover instructions on installing the required tools, which can be found on the respective repositories, nor does it provide and in-depth explanation of each of the components. It is recommended to first read the Kindelia network whitepaper to have a basic understanding of how it works.
After reading this, you'll have the required information to be able to write, deploy and use a simple kindelia contract. More complex examples may require some special considerations, but the fundamentals would be the same.
- Writing and deploying programs for the Kindelia network
- Table of Contents
- Introduction
- Writing a Kind program
- Writing Kind programs for the Kindelia network
- Compiling Kind programs to kdl
- Testing your Kindelia program
- Deploying to the kindelia network
- Interacting with programs on the network
- TL;DR
Programs in the Kindelia network are made of functions with an associated state for each of these functions. We can submit new programs to the network and interact with existing programs by running scripts that can access and modify their state.
Although the Kindelia HVM's native assembly-like language is considerably more high level than arm or x86 assembly, it is still too simple to write complex programs, does not provide many mechanisms to check that it's programs are correct. So, we recommend using a different higher-level language that provides more features and is able to compile programs to the Kindelia HVM language.
Here, we will be using Kind, a programming language designed from the ground up to be a good fit for the HVM. With its dependent types we are able to mathematically prove desirable properties about its programs. At the moment it is also the only language capable of compiling to the Kindelia network.
In Kind, like on the Kindelia network, programs are also comprised of a set of functions that may call each other. Unlike the functions on the network, Kind functions don't have state and all expressions have types. The compiler will take care of making exerything compatible, with some extra information we need to provide it.
Functions in Kind are defined like in the following example:
U120.summation (nums: List U120) : U120
U120.summation (List.cons head tail) = U120.add head (U120.summation tail)
U120.summation List.nil = 0u120
This defines a function that takes a list of 120 bit unsigned integers and adds up all its elements recursively.
Each function has a name, a sequence of parameters, a return type and then a sequence of rules that define the result of the function call.
In this example, the name of the function is U120.summation
, it must receive exactly one argument of type List U120
and return a value of type U120
. It has two rewrite rules: the first is applied when the list given as argument has at least one element and the second is applied when the list is empty.
When running some code that calls this function, the runtime environment will compare the "shape" of the arguments to each of the provided rules, replacing the function call by the expression on the right of the =
of the first rule that matches.
Following with this example function, when the expression (U120.summation (List.nil))
is executed, the runtime will check the rules of U120.summation
, compare List.nil
with the first pattern List.cons head tail
and see that they don't match. It will then proceed to the next rule and match the argument List.nil
and see that it does match. Then, the original expression U120.summation List.nil
will be replaced in memory by the expression 0u120
, the number zero stored in a 120 bit unsigned integer.
On the other hand, if the expression U120.summation (List.cons 4u120 List.nil)
is executed, it will be compared with the first rule of the function which does match. The original expression will be substituted by the right side of the equation U120.add head (U120.summation tail)
, with variables head
and tail
being substituted by the two values inside the argument, 4u120
and List.nil
respectively. So, the original expression is replaced in memory by the resulting expression U120.add 4u120 (U120.summation List.nil)
and the runtime environment will continue it's execution, eventually applying the same process to this new expression.
For more information on what the Kind language provides, you can read this document that describes all of the language's syntax.
Let's say we wrote the following Kind file named MyFile.kind2
:
Foo : Type
Foo.new (a: U60) : Foo
Bar : Type
Bar.new (b: Foo) : Bar
MyFun (a: Foo) : Bar
MyFun (Foo.new 0) = Bar.new (Foo.new 1)
MyFun (Foo.new x) = Bar.new (Bar.new (+ x x))
Main {
MyFun (Foo.new 2)
}
This defines two types, an operation that uses them and a main function that tests one potential use of our new function. However, if you look closely at the second rule of MyFun
, you may notice that it has an error, the second rule has a Bar.new
where there should instead be a Foo.new
.
We can run this code with kind2 run MyFile.kind2
and we'll get an incorrect result. This kind of error could be prevented by checking that all the used types match what was expected. We can do this with the command kind2 check MyFile.kind2
which will point out the error in this code:
Type mismatch
- Expected: Foo
- Detected: Bar
Kind.Context:
- x : U60
On 'test.kind2':
9 | MyFun (Foo.new x) = Bar.new (Bar.new (+ x x))
Type mismatch
- Expected: Foo
- Detected: U60
Kind.Context:
- x : U60
On 'test.kind2':
9 | MyFun (Foo.new x) = Bar.new (Bar.new (+ x x))
By taking advantage of the dependent type system we can even prove many things about the code we write. For example, look at this proof that addition is commutative:
Nat.add.comm (a: Nat) (b: Nat) : Equal Nat (Nat.add a b) (Nat.add b a)
Nat.add.comm Nat.zero b = Nat.add.comm.zero b
Nat.add.comm (Nat.succ a) b =
// Goal: (b + a) + 1 == (a + (b + 1))
// We know that a + b == b + a because of induction
let induction = (Nat.add.comm a b)
// (a + b) + 1 = (b + a) + 1
let induction = Equal.apply (x => Nat.succ x) induction
// (a + b) + 1 = (a + (b + 1))
let induction_tool = Equal.mirror (Nat.add.comm.succ b a)
// We can get our goal with this
Equal.chain induction induction_tool
Just like in this example, we can use the type system to prove many useful things, like that the sum of all tokens stay the same after a transfer or that no one can obtain an item in our game unless certain conditions are met.
With this short introduction to the Kind language, let's tryu to use it to make a smart contract for Kindelia. We'll be making a very simple contract that simply counts up when it is executed.
MyCounter (arg: ?) : ?
MyCounter ? = ?
With this function as the base of our contract, we can fill the ?
one by one to make something usable.
First off, we need to be able store a number on the state and to increment it when called. A contract's state can be read using the function Kindelia.IO.take
and stored back with a new value with function Kindelia.IO.save new_val
. If we check the definition of Kindelia.IO.take
, we can see that it returns the state encapsulated in a value of type Kindelia.IO
. To access the inside of this value we can use Kind's monadic do
syntax, like so:
MyCounter (arg: ?) : Kindelia.IO ?
MyCounter ? =
do Kindelia.IO {
ask old_val = Kindelia.IO.take
let new_val = U120.inc old_val
ask Kindelia.IO.save new_val
return ?
}
One of the thing that differentiates contracts from normal pure functions in Kindelia is that they have state. All contracts must return a Kindelia.IO.done
value, and so is the case of MyCounter
, that is one of the restrictions on how we must interface with contracts. Here this is done by the return
statement, and the return type of the function Kindelia.IO
matches it.
This is the bulk of our contract, but we haven't given it a way of reading the state of the contract, we can only interact with it by incrementing the counter. Maybe this is what you want, in this case our finished contract could be:
MyCounter : Kindelia.IO Unit
MyCounter =
do Kindelia.IO {
ask old_val = Kindelia.IO.take
let new_val = U120.inc old_val
ask Kindelia.IO.save new_val
return Unit.new
}
Most times we want a way of doing many types of operations in the same contract. We can do this by taking different paths by using pattern-matching on the argument. We could add a second action for reading the counter without changing it like so:
CounterAct : Type
CounterAct.read : CounterAct
CounterAct.inc : CounterAct
MyCounter (action: CounterAct): Kindelia.IO U120
MyCounter CounterAct.inc =
do Kindelia.IO {
ask old_val = Kindelia.IO.take
let new_val = U120.inc old_val
ask Kindelia.IO.save new_val
return 0u120
}
MyCounter CounterAct.read =
do Kindelia.IO {
ask counter = Kindelia.IO.load
return counter
}
Now we have a more complete example of a smart contract. By using an enumeration type we can define different actions for a contract, as if it were made up of different functions. Another restriction of the contract interface is that it must have at most one argument, and this argument must be a constructor that only encapsulates other numbers. In our case, we have two constructors encapsulating zero numbers, so it meets this criteria.
Some other things of note here:
- The
Kindelia.IO.load
function is similar toKindelia.IO.take
, except it takes the state, gives you one copy and stores back another copy. - All rules must return the same type of value. In this example, we return a
0
in the incrementing branch that can be safely discarded by whoever called the contract. In more complicated cases we can devise types that handle more complex returns. - We are using
U120
for our numbers. This is the native type of numbers in kindelia and is usually the type of value that can use most efficiently time (mana) and space (bits) on the Kindelia network. We can use it to make serializations, buffers, etc.
Our example is almost done, but we never defined the initial value of our counter. By default, all functions are created with state 0u120
, but we usually want to set it to some more useful initial value. In this example we actually want it to be zero, but we should go ahead and make it explicit for better maintainability. We can do this with a function attribute, a special annotation that tells something important to the compiler. In our case, we want to tell it that one Kind function is actually the initial value for the state of our Kindelia contract. This attribute is called kdl_state
and we use it like this:
#kdl_state = MyCounter.state
MyCounter (action: CounterAct): Kindelia.IO U120
... // Omitted for ease of reading, but the inside is the same as above
MyCounter.state : U120 {
0u120
}
Instead of compiling MyCounter.state
to a Kindelia function, it instead becoes the initial state of MyCounter
, setting it to 0u120
.
There are some other attributes that you might want to use for your programs, the most important ones are:
kdl_state
: Defines that another function is the initial state of the annotated one. Used like#kdl_state = <Function name>
.kdl_name
: Defines the name that the function will be compiled with on the Kindelia network. This is very useful to meet the name length restriction of Kindelia names, they must have at most 12 characters. If we don't give a function akdl_name
and it exceeds 12 characters, it's name will be swapped by the first 72bits of the hash of the original name. This is pretty unreadable, so if we want to be able to understand the generated kindelia code, we probably want to give it a custom name. Kindelia names also don't need to start with a capital letter like Kind functions, so we can have more name options. Used like#kdl_name = <Name it'll be compiled with>
.kdl_erase
: Usually, the compiler is smart enough to understand what it should compile, and what should be left out. Sometimes, it unfortunately fail. In these cases, we can mark a function as kindelia erased, meaning it won't ever be compiled. Be careful, the use case for this is very rare and you should only have to resort to it in case you're having actual problems. In the future, as the compiler gets more intelligent this attribute will be needed in less cases. Used like#kdl_erase
.inline
: By marking a function as inlined, any time it is used, the function application will be directly substituted by the body on compile time. This can be a nice optimization in some cases or can also make a function less efficient in others. Used like#inline
.
Attributes are used by putting them just before the function that they're applied to, like in the example above.
Putting everything together, our whole correct and compilable example is:
CounterAct : Type
#kdl_name = cntActionRd
CounterAct.read : CounterAct
#kdl_name = cntActionInc
CounterAct.inc : CounterAct
#kdl_state = MyCounter.state
#kdl_name = cntExample
MyCounter (action: CounterAct): Kindelia.IO U120
MyCounter CounterAct.inc =
do Kindelia.IO {
ask old_val = Kindelia.IO.take
let new_val = U120.inc old_val
ask Kindelia.IO.save new_val
return 0u120
}
MyCounter CounterAct.read =
do Kindelia.IO {
ask counter = Kindelia.IO.load
return counter
}
MyCounter.state : U120 {
0u120
}
Now that we have written a simple program (and type-checked it, hopefully also proved its most important properties), we want to put it on the blockchain. The first step is to compile our Kind code to Kindelia code. This is done with the kind2 to-kdl
command.
If our program was saved in the file MyCounter.kind2
we compile it like kind2 to-kdl MyCounter.kind2 > mycounter.kdl
. This compiles the code and saves it to mycounter.kdl
, which is a file that can be sent to a Kindelia node to be included in the network.
Although we checked our Kind program before, it could still have some problems, especially in details that are specific to the Kindelia compilation of our program. For example, there could already be a different function with the same name on the blockchain, or maybe we didn't fulfill all the requirements of the smart contract interface. We can run some tests to try and catch some of the problems before doing an actual deploy, which could be expensive and cannot be undone.
The kindelia
application provides some tooling that allows us to test our newly created functions.
TODO: run
statements.
First off, we can make a local and offline (without taking the blockchain state into consideration) test with the kindelia test
command. This executes your code, giving some important stats like how much size and mana it costs to run your file. It can also detect some simple errors like invalid syntax (if you for example made an incorrect manual edit to the compiled file). We can use it like kindelia test mycounter.kdl
.
We can check that our program actually fits in a block, which has a size limit of about 1300 bytes, by using one of the kindelia check
commands like so: kindelia check mycounter.kdl publish
. This will tell us how many bytes each of our functions takes and we can calculate how many blocks are needed to include all of them in the blockchain.
With just these two, it would be hard to catch any actual runtime errors, like bugs in the logic of our code or wrong assumptions we made about the problem we're trying to solve. But it is possible to add an extra element to run test code for the Kindelia network just like we would for normal computer programs we can write run
blocks that work as a test suite for the contract we're planning on deploying. With this, it is possible to simulate interactions with contracts and check out what happens after some specific series of contract invocations.
For example, look at this example that tests the cntExample
contract we developed before:
run {
ask (Call 'cntExample' {cntActionInc});
ask (Call 'cntExample' {cntActionInc});
ask (Call 'cntExample' {cntActionInc});
ask res = (Call 'cntExample' {cntActionRd});
let pass = (== res #3);
(Done pass)
}
What it does is increment the counter 3 times and then check if we actually get a 3 back. We just need to put this run
block at the end of mycounter.kdl
and then test it with kindelia test
.
These tests can be as long and complicated as we need them to be. However, if a run
block gets too big, it is no longer deployable and will fail the size check. During tests only we can ignore this restriction by calling kindelia test mycounter.kdl --sudo
.
Online tests, meaning it'll run with the current network state of a node and not with only the genesis block like was the case for kindelia test
, are also possible. To make a dry-run of a file, executing on top of the current network state, we can use the kindelia run-remote
command.
Since kindelia test
doesn't do the runtime checks and doesn't apply all restrictions, running kindelia run-remote
on a local node disconnected from the network, running at the genesis state, may actually give us more errors.
To start a node disconnected from the network:
- From a clean install of kindelia, run
kindelia node start
. Notice that it will display a message about creating a configuration file; - Stop the node and open this configuration file;
- Erase all values inside
initial_peers
. Keep a copy if you want to connect to network in the future. - Restart the node with
kindelia node start
. Notice that in the heartbeat messages, thepeers
field will have 0 other nodes. - Then, we can send our local node our file to dry-run with
kindelia run-remote mycounter.kdl
.
But probably it is more useful to run our test on the current state of the network. To connect a local node to the testnet, the original default values of the config files should work. Just leave the local node running until the number of pending
blocks go to zero. If we run the run-remote
command now, it'll be executed on the latest state of the network.
Just like with kindelia test
, we can use run-remote
to run some test scripts. But now we can test already running programs, interacting with their state in real time.
Most users don't want to run a node in their own machine, since it requires a lot of RAM and storage space. For most people, what they'll actually want to do is send an external node a file to be checked. As the name run-remote
indicates, this is exactly what this command was made to do. To point at a non-local node, we can either set the environment variable KINDELIA_API_URL
like so:
KINDELIA_API_URL=https://node-ams3.testnet.kindelia.org kindelia run-remote mycounter.kdl
Or we can pass the api url as an option to the kindelia
command, like this:
kindelia --api "https://node-ams3.testnet.kindelia.org" run-remote mycounter.kdl
The env var / api option applies to any command that communicates with a kindelia node.
Once you have you .kdl
file well defined, tested and checked, sending it to a node to be published is very simple. We simply call kindelia publish mycounter.kdl
and check that all transactions are published. If our file ran correctly using run-remote
, there should be no problems when publishing it.
The same considerations about publishing to local or remote node apply here as well.
Right now, the node doesn't notify us at the moment our transactions are included in the blockchain, so we need to keep checking until everything is in. As the web api for the node gets refined, these things will be made simpler.
So far, we successfully published a smart contract on the Kindelia chain. Now, we or our users will probably want to interact with our program on the network. Just like we did for our test, the way to interact with the network state is to write run
statements that Call
the desired contracts. We could, for example use the cntExample
contract to act as a visitor count for our website, publishing a transaction with a single !Call 'cntExample' {cntActionInc}
inside of a run
every time a new IP connects to a web server. We can be sure that this information will never be lost because it written immutably on the Kindelia blockchain.
The code for this transaction could be:
run {
ask (Call 'cntExample' {cntActionInc});
(Done #0)
}
And we could run kindelia --api "https://node-sfo3.testnet.kindelia.org" run-remote inc_counter.kdl
in the server to execute this action. Unlike functions that can only be added once, we can run the same run
statement as many times as we want, we just have to keep publishing it many times.
Sometimes, we may need to sign some of the transactions we want to publish. Signing a transaction puts the signer as the subject, and this can be required to execute certain programs with your account or to deploy functions inside of a namespace. This guide won't be covering how to do either of these things as it would be too long, but it is sufficient to say that is important to know how to sign transactions.
We could for example only allow certain whitelisted subjects to be able to increase our counter. For them to be able to correctly interact with the contract, they would need to sign the run
statement that does the incrementing.
This is done with the kindelia sign
command. To use it, you first need a private key with which you'll be doing the signing. This private key acts as your account number, and any transactions signed with this key "belong" to you.
As the name private implies, you should make it a secret from anyone else and store it securely. If anyone were to have access to your key they could steal your identity on the Kindelia network and cause all sorts of havoc, like stealing all of your tokens.
The first step then is to create this private key. Keys are 256bit long and must be stored as a hexadecimal string of 64 ascii characters in a text file. Let's say for example that you generated a secure random number 0xD94245287EDE9302C884C8B256A5C87507D7E442FF300F975947669549B255AD
. You need to store this number in a secret file, say my_secret_key.txt
with content D94245287EDE9302C884C8B256A5C87507D7E442FF300F975947669549B255AD
.
Then you can use this file to sign your kindelia code using the command kindelia sign inc_counter.kdl --secret_file "my_secret_key.txt" > inc_counter_signed.kdl
, which will output the signed statement on file inc_counter_signed.kdl
.
Now you just need to publish this new signed file like we did for the previous ones.
To sum up everything that was said here, to make contracts for the Kindelia network you should:
-
Make a program in Kind, using the
Kindelia.IO
functions to access kindelia specific functionalities like accessing the state, calling other contracts and getting block metadata. -
Annotate your functions with compiler directives
kdl_name
,kdl_state
,kdl_run
andkdl_erase
to indicate kindelia-only aspects of these functions. -
Typecheck your program with
kind2 check
, write proofs about it and, if everything is correct, compile it withkind2 to-kdl
. -
Optionally, sign your statements with the
kindelia sign
command. This may be required or not depending on what you're doing. -
Do an offline test of your contract using the
kindelia test
command. You may want to include a testing script in thekdl
file that can be compiled by writing a function annotated withkdl_run
. -
Do an online test of your contract by using the
kindelia run-remote
command. -
Send your statements to a node connected to the network using the
kindelia publish
command and wait for them to be included in the blockchain. -
Write a script for interacting with your new contract. Repeat the instructions above for compiling, testing and deploying it.