diff --git a/api/src/types.rs b/api/src/types.rs index 6d7faf013..9bc0d0be1 100644 --- a/api/src/types.rs +++ b/api/src/types.rs @@ -21,7 +21,6 @@ use chain; use p2p; use util; use util::secp::pedersen; -use util::secp::constants::MAX_PROOF_SIZE; use serde; use serde::ser::SerializeStruct; use serde::de::MapAccess; @@ -230,7 +229,7 @@ pub struct OutputPrintable { /// Whether the output has been spent pub spent: bool, /// Rangeproof (as hex string) - pub proof: Option<pedersen::RangeProof>, + pub proof: Option<String>, /// Rangeproof hash (as hex string) pub proof_hash: String, @@ -257,7 +256,7 @@ impl OutputPrintable { let spent = chain.is_unspent(&out_id).is_err(); let proof = if include_proof { - Some(output.proof) + Some(util::to_hex(output.proof.proof.to_vec())) } else { None }; @@ -289,9 +288,19 @@ impl OutputPrintable { } pub fn range_proof(&self) -> Result<pedersen::RangeProof, ser::Error> { - self.proof + let proof_str = self.proof .clone() .ok_or_else(|| ser::Error::HexError(format!("output range_proof missing"))) + .unwrap(); + let p_vec = util::from_hex(proof_str).unwrap(); + let mut p_bytes = [0; util::secp::constants::MAX_PROOF_SIZE]; + for i in 0..p_bytes.len() { + p_bytes[i] = p_vec[i]; + } + Ok(pedersen::RangeProof { + proof: p_bytes, + plen: p_bytes.len(), + }) } } @@ -370,22 +379,7 @@ impl<'de> serde::de::Deserialize<'de> for OutputPrintable { } Field::Proof => { no_dup!(proof); - - let val: Option<String> = map.next_value()?; - - if val.is_some() { - let vec = util::from_hex(val.unwrap().clone()) - .map_err(serde::de::Error::custom)?; - let mut bytes = [0; MAX_PROOF_SIZE]; - for i in 0..vec.len() { - bytes[i] = vec[i]; - } - - proof = Some(pedersen::RangeProof { - proof: bytes, - plen: vec.len(), - }) - } + proof = map.next_value()? } Field::ProofHash => { no_dup!(proof_hash); @@ -415,13 +409,8 @@ impl<'de> serde::de::Deserialize<'de> for OutputPrintable { } } - const FIELDS: &'static [&'static str] = &[ - "output_type", - "commit", - "spent", - "proof", - "proof_hash", - ]; + const FIELDS: &'static [&'static str] = + &["output_type", "commit", "spent", "proof", "proof_hash"]; deserializer.deserialize_struct("OutputPrintable", FIELDS, OutputPrintableVisitor) } } diff --git a/core/src/core/block.rs b/core/src/core/block.rs index 35cab27d4..99869e04e 100644 --- a/core/src/core/block.rs +++ b/core/src/core/block.rs @@ -18,7 +18,7 @@ use time; use rand::{thread_rng, Rng}; use std::collections::HashSet; -use core::{Committed, Input, KernelFeatures, Output, OutputFeatures, Proof, +use core::{Committed, Input, KernelFeatures, Output, OutputFeatures, Proof, ProofMessageElements, ShortId, Transaction, TxKernel}; use consensus; use consensus::{exceeds_weight, reward, VerifySortOrder, REWARD}; @@ -169,7 +169,7 @@ impl Readable for BlockHeader { let nonce = reader.read_u64()?; let pow = Proof::read(reader)?; - if timestamp > (1 << 55) || timestamp < -(1 << 55) { + if timestamp > (1 << 55) || timestamp < -(1 << 55) { return Err(ser::Error::CorruptedData); } @@ -784,20 +784,13 @@ impl Block { fees: u64, height: u64, ) -> Result<(Output, TxKernel), keychain::Error> { - let commit = keychain.commit(reward(fees), key_id)?; + let value = reward(fees); + let commit = keychain.commit(value, key_id)?; + let msg = ProofMessageElements::new(value, key_id); - trace!( - LOGGER, - "Block reward - Pedersen Commit is: {:?}", - commit, - ); + trace!(LOGGER, "Block reward - Pedersen Commit is: {:?}", commit,); - let rproof = keychain.range_proof( - reward(fees), - key_id, - commit, - None, - )?; + let rproof = keychain.range_proof(value, key_id, commit, None, msg.to_proof_message())?; let output = Output { features: OutputFeatures::COINBASE_OUTPUT, diff --git a/core/src/core/build.rs b/core/src/core/build.rs index a57072dfc..737c3f4d0 100644 --- a/core/src/core/build.rs +++ b/core/src/core/build.rs @@ -27,7 +27,7 @@ use util::{kernel_sig_msg, secp}; -use core::{Input, Output, OutputFeatures, Transaction, TxKernel}; +use core::{Input, Output, OutputFeatures, ProofMessageElements, Transaction, TxKernel}; use core::hash::Hash; use core::pmmr::MerkleProof; use keychain; @@ -101,20 +101,13 @@ pub fn output(value: u64, key_id: Identifier) -> Box<Append> { debug!(LOGGER, "Building an output: {}, {}", value, key_id,); let commit = build.keychain.commit(value, &key_id).unwrap(); - trace!( - LOGGER, - "Builder - Pedersen Commit is: {:?}", - commit, - ); + trace!(LOGGER, "Builder - Pedersen Commit is: {:?}", commit,); + + let msg = ProofMessageElements::new(value, &key_id); let rproof = build .keychain - .range_proof( - value, - &key_id, - commit, - None, - ) + .range_proof(value, &key_id, commit, None, msg.to_proof_message()) .unwrap(); ( diff --git a/core/src/core/transaction.rs b/core/src/core/transaction.rs index 672513a19..d97d77de6 100644 --- a/core/src/core/transaction.rs +++ b/core/src/core/transaction.rs @@ -15,10 +15,11 @@ //! Transactions use util::secp::{self, Message, Signature}; use util::{kernel_sig_msg, static_secp_instance}; -use util::secp::pedersen::{Commitment, RangeProof}; +use util::secp::pedersen::{Commitment, ProofMessage, RangeProof}; use std::cmp::max; use std::cmp::Ordering; use std::{error, fmt}; +use std::io::Cursor; use consensus; use consensus::VerifySortOrder; @@ -29,7 +30,7 @@ use core::hash::{Hash, Hashed, ZERO_HASH}; use core::pmmr::MerkleProof; use keychain; use keychain::{BlindingFactor, Keychain}; -use ser::{self, read_and_verify_sorted, PMMRable, Readable, Reader, Writeable, +use ser::{self, read_and_verify_sorted, ser_vec, PMMRable, Readable, Reader, Writeable, WriteableSorted, Writer}; use util; use util::LOGGER; @@ -95,6 +96,9 @@ pub enum Error { /// Error originating from an input attempting to spend an immature /// coinbase output ImmatureCoinbase, + /// Returns if the value hidden within the a RangeProof message isn't + /// repeated 3 times, indicating it's incorrect + InvalidProofMessage, } impl error::Error for Error { @@ -739,12 +743,7 @@ impl Output { pub fn verify_proof(&self) -> Result<(), secp::Error> { let secp = static_secp_instance(); let secp = secp.lock().unwrap(); - match Keychain::verify_range_proof( - &secp, - self.commit, - self.proof, - None, - ) { + match Keychain::verify_range_proof(&secp, self.commit, self.proof, None) { Ok(_) => Ok(()), Err(e) => Err(e), } @@ -786,7 +785,7 @@ impl OutputIdentifier { features: self.features, commit: self.commit, proof: proof, - } + } } /// Build an output_identifier from an existing input. @@ -833,6 +832,114 @@ impl Readable for OutputIdentifier { } } +/// A structure which contains fields that are to be commited to within +/// an Output's range (bullet) proof. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct ProofMessageElements { + /// The amount, stored to allow for wallet reconstruction as + /// rewinding isn't supported in bulletproofs just yet + /// This is going to be written 3 times, to facilitate checking + /// values on rewind + /// Note that rewinding with only the nonce will give you back + /// the first 32 bytes of the message. To get the second + /// 32 bytes, you need to provide the correct blinding factor as well + value: u64, + /// another copy of the value, to check on rewind + value_copy_1: u64, + /// another copy of the value + value_copy_2: u64, + /// the first 8 bytes of the blinding factor, used to avoid having to grind + /// through a proof each time you want to check against key possibilities + bf_first_8: Vec<u8>, + /// unused portion of message, used to test whether we have both nonce + /// and blinding correct + zeroes: Vec<u8>, +} + +impl Writeable for ProofMessageElements { + fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_u64(self.value)?; + writer.write_u64(self.value_copy_1)?; + writer.write_u64(self.value_copy_2)?; + writer.write_fixed_bytes(&self.bf_first_8)?; + for i in 0..32 { + let _ = writer.write_u8(self.zeroes[i]); + } + Ok(()) + } +} + +impl Readable for ProofMessageElements { + fn read(reader: &mut Reader) -> Result<ProofMessageElements, ser::Error> { + // if the value isn't repeated 3 times, it's most likely not the value, + // so reject + Ok(ProofMessageElements { + value: reader.read_u64()?, + value_copy_1: reader.read_u64()?, + value_copy_2: reader.read_u64()?, + bf_first_8: reader.read_fixed_bytes(8)?, + zeroes: reader.read_fixed_bytes(32)?, + }) + } +} + +impl ProofMessageElements { + /// Create a new proof message + pub fn new(value: u64, blinding: &keychain::Identifier) -> ProofMessageElements { + ProofMessageElements { + value: value, + value_copy_1: value, + value_copy_2: value, + bf_first_8: blinding.to_bytes()[0..8].to_vec(), + zeroes: [0u8; 32].to_vec(), + } + } + + /// Return the value if it's valid, an error otherwise + pub fn value(&self) -> Result<u64, Error> { + if self.value == self.value_copy_1 && self.value == self.value_copy_2 { + Ok(self.value) + } else { + Err(Error::InvalidProofMessage) + } + } + + /// Compare given identifier with first 8 bytes of what's stored + pub fn compare_bf_first_8(&self, in_id: &keychain::Identifier) -> bool { + let in_id_vec = in_id.to_bytes()[0..8].to_vec(); + for i in 0..8 { + if in_id_vec[i] != self.bf_first_8[i] { + return false; + } + } + true + } + + /// Whether our remainder is zero (as it should be if the BF and nonce used to unwind + /// are correct + pub fn zeroes_correct(&self) -> bool { + for i in 0..self.zeroes.len() { + if self.zeroes[i] != 0 { + return false; + } + } + true + } + + /// Serialise and return a ProofMessage + pub fn to_proof_message(&self) -> ProofMessage { + ProofMessage::from_bytes(&ser_vec(self).unwrap()) + } + + /// Deserialise and return the message elements + pub fn from_proof_message( + proof_message: ProofMessage, + ) -> Result<ProofMessageElements, ser::Error> { + let mut c = Cursor::new(proof_message.as_bytes()); + ser::deserialize::<ProofMessageElements>(&mut c) + } +} + #[cfg(test)] mod test { use super::*; @@ -891,14 +998,7 @@ mod test { let key_id = keychain.derive_key_id(1).unwrap(); let commit = keychain.commit(5, &key_id).unwrap(); let msg = secp::pedersen::ProofMessage::empty(); - let proof = keychain - .range_proof( - 5, - &key_id, - commit, - None, - ) - .unwrap(); + let proof = keychain.range_proof(5, &key_id, commit, None, msg).unwrap(); let out = Output { features: OutputFeatures::DEFAULT_OUTPUT, diff --git a/grin/tests/simulnet.rs b/grin/tests/simulnet.rs index aa7e9af01..594b68e3e 100644 --- a/grin/tests/simulnet.rs +++ b/grin/tests/simulnet.rs @@ -188,7 +188,7 @@ fn simulate_block_propagation() { // instantiates 5 servers on different ports let mut servers = vec![]; for n in 0..5 { - let s = grin::Server::new(config(10*n, test_name_dir, 0)).unwrap(); + let s = grin::Server::new(config(10 * n, test_name_dir, 0)).unwrap(); servers.push(s); thread::sleep(time::Duration::from_millis(100)); } diff --git a/keychain/src/extkey.rs b/keychain/src/extkey.rs index 52ce757a4..5bf0a3668 100644 --- a/keychain/src/extkey.rs +++ b/keychain/src/extkey.rs @@ -126,6 +126,10 @@ impl Identifier { Identifier(identifier) } + pub fn to_bytes(&self) -> [u8; IDENTIFIER_SIZE] { + self.0.clone() + } + pub fn from_pubkey(secp: &Secp256k1, pubkey: &PublicKey) -> Identifier { let bytes = pubkey.serialize_vec(secp, true); let identifier = blake2b(IDENTIFIER_SIZE, &[], &bytes[..]); diff --git a/keychain/src/keychain.rs b/keychain/src/keychain.rs index 038dd29df..6a63c58ef 100644 --- a/keychain/src/keychain.rs +++ b/keychain/src/keychain.rs @@ -214,15 +214,42 @@ impl Keychain { Ok(commit) } + pub fn rangeproof_create_nonce(&self, commit: &Commitment) -> SecretKey { + // hash(commit|masterkey) as nonce + let root_key = self.root_key_id().to_bytes(); + let res = blake2::blake2b::blake2b(32, &commit.0, &root_key); + let res = res.as_bytes(); + let mut ret_val = [0; 32]; + for i in 0..res.len() { + ret_val[i] = res[i]; + } + SecretKey::from_slice(&self.secp, &ret_val).unwrap() + } + pub fn range_proof( &self, amount: u64, key_id: &Identifier, _commit: Commitment, extra_data: Option<Vec<u8>>, + msg: ProofMessage, ) -> Result<RangeProof, Error> { + let commit = self.commit(amount, key_id)?; let skey = self.derived_key(key_id)?; - Ok(self.secp.bullet_proof(amount, skey, extra_data, None)) + let nonce = self.rangeproof_create_nonce(&commit); + if msg.len() == 0 { + return Ok(self.secp + .bullet_proof(amount, skey, nonce, extra_data, None)); + } else { + if msg.len() != 64 { + error!(LOGGER, "Bullet proof message must be 64 bytes."); + return Err(Error::RangeProof( + "Bullet proof message must be 64 bytes".to_string(), + )); + } + } + return Ok(self.secp + .bullet_proof(amount, skey, nonce, extra_data, Some(msg))); } pub fn verify_range_proof( @@ -245,9 +272,10 @@ impl Keychain { extra_data: Option<Vec<u8>>, proof: RangeProof, ) -> Result<ProofInfo, Error> { - let nonce = self.derived_key(key_id)?; + let skey = self.derived_key(key_id)?; + let nonce = self.rangeproof_create_nonce(&commit); let proof_message = self.secp - .unwind_bullet_proof(commit, nonce, nonce, extra_data, proof); + .unwind_bullet_proof(commit, skey, nonce, extra_data, proof); let proof_info = match proof_message { Ok(p) => ProofInfo { success: true, @@ -975,4 +1003,75 @@ mod test { assert!(sig_verifies); } } + + #[test] + fn test_rewind_range_proof() { + let keychain = Keychain::from_random_seed().unwrap(); + let key_id = keychain.derive_key_id(1).unwrap(); + let commit = keychain.commit(5, &key_id).unwrap(); + let mut msg = ProofMessage::from_bytes(&[0u8; 64]); + let extra_data = [99u8; 64]; + + let proof = keychain + .range_proof(5, &key_id, commit, Some(extra_data.to_vec().clone()), msg) + .unwrap(); + let proof_info = keychain + .rewind_range_proof(&key_id, commit, Some(extra_data.to_vec().clone()), proof) + .unwrap(); + + assert_eq!(proof_info.success, true); + + // now check the recovered message is "empty" (but not truncated) i.e. all + // zeroes + //Value is in the message in this case + assert_eq!( + proof_info.message, + secp::pedersen::ProofMessage::from_bytes(&[0; secp::constants::BULLET_PROOF_MSG_SIZE]) + ); + + let key_id2 = keychain.derive_key_id(2).unwrap(); + + // cannot rewind with a different nonce + let proof_info = keychain + .rewind_range_proof(&key_id2, commit, Some(extra_data.to_vec().clone()), proof) + .unwrap(); + // With bullet proofs, if you provide the wrong nonce you'll get gibberish back + // as opposed to a failure to recover the message + assert_ne!( + proof_info.message, + secp::pedersen::ProofMessage::from_bytes(&[0; secp::constants::BULLET_PROOF_MSG_SIZE]) + ); + assert_eq!(proof_info.value, 0); + + // cannot rewind with a commitment to the same value using a different key + let commit2 = keychain.commit(5, &key_id2).unwrap(); + let proof_info = keychain + .rewind_range_proof(&key_id, commit2, Some(extra_data.to_vec().clone()), proof) + .unwrap(); + assert_eq!(proof_info.success, false); + assert_eq!(proof_info.value, 0); + + // cannot rewind with a commitment to a different value + let commit3 = keychain.commit(4, &key_id).unwrap(); + let proof_info = keychain + .rewind_range_proof(&key_id, commit3, Some(extra_data.to_vec().clone()), proof) + .unwrap(); + assert_eq!(proof_info.success, false); + assert_eq!(proof_info.value, 0); + + // cannot rewind with wrong extra committed data + let commit3 = keychain.commit(4, &key_id).unwrap(); + let wrong_extra_data = [98u8; 64]; + let should_err = keychain + .rewind_range_proof( + &key_id, + commit3, + Some(wrong_extra_data.to_vec().clone()), + proof, + ) + .unwrap(); + + assert_eq!(proof_info.success, false); + assert_eq!(proof_info.value, 0); + } } diff --git a/p2p/src/serv.rs b/p2p/src/serv.rs index c93c575a4..1c9bccb50 100644 --- a/p2p/src/serv.rs +++ b/p2p/src/serv.rs @@ -205,7 +205,7 @@ impl ChainAdapter for DummyAdapter { fn total_height(&self) -> u64 { 0 } - fn transaction_received(&self, _: core::Transaction, stem: bool) {} + fn transaction_received(&self, _: core::Transaction, _stem: bool) {} fn compact_block_received(&self, _cb: core::CompactBlock, _addr: SocketAddr) -> bool { true } diff --git a/pool/src/graph.rs b/pool/src/graph.rs index 9c1de5302..1bf13ba47 100644 --- a/pool/src/graph.rs +++ b/pool/src/graph.rs @@ -297,6 +297,7 @@ mod tests { use keychain::Keychain; use rand; use core::core::OutputFeatures; + use core::core::transaction::ProofMessageElements; #[test] fn test_add_entry() { @@ -322,16 +323,13 @@ mod tests { ), ]; + let msg = ProofMessageElements::new(100, &key_id1); + let output = core::transaction::Output { features: OutputFeatures::DEFAULT_OUTPUT, commit: output_commit, proof: keychain - .range_proof( - 100, - &key_id1, - output_commit, - None, - ) + .range_proof(100, &key_id1, output_commit, None, msg.to_proof_message()) .unwrap(), }; diff --git a/pool/src/pool.rs b/pool/src/pool.rs index 1243b6cea..1cb1d0735 100644 --- a/pool/src/pool.rs +++ b/pool/src/pool.rs @@ -859,6 +859,7 @@ mod tests { use core::core::hash::{Hash, Hashed}; use core::core::pmmr::MerkleProof; use core::core::target::Difficulty; + use core::core::transaction::ProofMessageElements; use types::PoolError::InvalidTx; macro_rules! expect_output_parent { @@ -1710,9 +1711,11 @@ mod tests { fn test_output(value: u64) -> transaction::Output { let keychain = keychain_for_tests(); let key_id = keychain.derive_key_id(value as u32).unwrap(); + let msg = ProofMessageElements::new(value, &key_id); let commit = keychain.commit(value, &key_id).unwrap(); - let proof = keychain.range_proof(value, &key_id, commit, None).unwrap(); - + let proof = keychain + .range_proof(value, &key_id, commit, None, msg.to_proof_message()) + .unwrap(); transaction::Output { features: transaction::OutputFeatures::DEFAULT_OUTPUT, commit: commit, @@ -1724,9 +1727,11 @@ mod tests { fn test_coinbase_output(value: u64) -> transaction::Output { let keychain = keychain_for_tests(); let key_id = keychain.derive_key_id(value as u32).unwrap(); + let msg = ProofMessageElements::new(value, &key_id); let commit = keychain.commit(value, &key_id).unwrap(); - let proof = keychain.range_proof(value, &key_id, commit, None).unwrap(); - + let proof = keychain + .range_proof(value, &key_id, commit, None, msg.to_proof_message()) + .unwrap(); transaction::Output { features: transaction::OutputFeatures::COINBASE_OUTPUT, commit: commit, diff --git a/src/bin/grin.rs b/src/bin/grin.rs index 36390e832..e63a0f2d8 100644 --- a/src/bin/grin.rs +++ b/src/bin/grin.rs @@ -251,7 +251,11 @@ fn main() { .about("basic wallet contents summary")) .subcommand(SubCommand::with_name("init") - .about("Initialize a new wallet seed file."))) + .about("Initialize a new wallet seed file.")) + + .subcommand(SubCommand::with_name("restore") + .about("Attempt to restore wallet contents from the chain using seed and password. \ + NOTE: Backup wallet.* and run `wallet listen` before running restore."))) .get_matches(); @@ -490,6 +494,12 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) { wallet_config.check_node_api_http_addr = sa.to_string().clone(); } + let key_derivations: u32 = wallet_args + .value_of("key_derivations") + .unwrap() + .parse() + .unwrap(); + let mut show_spent = false; if wallet_args.is_present("show_spent") { show_spent = true; @@ -611,6 +621,9 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) { ("outputs", Some(_)) => { wallet::show_outputs(&wallet_config, &keychain, show_spent); } + ("restore", Some(_)) => { + let _ = wallet::restore(&wallet_config, &keychain, key_derivations); + } _ => panic!("Unknown wallet command, use 'grin help wallet' for details"), } } diff --git a/util/Cargo.toml b/util/Cargo.toml index 411f10e9d..b5e5c5557 100644 --- a/util/Cargo.toml +++ b/util/Cargo.toml @@ -20,6 +20,6 @@ zip = "^0.2.6" [dependencies.secp256k1zkp] git = "https://github.com/mimblewimble/rust-secp256k1-zkp" -tag = "grin_integration_15" +tag = "grin_integration_16" #path = "../../rust-secp256k1-zkp" features = ["bullet-proof-sizing"] diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index ce76ef60d..de7556f51 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -53,6 +53,7 @@ mod info; mod receiver; mod sender; mod types; +mod restore; pub mod client; pub mod server; @@ -62,3 +63,4 @@ pub use receiver::WalletReceiver; pub use sender::{issue_burn_tx, issue_send_tx}; pub use types::{BlockFees, CbData, Error, ErrorKind, WalletConfig, WalletInfo, WalletReceiveRequest, WalletSeed}; +pub use restore::restore; diff --git a/wallet/src/restore.rs b/wallet/src/restore.rs new file mode 100644 index 000000000..4a35de97d --- /dev/null +++ b/wallet/src/restore.rs @@ -0,0 +1,248 @@ +// 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 failure::{Fail, ResultExt}; +use keychain::{Identifier, Keychain}; +use util::LOGGER; +use util::secp::pedersen; +use api; +use core::global; +use core::core::transaction::ProofMessageElements; +use types::{Error, ErrorKind, OutputData, OutputStatus, WalletConfig, WalletData}; +use byteorder::{BigEndian, ByteOrder}; + +pub fn get_chain_height(config: &WalletConfig) -> Result<u64, Error> { + let url = format!("{}/v1/chain", config.check_node_api_http_addr); + + match api::client::get::<api::Tip>(url.as_str()) { + Ok(tip) => Ok(tip.height), + Err(e) => { + // if we got anything other than 200 back from server, bye + error!( + LOGGER, + "get_chain_height: Restore failed... unable to contact API {}. Error: {}", + config.check_node_api_http_addr, + e + ); + Err(e.context(ErrorKind::Node).into()) + } + } +} + +fn coinbase_status(output: &api::OutputPrintable) -> bool { + match output.output_type { + api::OutputType::Coinbase => true, + api::OutputType::Transaction => false, + } +} + +pub fn outputs_batch_block( + config: &WalletConfig, + start_height: u64, + end_height: u64, +) -> Result<Vec<api::BlockOutputs>, Error> { + let query_param = format!( + "start_height={}&end_height={}&include_rp", + start_height, end_height + ); + + let url = format!( + "{}/v1/chain/outputs/byheight?{}", + config.check_node_api_http_addr, query_param, + ); + + match api::client::get::<Vec<api::BlockOutputs>>(url.as_str()) { + Ok(outputs) => Ok(outputs), + Err(e) => { + // if we got anything other than 200 back from server, bye + error!( + LOGGER, + "outputs_batch_block: Restore failed... unable to contact API {}. Error: {}", + config.check_node_api_http_addr, + e + ); + Err(e.context(ErrorKind::Node))? + } + } +} + +// TODO - wrap the many return values in a struct +fn find_outputs_with_key( + keychain: &Keychain, + block_outputs: api::BlockOutputs, + key_iterations: &mut usize, +) -> Vec<(pedersen::Commitment, Identifier, u32, u64, u64, u64, bool)> { + let mut wallet_outputs: Vec<(pedersen::Commitment, Identifier, u32, u64, u64, u64, bool)> = + Vec::new(); + + info!( + LOGGER, + "Scanning block {}, {} outputs, over {} key derivations", + block_outputs.header.height, + block_outputs.outputs.len(), + *key_iterations, + ); + + // skey doesn't matter in this case + let skey = keychain.derive_key_id(1).unwrap(); + for output in block_outputs.outputs.iter().filter(|x| !x.spent) { + // attempt to unwind message from the RP and get a value.. note + // this will only return okay if the value is included in the + // message 3 times, indicating a strong match. Also, sec_key provided + // to unwind in this case will be meaningless. With only the nonce known + // only the first 32 bytes of the recovered message will be accurate + let info = keychain + .rewind_range_proof(&skey, output.commit, None, output.range_proof().unwrap()) + .unwrap(); + let message = ProofMessageElements::from_proof_message(info.message).unwrap(); + let value = message.value(); + if value.is_err() { + continue; + } + // we have a match, now check through our key iterations to find a partial match + let mut found = false; + for i in 1..*key_iterations { + let key_id = &keychain.derive_key_id(i as u32).unwrap(); + if !message.compare_bf_first_8(key_id) { + continue; + } + found = true; + // we have a partial match, let's just confirm + let info = keychain + .rewind_range_proof(key_id, output.commit, None, output.range_proof().unwrap()) + .unwrap(); + let message = ProofMessageElements::from_proof_message(info.message).unwrap(); + let value = message.value(); + if value.is_err() || !message.zeroes_correct() { + continue; + } + let value = value.unwrap(); + info!( + LOGGER, + "Output found: {:?}, key_index: {:?}", output.commit, i, + ); + + // add it to result set here + let commit_id = output.commit.0; + + let is_coinbase = coinbase_status(output); + + info!(LOGGER, "Amount: {}", value); + + let commit = keychain + .commit_with_key_index(BigEndian::read_u64(&commit_id), i as u32) + .expect("commit with key index"); + + let height = block_outputs.header.height; + let lock_height = if is_coinbase { + height + global::coinbase_maturity() + } else { + 0 + }; + + wallet_outputs.push(( + commit, + key_id.clone(), + i as u32, + value, + height, + lock_height, + is_coinbase, + )); + } + if !found { + warn!( + LOGGER, + "Very probable matching output found with amount: {} \ + run restore again with a larger value of key_iterations to claim", + message.value().unwrap() + ); + } + } + debug!( + LOGGER, + "Found {} wallet_outputs for block {}", + wallet_outputs.len(), + block_outputs.header.height, + ); + + wallet_outputs +} + +pub fn restore( + config: &WalletConfig, + keychain: &Keychain, + key_derivations: u32, +) -> Result<(), Error> { + // Don't proceed if wallet.dat has anything in it + let is_empty = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { + Ok(wallet_data.outputs.len() == 0) + }).context(ErrorKind::WalletData("could not read wallet"))?; + if !is_empty { + error!( + LOGGER, + "Not restoring. Please back up and remove existing wallet.dat first." + ); + return Ok(()); + } + + // Get height of chain from node (we'll check again when done) + let chain_height = get_chain_height(config)?; + info!( + LOGGER, + "Starting restore: Chain height is {}.", chain_height + ); + + let batch_size = 100; + // this will start here, then lower as outputs are found, moving backwards on + // the chain + let mut key_iterations = key_derivations as usize; + let mut h = chain_height; + while { + let end_batch = h; + if h >= batch_size { + h -= batch_size; + } else { + h = 0; + } + let mut blocks = outputs_batch_block(config, h + 1, end_batch)?; + blocks.reverse(); + + let _ = WalletData::with_wallet(&config.data_file_dir, |wallet_data| { + for block in blocks { + let result_vec = find_outputs_with_key(keychain, block, &mut key_iterations); + if result_vec.len() > 0 { + for output in result_vec.clone() { + let root_key_id = keychain.root_key_id(); + // Just plonk it in for now, and refresh actual values via wallet info + // command later + wallet_data.add_output(OutputData { + root_key_id: root_key_id.clone(), + key_id: output.1.clone(), + n_child: output.2, + value: output.3, + status: OutputStatus::Unconfirmed, + height: output.4, + lock_height: output.5, + is_coinbase: output.6, + block: None, + merkle_proof: None, + }); + } + } + } + }); + h > 0 + } {} + Ok(()) +}