Skip to content

Commit 58e52f8

Browse files
committed
Write validator definitions atomically (#2338)
## Issue Addressed Closes #2159 ## Proposed Changes Rather than trying to write the validator definitions to disk directly, use a temporary file called `.validator_defintions.yml.tmp` and then atomically rename it to `validator_definitions.yml`. This avoids truncating the primary file, which can cause permanent damage when the disk is full. The same treatment is also applied to the validator key cache, although the situation is less dire if it becomes corrupted because it can just be deleted without the user having to reimport keys or resupply passwords. ## Additional Info * `File::create` truncates upon opening: https://doc.rust-lang.org/std/fs/struct.File.html#method.create * `fs::rename` uses `rename` on UNIX and `MoveFileEx` on Windows: https://doc.rust-lang.org/std/fs/fn.rename.html * UNIX `rename` call is atomic: https://unix.stackexchange.com/questions/322038/is-mv-atomic-on-my-fs * Windows `MoveFileEx` is _not_ atomic in general, and Windows lacks any clear API for atomic file renames :( https://stackoverflow.com/questions/167414/is-an-atomic-file-rename-with-overwrite-possible-on-windows ## Further Work * Consider whether we want to try a different Windows syscall as part of #2333. The `rust-atomicwrites` crate seems promising, but actually uses the same syscall under the hood presently: untitaker/rust-atomicwrites#27.
1 parent 480b247 commit 58e52f8

File tree

3 files changed

+57
-25
lines changed

3 files changed

+57
-25
lines changed

common/account_utils/src/lib.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,32 @@ pub fn create_with_600_perms<P: AsRef<Path>>(path: P, bytes: &[u8]) -> Result<()
7676
Ok(())
7777
}
7878

