Today, I want to show how easy it is to build smart contracts on the upcoming Layer 1(and Layer 2. It’s a hybrid) blockchain called Partisia Blockchain. This is my first post on a series of posts I plan to do on Partisia Blockchain.
Partisia Blockchain(hereafter referred to as PBC) offers Zero-Knowledge computations(zk-Computation, specifically Secure Multiparty Computation, a.k.a MPC ) on-chain, off-chain and inter-chain. For those who don’t know what Zero-Knowledge is, I have also added a primer on that in this post.
Recently, there has been a great interest in Zero-Knowledge Proofs(ZKP) in the blockchain industry, with several players bringing in new ways to build ZKP-based Decentralized Applications(dApps). This has also led to the creation of ZK-based Virtual Machines(zkVMs).
ZKP is a way to prove the validity of a statement(secret) without revealing any information about the statement(secret) itself.
In ZKPs, we often have two actors - The prover and the Verifier. The Prover is the one who holds the secret, and Verifier is the one who validates the proof and tells whether it is valid or not. The implications of this are enormous.
Consider an example where you are supposed to prove that you are not from the US as part of KYC to a service provider(maybe an ICO or a new Centralized Exchange). Instead of sending a copy of your government-provided ID, you could generate mathematical proof that the service provider can easily verify.
This process of not revealing zero details about the information being exchanged is why the protocol is known as Zero-Knowledge.
Verifiable Computation in the context of blockchains is the process of off-loading certain computations to off-chain systems that later returns the result along with proof of valid computation.
Using Verifiable Computations, layer 1 protocols like Ethereum can offload computations to off-chain systems like Layer 2 or sidechains. These off-chain systems submit the computation result and the proof to the on-chain state. For layer 1, like Ethereum, to trust these computations, valid proof is required, which can be used to update the state of Ethereum without re-executing the offloaded transactions.
The off-chain systems that execute the transactions on their off-chain nodes use ZKP to generate these Validity Proofs that prove the correctness of the off-chain execution.
Examples of such systems are ZK Rollup systems like StarkNet, zkSync, and Polygon Zero(earlier known as Mir Protocol).
Traditionally, to perform computation on encrypted data, we have to decrypt it. But, with the help of specific cryptographic techniques, we can perform computations on top of encrypted data without decrypting them. This technique is called ZK Computation.
Public-Key Cryptography has helped us transmit and store data securely, but it doesn’t support computation on encrypted data. This is extremely important in this era of a cloud-first mindset.
ZK Computation techniques like Fully Homomorphic Encryption(FHE) and Functional Encryption help deal with this.
Consider a common situation when an online business owner wants to clean spam emails from his email list(leads). Usually, such a user uploads his email list to a service provider that performs the filtering process on its server. What most people never think about is the privacy of the email list that just got uploaded for filtering. The emails are now accessible to the service provider, and the user must trust the service provider to deal with the customer data safely.
Using Functional Encryption, the service provider can perform filtering on encrypted email list data without the need for decrypting the entire data.
Now, consider a situation where there is a need to do ZK computation on a distributed setup involving multiple users/nodes. The type of ZK Computation that can help with this is Secure Multi-Party Computation(sMPC/MPC).
An example of sMPC is the sharing of the total amount to invest in an ICO(Initial Coin Offering) among a group without revealing the individual investment of the parties involved.
Assume that 3 friends - Vinny, Uros and Yoshi want to calculate how much they are willing to invest in an ICO together such that they don’t want to reveal individual investments they would make among themselves or to any trusted third party.
Let’s say that Vinny decides to invest $100,000 while Uros and Yoshi decide to go with $50,000 and $10,000, respectively. sMPC makes use of a cryptographic technique known as Additive Secret Sharing, which allows a user to split their secret into multiple tiny pieces of data that can be shared with others.
In the case of our example, let’s assume that using Additive Secret Sharing, the 3 friends decided to split each of their investments as follows randomly:
In the table above, Vinny has split his $100,000 secret into 3 random secrets that add up to $100,000. And he has kept one share(18191) and distributed the other 2 shares to Uros(31,339) and Yoshi(50,470). Note that Vinny’s shared secrets are denoted in red colour.
Similarly, Uros has split his secret into three - 19476(shared with Vinny), 11990(kept himself) and 18534(shared with Yoshi). Yoshi’s 10,000 was split into 5191(shared with Vinny), 2642(shared with Uros) and 2167(kept himself).
When the secret sharing is done, all three participants hold secret slices from each. The critical fact is that each secret slice alone doesn’t yield any helpful information, but the different slices from all three together are useful.
As shown above, each participant can compute a Partial Result by adding the secret slices they have in their Privacy Zone. Once the partial results are there, the participants can combine these partial results to find the total investment. In the example above, we combine the partial results of Vinny(42858), Uros(45971) and Yoshi(71171) to get $160,000.
If we think about this, we understand that every privacy zone has a partial result, but none of them knows what the other party would invest. Yet, they could compute the total amount they were willing to invest for the ICO.
Partisia Blockchain is a perfect match made between blockchain and ZK Computation with a focus on Multi-Party Computation(MPC).
The ZK Computations are performed by a network of nodes in Partisia known as the ZK Nodes. At the core of the Partisia Blockchain solution, there are 3 categories of nodes:
Baker Nodes - Core network nodes that deal with the underlying P2P mechanism, Consensus, basic transaction handling and block production
ZK Nodes - Nodes that deal with the ZK Computation
Oracle Nodes - Nodes that deal with moving the data and blockchain-agnostic cross-chain transactions via Bring Your Own Coin(BYOC) mechanism
In PBC, the MPC is performed using Delegated ZK Computation. In this mechanism, the ZK computation is delegated to a set of trusted ZK Nodes.
In a Delegated ZK Computation, a.k.a Delegated Trust Model for ZK Computation, as depicted in the diagram above, a strong trust system can be achieved by incorporating trusted accredited ZK Nodes and having an incentive mechanism on top of it. This is how PBC handles the delegated ZK computation.
This is a high-level view of how PBC would deal with ZK Computation. But, PBC could accommodate a variety of trust models other than what I just described. For example, a trust model wherein a threshold could be defined such that X number of ZK Nodes from a pool of Y ZK Nodes could be malicious yet yield a secure computation result, where X and Y could be any arbitrary number.
One of the questions that might come up in your mind is how PBC could deal with CPU-intensive computations that usually require a pre-processing step. Well, PBC has some complex protocols that help with such pre-processing by using the Baker Nodes while retaining the security of the computation.
As shown in the diagram above, a set of Baker Nodes perform the pre-processing and sends it over to the ZK Nodes for ZK Computation via Oblivious Transfer Protocol. Oblivious Transfer(OT) is a cryptographic technique by which the sender sends data to a receiver but never knows what data was sent or received.
PBC allows flexible customization for the trust models. For instance, it allows us to choose the ZK Nodes based on region or jurisdiction. This could find application when we are concerned about keeping computations in specific Zk Nodes in specific jurisdictions like Asia, Europe, etc.
Much like how we have tech stacks for full-stack development like MERN(MongoDB+ ExpressJS + ReactJS + NodeJS), MEAN(MongoDB+ ExpressJS + Angular + NodeJS), etc., in blockchain, we have the stack of tech to help us build our solutions.
PBC's stack consists of seven different components, together named ZEUS.
I discussed the underlying concepts that would help you get started with PBC. But, the main intention of this post is to help you start building Decentralized Applications(dApps) on top of PBC leveraging its smart contracts.
Smart Contracts were initially created back in the early 1990s by Nick Szabo, who defined them as follows:
A set of promises, specified in digital form, including protocols within which the parties perform on these promises
Fast-forward to today, in the age of blockchains; they are the programs that are executed on-chain. Unlike most other popular blockchains, PBC allows you to have a privacy layer on top of its blockchain optionally. This is made possible via its smart contracts.
PBC has three different types of Smart Contracts:
System Smart Contracts - The core permanent contracts that maintain the PBC ecosystem. They deal with things like the maintenance of public and private smart contracts, help in block production and are involved in pre-processing of ZK computation.
Public Smart Contracts - They are the ones written in PBC smart contract language that is based on Rust. This is the normal type of contract(state and associated actions/methods) we see in blockchains like Ethereum.
Private Smart Contracts - They allow us to do ZK Computations using the privacy layer of PBC
As I described in the PBC Stack section, PBC’s smart contract tooling is Apollo. The unified approach In Apollo means that, as developers, we have a single, smart contract language(based on Rust) that can help us build public and private smart contracts.
In this post, I’ll explain how we can build public and private smart contracts on PBC. Before we start with the actual coding part, let’s prepare the prerequisites for development.
The prerequisites are:
Rust Compiler
Git
Java Runtime
Wasm target for Rust
cargo-partisia-contract
Cargo Subcommand
The smart contract programming language for PBC is based on Rust. If you are unfamiliar with Rust, head to Rust Lang Website to learn the basics.
Rust is a modern, high-performance and reliable programming language that has been voted the most loved programming language for the past seven years.
Rust supports multiple platforms, a.k.a targets. The official Rust team maintains some of these targets, while the community maintains some. Web Assembly is a target that is maintained by the community, and support for it can be added via:
rustup target add wasm32-unknown-unknown
PBC contracts are compiled to Web Assembly code(wasm). So, the above command has to be run after you have Rust installed on your system.
The ZK/Private Contracts require the Java SDK, and this can be installed from OpenJDK website. If you are on Mac like me, then you can install it via Homebrew:
brew install openjdk@17
We build smart contract projects using a cargo subcommand called cargo-partisia-contract
. Cargo Subcommands are a way to extend Cargo(Rust’s build system and package manager) such that we can install them via cargo install cargo-<subcommand name>
. To install the cargo-partisia-contract subcommand, do the following:
cargo install cargo-partisia-contract
Partisia Blockchain Wallet is required to deploy contract and interact with it. You will also need some gas to get started. You have few options to get some free test gas:
Hermes Bridge - Bridge ETH from Ethereum Goerli Testnet to PBC Testnet
Ask in the PBC Discord
Once you have some testnet gas, you could use the PBC Faucet to get more gas.
Let's create a simple hello world smart contract with PBC and deploy it on testnet to test the waters.
Create a smart contract project using the cargo-partisia-contract
subcommand.
cargo partisia-contract new hello-world
If it succeeded in creating the project, it should display the following message:
A boilerplate code is generated that shows an example smart contract. Let’s replace this default contract with the following that allows user to call a function greet
that sets the state variable greeting
to a string that greets the user :
//! hello-world
#[macro_use]
extern crate pbc_contract_codegen;
use pbc_contract_common::context::{ContractContext};
/// This is the state of the contract which is persisted on chain.
#[state]
struct ContractState {
greeting: String,
}
impl ContractState {
fn greet(&mut self, name: &str) {
self.greeting = "Hello ".to_string() + name;
}
}
#[init]
fn initialize(
_ctx: ContractContext
) -> ContractState {
let state = ContractState {
greeting: "Hello World".to_string(),
};
state
}
#[action(shortname = 0x01)]
fn greet(
_context: ContractContext,
state: ContractState,
name: String,
) -> ContractState {
assert!(!name.is_empty(), "name must not be empty");
let mut new_state = state;
new_state.greet(&name);
new_state
}
Notice that the contract has three core parts - Init, State and Actions on top of this state. The use of Rust’s attribute-like procedural macros indicates these.
#[init]
- This annotation declares that the annotated function code will be run when the contract is deployed. This is similar to the constructor
function in Ethereum
#[state]
- This macro declares that the annotated struct is the top level contract state
#[action]
- This optional macro declares that the annotated function can be called by client code or other contracts
In the contract above, you can see that the Action macro(#[action(shortname = 0x01)]
) contains a shortname. This a unique identifier that defaults to an automatically generated one. But, when specific names are required, we can set this to a u32 type via the syntax #[action(shortname = <short u32 value>)]
. This unsigned 32-bit integer value is encoded via Little Endian Base 128(LEB128) into a lowercase zero-padded hex value.
Apart from the macros we see in the example above, PBC comes with additional categories of macros as shown below - ABI Attribute specific and ZK Lifetime Attribute specific:
Let’s unpack the hello world contract we saw earlier.
pbc_contract_codegen
is a crate that comes with different macros(listed in the image above - ABI Attribute macros and ZK Lifetime Attribute macros) that allows us to annotate different parts of our contract code.
Although the example above imports all macros in the crate via #[macro_use]
, we could also do the following to explicitly import only the ones required:
use pbc_contract_codegen::{init, state, action};
ContractContext
from pbc_contract_common
contains the blockchain state information like:
contract_address
- Address of the contract being called
sender
- Sender of the transaction
block_time
- Block time
block_production_time
- Block production time in milliseconds UTC
current_transaction
- Hash of current transaction
original_transaction
- Hash of parent transaction, if available
Now, let’s have a look at the ContractState
.
The state
attribute-like macro annotation is a required one and should be defined only once in a contract. This macro internally derives a ReadWriteState
trait that comes with serialization and de-serialization functionalities for the state. This means that only fields that have an implementation for this trait can be used in the state. This means floating point data types like f32
or f64
can’t be used as they don’t impl
the ReadWriteState
.
In the example here, the greet method basically allows user to set a name to the greeting
message in the contract state. Note that greeting
is String
which doesn’t have a direct impl
of ReadWriteState
trait.
But, since String is internally represented as a vector of u8
, which has an impl
of ReadWriteState
, it works fine.
Now, let’s look at the initialize
function which is annotated by the #[init]
macro.
This is the code that gets executed when the contract is deployed and is similar to the constructor
function in Ethereum’s Solidity. Here, the function returns ContractState
. If the contract had interactions with other contracts, then the return type would have been a tuple (ContractState, Vec<EventGroup>)
.
EventGroup
is a struct that holds the list of events that represent the way to interact with other contracts along with an optional callback payload data. EventGroup
is created using EventGroupBuilder
.
Notice that EventGroup
contains the events
. Each event are the contract interactions(represented by the Interaction
struct, under the hood). The structure of an Interaction
is as follows:
dest
- Address of contract to call
payload
- Payload to send
cost
- The amount of gas to allocate for handling the callback. This is defaulted to None
, which automatically calculates the amount from the remaining gas
Let’s look at the action code now.
The action macro allows us to set a shortname for the function. This is for size optimization. When shortname isn’t given, PBC automatically defaults to a shortname based on the function name. This is done by taking the first four bytes of the SHA256 hash of the function name. In the example,above, the greet function returns the updated ContractState
. Technically, this could be a tuple of (ContractState, Vec<EventGroup>)
, similar to what I mentioned in initialize part.
The contract can be compiled using the partisia-contract
cargo subcommand.
Notice that in the above command, we are building the contract in release
mode. This is the optimized mode for deployment compared to the other default mode - debug
.
Once the command is run, we should get both the .wasm
contract as well as the .abi
file. These should be available in the following path:
We can deploy contracts to a PBC network(testnet or mainnet; we focus on testnet in this post)using the PBC Explorer’s Deploy Wasm Contract Page.
In this page, you can click on those buttons to choose the .wasm
and .abi
files from the compilation stage.
Copy the Deploy gas cost value from the text field as we will use that gas value to perform the deployment.
Now, click on the SUBMIT button to bring up the PBC Wallet confirmation dialog.
Then click on Edit Fee button to update the gas fee to the value copied in the previous step and click on save to confirm the updated value:
Once you click on the TRANSACT button, the transaction should be submitted to the blockchain, after password confirmation:
Once the contract is deployed, you get the contract address(027d207c3f0a810cc39aa3bdd7a03af3ca7a3630e3
above).
Notice that the last 20 bytes of the transaction hash is same as the last 20 bytes of the contract address.
Addresses in PBC are of four types:
User account - Prefix of 0x00
System Contract - Prefix of 0x01
Public Contract - Prefix of 0x02
ZK Contract - Prefix of 0x03
Each address is a combination of the Address Type and a unique 20 bytes identifier that is derived from the hash of the public key of an account.
In the hello world contract we deployed, the contract address is 027d207c3f0a810cc39aa3bdd7a03af3ca7a3630e3
. This address can be decomposed into:
To interact with the deployed contract, click on the contract address in the PBC explorer or search for the contract address.
Notice that the initial state is set to what we set in the initialize
function - Hello World
.
In the Contract Interaction section, you can see the actions available in the contract - GREET
. Click on that button to initiate an action.
Provide a name as shown and click the SUBMIT
button to trigger the action.
Once the action is successful, you should see an execution success message along with the RPC call data.
The state should also be updated as shown below:
We have seen how to deploy public smart contracts on PBC. In the upcoming posts, we will see how we can leverage PBC to build and deploy Private/ZK Smart contracts.
The code for this tutorial is available at - hello-world-pbc