Skip to main content

core_utils/circuit/
circuit_id.rs

1//! Deterministic, cross-machine identity for a [`v2`](crate::circuit::v2) circuit.
2//!
3//! [`CircuitId`] is a blake3 digest over the canonical little-endian `bincode` encoding of a
4//! circuit's MPC-field tag, gates (in definition order), and outputs. Two circuits map to the
5//! same id iff they were built for the same MPC field and have the same gates in the same order
6//! and the same outputs, which makes it a cheap stand-in for full circuit equality (e.g. when
7//! parties must agree on which circuit to run).
8//!
9//! The encoding is byte-identical across machines because every value reachable inside a
10//! [`Gate`](crate::circuit::v2::Gate) serializes to a fixed-width little-endian representation and
11//! `bincode` itself encodes lengths as `u64` and enum discriminants as `u32` with no dependence on
12//! pointer width or endianness. The id is *not* stable across code changes that reorder or insert
13//! gate/op enum variants, since the bincode encoding embeds the variant index — this is acceptable
14//! (and desirable) for agreement, where parties must run compatible code regardless.
15
16use serde::{Deserialize, Serialize};
17
18use crate::{circuit::v2::Circuit, config::MpcConfig};
19
20/// A deterministic 256-bit identity of a [`v2`](crate::circuit::v2) circuit, used for inexpensive
21/// circuit equality checks.
22#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
23pub struct CircuitId([u8; 32]);
24
25impl CircuitId {
26    /// Computes the id of the given circuit.
27    ///
28    /// Hashes the circuit's canonical `bincode` encoding (its `CompressedCircuit` form: gates in
29    /// order, then outputs), reusing the same representation circuits are serialized with on the
30    /// wire.
31    pub fn of<C: MpcConfig>(circuit: &Circuit<C>) -> Self {
32        let bytes =
33            bincode::serialize(circuit).expect("circuit bincode serialization is infallible");
34        Self(*blake3::hash(&bytes).as_bytes())
35    }
36
37    /// Returns the raw 32-byte digest.
38    pub fn as_bytes(&self) -> &[u8; 32] {
39        &self.0
40    }
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46    use crate::{
47        circuit::v2::{AlgebraicType, FieldShareBinaryOp, Gate, Input},
48        config::DefaultConfig as C,
49    };
50
51    fn input<C: MpcConfig>() -> Gate<C> {
52        Gate::Input(Input::SecretPlaintext {
53            inputer: 0,
54            algebraic_type: AlgebraicType::ScalarField,
55            batch_size: 1,
56        })
57    }
58
59    /// Builds a small `(x0 + x1) + (x2 + x3)` circuit using only the public API.
60    fn sample_circuit() -> Circuit<C> {
61        let mut circuit = Circuit::new();
62        let i0 = circuit.add_gate(input()).unwrap();
63        let i1 = circuit.add_gate(input()).unwrap();
64        let i2 = circuit.add_gate(input()).unwrap();
65        let i3 = circuit.add_gate(input()).unwrap();
66        let l = circuit
67            .add_gate(Gate::FieldShareBinaryOp {
68                x: i0,
69                y: i1,
70                op: FieldShareBinaryOp::Add,
71            })
72            .unwrap();
73        let r = circuit
74            .add_gate(Gate::FieldShareBinaryOp {
75                x: i2,
76                y: i3,
77                op: FieldShareBinaryOp::Add,
78            })
79            .unwrap();
80        let top = circuit
81            .add_gate(Gate::FieldShareBinaryOp {
82                x: l,
83                y: r,
84                op: FieldShareBinaryOp::Add,
85            })
86            .unwrap();
87        circuit.add_output(top).unwrap();
88        circuit
89    }
90
91    /// The same circuit always maps to the same id.
92    #[test]
93    fn deterministic() {
94        assert_eq!(
95            CircuitId::of(&sample_circuit()),
96            CircuitId::of(&sample_circuit())
97        );
98    }
99
100    /// A bincode round-trip preserves the id.
101    #[test]
102    fn stable_across_serialization_roundtrip() {
103        let circuit = sample_circuit();
104        let bytes = bincode::serialize(&circuit).unwrap();
105        let restored: Circuit<C> = bincode::deserialize(&bytes).unwrap();
106        assert_eq!(CircuitId::of(&circuit), CircuitId::of(&restored));
107    }
108
109    /// Reordering gates (same multiset, different order) changes the id.
110    #[test]
111    fn order_sensitive() {
112        // Two distinct inputers so the two input gates are not identical, letting us build the
113        // same set of gates in two different orders.
114        let mut a = Circuit::<C>::new();
115        let a0 = a.add_gate(input()).unwrap();
116        let a1 = a
117            .add_gate(Gate::Input(Input::SecretPlaintext {
118                inputer: 1,
119                algebraic_type: AlgebraicType::ScalarField,
120                batch_size: 1,
121            }))
122            .unwrap();
123        let a2 = a
124            .add_gate(Gate::FieldShareBinaryOp {
125                x: a0,
126                y: a1,
127                op: FieldShareBinaryOp::Add,
128            })
129            .unwrap();
130        a.add_output(a2).unwrap();
131
132        let mut b = Circuit::<C>::new();
133        let b0 = b
134            .add_gate(Gate::Input(Input::SecretPlaintext {
135                inputer: 1,
136                algebraic_type: AlgebraicType::ScalarField,
137                batch_size: 1,
138            }))
139            .unwrap();
140        let b1 = b.add_gate(input()).unwrap();
141        let b2 = b
142            .add_gate(Gate::FieldShareBinaryOp {
143                x: b0,
144                y: b1,
145                op: FieldShareBinaryOp::Add,
146            })
147            .unwrap();
148        b.add_output(b2).unwrap();
149
150        assert_ne!(CircuitId::of(&a), CircuitId::of(&b));
151    }
152
153    /// Editing a gate changes the id.
154    #[test]
155    fn gate_sensitive() {
156        let mut add = Circuit::<C>::new();
157        let x = add.add_gate(input()).unwrap();
158        let y = add.add_gate(input()).unwrap();
159        let z = add
160            .add_gate(Gate::FieldShareBinaryOp {
161                x,
162                y,
163                op: FieldShareBinaryOp::Add,
164            })
165            .unwrap();
166        add.add_output(z).unwrap();
167
168        let mut mul = Circuit::<C>::new();
169        let x = mul.add_gate(input()).unwrap();
170        let y = mul.add_gate(input()).unwrap();
171        let z = mul
172            .add_gate(Gate::FieldShareBinaryOp {
173                x,
174                y,
175                op: FieldShareBinaryOp::Mul,
176            })
177            .unwrap();
178        mul.add_output(z).unwrap();
179
180        assert_ne!(CircuitId::of(&add), CircuitId::of(&mul));
181    }
182
183    /// Different outputs over identical gates change the id.
184    #[test]
185    fn output_sensitive() {
186        let mut base = Circuit::<C>::new();
187        let x = base.add_gate(input()).unwrap();
188        let y = base.add_gate(input()).unwrap();
189        let z = base
190            .add_gate(Gate::FieldShareBinaryOp {
191                x,
192                y,
193                op: FieldShareBinaryOp::Add,
194            })
195            .unwrap();
196
197        let mut out_z = base.clone();
198        out_z.add_output(z).unwrap();
199
200        let mut out_x = base.clone();
201        out_x.add_output(x).unwrap();
202
203        assert_ne!(CircuitId::of(&out_z), CircuitId::of(&out_x));
204    }
205}