Skip to content
Merged
347 changes: 272 additions & 75 deletions bin/validator/src/commands/mod.rs

Large diffs are not rendered by default.

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(
"c8fc914e8109e47844744acea6a590dc3364acc7cc489205143bc0ee69b54520",
"100025e3daa05c2f7d5be2dc6ff096dbe916f1af4d95ae27cdfb2e23d6f0723a",
)];

#[test]
Expand Down
8 changes: 0 additions & 8 deletions bin/validator/src/db/migrations/001_initial.sql
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,3 @@ CREATE TABLE block_headers (
block_num BIGINT PRIMARY KEY,
block_header BLOB NOT NULL
) WITHOUT ROWID;

CREATE TABLE encryption_keys (
epoch BIGINT PRIMARY KEY,
scheme BIGINT NOT NULL,
key_id BLOB NOT NULL,
public_key BLOB NOT NULL,
secret_key BLOB NOT NULL
) WITHOUT ROWID;
135 changes: 0 additions & 135 deletions bin/validator/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,6 @@ mod sql {
pub(super) const COUNT_VALIDATED_TRANSACTIONS: &str =
include_str!("sql/count_validated_transactions.sql");
pub(super) const COUNT_SIGNED_BLOCKS: &str = include_str!("sql/count_signed_blocks.sql");
pub(super) const INSERT_ENCRYPTION_KEY: &str = include_str!("sql/insert_encryption_key.sql");
pub(super) const MAX_ARCHIVED_ENCRYPTION_KEY_EPOCH: &str =
include_str!("sql/max_archived_encryption_key_epoch.sql");
#[cfg(test)]
pub(super) const LOAD_ENCRYPTION_KEY: &str = include_str!("sql/load_encryption_key.sql");
}

/// Open a connection to the DB after verifying that it is at the latest schema version.
Expand Down Expand Up @@ -266,93 +261,6 @@ pub fn count_signed_blocks(tx: &ReadTx<'_>) -> Result<i64, DatabaseError> {
.unwrap_or(0))
}

/// A transaction encryption key of one epoch, as archived in the database.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ArchivedEncryptionKey {
/// Wire identifier of the encryption scheme.
pub scheme: u32,
/// Opaque identifier of the encryption key.
pub key_id: Vec<u8>,
/// Raw public key bytes of the encryption key.
pub public_key: Vec<u8>,
/// Raw secret key bytes of the encryption key.
pub secret_key: Vec<u8>,
}

/// Archives one epoch's encryption key.
///
/// A key is immutable once derived, so a row that already exists for the epoch is left
/// untouched.
#[miden_instrument(
target = COMPONENT,
skip(tx, key),
err,
)]
pub(crate) fn insert_encryption_key(
tx: &WriteTx<'_>,
epoch: u16,
key: &ArchivedEncryptionKey,
) -> Result<(), DatabaseError> {
tx.execute(
sql::INSERT_ENCRYPTION_KEY,
&[
&i64::from(epoch),
&i64::from(key.scheme),
&key.key_id,
&key.public_key,
&key.secret_key,
],
)?;
Ok(())
}

/// Returns the highest epoch whose encryption key has been archived, or `None` when the archive is
/// empty.
#[miden_instrument(
target = COMPONENT,
skip(tx),
err,
)]
pub(crate) fn max_archived_encryption_key_epoch(
tx: &ReadTx<'_>,
) -> Result<Option<u16>, DatabaseError> {
tx.query(sql::MAX_ARCHIVED_ENCRYPTION_KEY_EPOCH, &[], |row| row.get::<Option<i64>>(0))?
.into_iter()
.next()
.flatten()
.map(|epoch| {
u16::try_from(epoch).map_err(|err| {
DatabaseError::deserialization("archived epoch out of the u16 range", err)
})
})
.transpose()
}

/// Loads the archived encryption key of the given epoch.
///
/// Returns `None` if no key has been archived for the epoch.
///
/// Test-only until an archive recovery path consumes it.
#[cfg(test)]
pub(crate) fn load_encryption_key(
tx: &ReadTx<'_>,
epoch: u16,
) -> Result<Option<ArchivedEncryptionKey>, DatabaseError> {
Ok(tx
.query(sql::LOAD_ENCRYPTION_KEY, &[&i64::from(epoch)], |row| {
Ok(ArchivedEncryptionKey {
scheme: u32::try_from(row.get::<i64>(0)?).map_err(|err| {
DatabaseError::deserialization("archived scheme out of the u32 range", err)
})?,
key_id: row.get(1)?,
public_key: row.get(2)?,
secret_key: row.get(3)?,
})
})?
.into_iter()
.next())
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -415,47 +323,4 @@ mod tests {
.unwrap();
assert!(!unknown_exists, "an unknown transaction id should not be reported as existing");
}

fn test_archived_key(marker: u8) -> ArchivedEncryptionKey {
ArchivedEncryptionKey {
scheme: 1,
key_id: vec![marker; 4],
public_key: vec![marker; 32],
secret_key: vec![marker; 32],
}
}

/// Archived keys round-trip, the max archived epoch tracks inserts, and re-inserting an epoch
/// leaves the original row untouched.
#[tokio::test]
async fn encryption_key_archive_roundtrip() {
let temp_dir = tempfile::tempdir().expect("failed to create temp directory");
let db = setup(temp_dir.path().join("validator.sqlite3")).await.unwrap();

// The archive starts empty.
let max = db.read("max_epoch", max_archived_encryption_key_epoch).await.unwrap();
assert_eq!(max, None);
let missing = db.read("load_key", |tx| load_encryption_key(tx, 0)).await.unwrap();
assert_eq!(missing, None);

// Insert two epochs and read them back.
for (epoch, marker) in [(0u16, 7u8), (3u16, 9u8)] {
let key = test_archived_key(marker);
db.write("insert_key", move |tx| insert_encryption_key(tx, epoch, &key))
.await
.unwrap();
}
let max = db.read("max_epoch", max_archived_encryption_key_epoch).await.unwrap();
assert_eq!(max, Some(3));
let loaded = db.read("load_key", |tx| load_encryption_key(tx, 3)).await.unwrap();
assert_eq!(loaded, Some(test_archived_key(9)));

// Re-inserting an archived epoch must not overwrite the existing row.
let conflicting = test_archived_key(5);
db.write("insert_key", move |tx| insert_encryption_key(tx, 3, &conflicting))
.await
.unwrap();
let loaded = db.read("load_key", |tx| load_encryption_key(tx, 3)).await.unwrap();
assert_eq!(loaded, Some(test_archived_key(9)), "archived keys must be immutable");
}
}
2 changes: 0 additions & 2 deletions bin/validator/src/db/sql/insert_encryption_key.sql

