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,
QueryParamNullifierLimit,
QueryParamNullifierPrefixLimit,
};
use miden_protocol::block::BlockNumber;
use miden_protocol::note::Nullifier;
use miden_protocol::utils::serde::{Deserializable, Serializable};
use super::DatabaseError;
use crate::COMPONENT;
use crate::db::models::conv::{SqlTypeConvert, nullifier_prefix_to_raw_sql};
use crate::db::models::utils::{get_nullifier_prefix, 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;
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 })
}
#[tracing::instrument(
target = COMPONENT,
skip_all,
err,
)]
pub(crate) fn insert_nullifiers_for_block(
conn: &mut SqliteConnection,
nullifiers: &[Nullifier],
block_num: BlockNumber,
) -> Result<usize, DatabaseError> {
QueryParamNullifierLimit::check(nullifiers.len())?;
let serialized_nullifiers =
Vec::<Vec<u8>>::from_iter(nullifiers.iter().map(Nullifier::to_bytes));
let mut count = diesel::update(schema::notes::table)
.filter(schema::notes::nullifier.eq_any(&serialized_nullifiers))
.set(schema::notes::consumed_at.eq(Some(block_num.to_raw_sql())))
.execute(conn)?;
count += diesel::insert_into(schema::nullifiers::table)
.values(Vec::from_iter(nullifiers.iter().zip(serialized_nullifiers.iter()).map(
|(nullifier, bytes)| {
(
schema::nullifiers::nullifier.eq(bytes),
schema::nullifiers::nullifier_prefix
.eq(nullifier_prefix_to_raw_sql(get_nullifier_prefix(nullifier))),
schema::nullifiers::block_num.eq(block_num.to_raw_sql()),
)
},
)))
.execute(conn)?;
Ok(count)
}
#[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 })
}
}