1.3 Confidential Contract Examples

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 encode-hackathon-2021 branch of the phala-blockchain and the master branch of the js-sdk repo.

Walk-through of the Basic Confidential Contract

Contract

We have glanced at the main part of the contract in the previous chapter. The complete GuessNumber contract is available here.

GuessNumber contract stores a secret for any participants to guess, and only authorized users can read it. The typical model of the confidential contracts in Phala Network consists 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 secret, while you are free to use variables of any type in your contracts.

1type RandomNumber = u32;
2
3pub struct GuessNumber {
4    random_number: RandomNumber,
5}

There are two kinds of operations that can be used to interact with confidential contracts: Commands and Queries. The most significant difference between them is whether or not they can 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 GuessNumber contract, we define a NextRandom command which changes the value of the secret.

1pub enum GuessNumberCommand {
2    /// Refresh the random number
3    NextRandom,
4}

It is worth noting that you can define more than one command for a contract. For example, we can add a SetOwner command to set the owner of the contract and enable him/her to conduct privileged operations.

1pub enum GuessNumberCommand {
2    /// Refresh the random number
3    NextRandom,
4    /// Set the contract owner
5    SetOwner { owner: AccountId },
6}

All the commands are processed by the handle_command method which must be implemented. In this case, we only allow the contract owner and the pre-defined root account (i.e. ALICE) to change the secret, so we check the sender identity from origin.

 1fn handle_command(&mut self, context: &mut NativeContext, origin: MessageOrigin, cmd: Command) -> TransactionResult {
 2    // we want to limit the sender who can use the Commands to the pre-define root account
 3    let sender = match &origin {
 4        MessageOrigin::AccountId(account) => AccountId::from(*account.as_fixed_bytes()),
 5        _ => return Err(TransactionError::BadOrigin),
 6    };
 7    let alice = contracts::account_id_from_hex(ALICE)
 8        .expect("should not failed with valid address; qed.");
 9    match cmd {
10        Command::NextRandom => {
11            if sender != alice && sender != self.owner {
12                return Err(TransactionError::BadOrigin);
13            }
14            self.random_number = GuessNumber::gen_random_number(context);
15            Ok(())
16        }
17    }
18}

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.

 1/// The Queries to this contract
 2pub enum Request {
 3    /// Query the current owner of the contract
 4    QueryOwner,
 5    /// Make a guess on the number
 6    Guess { guess_number: RandomNumber },
 7    /// Peek random number (this should only be used by contract owner or root account)
 8    PeekRandomNumber,
 9}
10
11/// The Query results
12pub enum Response {
13    Owner(AccountId),
14    GuessResult(GuessResult),
15    RandomNumber(RandomNumber),
16}

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 to 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 QueryOwner query simply returns the current owner of the contract, the Guess query returns the guessing result and the PeekRandomNumber query only allows authorized users to peek at the secret.

 1fn handle_query(&mut self, origin: Option<&chain::AccountId>, req: Request) -> Result<Response, Error> {
 2    match req {
 3        Request::QueryOwner => Ok(Response::Owner(self.owner.clone())),
 4        Request::Guess { guess_number } => {
 5            if guess_number > self.random_number {
 6                Ok(Response::GuessResult(GuessResult::TooLarge))
 7            } else if guess_number < self.random_number {
 8                Ok(Response::GuessResult(GuessResult::TooSmall))
 9            } else {
10                Ok(Response::GuessResult(GuessResult::Correct))
11            }
12        }
13        Request::PeekRandomNumber => {
14            // also, we only allow Alice or contract owner to peek the number
15            let sender = origin.ok_or(Error::OriginUnavailable)?;
16            let alice = contracts::account_id_from_hex(ALICE)
17                .expect("should not failed with valid address; qed.");
18
19            if sender != &alice && sender != &self.owner {
20                return Err(Error::NotAuthorized);
21            }
22
23            Ok(Response::RandomNumber(self.random_number))
24        }
25    }
26}

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

Frontend SDK

Check our SDK tutorial and the example frontend of GuessGame to see how to send Commands and Queries to interact with the contracts.

The core of the frontend development is the definition of the type mapping between frontend and backend. We recommend reading the Polkadot.js tutorials on type.

Advanced Feature: Access HTTP Service in Confidential Contracts

In the traditional smart contract model, all the input data to the contract need to be sent from on-chain transactions. If a contract depends on some off-chain information, like the current price of BTC, it needs to get such information from a special kind of infrastructure called blockchain oracle. What’s more, a smart contract can’t initiate a request to the off-chain service.

Since Phala contracts are running in the off-chain pRuntime, it is possible for them to access the network service. While a challenge exists here about the state consistency across multiple instances of the same contract: we need to ensure that each instance performs exactly the same (their states and transaction-sending behavior are consistent at the same block height). Unfortunately, the HTTP request and response operate asynchronously. Even if we send the request at a determined block height, the response can come at any time and its contents may change.

In this section, we use a BtcPriceBot contract example to show how these problems are solved with our AsyncSideTask. The BtcPriceBot first reads the latest BTC price from a web service, then posts an HTTP request to a Telegram bot for notification. Such behavior is triggered with a ReportBtcPrice Command, and we show how it is implemented.

