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