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:

  1. Money::Stake
  2. 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 the Consensus::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 to Money::StakeV1
  • The Input from the current call is the same as the Input from the previous call (essentially copying it)
  • The value commitments in the Input and ConsensusOutput 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.