Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generalize client storage from the filesystem #2194

Merged
merged 11 commits into from
Jul 3, 2024
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
Loading