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:
Consensus::Unstake
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 theMoney::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 theConsensus::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.