Files
grin-node/wallet/src/file_wallet.rs
T
Yeastplume 9e0b3b6862 Store additional wallet detail and WalletInfo cleanup (#1167)
* adding wallet detail file, clean up wallet info output

* rustfmt

* ensure change outputs aren't written early

* rustfmt

* travis problems AGAIN

* file wallet explicit types
2018-06-14 17:02:05 +01:00

632 lines
20 KiB
Rust

// Copyright 2018 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use blake2;
use rand::{thread_rng, Rng};
use std::cmp::min;
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::{Path, MAIN_SEPARATOR};
use serde_json;
use tokio_core::reactor;
use tokio_retry::strategy::FibonacciBackoff;
use tokio_retry::Retry;
use failure::ResultExt;
use keychain::{self, Keychain};
use util::secp::pedersen;
use util::{self, LOGGER};
use error::{Error, ErrorKind};
use client;
use libtx::slate::Slate;
use libwallet;
use libwallet::types::{BlockFees, BlockIdentifier, CbData, MerkleProofWrapper, OutputData,
TxWrapper, WalletBackend, WalletClient, WalletDetails};
const DETAIL_FILE: &'static str = "wallet.det";
const DET_BCK_FILE: &'static str = "wallet.detbck";
const DAT_FILE: &'static str = "wallet.dat";
const BCK_FILE: &'static str = "wallet.bck";
const LOCK_FILE: &'static str = "wallet.lock";
const SEED_FILE: &'static str = "wallet.seed";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WalletConfig {
// Right now the decision to run or not a wallet is based on the command.
// This may change in the near-future.
// pub enable_wallet: bool,
// The api interface/ip_address that this api server (i.e. this wallet) will run
// by default this is 127.0.0.1 (and will not accept connections from external clients)
pub api_listen_interface: String,
// The port this wallet will run on
pub api_listen_port: u16,
// The api address of a running server node against which transaction inputs
// will be checked during send
pub check_node_api_http_addr: String,
// The directory in which wallet files are stored
pub data_file_dir: String,
}
impl Default for WalletConfig {
fn default() -> WalletConfig {
WalletConfig {
// enable_wallet: false,
api_listen_interface: "127.0.0.1".to_string(),
api_listen_port: 13415,
check_node_api_http_addr: "http://127.0.0.1:13413".to_string(),
data_file_dir: ".".to_string(),
}
}
}
impl WalletConfig {
pub fn api_listen_addr(&self) -> String {
format!("{}:{}", self.api_listen_interface, self.api_listen_port)
}
}
#[derive(Clone, PartialEq)]
pub struct WalletSeed([u8; 32]);
impl WalletSeed {
pub fn from_bytes(bytes: &[u8]) -> WalletSeed {
let mut seed = [0; 32];
for i in 0..min(32, bytes.len()) {
seed[i] = bytes[i];
}
WalletSeed(seed)
}
fn from_hex(hex: &str) -> Result<WalletSeed, Error> {
let bytes =
util::from_hex(hex.to_string()).context(ErrorKind::GenericError("Invalid hex"))?;
Ok(WalletSeed::from_bytes(&bytes))
}
pub fn to_hex(&self) -> String {
util::to_hex(self.0.to_vec())
}
pub fn derive_keychain<K: Keychain>(&self, password: &str) -> Result<K, Error> {
let seed = blake2::blake2b::blake2b(64, &password.as_bytes(), &self.0);
let result = K::from_seed(seed.as_bytes())?;
Ok(result)
}
pub fn init_new() -> WalletSeed {
let seed: [u8; 32] = thread_rng().gen();
WalletSeed(seed)
}
pub fn init_file(wallet_config: &WalletConfig) -> Result<WalletSeed, Error> {
// create directory if it doesn't exist
fs::create_dir_all(&wallet_config.data_file_dir).context(ErrorKind::IO)?;
let seed_file_path = &format!(
"{}{}{}",
wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE,
);
debug!(LOGGER, "Generating wallet seed file at: {}", seed_file_path,);
if Path::new(seed_file_path).exists() {
Err(ErrorKind::WalletSeedExists)?
} else {
let seed = WalletSeed::init_new();
let mut file = File::create(seed_file_path).context(ErrorKind::IO)?;
file.write_all(&seed.to_hex().as_bytes())
.context(ErrorKind::IO)?;
Ok(seed)
}
}
pub fn from_file(wallet_config: &WalletConfig) -> Result<WalletSeed, Error> {
// create directory if it doesn't exist
fs::create_dir_all(&wallet_config.data_file_dir).context(ErrorKind::IO)?;
let seed_file_path = &format!(
"{}{}{}",
wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE,
);
debug!(LOGGER, "Using wallet seed file at: {}", seed_file_path,);
if Path::new(seed_file_path).exists() {
let mut file = File::open(seed_file_path).context(ErrorKind::IO)?;
let mut buffer = String::new();
file.read_to_string(&mut buffer).context(ErrorKind::IO)?;
let wallet_seed = WalletSeed::from_hex(&buffer)?;
Ok(wallet_seed)
} else {
error!(
LOGGER,
"wallet seed file {} could not be opened (grin wallet init). \
Run \"grin wallet init\" to initialize a new wallet.",
seed_file_path
);
Err(ErrorKind::WalletSeedDoesntExist)?
}
}
}
/// Wallet information tracking all our outputs. Based on HD derivation and
/// avoids storing any key data, only storing output amounts and child index.
#[derive(Debug, Clone)]
pub struct FileWallet<K> {
/// Keychain
pub keychain: Option<K>,
/// Configuration
pub config: WalletConfig,
/// passphrase: TODO better ways of dealing with this other than storing
passphrase: String,
/// List of outputs
pub outputs: HashMap<String, OutputData>,
/// Details
pub details: WalletDetails,
/// Data file path
pub data_file_path: String,
/// Backup file path
pub backup_file_path: String,
/// lock file path
pub lock_file_path: String,
/// details file path
pub details_file_path: String,
/// Details backup file path
pub details_bak_path: String,
}
impl<K> WalletBackend<K> for FileWallet<K>
where
K: Keychain,
{
/// Initialize with whatever stored credentials we have
fn open_with_credentials(&mut self) -> Result<(), libwallet::Error> {
let wallet_seed = WalletSeed::from_file(&self.config)
.context(libwallet::ErrorKind::CallbackImpl("Error opening wallet"))?;
let keychain = wallet_seed.derive_keychain(&self.passphrase);
self.keychain = Some(keychain.context(libwallet::ErrorKind::CallbackImpl(
"Error deriving keychain",
))?);
// Just blow up password for now after it's been used
self.passphrase = String::from("");
Ok(())
}
/// Close wallet and remove any stored credentials (TBD)
fn close(&mut self) -> Result<(), libwallet::Error> {
self.keychain = None;
Ok(())
}
/// Return the keychain being used
fn keychain(&mut self) -> &mut K {
self.keychain.as_mut().unwrap()
}
/// Return the outputs directly
fn outputs(&mut self) -> &mut HashMap<String, OutputData> {
&mut self.outputs
}
/// Allows for reading wallet data (without needing to acquire the write
/// lock).
fn read_wallet<T, F>(&mut self, f: F) -> Result<T, libwallet::Error>
where
F: FnOnce(&mut Self) -> Result<T, libwallet::Error>,
{
self.read_or_create_paths()
.context(libwallet::ErrorKind::CallbackImpl("Error reading wallet"))?;
f(self)
}
/// Allows the reading and writing of the wallet data within a file lock.
/// Just provide a closure taking a mutable FileWallet. 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.
fn with_wallet<T, F>(&mut self, f: F) -> Result<T, libwallet::Error>
where
F: FnOnce(&mut Self) -> T,
{
// create directory if it doesn't exist
fs::create_dir_all(self.config.data_file_dir.clone()).unwrap_or_else(|why| {
info!(LOGGER, "! {:?}", why.kind());
});
info!(LOGGER, "Acquiring wallet lock ...");
let lock_file_path = self.lock_file_path.clone();
let action = || {
trace!(LOGGER, "making lock file for wallet lock");
fs::create_dir(&lock_file_path)
};
// use tokio_retry to cleanly define some retry logic
let mut core = reactor::Core::new().unwrap();
let retry_strategy = FibonacciBackoff::from_millis(1000).take(10);
let retry_future = Retry::spawn(core.handle(), retry_strategy, action);
let retry_result = core.run(retry_future)
.context(libwallet::ErrorKind::CallbackImpl(
"Failed to acquire lock file",
));
match retry_result {
Ok(_) => {}
Err(e) => {
error!(
LOGGER,
"Failed to acquire wallet lock file (multiple retries)",
);
return Err(e.into());
}
}
// We successfully acquired the lock - so do what needs to be done.
self.read_or_create_paths()
.context(libwallet::ErrorKind::CallbackImpl("Lock Error"))?;
self.write(&self.backup_file_path, &self.details_bak_path)
.context(libwallet::ErrorKind::CallbackImpl("Write Error"))?;
let res = f(self);
self.write(&self.data_file_path, &self.details_file_path)
.context(libwallet::ErrorKind::CallbackImpl("Write Error"))?;
// delete the lock file
fs::remove_dir(&self.lock_file_path).context(libwallet::ErrorKind::CallbackImpl(
&"Could not remove wallet lock file. Maybe insufficient rights? ",
))?;
info!(LOGGER, "... released wallet lock");
Ok(res)
}
/// Append a new output data to the wallet data.
/// TODO - we should check for overwriting here - only really valid for
/// unconfirmed coinbase
fn add_output(&mut self, out: OutputData) {
self.outputs.insert(out.key_id.to_hex(), out.clone());
}
// TODO - careful with this, only for Unconfirmed (maybe Locked)?
fn delete_output(&mut self, id: &keychain::Identifier) {
self.outputs.remove(&id.to_hex());
}
/// Lock an output data.
/// TODO - we should track identifier on these outputs (not just n_child)
fn lock_output(&mut self, out: &OutputData) {
if let Some(out_to_lock) = self.outputs.get_mut(&out.key_id.to_hex()) {
if out_to_lock.value == out.value {
out_to_lock.lock()
}
}
}
/// get a single output
fn get_output(&self, key_id: &keychain::Identifier) -> Option<&OutputData> {
self.outputs.get(&key_id.to_hex())
}
/// Next child index when we want to create a new output.
fn next_child(&mut self, root_key_id: keychain::Identifier) -> u32 {
let mut max_n = 0;
for out in self.outputs.values() {
if max_n < out.n_child && out.root_key_id == root_key_id {
max_n = out.n_child;
}
}
if self.details.last_child_index <= max_n {
self.details.last_child_index = max_n + 1;
} else {
self.details.last_child_index += 1;
}
self.details.last_child_index
}
/// Select spendable coins from the wallet.
/// Default strategy is to spend the maximum number of outputs (up to
/// max_outputs). Alternative strategy is to spend smallest outputs first
/// but only as many as necessary. When we introduce additional strategies
/// we should pass something other than a bool in.
fn select_coins(
&self,
root_key_id: keychain::Identifier,
amount: u64,
current_height: u64,
minimum_confirmations: u64,
max_outputs: usize,
select_all: bool,
) -> Vec<OutputData> {
// first find all eligible outputs based on number of confirmations
let mut eligible = self.outputs
.values()
.filter(|out| {
out.root_key_id == root_key_id
&& out.eligible_to_spend(current_height, minimum_confirmations)
})
.cloned()
.collect::<Vec<OutputData>>();
// sort eligible outputs by increasing value
eligible.sort_by_key(|out| out.value);
// use a sliding window to identify potential sets of possible outputs to spend
// Case of amount > total amount of max_outputs(500):
// The limit exists because by default, we always select as many inputs as
// possible in a transaction, to reduce both the Output set and the fees.
// But that only makes sense up to a point, hence the limit to avoid being too
// greedy. But if max_outputs(500) is actually not enough to cover the whole
// amount, the wallet should allow going over it to satisfy what the user
// wants to send. So the wallet considers max_outputs more of a soft limit.
if eligible.len() > max_outputs {
for window in eligible.windows(max_outputs) {
let windowed_eligibles = window.iter().cloned().collect::<Vec<_>>();
if let Some(outputs) = self.select_from(amount, select_all, windowed_eligibles) {
return outputs;
}
}
// Not exist in any window of which total amount >= amount.
// Then take coins from the smallest one up to the total amount of selected
// coins = the amount.
if let Some(outputs) = self.select_from(amount, false, eligible.clone()) {
debug!(
LOGGER,
"Extending maximum number of outputs. {} outputs selected.",
outputs.len()
);
return outputs;
}
} else {
if let Some(outputs) = self.select_from(amount, select_all, eligible.clone()) {
return outputs;
}
}
// we failed to find a suitable set of outputs to spend,
// so return the largest amount we can so we can provide guidance on what is
// possible
eligible.reverse();
eligible.iter().take(max_outputs).cloned().collect()
}
/// Return current metadata
fn details(&mut self) -> &mut WalletDetails {
&mut self.details
}
/// Restore wallet contents
fn restore(&mut self) -> Result<(), libwallet::Error> {
libwallet::internal::restore::restore(self).context(libwallet::ErrorKind::Restore)?;
Ok(())
}
}
impl<K> WalletClient for FileWallet<K> {
/// Return URL for check node
fn node_url(&self) -> &str {
&self.config.check_node_api_http_addr
}
/// Call the wallet API to create a coinbase transaction
fn create_coinbase(
&self,
dest: &str,
block_fees: &BlockFees,
) -> Result<CbData, libwallet::Error> {
let res = client::create_coinbase(dest, block_fees);
match res {
Ok(r) => Ok(r),
Err(e) => {
let message = format!("{}", e.cause().unwrap());
Err(libwallet::ErrorKind::WalletComms(message))?
}
}
}
/// Send a transaction slate to another listening wallet and return result
fn send_tx_slate(&self, dest: &str, slate: &Slate) -> Result<Slate, libwallet::Error> {
let res = client::send_tx_slate(dest, slate);
match res {
Ok(r) => Ok(r),
Err(e) => {
let message = format!("{}", e.cause().unwrap());
Err(libwallet::ErrorKind::WalletComms(message))?
}
}
}
/// Posts a transaction to a grin node
fn post_tx(&self, dest: &str, tx: &TxWrapper, fluff: bool) -> Result<(), libwallet::Error> {
let res = client::post_tx(dest, tx, fluff).context(libwallet::ErrorKind::Node)?;
Ok(res)
}
/// retrieves the current tip from the specified grin node
fn get_chain_height(&self, addr: &str) -> Result<u64, libwallet::Error> {
let res = client::get_chain_height(addr).context(libwallet::ErrorKind::Node)?;
Ok(res)
}
/// retrieve a list of outputs from the specified grin node
/// need "by_height" and "by_id" variants
fn get_outputs_from_node(
&self,
addr: &str,
wallet_outputs: Vec<pedersen::Commitment>,
) -> Result<HashMap<pedersen::Commitment, String>, libwallet::Error> {
let res = client::get_outputs_from_node(addr, wallet_outputs)
.context(libwallet::ErrorKind::Node)?;
Ok(res)
}
/// Get any missing block hashes from node
fn get_missing_block_hashes_from_node(
&self,
addr: &str,
height: u64,
wallet_outputs: Vec<pedersen::Commitment>,
) -> Result<
(
HashMap<pedersen::Commitment, (u64, BlockIdentifier)>,
HashMap<pedersen::Commitment, MerkleProofWrapper>,
),
libwallet::Error,
> {
let res = client::get_missing_block_hashes_from_node(addr, height, wallet_outputs)
.context(libwallet::ErrorKind::Node)?;
Ok(res)
}
/// retrieve merkle proof for a commit from a node
fn get_merkle_proof_for_commit(
&self,
_addr: &str,
_commit: &str,
) -> Result<MerkleProofWrapper, libwallet::Error> {
Err(libwallet::ErrorKind::GenericError("Not Implemented"))?
}
}
impl<K> FileWallet<K>
where
K: Keychain,
{
/// Create a new FileWallet instance
pub fn new(config: WalletConfig, passphrase: &str) -> Result<Self, Error> {
let mut retval = FileWallet {
keychain: None,
config: config.clone(),
passphrase: String::from(passphrase),
outputs: HashMap::new(),
details: WalletDetails::default(),
data_file_path: format!("{}{}{}", config.data_file_dir, MAIN_SEPARATOR, DAT_FILE),
backup_file_path: format!("{}{}{}", config.data_file_dir, MAIN_SEPARATOR, BCK_FILE),
lock_file_path: format!("{}{}{}", config.data_file_dir, MAIN_SEPARATOR, LOCK_FILE),
details_file_path: format!("{}{}{}", config.data_file_dir, MAIN_SEPARATOR, DETAIL_FILE),
details_bak_path: format!("{}{}{}", config.data_file_dir, MAIN_SEPARATOR, DET_BCK_FILE),
};
match retval.read_or_create_paths() {
Ok(_) => Ok(retval),
Err(e) => Err(e),
}
}
/// Read the wallet data or create brand files if the data
/// files don't yet exist
fn read_or_create_paths(&mut self) -> Result<(), Error> {
if !Path::new(&self.config.data_file_dir.clone()).exists() {
fs::create_dir_all(&self.config.data_file_dir.clone()).unwrap_or_else(|why| {
info!(LOGGER, "! {:?}", why.kind());
});
}
if Path::new(&self.data_file_path.clone()).exists() {
self.read()?;
}
if Path::new(&self.details_file_path.clone()).exists() {
self.details = self.read_details()?;
}
Ok(())
}
/// Read details file from disk
fn read_details(&self) -> Result<WalletDetails, Error> {
let details_file = File::open(self.details_file_path.clone())
.context(ErrorKind::FileWallet(&"Could not open wallet details file"))?;
serde_json::from_reader(details_file)
.context(ErrorKind::Format)
.map_err(|e| e.into())
}
/// Read output_data vec from disk.
fn read_outputs(&self) -> Result<Vec<OutputData>, Error> {
let data_file = File::open(self.data_file_path.clone())
.context(ErrorKind::FileWallet(&"Could not open wallet file"))?;
serde_json::from_reader(data_file)
.context(ErrorKind::Format)
.map_err(|e| e.into())
}
/// Populate wallet_data with output_data from disk.
fn read(&mut self) -> Result<(), Error> {
let outputs = self.read_outputs()?;
self.outputs = HashMap::new();
for out in outputs {
self.add_output(out);
}
Ok(())
}
/// Write the wallet and details data to disk.
fn write(&self, data_file_path: &str, details_file_path: &str) -> Result<(), Error> {
let mut data_file =
File::create(data_file_path).context(ErrorKind::FileWallet(&"Could not create "))?;
let mut outputs = self.outputs.values().collect::<Vec<_>>();
outputs.sort();
let res_json = serde_json::to_vec_pretty(&outputs)
.context(ErrorKind::FileWallet("Error serializing wallet data"))?;
data_file
.write_all(res_json.as_slice())
.context(ErrorKind::FileWallet(&"Error writing wallet file"))?;
// write details file
let mut details_file =
File::create(details_file_path).context(ErrorKind::FileWallet(&"Could not create "))?;
let res_json = serde_json::to_string_pretty(&self.details).context(ErrorKind::FileWallet(
"Error serializing wallet details file",
))?;
details_file
.write_all(res_json.into_bytes().as_slice())
.context(ErrorKind::FileWallet(&"Error writing wallet details file"))
.map_err(|e| e.into())
}
// Select the full list of outputs if we are using the select_all strategy.
// Otherwise select just enough outputs to cover the desired amount.
fn select_from(
&self,
amount: u64,
select_all: bool,
outputs: Vec<OutputData>,
) -> Option<Vec<OutputData>> {
let total = outputs.iter().fold(0, |acc, x| acc + x.value);
if total >= amount {
if select_all {
return Some(outputs.iter().cloned().collect());
} else {
let mut selected_amount = 0;
return Some(
outputs
.iter()
.take_while(|out| {
let res = selected_amount < amount;
selected_amount += out.value;
res
})
.cloned()
.collect(),
);
}
} else {
None
}
}
}