79+
/// Write a file atomically by using a temporary file as an intermediate.
80+
///
81+
/// Care is taken to preserve the permissions of the file at `file_path` being written.
82+
///
83+
/// If no file exists at `file_path` one will be created with restricted 0o600-equivalent
84+
/// permissions.
85+
pub fn write_file_via_temporary(
86+
file_path: &Path,
87+
temp_path: &Path,
88+
bytes: &[u8],
89+
) -> Result<(), io::Error> {
90+
// If the file already exists, preserve its permissions by copying it.
91+
// Otherwise, create a new file with restricted permissions.
92+
if file_path.exists() {
93+
fs::copy(&file_path, &temp_path)?;
94+
fs::write(&temp_path, &bytes)?;
95+
} else {
96+
create_with_600_perms(&temp_path, &bytes)?;
97+
}
98+
99+
// With the temporary file created, perform an atomic rename.
100+
fs::rename(&temp_path, &file_path)?;
101+
102+
Ok(())
103+
}
104+
79105
/// Generates a random alphanumeric password of length `DEFAULT_PASSWORD_LEN`.
80106
pub fn random_password() -> PlainText {
81107
rand::thread_rng()

common/account_utils/src/validator_definitions.rs

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//! Serves as the source-of-truth of which validators this validator client should attempt (or not
44
//! attempt) to load into the `crate::intialized_validators::InitializedValidators` struct.
55
6-
use crate::{create_with_600_perms, default_keystore_password_path, ZeroizeString};
6+
use crate::{default_keystore_password_path, write_file_via_temporary, ZeroizeString};
77
use directory::ensure_dir_exists;
88
use eth2_keystore::Keystore;
99
use regex::Regex;
@@ -19,6 +19,12 @@ use validator_dir::VOTING_KEYSTORE_FILE;
1919
/// The file name for the serialized `ValidatorDefinitions` struct.
2020
pub const CONFIG_FILENAME: &str = "validator_definitions.yml";
2121

22+
/// The temporary file name for the serialized `ValidatorDefinitions` struct.
23+
///
24+
/// This is used to achieve an atomic update of the contents on disk, without truncation.
25+
/// See: https://github.com/sigp/lighthouse/issues/2159
26+
pub const CONFIG_TEMP_FILENAME: &str = ".validator_definitions.yml.tmp";
27+
2228
#[derive(Debug)]
2329
pub enum Error {
2430
/// The config file could not be opened.
@@ -29,7 +35,7 @@ pub enum Error {
2935
UnableToSearchForKeystores(io::Error),
3036
/// The config file could not be serialized as YAML.
3137
UnableToEncodeFile(serde_yaml::Error),
32-
/// The config file could not be written to the filesystem.
38+
/// The config file or temp file could not be written to the filesystem.
3339
UnableToWriteFile(io::Error),
3440
/// The public key from the keystore is invalid.
3541
InvalidKeystorePubkey,
@@ -249,19 +255,19 @@ impl ValidatorDefinitions {
249255
Ok(new_defs_count)
250256
}
251257

252-
/// Encodes `self` as a YAML string it writes it to the `CONFIG_FILENAME` file in the
253-
/// `validators_dir` directory.
258+
/// Encodes `self` as a YAML string and atomically writes it to the `CONFIG_FILENAME` file in
259+
/// the `validators_dir` directory.
254260
///
255-
/// Will create a new file if it does not exist or over-write any existing file.
261+
/// Will create a new file if it does not exist or overwrite any existing file.
256262
pub fn save<P: AsRef<Path>>(&self, validators_dir: P) -> Result<(), Error> {
257263
let config_path = validators_dir.as_ref().join(CONFIG_FILENAME);
264+
let temp_path = validators_dir.as_ref().join(CONFIG_TEMP_FILENAME);
258265
let bytes = serde_yaml::to_vec(self).map_err(Error::UnableToEncodeFile)?;
259266

260-
if config_path.exists() {
261-
fs::write(config_path, &bytes).map_err(Error::UnableToWriteFile)
262-
} else {
263-
create_with_600_perms(&config_path, &bytes).map_err(Error::UnableToWriteFile)
264-
}
267+
write_file_via_temporary(&config_path, &temp_path, &bytes)
268+
.map_err(Error::UnableToWriteFile)?;
269+
270+
Ok(())
265271
}
266272

267273
/// Adds a new `ValidatorDefinition` to `self`.
@@ -392,7 +398,7 @@ mod tests {
392398

393399
#[test]
394400
fn graffiti_checks() {
395-
let no_graffiti = r#"---
401+
let no_graffiti = r#"---
396402
description: ""
397403
enabled: true
398404
type: local_keystore
@@ -402,7 +408,7 @@ mod tests {
402408
let def: ValidatorDefinition = serde_yaml::from_str(&no_graffiti).unwrap();
403409
assert!(def.graffiti.is_none());
404410

405-
let invalid_graffiti = r#"---
411+
let invalid_graffiti = r#"---
406412
description: ""
407413
enabled: true
408414
type: local_keystore
@@ -414,7 +420,7 @@ mod tests {
414420
let def: Result<ValidatorDefinition, _> = serde_yaml::from_str(&invalid_graffiti);
415421
assert!(def.is_err());
416422

417-
let valid_graffiti = r#"---
423+
let valid_graffiti = r#"---
418424
description: ""
419425
enabled: true
420426
type: local_keystore

validator_client/src/key_cache.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use account_utils::create_with_600_perms;
1+
use account_utils::write_file_via_temporary;
22
use bls::{Keypair, PublicKey};
33
use eth2_keystore::json_keystore::{
44
Aes128Ctr, ChecksumModule, Cipher, CipherModule, Crypto, EmptyMap, EmptyString, KdfModule,
@@ -12,12 +12,15 @@ use rand::prelude::*;
1212
use serde::{Deserialize, Serialize};
1313
use std::collections::HashMap;
1414
use std::fs::OpenOptions;
15+
use std::io;
1516
use std::path::{Path, PathBuf};
16-
use std::{fs, io};
1717

1818
/// The file name for the serialized `KeyCache` struct.
1919
pub const CACHE_FILENAME: &str = "validator_key_cache.json";
2020

21+
/// The file name for the temporary `KeyCache`.
22+
pub const TEMP_CACHE_FILENAME: &str = ".validator_key_cache.json.tmp";
23+
2124
#[derive(Debug, Copy, Clone, PartialEq)]
2225
pub enum State {
2326
NotDecrypted,
@@ -139,17 +142,14 @@ impl KeyCache {
139142
self.encrypt()?;
140143

141144
let cache_path = validators_dir.as_ref().join(CACHE_FILENAME);
145+
let temp_path = validators_dir.as_ref().join(TEMP_CACHE_FILENAME);
142146
let bytes = serde_json::to_vec(self).map_err(Error::UnableToEncodeFile)?;
143147

144-
let res = if cache_path.exists() {
145-
fs::write(cache_path, &bytes).map_err(Error::UnableToWriteFile)
146-
} else {
147-
create_with_600_perms(&cache_path, &bytes).map_err(Error::UnableToWriteFile)
148-
};
149-
if res.is_ok() {
150-
self.state = State::DecryptedAndSaved;
151-
}
152-
res.map(|_| true)
148+
write_file_via_temporary(&cache_path, &temp_path, &bytes)
149+
.map_err(Error::UnableToWriteFile)?;
150+
151+
self.state = State::DecryptedAndSaved;
152+
Ok(true)
153153
} else {
154154
Ok(false)
155155
}
@@ -243,7 +243,7 @@ pub enum Error {
243243
UnableToParseFile(serde_json::Error),
244244
/// The cache file could not be serialized as YAML.
245245
UnableToEncodeFile(serde_json::Error),
246-
/// The cache file could not be written to the filesystem.
246+
/// The cache file or its temporary could not be written to the filesystem.
247247
UnableToWriteFile(io::Error),
248248
/// Couldn't decrypt the cache file
249249
UnableToDecrypt(KeystoreError),

0 commit comments

Comments
 (0)