1.2 Hello World: your first confidential contract

Basic understanding of Rust language programming and smart contract development knowledge is necessary to follow this tutorial.

Overview

In this tutorial, we are going to continue on the development environment we have set up in the previous chapter, and explore how a confidential smart contract is made. By the end of this tutorial, you will:

  • Learn how to develop a confidential contract
  • Interact with the contract from the Web UI
  • Build your own confidential contract

For a high-level overview of Phala Network, please check the previous chapters.

Environment and Build

Please set up a development environment by following the previous chapter Run a Local Development Network. Make sure you are at the helloworld branch on both phala-blockchain and apps-ng repo.

Walk-through

Contract

The HelloWorld contract commit is available at here.

HelloWorld contract stores a counter which can be incremented by anyone, but only authorized user can read it. The typical model of the confidential contracts in Phala Network is consisted of the following three components which we will discuss in detail.

  • States
  • Commands
  • Queries

The States of a contract is described by certain variables. In this case, we define a 32-bit unsigned variable as the counter, while you are free to use variables of any types in your contracts.

1pub struct HelloWorld {
2    counter: u32,
3}

There are two kinds of operations which can be used to interact with confidential contracts: Commands and Queries. The most significant difference between them is whether or not they change the states of the contracts, and we explain them separately.

The Commands are supposed to change the states of contracts. They are also called Transactions, and they are just like the transactions on traditional smart contract blockchains like Ethereum: they must be sent to the blockchain first before their executions. In our HelloWorld contract, we define a Increment command which changes the value of counter.

1pub enum Command {
2    /// Increments the counter in the contract by some number
3    Increment {
4        value: u32,
5    },
6}

It is worth noting that you can define more than one commands for a contract. For example, we can add a Decrement command to decrease the counter as follow.

 1pub enum Command {
 2    /// Increments the counter in the contract by some number
 3    Increment {
 4        value: u32,
 5    },
 6    /// Decrements the counter in the contract by some number
 7    Decrement {
 8        value: u32,
 9    },
10}

All the commands are processed by the handle_command method which must be implemented. In this case, we allow any user to use this command, so we just increase the counter without checking the _origin.

 1fn handle_command(&mut self, _origin: &chain::AccountId, _txref: &TxRef, cmd: Command) -> TransactionStatus {
 2    match cmd {
 3        // Handle the `Increment` command with one parameter
 4        Command::Increment { value } => {
 5            // Simply increment the counter by some value.
 6            self.counter += value;
 7            // Returns TransactionStatus::Ok to indicate a successful transaction
 8            TransactionStatus::Ok
 9        },
10    }
11}

Opposed to commands, Queries shall not change the states of contacts. Queries are one of the innovations of Phala Network. They are designed to allow a quick examination of the states of contracts. To define a query, you need to define both the Request and the according Response.

 1pub enum Request {
 2    /// Ask for the value of the counter
 3    GetCount,
 4}
 5
 6/// Query responses.
 7pub enum Response {
 8    /// Returns the value of the counter
 9    GetCount {
10        count: u32,
11    },
12    /// Something wrong happened
13    Error(Error)
14}

handle_query method is supposed to handle all the queries. Unlike commands, Queries go directly to the contracts without the necessity to be sent to the blockchain. In confidential contracts queries are usually required to be signed to indicate the identities of the requesters. Therefore queries can be responded conditionally, which gives the developer great flexible control over the data in confidential contracts. The identity of the requester can be accessed from origin, the second argument of handle_query.

We are going to cover more about origin later in this tutorial, but it also supports anonymous queries where origin is None as shown below. The GetCount query simply returns the current value of the counter.

 1fn handle_query(&mut self, _origin: Option<&chain::AccountId>, req: Request) -> Response {
 2    let inner = || -> Result<Response, Error> {
 3        match req {
 4            // Handle the `GetCount` request.
 5            Request::GetCount => {
 6                // Respond with the counter in the contract states.
 7                Ok(Response::GetCount { count: self.counter })
 8            },
 9        }
10    };
11    match inner() {
12        Err(error) => Response::Error(error),
13        Ok(resp) => resp
14    }
15}

Unlike Ethereum, queries in confidential contracts are capable to carry arbitrary computation. So we recommend to introduce an authority check here to avoid potential Denial-of-Service attack with a huge amount of query requests.

