diff --git a/README.md b/README.md index 127f3a0..9ae0ee2 100644 --- a/README.md +++ b/README.md @@ -108,3 +108,5 @@ The files in `app_gui/assets` are distributed under the Flaticon License: - `uranium.png`: Uranium icons created by Freepik - Flaticon - `tomato.png`: Tomato icons created by PixelPerfect - Flaticon - `steel-sword.png`: Steel sword icons created by Assia Benkerroum - Flaticon +- `dust.png`: Dust icon created by Freepik +- `gem.png`: Dust icon created by Freepik diff --git a/app_cli/src/lib.rs b/app_cli/src/lib.rs index c1a90b9..b8945f1 100644 --- a/app_cli/src/lib.rs +++ b/app_cli/src/lib.rs @@ -7,7 +7,7 @@ use std::{ use alloy::primitives::Address; use anyhow::{Context as _, Result, anyhow, bail}; -use commitlib::{ItemBuilder, ItemDef, predicates::CommitPredicates}; +use commitlib::{BatchDef, ItemBuilder, ItemDef, predicates::CommitPredicates}; use common::{ payload::{Payload, PayloadProof}, set_from_value, @@ -15,7 +15,8 @@ use common::{ }; use craftlib::{ constants::{ - AXE_BLUEPRINT, AXE_MINING_MAX, AXE_WORK, STONE_BLUEPRINT, STONE_MINING_MAX, WOOD_BLUEPRINT, + AXE_BLUEPRINT, AXE_MINING_MAX, AXE_WORK, DUST_BLUEPRINT, DUST_MINING_MAX, DUST_WORK, + GEM_BLUEPRINT, STONE_BLUEPRINT, STONE_MINING_MAX, STONE_WORK_COST, WOOD_BLUEPRINT, WOOD_MINING_MAX, WOOD_WORK, WOODEN_AXE_BLUEPRINT, WOODEN_AXE_MINING_MAX, WOODEN_AXE_WORK, }, item::{CraftBuilder, MiningRecipe}, @@ -27,7 +28,8 @@ use pod2::{ backends::plonky2::mainpod::Prover, frontend::{MainPod, MainPodBuilder}, middleware::{ - CustomPredicateBatch, DEFAULT_VD_SET, F, Params, Pod, RawValue, VDSet, containers::Set, + CustomPredicateBatch, DEFAULT_VD_SET, F, Key, Params, Pod, RawValue, VDSet, Value, + containers::Set, }, }; use pod2utils::macros::BuildContext; @@ -94,6 +96,7 @@ pub enum Recipe { Wood, Axe, WoodenAxe, + DustGem, } impl Recipe { pub fn list() -> Vec { @@ -105,12 +108,14 @@ impl Recipe { pub enum ProductionType { Mine, Craft, + Disassemble, } impl fmt::Display for ProductionType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let text = match self { ProductionType::Mine => "Mine", ProductionType::Craft => "Craft", + ProductionType::Disassemble => "Disassemble", }; write!(f, "{text}") } @@ -123,6 +128,7 @@ impl Recipe { Self::Wood => ProductionType::Mine, Self::Axe => ProductionType::Craft, Self::WoodenAxe => ProductionType::Craft, + Self::DustGem => ProductionType::Disassemble, } } } @@ -135,6 +141,7 @@ impl FromStr for Recipe { "wood" => Ok(Self::Wood), "axe" => Ok(Self::Axe), "wooden-axe" => Ok(Self::WoodenAxe), + "dust-gem" => Ok(Self::DustGem), _ => Err(anyhow!("unknown recipe {s}")), } } @@ -147,6 +154,7 @@ impl fmt::Display for Recipe { Self::Wood => write!(f, "wood"), Self::Axe => write!(f, "axe"), Self::WoodenAxe => write!(f, "wooden-axe"), + Self::DustGem => write!(f, "dust-gem"), } } } @@ -183,48 +191,109 @@ impl Helper { pow_pod: Option, ) -> anyhow::Result { let prover = &Prover {}; + + // First take care of AllItemsInBatch statement. let mut builder = MainPodBuilder::new(&self.params, &self.vd_set); let mut item_builder = ItemBuilder::new(BuildContext::new(&mut builder, &self.batches), &self.params); + let st_all_items_in_batch = item_builder.st_all_items_in_batch(item_def.batch.clone())?; + + item_builder.ctx.builder.reveal(&st_all_items_in_batch); // 5: Required for committing via CommitCreation + let mut sts_input_item_key = Vec::new(); let mut sts_input_craft = Vec::new(); - for input_item_pod in input_item_pods { - let st_item_key = input_item_pod.pod.pub_statements()[0].clone(); - sts_input_item_key.push(st_item_key); - let st_craft = input_item_pod.pod.pub_statements()[3].clone(); - sts_input_craft.push(st_craft); - item_builder.ctx.builder.add_pod(input_item_pod); - } + let mut input_item_pod = None; - let (st_nullifiers, _nullifiers) = if sts_input_item_key.is_empty() { - item_builder.st_nullifiers(sts_input_item_key).unwrap() - } else { - // The default params don't have enough custom statement verifications to fit - // everything in a single pod, so we split it in two. - let (st_nullifiers, nullifiers) = - item_builder.st_nullifiers(sts_input_item_key).unwrap(); - item_builder.ctx.builder.reveal(&st_nullifiers); - // Propagate sts_input_craft for use in st_craft - for st_input_craft in &sts_input_craft { - item_builder.ctx.builder.reveal(st_input_craft); + // Need more intermediate PODs if there are input items. + if !input_item_pods.is_empty() { + info!("Proving all_items_in_batch_pod..."); + let all_items_in_batch_pod = item_builder.ctx.builder.prove(prover)?; + builder = MainPodBuilder::new(&self.params, &self.vd_set); + item_builder = + ItemBuilder::new(BuildContext::new(&mut builder, &self.batches), &self.params); + // TODO: Use recursion here to be able to make use of more than 2 input PODs. + for input_item_pod in input_item_pods { + let st_item_key = input_item_pod.pod.pub_statements()[0].clone(); + sts_input_item_key.push(st_item_key); + let st_craft = input_item_pod.pod.pub_statements()[4].clone(); + sts_input_craft.push(st_craft); + item_builder.ctx.builder.add_pod(input_item_pod); } - info!("Proving nullifiers_pod..."); - let nullifiers_pod = builder.prove(prover).unwrap(); - nullifiers_pod.pod.verify().unwrap(); + // Prove and proceed. + sts_input_item_key + .iter() + .chain(sts_input_craft.iter()) + .for_each(|st| item_builder.ctx.builder.reveal(st)); + info!("Proving input_item_pod..."); + input_item_pod = Some(item_builder.ctx.builder.prove(prover)?); + + // Take care of nullifiers. builder = MainPodBuilder::new(&self.params, &self.vd_set); item_builder = ItemBuilder::new(BuildContext::new(&mut builder, &self.batches), &self.params); - item_builder.ctx.builder.add_pod(nullifiers_pod); - (st_nullifiers, nullifiers) - }; + + item_builder + .ctx + .builder + .add_pod(input_item_pod.clone().unwrap()); + item_builder + .ctx + .builder + .add_pod(all_items_in_batch_pod.clone()); + all_items_in_batch_pod + .public_statements + .iter() + .for_each(|st| item_builder.ctx.builder.reveal(st)); + } + + // By the way, the default params don't have enough custom statement verifications + // to fit everything in a single pod, hence all the splits. + let (st_nullifiers, _nullifiers) = item_builder.st_nullifiers(sts_input_item_key).unwrap(); + item_builder.ctx.builder.reveal(&st_nullifiers); + item_builder.ctx.builder.reveal(&st_all_items_in_batch); + + info!("Proving nullifiers_et_al_pod..."); + let nullifiers_et_al_pod = builder.prove(prover).unwrap(); + nullifiers_et_al_pod.pod.verify().unwrap(); + + // Start afresh for item POD. + builder = MainPodBuilder::new(&self.params, &self.vd_set); + item_builder = + ItemBuilder::new(BuildContext::new(&mut builder, &self.batches), &self.params); + item_builder.ctx.builder.add_pod(nullifiers_et_al_pod); + + input_item_pod + .iter() + .for_each(|p| item_builder.ctx.builder.add_pod(p.clone())); let mut item_builder = ItemBuilder::new(BuildContext::new(&mut builder, &self.batches), &self.params); - let st_item_def = item_builder.st_item_def(item_def.clone()).unwrap(); + let st_batch_def = item_builder.st_batch_def(item_def.batch.clone())?; + let st_item_def = item_builder.st_item_def(item_def.clone(), st_batch_def.clone())?; let st_item_key = item_builder.st_item_key(st_item_def.clone()).unwrap(); + builder.reveal(&st_item_key); // 0: Required for consuming via Nullifiers + builder.reveal(&st_batch_def); // 1: Required for committing via CommitCreation + builder.reveal(&st_item_def); // 2: Explicit item predicate + builder.reveal(&st_nullifiers); // 3: Required for committing via CommitCreation + builder.reveal(&st_all_items_in_batch); // 4: Required for committing via CommitCreation + + info!("Proving item_pod"); + let start = std::time::Instant::now(); + let item_key_pod = builder.prove(prover).unwrap(); + log::info!("[TIME] pod proving time: {:?}", start.elapsed()); + item_key_pod.pod.verify().unwrap(); + + // new pod + let mut builder = MainPodBuilder::new(&self.params, &self.vd_set); + + builder.add_pod(item_key_pod); + input_item_pod + .iter() + .for_each(|p| builder.add_pod(p.clone())); + let mut craft_builder = CraftBuilder::new(BuildContext::new(&mut builder, &self.batches), &self.params); let st_craft = match recipe { @@ -238,35 +307,50 @@ impl Helper { params: craft_builder.params.clone(), }; craft_builder.ctx.builder.add_pod(main_pow_pod); - craft_builder.st_is_stone(item_def, st_item_def.clone(), st_pow)? + craft_builder.st_is_stone(item_def.clone(), st_item_def.clone(), st_pow)? } - Recipe::Wood => craft_builder.st_is_wood(item_def, st_item_def.clone())?, + Recipe::Wood => craft_builder.st_is_wood(item_def.clone(), st_item_def.clone())?, Recipe::Axe => craft_builder.st_is_axe( - item_def, + item_def.clone(), st_item_def.clone(), sts_input_craft[0].clone(), sts_input_craft[1].clone(), )?, Recipe::WoodenAxe => craft_builder.st_is_wooden_axe( - item_def, + item_def.clone(), st_item_def.clone(), sts_input_craft[0].clone(), sts_input_craft[1].clone(), )?, + Recipe::DustGem => { + let st_stone_disassemble_inputs_outputs = craft_builder + .st_stone_disassemble_inputs_outputs( + sts_input_craft[0].clone(), + sts_input_craft[1].clone(), + item_def.batch.clone(), + )?; + craft_builder.st_stone_disassemble( + st_stone_disassemble_inputs_outputs, + st_batch_def.clone(), + item_def.batch.clone(), + )? + } }; builder.reveal(&st_item_key); // 0: Required for consuming via Nullifiers - builder.reveal(&st_item_def); // 1: Required for committing via CommitCreation - builder.reveal(&st_nullifiers); // 2: Required for committing via CommitCreation - builder.reveal(&st_craft); // 3: App layer predicate + builder.reveal(&st_batch_def); // 1: Required for committing via CommitCreation + builder.reveal(&st_item_def); // 2: Explicit item predicate + builder.reveal(&st_nullifiers); // 3: Required for committing via CommitCreation + builder.reveal(&st_craft); // 4: App layer predicate + builder.reveal(&st_all_items_in_batch); // 5: Required for committing via CommitCreation - info!("Proving item_pod"); + info!("Proving final_pod"); let start = std::time::Instant::now(); - let item_key_pod = builder.prove(prover).unwrap(); + let final_pod = builder.prove(prover).unwrap(); log::info!("[TIME] pod proving time: {:?}", start.elapsed()); - item_key_pod.pod.verify().unwrap(); + final_pod.pod.verify().unwrap(); - Ok(item_key_pod) + Ok(final_pod) } fn make_commitment_pod( @@ -279,13 +363,15 @@ impl Helper { let mut item_builder = ItemBuilder::new(BuildContext::new(&mut builder, &self.batches), &self.params); - let st_item_def = crafted_item.pod.public_statements[1].clone(); - let st_nullifiers = crafted_item.pod.public_statements[2].clone(); + let st_batch_def = crafted_item.pod.public_statements[1].clone(); + let st_nullifiers = crafted_item.pod.public_statements[3].clone(); + let st_all_items_in_batch = crafted_item.pod.public_statements[5].clone(); let st_commit_creation = item_builder.st_commit_creation( - crafted_item.def.clone(), + crafted_item.def.batch.clone(), st_nullifiers, created_items.clone(), - st_item_def, + st_batch_def, + st_all_items_in_batch, )?; builder.reveal(&st_commit_creation); let prover = &Prover {}; @@ -300,11 +386,13 @@ impl Helper { pub fn craft_item( params: &Params, recipe: Recipe, - output: &Path, + outputs: &[PathBuf], inputs: &[PathBuf], -) -> anyhow::Result<()> { +) -> anyhow::Result> { let vd_set = DEFAULT_VD_SET.clone(); let key = rand_raw_value(); + let index = Key::new(format!("{recipe}")); + let keys = [(index.clone(), key.into())].into_iter().collect(); info!("About to craft \"{recipe}\" with key {key:#}"); let (item_def, input_items, pow_pod) = match recipe { Recipe::Stone => { @@ -313,25 +401,19 @@ pub fn craft_item( } let mining_recipe = MiningRecipe::new(STONE_BLUEPRINT.to_string(), &[]); let ingredients_def = mining_recipe - .do_mining(params, key, 0, STONE_MINING_MAX)? + .do_mining(params, keys, 0, STONE_MINING_MAX)? .unwrap(); let start = std::time::Instant::now(); let pow_pod = PowPod::new( params, vd_set.clone(), - 3, // num_iters + STONE_WORK_COST, // num_iters RawValue::from(ingredients_def.dict(params)?.commitment()), )?; log::info!("[TIME] PowPod proving time: {:?}", start.elapsed()); - ( - ItemDef { - ingredients: ingredients_def.clone(), - work: pow_pod.output, - }, - vec![], - Some(pow_pod), - ) + let batch_def = BatchDef::new(ingredients_def.clone(), pow_pod.output); + (vec![ItemDef::new(batch_def, index)?], vec![], Some(pow_pod)) } Recipe::Wood => { if !inputs.is_empty() { @@ -339,16 +421,10 @@ pub fn craft_item( } let mining_recipe = MiningRecipe::new(WOOD_BLUEPRINT.to_string(), &[]); let ingredients_def = mining_recipe - .do_mining(params, key, 0, WOOD_MINING_MAX)? + .do_mining(params, keys, 0, WOOD_MINING_MAX)? .unwrap(); - ( - ItemDef { - ingredients: ingredients_def.clone(), - work: WOOD_WORK, - }, - vec![], - None, - ) + let batch_def = BatchDef::new(ingredients_def.clone(), WOOD_WORK); + (vec![ItemDef::new(batch_def, index)?], vec![], None) } Recipe::Axe => { if inputs.len() != 2 { @@ -361,13 +437,11 @@ pub fn craft_item( &[wood.def.item_hash(params)?, stone.def.item_hash(params)?], ); let ingredients_def = mining_recipe - .do_mining(params, key, 0, AXE_MINING_MAX)? + .do_mining(params, keys, 0, AXE_MINING_MAX)? .unwrap(); + let batch_def = BatchDef::new(ingredients_def.clone(), AXE_WORK); ( - ItemDef { - ingredients: ingredients_def.clone(), - work: AXE_WORK, - }, + vec![ItemDef::new(batch_def, index)?], vec![wood, stone], None, ) @@ -383,29 +457,93 @@ pub fn craft_item( &[wood1.def.item_hash(params)?, wood2.def.item_hash(params)?], ); let ingredients_def = mining_recipe - .do_mining(params, key, 0, WOODEN_AXE_MINING_MAX)? + .do_mining(params, keys, 0, WOODEN_AXE_MINING_MAX)? .unwrap(); + let batch_def = BatchDef::new(ingredients_def.clone(), WOODEN_AXE_WORK); ( - ItemDef { - ingredients: ingredients_def.clone(), - work: WOODEN_AXE_WORK, - }, + vec![ItemDef::new(batch_def, index)?], vec![wood1, wood2], None, ) } + Recipe::DustGem => { + if inputs.len() != 2 { + bail!("{recipe} takes 2 inputs"); + } + let stone1 = load_item(&inputs[0])?; + let stone2 = load_item(&inputs[1])?; + let mining_recipe = MiningRecipe::new( + format!("{DUST_BLUEPRINT}+{GEM_BLUEPRINT}"), + &[stone1.def.item_hash(params)?, stone2.def.item_hash(params)?], + ); + let key_dust = rand_raw_value(); + let key_gem = rand_raw_value(); + let keys = [ + (DUST_BLUEPRINT.into(), key_dust.into()), + (GEM_BLUEPRINT.into(), key_gem.into()), + ] + .into_iter() + .collect(); + let ingredients_def = mining_recipe + .do_mining(params, keys, 0, DUST_MINING_MAX)? // NOTE: GEM_MINING_MAX unused + .unwrap(); + let batch_def = BatchDef::new(ingredients_def.clone(), DUST_WORK); // NOTE: GEM_WORK unused + ( + vec![ + ItemDef::new(batch_def.clone(), DUST_BLUEPRINT.into())?, + ItemDef::new(batch_def, GEM_BLUEPRINT.into())?, + ], + vec![stone1, stone2], + None, + ) + } }; + // create output dir (if there is a parent dir), in case it does not exist + // yet, so that later when creating the file we don't get an error if the + // directory does not exist + if let Some(dir) = outputs[0].parent() { + std::fs::create_dir_all(dir)?; + } + let helper = Helper::new(params.clone(), vd_set); let input_item_pods: Vec<_> = input_items.iter().map(|item| &item.pod).cloned().collect(); - let pod = helper.make_item_pod(recipe, item_def.clone(), input_item_pods, pow_pod)?; - - let crafted_item = CraftedItem { pod, def: item_def }; - let mut file = std::fs::File::create(output)?; - serde_json::to_writer(&mut file, &crafted_item)?; - info!("Stored crafted item mined with recipe {recipe} to {output:?}"); + // TODO: can optimize doing the loop inside 'make_item_pod' to reuse some + // batch computations + let pods: Vec<_> = item_def + .iter() + .map(|item_def_i| { + helper.make_item_pod( + recipe, + item_def_i.clone(), + input_item_pods.clone(), + pow_pod.clone(), + ) + }) + .collect::>>()?; + + let filenames: Vec = item_def + .iter() + .enumerate() + .map(|(i, _)| format! {"{}", outputs[i].display()}.into()) + .collect(); + + for (filename, (def, pod)) in + std::iter::zip(filenames.iter(), std::iter::zip(item_def, pods.iter())) + { + let crafted_item = CraftedItem { + pod: pod.clone(), + def, + }; + let mut file = std::fs::File::create(filename)?; + serde_json::to_writer(&mut file, &crafted_item)?; + info!( + "Stored crafted item mined with recipe {recipe} to {}", + filename.display() + ); + } - Ok(()) + Ok(filenames) } pub async fn commit_item(params: &Params, cfg: &Config, input: &Path) -> anyhow::Result<()> { @@ -427,9 +565,11 @@ pub async fn commit_item(params: &Params, cfg: &Config, input: &Path) -> anyhow: let st_commit_creation = pod.public_statements[0].clone(); let nullifier_set = set_from_value(&st_commit_creation.args()[1].literal()?)?; let nullifiers: Vec = nullifier_set.set().iter().map(|v| v.raw()).collect(); + // Single item => set containing one element + let items = vec![Value::from(crafted_item.def.item_hash(params)?).raw()]; let payload_bytes = Payload { proof: PayloadProof::Plonky2(Box::new(shrunk_main_pod_proof.clone())), - item: RawValue::from(crafted_item.def.item_hash(params)?), + items, created_items_root: RawValue::from(created_items.commitment()), nullifiers, } diff --git a/app_cli/src/main.rs b/app_cli/src/main.rs index 1aeef6e..ab739d5 100644 --- a/app_cli/src/main.rs +++ b/app_cli/src/main.rs @@ -28,8 +28,8 @@ enum Commands { Craft { #[arg(long, value_name = "RECIPE")] recipe: String, - #[arg(long, value_name = "FILE")] - output: PathBuf, + #[arg(long = "output", value_name = "FILE")] + outputs: Vec, #[arg(long = "input", value_name = "FILE")] inputs: Vec, }, @@ -59,11 +59,11 @@ async fn main() -> anyhow::Result<()> { match cli.command { Some(Commands::Craft { recipe, - output, + outputs, inputs, }) => { let recipe = Recipe::from_str(&recipe)?; - craft_item(¶ms, recipe, &output, &inputs)?; + craft_item(¶ms, recipe, &outputs, &inputs)?; } Some(Commands::Commit { input }) => { commit_item(¶ms, &cfg, &input).await?; @@ -74,14 +74,17 @@ async fn main() -> anyhow::Result<()> { // Verify that the item exists on-blob-space: // first get the merkle proof of item existence from the Synchronizer let item = RawValue::from(crafted_item.def.item_hash(¶ms)?); - let item_hex: String = format!("{item:#}"); + + // Single item => set containing one element + // TODO: Generalise. + let item_set_hex: String = format!("{item:#}"); let (epoch, _): (u64, RawValue) = reqwest::blocking::get(format!("{}/created_items_root", cfg.sync_url,))?.json()?; info!("Verifying commitment of item {item:#} via synchronizer at epoch {epoch}"); let (epoch, mtp): (u64, MerkleProof) = reqwest::blocking::get(format!( "{}/created_item/{}", cfg.sync_url, - &item_hex[2..] + &item_set_hex[2..] ))? .json()?; info!("mtp at epoch {epoch}: {mtp:?}"); diff --git a/app_gui/assets/dust.png b/app_gui/assets/dust.png new file mode 100644 index 0000000..f6abc56 Binary files /dev/null and b/app_gui/assets/dust.png differ diff --git a/app_gui/assets/gem.png b/app_gui/assets/gem.png new file mode 100644 index 0000000..290bb44 Binary files /dev/null and b/app_gui/assets/gem.png differ diff --git a/app_gui/src/crafting.rs b/app_gui/src/crafting.rs index d6f6b28..c8673ea 100644 --- a/app_gui/src/crafting.rs +++ b/app_gui/src/crafting.rs @@ -20,6 +20,7 @@ pub enum Process { Wood, Axe, WoodenAxe, + DisassembleStone, Mock(&'static str), } @@ -97,6 +98,63 @@ IsWoodenAxe(item, private: ingredients, inputs, key, work, s1, wood1, wood2) = A // prove the ingredients are correct. IsWood(wood1) IsWood(wood2) +)"#, + ..Default::default() + }; + static ref DISASSEMBLE_STONE_DATA: ProcessData = ProcessData { + description: "Disassemble Stone into Dust+Gem.", + input_ingredients: &["Stone", "Stone"], + outputs: &["Dust", "Gem"], + predicate: r#" +// inputs: 2 Stones +StoneDisassembleInputs(inputs, private: s1, stone1, stone2) = AND( + SetInsert(s1, {}, stone1) + SetInsert(inputs, s1, stone2) + + // prove the ingredients are correct + IsStone(stone1) + IsStone(stone2) +) + +// outputs: 1 Dust, 1 Gem +StoneDisassembleOutputs(batch, keys, + private: k1, dust, gem, _dust_key, _gem_key) = AND( + HashOf(dust, batch, "dust") + HashOf(gem, batch, "gem") + DictInsert(k1, {}, "dust", _dust_key) + DictInsert(keys, k1, "gem", _gem_key) +) + +// helper to have a single predicate for the inputs & outputs +StoneDisassembleInputsOutputs(inputs, batch, keys) = AND ( + StoneDisassembleInputs(inputs) + StoneDisassembleOutputs(batch, keys) +) + +StoneDisassemble(batch, keys, work, + private: inputs, ingredients) = AND( + BatchDef(batch, ingredients, inputs, keys, work) + DictContains(ingredients, "blueprint", "dust+gem") + + StoneDisassembleInputsOutputs(inputs, batch, keys) +) + +// can only obtain Dust from disassembling 2 stones +IsDust(item, private: batch, ingredients, inputs, keys, key, work) = AND( + HashOf(item, batch, "dust") + DictContains(keys, "dust", key) + Equal(work, {}) + + StoneDisassemble(batch, keys, work) +) + +// can only obtain Gem from disassembling 2 stones +IsGem(item, private: batch, ingredients, inputs, keys, key, work) = AND( + HashOf(item, batch, "gem") + DictContains(keys, "gem", key) + Equal(work, {}) + + StoneDisassemble(batch, keys, work) )"#, ..Default::default() }; @@ -400,6 +458,7 @@ impl Process { Self::Wood => Some(Recipe::Wood), Self::Axe => Some(Recipe::Axe), Self::WoodenAxe => Some(Recipe::WoodenAxe), + Self::DisassembleStone => Some(Recipe::DustGem), Self::Mock(_) => None, } } @@ -410,6 +469,7 @@ impl Process { Self::Wood => &WOOD_DATA, Self::Axe => &AXE_DATA, Self::WoodenAxe => &WOODEN_AXE_DATA, + Self::DisassembleStone => &DISASSEMBLE_STONE_DATA, Self::Mock("Destroy") => &DESTROY_DATA, Self::Mock("Tomato") => &TOMATO_DATA, Self::Mock("Steel Sword") => &STEEL_SWORD_DATA, @@ -461,7 +521,7 @@ impl Verb { ], Self::Craft => vec![Axe, WoodenAxe, Mock("Tree House")], Self::Produce => vec![Mock("Tomato"), Mock("Steel Sword")], - Self::Disassemble => vec![Mock("Disassemble-H2O")], + Self::Disassemble => vec![DisassembleStone, Mock("Disassemble-H2O")], Self::Destroy => vec![Mock("Destroy")], } } @@ -489,8 +549,8 @@ pub struct Crafting { pub selected_action: Option<&'static str>, // Input index to item index pub input_items: HashMap, - pub output_filename: String, - pub craft_result: Option>, + pub outputs_filename: Vec, + pub craft_result: Option>>, pub commit_result: Option>, } @@ -677,11 +737,20 @@ impl App { self.crafting.selected_action = selected_action; - // NOTE: If we don't show filenames in the left panel, then we shouldn't ask for a - // filename either. - if self.crafting.output_filename.is_empty() { - self.crafting.output_filename = - format!("{:?}_{}", process, self.items.len() + self.used_items.len()); + // prepare the outputs names that will be used to store the outputs files + let process_outputs = process.data().outputs; + if self.crafting.outputs_filename.is_empty() { + self.crafting.outputs_filename = process_outputs + .iter() + .enumerate() + .map(|(i, process_output)| { + format!( + "{}_{}", + process_output, + self.items.len() + self.used_items.len() + i + ) + }) + .collect(); } ui.add_space(8.0); @@ -711,11 +780,15 @@ impl App { }); if button_craft_clicked { - if self.crafting.output_filename.is_empty() { + if self.crafting.outputs_filename.is_empty() { self.crafting.craft_result = Some(Err(anyhow!("Please enter a filename."))); } else { - let output = - Path::new(&self.cfg.pods_path).join(&self.crafting.output_filename); + let outputs_paths = self + .crafting + .outputs_filename + .iter() + .map(|output| Path::new(&self.cfg.pods_path).join(output)) + .collect(); let input_paths = (0..inputs.len()) .map(|i| { self.crafting @@ -738,7 +811,7 @@ impl App { params: self.params.clone(), pods_path: self.cfg.pods_path.clone(), recipe, - output, + outputs: outputs_paths, input_paths, }) .unwrap(); @@ -749,10 +822,23 @@ impl App { } if button_commit_clicked { - if self.crafting.output_filename.is_empty() { + if self.crafting.outputs_filename.is_empty() { self.crafting.commit_result = Some(Err(anyhow!("Please enter a filename."))); + } else if self.crafting.craft_result.is_none() + || self.crafting.craft_result.as_ref().unwrap().is_err() + { + self.crafting.commit_result = Some(Err(anyhow!( + "The item(s) must first be successfully crafted." + ))); } else { - let input = Path::new(&self.cfg.pods_path).join(&self.crafting.output_filename); + let first_output_filename = &self + .crafting + .craft_result + .as_ref() + .unwrap() + .as_ref() + .unwrap()[0]; + let input = Path::new(&self.cfg.pods_path).join(first_output_filename); self.task_req_tx .send(Request::Commit { params: self.params.clone(), @@ -764,11 +850,15 @@ impl App { } if button_craft_and_commit_clicked { - if self.crafting.output_filename.is_empty() { + if self.crafting.outputs_filename.is_empty() { self.crafting.commit_result = Some(Err(anyhow!("Please enter a filename."))); } else { - let output = - Path::new(&self.cfg.pods_path).join(&self.crafting.output_filename); + let outputs_paths = self + .crafting + .outputs_filename + .iter() + .map(|output| Path::new(&self.cfg.pods_path).join(output)) + .collect(); let input_paths = (0..inputs.len()) .map(|i| { self.crafting @@ -792,7 +882,7 @@ impl App { cfg: self.cfg.clone(), pods_path: self.cfg.pods_path.clone(), recipe, - output, + outputs: outputs_paths, input_paths, }) .unwrap(); diff --git a/app_gui/src/main.rs b/app_gui/src/main.rs index 68c6457..c3b1f06 100644 --- a/app_gui/src/main.rs +++ b/app_gui/src/main.rs @@ -51,8 +51,10 @@ impl eframe::App for App { if let Ok(res) = self.task_res_rx.try_recv() { match res { Response::Craft(r) => { - if let Ok(entry) = &r { - self.load_item(entry, false).unwrap(); + if let Ok(entries) = &r { + entries + .iter() + .for_each(|entry| self.load_item(entry, false).unwrap()); } else { log::error!("{r:?}"); } @@ -66,21 +68,23 @@ impl eframe::App for App { log::error!("{e:?}"); } // Reset filename - self.crafting.output_filename = "".to_string(); + self.crafting.outputs_filename = vec![]; self.crafting.commit_result = Some(r); } Response::CraftAndCommit(r) => { - if let Ok(entry) = &r { - self.load_item(entry, false).unwrap(); + if let Ok(entries) = &r { + entries + .iter() + .for_each(|entry| self.load_item(entry, false).unwrap()); } else { log::error!("{r:?}"); } self.refresh_items().unwrap(); self.crafting.input_items = HashMap::new(); // Reset filename - self.crafting.output_filename = "".to_string(); + self.crafting.outputs_filename = vec![]; self.crafting.craft_result = None; - self.crafting.commit_result = Some(r); + self.crafting.commit_result = r.map(|entries| Ok(entries[0].clone())).ok(); } Response::Null => {} } @@ -227,10 +231,12 @@ impl App { ui.horizontal(|ui| { ui.set_min_height(32.0); // Mock toggle taken into account. - for verb in Verb::list() - .into_iter() - .filter(|v| self.mock_mode || v == &Verb::Gather || v == &Verb::Craft) - { + for verb in Verb::list().into_iter().filter(|v| { + self.mock_mode + || v == &Verb::Gather + || v == &Verb::Craft + || v == &Verb::Disassemble + }) { if ui .selectable_label(Some(verb) == self.crafting.selected_verb, verb.as_str()) .clicked() @@ -280,6 +286,10 @@ impl App { egui::include_image!("../assets/tomato.png") } else if name.starts_with("Steel Sword") { egui::include_image!("../assets/steel-sword.png") + } else if name.starts_with("Dust") { + egui::include_image!("../assets/dust.png") + } else if name.starts_with("Gem") { + egui::include_image!("../assets/gem.png") } else { egui::include_image!("../assets/empty.png") }) diff --git a/app_gui/src/task_system.rs b/app_gui/src/task_system.rs index c5a1b5b..5b06023 100644 --- a/app_gui/src/task_system.rs +++ b/app_gui/src/task_system.rs @@ -19,7 +19,7 @@ pub enum Request { params: Params, pods_path: String, recipe: Recipe, - output: PathBuf, + outputs: Vec, input_paths: Vec, }, Commit { @@ -32,16 +32,16 @@ pub enum Request { cfg: Config, pods_path: String, recipe: Recipe, - output: PathBuf, + outputs: Vec, input_paths: Vec, }, Exit, } pub enum Response { - Craft(Result), + Craft(Result>), Commit(Result), - CraftAndCommit(Result), + CraftAndCommit(Result>), Null, } @@ -55,34 +55,46 @@ pub fn handle_req(task_status: &RwLock, req: Request) -> Response { params, pods_path, recipe, - output, + outputs, input_paths, - } => craft(task_status, ¶ms, pods_path, recipe, output, input_paths), + } => craft( + task_status, + ¶ms, + pods_path, + recipe, + outputs, + input_paths, + ), Request::Commit { params, cfg, input } => commit(task_status, ¶ms, cfg, input), Request::CraftAndCommit { params, cfg, pods_path, recipe, - output, + outputs, input_paths, } => { - if let Response::Craft(Result::Err(e)) = craft( + let craft_res = craft( task_status, ¶ms, pods_path, recipe, - output.clone(), + outputs, input_paths, - ) { - return Response::CraftAndCommit(Result::Err(e)); - }; - let res = commit(task_status, ¶ms, cfg, output.clone()); - let r = match res { - Response::Commit(result) => result, - _ => Err(anyhow!("unexpected response")), - }; - Response::CraftAndCommit(r) + ); + match craft_res { + Response::Craft(Result::Err(e)) => Response::CraftAndCommit(Result::Err(e)), + Response::Craft(Result::Ok(output_paths)) => { + // TODO: Maybe have a separate batch or commitment POD? + let res = commit(task_status, ¶ms, cfg, output_paths[0].clone()); + let r = match res { + Response::Commit(_) => Result::Ok(output_paths), + _ => Err(anyhow!("unexpected response")), + }; + Response::CraftAndCommit(r) + } + _ => Response::CraftAndCommit(Err(anyhow!("unexpected response"))), + } } Request::Exit => Response::Null, } @@ -93,13 +105,13 @@ fn craft( params: &Params, pods_path: String, recipe: Recipe, - output: PathBuf, + outputs: Vec, input_paths: Vec, ) -> Response { set_busy_task(task_status, "Crafting"); let start = std::time::Instant::now(); - let r = craft_item(params, recipe, &output, &input_paths); + let r = craft_item(params, recipe, &outputs, &input_paths); log::info!("[TIME] total Craft Item time: {:?}", start.elapsed()); // move the files of the used inputs into the `used` subdir @@ -123,7 +135,7 @@ fn craft( } task_status.write().unwrap().busy = None; - Response::Craft(r.map(|_| output)) + Response::Craft(r) } fn commit( task_status: &RwLock, diff --git a/commitlib/src/lib.rs b/commitlib/src/lib.rs index 2a15d40..e61433f 100644 --- a/commitlib/src/lib.rs +++ b/commitlib/src/lib.rs @@ -3,6 +3,7 @@ pub mod util; use std::collections::{HashMap, HashSet}; +use anyhow::anyhow; use pod2::middleware::{ EMPTY_HASH, EMPTY_VALUE, Hash, Key, Params, RawValue, Statement, Value, containers::{Dictionary, Set}, @@ -21,7 +22,8 @@ pub const CONSUMED_ITEM_EXTERNAL_NULLIFIER: &str = "consumed item external nulli pub struct IngredientsDef { // These properties are committed on-chain pub inputs: HashSet, - pub key: RawValue, + // TODO: Maybe replace this with a Value -> Value map? + pub keys: HashMap, // These properties are used only by the application layer pub app_layer: HashMap, @@ -31,7 +33,13 @@ impl IngredientsDef { pub fn dict(&self, params: &Params) -> pod2::middleware::Result { let mut map = HashMap::new(); map.insert(Key::from("inputs"), Value::from(self.inputs_set(params)?)); - map.insert(Key::from("key"), Value::from(self.key)); + map.insert( + Key::from("keys"), + Value::from(Dictionary::new( + params.max_depth_mt_containers, + self.keys.clone(), + )?), + ); for (key, value) in &self.app_layer { map.insert(Key::from(key), value.clone()); } @@ -47,21 +55,45 @@ impl IngredientsDef { } } -// Rust-level definition of an item, used to derive its ID (hash). +// Rust-level definition of a batch, used to derive its ID (hash). #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ItemDef { +pub struct BatchDef { pub ingredients: IngredientsDef, pub work: RawValue, } -impl ItemDef { - pub fn item_hash(&self, params: &Params) -> pod2::middleware::Result { +impl BatchDef { + pub fn batch_hash(&self, params: &Params) -> pod2::middleware::Result { Ok(hash_values(&[ Value::from(self.ingredients.dict(params)?), Value::from(self.work), ])) } + pub fn new(ingredients: IngredientsDef, work: RawValue) -> Self { + Self { ingredients, work } + } +} + +// Rust-level definition of an item, used to derive its ID (hash). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ItemDef { + pub batch: BatchDef, + pub index: Key, +} + +impl ItemDef { + pub fn item_key(&self) -> Value { + self.batch.ingredients.keys[&self.index].clone() + } + + pub fn item_hash(&self, params: &Params) -> pod2::middleware::Result { + Ok(hash_values(&[ + Value::from(self.batch.batch_hash(params)?), + Value::from(self.index.hash()), + ])) + } + pub fn nullifier(&self, params: &Params) -> pod2::middleware::Result { Ok(hash_values(&[ Value::from(self.item_hash(params)?), @@ -69,8 +101,12 @@ impl ItemDef { ])) } - pub fn new(ingredients: IngredientsDef, work: RawValue) -> Self { - Self { ingredients, work } + pub fn new(batch: BatchDef, index: Key) -> anyhow::Result { + if batch.ingredients.keys.contains_key(&index) { + Ok(Self { batch, index }) + } else { + Err(anyhow!("Invalid index: {index}")) + } } } @@ -91,31 +127,6 @@ impl<'a> ItemBuilder<'a> { Self { ctx, params } } - // Adds statements to MainPodBilder to represent a generic item based on the - // ItemDef. Includes the following public predicates: ItemDef, ItemKey - // Returns the Statement object for ItemDef for use in further statements. - pub fn st_item_def(&mut self, item_def: ItemDef) -> anyhow::Result { - let ingredients_dict = item_def.ingredients.dict(self.params)?; - let inputs_set = item_def.ingredients.inputs_set(self.params)?; - let item_hash = item_def.item_hash(self.params)?; - - // Build ItemDef(item, ingredients, inputs, key, work) - Ok(st_custom!(self.ctx, - ItemDef() = ( - DictContains(ingredients_dict, "inputs", inputs_set), - DictContains(ingredients_dict, "key", item_def.ingredients.key), - HashOf(item_hash, ingredients_dict, item_def.work) - ))?) - } - - pub fn st_item_key(&mut self, st_item_def: Statement) -> anyhow::Result { - // Build ItemKey(item, key) - Ok(st_custom!(self.ctx, - ItemKey() = ( - st_item_def - ))?) - } - fn st_super_sub_set_recursive( &mut self, inputs_set: Set, @@ -166,6 +177,126 @@ impl<'a> ItemBuilder<'a> { } } + pub fn st_batch_def(&mut self, batch: BatchDef) -> anyhow::Result { + let ingredients_dict = batch.ingredients.dict(self.params)?; + let inputs_set = batch.ingredients.inputs_set(self.params)?; + let batch_hash = batch.batch_hash(self.params)?; + let keys_dict = Dictionary::new( + self.params.max_depth_mt_containers, + batch.ingredients.keys.clone(), + )?; + + // Build BatchDef(item, ingredients, inputs, key, work) + Ok(st_custom!(self.ctx, + BatchDef() = ( + DictContains(ingredients_dict, "inputs", inputs_set), + DictContains(ingredients_dict, "keys", keys_dict), + HashOf(batch_hash, ingredients_dict, batch.work) + ))?) + } + + pub fn st_item_in_batch(&mut self, item_def: ItemDef) -> anyhow::Result { + let item_hash = item_def.item_hash(self.params)?; + let batch_hash = item_def.batch.batch_hash(self.params)?; + let keys_dict = Dictionary::new( + self.params.max_depth_mt_containers, + item_def.batch.ingredients.keys.clone(), + )?; + + // Build ItemInBatch(item, batch) + Ok(st_custom!(self.ctx, + ItemInBatch() = ( + HashOf(item_hash, batch_hash, item_def.index.hash()), + DictContains(keys_dict, item_def.index.name(), item_def.item_key()) + ))?) + } + + // Adds statements to MainPodBilder to represent a generic item based on the + // ItemDef. Includes the following public predicates: ItemDef, ItemKey + // Returns the Statement object for ItemDef for use in further statements. + pub fn st_item_def( + &mut self, + item_def: ItemDef, + st_batch_def: Statement, + ) -> anyhow::Result { + let item_in_batch = self.st_item_in_batch(item_def.clone())?; + + let keys_dict = Dictionary::new( + self.params.max_depth_mt_containers, + item_def.batch.ingredients.keys.clone(), + )?; + + // Build ItemDef(item, ingredients, inputs, key, work) + Ok(st_custom!(self.ctx, + ItemDef() = ( + st_batch_def, + item_in_batch, + DictContains(keys_dict, item_def.index.name(), item_def.item_key()) + ))?) + } + + pub fn st_all_items_in_batch(&mut self, batch_def: BatchDef) -> anyhow::Result { + let batch_hash = batch_def.batch_hash(self.params)?; + + let empty_set = set!(self.params.max_depth_mt_containers)?; + let empty_dict = Dictionary::new(self.params.max_depth_mt_containers, HashMap::new())?; + + // Build AllItemsInBatch(items, batch, keys) + let st_all_items_in_batch_empty = st_custom!(self.ctx, + AllItemsInBatchEmpty(batch = batch_hash) = ( + Equal(&empty_set, EMPTY_VALUE), + Equal(&empty_dict, EMPTY_VALUE) + ))?; + let init_st = st_custom!(self.ctx, + AllItemsInBatch() = ( + st_all_items_in_batch_empty, + Statement::None + ))?; + + let (st_all_items_in_batch, _, _) = batch_def + .ingredients + .keys + .iter() + .try_fold::<_, _, anyhow::Result<_>>( + (init_st, empty_set.clone(), empty_dict.clone()), + |(st_all_items_in_batch_prev, items_prev, keys_prev), (index, key)| { + let item_hash = hash_values(&[batch_hash.into(), index.raw().into()]); + + let mut keys = keys_prev.clone(); + keys.insert(index, key)?; + + let mut items = items_prev.clone(); + items.insert(&item_hash.into())?; + + let st_all_items_in_batch_recursive = st_custom!(self.ctx, + AllItemsInBatchRecursive() = ( + st_all_items_in_batch_prev, + SetInsert(items, items_prev, item_hash), + DictInsert(keys, keys_prev, index.name(), key), + HashOf(item_hash, batch_hash, index.hash()) + ))?; + + let st_all_items_in_batch = st_custom!(self.ctx, + AllItemsInBatch() = ( + Statement::None, + st_all_items_in_batch_recursive + ))?; + + Ok((st_all_items_in_batch, items, keys)) + }, + )?; + + Ok(st_all_items_in_batch) + } + + pub fn st_item_key(&mut self, st_item_def: Statement) -> anyhow::Result { + // Build ItemKey(item, key) + Ok(st_custom!(self.ctx, + ItemKey() = ( + st_item_def + ))?) + } + // Adds statements to MainPodBilder to prove correct nullifiers for a set of // inputs. Returns the private Nullifiers. pub fn st_nullifiers( @@ -226,18 +357,22 @@ impl<'a> ItemBuilder<'a> { // the root to prove that inputs were previously created. pub fn st_commit_creation( &mut self, - item_def: ItemDef, + batch_def: BatchDef, st_nullifiers: Statement, created_items: Set, - st_item_def: Statement, + st_batch_def: Statement, + st_all_items_in_batch: Statement, ) -> anyhow::Result { - let st_inputs_subset = - self.st_super_sub_set(item_def.ingredients.inputs_set(self.params)?, created_items)?; + let st_inputs_subset = self.st_super_sub_set( + batch_def.ingredients.inputs_set(self.params)?, + created_items, + )?; // Build CommitCreation(item, nullifiers, created_items) let st_commit_creation = st_custom!(self.ctx, CommitCreation() = ( - st_item_def.clone(), + st_batch_def, + st_all_items_in_batch, st_inputs_subset, st_nullifiers ))?; @@ -284,16 +419,17 @@ mod tests { item_builder.ctx.builder.add_pod(input_item_key_pod); } - let key = Value::from(key).raw(); + let index: Key = "0".into(); + let key = Value::from(key); let ingredients_def = IngredientsDef { inputs: input_item_hashes, - key, + keys: [(index.clone(), key)].into_iter().collect(), app_layer: HashMap::from([("blueprint".to_string(), Value::from(blueprint))]), }; - let item_def = ItemDef { - ingredients: ingredients_def, - work: Value::from(42).raw(), - }; + + let batch_def = BatchDef::new(ingredients_def, Value::from(42).raw()); + let item_def = ItemDef::new(batch_def.clone(), index).unwrap(); + let (st_nullifiers, _nullifiers) = if sts_item_key.is_empty() { item_builder.st_nullifiers(sts_item_key).unwrap() } else { @@ -313,14 +449,17 @@ mod tests { let item_hash = item_def.item_hash(params).unwrap(); created_items.insert(&Value::from(item_hash)).unwrap(); - let st_item_def = item_builder.st_item_def(item_def.clone()).unwrap(); - + let st_batch_def = item_builder.st_batch_def(batch_def.clone()).unwrap(); + let st_all_items_in_batch = item_builder + .st_all_items_in_batch(batch_def.clone()) + .unwrap(); let _st_commit_creation = item_builder .st_commit_creation( - item_def.clone(), + batch_def.clone(), st_nullifiers, created_items.clone(), - st_item_def, + st_batch_def, + st_all_items_in_batch, ) .unwrap(); @@ -330,7 +469,8 @@ mod tests { let mut builder = MainPodBuilder::new(params, vd_set); let mut item_builder = ItemBuilder::new(BuildContext::new(&mut builder, batches), params); - let st_item_def = item_builder.st_item_def(item_def).unwrap(); + let st_batch_def = item_builder.st_batch_def(batch_def.clone()).unwrap(); + let st_item_def = item_builder.st_item_def(item_def, st_batch_def).unwrap(); let st_item_key = item_builder.st_item_key(st_item_def).unwrap(); item_builder.ctx.builder.reveal(&st_item_key); diff --git a/commitlib/src/predicates.rs b/commitlib/src/predicates.rs index 1687c63..1b116b3 100644 --- a/commitlib/src/predicates.rs +++ b/commitlib/src/predicates.rs @@ -8,12 +8,19 @@ pub struct CommitPredicates { pub subset_of: CustomPredicateRef, pub subset_of_recursive: CustomPredicateRef, + pub batch_def: CustomPredicateRef, + pub item_in_batch: CustomPredicateRef, + pub item_def: CustomPredicateRef, - pub item_key: CustomPredicateRef, + pub all_items_in_batch: CustomPredicateRef, + pub all_items_in_batch_empty: CustomPredicateRef, + pub all_items_in_batch_recursive: CustomPredicateRef, + pub item_key: CustomPredicateRef, pub nullifiers: CustomPredicateRef, pub nullifiers_empty: CustomPredicateRef, pub nullifiers_recursive: CustomPredicateRef, + pub commit_creation: CustomPredicateRef, } @@ -25,6 +32,7 @@ impl CommitPredicates { // 8 arguments per predicate, at most 5 of which are public // 5 statements per predicate let batch_defs = [ + // 1 r#" // Generic recursive construction confirming subset. Relies on the Merkle // tree already requiring unique keys (so no inserts on super) @@ -39,23 +47,77 @@ impl CommitPredicates { SubsetOf(smaller, super) ) - // Prove proper derivation of item ID from defined inputs - // The ingredients dict is explicitly allowed to contain more fields - // for use in item predicates. - ItemDef(item, ingredients, inputs, key, work) = AND( + // Core commitment to a crafting operation: + // batch = a single hash representing all outputs + // ingredients = dict with 2 required fields (inputs, keys), + // but allowing other fields usable by the item layer + // keys = root of a dict containing one key per item + // inputs = root of a set of item IDs of inputs consumed + // work = opaque value (hash) used by item layer for sequential work + BatchDef(batch, ingredients, inputs, keys, work) = AND( DictContains(ingredients, "inputs", inputs) - DictContains(ingredients, "key", key) - HashOf(item, ingredients, work) + DictContains(ingredients, "keys", keys) + HashOf(batch, ingredients, work) + ) + + // Each item in a batch has an index (likely 0..N, but could be any + // value) which must correspond to its key. + // It confirms that the item ID and keys use the same indexes, for + // consistent nullifiers. + ItemInBatch(item, batch, index, keys, private: key) = AND( + HashOf(item, batch, index) + DictContains(keys, index, key) + ) + "#, + // 2 + r#" + // Predicate constructing the ID of one item from a batch without + // any reference to the batch. + // Each item in a batch has an index (possibly 0..N, but could be + // any value) which must correspond to the index of its key. + ItemDef(item, ingredients, inputs, key, work, private: batch, index, keys) = AND( + BatchDef(batch, ingredients, inputs, keys, work) + ItemInBatch(item, batch, index, keys) + DictContains(keys, index, key) ) + // Recursive construction to extract all the individual item IDs + // from a batch. They must be 1:1 with the input keys. The `All` in + // the name means this is a strict predicate, which doesn't allow + // for extra values in sets. Note that `keys` is public here, + // because CommitCrafting needs to match it up against the + // CraftingDef. Note this allows for empty batches. + AllItemsInBatch(items, batch, keys) = OR( + AllItemsInBatchEmpty(items, batch, keys) + AllItemsInBatchRecursive(items, batch, keys) + ) + + AllItemsInBatchEmpty(items, batch, keys) = AND( + Equal(items, {}) + Equal(keys, {}) + // batch is intentionally unconstrained + ) + + AllItemsInBatchRecursive(items, batch, keys, private: prev_items, prev_keys, item, index, key) = AND( + AllItemsInBatch(prev_items, batch, prev_keys) + SetInsert(items, prev_items, item) + DictInsert(keys, prev_keys, index, key) + + // Inlined version of ItemInBatch. Here we need to explicitly + // set all values, and don't want to pay for an extra statement + // to do so. + HashOf(item, batch, index) + ) + "#, + // 3 + &format!( + r#" // Helper to expose just the item and key from ItemId calculation. // This is just the CreatedItem pattern with some of inupts private. ItemKey(item, key, private: ingredients, inputs, work) = AND( ItemDef(item, ingredients, inputs, key, work) ) - "#, - &format!( - r#" + // Derive nullifiers from items (using a recursive foreach construction) // This proves the relationship between an item and its key before using // the key to calculate a nullifier. @@ -77,7 +139,10 @@ impl CommitPredicates { SetInsert(inputs, inputs_prev, input) Nullifiers(nullifiers_prev, inputs_prev) ) - + "# + ), + // 4 + r#" // ZK version of CreatedItem for committing on-chain. // Validator/Logger/Archiver needs to maintain 2 append-only // sets of items and nullifiers. New creating is @@ -85,10 +150,14 @@ impl CommitPredicates { // - item is not already in item set // - all nullifiers are not already in nullifier set // - createdItems is one of the historical item set roots - CommitCreation(item, nullifiers, created_items, - private: ingredients, inputs, key, work) = AND( - // Prove the item hash includes all of its committed properties - ItemDef(item, ingredients, inputs, key, work) + CommitCreation(items, nullifiers, created_items, + private: batch, ingredients, inputs, keys, work) = AND( + // Prove the core crafting operation. + // The batch hash includes all of its committed properties + BatchDef(batch, ingredients, inputs, keys, work) + + // Prove that the item set represents all outputs of this batch. + AllItemsInBatch(items, batch, keys) // Prove all inputs are in the created set SubsetOf(inputs, created_items) @@ -96,8 +165,7 @@ impl CommitPredicates { // Expose nullifiers for all inputs Nullifiers(nullifiers, inputs) ) - "# - ), + "#, ]; let defs = PredicateDefs::new(params, &batch_defs, &[]); @@ -105,12 +173,23 @@ impl CommitPredicates { CommitPredicates { subset_of: defs.predicate_ref_by_name("SubsetOf").unwrap(), subset_of_recursive: defs.predicate_ref_by_name("SubsetOfRecursive").unwrap(), + batch_def: defs.predicate_ref_by_name("BatchDef").unwrap(), + item_in_batch: defs.predicate_ref_by_name("ItemInBatch").unwrap(), + item_def: defs.predicate_ref_by_name("ItemDef").unwrap(), + all_items_in_batch: defs.predicate_ref_by_name("AllItemsInBatch").unwrap(), + all_items_in_batch_empty: defs.predicate_ref_by_name("AllItemsInBatchEmpty").unwrap(), + all_items_in_batch_recursive: defs + .predicate_ref_by_name("AllItemsInBatchRecursive") + .unwrap(), + item_key: defs.predicate_ref_by_name("ItemKey").unwrap(), nullifiers: defs.predicate_ref_by_name("Nullifiers").unwrap(), nullifiers_empty: defs.predicate_ref_by_name("NullifiersEmpty").unwrap(), nullifiers_recursive: defs.predicate_ref_by_name("NullifiersRecursive").unwrap(), + commit_creation: defs.predicate_ref_by_name("CommitCreation").unwrap(), + defs, } } @@ -124,6 +203,6 @@ mod tests { fn test_compile_custom_predicates() { let params = Params::default(); let commit_preds = CommitPredicates::compile(¶ms); - assert!(commit_preds.defs.batches.len() == 2); + assert!(commit_preds.defs.batches.len() == 4); } } diff --git a/common/src/payload.rs b/common/src/payload.rs index 440192f..f54c046 100644 --- a/common/src/payload.rs +++ b/common/src/payload.rs @@ -37,7 +37,7 @@ pub fn read_elems(bytes: &mut impl Read) -> Result<[F; N]> { #[allow(clippy::large_enum_variant)] pub struct Payload { pub proof: PayloadProof, - pub item: RawValue, + pub items: Vec, pub created_items_root: RawValue, pub nullifiers: Vec, } @@ -51,9 +51,17 @@ impl Payload { .write_all(&PAYLOAD_MAGIC.to_le_bytes()) .expect("vec write"); self.proof.write_bytes(&mut buffer); - write_elems(&mut buffer, &self.item.0); write_elems(&mut buffer, &self.created_items_root.0); - assert!(self.nullifiers.len() <= 255); + + assert!(self.items.len() < 256); + buffer + .write_all(&(self.items.len() as u8).to_le_bytes()) + .expect("vec write"); + for item in &self.items { + write_elems(&mut buffer, &item.0); + } + + assert!(self.nullifiers.len() < 256); buffer .write_all(&(self.nullifiers.len() as u8).to_le_bytes()) .expect("vec write"); @@ -76,8 +84,16 @@ impl Payload { let (proof, len) = PayloadProof::from_bytes(bytes, common_data)?; bytes = &bytes[len..]; - let item = RawValue(read_elems(&mut bytes)?); let created_items_root = RawValue(read_elems(&mut bytes)?); + let items_len = { + let mut buffer = [0; 1]; + bytes.read_exact(&mut buffer)?; + u8::from_le_bytes(buffer) + }; + let mut items = Vec::with_capacity(items_len as usize); + for _ in 0..items_len { + items.push(RawValue(read_elems(&mut bytes)?)); + } let nullifiers_len = { let mut buffer = [0; 1]; bytes.read_exact(&mut buffer)?; @@ -89,7 +105,7 @@ impl Payload { } Ok(Self { proof, - item, + items, created_items_root, nullifiers, }) @@ -194,7 +210,14 @@ mod tests { let payload = { let mut builder = MainPodBuilder::new(¶ms, vd_set); - let item = Value::from("dummy_item"); + let items = vec![Value::from("dummy_item").raw()]; + let item_set = Value::from( + Set::new( + params.max_depth_mt_containers, + items.iter().map(|rv| (*rv).into()).collect(), + ) + .unwrap(), + ); let nullifiers = vec![ Value::from(1).raw(), Value::from(2).raw(), @@ -213,7 +236,7 @@ mod tests { .op( true, vec![ - (0, item.clone()), + (0, item_set.clone()), (1, nullifiers_set.clone()), (2, created_items.clone()), ], @@ -237,7 +260,7 @@ mod tests { Payload { proof: PayloadProof::Plonky2(Box::new(shrunk_main_pod_proof.clone())), - item: item.raw(), + items, created_items_root: created_items.raw(), nullifiers, } @@ -257,10 +280,15 @@ mod tests { ) .unwrap(), ); + let item_set = Set::new( + params.max_depth_mt_containers, + payload.items.iter().map(|rv| (*rv).into()).collect(), + ) + .unwrap(); let st = Statement::Custom( pred, vec![ - Value::from(payload.item), + item_set.into(), nullifiers_set, Value::from(payload.created_items_root), ], diff --git a/craftlib/src/constants.rs b/craftlib/src/constants.rs index f5df968..fdbb854 100644 --- a/craftlib/src/constants.rs +++ b/craftlib/src/constants.rs @@ -3,6 +3,7 @@ use pod2::middleware::{EMPTY_VALUE, RawValue}; pub const STONE_BLUEPRINT: &str = "stone"; pub const STONE_MINING_MAX: u64 = 0x0020_0000_0000_0000; pub const STONE_WORK: RawValue = EMPTY_VALUE; +pub const STONE_WORK_COST: usize = 2; pub const WOOD_BLUEPRINT: &str = "wood"; pub const WOOD_MINING_MAX: u64 = 0x0020_0000_0000_0000; @@ -16,3 +17,11 @@ pub const AXE_WORK: RawValue = EMPTY_VALUE; pub const WOODEN_AXE_BLUEPRINT: &str = "wooden-axe"; pub const WOODEN_AXE_MINING_MAX: u64 = 0x0020_0000_0000_0000; pub const WOODEN_AXE_WORK: RawValue = EMPTY_VALUE; + +pub const DUST_BLUEPRINT: &str = "dust"; +pub const DUST_MINING_MAX: u64 = 0x0020_0000_0000_0000; +pub const DUST_WORK: RawValue = EMPTY_VALUE; + +pub const GEM_BLUEPRINT: &str = "gem"; +pub const GEM_MINING_MAX: u64 = 0x0020_0000_0000_0000; +pub const GEM_WORK: RawValue = EMPTY_VALUE; diff --git a/craftlib/src/item.rs b/craftlib/src/item.rs index 4b967ca..62d4320 100644 --- a/craftlib/src/item.rs +++ b/craftlib/src/item.rs @@ -1,29 +1,36 @@ use std::collections::{HashMap, HashSet}; -use commitlib::{IngredientsDef, ItemDef}; +use commitlib::{BatchDef, IngredientsDef, ItemDef}; use log; -use pod2::middleware::{EMPTY_VALUE, Hash, Params, RawValue, Statement, ToFields, Value}; +use pod2::middleware::{ + EMPTY_VALUE, Hash, Key, Params, Statement, ToFields, Value, containers::Dictionary, hash_values, +}; use pod2utils::{macros::BuildContext, set, st_custom}; -use crate::constants::{AXE_BLUEPRINT, STONE_BLUEPRINT, WOOD_BLUEPRINT, WOODEN_AXE_BLUEPRINT}; +use crate::constants::{ + AXE_BLUEPRINT, DUST_BLUEPRINT, GEM_BLUEPRINT, STONE_BLUEPRINT, WOOD_BLUEPRINT, + WOODEN_AXE_BLUEPRINT, +}; // Reusable recipe for an item to be mined, not including the variable // cryptographic values. #[derive(Debug, Clone)] pub struct MiningRecipe { pub inputs: HashSet, + // Q: should it always be a string even for multiple outputs? for now we're + // joining the multiple strings with a '+' pub blueprint: String, } impl MiningRecipe { - pub fn prep_ingredients(&self, key: RawValue, seed: i64) -> IngredientsDef { + pub fn prep_ingredients(&self, keys: HashMap, seed: i64) -> IngredientsDef { let app_layer = HashMap::from([ ("blueprint".to_string(), Value::from(self.blueprint.clone())), ("seed".to_string(), Value::from(seed)), ]); IngredientsDef { inputs: self.inputs.clone(), - key, + keys, app_layer, } } @@ -31,13 +38,13 @@ impl MiningRecipe { pub fn do_mining( &self, params: &Params, - key: RawValue, + keys: HashMap, start_seed: i64, mine_max: u64, ) -> pod2::middleware::Result> { log::info!("Mining..."); for seed in start_seed..=i64::MAX { - let ingredients = self.prep_ingredients(key, seed); + let ingredients = self.prep_ingredients(keys.clone(), seed); let ingredients_hash = ingredients.hash(params)?; let mining_val = ingredients_hash.to_fields(params)[0]; if mining_val.0 <= mine_max { @@ -81,8 +88,8 @@ impl<'a> CraftBuilder<'a> { Ok(st_custom!(self.ctx, IsStone() = ( st_item_def, - Equal(item_def.ingredients.inputs_set(self.params)?, EMPTY_VALUE), - DictContains(item_def.ingredients.dict(self.params)?, "blueprint", STONE_BLUEPRINT), + Equal(item_def.batch.ingredients.inputs_set(self.params)?, EMPTY_VALUE), + DictContains(item_def.batch.ingredients.dict(self.params)?, "blueprint", STONE_BLUEPRINT), st_pow ))?) } @@ -96,9 +103,9 @@ impl<'a> CraftBuilder<'a> { Ok(st_custom!(self.ctx, IsWood() = ( st_item_def, - Equal(item_def.ingredients.inputs_set(self.params)?, EMPTY_VALUE), - DictContains(item_def.ingredients.dict(self.params)?, "blueprint", WOOD_BLUEPRINT), - Equal(item_def.work, EMPTY_VALUE) + Equal(item_def.batch.ingredients.inputs_set(self.params)?, EMPTY_VALUE), + DictContains(item_def.batch.ingredients.dict(self.params)?, "blueprint", WOOD_BLUEPRINT), + Equal(item_def.batch.work, EMPTY_VALUE) ))?) } @@ -135,8 +142,8 @@ impl<'a> CraftBuilder<'a> { Ok(st_custom!(self.ctx, IsAxe() = ( st_item_def, - DictContains(item_def.ingredients.dict(self.params)?, "blueprint", AXE_BLUEPRINT), - Equal(item_def.work, EMPTY_VALUE), + DictContains(item_def.batch.ingredients.dict(self.params)?, "blueprint", AXE_BLUEPRINT), + Equal(item_def.batch.work, EMPTY_VALUE), st_axe_inputs ))?) } @@ -174,11 +181,131 @@ impl<'a> CraftBuilder<'a> { Ok(st_custom!(self.ctx, IsWoodenAxe() = ( st_item_def, - DictContains(item_def.ingredients.dict(self.params)?, "blueprint", WOODEN_AXE_BLUEPRINT), - Equal(item_def.work, EMPTY_VALUE), + DictContains(item_def.batch.ingredients.dict(self.params)?, "blueprint", WOODEN_AXE_BLUEPRINT), + Equal(item_def.batch.work, EMPTY_VALUE), st_wooden_axe_inputs ))?) } + + pub fn st_stone_disassemble_inputs( + &mut self, + st_is_stone1: Statement, + st_is_stone2: Statement, + ) -> anyhow::Result { + let stone1 = st_is_stone1.args()[0].literal().unwrap(); + let stone2 = st_is_stone2.args()[0].literal().unwrap(); + let empty_set = set!(self.params.max_depth_mt_containers).unwrap(); + let mut s1 = empty_set.clone(); + s1.insert(&stone1).unwrap(); + let mut inputs = s1.clone(); + inputs.insert(&stone2).unwrap(); + Ok(st_custom!(self.ctx, + StoneDisassembleInputs() = ( + SetInsert(s1, empty_set, stone1), + SetInsert(inputs, s1, stone2), + st_is_stone1, + st_is_stone2 + ))?) + } + pub fn st_stone_disassemble_outputs( + &mut self, + batch_def: BatchDef, + ) -> anyhow::Result { + let batch_hash = batch_def.batch_hash(self.params)?; + let dust_hash = hash_values(&[batch_hash.into(), DUST_BLUEPRINT.into()]); + let gem_hash = hash_values(&[batch_hash.into(), GEM_BLUEPRINT.into()]); + let dust_key = batch_def.ingredients.keys[&DUST_BLUEPRINT.into()].clone(); + let gem_key = batch_def.ingredients.keys[&GEM_BLUEPRINT.into()].clone(); + + let keys_dict = Dictionary::new( + self.params.max_depth_mt_containers, + batch_def.ingredients.keys.clone(), + )?; + + let empty_dict = Dictionary::new(self.params.max_depth_mt_containers, HashMap::new())?; + let mut k1_dict = empty_dict.clone(); + k1_dict.insert(&DUST_BLUEPRINT.into(), &dust_key)?; + + Ok(st_custom!(self.ctx, + StoneDisassembleOutputs() = ( + HashOf(dust_hash, batch_hash, DUST_BLUEPRINT), + HashOf(gem_hash, batch_hash, GEM_BLUEPRINT), + DictInsert(k1_dict, empty_dict, DUST_BLUEPRINT, dust_key), + DictInsert(keys_dict, k1_dict, GEM_BLUEPRINT, gem_key) + ))?) + } + pub fn st_stone_disassemble_inputs_outputs( + &mut self, + st_is_stone1: Statement, + st_is_stone2: Statement, + batch_def: BatchDef, + ) -> anyhow::Result { + let st_stone_disassemble_inputs = + self.st_stone_disassemble_inputs(st_is_stone1, st_is_stone2)?; + let st_stone_disassemble_outputs = self.st_stone_disassemble_outputs(batch_def)?; + + Ok(st_custom!(self.ctx, + StoneDisassembleInputsOutputs() = ( + st_stone_disassemble_inputs, + st_stone_disassemble_outputs + ))?) + } + pub fn st_is_dust( + &mut self, + item_def: ItemDef, + st_stone_disassemble: Statement, + ) -> anyhow::Result { + let batch_hash = item_def.batch.batch_hash(self.params)?; + let dust_hash = hash_values(&[batch_hash.into(), DUST_BLUEPRINT.into()]); + let keys_dict = Dictionary::new( + self.params.max_depth_mt_containers, + item_def.batch.ingredients.keys.clone(), + )?; + let dust_key = item_def.batch.ingredients.keys[&DUST_BLUEPRINT.into()].clone(); + Ok(st_custom!(self.ctx, + IsDust() = ( + HashOf(dust_hash, batch_hash, DUST_BLUEPRINT), + DictContains(keys_dict, DUST_BLUEPRINT, dust_key), + Equal(item_def.batch.work, EMPTY_VALUE), + st_stone_disassemble + ))?) + } + + pub fn st_is_gem( + &mut self, + item_def: ItemDef, + st_stone_disassemble: Statement, + ) -> anyhow::Result { + let batch_hash = item_def.batch.batch_hash(self.params)?; + let gem_hash = hash_values(&[batch_hash.into(), GEM_BLUEPRINT.into()]); + let keys_dict = Dictionary::new( + self.params.max_depth_mt_containers, + item_def.batch.ingredients.keys.clone(), + )?; + let gem_key = item_def.batch.ingredients.keys[&GEM_BLUEPRINT.into()].clone(); + + Ok(st_custom!(self.ctx, + IsGem() = ( + HashOf(gem_hash, batch_hash, GEM_BLUEPRINT), + DictContains(keys_dict, GEM_BLUEPRINT, gem_key), + Equal(item_def.batch.work, EMPTY_VALUE), + st_stone_disassemble + ))?) + } + + pub fn st_stone_disassemble( + &mut self, + st_stone_disassemble_inputs_outputs: Statement, + st_batch_def: Statement, + batch_def: BatchDef, + ) -> anyhow::Result { + Ok(st_custom!(self.ctx, + StoneDisassemble() = ( + st_batch_def, + DictContains(batch_def.ingredients.dict(self.params)?, "blueprint", format!("{DUST_BLUEPRINT}+{GEM_BLUEPRINT}")), + st_stone_disassemble_inputs_outputs + ))?) + } } #[cfg(test)] @@ -186,14 +313,17 @@ mod tests { use std::{collections::HashMap, sync::Arc}; - use commitlib::{ItemBuilder, ItemDef, predicates::CommitPredicates, util::set_from_hashes}; + use commitlib::{ + BatchDef, ItemBuilder, ItemDef, predicates::CommitPredicates, util::set_from_hashes, + }; use pod2::{ backends::plonky2::mock::mainpod::MockProver, frontend::{MainPod, MainPodBuilder}, lang::parse, middleware::{ CustomPredicateBatch, EMPTY_VALUE, MainPodProver, Params, Pod, RawValue, VDSet, Value, - containers::Set, hash_value, + containers::{Dictionary, Set}, + hash_value, }, }; @@ -220,17 +350,29 @@ mod tests { prover: &dyn MainPodProver, vd_set: &VDSet, ) -> anyhow::Result { + // Prove AllItemsInBatch let mut builder = MainPodBuilder::new(&Default::default(), vd_set); let mut item_builder = ItemBuilder::new(BuildContext::new(&mut builder, batches), params); - let st_item_def = item_builder.st_item_def(item_def.clone())?; + let st_all_items_in_batch = item_builder.st_all_items_in_batch(item_def.batch.clone())?; + item_builder.ctx.builder.reveal(&st_all_items_in_batch); + let all_items_in_batch_pod = item_builder.ctx.builder.prove(prover)?; + + let mut builder = MainPodBuilder::new(&Default::default(), vd_set); + let mut item_builder = ItemBuilder::new(BuildContext::new(&mut builder, batches), params); + let st_batch_def = item_builder.st_batch_def(item_def.batch.clone())?; + let st_item_def = item_builder.st_item_def(item_def.clone(), st_batch_def.clone())?; + item_builder.ctx.builder.reveal(&st_batch_def); item_builder.ctx.builder.reveal(&st_item_def); let st_item_key = item_builder.st_item_key(st_item_def.clone())?; item_builder.ctx.builder.reveal(&st_item_key); + let st_all_items_in_batch = all_items_in_batch_pod.public_statements[0].clone(); + item_builder.ctx.builder.reveal(&st_all_items_in_batch); let st_pow = pow_pod.public_statements[0].clone(); let mut craft_builder = CraftBuilder::new(BuildContext::new(&mut builder, batches), params); craft_builder.ctx.builder.add_pod(pow_pod); + craft_builder.ctx.builder.add_pod(all_items_in_batch_pod); let st_is_stone = craft_builder.st_is_stone(item_def, st_item_def, st_pow)?; craft_builder.ctx.builder.reveal(&st_is_stone); @@ -255,13 +397,19 @@ mod tests { let mut builder = MainPodBuilder::new(&Default::default(), vd_set); // TODO: Consider a more robust lookup for this which doesn't depend on index. - let st_item_def = item_main_pod.public_statements[0].clone(); + let st_batch_def = item_main_pod.public_statements[0].clone(); + let st_all_items_in_batch = item_main_pod.public_statements[3].clone(); builder.add_pod(item_main_pod); let mut item_builder = ItemBuilder::new(BuildContext::new(&mut builder, batches), params); let (st_nullifier, _) = item_builder.st_nullifiers(vec![])?; - let st_commit_creation = - item_builder.st_commit_creation(item_def, st_nullifier, created_items, st_item_def)?; + let st_commit_creation = item_builder.st_commit_creation( + item_def.batch, + st_nullifier, + created_items, + st_batch_def, + st_all_items_in_batch, + )?; builder.reveal(&st_commit_creation); // Prove MainPOD @@ -279,17 +427,20 @@ mod tests { fn test_mine_stone() -> anyhow::Result<()> { let params = Params::default(); let mining_recipe = MiningRecipe::new(STONE_BLUEPRINT.to_string(), &[]); - let key = RawValue::from(0xBADC0DE); + let index: Key = "stone".into(); + let key = Value::from(0xBADC0DE); + let keys: HashMap<_, _> = [(index.clone(), key.clone())].into_iter().collect(); // Seed of 2612=0xA34 is a match with hash 6647892930992163=0x000A7EE9D427E832. // TODO: This test is going to get slower (~2s) whenever the ingredient // dict definition changes. Need a better approach to testing mining. let mine_success = - mining_recipe.do_mining(¶ms, key, STONE_START_SEED, STONE_MINING_MAX)?; + mining_recipe.do_mining(¶ms, keys, STONE_START_SEED, STONE_MINING_MAX)?; assert!(mine_success.is_some()); let ingredients_def = mine_success.unwrap(); - let item_def = ItemDef::new(ingredients_def.clone(), STONE_WORK); + let batch_def = BatchDef::new(ingredients_def.clone(), STONE_WORK); + let item_def = ItemDef::new(batch_def, index)?; let item_hash = item_def.item_hash(¶ms)?; println!( "Mined stone {:?} from ingredients {:?}", @@ -312,16 +463,18 @@ mod tests { let vd_set = &mock_vd_set(); // Mine stone with a selected key. - let key = RawValue::from(0xBADC0DE); + let index: Key = "stone".into(); + let key = Value::from(0xBADC0DE); + let keys: HashMap<_, _> = [(index.clone(), key.clone())].into_iter().collect(); let mining_recipe = MiningRecipe::new(STONE_BLUEPRINT.to_string(), &[]); let ingredients_def = mining_recipe - .do_mining(¶ms, key, STONE_START_SEED, STONE_MINING_MAX)? + .do_mining(¶ms, keys.clone(), STONE_START_SEED, STONE_MINING_MAX)? .unwrap(); let pow_pod = PowPod::new( ¶ms, vd_set.clone(), - 3, // num_iters + crate::constants::STONE_WORK_COST, // num_iters RawValue::from(ingredients_def.dict(¶ms)?.commitment()), )?; let main_pow_pod = MainPod { @@ -333,10 +486,8 @@ mod tests { // Pre-calculate hashes and intermediate values. let ingredients_dict = ingredients_def.dict(¶ms)?; let inputs_set = ingredients_def.inputs_set(¶ms)?; - let item_def = ItemDef { - ingredients: ingredients_def.clone(), - work: pow_pod.output, - }; + let batch_def = BatchDef::new(ingredients_def.clone(), pow_pod.output); + let item_def = ItemDef::new(batch_def.clone(), index)?; let item_hash = item_def.item_hash(¶ms)?; // Prove a stone POD. This is the private POD for the player to store @@ -351,8 +502,8 @@ mod tests { )?; stone_main_pod.pod.verify()?; - assert_eq!(stone_main_pod.public_statements.len(), 3); - //println!("Stone POD: {:?}", stone_main_pod.pod); + assert_eq!(stone_main_pod.public_statements.len(), 5); + println!("Stone POD: {:?}", stone_main_pod.pod); // PODLang query to check the final statements. let stone_query = format!( @@ -361,7 +512,8 @@ mod tests { {} REQUEST( - ItemDef(item, ingredients, inputs, key, work) + BatchDef(batch, ingredients, inputs, keys, work) + ItemDef (item, ingredients, inputs, key, work) ItemKey(item, key) IsStone(item) ) @@ -385,10 +537,15 @@ mod tests { check_matched_wildcards( matched_wildcards, HashMap::from([ + ("batch".into(), Value::from(batch_def.batch_hash(¶ms)?)), ("item".to_string(), Value::from(item_hash)), ("ingredients".to_string(), Value::from(ingredients_dict)), ("inputs".to_string(), Value::from(inputs_set)), - ("key".to_string(), Value::from(key)), + ("key".to_string(), key.clone()), + ( + "keys".into(), + Value::from(Dictionary::new(params.max_depth_mt_containers, keys)?), + ), ("work".to_string(), Value::from(pow_pod.output)), ]), ); @@ -416,7 +573,7 @@ mod tests { commit_main_pod.pod.verify()?; assert_eq!(commit_main_pod.public_statements.len(), 1); - //println!("Commit POD: {:?}", stone_main_pod.pod); + println!("Commit POD: {:?}", commit_main_pod.pod); // PODLang query to check the final statement. let commit_query = format!( @@ -424,7 +581,7 @@ mod tests { {} REQUEST( - CommitCreation(item, nullifiers, created_items) + CommitCreation(items, nullifiers, created_items) ) "#, &commit_preds.defs.imports, @@ -446,7 +603,13 @@ mod tests { check_matched_wildcards( matched_wildcards, HashMap::from([ - ("item".to_string(), Value::from(item_hash)), + ( + "items".to_string(), + Value::from(Set::new( + params.max_depth_mt_containers, + [item_hash.into()].into_iter().collect(), + )?), + ), ("created_items".to_string(), Value::from(created_items)), ("nullifiers".to_string(), Value::from(EMPTY_VALUE)), ]), diff --git a/craftlib/src/predicates.rs b/craftlib/src/predicates.rs index 29eaeb7..0668419 100644 --- a/craftlib/src/predicates.rs +++ b/craftlib/src/predicates.rs @@ -4,6 +4,8 @@ use commitlib::predicates::CommitPredicates; use pod2::middleware::{CustomPredicateRef, Params}; use pod2utils::PredicateDefs; +use crate::constants::STONE_WORK_COST; + pub struct ItemPredicates { pub defs: PredicateDefs, @@ -18,7 +20,8 @@ impl ItemPredicates { // 8 arguments per predicate, at most 5 of which are public // 5 statements per predicate let batch_defs = [ - r#" + &format!( + r#" use intro Pow(count, input, output) from 0x3493488bc23af15ac5fabe38c3cb6c4b66adb57e3898adf201ae50cc57183f65 // powpod vd hash // Example of a mined item with no inputs or sequential work. @@ -26,26 +29,25 @@ impl ItemPredicates { // 10 leading 0s. IsStone(item, private: ingredients, inputs, key, work) = AND( ItemDef(item, ingredients, inputs, key, work) - Equal(inputs, {}) + Equal(inputs, {{}}) DictContains(ingredients, "blueprint", "stone") - Pow(3, ingredients, work) + Pow({STONE_WORK_COST}, ingredients, work) ) // Example of a mined item which is more common but takes more work to // extract. IsWood(item, private: ingredients, inputs, key, work) = AND( ItemDef(item, ingredients, inputs, key, work) - Equal(inputs, {}) + Equal(inputs, {{}}) DictContains(ingredients, "blueprint", "wood") - Equal(work, {}) + Equal(work, {{}}) // TODO input POD: SequentialWork(ingredients, work, 5) // TODO input POD: HashInRange(0, 1<<5, ingredients) ) - "#, - r#" + AxeInputs(inputs, private: s1, wood, stone) = AND( // 2 ingredients - SetInsert(s1, {}, wood) + SetInsert(s1, {{}}, wood) SetInsert(inputs, s1, stone) // prove the ingredients are correct. @@ -58,10 +60,13 @@ impl ItemPredicates { IsAxe(item, private: ingredients, inputs, key, work) = AND( ItemDef(item, ingredients, inputs, key, work) DictContains(ingredients, "blueprint", "axe") - Equal(work, {}) + Equal(work, {{}}) AxeInputs(inputs) ) + "# + ), + r#" // Wooden Axe: WoodenAxeInputs(inputs, private: s1, wood1, wood2) = AND( @@ -82,6 +87,64 @@ impl ItemPredicates { WoodenAxeInputs(inputs) ) + + + // multi-output related predicates: + // (simplified version without tools & durability) + // disassemble 2 Stones into 2 outputs: Dust,Gem. + + // inputs: 2 Stones + StoneDisassembleInputs(inputs, private: s1, stone1, stone2) = AND( + SetInsert(s1, {}, stone1) + SetInsert(inputs, s1, stone2) + + // prove the ingredients are correct + IsStone(stone1) + IsStone(stone2) + ) + + // outputs: 1 Dust, 1 Gem + StoneDisassembleOutputs(batch, keys, + private: k1, dust, gem, _dust_key, _gem_key) = AND( + HashOf(dust, batch, "dust") + HashOf(gem, batch, "gem") + DictInsert(k1, {}, "dust", _dust_key) + DictInsert(keys, k1, "gem", _gem_key) + ) + "#, + r#" + + // helper to have a single predicate for the inputs & outputs + StoneDisassembleInputsOutputs(inputs, batch, keys) = AND ( + StoneDisassembleInputs(inputs) + StoneDisassembleOutputs(batch, keys) + ) + + StoneDisassemble(batch, keys, work, + private: inputs, ingredients) = AND( + BatchDef(batch, ingredients, inputs, keys, work) + DictContains(ingredients, "blueprint", "dust+gem") + + StoneDisassembleInputsOutputs(inputs, batch, keys) + ) + + // can only obtain Dust from disassembling 2 stones + IsDust(item, private: batch, ingredients, inputs, keys, key, work) = AND( + HashOf(item, batch, "dust") + DictContains(keys, "dust", key) + Equal(work, {}) + + StoneDisassemble(batch, keys, work) + ) + + // can only obtain Gem from disassembling 2 stones + IsGem(item, private: batch, ingredients, inputs, keys, key, work) = AND( + HashOf(item, batch, "gem") + DictContains(keys, "gem", key) + Equal(work, {}) + + StoneDisassemble(batch, keys, work) + ) "#, ]; let defs = PredicateDefs::new(params, &batch_defs, slice::from_ref(&commit_preds.defs)); @@ -97,12 +160,16 @@ impl ItemPredicates { mod tests { use std::collections::{HashMap, HashSet}; - use commitlib::{IngredientsDef, ItemDef, util::set_from_hashes}; + use commitlib::{BatchDef, IngredientsDef, ItemDef, util::set_from_hashes}; use pod2::{ backends::plonky2::mock::mainpod::MockProver, frontend::{MainPod, MainPodBuilder, Operation}, lang::parse, - middleware::{EMPTY_VALUE, Pod, RawValue, Statement, Value, hash_value}, + middleware::{ + EMPTY_VALUE, Key, Pod, RawValue, Statement, Value, + containers::{Dictionary, Set}, + hash_value, + }, }; use super::*; @@ -116,10 +183,10 @@ mod tests { fn test_compile_custom_predicates() { let params = Params::default(); let commit_preds = CommitPredicates::compile(¶ms); - assert!(commit_preds.defs.batches.len() == 2); + assert!(commit_preds.defs.batches.len() == 4); let item_preds = ItemPredicates::compile(¶ms, &commit_preds); - assert!(item_preds.defs.batches.len() == 2); + assert!(item_preds.defs.batches.len() == 3); } #[test] @@ -132,12 +199,13 @@ mod tests { // Item recipe constants let seed: i64 = 0xA34; + let index: Key = "0".into(); let key = 0xBADC0DE; // Pre-calculate hashes and intermediate values. let ingredients_def: IngredientsDef = IngredientsDef { inputs: HashSet::new(), - key: RawValue::from(key), + keys: [(index.clone(), Value::from(key))].into_iter().collect(), app_layer: HashMap::from([ ("blueprint".to_string(), Value::from(STONE_BLUEPRINT)), ("seed".to_string(), Value::from(seed)), @@ -150,7 +218,7 @@ mod tests { let pow_pod = PowPod::new( ¶ms, vd_set.clone(), - 3, + STONE_WORK_COST, RawValue::from(ingredients_def.dict(¶ms)?.commitment()), )?; let main_pow_pod = MainPod { @@ -161,11 +229,12 @@ mod tests { let work: RawValue = pow_pod.output; let st_pow = main_pow_pod.public_statements[0].clone(); builder.add_pod(main_pow_pod); - let item_def = ItemDef { - ingredients: ingredients_def.clone(), - work, - }; + let batch_def = BatchDef::new(ingredients_def.clone(), work); + let batch_hash = batch_def.batch_hash(¶ms)?; + let item_def = ItemDef::new(batch_def.clone(), index.clone())?; let item_hash = item_def.item_hash(¶ms)?; + let key_dict = + Dictionary::new(params.max_depth_mt_containers, ingredients_def.keys.clone())?; // Sets for on-chain commitment let nullifiers = set_from_hashes(¶ms, &HashSet::new())?; @@ -177,25 +246,44 @@ mod tests { ]), )?; - // Build ItemDef(item, ingredients, inputs, key, work) + // Build BatchDef(batch, ingredients, inputs, keys, work) let st_contains_inputs = builder.priv_op(Operation::dict_contains( ingredients_dict.clone(), "inputs", inputs_set.clone(), ))?; - let st_contains_key = builder.priv_op(Operation::dict_contains( + let st_contains_keys = builder.priv_op(Operation::dict_contains( ingredients_dict.clone(), - "key", - ingredients_def.key, + "keys", + key_dict.clone(), ))?; - let st_item_hash = builder.priv_op(Operation::hash_of( - item_hash, + let st_batch_hash = builder.priv_op(Operation::hash_of( + batch_hash, ingredients_dict.clone(), - item_def.work, + batch_def.work, + ))?; + let st_batch_def = builder.pub_op(Operation::custom( + commit_preds.batch_def.clone(), + [st_contains_inputs, st_contains_keys, st_batch_hash], ))?; - let st_item_def = builder.pub_op(Operation::custom( + + // Build ItemInBatch(item, batch, index, keys) + let st_item_hash = + builder.priv_op(Operation::hash_of(item_hash, batch_hash, index.hash()))?; + let st_contains_key = builder.priv_op(Operation::dict_contains( + key_dict.clone(), + index.name(), + ingredients_def.keys[&index].clone(), + ))?; + let st_item_in_batch = builder.pub_op(Operation::custom( + commit_preds.item_in_batch.clone(), + [st_item_hash.clone(), st_contains_key.clone()], + ))?; + + // Build ItemDef(item, ingredients, inputs, key, work) + let st_item_def = builder.priv_op(Operation::custom( commit_preds.item_def.clone(), - [st_contains_inputs, st_contains_key, st_item_hash], + [st_batch_def.clone(), st_item_in_batch, st_contains_key], ))?; // Build ItemKey(item, key) @@ -230,10 +318,60 @@ mod tests { [st_nullifiers_empty, Statement::None], ))?; + // Build AllItemsInBatch(items, batch, keys) + let empty_set = Set::new(params.max_depth_mt_containers, HashSet::new())?; + let empty_dict = Dictionary::new(params.max_depth_mt_containers, HashMap::new())?; + let st_all_items_in_batch_empty = builder.op( + false, + vec![(1, batch_hash.into())], + Operation::custom( + commit_preds.all_items_in_batch_empty.clone(), + [ + Statement::Equal(empty_set.clone().into(), EMPTY_VALUE.into()), + Statement::Equal(empty_dict.clone().into(), EMPTY_VALUE.into()), + ], + ), + )?; + let st_all_items_in_batch1 = builder.priv_op(Operation::custom( + commit_preds.all_items_in_batch.clone(), + [st_all_items_in_batch_empty, Statement::None], + ))?; + let items = { + let mut items = empty_set.clone(); + items.insert(&item_hash.into())?; + items + }; + let st_set_insert = + builder.priv_op(Operation::set_insert(items.clone(), empty_set, item_hash))?; + let st_dict_insert = builder.priv_op(Operation::dict_insert( + key_dict.clone(), + empty_dict, + index.name(), + key, + ))?; + let st_all_items_in_batch_recursive = builder.priv_op(Operation::custom( + commit_preds.all_items_in_batch_recursive.clone(), + [ + st_all_items_in_batch1, + st_set_insert, + st_dict_insert, + st_item_hash, + ], + ))?; + let st_all_items_in_batch = builder.priv_op(Operation::custom( + commit_preds.all_items_in_batch.clone(), + [Statement::None, st_all_items_in_batch_recursive], + ))?; + // Build CommitCreation(item, nullifiers, created_items) let _st_commit_crafting = builder.pub_op(Operation::custom( commit_preds.commit_creation.clone(), - [st_item_def.clone(), st_inputs_subset, st_nullifiers], + [ + st_batch_def, + st_all_items_in_batch, + st_inputs_subset, + st_nullifiers, + ], ))?; // Build IsStone(item) @@ -266,11 +404,11 @@ mod tests { {} REQUEST( - ItemDef(item, ingredients, inputs, key, work) + BatchDef(batch, ingredients, inputs, keys, work) ItemKey(item, key) SubsetOf(inputs, created_items) Nullifiers(nullifiers, inputs) - CommitCreation(item, nullifiers, created_items) + CommitCreation(items, nullifiers, created_items) IsStone(item) ) "#, @@ -289,10 +427,13 @@ mod tests { check_matched_wildcards( matched_wildcards, HashMap::from([ + ("batch".into(), batch_hash.into()), ("item".to_string(), Value::from(item_hash)), + ("items".into(), items.into()), ("ingredients".to_string(), Value::from(ingredients_dict)), ("inputs".to_string(), Value::from(inputs_set)), ("key".to_string(), Value::from(key)), + ("keys".into(), key_dict.into()), ("work".to_string(), Value::from(work)), ("created_items".to_string(), Value::from(created_items)), ("nullifiers".to_string(), Value::from(nullifiers)), diff --git a/full-flow.sh b/full-flow.sh index fe50166..ecfdc81 100755 --- a/full-flow.sh +++ b/full-flow.sh @@ -27,7 +27,7 @@ $tmux new-session -d -s fullflow $tmux split-window -v # run the Synchronizer server -$tmux send-keys -t fullflow:0.0 'RUST_LOG=synchronizer=debug cargo run --release -p synchronizer' C-m +$tmux send-keys -t fullflow:0.0 'RUST_LOG=synchronizer=debug,info cargo run --release -p synchronizer' C-m # app command line: diff --git a/synchronizer/src/main.rs b/synchronizer/src/main.rs index 793731c..3bf61c0 100644 --- a/synchronizer/src/main.rs +++ b/synchronizer/src/main.rs @@ -380,7 +380,7 @@ impl Node { info!("Valid do_blob at slot {}, blob_index {}!", slot, blob.index); } Err(e) => { - info!("Invalid do_blob: {:?}", e); + info!("Ignoring blob due to invalid do_blob: {:?}", e); continue; } }; @@ -407,9 +407,11 @@ impl Node { ); } - // Check that output is unique - if state.created_items.contains(&Value::from(payload.item)) { - bail!("item {} exists in created_items", payload.item); + // Check that outputs are unique + for item in &payload.items { + if state.created_items.contains(&(*item).into()) { + bail!("item {} exists in created_items", item); + } } // Check that inputs are unique @@ -426,10 +428,14 @@ impl Node { ) .unwrap(), ); + let item_set = Set::new( + self.params.max_depth_mt_containers, + payload.items.iter().map(|rv| (*rv).into()).collect(), + )?; let st_commit_creation = Statement::Custom( self.pred_commit_creation.clone(), vec![ - Value::from(payload.item), + item_set.into(), nullifiers_set, Value::from(payload.created_items_root), ], @@ -442,11 +448,11 @@ impl Node { for nullifier in &payload.nullifiers { state.nullifiers.insert(*nullifier); } - // Register item - state - .created_items - .insert(&Value::from(payload.item)) - .unwrap(); + + // Register items + for item in payload.items { + state.created_items.insert(&item.into())?; + } state.epoch += 1; let created_items_root = state.created_items.commitment(); @@ -474,12 +480,10 @@ impl Node { proof: *shrunk_main_pod_proof, public_inputs, }; - let proof = proof_with_pis - .decompress( - &self.verifier_circuit_data.verifier_only.circuit_digest, - &self.common_circuit_data, - ) - .unwrap(); + let proof = proof_with_pis.decompress( + &self.verifier_circuit_data.verifier_only.circuit_digest, + &self.common_circuit_data, + )?; self.verifier_circuit_data.verify(proof) } }