use std::{
array,
borrow::{Borrow, BorrowMut},
iter,
};
use openvm_circuit_primitives::{ColumnsAir, StructReflection, StructReflectionHelper};
use openvm_circuit_primitives_derive::AlignedBorrow;
use openvm_cpu_backend::CpuBackend;
use openvm_stark_backend::{
interaction::{InteractionBuilder, PermutationCheckBus},
p3_air::{Air, AirBuilder, BaseAir},
p3_field::{PrimeCharacteristicRing, PrimeField32},
p3_matrix::{dense::RowMajorMatrix, Matrix},
p3_maybe_rayon::prelude::*,
prover::AirProvingContext,
BaseAirWithPublicValues, PartitionedBaseAir, StarkProtocolConfig, Val,
};
use tracing::instrument;
use super::{merkle::SerialReceiver, online::INITIAL_TIMESTAMP};
use crate::{
arch::{hasher::Hasher, ADDR_SPACE_OFFSET, DEFAULT_BLOCK_SIZE},
primitives::Chip,
system::memory::{
controller::CHUNK, offline_checker::MemoryBus, MemoryAddress, MemoryImage,
TimestampedEquipartition,
},
};
pub const BLOCKS_PER_CHUNK: usize = CHUNK / DEFAULT_BLOCK_SIZE;
#[repr(C)]
#[derive(Debug, AlignedBorrow, StructReflection)]
pub struct PersistentBoundaryCols<T, const CHUNK: usize> {
pub expand_direction: T,
pub address_space: T,
pub leaf_label: T,
pub values: [T; CHUNK],
pub hash: [T; CHUNK],
pub timestamps: [T; BLOCKS_PER_CHUNK],
}
#[derive(Clone, Debug, ColumnsAir)]
#[columns_via(PersistentBoundaryCols<u8, CHUNK>)]
pub struct PersistentBoundaryAir<const CHUNK: usize> {
pub memory_bus: MemoryBus,
pub merkle_bus: PermutationCheckBus,
pub compression_bus: PermutationCheckBus,
}
impl<const CHUNK: usize, F> BaseAir<F> for PersistentBoundaryAir<CHUNK> {
fn width(&self) -> usize {
PersistentBoundaryCols::<F, CHUNK>::width()
}
}
impl<const CHUNK: usize, F> BaseAirWithPublicValues<F> for PersistentBoundaryAir<CHUNK> {}
impl<const CHUNK: usize, F> PartitionedBaseAir<F> for PersistentBoundaryAir<CHUNK> {}
impl<const CHUNK: usize, AB: InteractionBuilder> Air<AB> for PersistentBoundaryAir<CHUNK> {
fn eval(&self, builder: &mut AB) {
let main = builder.main();
let local = main.row_slice(0).expect("window should have two elements");
let local: &PersistentBoundaryCols<AB::Var, CHUNK> = (*local).borrow();
builder.assert_eq(
local.expand_direction,
local.expand_direction * local.expand_direction * local.expand_direction,
);
let mut when_initial =
builder.when(local.expand_direction * (local.expand_direction + AB::F::ONE));
for i in 0..BLOCKS_PER_CHUNK {
when_initial.assert_zero(local.timestamps[i]);
}
let mut expand_fields = vec![
local.expand_direction.into(),
AB::Expr::ZERO,
local.address_space - AB::F::from_u32(ADDR_SPACE_OFFSET),
local.leaf_label.into(),
];
expand_fields.extend(local.hash.map(Into::into));
self.merkle_bus
.interact(builder, expand_fields, local.expand_direction.into());
self.compression_bus.interact(
builder,
iter::empty()
.chain(local.values.map(Into::into))
.chain(iter::repeat_n(AB::Expr::ZERO, CHUNK))
.chain(local.hash.map(Into::into)),
local.expand_direction * local.expand_direction,
);
let chunk_size_f = AB::F::from_usize(CHUNK);
for block_idx in 0..BLOCKS_PER_CHUNK {
let offset = AB::F::from_usize(block_idx * DEFAULT_BLOCK_SIZE);
self.memory_bus
.send(
MemoryAddress::new(
local.address_space,
local.leaf_label * chunk_size_f + offset,
),
local.values
[block_idx * DEFAULT_BLOCK_SIZE..(block_idx + 1) * DEFAULT_BLOCK_SIZE]
.to_vec(),
local.timestamps[block_idx],
)
.eval(builder, local.expand_direction);
}
}
}
pub struct PersistentBoundaryChip<F, const CHUNK: usize> {
pub air: PersistentBoundaryAir<CHUNK>,
touched_labels: Option<Vec<FinalTouchedLabel<F, CHUNK>>>,
overridden_height: Option<usize>,
}
#[derive(Debug)]
pub struct FinalTouchedLabel<F, const CHUNK: usize> {
address_space: u32,
label: u32,
init_values: [F; CHUNK],
final_values: [F; CHUNK],
init_hash: [F; CHUNK],
final_hash: [F; CHUNK],
final_timestamps: [u32; BLOCKS_PER_CHUNK],
}
type BlockInfo<F> = (usize, u32, [F; DEFAULT_BLOCK_SIZE]); type EnrichedEntry<F> = ((u32, u32), BlockInfo<F>); pub(crate) type ChunkedTouchedMemory<F> = Vec<((u32, u32), Vec<BlockInfo<F>>)>;
pub(crate) fn group_touched_memory_by_chunk<F: Copy + Send + Sync>(
final_memory: &TimestampedEquipartition<F, DEFAULT_BLOCK_SIZE>,
) -> ChunkedTouchedMemory<F> {
let mut enriched: Vec<EnrichedEntry<F>> = final_memory
.par_iter()
.map(|&((addr_space, ptr), ts_values)| {
let chunk_label = ptr / CHUNK as u32;
let block_idx = ((ptr % CHUNK as u32) / DEFAULT_BLOCK_SIZE as u32) as usize;
let key = (addr_space, chunk_label);
let block_info = (block_idx, ts_values.timestamp, ts_values.values);
(key, block_info)
})
.collect();
enriched.sort_unstable_by_key(|(key, _)| *key);
enriched
.chunk_by(|a, b| a.0 == b.0)
.map(|group| {
let key = group[0].0;
let blocks = group.iter().map(|&(_, info)| info).collect();
(key, blocks)
})
.collect()
}
impl<const CHUNK: usize, F: PrimeField32> PersistentBoundaryChip<F, CHUNK> {
pub fn new(
memory_bus: MemoryBus,
merkle_bus: PermutationCheckBus,
compression_bus: PermutationCheckBus,
) -> Self {
Self {
air: PersistentBoundaryAir {
memory_bus,
merkle_bus,
compression_bus,
},
touched_labels: None,
overridden_height: None,
}
}
pub fn set_overridden_height(&mut self, overridden_height: usize) {
self.overridden_height = Some(overridden_height);
}
#[instrument(name = "boundary_finalize", level = "debug", skip_all)]
pub(crate) fn finalize<H>(
&mut self,
initial_memory: &MemoryImage,
final_memory: &TimestampedEquipartition<F, DEFAULT_BLOCK_SIZE>,
hasher: &H,
) where
H: Hasher<CHUNK, F> + Sync + for<'a> SerialReceiver<&'a [F]>,
{
let final_touched_labels: Vec<_> = group_touched_memory_by_chunk(final_memory)
.into_par_iter()
.map(|((addr_space, chunk_label), blocks)| {
let chunk_ptr = chunk_label * CHUNK as u32;
let init_values: [F; CHUNK] = array::from_fn(|i| unsafe {
initial_memory.get_f::<F>(addr_space, chunk_ptr + i as u32)
});
let mut final_values = init_values;
let mut timestamps = [0u32; BLOCKS_PER_CHUNK];
for (block_idx, ts, values) in blocks {
timestamps[block_idx] = ts;
for (i, &val) in values.iter().enumerate() {
final_values[block_idx * DEFAULT_BLOCK_SIZE + i] = val;
}
}
let initial_hash = hasher.hash(&init_values);
let final_hash = hasher.hash(&final_values);
FinalTouchedLabel {
address_space: addr_space,
label: chunk_label,
init_values,
final_values,
init_hash: initial_hash,
final_hash,
final_timestamps: timestamps,
}
})
.collect();
for l in &final_touched_labels {
hasher.receive(&l.init_values);
hasher.receive(&l.final_values);
}
self.touched_labels = Some(final_touched_labels);
}
}
impl<const CHUNK: usize, RA, SC> Chip<RA, CpuBackend<SC>> for PersistentBoundaryChip<Val<SC>, CHUNK>
where
SC: StarkProtocolConfig,
Val<SC>: PrimeField32,
{
fn generate_proving_ctx(&self, _: RA) -> AirProvingContext<CpuBackend<SC>> {
let trace = {
let touched_labels = self
.touched_labels
.as_ref()
.expect("Cannot generate trace before finalization");
let width = PersistentBoundaryCols::<Val<SC>, CHUNK>::width();
let mut height = (2 * touched_labels.len()).next_power_of_two();
if let Some(mut oh) = self.overridden_height {
oh = oh.next_power_of_two();
assert!(
oh >= height,
"Overridden height is less than the required height"
);
height = oh;
}
let mut rows = Val::<SC>::zero_vec(height * width);
rows.par_chunks_mut(2 * width)
.zip(touched_labels.par_iter())
.for_each(|(row, touched_label)| {
let (initial_row, final_row) = row.split_at_mut(width);
*initial_row.borrow_mut() = PersistentBoundaryCols {
expand_direction: Val::<SC>::ONE,
address_space: Val::<SC>::from_u32(touched_label.address_space),
leaf_label: Val::<SC>::from_u32(touched_label.label),
values: touched_label.init_values,
hash: touched_label.init_hash,
timestamps: [Val::<SC>::from_u32(INITIAL_TIMESTAMP); BLOCKS_PER_CHUNK],
};
*final_row.borrow_mut() = PersistentBoundaryCols {
expand_direction: Val::<SC>::NEG_ONE,
address_space: Val::<SC>::from_u32(touched_label.address_space),
leaf_label: Val::<SC>::from_u32(touched_label.label),
values: touched_label.final_values,
hash: touched_label.final_hash,
timestamps: touched_label.final_timestamps.map(Val::<SC>::from_u32),
};
});
RowMajorMatrix::new(rows, width)
};
AirProvingContext::simple_no_pis(trace)
}
}