Skip to content

MPC smart contracts

A MPC smart contract handles the orchestration of inputs and computation on the inputs. The state of a MPC smart contract can be split into 2 parts. The secret orchestration state and the normal state. The normal state is just like the state for a normal smart contract.

The secret orchestration state, also called the secret state, holds all the info that the MPC nodes needs to perform MPC. This includes the metadata connected to secret inputs, encrypted inputs that have been sent through the ledger and verification values to verify inputs, that was sent directly to the nodes.

A MPC smart contract has actions and callbacks like normal smart contracts, but a MPC smart contract also have actions that lets a developer add functionality to input secret inputs, delete secret inputs, start a computation using the secret inputs. These actions are defined by in the smart contract language.

MPC Nodes

MPC smart contracts uses MPC nodes to carry out the computation, the MPC nodes are allocated to carry out the MPC computations of the MPC contracts during deployment of the contract. The MPC nodes are registered in jurisdictions, this lets you choose what region the MPC nodes should be located, to control where the secret data is stored.

Example of a MPC smart contract - Vickrey Auction

In this example we will use our contract that creates a Vickrey Auction (second price auction), which is a sealed bid auction where the winner is the person with the highest bid (as in a normal auction). The second price auction takes as inputs the bids from the registered participants. The bids are delivered encrypted and secret-shared to the MPC nodes allocated to the contract. When the computation is initiated by the contract owner, the MPC nodes reads the collected input and then create a bit vector consisting of prices and the ordering number. The list of bit vectors is now sorted in MPC. The winner is the first entry (the bidder with the highest price-bid), the price is determined by the size of the second-highest bid.

The contract follows these phases:

  1. Initialization on the blockchain.
  2. Registration of bidders allowed to participate in the auction.
  3. Retrieval of secret bids.
  4. Once enough bids have been received, the owner of the contract can initialize computation of the auction result.
  5. The MPC nodes derive the winning bid in a secure manner by executing a Secure Multiparty Computation protocol off-chain.
  6. Once the Multi Party computation concludes, the winning bid will be published and the winner will be stored in the state, together with their bid.
  7. The MPC nodes sign the result of the auction with a digital signature proving that the nodes in question were responsible for the generating the result of the auction.

Below you can see the Rust implementation of the MPC smart contract for the Vickrey Auction:

The function zk_compute defines a MPC computation on the secret shared bids (step 5 above), it returns the index of the highest bidder and amount of second-highest bid.

use crate::zk_lib::{num_secret_variables, sbi32_from, sbi32_input, sbi32_metadata, Sbi32};

pub fn zk_compute() -> (Sbi32, Sbi32) {
    // Initialize state
    let mut highest_bidder: Sbi32 = sbi32_from(sbi32_metadata(1));
    let mut highest_amount: Sbi32 = sbi32_from(0);
    let mut second_highest_amount: Sbi32 = sbi32_from(0);

    // Determine max
    for variable_id in 1..(num_secret_variables() + 1) {
        if sbi32_input(variable_id) > highest_amount {
            second_highest_amount = highest_amount;
            highest_amount = sbi32_input(variable_id);
            highest_bidder = sbi32_from(sbi32_metadata(variable_id));
        } else if sbi32_input(variable_id) > second_highest_amount {
            second_highest_amount = sbi32_input(variable_id);
        }
    }

    // Return highest bidder index, and second highest amount
    (highest_bidder, second_highest_amount)
}

Full MPC-SC code example

#![allow(unused_variables)]

#[macro_use]
extern crate pbc_contract_codegen;
extern crate pbc_contract_common;
extern crate pbc_lib;

use create_type_spec_derive::CreateTypeSpec;
use pbc_contract_common::address::Address;
use pbc_contract_common::context::ContractContext;
use pbc_contract_common::events::EventGroup;
use pbc_contract_common::zk::{
    AttestationId, CalculationStatus, SecretVarId, ZkInputDef, ZkState, ZkStateChange,
};
use pbc_traits::{ReadWriteRPC, ReadWriteState};
use read_write_rpc_derive::ReadWriteRPC;
use read_write_state_derive::ReadWriteState;

/// Id of a contract bidder.
#[repr(transparent)]
#[derive(PartialEq, ReadWriteRPC, ReadWriteState, Debug, Clone, Copy, CreateTypeSpec)]
#[non_exhaustive]
struct BidderId {
    id: i32,
}

