use std::num::NonZeroUsize;
use std::ops::RangeInclusive;
use diesel::query_dsl::methods::SelectDsl;
use diesel::{
ExpressionMethods,
QueryDsl,
Queryable,
QueryableByName,
RunQueryDsl,
Selectable,
SelectableHelper,
SqliteConnection,
};
use miden_node_utils::limiter::{
MAX_RESPONSE_PAYLOAD_BYTES,
QueryParamLimiter,
QueryParamNullifierPrefixLimit,
};
use miden_protocol::block::BlockNumber;
use miden_protocol::note::Nullifier;
use miden_protocol::utils::serde::{Deserializable, Serializable};
use super::DatabaseError;
use crate::db::models::conv::{SqlTypeConvert, nullifier_prefix_to_raw_sql};
use crate::db::models::utils::vec_raw_try_into;
use crate::db::{NullifierInfo, schema};
pub(crate) fn select_nullifiers_by_prefix(
conn: &mut SqliteConnection,
prefix_len: u8,
nullifier_prefixes: &[u16],
block_range: RangeInclusive<BlockNumber>,
) -> Result<(Vec<NullifierInfo>, BlockNumber), DatabaseError> {
pub const NULLIFIER_BYTES: usize = 32; pub const BLOCK_NUM_BYTES: usize = 4; pub const ROW_OVERHEAD_BYTES: usize = NULLIFIER_BYTES + BLOCK_NUM_BYTES; pub const MAX_ROWS: usize = MAX_RESPONSE_PAYLOAD_BYTES / ROW_OVERHEAD_BYTES;
const _: () = assert!(
miden_protocol::MAX_INPUT_NOTES_PER_BLOCK <= MAX_ROWS,
"a block's nullifiers must fit in one response page or pagination cannot make progress",
);
assert_eq!(prefix_len, 16, "Only 16-bit prefixes are supported");
if block_range.is_empty() {
return Err(DatabaseError::InvalidBlockRange {
from: *block_range.start(),
to: *block_range.end(),
});
}
QueryParamNullifierPrefixLimit::check(nullifier_prefixes.len())?;
let prefixes = nullifier_prefixes.iter().map(|prefix| nullifier_prefix_to_raw_sql(*prefix));
let raw = SelectDsl::select(
schema::nullifiers::table,
NullifierWithoutPrefixRawRow::as_select(),
)
.filter(schema::nullifiers::nullifier_prefix.eq_any(prefixes))
.filter(schema::nullifiers::block_num.ge(block_range.start().to_raw_sql()))
.filter(schema::nullifiers::block_num.le(block_range.end().to_raw_sql()))
.order(schema::nullifiers::block_num.asc())
.limit(i64::try_from(MAX_ROWS + 1).expect("limit fits within i64"))
.load::<NullifierWithoutPrefixRawRow>(conn)?;
if let Some(last) = raw.last()
&& raw.len() > MAX_ROWS
{
let last_block_num_i64 = last.block_num;
let nullifiers = vec_raw_try_into(
raw.into_iter().take_while(|row| row.block_num != last_block_num_i64),
)?;
let last_block_included = BlockNumber::from_raw_sql(last_block_num_i64.saturating_sub(1))?;
Ok((nullifiers, last_block_included))
} else {
Ok((vec_raw_try_into(raw)?, *block_range.end()))
}
}
#[cfg(test)]
pub(crate) fn select_all_nullifiers(
conn: &mut SqliteConnection,
) -> Result<Vec<NullifierInfo>, DatabaseError> {
let nullifiers_raw =
SelectDsl::select(schema::nullifiers::table, NullifierWithoutPrefixRawRow::as_select())
.load::<NullifierWithoutPrefixRawRow>(conn)?;
vec_raw_try_into(nullifiers_raw)
}
#[derive(Debug)]
pub struct NullifiersPage {
pub nullifiers: Vec<NullifierInfo>,
pub next_cursor: Option<Nullifier>,
}
pub(crate) fn select_nullifiers_paged(
conn: &mut SqliteConnection,
page_size: NonZeroUsize,
after_nullifier: Option<Nullifier>,
) -> Result<NullifiersPage, DatabaseError> {
#[expect(clippy::cast_possible_wrap)]
let limit = (page_size.get() + 1) as i64;
let mut query =
SelectDsl::select(schema::nullifiers::table, NullifierWithoutPrefixRawRow::as_select())
.order_by(schema::nullifiers::nullifier.asc())
.limit(limit)
.into_boxed();
if let Some(cursor) = after_nullifier {
query = query.filter(schema::nullifiers::nullifier.gt(cursor.to_bytes()));
}
let nullifiers_raw = query.load::<NullifierWithoutPrefixRawRow>(conn)?;
let mut nullifiers: Vec<NullifierInfo> = vec_raw_try_into(nullifiers_raw)?;
let next_cursor = if nullifiers.len() > page_size.get() {
nullifiers.pop(); nullifiers.last().map(|info| info.nullifier)
} else {
None
};
Ok(NullifiersPage { nullifiers, next_cursor })
}
#[derive(Debug, Clone, Queryable, QueryableByName, Selectable)]
#[diesel(table_name = schema::nullifiers)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct NullifierWithoutPrefixRawRow {
pub nullifier: Vec<u8>,
pub block_num: i64,
}
impl TryInto<NullifierInfo> for NullifierWithoutPrefixRawRow {
type Error = DatabaseError;
fn try_into(self) -> Result<NullifierInfo, Self::Error> {
let nullifier = Nullifier::read_from_bytes(&self.nullifier)?;
let block_num = BlockNumber::from_raw_sql(self.block_num)?;
Ok(NullifierInfo { nullifier, block_num })
}
}