To define an AsyncSideTask, several arguments need to be specified. The current block_number determines when the task starts, and the duration determines when it ends. At the block height of (block_number + duration), the task must report the results to the chain, no matter succeeded or failed. After the results are first uploaded, they are finalized, which means when other instances try to replay the side task, they directly get the results from the chain instead of re-sending the request. These arguments make the side task deterministic from the view of the blockchain.

 1let block_number = context.block.block_number;
 2let duration = 2;
 3
 4let task = AsyncSideTask::spawn(
 5    block_number,
 6    duration,
 7    async {
 8        // async task
 9    },
10    |result, context| {
11        // result process
12    },
13);

In the side task body, you can send any asynchronous requests. In this case, our side task sends two HTTP requests: one to https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD to get the BTC price, and another to https://api.telegram.org to notify the Telegram bot.

 1async move {
 2    // Do network request in this block and return the result.
 3    // Do NOT send mq message in this block.
 4    log::info!("Side task starts to get BTC price");
 5    let mut resp = match surf::get(
 6        "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD",
 7    )
 8    .send()
 9    .await
10    {
11        Ok(r) => r,
12        Err(err) => {
13            return format!("Network error: {:?}", err);
14        }
15    };
16    let result = match resp.body_string().await {
17        Ok(body) => body,
18        Err(err) => {
19            format!("Network error: {:?}", err)
20        }
21    };
22    log::info!("Side task got BTC price: {}", result);
23
24    let price: BtcPrice =
25        serde_json::from_str(result.as_str()).expect("broken BTC price result");
26    let text = format!("BTC price: ${}", price.usd);
27    let uri = format!(
28        "https://api.telegram.org/bot{}/{}",
29        bot_token, "sendMessage"
30    );
31    let data = &TgMessage { chat_id, text };
32
33    let mut resp = match surf::post(uri)
34        .body_json(data)
35        .expect("should not fail with valid data; qed.")
36        .await
37    {
38        Ok(r) => r,
39        Err(err) => {
40            return format!("Network error: {:?}", err);
41        }
42    };
43    let result = match resp.body_string().await {
44        Ok(body) => body,
45        Err(err) => {
46            format!("Network error: {:?}", err)
47        }
48    };
49    log::info!("Side task sent BTC price: {}", result);
50    result
51},

Finally, the resulting process defines the contract reaction to the side task result, and you can send transactions to the chain if needed. The limitation here is also about state consistency: the resulting process should behave the same given the identical result. In this case, we define an empty result process since the price is already reported to the bot through an HTTP request.

To put all these together:

 1Command::ReportBtcPrice => {
 2    if sender != alice && sender != self.owner {
 3        return Err(TransactionError::BadOrigin);
 4    }
 5
 6    let bot_token = self.bot_token.clone();
 7    let chat_id = self.chat_id.clone();
 8
 9    // This Command triggers the use of `AsyncSideTask`, it first sends an HTTP request to get the current BTC
10    // price from https://min-api.cryptocompare.com/, then sends the price to a Telegram bot through another
11    // HTTP request
12    //
13    // To ensure the state consistency, the time to start the task and the time to upload the HTTP response
14    // to chain must be determined. In this case, we start the task in the current `block_number`, and report
15    // the result, whether succeeded or failed, to the chain after `duration`
16    //
17    // Report the result after 2 blocks no matter whether has received the HTTP response
18    let block_number = context.block.block_number;
19    let duration = 2;
20
21    let task = AsyncSideTask::spawn(
22        block_number,
23        duration,
24        async move {
25            // Do network request in this block and return the result.
26            // Do NOT send mq message in this block.
27            log::info!("Side task starts to get BTC price");
28            let mut resp = match surf::get(
29                "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD",
30            )
31            .send()
32            .await
33            {
34                Ok(r) => r,
35                Err(err) => {
36                    return format!("Network error: {:?}", err);
37                }
38            };
39            let result = match resp.body_string().await {
40                Ok(body) => body,
41                Err(err) => {
42                    format!("Network error: {:?}", err)
43                }
44            };
45            log::info!("Side task got BTC price: {}", result);
46
47            let price: BtcPrice =
48                serde_json::from_str(result.as_str()).expect("broken BTC price result");
49            let text = format!("BTC price: ${}", price.usd);
50            let uri = format!(
51                "https://api.telegram.org/bot{}/{}",
52                bot_token, "sendMessage"
53            );
54            let data = &TgMessage { chat_id, text };
55
56            let mut resp = match surf::post(uri)
57                .body_json(data)
58                .expect("should not fail with valid data; qed.")
59                .await
60            {
61                Ok(r) => r,
62                Err(err) => {
63                    return format!("Network error: {:?}", err);
64                }
65            };
66            let result = match resp.body_string().await {
67                Ok(body) => body,
68                Err(err) => {
69                    format!("Network error: {:?}", err)
70                }
71            };
72            log::info!("Side task sent BTC price: {}", result);
73            result
74        },
75        |_result, _context| {
76            // You can send deterministic number of transactions in the result process
77            // In this case, we don't send the price since it has already been reported to the TG bot above
78        },
79    );
80    context.block.side_task_man.add_task(task);
81
82    Ok(())
83}

Summary

In this tutorial, we have covered a walk-through for the basic GuessNumber contract template and the BtcPriceBot contract that leverages the advanced network access feature of Phala contracts. Now it’s your turn to build something new!

Submit Your Work

This tutorial is a part of Phala x Polkadot Encode Club Hackathon. To win the task, please do the following:

  1. Fork the core blockchain and the Web UI repo into your own GitHub account
  2. Develop your own contract on the templates at “encode-hackathon-2021” branch (must be a different one from existing submissions)
  3. Launch your full development stack and take screenshots of your DApp
  4. Push your work to your forked repos. They must be open-sourced
  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 link to your tweet