Skip to content

Commit

Permalink
Generalize client storage from the filesystem (#2194)
Browse files Browse the repository at this point in the history
* `linera-core`: add `Persistent` trait

* `linera-service`: use new `Persistent` trait

* `linera-service`: don't forget to create the file for `persistent::File::new`

* `linera-service`: refresh PRNG seed more to align with tests

* `linera-service`: rename `Persistent` to `Persist`

This is more in line both with Rust standard library traits, which
tend to be transitive verbs, and also with the Rust trait naming
conventions RFC rust-lang/rfcs#344, which states:

> Prefer (transitive) verbs, nouns, and then adjectives; avoid
> grammatical suffixes (like able).

* `linera-service`: document `Persist` trait

* `linera-service::persistent::File`: when an error occurs during error handling, do not hide the outer error

* `linera_service::persistent`: single-space documentation

Co-authored-by: Andreas Fackler <[email protected]>
Signed-off-by: James Kay <[email protected]>

* `linera-service`: mention bug #2053

* `linera_service::proxy`: replace `expect` with `?`

* `linera_service::persistent`: use the indicative mood for function documentation

---------

Signed-off-by: James Kay <[email protected]>
Co-authored-by: Andreas Fackler <[email protected]>
  • Loading branch information
Twey and afck authored Jul 3, 2024
1 parent 53d6bd3 commit e27a55c
Show file tree
Hide file tree
Showing 13 changed files with 349 additions and 255 deletions.
8 changes: 5 additions & 3 deletions linera-service/src/cli_wrappers/remote_net.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::{
local_net::PathProvider, ClientWrapper, Faucet, FaucetOption, LineraNet, LineraNetConfig,
Network,
},
config::Export,
persistent::{self, Persist},
};

