diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 0000000000..e64badc89e --- /dev/null +++ b/.config/nextest.toml @@ -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" diff --git a/Cargo.lock b/Cargo.lock index 67787e3393..756153e88f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3863,6 +3863,7 @@ dependencies = [ "miden-node-store", "miden-node-utils", "miden-protocol", + "miden-testing", "miden-tx", "rand 0.10.2", "tempfile", diff --git a/bin/validator/Cargo.toml b/bin/validator/Cargo.toml index 8946a77cf1..214fb66918 100644 --- a/bin/validator/Cargo.toml +++ b/bin/validator/Cargo.toml @@ -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 } diff --git a/bin/validator/src/db/migrations.rs b/bin/validator/src/db/migrations.rs index 0411ce605d..3fa24ed3ad 100644 --- a/bin/validator/src/db/migrations.rs +++ b/bin/validator/src/db/migrations.rs @@ -71,7 +71,7 @@ mod tests { use super::*; const EXPECTED_SCHEMA_HASHES: [SchemaHash; 1] = [SchemaHash::from_hex( - "100025e3daa05c2f7d5be2dc6ff096dbe916f1af4d95ae27cdfb2e23d6f0723a", + "68955ff3be5db1aa9967285fca66c14e56e04f22c5e9bc5729f586e09d6c0531", )]; #[test] diff --git a/bin/validator/src/db/migrations/001_initial.sql b/bin/validator/src/db/migrations/001_initial.sql index 1aa7c89388..1bc63de88b 100644 --- a/bin/validator/src/db/migrations/001_initial.sql +++ b/bin/validator/src/db/migrations/001_initial.sql @@ -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 diff --git a/bin/validator/src/db/mod.rs b/bin/validator/src/db/mod.rs index a1b4886166..93b137bad1 100644 --- a/bin/validator/src/db/mod.rs +++ b/bin/validator/src/db/mod.rs @@ -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"); @@ -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, + pub sealed_transaction_inputs: Vec, +} + +/// 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 { - 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, DatabaseError> { + tx.query(sql::LOAD_TRANSACTION, &[&tx_id.to_bytes()], |row| { + let submission_scheme = row + .get::(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, @@ -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 = 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 diff --git a/bin/validator/src/db/sql/insert_transaction.sql b/bin/validator/src/db/sql/insert_transaction.sql index 312a4fa8a8..a4e540347a 100644 --- a/bin/validator/src/db/sql/insert_transaction.sql +++ b/bin/validator/src/db/sql/insert_transaction.sql @@ -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; diff --git a/bin/validator/src/db/sql/load_transaction.sql b/bin/validator/src/db/sql/load_transaction.sql new file mode 100644 index 0000000000..147817dacb --- /dev/null +++ b/bin/validator/src/db/sql/load_transaction.sql @@ -0,0 +1,6 @@ +SELECT + submission_scheme, + submission_key_id, + sealed_transaction_inputs +FROM validated_transactions +WHERE id = ?1; diff --git a/bin/validator/src/server/validator_service/submit_proven_transaction.rs b/bin/validator/src/server/validator_service/submit_proven_transaction.rs index 53df4e0696..8ca618718f 100644 --- a/bin/validator/src/server/validator_service/submit_proven_transaction.rs +++ b/bin/validator/src/server/validator_service/submit_proven_transaction.rs @@ -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] @@ -30,12 +30,13 @@ impl grpc::server::validator_api::SubmitProvenTransaction for ValidatorService { _metadata: &tonic::metadata::MetadataMap, _extensions: &tonic::codegen::http::Extensions, ) -> tonic::Result { - 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 @@ -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")) diff --git a/bin/validator/src/server/validator_service/tests.rs b/bin/validator/src/server/validator_service/tests.rs index 0d82ff5b11..5015821849 100644 --- a/bin/validator/src/server/validator_service/tests.rs +++ b/bin/validator/src/server/validator_service/tests.rs @@ -12,9 +12,13 @@ use miden_node_store::{BlockStore, GenesisState}; use miden_node_utils::fee::test_fee_params; use miden_protocol::Word; use miden_protocol::account::AccountUpdateDetails; +use miden_protocol::account::auth::AuthScheme; +use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::block::{BlockHeader, BlockInputs, BlockNumber, ProposedBlock, ValidatorKeys}; use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SigningKey; use miden_protocol::crypto::dsa::eddsa_25519_sha512::KeyExchangeKey; +use miden_protocol::note::NoteType; +use miden_protocol::testing::account_id::{ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, ACCOUNT_ID_SENDER}; use miden_protocol::testing::random_secret_key::random_secret_key; use miden_protocol::transaction::{ InputNoteCommitment, @@ -22,13 +26,24 @@ use miden_protocol::transaction::{ PartialBlockchain, ProvenTransaction, TransactionId, + TransactionInputs, TxAccountUpdate, }; use miden_protocol::vm::ExecutionProof; +use miden_testing::{Auth, MockChainBuilder}; +use miden_tx::LocalTransactionProver; use miden_tx::utils::serde::{Deserializable, Serializable}; +use tokio::sync::OnceCell; use super::{ValidatorError, ValidatorService}; -use crate::db::{load_chain_tip, setup, upsert_block_header}; +use crate::db::{ + ValidatedTransactionRecord, + count_validated_transactions, + load_chain_tip, + load_transaction, + setup, + upsert_block_header, +}; use crate::{LocalX25519TransactionInputDecrypter, TransactionInputDecrypter, ValidatorSigner}; // TEST HELPERS @@ -167,6 +182,34 @@ impl TestValidator { .expect("status should always be available") } + /// Loads the sealed record for `tx_id`, if validation stored one. + async fn load_transaction(&self, tx_id: TransactionId) -> Option { + self.server + .db + .read("load_transaction", move |tx| load_transaction(tx, tx_id)) + .await + .unwrap() + } + + /// Returns the persisted validated transaction count. + async fn validated_transaction_count(&self) -> i64 { + self.server + .db + .read("count_validated_transactions", count_validated_transactions) + .await + .unwrap() + } + + /// Asserts that a rejected transaction did not change either validated count. + async fn assert_transaction_absent(&self, tx_id: TransactionId, expected_count: i64) { + assert_eq!(self.load_transaction(tx_id).await, None); + assert_eq!(self.validated_transaction_count().await, expected_count); + assert_eq!( + self.call_status().await.validated_transactions_count, + u64::try_from(expected_count).unwrap(), + ); + } + /// Calls the `get_transaction_encryption_key` endpoint on the validator server. async fn call_get_transaction_encryption_key( &self, @@ -280,6 +323,78 @@ fn dummy_proven_tx(seed: u8) -> ProvenTransaction { .unwrap() } +/// A proven transaction and alternate inputs used to reach each validation stage. +struct ProvenTransactionFixture { + transaction: ProvenTransaction, + inputs: TransactionInputs, + execution_failure_inputs: TransactionInputs, + mismatch_inputs: TransactionInputs, +} + +/// Builds one real proof and two alternate, well-formed input sets. +async fn proven_transaction_fixture() -> &'static ProvenTransactionFixture { + static FIXTURE: OnceCell = OnceCell::const_new(); + + FIXTURE + .get_or_init(|| async { + let mut chain_builder = MockChainBuilder::new(); + let auth = Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + }; + let account_a = chain_builder.add_existing_wallet(auth.clone()).unwrap(); + let account_b = chain_builder.add_existing_wallet(auth).unwrap(); + assert_ne!(account_a.id(), account_b.id()); + + let asset: Asset = + FungibleAsset::new(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET.try_into().unwrap(), 100) + .unwrap() + .into(); + let note_a = chain_builder + .add_p2id_note( + ACCOUNT_ID_SENDER.try_into().unwrap(), + account_a.id(), + &[asset], + NoteType::Private, + ) + .unwrap(); + let note_b = chain_builder + .add_p2id_note( + ACCOUNT_ID_SENDER.try_into().unwrap(), + account_b.id(), + &[asset], + NoteType::Private, + ) + .unwrap(); + let chain = chain_builder.build().unwrap(); + + let context_a = chain + .build_tx_context(account_a.id(), &[note_a.id()], &[]) + .unwrap() + .build() + .unwrap(); + let executed_a = Box::pin(context_a.execute()).await.unwrap(); + let inputs = executed_a.tx_inputs().clone(); + let transaction = LocalTransactionProver::default().prove(inputs.clone()).unwrap(); + + let context_b = chain + .build_tx_context(account_b.id(), &[note_b.id()], &[]) + .unwrap() + .build() + .unwrap(); + let mismatch_inputs = Box::pin(context_b.execute()).await.unwrap().tx_inputs().clone(); + let mut execution_failure_inputs = inputs.clone(); + execution_failure_inputs.set_input_notes(vec![note_b]); + + ProvenTransactionFixture { + transaction, + inputs, + execution_failure_inputs, + mismatch_inputs, + } + }) + .await +} + // TESTS // ================================================================================================ @@ -998,6 +1113,7 @@ async fn submit_rejects_missing_encrypted_inputs() { assert_eq!(status.code(), tonic::Code::InvalidArgument); assert!(status.message().contains("Missing sealed transaction inputs")); + tv.assert_transaction_absent(tx.id(), 0).await; } /// Plaintext transaction inputs must be impossible to submit. This is the central guarantee of the @@ -1015,6 +1131,7 @@ async fn submit_rejects_plaintext_inputs() { assert_eq!(status.code(), tonic::Code::InvalidArgument); assert!(status.message().contains("unseal"), "got: {}", status.message()); + tv.assert_transaction_absent(tx.id(), 0).await; } /// A key id that does not match the validator's earns a distinct, actionable status so a client @@ -1041,6 +1158,7 @@ async fn submit_rejects_unknown_key_id() { !status.message().contains(&own_key_id), "the rejection must not echo the validator's key id", ); + tv.assert_transaction_absent(tx.id(), 0).await; } /// The validator enforces the associated data, so a ciphertext captured from one transaction cannot @@ -1059,6 +1177,7 @@ async fn submit_rejects_inputs_sealed_for_a_different_transaction() { assert_eq!(status.code(), tonic::Code::InvalidArgument); assert!(status.message().contains("unseal"), "got: {}", status.message()); + tv.assert_transaction_absent(tx_b.id(), 0).await; } /// Correctly sealed inputs get past the unseal and fail later, at deserialization. Without this the @@ -1082,4 +1201,101 @@ async fn correctly_sealed_inputs_reach_the_deserialization_stage() { "the unseal must have succeeded, got: {}", status.message(), ); + tv.assert_transaction_absent(tx.id(), 0).await; +} + +/// A failed proof must not store the authenticated transaction inputs. +#[tokio::test] +async fn failed_proof_verification_does_not_store_inputs() { + let tv = TestValidator::new().await; + let tx = dummy_proven_tx(11); + let fixture = proven_transaction_fixture().await; + let sealed = tv.seal(tx.id(), &fixture.inputs.to_bytes()); + + let status = tv.call_submit_proven_transaction(&tx, sealed).await.unwrap_err(); + + assert_eq!(status.code(), tonic::Code::InvalidArgument); + assert!(status.message().contains("proof verification"), "got: {}", status.message()); + tv.assert_transaction_absent(tx.id(), 0).await; +} + +/// A transaction that cannot be re-executed must not create a sealed record. +#[tokio::test] +async fn failed_reexecution_does_not_store_inputs() { + let tv = TestValidator::new().await; + let fixture = proven_transaction_fixture().await; + let tx = &fixture.transaction; + let sealed = tv.seal(tx.id(), &fixture.execution_failure_inputs.to_bytes()); + + let status = tv.call_submit_proven_transaction(tx, sealed).await.unwrap_err(); + + assert_eq!(status.code(), tonic::Code::InvalidArgument); + assert!(status.message().contains("re-executed"), "got: {}", status.message()); + tv.assert_transaction_absent(tx.id(), 0).await; +} + +/// A successful re-execution with a different header must not create a sealed record. +#[tokio::test] +async fn header_mismatch_does_not_store_inputs() { + let tv = TestValidator::new().await; + let fixture = proven_transaction_fixture().await; + let tx = &fixture.transaction; + let sealed = tv.seal(tx.id(), &fixture.mismatch_inputs.to_bytes()); + + let status = tv.call_submit_proven_transaction(tx, sealed).await.unwrap_err(); + + assert_eq!(status.code(), tonic::Code::InvalidArgument); + assert!(status.message().contains("did not match"), "got: {}", status.message()); + tv.assert_transaction_absent(tx.id(), 0).await; +} + +/// A valid submission stores its exact envelope and a duplicate cannot replace it. +#[tokio::test] +async fn valid_submission_stores_first_sealed_inputs() { + let tv = TestValidator::new().await; + let fixture = proven_transaction_fixture().await; + let tx = &fixture.transaction; + let first = tv.seal(tx.id(), &fixture.inputs.to_bytes()); + let second = tv.seal(tx.id(), &fixture.inputs.to_bytes()); + assert_ne!(first.ciphertext, second.ciphertext); + + tv.call_submit_proven_transaction(tx, first.clone()).await.unwrap(); + tv.call_submit_proven_transaction(tx, second).await.unwrap(); + + assert_eq!( + tv.load_transaction(tx.id()).await, + Some(ValidatedTransactionRecord { + transaction_id: tx.id(), + submission_scheme: TransactionEncryptionScheme::X25519XChaCha20Poly1305.as_u32(), + submission_key_id: first.key_id, + sealed_transaction_inputs: first.ciphertext, + }), + ); + assert_eq!(tv.validated_transaction_count().await, 1); + assert_eq!(tv.call_status().await.validated_transactions_count, 1); +} + +/// `ValidatorClient::submit_batch` forwards items through this handler one at a time. A failed item +/// must not create its own record when another item in that sequence succeeds. +#[tokio::test] +async fn failed_batch_item_does_not_store_inputs() { + let tv = TestValidator::new().await; + let fixture = proven_transaction_fixture().await; + let valid_tx = &fixture.transaction; + let rejected_tx = dummy_proven_tx(12); + + tv.call_submit_proven_transaction(valid_tx, tv.seal(valid_tx.id(), &fixture.inputs.to_bytes())) + .await + .unwrap(); + let status = tv + .call_submit_proven_transaction( + &rejected_tx, + tv.seal(rejected_tx.id(), &fixture.inputs.to_bytes()), + ) + .await + .unwrap_err(); + + assert_eq!(status.code(), tonic::Code::InvalidArgument); + tv.assert_transaction_absent(rejected_tx.id(), 1).await; + assert!(tv.load_transaction(valid_tx.id()).await.is_some()); } diff --git a/bin/validator/src/tx_validation/mod.rs b/bin/validator/src/tx_validation/mod.rs index 5671bc44f9..dabd0cd929 100644 --- a/bin/validator/src/tx_validation/mod.rs +++ b/bin/validator/src/tx_validation/mod.rs @@ -1,5 +1,4 @@ mod data_store; -mod validated_tx; pub use data_store::TransactionInputsDataStore; use miden_node_utils::spawn::{spawn_blocking_in_current_span, spawn_blocking_in_span}; @@ -15,7 +14,6 @@ use miden_protocol::transaction::{ use miden_tx::auth::UnreachableAuth; use miden_tx::{TransactionExecutor, TransactionExecutorError}; use tracing::{Instrument, info_span}; -pub use validated_tx::ValidatedTransaction; use crate::COMPONENT; @@ -41,7 +39,6 @@ pub enum TransactionValidationError { /// Validates a transaction by verifying its proof, executing it and comparing its header with the /// provided proven transaction. /// -/// Returns the header of the executed transaction if successful. #[miden_instrument( target = COMPONENT, skip_all, @@ -50,7 +47,7 @@ pub enum TransactionValidationError { pub async fn validate_transaction( proven_tx: ProvenTransaction, tx_inputs: TransactionInputs, -) -> Result { +) -> Result<(), TransactionValidationError> { // Proof verification is CPU-intensive; run it on a dedicated blocking thread. let proven_tx_clone = proven_tx.clone(); spawn_blocking_in_span( @@ -90,7 +87,7 @@ pub async fn validate_transaction( let executed_tx_header: TransactionHeader = (&executed_tx).into(); let proven_tx_header: TransactionHeader = (&proven_tx).into(); if executed_tx_header == proven_tx_header { - Ok(ValidatedTransaction::new(executed_tx)) + Ok(()) } else { Err(TransactionValidationError::Mismatch { proven_tx_header: proven_tx_header.into(), diff --git a/bin/validator/src/tx_validation/validated_tx.rs b/bin/validator/src/tx_validation/validated_tx.rs deleted file mode 100644 index 5fd52217d6..0000000000 --- a/bin/validator/src/tx_validation/validated_tx.rs +++ /dev/null @@ -1,64 +0,0 @@ -use miden_protocol::Word; -use miden_protocol::account::{AccountId, AccountPatch}; -use miden_protocol::block::BlockNumber; -use miden_protocol::transaction::{ - ExecutedTransaction, - InputNote, - InputNotes, - RawOutputNotes, - TransactionId, -}; - -/// Re-executed and validated transaction that the Validator, or some ad-hoc -/// auditing procedure, might need to analyze. -/// -/// Constructed from an [`ExecutedTransaction`] that the Validator would have created while -/// re-executing and validating a [`miden_protocol::transaction::ProvenTransaction`]. -pub struct ValidatedTransaction(ExecutedTransaction); - -impl ValidatedTransaction { - /// Creates a new instance of [`ValidatedTransaction`]. - pub fn new(tx: ExecutedTransaction) -> Self { - Self(tx) - } - - /// Returns ID of the transaction. - pub fn tx_id(&self) -> TransactionId { - self.0.id() - } - - /// Returns the block number in which the transaction was executed. - pub fn block_num(&self) -> BlockNumber { - self.0.block_header().block_num() - } - - /// Returns ID of the account against which this transaction was executed. - pub fn account_id(&self) -> AccountId { - self.0.account_id() - } - - /// Returns a description of changes between the initial and final account states. - pub fn account_patch(&self) -> &AccountPatch { - self.0.account_patch() - } - - /// Returns the notes consumed in this transaction. - pub fn input_notes(&self) -> &InputNotes { - self.0.input_notes() - } - - /// Returns the notes created in this transaction. - pub fn output_notes(&self) -> &RawOutputNotes { - self.0.output_notes() - } - - /// Returns the commitment of the initial account state. - pub fn initial_account_hash(&self) -> Word { - self.0.initial_account().initial_commitment() - } - - /// Returns the commitment of the final account state. - pub fn final_account_hash(&self) -> Word { - self.0.final_account().to_commitment() - } -} diff --git a/docs/external/src/network-operator/validator.md b/docs/external/src/network-operator/validator.md index 930b34489a..5f45d433bd 100644 --- a/docs/external/src/network-operator/validator.md +++ b/docs/external/src/network-operator/validator.md @@ -51,4 +51,11 @@ so its AWS identity needs that permission on the wrapping key. Note that, unlike encryption key is held in validator memory: AWS KMS cannot perform X25519 key agreement itself, so envelope encryption is the supported provisioning path. +The validator stores accepted private inputs under this shared key. Any holder of the shared secret can decrypt those +records. Run each validator inside its trusted execution environment. If transaction proving uses a remote prover, that +prover also receives the plaintext inputs and must run inside the same trusted boundary. + +The sealed-input storage migration cannot recover ciphertext for older validated rows. Bootstrap a fresh validator +database before deploying a build with this schema. + Use `miden-validator start --help` for the complete current option list. diff --git a/docs/internal/src/validator.md b/docs/internal/src/validator.md index c4132bc199..54466db6f9 100644 --- a/docs/internal/src/validator.md +++ b/docs/internal/src/validator.md @@ -57,3 +57,8 @@ mismatched `key_id` cannot influence which key is tried: it only lets the valida The unseal happens before the serve lock is taken, so a slow or hung decrypt backend cannot starve the exclusive lock that a backup block subscription needs. The cost is that an already-validated resubmission pays for the unseal before being short-circuited. + +After the proof, re-execution, and header checks pass, the validator stores the exact sealed input +envelope with its scheme and key ID. It does not store the plaintext or fields derived from it. This +is a Phase 1 stand-in. Phase 2 will store the validated inputs under threshold encryption instead. +A rejected transaction never creates a record.