Skip to main content

miden_ace_codegen/
encode.rs

1//! ACE circuit encoding for the chiplet format.
2//!
3//! Encoding rules:
4//! - The READ section stores extension-field (EF) elements; each EF occupies two base-field
5//!   elements.
6//! - Each ACE READ row consumes two EF elements (four base-field elements or a `Word`).
7//! - The EVAL section stores one operation per row, encoded as a single base-field element.
8//!
9//! The encoded stream concatenates constants (EF) followed by operations
10//! (base-field), then pads to an `adv_pipe` block boundary.
11
12use miden_core::{Felt, Word, crypto::hash::Poseidon2};
13use miden_crypto::field::ExtensionField;
14
15use crate::{
16    AceError,
17    circuit::{AceCircuit, AceNode, AceOp, AceOpNode},
18};
19
20// NOTE: `num_vars`/`num_const_nodes` count extension-field (EF) nodes, while the
21// instruction stream (`instructions.len()`) is measured in base field elements.
22
23/// Number of base field elements per extension field element.
24const BASE_FELTS_PER_EF: usize = crate::EXT_DEGREE;
25/// Number of EF nodes read per ACE READ row (two EF per row).
26const ACE_READ_ROW_EF_NODES: usize = 2;
27/// Constants are padded to an even number of EF nodes (full READ rows).
28const CONST_EF_ALIGN: usize = 2;
29/// Instruction stream padding unit in base felts (adv_pipe block size), so that
30/// the constants+ops stream can be read in aligned chunks.
31const ADV_PIPE_BLOCK_FELTS: usize = 8;
32
33/// Encoded ACE circuit ready for chiplet consumption.
34///
35/// This packs the circuit into the chiplet instruction stream and exposes
36/// helpers for stream sizing. `num_vars` counts extension-field nodes
37/// (inputs + constants + padding). `num_ops` and `num_eval_rows` count
38/// base-field operation rows (including padding ops).
39#[derive(Debug, Clone)]
40pub struct EncodedCircuit {
41    num_vars: usize,
42    num_ops: usize,
43    instructions: Vec<Felt>,
44}
45
46impl EncodedCircuit {
47    /// Number of ACE READ rows (two EF nodes per row).
48    pub fn num_read_rows(&self) -> usize {
49        self.num_vars() / ACE_READ_ROW_EF_NODES
50    }
51
52    /// Number of rows needed to evaluate operations (one op per base-field row).
53    pub fn num_eval_rows(&self) -> usize {
54        self.num_ops
55    }
56
57    /// Total number of variable slots (inputs + constants + padding), counted in EF nodes.
58    pub fn num_vars(&self) -> usize {
59        self.num_vars
60    }
61
62    /// Number of input slots in the READ section.
63    pub fn num_inputs(&self) -> usize {
64        self.num_vars - self.num_constants()
65    }
66
67    /// Number of constants encoded into the circuit stream, counted in EF nodes.
68    pub fn num_constants(&self) -> usize {
69        (self.instructions.len() - self.num_ops) / BASE_FELTS_PER_EF
70    }
71
72    /// Total number of nodes (inputs + constants + ops).
73    pub fn num_nodes(&self) -> usize {
74        self.num_vars + self.num_ops
75    }
76
77    /// Raw instruction stream (constants + ops).
78    pub fn instructions(&self) -> &[Felt] {
79        &self.instructions
80    }
81
82    /// Instruction stream length in base field elements.
83    pub fn size_in_felt(&self) -> usize {
84        self.instructions.len()
85    }
86
87    /// Poseidon2 digest of the instruction stream.
88    pub fn circuit_hash(&self) -> Word {
89        Poseidon2::hash_elements(self.instructions())
90    }
91}
92
93impl<EF> AceCircuit<EF>
94where
95    EF: ExtensionField<Felt>,
96{
97    /// Encode the circuit into the ACE chiplet format.
98    pub fn to_ace(&self) -> Result<EncodedCircuit, AceError> {
99        const MAX_NODE_ID: u64 = (1 << 30) - 1;
100
101        if !self.layout.total_inputs.is_multiple_of(ACE_READ_ROW_EF_NODES) {
102            return Err(AceError::InvalidInputLayout {
103                message: "ACE READ layout must be aligned to two EF nodes (use LayoutKind::Masm or pad inputs)"
104                    .to_string(),
105            });
106        }
107
108        let num_input_nodes = self.layout.total_inputs;
109        let num_const_nodes = self.constants.len().next_multiple_of(CONST_EF_ALIGN);
110        let num_op_nodes = self.operations.len();
111        if num_op_nodes == 0 {
112            return Err(AceError::InvalidInputLayout {
113                message: "ACE circuit has no operations to encode".to_string(),
114            });
115        }
116
117        // Constants are serialized as EF elements (2 base felts per EF).
118        let num_const_felts = num_const_nodes * BASE_FELTS_PER_EF;
119        let num_op_felts = num_op_nodes;
120        // The instruction stream is measured in base felts:
121        // - constants are EF-encoded (2 base felts each)
122        // - ops are 1 base felt each
123        let len_circuit = num_const_felts + num_op_felts;
124        let len_circuit_padded = len_circuit.next_multiple_of(ADV_PIPE_BLOCK_FELTS);
125
126        let num_padding_felts = len_circuit_padded - len_circuit;
127        let num_padding_nodes = num_padding_felts;
128        let num_nodes = num_input_nodes + num_const_nodes + num_op_nodes + num_padding_nodes;
129
130        if num_nodes as u64 > MAX_NODE_ID {
131            return Err(AceError::InvalidInputLayout {
132                message: format!("ACE circuit has {num_nodes} nodes, exceeds 2^30-1 limit"),
133            });
134        }
135
136        let mut instructions = Vec::with_capacity(len_circuit_padded);
137        for constant in &self.constants {
138            let coeffs = constant.as_basis_coefficients_slice();
139            instructions.push(coeffs[0]);
140            instructions.push(coeffs[1]);
141        }
142        instructions.resize(num_const_felts, Felt::ZERO);
143
144        let node_id = |node: AceNode| -> Result<u64, AceError> {
145            let input_start = num_nodes - 1;
146            let constants_start = input_start - num_input_nodes;
147            let ops_start = constants_start - num_const_nodes;
148
149            let id = match node {
150                AceNode::Input(idx) => input_start.checked_sub(idx),
151                AceNode::Constant(idx) => constants_start.checked_sub(idx),
152                AceNode::Operation(idx) => ops_start.checked_sub(idx),
153            }
154            .ok_or_else(|| AceError::InvalidInputLayout {
155                message: format!("ACE circuit node index out of range: {node:?}"),
156            })?;
157            Ok(id as u64)
158        };
159
160        let op_tag = |op: AceOp| -> u64 {
161            match op {
162                AceOp::Sub => 0,
163                AceOp::Mul => 1,
164                AceOp::Add => 2,
165            }
166        };
167
168        let encode_operation = |op: &AceOpNode| -> Result<Felt, AceError> {
169            // Pack as: lhs_id + rhs_id * 2^30 + op_tag * 2^60.
170            const RHS_NODE_OFFSET: u64 = 1 << 30;
171            const OP_TAG_OFFSET: u64 = 1 << 60;
172            let lhs_id = node_id(op.lhs)?;
173            let rhs_id = node_id(op.rhs)?;
174            let tag = op_tag(op.op);
175            Ok(Felt::new(lhs_id + rhs_id * RHS_NODE_OFFSET + tag * OP_TAG_OFFSET))
176        };
177
178        for op in &self.operations {
179            instructions.push(encode_operation(op)?);
180        }
181
182        let mut last_node_index = num_op_nodes - 1;
183        while instructions.len() < len_circuit_padded {
184            let last_node = AceNode::Operation(last_node_index);
185            let dummy_op = AceOpNode {
186                op: AceOp::Mul,
187                lhs: last_node,
188                rhs: last_node,
189            };
190            instructions.push(encode_operation(&dummy_op)?);
191            last_node_index += 1;
192        }
193
194        let num_vars = num_input_nodes + num_const_nodes;
195        let num_ops = num_op_nodes + num_padding_nodes;
196        Ok(EncodedCircuit { num_vars, num_ops, instructions })
197    }
198
199    /// Return true if inputs/constants/ops satisfy chiplet padding rules:
200    /// - inputs/constants are aligned to full READ rows (EF nodes)
201    /// - constants+ops stream is aligned to adv_pipe blocks (base felts)
202    pub fn is_padded(&self) -> bool {
203        if !self.layout.total_inputs.is_multiple_of(ACE_READ_ROW_EF_NODES) {
204            return false;
205        }
206        if !self.constants.len().is_multiple_of(CONST_EF_ALIGN) {
207            return false;
208        }
209        let const_felts = self.constants.len() * BASE_FELTS_PER_EF;
210        let op_felts = self.operations.len();
211        (const_felts + op_felts).is_multiple_of(ADV_PIPE_BLOCK_FELTS)
212    }
213}