Protocol Design
Murena's shielded transfers hide amounts and linkages while enforcing balance with zk proofs.
Primitives
- Spend key (sk_spend) — authorizes spending of notes.
- View key (vk_view) — optional, read-only; enables selective disclosure.
- Commitment — opaque note commitment stored on-chain.
- Nullifier — one-time marker that a note was spent.
Constraints (informal)
- Inputs exist in the current Merkle tree (membership proof).
- Inputs are owned by the prover (knows sk_spend).
- Inputs were not previously spent (nullifier uniqueness).
- Sum(inputs) = Sum(outputs) (conservation)
- Optional: range/amount padding to reduce leakage.
Pseudo-circuit sketch
Given: root, pathElements[], pathIndices[], inputNotes[], outputNotes[] Prove: - For each input: MerklePath(root, commitment(inputNote)) == true - nullifier = H(sk_spend || note_id) - Σ input.value == Σ output.value - Bind memo_hash to outputNote (encrypted off-chain) Public signals: root, nullifiers[], outputCommitments[]
On-chain verification (Rust sketch)
// NOT PRODUCTION — illustrative only
pub fn process_verify(ctx: Context<Verify>, proof: Vec<u8>, pub_signals: Vec<[u8;32]>) -> Result<()> {
// 1) Verify zk proof (e.g., Groth16/altbn128) via builtin or custom crate
require!(verify_groth16(&proof, &pub_signals)?, ErrorCode::InvalidProof);
// 2) Extract root, nullifiers, outputs from pub_signals
let root = PubSig::root(&pub_signals)?;
let nullifiers = PubSig::nullifiers(&pub_signals)?;
let outputs = PubSig::outputs(&pub_signals)?;
// 3) Check root is known
require!(ctx.accounts.global.roots.contains(&root), ErrorCode::UnknownRoot);
// 4) Enforce nullifier uniqueness
for n in nullifiers.iter() {
require!(!ctx.accounts.nullifier_set.contains(n), ErrorCode::NullifierUsed);
ctx.accounts.nullifier_set.insert(*n);
}
// 5) Append outputs to commitment tree (or event for off-chain tree updater)
emit!(NewCommitments { commitments: outputs });
Ok(())
}