Unstake

The Consensus::Unstake and Money::Unstake functions are used in order to fully exit from the consensus participation and move back the staked funds into the Money state.

The Unstake transaction consists of two contract calls, calling the above mentioned functions. The parameters, respectively, are:

pub struct ConsensusUnstakeParamsV1 {
    /// Anonymous input
    pub input: ConsensusInput,
}

pub struct MoneyUnstakeParamsV1 {
    /// Burnt token revealed info
    pub input: ConsensusInput,
    /// Anonymous output
    pub output: Output,
}

These two contract calls need to happen atomically, meaning they should be part of a single transaction being executed on the network. On a high level, what is happening in the unstake process is burning the coin previously created through UnstakeRequest in the Consensus state and minting a new coin in the Money state where it can then again be used for other functionality outside of consensus.

The contract calls execute in sequence:

  1. Consensus::Unstake
  2. Money::Unstake

The ZK proof we use to prove burning of the coin in Consensus is the ConsensusBurn_V1 circuit:

k = 13;
field = "pallas";

constant "ConsensusBurn_V1" {
	EcFixedPointShort VALUE_COMMIT_VALUE,
	EcFixedPoint VALUE_COMMIT_RANDOM,
	EcFixedPointBase NULLIFIER_K,
}

witness "ConsensusBurn_V1" {
	# The value of this coin
	Base value,
	# The epoch this coin was minted on
	Base epoch,
	# Unique serial number corresponding to this coin
	Base serial,
	# Random blinding factor for value commitment
	Scalar value_blind,
	# Secret key used to derive nullifier and coins' public key
	Base secret,
	# Leaf position of the coin in the Merkle tree of coins
	Uint32 leaf_pos,
	# Merkle path to the coin
	MerklePath path,
}

circuit "ConsensusBurn_V1" {
	# Poseidon hash of the nullifier
	nullifier = poseidon_hash(secret, serial);
	constrain_instance(nullifier);

	# Constrain the epoch this coin was minted on
	constrain_instance(epoch);

	# We derive coins' public key for the signature and
	# constrain its coordinates:
	pub = ec_mul_base(secret, NULLIFIER_K);
	pub_x = ec_get_x(pub);
	pub_y = ec_get_y(pub);
	constrain_instance(pub_x);
	constrain_instance(pub_y);

	# Coin hash	
	C = poseidon_hash(
		pub_x,
		pub_y,
		value,
		epoch,
		serial,
	);

	# Merkle root
	root = merkle_root(leaf_pos, path, C);
	constrain_instance(root);
	
	# Pedersen commitment for coin's value
	vcv = ec_mul_short(value, VALUE_COMMIT_VALUE);
	vcr = ec_mul(value_blind, VALUE_COMMIT_RANDOM);
	value_commit = ec_add(vcv, vcr);
	# Since value_commit is a curve point, we fetch its coordinates
	# and constrain them:
	constrain_instance(ec_get_x(value_commit));
	constrain_instance(ec_get_y(value_commit));

	# At this point we've enforced all of our public inputs.
}

The ZK proof we use to prove minting of the coin in Money is the Mint_V1 circuit:

# The k parameter defining the number of rows used in our circuit (2^k)
k = 13;
field = "pallas";

# The constants we define for our circuit
constant "Mint_V1" {
	EcFixedPointShort VALUE_COMMIT_VALUE,
	EcFixedPoint VALUE_COMMIT_RANDOM,
	EcFixedPointBase NULLIFIER_K,
}

# The witness values we define for our circuit
witness "Mint_V1" {
	# X coordinate for public key
	Base pub_x,
	# Y coordinate for public key
	Base pub_y,
	# The value of this coin
	Base value,
	# The token ID
	Base token,
	# Unique serial number corresponding to this coin
	Base serial,
	# Allows composing this ZK proof to invoke other contracts
	Base spend_hook,
	# Data passed from this coin to the invoked contract
	Base user_data,
	# Random blinding factor for the value commitment
	Scalar value_blind,
	# Random blinding factor for the token ID
	Base token_blind,
}

# The definition of our circuit
circuit "Mint_V1" {
	# Poseidon hash of the coin
	C = poseidon_hash(
		pub_x,
		pub_y,
		value,
		token,
		serial,
		spend_hook,
		user_data,
	);
	constrain_instance(C);

	# Pedersen commitment for coin's value
	vcv = ec_mul_short(value, VALUE_COMMIT_VALUE);
	vcr = ec_mul(value_blind, VALUE_COMMIT_RANDOM);
	value_commit = ec_add(vcv, vcr);
	# Since the value commit is a curve point, we fetch its coordinates
	# and constrain them:
	constrain_instance(ec_get_x(value_commit));
	constrain_instance(ec_get_y(value_commit));

	# Commitment for coin's token ID. We do a poseidon hash since it's
	# cheaper than EC operations and doesn't need the homomorphic prop.
	token_commit = poseidon_hash(token, token_blind);
	constrain_instance(token_commit);

	# At this point we've enforced all of our public inputs.
}

Contract logic

Consensus::get_metadata()

In the consensus_unstake_get_metadata_v1 function, we gather the public inputs necessary to verify the ConsensusBurn_V1 ZK proof, and additionally the public key used to verify the transaction signature. This pubkey is also derived and enforced in ZK.

Consensus::process_instruction()

For the Consensus state transition, we use the consensus_unstake_process_instruction_v1 function. We enforce that:

  • The next call_idx is a call to the Money::UnstakeV1 function
  • The input in the params to the next function is the same as current input
  • The timelock from UnstakeRequest has expired
  • The input coin Merkle inclusion proof is valid
  • The input nullifier was not published before

If these checks pass, we create a state update with the revealed nullifier:

pub struct ConsensusUnstakeUpdateV1 {
    /// Revealed nullifier
    pub nullifier: Nullifier,
}

Consensus::process_update()

For the Consensus state update, we use the consensus_unstake_process_update_v1 function. This will simply append the revealed nullifier to the existing set of nullifiers in order to prevent double-spending.

After the Consensus::Unstake state transition has passed, we move on to executing the Money::Unstake state transition. This is supposed to mint the new coin in the Money state.

Money::get_metadata()

In the money_unstake_get_metadata_v1 function, we gather the public inputs necessary to verify the Mint_V1 ZK proof. It is not necessary to grab any public keys for signature verification, as they're already collected in Consensus::get_metadata().

Money::process_instruction()

In the money_unstake_process_instruction_v1 function, we perform the state transition. We enforce that:

  • The previous call_idx is a call to the Consensus::UnstakeV1 function
  • The token pedersen commitment is a commitment to the native network token
  • The value pedersen commitments in the input and output match
  • The input coin Merkle inclusion proof is valid for Consensus
  • The input nullifier was published in Consensus
  • The output coin was not seen before in the set of coins in Money

If these checks pass, we create a state update with the revealed minted coin:

pub struct MoneyUnstakeUpdateV1 {
    /// The newly minted coin
    pub coin: Coin,
}

Money::process_update()

In money_unstake_process_update_v1 we simply append the newly minted coin to the set of seen coins in Money, and we add it to the Merkle tree of coins in Money so further inclusion proofs can be validated.