November 10, 2022

By Stringhandler and CjS77

A First Look at Tari Contracts

The development effort this quarter is largely focussed on preparing the base layer code for an audit ahead of a possible mainnet release TBD. Work on the contract layer has not stopped though, and we’re quite excited to share the progress that has been made on that front.

Things are still very much in their infancy, so while these developments are pretty exciting, there’s still a long road for these prototypes to travel before being production ready.

With that caveat in place, we’re going to walk though

  • Writing a dead-simple contract template in Rust.
  • Compiling the template to WASM and registering it on the Tari base layer.
  • Running a small network of 2 validator nodes that:
    • process an instruction to instantiate a copy of the template into a contract.
    • reach consensus on a valid instruction.
    • queries the network for the updated state.

If you’re up for a bit of elbow grease, you can run the following exercise yourself using the code in DAN repo.

The contract template

As the seasoned reader of this blog will know, it has always been Tari’s goal to improve the overall safety of the ecosystem by allowing users to draw from a library of well-tested, reliable, performance contract templates, rather than having to write their own contracts from scratch.

These templates are written in Rust and, today, look something like this:

use tari_template_macros::template;

#[template]
mod counter {
    pub struct Counter {
        value: u32,  // Contract state
    }

    impl Counter {
        // Method to create a new contract instance
        pub fn new() -> Self {
            Self { value: 0 }
        }

        // A read-only method
        pub fn value(&self) -> u32 {
            self.value
        }

        // A method that updates the contract state
        pub fn increase(&mut self) {
            self.value += 1;
        }
    }
}

This template doesn’t do much. It simply keeps track of a single number, and allows one to increment it. Like we said, it’s early days.

The good news is that it’s very clear what this template does. There’s almost no boilerplate here, which is a key design principle of the DAN template development experience: keeping things nice and clean.

Compiling the template to WASM

Isolated code like the snippet above is nowhere near sufficient to work effectively in a decentralised, permissionless smart-contract network. There is a huge amount of infrastructure and glue code that must be packed around the template code above to let it slot seamlessly into the DAN.

Suffice to say, the innocuous little
#template annotation is doing a huge amount of lifting behind the scenes. Not to mention the huge amount of work that @sdbondi and @mrnaveira have put into making it look so sweet and innocent.

We must also tip our hats to the Rust web-assembly working group for their amazing work in making Rust and WASM work seamlessly together.

In future the process will be more streamlined and seamless, for now, it’s a couple of manual steps (early days!):

First we generate the WASM project and stuff all our boilerplate in the right spots using cargo-generate:

$ cargo generate https://github.com/tari-project/wasm-template.git counter

 Favorite `https://github.com/tari-project/wasm-template.git` not found in config, using it as a git repository: https://github.com/tari-project/wasm-template.git

 Project Name : CounterDemo

Renaming project called `CounterDemo` to `counter-demo`...
 Destination: C:\projects\counter-demo ...
 Generating template ...
[ 1/12]   Done: .cargo\config
[ 2/12]   Done: .cargo
[ 3/12]   Done: .gitignore
[ 4/12]   Done: Cargo.toml
[ 5/12]   Done: package\Cargo.toml
[ 6/12]   Done: package\src\lib.rs
[ 7/12]   Done: package\src
[ 8/12]   Done: package
[ 9/12]   Done: tests\Cargo.toml
[10/12]   Done: tests\tests\test.rs
[11/12]   Done: tests\tests
[12/12]   Done: tests
 Moving generated files into: `C:\projects\counter-demo`...
 Initializing a fresh Git repository
 Done! New project created C:\projects\counter-demo

If you pry into the generated code, you’ll find our contract template in package\src\lib.rs. Everything else is the tooling, glue code and infrastructure needed to be able able to plug this contract into the DAN.

You can run the built-in unit tests to make sure everything is working fine up until this point.

$ cargo test

.....
    Finished test [unoptimized + debuginfo] target(s) in 16.38s
     Running tests\test.rs (target\debug\deps\test-10fba866873c5eef.exe)

running 1 test
test test::test_increment ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 5.56s

Now we can build the WASM binary.

cd package
cargo build-wasm

We cheated a bit here. The build-wasm command is an alias defined in .cargo/config.toml”. You can also specify cargo build --target=wasm32-unknown-unknown

[alias]
build-wasm = "build --target=wasm32-unknown-unknown"

All of these details will be neatly packaged and abstracted away from developers in due time.

Deploying the contract

Now we get to register the template on the blockchain. As per RFC-0303, templates are registered on the base layer so that all nodes have a global index of the templates that are available.

