Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .config/nextest.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[[profile.default.overrides]]
filter = 'package(miden-validator) & test(/(failed_(batch_item|proof_verification|reexecution)|header_mismatch)_does_not_store_inputs|valid_submission_stores_first_sealed_inputs/)'
threads-required = "num-test-threads"
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion bin/validator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ miden-node-db = { workspace = true }
miden-node-store = { workspace = true }
miden-node-utils = { features = ["testing"], workspace = true }
miden-protocol = { default-features = true, features = ["testing"], workspace = true }
miden-testing = { workspace = true }
rand = { workspace = true }
tempfile = { workspace = true }
tokio = { features = ["macros", "rt-multi-thread"], workspace = true }
tokio = { features = ["macros", "rt-multi-thread", "sync"], workspace = true }
2 changes: 1 addition & 1 deletion bin/validator/src/db/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ mod tests {
use super::*;

const EXPECTED_SCHEMA_HASHES: [SchemaHash; 1] = [SchemaHash::from_hex(
"100025e3daa05c2f7d5be2dc6ff096dbe916f1af4d95ae27cdfb2e23d6f0723a",
"68955ff3be5db1aa9967285fca66c14e56e04f22c5e9bc5729f586e09d6c0531",
)];

#[test]
Expand Down
15 changes: 4 additions & 11 deletions bin/validator/src/db/migrations/001_initial.sql
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
CREATE TABLE validated_transactions (
id BLOB NOT NULL,
block_num BIGINT NOT NULL,
account_id BLOB NOT NULL,
account_patch BLOB NOT NULL,
input_notes BLOB,
output_notes BLOB,
initial_account_hash BLOB NOT NULL,
final_account_hash BLOB NOT NULL,
id BLOB NOT NULL,
submission_scheme BIGINT NOT NULL,
submission_key_id BLOB NOT NULL,
sealed_transaction_inputs BLOB NOT NULL,
PRIMARY KEY (id)
) WITHOUT ROWID;

CREATE INDEX idx_validated_transactions_account_id ON validated_transactions(account_id);
CREATE INDEX idx_validated_transactions_block_num ON validated_transactions(block_num);

CREATE TABLE block_headers (
block_num BIGINT PRIMARY KEY,
block_header BLOB NOT NULL
Expand Down
72 changes: 47 additions & 25 deletions bin/validator/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ use miden_protocol::transaction::TransactionId;
use miden_protocol::utils::serde::Serializable;

use crate::db::migrations::{bootstrap_database, migrate_database, verify_latest_schema};
use crate::tx_validation::ValidatedTransaction;
use crate::{COMPONENT, LOG_TARGET};

/// SQL statements, kept in dedicated `.sql` files (under `sql/`).
mod sql {
pub(super) const INSERT_TRANSACTION: &str = include_str!("sql/insert_transaction.sql");
#[cfg(test)]
pub(super) const LOAD_TRANSACTION: &str = include_str!("sql/load_transaction.sql");
pub(super) const TRANSACTION_EXISTS: &str = include_str!("sql/transaction_exists.sql");
pub(super) const UPSERT_BLOCK_HEADER: &str = include_str!("sql/upsert_block_header.sql");
pub(super) const LOAD_CHAIN_TIP: &str = include_str!("sql/load_chain_tip.sql");
Expand Down Expand Up @@ -97,43 +98,66 @@ fn open_with_pool_size(
Ok(db)
}

/// Inserts a new validated transaction into the database.
/// The sealed transaction inputs accepted by the validator.
///
/// This is the Phase 1 storage record. Phase 2 will replace the client envelope with inputs
/// re-encrypted under a fresh content key protected by Golden EHTDH1.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ValidatedTransactionRecord {
pub transaction_id: TransactionId,
pub submission_scheme: u32,
pub submission_key_id: Vec<u8>,
pub sealed_transaction_inputs: Vec<u8>,
}

/// Inserts the accepted sealed inputs and validated marker in one database write.
#[miden_instrument(
target = COMPONENT,
skip_all,
fields(
transaction.id = %tx_info.tx_id(),
transaction.id = %record.transaction_id,
),
err,
)]
pub(crate) fn insert_transaction(
tx: &WriteTx<'_>,
tx_info: &ValidatedTransaction,
record: &ValidatedTransactionRecord,
) -> Result<usize, DatabaseError> {
let id = tx_info.tx_id().to_bytes();
let block_num = i64::from(tx_info.block_num().as_u32());
let account_id = tx_info.account_id().to_bytes();
let account_patch = tx_info.account_patch().to_bytes();
let input_notes = tx_info.input_notes().to_bytes();
let output_notes = tx_info.output_notes().to_bytes();
let initial_account_hash = tx_info.initial_account_hash().to_bytes();
let final_account_hash = tx_info.final_account_hash().to_bytes();
let id = record.transaction_id.to_bytes();
let submission_scheme = i64::from(record.submission_scheme);

tx.execute(
sql::INSERT_TRANSACTION,
&[
&id,
&block_num,
&account_id,
&account_patch,
&input_notes,
&output_notes,
&initial_account_hash,
&final_account_hash,
&submission_scheme,
&record.submission_key_id,
&record.sealed_transaction_inputs,
],
)
}

