Skip to content
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
1 change: 1 addition & 0 deletions contract/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ dusk-poseidon = "=0.42.0-rc.0"
dusk-bytes = "=0.1.7"
dusk-vm = "=1.6.0"
dusk-core = { version = "=1.6.0", features = ["zk"] }
sha2 = { version = "0.10.8", default-features = false }
11 changes: 11 additions & 0 deletions contract/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ cargo test --release --test license_contract

The build script first tries to download the Dusk trusted setup and verify its SHA-256 hash. If the download is unavailable it generates local setup material so tests can run, but those generated keys are not deployment-ready.

The build requires Cargo's `OUT_DIR` and writable output paths for generated
artifacts. Missing build environment or artifact-write failures are fatal build
errors.

## Reverts

Contract entrypoints use `panic!` to reject invalid calls, which Dusk VM treats
as contract reverts. Capacity guards such as a full license tree follow the
same revert model as malformed public inputs, stale roots, duplicate
nullifiers, and failed proof verification.

## License

This project is licensed under the [Mozilla Public License 2.0](../LICENSE).
181 changes: 110 additions & 71 deletions contract/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use sha2::{Digest, Sha256};
use std::env;
use std::fs::{self, File};
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use zk_citadel::circuit;

static LABEL: &[u8; 12] = b"dusk-network";
Expand All @@ -22,90 +22,115 @@ const CRS_17_HASH: &str = "6161605616b62356cf09fa28252c672ef53b2c8489ad5f81d87af
const PROVER_PATH: &str = "../target/prover";
const VERIFIER_PATH: &str = "../target/verifier";
const CIRCUIT_MARKER_PATH: &str = "../target/license-circuit-build-id";
const CIRCUIT_BUILD_ID: &str = "zk-citadel-license-circuit";
const OUT_DIR_VERIFIER_FILENAME: &str = "license_verifier";
const CIRCUIT_SOURCE_FINGERPRINT_DOMAIN: &[u8] = b"zk-citadel-license-circuit-source-fingerprint";
const CIRCUIT_SOURCE_PATHS: &[(&str, &str)] = &[
("core/Cargo.toml", "../core/Cargo.toml"),
("core/src/helpers.rs", "../core/src/helpers.rs"),
("core/src/signatures.rs", "../core/src/signatures.rs"),
("core/src/zk/circuit.rs", "../core/src/zk/circuit.rs"),
("core/src/zk/gadgets.rs", "../core/src/zk/gadgets.rs"),
];

#[tokio::main]
async fn main() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what are the advantages of all these changes to the contract built?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what are the advantages of all these changes to the contract built?

Prior setup files could be reused after circuit source changes because the marker was static. These changes make Cargo track the circuit sources, regenerating the prover/verifier together, embedding the verifier produced by the build and expose the fingerprints so wallets can reject mismatched artifacts.

It gives stronger guarantees

if setup_material_stale() {
let response = reqwest::get(CRS_URL).await;

match response {
Ok(pp_bytes) => {
// If setup didn't exist locally, we download the setup again from server
let pp_bytes = pp_bytes.bytes().await.unwrap();
let mut hasher = Sha256::new();
hasher.update(pp_bytes.clone());
let hash = format!("{:x}", hasher.finalize());

// We check the file integrity
assert_eq!(hash, CRS_17_HASH);

let pp = PublicParameters::from_slice(pp_bytes.to_vec().as_slice())
.expect("Creating PublicParameters from slice failed.");

// Compile the license circuit
let (prover, verifier) = Compiler::compile::<circuit::LicenseCircuit>(&pp, LABEL)
.expect("failed to compile circuit");

// Write prover key to disk
let mut file = File::create(PROVER_PATH).unwrap();
file.write_all(&prover.to_bytes()).unwrap();

// Write verifier key to disk
let mut file = File::create(VERIFIER_PATH).unwrap();
file.write_all(&verifier.to_bytes()).unwrap();
write_circuit_marker();

info!("Local trusted setup not found, a new one was downloaded.");
}
Err(_e) => {
// If download fails, we create a setup from scratch
let pp = PublicParameters::setup(1 << circuit::CAPACITY, &mut OsRng).unwrap();

// Compile the license circuit
let (prover, verifier) = Compiler::compile::<circuit::LicenseCircuit>(&pp, LABEL)
.expect("failed to compile circuit");

// Write prover key to disk
let mut file = File::create(PROVER_PATH).unwrap();
file.write_all(&prover.to_bytes()).unwrap();

// Write verifier key to disk
let mut file = File::create(VERIFIER_PATH).unwrap();
file.write_all(&verifier.to_bytes()).unwrap();
write_circuit_marker();

warn!(
"Download of trusted setup from server failed. A new one was generated from scratch. USE AT YOUR OWN RISK."
);
}
}
emit_rerun_directives();

let circuit_build_id = circuit_build_id();
let response = reqwest::get(CRS_URL).await;
let verifier_bytes = match response {
Ok(pp_bytes) => compile_from_trusted_crs(pp_bytes, &circuit_build_id).await,
Err(_e) => compile_from_fresh_setup(&circuit_build_id),
};

write_metadata_hashes(&circuit_build_id, &verifier_bytes);
}