/// Secret variable metadata. Contains unique ID of the bidder.
#[derive(ReadWriteState, ReadWriteRPC, Debug)]
struct SecretVarMetadata {
    bidder_id: BidderId,
}

/// The size of the MPC bid input variables.
const BITLENGTH_OF_SECRET_BID_VARIABLES: [u32; 1] = [32];

/// Number of bids required before starting auction computation.
const MIN_NUM_BIDDERS: u32 = 3;

/// Type of tracking bid amount
type BidAmount = i32;

/// This state of the contract.
#[state]
struct ContractState {
    /// Owner of the contract
    owner: Address,
    /// Registered bidders - only registered bidders are allowed to bid.
    registered_bidders: Vec<RegisteredBidder>,
    /// The auction result
    auction_result: Option<AuctionResult>,
}

#[derive(Clone, ReadWriteState, CreateTypeSpec, ReadWriteRPC)]
struct AuctionResult {
    /// Bidder id of the auction winner
    winner: BidderId,
    /// The winning bid
    second_highest_bid: BidAmount,
}

/// Representation of a registered bidder with an address
#[derive(Clone, ReadWriteState, CreateTypeSpec)]
struct RegisteredBidder {
    bidder_id: BidderId,
    address: Address,
}

/// Initializes contract
///
/// Note that owner is set to whoever initializes the contact.
#[init]
fn initialize(context: ContractContext, zk_state: ZkState<SecretVarMetadata>) -> ContractState {
    ContractState {
        owner: context.sender,
        registered_bidders: Vec::new(),
        auction_result: None,
    }
}

/// Registers a bidder with an address and updates the state accordingly.
////
/// Ensures that only the owner of the contract is able to register bidders.
#[action(shortname = 0x30)]
fn register_bidder(
    context: ContractContext,
    mut state: ContractState,
    zk_state: ZkState<SecretVarMetadata>,
    bidder_id: i32,
    address: Address,
) -> ContractState {
    let bidder_id = BidderId { id: bidder_id };

    assert_eq!(
        context.sender, state.owner,
        "Only the owner can register bidders"
    );

    assert!(
        state
            .registered_bidders
            .iter()
            .all(|x| x.address != address),
        "Duplicate bidder address: {:?}",
        address,
    );

    assert!(
        state
            .registered_bidders
            .iter()
            .all(|x| x.bidder_id != bidder_id),
        "Duplicate bidder id: {:?}",
        bidder_id,
    );

    state
        .registered_bidders
        .push(RegisteredBidder { bidder_id, address });

    state
}

/// Adds another bid variable to the ZkState.
///
/// The ZkInputDef encodes that variables should have size [`BITLENGTH_OF_SECRET_BID_VARIABLES`].
#[zk_on_secret_input(shortname = 0x40)]
fn add_bid(
    context: ContractContext,
    state: ContractState,
    zk_state: ZkState<SecretVarMetadata>,
) -> (
    ContractState,
    Vec<EventGroup>,
    ZkInputDef<SecretVarMetadata>,
) {
    let bidder_info = state
        .registered_bidders
        .iter()
        .find(|x| x.address == context.sender);

    let bidder_info = match bidder_info {
        Some(bidder_info) => bidder_info,
        None => panic!("{:?} is not a registered bidder", context.sender),
    };

    // Assert that only one bid is placed per bidder
    assert!(
        zk_state
            .secret_variables
            .iter()
            .chain(zk_state.pending_inputs.iter())
            .all(|v| v.owner != context.sender),
        "Each bidder is only allowed to send one bid. : {:?}",
        bidder_info.bidder_id,
    );

    let input_def = ZkInputDef {
        seal: false,
        metadata: SecretVarMetadata {
            bidder_id: bidder_info.bidder_id,
        },
        expected_bit_lengths: BITLENGTH_OF_SECRET_BID_VARIABLES.to_vec(),
    };

    (state, vec![], input_def)
}