Frontend

Interact with the contract: how to send command and queries.

Implement a secret notebook

After a general understanding of the model of confidential contracts, let's make something practical and implement a contract which can store the secret note of each visitor. In this contract, we allow any user to store one note, and only the user himself is allowed to read his note.

The SecretNote contract commit is available at https://github.com/Phala-Network/phala-blockchain/commit/d91f94c9ed21290b7353991899f7a6da18cfab61 (CHANGE THIS). We thank Laurent for his implementation of this contract.

Contract

We use a map to store the users with their notes, and provide two interface SetNote and GetNote for them to operate their notes. We frist define the contract state structure struct SecretNote, with a map notes to store a mapping from the account to the notes. In Phala contracts, an account can be represented by an AccountIdWrapper.

1pub struct SecretNote {
2    notes: BTreeMap<AccountIdWrapper, String>,
3}

In Rust's std collection library, there are two map implementations: HashMap and BTreeMap. Since our AccountIdWrapper does not derive Hash needed by HashMap, we use BTreeMap to store the mapping between user accounts and their notes.

Here we recall the difference between commands and queries. In SecretNote, SetNote changes the states, so it is a command, and GetNote is a query. For each user, we only keep the latest note. So in SetNote, we call the insert to add a note if no previous one exists or directly overwrite the existing one.

 1pub enum Command {
 2    /// Set the note for current user
 3    SetNote {
 4        note: String,
 5    },
 6}
 7
 8impl contracts::Contract<Command, Request, Response> for SecretNote {
 9    fn handle_command(&mut self, origin: &chain::AccountId, _txref: &TxRef, cmd: Command) -> TransactionStatus {
10        match cmd {
11            // Handle the `SetNote` command with one parameter
12            Command::SetNote { note } => {
13                // Simply increment the counter by some value
14                let current_user = AccountIdWrapper(origin.clone());
15                // Insert the note, we only keep the latest note
16                self.notes.insert(current_user, note);
17                // Returns TransactionStatus::Ok to indicate a successful transaction
18                TransactionStatus::Ok
19            },
20        }
21    }
22}

Now we can move to the GetNote handler. It's a little tricky since we only allow the owner of the note to access his note. In other words, we need to ensure that the user has signed the query, and then we respond with his note. For a signed query, the origin argument in handle_query method contains the account id of requester.

We implement handle_query as shown below. It first checks if the query is signed by checking origin, and then returns the note stored in the contract states. It returns an NotAuthorized response if the query is not signed.

 1/// Queries are not supposed to write to the contract states.
 2#[derive(Serialize, Deserialize, Debug, Clone)]
 3pub enum Request {
 4    /// Read the note for current user
 5    GetNote,
 6}
 7
 8/// Query responses.
 9#[derive(Serialize, Deserialize, Debug)]
10pub enum Response {
11    /// Return the note for current user
12    GetNote {
13        note: String,
14    },
15    /// Something wrong happened
16    Error(Error)
17}
18
19impl contracts::Contract<Command, Request, Response> for SecretNote {
20
21    fn handle_query(&mut self, origin: Option<&chain::AccountId>, req: Request) -> Response {
22        let inner = || -> Result<Response, Error> {
23            match req {
24                // Handle the `GetNote` request
25                Request::GetNote => {
26                    // Unwrap the current user account
27                    if let Some(account) = origin {
28                        let current_user = AccountIdWrapper(account.clone());
29                        if self.notes.contains_key(&current_user) {
30                            // Respond with the note in the notes
31                            let note = self.notes.get(&current_user);
32                            return Ok(Response::GetNote { note: note.unwrap().clone() })
33                        }
34                    }
35
36                    // Respond NotAuthorized when no account is specified
37                    Err(Error::NotAuthorized)
38                },
39            }
40        };
41        match inner() {
42            Err(error) => Response::Error(error),
43            Ok(resp) => resp
44        }
45    }
46}

Frontend

  1. Set note UI
  2. Query UI
  3. Handle error

