use miden_core::{Felt, Word, crypto::hash::Poseidon2};
use miden_crypto::field::ExtensionField;
use crate::{
AceError,
circuit::{AceCircuit, AceNode, AceOp, AceOpNode},
};
const BASE_FELTS_PER_EF: usize = crate::EXT_DEGREE;
const ACE_READ_ROW_EF_NODES: usize = 2;
pub(crate) const CONST_EF_ALIGN: usize = 2;
pub(crate) const ADV_PIPE_BLOCK_FELTS: usize = 8;
const MAX_NUM_ACE_NODES: usize = (1 << 30) - 1;
#[derive(Debug, Clone)]
pub struct EncodedCircuit {
num_vars: usize,
num_ops: usize,
instructions: Vec<Felt>,
}
impl EncodedCircuit {
pub fn num_read_rows(&self) -> usize {
self.num_vars() / ACE_READ_ROW_EF_NODES
}
pub fn num_eval_rows(&self) -> usize {
self.num_ops
}
pub fn num_vars(&self) -> usize {
self.num_vars
}
pub fn num_inputs(&self) -> usize {
self.num_vars - self.num_constants()
}
pub fn num_constants(&self) -> usize {
(self.instructions.len() - self.num_ops) / BASE_FELTS_PER_EF
}
pub fn num_nodes(&self) -> usize {
self.num_vars + self.num_ops
}
pub fn instructions(&self) -> &[Felt] {
&self.instructions
}
pub fn size_in_felt(&self) -> usize {
self.instructions.len()
}
pub fn circuit_hash(&self) -> Word {
Poseidon2::hash_elements(self.instructions())
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct StreamGeometry {
input_start: usize,
constants_start: usize,
ops_start: usize,
}
impl StreamGeometry {
pub(crate) fn from_counts(num_inputs: usize, num_constants: usize, num_ops: usize) -> Self {
let num_const_nodes = num_constants.next_multiple_of(CONST_EF_ALIGN);
let const_felts = num_const_nodes * BASE_FELTS_PER_EF;
let num_ops_padded =
(const_felts + num_ops).next_multiple_of(ADV_PIPE_BLOCK_FELTS) - const_felts;
Self::new(num_inputs, num_const_nodes, num_ops_padded)
}
fn new(num_inputs: usize, num_constants: usize, num_ops: usize) -> Self {
let num_nodes = num_inputs + num_constants + num_ops;
let input_start = num_nodes - 1;
let constants_start = input_start - num_inputs;
let ops_start = constants_start - num_constants;
Self { input_start, constants_start, ops_start }
}
fn num_inputs(&self) -> usize {
self.input_start - self.constants_start
}
pub(crate) fn num_const_nodes(&self) -> usize {
self.constants_start - self.ops_start
}
pub(crate) fn num_padded_ops(&self) -> usize {
self.ops_start + 1
}
fn num_nodes(&self) -> usize {
self.input_start + 1
}
pub(crate) fn validate(&self) -> Result<(), AceError> {
if !self.num_inputs().is_multiple_of(ACE_READ_ROW_EF_NODES) {
return Err(AceError::InvalidInputLayout {
message: "ACE READ layout must be aligned to two EF nodes (use LayoutKind::Masm or pad inputs)"
.to_string(),
});
}
if self.num_nodes() > MAX_NUM_ACE_NODES {
return Err(AceError::InvalidInputLayout {
message: format!(
"ACE circuit has {} nodes, must be less than 2^30",
self.num_nodes()
),
});
}
Ok(())
}
fn node_id(&self, node: AceNode) -> Result<u64, AceError> {
let id = match node {
AceNode::Input(idx) => self.input_start.checked_sub(idx),
AceNode::Constant(idx) => self.constants_start.checked_sub(idx),
AceNode::Operation(idx) => self.ops_start.checked_sub(idx),
}
.ok_or_else(|| AceError::InvalidInputLayout {
message: format!("ACE circuit node index out of range: {node:?}"),
})?;
Ok(id as u64)
}
pub(crate) fn encode_operation(&self, op: &AceOpNode) -> Result<Felt, AceError> {
const RHS_NODE_OFFSET: u64 = 1 << 30;
const OP_TAG_OFFSET: u64 = 1 << 60;
let tag = match op.op {
AceOp::Sub => 0,
AceOp::Mul => 1,
AceOp::Add => 2,
};
let lhs_id = self.node_id(op.lhs)?;
let rhs_id = self.node_id(op.rhs)?;
Ok(Felt::new_unchecked(lhs_id + rhs_id * RHS_NODE_OFFSET + tag * OP_TAG_OFFSET))
}
}
impl<EF> AceCircuit<EF>
where
EF: ExtensionField<Felt>,
{
pub fn to_ace(&self) -> Result<EncodedCircuit, AceError> {
let num_input_nodes = self.layout.total_inputs;
let num_op_nodes = self.operations.len();
if num_op_nodes == 0 {
return Err(AceError::InvalidInputLayout {
message: "ACE circuit has no operations to encode".to_string(),
});
}
if self.root != AceNode::Operation(num_op_nodes - 1) {
return Err(AceError::InvalidInputLayout {
message: "ACE circuit root must be the last operation before padding".to_string(),
});
}
let geometry =
StreamGeometry::from_counts(num_input_nodes, self.constants.len(), num_op_nodes);
geometry.validate()?;
let num_const_nodes = geometry.num_const_nodes();
let num_const_felts = num_const_nodes * BASE_FELTS_PER_EF;
let len_circuit_padded = num_const_felts + geometry.num_padded_ops();
let mut instructions = Vec::with_capacity(len_circuit_padded);
for constant in &self.constants {
let coeffs = constant.as_basis_coefficients_slice();
instructions.push(coeffs[0]);
instructions.push(coeffs[1]);
}
instructions.resize(num_const_felts, Felt::ZERO);
for op in &self.operations {
instructions.push(geometry.encode_operation(op)?);
}
let mut last_node_index = num_op_nodes - 1;
while instructions.len() < len_circuit_padded {
let last_node = AceNode::Operation(last_node_index);
let dummy_op = AceOpNode {
op: AceOp::Mul,
lhs: last_node,
rhs: last_node,
};
instructions.push(geometry.encode_operation(&dummy_op)?);
last_node_index += 1;
}
let num_vars = num_input_nodes + num_const_nodes;
let num_ops = geometry.num_padded_ops();
Ok(EncodedCircuit { num_vars, num_ops, instructions })
}
pub fn is_padded(&self) -> bool {
if !self.layout.total_inputs.is_multiple_of(ACE_READ_ROW_EF_NODES) {
return false;
}
if !self.constants.len().is_multiple_of(CONST_EF_ALIGN) {
return false;
}
let const_felts = self.constants.len() * BASE_FELTS_PER_EF;
let op_felts = self.operations.len();
(const_felts + op_felts).is_multiple_of(ADV_PIPE_BLOCK_FELTS)
}
}