This file was deleted.

1 change: 0 additions & 1 deletion bin/validator/src/db/sql/load_encryption_key.sql

This file was deleted.

This file was deleted.

6 changes: 3 additions & 3 deletions bin/validator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ mod tx_validation;
pub use data_directory::DataDirectory;
pub use server::ValidatorServer;
pub use signers::{
EncryptionKeySet,
KmsSigner,
LocalX25519TransactionInputDecrypter,
NextEncryptionKeyInfo,
NextTransactionEncryptionKey,
TransactionEncryptionKeyInfo,
TransactionEncryptionKeySchedule,
TransactionInputDecrypter,
TransactionInputDecryptionError,
ValidatorSigner,
attestation_commitment,
decrypt_key_material,
};

Expand Down
4 changes: 0 additions & 4 deletions bin/validator/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,6 @@ impl ValidatorServer {
.await
.context("failed to initialize validator server")?;

// Rotate and re-attest the transaction encryption key as the chain crosses epoch
// boundaries. The task follows the committed tip and stops on shutdown.
service.spawn_key_rotation_task(shutdown.clone());

// Build the gRPC server with the API service and trace layer.
tonic::transport::Server::builder()
.layer(CatchPanicLayer::custom(catch_panic_layer_fn))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,47 +33,43 @@ impl grpc::server::validator_api::GetTransactionEncryptionKey for ValidatorServi
_metadata: &tonic::metadata::MetadataMap,
_extensions: &tonic::codegen::http::Extensions,
) -> tonic::Result<Self::Output> {
// Built entirely from in-memory attested state, so the endpoint stays available while a
// backup subscription holds the serve lock.
let attested = self.attested_encryption_keys();
// The schedule is cached in memory and re-attested lazily at most once per epoch, so this
// endpoint remains independent of the backup serve lock.
let attested = self
.attested_encryption_key_schedule()
.await
.map_err(|err| tonic::Status::failed_precondition(err.to_string()))?;
let validator_public_key = self.signer.public_key().to_bytes();

let current_key = encode_key(
&attested.keys.current,
&validator_public_key,
&attested.current_attestation.to_bytes(),
);
let next_key = attested.keys.next.as_ref().map(|next| {
let attestation = attested
.next_attestation
.as_ref()
.expect("a next key is always attested together with the current key");
let current_key = encode_key(&attested.schedule.current_key);
let next_key = attested.schedule.next_key.as_ref().map(|next| {
grpc::transaction::NextTransactionEncryptionKey {
key: Some(encode_key(&next.key, &validator_public_key, &attestation.to_bytes())),
rotation_block_num: next.rotation_block_num,
key: Some(encode_key(&next.key)),
activation_block_num: next.activation_block_num.as_u32(),
}
});

Ok(grpc::transaction::TransactionEncryptionKeyResponse {
current_key: Some(current_key),
next_key,
current_key_activation_block_num: attested
.schedule
.current_key_activation_block_num
.as_u32(),
attestation_epoch: u32::from(attested.epoch),
attestations: vec![grpc::transaction::ValidatorKeyAttestation {
validator_public_key,
signature: attested.attestation.to_bytes(),
}],
})
}
}

/// Encodes one attested encryption key in wire format.
fn encode_key(
key: &TransactionEncryptionKeyInfo,
validator_public_key: &[u8],
signature: &[u8],
) -> grpc::transaction::TransactionEncryptionKey {
/// Encodes one encryption key in wire format.
fn encode_key(key: &TransactionEncryptionKeyInfo) -> grpc::transaction::TransactionEncryptionKey {
grpc::transaction::TransactionEncryptionKey {
scheme: i32::try_from(key.scheme).expect("scheme identifier must fit in i32"),
key_id: key.key_id.clone(),
public_key: key.public_key.clone(),
attestations: vec![grpc::transaction::ValidatorKeyAttestation {
validator_public_key: validator_public_key.to_vec(),
signature: signature.to_vec(),
}],
}
}
Loading
Loading