Repository: Cardinal-Cryptography/zkOS-monorepo
Pull request #141: ZK-708: Make id depend on chain_id
Author: login: kroist
Created: 2025-03-03T16:39:07Z | Merged: 2025-03-04T10:50:32Z

- add `chain_id` as a payload for `id` derivation.
- refactor `secrets::` module, breaks backward-compatibility of secrets, but we are allowed to do that before the release


Diff:
diff --git a/.github/actions/prepare-rust-env/action.yml b/.github/actions/prepare-rust-env/action.yml
index 089dce21..e24b963b 100644
--- a/.github/actions/prepare-rust-env/action.yml
+++ b/.github/actions/prepare-rust-env/action.yml
@@ -1,13 +1,13 @@
 ---
-name: 'Prepare Rust environment'
+name: "Prepare Rust environment"
 description: >
   Installs Rust toolchain, authenticates with SSH (in order to access Github private repos)
 inputs:
   poseidon-gadget-private-key:
-    description: 'SSH private key that corresponds to the deploy key in poseidon2-gadget repository'
+    description: "SSH private key that corresponds to the deploy key in poseidon2-gadget repository"
     required: true
   zkos-circuits-private-key:
-    description: 'SSH private key that corresponds to the deploy key in zkos-circuits repository'
+    description: "SSH private key that corresponds to the deploy key in zkos-circuits repository"
     required: true
 
 runs:
@@ -24,6 +24,10 @@ runs:
       with:
         channel: nightly
 
+    - name: Install active toolchain
+      shell: bash
+      run: rustup toolchain install
+
     - name: Install sccache
       shell: bash
       run: scripts/install-sccache
diff --git a/.github/workflows/_build-wasm-packages.yml b/.github/workflows/_build-wasm-packages.yml
index 5128cff5..86916b00 100644
--- a/.github/workflows/_build-wasm-packages.yml
+++ b/.github/workflows/_build-wasm-packages.yml
@@ -33,3 +33,10 @@ jobs:
           name: wasm-pkg
           path: crates/shielder_bindings/pkg
           retention-days: 1
+
+      - name: Upload generated proving keys to artifacts
+        uses: actions/upload-artifact@v4
+        with:
+          name: proving-keys-pkg
+          path: crates/shielder_bindings/artifacts
+          retention-days: 1
diff --git a/.github/workflows/_ts-checks.yml b/.github/workflows/_ts-checks.yml
index 2b3d6eea..d0397032 100644
--- a/.github/workflows/_ts-checks.yml
+++ b/.github/workflows/_ts-checks.yml
@@ -35,6 +35,12 @@ jobs:
           name: wasm-pkg
           path: crates/shielder_bindings/pkg
 
+      - name: Download generated proving keys from artifacts
+        uses: actions/download-artifact@v4
+        with:
+          name: proving-keys-pkg
+          path: crates/shielder_bindings/artifacts
+
       - name: Download generated build cache
         uses: actions/download-artifact@v4
         with:
@@ -115,6 +121,12 @@ jobs:
           name: wasm-pkg
           path: crates/shielder_bindings/pkg
 
+      - name: Download generated proving keys from artifacts
+        uses: actions/download-artifact@v4
+        with:
+          name: proving-keys-pkg
+          path: crates/shielder_bindings/artifacts
+
       - name: Download generated build cache
         uses: actions/download-artifact@v4
         with:
diff --git a/.github/workflows/on-pull-request-commit.yml b/.github/workflows/on-pull-request-commit.yml
index 6103d62a..461c50b1 100644
--- a/.github/workflows/on-pull-request-commit.yml
+++ b/.github/workflows/on-pull-request-commit.yml
@@ -54,7 +54,9 @@ jobs:
     steps:
       - uses: geekyeggo/delete-artifact@v5
         with:
-          name: wasm-pkg
+          name: |
+            wasm-pkg
+            proving-keys-pkg
 
   measure-gas-usage-and-verifier-code-size:
     name: Measure gas usage
diff --git a/crates/shielder-account/src/secrets.rs b/crates/shielder-account/src/secrets.rs
index 35f0a095..d4965622 100644
--- a/crates/shielder-account/src/secrets.rs
+++ b/crates/shielder-account/src/secrets.rs
@@ -3,9 +3,21 @@ use rand::{rngs::OsRng, Rng};
 use sha3::Digest;
 use shielder_circuits::consts::NONCE_UPPER_LIMIT;
 
-const NULLIFIER_LABEL: &[u8] = b"nullifier";
-const TRAPDOOR_LABEL: &[u8] = b"trapdoor";
-const ID_LABEL: &[u8] = b"id";
+enum Label {
+    Nullifier,
+    Trapdoor,
+    Id,
+}
+
+impl Label {
+    fn as_bytes(&self) -> &'static [u8] {
+        match self {
+            Label::Nullifier => b"nullifier",
+            Label::Trapdoor => b"trapdoor",
+            Label::Id => b"id",
+        }
+    }
+}
 
 // Copied from `src/bn256/fr.rs` in `halo2curves`. Unfortunately only a string constant is public
 // in `halo2curves`, and we need bytes for performance reasons.
@@ -16,34 +28,44 @@ const FIELD_MODULUS: U256 = U256::from_limbs([
     0x30644e72e131a029,
 ]);
 
