diff --git a/artifacts/zk/commitment.json b/artifacts/zk/commitment.json index 12e3094..153ae16 100644 --- a/artifacts/zk/commitment.json +++ b/artifacts/zk/commitment.json @@ -1 +1,391 @@ -{"noir_version":"1.0.0-beta.20+b4236c1957d0c26cb65d82adc9e5447b6ff1d629","hash":"12985402847476950180","abi":{"parameters":[{"name":"nullifier","type":{"kind":"field"},"visibility":"private"},{"name":"secret","type":{"kind":"field"},"visibility":"private"},{"name":"pool_id","type":{"kind":"field"},"visibility":"public"},{"name":"commitment","type":{"kind":"field"},"visibility":"public"}],"return_type":null,"error_types":{"11287755292792099533":{"error_kind":"string","string":"commitment mismatch: invalid nullifier/secret pair"}}},"bytecode":"H4sIAAAAAAAA/42QLQvCQByHb5tvH8OozZdmU1FMsqYIhjEPPJx3cjdB432D7T/BYhARBZNBRPuq+BnWjBa7lg0ERZ/0hF94+GngztdDg9CUJ3dlITC3O5gzz3HBT+d6TR7kl9mTXjtK2e5mirfG9Dxyq8HDuyOEYn4afUYLRe4rlmEOKmxSH1OzaliWPOhMYNJjtKBjPhzbhk0YhZnctohNsRAoMiUyNbLYLJ5Ipt5b4Y9WDb7GhijxnxMESAFVe70j12WTcBWui012VerDxXGehFReP0wBAAA=","debug_symbols":"nZNNjoQgEIXvwtoFhYDYV5lMDCp2SAgaWjuZmL77lEb8WWAmsxGh/J6v8qiZtKaenpX1Xf8ij6+Z1ME6Z5+V6xs92t7j6fzJSNxWYzAGj8ipjtSgg/EjefjJuYy8tZvWj16D9us66oBVmhHjW1xRsLPOLG+f7KBpGgVWwkZDzvNdQMBFAdIKUvBNoAB28PzCszTPJdt4QeXB5xc+v+sgtg+C/oNnEP0zzlK8uPEPRTTAGS12BVB/dsBVdCDKlIO7DDmLLQBXKpWhSiuUudgESnVkIOWFL+8ykLuBE4934Bt3urHhcu8JxWJGYH0yhJbc3jpYXTuzDUY3+eY0J+PPECtxkobQN6adglm01xr+7Rc=","file_map":{"18":{"source":"// Exposed only for usage in `std::meta`\npub(crate) mod poseidon2;\n\nuse crate::default::Default;\nuse crate::embedded_curve_ops::{\n EmbeddedCurvePoint, EmbeddedCurveScalar, multi_scalar_mul, multi_scalar_mul_array_return,\n};\nuse crate::meta::derive_via;\nuse crate::static_assert;\n\n/// The size of the state accepted by the backend in `poseidon2_permutation`.\nglobal POSEIDON2_CONFIG_STATE_SIZE: u32 = poseidon2_config_state_size();\n\n#[foreign(sha256_compression)]\n// docs:start:sha256_compression\npub fn sha256_compression(input: [u32; 16], state: [u32; 8]) -> [u32; 8] {}\n// docs:end:sha256_compression\n\n#[foreign(keccakf1600)]\n// docs:start:keccakf1600\npub fn keccakf1600(input: [u64; 25]) -> [u64; 25] {}\n// docs:end:keccakf1600\n\npub mod keccak {\n #[deprecated(\"This function has been moved to std::hash::keccakf1600\")]\n pub fn keccakf1600(input: [u64; 25]) -> [u64; 25] {\n super::keccakf1600(input)\n }\n}\n\n#[foreign(blake2s)]\n// docs:start:blake2s\npub fn blake2s(input: [u8; N]) -> [u8; 32]\n// docs:end:blake2s\n{}\n\n// docs:start:blake3\npub fn blake3(input: [u8; N]) -> [u8; 32]\n// docs:end:blake3\n{\n if crate::runtime::is_unconstrained() {\n // Temporary measure while Barretenberg is main proving system.\n // Please open an issue if you're working on another proving system and running into problems due to this.\n crate::static_assert(\n N <= 1024,\n \"Barretenberg cannot prove blake3 hashes with inputs larger than 1024 bytes\",\n );\n }\n __blake3(input)\n}\n\n#[foreign(blake3)]\nfn __blake3(input: [u8; N]) -> [u8; 32] {}\n\n// docs:start:pedersen_commitment\npub fn pedersen_commitment(input: [Field; N]) -> EmbeddedCurvePoint {\n // docs:end:pedersen_commitment\n pedersen_commitment_with_separator(input, 0)\n}\n\n#[inline_always]\npub fn pedersen_commitment_with_separator(\n input: [Field; N],\n separator: u32,\n) -> EmbeddedCurvePoint {\n let mut points = [EmbeddedCurveScalar { lo: 0, hi: 0 }; N];\n for i in 0..N {\n points[i] = EmbeddedCurveScalar::from_field(input[i]);\n }\n let generators = derive_generators(\"DEFAULT_DOMAIN_SEPARATOR\".as_bytes(), separator);\n multi_scalar_mul(generators, points)\n}\n\n// docs:start:pedersen_hash\npub fn pedersen_hash(input: [Field; N]) -> Field\n// docs:end:pedersen_hash\n{\n pedersen_hash_with_separator(input, 0)\n}\n\n#[no_predicates]\npub fn pedersen_hash_with_separator(input: [Field; N], separator: u32) -> Field {\n let mut scalars: [EmbeddedCurveScalar; N + 1] = [EmbeddedCurveScalar { lo: 0, hi: 0 }; N + 1];\n let mut generators: [EmbeddedCurvePoint; N + 1] =\n [EmbeddedCurvePoint::point_at_infinity(); N + 1];\n crate::assert_constant(separator);\n let domain_generators: [EmbeddedCurvePoint; N] =\n derive_generators(\"DEFAULT_DOMAIN_SEPARATOR\".as_bytes(), separator);\n\n for i in 0..N {\n scalars[i] = EmbeddedCurveScalar::from_field(input[i]);\n generators[i] = domain_generators[i];\n }\n scalars[N] = EmbeddedCurveScalar { lo: N as Field, hi: 0 as Field };\n\n let length_generator: [EmbeddedCurvePoint; 1] =\n derive_generators(\"pedersen_hash_length\".as_bytes(), 0);\n generators[N] = length_generator[0];\n multi_scalar_mul_array_return(generators, scalars, true)[0].x\n}\n\n#[field(bn254)]\n#[inline_always]\npub fn derive_generators(\n domain_separator_bytes: [u8; M],\n starting_index: u32,\n) -> [EmbeddedCurvePoint; N] {\n crate::assert_constant(domain_separator_bytes);\n crate::assert_constant(starting_index);\n __derive_generators(domain_separator_bytes, starting_index)\n}\n\n#[builtin(derive_pedersen_generators)]\n#[field(bn254)]\nfn __derive_generators(\n domain_separator_bytes: [u8; M],\n starting_index: u32,\n) -> [EmbeddedCurvePoint; N] {}\n\npub fn poseidon2_permutation(input: [Field; N]) -> [Field; N] {\n static_assert(\n N == POSEIDON2_CONFIG_STATE_SIZE,\n f\"the input length must equal the state size in the Poseidon2 config; expected {POSEIDON2_CONFIG_STATE_SIZE}, got {N}\",\n );\n poseidon2_permutation_internal(input)\n}\n\n#[foreign(poseidon2_permutation)]\nfn poseidon2_permutation_internal(input: [Field; N]) -> [Field; N] {}\n\n#[foreign(poseidon2_config_state_size)]\ncomptime fn poseidon2_config_state_size() -> u32 {}\n\n// Generic hashing support.\n// Partially ported and impacted by rust.\n\n// Hash trait shall be implemented per type.\n#[derive_via(derive_hash)]\npub trait Hash {\n fn hash(self, state: &mut H)\n where\n H: Hasher;\n}\n\n// docs:start:derive_hash\ncomptime fn derive_hash(s: TypeDefinition) -> Quoted {\n let name = quote { $crate::hash::Hash };\n let signature = quote { fn hash(_self: Self, _state: &mut H) where H: $crate::hash::Hasher };\n let for_each_field = |name| quote { _self.$name.hash(_state); };\n crate::meta::make_trait_impl(\n s,\n name,\n signature,\n for_each_field,\n quote {},\n |fields| fields,\n )\n}\n// docs:end:derive_hash\n\n// Hasher trait shall be implemented by algorithms to provide hash-agnostic means.\n// TODO: consider making the types generic here ([u8], [Field], etc.)\npub trait Hasher {\n fn finish(self) -> Field;\n\n /// Returns the hash value without consuming the hasher.\n /// Override this for more efficient implementations that avoid copying.\n /// TODO: deprecate finish() and replace it\n fn finish_ref(&self) -> Field {\n (*self).finish()\n }\n\n fn write(&mut self, input: Field);\n}\n\n// BuildHasher is a factory trait, responsible for production of specific Hasher.\npub trait BuildHasher {\n type H: Hasher;\n\n fn build_hasher(self) -> H;\n}\n\npub struct BuildHasherDefault;\n\nimpl BuildHasher for BuildHasherDefault\nwhere\n H: Hasher + Default,\n{\n type H = H;\n\n fn build_hasher(_self: Self) -> H {\n H::default()\n }\n}\n\nimpl Default for BuildHasherDefault\nwhere\n H: Hasher + Default,\n{\n fn default() -> Self {\n BuildHasherDefault {}\n }\n}\n\nimpl Hash for Field {\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n H::write(state, self);\n }\n}\n\nimpl Hash for u8 {\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n H::write(state, self as Field);\n }\n}\n\nimpl Hash for u16 {\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n H::write(state, self as Field);\n }\n}\n\nimpl Hash for u32 {\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n H::write(state, self as Field);\n }\n}\n\nimpl Hash for u64 {\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n H::write(state, self as Field);\n }\n}\n\nimpl Hash for u128 {\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n H::write(state, self as Field);\n }\n}\n\nimpl Hash for i8 {\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n H::write(state, self as u8 as Field);\n }\n}\n\nimpl Hash for i16 {\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n H::write(state, self as u16 as Field);\n }\n}\n\nimpl Hash for i32 {\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n H::write(state, self as u32 as Field);\n }\n}\n\nimpl Hash for i64 {\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n H::write(state, self as u64 as Field);\n }\n}\n\nimpl Hash for bool {\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n H::write(state, self as Field);\n }\n}\n\nimpl Hash for () {\n fn hash(_self: Self, _state: &mut H)\n where\n H: Hasher,\n {}\n}\n\nimpl Hash for [T; N]\nwhere\n T: Hash,\n{\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n for elem in self {\n elem.hash(state);\n }\n }\n}\n\nimpl Hash for [T]\nwhere\n T: Hash,\n{\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n self.len().hash(state);\n for elem in self {\n elem.hash(state);\n }\n }\n}\n\nimpl Hash for (A, B)\nwhere\n A: Hash,\n B: Hash,\n{\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n self.0.hash(state);\n self.1.hash(state);\n }\n}\n\nimpl Hash for (A, B, C)\nwhere\n A: Hash,\n B: Hash,\n C: Hash,\n{\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n self.0.hash(state);\n self.1.hash(state);\n self.2.hash(state);\n }\n}\n\nimpl Hash for (A, B, C, D)\nwhere\n A: Hash,\n B: Hash,\n C: Hash,\n D: Hash,\n{\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n self.0.hash(state);\n self.1.hash(state);\n self.2.hash(state);\n self.3.hash(state);\n }\n}\n\nimpl Hash for (A, B, C, D, E)\nwhere\n A: Hash,\n B: Hash,\n C: Hash,\n D: Hash,\n E: Hash,\n{\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n self.0.hash(state);\n self.1.hash(state);\n self.2.hash(state);\n self.3.hash(state);\n self.4.hash(state);\n }\n}\n\n// Some test vectors for Pedersen hash and Pedersen Commitment.\n// They have been generated using the same functions so the tests are for now useless\n// but they will be useful when we switch to Noir implementation.\n#[test]\nfn assert_pedersen() {\n assert_eq(\n pedersen_hash_with_separator([1], 1),\n 0x1b3f4b1a83092a13d8d1a59f7acb62aba15e7002f4440f2275edb99ebbc2305f,\n );\n assert_eq(\n pedersen_commitment_with_separator([1], 1),\n EmbeddedCurvePoint {\n x: 0x054aa86a73cb8a34525e5bbed6e43ba1198e860f5f3950268f71df4591bde402,\n y: 0x209dcfbf2cfb57f9f6046f44d71ac6faf87254afc7407c04eb621a6287cac126,\n },\n );\n\n assert_eq(\n pedersen_hash_with_separator([1, 2], 2),\n 0x26691c129448e9ace0c66d11f0a16d9014a9e8498ee78f4d69f0083168188255,\n );\n assert_eq(\n pedersen_commitment_with_separator([1, 2], 2),\n EmbeddedCurvePoint {\n x: 0x2e2b3b191e49541fe468ec6877721d445dcaffe41728df0a0eafeb15e87b0753,\n y: 0x2ff4482400ad3a6228be17a2af33e2bcdf41be04795f9782bd96efe7e24f8778,\n },\n );\n assert_eq(\n pedersen_hash_with_separator([1, 2, 3], 3),\n 0x0bc694b7a1f8d10d2d8987d07433f26bd616a2d351bc79a3c540d85b6206dbe4,\n );\n assert_eq(\n pedersen_commitment_with_separator([1, 2, 3], 3),\n EmbeddedCurvePoint {\n x: 0x1fee4e8cf8d2f527caa2684236b07c4b1bad7342c01b0f75e9a877a71827dc85,\n y: 0x2f9fedb9a090697ab69bf04c8bc15f7385b3e4b68c849c1536e5ae15ff138fd1,\n },\n );\n assert_eq(\n pedersen_hash_with_separator([1, 2, 3, 4], 4),\n 0xdae10fb32a8408521803905981a2b300d6a35e40e798743e9322b223a5eddc,\n );\n assert_eq(\n pedersen_commitment_with_separator([1, 2, 3, 4], 4),\n EmbeddedCurvePoint {\n x: 0x07ae3e202811e1fca39c2d81eabe6f79183978e6f12be0d3b8eda095b79bdbc9,\n y: 0x0afc6f892593db6fbba60f2da558517e279e0ae04f95758587760ba193145014,\n },\n );\n assert_eq(\n pedersen_hash_with_separator([1, 2, 3, 4, 5], 5),\n 0xfc375b062c4f4f0150f7100dfb8d9b72a6d28582dd9512390b0497cdad9c22,\n );\n assert_eq(\n pedersen_commitment_with_separator([1, 2, 3, 4, 5], 5),\n EmbeddedCurvePoint {\n x: 0x1754b12bd475a6984a1094b5109eeca9838f4f81ac89c5f0a41dbce53189bb29,\n y: 0x2da030e3cfcdc7ddad80eaf2599df6692cae0717d4e9f7bfbee8d073d5d278f7,\n },\n );\n assert_eq(\n pedersen_hash_with_separator([1, 2, 3, 4, 5, 6], 6),\n 0x1696ed13dc2730062a98ac9d8f9de0661bb98829c7582f699d0273b18c86a572,\n );\n assert_eq(\n pedersen_commitment_with_separator([1, 2, 3, 4, 5, 6], 6),\n EmbeddedCurvePoint {\n x: 0x190f6c0e97ad83e1e28da22a98aae156da083c5a4100e929b77e750d3106a697,\n y: 0x1f4b60f34ef91221a0b49756fa0705da93311a61af73d37a0c458877706616fb,\n },\n );\n assert_eq(\n pedersen_hash_with_separator([1, 2, 3, 4, 5, 6, 7], 7),\n 0x128c0ff144fc66b6cb60eeac8a38e23da52992fc427b92397a7dffd71c45ede3,\n );\n assert_eq(\n pedersen_commitment_with_separator([1, 2, 3, 4, 5, 6, 7], 7),\n EmbeddedCurvePoint {\n x: 0x015441e9d29491b06563fac16fc76abf7a9534c715421d0de85d20dbe2965939,\n y: 0x1d2575b0276f4e9087e6e07c2cb75aa1baafad127af4be5918ef8a2ef2fea8fc,\n },\n );\n assert_eq(\n pedersen_hash_with_separator([1, 2, 3, 4, 5, 6, 7, 8], 8),\n 0x2f960e117482044dfc99d12fece2ef6862fba9242be4846c7c9a3e854325a55c,\n );\n assert_eq(\n pedersen_commitment_with_separator([1, 2, 3, 4, 5, 6, 7, 8], 8),\n EmbeddedCurvePoint {\n x: 0x1657737676968887fceb6dd516382ea13b3a2c557f509811cd86d5d1199bc443,\n y: 0x1f39f0cb569040105fa1e2f156521e8b8e08261e635a2b210bdc94e8d6d65f77,\n },\n );\n assert_eq(\n pedersen_hash_with_separator([1, 2, 3, 4, 5, 6, 7, 8, 9], 9),\n 0x0c96db0790602dcb166cc4699e2d306c479a76926b81c2cb2aaa92d249ec7be7,\n );\n assert_eq(\n pedersen_commitment_with_separator([1, 2, 3, 4, 5, 6, 7, 8, 9], 9),\n EmbeddedCurvePoint {\n x: 0x0a3ceae42d14914a432aa60ec7fded4af7dad7dd4acdbf2908452675ec67e06d,\n y: 0xfc19761eaaf621ad4aec9a8b2e84a4eceffdba78f60f8b9391b0bd9345a2f2,\n },\n );\n assert_eq(\n pedersen_hash_with_separator([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 10),\n 0x2cd37505871bc460a62ea1e63c7fe51149df5d0801302cf1cbc48beb8dff7e94,\n );\n assert_eq(\n pedersen_commitment_with_separator([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 10),\n EmbeddedCurvePoint {\n x: 0x2fb3f8b3d41ddde007c8c3c62550f9a9380ee546fcc639ffbb3fd30c8d8de30c,\n y: 0x300783be23c446b11a4c0fabf6c91af148937cea15fcf5fb054abf7f752ee245,\n },\n );\n}\n","path":"std/hash/mod.nr","function_locations":[{"start":572,"name":"sha256_compression"},{"start":707,"name":"keccakf1600"},{"start":882,"name":"keccak::keccakf1600"},{"start":1044,"name":"blake2s"},{"start":1142,"name":"blake3"},{"start":1629,"name":"__blake3"},{"start":1747,"name":"pedersen_commitment"},{"start":1976,"name":"pedersen_commitment_with_separator"},{"start":2380,"name":"pedersen_hash"},{"start":2537,"name":"pedersen_hash_with_separator"},{"start":3531,"name":"derive_generators"},{"start":3890,"name":"__derive_generators"},{"start":3968,"name":"poseidon2_permutation"},{"start":4324,"name":"poseidon2_permutation_internal"},{"start":4417,"name":"poseidon2_config_state_size"},{"start":4728,"name":"derive_hash"},{"start":5953,"name":">::build_hasher"},{"start":6085,"name":">::default"},{"start":6217,"name":"::hash"},{"start":6347,"name":"::hash"},{"start":6487,"name":"::hash"},{"start":6627,"name":"::hash"},{"start":6767,"name":"::hash"},{"start":6908,"name":"::hash"},{"start":7047,"name":"::hash"},{"start":7193,"name":"::hash"},{"start":7340,"name":"::hash"},{"start":7487,"name":"::hash"},{"start":7635,"name":"::hash"},{"start":7782,"name":"::hash"},{"start":7914,"name":"::hash"},{"start":8103,"name":"::hash"},{"start":8343,"name":"::hash"},{"start":8559,"name":"::hash"},{"start":8822,"name":"::hash"},{"start":9132,"name":"::hash"},{"start":9528,"name":"assert_pedersen"}]},"51":{"source":"// ============================================================\n// PrivacyLayer - Commitment Circuit\n// ============================================================\n// Proves knowledge of (nullifier, secret) such that:\n// commitment = Hash(nullifier, secret)\n//\n// This is the simplest building block - used during deposit.\n// The commitment is stored publicly in the on-chain Merkle tree.\n// The nullifier and secret are kept private by the user.\n//\n// Inspired by:\n// - Tornado Cash commitment scheme\n// - Penumbra note commitment (protocol.penumbra.zone)\n// ============================================================\n\nuse lib::hash;\nuse lib::validation;\nmod fixtures;\n\n/// Commitment circuit - proves knowledge of a note's preimage.\n///\n/// # Private inputs\n/// - `nullifier` : unique per-note random field element; revealed on spend\n/// - `secret` : random field element; never revealed\n///\n/// # Public inputs\n/// - `pool_id` : unique identifier for the shielded pool\n/// - `commitment` : Hash(nullifier, secret, pool_id) - stored on-chain\nfn main(\n // Private witnesses\n nullifier: Field,\n secret: Field,\n // Public statement\n pool_id: pub Field,\n commitment: pub Field,\n) {\n // Compute commitment from private inputs\n let computed_commitment = hash::compute_commitment(nullifier, secret, pool_id);\n\n // Validate the computed commitment matches the public on-chain value\n validation::validate_commitment(computed_commitment, commitment);\n}\n\n// ============================================================\n// Tests - following noir-lang/noir-examples test patterns\n// ============================================================\n// Total tests: 17\n// - Happy path tests (4)\n// - Zero / boundary tests (4)\n// - Collision / uniqueness tests (4)\n// - Attack / failure tests (5)\n// ============================================================\n\n// --------------------------------------------\n// Happy Path Tests\n// --------------------------------------------\n\n/// TC-C-01: Basic valid commitment with small field values.\n/// Verifies the circuit accepts correctly-formed proofs.\n#[test]\nfn test_valid_commitment() {\n // Known values: commitment = Hash(nullifier=1, secret=2, pool_id=3)\n let nullifier: Field = 1;\n let secret: Field = 2;\n let pool_id: Field = 3;\n let commitment = hash::compute_commitment(nullifier, secret, pool_id);\n\n // Should pass - correct preimage\n main(nullifier, secret, pool_id, commitment);\n}\n\n/// TC-C-02: Valid commitment with large hex-encoded field elements\n/// (simulates realistic SDK-generated random values near field size).\n#[test]\nfn test_valid_commitment_large_values() {\n let nullifier: Field = 0x0000000000000000000000000000000000000000000000000000000000000064; // 100\n let secret: Field = 0x00000000000000000000000000000000000000000000000000000000000003e8; // 1000\n let pool_id: Field = 0x0000000000000000000000000000000000000000000000000000000000000001; // 1\n let commitment = hash::compute_commitment(nullifier, secret, pool_id);\n\n main(nullifier, secret, pool_id, commitment);\n}\n\n/// TC-C-03: Valid commitment with maximum safe field value.\n/// BN254 scalar field modulus r = 2^254 - ... This uses a value that is\n/// valid within the field. The circuit must handle high-valued witnesses.\n#[test]\nfn test_valid_commitment_near_max_field() {\n // A large prime-like field element near (but within) the BN254 scalar field.\n // BN254 r ~ 2^254, so 2^253 is safely inside the field.\n let nullifier: Field = 0x0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;\n let secret: Field = 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;\n let pool_id: Field = 0x000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;\n let commitment = hash::compute_commitment(nullifier, secret, pool_id);\n\n main(nullifier, secret, pool_id, commitment);\n}\n\n/// TC-C-04: Determinism -- same inputs always yield the exact same commitment.\n/// Essential for note reconstruction and proof generation.\n#[test]\nfn test_commitment_is_deterministic() {\n let nullifier: Field = 0xdeadbeef;\n let secret: Field = 0xcafebabe;\n let pool_id: Field = 0x12345678;\n\n let c1 = hash::compute_commitment(nullifier, secret, pool_id);\n let c2 = hash::compute_commitment(nullifier, secret, pool_id);\n\n // If commitments are equal, the circuit accepts both\n assert(c1 == c2, \"commitment must be deterministic\");\n main(nullifier, secret, pool_id, c1);\n}\n\n/// Shared cross-stack fixtures generated from the same source as\n/// artifacts/zk/commitment_vectors.json. These pin exact Noir <-> SDK outputs.\n#[test]\nfn test_shared_fixture_cv_001() {\n let (nullifier, secret, pool_id, commitment) = fixtures::fixture_cv_001();\n assert(hash::compute_commitment(nullifier, secret, pool_id) == commitment);\n main(nullifier, secret, pool_id, commitment);\n}\n\n#[test]\nfn test_shared_fixture_cv_002() {\n let (nullifier, secret, pool_id, commitment) = fixtures::fixture_cv_002();\n assert(hash::compute_commitment(nullifier, secret, pool_id) == commitment);\n main(nullifier, secret, pool_id, commitment);\n}\n\n#[test]\nfn test_shared_fixture_cv_003() {\n let (nullifier, secret, pool_id, commitment) = fixtures::fixture_cv_003();\n assert(hash::compute_commitment(nullifier, secret, pool_id) == commitment);\n main(nullifier, secret, pool_id, commitment);\n}\n\n#[test]\nfn test_shared_fixture_cv_004() {\n let (nullifier, secret, pool_id, commitment) = fixtures::fixture_cv_004();\n assert(hash::compute_commitment(nullifier, secret, pool_id) == commitment);\n main(nullifier, secret, pool_id, commitment);\n}\n\n// --------------------------------------------\n// Zero / Boundary Value Tests\n// --------------------------------------------\n\n/// TC-C-05: Zero nullifier with zero secret -- edge case where both\n/// inputs are the additive identity. The circuit must compute and\n/// accept H(0, 0, 0) as a valid commitment (not special-cased to fail).\n#[test]\nfn test_zero_inputs_valid_commitment() {\n let nullifier: Field = 0;\n let secret: Field = 0;\n let pool_id: Field = 0;\n let commitment = hash::compute_commitment(nullifier, secret, pool_id);\n main(nullifier, secret, pool_id, commitment);\n}\n\n/// TC-C-06: Zero nullifier -- only the secret is non-zero.\n/// Verifies partial-zero inputs are handled correctly.\n#[test]\nfn test_zero_nullifier_nonzero_secret() {\n let nullifier: Field = 0;\n let secret: Field = 12345;\n let pool_id: Field = 1;\n let commitment = hash::compute_commitment(nullifier, secret, pool_id);\n main(nullifier, secret, pool_id, commitment);\n}\n\n/// TC-C-07: Non-zero nullifier with zero secret.\n/// Mirror of TC-C-06 for the other input.\n#[test]\nfn test_nonzero_nullifier_zero_secret() {\n let nullifier: Field = 99999;\n let secret: Field = 0;\n let pool_id: Field = 1;\n let commitment = hash::compute_commitment(nullifier, secret, pool_id);\n main(nullifier, secret, pool_id, commitment);\n}\n\n/// TC-C-08: nullifier == secret (identical inputs).\n/// The hash function ought to distinguish this from other cases;\n/// the circuit accepts it as long as the commitment is correct.\n#[test]\nfn test_identical_nullifier_and_secret() {\n let nullifier: Field = 7777;\n let secret: Field = 7777;\n let pool_id: Field = 1;\n let commitment = hash::compute_commitment(nullifier, secret, pool_id);\n main(nullifier, secret, pool_id, commitment);\n}\n\n// --------------------------------------------\n// Collision / Uniqueness Tests\n// --------------------------------------------\n\n/// TC-C-09: Different notes must produce different commitments (collision resistance).\n/// A collision would allow two depositors to share a single on-chain slot.\n#[test]\nfn test_no_commitment_collision_different_inputs() {\n let c1 = hash::compute_commitment(1, 2, 1);\n let c2 = hash::compute_commitment(3, 4, 1);\n assert(c1 != c2, \"different (nullifier, secret) pairs must not collide\");\n}\n\n/// TC-C-10: Verify the hash is NOT symmetric, i.e. H(a,b,c) != H(b,a,c).\n/// If it were symmetric, swapped inputs would open any note.\n#[test]\nfn test_commitment_is_not_symmetric() {\n let a: Field = 10;\n let b: Field = 20;\n let c: Field = 30;\n let c_abc = hash::compute_commitment(a, b, c);\n let c_bac = hash::compute_commitment(b, a, c);\n assert(c_abc != c_bac, \"H(a,b,c) must differ from H(b,a,c) – hash must be non-symmetric\");\n}\n\n/// TC-C-11: Verifying that incrementing nullifier by 1 changes the commitment.\n/// Ensures small deltas are visible (no partial-preimage attack surface).\n#[test]\nfn test_adjacent_nullifiers_produce_distinct_commitments() {\n let secret: Field = 100;\n let pool_id: Field = 1;\n let c1 = hash::compute_commitment(1, secret, pool_id);\n let c2 = hash::compute_commitment(2, secret, pool_id);\n assert(c1 != c2, \"adjacent nullifiers must produce different commitments\");\n}\n\n/// TC-C-17: Regression: Same secret material in different pools must yield different commitments.\n/// This prevents note replay across pools.\n#[test]\nfn test_same_secret_different_pools_distinct_commitments() {\n let nullifier: Field = 0xdead;\n let secret: Field = 0xbeef;\n \n let c_pool1 = hash::compute_commitment(nullifier, secret, 1);\n let c_pool2 = hash::compute_commitment(nullifier, secret, 2);\n \n assert(c_pool1 != c_pool2, \"commitments with same secret must differ across pools\");\n}\n\n// --------------------------------------------\n// Attack / Failure Tests\n// --------------------------------------------\n\n/// TC-C-12: Wrong nullifier with correct secret and real commitment -- must fail.\n/// Simulates an adversary who knows the secret but not the nullifier.\n#[test(should_fail_with = \"commitment mismatch: invalid nullifier/secret pair\")]\nfn test_wrong_nullifier_fails() {\n let nullifier: Field = 1;\n let secret: Field = 2;\n let pool_id: Field = 3;\n let commitment = hash::compute_commitment(nullifier, secret, pool_id);\n\n // Provide wrong nullifier - should fail the assertion\n main(999, secret, pool_id, commitment);\n}\n\n/// TC-C-13: Wrong secret with correct nullifier and real commitment -- must fail.\n/// Simulates an adversary who knows the nullifier but not the secret.\n#[test(should_fail_with = \"commitment mismatch: invalid nullifier/secret pair\")]\nfn test_wrong_secret_fails() {\n let nullifier: Field = 1;\n let secret: Field = 2;\n let pool_id: Field = 3;\n let commitment = hash::compute_commitment(nullifier, secret, pool_id);\n\n // Provide wrong secret - should fail the assertion\n main(nullifier, 888, pool_id, commitment);\n}\n\n/// TC-C-14: Swapped nullifier / secret -- must fail.\n/// Even when both values are known, order matters in H(nullifier, secret).\n#[test(should_fail_with = \"commitment mismatch: invalid nullifier/secret pair\")]\nfn test_swapped_inputs_fails() {\n let nullifier: Field = 1;\n let secret: Field = 2;\n let pool_id: Field = 3;\n let commitment = hash::compute_commitment(nullifier, secret, pool_id);\n\n // Swap nullifier and secret - should fail (hash is not symmetric)\n main(secret, nullifier, pool_id, commitment);\n}\n\n/// TC-C-15: Fabricated (random) commitment value with zero inputs -- must fail.\n/// An attacker who cannot compute the preimage should never be able to pass\n/// a random commitment through the circuit.\n#[test(should_fail_with = \"commitment mismatch: invalid nullifier/secret pair\")]\nfn test_zero_inputs_with_fabricated_commitment_fails() {\n // Both zero with fabricated commitment value - should fail\n main(0, 0, 0, 12345);\n}\n\n/// TC-C-16: Off-by-one commitment -- commitment is H(nullifier, secret) + 1.\n/// Ensures the constraint rejects values that are only slightly wrong.\n#[test(should_fail_with = \"commitment mismatch: invalid nullifier/secret pair\")]\nfn test_off_by_one_commitment_fails() {\n let nullifier: Field = 42;\n let secret: Field = 43;\n let pool_id: Field = 44;\n let real_commitment = hash::compute_commitment(nullifier, secret, pool_id);\n // Add 1 to produce an off-by-one value (will wrap in the field -- still wrong)\n let tampered: Field = real_commitment + 1;\n main(nullifier, secret, pool_id, tampered);\n}\n\n// ============================================================\n// ZK-018: Edge-case commitment regression corpus\n// ============================================================\n// Additional tests covering near-field-limit values, incremental\n// deltas on all three inputs, and cross-domain isolation.\n// These vectors are shared between this Noir test suite and the\n// SDK commitment corpus tests in sdk/test/commitment_corpus.test.ts.\n// ============================================================\n\n/// TC-C-18: Near-field-limit nullifier with zero secret.\n/// Tests that a nullifier at the high end of the safe field range\n/// produces a valid and distinct commitment.\n#[test]\nfn test_near_field_limit_nullifier_zero_secret() {\n // 0x0FFFF... is well within BN254 (< r) but near the 31-byte ceiling\n let nullifier: Field = 0x0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;\n let secret: Field = 0;\n let pool_id: Field = 1;\n let c = hash::compute_commitment(nullifier, secret, pool_id);\n main(nullifier, secret, pool_id, c);\n assert(c != 0, \"near-max nullifier with zero secret must still produce non-zero commitment\");\n}\n\n/// TC-C-19: Zero nullifier with near-field-limit secret.\n/// Mirror of TC-C-18 to confirm neither field position is special-cased.\n#[test]\nfn test_zero_nullifier_near_field_limit_secret() {\n let nullifier: Field = 0;\n let secret: Field = 0x0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;\n let pool_id: Field = 1;\n let c = hash::compute_commitment(nullifier, secret, pool_id);\n main(nullifier, secret, pool_id, c);\n assert(c != 0, \"zero nullifier with near-max secret must produce non-zero commitment\");\n}\n\n/// TC-C-20: TC-C-18 and TC-C-19 produce different commitments.\n/// Verifies that swapping near-max and zero between the two positions\n/// does not accidentally collide.\n#[test]\nfn test_near_field_limit_position_swap_produces_distinct_commitments() {\n let high: Field = 0x0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;\n let pool_id: Field = 1;\n let c1 = hash::compute_commitment(high, 0, pool_id);\n let c2 = hash::compute_commitment(0, high, pool_id);\n assert(c1 != c2, \"swapping near-max and zero between nullifier/secret must yield distinct commitments\");\n}\n\n/// TC-C-21: Incremental delta on all three inputs simultaneously.\n/// Verifies that a unit increment on each input changes the commitment,\n/// ruling out any accidental cancellation in the hash.\n#[test]\nfn test_incremental_delta_all_inputs_changes_commitment() {\n let c_base = hash::compute_commitment(100, 200, 300);\n let c_null_inc = hash::compute_commitment(101, 200, 300);\n let c_sec_inc = hash::compute_commitment(100, 201, 300);\n let c_pool_inc = hash::compute_commitment(100, 200, 301);\n\n assert(c_base != c_null_inc, \"nullifier+1 must change the commitment\");\n assert(c_base != c_sec_inc, \"secret+1 must change the commitment\");\n assert(c_base != c_pool_inc, \"pool_id+1 must change the commitment\");\n // All four must be mutually distinct\n assert(c_null_inc != c_sec_inc, \"nullifier-delta and secret-delta commitments must differ\");\n assert(c_null_inc != c_pool_inc, \"nullifier-delta and pool_id-delta commitments must differ\");\n assert(c_sec_inc != c_pool_inc, \"secret-delta and pool_id-delta commitments must differ\");\n}\n\n/// TC-C-22: All-equal inputs (nullifier == secret == pool_id).\n/// An edge case where the three preimage positions carry identical values;\n/// the circuit must still accept the commitment and produce a unique output.\n#[test]\nfn test_all_equal_inputs_valid_and_unique() {\n let v: Field = 0xabcdef;\n let c = hash::compute_commitment(v, v, v);\n main(v, v, v, c);\n // Must differ from a commitment with only two equal inputs\n let c2 = hash::compute_commitment(v, v, 0);\n assert(c != c2, \"all-equal inputs must yield a different commitment than a partially-zero variant\");\n}\n","path":"/Users/ab/stellardripsjess/PrivacyLayer/circuits/commitment/src/main.nr","function_locations":[{"start":1213,"name":"main"},{"start":2170,"name":"test_valid_commitment"},{"start":2684,"name":"test_valid_commitment_large_values"},{"start":3379,"name":"test_valid_commitment_near_max_field"},{"start":4124,"name":"test_commitment_is_deterministic"},{"start":4726,"name":"test_shared_fixture_cv_001"},{"start":4980,"name":"test_shared_fixture_cv_002"},{"start":5234,"name":"test_shared_fixture_cv_003"},{"start":5488,"name":"test_shared_fixture_cv_004"},{"start":6086,"name":"test_zero_inputs_valid_commitment"},{"start":6470,"name":"test_zero_nullifier_nonzero_secret"},{"start":6835,"name":"test_nonzero_nullifier_zero_secret"},{"start":7292,"name":"test_identical_nullifier_and_secret"},{"start":7870,"name":"test_no_commitment_collision_different_inputs"},{"start":8231,"name":"test_commitment_is_not_symmetric"},{"start":8726,"name":"test_adjacent_nullifiers_produce_distinct_commitments"},{"start":9196,"name":"test_same_secret_different_pools_distinct_commitments"},{"start":9889,"name":"test_wrong_nullifier_fails"},{"start":10427,"name":"test_wrong_secret_fails"},{"start":10943,"name":"test_swapped_inputs_fails"},{"start":11574,"name":"test_zero_inputs_with_fabricated_commitment_fails"},{"start":11938,"name":"test_off_by_one_commitment_fails"},{"start":13026,"name":"test_near_field_limit_nullifier_zero_secret"},{"start":13654,"name":"test_zero_nullifier_near_field_limit_secret"},{"start":14262,"name":"test_near_field_limit_position_swap_produces_distinct_commitments"},{"start":14870,"name":"test_incremental_delta_all_inputs_changes_commitment"},{"start":15959,"name":"test_all_equal_inputs_valid_and_unique"}]},"53":{"source":"use std::hash::poseidon2_permutation;\n\nfn poseidon2_hash_3(a: Field, b: Field, c: Field) -> Field {\n let iv: Field = 3 * 18_446_744_073_709_551_616;\n let state: [Field; 4] = [a, b, c, iv];\n let permuted = poseidon2_permutation(state);\n permuted[0]\n}\n\n/// Compute commitment from nullifier, secret and pool_id.\n/// commitment = Hash(nullifier, secret, pool_id)\npub fn compute_commitment(nullifier: Field, secret: Field, pool_id: Field) -> Field {\n poseidon2_hash_3(nullifier, secret, pool_id)\n}\n","path":"/Users/ab/stellardripsjess/PrivacyLayer/circuits/lib/src/hash/commitment.nr","function_locations":[{"start":98,"name":"poseidon2_hash_3"},{"start":456,"name":"compute_commitment"}]},"54":{"source":"// ============================================================\n// Hash Utilities\n// ============================================================\n// Centralized hash functions used across all circuits.\n//\n// The top-level API stays stable for downstream circuits while the\n// implementation lives in focused submodules.\n// ============================================================\n\npub mod commitment;\npub mod nullifier;\npub mod pair;\npub mod zeroes;\n\n/// Compute commitment from nullifier, secret and pool_id.\n/// commitment = Hash(nullifier, secret, pool_id)\npub fn compute_commitment(nullifier: Field, secret: Field, pool_id: Field) -> Field {\n commitment::compute_commitment(nullifier, secret, pool_id)\n}\n\n/// Compute nullifier hash bound to a specific root.\n/// nullifier_hash = Hash(nullifier, root)\n/// This prevents replay attacks across different pool states.\npub fn compute_nullifier_hash(nullifier: Field, root: Field) -> Field {\n nullifier::compute_nullifier_hash(nullifier, root)\n}\n\n/// Compute parent hash in Merkle tree.\n/// parent = Hash(left, right)\npub fn hash_pair(left: Field, right: Field) -> Field {\n pair::hash_pair(left, right)\n}\n\n/// Canonical zero leaf: H(0, 0).\npub fn zero_leaf() -> Field {\n zeroes::zero_leaf()\n}\n\n/// Canonical zero node at the given Merkle level.\npub fn zero_node_at_level(level: u32) -> Field {\n zeroes::zero_node_at_level(level)\n}\n\n/// Canonical zero-sibling path for a sparse single-leaf tree.\npub fn zero_sibling_path() -> [Field; 20] {\n zeroes::zero_sibling_path()\n}\n","path":"/Users/ab/stellardripsjess/PrivacyLayer/circuits/lib/src/hash/mod.nr","function_locations":[{"start":648,"name":"compute_commitment"},{"start":945,"name":"compute_nullifier_hash"},{"start":1129,"name":"hash_pair"},{"start":1229,"name":"zero_leaf"},{"start":1356,"name":"zero_node_at_level"},{"start":1504,"name":"zero_sibling_path"}]},"64":{"source":"/// Validate commitment matches the computed value.\npub fn validate_commitment(computed: Field, expected: Field) {\n assert(computed == expected, \"commitment mismatch: invalid nullifier/secret pair\");\n}\n","path":"/Users/ab/stellardripsjess/PrivacyLayer/circuits/lib/src/validation/commitment.nr","function_locations":[{"start":113,"name":"validate_commitment"}]},"66":{"source":"// ============================================================\n// Validation Utilities\n// ============================================================\n// Common validation functions used across circuits.\n// ============================================================\n\npub mod commitment;\npub mod fee;\npub mod nullifier;\npub mod relayer;\n\n// Test helper utilities (shared across all circuit test suites)\npub mod test_helpers;\n\n/// Validate that fee does not exceed the withdrawal amount.\npub fn validate_fee(fee_value: Field, amount: Field) {\n fee::validate_fee(fee_value, amount);\n}\n\n/// Validate relayer address consistency with fee.\n/// If fee is zero, relayer must be zero address.\npub fn validate_relayer(relayer_value: Field, fee_value: Field) {\n relayer::validate_relayer(relayer_value, fee_value);\n}\n\n/// Validate commitment matches the computed value.\npub fn validate_commitment(computed: Field, expected: Field) {\n commitment::validate_commitment(computed, expected);\n}\n\n/// Validate nullifier hash matches the computed value.\npub fn validate_nullifier_hash(computed: Field, expected: Field) {\n nullifier::validate_nullifier_hash(computed, expected);\n}\n","path":"/Users/ab/stellardripsjess/PrivacyLayer/circuits/lib/src/validation/mod.nr","function_locations":[{"start":542,"name":"validate_fee"},{"start":754,"name":"validate_relayer"},{"start":929,"name":"validate_commitment"},{"start":1112,"name":"validate_nullifier_hash"}]}}} \ No newline at end of file +{ + "noir_version": "1.0.0-beta.20+b4236c1957d0c26cb65d82adc9e5447b6ff1d629", + "hash": "134997435106575238", + "abi": { + "parameters": [ + { + "name": "nullifier", + "type": { + "kind": "field" + }, + "visibility": "private" + }, + { + "name": "secret", + "type": { + "kind": "field" + }, + "visibility": "private" + }, + { + "name": "pool_id", + "type": { + "kind": "field" + }, + "visibility": "public" + }, + { + "name": "commitment", + "type": { + "kind": "field" + }, + "visibility": "public" + } + ], + "return_type": null, + "error_types": { + "11287755292792099533": { + "error_kind": "string", + "string": "commitment mismatch: invalid nullifier/secret pair" + } + } + }, + "bytecode": "H4sIAAAAAAAA/42QLQvCQByHb5tvH8OozZdmU1FMsqYIhjEPPJx3cjdB432D7T/BYhARBZNBRPuq+BnWjBa7lg0ERZ/0hF94+GngztdDg9CUJ3dlITC3O5gzz3HBT+d6TR7kl9mTXjtK2e5mirfG9Dxyq8HDuyOEYn4afUYLRe4rlmEOKmxSH1OzaliWPOhMYNJjtKBjPhzbhk0YhZnctohNsRAoMiUyNbLYLJ5Ipt5b4Y9WDb7GhijxnxMESAFVe70j12WTcBWui012VerDxXGehFReP0wBAAA=", + "debug_symbols": "nZPRioQgFIbfxesuPKZW8yrLElY2CGLh5MISvftaZI4XxjI3lZ6+z1+OrmiQnXu2yozTCz2+VtRZpbV6tnrqxaIm42fXrUBh2C5WSj+F3uqemoWVZkEP47Qu0I/Q7vjpNQtzvBdhfRUXSJrBv71wVFruX1sRaZxHgTRw0lDS8hIwSAyQN3BGT0EFJPI04Umep5ycPMM88mXCl3c7CNsHhj/gCYT8hJIcz27yQxUCUIKrywD1vxPQOiRgTS7BXQ8pCVsAWte5HtZ5Q8OqIMA4CniVCJq7JvArQWwi94fg249Er2xy8BH2xQLB8ST7qtvutkp0Wp43Y3Smf7soy+8cKuEqzXbq5eCs3N1Hza/2Bw==", + "file_map": { + "18": { + "source": "// Exposed only for usage in `std::meta`\npub(crate) mod poseidon2;\n\nuse crate::default::Default;\nuse crate::embedded_curve_ops::{\n EmbeddedCurvePoint, EmbeddedCurveScalar, multi_scalar_mul, multi_scalar_mul_array_return,\n};\nuse crate::meta::derive_via;\nuse crate::static_assert;\n\n/// The size of the state accepted by the backend in `poseidon2_permutation`.\nglobal POSEIDON2_CONFIG_STATE_SIZE: u32 = poseidon2_config_state_size();\n\n#[foreign(sha256_compression)]\n// docs:start:sha256_compression\npub fn sha256_compression(input: [u32; 16], state: [u32; 8]) -> [u32; 8] {}\n// docs:end:sha256_compression\n\n#[foreign(keccakf1600)]\n// docs:start:keccakf1600\npub fn keccakf1600(input: [u64; 25]) -> [u64; 25] {}\n// docs:end:keccakf1600\n\npub mod keccak {\n #[deprecated(\"This function has been moved to std::hash::keccakf1600\")]\n pub fn keccakf1600(input: [u64; 25]) -> [u64; 25] {\n super::keccakf1600(input)\n }\n}\n\n#[foreign(blake2s)]\n// docs:start:blake2s\npub fn blake2s(input: [u8; N]) -> [u8; 32]\n// docs:end:blake2s\n{}\n\n// docs:start:blake3\npub fn blake3(input: [u8; N]) -> [u8; 32]\n// docs:end:blake3\n{\n if crate::runtime::is_unconstrained() {\n // Temporary measure while Barretenberg is main proving system.\n // Please open an issue if you're working on another proving system and running into problems due to this.\n crate::static_assert(\n N <= 1024,\n \"Barretenberg cannot prove blake3 hashes with inputs larger than 1024 bytes\",\n );\n }\n __blake3(input)\n}\n\n#[foreign(blake3)]\nfn __blake3(input: [u8; N]) -> [u8; 32] {}\n\n// docs:start:pedersen_commitment\npub fn pedersen_commitment(input: [Field; N]) -> EmbeddedCurvePoint {\n // docs:end:pedersen_commitment\n pedersen_commitment_with_separator(input, 0)\n}\n\n#[inline_always]\npub fn pedersen_commitment_with_separator(\n input: [Field; N],\n separator: u32,\n) -> EmbeddedCurvePoint {\n let mut points = [EmbeddedCurveScalar { lo: 0, hi: 0 }; N];\n for i in 0..N {\n points[i] = EmbeddedCurveScalar::from_field(input[i]);\n }\n let generators = derive_generators(\"DEFAULT_DOMAIN_SEPARATOR\".as_bytes(), separator);\n multi_scalar_mul(generators, points)\n}\n\n// docs:start:pedersen_hash\npub fn pedersen_hash(input: [Field; N]) -> Field\n// docs:end:pedersen_hash\n{\n pedersen_hash_with_separator(input, 0)\n}\n\n#[no_predicates]\npub fn pedersen_hash_with_separator(input: [Field; N], separator: u32) -> Field {\n let mut scalars: [EmbeddedCurveScalar; N + 1] = [EmbeddedCurveScalar { lo: 0, hi: 0 }; N + 1];\n let mut generators: [EmbeddedCurvePoint; N + 1] =\n [EmbeddedCurvePoint::point_at_infinity(); N + 1];\n crate::assert_constant(separator);\n let domain_generators: [EmbeddedCurvePoint; N] =\n derive_generators(\"DEFAULT_DOMAIN_SEPARATOR\".as_bytes(), separator);\n\n for i in 0..N {\n scalars[i] = EmbeddedCurveScalar::from_field(input[i]);\n generators[i] = domain_generators[i];\n }\n scalars[N] = EmbeddedCurveScalar { lo: N as Field, hi: 0 as Field };\n\n let length_generator: [EmbeddedCurvePoint; 1] =\n derive_generators(\"pedersen_hash_length\".as_bytes(), 0);\n generators[N] = length_generator[0];\n multi_scalar_mul_array_return(generators, scalars, true)[0].x\n}\n\n#[field(bn254)]\n#[inline_always]\npub fn derive_generators(\n domain_separator_bytes: [u8; M],\n starting_index: u32,\n) -> [EmbeddedCurvePoint; N] {\n crate::assert_constant(domain_separator_bytes);\n crate::assert_constant(starting_index);\n __derive_generators(domain_separator_bytes, starting_index)\n}\n\n#[builtin(derive_pedersen_generators)]\n#[field(bn254)]\nfn __derive_generators(\n domain_separator_bytes: [u8; M],\n starting_index: u32,\n) -> [EmbeddedCurvePoint; N] {}\n\npub fn poseidon2_permutation(input: [Field; N]) -> [Field; N] {\n static_assert(\n N == POSEIDON2_CONFIG_STATE_SIZE,\n f\"the input length must equal the state size in the Poseidon2 config; expected {POSEIDON2_CONFIG_STATE_SIZE}, got {N}\",\n );\n poseidon2_permutation_internal(input)\n}\n\n#[foreign(poseidon2_permutation)]\nfn poseidon2_permutation_internal(input: [Field; N]) -> [Field; N] {}\n\n#[foreign(poseidon2_config_state_size)]\ncomptime fn poseidon2_config_state_size() -> u32 {}\n\n// Generic hashing support.\n// Partially ported and impacted by rust.\n\n// Hash trait shall be implemented per type.\n#[derive_via(derive_hash)]\npub trait Hash {\n fn hash(self, state: &mut H)\n where\n H: Hasher;\n}\n\n// docs:start:derive_hash\ncomptime fn derive_hash(s: TypeDefinition) -> Quoted {\n let name = quote { $crate::hash::Hash };\n let signature = quote { fn hash(_self: Self, _state: &mut H) where H: $crate::hash::Hasher };\n let for_each_field = |name| quote { _self.$name.hash(_state); };\n crate::meta::make_trait_impl(\n s,\n name,\n signature,\n for_each_field,\n quote {},\n |fields| fields,\n )\n}\n// docs:end:derive_hash\n\n// Hasher trait shall be implemented by algorithms to provide hash-agnostic means.\n// TODO: consider making the types generic here ([u8], [Field], etc.)\npub trait Hasher {\n fn finish(self) -> Field;\n\n /// Returns the hash value without consuming the hasher.\n /// Override this for more efficient implementations that avoid copying.\n /// TODO: deprecate finish() and replace it\n fn finish_ref(&self) -> Field {\n (*self).finish()\n }\n\n fn write(&mut self, input: Field);\n}\n\n// BuildHasher is a factory trait, responsible for production of specific Hasher.\npub trait BuildHasher {\n type H: Hasher;\n\n fn build_hasher(self) -> H;\n}\n\npub struct BuildHasherDefault;\n\nimpl BuildHasher for BuildHasherDefault\nwhere\n H: Hasher + Default,\n{\n type H = H;\n\n fn build_hasher(_self: Self) -> H {\n H::default()\n }\n}\n\nimpl Default for BuildHasherDefault\nwhere\n H: Hasher + Default,\n{\n fn default() -> Self {\n BuildHasherDefault {}\n }\n}\n\nimpl Hash for Field {\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n H::write(state, self);\n }\n}\n\nimpl Hash for u8 {\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n H::write(state, self as Field);\n }\n}\n\nimpl Hash for u16 {\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n H::write(state, self as Field);\n }\n}\n\nimpl Hash for u32 {\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n H::write(state, self as Field);\n }\n}\n\nimpl Hash for u64 {\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n H::write(state, self as Field);\n }\n}\n\nimpl Hash for u128 {\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n H::write(state, self as Field);\n }\n}\n\nimpl Hash for i8 {\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n H::write(state, self as u8 as Field);\n }\n}\n\nimpl Hash for i16 {\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n H::write(state, self as u16 as Field);\n }\n}\n\nimpl Hash for i32 {\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n H::write(state, self as u32 as Field);\n }\n}\n\nimpl Hash for i64 {\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n H::write(state, self as u64 as Field);\n }\n}\n\nimpl Hash for bool {\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n H::write(state, self as Field);\n }\n}\n\nimpl Hash for () {\n fn hash(_self: Self, _state: &mut H)\n where\n H: Hasher,\n {}\n}\n\nimpl Hash for [T; N]\nwhere\n T: Hash,\n{\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n for elem in self {\n elem.hash(state);\n }\n }\n}\n\nimpl Hash for [T]\nwhere\n T: Hash,\n{\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n self.len().hash(state);\n for elem in self {\n elem.hash(state);\n }\n }\n}\n\nimpl Hash for (A, B)\nwhere\n A: Hash,\n B: Hash,\n{\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n self.0.hash(state);\n self.1.hash(state);\n }\n}\n\nimpl Hash for (A, B, C)\nwhere\n A: Hash,\n B: Hash,\n C: Hash,\n{\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n self.0.hash(state);\n self.1.hash(state);\n self.2.hash(state);\n }\n}\n\nimpl Hash for (A, B, C, D)\nwhere\n A: Hash,\n B: Hash,\n C: Hash,\n D: Hash,\n{\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n self.0.hash(state);\n self.1.hash(state);\n self.2.hash(state);\n self.3.hash(state);\n }\n}\n\nimpl Hash for (A, B, C, D, E)\nwhere\n A: Hash,\n B: Hash,\n C: Hash,\n D: Hash,\n E: Hash,\n{\n fn hash(self, state: &mut H)\n where\n H: Hasher,\n {\n self.0.hash(state);\n self.1.hash(state);\n self.2.hash(state);\n self.3.hash(state);\n self.4.hash(state);\n }\n}\n\n// Some test vectors for Pedersen hash and Pedersen Commitment.\n// They have been generated using the same functions so the tests are for now useless\n// but they will be useful when we switch to Noir implementation.\n#[test]\nfn assert_pedersen() {\n assert_eq(\n pedersen_hash_with_separator([1], 1),\n 0x1b3f4b1a83092a13d8d1a59f7acb62aba15e7002f4440f2275edb99ebbc2305f,\n );\n assert_eq(\n pedersen_commitment_with_separator([1], 1),\n EmbeddedCurvePoint {\n x: 0x054aa86a73cb8a34525e5bbed6e43ba1198e860f5f3950268f71df4591bde402,\n y: 0x209dcfbf2cfb57f9f6046f44d71ac6faf87254afc7407c04eb621a6287cac126,\n },\n );\n\n assert_eq(\n pedersen_hash_with_separator([1, 2], 2),\n 0x26691c129448e9ace0c66d11f0a16d9014a9e8498ee78f4d69f0083168188255,\n );\n assert_eq(\n pedersen_commitment_with_separator([1, 2], 2),\n EmbeddedCurvePoint {\n x: 0x2e2b3b191e49541fe468ec6877721d445dcaffe41728df0a0eafeb15e87b0753,\n y: 0x2ff4482400ad3a6228be17a2af33e2bcdf41be04795f9782bd96efe7e24f8778,\n },\n );\n assert_eq(\n pedersen_hash_with_separator([1, 2, 3], 3),\n 0x0bc694b7a1f8d10d2d8987d07433f26bd616a2d351bc79a3c540d85b6206dbe4,\n );\n assert_eq(\n pedersen_commitment_with_separator([1, 2, 3], 3),\n EmbeddedCurvePoint {\n x: 0x1fee4e8cf8d2f527caa2684236b07c4b1bad7342c01b0f75e9a877a71827dc85,\n y: 0x2f9fedb9a090697ab69bf04c8bc15f7385b3e4b68c849c1536e5ae15ff138fd1,\n },\n );\n assert_eq(\n pedersen_hash_with_separator([1, 2, 3, 4], 4),\n 0xdae10fb32a8408521803905981a2b300d6a35e40e798743e9322b223a5eddc,\n );\n assert_eq(\n pedersen_commitment_with_separator([1, 2, 3, 4], 4),\n EmbeddedCurvePoint {\n x: 0x07ae3e202811e1fca39c2d81eabe6f79183978e6f12be0d3b8eda095b79bdbc9,\n y: 0x0afc6f892593db6fbba60f2da558517e279e0ae04f95758587760ba193145014,\n },\n );\n assert_eq(\n pedersen_hash_with_separator([1, 2, 3, 4, 5], 5),\n 0xfc375b062c4f4f0150f7100dfb8d9b72a6d28582dd9512390b0497cdad9c22,\n );\n assert_eq(\n pedersen_commitment_with_separator([1, 2, 3, 4, 5], 5),\n EmbeddedCurvePoint {\n x: 0x1754b12bd475a6984a1094b5109eeca9838f4f81ac89c5f0a41dbce53189bb29,\n y: 0x2da030e3cfcdc7ddad80eaf2599df6692cae0717d4e9f7bfbee8d073d5d278f7,\n },\n );\n assert_eq(\n pedersen_hash_with_separator([1, 2, 3, 4, 5, 6], 6),\n 0x1696ed13dc2730062a98ac9d8f9de0661bb98829c7582f699d0273b18c86a572,\n );\n assert_eq(\n pedersen_commitment_with_separator([1, 2, 3, 4, 5, 6], 6),\n EmbeddedCurvePoint {\n x: 0x190f6c0e97ad83e1e28da22a98aae156da083c5a4100e929b77e750d3106a697,\n y: 0x1f4b60f34ef91221a0b49756fa0705da93311a61af73d37a0c458877706616fb,\n },\n );\n assert_eq(\n pedersen_hash_with_separator([1, 2, 3, 4, 5, 6, 7], 7),\n 0x128c0ff144fc66b6cb60eeac8a38e23da52992fc427b92397a7dffd71c45ede3,\n );\n assert_eq(\n pedersen_commitment_with_separator([1, 2, 3, 4, 5, 6, 7], 7),\n EmbeddedCurvePoint {\n x: 0x015441e9d29491b06563fac16fc76abf7a9534c715421d0de85d20dbe2965939,\n y: 0x1d2575b0276f4e9087e6e07c2cb75aa1baafad127af4be5918ef8a2ef2fea8fc,\n },\n );\n assert_eq(\n pedersen_hash_with_separator([1, 2, 3, 4, 5, 6, 7, 8], 8),\n 0x2f960e117482044dfc99d12fece2ef6862fba9242be4846c7c9a3e854325a55c,\n );\n assert_eq(\n pedersen_commitment_with_separator([1, 2, 3, 4, 5, 6, 7, 8], 8),\n EmbeddedCurvePoint {\n x: 0x1657737676968887fceb6dd516382ea13b3a2c557f509811cd86d5d1199bc443,\n y: 0x1f39f0cb569040105fa1e2f156521e8b8e08261e635a2b210bdc94e8d6d65f77,\n },\n );\n assert_eq(\n pedersen_hash_with_separator([1, 2, 3, 4, 5, 6, 7, 8, 9], 9),\n 0x0c96db0790602dcb166cc4699e2d306c479a76926b81c2cb2aaa92d249ec7be7,\n );\n assert_eq(\n pedersen_commitment_with_separator([1, 2, 3, 4, 5, 6, 7, 8, 9], 9),\n EmbeddedCurvePoint {\n x: 0x0a3ceae42d14914a432aa60ec7fded4af7dad7dd4acdbf2908452675ec67e06d,\n y: 0xfc19761eaaf621ad4aec9a8b2e84a4eceffdba78f60f8b9391b0bd9345a2f2,\n },\n );\n assert_eq(\n pedersen_hash_with_separator([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 10),\n 0x2cd37505871bc460a62ea1e63c7fe51149df5d0801302cf1cbc48beb8dff7e94,\n );\n assert_eq(\n pedersen_commitment_with_separator([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 10),\n EmbeddedCurvePoint {\n x: 0x2fb3f8b3d41ddde007c8c3c62550f9a9380ee546fcc639ffbb3fd30c8d8de30c,\n y: 0x300783be23c446b11a4c0fabf6c91af148937cea15fcf5fb054abf7f752ee245,\n },\n );\n}\n", + "path": "std/hash/mod.nr", + "function_locations": [ + { + "start": 572, + "name": "sha256_compression" + }, + { + "start": 707, + "name": "keccakf1600" + }, + { + "start": 882, + "name": "keccak::keccakf1600" + }, + { + "start": 1044, + "name": "blake2s" + }, + { + "start": 1142, + "name": "blake3" + }, + { + "start": 1629, + "name": "__blake3" + }, + { + "start": 1747, + "name": "pedersen_commitment" + }, + { + "start": 1976, + "name": "pedersen_commitment_with_separator" + }, + { + "start": 2380, + "name": "pedersen_hash" + }, + { + "start": 2537, + "name": "pedersen_hash_with_separator" + }, + { + "start": 3531, + "name": "derive_generators" + }, + { + "start": 3890, + "name": "__derive_generators" + }, + { + "start": 3968, + "name": "poseidon2_permutation" + }, + { + "start": 4324, + "name": "poseidon2_permutation_internal" + }, + { + "start": 4417, + "name": "poseidon2_config_state_size" + }, + { + "start": 4728, + "name": "derive_hash" + }, + { + "start": 5953, + "name": ">::build_hasher" + }, + { + "start": 6085, + "name": ">::default" + }, + { + "start": 6217, + "name": "::hash" + }, + { + "start": 6347, + "name": "::hash" + }, + { + "start": 6487, + "name": "::hash" + }, + { + "start": 6627, + "name": "::hash" + }, + { + "start": 6767, + "name": "::hash" + }, + { + "start": 6908, + "name": "::hash" + }, + { + "start": 7047, + "name": "::hash" + }, + { + "start": 7193, + "name": "::hash" + }, + { + "start": 7340, + "name": "::hash" + }, + { + "start": 7487, + "name": "::hash" + }, + { + "start": 7635, + "name": "::hash" + }, + { + "start": 7782, + "name": "::hash" + }, + { + "start": 7914, + "name": "::hash" + }, + { + "start": 8103, + "name": "::hash" + }, + { + "start": 8343, + "name": "::hash" + }, + { + "start": 8559, + "name": "::hash" + }, + { + "start": 8822, + "name": "::hash" + }, + { + "start": 9132, + "name": "::hash" + }, + { + "start": 9528, + "name": "assert_pedersen" + } + ] + }, + "51": { + "source": "// ============================================================\n// PrivacyLayer - Commitment Circuit\n// ============================================================\n// Proves knowledge of (nullifier, secret) such that:\n// commitment = Hash(nullifier, secret)\n//\n// This is the simplest building block - used during deposit.\n// The commitment is stored publicly in the on-chain Merkle tree.\n// The nullifier and secret are kept private by the user.\n//\n// Inspired by:\n// - Tornado Cash commitment scheme\n// - Penumbra note commitment (protocol.penumbra.zone)\n// ============================================================\n\nuse lib::hash;\nuse lib::validation;\nmod fixtures;\n\n/// Commitment circuit - proves knowledge of a note's preimage.\n///\n/// # Private inputs\n/// - `nullifier` : unique per-note random field element; revealed on spend\n/// - `secret` : random field element; never revealed\n///\n/// # Public inputs\n/// - `pool_id` : unique identifier for the shielded pool\n/// - `commitment` : Hash(nullifier, secret, pool_id) - stored on-chain\nfn main(\n // Private witnesses\n nullifier: Field,\n secret: Field,\n // Public statement\n pool_id: pub Field,\n commitment: pub Field,\n) {\n // Compute commitment from private inputs\n let computed_commitment = hash::compute_commitment(nullifier, secret, pool_id);\n\n // Validate the computed commitment matches the public on-chain value\n validation::validate_commitment(computed_commitment, commitment);\n}\n\n// ============================================================\n// Tests - following noir-lang/noir-examples test patterns\n// ============================================================\n// Total tests: 17\n// - Happy path tests (4)\n// - Zero / boundary tests (4)\n// - Collision / uniqueness tests (4)\n// - Attack / failure tests (5)\n// ============================================================\n\n// --------------------------------------------\n// Happy Path Tests\n// --------------------------------------------\n\n/// TC-C-01: Basic valid commitment with small field values.\n/// Verifies the circuit accepts correctly-formed proofs.\n#[test]\nfn test_valid_commitment() {\n // Known values: commitment = Hash(nullifier=1, secret=2, pool_id=3)\n let nullifier: Field = 1;\n let secret: Field = 2;\n let pool_id: Field = 3;\n let commitment = hash::compute_commitment(nullifier, secret, pool_id);\n\n // Should pass - correct preimage\n main(nullifier, secret, pool_id, commitment);\n}\n\n/// TC-C-02: Valid commitment with large hex-encoded field elements\n/// (simulates realistic SDK-generated random values near field size).\n#[test]\nfn test_valid_commitment_large_values() {\n let nullifier: Field = 0x0000000000000000000000000000000000000000000000000000000000000064; // 100\n let secret: Field = 0x00000000000000000000000000000000000000000000000000000000000003e8; // 1000\n let pool_id: Field = 0x0000000000000000000000000000000000000000000000000000000000000001; // 1\n let commitment = hash::compute_commitment(nullifier, secret, pool_id);\n\n main(nullifier, secret, pool_id, commitment);\n}\n\n/// TC-C-03: Valid commitment with maximum safe field value.\n/// BN254 scalar field modulus r = 2^254 - ... This uses a value that is\n/// valid within the field. The circuit must handle high-valued witnesses.\n#[test]\nfn test_valid_commitment_near_max_field() {\n // A large prime-like field element near (but within) the BN254 scalar field.\n // BN254 r ~ 2^254, so 2^253 is safely inside the field.\n let nullifier: Field = 0x0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;\n let secret: Field = 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;\n let pool_id: Field = 0x000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;\n let commitment = hash::compute_commitment(nullifier, secret, pool_id);\n\n main(nullifier, secret, pool_id, commitment);\n}\n\n/// TC-C-04: Determinism -- same inputs always yield the exact same commitment.\n/// Essential for note reconstruction and proof generation.\n#[test]\nfn test_commitment_is_deterministic() {\n let nullifier: Field = 0xdeadbeef;\n let secret: Field = 0xcafebabe;\n let pool_id: Field = 0x12345678;\n\n let c1 = hash::compute_commitment(nullifier, secret, pool_id);\n let c2 = hash::compute_commitment(nullifier, secret, pool_id);\n\n // If commitments are equal, the circuit accepts both\n assert(c1 == c2, \"commitment must be deterministic\");\n main(nullifier, secret, pool_id, c1);\n}\n\n/// Shared cross-stack fixtures generated from the same source as\n/// artifacts/zk/commitment_vectors.json. These pin exact Noir <-> SDK outputs.\n#[test]\nfn test_shared_fixture_cv_001() {\n let (nullifier, secret, pool_id, commitment) = fixtures::fixture_cv_001();\n assert(hash::compute_commitment(nullifier, secret, pool_id) == commitment);\n main(nullifier, secret, pool_id, commitment);\n}\n\n#[test]\nfn test_shared_fixture_cv_002() {\n let (nullifier, secret, pool_id, commitment) = fixtures::fixture_cv_002();\n assert(hash::compute_commitment(nullifier, secret, pool_id) == commitment);\n main(nullifier, secret, pool_id, commitment);\n}\n\n#[test]\nfn test_shared_fixture_cv_003() {\n let (nullifier, secret, pool_id, commitment) = fixtures::fixture_cv_003();\n assert(hash::compute_commitment(nullifier, secret, pool_id) == commitment);\n main(nullifier, secret, pool_id, commitment);\n}\n\n#[test]\nfn test_shared_fixture_cv_004() {\n let (nullifier, secret, pool_id, commitment) = fixtures::fixture_cv_004();\n assert(hash::compute_commitment(nullifier, secret, pool_id) == commitment);\n main(nullifier, secret, pool_id, commitment);\n}\n\n// --------------------------------------------\n// Zero / Boundary Value Tests\n// --------------------------------------------\n\n/// TC-C-05: Zero nullifier with zero secret -- edge case where both\n/// inputs are the additive identity. The circuit must compute and\n/// accept H(0, 0, 0) as a valid commitment (not special-cased to fail).\n#[test]\nfn test_zero_inputs_valid_commitment() {\n let nullifier: Field = 0;\n let secret: Field = 0;\n let pool_id: Field = 0;\n let commitment = hash::compute_commitment(nullifier, secret, pool_id);\n main(nullifier, secret, pool_id, commitment);\n}\n\n/// TC-C-06: Zero nullifier -- only the secret is non-zero.\n/// Verifies partial-zero inputs are handled correctly.\n#[test]\nfn test_zero_nullifier_nonzero_secret() {\n let nullifier: Field = 0;\n let secret: Field = 12345;\n let pool_id: Field = 1;\n let commitment = hash::compute_commitment(nullifier, secret, pool_id);\n main(nullifier, secret, pool_id, commitment);\n}\n\n/// TC-C-07: Non-zero nullifier with zero secret.\n/// Mirror of TC-C-06 for the other input.\n#[test]\nfn test_nonzero_nullifier_zero_secret() {\n let nullifier: Field = 99999;\n let secret: Field = 0;\n let pool_id: Field = 1;\n let commitment = hash::compute_commitment(nullifier, secret, pool_id);\n main(nullifier, secret, pool_id, commitment);\n}\n\n/// TC-C-08: nullifier == secret (identical inputs).\n/// The hash function ought to distinguish this from other cases;\n/// the circuit accepts it as long as the commitment is correct.\n#[test]\nfn test_identical_nullifier_and_secret() {\n let nullifier: Field = 7777;\n let secret: Field = 7777;\n let pool_id: Field = 1;\n let commitment = hash::compute_commitment(nullifier, secret, pool_id);\n main(nullifier, secret, pool_id, commitment);\n}\n\n// --------------------------------------------\n// Collision / Uniqueness Tests\n// --------------------------------------------\n\n/// TC-C-09: Different notes must produce different commitments (collision resistance).\n/// A collision would allow two depositors to share a single on-chain slot.\n#[test]\nfn test_no_commitment_collision_different_inputs() {\n let c1 = hash::compute_commitment(1, 2, 1);\n let c2 = hash::compute_commitment(3, 4, 1);\n assert(c1 != c2, \"different (nullifier, secret) pairs must not collide\");\n}\n\n/// TC-C-10: Verify the hash is NOT symmetric, i.e. H(a,b,c) != H(b,a,c).\n/// If it were symmetric, swapped inputs would open any note.\n#[test]\nfn test_commitment_is_not_symmetric() {\n let a: Field = 10;\n let b: Field = 20;\n let c: Field = 30;\n let c_abc = hash::compute_commitment(a, b, c);\n let c_bac = hash::compute_commitment(b, a, c);\n assert(c_abc != c_bac, \"H(a,b,c) must differ from H(b,a,c) \u2013 hash must be non-symmetric\");\n}\n\n/// TC-C-11: Verifying that incrementing nullifier by 1 changes the commitment.\n/// Ensures small deltas are visible (no partial-preimage attack surface).\n#[test]\nfn test_adjacent_nullifiers_produce_distinct_commitments() {\n let secret: Field = 100;\n let pool_id: Field = 1;\n let c1 = hash::compute_commitment(1, secret, pool_id);\n let c2 = hash::compute_commitment(2, secret, pool_id);\n assert(c1 != c2, \"adjacent nullifiers must produce different commitments\");\n}\n\n/// TC-C-17: Regression: Same secret material in different pools must yield different commitments.\n/// This prevents note replay across pools.\n#[test]\nfn test_same_secret_different_pools_distinct_commitments() {\n let nullifier: Field = 0xdead;\n let secret: Field = 0xbeef;\n \n let c_pool1 = hash::compute_commitment(nullifier, secret, 1);\n let c_pool2 = hash::compute_commitment(nullifier, secret, 2);\n \n assert(c_pool1 != c_pool2, \"commitments with same secret must differ across pools\");\n}\n\n// --------------------------------------------\n// Attack / Failure Tests\n// --------------------------------------------\n\n/// TC-C-12: Wrong nullifier with correct secret and real commitment -- must fail.\n/// Simulates an adversary who knows the secret but not the nullifier.\n#[test(should_fail_with = \"commitment mismatch: invalid nullifier/secret pair\")]\nfn test_wrong_nullifier_fails() {\n let nullifier: Field = 1;\n let secret: Field = 2;\n let pool_id: Field = 3;\n let commitment = hash::compute_commitment(nullifier, secret, pool_id);\n\n // Provide wrong nullifier - should fail the assertion\n main(999, secret, pool_id, commitment);\n}\n\n/// TC-C-13: Wrong secret with correct nullifier and real commitment -- must fail.\n/// Simulates an adversary who knows the nullifier but not the secret.\n#[test(should_fail_with = \"commitment mismatch: invalid nullifier/secret pair\")]\nfn test_wrong_secret_fails() {\n let nullifier: Field = 1;\n let secret: Field = 2;\n let pool_id: Field = 3;\n let commitment = hash::compute_commitment(nullifier, secret, pool_id);\n\n // Provide wrong secret - should fail the assertion\n main(nullifier, 888, pool_id, commitment);\n}\n\n/// TC-C-14: Swapped nullifier / secret -- must fail.\n/// Even when both values are known, order matters in H(nullifier, secret).\n#[test(should_fail_with = \"commitment mismatch: invalid nullifier/secret pair\")]\nfn test_swapped_inputs_fails() {\n let nullifier: Field = 1;\n let secret: Field = 2;\n let pool_id: Field = 3;\n let commitment = hash::compute_commitment(nullifier, secret, pool_id);\n\n // Swap nullifier and secret - should fail (hash is not symmetric)\n main(secret, nullifier, pool_id, commitment);\n}\n\n/// TC-C-15: Fabricated (random) commitment value with zero inputs -- must fail.\n/// An attacker who cannot compute the preimage should never be able to pass\n/// a random commitment through the circuit.\n#[test(should_fail_with = \"commitment mismatch: invalid nullifier/secret pair\")]\nfn test_zero_inputs_with_fabricated_commitment_fails() {\n // Both zero with fabricated commitment value - should fail\n main(0, 0, 0, 12345);\n}\n\n/// TC-C-16: Off-by-one commitment -- commitment is H(nullifier, secret) + 1.\n/// Ensures the constraint rejects values that are only slightly wrong.\n#[test(should_fail_with = \"commitment mismatch: invalid nullifier/secret pair\")]\nfn test_off_by_one_commitment_fails() {\n let nullifier: Field = 42;\n let secret: Field = 43;\n let pool_id: Field = 44;\n let real_commitment = hash::compute_commitment(nullifier, secret, pool_id);\n // Add 1 to produce an off-by-one value (will wrap in the field -- still wrong)\n let tampered: Field = real_commitment + 1;\n main(nullifier, secret, pool_id, tampered);\n}\n\n// ============================================================\n// ZK-018: Edge-case commitment regression corpus\n// ============================================================\n// Additional tests covering near-field-limit values, incremental\n// deltas on all three inputs, and cross-domain isolation.\n// These vectors are shared between this Noir test suite and the\n// SDK commitment corpus tests in sdk/test/commitment_corpus.test.ts.\n// ============================================================\n\n/// TC-C-18: Near-field-limit nullifier with zero secret.\n/// Tests that a nullifier at the high end of the safe field range\n/// produces a valid and distinct commitment.\n#[test]\nfn test_near_field_limit_nullifier_zero_secret() {\n // 0x0FFFF... is well within BN254 (< r) but near the 31-byte ceiling\n let nullifier: Field = 0x0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;\n let secret: Field = 0;\n let pool_id: Field = 1;\n let c = hash::compute_commitment(nullifier, secret, pool_id);\n main(nullifier, secret, pool_id, c);\n assert(c != 0, \"near-max nullifier with zero secret must still produce non-zero commitment\");\n}\n\n/// TC-C-19: Zero nullifier with near-field-limit secret.\n/// Mirror of TC-C-18 to confirm neither field position is special-cased.\n#[test]\nfn test_zero_nullifier_near_field_limit_secret() {\n let nullifier: Field = 0;\n let secret: Field = 0x0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;\n let pool_id: Field = 1;\n let c = hash::compute_commitment(nullifier, secret, pool_id);\n main(nullifier, secret, pool_id, c);\n assert(c != 0, \"zero nullifier with near-max secret must produce non-zero commitment\");\n}\n\n/// TC-C-20: TC-C-18 and TC-C-19 produce different commitments.\n/// Verifies that swapping near-max and zero between the two positions\n/// does not accidentally collide.\n#[test]\nfn test_near_field_limit_position_swap_produces_distinct_commitments() {\n let high: Field = 0x0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;\n let pool_id: Field = 1;\n let c1 = hash::compute_commitment(high, 0, pool_id);\n let c2 = hash::compute_commitment(0, high, pool_id);\n assert(c1 != c2, \"swapping near-max and zero between nullifier/secret must yield distinct commitments\");\n}\n\n/// TC-C-21: Incremental delta on all three inputs simultaneously.\n/// Verifies that a unit increment on each input changes the commitment,\n/// ruling out any accidental cancellation in the hash.\n#[test]\nfn test_incremental_delta_all_inputs_changes_commitment() {\n let c_base = hash::compute_commitment(100, 200, 300);\n let c_null_inc = hash::compute_commitment(101, 200, 300);\n let c_sec_inc = hash::compute_commitment(100, 201, 300);\n let c_pool_inc = hash::compute_commitment(100, 200, 301);\n\n assert(c_base != c_null_inc, \"nullifier+1 must change the commitment\");\n assert(c_base != c_sec_inc, \"secret+1 must change the commitment\");\n assert(c_base != c_pool_inc, \"pool_id+1 must change the commitment\");\n // All four must be mutually distinct\n assert(c_null_inc != c_sec_inc, \"nullifier-delta and secret-delta commitments must differ\");\n assert(c_null_inc != c_pool_inc, \"nullifier-delta and pool_id-delta commitments must differ\");\n assert(c_sec_inc != c_pool_inc, \"secret-delta and pool_id-delta commitments must differ\");\n}\n\n/// TC-C-22: All-equal inputs (nullifier == secret == pool_id).\n/// An edge case where the three preimage positions carry identical values;\n/// the circuit must still accept the commitment and produce a unique output.\n#[test]\nfn test_all_equal_inputs_valid_and_unique() {\n let v: Field = 0xabcdef;\n let c = hash::compute_commitment(v, v, v);\n main(v, v, v, c);\n // Must differ from a commitment with only two equal inputs\n let c2 = hash::compute_commitment(v, v, 0);\n assert(c != c2, \"all-equal inputs must yield a different commitment than a partially-zero variant\");\n}\n", + "path": "/home/modev/opensource/PrivacyLayer/circuits/commitment/src/main.nr", + "function_locations": [ + { + "start": 1213, + "name": "main" + }, + { + "start": 2170, + "name": "test_valid_commitment" + }, + { + "start": 2684, + "name": "test_valid_commitment_large_values" + }, + { + "start": 3379, + "name": "test_valid_commitment_near_max_field" + }, + { + "start": 4124, + "name": "test_commitment_is_deterministic" + }, + { + "start": 4726, + "name": "test_shared_fixture_cv_001" + }, + { + "start": 4980, + "name": "test_shared_fixture_cv_002" + }, + { + "start": 5234, + "name": "test_shared_fixture_cv_003" + }, + { + "start": 5488, + "name": "test_shared_fixture_cv_004" + }, + { + "start": 6086, + "name": "test_zero_inputs_valid_commitment" + }, + { + "start": 6470, + "name": "test_zero_nullifier_nonzero_secret" + }, + { + "start": 6835, + "name": "test_nonzero_nullifier_zero_secret" + }, + { + "start": 7292, + "name": "test_identical_nullifier_and_secret" + }, + { + "start": 7870, + "name": "test_no_commitment_collision_different_inputs" + }, + { + "start": 8231, + "name": "test_commitment_is_not_symmetric" + }, + { + "start": 8726, + "name": "test_adjacent_nullifiers_produce_distinct_commitments" + }, + { + "start": 9196, + "name": "test_same_secret_different_pools_distinct_commitments" + }, + { + "start": 9889, + "name": "test_wrong_nullifier_fails" + }, + { + "start": 10427, + "name": "test_wrong_secret_fails" + }, + { + "start": 10943, + "name": "test_swapped_inputs_fails" + }, + { + "start": 11574, + "name": "test_zero_inputs_with_fabricated_commitment_fails" + }, + { + "start": 11938, + "name": "test_off_by_one_commitment_fails" + }, + { + "start": 13026, + "name": "test_near_field_limit_nullifier_zero_secret" + }, + { + "start": 13654, + "name": "test_zero_nullifier_near_field_limit_secret" + }, + { + "start": 14262, + "name": "test_near_field_limit_position_swap_produces_distinct_commitments" + }, + { + "start": 14870, + "name": "test_incremental_delta_all_inputs_changes_commitment" + }, + { + "start": 15959, + "name": "test_all_equal_inputs_valid_and_unique" + } + ] + }, + "53": { + "source": "use std::hash::poseidon2_permutation;\n\nfn poseidon2_hash_3(a: Field, b: Field, c: Field) -> Field {\n let iv: Field = 3 * 18_446_744_073_709_551_616;\n let state: [Field; 4] = [a, b, c, iv];\n let permuted = poseidon2_permutation(state);\n permuted[0]\n}\n\n/// Compute commitment from nullifier, secret and pool_id.\n/// commitment = Hash(nullifier, secret, pool_id)\npub fn compute_commitment(nullifier: Field, secret: Field, pool_id: Field) -> Field {\n poseidon2_hash_3(nullifier, secret, pool_id)\n}\n", + "path": "/home/modev/opensource/PrivacyLayer/circuits/lib/src/hash/commitment.nr", + "function_locations": [ + { + "start": 98, + "name": "poseidon2_hash_3" + }, + { + "start": 456, + "name": "compute_commitment" + } + ] + }, + "54": { + "source": "// ============================================================\n// Hash Utilities\n// ============================================================\n// Centralized hash functions used across all circuits.\n//\n// The top-level API stays stable for downstream circuits while the\n// implementation lives in focused submodules.\n// ============================================================\n\npub mod commitment;\npub mod nullifier;\npub mod pair;\npub mod zeroes;\n\n/// Compute commitment from nullifier, secret and pool_id.\n/// commitment = Hash(nullifier, secret, pool_id)\npub fn compute_commitment(nullifier: Field, secret: Field, pool_id: Field) -> Field {\n commitment::compute_commitment(nullifier, secret, pool_id)\n}\n\n/// Compute nullifier hash bound to a specific root.\n/// nullifier_hash = Hash(nullifier, root)\n/// This prevents replay attacks across different pool states.\npub fn compute_nullifier_hash(nullifier: Field, root: Field) -> Field {\n nullifier::compute_nullifier_hash(nullifier, root)\n}\n\n/// Compute parent hash in Merkle tree.\n/// parent = Hash(left, right)\npub fn hash_pair(left: Field, right: Field) -> Field {\n pair::hash_pair(left, right)\n}\n\n/// Canonical zero leaf: H(0, 0).\npub fn zero_leaf() -> Field {\n zeroes::zero_leaf()\n}\n\n/// Canonical zero node at the given Merkle level.\npub fn zero_node_at_level(level: u32) -> Field {\n zeroes::zero_node_at_level(level)\n}\n\n/// Canonical zero-sibling path for a sparse single-leaf tree.\npub fn zero_sibling_path() -> [Field; 20] {\n zeroes::zero_sibling_path()\n}\n", + "path": "/home/modev/opensource/PrivacyLayer/circuits/lib/src/hash/mod.nr", + "function_locations": [ + { + "start": 648, + "name": "compute_commitment" + }, + { + "start": 945, + "name": "compute_nullifier_hash" + }, + { + "start": 1129, + "name": "hash_pair" + }, + { + "start": 1229, + "name": "zero_leaf" + }, + { + "start": 1356, + "name": "zero_node_at_level" + }, + { + "start": 1504, + "name": "zero_sibling_path" + } + ] + }, + "64": { + "source": "/// Validate commitment matches the computed value.\npub fn validate_commitment(computed: Field, expected: Field) {\n assert(computed == expected, \"commitment mismatch: invalid nullifier/secret pair\");\n}\n", + "path": "/home/modev/opensource/PrivacyLayer/circuits/lib/src/validation/commitment.nr", + "function_locations": [ + { + "start": 113, + "name": "validate_commitment" + } + ] + }, + "67": { + "source": "// ============================================================\n// Validation Utilities\n// ============================================================\n// Common validation functions used across circuits.\n// ============================================================\n\npub mod commitment;\npub mod denomination;\npub mod fee;\npub mod nullifier;\npub mod relayer;\n\n// Test helper utilities (shared across all circuit test suites)\npub mod test_helpers;\n\n/// Validate that fee does not exceed the withdrawal amount.\npub fn validate_fee(fee_value: Field, amount: Field) {\n fee::validate_fee(fee_value, amount);\n}\n\n/// Validate relayer address consistency with fee.\n/// If fee is zero, relayer must be zero address.\npub fn validate_relayer(relayer_value: Field, fee_value: Field) {\n relayer::validate_relayer(relayer_value, fee_value);\n}\n\n/// Validate commitment matches the computed value.\npub fn validate_commitment(computed: Field, expected: Field) {\n commitment::validate_commitment(computed, expected);\n}\n\n/// Validate nullifier hash matches the computed value.\npub fn validate_nullifier_hash(computed: Field, expected: Field) {\n nullifier::validate_nullifier_hash(computed, expected);\n}\n", + "path": "/home/modev/opensource/PrivacyLayer/circuits/lib/src/validation/mod.nr", + "function_locations": [ + { + "start": 564, + "name": "validate_fee" + }, + { + "start": 776, + "name": "validate_relayer" + }, + { + "start": 951, + "name": "validate_commitment" + }, + { + "start": 1134, + "name": "validate_nullifier_hash" + } + ] + } + } +} \ No newline at end of file diff --git a/circuits/lib/src/validation/mod.nr b/circuits/lib/src/validation/mod.nr index fa2cbbe..d709fb2 100644 --- a/circuits/lib/src/validation/mod.nr +++ b/circuits/lib/src/validation/mod.nr @@ -33,3 +33,7 @@ pub fn validate_commitment(computed: Field, expected: Field) { pub fn validate_nullifier_hash(computed: Field, expected: Field) { nullifier::validate_nullifier_hash(computed, expected); } + +pub fn validate_denomination(amount: Field, denomination: Field) { + denomination::validate_denomination(amount, denomination); +} diff --git a/circuits/lib/src/validation/test_helpers.nr b/circuits/lib/src/validation/test_helpers.nr index cd209c3..154ccd3 100644 --- a/circuits/lib/src/validation/test_helpers.nr +++ b/circuits/lib/src/validation/test_helpers.nr @@ -276,6 +276,7 @@ pub struct WithdrawalBuilder { pub amount: Field, pub relayer: Field, pub fee: Field, + pub denomination: Field, pub override_hash_path: bool, pub hash_path: [Field; 20], @@ -298,6 +299,7 @@ impl WithdrawalBuilder { amount: KAT_AMOUNT, relayer: 0, fee: 0, + denomination: KAT_AMOUNT, override_hash_path: false, hash_path: [0; 20], @@ -350,6 +352,11 @@ impl WithdrawalBuilder { self } + pub fn with_denomination(mut self, denomination: Field) -> Self { + self.denomination = denomination; + self + } + pub fn with_root(mut self, root: Field) -> Self { self.override_root = true; self.root = root; @@ -370,7 +377,7 @@ impl WithdrawalBuilder { /// Builds the full witness tuple, computing the commitment, path, root, and nullifier_hash /// dynamically unless overridden. - pub fn build(self) -> (Field, Field, Field, [Field; 20], Field, Field, Field, Field, Field, Field, Field) { + pub fn build(self) -> (Field, Field, Field, [Field; 20], Field, Field, Field, Field, Field, Field, Field, Field) { let commitment = hash::compute_commitment(self.nullifier, self.secret, self.pool_id); let hash_path = if self.override_hash_path { @@ -402,7 +409,8 @@ impl WithdrawalBuilder { self.recipient, self.amount, self.relayer, - self.fee + self.fee, + self.denomination ) } } diff --git a/circuits/withdraw/src/tests.nr b/circuits/withdraw/src/tests.nr index 81d1258..3eda4a1 100644 --- a/circuits/withdraw/src/tests.nr +++ b/circuits/withdraw/src/tests.nr @@ -4,8 +4,8 @@ use lib::hash; use lib::merkle; fn execute(builder: WithdrawalBuilder) { - let (n, s, li, hp, p, r, nh, rec, a, rel, f) = builder.build(); - main(n, s, li, hp, p, r, nh, rec, a, rel, f); + let (n, s, li, hp, p, r, nh, rec, a, rel, f, d) = builder.build(); + main(n, s, li, hp, p, r, nh, rec, a, rel, f, d); } #[test] @@ -305,7 +305,7 @@ fn test_leaf_index_exceeds_tree_capacity() { // 2^20 = 1,048,576 -- first invalid index main( nullifier, secret, 1_048_576, hash_path, - pool_id, root, nullifier_hash, 0xFFFF, 100_0000000, 0, 0, + pool_id, root, nullifier_hash, 0xFFFF, 100_0000000, 0, 0, 100_0000000, ); } @@ -322,7 +322,7 @@ fn test_leaf_index_large_out_of_range() { // Far outside valid range main( nullifier, secret, 99_999_999, hash_path, - pool_id, root, nullifier_hash, 0xFFFF, 100_0000000, 0, 0, + pool_id, root, nullifier_hash, 0xFFFF, 100_0000000, 0, 0, 100_0000000, ); } @@ -340,7 +340,7 @@ fn test_leaf_index_zero_boundary() { main( nullifier, secret, leaf_index, hash_path, - pool_id, root, nullifier_hash, 0x1234, 100_0000000, 0, 0, + pool_id, root, nullifier_hash, 0x1234, 100_0000000, 0, 0, 100_0000000, ); } @@ -358,7 +358,7 @@ fn test_leaf_index_max_boundary() { main( nullifier, secret, leaf_index, hash_path, - pool_id, root, nullifier_hash, 0x5678, 100_0000000, 0, 0, + pool_id, root, nullifier_hash, 0x5678, 100_0000000, 0, 0, 100_0000000, ); } @@ -376,6 +376,6 @@ fn test_leaf_index_high_bit_only() { main( nullifier, secret, leaf_index, hash_path, - pool_id, root, nullifier_hash, 0x9ABC, 100_0000000, 0, 0, + pool_id, root, nullifier_hash, 0x9ABC, 100_0000000, 0, 0, 100_0000000, ); }