#![allow(clippy::items_after_statements)]
use std::collections::BTreeMap;
use std::rc::Rc;
use std::string::{String, ToString};
use std::vec::Vec;
use miden_client::Word;
use miden_client::account::AccountId;
use miden_client::note::{
BlockNumber,
NoteAssets,
NoteDetails,
NoteMetadata,
NoteRecipient,
NoteScript,
NoteUpdateTracker,
NoteUpdateType,
Nullifier,
};
use miden_client::store::{
InputNoteRecord,
InputNoteState,
NoteFilter,
OutputNoteRecord,
OutputNoteState,
StoreError,
};
use miden_client::utils::{Deserializable, Serializable};
use miden_protocol::note::NoteStorage;
use rusqlite::types::Value;
use rusqlite::{Connection, Transaction, params, params_from_iter};
use super::SqliteStore;
use crate::chain_data::set_block_header_has_client_notes;
use crate::note::filters::{note_filter_to_query_input_notes, note_filter_to_query_output_notes};
use crate::sql_error::SqlResultExt;
use crate::{insert_sql, subst};
mod filters;
const INPUT_NOTE_BATCH_SIZE: usize = 50;
const OUTPUT_NOTE_BATCH_SIZE: usize = 80;
const SCRIPT_BATCH_SIZE: usize = 200;
#[cfg(test)]
mod tests;
struct SerializedInputNoteData {
pub id: String,
pub assets: Vec<u8>,
pub serial_number: Vec<u8>,
pub inputs: Vec<u8>,
pub script_root: String,
pub script: Vec<u8>,
pub nullifier: String,
pub state_discriminant: u8,
pub state: Vec<u8>,
pub created_at: u64,
pub consumed_block_height: Option<u32>,
pub consumed_tx_order: Option<u32>,
pub consumer_account_id: Option<String>,
}
struct SerializedOutputNoteData {
pub id: String,
pub assets: Vec<u8>,
pub metadata: Vec<u8>,
pub nullifier: Option<String>,
pub recipient_digest: String,
pub expected_height: u32,
pub state_discriminant: u8,
pub state: Vec<u8>,
}
struct SerializedInputNoteParts {
pub assets: Vec<u8>,
pub serial_number: Vec<u8>,
pub inputs: Vec<u8>,
pub script: Vec<u8>,
pub state: Vec<u8>,
pub created_at: u64,
}
struct SerializedOutputNoteParts {
pub assets: Vec<u8>,
pub metadata: Vec<u8>,
pub recipient_digest: String,
pub expected_height: u32,
pub state: Vec<u8>,
}
struct SerializedInputNoteStateUpdate {
pub id: String,
pub state_discriminant: u8,
pub state: Vec<u8>,
pub consumed_block_height: Option<u32>,
pub consumed_tx_order: Option<u32>,
pub consumer_account_id: Option<String>,
}
struct SerializedOutputNoteStateUpdate {
pub id: String,
pub state_discriminant: u8,
pub state: Vec<u8>,
}
struct SerializedNoteScriptPars {
pub script: Vec<u8>,
}
impl SqliteStore {
pub(crate) fn get_input_notes(
conn: &mut Connection,
filter: &NoteFilter,
) -> Result<Vec<InputNoteRecord>, StoreError> {
let (query, params) = note_filter_to_query_input_notes(filter);
let notes = conn
.prepare(query.as_str())
.into_store_error()?
.query_map(params_from_iter(params), parse_input_note_columns)
.expect("no binding parameters used in query")
.map(|result| Ok(result.into_store_error()?).and_then(parse_input_note))
.collect::<Result<Vec<InputNoteRecord>, _>>()?;
Ok(notes)
}
pub(crate) fn get_output_notes(
conn: &mut Connection,
filter: &NoteFilter,
) -> Result<Vec<OutputNoteRecord>, StoreError> {
let (query, params) = note_filter_to_query_output_notes(filter);
let notes = conn
.prepare(&query)
.into_store_error()?
.query_map(params_from_iter(params), parse_output_note_columns)
.expect("no binding parameters used in query")
.map(|result| Ok(result.into_store_error()?).and_then(parse_output_note))
.collect::<Result<Vec<OutputNoteRecord>, _>>()?;
Ok(notes)
}
pub(crate) fn get_input_note_by_offset(
conn: &mut Connection,
filter: &NoteFilter,
consumer: AccountId,
block_start: Option<BlockNumber>,
block_end: Option<BlockNumber>,
offset: u32,
) -> Result<Option<InputNoteRecord>, StoreError> {
let consumer_hex = consumer.to_hex();
let (query, params) = filters::note_filter_to_query_input_note_by_offset(
filter,
&consumer_hex,
block_start,
block_end,
offset,
);
let note = conn
.prepare(&query)
.into_store_error()?
.query_map(params_from_iter(params), parse_input_note_columns)
.expect("no binding parameters used in query")
.map(|result| Ok(result.into_store_error()?).and_then(parse_input_note))
.next()
.transpose()?;
Ok(note)
}
pub(crate) fn upsert_input_notes(
conn: &mut Connection,
notes: &[InputNoteRecord],
) -> Result<(), StoreError> {
let tx = conn.transaction().into_store_error()?;
for note in notes {
upsert_input_note_tx(&tx, note)?;
if let Some(inclusion_proof) = note.inclusion_proof() {
set_block_header_has_client_notes(
&tx,
inclusion_proof.location().block_num().as_u64(),
true,
)?;
}
}
tx.commit().into_store_error()
}
pub(crate) fn get_unspent_input_note_nullifiers(
conn: &mut Connection,
) -> Result<Vec<Nullifier>, StoreError> {
const QUERY: &str =
"SELECT nullifier FROM input_notes WHERE state_discriminant NOT IN rarray(?)";
let unspent_filters = Rc::new(vec![
Value::from(InputNoteState::STATE_CONSUMED_AUTHENTICATED_LOCAL.to_string()),
Value::from(InputNoteState::STATE_CONSUMED_UNAUTHENTICATED_LOCAL.to_string()),
Value::from(InputNoteState::STATE_CONSUMED_EXTERNAL.to_string()),
]);
conn.prepare(QUERY)
.into_store_error()?
.query_map([unspent_filters], |row| row.get(0))
.expect("no binding parameters used in query")
.map(|result| {
result
.map_err(|err| StoreError::ParsingError(err.to_string()))
.and_then(|v: String| Ok(Nullifier::from_hex(&v)?))
})
.collect::<Result<Vec<Nullifier>, _>>()
}
pub(crate) fn upsert_note_scripts(
conn: &mut Connection,
note_scripts: &[NoteScript],
) -> Result<(), StoreError> {
let tx = conn.transaction().into_store_error()?;
for note_script in note_scripts {
upsert_note_script_tx(&tx, note_script)?;
}
tx.commit().into_store_error()
}
pub(crate) fn get_note_script(
conn: &mut Connection,
script_root: Word,
) -> Result<NoteScript, StoreError> {
let script_root = script_root.to_hex();
let query = "SELECT * FROM notes_scripts WHERE script_root = ?";
let note_script = conn
.prepare(query)
.into_store_error()?
.query_map([script_root.clone()], parse_note_scripts_columns)
.expect("no binding parameters used in query")
.map(|result| Ok(result.into_store_error()?).and_then(|s| parse_note_script(&s)))
.collect::<Result<Vec<NoteScript>, _>>()?
.first()
.cloned()
.ok_or(StoreError::NoteScriptNotFound(script_root))?;
Ok(note_script)
}
}
pub(super) fn upsert_input_note_tx(
tx: &Transaction<'_>,
note: &InputNoteRecord,
) -> Result<(), StoreError> {
let SerializedInputNoteData {
id,
assets,
serial_number,
inputs,
script_root,
script,
nullifier,
state_discriminant,
state,
created_at,
consumed_block_height,
consumed_tx_order,
consumer_account_id,
} = serialize_input_note(note);
const SCRIPT_QUERY: &str =
insert_sql!(notes_scripts { script_root, serialized_note_script } | REPLACE);
tx.prepare_cached(SCRIPT_QUERY)
.into_store_error()?
.execute(params![script_root, script])
.into_store_error()?;
const NOTE_QUERY: &str = insert_sql!(
input_notes {
note_id,
assets,
serial_number,
inputs,
script_root,
nullifier,
state_discriminant,
state,
created_at,
consumed_block_height,
consumed_tx_order,
consumer_account_id,
} | REPLACE
);
tx.prepare_cached(NOTE_QUERY)
.into_store_error()?
.execute(params![
id,
assets,
serial_number,
inputs,
script_root,
nullifier,
state_discriminant,
state,
created_at,
consumed_block_height,
consumed_tx_order,
consumer_account_id,
])
.into_store_error()?;
Ok(())
}
fn parse_input_note_columns(
row: &rusqlite::Row<'_>,
) -> Result<SerializedInputNoteParts, rusqlite::Error> {
let assets: Vec<u8> = row.get(0)?;
let serial_number: Vec<u8> = row.get(1)?;
let inputs: Vec<u8> = row.get(2)?;
let script: Vec<u8> = row.get(3)?;
let state: Vec<u8> = row.get(4)?;
let created_at: u64 = row.get(5)?;
Ok(SerializedInputNoteParts {
assets,
serial_number,
inputs,
script,
state,
created_at,
})
}
fn parse_input_note(
serialized_input_note_parts: SerializedInputNoteParts,
) -> Result<InputNoteRecord, StoreError> {
let SerializedInputNoteParts {
assets,
serial_number,
inputs,
script,
state,
created_at,
} = serialized_input_note_parts;
let assets = NoteAssets::read_from_bytes(&assets)?;
let serial_number = Word::read_from_bytes(&serial_number)?;
let script = NoteScript::read_from_bytes(&script)?;
let inputs = NoteStorage::read_from_bytes(&inputs)?;
let recipient = NoteRecipient::new(serial_number, script, inputs);
let details = NoteDetails::new(assets, recipient);
let state = InputNoteState::read_from_bytes(&state)?;
Ok(InputNoteRecord::new(details, Some(created_at), state))
}
fn serialize_input_note(note: &InputNoteRecord) -> SerializedInputNoteData {
let id = note.id().as_word().to_string();
let nullifier = note.nullifier().to_hex();
let created_at = note.created_at().unwrap_or(0);
let details = note.details();
let assets = details.assets().to_bytes();
let recipient = details.recipient();
let serial_number = recipient.serial_num().to_bytes();
let script = recipient.script().to_bytes();
let inputs = recipient.storage().to_bytes();
let script_root = recipient.script().root().to_hex();
let state_discriminant = note.state().discriminant();
let state = note.state().to_bytes();
let consumed_block_height = note.state().consumed_block_height().map(|h| h.as_u32());
let consumed_tx_order = note.state().consumed_tx_order();
let consumer_account_id = note.consumer_account().map(AccountId::to_hex);
SerializedInputNoteData {
id,
assets,
serial_number,
inputs,
script_root,
script,
nullifier,
state_discriminant,
state,
created_at,
consumed_block_height,
consumed_tx_order,
consumer_account_id,
}
}
fn parse_output_note_columns(
row: &rusqlite::Row<'_>,
) -> Result<SerializedOutputNoteParts, rusqlite::Error> {
let recipient_digest: String = row.get(0)?;
let assets: Vec<u8> = row.get(1)?;
let metadata: Vec<u8> = row.get(2)?;
let expected_height: u32 = row.get(3)?;
let state: Vec<u8> = row.get(4)?;
Ok(SerializedOutputNoteParts {
assets,
metadata,
recipient_digest,
expected_height,
state,
})
}
fn parse_output_note(
serialized_output_note_parts: SerializedOutputNoteParts,
) -> Result<OutputNoteRecord, StoreError> {
let SerializedOutputNoteParts {
recipient_digest,
assets,
metadata,
expected_height,
state,
} = serialized_output_note_parts;
let recipient_digest = Word::try_from(recipient_digest)?;
let assets = NoteAssets::read_from_bytes(&assets)?;
let metadata = NoteMetadata::read_from_bytes(&metadata)?;
let state = OutputNoteState::read_from_bytes(&state)?;
Ok(OutputNoteRecord::new(
recipient_digest,
assets,
metadata,
state,
BlockNumber::from(expected_height),
))
}
fn serialize_input_note_state(note: &InputNoteRecord) -> SerializedInputNoteStateUpdate {
let consumed_block_height = note.state().consumed_block_height().map(|h| h.as_u32());
let consumed_tx_order = note.state().consumed_tx_order();
let consumer_account_id = note.consumer_account().map(AccountId::to_hex);
SerializedInputNoteStateUpdate {
id: note.id().as_word().to_string(),
state_discriminant: note.state().discriminant(),
state: note.state().to_bytes(),
consumed_block_height,
consumed_tx_order,
consumer_account_id,
}
}
fn serialize_output_note_state(note: &OutputNoteRecord) -> SerializedOutputNoteStateUpdate {
SerializedOutputNoteStateUpdate {
id: note.id().as_word().to_string(),
state_discriminant: note.state().discriminant(),
state: note.state().to_bytes(),
}
}
fn serialize_output_note(note: &OutputNoteRecord) -> SerializedOutputNoteData {
let id = note.id().as_word().to_string();
let assets = note.assets().to_bytes();
let recipient_digest = note.recipient_digest().to_hex();
let metadata = note.metadata().to_bytes();
let nullifier = note.nullifier().map(|nullifier| nullifier.to_hex());
let state_discriminant = note.state().discriminant();
let state = note.state().to_bytes();
SerializedOutputNoteData {
id,
assets,
metadata,
nullifier,
recipient_digest,
expected_height: note.expected_height().as_u32(),
state_discriminant,
state,
}
}
pub(crate) fn apply_note_updates_tx(
tx: &Transaction,
note_updates: &NoteUpdateTracker,
) -> Result<(), StoreError> {
let mut input_inserts = Vec::new();
let mut input_updates = Vec::new();
let mut scripts: BTreeMap<String, Vec<u8>> = BTreeMap::new();
for input_note in note_updates.updated_input_notes() {
match input_note.update_type() {
NoteUpdateType::Insert => {
let serialized = serialize_input_note(input_note.inner());
scripts.insert(serialized.script_root.clone(), serialized.script.clone());
input_inserts.push(serialized);
},
NoteUpdateType::Update => {
input_updates.push(serialize_input_note_state(input_note.inner()));
},
NoteUpdateType::None => {},
}
}
batch_upsert_scripts(tx, &scripts)?;
batch_insert_input_notes(tx, &input_inserts)?;
batch_update_input_note_states(tx, &input_updates)?;
let mut output_inserts = Vec::new();
let mut output_updates = Vec::new();
for output_note in note_updates.updated_output_notes() {
match output_note.update_type() {
NoteUpdateType::Insert => {
output_inserts.push(serialize_output_note(output_note.inner()));
},
NoteUpdateType::Update => {
output_updates.push(serialize_output_note_state(output_note.inner()));
},
NoteUpdateType::None => {},
}
}
batch_insert_output_notes(tx, &output_inserts)?;
batch_update_output_note_states(tx, &output_updates)?;
Ok(())
}
fn batch_upsert_scripts(
tx: &Transaction,
scripts: &BTreeMap<String, Vec<u8>>,
) -> Result<(), StoreError> {
if scripts.is_empty() {
return Ok(());
}
let entries: Vec<_> = scripts.iter().collect();
for chunk in entries.chunks(SCRIPT_BATCH_SIZE) {
let placeholders = vec!["(?, ?)"; chunk.len()].join(", ");
let query = format!(
"INSERT OR REPLACE INTO `notes_scripts` (`script_root`, `serialized_note_script`) \
VALUES {placeholders}"
);
let mut param_values: Vec<Value> = Vec::with_capacity(chunk.len() * 2);
for (root, script) in chunk {
param_values.push(Value::Text((*root).clone()));
param_values.push(Value::Blob((*script).clone()));
}
tx.execute(&query, params_from_iter(param_values)).into_store_error()?;
}
Ok(())
}
fn batch_insert_input_notes(
tx: &Transaction,
notes: &[SerializedInputNoteData],
) -> Result<(), StoreError> {
if notes.is_empty() {
return Ok(());
}
for chunk in notes.chunks(INPUT_NOTE_BATCH_SIZE) {
let placeholders = vec!["(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; chunk.len()].join(", ");
let query = format!(
"INSERT OR REPLACE INTO `input_notes` \
(`note_id`, `assets`, `serial_number`, `inputs`, `script_root`, \
`nullifier`, `state_discriminant`, `state`, `created_at`, \
`consumed_block_height`, `consumed_tx_order`, `consumer_account_id`) \
VALUES {placeholders}"
);
let mut param_values: Vec<Value> = Vec::with_capacity(chunk.len() * 12);
for note in chunk {
param_values.push(Value::Text(note.id.clone()));
param_values.push(Value::Blob(note.assets.clone()));
param_values.push(Value::Blob(note.serial_number.clone()));
param_values.push(Value::Blob(note.inputs.clone()));
param_values.push(Value::Text(note.script_root.clone()));
param_values.push(Value::Text(note.nullifier.clone()));
param_values.push(Value::Integer(i64::from(note.state_discriminant)));
param_values.push(Value::Blob(note.state.clone()));
#[allow(clippy::cast_possible_wrap)]
param_values.push(Value::Integer(note.created_at as i64));
match note.consumed_block_height {
Some(h) => param_values.push(Value::Integer(i64::from(h))),
None => param_values.push(Value::Null),
}
match note.consumed_tx_order {
Some(o) => param_values.push(Value::Integer(i64::from(o))),
None => param_values.push(Value::Null),
}
match ¬e.consumer_account_id {
Some(id) => param_values.push(Value::Text(id.clone())),
None => param_values.push(Value::Null),
}
}
tx.execute(&query, params_from_iter(param_values)).into_store_error()?;
}
Ok(())
}
fn batch_update_input_note_states(
tx: &Transaction,
updates: &[SerializedInputNoteStateUpdate],
) -> Result<(), StoreError> {
if updates.is_empty() {
return Ok(());
}
let mut stmt = tx
.prepare_cached(
"UPDATE `input_notes` SET state_discriminant = ?, state = ?, \
consumed_block_height = ?, consumed_tx_order = ?, consumer_account_id = ? \
WHERE note_id = ?",
)
.into_store_error()?;
for update in updates {
stmt.execute(params![
update.state_discriminant,
update.state,
update.consumed_block_height,
update.consumed_tx_order,
update.consumer_account_id,
update.id,
])
.into_store_error()?;
}
Ok(())
}
fn batch_insert_output_notes(
tx: &Transaction,
notes: &[SerializedOutputNoteData],
) -> Result<(), StoreError> {
if notes.is_empty() {
return Ok(());
}
for chunk in notes.chunks(OUTPUT_NOTE_BATCH_SIZE) {
let placeholders = vec!["(?, ?, ?, ?, ?, ?, ?, ?)"; chunk.len()].join(", ");
let query = format!(
"INSERT OR REPLACE INTO `output_notes` \
(`note_id`, `assets`, `recipient_digest`, `metadata`, \
`nullifier`, `expected_height`, `state_discriminant`, `state`) \
VALUES {placeholders}"
);
let mut param_values: Vec<Value> = Vec::with_capacity(chunk.len() * 8);
for note in chunk {
param_values.push(Value::Text(note.id.clone()));
param_values.push(Value::Blob(note.assets.clone()));
param_values.push(Value::Text(note.recipient_digest.clone()));
param_values.push(Value::Blob(note.metadata.clone()));
match ¬e.nullifier {
Some(n) => param_values.push(Value::Text(n.clone())),
None => param_values.push(Value::Null),
}
param_values.push(Value::Integer(i64::from(note.expected_height)));
param_values.push(Value::Integer(i64::from(note.state_discriminant)));
param_values.push(Value::Blob(note.state.clone()));
}
tx.execute(&query, params_from_iter(param_values)).into_store_error()?;
}
Ok(())
}
fn batch_update_output_note_states(
tx: &Transaction,
updates: &[SerializedOutputNoteStateUpdate],
) -> Result<(), StoreError> {
if updates.is_empty() {
return Ok(());
}
let mut stmt = tx
.prepare_cached(
"UPDATE `output_notes` SET state_discriminant = ?, state = ? WHERE note_id = ?",
)
.into_store_error()?;
for update in updates {
stmt.execute(params![update.state_discriminant, update.state, update.id])
.into_store_error()?;
}
Ok(())
}
pub(super) fn upsert_note_script_tx(
tx: &Transaction<'_>,
note_script: &NoteScript,
) -> Result<(), StoreError> {
const QUERY: &str =
insert_sql!(notes_scripts { script_root, serialized_note_script } | REPLACE);
tx.prepare_cached(QUERY)
.into_store_error()?
.execute(params![note_script.root().to_hex(), note_script.to_bytes()])
.into_store_error()?;
Ok(())
}
fn parse_note_scripts_columns(
row: &rusqlite::Row<'_>,
) -> Result<SerializedNoteScriptPars, rusqlite::Error> {
let script = row.get(1)?;
Ok(SerializedNoteScriptPars { script })
}
fn parse_note_script(
serialized_note_script_parts: &SerializedNoteScriptPars,
) -> Result<NoteScript, StoreError> {
let note_script = NoteScript::from_bytes(&serialized_note_script_parts.script)?;
Ok(note_script)
}