Stake
The Money::Stake
and Consensus::Stake
functions are used in order
to apply to become eligible for participation in the block proposal
process, commonly known as Consensus.
The Stake transaction consists of two contract calls, calling the above mentioned functions. The parameters, respectively, are:
pub struct MoneyStakeParamsV1 {
/// Blinding factor for `token_id`
pub token_blind: pallas::Base,
/// Anonymous input
pub input: Input,
}
pub struct ConsensusStakeParamsV1 {
/// Burnt token revealed info
pub input: Input,
/// Anonymous output
pub output: ConsensusOutput,
}
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 stake process is burning a coin in the state of Money and minting a coin in the state of Consensus in order to start being able to participate in consensus and propose blocks.
The contract calls execute in sequence:
Money::Stake
Consensus::Stake
The ZK proof we use to prove burning of the coin in Money is the
Burn_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 "Burn_V1" {
EcFixedPointShort VALUE_COMMIT_VALUE,
EcFixedPoint VALUE_COMMIT_RANDOM,
EcFixedPointBase NULLIFIER_K,
}
# The witness values we define for our circuit
witness "Burn_V1" {
# The value of this coin
Base value,
# The token ID
Base token,
# Random blinding factor for value commitment
Scalar value_blind,
# Random blinding factor for the token ID
Base token_blind,
# 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,
# Blinding factor for the encrypted user_data
Base user_data_blind,
# Secret key used to derive nullifier and coin's 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,
# Secret key used to derive public key for the tx signature
Base signature_secret,
}
# The definition of our circuit
circuit "Burn_V1" {
# Poseidon hash of the nullifier
nullifier = poseidon_hash(secret, serial);
constrain_instance(nullifier);
# 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));
# 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);
# Derive the public key used in the coin from its secret counterpart
pub = ec_mul_base(secret, NULLIFIER_K);
# Coin hash
C = poseidon_hash(
ec_get_x(pub),
ec_get_y(pub),
value,
token,
serial,
spend_hook,
user_data,
);
# With this, we can actually produce a fake coin of value 0
# above and use it as a dummy input. The inclusion merkle tree
# has a 0x00 leaf at position 0, so zero_cond will output value
# iff value is 0 - which is equivalent to 0x00 so that's the
# trick we use to make the inclusion proof.
coin_incl = zero_cond(value, C);
# Merkle root
root = merkle_root(leaf_pos, path, coin_incl);
constrain_instance(root);
# Export user_data
user_data_enc = poseidon_hash(user_data, user_data_blind);
constrain_instance(user_data_enc);
# Reveal spend_hook
constrain_instance(spend_hook);
# Finally, we derive a public key for the signature and
# constrain its coordinates:
signature_public = ec_mul_base(signature_secret, NULLIFIER_K);
constrain_instance(ec_get_x(signature_public));
constrain_instance(ec_get_y(signature_public));
# At this point we've enforced all of our public inputs.
}
The ZK proof we use to prove minting of the coin in Consensus is the
ConsensusMint_V1
circuit:
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
Money::get_metadata()
In the money_stake_get_metadata_v1
function, we gather the input
pubkey for signature verification, and extract necessary public inputs
for verifying the money burn ZK proof.
Money::process_instruction()
In the money_stake_process_instruction_v1
function, we perform the
state transition. We enforce that:
- The input
spend_hook
is 0 (zero) (for now we don't have protocol-owned stake) - The input token ID corresponds to the native network token (the commitment blind is revealed in the params)
- The input coin Merkle inclusion proof is valid
- The input nullifier was not published before
- The next
call_idx
is a call to theConsensus::StakeV1
function - The input in the params to the next function is the same as the current input
If these checks pass, we create a state update with the revealed nullifier:
pub struct MoneyStakeUpdateV1 {
/// Revealed nullifier
pub nullifier: Nullifier,
}
Money::process_update()
For the Money state update, we use the
money_stake_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 Money::Stake
state transition has passed, we move on to
executing the Consensus::Stake
state transition. This is supposed
to mint the new coin in the Consensus state.
Consensus::get_metadata()
In consensus_stake_get_metadata_v1
we grab the current epoch of
the slot where we're executing this contract call and use it as one
of the public inputs for the ZK proof of minting the new coin. This
essentially serves as a timelock where we can enforce a grace period
for this staked coin before it is able to start proposing blocks. More
information on this can be found in the Proposal page.
Additionally we extract the coin and the value commitment to use as
the proof's public inputs.
Consensus::process_instruction()
In consensus_stake_process_instruction_v1
we perform the state
transition. We enforce that:
- The previous
call_idx
is a call toMoney::StakeV1
- The
Input
from the current call is the same as theInput
from the previous call (essentially copying it) - The value commitments in the
Input
andConsensusOutput
match - The
Input
coin's Merkle inclusion proof is valid in the Money state - The input's nullifier is revealed and exists in the Money state
- The
ConsensusOutput
coin hasn't existed in the Consensus state before - The
ConsensusOutput
coin hasn't existed in the Unstaked Consensus state before
If these checks pass we create a state update with the minted coin that is now considered staked in Consensus:
pub struct ConsensusStakeUpdateV1 {
/// The newly minted coin
pub coin: Coin,
}
Consensus::process_update()
For the state update, we use the consensus_stake_process_update_v1
function. This takes the coin from the ConsensusOutput
and adds
it to the set of staked coins, and appends it to the Merkle tree of
staked coins so participants are able to create inclusion proofs in
the future.