Skip to main content

core_utils/circuit/
circuit_id.rs

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