mirror of
https://github.com/vacp2p/zerokit.git
synced 2026-01-10 06:08:09 -05:00
Compare commits
2 Commits
zerokit_ut
...
fix-versio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc0c23acb8 | ||
|
|
b471545b94 |
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -2686,7 +2686,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rln"
|
||||
version = "0.3.1"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"ark-bn254 0.4.0",
|
||||
"ark-circom 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -4260,7 +4260,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerokit_utils"
|
||||
version = "0.3.1"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"ark-bn254 0.4.0",
|
||||
"ark-ff 0.4.1",
|
||||
|
||||
@@ -8,7 +8,6 @@ members = [
|
||||
"rln-wasm",
|
||||
"utils",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
# Compilation profile for any non-workspace member.
|
||||
# Dependencies are optimized, even in a dev build. This improves dev performance
|
||||
@@ -21,4 +20,4 @@ opt-level = 3
|
||||
opt-level = "s"
|
||||
|
||||
[profile.release.package."semaphore"]
|
||||
codegen-units = 1
|
||||
codegen-units = 1
|
||||
@@ -1,12 +1,9 @@
|
||||
[package]
|
||||
name = "rln"
|
||||
version = "0.3.1"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "APIs to manage, compute and verify zkSNARK proofs and RLN primitives"
|
||||
documentation = "https://github.com/vacp2p/zerokit"
|
||||
homepage = "https://vac.dev"
|
||||
repository = "https://github.com/vacp2p/zerokit"
|
||||
|
||||
[lib]
|
||||
crate-type = ["rlib", "staticlib"]
|
||||
@@ -42,7 +39,7 @@ once_cell = "=1.17.1"
|
||||
rand = "=0.8.5"
|
||||
rand_chacha = "=0.3.1"
|
||||
tiny-keccak = { version = "=2.0.2", features = ["keccak"] }
|
||||
utils = { package = "zerokit_utils", version = "=0.3.1", path = "../utils/", default-features = false }
|
||||
utils = { package = "zerokit_utils", version = "=0.3.0", path = "../utils/", default-features = false }
|
||||
|
||||
# serialization
|
||||
serde_json = "=1.0.96"
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
/// This crate instantiates the Poseidon hash algorithm.
|
||||
// This crate instantiate the Poseidon hash algorithm
|
||||
|
||||
use crate::{circuit::Fr, utils::bytes_le_to_fr};
|
||||
use once_cell::sync::Lazy;
|
||||
use tiny_keccak::{Hasher, Keccak};
|
||||
use utils::poseidon::Poseidon;
|
||||
|
||||
/// These indexed constants hardcode the supported round parameters tuples (t, RF, RN, SKIP_MATRICES) for the Bn254 scalar field.
|
||||
/// SKIP_MATRICES is the index of the randomly generated secure MDS matrix.
|
||||
/// TODO: generate these parameters
|
||||
// These indexed constants hardcodes the supported round parameters tuples (t, RF, RN, SKIP_MATRICES) for the Bn254 scalar field
|
||||
// SKIP_MATRICES is the index of the randomly generated secure MDS matrix. See security note in the zerokit_utils::poseidon::poseidon_constants crate on this.
|
||||
// TODO: generate these parameters
|
||||
pub const ROUND_PARAMS: [(usize, usize, usize, usize); 8] = [
|
||||
(2, 8, 56, 0),
|
||||
(3, 8, 57, 0),
|
||||
@@ -18,7 +19,7 @@ pub const ROUND_PARAMS: [(usize, usize, usize, usize); 8] = [
|
||||
(9, 8, 63, 0),
|
||||
];
|
||||
|
||||
/// Poseidon Hash wrapper over above implementation.
|
||||
// Poseidon Hash wrapper over above implementation. Adapted from semaphore-rs poseidon hash wrapper.
|
||||
static POSEIDON: Lazy<Poseidon<Fr>> = Lazy::new(|| Poseidon::<Fr>::from(&ROUND_PARAMS));
|
||||
|
||||
pub fn poseidon_hash(input: &[Fr]) -> Fr {
|
||||
@@ -27,11 +28,11 @@ pub fn poseidon_hash(input: &[Fr]) -> Fr {
|
||||
.expect("hash with fixed input size can't fail")
|
||||
}
|
||||
|
||||
/// The zerokit RLN Merkle tree Hasher.
|
||||
// The zerokit RLN Merkle tree Hasher
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub struct PoseidonHash;
|
||||
|
||||
/// The default Hasher trait used by Merkle tree implementation in utils.
|
||||
// The default Hasher trait used by Merkle tree implementation in utils
|
||||
impl utils::merkle_tree::Hasher for PoseidonHash {
|
||||
type Fr = Fr;
|
||||
|
||||
@@ -44,9 +45,10 @@ impl utils::merkle_tree::Hasher for PoseidonHash {
|
||||
}
|
||||
}
|
||||
|
||||
/// Hashes arbitrary signal to the underlying prime field.
|
||||
// Hashes arbitrary signal to the underlying prime field
|
||||
pub fn hash_to_field(signal: &[u8]) -> Fr {
|
||||
// We hash the input signal using Keccak256
|
||||
// (note that a bigger curve order might require a bigger hash blocksize)
|
||||
let mut hash = [0; 32];
|
||||
let mut hasher = Keccak::v256();
|
||||
hasher.update(signal);
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
use std::fmt::Debug;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use color_eyre::{Report, Result};
|
||||
use serde_json::Value;
|
||||
|
||||
use utils::pmtree::{Database, Hasher};
|
||||
use utils::*;
|
||||
|
||||
use crate::circuit::Fr;
|
||||
use crate::hashers::{poseidon_hash, PoseidonHash};
|
||||
use crate::utils::{bytes_le_to_fr, fr_to_bytes_le};
|
||||
use color_eyre::{Report, Result};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Debug;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use utils::pmtree::{Database, Hasher};
|
||||
use utils::*;
|
||||
|
||||
const METADATA_KEY: [u8; 8] = *b"metadata";
|
||||
|
||||
@@ -27,9 +25,13 @@ pub struct PmTreeProof {
|
||||
pub type FrOf<H> = <H as Hasher>::Fr;
|
||||
|
||||
// The pmtree Hasher trait used by pmtree Merkle tree
|
||||
impl Hasher for PoseidonHash {
|
||||
impl pmtree::Hasher for PoseidonHash {
|
||||
type Fr = Fr;
|
||||
|
||||
fn default_leaf() -> Self::Fr {
|
||||
Fr::from(0)
|
||||
}
|
||||
|
||||
fn serialize(value: Self::Fr) -> pmtree::Value {
|
||||
fr_to_bytes_le(&value)
|
||||
}
|
||||
@@ -39,16 +41,12 @@ impl Hasher for PoseidonHash {
|
||||
fr
|
||||
}
|
||||
|
||||
fn default_leaf() -> Self::Fr {
|
||||
Fr::from(0)
|
||||
}
|
||||
|
||||
fn hash(inputs: &[Self::Fr]) -> Self::Fr {
|
||||
poseidon_hash(inputs)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_tmp_path() -> PathBuf {
|
||||
fn get_tmp_path() -> std::path::PathBuf {
|
||||
std::env::temp_dir().join(format!("pmtree-{}", rand::random::<u64>()))
|
||||
}
|
||||
|
||||
@@ -56,7 +54,7 @@ fn get_tmp() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub struct PmtreeConfig(Config);
|
||||
pub struct PmtreeConfig(pm_tree::Config);
|
||||
|
||||
impl FromStr for PmtreeConfig {
|
||||
type Err = Report;
|
||||
@@ -87,7 +85,7 @@ impl FromStr for PmtreeConfig {
|
||||
)));
|
||||
}
|
||||
|
||||
let config = Config::new()
|
||||
let config = pm_tree::Config::new()
|
||||
.temporary(temporary.unwrap_or(get_tmp()))
|
||||
.path(path.unwrap_or(get_tmp_path()))
|
||||
.cache_capacity(cache_capacity.unwrap_or(1024 * 1024 * 1024))
|
||||
@@ -102,7 +100,7 @@ impl Default for PmtreeConfig {
|
||||
fn default() -> Self {
|
||||
let tmp_path = get_tmp_path();
|
||||
PmtreeConfig(
|
||||
Config::new()
|
||||
pm_tree::Config::new()
|
||||
.temporary(true)
|
||||
.path(tmp_path)
|
||||
.cache_capacity(150_000)
|
||||
@@ -147,6 +145,10 @@ impl ZerokitMerkleTree for PmTree {
|
||||
})
|
||||
}
|
||||
|
||||
fn close_db_connection(&mut self) -> Result<()> {
|
||||
self.tree.db.close().map_err(|e| Report::msg(e.to_string()))
|
||||
}
|
||||
|
||||
fn depth(&self) -> usize {
|
||||
self.tree.depth()
|
||||
}
|
||||
@@ -163,16 +165,16 @@ impl ZerokitMerkleTree for PmTree {
|
||||
self.tree.root()
|
||||
}
|
||||
|
||||
fn compute_root(&mut self) -> Result<FrOf<Self::Hasher>> {
|
||||
Ok(self.tree.root())
|
||||
}
|
||||
|
||||
fn set(&mut self, index: usize, leaf: FrOf<Self::Hasher>) -> Result<()> {
|
||||
self.tree
|
||||
.set(index, leaf)
|
||||
.map_err(|e| Report::msg(e.to_string()))
|
||||
}
|
||||
|
||||
fn get(&self, index: usize) -> Result<FrOf<Self::Hasher>> {
|
||||
self.tree.get(index).map_err(|e| Report::msg(e.to_string()))
|
||||
}
|
||||
|
||||
fn set_range<I: IntoIterator<Item = FrOf<Self::Hasher>>>(
|
||||
&mut self,
|
||||
start: usize,
|
||||
@@ -183,10 +185,6 @@ impl ZerokitMerkleTree for PmTree {
|
||||
.map_err(|e| Report::msg(e.to_string()))
|
||||
}
|
||||
|
||||
fn get(&self, index: usize) -> Result<FrOf<Self::Hasher>> {
|
||||
self.tree.get(index).map_err(|e| Report::msg(e.to_string()))
|
||||
}
|
||||
|
||||
fn override_range<I: IntoIterator<Item = FrOf<Self::Hasher>>, J: IntoIterator<Item = usize>>(
|
||||
&mut self,
|
||||
start: usize,
|
||||
@@ -194,15 +192,33 @@ impl ZerokitMerkleTree for PmTree {
|
||||
indices: J,
|
||||
) -> Result<()> {
|
||||
let leaves = leaves.into_iter().collect::<Vec<_>>();
|
||||
let mut indices = indices.into_iter().collect::<Vec<_>>();
|
||||
indices.sort();
|
||||
let indices = indices.into_iter().collect::<HashSet<_>>();
|
||||
let end = start + leaves.len();
|
||||
|
||||
match (leaves.is_empty(), indices.is_empty()) {
|
||||
(true, true) => Err(Report::msg("no leaves or indices to be removed")),
|
||||
(false, true) => self.set_range_with_leaves(start, leaves),
|
||||
(true, false) => self.remove_indices(indices),
|
||||
(false, false) => self.remove_indices_and_set_leaves(start, leaves, indices),
|
||||
if leaves.len() + start - indices.len() > self.capacity() {
|
||||
return Err(Report::msg("index out of bounds"));
|
||||
}
|
||||
|
||||
// extend the range to include indices to be removed
|
||||
let min_index = indices.iter().min().unwrap_or(&start);
|
||||
let max_index = indices.iter().max().unwrap_or(&end);
|
||||
|
||||
let mut new_leaves = Vec::new();
|
||||
|
||||
// insert leaves into new_leaves
|
||||
for i in *min_index..*max_index {
|
||||
if indices.contains(&i) {
|
||||
// insert 0
|
||||
new_leaves.push(Self::Hasher::default_leaf());
|
||||
} else {
|
||||
// insert leaf
|
||||
new_leaves.push(leaves[i - start]);
|
||||
}
|
||||
}
|
||||
|
||||
self.tree
|
||||
.set_range(start, new_leaves)
|
||||
.map_err(|e| Report::msg(e.to_string()))
|
||||
}
|
||||
|
||||
fn update_next(&mut self, leaf: FrOf<Self::Hasher>) -> Result<()> {
|
||||
@@ -230,6 +246,10 @@ impl ZerokitMerkleTree for PmTree {
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_root(&mut self) -> Result<FrOf<Self::Hasher>> {
|
||||
Ok(self.tree.root())
|
||||
}
|
||||
|
||||
fn set_metadata(&mut self, metadata: &[u8]) -> Result<()> {
|
||||
self.tree.db.put(METADATA_KEY, metadata.to_vec())?;
|
||||
self.metadata = metadata.to_vec();
|
||||
@@ -248,70 +268,6 @@ impl ZerokitMerkleTree for PmTree {
|
||||
}
|
||||
Ok(data.unwrap())
|
||||
}
|
||||
|
||||
fn close_db_connection(&mut self) -> Result<()> {
|
||||
self.tree.db.close().map_err(|e| Report::msg(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
type PmTreeHasher = <PmTree as ZerokitMerkleTree>::Hasher;
|
||||
type FrOfPmTreeHasher = FrOf<PmTreeHasher>;
|
||||
|
||||
impl PmTree {
|
||||
fn set_range_with_leaves(&mut self, start: usize, leaves: Vec<FrOfPmTreeHasher>) -> Result<()> {
|
||||
self.tree
|
||||
.set_range(start, leaves)
|
||||
.map_err(|e| Report::msg(e.to_string()))
|
||||
}
|
||||
|
||||
fn remove_indices(&mut self, indices: Vec<usize>) -> Result<()> {
|
||||
let start = indices[0];
|
||||
let end = indices.last().unwrap() + 1;
|
||||
|
||||
let mut new_leaves: Vec<_> = (start..end)
|
||||
.map(|i| self.tree.get(i))
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
new_leaves
|
||||
.iter_mut()
|
||||
.take(indices.len())
|
||||
.for_each(|leaf| *leaf = PmTreeHasher::default_leaf());
|
||||
|
||||
self.tree
|
||||
.set_range(start, new_leaves)
|
||||
.map_err(|e| Report::msg(e.to_string()))
|
||||
}
|
||||
|
||||
fn remove_indices_and_set_leaves(
|
||||
&mut self,
|
||||
start: usize,
|
||||
leaves: Vec<FrOfPmTreeHasher>,
|
||||
indices: Vec<usize>,
|
||||
) -> Result<()> {
|
||||
let min_index = *indices.first().unwrap();
|
||||
let max_index = start + leaves.len();
|
||||
|
||||
// Generated a placeholder with the exact size needed,
|
||||
// Initiated with default values to be overridden throughout the method
|
||||
let mut set_values = vec![PmTreeHasher::default_leaf(); max_index - min_index];
|
||||
|
||||
// If the index is not in indices list, keep the original value
|
||||
for i in min_index..start {
|
||||
if !indices.contains(&i) {
|
||||
let value = self.tree.get(i)?;
|
||||
set_values[i - min_index] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert new leaves after 'start' position
|
||||
for (i, &leaf) in leaves.iter().enumerate() {
|
||||
set_values[start - min_index + i] = leaf;
|
||||
}
|
||||
|
||||
self.tree
|
||||
.set_range(min_index, set_values)
|
||||
.map_err(|e| Report::msg(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl ZerokitMerkleProof for PmTreeProof {
|
||||
|
||||
@@ -362,7 +362,7 @@ impl RLN<'_> {
|
||||
// We set the leaves
|
||||
self.tree
|
||||
.override_range(index, leaves, indices)
|
||||
.map_err(|e| Report::msg(format!("Could not perform the batch operation: {e}")))?;
|
||||
.map_err(|_| Report::msg("Could not perform the batch operation"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1387,7 +1387,7 @@ mod test {
|
||||
|
||||
assert_eq!(root_batch_with_init, root_single_additions);
|
||||
|
||||
rln.flush().unwrap();
|
||||
rln.flush();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1430,7 +1430,7 @@ mod test {
|
||||
let indices_buffer = Cursor::new(vec_u8_to_bytes_le(&indices).unwrap());
|
||||
let leaves_buffer = Cursor::new(vec_fr_to_bytes_le(&last_leaf).unwrap());
|
||||
|
||||
rln.atomic_operation(last_leaf_index, leaves_buffer, indices_buffer)
|
||||
rln.atomic_operation(no_of_leaves, leaves_buffer, indices_buffer)
|
||||
.unwrap();
|
||||
|
||||
// We get the root of the tree obtained after a no-op
|
||||
@@ -1441,105 +1441,6 @@ mod test {
|
||||
assert_eq!(root_after_insertion, root_after_noop);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_atomic_operation_zero_indexed() {
|
||||
// Test duplicated from https://github.com/waku-org/go-zerokit-rln/pull/12/files
|
||||
let tree_height = TEST_TREE_HEIGHT;
|
||||
let no_of_leaves = 256;
|
||||
|
||||
// We generate a vector of random leaves
|
||||
let mut leaves: Vec<Fr> = Vec::new();
|
||||
let mut rng = thread_rng();
|
||||
for _ in 0..no_of_leaves {
|
||||
leaves.push(Fr::rand(&mut rng));
|
||||
}
|
||||
|
||||
// We create a new tree
|
||||
let input_buffer =
|
||||
Cursor::new(json!({ "resources_folder": TEST_RESOURCES_FOLDER }).to_string());
|
||||
let mut rln = RLN::new(tree_height, input_buffer).unwrap();
|
||||
|
||||
// We add leaves in a batch into the tree
|
||||
let mut buffer = Cursor::new(vec_fr_to_bytes_le(&leaves).unwrap());
|
||||
rln.init_tree_with_leaves(&mut buffer).unwrap();
|
||||
|
||||
// We check if number of leaves set is consistent
|
||||
assert_eq!(rln.tree.leaves_set(), no_of_leaves);
|
||||
|
||||
// We get the root of the tree obtained adding leaves in batch
|
||||
let mut buffer = Cursor::new(Vec::<u8>::new());
|
||||
rln.get_root(&mut buffer).unwrap();
|
||||
let (root_after_insertion, _) = bytes_le_to_fr(&buffer.into_inner());
|
||||
|
||||
let zero_index = 0;
|
||||
let indices = vec![zero_index as u8];
|
||||
let zero_leaf: Vec<Fr> = vec![];
|
||||
let indices_buffer = Cursor::new(vec_u8_to_bytes_le(&indices).unwrap());
|
||||
let leaves_buffer = Cursor::new(vec_fr_to_bytes_le(&zero_leaf).unwrap());
|
||||
rln.atomic_operation(0, leaves_buffer, indices_buffer)
|
||||
.unwrap();
|
||||
|
||||
// We get the root of the tree obtained after a deletion
|
||||
let mut buffer = Cursor::new(Vec::<u8>::new());
|
||||
rln.get_root(&mut buffer).unwrap();
|
||||
let (root_after_deletion, _) = bytes_le_to_fr(&buffer.into_inner());
|
||||
|
||||
assert_ne!(root_after_insertion, root_after_deletion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_atomic_operation_consistency() {
|
||||
// Test duplicated from https://github.com/waku-org/go-zerokit-rln/pull/12/files
|
||||
let tree_height = TEST_TREE_HEIGHT;
|
||||
let no_of_leaves = 256;
|
||||
|
||||
// We generate a vector of random leaves
|
||||
let mut leaves: Vec<Fr> = Vec::new();
|
||||
let mut rng = thread_rng();
|
||||
for _ in 0..no_of_leaves {
|
||||
leaves.push(Fr::rand(&mut rng));
|
||||
}
|
||||
|
||||
// We create a new tree
|
||||
let input_buffer =
|
||||
Cursor::new(json!({ "resources_folder": TEST_RESOURCES_FOLDER }).to_string());
|
||||
let mut rln = RLN::new(tree_height, input_buffer).unwrap();
|
||||
|
||||
// We add leaves in a batch into the tree
|
||||
let mut buffer = Cursor::new(vec_fr_to_bytes_le(&leaves).unwrap());
|
||||
rln.init_tree_with_leaves(&mut buffer).unwrap();
|
||||
|
||||
// We check if number of leaves set is consistent
|
||||
assert_eq!(rln.tree.leaves_set(), no_of_leaves);
|
||||
|
||||
// We get the root of the tree obtained adding leaves in batch
|
||||
let mut buffer = Cursor::new(Vec::<u8>::new());
|
||||
rln.get_root(&mut buffer).unwrap();
|
||||
let (root_after_insertion, _) = bytes_le_to_fr(&buffer.into_inner());
|
||||
|
||||
let set_index = rng.gen_range(0..no_of_leaves) as usize;
|
||||
let indices = vec![set_index as u8];
|
||||
let zero_leaf: Vec<Fr> = vec![];
|
||||
let indices_buffer = Cursor::new(vec_u8_to_bytes_le(&indices).unwrap());
|
||||
let leaves_buffer = Cursor::new(vec_fr_to_bytes_le(&zero_leaf).unwrap());
|
||||
rln.atomic_operation(0, leaves_buffer, indices_buffer)
|
||||
.unwrap();
|
||||
|
||||
// We get the root of the tree obtained after a deletion
|
||||
let mut buffer = Cursor::new(Vec::<u8>::new());
|
||||
rln.get_root(&mut buffer).unwrap();
|
||||
let (root_after_deletion, _) = bytes_le_to_fr(&buffer.into_inner());
|
||||
|
||||
assert_ne!(root_after_insertion, root_after_deletion);
|
||||
|
||||
// We get the leaf
|
||||
let mut output_buffer = Cursor::new(Vec::<u8>::new());
|
||||
rln.get_leaf(set_index, &mut output_buffer).unwrap();
|
||||
let (received_leaf, _) = bytes_le_to_fr(output_buffer.into_inner().as_ref());
|
||||
|
||||
assert_eq!(received_leaf, Fr::from(0));
|
||||
}
|
||||
|
||||
#[allow(unused_must_use)]
|
||||
#[test]
|
||||
// This test checks if `set_leaves_from` throws an error when the index is out of bounds
|
||||
|
||||
@@ -265,7 +265,7 @@ mod test {
|
||||
|
||||
let success = atomic_operation(
|
||||
rln_pointer,
|
||||
last_leaf_index as usize,
|
||||
no_of_leaves as usize,
|
||||
leaves_buffer,
|
||||
indices_buffer,
|
||||
);
|
||||
|
||||
@@ -47,4 +47,4 @@ opt-level = 3
|
||||
# Dependencies are optimized, even in a dev build. This improves dev performance
|
||||
# while having neglible impact on incremental build times.
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
opt-level = 3
|
||||
@@ -1,12 +1,9 @@
|
||||
[package]
|
||||
name = "zerokit_utils"
|
||||
version = "0.3.1"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Various utilities for Zerokit"
|
||||
documentation = "https://github.com/vacp2p/zerokit"
|
||||
homepage = "https://vac.dev"
|
||||
repository = "https://github.com/vacp2p/zerokit"
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
|
||||
Reference in New Issue
Block a user