mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-14 18:32:42 +00:00
Implement standard keystore API (#2736)
## Issue Addressed Implements the standard key manager API from https://ethereum.github.io/keymanager-APIs/, formerly https://github.com/ethereum/beacon-APIs/pull/151 Related to https://github.com/sigp/lighthouse/issues/2557 ## Proposed Changes - [x] Add all of the new endpoints from the standard API: GET, POST and DELETE. - [x] Add a `validators.enabled` column to the slashing protection database to support atomic disable + export. - [x] Add tests for all the common sequential accesses of the API - [x] Add tests for interactions with remote signer validators - [x] Add end-to-end tests for migration of validators from one VC to another - [x] Implement the authentication scheme from the standard (token bearer auth) ## Additional Info The `enabled` column in the validators SQL database is necessary to prevent a race condition when exporting slashing protection data. Without the slashing protection database having a way of knowing that a key has been disabled, a concurrent request to sign a message could insert a new record into the database. The `delete_concurrent_with_signing` test exercises this code path, and was indeed failing before the `enabled` column was added. The validator client authentication has been modified from basic auth to bearer auth, with basic auth preserved for backwards compatibility.
This commit is contained in:
@@ -28,6 +28,9 @@ pub const CONNECTION_TIMEOUT: Duration = Duration::from_millis(100);
|
||||
/// Supported version of the interchange format.
|
||||
pub const SUPPORTED_INTERCHANGE_FORMAT_VERSION: u64 = 5;
|
||||
|
||||
/// Column ID of the `validators.enabled` column.
|
||||
pub const VALIDATORS_ENABLED_CID: i64 = 2;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SlashingDatabase {
|
||||
conn_pool: Pool,
|
||||
@@ -55,7 +58,7 @@ impl SlashingDatabase {
|
||||
|
||||
restrict_file_permissions(path).map_err(|_| NotSafe::PermissionsError)?;
|
||||
let conn_pool = Self::open_conn_pool(path)?;
|
||||
let conn = conn_pool.get()?;
|
||||
let mut conn = conn_pool.get()?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE validators (
|
||||
@@ -88,13 +91,55 @@ impl SlashingDatabase {
|
||||
params![],
|
||||
)?;
|
||||
|
||||
// The tables created above are for the v0 schema. We immediately update them
|
||||
// to the latest schema without dropping the connection.
|
||||
let txn = conn.transaction()?;
|
||||
Self::apply_schema_migrations(&txn)?;
|
||||
txn.commit()?;
|
||||
|
||||
Ok(Self { conn_pool })
|
||||
}
|
||||
|
||||
/// Open an existing `SlashingDatabase` from disk.
|
||||
///
|
||||
/// This will automatically check for and apply the latest schema migrations.
|
||||
pub fn open(path: &Path) -> Result<Self, NotSafe> {
|
||||
let conn_pool = Self::open_conn_pool(path)?;
|
||||
Ok(Self { conn_pool })
|
||||
let db = Self { conn_pool };
|
||||
db.with_transaction(Self::apply_schema_migrations)?;
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
fn apply_schema_migrations(txn: &Transaction) -> Result<(), NotSafe> {
|
||||
// Add the `enabled` column to the `validators` table if it does not already exist.
|
||||
let enabled_col_exists = txn
|
||||
.query_row(
|
||||
"SELECT cid, name FROM pragma_table_info('validators') WHERE name = 'enabled'",
|
||||
params![],
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)
|
||||
.optional()?
|
||||
.map(|(cid, name): (i64, String)| {
|
||||
// Check that the enabled column is in the correct position with the right name.
|
||||
// This is a defensive check that shouldn't do anything in practice unless the
|
||||
// slashing DB has been manually edited.
|
||||
if cid == VALIDATORS_ENABLED_CID && name == "enabled" {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(NotSafe::ConsistencyError)
|
||||
}
|
||||
})
|
||||
.transpose()?
|
||||
.is_some();
|
||||
|
||||
if !enabled_col_exists {
|
||||
txn.execute(
|
||||
"ALTER TABLE validators ADD COLUMN enabled BOOL NOT NULL DEFAULT TRUE",
|
||||
params![],
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Open a new connection pool with all of the necessary settings and tweaks.
|
||||
@@ -166,15 +211,37 @@ impl SlashingDatabase {
|
||||
public_keys: impl Iterator<Item = &'a PublicKeyBytes>,
|
||||
txn: &Transaction,
|
||||
) -> Result<(), NotSafe> {
|
||||
let mut stmt = txn.prepare("INSERT INTO validators (public_key) VALUES (?1)")?;
|
||||
let mut stmt =
|
||||
txn.prepare("INSERT INTO validators (public_key, enabled) VALUES (?1, TRUE)")?;
|
||||
for pubkey in public_keys {
|
||||
if self.get_validator_id_opt(txn, pubkey)?.is_none() {
|
||||
stmt.execute([pubkey.as_hex_string()])?;
|
||||
match self.get_validator_id_with_status(txn, pubkey)? {
|
||||
None => {
|
||||
stmt.execute([pubkey.as_hex_string()])?;
|
||||
}
|
||||
Some((validator_id, false)) => {
|
||||
self.update_validator_status(txn, validator_id, true)?;
|
||||
}
|
||||
Some((_, true)) => {
|
||||
// Validator already registered and enabled.
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_validator_status(
|
||||
&self,
|
||||
txn: &Transaction,
|
||||
validator_id: i64,
|
||||
status: bool,
|
||||
) -> Result<(), NotSafe> {
|
||||
txn.execute(
|
||||
"UPDATE validators SET enabled = ? WHERE id = ?",
|
||||
params![status, validator_id],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check that all of the given validators are registered.
|
||||
pub fn check_validator_registrations<'a>(
|
||||
&self,
|
||||
@@ -203,7 +270,7 @@ impl SlashingDatabase {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get the database-internal ID for a validator.
|
||||
/// Get the database-internal ID for an enabled validator.
|
||||
///
|
||||
/// This is NOT the same as a validator index, and depends on the ordering that validators
|
||||
/// are registered with the slashing protection database (and may vary between machines).
|
||||
@@ -213,26 +280,43 @@ impl SlashingDatabase {
|
||||
self.get_validator_id_in_txn(&txn, public_key)
|
||||
}
|
||||
|
||||
fn get_validator_id_in_txn(
|
||||
pub fn get_validator_id_in_txn(
|
||||
&self,
|
||||
txn: &Transaction,
|
||||
public_key: &PublicKeyBytes,
|
||||
) -> Result<i64, NotSafe> {
|
||||
self.get_validator_id_opt(txn, public_key)?
|
||||
.ok_or_else(|| NotSafe::UnregisteredValidator(*public_key))
|
||||
let (validator_id, enabled) = self
|
||||
.get_validator_id_with_status(txn, public_key)?
|
||||
.ok_or_else(|| NotSafe::UnregisteredValidator(*public_key))?;
|
||||
if enabled {
|
||||
Ok(validator_id)
|
||||
} else {
|
||||
Err(NotSafe::DisabledValidator(*public_key))
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional version of `get_validator_id`.
|
||||
fn get_validator_id_opt(
|
||||
/// Get validator ID regardless of whether or not it is enabled.
|
||||
pub fn get_validator_id_ignoring_status(
|
||||
&self,
|
||||
txn: &Transaction,
|
||||
public_key: &PublicKeyBytes,
|
||||
) -> Result<Option<i64>, NotSafe> {
|
||||
) -> Result<i64, NotSafe> {
|
||||
let (validator_id, _) = self
|
||||
.get_validator_id_with_status(txn, public_key)?
|
||||
.ok_or_else(|| NotSafe::UnregisteredValidator(*public_key))?;
|
||||
Ok(validator_id)
|
||||
}
|
||||
|
||||
pub fn get_validator_id_with_status(
|
||||
&self,
|
||||
txn: &Transaction,
|
||||
public_key: &PublicKeyBytes,
|
||||
) -> Result<Option<(i64, bool)>, NotSafe> {
|
||||
Ok(txn
|
||||
.query_row(
|
||||
"SELECT id FROM validators WHERE public_key = ?1",
|
||||
"SELECT id, enabled FROM validators WHERE public_key = ?1",
|
||||
params![&public_key.as_hex_string()],
|
||||
|row| row.get(0),
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)
|
||||
.optional()?)
|
||||
}
|
||||
@@ -722,13 +806,21 @@ impl SlashingDatabase {
|
||||
) -> Result<Interchange, InterchangeError> {
|
||||
let mut conn = self.conn_pool.get()?;
|
||||
let txn = &conn.transaction()?;
|
||||
self.export_interchange_info_in_txn(genesis_validators_root, selected_pubkeys, txn)
|
||||
}
|
||||
|
||||
pub fn export_interchange_info_in_txn(
|
||||
&self,
|
||||
genesis_validators_root: Hash256,
|
||||
selected_pubkeys: Option<&[PublicKeyBytes]>,
|
||||
txn: &Transaction,
|
||||
) -> Result<Interchange, InterchangeError> {
|
||||
// Determine the validator IDs and public keys to export data for.
|
||||
let to_export = if let Some(selected_pubkeys) = selected_pubkeys {
|
||||
selected_pubkeys
|
||||
.iter()
|
||||
.map(|pubkey| {
|
||||
let id = self.get_validator_id_in_txn(txn, pubkey)?;
|
||||
let id = self.get_validator_id_ignoring_status(txn, pubkey)?;
|
||||
Ok((id, *pubkey))
|
||||
})
|
||||
.collect::<Result<_, InterchangeError>>()?
|
||||
@@ -1089,7 +1181,6 @@ impl From<serde_json::Error> for InterchangeError {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::pubkey;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
@@ -1106,8 +1197,7 @@ mod tests {
|
||||
let file = dir.path().join("db.sqlite");
|
||||
let _db1 = SlashingDatabase::create(&file).unwrap();
|
||||
|
||||
let db2 = SlashingDatabase::open(&file).unwrap();
|
||||
db2.register_validator(pubkey(0)).unwrap_err();
|
||||
SlashingDatabase::open(&file).unwrap_err();
|
||||
}
|
||||
|
||||
// Attempting to create the same database twice should error.
|
||||
@@ -1152,9 +1242,12 @@ mod tests {
|
||||
fn test_transaction_failure() {
|
||||
let dir = tempdir().unwrap();
|
||||
let file = dir.path().join("db.sqlite");
|
||||
let _db1 = SlashingDatabase::create(&file).unwrap();
|
||||
let db = SlashingDatabase::create(&file).unwrap();
|
||||
|
||||
let db2 = SlashingDatabase::open(&file).unwrap();
|
||||
db2.test_transaction().unwrap_err();
|
||||
db.with_transaction(|_| {
|
||||
db.test_transaction().unwrap_err();
|
||||
Ok::<(), NotSafe>(())
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user