mirror of
https://github.com/mimblewimble/grin.git
synced 2025-01-20 19:11:08 +03:00
Protect wallet data file with a file lock
Operations on the wallet data file are now fenced by a lock to avoid potentially messy concurrent modifications by multiple processes (i.e. the wallet receiver and a send command). The lock is done using a create-only lock file, which is an atomic operation.
This commit is contained in:
parent
6523966f9e
commit
fbbd703e99
5 changed files with 129 additions and 92 deletions
|
@ -114,7 +114,6 @@ impl<T> TransactionPool<T> where T: BlockChain {
|
|||
// Making sure the transaction is valid before anything else.
|
||||
let secp = secp::Secp256k1::with_caps(secp::ContextFlag::Commit);
|
||||
tx.validate(&secp).map_err(|_| PoolError::Invalid)?;
|
||||
|
||||
|
||||
// The first check invovles ensuring that an identical transaction is
|
||||
// not already in the pool's transaction set.
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
//! the wallet storage and update them.
|
||||
|
||||
use api;
|
||||
use core::core::{Output, DEFAULT_OUTPUT, COINBASE_OUTPUT};
|
||||
use core::core::Output;
|
||||
use secp::{self, pedersen};
|
||||
use util;
|
||||
|
||||
|
@ -27,32 +27,34 @@ use types::{WalletConfig, OutputStatus, WalletData};
|
|||
/// with a node whether their status has changed.
|
||||
pub fn refresh_outputs(config: &WalletConfig, ext_key: &ExtendedKey) {
|
||||
let secp = secp::Secp256k1::with_caps(secp::ContextFlag::Commit);
|
||||
let mut wallet_data = WalletData::read_or_create().expect("Could not open wallet data.");
|
||||
|
||||
let mut changed = 0;
|
||||
for out in &mut wallet_data.outputs {
|
||||
if out.status != OutputStatus::Spent {
|
||||
let key = ext_key.derive(&secp, out.n_child).unwrap();
|
||||
let commitment = secp.commit(out.value, key.key).unwrap();
|
||||
// operate within a lock on wallet data
|
||||
WalletData::with_wallet(|wallet_data| {
|
||||
|
||||
// TODO check the pool for unconfirmed
|
||||
// check each output that's not spent
|
||||
for out in &mut wallet_data.outputs {
|
||||
if out.status != OutputStatus::Spent {
|
||||
|
||||
let out_res = get_output_by_commitment(config, commitment);
|
||||
if out_res.is_ok() {
|
||||
out.status = OutputStatus::Unspent;
|
||||
changed += 1;
|
||||
} else if out.status == OutputStatus::Unspent {
|
||||
// a UTXO we can't find anymore has been spent
|
||||
if let Err(api::Error::NotFound) = out_res {
|
||||
out.status = OutputStatus::Spent;
|
||||
changed += 1;
|
||||
// figure out the commitment
|
||||
let key = ext_key.derive(&secp, out.n_child).unwrap();
|
||||
let commitment = secp.commit(out.value, key.key).unwrap();
|
||||
|
||||
// TODO check the pool for unconfirmed
|
||||
|
||||
let out_res = get_output_by_commitment(config, commitment);
|
||||
if out_res.is_ok() {
|
||||
// output is known, it's a new utxo
|
||||
out.status = OutputStatus::Unspent;
|
||||
|
||||
} else if out.status == OutputStatus::Unspent {
|
||||
// a UTXO we can't find anymore has been spent
|
||||
if let Err(api::Error::NotFound) = out_res {
|
||||
out.status = OutputStatus::Spent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if changed > 0 {
|
||||
wallet_data.write().unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// queries a reachable node for a given output, checking whether it's been
|
||||
|
|
|
@ -143,24 +143,25 @@ impl ApiEndpoint for WalletReceiver {
|
|||
fn receive_coinbase(ext_key: &ExtendedKey, amount: u64) -> Result<(Output, TxKernel), Error> {
|
||||
let secp = secp::Secp256k1::with_caps(secp::ContextFlag::Commit);
|
||||
|
||||
// derive a new private for the reward
|
||||
let mut wallet_data = WalletData::read_or_create()?;
|
||||
let next_child = wallet_data.next_child(ext_key.fingerprint);
|
||||
let coinbase_key = ext_key.derive(&secp, next_child).map_err(|e| Error::Key(e))?;
|
||||
// operate within a lock on wallet data
|
||||
WalletData::with_wallet(|wallet_data| {
|
||||
|
||||
// track the new output and return the stuff needed for reward
|
||||
wallet_data.append_output(OutputData {
|
||||
fingerprint: coinbase_key.fingerprint,
|
||||
n_child: coinbase_key.n_child,
|
||||
value: amount,
|
||||
status: OutputStatus::Unconfirmed,
|
||||
});
|
||||
wallet_data.write()?;
|
||||
// derive a new private for the reward
|
||||
let next_child = wallet_data.next_child(ext_key.fingerprint);
|
||||
let coinbase_key = ext_key.derive(&secp, next_child).map_err(|e| Error::Key(e))?;
|
||||
|
||||
debug!("Using child {} for a new coinbase output.",
|
||||
coinbase_key.n_child);
|
||||
// track the new output and return the stuff needed for reward
|
||||
wallet_data.append_output(OutputData {
|
||||
fingerprint: coinbase_key.fingerprint,
|
||||
n_child: coinbase_key.n_child,
|
||||
value: amount,
|
||||
status: OutputStatus::Unconfirmed,
|
||||
});
|
||||
debug!("Using child {} for a new coinbase output.",
|
||||
coinbase_key.n_child);
|
||||
|
||||
Block::reward_output(coinbase_key.key, &secp).map_err(&From::from)
|
||||
Block::reward_output(coinbase_key.key, &secp).map_err(&From::from)
|
||||
})?
|
||||
}
|
||||
|
||||
/// Builds a full transaction from the partial one sent to us for transfer
|
||||
|
@ -172,30 +173,31 @@ fn receive_transaction(ext_key: &ExtendedKey,
|
|||
|
||||
let secp = secp::Secp256k1::with_caps(secp::ContextFlag::Commit);
|
||||
|
||||
// derive a new private for the receiving output
|
||||
let mut wallet_data = WalletData::read_or_create()?;
|
||||
let next_child = wallet_data.next_child(ext_key.fingerprint);
|
||||
let out_key = ext_key.derive(&secp, next_child).map_err(|e| Error::Key(e))?;
|
||||
// operate within a lock on wallet data
|
||||
WalletData::with_wallet(|wallet_data| {
|
||||
|
||||
let (tx_final, _) = build::transaction(vec![build::initial_tx(partial),
|
||||
build::with_excess(blinding),
|
||||
build::output(amount, out_key.key)])?;
|
||||
let next_child = wallet_data.next_child(ext_key.fingerprint);
|
||||
let out_key = ext_key.derive(&secp, next_child).map_err(|e| Error::Key(e))?;
|
||||
|
||||
// make sure the resulting transaction is valid (could have been lied to
|
||||
// on excess)
|
||||
tx_final.validate(&secp)?;
|
||||
let (tx_final, _) = build::transaction(vec![build::initial_tx(partial),
|
||||
build::with_excess(blinding),
|
||||
build::output(amount, out_key.key)])?;
|
||||
|
||||
// track the new output and return the finalized transaction to broadcast
|
||||
wallet_data.append_output(OutputData {
|
||||
fingerprint: out_key.fingerprint,
|
||||
n_child: out_key.n_child,
|
||||
value: amount,
|
||||
status: OutputStatus::Unconfirmed,
|
||||
});
|
||||
wallet_data.write()?;
|
||||
// make sure the resulting transaction is valid (could have been lied to
|
||||
// on excess)
|
||||
tx_final.validate(&secp)?;
|
||||
|
||||
debug!("Using child {} for a new transaction output.",
|
||||
out_key.n_child);
|
||||
// track the new output and return the finalized transaction to broadcast
|
||||
wallet_data.append_output(OutputData {
|
||||
fingerprint: out_key.fingerprint,
|
||||
n_child: out_key.n_child,
|
||||
value: amount,
|
||||
status: OutputStatus::Unconfirmed,
|
||||
});
|
||||
|
||||
Ok(tx_final)
|
||||
debug!("Using child {} for a new transaction output.",
|
||||
out_key.n_child);
|
||||
|
||||
Ok(tx_final)
|
||||
})?
|
||||
}
|
||||
|
|
|
@ -46,38 +46,41 @@ fn build_send_tx(ext_key: &ExtendedKey, amount: u64) -> Result<(Transaction, Sec
|
|||
// first, rebuild the private key from the seed
|
||||
let secp = secp::Secp256k1::with_caps(secp::ContextFlag::Commit);
|
||||
|
||||
// second, check from our local wallet data for outputs to spend
|
||||
let mut wallet_data = WalletData::read()?;
|
||||
let (coins, change) = wallet_data.select(ext_key.fingerprint, amount);
|
||||
if change < 0 {
|
||||
return Err(Error::NotEnoughFunds((-change) as u64));
|
||||
}
|
||||
// operate within a lock on wallet data
|
||||
WalletData::with_wallet(|wallet_data| {
|
||||
|
||||
// TODO add fees, which is likely going to make this iterative
|
||||
// second, check from our local wallet data for outputs to spend
|
||||
let (coins, change) = wallet_data.select(ext_key.fingerprint, amount);
|
||||
if change < 0 {
|
||||
return Err(Error::NotEnoughFunds((-change) as u64));
|
||||
}
|
||||
|
||||
// third, build inputs using the appropriate key
|
||||
let mut parts = vec![];
|
||||
for coin in &coins {
|
||||
let in_key = ext_key.derive(&secp, coin.n_child).map_err(|e| Error::Key(e))?;
|
||||
parts.push(build::input(coin.value, in_key.key));
|
||||
}
|
||||
// TODO add fees, which is likely going to make this iterative
|
||||
|
||||
// fourth, derive a new private for change and build the change output
|
||||
let next_child = wallet_data.next_child(ext_key.fingerprint);
|
||||
let change_key = ext_key.derive(&secp, next_child).map_err(|e| Error::Key(e))?;
|
||||
parts.push(build::output(change as u64, change_key.key));
|
||||
// third, build inputs using the appropriate key
|
||||
let mut parts = vec![];
|
||||
for coin in &coins {
|
||||
let in_key = ext_key.derive(&secp, coin.n_child).map_err(|e| Error::Key(e))?;
|
||||
parts.push(build::input(coin.value, in_key.key));
|
||||
}
|
||||
|
||||
// we got that far, time to start tracking the new output, finalize tx
|
||||
// and lock the outputs used
|
||||
wallet_data.append_output(OutputData {
|
||||
fingerprint: change_key.fingerprint,
|
||||
n_child: change_key.n_child,
|
||||
value: change as u64,
|
||||
status: OutputStatus::Unconfirmed,
|
||||
});
|
||||
for mut coin in coins {
|
||||
coin.lock();
|
||||
}
|
||||
wallet_data.write()?;
|
||||
build::transaction(parts).map_err(&From::from)
|
||||
// fourth, derive a new private for change and build the change output
|
||||
let next_child = wallet_data.next_child(ext_key.fingerprint);
|
||||
let change_key = ext_key.derive(&secp, next_child).map_err(|e| Error::Key(e))?;
|
||||
parts.push(build::output(change as u64, change_key.key));
|
||||
|
||||
// we got that far, time to start tracking the new output, finalize tx
|
||||
// and lock the outputs used
|
||||
wallet_data.append_output(OutputData {
|
||||
fingerprint: change_key.fingerprint,
|
||||
n_child: change_key.n_child,
|
||||
value: change as u64,
|
||||
status: OutputStatus::Unconfirmed,
|
||||
});
|
||||
for mut coin in coins {
|
||||
coin.lock();
|
||||
}
|
||||
|
||||
build::transaction(parts).map_err(&From::from)
|
||||
})?
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
use std::convert::From;
|
||||
use std::fs::File;
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::num;
|
||||
use std::path::Path;
|
||||
|
@ -30,6 +30,7 @@ use extkey;
|
|||
use util;
|
||||
|
||||
const DAT_FILE: &'static str = "wallet.dat";
|
||||
const LOCK_FILE: &'static str = "wallet.lock";
|
||||
|
||||
/// Wallet errors, mostly wrappers around underlying crypto or I/O errors.
|
||||
#[derive(Debug)]
|
||||
|
@ -133,8 +134,38 @@ pub struct WalletData {
|
|||
}
|
||||
|
||||
impl WalletData {
|
||||
/// Allows the reading and writing of the wallet data within a file lock.
|
||||
/// Just provide a closure taking a mutable WalletData. The lock should
|
||||
/// be held for as short a period as possible to avoid contention.
|
||||
/// Note that due to the impossibility to do an actual file lock easily
|
||||
/// across operating systems, this just creates a lock file with a "should
|
||||
/// not exist" option.
|
||||
pub fn with_wallet<T, F>(f: F) -> Result<T, Error>
|
||||
where F: FnOnce(&mut WalletData) -> T
|
||||
{
|
||||
// create the lock files, if it already exists, will produce an error
|
||||
OpenOptions::new().write(true).create_new(true).open(LOCK_FILE).map_err(|e| {
|
||||
Error::WalletData(format!("Could not create wallet lock file. Either \
|
||||
some other process is using the wallet or there's a write access \
|
||||
issue."))
|
||||
})?;
|
||||
|
||||
// do what needs to be done
|
||||
let mut wdat = WalletData::read_or_create()?;
|
||||
let res = f(&mut wdat);
|
||||
wdat.write()?;
|
||||
|
||||
// delete the lock file
|
||||
fs::remove_file(LOCK_FILE).map_err(|e| {
|
||||
Error::WalletData(format!("Could not remove wallet lock file. Maybe insufficient \
|
||||
rights?"))
|
||||
})?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Read the wallet data or created a brand new one if it doesn't exist yet
|
||||
pub fn read_or_create() -> Result<WalletData, Error> {
|
||||
fn read_or_create() -> Result<WalletData, Error> {
|
||||
if Path::new(DAT_FILE).exists() {
|
||||
WalletData::read()
|
||||
} else {
|
||||
|
@ -144,7 +175,7 @@ impl WalletData {
|
|||
}
|
||||
|
||||
/// Read the wallet data from disk.
|
||||
pub fn read() -> Result<WalletData, Error> {
|
||||
fn read() -> Result<WalletData, Error> {
|
||||
let data_file = File::open(DAT_FILE)
|
||||
.map_err(|e| Error::WalletData(format!("Could not open {}: {}", DAT_FILE, e)))?;
|
||||
serde_json::from_reader(data_file)
|
||||
|
@ -152,7 +183,7 @@ impl WalletData {
|
|||
}
|
||||
|
||||
/// Write the wallet data to disk.
|
||||
pub fn write(&self) -> Result<(), Error> {
|
||||
fn write(&self) -> Result<(), Error> {
|
||||
let mut data_file = File::create(DAT_FILE)
|
||||
.map_err(|e| Error::WalletData(format!("Could not create {}: {}", DAT_FILE, e)))?;
|
||||
let res_json = serde_json::to_vec_pretty(self)
|
||||
|
|
Loading…
Reference in a new issue