grin-wallet/libwallet/src/internal/restore.rs

453 lines
12 KiB
Rust
Raw Normal View History

2019-02-13 18:05:19 +03:00
// 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.
//! Functions to restore a wallet's outputs from just the master seed
use crate::core::global;
use crate::core::libtx::proof;
use crate::keychain::{ExtKeychain, Identifier, Keychain};
use crate::internal::{keys, updater};
use crate::types::*;
use crate::Error;
use crate::util::secp::{key::SecretKey, pedersen};
use std::collections::HashMap;
/// Utility struct for return values from below
#[derive(Clone)]
struct OutputResult {
///
pub commit: pedersen::Commitment,
///
pub key_id: Identifier,
///
pub n_child: u32,
///
pub mmr_index: u64,
///
pub value: u64,
///
pub height: u64,
///
pub lock_height: u64,
///
pub is_coinbase: bool,
///
pub blinding: SecretKey,
}
#[derive(Debug, Clone)]
/// Collect stats in case we want to just output a single tx log entry
/// for restored non-coinbase outputs
struct RestoredTxStats {
///
pub log_id: u32,
///
pub amount_credited: u64,
///
pub num_outputs: usize,
}
fn identify_utxo_outputs<T, C, K>(
wallet: &mut T,
outputs: Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64, u64)>,
) -> Result<Vec<OutputResult>, Error>
where
T: WalletBackend<C, K>,
C: NodeClient,
K: Keychain,
{
let mut wallet_outputs: Vec<OutputResult> = Vec::new();
warn!(
"Scanning {} outputs in the current Grin utxo set",
outputs.len(),
);
for output in outputs.iter() {
let (commit, proof, is_coinbase, height, mmr_index) = output;
// attempt to unwind message from the RP and get a value
// will fail if it's not ours
let info = proof::rewind(wallet.keychain(), *commit, None, *proof)?;
if !info.success {
continue;
}
let lock_height = if *is_coinbase {
*height + global::coinbase_maturity()
} else {
*height
};
// TODO: Output paths are always going to be length 3 for now, but easy enough to grind
// through to find the right path if required later
let key_id = Identifier::from_serialized_path(3u8, &info.message.as_bytes());
info!(
"Output found: {:?}, amount: {:?}, key_id: {:?}, mmr_index: {},",
commit, info.value, key_id, mmr_index,
);
wallet_outputs.push(OutputResult {
commit: *commit,
key_id: key_id.clone(),
n_child: key_id.to_path().last_path_index(),
value: info.value,
height: *height,
lock_height: lock_height,
is_coinbase: *is_coinbase,
blinding: info.blinding,
mmr_index: *mmr_index,
});
}
Ok(wallet_outputs)
}
fn collect_chain_outputs<T, C, K>(wallet: &mut T) -> Result<Vec<OutputResult>, Error>
where
T: WalletBackend<C, K>,
C: NodeClient,
K: Keychain,
{
let batch_size = 1000;
let mut start_index = 1;
let mut result_vec: Vec<OutputResult> = vec![];
loop {
let (highest_index, last_retrieved_index, outputs) = wallet
.w2n_client()
.get_outputs_by_pmmr_index(start_index, batch_size)?;
warn!(
"Checking {} outputs, up to index {}. (Highest index: {})",
outputs.len(),
highest_index,
last_retrieved_index,
);
result_vec.append(&mut identify_utxo_outputs(wallet, outputs.clone())?);
if highest_index == last_retrieved_index {
break;
}
start_index = last_retrieved_index + 1;
}
Ok(result_vec)
}
///
fn restore_missing_output<T, C, K>(
wallet: &mut T,
output: OutputResult,
found_parents: &mut HashMap<Identifier, u32>,
tx_stats: &mut Option<&mut HashMap<Identifier, RestoredTxStats>>,
) -> Result<(), Error>
where
T: WalletBackend<C, K>,
C: NodeClient,
K: Keychain,
{
let commit = wallet.calc_commit_for_cache(output.value, &output.key_id)?;
let mut batch = wallet.batch()?;
let parent_key_id = output.key_id.parent_path();
if !found_parents.contains_key(&parent_key_id) {
found_parents.insert(parent_key_id.clone(), 0);
if let Some(ref mut s) = tx_stats {
s.insert(
parent_key_id.clone(),
RestoredTxStats {
log_id: batch.next_tx_log_id(&parent_key_id)?,
amount_credited: 0,
num_outputs: 0,
},
);
}
}
let log_id = if tx_stats.is_none() || output.is_coinbase {
let log_id = batch.next_tx_log_id(&parent_key_id)?;
let entry_type = match output.is_coinbase {
true => TxLogEntryType::ConfirmedCoinbase,
false => TxLogEntryType::TxReceived,
};
let mut t = TxLogEntry::new(parent_key_id.clone(), entry_type, log_id);
t.confirmed = true;
t.amount_credited = output.value;
t.num_outputs = 1;
t.update_confirmation_ts();
batch.save_tx_log_entry(t, &parent_key_id)?;
log_id
} else {
if let Some(ref mut s) = tx_stats {
let ts = s.get(&parent_key_id).unwrap().clone();
s.insert(
parent_key_id.clone(),
RestoredTxStats {
log_id: ts.log_id,
amount_credited: ts.amount_credited + output.value,
num_outputs: ts.num_outputs + 1,
},
);
ts.log_id
} else {
0
}
};
let _ = batch.save(OutputData {
root_key_id: parent_key_id.clone(),
key_id: output.key_id,
n_child: output.n_child,
mmr_index: Some(output.mmr_index),
commit: commit,
value: output.value,
status: OutputStatus::Unspent,
height: output.height,
lock_height: output.lock_height,
is_coinbase: output.is_coinbase,
tx_log_entry: Some(log_id),
});
let max_child_index = found_parents.get(&parent_key_id).unwrap().clone();
if output.n_child >= max_child_index {
found_parents.insert(parent_key_id.clone(), output.n_child);
}
batch.commit()?;
Ok(())
}
///
fn cancel_tx_log_entry<T, C, K>(wallet: &mut T, output: &OutputData) -> Result<(), Error>
where
T: WalletBackend<C, K>,
C: NodeClient,
K: Keychain,
{
let parent_key_id = output.key_id.parent_path();
let updated_tx_entry = if output.tx_log_entry.is_some() {
let entries = updater::retrieve_txs(
wallet,
output.tx_log_entry.clone(),
None,
Some(&parent_key_id),
false,
)?;
if entries.len() > 0 {
let mut entry = entries[0].clone();
match entry.tx_type {
TxLogEntryType::TxSent => entry.tx_type = TxLogEntryType::TxSentCancelled,
TxLogEntryType::TxReceived => entry.tx_type = TxLogEntryType::TxReceivedCancelled,
_ => {}
}
Some(entry)
} else {
None
}
} else {
None
};
let mut batch = wallet.batch()?;
if let Some(t) = updated_tx_entry {
batch.save_tx_log_entry(t, &parent_key_id)?;
}
batch.commit()?;
Ok(())
}
/// Check / repair wallet contents
/// assume wallet contents have been freshly updated with contents
/// of latest block
pub fn check_repair<T, C, K>(wallet: &mut T) -> Result<(), Error>
where
T: WalletBackend<C, K>,
C: NodeClient,
K: Keychain,
{
// First, get a definitive list of outputs we own from the chain
warn!("Starting wallet check.");
let chain_outs = collect_chain_outputs(wallet)?;
warn!(
"Identified {} wallet_outputs as belonging to this wallet",
chain_outs.len(),
);
// Now, get all outputs owned by this wallet (regardless of account)
let wallet_outputs = {
let res = updater::retrieve_outputs(&mut *wallet, true, None, None)?;
res
};
let mut missing_outs = vec![];
let mut accidental_spend_outs = vec![];
let mut locked_outs = vec![];
// check all definitive outputs exist in the wallet outputs
for deffo in chain_outs.into_iter() {
let matched_out = wallet_outputs.iter().find(|wo| wo.1 == deffo.commit);
match matched_out {
Some(s) => {
if s.0.status == OutputStatus::Spent {
accidental_spend_outs.push((s.0.clone(), deffo.clone()));
}
if s.0.status == OutputStatus::Locked {
locked_outs.push((s.0.clone(), deffo.clone()));
}
}
None => missing_outs.push(deffo),
}
}
// mark problem spent outputs as unspent (confirmed against a short-lived fork, for example)
for m in accidental_spend_outs.into_iter() {
let mut o = m.0;
warn!(
"Output for {} with ID {} ({:?}) marked as spent but exists in UTXO set. \
Marking unspent and cancelling any associated transaction log entries.",
o.value, o.key_id, m.1.commit,
);
o.status = OutputStatus::Unspent;
// any transactions associated with this should be cancelled
cancel_tx_log_entry(wallet, &o)?;
let mut batch = wallet.batch()?;
batch.save(o)?;
batch.commit()?;
}
let mut found_parents: HashMap<Identifier, u32> = HashMap::new();
// Restore missing outputs, adding transaction for it back to the log
for m in missing_outs.into_iter() {
warn!(
"Confirmed output for {} with ID {} ({:?}) exists in UTXO set but not in wallet. \
Restoring.",
m.value, m.key_id, m.commit,
);
restore_missing_output(wallet, m, &mut found_parents, &mut None)?;
}
// Unlock locked outputs
for m in locked_outs.into_iter() {
let mut o = m.0;
warn!(
"Confirmed output for {} with ID {} ({:?}) exists in UTXO set and is locked. \
Unlocking and cancelling associated transaction log entries.",
o.value, o.key_id, m.1.commit,
);
o.status = OutputStatus::Unspent;
cancel_tx_log_entry(wallet, &o)?;
let mut batch = wallet.batch()?;
batch.save(o)?;
batch.commit()?;
}
let unconfirmed_outs: Vec<&(OutputData, pedersen::Commitment)> = wallet_outputs
.iter()
.filter(|o| o.0.status == OutputStatus::Unconfirmed)
.collect();
// Delete unconfirmed outputs
for m in unconfirmed_outs.into_iter() {
let o = m.0.clone();
warn!(
"Unconfirmed output for {} with ID {} ({:?}) not in UTXO set. \
Deleting and cancelling associated transaction log entries.",
o.value, o.key_id, m.1,
);
cancel_tx_log_entry(wallet, &o)?;
let mut batch = wallet.batch()?;
batch.delete(&o.key_id, &o.mmr_index)?;
batch.commit()?;
}
// restore labels, account paths and child derivation indices
let label_base = "account";
let mut acct_index = 1;
for (path, max_child_index) in found_parents.iter() {
// default path already exists
if *path != ExtKeychain::derive_key_id(2, 0, 0, 0, 0) {
let label = format!("{}_{}", label_base, acct_index);
keys::set_acct_path(wallet, &label, path)?;
acct_index += 1;
}
let mut batch = wallet.batch()?;
debug!("Next child for account {} is {}", path, max_child_index + 1);
batch.save_child_index(path, max_child_index + 1)?;
batch.commit()?;
}
Ok(())
}
/// Restore a wallet
pub fn restore<T, C, K>(wallet: &mut T) -> Result<(), Error>
where
T: WalletBackend<C, K>,
C: NodeClient,
K: Keychain,
{
// Don't proceed if wallet_data has anything in it
let is_empty = wallet.iter().next().is_none();
if !is_empty {
error!("Not restoring. Please back up and remove existing db directory first.");
return Ok(());
}
warn!("Starting restore.");
let result_vec = collect_chain_outputs(wallet)?;
warn!(
"Identified {} wallet_outputs as belonging to this wallet",
result_vec.len(),
);
let mut found_parents: HashMap<Identifier, u32> = HashMap::new();
let mut restore_stats = HashMap::new();
// Now save what we have
for output in result_vec {
restore_missing_output(
wallet,
output,
&mut found_parents,
&mut Some(&mut restore_stats),
)?;
}
// restore labels, account paths and child derivation indices
let label_base = "account";
let mut acct_index = 1;
for (path, max_child_index) in found_parents.iter() {
// default path already exists
if *path != ExtKeychain::derive_key_id(2, 0, 0, 0, 0) {
let label = format!("{}_{}", label_base, acct_index);
keys::set_acct_path(wallet, &label, path)?;
acct_index += 1;
}
// restore tx log entry for non-coinbase outputs
if let Some(s) = restore_stats.get(path) {
let mut batch = wallet.batch()?;
let mut t = TxLogEntry::new(path.clone(), TxLogEntryType::TxReceived, s.log_id);
t.confirmed = true;
t.amount_credited = s.amount_credited;
t.num_outputs = s.num_outputs;
t.update_confirmation_ts();
batch.save_tx_log_entry(t, &path)?;
batch.commit()?;
}
let mut batch = wallet.batch()?;
batch.save_child_index(path, max_child_index + 1)?;
debug!("Next child for account {} is {}", path, max_child_index + 1);
batch.commit()?;
}
Ok(())
}