fn emit_rerun_directives() {
std::println!("cargo:rerun-if-changed=build.rs");
for (_, disk_path) in CIRCUIT_SOURCE_PATHS {
std::println!("cargo:rerun-if-changed={disk_path}");
}
}

async fn compile_from_trusted_crs(pp_bytes: reqwest::Response, circuit_build_id: &str) -> Vec<u8> {
let pp_bytes = pp_bytes.bytes().await.unwrap();
let mut hasher = Sha256::new();
hasher.update(pp_bytes.clone());
let hash = format!("{:x}", hasher.finalize());

// We check the file integrity
assert_eq!(hash, CRS_17_HASH);

write_metadata_hashes();
let pp = PublicParameters::from_slice(pp_bytes.to_vec().as_slice())
.expect("Creating PublicParameters from slice failed.");
let verifier_bytes = compile_and_write_setup_material(&pp, circuit_build_id);

info!("License circuit setup material was regenerated from the trusted CRS.");
verifier_bytes
}

fn setup_material_stale() -> bool {
if !(Path::new(PROVER_PATH).exists()) || !(Path::new(VERIFIER_PATH).exists()) {
return true;
fn compile_from_fresh_setup(circuit_build_id: &str) -> Vec<u8> {
// If download fails, we create a setup from scratch
let pp = PublicParameters::setup(1 << circuit::CAPACITY, &mut OsRng).unwrap();
let verifier_bytes = compile_and_write_setup_material(&pp, circuit_build_id);

warn!(
"Download of trusted setup from server failed. A new one was generated from scratch. USE AT YOUR OWN RISK."
);
verifier_bytes
}

fn compile_and_write_setup_material(pp: &PublicParameters, circuit_build_id: &str) -> Vec<u8> {
let (prover, verifier) =
Compiler::compile::<circuit::LicenseCircuit>(pp, LABEL).expect("failed to compile circuit");
let prover_bytes = prover.to_bytes();
let verifier_bytes = verifier.to_bytes();

// Keep the legacy target paths for wallets/tests, but embed the verifier
// from OUT_DIR so deployed bytecode cannot observe a stale target file.
let mut file = File::create(PROVER_PATH).unwrap();
file.write_all(&prover_bytes).unwrap();

let mut file = File::create(VERIFIER_PATH).unwrap();
file.write_all(&verifier_bytes).unwrap();

let out_dir_verifier = out_dir().join(OUT_DIR_VERIFIER_FILENAME);
fs::write(out_dir_verifier, &verifier_bytes).expect("OUT_DIR verifier should be written");

write_circuit_marker(circuit_build_id);
verifier_bytes
}

fn circuit_build_id() -> String {
let mut hasher = Sha256::new();
hasher.update(CIRCUIT_SOURCE_FINGERPRINT_DOMAIN);

for (canonical_path, disk_path) in CIRCUIT_SOURCE_PATHS {
let source = fs::read(disk_path)
.unwrap_or_else(|error| panic!("failed to read {disk_path}: {error}"));
hasher.update((*canonical_path).as_bytes());
hasher.update((source.len() as u64).to_le_bytes());
hasher.update(source);
}

fs::read_to_string(CIRCUIT_MARKER_PATH)
.map(|marker| marker.trim() != CIRCUIT_BUILD_ID)
.unwrap_or(true)
format!("{:x}", hasher.finalize())
}

fn write_circuit_marker() {
fs::write(CIRCUIT_MARKER_PATH, CIRCUIT_BUILD_ID).expect("circuit marker should be written");
fn write_circuit_marker(circuit_build_id: &str) {
let prover_hash = file_sha256_hex(PROVER_PATH).expect("prover hash should be computed");
let verifier_hash = file_sha256_hex(VERIFIER_PATH).expect("verifier hash should be computed");
let marker = format!(
"circuit_build_id={circuit_build_id}\nprover_hash={prover_hash}\nverifier_hash={verifier_hash}\n"
);
fs::write(CIRCUIT_MARKER_PATH, marker).expect("circuit marker should be written");
}

fn write_metadata_hashes() {
let verifier = fs::read(VERIFIER_PATH).expect("verifier key should exist after build");
let verifier_key_hash = scalar_from_sha256(b"CITADEL_VERIFIER_KEY_HASH_V1", &verifier);
let circuit_hash = scalar_from_sha256(b"CITADEL_CIRCUIT_HASH_V1", CIRCUIT_BUILD_ID.as_bytes());
fn write_metadata_hashes(circuit_build_id: &str, verifier: &[u8]) {
let verifier_key_hash = scalar_from_sha256(b"CITADEL_VERIFIER_KEY_HASH_V1", verifier);
let circuit_hash = scalar_from_sha256(b"CITADEL_CIRCUIT_HASH_V1", circuit_build_id.as_bytes());

let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR should be set"));
let generated = out_dir.join("metadata_hashes.rs");
let generated = out_dir().join("metadata_hashes.rs");
let contents = format!(
"const VERIFIER_KEY_HASH: BlsScalar = BlsScalar::from_raw({verifier_key_hash:?});\n\
const CIRCUIT_HASH: BlsScalar = BlsScalar::from_raw({circuit_hash:?});\n",
Expand All @@ -114,6 +139,10 @@ fn write_metadata_hashes() {
fs::write(generated, contents).expect("metadata hash constants should be written");
}

fn out_dir() -> PathBuf {
PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR should be set"))
}

fn scalar_from_sha256(domain: &[u8], bytes: &[u8]) -> [u64; 4] {
let mut hasher = Sha256::new();
hasher.update(domain);
Expand All @@ -131,3 +160,13 @@ fn scalar_from_sha256(domain: &[u8], bytes: &[u8]) -> [u64; 4] {

limbs
}

fn file_sha256_hex(path: &str) -> std::io::Result<String> {
fs::read(path).map(|bytes| sha256_hex(&bytes))
}

fn sha256_hex(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
format!("{:x}", hasher.finalize())
}
2 changes: 1 addition & 1 deletion contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub use license_types::{
UseLicenseArg,
};

const VD_LICENSE_CIRCUIT: &[u8] = include_bytes!("../../target/verifier");
const VD_LICENSE_CIRCUIT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/license_verifier"));

/// Verifier data for the `License` circuit.
#[allow(dead_code)]
Expand Down
46 changes: 31 additions & 15 deletions contract/tests/license_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use dusk_bytes::Serializable;
use rand::rngs::StdRng;
use rand::{CryptoRng, RngCore, SeedableRng};
use rkyv::{Deserialize, Infallible, check_archived_root};
use sha2::{Digest, Sha256};
use zk_citadel::{License, LicenseOptions, LicenseOrigin, SessionCookie, circuit, gadgets};

const PROVER_BYTES: &[u8] = include_bytes!("../../target/prover");
Expand Down Expand Up @@ -90,9 +91,9 @@ fn initialize() -> Session {
}

/// Deserializes license, panics if deserialization fails.
fn deserialise_license(v: &Vec<u8>) -> License {
fn deserialise_license(v: &[u8]) -> License {
let response_data =
check_archived_root::<License>(v.as_slice()).expect("License should deserialize correctly");
check_archived_root::<License>(v).expect("License should deserialize correctly");
let license: License = response_data
.deserialize(&mut Infallible)
.expect("Infallible");
Expand All @@ -101,14 +102,11 @@ fn deserialise_license(v: &Vec<u8>) -> License {

/// Finds owned license in a collection of licenses.
/// It searches in a reverse order to return a newest license.
fn find_owned_license(
sk_user: &SecretKey,
licenses: &Vec<(u64, Vec<u8>)>,
) -> Option<(u64, License)> {
fn find_owned_license(sk_user: &SecretKey, licenses: &[(u64, Vec<u8>)]) -> Option<(u64, License)> {
for (pos, license) in licenses.iter().rev() {
let license = deserialise_license(&license);
let license = deserialise_license(license);
if ViewKey::from(sk_user).owns(&license.lsa) {
return Some((pos.clone(), license));
return Some((*pos, license));
}
}
None
Expand Down Expand Up @@ -169,8 +167,7 @@ fn license_issue_get_merkle() {
u64::MAX,
feeder,
)
.expect("Querying of the licenses should succeed")
.data;
.expect("Querying of the licenses should succeed");

let pos_license_pairs: Vec<(u64, Vec<u8>)> = receiver
.iter()
Expand Down Expand Up @@ -237,8 +234,7 @@ fn multiple_licenses_issue_get_merkle() {
u64::MAX,
feeder,
)
.expect("Querying of the licenses should succeed")
.data;
.expect("Querying of the licenses should succeed");

let pos_license_pairs: Vec<(u64, Vec<u8>)> = receiver
.iter()
Expand Down Expand Up @@ -278,7 +274,10 @@ fn metadata_and_info_track_state() {
assert_eq!(metadata.protocol_version, BlsScalar::one());
assert_eq!(metadata.chain_id, BlsScalar::zero());
assert_eq!(metadata.contract_id, BlsScalar::zero());
assert_ne!(metadata.verifier_key_hash, BlsScalar::zero());
assert_eq!(
Comment thread
HDauven marked this conversation as resolved.
metadata.verifier_key_hash,
metadata_scalar_from_sha256(b"CITADEL_VERIFIER_KEY_HASH_V1", VERIFIER_BYTES)
);
assert_ne!(metadata.circuit_hash, BlsScalar::zero());
assert_eq!(metadata.merkle_arity, 4);
assert_eq!(metadata.merkle_depth, circuit::DEPTH as u32);
Expand Down Expand Up @@ -380,6 +379,24 @@ fn metadata_and_info_track_state() {
assert!(opening.is_some());
}

fn metadata_scalar_from_sha256(domain: &[u8], bytes: &[u8]) -> BlsScalar {
let mut hasher = Sha256::new();
hasher.update(domain);
hasher.update((bytes.len() as u64).to_le_bytes());
hasher.update(bytes);
let digest = hasher.finalize();

let mut scalar_bytes = [0u8; 32];
scalar_bytes[..31].copy_from_slice(&digest[..31]);

let mut limbs = [0u64; 4];
for (i, byte) in scalar_bytes.iter().enumerate() {
limbs[i / 8] |= (*byte as u64) << ((i % 8) * 8);
}

BlsScalar::from_raw(limbs)
}

#[test]
fn issue_license_rejects_invalid_and_duplicate_public_keys() {
let rng = &mut StdRng::seed_from_u64(0xcafe);
Expand Down Expand Up @@ -585,8 +602,7 @@ fn use_license_get_session() {
u64::MAX,
feeder,
)
.expect("Querying the license should succeed")
.data;
.expect("Querying the license should succeed");

let pos_license_pairs: Vec<(u64, Vec<u8>)> = receiver
.iter()
Expand Down
Loading
Loading