From 8662086c007a1398d673415dca57c187f548db97 Mon Sep 17 00:00:00 2001 From: Davidson Souza Date: Tue, 3 Oct 2023 13:16:05 -0300 Subject: [PATCH] Update some RPCs to show more useful data Some rpcs are returning just some basic data, and are very different from Core. This commit makes them return more information and for getblock and getrawtransaction, mimic core's response. --- .../src/pruned_utreexo/chain_state.rs | 4 + .../floresta-chain/src/pruned_utreexo/mod.rs | 3 + crates/floresta-cli/README.md | 203 ++++++++++-------- florestad/src/json_rpc/res.rs | 77 +++++++ florestad/src/json_rpc/server.rs | 199 ++++++++++++++++- 5 files changed, 391 insertions(+), 95 deletions(-) diff --git a/crates/floresta-chain/src/pruned_utreexo/chain_state.rs b/crates/floresta-chain/src/pruned_utreexo/chain_state.rs index 422f85c7..180c5706 100644 --- a/crates/floresta-chain/src/pruned_utreexo/chain_state.rs +++ b/crates/floresta-chain/src/pruned_utreexo/chain_state.rs @@ -699,6 +699,10 @@ impl BlockchainInterface for ChainState bool { self.inner.read().ibd } + fn get_block_height(&self, hash: &BlockHash) -> Result, Self::Error> { + self.get_disk_block_header(hash) + .map(|header| header.height()) + } fn get_block_hash(&self, height: u32) -> Result { let inner = self.inner.read(); diff --git a/crates/floresta-chain/src/pruned_utreexo/mod.rs b/crates/floresta-chain/src/pruned_utreexo/mod.rs index f665f179..00e5889a 100644 --- a/crates/floresta-chain/src/pruned_utreexo/mod.rs +++ b/crates/floresta-chain/src/pruned_utreexo/mod.rs @@ -48,7 +48,10 @@ pub trait BlockchainInterface { fn get_validation_index(&self) -> Result; /// Triggers a rescan, downloading (but not validating) all blocks in [start_height:tip] fn rescan(&self, start_height: u32) -> Result<(), Self::Error>; + /// Returns where we are in the rescan fn get_rescan_index(&self) -> Option; + /// Returns the height of a block, given it's hash + fn get_block_height(&self, hash: &BlockHash) -> Result, Self::Error>; } /// [UpdatableChainstate] is a contract that a is expected from a chainstate /// implementation, that wishes to be updated. Using those methods, a backend like the p2p-node, diff --git a/crates/floresta-cli/README.md b/crates/floresta-cli/README.md index 5542a13d..bb09e999 100644 --- a/crates/floresta-cli/README.md +++ b/crates/floresta-cli/README.md @@ -25,56 +25,87 @@ This command takes no params and returns some useful data about the current acti **Args**: None **Return** -```json -{ - "best_block": "A hash of the latest block we know about", - "height": "The position of our current best known block", - "ibd": "Whether we are on Initial Block Download" -} -``` + +`best_block`: The best block we have headers for +`chain`: The name of the current active network(eg: bitcoin, testnet, regtest) +`difficulty`: Current network difficulty +`height`: The height of the best block we have headers for +`ibd`: Whether we are currently in initial block download +`latest_block_time`: The time in which the latest block was mined +`latest_work`: The work of the latest block (e.g the amount of hashes needed to mine it, on average) +`leaf_count`: The amount of leaves in our current forest state +`progress`: The percentage of blocks we have validated so far +`root_count`: The amount of roots in our current forest state +`root_hashes`: The hashes of the roots in our current forest state +`validated`: The amount of blocks we have validated so far +`verification_progress`: The percentage of blocks we have verified so far ### getblockhash Returns the block hash associated with a given height **Args** -``` -height: A numerical identifier for a block -``` + +`height`: A numerical identifier for a block + **Return** -``` -block_hash: A string containing a hex-encoded block hash -``` + +`block_hash`: A string containing a hex-encoded block hash + ### gettxout Returns a cached transaction output. The output itself doesn't have to be ours. But the transaction containing it should be cached by our internal wallet. **Args** -``` -tx_id: A transaction id -vout: A index for the desired output -``` + +`tx_id`: A transaction id +`vout`: A index for the desired output + **Returns** -```json -{ - "value": "The amount of satoshis in this output", - "spk": "The redeem script for this output" -} -``` + +`value`: The amount of satoshis in this output, +`spk`: The redeem script for this output + ### getrawtransaction Returns a transaction data, given its id. The transaction itself doesn't have to be ours. But it should be cached by our internal wallet or in the mempool. **Args** -``` - tx_id: The id of a transaction -``` + +`tx_id`: The id of a transaction + **Returns** -```json -{ - "tx": "A object describing a transaction" -} -``` + + `blockhash`: The hash of the block containing this transaction, if it is in a block + `blocktime`: Time when the block containing this transaction was mined, if it is in a block + `confirmations`: The amount of confirmations this transaction has, if it is in a block + `hash`: The hash of this transaction, a.k.a wtxid + `hex`: The hex-encoded transaction + `in_active_chain`: Whether this transaction is in the active chain + `locktime`: The locktime value of this transaction. + `size`: The size of this transaction in bytes. + `time`: The time when this transaction was mined, if it is in a block + `txid`: The id of this transaction. Only for witness transactions, this is `different` from the wtxid + `version`: The version of this transaction + `vin`: A vector of inputs + `script_sig`: The script signature for this input + `asm`: The disassembled script signature + `hex`: Raw hex-encoded script signature + `sequence`: The nSequence value for this input + `txid`: The id of the transaction containing the output we are spending + `vout`: The index of the output we are spending + `witness`: A vector of witness data + `vout`: A vector of outputs + `n`: The index of this output + `script_pub_key`: The script pubkey for this output + `address`: The address this output pays to, if it's a standard output + `asm`: The disassembled script pubkey + `hex`: Raw hex-encoded script pubkey + `req_sigs`: The amount of signatures required to spend this output (Deprecated) + `type`: The type of this output (e.g pubkeyhash, scripthash, etc) + `value`: The amount of satoshis in this output + `vsize`: The size of this transaction, in virtual bytes + `weight`: The weight of this transaction ### rescan @@ -82,84 +113,91 @@ Tells our node to rescan blocks. This will make our node download all blocks all This rpc is useful if you add another address, descriptor or xpub to our wallet, and you know it have historical transactions that are not indexed yet. **Args** -``` -height: The height we should start -``` + +`height`: The height we should start + **Return** -``` -success: Whether we successfully started rescanning -``` + +`success`: Whether we successfully started rescanning + ### sendrawtransaction Submits a transaction to the network **Args** -``` -tx_hex: A hex-encoded transaction -``` + +`tx_hex`: A hex-encoded transaction + **Return** -``` -tx_id: The transaction id if we succeed -``` + +`tx_id`: The transaction id if we succeed + ### getblockheader Returns the header of a block, giving its hash **Args** -``` -block_hash: The id of a block -``` + +`block_hash`: The id of a block + **Return**: -```json -{ - "block_header": { - "bits": "A compact representation of the block target", - "merkle_root": "The root of a tree formed by all transactions in this block", - "nonce": "The nonce used to mine this block", - "prev_blockhash": "The hash of this block's ancestor", - "time": "The time in which this block was created", - "version": "This block's version" - } -} -``` + +`bits`: A compact representation of the block target +`merkle_root`: The root of a tree formed by all transactions in this block +`nonce`: The nonce used to mine this block +`prev_blockhash`: The hash of this block's ancestor +`time`: The time in which this block was created +`version`: This block's version + ### loaddescriptor Tells our wallet to follow this new descriptor. Optionally, whether we should rescan the blockchain if there's any historical transaction associated with this descriptor. **Args** -``` -descriptor: A output descriptor -``` + +`descriptor`: A output descriptor + **Return** -``` -status: Whether we succeed loading this descriptor -``` + +`status`: Whether we succeed loading this descriptor + ### getroots Returns the roots of our current forest state **Args**: None **Return** -``` -roots: A vec of hashes -``` + +`roots`: A vec of hashes ### getblock Returns a full block, given its hash. Notice that this rpc will cause a actual network request to our node, so it may be slow, and if used too often, may cause more network usage. **Args** -``` -block_hash: The hash of a block -``` -**Return** -```json -{ - "block": "A block object" -} -``` + +`block_hash`: The hash of a block +`bits`: A compact representation of the block target +`chainwork`: The combined work of all blocks in this blockchain +`confirmations`: The amount of confirmations this block has +`difficulty`: This block's difficulty +`hash`: This block's hash +`height`: This block's height +`mediantime`: The median of the timestamps of the last 11 blocks +`merkleroot`: The root of a tree formed by all transactions in this block +`n_tx`: The amount of transactions in this block +`nextblockhash`": The hash of the next block, if any +`nonce`: The nonce used to mine this block +`previousblockhash`": The hash of this block's ancestor +`size`: The size of this block in bytes +`strippedsize`: The size of this block in bytes, excluding witness data +`time`: The time in which this block was created +`tx`: A txid vector of transactions in this block +`version`: This block's version +`versionHex`: This block's version, in hex +`weight`: The weight of this block ### getpeerinfo @@ -168,12 +206,7 @@ Returns a list of peers connected to our node, and some useful information about **Args**: None **Returns** -```json -[ - { - "address": "This peer's network address", - "services": "The services this peer announces as supported", - "user_agent": "A string representing this peer's software", - } -] -``` \ No newline at end of file +`peers`: A vector of peers connected to our node + `address`: This peer's network address + `services`: The services this peer announces as supported + `user_agent`: A string representing this peer's software \ No newline at end of file diff --git a/florestad/src/json_rpc/res.rs b/florestad/src/json_rpc/res.rs index 64380443..9573864e 100644 --- a/florestad/src/json_rpc/res.rs +++ b/florestad/src/json_rpc/res.rs @@ -9,6 +9,83 @@ pub struct GetBlockchainInfoRes { pub height: u32, pub ibd: bool, pub validated: u32, + pub latest_work: String, + pub latest_block_time: u32, + pub leaf_count: u32, + pub root_count: u32, + pub root_hashes: Vec, + pub chain: String, + pub progress: f32, + pub difficulty: u64, +} +#[derive(Deserialize, Serialize)] +pub struct RawTxJson { + pub in_active_chain: bool, + pub hex: String, + pub txid: String, + pub hash: String, + pub size: u32, + pub vsize: u32, + pub weight: u32, + pub version: u32, + pub locktime: u32, + pub vin: Vec, + pub vout: Vec, + pub blockhash: String, + pub confirmations: u32, + pub blocktime: u32, + pub time: u32, +} +#[derive(Deserialize, Serialize)] +pub struct TxOutJson { + pub value: u64, + pub n: u32, + pub script_pub_key: ScriptPubKeyJson, +} +#[derive(Deserialize, Serialize)] +pub struct ScriptPubKeyJson { + pub asm: String, + pub hex: String, + pub req_sigs: u32, + #[serde(rename = "type")] + pub type_: String, + pub address: String, +} +#[derive(Deserialize, Serialize)] +pub struct TxInJson { + pub txid: String, + pub vout: u32, + pub script_sig: ScriptSigJson, + pub sequence: u32, + pub witness: Vec, +} +#[derive(Deserialize, Serialize)] +pub struct ScriptSigJson { + pub asm: String, + pub hex: String, +} +#[derive(Deserialize, Serialize)] +pub struct BlockJson { + pub hash: String, + pub confirmations: u32, + pub strippedsize: usize, + pub size: usize, + pub weight: usize, + pub height: u32, + pub version: i32, + #[serde(rename = "versionHex")] + pub version_hex: String, + pub merkleroot: String, + pub tx: Vec, + pub time: u32, + pub mediantime: u32, + pub nonce: u32, + pub bits: String, + pub difficulty: u64, + pub chainwork: String, + pub n_tx: usize, + pub previousblockhash: String, + pub nextblockhash: Option, } #[derive(Debug)] diff --git a/florestad/src/json_rpc/server.rs b/florestad/src/json_rpc/server.rs index 6064764e..4d0f8bf5 100644 --- a/florestad/src/json_rpc/server.rs +++ b/florestad/src/json_rpc/server.rs @@ -1,14 +1,17 @@ use async_std::sync::RwLock; use bitcoin::{ consensus::{deserialize, serialize}, - hashes::hex::{FromHex, ToHex}, - BlockHash, BlockHeader, Network, Transaction, TxOut, Txid, + hashes::{ + hex::{FromHex, ToHex}, + Hash, + }, + Address, BlockHash, BlockHeader, Network, Script, TxIn, TxOut, Txid, }; use floresta_chain::{ pruned_utreexo::{BlockchainInterface, UpdatableChainstate}, ChainState, KvChainStore, }; -use floresta_watch_only::{kv_database::KvDatabase, AddressCache}; +use floresta_watch_only::{kv_database::KvDatabase, AddressCache, CachedTransaction}; use floresta_wire::node_interface::{NodeInterface, NodeMethods, PeerInfo}; use futures::executor::block_on; use jsonrpc_core::Result; @@ -17,7 +20,10 @@ use jsonrpc_http_server::ServerBuilder; use serde_json::{json, Value}; use std::sync::Arc; -use super::res::{Error, GetBlockchainInfoRes}; +use super::res::{ + BlockJson, Error, GetBlockchainInfoRes, RawTxJson, ScriptPubKeyJson, ScriptSigJson, TxInJson, + TxOutJson, +}; #[rpc] pub trait Rpc { @@ -28,7 +34,7 @@ pub trait Rpc { #[rpc(name = "getblockheader")] fn get_block_header(&self, hash: BlockHash) -> Result; #[rpc(name = "gettransaction")] - fn get_transaction(&self, tx_id: Txid) -> Result; + fn get_transaction(&self, tx_id: Txid, verbosity: Option) -> Result; #[rpc(name = "gettxproof")] fn get_tx_proof(&self, tx_id: Txid) -> Result>; #[rpc(name = "gettxout")] @@ -88,11 +94,32 @@ impl Rpc for RpcImpl { let (height, hash) = self.chain.get_best_block().unwrap(); let validated = self.chain.get_validation_index().unwrap(); let ibd = self.chain.is_in_idb(); + let latest_header = self.chain.get_block_header(&hash).unwrap(); + let latest_work = latest_header.work(); + let latest_block_time = latest_header.time; + let leaf_count = self.chain.acc().leaves as u32; + let root_count = self.chain.acc().roots.len() as u32; + let root_hashes = self + .chain + .acc() + .roots + .into_iter() + .map(|r| r.to_string()) + .collect(); + let validated_blocks = self.chain.get_validation_index().unwrap(); Ok(GetBlockchainInfoRes { best_block: hash.to_string(), height, ibd, validated, + latest_work: latest_work.to_string(), + latest_block_time, + leaf_count, + root_count, + root_hashes, + chain: self.network.to_string(), + difficulty: latest_header.difficulty(self.network), + progress: validated_blocks as f32 / height as f32, }) } @@ -115,13 +142,16 @@ impl Rpc for RpcImpl { Err(Error::BlockNotFound.into()) } - fn get_transaction(&self, tx_id: Txid) -> Result { + fn get_transaction(&self, tx_id: Txid, verbosity: Option) -> Result { let wallet = block_on(self.wallet.read()); - if let Some(tx) = wallet.get_transaction(&tx_id) { - return Ok(tx.tx); + if verbosity == Some(true) { + if let Some(tx) = wallet.get_transaction(&tx_id) { + return Ok(serde_json::to_value(serialize(&tx.tx)).unwrap()); + } + return Err(Error::TxNotFound.into()); } - if let Ok(Some(tx)) = self.node.get_mempool_transaction(tx_id) { - return Ok(tx); + if let Some(tx) = wallet.get_transaction(&tx_id) { + return Ok(serde_json::to_value(self.make_raw_transaction(tx)).unwrap()); } Err(Error::TxNotFound.into()) } @@ -184,6 +214,55 @@ impl Rpc for RpcImpl { let verbosity = verbosity.unwrap_or(1); if let Ok(Some(block)) = self.node.get_block(hash) { if verbosity == 1 { + let tip = self.chain.get_height().map_err(|_| Error::ChainError)?; + let height = self + .chain + .get_block_height(&hash) + .map_err(|_| Error::ChainError)? + .unwrap(); + let mut last_block_times: Vec<_> = ((height - 11)..height) + .into_iter() + .map(|h| { + self.chain + .get_block_header(&self.chain.get_block_hash(h).unwrap()) + .unwrap() + .time + }) + .collect(); + last_block_times.sort(); + + let median_time_past = last_block_times[5]; + + let block = BlockJson { + bits: block.header.bits.to_hex(), + chainwork: block.header.work().to_string(), + confirmations: (tip - height) + 1, + difficulty: block.header.difficulty(self.network), + hash: block.header.block_hash().to_string(), + height: height, + merkleroot: block.header.merkle_root.to_string(), + nonce: block.header.nonce, + previousblockhash: block.header.prev_blockhash.to_string(), + size: block.size(), + time: block.header.time, + tx: block + .txdata + .iter() + .map(|tx| tx.txid().to_string()) + .collect(), + version: block.header.version, + version_hex: format!("{:x}", block.header.version), + weight: block.weight(), + mediantime: median_time_past, + n_tx: block.txdata.len(), + nextblockhash: self + .chain + .get_block_hash(height + 1) + .ok() + .map(|h| h.to_string()), + strippedsize: block.strippedsize(), + }; + return Ok(serde_json::to_value(block).unwrap()); } return Ok(json!(serialize(&block).to_hex())); @@ -225,6 +304,106 @@ impl Rpc for RpcImpl { } } impl RpcImpl { + fn make_vin(&self, input: TxIn) -> TxInJson { + let txid = input.previous_output.txid.to_hex(); + let vout = input.previous_output.vout; + let sequence = input.sequence.0; + TxInJson { + txid, + vout, + script_sig: ScriptSigJson { + asm: input.script_sig.asm(), + hex: input.script_sig.to_hex(), + }, + witness: input.witness.iter().map(|w| w.to_hex()).collect(), + sequence, + } + } + fn get_script_type(script: Script) -> Option<&'static str> { + if script.is_p2pkh() { + return Some("p2pkh"); + } + if script.is_p2sh() { + return Some("p2sh"); + } + if script.is_v0_p2wpkh() { + return Some("v0_p2wpkh"); + } + if script.is_v0_p2wsh() { + return Some("v0_p2wsh"); + } + None + } + fn make_vout(&self, output: TxOut, n: u32) -> TxOutJson { + let value = output.value; + TxOutJson { + value, + n, + script_pub_key: ScriptPubKeyJson { + asm: output.script_pubkey.asm(), + hex: output.script_pubkey.to_hex(), + req_sigs: 0, // This field is deprecated + address: Address::from_script(&output.script_pubkey, self.network) + .map(|a| a.to_string()) + .unwrap(), + type_: Self::get_script_type(output.script_pubkey) + .or(Some("nonstandard")) + .unwrap() + .to_string(), + }, + } + } + fn make_raw_transaction(&self, tx: CachedTransaction) -> RawTxJson { + let raw_tx = tx.tx; + let in_active_chain = tx.height != 0; + let hex = serialize(&raw_tx).to_hex(); + let txid = raw_tx.txid().to_hex(); + let block_hash = self + .chain + .get_block_hash(tx.height) + .unwrap_or(BlockHash::all_zeros()); + let tip = self.chain.get_height().unwrap(); + let confirmations = if in_active_chain { + tip - tx.height + 1 + } else { + 0 + }; + + RawTxJson { + in_active_chain, + hex, + txid, + hash: raw_tx.wtxid().to_hex(), + size: raw_tx.size() as u32, + vsize: raw_tx.vsize() as u32, + weight: raw_tx.weight() as u32, + version: raw_tx.version as u32, + locktime: raw_tx.lock_time.0, + vin: raw_tx + .input + .iter() + .map(|input| self.make_vin(input.clone())) + .collect(), + vout: raw_tx + .output + .into_iter() + .enumerate() + .map(|(i, output)| self.make_vout(output, i as u32)) + .collect(), + blockhash: block_hash.to_hex(), + confirmations, + blocktime: self + .chain + .get_block_header(&block_hash) + .map(|h| h.time) + .unwrap_or(0), + time: self + .chain + .get_block_header(&block_hash) + .map(|h| h.time) + .unwrap_or(0), + } + } fn get_port(net: &Network) -> u16 { match net { Network::Bitcoin => 8332,