Proposal
The Consensus::Proposal
function is used whenever a consensus
participant is able to produce a winning proof and wants to prove
they're the current consensus leader and are eligible to propose a
block. By itself, this smart contract has nothing to do with blocks
themself, it is up to the leader to choose which transactions to
include in the block they're proposing. The Consensus::Proposal
function simply serves as a way to verify that the block proposer is
indeed an eligible leader.
The parameters to execute this function are 1 anonymous input and 1 anonymous output, and other necessary metadata. Essentially we burn the winning coin, and mint a new one in order to compete in further slots. Every time a proposer wins the leader election, they have to burn their competing coin, prove they're the winner, and then mint a new coin that includes the block reward and is eligible to compete in upcoming future slots.
pub struct ConsensusProposalParamsV1 {
/// Anonymous input
pub input: ConsensusInput,
/// Anonymous output
pub output: ConsensusOutput,
/// Reward value
pub reward: u64,
/// Revealed blinding factor for reward value
pub reward_blind: pallas::Scalar,
/// Extending fork last proposal/block hash
pub fork_hash: blake3::Hash,
/// Extending fork second to last proposal/block hash
pub fork_previous_hash: blake3::Hash,
/// VRF proof for eta calculation
pub vrf_proof: VrfProof,
/// Coin y
pub y: pallas::Base,
/// Lottery rho used
pub rho: pallas::Base,
}
The ZK proof we use for this is a single circuit,
ConsensusProposal_V1
:
k = 13;
field = "pallas";
constant "ConsensusProposal_V1" {
EcFixedPointShort VALUE_COMMIT_VALUE,
EcFixedPoint VALUE_COMMIT_RANDOM,
EcFixedPointBase NULLIFIER_K,
}
witness "ConsensusProposal_V1" {
# Burnt coin secret key
Base input_secret_key,
# Unique serial number corresponding to the burnt coin
Base input_serial,
# The value of the burnt coin
Base input_value,
# The epoch the burnt coin was minted on
Base epoch,
# The reward value
Base reward,
# Random blinding factor for the value commitment
Scalar input_value_blind,
# Leaf position of the coin in the Merkle tree of coins
Uint32 leaf_pos,
# Merkle path to the coin
MerklePath path,
# Random blinding factor for the value commitment of the new coin
Scalar output_value_blind,
# Election seed y
Base mu_y,
# Election seed rho
Base mu_rho,
# Sigma1
Base sigma1,
# Sigma2
Base sigma2,
# Lottery headstart
Base headstart,
}
circuit "ConsensusProposal_V1" {
# Witnessed constants
ZERO = witness_base(0);
SERIAL_PREFIX = witness_base(2);
SEED_PREFIX = witness_base(3);
SECRET_PREFIX = witness_base(4);
# =============
# Burn old coin
# =============
# Poseidon hash of the nullifier
nullifier = poseidon_hash(input_secret_key, input_serial);
constrain_instance(nullifier);
# Constrain the epoch this coin was minted on.
# We use this as our timelock mechanism.
constrain_instance(epoch);
# We derive the coin's public key for the signature and
# VRF proof verification and constrain its coordinates:
input_pub = ec_mul_base(input_secret_key, NULLIFIER_K);
pub_x = ec_get_x(input_pub);
pub_y = ec_get_y(input_pub);
constrain_instance(pub_x);
constrain_instance(pub_y);
# Construct the burned coin
C = poseidon_hash(
pub_x,
pub_y,
input_value,
epoch,
input_serial,
);
# Merkle inclusion proof
root = merkle_root(leaf_pos, path, C);
constrain_instance(root);
# Pedersen commitment for burned coin's value
vcv = ec_mul_short(input_value, VALUE_COMMIT_VALUE);
vcr = ec_mul(input_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));
# =============
# Mint new coin
# =============
# Constrain reward value
constrain_instance(reward);
# Pedersen commitment for new coin's value (old value + reward)
output_value = base_add(input_value, reward);
nvcv = ec_mul_short(output_value, VALUE_COMMIT_VALUE);
nvcr = ec_mul(output_value_blind, VALUE_COMMIT_RANDOM);
output_value_commit = ec_add(nvcv, nvcr);
# Since the new value commit is also a curve point, we'll do the same
# coordinate dance:
constrain_instance(ec_get_x(output_value_commit));
constrain_instance(ec_get_y(output_value_commit));
# The serial of the new coin is derived from the old coin
output_serial = poseidon_hash(SERIAL_PREFIX, input_secret_key, input_serial);
# The secret key of the new coin is derived from old coin
output_secret_key = poseidon_hash(SECRET_PREFIX, input_secret_key);
output_pub = ec_mul_base(output_secret_key, NULLIFIER_K);
output_pub_x = ec_get_x(output_pub);
output_pub_y = ec_get_y(output_pub);
# Poseidon hash of the new coin
# In here we set the new epoch as ZERO, thus removing a
# potentially existing timelock.
output_coin = poseidon_hash(
output_pub_x,
output_pub_y,
output_value,
ZERO,
output_serial,
);
constrain_instance(output_coin);
# ============================
# Constrain lottery parameters
# ============================
# Coin y, constructed with the old serial for seeding:
seed = poseidon_hash(SEED_PREFIX, input_serial);
y = poseidon_hash(seed, mu_y);
constrain_instance(mu_y);
constrain_instance(y);
# Coin rho (seed):
rho = poseidon_hash(seed, mu_rho);
constrain_instance(mu_rho);
constrain_instance(rho);
# Calculate lottery target
term_1 = base_mul(sigma1, input_value);
term_2 = base_mul(sigma2, input_value);
shifted_term_2 = base_mul(term_2, input_value);
target = base_add(term_1, shifted_term_2);
shifted_target = base_add(target, headstart);
constrain_instance(sigma1);
constrain_instance(sigma2);
constrain_instance(headstart);
# Play lottery
less_than_strict(y, shifted_target);
# At this point we've enforced all of our public inputs.
}
Contract logic
get_metadata()
In the consensus_proposal_get_metadata_v1
function, we gather
the necessary metadata that we use to verify the ZK proof and the
transaction signature. Inside this function, we also verify the
VRF proof executed by the proposer using a deterministic input and
the proposer's revealed public key. This public key is derived from
the input (burned) coin in ZK and is also used to sign the entire
transaction.
process_instruction()
In the consensus_proposal_process_instruction_v1
function, we
perform the state transition. We enforce that:
- The timelock of the burned coin has passed and the coin is eligible to compete
- The Merkle inclusion proof of the burned coin is valid
- The revealed nullifier of the burned coin has not been seen before
- The value commitments match, this is done as
input+reward=output
- The newly minted coin was not seen before
If these checks pass, we create a state update with the burned nullifier and the minted 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_proposal_process_update_v1
function. This takes the state update produced by
consensus_proposal_process_instruction_v1
and appends the new
nullifier to the set of seen nullifiers, adds the minted coin to the
set of coins and appends it to the Merkle tree of all coins in the
consensus state.