use alloc::{
collections::{BTreeMap, BTreeSet},
vec::Vec,
};
use miden_core::{
Felt,
deferred::{Digest, fold_deferred_root},
field::QuadFelt,
utils::RowMajorMatrix,
};
use miden_precompiles::CurvePrecompile;
use crate::{
ec::{
EcRequire,
trace::{EcGroupPtr, EcPointPtr},
},
logup::build_logup_aux_trace,
relations::ProvideMult,
transcript::{
eval::{
COL_A_PTR, COL_ACT, COL_B_PTR, COL_BOUND_PTR, COL_EC_CONTEXT_GROUP_PTR,
COL_EC_CREATE_COORD_BOUND_PTR, COL_EC_CREATE_GROUP_PTR, COL_EC_CREATE_POINT_PTR,
COL_EC_CREATE_X_PTR, COL_EC_CREATE_Y_PTR, COL_H_BEGIN, COL_H_END, COL_IS_ADD,
COL_IS_AND, COL_IS_EC_CREATE, COL_IS_EC_MSM, COL_IS_EC_OP, COL_IS_EC_PAI, COL_IS_IS,
COL_IS_MSM_LAST, COL_IS_MUL, COL_IS_PINNED, COL_IS_SUB, COL_IS_UINT_LEAF,
COL_IS_UINT_OP, COL_IS_ZERO, COL_LHS_BEGIN, COL_LHS_END, COL_MSM_EXPR, COL_MSM_IDX,
COL_MSM_IS_HEAD, COL_OUT_MULT, COL_PERM_SEQ_ID, COL_PIN_CLAIM_BOUND_PTR,
COL_PIN_CLAIM_PIN_PTR, COL_PTR, COL_RHS_BEGIN, COL_RHS_END, COL_TAG_ARG0,
COL_UINT_VALUE_BOUND_PTR, DIGEST_WIDTH, NUM_MAIN_COLS, TranscriptEvalAir,
},
nodes::{EcOpId, UintOpId},
poseidon2::{
P2Cap, P2Digest,
math::STATE_WIDTH,
trace::{PermSeqId, Poseidon2Requires, apply_permutation},
},
},
uint::{
UintRequire,
trace::{UintPtr, UintStoreRequires},
},
};
#[derive(Debug)]
pub struct Truthy {
id: u32,
hash: P2Digest,
}
impl Truthy {
pub fn hash(&self) -> P2Digest {
self.hash
}
}
#[derive(Debug, Clone, Copy)]
pub struct UintNode {
pub(crate) id: u32,
pub(crate) hash: P2Digest,
pub(crate) ptr: UintPtr,
pub(crate) bound_ptr: UintPtr,
}
impl UintNode {
pub fn hash(&self) -> P2Digest {
self.hash
}
}
#[derive(Debug, Clone, Copy)]
pub struct EcNode {
pub(crate) id: u32,
pub(crate) hash: P2Digest,
pub(crate) point: EcPointPtr,
}
impl EcNode {
pub fn hash(&self) -> P2Digest {
self.hash
}
}
#[derive(Debug)]
struct EvalNode {
id: u32,
absorbed: Option<Absorbed>,
kind: NodeKind,
}
#[derive(Debug, Clone, Copy)]
struct Absorbed {
hash: P2Digest,
perm_seq_id: PermSeqId,
}
#[derive(Debug, Clone, Copy)]
struct MsmAbsorb {
base_hash: P2Digest,
scalar_hash: P2Digest,
base_ptr: u32,
scalar_ptr: u32,
perm_seq_id: u32,
digest: P2Digest,
}
#[derive(Debug)]
enum NodeKind {
Zero,
And { lhs: P2Digest, rhs: P2Digest },
UintLeaf {
ptr: u32,
bound_ptr: u32,
is_pinned: bool,
lo: [Felt; DIGEST_WIDTH],
hi: [Felt; DIGEST_WIDTH],
},
UintOp {
op: UintOpId,
lhs: P2Digest,
rhs: P2Digest,
a_ptr: u32,
b_ptr: u32,
r_ptr: u32,
bound_ptr: u32,
},
EcCreate {
x_hash: P2Digest,
y_hash: P2Digest,
x_ptr: u32,
y_ptr: u32,
group_ptr: u32,
point_ptr: u32,
bound_ptr: u32,
is_pai: bool,
},
EcBinOp {
op: EcOpId,
lhs: P2Digest,
rhs: P2Digest,
p_ptr: u32,
q_ptr: u32,
r_ptr: u32,
group_ptr: u32,
},
EcMsm {
absorbs: Vec<MsmAbsorb>,
expr: u32,
group: u32,
val: u32,
bound: u32,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum UintKey {
Leaf(UintPtr),
Op(UintOpId, P2Digest, P2Digest),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum EcKey {
Create(u32, P2Digest, P2Digest),
Op(EcOpId, P2Digest, P2Digest),
Msm(u32),
}
#[derive(Debug, Default)]
pub struct TranscriptEvalRequires {
next_id: u32,
live: BTreeSet<u32>,
node_consumers: BTreeMap<u32, ProvideMult>,
nodes: Vec<EvalNode>,
uint_dedup: BTreeMap<UintKey, UintNode>,
ec_dedup: BTreeMap<EcKey, EcNode>,
}
impl TranscriptEvalRequires {
pub fn new() -> Self {
Self::default()
}
pub fn issue(&mut self, hash: P2Digest) -> Truthy {
self.fresh(hash)
}
pub fn zero(&mut self) -> Truthy {
let t = self.fresh(P2Digest::default());
self.nodes.push(EvalNode {
id: t.id,
absorbed: None,
kind: NodeKind::Zero,
});
t
}
pub fn record_and(&mut self, a: Truthy, b: Truthy, p2: &mut Poseidon2Requires) -> Truthy {
let (lhs, rhs) = (a.hash, b.hash);
self.consume(a);
self.consume(b);
let absorption = p2.require_one_shot(P2Cap::and(), lhs.as_array(), rhs.as_array());
let _ = p2.require_digest(absorption.digest);
debug_assert_eq!(
absorption.digest,
P2Digest::from(fold_deferred_root(
Digest::new(lhs.as_array()),
Digest::new(rhs.as_array()),
)),
);
let hash = absorption.digest;
let out = self.fresh(hash);
self.nodes.push(EvalNode {
id: out.id,
absorbed: Some(Absorbed { hash, perm_seq_id: absorption.head() }),
kind: NodeKind::And { lhs, rhs },
});
out
}
fn push_uint_leaf(
&mut self,
ptr: UintPtr,
bound_ptr: UintPtr,
is_pinned: bool,
value: [u32; 8],
p2: &mut Poseidon2Requires,
) -> (u32, P2Digest) {
let lo: [Felt; DIGEST_WIDTH] = core::array::from_fn(|i| Felt::from(value[i]));
let hi: [Felt; DIGEST_WIDTH] =
core::array::from_fn(|i| Felt::from(value[DIGEST_WIDTH + i]));
let cap = if is_pinned {
P2Cap::uint_pin_claim(bound_ptr.addr(), ptr.addr())
} else {
P2Cap::uint_value(bound_ptr.addr())
};
let absorption = p2.require_one_shot(cap, lo, hi);
let _ = p2.require_digest(absorption.digest);
let hash = absorption.digest;
let id = self.next_id;
self.next_id += 1;
self.nodes.push(EvalNode {
id,
absorbed: Some(Absorbed { hash, perm_seq_id: absorption.head() }),
kind: NodeKind::UintLeaf {
ptr: ptr.addr(),
bound_ptr: bound_ptr.addr(),
is_pinned,
lo,
hi,
},
});
(id, hash)
}
pub fn uint_leaf(
&mut self,
ptr: UintPtr,
bound_ptr: UintPtr,
value: [u32; 8],
store: &mut UintStoreRequires,
p2: &mut Poseidon2Requires,
) -> UintNode {
if let Some(&node) = self.uint_dedup.get(&UintKey::Leaf(ptr)) {
return node;
}
store.require_uintval(ptr);
let (id, hash) = self.push_uint_leaf(ptr, bound_ptr, false, value, p2);
self.node_consumers.insert(id, 0);
let node = UintNode { id, hash, ptr, bound_ptr };
self.uint_dedup.insert(UintKey::Leaf(ptr), node);
node
}
pub fn uint_op(
&mut self,
op: UintOpId,
a: &UintNode,
b: &UintNode,
mut uints: UintRequire<'_>,
p2: &mut Poseidon2Requires,
) -> UintNode {
assert!(!matches!(op, UintOpId::Is), "Is goes through record_is");
assert_eq!(a.bound_ptr, b.bound_ptr, "op operands must share a modulus");
let key = UintKey::Op(op, a.hash, b.hash);
if let Some(&node) = self.uint_dedup.get(&key) {
return node;
}
let r_ptr = match op {
UintOpId::Add => uints.add(a.ptr, b.ptr),
UintOpId::Sub => uints.sub(a.ptr, b.ptr),
UintOpId::Mul => uints.mac(1, a.ptr, b.ptr, 0, a.bound_ptr),
UintOpId::Is => unreachable!("Is goes through record_is"),
};
let bound_ptr = a.bound_ptr;
self.consume_uint(a);
self.consume_uint(b);
let absorption =
p2.require_one_shot(P2Cap::uint_op(op), a.hash.as_array(), b.hash.as_array());
let _ = p2.require_digest(absorption.digest);
let hash = absorption.digest;
let id = self.next_id;
self.next_id += 1;
self.nodes.push(EvalNode {
id,
absorbed: Some(Absorbed { hash, perm_seq_id: absorption.head() }),
kind: NodeKind::UintOp {
op,
lhs: a.hash,
rhs: b.hash,
a_ptr: a.ptr.addr(),
b_ptr: b.ptr.addr(),
r_ptr: r_ptr.addr(),
bound_ptr: bound_ptr.addr(),
},
});
self.node_consumers.insert(id, 0);
let node = UintNode { id, hash, ptr: r_ptr, bound_ptr };
self.uint_dedup.insert(key, node);
node
}
pub fn record_is(&mut self, a: &UintNode, b: &UintNode, p2: &mut Poseidon2Requires) -> Truthy {
assert_eq!(a.bound_ptr, b.bound_ptr, "Is operands must share a modulus");
assert_eq!(
a.ptr, b.ptr,
"Is operands are unequal (distinct interned ptrs) — the claim is unprovable",
);
self.consume_uint(a);
self.consume_uint(b);
let absorption =
p2.require_one_shot(P2Cap::uint_op(UintOpId::Is), a.hash.as_array(), b.hash.as_array());
let _ = p2.require_digest(absorption.digest);
let hash = absorption.digest;
let out = self.fresh(hash);
self.nodes.push(EvalNode {
id: out.id,
absorbed: Some(Absorbed { hash, perm_seq_id: absorption.head() }),
kind: NodeKind::UintOp {
op: UintOpId::Is,
lhs: a.hash,
rhs: b.hash,
a_ptr: a.ptr.addr(),
b_ptr: b.ptr.addr(),
r_ptr: 0,
bound_ptr: a.bound_ptr.addr(),
},
});
out
}
pub fn ec_create(
&mut self,
group_ptr: u32,
x: &UintNode,
y: &UintNode,
mut ec: EcRequire<'_>,
p2: &mut Poseidon2Requires,
) -> EcNode {
assert_eq!(x.bound_ptr, y.bound_ptr, "coordinates must share a modulus");
let key = EcKey::Create(group_ptr, x.hash, y.hash);
if let Some(&node) = self.ec_dedup.get(&key) {
return node;
}
let point = ec.point_on_group(EcGroupPtr::from_addr(group_ptr), x.ptr, y.ptr);
self.consume_uint(x);
self.consume_uint(y);
let absorption =
p2.require_one_shot(P2Cap::ec_create(group_ptr), x.hash.as_array(), y.hash.as_array());
let _ = p2.require_digest(absorption.digest);
let hash = absorption.digest;
let id = self.next_id;
self.next_id += 1;
self.nodes.push(EvalNode {
id,
absorbed: Some(Absorbed { hash, perm_seq_id: absorption.head() }),
kind: NodeKind::EcCreate {
x_hash: x.hash,
y_hash: y.hash,
x_ptr: x.ptr.addr(),
y_ptr: y.ptr.addr(),
group_ptr,
point_ptr: point.addr(),
bound_ptr: x.bound_ptr.addr(),
is_pai: false,
},
});
self.node_consumers.insert(id, 0);
let node = EcNode { id, hash, point };
self.ec_dedup.insert(key, node);
node
}
pub fn ec_pai(
&mut self,
group_ptr: u32,
mut ec: EcRequire<'_>,
p2: &mut Poseidon2Requires,
) -> EcNode {
let key = EcKey::Create(group_ptr, P2Digest::default(), P2Digest::default());
if let Some(&node) = self.ec_dedup.get(&key) {
return node;
}
let pai = ec.pai_on_group(EcGroupPtr::from_addr(group_ptr));
let absorption = p2.require_one_shot(
P2Cap::ec_create(group_ptr),
P2Digest::default().as_array(),
P2Digest::default().as_array(),
);
let _ = p2.require_digest(absorption.digest);
let hash = absorption.digest;
let id = self.next_id;
self.next_id += 1;
self.nodes.push(EvalNode {
id,
absorbed: Some(Absorbed { hash, perm_seq_id: absorption.head() }),
kind: NodeKind::EcCreate {
x_hash: P2Digest::default(),
y_hash: P2Digest::default(),
x_ptr: 0,
y_ptr: 0,
group_ptr,
point_ptr: pai.addr(),
bound_ptr: 0,
is_pai: true,
},
});
self.node_consumers.insert(id, 0);
let node = EcNode { id, hash, point: pai };
self.ec_dedup.insert(key, node);
node
}
pub fn ec_add(
&mut self,
p: &EcNode,
q: &EcNode,
mut ec: EcRequire<'_>,
p2: &mut Poseidon2Requires,
) -> EcNode {
let key = EcKey::Op(EcOpId::Add, p.hash, q.hash);
if let Some(&node) = self.ec_dedup.get(&key) {
return node;
}
let group = ec.group_of(p.point);
let r = ec.add(p.point, q.point, 1);
self.consume_ec(p);
self.consume_ec(q);
let absorption =
p2.require_one_shot(P2Cap::ec_op(EcOpId::Add), p.hash.as_array(), q.hash.as_array());
let _ = p2.require_digest(absorption.digest);
let hash = absorption.digest;
let id = self.next_id;
self.next_id += 1;
self.nodes.push(EvalNode {
id,
absorbed: Some(Absorbed { hash, perm_seq_id: absorption.head() }),
kind: NodeKind::EcBinOp {
op: EcOpId::Add,
lhs: p.hash,
rhs: q.hash,
p_ptr: p.point.addr(),
q_ptr: q.point.addr(),
r_ptr: r.addr(),
group_ptr: group.addr(),
},
});
self.node_consumers.insert(id, 0);
let node = EcNode { id, hash, point: r };
self.ec_dedup.insert(key, node);
node
}
pub fn ec_sub(
&mut self,
p: &EcNode,
q: &EcNode,
mut ec: EcRequire<'_>,
p2: &mut Poseidon2Requires,
) -> EcNode {
let key = EcKey::Op(EcOpId::Sub, p.hash, q.hash);
if let Some(&node) = self.ec_dedup.get(&key) {
return node;
}
let group = ec.group_of(p.point);
let r = ec.sub(p.point, q.point, 1);
self.consume_ec(p);
self.consume_ec(q);
let absorption =
p2.require_one_shot(P2Cap::ec_op(EcOpId::Sub), p.hash.as_array(), q.hash.as_array());
let _ = p2.require_digest(absorption.digest);
let hash = absorption.digest;
let id = self.next_id;
self.next_id += 1;
self.nodes.push(EvalNode {
id,
absorbed: Some(Absorbed { hash, perm_seq_id: absorption.head() }),
kind: NodeKind::EcBinOp {
op: EcOpId::Sub,
lhs: p.hash,
rhs: q.hash,
p_ptr: p.point.addr(),
q_ptr: q.point.addr(),
r_ptr: r.addr(),
group_ptr: group.addr(),
},
});
self.node_consumers.insert(id, 0);
let node = EcNode { id, hash, point: r };
self.ec_dedup.insert(key, node);
node
}
pub fn ec_is(&mut self, p: &EcNode, q: &EcNode, p2: &mut Poseidon2Requires) -> Truthy {
assert_eq!(
p.point, q.point,
"Is operands are unequal points (distinct interned ptrs) — unprovable",
);
self.consume_ec(p);
self.consume_ec(q);
let absorption =
p2.require_one_shot(P2Cap::ec_op(EcOpId::Is), p.hash.as_array(), q.hash.as_array());
let _ = p2.require_digest(absorption.digest);
let hash = absorption.digest;
let out = self.fresh(hash);
self.nodes.push(EvalNode {
id: out.id,
absorbed: Some(Absorbed { hash, perm_seq_id: absorption.head() }),
kind: NodeKind::EcBinOp {
op: EcOpId::Is,
lhs: p.hash,
rhs: q.hash,
p_ptr: p.point.addr(),
q_ptr: q.point.addr(), r_ptr: 0,
group_ptr: 0,
},
});
out
}
pub fn record_ec_msm(
&mut self,
expr: u32,
group: u32,
val: EcPointPtr,
bound: u32,
terms: &[(EcNode, UintNode)],
p2: &mut Poseidon2Requires,
) -> EcNode {
if let Some(&node) = self.ec_dedup.get(&EcKey::Msm(expr)) {
return node;
}
assert!(!terms.is_empty(), "an MSM claim needs at least one term");
let blocks: Vec<_> = terms
.iter()
.map(|(base, scalar)| {
assert_eq!(
scalar.bound_ptr.addr(),
bound,
"term scalar must be stored under the claim's scalar bound",
);
(base.hash.as_array(), scalar.hash.as_array())
})
.collect();
let initial_cap = P2Cap::ec_msm_iv();
let absorption = p2.require_absorption(initial_cap, blocks.iter().copied());
let _ = p2.require_digest(absorption.digest);
let h_claim = absorption.digest;
let mut absorbs = Vec::with_capacity(terms.len());
let mut cap = initial_cap.as_array();
let span_head = absorption.head().seq();
for (idx, ((base, scalar), &(rate0, rate1))) in terms.iter().zip(blocks.iter()).enumerate()
{
let mut state = [Felt::ZERO; STATE_WIDTH];
state[0..4].copy_from_slice(&rate0);
state[4..8].copy_from_slice(&rate1);
state[8..12].copy_from_slice(&cap);
let state_out = apply_permutation(state);
let digest = P2Digest([state_out[0], state_out[1], state_out[2], state_out[3]]);
cap = [state_out[8], state_out[9], state_out[10], state_out[11]];
absorbs.push(MsmAbsorb {
base_hash: base.hash,
scalar_hash: scalar.hash,
base_ptr: base.point.addr(),
scalar_ptr: scalar.ptr.addr(),
perm_seq_id: span_head + idx as u32,
digest,
});
self.consume_ec(base);
self.consume_uint(scalar);
}
debug_assert_eq!(absorbs.last().expect("non-empty MSM").digest, h_claim);
let id = self.next_id;
self.next_id += 1;
self.nodes.push(EvalNode {
id,
absorbed: None, kind: NodeKind::EcMsm {
absorbs,
expr,
group,
val: val.addr(),
bound,
},
});
self.node_consumers.insert(id, 0);
let node = EcNode { id, hash: h_claim, point: val };
self.ec_dedup.insert(EcKey::Msm(expr), node);
node
}
fn consume_ec(&mut self, node: &EcNode) {
*self
.node_consumers
.get_mut(&node.id)
.expect("EcNode consumed under a foreign requires") += 1;
}
fn consume_uint(&mut self, node: &UintNode) {
*self
.node_consumers
.get_mut(&node.id)
.expect("UintNode consumed under a foreign requires") += 1;
}
pub fn assert_no_stray_values(&self) {
if let Some((id, _)) = self.node_consumers.iter().find(|&(_, &count)| count == 0) {
panic!("stray uint value node (id {id}): recorded but never consumed by an op");
}
}
pub fn pin_uint(
&mut self,
ptr: UintPtr,
bound_ptr: UintPtr,
value: [u32; 8],
store: &mut UintStoreRequires,
p2: &mut Poseidon2Requires,
) -> Truthy {
store.require_uintval(ptr);
let (id, hash) = self.push_uint_leaf(ptr, bound_ptr, true, value, p2);
self.live.insert(id);
Truthy { id, hash }
}
fn fresh(&mut self, hash: P2Digest) -> Truthy {
let id = self.next_id;
self.next_id += 1;
self.live.insert(id);
Truthy { id, hash }
}
fn consume(&mut self, t: Truthy) {
assert!(self.live.remove(&t.id), "Truthy consumed twice");
}
}
pub fn generate_trace(requires: TranscriptEvalRequires, root: Truthy) -> RowMajorMatrix<Felt> {
let root_id = root.id;
let public_root = root.hash;
assert!(
requires.live.len() == 1 && requires.live.contains(&root_id),
"transcript has stray unasserted claims or root is not live: {} live",
requires.live.len(),
);
let root_node = requires.nodes.iter().find(|n| n.id == root_id).expect(
"root must be a recorded node (zero leaf, AND, or Is node), not a raw keccak handle",
);
let non_root = |n: &&EvalNode| n.id != root_id;
let zero_mult: ProvideMult = requires
.nodes
.iter()
.filter(non_root)
.filter(|n| matches!(n.kind, NodeKind::Zero))
.map(|_| 1u32)
.sum();
let rows: Vec<(&EvalNode, u32)> = requires
.nodes
.iter()
.filter(non_root)
.filter_map(|n| {
let out_mult = match &n.kind {
NodeKind::Zero => return None, NodeKind::And { .. }
| NodeKind::UintLeaf { is_pinned: true, .. }
| NodeKind::UintOp { op: UintOpId::Is, .. }
| NodeKind::EcBinOp { op: EcOpId::Is, .. } => 1,
NodeKind::UintLeaf { .. }
| NodeKind::UintOp { .. }
| NodeKind::EcCreate { .. }
| NodeKind::EcBinOp { .. }
| NodeKind::EcMsm { .. } => requires.node_consumers[&n.id],
};
Some((n, out_mult))
})
.collect();
let node_rows = |kind: &NodeKind| match kind {
NodeKind::EcMsm { absorbs, .. } => absorbs.len(),
_ => 1,
};
let n_rows = 1
+ rows.iter().map(|(n, _)| node_rows(&n.kind)).sum::<usize>()
+ usize::from(zero_mult > 0);
let height = n_rows.next_power_of_two().max(2);
let mut trace = Vec::with_capacity(height * NUM_MAIN_COLS);
push_node_row(&mut trace, root_node, 0);
for (node, out_mult) in rows {
push_node_row(&mut trace, node, out_mult);
}
if zero_mult > 0 {
push_node_row(
&mut trace,
&EvalNode {
id: 0,
absorbed: None,
kind: NodeKind::Zero,
},
zero_mult,
);
}
trace.resize(height * NUM_MAIN_COLS, Felt::ZERO);
debug_assert_eq!(public_root, root_hash(&trace), "row 0's hash must pin public_root");
RowMajorMatrix::new(trace, NUM_MAIN_COLS)
}
fn uint_op_col(op: UintOpId) -> usize {
match op {
UintOpId::Add => COL_IS_ADD,
UintOpId::Sub => COL_IS_SUB,
UintOpId::Mul => COL_IS_MUL,
UintOpId::Is => COL_IS_IS,
}
}
fn ec_op_col(op: EcOpId) -> usize {
match op {
EcOpId::Add => COL_IS_ADD,
EcOpId::Sub => COL_IS_SUB,
EcOpId::Is => COL_IS_IS,
}
}
fn ec_op_id(op: EcOpId) -> u64 {
match op {
EcOpId::Add => CurvePrecompile::ADD_OP_ID,
EcOpId::Sub => CurvePrecompile::SUB_OP_ID,
EcOpId::Is => CurvePrecompile::EQ_OP_ID,
}
}
fn write_children(row: &mut [Felt; NUM_MAIN_COLS], lhs: &P2Digest, rhs: &P2Digest) {
let lhs = lhs.as_array();
let rhs = rhs.as_array();
row[COL_LHS_BEGIN..COL_LHS_END].copy_from_slice(&lhs);
row[COL_RHS_BEGIN..COL_RHS_END].copy_from_slice(&rhs);
}
fn push_node_row(trace: &mut Vec<Felt>, node: &EvalNode, out_mult: ProvideMult) {
if let NodeKind::EcMsm { absorbs, expr, group, val, bound } = &node.kind {
let k = absorbs.len();
for (idx, a) in absorbs.iter().enumerate() {
let is_last = idx == k - 1;
let mut row = [Felt::ZERO; NUM_MAIN_COLS];
row[COL_ACT] = Felt::ONE;
row[COL_IS_EC_MSM] = Felt::ONE;
row[COL_IS_MSM_LAST] = Felt::from(is_last as u8);
row[COL_MSM_IS_HEAD] = Felt::from((idx == 0) as u8);
row[COL_PERM_SEQ_ID] = Felt::from(a.perm_seq_id);
let base_hash = a.base_hash.as_array();
let scalar_hash = a.scalar_hash.as_array();
let digest = a.digest.as_array();
row[COL_LHS_BEGIN..COL_LHS_END].copy_from_slice(&base_hash);
row[COL_RHS_BEGIN..COL_RHS_END].copy_from_slice(&scalar_hash);
row[COL_H_BEGIN..COL_H_END].copy_from_slice(&digest);
row[COL_A_PTR] = Felt::from(a.base_ptr);
row[COL_B_PTR] = Felt::from(a.scalar_ptr);
row[COL_MSM_IDX] = Felt::from(idx as u32);
row[COL_MSM_EXPR] = Felt::from(*expr);
row[COL_EC_CONTEXT_GROUP_PTR] = Felt::from(*group);
row[COL_BOUND_PTR] = Felt::from(*bound);
if is_last {
row[COL_PTR] = Felt::from(*val);
row[COL_OUT_MULT] = Felt::from(out_mult);
}
trace.extend(row);
}
return;
}
let mut row = [Felt::ZERO; NUM_MAIN_COLS];
row[COL_ACT] = Felt::ONE;
row[COL_OUT_MULT] = Felt::from(out_mult);
let Some(Absorbed { hash, perm_seq_id }) = node.absorbed else {
row[COL_IS_ZERO] = Felt::ONE;
trace.extend(row);
return;
};
row[COL_PERM_SEQ_ID] = Felt::from(perm_seq_id.seq());
let hash = hash.as_array();
row[COL_H_BEGIN..COL_H_END].copy_from_slice(&hash);
match &node.kind {
NodeKind::Zero => unreachable!("Zero carries no absorption"),
NodeKind::And { lhs, rhs } => {
write_children(&mut row, lhs, rhs);
row[COL_IS_AND] = Felt::ONE;
},
NodeKind::UintLeaf { ptr, bound_ptr, is_pinned, lo, hi } => {
row[COL_LHS_BEGIN..COL_LHS_END].copy_from_slice(lo);
row[COL_RHS_BEGIN..COL_RHS_END].copy_from_slice(hi);
row[COL_IS_UINT_LEAF] = Felt::ONE;
row[COL_IS_PINNED] = Felt::from(*is_pinned as u8);
row[COL_PTR] = Felt::from(*ptr);
row[COL_BOUND_PTR] = Felt::from(*bound_ptr);
if *is_pinned {
row[COL_PIN_CLAIM_BOUND_PTR] = Felt::from(*bound_ptr);
row[COL_PIN_CLAIM_PIN_PTR] = Felt::from(*ptr);
} else {
row[COL_UINT_VALUE_BOUND_PTR] = Felt::from(*bound_ptr);
}
},
NodeKind::UintOp {
op,
lhs,
rhs,
a_ptr,
b_ptr,
r_ptr,
bound_ptr,
} => {
write_children(&mut row, lhs, rhs);
row[COL_IS_UINT_OP] = Felt::ONE;
row[uint_op_col(*op)] = Felt::ONE;
row[COL_PTR] = Felt::from(*r_ptr); row[COL_BOUND_PTR] = Felt::from(*bound_ptr);
row[COL_A_PTR] = Felt::from(*a_ptr);
row[COL_B_PTR] = Felt::from(*b_ptr);
row[COL_TAG_ARG0] = Felt::from(*op as u8); },
NodeKind::EcCreate {
x_hash,
y_hash,
x_ptr,
y_ptr,
group_ptr,
point_ptr,
bound_ptr,
is_pai,
} => {
write_children(&mut row, x_hash, y_hash); row[if *is_pai { COL_IS_EC_PAI } else { COL_IS_EC_CREATE }] = Felt::ONE;
row[COL_EC_CREATE_POINT_PTR] = Felt::from(*point_ptr); row[COL_EC_CREATE_COORD_BOUND_PTR] = Felt::from(*bound_ptr); row[COL_EC_CREATE_X_PTR] = Felt::from(*x_ptr); row[COL_EC_CREATE_Y_PTR] = Felt::from(*y_ptr); row[COL_EC_CREATE_GROUP_PTR] = Felt::from(*group_ptr); },
NodeKind::EcBinOp {
op,
lhs,
rhs,
p_ptr,
q_ptr,
r_ptr,
group_ptr,
} => {
write_children(&mut row, lhs, rhs); row[COL_IS_EC_OP] = Felt::ONE;
row[ec_op_col(*op)] = Felt::ONE;
row[COL_PTR] = Felt::from(*r_ptr); row[COL_A_PTR] = Felt::from(*p_ptr); row[COL_B_PTR] = Felt::from(*q_ptr); row[COL_TAG_ARG0] = Felt::from_u32(ec_op_id(*op) as u32); row[COL_EC_CONTEXT_GROUP_PTR] = Felt::from(*group_ptr); },
NodeKind::EcMsm { .. } => unreachable!("EcMsm is laid as a multi-row run above"),
}
trace.extend(row);
}
fn root_hash(trace: &[Felt]) -> P2Digest {
P2Digest(core::array::from_fn(|i| trace[COL_H_BEGIN + i]))
}
pub(crate) fn build_aux(
main: &RowMajorMatrix<Felt>,
challenges: &[QuadFelt],
) -> (RowMajorMatrix<QuadFelt>, Vec<QuadFelt>) {
build_logup_aux_trace(&TranscriptEvalAir, main, challenges)
}