mirror of
https://github.com/mimblewimble/grin-wallet.git
synced 2025-03-14 21:11:10 +03:00
453 lines
12 KiB
Rust
453 lines
12 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.
|
||
|
//! 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(())
|
||
|
}
|