Skip to content
This repository has been archived by the owner on Jun 6, 2024. It is now read-only.

zkvm: nonce units [WIP] #317

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions zkvm/src/blockchain/block.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use merlin::Transcript;

use super::super::utreexo;
use crate::{MerkleTree, Tx, TxID};
use crate::{utreexo, MerkleTree, Predicate, Tx, TxID};

/// Identifier of the block, computed as a hash of the `BlockHeader`.
#[derive(Clone, Copy, PartialEq)]
Expand Down Expand Up @@ -33,6 +32,8 @@ pub struct BlockHeader {
pub struct Block {
/// Block header.
pub header: BlockHeader,
/// List of nit allocations
pub nits: Vec<(u64, Predicate)>,
/// List of transactions.
pub txs: Vec<Tx>,
/// UTXO proofs
Expand Down
4 changes: 4 additions & 0 deletions zkvm/src/blockchain/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ pub enum BlockchainError {
#[fail(display = "Inconsistent data in the block header.")]
InconsistentHeader,

/// Occurs when the block allocates incorrect amount of nits.
#[fail(display = "Invalid allocation of nits in the block.")]
InvalidNitAllocation,

/// Occurs when extension field is non-empty in v1 blocks.
#[fail(display = "Extension field must be empty in v1 blocks.")]
IllegalExtension,
Expand Down
1 change: 1 addition & 0 deletions zkvm/src/blockchain/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

mod block;
mod errors;
pub mod nits;
mod state;

#[cfg(test)]
Expand Down
112 changes: 112 additions & 0 deletions zkvm/src/blockchain/nits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//! "Nonce units", aka "nits" are built-in assets used to provide uniqueness anchors to the ZkVM transactions.
//! Nits have a special all-zero flavor to domain-separate them from all issued assets.
//! Nits are introduced gradually, so they can always be available
//! to new users for mixing into their transactions.
//!
//! Issuance is 1 nit per second, using an exponentially decreasing schedule
//! in order to cap the total amount to fit under u64.
//! 1 millionth of a nit is simply called "micro nit" or "unit".

use curve25519_dalek::scalar::Scalar;
use merlin::Transcript;

use crate::{Anchor, Commitment, Contract, PortableItem, Predicate, Value};

/// Interval of halving the amount of issued nits, in ms.
pub const HALVING_INTERVAL: u64 = 4 * 365 * 24 * 3600 * 1000;

/// Number of units issued per millisecond, before halvings.
/// This translates to 1 nit per second.
pub const UNITS_PER_MS: u64 = 1000;

/// Returns amount eligible for circulation between
/// the initial time and the current time, in units.
pub fn circulation(initial_time_ms: u64, current_time_ms: u64) -> u64 {
if current_time_ms < initial_time_ms {
return 0;
}

let mut interval_ms = current_time_ms - initial_time_ms;
let mut circulation = 0u64;
let mut halvings = 0u64;

while interval_ms > 0 {
let increment_ms = if interval_ms > HALVING_INTERVAL {
interval_ms -= HALVING_INTERVAL;
HALVING_INTERVAL
} else {
let tmp = interval_ms;
interval_ms = 0;
tmp
};
if halvings < 64 {
circulation += (increment_ms * UNITS_PER_MS) >> halvings;
halvings += 1;
}
}
circulation
}

/// Returns amount eligible for the block created between
/// the `prev_time_ms` and `new_time_ms`.
pub fn block_allowance(initial_time_ms: u64, prev_time_ms: u64, new_time_ms: u64) -> u64 {
circulation(initial_time_ms, new_time_ms) - circulation(initial_time_ms, prev_time_ms)
}

/// Creates a new contract with a given quantity of nits.
pub fn make_nit_contract(
qty: u64,
predicate: Predicate,
anchoring_transcript: &mut Transcript,
) -> Contract {
Contract::new(
predicate,
vec![PortableItem::Value(Value {
qty: Commitment::unblinded(Scalar::from(qty)),
flv: Commitment::unblinded(Scalar::zero()),
})],
Anchor::nit_anchor(anchoring_transcript),
)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn zero_circulation() {
assert_eq!(circulation(0, 0), 0);
assert_eq!(circulation(2, 1), 0);
}

#[test]
fn simple_circulation() {
assert_eq!(circulation(0, 1), UNITS_PER_MS);
assert_eq!(circulation(100, 101), UNITS_PER_MS);
}

#[test]
fn halving() {
assert_eq!(block_allowance(0, 100, 101), UNITS_PER_MS);
assert_eq!(
block_allowance(0, HALVING_INTERVAL, HALVING_INTERVAL + 1),
UNITS_PER_MS / 2
);
assert_eq!(
block_allowance(0, HALVING_INTERVAL + 1, HALVING_INTERVAL + 2),
UNITS_PER_MS / 2
);
assert_eq!(
block_allowance(0, 2 * HALVING_INTERVAL, 2 * HALVING_INTERVAL + 1),
UNITS_PER_MS / 4
);
assert_eq!(
block_allowance(0, 3 * HALVING_INTERVAL, 3 * HALVING_INTERVAL + 1),
UNITS_PER_MS / 8
);
assert_eq!(
block_allowance(0, 4 * HALVING_INTERVAL, 4 * HALVING_INTERVAL + 1),
UNITS_PER_MS / 16
);
}
}
84 changes: 77 additions & 7 deletions zkvm/src/blockchain/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ use core::borrow::Borrow;

use super::block::{Block, BlockHeader, BlockID};
use super::errors::BlockchainError;
use super::nits;
use crate::utreexo::{self, Catchup, Forest, WorkForest};
use crate::{ContractID, MerkleTree, Tx, TxEntry, TxHeader, VerifiedTx, Verifier};
use crate::{
Anchor, ContractID, MerkleTree, Predicate, Tx, TxEntry, TxHeader, VerifiedTx, Verifier,
};

/// State of the blockchain node.
#[derive(Clone)]
pub struct BlockchainState {
/// Initial block of the given network.
pub initial_id: BlockID,
/// Initial block header of the given network.
pub initial: BlockHeader,
/// Latest block header in the chain.
pub tip: BlockHeader,
/// The utreexo state.
Expand Down Expand Up @@ -44,7 +47,7 @@ impl BlockchainState {

let tip = BlockHeader::make_initial(timestamp_ms, utreexo.root());
let state = BlockchainState {
initial_id: tip.id(),
initial: tip.clone(),
tip,
utreexo,
catchup,
Expand All @@ -55,14 +58,27 @@ impl BlockchainState {

/// Applies the block to the current state and returns a new one.
pub fn apply_block(
&mut self,
&self,
block: &Block,
bp_gens: &BulletproofGens,
) -> Result<BlockchainState, BlockchainError> {
check_block_header(&block.header, &self.tip)?;

check_nit_allocation(
self,
block.header.timestamp_ms,
block.nits.iter().map(|(q, _)| *q),
)?;

let mut work_forest = self.utreexo.work_forest();

// Apply nit contracts to the utreexo
let nit_proofs = apply_nits(
block.header.prev,
block.nits.iter().map(|(qty, pred)| (*qty, pred.clone())),
&mut work_forest,
);

let txroot = apply_txs(
block.header.version,
block.header.timestamp_ms,
Expand All @@ -83,7 +99,7 @@ impl BlockchainState {
}

Ok(BlockchainState {
initial_id: self.initial_id,
initial: self.initial.clone(),
tip: block.header.clone(),
utreexo: new_forest,
catchup: new_catchup,
Expand All @@ -97,6 +113,7 @@ impl BlockchainState {
block_version: u64,
timestamp_ms: u64,
ext: Vec<u8>,
nits: Vec<(u64, Predicate)>,
txs: Vec<Tx>,
utxo_proofs: Vec<utreexo::Proof>,
bp_gens: &BulletproofGens,
Expand All @@ -109,9 +126,16 @@ impl BlockchainState {
timestamp_ms > self.tip.timestamp_ms,
BlockchainError::InconsistentHeader,
)?;
check_nit_allocation(self, timestamp_ms, nits.iter().map(|(q, _)| *q))?;

let mut work_forest = self.utreexo.work_forest();

let nit_proofs = apply_nits(
self.tip.id(),
nits.iter().map(|(qty, pred)| (*qty, pred.clone())),
&mut work_forest,
);

let txroot = apply_txs(
block_version,
timestamp_ms,
Expand All @@ -123,6 +147,12 @@ impl BlockchainState {

let (new_forest, new_catchup) = work_forest.normalize();

// TBD: update nit proofs with new_catchup
// Need to keep around contract ids
// let nit_proofs = nit_proofs.into_iter().map(|proof| {
// new_catchup.update_proof()
// })

let utxoroot = new_forest.root();

let new_block = Block {
Expand All @@ -135,12 +165,13 @@ impl BlockchainState {
utxoroot,
ext,
},
nits,
txs,
all_utxo_proofs: utxo_proofs,
};

let new_state = BlockchainState {
initial_id: self.initial_id,
initial: self.initial.clone(),
tip: new_block.header.clone(),
utreexo: new_forest,
catchup: new_catchup,
Expand Down Expand Up @@ -188,6 +219,23 @@ fn apply_tx<P: Borrow<utreexo::Proof>>(
Ok(verified_tx)
}

/// Applies a list of new nit allocations to the state and
/// returns an iterator of the corresponding utxo proofs.
fn apply_nits(
prev_id: BlockID,
nits: impl IntoIterator<Item = (u64, Predicate)>,
mut work_forest: &mut WorkForest<ContractID>,
) -> Vec<utreexo::Proof> {
let mut transcript = Anchor::create_nit_anchoring_transcript(prev_id.0);

nits.into_iter()
.map(|(qty, pred)| {
let contract = nits::make_nit_contract(qty, pred, &mut transcript);
work_forest.insert(&contract.id())
})
.collect::<Vec<_>>()
}

/// Applies a list of transactions to the state and returns the txroot.
fn apply_txs<T: Borrow<Tx>, P: Borrow<utreexo::Proof>>(
block_version: u64,
Expand Down Expand Up @@ -217,6 +265,28 @@ fn apply_txs<T: Borrow<Tx>, P: Borrow<utreexo::Proof>>(
Ok(MerkleTree::root(b"ZkVM.txroot", &txids))
}

/// Checks that nits allocation is not exceeding the limit for the current blockchain state.
fn check_nit_allocation(
state: &BlockchainState,
new_timestamp_ms: u64,
allocations: impl Iterator<Item = u64>,
) -> Result<(), BlockchainError> {
let allowance = nits::block_allowance(
state.initial.timestamp_ms,
state.tip.timestamp_ms,
new_timestamp_ms,
);
// Note: we are checking each individual value before adding them up to prevent overflow.
let total = allocations.fold(Ok(0u64), |total, qty| {
total.and_then(|total| {
check(qty <= allowance, BlockchainError::InvalidNitAllocation)?;
Ok(total + qty)
})
})?;
check(total <= allowance, BlockchainError::InvalidNitAllocation)?;
Ok(())
}

/// Verifies consistency of the block header with respect to the previous block header.
fn check_block_header(
block_header: &BlockHeader,
Expand Down
2 changes: 1 addition & 1 deletion zkvm/src/blockchain/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ fn test_state_machine() {
};

let (block, future_state) = state
.make_block(1, 1, Vec::new(), vec![tx], proofs, &bp_gens)
.make_block(1, 1, Vec::new(), Vec::new(), vec![tx], proofs, &bp_gens)
.unwrap();

// Apply the block to the state
Expand Down
14 changes: 14 additions & 0 deletions zkvm/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,20 @@ impl Anchor {
t.challenge_bytes(b"new", &mut self.0);
self
}

/// Create a transcript for creating anchors for nit-containing contracts.
pub fn create_nit_anchoring_transcript(seed: [u8; 32]) -> Transcript {
let mut t = Transcript::new(b"ZkVM.nits-anchoring");
t.commit_bytes(b"seed", &seed);
t
}

/// Create another anchor
pub fn nit_anchor(transcript: &mut Transcript) -> Self {
let mut buf = [0u8; 32];
transcript.challenge_bytes(b"nit-anchor", &mut buf);
Self(buf)
}
}

impl ContractID {
Expand Down