From 755925daed99b6f3dc23df980cd0dac9e318f1c1 Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Mon, 1 Nov 2021 11:39:03 +0000 Subject: [PATCH] Add the `/headers[/:from_hash[/:count]]` endpoint Add a new endpoint to download headers in bulk, up to 2000 with a single request. Headers are returned in binary form, with the VarInt-encoded number of items in the body preceeding them. By default `from_hash` is the current best hash, but a different starting block can be specified. The returned list goes "backwards" returning the `count - 1` blocks *before* `from_hash` plus the header of `from_hash` itself. This allows caching the response indefinitely, since it's guaranteed that the headers that come before a given block will never change. Returns an error if `from_hash` is not a valid block or it isn't found in the blockchain. If `count` is greater than the limit of `2000` it will be silently capped to said value. --- src/new_index/schema.rs | 27 +++++++++++++++++++++++++++ src/rest.rs | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/src/new_index/schema.rs b/src/new_index/schema.rs index 120eae454..972bbfac0 100644 --- a/src/new_index/schema.rs +++ b/src/new_index/schema.rs @@ -424,6 +424,33 @@ impl ChainQuery { } } + pub fn get_headers(&self, from: &BlockHash, count: usize) -> Option> { + let _timer = self.start_timer("get_headers"); + + if let Some(from_header) = self + .store + .indexed_headers + .read() + .unwrap() + .header_by_blockhash(from) + { + Some( + self.store + .indexed_headers + .read() + .unwrap() + .iter() + .rev() + .skip(self.best_height() - from_header.height()) + .take(count) + .map(|e| e.header().clone()) + .collect(), + ) + } else { + None + } + } + pub fn get_block_header(&self, hash: &BlockHash) -> Option { let _timer = self.start_timer("get_block_header"); Some(self.header_by_hash(hash)?.header().clone()) diff --git a/src/rest.rs b/src/rest.rs index 00a971df8..46eb85d49 100644 --- a/src/rest.rs +++ b/src/rest.rs @@ -627,6 +627,40 @@ fn handle_request( path.get(3), path.get(4), ) { + (&Method::GET, Some(&"headers"), hash, count, None, None) => { + let count = count + .and_then(|c| c.parse::().ok()) + .and_then(|c| match c { + 0 => Some(1), + 1..=2000 => Some(c), + _ => None, + }) + .unwrap_or(2000); + let from_hash = hash + .map(|h| BlockHash::from_hex(h)) + .transpose()? + .unwrap_or_else(|| query.chain().best_hash()); + + let headers = match query.chain().get_headers(&from_hash, count) { + Some(headers) => headers, + None => return Err(HttpError::not_found("Block not found".to_string())), + }; + + let mut raw = Vec::with_capacity(8 + 80 * headers.len()); + raw.append(&mut encode::serialize( + &encode::VarInt(headers.len() as u64), + )); + for header in headers.iter() { + raw.append(&mut encode::serialize(header)); + } + + Ok(Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "application/octet-stream") + .header("Cache-Control", format!("public, max-age={:}", TTL_LONG)) + .body(Body::from(raw)) + .unwrap()) + } (&Method::GET, Some(&"blocks"), Some(&"tip"), Some(&"hash"), None, None) => http_message( StatusCode::OK, query.chain().best_hash().to_hex(),