Put everything together

  1use serde::{Serialize, Deserialize};
  2
  3use crate::contracts;
  4use crate::types::TxRef;
  5use crate::TransactionStatus;
  6use crate::contracts::AccountIdWrapper;
  7
  8use crate::std::collections::BTreeMap;
  9use crate::std::string::String;
 10
 11/// SecretNote contract states.
 12#[derive(Serialize, Deserialize, Debug, Default)]
 13pub struct SecretNote {
 14    notes: BTreeMap<AccountIdWrapper, String>,
 15}
 16
 17/// The commands that the contract accepts from the blockchain. Also called transactions.
 18/// Commands are supposed to update the states of the contract.
 19#[derive(Serialize, Deserialize, Debug)]
 20pub enum Command {
 21    /// Set the note for current user
 22    SetNote {
 23        note: String,
 24    },
 25}
 26
 27/// The errors that the contract could throw for some queries
 28#[derive(Serialize, Deserialize, Debug)]
 29pub enum Error {
 30    NotAuthorized,
 31}
 32
 33/// Query requests. The end users can only query the contract states by sending requests.
 34/// Queries are not supposed to write to the contract states.
 35#[derive(Serialize, Deserialize, Debug, Clone)]
 36pub enum Request {
 37    /// Read the note for current user
 38    GetNote,
 39}
 40
 41/// Query responses.
 42#[derive(Serialize, Deserialize, Debug)]
 43pub enum Response {
 44    /// Return the note for current user
 45    GetNote {
 46        note: String,
 47    },
 48    /// Something wrong happened
 49    Error(Error)
 50}
 51
 52
 53impl SecretNote {
 54    /// Initializes the contract
 55    pub fn new() -> Self {
 56        Default::default()
 57    }
 58}
 59
 60impl contracts::Contract<Command, Request, Response> for SecretNote {
 61    // Returns the contract id
 62    fn id(&self) -> contracts::ContractId { contracts::SECRET_NOTE }
 63
 64    // Handles the commands from transactions on the blockchain. This method doesn't respond.
 65    fn handle_command(&mut self, origin: &chain::AccountId, _txref: &TxRef, cmd: Command) -> TransactionStatus {
 66        match cmd {
 67            // Handle the `SetNote` command with one parameter
 68            Command::SetNote { note } => {
 69                // Simply increment the counter by some value
 70                let current_user = AccountIdWrapper(origin.clone());
 71                // Insert the note, we only keep the latest note
 72                self.notes.insert(current_user, note);
 73                // Returns TransactionStatus::Ok to indicate a successful transaction
 74                TransactionStatus::Ok
 75            },
 76        }
 77    }
 78
 79    // Handles a direct query and responds to the query. It shouldn't modify the contract states.
 80    fn handle_query(&mut self, origin: Option<&chain::AccountId>, req: Request) -> Response {
 81        let inner = || -> Result<Response, Error> {
 82            match req {
 83                // Handle the `GetNote` request
 84                Request::GetNote => {
 85                    // Unwrap the current user account
 86                    if let Some(account) = origin {
 87                        let current_user = AccountIdWrapper(account.clone());
 88                        if self.notes.contains_key(&current_user) {
 89                            // Respond with the note in the notes
 90                            let note = self.notes.get(&current_user);
 91                            return Ok(Response::GetNote { note: note.unwrap().clone() })
 92                        }
 93                    }
 94
 95                    // Respond NotAuthorized when no account is specified
 96                    Err(Error::NotAuthorized)
 97                },
 98            }
 99        };
100        match inner() {
101            Err(error) => Response::Error(error),
102            Ok(resp) => resp
103        }
104    }
105}

Summary

In this tutorial, we have covered a walk-through for the HelloWorld contract template, and demostrate how we can build a bit more advanced contract SecretNote that leverages the confidentiality of Phala contract. Now it's your turn to build something new!

Submit your work

This tutorial is a part of Polkadot "Hello World" virtual hackathon challenge at gitcoin.co. In order to win the task, please do the followings:

  1. Fork the core blockchain and the Web UI repo (helloworld branch) into your own GitHub account
  2. Develop your own contract on the templates at “helloworld” branch (must be a different one from existing submissions)
  3. Launch your full development stack and take screenshots of your dapps
  4. Push your work to your forked repos. They must be open source
  5. Make a tweet with the link to your repos, the screenshots, and describe what you are building on Twitter
  6. Join our Discord server and submit the the link to your tweet