$ cargo run --bin tari_validator_node_cli -- -b . templates publish -p c:\projects\counter-demo\package

Template code path c:\projects\temp2\counter-demo\package
⏳️ Compiling template...
✅ Template compilation successful (WASM file size: 95435 bytes)

Choose an user-friendly name for the template (max 32 characters):
> counterdemo 

Template version: (Default: 0)
> 0                           

Compiled template WASM file location: c:\projects\counter-demo\package\target/wasm32-unknown-unknown/release/package.wasm
Please upload the file to a public web location and then paste the URL                                                         
WASM public URL (max 255 characters):                                                                                          
> http://localhost:8000/counter_demo.wasm                                                                                      

✅ Template registration submitted

The template address will be 4addd7d36790130fa2ffcfc52551e5018e91dd87050ce59d2f93a6b212f6542c

What happened here was:

  • The template code was hashed along with some metadata, to obtain the unique, permanent id of the template.
  • The validator node connected with a Tari wallet to construct a template registration transaction.
  • Note that the code itself (95kB) is not stored on-chain; just the hash, so we need a method of fetching the actual code when we need it. This is why we need to provide a URL. A URL to a local HTTP server was given in this demo. In general, it will be an IPFS URI or something similar until someone builds their own better version on Tari :).

Once the transaction is broadcast, it shows up like any other transaction in the Tari mempool:

┌Tari Console Wallet────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Transactions │ Send │ Receive │ Contacts │ Network │ Events │ Log │ Notifications(24)                                             │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌Balance────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│Available: 288018.943629 T (Time Locked: 16615.757352 T)         Pending Incoming: 22145.448812 T Pending Outgoing: 5529.713495 T  │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
 Up↑/Down↓ select Tx (C) cancel selected pending Txs (A) show/hide mining (R) rebroadcast Txs (Esc) exit list                     
┌Completed (T)ransactions (69) ─────────────────────────────────────────────────────────────────────────────────────────────────────┐
│   Source/Destination Public Key                                    Amount/Token      Local Date/Time     Status                   │
│ 5eeee5ce53af80048b6e5d994aa52c639c79b8c503094d26dfe039fa20fe514a   0 µT              2022-11-10 15:35:24 Broadcast                │
│ 5eeee5ce53af80048b6e5d994aa52c639c79b8c503094d26dfe039fa20fe514a   5538.576052 T     2022-11-10 15:13:27 Mined Unconfirmed        │
│ 5eeee5ce53af80048b6e5d994aa52c639c79b8c503094d26dfe039fa20fe514a   5538.578485 T     2022-11-10 15:13:16 Mined Unconfirmed        │
│ 5eeee5ce53af80048b6e5d994aa52c639c79b8c503094d26dfe039fa20fe514a   5538.580918 T     2022-11-10 15:13:09 Mined Unconfirmed        │
│ 5eeee5ce53af80048b6e5d994aa52c639c79b8c503094d26dfe039fa20fe514a   5538.583351 T     2022-11-09 13:32:10 Mined Confirmed          │
│ 5eeee5ce53af80048b6e5d994aa52c639c79b8c503094d26dfe039fa20fe514a   5538.585784 T     2022-11-09 13:31:34 Mined Confirmed          │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
 Base Node Status  -  Chain Tip: #111  Synced.  Latency 1659 ms                   Connected Base Node ID: e62a9cee9aa5532a399828151f 
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 Network: igor             Version: 0.38.7           LeftArrow: Previous Tab  Tab/RightArrow: Next Tab              F10/Ctrl-Q: Quit 

For now, the value is zero, meaning that it’s free to register a template. This might change in future to prevent spam template registrations.

After the transaction is mined, it gets recognised as a contract template:

# In the validator node logs:

16:15 [tari::validator_node::base_layer_scanner] INFO  ⛓️ Scanning base layer block 112 of 112
16:15 [tari::validator_node::base_layer_scanner] INFO  🌠 new template found with address 4addd7d36790130fa2ffcfc52551e5018e91dd87050ce59d2f93a6b212f6542c at height 112
16:15 [tari::validator_node::template_manager] INFO  ⏳️️ Template 4addd7d36790130fa2ffcfc52551e5018e91dd87050ce59d2f93a6b212f6542c queued for download
16:15 [reqwest::connect] DEBUG starting new connection: http://localhost:8000/
16:16 [tari::validator_node::template_manager] INFO  ✅ Template 4addd7d36790130fa2ffcfc52551e5018e91dd87050ce59d2f93a6b212f6542c downloaded successfully
16:16 [tari::validator_node::template_manager] INFO  ✅ Template 4addd7d36790130fa2ffcfc52551e5018e91dd87050ce59d2f93a6b212f6542c has status Active