pub struct RemoteNetTestingConfig {
Expand Down Expand Up @@ -116,9 +116,11 @@ impl LineraNet for RemoteNet {
impl RemoteNet {
async fn new(testing_prng_seed: Option<u64>, faucet: &Faucet) -> Result<Self> {
let tmp_dir = Arc::new(tempdir()?);
let genesis_config = faucet.genesis_config().await?;
// Write json config to disk
genesis_config.write(tmp_dir.path().join("genesis.json").as_path())?;
Persist::persist(&mut persistent::File::new(
tmp_dir.path().join("genesis.json").as_path(),
faucet.genesis_config().await?,
)?)?;
Ok(Self {
network: Network::Grpc,
testing_prng_seed,
Expand Down
2 changes: 1 addition & 1 deletion linera-service/src/cli_wrappers/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,7 @@ impl ClientWrapper {
}

pub fn load_wallet(&self) -> Result<Wallet> {
Ok(WalletState::from_file(self.wallet_path().as_path())?.into_inner())
Ok(WalletState::from_file(self.wallet_path().as_path())?.into_value())
}

pub fn wallet_path(&self) -> PathBuf {
Expand Down
190 changes: 40 additions & 150 deletions linera-service/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,8 @@
// Copyright (c) Zefchain Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use std::{
io::{BufRead, BufReader, BufWriter, Write},
iter::IntoIterator,
path::{Path, PathBuf},
};
use std::{iter::IntoIterator, path::Path};

use anyhow::{bail, Context as _};
use fs4::FileExt as _;
use fs_err::{self, File, OpenOptions};
use linera_base::{
crypto::{BcsSignable, CryptoRng, KeyPair, PublicKey},
data_types::{Amount, Timestamp},
Expand All @@ -23,27 +16,12 @@ use linera_execution::{
use linera_rpc::config::{ValidatorInternalNetworkConfig, ValidatorPublicNetworkConfig};
use linera_storage::Storage;
use linera_views::views::ViewError;
use serde::{de::DeserializeOwned, Deserialize, Serialize};

use crate::wallet::{UserChain, Wallet};

pub trait Import: DeserializeOwned {
fn read(path: &Path) -> Result<Self, std::io::Error> {
let data = fs_err::read(path)?;
Ok(serde_json::from_slice(data.as_slice())?)
}
}
use serde::{Deserialize, Serialize};

pub trait Export: Serialize {
fn write(&self, path: &Path) -> Result<(), std::io::Error> {
let file = OpenOptions::new().create(true).write(true).open(path)?;
let mut writer = BufWriter::new(file);
let data = serde_json::to_string_pretty(self).unwrap();
writer.write_all(data.as_ref())?;
writer.write_all(b"\n")?;
Ok(())
}
}
use crate::{
persistent::{self, Persist},
wallet::{UserChain, Wallet},
};

/// The public configuration of a validator.
#[derive(Clone, Debug, Serialize, Deserialize)]
Expand All @@ -62,18 +40,12 @@ pub struct ValidatorServerConfig {
pub internal_network: ValidatorInternalNetworkConfig,
}

impl Import for ValidatorServerConfig {}
impl Export for ValidatorServerConfig {}

/// The (public) configuration for all validators.
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct CommitteeConfig {
pub validators: Vec<ValidatorConfig>,
}

impl Import for CommitteeConfig {}
impl Export for CommitteeConfig {}

impl CommitteeConfig {
pub fn into_committee(self, policy: ResourceControlPolicy) -> Committee {
let validators = self
Expand All @@ -93,147 +65,67 @@ impl CommitteeConfig {
}
}

/// A guard that keeps an exclusive lock on a file.
pub struct FileLock {
file: File,
/// The runtime state of the wallet, persisted atomically on change via an instance of
/// [`Persist`].
pub struct WalletState {
wallet: persistent::File<Wallet>,
prng: Box<dyn CryptoRng>,
}

impl FileLock {
/// Acquires an exclusive lock on a provided `file`, returning a [`FileLock`] which will
/// release the lock when dropped.
pub fn new(file: File, path: &Path) -> Result<Self, anyhow::Error> {
file.file().try_lock_exclusive().with_context(|| {
format!(
"Error getting write lock to wallet \"{}\". Please make sure the file exists \
and that it is not in use by another process already.",
path.display()
)
})?;
impl std::ops::Deref for WalletState {
type Target = Wallet;

Ok(FileLock { file })
fn deref(&self) -> &Wallet {
&self.wallet
}
}

impl Drop for FileLock {
fn drop(&mut self) {
if let Err(error) = self.file.file().unlock() {
tracing::warn!("Failed to unlock wallet file: {error}");
}
impl Persist for WalletState {
type Error = anyhow::Error;

fn persist(this: &mut Self) -> anyhow::Result<()> {
Persist::mutate(&mut this.wallet).refresh_prng_seed(&mut this.prng);
tracing::debug!("Persisted user chains");
Ok(())
}
}

/// A wrapper around `Wallet` which owns a [`FileLock`] to prevent
/// two processes accessing it at the same time.
pub struct WalletState {
inner: Wallet,
prng: Box<dyn CryptoRng>,
wallet_path: PathBuf,
_lock: FileLock,
fn as_mut(this: &mut Self) -> &mut Wallet {
Persist::as_mut(&mut this.wallet)
}
}

impl Extend<UserChain> for WalletState {
fn extend<Chains: IntoIterator<Item = UserChain>>(&mut self, chains: Chains) {
self.inner.extend(chains);
Persist::mutate(self).extend(chains);
}
}

impl WalletState {
pub fn inner(&self) -> &Wallet {
&self.inner
}

pub fn inner_mut(&mut self) -> &mut Wallet {
&mut self.inner
}

pub fn into_inner(self) -> Wallet {
self.inner
fn new(wallet: persistent::File<Wallet>) -> Self {
Self {
prng: wallet.make_prng(),
wallet,
}
}

pub fn from_file(path: &Path) -> Result<Self, anyhow::Error> {
let file = OpenOptions::new().read(true).write(true).open(path)?;
let file_lock = FileLock::new(file, path)?;
let inner: Wallet = serde_json::from_reader(BufReader::new(&file_lock.file))?;
Ok(Self {
prng: inner.make_prng(),
inner,
wallet_path: path.into(),
_lock: file_lock,
})
}

pub fn create(
path: &Path,
genesis_config: GenesisConfig,
testing_prng_seed: Option<u64>,
) -> Result<Self, anyhow::Error> {
let file = Self::open_options().read(true).open(path)?;
let file_lock = FileLock::new(file, path)?;
let mut reader = BufReader::new(&file_lock.file);
let inner = if reader.fill_buf()?.is_empty() {
Wallet::new(genesis_config, testing_prng_seed)
} else {
serde_json::from_reader(reader)?
};

Ok(Self {
prng: inner.make_prng(),
inner,
wallet_path: path.into(),
_lock: file_lock,
})
Ok(Self::new(persistent::File::read_or_create(path, || {
anyhow::bail!("wallet file not found: {}", path.display())
})?))
}

/// Writes the wallet to disk.
///
/// The contents of the wallet need to be over-written completely, so
/// a temporary file is created as a backup in case a crash occurs while
/// writing to disk.
///
/// The temporary file is then renamed to the original wallet name. If
/// serialization or writing to disk fails, the temporary filed is
/// deleted.
pub fn write(&mut self) -> Result<(), anyhow::Error> {
let mut temp_file_path = self.wallet_path.clone();
temp_file_path.set_extension("json.bak");
let backup_file = Self::open_options().open(&temp_file_path)?;
let mut temp_file_writer = BufWriter::new(backup_file);
if let Err(e) = serde_json::to_writer_pretty(&mut temp_file_writer, &self.inner) {
fs_err::remove_file(&temp_file_path)?;
bail!("failed to serialize the wallet state: {}", e)
}
if let Err(e) = temp_file_writer.flush() {
fs_err::remove_file(&temp_file_path)?;
bail!("failed to write the wallet state: {}", e);
}
fs_err::rename(&temp_file_path, &self.wallet_path)?;
Ok(())
pub fn create(path: &Path, wallet: Wallet) -> Result<Self, anyhow::Error> {
Ok(Self::new(persistent::File::read_or_create(path, || {
Ok(wallet)
})?))
}

pub fn generate_key_pair(&mut self) -> KeyPair {
KeyPair::generate_from(&mut self.prng)
}

pub fn save(&mut self) -> anyhow::Result<()> {
self.inner.refresh_prng_seed(&mut self.prng);
self.write()?;
tracing::info!("Saved user chain states");
Ok(())
}

pub fn refresh_prng_seed(&mut self) {
self.inner.refresh_prng_seed(&mut self.prng)
}

/// Returns options for opening and writing to the wallet file, creating it if it doesn't
/// exist. On Unix, this restricts read and write permissions to the current user.
// TODO(#1924): Implement better key management.
fn open_options() -> OpenOptions {
let mut options = OpenOptions::new();
#[cfg(target_family = "unix")]
fs_err::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o600);
options.create(true).write(true);
options
pub fn into_value(self) -> Wallet {
self.wallet.into_value()
}
}

Expand All @@ -247,8 +139,6 @@ pub struct GenesisConfig {
pub network_name: String,
}

impl Import for GenesisConfig {}
impl Export for GenesisConfig {}
impl BcsSignable for GenesisConfig {}

impl GenesisConfig {
Expand Down
1 change: 1 addition & 0 deletions linera-service/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod config;
pub mod faucet;
pub mod grpc_proxy;
pub mod node_service;
pub mod persistent;
pub mod project;
#[cfg(with_metrics)]
pub mod prometheus_server;
Expand Down
Loading

0 comments on commit e27a55c

Please sign in to comment.