diff --git a/docs/src/inscriptions/recursion.md b/docs/src/inscriptions/recursion.md index 5b116f084f..023568c005 100644 --- a/docs/src/inscriptions/recursion.md +++ b/docs/src/inscriptions/recursion.md @@ -25,6 +25,7 @@ The recursive endpoints are: - `/r/blockhash/`: block hash at given block height. - `/r/blockhash`: latest block hash. - `/r/blockheight`: latest block height. +- `/r/blockinfo/`: block info. `` may be a block height or block hash. - `/r/blocktime`: UNIX time stamp of latest block. - `/r/children/`: the first 100 child inscription ids. - `/r/children//`: the set of 100 child inscription ids on ``. @@ -108,3 +109,25 @@ Examples "page":49 } ``` + +- `/r/blockinfo/0`: + +```json +{ + "bits": 486604799, + "chainwork": 0, + "confirmations": 0, + "difficulty": 0.0, + "hash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", + "height": 0, + "median_time": null, + "merkle_root": "0000000000000000000000000000000000000000000000000000000000000000", + "next_block": null, + "nonce": 0, + "previous_block": null, + "target": "00000000ffff0000000000000000000000000000000000000000000000000000", + "timestamp": 0, + "transaction_count": 0, + "version": 1 +} +``` diff --git a/src/lib.rs b/src/lib.rs index a178e1f4b7..e2fdc9449c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,7 +32,7 @@ use { locktime::absolute::LockTime, }, consensus::{self, Decodable, Encodable}, - hash_types::BlockHash, + hash_types::{BlockHash, TxMerkleNode}, hashes::Hash, opcodes, script::{self, Instruction}, @@ -172,6 +172,10 @@ pub fn timestamp(seconds: u32) -> DateTime { Utc.timestamp_opt(seconds.into(), 0).unwrap() } +fn target_as_block_hash(target: bitcoin::Target) -> BlockHash { + BlockHash::from_raw_hash(Hash::from_byte_array(target.to_le_bytes())) +} + fn unbound_outpoint() -> OutPoint { OutPoint { txid: Hash::all_zeros(), diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index b20182b950..faa0c65fad 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -8,8 +8,8 @@ use { crate::{ server_config::ServerConfig, templates::{ - BlockHtml, BlockJson, BlocksHtml, BlocksJson, ChildrenHtml, ChildrenJson, ClockSvg, - CollectionsHtml, HomeHtml, InputHtml, InscriptionHtml, InscriptionJson, + BlockHtml, BlockInfoJson, BlockJson, BlocksHtml, BlocksJson, ChildrenHtml, ChildrenJson, + ClockSvg, CollectionsHtml, HomeHtml, InputHtml, InscriptionHtml, InscriptionJson, InscriptionsBlockHtml, InscriptionsHtml, InscriptionsJson, OutputHtml, OutputJson, PageContent, PageHtml, PreviewAudioHtml, PreviewCodeHtml, PreviewFontHtml, PreviewImageHtml, PreviewMarkdownHtml, PreviewModelHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, @@ -124,6 +124,15 @@ impl Display for StaticHtml { } } +fn chainwork(chainwork: &[u8]) -> u128 { + chainwork + .iter() + .rev() + .enumerate() + .map(|(i, byte)| u128::from(*byte) * 256u128.pow(i.try_into().unwrap())) + .sum() +} + #[derive(Debug, Parser, Clone)] pub struct Server { #[arg( @@ -255,6 +264,7 @@ impl Server { ) .route("/r/blockheight", get(Self::block_height)) .route("/r/blocktime", get(Self::block_time)) + .route("/r/blockinfo/:query", get(Self::block_info)) .route("/r/children/:inscription_id", get(Self::children_recursive)) .route( "/r/children/:inscription_id/:page", @@ -1054,6 +1064,49 @@ impl Server { }) } + async fn block_info( + Extension(index): Extension>, + Path(DeserializeFromStr(query)): Path>, + ) -> ServerResult> { + task::block_in_place(|| { + let hash = match query { + BlockQuery::Hash(hash) => hash, + BlockQuery::Height(height) => index + .block_hash(Some(height))? + .ok_or_not_found(|| format!("block {height}"))?, + }; + + let info = index + .block_header_info(hash)? + .ok_or_not_found(|| format!("block {hash}"))?; + + let header = index + .block_header(hash)? + .ok_or_not_found(|| format!("block {hash}"))?; + + Ok(Json(BlockInfoJson { + bits: header.bits.to_consensus(), + chainwork: chainwork(&info.chainwork), + confirmations: info.confirmations, + difficulty: info.difficulty, + hash, + height: info.height.try_into().unwrap(), + median_time: info + .median_time + .map(|median_time| median_time.try_into().unwrap()), + merkle_root: info.merkle_root, + next_block: info.next_block_hash, + nonce: info.nonce, + previous_block: info.previous_block_hash, + target: target_as_block_hash(header.target()), + timestamp: info.time.try_into().unwrap(), + transaction_count: info.n_tx.try_into().unwrap(), + #[allow(clippy::cast_sign_loss)] + version: info.version.to_consensus() as u32, + })) + }) + } + async fn block_time(Extension(index): Extension>) -> ServerResult { task::block_in_place(|| { Ok( @@ -5180,4 +5233,70 @@ next server.assert_response(format!("/preview/{id}"), StatusCode::OK, "foo"); } + + #[test] + fn chainwork_conversion_to_integer() { + assert_eq!(chainwork(&[]), 0); + assert_eq!(chainwork(&[1]), 1); + assert_eq!(chainwork(&[1, 0]), 256); + assert_eq!(chainwork(&[1, 1]), 257); + assert_eq!(chainwork(&[1, 0, 0]), 65536); + assert_eq!(chainwork(&[1, 0, 1]), 65537); + assert_eq!(chainwork(&[1, 1, 1]), 65793); + } + + #[test] + fn block_info() { + let server = TestServer::new(); + + pretty_assert_eq!( + server.get_json::("/r/blockinfo/0"), + BlockInfoJson { + bits: 486604799, + chainwork: 0, + confirmations: 0, + difficulty: 0.0, + hash: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" + .parse() + .unwrap(), + height: 0, + median_time: None, + merkle_root: TxMerkleNode::all_zeros(), + next_block: None, + nonce: 0, + previous_block: None, + target: "00000000ffff0000000000000000000000000000000000000000000000000000" + .parse() + .unwrap(), + timestamp: 0, + transaction_count: 0, + version: 1, + }, + ); + + server.mine_blocks(1); + + pretty_assert_eq!( + server.get_json::("/r/blockinfo/1"), + BlockInfoJson { + bits: 0, + chainwork: 0, + confirmations: 0, + difficulty: 0.0, + hash: "56d05060a0280d0712d113f25321158747310ece87ea9e299bde06cf385b8d85" + .parse() + .unwrap(), + height: 1, + median_time: None, + merkle_root: TxMerkleNode::all_zeros(), + next_block: None, + nonce: 0, + previous_block: None, + target: BlockHash::all_zeros(), + timestamp: 0, + transaction_count: 0, + version: 1, + }, + ) + } } diff --git a/src/templates.rs b/src/templates.rs index 5c617becb4..39642b4210 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -1,7 +1,7 @@ use {super::*, boilerplate::Boilerplate}; pub(crate) use { - block::{BlockHtml, BlockJson}, + block::{BlockHtml, BlockInfoJson, BlockJson}, blocks::{BlocksHtml, BlocksJson}, children::{ChildrenHtml, ChildrenJson}, clock::ClockSvg, diff --git a/src/templates/block.rs b/src/templates/block.rs index 52ae3d6c81..fb4bfba846 100644 --- a/src/templates/block.rs +++ b/src/templates/block.rs @@ -1,9 +1,5 @@ use super::*; -fn target_as_block_hash(target: bitcoin::Target) -> BlockHash { - BlockHash::from_raw_hash(Hash::from_byte_array(target.to_le_bytes())) -} - #[derive(Boilerplate)] pub(crate) struct BlockHtml { hash: BlockHash, @@ -61,6 +57,25 @@ impl BlockJson { } } +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct BlockInfoJson { + pub bits: u32, + pub chainwork: u128, + pub confirmations: i32, + pub difficulty: f64, + pub hash: BlockHash, + pub height: u32, + pub median_time: Option, + pub merkle_root: TxMerkleNode, + pub next_block: Option, + pub nonce: u32, + pub previous_block: Option, + pub target: BlockHash, + pub timestamp: u64, + pub transaction_count: u64, + pub version: u32, +} + impl PageContent for BlockHtml { fn title(&self) -> String { format!("Block {}", self.height)