/// Allows the owner of the contract to start the computation, computing the winner of the auction.
///
/// The second price auction computation is beyond this call, involving several Multi Party computation steps.
#[action(shortname = 0x01)]
fn compute_winner(
    context: ContractContext,
    state: ContractState,
    zk_state: ZkState<SecretVarMetadata>,
) -> (ContractState, Vec<EventGroup>, Vec<ZkStateChange>) {
    assert_eq!(
        zk_state.calculation_state,
        CalculationStatus::Waiting,
        "Computation must start from Waiting state, but was {:?}",
        zk_state.calculation_state,
    );
    assert_eq!(
        zk_state.data_attestations.len(),
        0,
        "Auction must have exactly zero data_attestations at this point"
    );

    assert_eq!(
        context.sender, state.owner,
        "Only contract owner can start the auction"
    );
    let amount_of_bidders = zk_state.secret_variables.len() as u32;

    assert!(
        amount_of_bidders >= MIN_NUM_BIDDERS,
        "At least {} bidders must have submitted bids for the auction to start",
        MIN_NUM_BIDDERS
    );

    (
        state,
        vec![],
        vec![ZkStateChange::start_computation(vec![
            SecretVarMetadata {
                bidder_id: BidderId { id: -1 },
            },
            SecretVarMetadata {
                bidder_id: BidderId { id: -1 },
            },
        ])],
    )
}

/// Automatically called when the computation is completed
///
/// The only thing we do is instantly open/declassify the output variables.
#[zk_on_compute_complete]
fn auction_compute_complete(
    context: ContractContext,
    state: ContractState,
    zk_state: ZkState<SecretVarMetadata>,
    output_variables: Vec<SecretVarId>,
) -> (ContractState, Vec<EventGroup>, Vec<ZkStateChange>) {
    assert_eq!(
        zk_state.data_attestations.len(),
        0,
        "Auction must have exactly zero data_attestations at this point"
    );
    (
        state,
        vec![],
        vec![ZkStateChange::OpenVariables {
            variables: output_variables,
        }],
    )
}

/// Automatically called when the auction result is declassified. Updates state to contain result,
/// and requests attestation from nodes.
#[zk_on_variables_opened]
fn open_auction_variable(
    context: ContractContext,
    state: ContractState,
    zk_state: ZkState<SecretVarMetadata>,
    opened_variables: Vec<SecretVarId>,
) -> (ContractState, Vec<EventGroup>, Vec<ZkStateChange>) {
    assert_eq!(
        opened_variables.len(),
        2,
        "Unexpected number of output variables"
    );
    assert_eq!(
        zk_state.data_attestations.len(),
        0,
        "Auction must have exactly zero data_attestations at this point"
    );

    let auction_result = AuctionResult {
        winner: read_variable(&zk_state, opened_variables.get(0)),
        second_highest_bid: read_variable(&zk_state, opened_variables.get(1)),
    };

    let attest_request = ZkStateChange::Attest {
        data_to_attest: serialize_as_big_endian(&auction_result),
    };

    (state, vec![], vec![attest_request])
}

/// Automatically called when some data is attested
#[zk_on_attestation_complete]
fn auction_results_attested(
    context: ContractContext,
    mut state: ContractState,
    zk_state: ZkState<SecretVarMetadata>,
    attestation_id: AttestationId,
) -> (ContractState, Vec<EventGroup>, Vec<ZkStateChange>) {
    assert_eq!(
        zk_state.data_attestations.len(),
        1,
        "Auction must have exactly one attestation"
    );
    let attestation = zk_state.get_attestation(attestation_id).unwrap();

    assert_eq!(attestation.signatures.len(), 4, "Must have four signatures");

    let auction_result = AuctionResult::rpc_read_from(&mut attestation.data.as_slice());

    state.auction_result = Some(auction_result);

    (state, vec![], vec![ZkStateChange::ContractDone])
}

/// Writes some value as RPC data.
fn serialize_as_big_endian<T: ReadWriteRPC>(it: &T) -> Vec<u8> {
    let mut output: Vec<u8> = vec![];
    it.rpc_write_to(&mut output).expect("Could not serialize");
    output
}

/// Reads a variable's data as some state value
fn read_variable<T: ReadWriteState>(
    zk_state: &ZkState<SecretVarMetadata>,
    variable_id: Option<&SecretVarId>,
) -> T {
    let variable_id = *variable_id.unwrap();
    let variable = zk_state.get_variable(variable_id).unwrap();
    let buffer: Vec<u8> = variable.data.clone().unwrap();
    T::state_read_from(&mut buffer.as_slice())
}

Partisia All Rights Reserved © 2023