-fn hash(id: U256, label: &[u8], payload: &[u8]) -> U256 {
-    let mut hasher = sha3::Keccak256::new();
-    hasher.update(id.to_be_bytes_vec());
-    hasher.update(label);
-    hasher.update(payload);
+fn finalize_hash(hasher: sha3::Keccak256) -> U256 {
     U256::from_be_slice(hasher.finalize().as_slice()).reduce_mod(FIELD_MODULUS)
 }
 
 /// Nonce-dependent derivation.
 pub mod nonced {
     use alloy_primitives::U256;
+    use sha3::Digest;
 
-    use super::{hash, NULLIFIER_LABEL, TRAPDOOR_LABEL};
+    use super::{finalize_hash, Label};
 
     /// Returns a pseudorandom field element deterministically computed from `id` and `nonce`.
     pub fn derive_nullifier(id: U256, nonce: u32) -> U256 {
-        hash(id, NULLIFIER_LABEL, &nonce.to_be_bytes())
+        let mut hasher = sha3::Keccak256::new();
+        hasher.update(id.to_be_bytes_vec());
+        hasher.update(Label::Nullifier.as_bytes());
+        hasher.update(nonce.to_be_bytes());
+        finalize_hash(hasher)
     }
 
     /// Returns a pseudorandom field element deterministically computed from `id` and `nonce`.
     pub fn derive_trapdoor(id: U256, nonce: u32) -> U256 {
-        hash(id, TRAPDOOR_LABEL, &nonce.to_be_bytes())
+        let mut hasher = sha3::Keccak256::new();
+        hasher.update(id.to_be_bytes_vec());
+        hasher.update(Label::Trapdoor.as_bytes());
+        hasher.update(nonce.to_be_bytes());
+        finalize_hash(hasher)
     }
 }
 
-/// Private-key-dependent derivation of a per-token private ID.
-pub fn derive_id(private_key: U256, token_address: Address) -> U256 {
-    hash(private_key, ID_LABEL, token_address.into_word().as_ref())
+/// Private-key-dependent derivation of a per-chain & per-token private ID.
+pub fn derive_id(private_key: U256, chain_id: u64, token_address: Address) -> U256 {
+    let mut hasher = sha3::Keccak256::new();
+    hasher.update(private_key.to_be_bytes_vec());
+    hasher.update(Label::Id.as_bytes());
+    hasher.update(chain_id.to_be_bytes());
+    hasher.update(token_address.into_word());
+    finalize_hash(hasher)
 }
 
 /// Random generation of a nonce for ID hiding.
@@ -110,15 +132,17 @@ mod tests {
     #[test]
     pub fn derive_id_is_correct() {
         // Calculated using online tools as the Keccak-256 of the concatenation of:
-        //   000000000000000000000000000000000000000000000000000000000000000f
+        //   0000000000000000000000000000000000000000000000000000000000000010
         //   6964 ("id")
+        //   000000000000001a
         //   000000000000000000000000ffffffffffffffffffffffffffffffffffffffff
         let expected_before_modulo =
-            U256::from_str("0x4693e1cf4cbddec0d897bbf2aff897098f5f441732f85cb6442ded2159f3ce68")
+            U256::from_str("0x3c65a829ca81bc03ad42ab3ce088f2c3d29060ac119910879bc7b584e841896d")
                 .unwrap();
 
         let actual = derive_id(
-            U256::from(15),
+            U256::from(16),
+            26,
             address!("ffffffffffffffffffffffffffffffffffffffff"),
         );
 
diff --git a/crates/shielder_bindings/src/circuits/deposit.rs b/crates/shielder_bindings/src/circuits/deposit.rs
index 692a5156..efc9c71b 100644
--- a/crates/shielder_bindings/src/circuits/deposit.rs
+++ b/crates/shielder_bindings/src/circuits/deposit.rs
@@ -61,8 +61,8 @@ pub struct DepositCircuit(super::DepositCircuit);
 impl DepositCircuit {
     #[cfg_attr(feature = "build-uniffi", uniffi::constructor)]
     #[cfg_attr(feature = "build-wasm", wasm_bindgen(constructor))]
-    pub fn new_pronto() -> Self {
-        DepositCircuit(super::DepositCircuit::new_pronto())
+    pub fn new_pronto(params_buf: &[u8], pk_buf: &[u8]) -> Self {
+        DepositCircuit(super::DepositCircuit::new_pronto(params_buf, pk_buf))
     }
 
     #[allow(clippy::too_many_arguments)]
diff --git a/crates/shielder_bindings/src/circuits/mod.rs b/crates/shielder_bindings/src/circuits/mod.rs
index 8bba736f..86c567a7 100644
--- a/crates/shielder_bindings/src/circuits/mod.rs
+++ b/crates/shielder_bindings/src/circuits/mod.rs
@@ -20,7 +20,7 @@ pub mod new_account;
 pub mod withdraw;
 
 pub trait WasmCircuit {
-    fn load_files() -> (Params, ProvingKey, u32);
+    fn decode_from_bytes(params_buf: &[u8], pk_buf: &[u8]) -> (Params, ProvingKey, u32);
 }
 
 #[derive(Clone, Debug)]
@@ -32,21 +32,14 @@ pub struct Circuit<PK: ProverKnowledge> {
     _phantom: PhantomData<PK>,
 }
 
-macro_rules! impl_load_files {
+macro_rules! impl_decode_bytes {
     ($circuit_type:ty, $circuit_name:literal) => {
         impl WasmCircuit for Circuit<$circuit_type> {
-            fn load_files() -> (Params, ProvingKey, u32) {
-                let params = unmarshall_params(include_bytes!(concat!(
-                    "../../artifacts/",
-                    $circuit_name,
-                    "/params.bin"
-                )))
-                .expect("Failed to unmarshall params");
-
-                let (k, pk) = unmarshall_pk::<<$circuit_type as ProverKnowledge>::Circuit>(
-                    include_bytes!(concat!("../../artifacts/", $circuit_name, "/pk.bin")),
-                )
-                .expect("Failed to unmarshall pk");
+            fn decode_from_bytes(params_buf: &[u8], pk_buf: &[u8]) -> (Params, ProvingKey, u32) {
+                let params = unmarshall_params(params_buf).expect("Failed to unmarshall params");
+
+                let (k, pk) = unmarshall_pk::<<$circuit_type as ProverKnowledge>::Circuit>(pk_buf)
+                    .expect("Failed to unmarshall pk");
 
                 (params, pk, k)
             }
@@ -54,9 +47,9 @@ macro_rules! impl_load_files {
     };
 }
 
-impl_load_files!(DepositProverKnowledge<Fr>, "deposit");
-impl_load_files!(NewAccountProverKnowledge<Fr>, "new_account");
-impl_load_files!(WithdrawProverKnowledge<Fr>, "withdraw");
+impl_decode_bytes!(DepositProverKnowledge<Fr>, "deposit");
+impl_decode_bytes!(NewAccountProverKnowledge<Fr>, "new_account");
+impl_decode_bytes!(WithdrawProverKnowledge<Fr>, "withdraw");
 
 impl<PK: ProverKnowledge> Circuit<PK>
 where
@@ -93,8 +86,8 @@ where
     }
 
     /// Create a new DepositCircuit with hardcoded keys, which is faster than generating new keys.
-    pub fn new_pronto() -> Self {
-        let (params, pk, k) = Self::load_files();
+    pub fn new_pronto(params_buf: &[u8], pk_buf: &[u8]) -> Self {
+        let (params, pk, k) = Self::decode_from_bytes(params_buf, pk_buf);
 
         let vk = pk.get_vk().clone();
 
@@ -147,7 +140,10 @@ mod tests {
     #[test]
     fn deposit_pronto() {
         let mut rng = rand::thread_rng();
-        let circuit = DepositCircuit::new_pronto();
+        let circuit = DepositCircuit::new_pronto(
+            include_bytes!("../../artifacts/deposit/params.bin"),
+            include_bytes!("../../artifacts/deposit/pk.bin"),
+        );
         let values = DepositProverKnowledge::<Fr>::random_correct_example(&mut rng);
         let proof = circuit.prove(&values, &mut rng);
         circuit.verify(&values, proof).unwrap();
@@ -156,7 +152,10 @@ mod tests {
     #[test]
     fn new_account_pronto() {
         let mut rng = rand::thread_rng();
-        let circuit = NewAccountCircuit::new_pronto();
+        let circuit = NewAccountCircuit::new_pronto(
+            include_bytes!("../../artifacts/new_account/params.bin"),
+            include_bytes!("../../artifacts/new_account/pk.bin"),
+        );
         let values = NewAccountProverKnowledge::<Fr>::random_correct_example(&mut rng);
         let proof = circuit.prove(&values, &mut rng);
         circuit.verify(&values, proof).unwrap();
@@ -165,7 +164,10 @@ mod tests {
     #[test]
     fn withdraw_pronto() {
         let mut rng = rand::thread_rng();
-        let circuit = WithdrawCircuit::new_pronto();
+        let circuit = WithdrawCircuit::new_pronto(
+            include_bytes!("../../artifacts/withdraw/params.bin"),
+            include_bytes!("../../artifacts/withdraw/pk.bin"),
+        );
         let values = WithdrawProverKnowledge::<Fr>::random_correct_example(&mut rng);
         let proof = circuit.prove(&values, &mut rng);
         circuit.verify(&values, proof).unwrap();
diff --git a/crates/shielder_bindings/src/circuits/new_account.rs b/crates/shielder_bindings/src/circuits/new_account.rs
index a9aca3d3..2b845fa4 100644
--- a/crates/shielder_bindings/src/circuits/new_account.rs
+++ b/crates/shielder_bindings/src/circuits/new_account.rs
@@ -74,8 +74,8 @@ pub struct NewAccountCircuit(super::NewAccountCircuit);
 impl NewAccountCircuit {
     #[cfg_attr(feature = "build-uniffi", uniffi::constructor)]
     #[cfg_attr(feature = "build-wasm", wasm_bindgen(constructor))]
-    pub fn new_pronto() -> Self {
-        NewAccountCircuit(super::NewAccountCircuit::new_pronto())
+    pub fn new_pronto(params_buf: &[u8], pk_buf: &[u8]) -> Self {
+        NewAccountCircuit(super::NewAccountCircuit::new_pronto(params_buf, pk_buf))
     }
 
     #[allow(clippy::too_many_arguments)]
diff --git a/crates/shielder_bindings/src/circuits/withdraw.rs b/crates/shielder_bindings/src/circuits/withdraw.rs
index df756be2..d61b75ef 100644
--- a/crates/shielder_bindings/src/circuits/withdraw.rs
+++ b/crates/shielder_bindings/src/circuits/withdraw.rs
@@ -67,8 +67,8 @@ pub struct WithdrawCircuit(super::WithdrawCircuit);
 impl WithdrawCircuit {
     #[cfg_attr(feature = "build-uniffi", uniffi::constructor)]
     #[cfg_attr(feature = "build-wasm", wasm_bindgen(constructor))]
-    pub fn new_pronto() -> Self {
-        WithdrawCircuit(super::WithdrawCircuit::new_pronto())
+    pub fn new_pronto(params_buf: &[u8], pk_buf: &[u8]) -> Self {
+        WithdrawCircuit(super::WithdrawCircuit::new_pronto(params_buf, pk_buf))
     }
 
     #[allow(clippy::too_many_arguments)]
diff --git a/crates/shielder_bindings/src/secrets.rs b/crates/shielder_bindings/src/secrets.rs
index 0154ba91..07d8077f 100644
--- a/crates/shielder_bindings/src/secrets.rs
+++ b/crates/shielder_bindings/src/secrets.rs
@@ -34,9 +34,10 @@ pub fn get_action_secrets(id: Vec<u8>, nonce: u32) -> ShielderActionSecrets {
 
 #[cfg_attr(feature = "build-wasm", wasm_bindgen)]
 #[cfg_attr(feature = "build-uniffi", uniffi::export)]
-pub fn derive_id(private_key_hex: &str, token_address_hex: &str) -> Vec<u8> {
+pub fn derive_id(private_key_hex: &str, chain_id: u64, token_address_hex: &str) -> Vec<u8> {
     let id_seed_u256 = secrets::derive_id(
         hex_to_u256(private_key_hex).unwrap(),
+        chain_id,
         Address::from_hex(token_address_hex).unwrap(),
     );
     let id_seed_fr: Fr = u256_to_field(id_seed_u256);
diff --git a/ts/.gitignore b/ts/.gitignore
index 97175c68..fe83645b 100644
--- a/ts/.gitignore
+++ b/ts/.gitignore
@@ -1,2 +1,3 @@
 dist/
 dist-vite/
+dist-keys/
\ No newline at end of file
diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml
index 7fdfab1f..14b53b1b 100644
--- a/ts/pnpm-lock.yaml
+++ b/ts/pnpm-lock.yaml
@@ -199,7 +199,7 @@ importers:
         specifier: ^1.45.0
         version: 1.50.1
       '@types/node':
-        specifier: ^20.14.14
+        specifier: ^20.17.19
         version: 20.17.19
       '@types/react':
         specifier: ^18.3.3
diff --git a/ts/shielder-sdk-crypto-wasm/build.sh b/ts/shielder-sdk-crypto-wasm/build.sh
index bc6c0313..637262bd 100755
--- a/ts/shielder-sdk-crypto-wasm/build.sh
+++ b/ts/shielder-sdk-crypto-wasm/build.sh
@@ -14,3 +14,5 @@ node update-imports.mjs
 # Create Vite-specific build
 ./patches/vite-patch.sh
 
+# Copy Proving keys
+./copy-keys.sh
diff --git a/ts/shielder-sdk-crypto-wasm/copy-keys.sh b/ts/shielder-sdk-crypto-wasm/copy-keys.sh
new file mode 100755
index 00000000..0f73584a
--- /dev/null
+++ b/ts/shielder-sdk-crypto-wasm/copy-keys.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+# Create dist-keys directory and subdirectories
+mkdir -p dist-keys/new_account
+mkdir -p dist-keys/deposit
+mkdir -p dist-keys/withdraw
+
+# Copy binary files from artifacts to dist-keys
+cp ../../crates/shielder_bindings/artifacts/new_account/params.bin \
+    ../../crates/shielder_bindings/artifacts/new_account/pk.bin \
+    dist-keys/new_account/
+cp ../../crates/shielder_bindings/artifacts/deposit/params.bin \
+    ../../crates/shielder_bindings/artifacts/deposit/pk.bin \
+    dist-keys/deposit/
+cp ../../crates/shielder_bindings/artifacts/withdraw/params.bin \
+    ../../crates/shielder_bindings/artifacts/withdraw/pk.bin \
+    dist-keys/withdraw/
+
+echo "Circuit key files copied to dist-keys directory"
\ No newline at end of file
diff --git a/ts/shielder-sdk-crypto-wasm/package.json b/ts/shielder-sdk-crypto-wasm/package.json
index 3ee8cf6e..f1f495d5 100644
--- a/ts/shielder-sdk-crypto-wasm/package.json
+++ b/ts/shielder-sdk-crypto-wasm/package.json
@@ -18,7 +18,8 @@
   "license": "Apache-2.0",
   "files": [
     "/dist",
-    "/dist-vite"
+    "/dist-vite",
+    "/dist-keys"
   ],
   "exports": {
     ".": {
@@ -40,7 +41,8 @@
       "import": "./dist/crates/shielder_bindings/pkg/pkg-web-multithreaded/shielder_bindings_bg.wasm",
       "require": "./dist/crates/shielder_bindings/pkg/pkg-web-multithreaded/shielder_bindings_bg.wasm",
       "types": "./dist/crates/shielder_bindings/pkg/pkg-web-multithreaded/shielder_bindings_bg.wasm.d.ts"
-    }
+    },
+    "./keys/*": "./dist-keys/*"
   },
   "devDependencies": {
     "@eslint/js": "^9.15.0",
diff --git a/ts/shielder-sdk-crypto-wasm/src/circuits/deposit.ts b/ts/shielder-sdk-crypto-wasm/src/circuits/deposit.ts
index b45f18da..32715090 100644
--- a/ts/shielder-sdk-crypto-wasm/src/circuits/deposit.ts
+++ b/ts/shielder-sdk-crypto-wasm/src/circuits/deposit.ts
@@ -7,6 +7,7 @@ import {
 } from "@cardinal-cryptography/shielder-sdk-crypto";
 import { Caller } from "../wasmClient";
 import { WasmClientModuleBase } from "../utils/wasmModuleLoader";
+import { CircuitParamsPkBuffer } from "@/types";
 
 type WasmDepositCircuit =
   | typeof import("shielder_bindings/web-singlethreaded").DepositCircuit
@@ -17,12 +18,15 @@ export class DepositCircuit
   implements IDepositCircuit
 {
   private wasmCircuit: InstanceType<WasmDepositCircuit> | undefined;
-  init(caller: Caller) {
+  init(caller: Caller, buf: CircuitParamsPkBuffer) {
     super.init(caller);
     if (!this.wasmModule) {
       throw new Error("Wasm module not loaded");
     }
-    this.wasmCircuit = new this.wasmModule.DepositCircuit();
+    this.wasmCircuit = new this.wasmModule.DepositCircuit(
+      buf.paramsBuf,
+      buf.pkBuf
+    );
   }
 
   prove(values: DepositAdvice<Scalar>): Promise<Proof> {
diff --git a/ts/shielder-sdk-crypto-wasm/src/circuits/newAccount.ts b/ts/shielder-sdk-crypto-wasm/src/circuits/newAccount.ts
index 566eac9d..1255c7d5 100644
--- a/ts/shielder-sdk-crypto-wasm/src/circuits/newAccount.ts
+++ b/ts/shielder-sdk-crypto-wasm/src/circuits/newAccount.ts
@@ -7,6 +7,7 @@ import {
 } from "@cardinal-cryptography/shielder-sdk-crypto";
 import { Caller } from "../wasmClient";
 import { WasmClientModuleBase } from "../utils/wasmModuleLoader";
+import { CircuitParamsPkBuffer } from "@/types";
 
 type WasmNewAccountCircuit =
   | typeof import("shielder_bindings/web-singlethreaded").NewAccountCircuit
@@ -18,12 +19,15 @@ export class NewAccountCircuit
 {
   private wasmCircuit: InstanceType<WasmNewAccountCircuit> | undefined;
 
-  init(caller: Caller) {
+  init(caller: Caller, buf: CircuitParamsPkBuffer) {
     super.init(caller);
     if (!this.wasmModule) {
       throw new Error("Wasm module not loaded");
     }
-    this.wasmCircuit = new this.wasmModule.NewAccountCircuit();
+    this.wasmCircuit = new this.wasmModule.NewAccountCircuit(
+      buf.paramsBuf,
+      buf.pkBuf
+    );
   }
 
   prove(values: NewAccountAdvice<Scalar>): Promise<Proof> {
diff --git a/ts/shielder-sdk-crypto-wasm/src/circuits/withdraw.ts b/ts/shielder-sdk-crypto-wasm/src/circuits/withdraw.ts
index 741bb4f1..a6d7144c 100644
--- a/ts/shielder-sdk-crypto-wasm/src/circuits/withdraw.ts
+++ b/ts/shielder-sdk-crypto-wasm/src/circuits/withdraw.ts
@@ -7,6 +7,7 @@ import {
 } from "@cardinal-cryptography/shielder-sdk-crypto";
 import { Caller } from "../wasmClient";
 import { WasmClientModuleBase } from "../utils/wasmModuleLoader";
+import { CircuitParamsPkBuffer } from "@/types";
 
 type WasmWithdrawCircuit =
   | typeof import("shielder_bindings/web-singlethreaded").WithdrawCircuit
@@ -17,12 +18,15 @@ export class WithdrawCircuit
   implements IWithdrawCircuit
 {
   private wasmCircuit: InstanceType<WasmWithdrawCircuit> | undefined;
-  init(caller: Caller) {
+  init(caller: Caller, buf: CircuitParamsPkBuffer) {
     super.init(caller);
     if (!this.wasmModule) {
       throw new Error("Wasm module not loaded");
     }
-    this.wasmCircuit = new this.wasmModule.WithdrawCircuit();
+    this.wasmCircuit = new this.wasmModule.WithdrawCircuit(
+      buf.paramsBuf,
+      buf.pkBuf
+    );
   }
 
   prove(values: WithdrawAdvice<Scalar>): Promise<Proof> {
diff --git a/ts/shielder-sdk-crypto-wasm/src/secretGenerator.ts b/ts/shielder-sdk-crypto-wasm/src/secretGenerator.ts
index ed5470fe..c02846ae 100644
--- a/ts/shielder-sdk-crypto-wasm/src/secretGenerator.ts
+++ b/ts/shielder-sdk-crypto-wasm/src/secretGenerator.ts
@@ -30,12 +30,13 @@ export class SecretGenerator
 
   deriveId(
     privateKey: `0x${string}`,
+    chainId: bigint,
     tokenAddress: `0x${string}`
   ): Promise<Scalar> {
     if (!this.wasmModule) {
       throw new Error("Wasm module not initialized");
     }
-    const result = this.wasmModule.derive_id(privateKey, tokenAddress);
+    const result = this.wasmModule.derive_id(privateKey, chainId, tokenAddress);
     return Promise.resolve(new Scalar(result));
   }
 }
diff --git a/ts/shielder-sdk-crypto-wasm/src/types.ts b/ts/shielder-sdk-crypto-wasm/src/types.ts
new file mode 100644
index 00000000..ded9116d
--- /dev/null
+++ b/ts/shielder-sdk-crypto-wasm/src/types.ts
@@ -0,0 +1,4 @@
+export type CircuitParamsPkBuffer = {
+  paramsBuf: Uint8Array;
+  pkBuf: Uint8Array;
+};
diff --git a/ts/shielder-sdk-crypto-wasm/src/wasmClient.ts b/ts/shielder-sdk-crypto-wasm/src/wasmClient.ts
index d168d0aa..db1f0619 100644
--- a/ts/shielder-sdk-crypto-wasm/src/wasmClient.ts
+++ b/ts/shielder-sdk-crypto-wasm/src/wasmClient.ts
@@ -8,6 +8,7 @@ import { SecretGenerator } from "@/secretGenerator";
 import { NoteTreeConfig } from "@/noteTreeConfig";
 import { CryptoClient } from "@cardinal-cryptography/shielder-sdk-crypto";
 import { Converter } from "@/conversion";
+import { CircuitParamsPkBuffer } from "./types";
 
 export type Caller = "web_singlethreaded" | "web_multithreaded";
 
@@ -35,6 +36,9 @@ export class WasmClient implements CryptoClient {
   async init(
     caller: Caller,
     threads: number,
+    newAccountBuf: CircuitParamsPkBuffer,
+    depositBuf: CircuitParamsPkBuffer,
+    withdrawBuf: CircuitParamsPkBuffer,
     wasm_url?: string
   ): Promise<void> {
     const time = Date.now();
@@ -47,9 +51,9 @@ export class WasmClient implements CryptoClient {
     } else {
       throw new Error("Invalid caller");
     }
-    this.newAccountCircuit.init(caller);
-    this.depositCircuit.init(caller);
-    this.withdrawCircuit.init(caller);
+    this.newAccountCircuit.init(caller, newAccountBuf);
+    this.depositCircuit.init(caller, depositBuf);
+    this.withdrawCircuit.init(caller, withdrawBuf);
     this.hasher.init(caller);
     this.secretManager.init(caller);
     this.noteTreeConfig.init(caller);
diff --git a/ts/shielder-sdk-crypto-wasm/src/wasmClientWorker.ts b/ts/shielder-sdk-crypto-wasm/src/wasmClientWorker.ts
index 7ffca458..56d12a0c 100644
--- a/ts/shielder-sdk-crypto-wasm/src/wasmClientWorker.ts
+++ b/ts/shielder-sdk-crypto-wasm/src/wasmClientWorker.ts
@@ -1,6 +1,7 @@
 import { expose, wrap } from "comlink";
 import { WasmClient } from "./wasmClient";
 import { CryptoClient } from "@cardinal-cryptography/shielder-sdk-crypto";
+import { CircuitParamsPkBuffer } from "./types";
 
 // Create worker instance
 const wasmClientWorker = new WasmClient();
@@ -38,12 +39,21 @@ expose(exposed);
  * Pass `wasm_url` only if you need the special setup (such as vite-patched distribution).
  *
  * @param threads - The number of threads to use (1 for single-threaded, >1 for multi-threaded).
+ * @param new_account_params_buf - Uint8Array containing the new account params binary data
+ * @param new_account_pk_buf - Uint8Array containing the new account pk binary data
+ * @param deposit_params_buf - Uint8Array containing the deposit params binary data
+ * @param deposit_pk_buf - Uint8Array containing the deposit pk binary data
+ * @param withdraw_params_buf - Uint8Array containing the withdraw params binary data
+ * @param withdraw_pk_buf - Uint8Array containing the withdraw pk binary data
  * @param wasm_url - Optional URL to the WASM binary.
  * @returns A promise that resolves to a Comlink-wrapped worker implementing CryptoClient.
  * @throws Will throw an error if the worker initialization fails.
  */
 export const initWasmWorker = async (
   threads: number,
+  newAccountBuf: CircuitParamsPkBuffer,
+  depositBuf: CircuitParamsPkBuffer,
+  withdrawBuf: CircuitParamsPkBuffer,
   wasm_url?: string
 ): Promise<CryptoClient> => {
   // Create a new worker instance
@@ -60,7 +70,14 @@ export const initWasmWorker = async (
   try {
     // Initialize with single or multi-threaded mode
     const caller = threads === 1 ? "web_singlethreaded" : "web_multithreaded";
-    await wrappedWorker.init(caller, threads, wasm_url);
+    await wrappedWorker.init(
+      caller,
+      threads,
+      newAccountBuf,
+      depositBuf,
+      withdrawBuf,
+      wasm_url
+    );
     return wrappedWorker;
   } catch (error) {
     console.error("Failed to initialize WASM worker:", error);
diff --git a/ts/shielder-sdk-crypto/src/cryptoClient.ts b/ts/shielder-sdk-crypto/src/cryptoClient.ts
index 95de9d4e..5c580eed 100644
--- a/ts/shielder-sdk-crypto/src/cryptoClient.ts
+++ b/ts/shielder-sdk-crypto/src/cryptoClient.ts
@@ -43,6 +43,7 @@ export interface SecretManager {
   getSecrets(id: Scalar, nonce: number): Promise<ShielderActionSecrets<Scalar>>;
   deriveId(
     privateKey: `0x${string}`,
+    chainId: bigint,
     tokenAddress: `0x${string}`
   ): Promise<Scalar>;
 }
diff --git a/ts/shielder-sdk-tests/package.json b/ts/shielder-sdk-tests/package.json
index ee0e684f..527bf19b 100644
--- a/ts/shielder-sdk-tests/package.json
+++ b/ts/shielder-sdk-tests/package.json
@@ -18,7 +18,7 @@
     "@cardinal-cryptography/shielder-sdk-crypto": "workspace:*",
     "@cardinal-cryptography/shielder-sdk-crypto-wasm": "workspace:*",
     "@playwright/test": "^1.45.0",
-    "@types/node": "^20.14.14",
+    "@types/node": "^20.17.19",
     "@types/react": "^18.3.3",
     "@types/react-dom": "^18.3.0",
     "@vitejs/plugin-react": "^4.3.1",
diff --git a/ts/shielder-sdk-tests/tsconfig.json b/ts/shielder-sdk-tests/tsconfig.json
index c28bcb36..3f6bf61e 100644
--- a/ts/shielder-sdk-tests/tsconfig.json
+++ b/ts/shielder-sdk-tests/tsconfig.json
@@ -24,7 +24,8 @@
       "@/*": ["./web/*"],
       "@tests/*": ["./tests/*"],
       "~*": ["./web/*"]
-    }
+    },
+    "types": ["vite/client", "node"]
   },
   "include": [".plasmo/index.d.ts", "web/**/*", "tests/**/*"]
 }
diff --git a/ts/shielder-sdk-tests/web/EntryPoint.tsx b/ts/shielder-sdk-tests/web/EntryPoint.tsx
index aa43b60f..1aec2b4e 100644
--- a/ts/shielder-sdk-tests/web/EntryPoint.tsx
+++ b/ts/shielder-sdk-tests/web/EntryPoint.tsx
@@ -18,6 +18,13 @@ import {
   setupShielderTest
 } from "./fixtures/shielderTest/setup";
 
+import newAccountParamsUrl from "@cardinal-cryptography/shielder-sdk-crypto-wasm/keys/new_account/params.bin?url";
+import newAccountPkUrl from "@cardinal-cryptography/shielder-sdk-crypto-wasm/keys/new_account/pk.bin?url";
+import depositParamsUrl from "@cardinal-cryptography/shielder-sdk-crypto-wasm/keys/deposit/params.bin?url";
+import depositPkUrl from "@cardinal-cryptography/shielder-sdk-crypto-wasm/keys/deposit/pk.bin?url";
+import withdrawParamsUrl from "@cardinal-cryptography/shielder-sdk-crypto-wasm/keys/withdraw/params.bin?url";
+import withdrawPkUrl from "@cardinal-cryptography/shielder-sdk-crypto-wasm/keys/withdraw/pk.bin?url";
+
 declare global {
   interface Window {
     wasmCryptoClient: {
@@ -49,20 +56,47 @@ declare global {
   }
 }
 
+async function fetchArrayBuffer(url: string): Promise<Uint8Array> {
+  return await fetch(url).then((r) => r.bytes());
+}
+
 function EntryPoint() {
   useEffect(() => {
-    // test fixtures initialization
-    window.testFixtures = window.testFixtures || {};
-    window.testFixtures.setupHappyTest = setupShielderTest;
-    // Wasm crypto client initialization
-    window.wasmCryptoClient = window.wasmCryptoClient || {};
-    window.wasmCryptoClient.cryptoClient = initWasmWorker(envThreadsNumber());
-    // Expose shielder utilities
-    window.shielder = window.shielder || {};
-    window.shielder.createShielderClient = createShielderClient;
-    window.shielder.nativeToken = nativeToken;
-    window.shielder.erc20Token = erc20Token;
-    window.initialized = true;
+    const initialize = async () => {
+      const newAccountParams = await fetchArrayBuffer(newAccountParamsUrl);
+      const newAccountPk = await fetchArrayBuffer(newAccountPkUrl);
+      const depositParams = await fetchArrayBuffer(depositParamsUrl);
+      const depositPk = await fetchArrayBuffer(depositPkUrl);
+      const withdrawParams = await fetchArrayBuffer(withdrawParamsUrl);
+      const withdrawPk = await fetchArrayBuffer(withdrawPkUrl);
+      // test fixtures initialization
+      window.testFixtures = window.testFixtures || {};
+      window.testFixtures.setupHappyTest = setupShielderTest;
+      // Wasm crypto client initialization
+      window.wasmCryptoClient = window.wasmCryptoClient || {};
+      window.wasmCryptoClient.cryptoClient = initWasmWorker(
+        envThreadsNumber(),
+        {
+          paramsBuf: newAccountParams,
+          pkBuf: newAccountPk
+        },
+        {
+          paramsBuf: depositParams,
+          pkBuf: depositPk
+        },
+        {
+          paramsBuf: withdrawParams,
+          pkBuf: withdrawPk
+        }
+      );
+      // Expose shielder utilities
+      window.shielder = window.shielder || {};
+      window.shielder.createShielderClient = createShielderClient;
+      window.shielder.nativeToken = nativeToken;
+      window.shielder.erc20Token = erc20Token;
+      window.initialized = true;
+    };
+    void initialize();
   }, []);
 
   return null;
diff --git a/ts/shielder-sdk/__tests/state/manager.test.ts b/ts/shielder-sdk/__tests/state/manager.test.ts
index 5b229796..b48be982 100644
--- a/ts/shielder-sdk/__tests/state/manager.test.ts
+++ b/ts/shielder-sdk/__tests/state/manager.test.ts
@@ -33,6 +33,7 @@ describe("StateManager", () => {
   const testPrivateKey =
     "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
   let testId: Scalar;
+  const mockChainId = 2n;
 
   beforeEach(async () => {
     // Create mock storage
@@ -44,9 +45,15 @@ describe("StateManager", () => {
       }
     };
     cryptoClient = new MockedCryptoClient();
-    stateManager = new StateManager(testPrivateKey, storage, cryptoClient);
+    stateManager = new StateManager(
+      testPrivateKey,
+      mockChainId,
+      storage,
+      cryptoClient
+    );
     testId = await cryptoClient.secretManager.deriveId(
       testPrivateKey,
+      mockChainId,
       nativeTokenAddress
     );
   });
@@ -56,6 +63,7 @@ describe("StateManager", () => {
       const state = await stateManager.accountState(nativeToken());
       const expectedId = await cryptoClient.secretManager.deriveId(
         testPrivateKey,
+        mockChainId,
         nativeTokenAddress
       );
 
diff --git a/ts/shielder-sdk/src/client/client.ts b/ts/shielder-sdk/src/client/client.ts
index 89b45b6e..df5a9f36 100644
--- a/ts/shielder-sdk/src/client/client.ts
+++ b/ts/shielder-sdk/src/client/client.ts
@@ -111,6 +111,7 @@ export class ShielderClient {
     const internalStorage = createStorage(storage);
     this.stateManager = new StateManager(
       shielderSeedPrivateKey,
+      BigInt(chainId),
       internalStorage,
       cryptoClient
     );
diff --git a/ts/shielder-sdk/src/state/manager.ts b/ts/shielder-sdk/src/state/manager.ts
index 2dcd24ba..29f41705 100644
--- a/ts/shielder-sdk/src/state/manager.ts
+++ b/ts/shielder-sdk/src/state/manager.ts
@@ -14,16 +14,19 @@ import { getTokenAddress } from "@/utils";
 export class StateManager {
   private storage: StorageInterface;
   private privateKey: Hex;
+  private chainId: bigint;
   private idPerToken: Map<`0x${string}`, Scalar> = new Map();
   private idHashPerToken: Map<`0x${string}`, Scalar> = new Map();
   private cryptoClient: CryptoClient;
 
   constructor(
     privateKey: Hex,
+    chainId: bigint,
     storage: StorageInterface,
     cryptoClient: CryptoClient
   ) {
     this.privateKey = privateKey;
+    this.chainId = chainId;
     this.storage = storage;
     this.cryptoClient = cryptoClient;
   }
@@ -90,6 +93,7 @@ export class StateManager {
     if (!id) {
       id = await this.cryptoClient.secretManager.deriveId(
         this.privateKey,
+        this.chainId,
         tokenAddress
       );
       this.idPerToken.set(tokenAddress, id);


Reviews:
user: login: pmikolajczyk41
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T17:47:23Z
user: login: kroist
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T20:46:51Z
user: login: kroist
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T20:47:47Z
user: login: kroist
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T20:57:14Z
user: login: kroist
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T20:57:33Z
user: login: kroist
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T20:58:43Z
user: login: pmikolajczyk41
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T08:42:39Z
user: login: kroist
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T08:44:39Z
user: login: pmikolajczyk41
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T08:47:16Z
user: login: kroist
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T08:54:00Z
user: login: pmikolajczyk41
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T08:54:49Z
user: login: guspiel
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T10:49:35Z

Review comments:
diff_hunk: @@ -3,9 +3,21 @@ use rand::{rngs::OsRng, Rng};
 use sha3::Digest;
 use shielder_circuits::consts::NONCE_UPPER_LIMIT;
 
-const NULLIFIER_LABEL: &[u8] = b"nullifier";
-const TRAPDOOR_LABEL: &[u8] = b"trapdoor";
-const ID_LABEL: &[u8] = b"id";
+pub enum Label {
path: crates/shielder-account/src/secrets.rs
user: login: pmikolajczyk41
body: if `pub`, then pls derive `Copy`, `Clone` and `Debug`
created_at: 2025-03-03T17:39:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,10 +28,9 @@ const FIELD_MODULUS: U256 = U256::from_limbs([
     0x30644e72e131a029,
 ]);
 
-fn hash(id: U256, label: &[u8], payload: &[u8]) -> U256 {
+fn hash(label: Label, payload: &[u8]) -> U256 {
     let mut hasher = sha3::Keccak256::new();
-    hasher.update(id.to_be_bytes_vec());
-    hasher.update(label);
+    hasher.update(label.as_bytes());
     hasher.update(payload);
path: crates/shielder-account/src/secrets.rs
user: login: pmikolajczyk41
body: to be precise, this is not compliant with our docs (`h(id, label, nonce)` vs `h(label, id, nonce)`)
created_at: 2025-03-03T17:41:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,22 +39,34 @@ fn hash(id: U256, label: &[u8], payload: &[u8]) -> U256 {
 pub mod nonced {
     use alloy_primitives::U256;
 
-    use super::{hash, NULLIFIER_LABEL, TRAPDOOR_LABEL};
+    use super::{hash, Label};
 
     /// Returns a pseudorandom field element deterministically computed from `id` and `nonce`.
     pub fn derive_nullifier(id: U256, nonce: u32) -> U256 {
-        hash(id, NULLIFIER_LABEL, &nonce.to_be_bytes())
+        let mut payload = Vec::new();
+        payload.extend(id.to_be_bytes_vec());
+        payload.extend_from_slice(&nonce.to_be_bytes());
+        hash(Label::Nullifier, &payload)
     }
 
     /// Returns a pseudorandom field element deterministically computed from `id` and `nonce`.
     pub fn derive_trapdoor(id: U256, nonce: u32) -> U256 {
-        hash(id, TRAPDOOR_LABEL, &nonce.to_be_bytes())
+        let mut payload = Vec::new();
+        payload.extend(id.to_be_bytes_vec());
+        payload.extend_from_slice(&nonce.to_be_bytes());
+        hash(Label::Trapdoor, &payload)
     }
 }
 
 /// Private-key-dependent derivation of a per-token private ID.
-pub fn derive_id(private_key: U256, token_address: Address) -> U256 {
-    hash(private_key, ID_LABEL, token_address.into_word().as_ref())
+pub fn derive_id(private_key: U256, chain_id: u64, token_address: Address) -> U256 {
+    // Concatenate chain_id bytes and token_address bytes
+    let mut payload = Vec::new();
+    payload.extend(private_key.to_be_bytes_vec());
+    payload.extend_from_slice(&chain_id.to_be_bytes());
+    payload.extend_from_slice(token_address.into_word().as_ref());
+
+    hash(Label::Id, &payload)
path: crates/shielder-account/src/secrets.rs
user: login: pmikolajczyk41
body: I get the idea with a common `hash` function that takes label and some arbitrary payload, but for the sake of explicit argument order, I'd give up this intermediate opaque payload
created_at: 2025-03-03T17:46:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,22 +39,34 @@ fn hash(id: U256, label: &[u8], payload: &[u8]) -> U256 {
 pub mod nonced {
     use alloy_primitives::U256;
 
-    use super::{hash, NULLIFIER_LABEL, TRAPDOOR_LABEL};
+    use super::{hash, Label};
 
     /// Returns a pseudorandom field element deterministically computed from `id` and `nonce`.
     pub fn derive_nullifier(id: U256, nonce: u32) -> U256 {
-        hash(id, NULLIFIER_LABEL, &nonce.to_be_bytes())
+        let mut payload = Vec::new();
+        payload.extend(id.to_be_bytes_vec());
+        payload.extend_from_slice(&nonce.to_be_bytes());
+        hash(Label::Nullifier, &payload)
     }
 
     /// Returns a pseudorandom field element deterministically computed from `id` and `nonce`.
     pub fn derive_trapdoor(id: U256, nonce: u32) -> U256 {
-        hash(id, TRAPDOOR_LABEL, &nonce.to_be_bytes())
+        let mut payload = Vec::new();
+        payload.extend(id.to_be_bytes_vec());
+        payload.extend_from_slice(&nonce.to_be_bytes());
+        hash(Label::Trapdoor, &payload)
     }
 }
 
 /// Private-key-dependent derivation of a per-token private ID.
-pub fn derive_id(private_key: U256, token_address: Address) -> U256 {
-    hash(private_key, ID_LABEL, token_address.into_word().as_ref())
+pub fn derive_id(private_key: U256, chain_id: u64, token_address: Address) -> U256 {
+    // Concatenate chain_id bytes and token_address bytes
+    let mut payload = Vec::new();
+    payload.extend(private_key.to_be_bytes_vec());
+    payload.extend_from_slice(&chain_id.to_be_bytes());
+    payload.extend_from_slice(token_address.into_word().as_ref());
path: crates/shielder-account/src/secrets.rs
user: login: pmikolajczyk41
body: according to the docs, we should get here account nonce instead of token address, shouldn't we?
created_at: 2025-03-03T17:46:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,22 +39,34 @@ fn hash(id: U256, label: &[u8], payload: &[u8]) -> U256 {
 pub mod nonced {
     use alloy_primitives::U256;
 
-    use super::{hash, NULLIFIER_LABEL, TRAPDOOR_LABEL};
+    use super::{hash, Label};
 
     /// Returns a pseudorandom field element deterministically computed from `id` and `nonce`.
     pub fn derive_nullifier(id: U256, nonce: u32) -> U256 {
-        hash(id, NULLIFIER_LABEL, &nonce.to_be_bytes())
+        let mut payload = Vec::new();
+        payload.extend(id.to_be_bytes_vec());
+        payload.extend_from_slice(&nonce.to_be_bytes());
+        hash(Label::Nullifier, &payload)
     }
 
     /// Returns a pseudorandom field element deterministically computed from `id` and `nonce`.
     pub fn derive_trapdoor(id: U256, nonce: u32) -> U256 {
-        hash(id, TRAPDOOR_LABEL, &nonce.to_be_bytes())
+        let mut payload = Vec::new();
+        payload.extend(id.to_be_bytes_vec());
+        payload.extend_from_slice(&nonce.to_be_bytes());
+        hash(Label::Trapdoor, &payload)
     }
 }
 
 /// Private-key-dependent derivation of a per-token private ID.
-pub fn derive_id(private_key: U256, token_address: Address) -> U256 {
-    hash(private_key, ID_LABEL, token_address.into_word().as_ref())
+pub fn derive_id(private_key: U256, chain_id: u64, token_address: Address) -> U256 {
+    // Concatenate chain_id bytes and token_address bytes
+    let mut payload = Vec::new();
+    payload.extend(private_key.to_be_bytes_vec());
+    payload.extend_from_slice(&chain_id.to_be_bytes());
+    payload.extend_from_slice(token_address.into_word().as_ref());
path: crates/shielder-account/src/secrets.rs
user: login: kroist
body: that's something which will be done in next PRs
created_at: 2025-03-03T20:46:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,22 +39,34 @@ fn hash(id: U256, label: &[u8], payload: &[u8]) -> U256 {
 pub mod nonced {
     use alloy_primitives::U256;
 
-    use super::{hash, NULLIFIER_LABEL, TRAPDOOR_LABEL};
+    use super::{hash, Label};
 
     /// Returns a pseudorandom field element deterministically computed from `id` and `nonce`.
     pub fn derive_nullifier(id: U256, nonce: u32) -> U256 {
-        hash(id, NULLIFIER_LABEL, &nonce.to_be_bytes())
+        let mut payload = Vec::new();
+        payload.extend(id.to_be_bytes_vec());
+        payload.extend_from_slice(&nonce.to_be_bytes());
+        hash(Label::Nullifier, &payload)
     }
 
     /// Returns a pseudorandom field element deterministically computed from `id` and `nonce`.
     pub fn derive_trapdoor(id: U256, nonce: u32) -> U256 {
-        hash(id, TRAPDOOR_LABEL, &nonce.to_be_bytes())
+        let mut payload = Vec::new();
+        payload.extend(id.to_be_bytes_vec());
+        payload.extend_from_slice(&nonce.to_be_bytes());
+        hash(Label::Trapdoor, &payload)
     }
 }
 
 /// Private-key-dependent derivation of a per-token private ID.
-pub fn derive_id(private_key: U256, token_address: Address) -> U256 {
-    hash(private_key, ID_LABEL, token_address.into_word().as_ref())
+pub fn derive_id(private_key: U256, chain_id: u64, token_address: Address) -> U256 {
+    // Concatenate chain_id bytes and token_address bytes
+    let mut payload = Vec::new();
+    payload.extend(private_key.to_be_bytes_vec());
+    payload.extend_from_slice(&chain_id.to_be_bytes());
+    payload.extend_from_slice(token_address.into_word().as_ref());
+
+    hash(Label::Id, &payload)
path: crates/shielder-account/src/secrets.rs
user: login: kroist
body: get your point, in that case I propose we get rid of `hash` function, and build up the hashing structure directly in the `derive_` functions
created_at: 2025-03-03T20:47:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,9 +3,21 @@ use rand::{rngs::OsRng, Rng};
 use sha3::Digest;
 use shielder_circuits::consts::NONCE_UPPER_LIMIT;
 
-const NULLIFIER_LABEL: &[u8] = b"nullifier";
-const TRAPDOOR_LABEL: &[u8] = b"trapdoor";
-const ID_LABEL: &[u8] = b"id";
+pub enum Label {
path: crates/shielder-account/src/secrets.rs
user: login: kroist
body: made non-pub
created_at: 2025-03-03T20:57:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,10 +28,9 @@ const FIELD_MODULUS: U256 = U256::from_limbs([
     0x30644e72e131a029,
 ]);
 
-fn hash(id: U256, label: &[u8], payload: &[u8]) -> U256 {
+fn hash(label: Label, payload: &[u8]) -> U256 {
     let mut hasher = sha3::Keccak256::new();
-    hasher.update(id.to_be_bytes_vec());
-    hasher.update(label);
+    hasher.update(label.as_bytes());
     hasher.update(payload);
path: crates/shielder-account/src/secrets.rs
user: login: kroist
body: reverted
created_at: 2025-03-03T20:57:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,22 +39,34 @@ fn hash(id: U256, label: &[u8], payload: &[u8]) -> U256 {
 pub mod nonced {
     use alloy_primitives::U256;
 
-    use super::{hash, NULLIFIER_LABEL, TRAPDOOR_LABEL};
+    use super::{hash, Label};
 
     /// Returns a pseudorandom field element deterministically computed from `id` and `nonce`.
     pub fn derive_nullifier(id: U256, nonce: u32) -> U256 {
-        hash(id, NULLIFIER_LABEL, &nonce.to_be_bytes())
+        let mut payload = Vec::new();
+        payload.extend(id.to_be_bytes_vec());
+        payload.extend_from_slice(&nonce.to_be_bytes());
+        hash(Label::Nullifier, &payload)
     }
 
     /// Returns a pseudorandom field element deterministically computed from `id` and `nonce`.
     pub fn derive_trapdoor(id: U256, nonce: u32) -> U256 {
-        hash(id, TRAPDOOR_LABEL, &nonce.to_be_bytes())
+        let mut payload = Vec::new();
+        payload.extend(id.to_be_bytes_vec());
+        payload.extend_from_slice(&nonce.to_be_bytes());
+        hash(Label::Trapdoor, &payload)
     }
 }
 
 /// Private-key-dependent derivation of a per-token private ID.
-pub fn derive_id(private_key: U256, token_address: Address) -> U256 {
-    hash(private_key, ID_LABEL, token_address.into_word().as_ref())
+pub fn derive_id(private_key: U256, chain_id: u64, token_address: Address) -> U256 {
+    // Concatenate chain_id bytes and token_address bytes
+    let mut payload = Vec::new();
+    payload.extend(private_key.to_be_bytes_vec());
+    payload.extend_from_slice(&chain_id.to_be_bytes());
+    payload.extend_from_slice(token_address.into_word().as_ref());
+
+    hash(Label::Id, &payload)
path: crates/shielder-account/src/secrets.rs
user: login: kroist
body: look at it now. I think that previous `hash` function was really non-intuitive.
created_at: 2025-03-03T20:58:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -87,7 +109,6 @@ mod tests {
 
         let actual = derive_nullifier(U256::from(15), 0x000000ff);
 
-        assert_ne!(expected_before_modulo, actual);
path: crates/shielder-account/src/secrets.rs
user: login: pmikolajczyk41
body: are they equal now?
created_at: 2025-03-04T08:42:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -87,7 +109,6 @@ mod tests {
 
         let actual = derive_nullifier(U256::from(15), 0x000000ff);
 
-        assert_ne!(expected_before_modulo, actual);
path: crates/shielder-account/src/secrets.rs
user: login: kroist
body: they are, hash is less than modulo 🤷‍♂️
created_at: 2025-03-04T08:44:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -87,7 +109,6 @@ mod tests {
 
         let actual = derive_nullifier(U256::from(15), 0x000000ff);
 
-        assert_ne!(expected_before_modulo, actual);
path: crates/shielder-account/src/secrets.rs
user: login: pmikolajczyk41
body: if I'm not mistaken, it should be fairly easy to find a new id, so that we go over modulo; this would make sense in our case (ensuring that the code indeed takes modulo)
created_at: 2025-03-04T08:47:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -87,7 +109,6 @@ mod tests {
 
         let actual = derive_nullifier(U256::from(15), 0x000000ff);
 
-        assert_ne!(expected_before_modulo, actual);
path: crates/shielder-account/src/secrets.rs
user: login: kroist
body: added over-modulo check
created_at: 2025-03-04T08:54:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -87,7 +109,6 @@ mod tests {
 
         let actual = derive_nullifier(U256::from(15), 0x000000ff);
 
-        assert_ne!(expected_before_modulo, actual);
path: crates/shielder-account/src/secrets.rs
user: login: pmikolajczyk41
body: awesome, thank you very much!
created_at: 2025-03-04T08:54:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,9 +3,21 @@ use rand::{rngs::OsRng, Rng};
 use sha3::Digest;
 use shielder_circuits::consts::NONCE_UPPER_LIMIT;
 
-const NULLIFIER_LABEL: &[u8] = b"nullifier";
-const TRAPDOOR_LABEL: &[u8] = b"trapdoor";
-const ID_LABEL: &[u8] = b"id";
+enum Label {
path: crates/shielder-account/src/secrets.rs
user: login: guspiel
body: I consider constants better here for the sake of code simplicity, but feel free to leave it as it is.
created_at: 2025-03-04T10:22:11Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-03T17:41:07Z
body: <table><tr><th>Transaction Name</th><th>Main</th><th>Current</th><th>Difference (%)</th></tr><tr><td>NewAccount (gas)</td><td>1995803</td><td>1995803</td><td>0.00000%</td></tr><tr><td>Deposit (gas)</td><td>1793102</td><td>1793090</td><td>-0.00067%</td></tr><tr><td>Withdraw (gas)</td><td>1863027</td><td>1863015</td><td>-0.00064%</td></tr><tr><td>Deposit (size)</td><td>21576</td><td>21576</td><td>0.00000%</td></tr><tr><td>Withdraw (size)</td><td>21575</td><td>21575</td><td>0.00000%</td></tr><tr><td>NewAccount (size)</td><td>22866</td><td>22866</td><td>0.00000%</td></tr></table>
<!-- thollander/actions-comment-pull-request "hashFiles('report.html')" -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-03T17:50:27Z
body: # 📊 Coverage Report

## 📈 Total Coverage Summary

| Type | Covered | Total | Coverage |
|------|---------|--------|----------|
| 📝 Lines | 2365 | 2976 | 🟠 79.46% |
| 📄 Statements | 2365 | 2976 | 🟠 79.46% |
| ⚡ Functions | 72 | 84 | 🟡 85.71% |
| 🔀 Branches | 198 | 210 | 🟡 94.28% |

### Coverage Legend

- ✅ 100% Coverage
- 🟡 80-99% Coverage
- 🟠 50-79% Coverage
- ❌ 0-49% Coverage

## 📁 File Coverage

<details>
<summary>📋 Detailed Coverage Report</summary>

| File | Lines | Statements | Functions | Branches |
|------|-------|------------|-----------|----------|
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/_generated/abi.ts](zkOS-monorepo/ts/shielder-sdk/src/_generated/abi.ts) | 100% | 100% | 100% | 100% |
| 🟡 [zkOS-monorepo/ts/shielder-sdk/src/actions/deposit.ts](zkOS-monorepo/ts/shielder-sdk/src/actions/deposit.ts) | 88.57% | 88.57% | 100% | 86.95% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/actions/index.ts](zkOS-monorepo/ts/shielder-sdk/src/actions/index.ts) | 100% | 100% | 100% | 100% |
| 🟡 [zkOS-monorepo/ts/shielder-sdk/src/actions/newAccount.ts](zkOS-monorepo/ts/shielder-sdk/src/actions/newAccount.ts) | 86.44% | 86.44% | 100% | 83.33% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/actions/utils.ts](zkOS-monorepo/ts/shielder-sdk/src/actions/utils.ts) | 100% | 100% | 100% | 100% |
| 🟠 [zkOS-monorepo/ts/shielder-sdk/src/actions/withdraw.ts](zkOS-monorepo/ts/shielder-sdk/src/actions/withdraw.ts) | 76.98% | 76.98% | 87.5% | 100% |
| ❌ [zkOS-monorepo/ts/shielder-sdk/src/chain/contract.ts](zkOS-monorepo/ts/shielder-sdk/src/chain/contract.ts) | 3.56% | 3.56% | 25% | 50% |
| ❌ [zkOS-monorepo/ts/shielder-sdk/src/chain/relayer.ts](zkOS-monorepo/ts/shielder-sdk/src/chain/relayer.ts) | 18.8% | 18.8% | 33.33% | 50% |
| 🟡 [zkOS-monorepo/ts/shielder-sdk/src/client/client.ts](zkOS-monorepo/ts/shielder-sdk/src/client/client.ts) | 85.8% | 85.8% | 77.77% | 95.65% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/client/index.ts](zkOS-monorepo/ts/shielder-sdk/src/client/index.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/client/types.ts](zkOS-monorepo/ts/shielder-sdk/src/client/types.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/constants.ts](zkOS-monorepo/ts/shielder-sdk/src/constants.ts) | 100% | 100% | 100% | 100% |
| ❌ [zkOS-monorepo/ts/shielder-sdk/src/index.ts](zkOS-monorepo/ts/shielder-sdk/src/index.ts) | 0% | 0% | 0% | 0% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/state/events.ts](zkOS-monorepo/ts/shielder-sdk/src/state/events.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/state/index.ts](zkOS-monorepo/ts/shielder-sdk/src/state/index.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/state/manager.ts](zkOS-monorepo/ts/shielder-sdk/src/state/manager.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/state/storageSchema.ts](zkOS-monorepo/ts/shielder-sdk/src/state/storageSchema.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/state/sync.ts](zkOS-monorepo/ts/shielder-sdk/src/state/sync.ts) | 100% | 100% | 100% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/state/types.ts](zkOS-monorepo/ts/shielder-sdk/src/state/types.ts) | 100% | 100% | 100% | 100% |
| 🟠 [zkOS-monorepo/ts/shielder-sdk/src/types.ts](zkOS-monorepo/ts/shielder-sdk/src/types.ts) | 66.66% | 66.66% | 50% | 100% |
| ✅ [zkOS-monorepo/ts/shielder-sdk/src/utils.ts](zkOS-monorepo/ts/shielder-sdk/src/utils.ts) | 100% | 100% | 100% | 85.71% |

</details>

<!-- Sticky Pull Request Comment -->
author_association: NONE