The blockchain is just a slow-ass database, so surely we can “query” for all the templates that have been mined so far?

$ cargo run --bin tari_validator_node_cli -- -b . templates list                                             

Templates:
Address                                             | Download Url                            | Mined Height | Status
--------------------------------------------------- | ----------------------------------------| ------------ | ------
deb2a34c31891acac1e6...c48be6c4f138e1d1a3e6971da6b0 | ...                                     | 39           | Active
201ee40f24559f43d0bc...73b2fcd4616436c4bd35d948323a | ...                                     | 61           | Active
7ece3f243425916e47ad...df3b44e532c6e4e662123c3c8a83 | ...                                     | 73           | Active
4addd7d36790130fa2ff...dd87050ce59d2f93a6b212f6542c | http://localhost:8000/counter_demo.wasm | 112          | Active

4 row(s)

🤩

That’s not all! Remember all the heavy lifting that the #[template] annotation did? Of course you don’t, it all happened behind the scenes. But let’s feast on the fruits of its labour now by inspecting the template’s Application Binary Interface (ABI), or in other words, all the functions that the template exposes, using templates get <template_address>:

$ cargo run --bin tari_validator_node_cli -- -b . templates get 4addd7d36790130fa2ffcfc52551e5018e91dd87050ce59d2f93a6b212f6542c

Template 4addd7d36790130fa2ffcfc52551e5018e91dd87050ce59d2f93a6b212f6542c | Mined at 112

Function          | Args | Returns
----------------- | ---- | -------
Counter::new      |      | U32
Counter::value    | U32  | U32
Counter::increase | U32  | Unit

🔥

Instantiating a contract

Now to create an instance of the contract on the DAN.

We call Counter::new for that. This can be called many times. Each time, a new instance of Counter is created, each managing its own state.

If we look into the future a bit, you can see the benefit of the template-based approach here. A hypothetical NFT template could be used by anyone wanting to launch their own token series. This could even be set up in an environment where no coding is required; just fill in a few fields, link to your assets, connect to your wallet and click “Deploy”.

But you know, early days.

Right now we still have to type in this monstrosity:


tari_validator_node_cli.exe -b . transactions submit -w -n 1 call-function 4addd7d36790130fa2ffcfc52551e5018e91dd87050ce59d2f93a6
b212f6542c new

✅ Transaction f31c90a8ae1f79473ef84e3c8eaeb03e831e6d034b9b7fac33a86166c5e01243 submitted.
⏳️ Waiting for transaction result...                                                     

✅️ Transaction finalized

Epoch: 11
Signed by: 2 validator nodes

========= Substates =========
️🌲 UP substate 391d01650715440c3b652a6e12919f3e9558d8727c1fdca51743a8b2ea4f591f (v0)
       ▶ component (Counter): 391d01650715440c3b652a6e12919f3e9558d8727c1fdca51743a8b2ea4f591f

========= Pledges =========
Shard:391d01650715440c3b652a6e12919f3e9558d8727c1fdca51743a8b2ea4f591f Pledge:DoesNotExist

========= Return Values =========
u32: 1694571833

========= LOGS =========
1668094546 DEBUG Dispatcher called with function new

OVERALL DECISION: Accept

🥰

A lot just happened there, so let’s unpack it.

Firstly, the instruction, “Create a new counter” is hashed and signed and passed to the Json-RPC API of a validator node.

(To avoid confusion, we refer to smart contract transactions and invocations handled by the DAN as instructions, while we call base layer transactions, transactions).

There are no inputs to this instruction, and we expect a single output, the Counter instance.

The consensus layer checks with the VM that this is the expected result, and gets the list of shard addresses for the output. In this case, it gets the single shard, 391d...591f.

The Hotstuff consensus algorithm then runs over the 2 nodes in the network (logs omitted for brevity) to determine whether they agree on this output.

They do. Specifically, at least two-thirds plus one do, and the instruction is ACCEPTED.

The other output is related to some technical aspects of the Cerberus consensus algorithm.

Invoking contract methods

Now that we have a live contract on our 2-node DAN (also the name of my punk rock band!), we can do things with it.

Let’s call the value method to get the current value of the counter. This requires a lot of typing and copy-pasting but early days and all that.

tari_validator_node_cli.exe -b . transactions submit -w -n 0 call-method \
  4addd7d36790130fa2ffcfc52551e5018e91dd87050ce59d2f93a6b212f6542c \ 
  391d01650715440c3b652a6e12919f3e9558d8727c1fdca51743a8b2ea4f591f value


