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