From 8440aad7ea085fdda5e3e5af5c00b04771b3c860 Mon Sep 17 00:00:00 2001 From: Yeastplume Date: Mon, 20 Aug 2018 19:02:44 +0100 Subject: [PATCH] Retrieve header by output commit (#1388) * Add get_header_for_output function to chain * add api call to retrieve associated header for a given output * rustfmt --- api/src/handlers.rs | 66 +++++++++++++++++++++----------- chain/src/chain.rs | 40 ++++++++++++++++++- chain/src/txhashset.rs | 17 ++++---- chain/tests/mine_simple_chain.rs | 49 ++++++++++++++++++++++++ util/src/hex.rs | 7 ++++ 5 files changed, 147 insertions(+), 32 deletions(-) diff --git a/api/src/handlers.rs b/api/src/handlers.rs index 8def8fa0f..2ad468bb0 100644 --- a/api/src/handlers.rs +++ b/api/src/handlers.rs @@ -50,6 +50,31 @@ fn w(weak: &Weak) -> Arc { weak.upgrade().unwrap() } +/// Retrieves an output from the chain given a commit id (a tiny bit iteratively) +fn get_output(chain: &Weak, id: &str) -> Result<(Output, OutputIdentifier), Error> { + let c = util::from_hex(String::from(id)).context(ErrorKind::Argument(format!( + "Not a valid commitment: {}", + id + )))?; + let commit = Commitment::from_vec(c); + + // We need the features here to be able to generate the necessary hash + // to compare against the hash in the output MMR. + // For now we can just try both (but this probably needs to be part of the api + // params) + let outputs = [ + OutputIdentifier::new(OutputFeatures::DEFAULT_OUTPUT, &commit), + OutputIdentifier::new(OutputFeatures::COINBASE_OUTPUT, &commit), + ]; + + for x in outputs.iter() { + if let Ok(_) = w(chain).is_unspent(&x) { + return Ok((Output::new(&commit), x.clone())); + } + } + Err(ErrorKind::NotFound)? +} + // RESTful index of available api endpoints // GET /v1/ struct IndexHandler { @@ -74,27 +99,8 @@ struct OutputHandler { impl OutputHandler { fn get_output(&self, id: &str) -> Result { - let c = util::from_hex(String::from(id)).context(ErrorKind::Argument(format!( - "Not a valid commitment: {}", - id - )))?; - let commit = Commitment::from_vec(c); - - // We need the features here to be able to generate the necessary hash - // to compare against the hash in the output MMR. - // For now we can just try both (but this probably needs to be part of the api - // params) - let outputs = [ - OutputIdentifier::new(OutputFeatures::DEFAULT_OUTPUT, &commit), - OutputIdentifier::new(OutputFeatures::COINBASE_OUTPUT, &commit), - ]; - - for x in outputs.iter() { - if let Ok(_) = w(&self.chain).is_unspent(&x) { - return Ok(Output::new(&commit)); - } - } - Err(ErrorKind::NotFound)? + let res = get_output(&self.chain, id)?; + Ok(res.0) } fn outputs_by_ids(&self, req: &Request) -> Result, Error> { @@ -551,9 +557,10 @@ impl Handler for ChainCompactHandler { } } -/// Gets block headers given either a hash or height. +/// Gets block headers given either a hash or height or an output commit. /// GET /v1/headers/ /// GET /v1/headers/ +/// GET /v1/headers/ /// pub struct HeaderHandler { pub chain: Weak, @@ -561,6 +568,10 @@ pub struct HeaderHandler { impl HeaderHandler { fn get_header(&self, input: String) -> Result { + // will fail quick if the provided isn't a commitment + if let Ok(h) = self.get_header_for_output(input.clone()) { + return Ok(h); + } if let Ok(height) = input.parse() { match w(&self.chain).get_header_by_height(height) { Ok(header) => return Ok(BlockHeaderPrintable::from_header(&header)), @@ -576,11 +587,20 @@ impl HeaderHandler { .context(ErrorKind::NotFound)?; Ok(BlockHeaderPrintable::from_header(&header)) } + + fn get_header_for_output(&self, commit_id: String) -> Result { + let oid = get_output(&self.chain, &commit_id)?.1; + match w(&self.chain).get_header_for_output(&oid) { + Ok(header) => return Ok(BlockHeaderPrintable::from_header(&header)), + Err(_) => return Err(ErrorKind::NotFound)?, + } + } } -/// Gets block details given either a hash or height. +/// Gets block details given either a hash or an unspent commit /// GET /v1/blocks/ /// GET /v1/blocks/ +/// GET /v1/blocks/ /// /// Optionally return results as "compact blocks" by passing "?compact" query /// param GET /v1/blocks/?compact diff --git a/chain/src/chain.rs b/chain/src/chain.rs index 58e3d0008..78b1dbda1 100644 --- a/chain/src/chain.rs +++ b/chain/src/chain.rs @@ -25,7 +25,9 @@ use lmdb; use core::core::hash::{Hash, Hashed}; use core::core::merkle_proof::MerkleProof; use core::core::target::Difficulty; -use core::core::{Block, BlockHeader, Output, OutputIdentifier, Transaction, TxKernel}; +use core::core::{ + Block, BlockHeader, Output, OutputFeatures, OutputIdentifier, Transaction, TxKernel, +}; use core::global; use error::{Error, ErrorKind}; use grin_store::Error::NotFoundErr; @@ -391,7 +393,11 @@ impl Chain { /// work) fork. pub fn is_unspent(&self, output_ref: &OutputIdentifier) -> Result { let mut txhashset = self.txhashset.write().unwrap(); - txhashset.is_unspent(output_ref) + let res = txhashset.is_unspent(output_ref); + match res { + Err(e) => Err(e), + Ok((h, p)) => Ok(h), + } } fn next_block_height(&self) -> Result { @@ -804,6 +810,36 @@ impl Chain { .map_err(|e| ErrorKind::StoreErr(e, "chain get header by height".to_owned()).into()) } + /// Gets the block header in which a given output appears in the txhashset + pub fn get_header_for_output( + &self, + output_ref: &OutputIdentifier, + ) -> Result { + let mut txhashset = self.txhashset.write().unwrap(); + let (_, pos) = txhashset.is_unspent(output_ref)?; + let mut min = 1; + let mut max = { + let h = self.head.lock().unwrap(); + h.height + }; + + loop { + let search_height = max - (max - min) / 2; + let h = self.get_header_by_height(search_height)?; + let h_prev = self.get_header_by_height(search_height - 1)?; + if pos > h.output_mmr_size { + min = search_height; + } else if pos < h_prev.output_mmr_size { + max = search_height; + } else { + if pos == h_prev.output_mmr_size { + return Ok(h_prev); + } + return Ok(h); + } + } + } + /// Verifies the given block header is actually on the current chain. /// Checks the header_by_height index to verify the header is where we say /// it is diff --git a/chain/src/txhashset.rs b/chain/src/txhashset.rs index 89996d18a..dcfeb3445 100644 --- a/chain/src/txhashset.rs +++ b/chain/src/txhashset.rs @@ -130,14 +130,14 @@ impl TxHashSet { /// Check if an output is unspent. /// We look in the index to find the output MMR pos. /// Then we check the entry in the output MMR and confirm the hash matches. - pub fn is_unspent(&mut self, output_id: &OutputIdentifier) -> Result { + pub fn is_unspent(&mut self, output_id: &OutputIdentifier) -> Result<(Hash, u64), Error> { match self.commit_index.get_output_pos(&output_id.commit) { Ok(pos) => { let output_pmmr: PMMR = PMMR::at(&mut self.output_pmmr_h.backend, self.output_pmmr_h.last_pos); if let Some(hash) = output_pmmr.get_hash(pos) { if hash == output_id.hash_with_index(pos - 1) { - Ok(hash) + Ok((hash, pos)) } else { Err(ErrorKind::TxHashSetErr(format!("txhashset hash mismatch")).into()) } @@ -989,8 +989,8 @@ impl<'a> Extension<'a> { { let now = Instant::now(); - let mut commits:Vec = vec![]; - let mut proofs:Vec = vec![]; + let mut commits: Vec = vec![]; + let mut proofs: Vec = vec![]; let mut proof_count = 0; let total_rproofs = pmmr::n_leaves(self.output_pmmr.unpruned_size()); @@ -1132,7 +1132,8 @@ fn check_and_remove_files(txhashset_path: &PathBuf, header: &BlockHeader) -> Res .file_name() .and_then(|n| n.to_str().map(|s| String::from(s))) }) - }).collect(); + }) + .collect(); let dir_difference: Vec = subdirectories_found .difference(&subdirectories_expected) @@ -1161,7 +1162,8 @@ fn check_and_remove_files(txhashset_path: &PathBuf, header: &BlockHeader) -> Res } else { String::from(s) } - }).collect(); + }) + .collect(); let subdirectories = fs::read_dir(txhashset_path)?; for subdirectory in subdirectories { @@ -1174,7 +1176,8 @@ fn check_and_remove_files(txhashset_path: &PathBuf, header: &BlockHeader) -> Res .file_name() .and_then(|n| n.to_str().map(|s| String::from(s))) }) - }).collect(); + }) + .collect(); let difference: Vec = pmmr_files_found .difference(&pmmr_files_expected) .cloned() diff --git a/chain/tests/mine_simple_chain.rs b/chain/tests/mine_simple_chain.rs index a0b153dc0..3810a8698 100644 --- a/chain/tests/mine_simple_chain.rs +++ b/chain/tests/mine_simple_chain.rs @@ -362,6 +362,55 @@ fn spend_in_fork_and_compact() { } } +/// Test ability to retrieve block headers for a given output +#[test] +fn output_header_mappings() { + global::set_mining_mode(ChainTypes::AutomatedTesting); + let chain = setup( + ".grin_header_for_output", + pow::mine_genesis_block().unwrap(), + ); + let keychain = ExtKeychain::from_random_seed().unwrap(); + let mut reward_outputs = vec![]; + + for n in 1..15 { + let prev = chain.head_header().unwrap(); + let difficulty = consensus::next_difficulty(chain.difficulty_iter()).unwrap(); + let pk = keychain.derive_key_id(n as u32).unwrap(); + let reward = libtx::reward::output(&keychain, &pk, 0, prev.height).unwrap(); + reward_outputs.push(reward.0.clone()); + let mut b = core::core::Block::new(&prev, vec![], difficulty.clone(), reward).unwrap(); + b.header.timestamp = prev.timestamp + Duration::seconds(60); + + chain.set_txhashset_roots(&mut b, false).unwrap(); + + let sizeshift = if n == 2 { + global::min_sizeshift() + 1 + } else { + global::min_sizeshift() + }; + b.header.pow.cuckoo_sizeshift = sizeshift; + pow::pow_size(&mut b.header, difficulty, global::proofsize(), sizeshift).unwrap(); + b.header.pow.cuckoo_sizeshift = sizeshift; + + chain.process_block(b, chain::Options::MINE).unwrap(); + + let header_for_output = chain + .get_header_for_output(&OutputIdentifier::from_output(&reward_outputs[n - 1])) + .unwrap(); + assert_eq!(header_for_output.height, n as u64); + + chain.validate(false).unwrap(); + } + + // Check all output positions are as expected + for n in 1..15 { + let header_for_output = chain + .get_header_for_output(&OutputIdentifier::from_output(&reward_outputs[n - 1])) + .unwrap(); + assert_eq!(header_for_output.height, n as u64); + } +} fn prepare_block(kc: &K, prev: &BlockHeader, chain: &Chain, diff: u64) -> Block where K: Keychain, diff --git a/util/src/hex.rs b/util/src/hex.rs index 8d764cf79..fc5c63715 100644 --- a/util/src/hex.rs +++ b/util/src/hex.rs @@ -30,6 +30,13 @@ pub fn to_hex(bytes: Vec) -> String { /// Decode a hex string into bytes. pub fn from_hex(hex_str: String) -> Result, num::ParseIntError> { + if hex_str.len() % 2 == 1 { + // TODO: other way to instantiate a ParseIntError? + let err = ("QQQ").parse::(); + if let Err(e) = err { + return Err(e); + } + } let hex_trim = if &hex_str[..2] == "0x" { hex_str[2..].to_owned() } else {