Unstake request

The Consensus::UnstakeRequest function is used when a consensus participant wants to exit participation and plans to unstake their staked coin. What the user is essentially doing here is burning their coin they have been using for consensus participation, and minting a new coin that isn't able to compete anymore, and is timelocked for a predefined amount of time. This new coin then has to wait until the timelock is expired, and then it can be used in the Unstake function in order to be redeemed back into the Money state.

The parameters to execute this function are 1 anonymous input and 1 anonymous output:

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

In this function, we have two ZK proofs, ConsensusBurn_V1 and ConsensusMint_V1:

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.
}
k = 13;
field = "pallas";

constant "ConsensusMint_V1" {
	EcFixedPointShort VALUE_COMMIT_VALUE,
	EcFixedPoint VALUE_COMMIT_RANDOM,
}

witness "ConsensusMint_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 epoch this coin was minted on
	Base epoch,
	# Unique serial number corresponding to this coin
	Base serial,
	# Random blinding factor for the value commitment
	Scalar value_blind,
}

circuit "ConsensusMint_V1" {
	# Constrain the epoch this coin was minted on
	constrain_instance(epoch);

	# Poseidon hash of the coin
	C = poseidon_hash(
		pub_x,
		pub_y,
		value,
		epoch,
		serial,
	);
	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));

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

Contract logic

get_metadata()

In the consensus_unstake_request_get_metadata_v1 function, we gather the public inputs necessary to verify the given ZK proofs. It's pretty straightforward, and more or less the same as other get_metadata functions in this smart contract.

process_instruction()

We perform the state transition in consensus_unstake_request_process_instruction_v1. We enforce that:

  • The timelock of the burned coin has passed and the coin is eligible for unstaking
  • The Merkle inclusion proof of the burned coin is valid
  • The revealed nullifier of the burned coin has not been seen before
  • The input and output value commitments are the same
  • The output/minted coin has not been seen before

When this is done, and everything passes, we create a state update with the burned nullifier and the minted coin. Here we use the same parameters like we do in Proposal - a nullifier and a coin:

pub struct ConsensusProposalUpdateV1 {
    /// Revealed nullifier
    pub nullifier: Nullifier,
    /// The newly minted coin
    pub coin: Coin,
}

process_update()

For the state update, we use the consensus_unstake_request_process_update_v1 function. This takes the state update produced by consensus_unstake_request_process_instruction_v1. With it, we append the revealed nullifier to the set of seen nullifiers. The minted coin, in this case however, does not get added to the Merkle tree of staked coins. Instead, we add it to the Merkle tree of unstaked coins where it lives in a separate state. By doing this, we essentially disallow the new coin to compete in consensus again because in that state it does not exist. It only exists in the unstaked state, and as such can only be operated with other functions that actually read from this state - namely Unstake