✅ Transaction 53c55e04ee4e94e2bde425725188010755215c3b93a77ecf7c26d2da1520533b submitted.
⏳️ Waiting for transaction result...

✅️ Transaction finalized

Epoch: 11
Payload height: NodeHeight(2)
Signed by: 2 validator nodes

========= Substates =========
========= Pledges =========
Shard:391d01650715440c3b652a6e12919f3e9558d8727c1fdca51743a8b2ea4f591f Pledge:Up

========= Return Values =========
u32: 0

========= LOGS =========
1668095320 DEBUG Dispatcher called with function value

OVERALL DECISION: Accept

😏

Invoking the value method required knowing the template address (4addd..542c) and the instance address (391d0..591f).

The instance address is the shard address of the substate we created in the previous step.

The return value is what we’re really interested in, and unsurprisingly, is currently 0.

Let’s change that.


tari_validator_node_cli.exe -b . transactions submit -w -n 0 call-method \ 
4addd7d36790130fa2ffcfc52551e5018e91dd87050ce59d2f93a6b212f6542c \ 
391d01650715440c3b652a6e12919f3e9558d8727c1fdca51743a8b2ea4f591f increase

✅ Transaction ccbe0aad71cb89c584f965c241ed172a3edfb775959df3a54667fff90feab2e1 submitted.
⏳️ Waiting for transaction result...

✅️ Transaction finalized

Epoch: 11
Signed by: 2 validator nodes

========= Substates =========
️🌲 UP substate 391d01650715440c3b652a6e12919f3e9558d8727c1fdca51743a8b2ea4f591f (v1)
       ▶ component (Counter): 391d01650715440c3b652a6e12919f3e9558d8727c1fdca51743a8b2ea4f591f

🗑️ DOWN substate 391d01650715440c3b652a6e12919f3e9558d8727c1fdca51743a8b2ea4f591f

========= Pledges =========
Shard:391d01650715440c3b652a6e12919f3e9558d8727c1fdca51743a8b2ea4f591f Pledge:Up

========= Return Values =========

========= LOGS =========
1668095603 DEBUG Dispatcher called with function increase

OVERALL DECISION: Accept

😎

Again we got an accept result. In this case there was a Down substate and an Up substate. In the current proof of concept, the substate is being updated in place. This is because we’re still working on the content-addressing of our contracts but expect this to change in future.

Finally, let’s call Counter::value to see if there was a change…


tari_validator_node_cli.exe -b . transactions submit -w -n 0 call-method 4addd7d36790130fa2ffcfc52551e5018e91dd87050ce59d2f93a6b2
12f6542c 391d01650715440c3b652a6e12919f3e9558d8727c1fdca51743a8b2ea4f591f value

✅ Transaction 18161023956be0f62f5b81e18cb59ce3a7a760e75247f8f0f635fe817fd10e45 submitted.
⏳️ Waiting for transaction result...

✅️ Transaction finalized

Epoch: 11
Signed by: 2 validator nodes

========= Substates =========
========= Pledges =========
Shard:391d01650715440c3b652a6e12919f3e9558d8727c1fdca51743a8b2ea4f591f Pledge:Up

========= Return Values =========
u32: 1

========= LOGS =========
1668095804 DEBUG Dispatcher called with function value

OVERALL DECISION: Accept

And it returned 1. 🥳

tl;dr

If all of this seemed like a lot of effort to show that 0 + 1 = 1, let’s take a step back and highlight what we’ve accomplished:

  • Clean, ergonomic template writing with all the boilerplate neatly abstracted away.
  • Registration of a template on the Tari L1.
  • Compilation, packaging and execution of Tari templates inside the Tari WASM VM.
  • Running of a rudimentary Layer 2 network.
  • Instantiation of a contract on the Layer 2 network, with nodes reaching finality on its correctness.
  • Querying contract state, and reaching consensus on that state.
  • Updating contract state by submitting an instruction to the Layer 2.

That’s quite a lot. But there’s still an awful lot left to do.

  • Registering validator nodes on the base layer network.
  • Verifying that VNs are in the correct committees.
  • Robust leader selection.
  • Robust failure recovery.
  • DAN economics and VN fees.
  • Permissions and “access control” in contracts.
  • Template composability.
  • Seamless integration of all these steps.
  • Epoch management and transitions.
  • Indexing nodes.

This doesn’t even get into building great developer tooling, web tooling, the template “standard library” and of course, choosing better names for some of these things (there are only 2 hard problems in computer science, right?) 😅