/// Loads the sealed record stored for a validated transaction.
#[cfg(test)]
pub(crate) fn load_transaction(
tx: &ReadTx<'_>,
tx_id: TransactionId,
) -> Result<Option<ValidatedTransactionRecord>, DatabaseError> {
tx.query(sql::LOAD_TRANSACTION, &[&tx_id.to_bytes()], |row| {
let submission_scheme = row
.get::<i64>(0)?
.try_into()
.expect("stored submission scheme should fit in u32");
Ok(ValidatedTransactionRecord {
transaction_id: tx_id,
submission_scheme,
submission_key_id: row.get(1)?,
sealed_transaction_inputs: row.get(2)?,
})
})
.map(|mut records| records.pop())
}

/// Returns whether a transaction with the given id has already been validated.
#[miden_instrument(
target = COMPONENT,
Expand Down Expand Up @@ -295,17 +319,15 @@ mod tests {
let validated_id = TransactionId::from_raw(Word::try_from([1u64, 2, 3, 4]).unwrap());
let unknown_id = TransactionId::from_raw(Word::try_from([5u64, 6, 7, 8]).unwrap());

// Insert a row keyed by `validated_id`. Only the primary key matters for this query, so the
// remaining columns are filled with placeholder bytes.
// Insert a row keyed by `validated_id`.
let id = validated_id.to_bytes();
let empty: Vec<u8> = vec![];
db.write("insert_row", move |tx| {
tx.execute(
"INSERT INTO validated_transactions \
(id, block_num, account_id, account_patch, input_notes, output_notes, \
initial_account_hash, final_account_hash) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
&[&id, &0i64, &empty, &empty, &empty, &empty, &empty, &empty],
(id, submission_scheme, submission_key_id, sealed_transaction_inputs) \
VALUES (?1, ?2, ?3, ?4)",
&[&id, &1i64, &empty, &empty],
)
})
.await
Expand Down
12 changes: 4 additions & 8 deletions bin/validator/src/db/sql/insert_transaction.sql
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
INSERT INTO validated_transactions (
id,
block_num,
account_id,
account_patch,
input_notes,
output_notes,
initial_account_hash,
final_account_hash
submission_scheme,
submission_key_id,
sealed_transaction_inputs
)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
VALUES (?1, ?2, ?3, ?4)
ON CONFLICT DO NOTHING;
6 changes: 6 additions & 0 deletions bin/validator/src/db/sql/load_transaction.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
SELECT
submission_scheme,
submission_key_id,
sealed_transaction_inputs
FROM validated_transactions
WHERE id = ?1;
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use tonic::Status;

use super::ValidatorService;
use crate::COMPONENT;
use crate::db::{insert_transaction, transaction_exists};
use crate::db::{ValidatedTransactionRecord, insert_transaction, transaction_exists};
use crate::tx_validation::validate_transaction;

#[tonic::async_trait]
Expand All @@ -30,12 +30,13 @@ impl grpc::server::validator_api::SubmitProvenTransaction for ValidatorService {
_metadata: &tonic::metadata::MetadataMap,
_extensions: &tonic::codegen::http::Extensions,
) -> tonic::Result<Self::Output> {
let tx_id = input.tx.id();
let Input { tx, sealed } = input;
let tx_id = tx.id();
miden_span_record!(
transaction.id = %tx_id,
);

let inputs = self.unseal_transaction_inputs(&input.sealed, tx_id).await?;
let inputs = self.unseal_transaction_inputs(&sealed, tx_id).await?;

// Reject requests while a backup subscription is streaming.
let _guard = self
Expand All @@ -56,14 +57,21 @@ impl grpc::server::validator_api::SubmitProvenTransaction for ValidatorService {
}

// Validate the transaction.
let tx_info = validate_transaction(input.tx, inputs).await.map_err(|err| {
validate_transaction(tx, inputs).await.map_err(|err| {
Status::invalid_argument(err.as_report_context("Invalid transaction"))
})?;

// Store the validated transaction.
// This Phase 1 record stores the accepted client envelope only after plaintext validation.
// Phase 2 will instead store the validated inputs under threshold encryption.
let record = ValidatedTransactionRecord {
transaction_id: tx_id,
submission_scheme: self.encryption_key_info.scheme.as_u32(),
submission_key_id: sealed.key_id,
sealed_transaction_inputs: sealed.ciphertext,
};
let count = self
.db
.write("insert_transaction", move |tx| insert_transaction(tx, &tx_info))
.write("insert_transaction", move |tx| insert_transaction(tx, &record))
.await
.map_err(|err| {
Status::internal(err.as_report_context("Failed to insert transaction"))
Expand Down
Loading
Loading