diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index c830e1766..200f63e6e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -43,10 +43,16 @@ jobs: - name: Run cargo fmt run: cargo +nightly fmt --all --check - - name: Run cargo doc + - name: Run cargo doc (each crate) run: | - RUSTDOCFLAGS="--cfg docsrs -D warnings" \ - cargo +nightly doc --workspace --no-deps --lib --all-features --document-private-items + #!/usr/bin/env bash + set -e + CRATES=$(cargo metadata --format-version 1 --no-deps | jq -r '.packages[] | select(.source == null and (.targets | map(.kind | contains(["lib"])) | any)) | .name' | tr '\n' ' ') + echo "Building docs for crates: $CRATES" + for crate in $CRATES; do + echo "Running cargo doc for: $crate" + RUSTDOCFLAGS="--cfg docsrs -D warnings" cargo +nightly doc -p "$crate" --no-deps --lib --all-features --document-private-items || exit 1 + done - name: Run cargo clippy run: ./contrib/feature_matrix.sh clippy '-- -D warnings' @@ -131,9 +137,11 @@ jobs: ${{ runner.os }}-cargo-${{ env.CACHE_VERSION }}- ${{ runner.os }}-cargo- - # Build only the binaries + # Build only the binaries (florestad and floresta-cli separately to avoid feature conflicts) - name: Build binaries - run: cargo build --release --bins --verbose + run: | + cargo build --release -p florestad --verbose + cargo build --release -p floresta-cli --verbose # Run the feature testing script - name: Run feature tests diff --git a/Cargo.lock b/Cargo.lock index 79a0a3792..269fcb5fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1148,13 +1148,13 @@ version = "0.9.0" dependencies = [ "axum", "bitcoin", - "corepc-types", "dns-lookup", "floresta-chain", "floresta-common", "floresta-compact-filters", "floresta-electrum", "floresta-mempool", + "floresta-rpc", "floresta-watch-only", "floresta-wire", "libc", @@ -1181,6 +1181,7 @@ dependencies = [ "clap", "corepc-types", "jsonrpc", + "maybe_async", "rand", "rcgen", "serde", @@ -1893,6 +1894,16 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "maybe_async" +version = "0.1.0" +source = "git+https://github.com/Davidson-Souza/maybe-async2.git?branch=master#84c6c3afcf377e1b5a21387a71a43bd27453fdfb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "memchr" version = "2.8.0" diff --git a/Dockerfile b/Dockerfile index 716d8322a..0f93c417c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,11 +28,14 @@ COPY fuzz/ fuzz/ COPY metrics/ metrics/ COPY doc/ doc/ RUN --mount=type=cache,target=/usr/local/cargo/registry \ + echo "Building florestad..." && \ if [ -n "$BUILD_FEATURES" ]; then \ - cargo build --release --features "$BUILD_FEATURES"; \ + cargo build --release -p florestad --features "$BUILD_FEATURES"; \ else \ - cargo build --release; \ - fi + cargo build --release -p florestad; \ + fi && \ + echo "Building floresta-cli..." && \ + cargo build --release -p floresta-cli FROM debian:13.2-slim@sha256:18764e98673c3baf1a6f8d960b5b5a1ec69092049522abac4e24a7726425b016 diff --git a/bin/floresta-cli/src/main.rs b/bin/floresta-cli/src/main.rs index 346d13882..059d9584c 100644 --- a/bin/floresta-cli/src/main.rs +++ b/bin/floresta-cli/src/main.rs @@ -10,7 +10,11 @@ use bitcoin::Txid; use clap::Parser; use clap::Subcommand; use floresta_rpc::jsonrpc_client::Client; -use floresta_rpc::rpc::FlorestaRPC; +use floresta_rpc::rpc_interfaces::BlockchainRpc; +use floresta_rpc::rpc_interfaces::ControlRpc; +use floresta_rpc::rpc_interfaces::NetworkRpc; +use floresta_rpc::rpc_interfaces::RawTransactionRpc; +use floresta_rpc::rpc_interfaces::WalletRpc; use floresta_rpc::rpc_types::AddNodeCommand; use floresta_rpc::rpc_types::RescanConfidence; @@ -63,24 +67,24 @@ fn do_request(cmd: &Cli, client: Client) -> anyhow::Result { Methods::GetBestBlockHash => serde_json::to_string_pretty(&client.get_best_block_hash()?)?, Methods::GetBlockCount => serde_json::to_string_pretty(&client.get_block_count()?)?, Methods::GetTxOut { txid, vout } => { - serde_json::to_string_pretty(&client.get_tx_out(txid, vout)?)? + serde_json::to_string_pretty(&client.get_tx_out(txid, vout, false)?)? } Methods::GetTxOutProof { txids, blockhash } => { - serde_json::to_string_pretty(&client.get_txout_proof(txids, blockhash))? + serde_json::to_string_pretty(&client.get_txout_proof(&txids, blockhash)?)? } - Methods::GetTransaction { txid, .. } => { - serde_json::to_string_pretty(&client.get_transaction(txid, Some(true))?)? + Methods::GetTransaction { txid, verbose } => { + serde_json::to_string_pretty(&client.get_raw_transaction(txid, verbose)?)? } Methods::RescanBlockchain { start_block, stop_block, use_timestamp, confidence, - } => serde_json::to_string_pretty(&client.rescanblockchain( + } => serde_json::to_string_pretty(&client.rescan_blockchain( Some(start_block), Some(stop_block), use_timestamp, - confidence, + Some(confidence), )?)?, Methods::SendRawTransaction { tx } => { serde_json::to_string_pretty(&client.send_raw_transaction(tx)?)? @@ -215,7 +219,7 @@ pub enum Methods { /// Returns the transaction, assuming it is cached by our watch only wallet #[command(name = "gettransaction")] - GetTransaction { txid: Txid, verbose: Option }, + GetTransaction { txid: Txid, verbose: Option }, #[doc = include_str!("../../../doc/rpc/rescanblockchain.md")] #[command( @@ -362,7 +366,7 @@ pub enum Methods { )] DisconnectNode { node_address: String, - node_id: Option, + node_id: Option, }, #[command(name = "findtxout")] diff --git a/bin/florestad/docs/tutorial(EN).md b/bin/florestad/docs/tutorial(EN).md index 678313b70..2927e9576 100644 --- a/bin/florestad/docs/tutorial(EN).md +++ b/bin/florestad/docs/tutorial(EN).md @@ -30,7 +30,9 @@ cd Floresta/ compile with: ```bash -cargo build --release +# Build florestad and floresta-cli separately +cargo build --release -p florestad +cargo build --release -p floresta-cli ``` if everything is ok, it will compile the program and save the executable in `./target/release/`. @@ -83,5 +85,3 @@ descriptors = [ ### Screenshot of Program Running ![A screenshot of logs from a Floresta instance running in a terminal on a GNU/Linux distribution](./assets/Screenshot_ibd.jpg) - - diff --git a/bin/florestad/docs/tutorial(PT-BR).md b/bin/florestad/docs/tutorial(PT-BR).md index d387bc888..2e0a45fb0 100644 --- a/bin/florestad/docs/tutorial(PT-BR).md +++ b/bin/florestad/docs/tutorial(PT-BR).md @@ -30,7 +30,9 @@ cd Floresta/ compile com: ```bash -cargo build --release +# Build florestad and floresta-cli separately +cargo build --release -p florestad +cargo build --release -p floresta-cli ``` se tudo estiver ok, irá compilar o programa e salvar o executável em `./target/release/`. diff --git a/contrib/install.sh b/contrib/install.sh index 0cde43d41..af94d6902 100644 --- a/contrib/install.sh +++ b/contrib/install.sh @@ -359,15 +359,19 @@ build_floresta() { echo "🌳 Extracting floresta $tarDest to /tmp..." tar -xzf $tarDest -C /tmp - echo "🦀 Building florestad and floresta-cli $defaultTag..." + echo "🦀 Building florestad $defaultTag..." cd $bdlDir RUSTFLAGS="-C link-arg=-fuse-ld=mold -C target-cpu=native" cargo build --release \ - --bin florestad \ - --bin floresta-cli \ + -p florestad \ --features json-rpc \ --locked + echo "🦀 Building floresta-cli $defaultTag..." + cargo build --release \ + -p floresta-cli \ + --locked + echo "🌳 Copying binaries to /usr/local/bin (need sudo)..." sudo install -m 0755 -t /usr/local/bin/ $bdlDir/target/release/florestad sudo install -m 0755 -t /usr/local/bin/ $bdlDir/target/release/floresta-cli diff --git a/crates/floresta-node/Cargo.toml b/crates/floresta-node/Cargo.toml index 7ecade4c2..6936cc9cb 100644 --- a/crates/floresta-node/Cargo.toml +++ b/crates/floresta-node/Cargo.toml @@ -9,7 +9,6 @@ authors = ["Floresta Developers "] [dependencies] axum = { workspace = true, optional = true } bitcoin = { workspace = true } -corepc-types = { workspace = true } dns-lookup = { workspace = true } miniscript = { workspace = true, features = ["std"] } rcgen = { workspace = true } @@ -31,6 +30,7 @@ floresta-electrum = { workspace = true } floresta-mempool = { workspace = true } floresta-watch-only = { workspace = true } floresta-wire = { workspace = true } +floresta-rpc = { workspace = true, features = ["async"] } metrics = { workspace = true, optional = true } [dev-dependencies] diff --git a/crates/floresta-node/src/json_rpc/blockchain.rs b/crates/floresta-node/src/json_rpc/blockchain.rs index 8254e2f90..0b3ef1190 100644 --- a/crates/floresta-node/src/json_rpc/blockchain.rs +++ b/crates/floresta-node/src/json_rpc/blockchain.rs @@ -14,29 +14,26 @@ use bitcoin::Script; use bitcoin::ScriptBuf; use bitcoin::Txid; use bitcoin::VarInt; -use corepc_types::v29::GetTxOut; -use corepc_types::v30::GetBlockHeaderVerbose; -use corepc_types::v30::GetBlockVerboseOne; -use corepc_types::ScriptPubkey; use floresta_chain::extensions::HeaderExt; use floresta_chain::extensions::WorkExt; +use floresta_rpc::rpc_interfaces::BlockchainRpc; +use floresta_rpc::rpc_types::GetBlockHeaderRes; +use floresta_rpc::rpc_types::GetBlockHeaderVerbose; +use floresta_rpc::rpc_types::GetBlockRes; +use floresta_rpc::rpc_types::GetBlockVerboseOne; +use floresta_rpc::rpc_types::GetBlockchainInfoRes; +use floresta_rpc::rpc_types::GetTxOut; +use floresta_rpc::rpc_types::GetTxOutProof; +use floresta_rpc::rpc_types::ScriptPubkey; use miniscript::descriptor::checksum; -use serde_json::json; -use serde_json::Value; -use tracing::debug; -use super::res::GetBlockHeaderRes; -use super::res::GetBlockchainInfoRes; -use super::res::GetTxOutProof; use super::res::JsonRpcError; use super::server::RpcChain; use super::server::RpcImpl; -use crate::json_rpc::res::GetBlockRes; -use crate::json_rpc::res::RescanConfidence; impl RpcImpl { async fn get_block_inner(&self, hash: BlockHash) -> Result { - let is_genesis = self.chain.get_block_hash(0).unwrap().eq(&hash); + let is_genesis = self.get_block_hash_inner(0)?.eq(&hash); if is_genesis { return Ok(genesis_block(self.network)); @@ -55,270 +52,18 @@ impl RpcImpl { .wallet .get_height(txid) .ok_or(JsonRpcError::TxNotFound)?; - let blockhash = self.chain.get_block_hash(height).unwrap(); + let blockhash = self.get_block_hash_inner(height)?; self.chain .get_block(&blockhash) .map_err(|_| JsonRpcError::BlockNotFound) } - pub fn get_rescan_interval( - &self, - use_timestamp: bool, - start: Option, - stop: Option, - confidence: Option, - ) -> Result<(u32, u32), JsonRpcError> { - let start = start.unwrap_or(0u32); - let stop = stop.unwrap_or(0u32); - - if use_timestamp { - let confidence = confidence.unwrap_or(RescanConfidence::Medium); - // `get_block_height_by_timestamp` already does the time validity checks. - - let start_height = self.get_block_height_by_timestamp(start, &confidence)?; - - let stop_height = self.get_block_height_by_timestamp(stop, &RescanConfidence::Exact)?; - - return Ok((start_height, stop_height)); - } - - let (tip, _) = self - .chain - .get_best_block() - .map_err(|_| JsonRpcError::Chain)?; - - if stop > tip { - return Err(JsonRpcError::InvalidRescanVal); - } - - Ok((start, stop)) - } - - /// Retrieves the height of the block that was mined in the given timestamp. - /// - /// `timestamp` has an alias, 0 will directly refer to the network's genesis timestamp. - pub fn get_block_height_by_timestamp( - &self, - timestamp: u32, - confidence: &RescanConfidence, - ) -> Result { - /// Simple helper to avoid code reuse. - fn get_block_time( - provider: &RpcImpl, - at: u32, - ) -> Result { - let hash = provider.get_block_hash(at)?; - let block = provider.get_block_header_inner(hash)?; - Ok(block.time) - } - - let genesis_timestamp = genesis_block(self.network).header.time; - - if timestamp == 0 || timestamp == genesis_timestamp { - return Ok(0); - }; - - let (tip_height, _) = self - .chain - .get_best_block() - .map_err(|_| JsonRpcError::BlockNotFound)?; - - let tip_time = get_block_time(self, tip_height)?; - - if timestamp < genesis_timestamp || timestamp > tip_time { - return Err(JsonRpcError::InvalidTimestamp); - } - - let adjusted_target = timestamp.saturating_sub(confidence.as_secs()); - - let mut high = tip_height; - let mut low = 0; - let max_iters = tip_height.ilog2() + 1; - for _ in 0..max_iters { - let cut = (high + low) / 2; - - let block_timestamp = get_block_time(self, cut)?; - - if block_timestamp == adjusted_target { - debug!("found a precise block; returning {cut}"); - return Ok(cut); - } - - if high - low <= 2 { - debug!("didn't find a precise block; returning {low}"); - return Ok(low); - } - - if block_timestamp > adjusted_target { - high = cut; - } else { - low = cut; - } - } - - // This is pretty much unreachable. - Err(JsonRpcError::BlockNotFound) - } -} - -// blockchain rpcs -impl RpcImpl { - // dumputxoutset - - // getbestblockhash - pub(super) fn get_best_block_hash(&self) -> Result { - Ok(self.chain.get_best_block().unwrap().1) - } - - // getblock - pub(super) async fn get_block( - &self, - hash: BlockHash, - verbosity: u8, - ) -> Result { - let block = self.get_block_inner(hash).await?; - - if verbosity == 0 { - let hex = serialize_hex(&block); - - return Ok(GetBlockRes::Zero(hex)); - } - if verbosity == 1 { - let header_fields = self.get_block_header_verbose_inner(&block)?; - - // Stripped size is the size of the block without witness data - // Header + VarInt for number of transactions + sum of base sizes of each transaction - let tx_count_varint_size = VarInt::from(block.txdata.len()).size(); - let total_tx_base_size: usize = block.txdata.iter().map(|tx| tx.base_size()).sum(); - let stripped_size_bytes = Header::SIZE + tx_count_varint_size + total_tx_base_size; - - let stripped_size = Some(stripped_size_bytes.try_into()?); - - let tx = block - .txdata - .iter() - .map(|tx| tx.compute_txid().to_string()) - .collect(); - - let block = GetBlockVerboseOne { - bits: header_fields.bits, - chain_work: header_fields.chain_work, - confirmations: header_fields.confirmations, - difficulty: header_fields.difficulty, - hash: header_fields.hash, - height: header_fields.height, - merkle_root: header_fields.merkle_root, - nonce: header_fields.nonce, - previous_block_hash: header_fields.previous_block_hash, - size: block.total_size().try_into()?, - time: header_fields.time, - tx, - version: header_fields.version, - version_hex: header_fields.version_hex, - weight: block.weight().to_wu(), - median_time: Some(header_fields.median_time), - n_tx: header_fields.n_tx.into(), - next_block_hash: header_fields.next_block_hash, - stripped_size, - target: header_fields.target, - }; - - return Ok(GetBlockRes::One(Box::new(block))); - } - Err(JsonRpcError::InvalidVerbosityLevel) - } - - // getblockchaininfo - pub(super) fn get_blockchain_info(&self) -> Result { - let (height, hash) = self.chain.get_best_block().unwrap(); - let validated = self.chain.get_validation_index().unwrap(); - let ibd = self.chain.is_in_ibd(); - let latest_header = self.chain.get_block_header(&hash).unwrap(); - let latest_work = latest_header - .calculate_chain_work(&self.chain)? - .to_string_hex(); - 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(); - - let validated_percentage = if height != 0 { - validated_blocks as f32 / height as f32 - } else { - 0.0 - }; - - Ok(GetBlockchainInfoRes { - best_block: hash.to_string(), - height, - ibd, - validated, - latest_work, - latest_block_time, - leaf_count, - root_count, - root_hashes, - chain: self.network.to_string(), - difficulty: latest_header.difficulty(self.chain.get_params()) as u64, - progress: validated_percentage, - }) - } - - // getblockcount - pub(super) fn get_block_count(&self) -> Result { - Ok(self.chain.get_height().unwrap()) - } - - // getblockfilter - // getblockfrompeer (just call getblock) - - // getblockhash - pub(super) fn get_block_hash(&self, height: u32) -> Result { + pub(super) fn get_block_hash_inner(&self, height: u32) -> Result { self.chain .get_block_hash(height) .map_err(|_| JsonRpcError::BlockNotFound) } - // getblockheader - pub(super) async fn get_block_header( - &self, - hash: BlockHash, - verbosity: bool, - ) -> Result { - let header = self.get_block_header_inner(hash)?; - - if !verbosity { - let hex = serialize_hex(&header); - return Ok(GetBlockHeaderRes::Raw(hex)); - } - - let block = self.get_block_inner(hash).await?; - - let get_block_header = self.get_block_header_verbose_inner(&block)?; - - Ok(GetBlockHeaderRes::Verbose(Box::new(get_block_header))) - } - - // getblockstats - // getchainstates - // getchaintips - // getchaintxstats - // getdeploymentinfo - // getdifficulty - // getmempoolancestors - // getmempooldescendants - // getmempoolentry - // getmempoolinfo - // getrawmempool - /// Same as `get_block_header_inner` but verbose. fn get_block_header_verbose_inner( &self, @@ -362,7 +107,7 @@ impl RpcImpl { } /// Helper method to get a block header by its hash, used by multiple rpcs. - fn get_block_header_inner(&self, hash: BlockHash) -> Result { + pub(super) fn get_block_header_inner(&self, hash: BlockHash) -> Result { self.chain .get_block_header(&hash) .map_err(|_| JsonRpcError::BlockNotFound) @@ -515,9 +260,229 @@ impl RpcImpl { Ok(asm.join(" ")) } +} + +// blockchain rpcs +impl BlockchainRpc for RpcImpl { + type Error = JsonRpcError; + + // floresta flavored rpcs. These are not part of the bitcoin rpc spec + // findtxout + async fn find_tx_out( + &self, + txid: Txid, + vout: u32, + script: String, + height: u32, + ) -> Result, JsonRpcError> { + if let Ok(Some(txout)) = self.get_tx_out(txid, vout, false).await { + return Ok(Some(txout)); + } + + // if we are on IBD, we don't have any filters to find this txout. + if self.chain.is_in_ibd() { + return Err(JsonRpcError::InInitialBlockDownload); + } + + // can't proceed without block filters + let Some(cfilters) = self.block_filter_storage.as_ref() else { + return Err(JsonRpcError::NoBlockFilters); + }; + + let script = ScriptBuf::from_hex(&script).map_err(|_| JsonRpcError::InvalidScript)?; + self.wallet.cache_address(script.clone()); + let filter_key = script.to_bytes(); + let candidates = cfilters + .match_any( + vec![filter_key.as_slice()], + Some(height), + None, + self.chain.clone(), + ) + .map_err(|e| JsonRpcError::Filters(e.to_string()))?; + + for candidate in candidates { + let candidate = self.node.get_block(candidate).await; + let candidate = match candidate { + Err(e) => { + return Err(JsonRpcError::Node(e.to_string())); + } + Ok(None) => { + return Err(JsonRpcError::Node(format!( + "BUG: block {candidate:?} is a match in our filters, but we can't get it?" + ))); + } + Ok(Some(candidate)) => candidate, + }; + + let Ok(Some(height)) = self.chain.get_block_height(&candidate.block_hash()) else { + return Err(JsonRpcError::BlockNotFound); + }; + + self.wallet.block_process(&candidate, height); + } + + self.get_tx_out(txid, vout, false).await + } + + // dumputxoutset + + // getbestblockhash + async fn get_best_block_hash(&self) -> Result { + Ok(self.chain.get_best_block().unwrap().1) + } + + // getblock + async fn get_block( + &self, + hash: BlockHash, + verbosity: Option, + ) -> Result { + let block = self.get_block_inner(hash).await?; + let verbosity = verbosity.unwrap_or(1); + + if verbosity == 0 { + let hex = serialize_hex(&block); + + return Ok(GetBlockRes::Zero(hex)); + } + if verbosity == 1 { + let header_fields = self.get_block_header_verbose_inner(&block)?; + + // Stripped size is the size of the block without witness data + // Header + VarInt for number of transactions + sum of base sizes of each transaction + let tx_count_varint_size = VarInt::from(block.txdata.len()).size(); + let total_tx_base_size: usize = block.txdata.iter().map(|tx| tx.base_size()).sum(); + let stripped_size_bytes = Header::SIZE + tx_count_varint_size + total_tx_base_size; + + let stripped_size = Some(stripped_size_bytes.try_into()?); + + let tx = block + .txdata + .iter() + .map(|tx| tx.compute_txid().to_string()) + .collect(); + + let block = GetBlockVerboseOne { + bits: header_fields.bits, + chain_work: header_fields.chain_work, + confirmations: header_fields.confirmations, + difficulty: header_fields.difficulty, + hash: header_fields.hash, + height: header_fields.height, + merkle_root: header_fields.merkle_root, + nonce: header_fields.nonce, + previous_block_hash: header_fields.previous_block_hash, + size: block.total_size().try_into()?, + time: header_fields.time, + tx, + version: header_fields.version, + version_hex: header_fields.version_hex, + weight: block.weight().to_wu(), + median_time: Some(header_fields.median_time), + n_tx: header_fields.n_tx.into(), + next_block_hash: header_fields.next_block_hash, + stripped_size, + target: header_fields.target, + }; + + return Ok(GetBlockRes::One(Box::new(block))); + } + Err(JsonRpcError::InvalidVerbosityLevel) + } + + // getblockchaininfo + async fn get_blockchain_info(&self) -> Result { + let (height, hash) = self.chain.get_best_block().unwrap(); + let validated = self.chain.get_validation_index().unwrap(); + let ibd = self.chain.is_in_ibd(); + let latest_header = self.chain.get_block_header(&hash).unwrap(); + let latest_work = latest_header + .calculate_chain_work(&self.chain)? + .to_string_hex(); + 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(); + + let validated_percentage = if height != 0 { + validated_blocks as f32 / height as f32 + } else { + 0.0 + }; + + Ok(GetBlockchainInfoRes { + best_block: hash.to_string(), + height, + ibd, + validated, + latest_work, + latest_block_time, + leaf_count, + root_count, + root_hashes, + chain: self.network.to_string(), + difficulty: latest_header.difficulty(self.chain.get_params()) as u64, + progress: validated_percentage, + }) + } + + // getblockcount + async fn get_block_count(&self) -> Result { + Ok(self.chain.get_height().unwrap()) + } + + // getblockfilter + // getblockfrompeer (just call getblock) + + // getblockhash + async fn get_block_hash(&self, height: u32) -> Result { + self.get_block_hash_inner(height) + } + + // getblockheader + async fn get_block_header( + &self, + hash: BlockHash, + verbosity: Option, + ) -> Result { + let header = self.get_block_header_inner(hash)?; + let verbosity = verbosity.unwrap_or(true); + + if !verbosity { + let hex = serialize_hex(&header); + return Ok(GetBlockHeaderRes::Raw(hex)); + } + + let block = self.get_block_inner(hash).await?; + + let get_block_header = self.get_block_header_verbose_inner(&block)?; + + Ok(GetBlockHeaderRes::Verbose(Box::new(get_block_header))) + } + + // getblockstats + // getchainstates + // getchaintips + // getchaintxstats + // getdeploymentinfo + // getdifficulty + // getmempoolancestors + // getmempooldescendants + // getmempoolentry + // getmempoolinfo + // getrawmempool /// gettxout: returns details about an unspent transaction output. - pub(super) fn get_tx_out( + async fn get_tx_out( &self, txid: Txid, outpoint: u32, @@ -583,7 +548,7 @@ impl RpcImpl { /// watch-only wallet which may not have the transaction cached. /// /// Not finding one of the specified transactions will raise [`JsonRpcError::TxNotFound`]. - pub(super) async fn get_txout_proof( + async fn get_txout_proof( &self, tx_ids: &[Txid], blockhash: Option, @@ -638,79 +603,9 @@ impl RpcImpl { // verifychain // verifytxoutproof - // floresta flavored rpcs. These are not part of the bitcoin rpc spec - // findtxout - pub(super) async fn find_tx_out( - &self, - txid: Txid, - vout: u32, - script: ScriptBuf, - height: u32, - ) -> Result { - if let Some(txout) = self.wallet.get_utxo(&OutPoint { txid, vout }) { - return Ok(serde_json::to_value(txout).unwrap()); - } - - // if we are on IBD, we don't have any filters to find this txout. - if self.chain.is_in_ibd() { - return Err(JsonRpcError::InInitialBlockDownload); - } - - // can't proceed without block filters - let Some(cfilters) = self.block_filter_storage.as_ref() else { - return Err(JsonRpcError::NoBlockFilters); - }; - - self.wallet.cache_address(script.clone()); - let filter_key = script.to_bytes(); - let candidates = cfilters - .match_any( - vec![filter_key.as_slice()], - Some(height), - None, - self.chain.clone(), - ) - .map_err(|e| JsonRpcError::Filters(e.to_string()))?; - - for candidate in candidates { - let candidate = self.node.get_block(candidate).await; - let candidate = match candidate { - Err(e) => { - return Err(JsonRpcError::Node(e.to_string())); - } - Ok(None) => { - return Err(JsonRpcError::Node(format!( - "BUG: block {candidate:?} is a match in our filters, but we can't get it?" - ))); - } - Ok(Some(candidate)) => candidate, - }; - - let Ok(Some(height)) = self.chain.get_block_height(&candidate.block_hash()) else { - return Err(JsonRpcError::BlockNotFound); - }; - - self.wallet.block_process(&candidate, height); - } - - let val = match self.get_tx_out(txid, vout, false)? { - Some(gettxout) => json!(gettxout), - None => json!({}), - }; - Ok(val) - } - // getroots - pub(super) fn get_roots(&self) -> Result, JsonRpcError> { + async fn get_roots(&self) -> Result, JsonRpcError> { let hashes = self.chain.get_root_hashes(); Ok(hashes.iter().map(|h| h.to_string()).collect()) } - - pub(super) fn list_descriptors(&self) -> Result, JsonRpcError> { - let descriptors = self - .wallet - .get_descriptors() - .map_err(|e| JsonRpcError::Wallet(e.to_string()))?; - Ok(descriptors) - } } diff --git a/crates/floresta-node/src/json_rpc/control.rs b/crates/floresta-node/src/json_rpc/control.rs index 874b4a2ea..00ad00403 100644 --- a/crates/floresta-node/src/json_rpc/control.rs +++ b/crates/floresta-node/src/json_rpc/control.rs @@ -1,16 +1,22 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -use serde::Deserialize; -use serde::Serialize; +use floresta_rpc::rpc_interfaces::ControlRpc; +use floresta_rpc::rpc_types::ActiveCommand; +use floresta_rpc::rpc_types::GetMemInfoRes; +use floresta_rpc::rpc_types::GetMemInfoStats; +use floresta_rpc::rpc_types::GetRpcInfoRes; +use floresta_rpc::rpc_types::MemInfoLocked; use super::res::JsonRpcError; use super::server::RpcChain; use super::server::RpcImpl; -impl RpcImpl { - pub(super) fn get_memory_info(&self, mode: &str) -> Result { +impl ControlRpc for RpcImpl { + type Error = JsonRpcError; + + async fn get_memory_info(&self, mode: String) -> Result { #[cfg(target_env = "gnu")] - match mode { + match mode.as_str() { "stats" => { let info = unsafe { libc::mallinfo() }; @@ -49,7 +55,7 @@ impl RpcImpl { } #[cfg(target_os = "macos")] - match mode { + match mode.as_str() { "stats" => { let mut info: libc::malloc_statistics_t = unsafe { std::mem::zeroed() }; unsafe { @@ -94,14 +100,14 @@ impl RpcImpl { #[cfg(not(any(target_env = "gnu", target_os = "macos")))] // Just return zeroed stats for non-GNU and non-MacOS targets - match mode { + match mode.as_str() { "stats" => Ok(GetMemInfoRes::Stats(GetMemInfoStats::default())), "mallocinfo" => Ok(GetMemInfoRes::MallocInfo(String::new())), _ => Err(JsonRpcError::InvalidMemInfoMode), } } - pub(super) async fn get_rpc_info(&self) -> Result { + async fn get_rpc_info(&self) -> Result { let active_commands = self .inflight .read() @@ -123,48 +129,14 @@ impl RpcImpl { // logging // stop - pub(super) async fn stop(&self) -> Result<&str, JsonRpcError> { + async fn stop(&self) -> Result { *self.kill_signal.write().await = true; - Ok("Floresta stopping") + Ok("Floresta stopping".to_string()) } // uptime - pub(super) fn uptime(&self) -> u64 { - self.start_time.elapsed().as_secs() + async fn uptime(&self) -> Result { + Ok(self.start_time.elapsed().as_secs()) } } - -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct GetMemInfoStats { - locked: MemInfoLocked, -} - -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct MemInfoLocked { - used: u64, - free: u64, - total: u64, - locked: u64, - chunks_used: u64, - chunks_free: u64, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(untagged)] -pub enum GetMemInfoRes { - Stats(GetMemInfoStats), - MallocInfo(String), -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ActiveCommand { - method: String, - duration: u64, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct GetRpcInfoRes { - active_commands: Vec, - logpath: String, -} diff --git a/crates/floresta-node/src/json_rpc/mod.rs b/crates/floresta-node/src/json_rpc/mod.rs index 4d69352b4..3f32e2943 100644 --- a/crates/floresta-node/src/json_rpc/mod.rs +++ b/crates/floresta-node/src/json_rpc/mod.rs @@ -8,3 +8,5 @@ pub mod server; mod blockchain; mod control; mod network; +mod raw_transaction; +mod wallet; diff --git a/crates/floresta-node/src/json_rpc/network.rs b/crates/floresta-node/src/json_rpc/network.rs index 1e48c7b5d..00ea233f1 100644 --- a/crates/floresta-node/src/json_rpc/network.rs +++ b/crates/floresta-node/src/json_rpc/network.rs @@ -6,15 +6,15 @@ use core::net::IpAddr; use core::net::SocketAddr; use bitcoin::Network; -use corepc_types::v30::GetNetworkInfo; -use corepc_types::v30::GetNetworkInfoNetwork; use floresta_common::advertised_services; use floresta_common::service_flags_strings; use floresta_common::PROTOCOL_VERSION; +use floresta_rpc::rpc_interfaces::NetworkRpc; +use floresta_rpc::rpc_types::AddNodeCommand; +use floresta_rpc::rpc_types::GetNetworkInfo; +use floresta_rpc::rpc_types::GetNetworkInfoNetwork; +use floresta_rpc::rpc_types::PeerInfo; use floresta_wire::address_man::ReachableNetworks; -use floresta_wire::node_interface::PeerInfo; -use serde_json::json; -use serde_json::Value; use super::res::JsonRpcError; use super::server::RpcChain; @@ -42,20 +42,22 @@ fn parse_mmmmpp(version: &str) -> usize { major * 10_000 + minor * 100 + patch } -impl RpcImpl { - pub(crate) async fn ping(&self) -> Result { +impl NetworkRpc for RpcImpl { + type Error = JsonRpcError; + + async fn ping(&self) -> Result { self.node .ping() .await .map_err(|e| JsonRpcError::Node(e.to_string())) } - pub(crate) async fn add_node( + async fn add_node( &self, node_address: String, - command: String, + command: AddNodeCommand, v2transport: bool, - ) -> Result { + ) -> Result<()> { // Try to parse both IP address and port. let (addr, port) = if let Ok(socket_addr) = node_address.parse::() { (socket_addr.ip(), socket_addr.port()) @@ -78,21 +80,16 @@ impl RpcImpl { (ip, default_port) }; - let _ = match command.as_str() { - "add" => self.node.add_peer(addr, port, v2transport).await, - "remove" => self.node.remove_peer(addr, port).await, - "onetry" => self.node.onetry_peer(addr, port, v2transport).await, - _ => return Err(JsonRpcError::InvalidAddnodeCommand), + let _ = match command { + AddNodeCommand::Add => self.node.add_peer(addr, port, v2transport).await, + AddNodeCommand::Remove => self.node.remove_peer(addr, port).await, + AddNodeCommand::Onetry => self.node.onetry_peer(addr, port, v2transport).await, }; - Ok(json!(null)) + Ok(()) } - pub(crate) async fn disconnect_node( - &self, - node_address: String, - node_id: Option, - ) -> Result { + async fn disconnect_node(&self, node_address: String, node_id: Option) -> Result<()> { let (peer_addr, peer_port) = match (node_address.is_empty(), node_id) { // Reference the peer by it's IP address and port. (false, None) => { @@ -135,24 +132,41 @@ impl RpcImpl { return Err(JsonRpcError::PeerNotFound); } - Ok(json!(null)) + Ok(()) } - pub(crate) async fn get_peer_info(&self) -> Result> { - self.node + async fn get_peer_info(&self) -> Result> { + let infos = self + .node .get_peer_info() .await - .map_err(|_| JsonRpcError::Node("Failed to get peer information".to_string())) + .map_err(|_| JsonRpcError::Node("Failed to get peer information".to_string()))?; + + let response = infos + .into_iter() + .map(|info| PeerInfo { + id: info.id, + address: info.address.to_string(), + services: info.services.to_string(), + user_agent: info.user_agent, + initial_height: info.initial_height, + kind: format!("{:?}", info.kind).to_lowercase(), + state: format!("{:?}", info.state), + transport_protocol: format!("{:?}", info.transport_protocol), + }) + .collect(); + + Ok(response) } - pub(crate) async fn get_connection_count(&self) -> Result { + async fn get_connection_count(&self) -> Result { self.node .get_connection_count() .await .map_err(|_| JsonRpcError::Node("Failed to get connection count".to_string())) } - pub(crate) async fn get_network_info(&self) -> Result { + async fn get_network_info(&self) -> Result { // Floresta does not listen for inbound connections, so every peer is outbound. let connections_in = 0; let connections_out = self diff --git a/crates/floresta-node/src/json_rpc/raw_transaction.rs b/crates/floresta-node/src/json_rpc/raw_transaction.rs new file mode 100644 index 000000000..0b57f7a97 --- /dev/null +++ b/crates/floresta-node/src/json_rpc/raw_transaction.rs @@ -0,0 +1,174 @@ +use bitcoin::consensus::deserialize; +use bitcoin::consensus::encode::serialize_hex; +use bitcoin::hashes::hex::FromHex; +use bitcoin::hashes::Hash; +use bitcoin::hex::DisplayHex; +use bitcoin::Address; +use bitcoin::BlockHash; +use bitcoin::ScriptBuf; +use bitcoin::Transaction; +use bitcoin::TxIn; +use bitcoin::TxOut; +use bitcoin::Txid; +use floresta_rpc::rpc_interfaces::RawTransactionRpc; +use floresta_rpc::rpc_types::RawTx; +use floresta_rpc::rpc_types::RawTxResp; +use floresta_rpc::rpc_types::ScriptPubKeyJson; +use floresta_rpc::rpc_types::ScriptSigJson; +use floresta_rpc::rpc_types::TxInJson; +use floresta_rpc::rpc_types::TxOutJson; +use floresta_watch_only::CachedTransaction; + +use super::res::JsonRpcError; +use super::server::RpcChain; +use super::server::RpcImpl; + +impl RawTransactionRpc for RpcImpl { + type Error = JsonRpcError; + + async fn get_raw_transaction( + &self, + tx_id: Txid, + verbosity: Option, + ) -> Result { + let verbosity = verbosity.unwrap_or(0); + if verbosity > 1 { + return Err(JsonRpcError::InvalidVerbosityLevel); + } + + let tx = self + .wallet + .get_transaction(&tx_id) + .ok_or(JsonRpcError::TxNotFound)?; + + if verbosity == 0 { + let hex = serialize_hex(&tx.tx); + return Ok(RawTxResp::Zero(hex)); + } + + let raw_tx = self.make_raw_transaction(tx); + + Ok(RawTxResp::One(Box::new(raw_tx))) + } + + async fn send_raw_transaction(&self, tx: String) -> Result { + let tx_hex = Vec::from_hex(&tx).map_err(|_| JsonRpcError::InvalidHex)?; + let tx: Transaction = + deserialize(&tx_hex).map_err(|e| JsonRpcError::Decode(e.to_string()))?; + + Ok(self + .node + .broadcast_transaction(tx) + .await + .map_err(|e| JsonRpcError::Node(e.to_string()))??) + } +} + +impl RpcImpl { + fn make_vin(&self, input: TxIn) -> TxInJson { + let txid = serialize_hex(&input.previous_output.txid); + let vout = input.previous_output.vout; + let sequence = input.sequence.0; + TxInJson { + txid, + vout, + script_sig: ScriptSigJson { + asm: input.script_sig.to_asm_string(), + hex: input.script_sig.to_hex_string(), + }, + witness: input + .witness + .iter() + .map(|w| w.to_hex_string(bitcoin::hex::Case::Upper)) + .collect(), + sequence, + } + } + + fn get_script_type(script: ScriptBuf) -> Option<&'static str> { + if script.is_p2pkh() { + return Some("p2pkh"); + } + if script.is_p2sh() { + return Some("p2sh"); + } + if script.is_p2wpkh() { + return Some("v0_p2wpkh"); + } + if script.is_p2wsh() { + return Some("v0_p2wsh"); + } + None + } + + fn make_vout(&self, output: TxOut, n: u32) -> TxOutJson { + let value = output.value; + TxOutJson { + value: value.to_sat(), + n, + script_pub_key: ScriptPubKeyJson { + asm: output.script_pubkey.to_asm_string(), + hex: output.script_pubkey.to_hex_string(), + 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) + .unwrap_or("nonstandard") + .to_string(), + }, + } + } + + pub(super) fn make_raw_transaction(&self, tx: CachedTransaction) -> RawTx { + let raw_tx = tx.tx; + let in_active_chain = tx.height != 0; + let hex = serialize_hex(&raw_tx); + let txid = serialize_hex(&raw_tx.compute_txid()); + 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 + }; + + RawTx { + in_active_chain, + hex, + txid, + hash: serialize_hex(&raw_tx.compute_wtxid()), + size: raw_tx.total_size() as u32, + vsize: raw_tx.vsize() as u32, + weight: raw_tx.weight().to_wu() as u32, + version: raw_tx.version.0 as u32, + locktime: raw_tx.lock_time.to_consensus_u32(), + 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: serialize_hex(&block_hash), + 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), + } + } +} diff --git a/crates/floresta-node/src/json_rpc/res.rs b/crates/floresta-node/src/json_rpc/res.rs index 5710cc1d2..81664633c 100644 --- a/crates/floresta-node/src/json_rpc/res.rs +++ b/crates/floresta-node/src/json_rpc/res.rs @@ -8,8 +8,6 @@ use core::num::TryFromIntError; use std::convert::Infallible; use axum::response::IntoResponse; -use corepc_types::v30::GetBlockHeaderVerbose; -use corepc_types::v30::GetBlockVerboseOne; use floresta_chain::extensions::HeaderExtError; use floresta_common::impl_error_from; use floresta_mempool::mempool::MempoolError; @@ -17,135 +15,6 @@ use floresta_watch_only::descriptor::DescriptorError; use serde::Deserialize; use serde::Serialize; -#[derive(Deserialize, Serialize)] -pub struct GetBlockchainInfoRes { - pub best_block: String, - 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, -} - -/// A confidence enum to auxiliate rescan timestamp values. -/// -/// Serves to tell how much confidence you need in such a rescan request. That is, the need for a high confidence rescan -/// will make the rescan to start in a block that have an lower timestamp than the given in order to be more secure -/// about finding addresses and relevant transactions, a lower confidence will make the rescan to be closer to the given value. -/// -/// This input is necessary to cover network variancy specially in testnet, for mainnet you can safely use low or medium confidences -/// depending on how much sure you are about the given timestamp covering the addresses you need. -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(rename_all = "lowercase")] -pub enum RescanConfidence { - /// `high`: 99% confidence interval. Returning 46 minutes in seconds for `val`. - High, - - /// `medium` (default): 95% confidence interval. Returning 30 minutes in seconds for `val`. - Medium, - - /// `low`: 90% confidence interval. Returning 23 minutes in seconds for `val`. - Low, - - /// `exact`: Removes any lookback addition. Returning 0 for `val` - Exact, -} - -impl RescanConfidence { - /// In cases where `use_timestamp` is set, tells how much confidence the user wants for finding its addresses from this rescan request, a higher confidence will add more lookback seconds to the targeted timestamp and rescanning more blocks. - /// Under the hood this uses an [Exponential distribution](https://en.wikipedia.org/wiki/Exponential_distribution) [cumulative distribution function (CDF)](https:///en.wikipedia.org/wiki/Cumulative_distribution_function) where the parameter $\lambda$ (rate) is $\frac{1}{600}$ (1 block every 600 seconds, 10 minutes). - /// The supplied string can be one of: - /// - /// - `high`: 99% confidence interval. Returning 46 minutes in seconds for `val`. - /// - `medium` (default): 95% confidence interval. Returning 30 minutes in seconds for `val`. - /// - `low`: 90% confidence interval. Returning 23 minutes in seconds for `val`. - /// - `exact`: Removes any lookback addition. Returning 0 for `val` - pub const fn as_secs(&self) -> u32 { - match self { - Self::Exact => 0, - Self::Low => 1_380, - Self::Medium => 1_800, - Self::High => 2_760, - } - } -} - -#[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(Debug, Deserialize, Serialize)] -#[serde(untagged)] -pub enum GetBlockRes { - Zero(String), - One(Box), -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(untagged)] -/// The response for `getblockheader`, which can be either a raw hex-encoded block header or a verbose -/// one with all the fields parsed and decoded. -pub enum GetBlockHeaderRes { - /// The raw hex-encoded block header, as returned by `getblockheader` with verbosity false - Raw(String), - - /// A verbose block header, as returned by `getblockheader` with verbosity true - Verbose(Box), -} - #[derive(Debug, Deserialize, Serialize)] pub struct RpcError { pub code: i32, @@ -153,12 +22,6 @@ pub struct RpcError { pub data: Option, } -/// Return type for the `gettxoutproof` rpc command, the internal is -/// just the hex representation of the Merkle Block, which was defined -/// by btc core. -#[derive(Debug, Deserialize, Serialize)] -pub struct GetTxOutProof(pub Vec); - #[derive(Debug)] pub enum JsonRpcError { /// There was a rescan request but we do not have any addresses in the watch-only wallet. diff --git a/crates/floresta-node/src/json_rpc/server.rs b/crates/floresta-node/src/json_rpc/server.rs index 0c2f0db45..2c3f42b69 100644 --- a/crates/floresta-node/src/json_rpc/server.rs +++ b/crates/floresta-node/src/json_rpc/server.rs @@ -2,6 +2,7 @@ use core::net::SocketAddr; use std::collections::HashMap; +use std::str::FromStr; use std::sync::Arc; use std::time::Instant; @@ -14,25 +15,21 @@ use axum::http::StatusCode; use axum::routing::post; use axum::Json; use axum::Router; -use bitcoin::consensus::deserialize; -use bitcoin::consensus::encode::serialize_hex; -use bitcoin::hashes::hex::FromHex; -use bitcoin::hashes::Hash; use bitcoin::hex::DisplayHex; -use bitcoin::Address; -use bitcoin::BlockHash; use bitcoin::Network; -use bitcoin::ScriptBuf; -use bitcoin::Transaction; -use bitcoin::TxIn; -use bitcoin::TxOut; -use bitcoin::Txid; use floresta_chain::ThreadSafeChain; use floresta_compact_filters::flat_filters_store::FlatFiltersStore; use floresta_compact_filters::network_filters::NetworkFilters; +use floresta_rpc::rpc_interfaces::BlockchainRpc; +use floresta_rpc::rpc_interfaces::ControlRpc; +use floresta_rpc::rpc_interfaces::NetworkRpc; +use floresta_rpc::rpc_interfaces::RawTransactionRpc; +use floresta_rpc::rpc_interfaces::RpcMethods; +use floresta_rpc::rpc_interfaces::WalletRpc; +use floresta_rpc::rpc_types::AddNodeCommand; +use floresta_rpc::rpc_types::RescanConfidence; use floresta_watch_only::kv_database::KvDatabase; use floresta_watch_only::AddressCache; -use floresta_watch_only::CachedTransaction; use floresta_wire::node_interface::NodeInterface; use serde_json::json; use serde_json::Value; @@ -43,12 +40,7 @@ use tracing::error; use tracing::info; use super::res::JsonRpcError; -use super::res::RawTxJson; use super::res::RpcError; -use super::res::ScriptPubKeyJson; -use super::res::ScriptSigJson; -use super::res::TxInJson; -use super::res::TxOutJson; use crate::json_rpc::request::arg_parser::get_bool; use crate::json_rpc::request::arg_parser::get_hash; use crate::json_rpc::request::arg_parser::get_hashes_array; @@ -56,7 +48,6 @@ use crate::json_rpc::request::arg_parser::get_numeric; use crate::json_rpc::request::arg_parser::get_optional_field; use crate::json_rpc::request::arg_parser::get_string; use crate::json_rpc::request::RpcRequest; -use crate::json_rpc::res::RescanConfidence; pub(super) struct InflightRpc { pub method: String, @@ -87,107 +78,6 @@ pub struct RpcImpl { type Result = std::result::Result; -impl RpcImpl { - fn get_transaction(&self, tx_id: Txid, verbosity: Option) -> Result { - if verbosity == Some(true) { - let tx = self - .wallet - .get_transaction(&tx_id) - .ok_or(JsonRpcError::TxNotFound); - return tx.map(|tx| serde_json::to_value(self.make_raw_transaction(tx)).unwrap()); - } - - self.wallet - .get_transaction(&tx_id) - .and_then(|tx| serde_json::to_value(self.make_raw_transaction(tx)).ok()) - .ok_or(JsonRpcError::TxNotFound) - } - - fn load_descriptor(&self, descriptor: String) -> Result { - let addresses = self.wallet.push_descriptor(&descriptor)?; - info!("Descriptor pushed: {descriptor}"); - debug!("Rescanning with block filters for addresses: {addresses:?}"); - - let addresses = self.wallet.get_cached_addresses(); - let wallet = self.wallet.clone(); - if self.block_filter_storage.is_none() { - return Err(JsonRpcError::InInitialBlockDownload); - }; - - let cfilters = self.block_filter_storage.as_ref().unwrap().clone(); - let node = self.node.clone(); - let chain = self.chain.clone(); - - tokio::task::spawn(Self::rescan_with_block_filters( - addresses, chain, wallet, cfilters, node, None, None, - )); - - Ok(true) - } - - fn rescan_blockchain( - &self, - start: Option, - stop: Option, - use_timestamp: bool, - confidence: Option, - ) -> Result { - let (start_height, stop_height) = - self.get_rescan_interval(use_timestamp, start, stop, confidence)?; - - if stop_height != 0 && start_height >= stop_height { - // When stop height is a non zero value it needs atleast to be greater than start_height. - return Err(JsonRpcError::InvalidRescanVal); - } - - // if we are on ibd, we don't have any filters to rescan - if self.chain.is_in_ibd() { - return Err(JsonRpcError::InInitialBlockDownload); - } - - let addresses = self.wallet.get_cached_addresses(); - - if addresses.is_empty() { - return Err(JsonRpcError::NoAddressesToRescan); - } - - let wallet = self.wallet.clone(); - - if self.block_filter_storage.is_none() { - return Err(JsonRpcError::NoBlockFilters); - }; - - let cfilters = self.block_filter_storage.as_ref().unwrap().clone(); - - let node = self.node.clone(); - - let chain = self.chain.clone(); - - tokio::task::spawn(Self::rescan_with_block_filters( - addresses, - chain, - wallet, - cfilters, - node, - (start_height != 0).then_some(start_height), // Its ugly but to maintain the API here its necessary to recast to a Option. - (stop_height != 0).then_some(stop_height), - )); - Ok(true) - } - - async fn send_raw_transaction(&self, tx: String) -> Result { - let tx_hex = Vec::from_hex(&tx).map_err(|_| JsonRpcError::InvalidHex)?; - let tx: Transaction = - deserialize(&tx_hex).map_err(|e| JsonRpcError::Decode(e.to_string()))?; - - Ok(self - .node - .broadcast_transaction(tx) - .await - .map_err(|e| JsonRpcError::Node(e.to_string()))??) - } -} - async fn handle_json_rpc_request( req: RpcRequest, state: Arc>, @@ -213,18 +103,19 @@ async fn handle_json_rpc_request( }, ); - match method.as_str() { + let method = RpcMethods::from_str(&method).map_err(|_| JsonRpcError::MethodNotFound)?; + + match method { // blockchain - "getbestblockhash" => { - let hash = state.get_best_block_hash()?; + RpcMethods::GetBestBlockHash => { + let hash = state.get_best_block_hash().await?; Ok(serde_json::to_value(hash).unwrap()) } - "getblock" => { + RpcMethods::GetBlock => { let hash = get_hash(¶ms, 0, "block_hash")?; // Default value in case of missing parameter is 1 - let verbosity: u8 = - get_optional_field(¶ms, 1, "verbosity", get_numeric)?.unwrap_or(1); + let verbosity = get_optional_field(¶ms, 1, "verbosity", get_numeric)?; state .get_block(hash, verbosity) @@ -232,32 +123,35 @@ async fn handle_json_rpc_request( .map(|v| serde_json::to_value(v).expect("GetBlockRes implements serde")) } - "getblockchaininfo" => state + RpcMethods::GetBlockchainInfo => state .get_blockchain_info() + .await .map(|v| serde_json::to_value(v).unwrap()), - "getblockcount" => state + RpcMethods::GetBlockCount => state .get_block_count() + .await .map(|v| serde_json::to_value(v).unwrap()), - "getblockfrompeer" => { + RpcMethods::GetBlockFromPeer => { let hash = get_hash(¶ms, 0, "block_hash")?; - state.get_block(hash, 0).await?; + state.get_block(hash, Some(0)).await?; Ok(Value::Null) } - "getblockhash" => { + RpcMethods::GetBlockHash => { let height = get_numeric(¶ms, 0, "block_height")?; state .get_block_hash(height) + .await .map(|h| serde_json::to_value(h).unwrap()) } - "getblockheader" => { + RpcMethods::GetBlockHeader => { let hash = get_hash(¶ms, 0, "block_hash")?; - let verbosity = get_optional_field(¶ms, 1, "verbosity", get_bool)?.unwrap_or(true); + let verbosity = get_optional_field(¶ms, 1, "verbosity", get_bool)?; state .get_block_header(hash, verbosity) @@ -265,7 +159,7 @@ async fn handle_json_rpc_request( .map(|h| serde_json::to_value(h).unwrap()) } - "gettxout" => { + RpcMethods::GetTxOut => { let txid = get_hash(¶ms, 0, "txid")?; let vout = get_numeric(¶ms, 1, "vout")?; let include_mempool = @@ -273,10 +167,11 @@ async fn handle_json_rpc_request( state .get_tx_out(txid, vout, include_mempool) + .await .map(|v| serde_json::to_value(v).unwrap()) } - "gettxoutproof" => { + RpcMethods::GetTxOutProof => { let txids = get_hashes_array(¶ms, 0, "txids")?; let block_hash = get_optional_field(¶ms, 1, "block_hash", get_hash)?; @@ -290,106 +185,115 @@ async fn handle_json_rpc_request( .expect("GetTxOutProof implements serde")) } - "getrawtransaction" => { + RpcMethods::GetRawTransaction => { let txid = get_hash(¶ms, 0, "txid")?; - let verbosity = get_optional_field(¶ms, 1, "verbosity", get_bool)?; + let verbosity = get_optional_field(¶ms, 1, "verbosity", get_numeric)?; state - .get_transaction(txid, verbosity) + .get_raw_transaction(txid, verbosity) + .await .map(|v| serde_json::to_value(v).unwrap()) } - "getroots" => state.get_roots().map(|v| serde_json::to_value(v).unwrap()), + RpcMethods::GetRoots => state + .get_roots() + .await + .map(|v| serde_json::to_value(v).unwrap()), - "findtxout" => { + RpcMethods::FindTxOut => { let txid = get_hash(¶ms, 0, "txid")?; let vout = get_numeric(¶ms, 1, "vout")?; let script = get_string(¶ms, 2, "script")?; - let script = ScriptBuf::from_hex(&script).map_err(|_| JsonRpcError::InvalidScript)?; let height = get_numeric(¶ms, 3, "height")?; let state = state.clone(); - state.find_tx_out(txid, vout, script, height).await + state + .find_tx_out(txid, vout, script, height) + .await + .map(|v| serde_json::to_value(v).unwrap()) } // control - "getmemoryinfo" => { + RpcMethods::GetMemoryInfo => { let mode = get_optional_field(¶ms, 0, "mode", get_string)?.unwrap_or("stats".into()); state - .get_memory_info(&mode) + .get_memory_info(mode) + .await .map(|v| serde_json::to_value(v).unwrap()) } - "getrpcinfo" => state + RpcMethods::GetRpcInfo => state .get_rpc_info() .await .map(|v| serde_json::to_value(v).unwrap()), // help // logging - "stop" => state.stop().await.map(|v| serde_json::to_value(v).unwrap()), + RpcMethods::Stop => state.stop().await.map(|v| serde_json::to_value(v).unwrap()), - "uptime" => { - let uptime = state.uptime(); + RpcMethods::Uptime => { + let uptime = state.uptime().await?; Ok(serde_json::to_value(uptime).unwrap()) } // network - "getpeerinfo" => state + RpcMethods::GetPeerInfo => state .get_peer_info() .await .map(|v| serde_json::to_value(v).unwrap()), - "getconnectioncount" => state + RpcMethods::GetConnectionCount => state .get_connection_count() .await .map(|v| serde_json::to_value(v).unwrap()), - "getnetworkinfo" => state + RpcMethods::GetNetworkInfo => state .get_network_info() .await .map(|v| serde_json::to_value(v).unwrap()), - "addnode" => { + RpcMethods::AddNode => { let node = get_string(¶ms, 0, "node")?; let command = get_string(¶ms, 1, "command")?; let v2transport = get_optional_field(¶ms, 2, "V2transport", get_bool)?.unwrap_or(false); - state - .add_node(node, command, v2transport) - .await - .map(|v| serde_json::to_value(v).unwrap()) + let command = AddNodeCommand::from_str(&command) + .map_err(|_| JsonRpcError::InvalidAddnodeCommand)?; + + state.add_node(node, command, v2transport).await?; + + Ok(serde_json::json!(null)) } - "disconnectnode" => { + RpcMethods::DisconnectNode => { let node_address = get_string(¶ms, 0, "node_address")?; let node_id = get_optional_field(¶ms, 1, "node_id", get_numeric)?; - state - .disconnect_node(node_address, node_id) - .await - .map(|v| serde_json::to_value(v).unwrap()) + state.disconnect_node(node_address, node_id).await?; + + Ok(serde_json::json!(null)) } - "ping" => { + RpcMethods::Ping => { state.ping().await?; Ok(serde_json::json!(null)) } // wallet - "loaddescriptor" => { + RpcMethods::LoadDescriptor => { let descriptor = get_string(¶ms, 0, "descriptor")?; state .load_descriptor(descriptor) + .await .map(|v| serde_json::to_value(v).unwrap()) } - "rescanblockchain" => { + RpcMethods::RescanBlockchain => { let start_height = get_optional_field(¶ms, 0, "start_height", get_numeric)?; let stop_height = get_optional_field(¶ms, 1, "stop_height", get_numeric)?; let use_timestamp = @@ -407,10 +311,11 @@ async fn handle_json_rpc_request( state .rescan_blockchain(start_height, stop_height, use_timestamp, Some(confidence)) + .await .map(|v| serde_json::to_value(v).unwrap()) } - "sendrawtransaction" => { + RpcMethods::SendRawTransaction => { let tx = get_string(¶ms, 0, "hex")?; state .send_raw_transaction(tx) @@ -418,14 +323,10 @@ async fn handle_json_rpc_request( .map(|v| serde_json::to_value(v).unwrap()) } - "listdescriptors" => state + RpcMethods::ListDescriptors => state .list_descriptors() + .await .map(|v| serde_json::to_value(v).unwrap()), - - _ => { - let error = JsonRpcError::MethodNotFound; - Err(error) - } } } @@ -579,147 +480,6 @@ async fn cannot_get(_state: State>>) -> Json RpcImpl { - async fn rescan_with_block_filters( - addresses: Vec, - chain: Blockchain, - wallet: Arc>, - cfilters: Arc>, - node: NodeInterface, - start_height: Option, - stop_height: Option, - ) -> Result<()> { - let blocks = cfilters - .match_any( - addresses.iter().map(|a| a.as_bytes()).collect(), - start_height, - stop_height, - chain.clone(), - ) - .unwrap(); - - info!("rescan filter hits: {blocks:?}"); - - for block in blocks { - if let Ok(Some(block)) = node.get_block(block).await { - let height = chain - .get_block_height(&block.block_hash()) - .unwrap() - .unwrap(); - - wallet.block_process(&block, height); - } - } - - Ok(()) - } - - fn make_vin(&self, input: TxIn) -> TxInJson { - let txid = serialize_hex(&input.previous_output.txid); - let vout = input.previous_output.vout; - let sequence = input.sequence.0; - TxInJson { - txid, - vout, - script_sig: ScriptSigJson { - asm: input.script_sig.to_asm_string(), - hex: input.script_sig.to_hex_string(), - }, - witness: input - .witness - .iter() - .map(|w| w.to_hex_string(bitcoin::hex::Case::Upper)) - .collect(), - sequence, - } - } - - fn get_script_type(script: ScriptBuf) -> Option<&'static str> { - if script.is_p2pkh() { - return Some("p2pkh"); - } - if script.is_p2sh() { - return Some("p2sh"); - } - if script.is_p2wpkh() { - return Some("v0_p2wpkh"); - } - if script.is_p2wsh() { - return Some("v0_p2wsh"); - } - None - } - - fn make_vout(&self, output: TxOut, n: u32) -> TxOutJson { - let value = output.value; - TxOutJson { - value: value.to_sat(), - n, - script_pub_key: ScriptPubKeyJson { - asm: output.script_pubkey.to_asm_string(), - hex: output.script_pubkey.to_hex_string(), - 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) - .unwrap_or("nonstandard") - .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_hex(&raw_tx); - let txid = serialize_hex(&raw_tx.compute_txid()); - 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: serialize_hex(&raw_tx.compute_wtxid()), - size: raw_tx.total_size() as u32, - vsize: raw_tx.vsize() as u32, - weight: raw_tx.weight().to_wu() as u32, - version: raw_tx.version.0 as u32, - locktime: raw_tx.lock_time.to_consensus_u32(), - 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: serialize_hex(&block_hash), - 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), - } - } - // TODO(@luisschwab): get rid of this once // https://github.com/rust-bitcoin/rust-bitcoin/pull/4639 makes it into a release. fn get_port(net: &Network) -> u16 { diff --git a/crates/floresta-node/src/json_rpc/wallet.rs b/crates/floresta-node/src/json_rpc/wallet.rs new file mode 100644 index 000000000..4271d69c0 --- /dev/null +++ b/crates/floresta-node/src/json_rpc/wallet.rs @@ -0,0 +1,238 @@ +use std::sync::Arc; + +use bitcoin::constants::genesis_block; +use bitcoin::ScriptBuf; +use floresta_compact_filters::flat_filters_store::FlatFiltersStore; +use floresta_compact_filters::network_filters::NetworkFilters; +use floresta_rpc::rpc_interfaces::WalletRpc; +use floresta_rpc::rpc_types::RescanConfidence; +use floresta_watch_only::kv_database::KvDatabase; +use floresta_watch_only::AddressCache; +use floresta_wire::node_interface::NodeInterface; +use tracing::debug; +use tracing::info; + +use super::res::JsonRpcError; +use super::server::RpcChain; +use super::server::RpcImpl; + +impl WalletRpc for RpcImpl { + type Error = JsonRpcError; + + // rescanblockchain + async fn rescan_blockchain( + &self, + start: Option, + stop: Option, + use_timestamp: bool, + confidence: Option, + ) -> Result { + let (start_height, stop_height) = + self.get_rescan_interval(use_timestamp, start, stop, confidence)?; + + if stop_height != 0 && start_height >= stop_height { + // When stop height is a non zero value it needs atleast to be greater than start_height. + return Err(JsonRpcError::InvalidRescanVal); + } + + // if we are on ibd, we don't have any filters to rescan + if self.chain.is_in_ibd() { + return Err(JsonRpcError::InInitialBlockDownload); + } + + let addresses = self.wallet.get_cached_addresses(); + + if addresses.is_empty() { + return Err(JsonRpcError::NoAddressesToRescan); + } + + let wallet = self.wallet.clone(); + + if self.block_filter_storage.is_none() { + return Err(JsonRpcError::NoBlockFilters); + }; + + let cfilters = self.block_filter_storage.as_ref().unwrap().clone(); + + let node = self.node.clone(); + + let chain = self.chain.clone(); + + tokio::task::spawn(Self::rescan_with_block_filters( + addresses, + chain, + wallet, + cfilters, + node, + (start_height != 0).then_some(start_height), // Its ugly but to maintain the API here its necessary to recast to a Option. + (stop_height != 0).then_some(stop_height), + )); + Ok(true) + } + + // listdescriptors + async fn list_descriptors(&self) -> Result, JsonRpcError> { + let descriptors = self + .wallet + .get_descriptors() + .map_err(|e| JsonRpcError::Wallet(e.to_string()))?; + Ok(descriptors) + } + + // loaddescriptor + async fn load_descriptor(&self, descriptor: String) -> Result { + let addresses = self.wallet.push_descriptor(&descriptor)?; + info!("Descriptor pushed: {descriptor}"); + debug!("Rescanning with block filters for addresses: {addresses:?}"); + + let addresses = self.wallet.get_cached_addresses(); + let wallet = self.wallet.clone(); + if self.block_filter_storage.is_none() { + return Err(JsonRpcError::InInitialBlockDownload); + }; + + let cfilters = self.block_filter_storage.as_ref().unwrap().clone(); + let node = self.node.clone(); + let chain = self.chain.clone(); + + tokio::task::spawn(Self::rescan_with_block_filters( + addresses, chain, wallet, cfilters, node, None, None, + )); + + Ok(true) + } +} + +impl RpcImpl { + async fn rescan_with_block_filters( + addresses: Vec, + chain: Blockchain, + wallet: Arc>, + cfilters: Arc>, + node: NodeInterface, + start_height: Option, + stop_height: Option, + ) -> Result<(), JsonRpcError> { + let blocks = cfilters + .match_any( + addresses.iter().map(|a| a.as_bytes()).collect(), + start_height, + stop_height, + chain.clone(), + ) + .map_err(|_| JsonRpcError::NoBlockFilters)?; + + info!("rescan filter hits: {blocks:?}"); + + for block in blocks { + if let Ok(Some(block)) = node.get_block(block).await { + let height = chain + .get_block_height(&block.block_hash()) + .map_err(|_| JsonRpcError::Chain)? + .ok_or(JsonRpcError::BlockNotFound)?; + wallet.block_process(&block, height); + } + } + + Ok(()) + } + + fn get_rescan_interval( + &self, + use_timestamp: bool, + start: Option, + stop: Option, + confidence: Option, + ) -> Result<(u32, u32), JsonRpcError> { + let start = start.unwrap_or(0u32); + let stop = stop.unwrap_or(0u32); + + if use_timestamp { + let confidence = confidence.unwrap_or(RescanConfidence::Medium); + // `get_block_height_by_timestamp` already does the time validity checks. + + let start_height = self.get_block_height_by_timestamp(start, &confidence)?; + + let stop_height = self.get_block_height_by_timestamp(stop, &RescanConfidence::Exact)?; + + return Ok((start_height, stop_height)); + } + + let (tip, _) = self + .chain + .get_best_block() + .map_err(|_| JsonRpcError::Chain)?; + + if stop > tip { + return Err(JsonRpcError::InvalidRescanVal); + } + + Ok((start, stop)) + } + + /// Retrieves the height of the block that was mined in the given timestamp. + /// + /// `timestamp` has an alias, 0 will directly refer to the network's genesis timestamp. + fn get_block_height_by_timestamp( + &self, + timestamp: u32, + confidence: &RescanConfidence, + ) -> Result { + /// Simple helper to avoid code reuse. + fn get_block_time( + provider: &RpcImpl, + at: u32, + ) -> Result { + let hash = provider.get_block_hash_inner(at)?; + let block = provider.get_block_header_inner(hash)?; + Ok(block.time) + } + + let genesis_timestamp = genesis_block(self.network).header.time; + + if timestamp == 0 || timestamp == genesis_timestamp { + return Ok(0); + }; + + let (tip_height, _) = self + .chain + .get_best_block() + .map_err(|_| JsonRpcError::BlockNotFound)?; + + let tip_time = get_block_time(self, tip_height)?; + + if timestamp < genesis_timestamp || timestamp > tip_time { + return Err(JsonRpcError::InvalidTimestamp); + } + + let adjusted_target = timestamp.saturating_sub(confidence.as_secs()); + + let mut high = tip_height; + let mut low = 0; + let max_iters = tip_height.ilog2() + 1; + for _ in 0..max_iters { + let cut = (high + low) / 2; + + let block_timestamp = get_block_time(self, cut)?; + + if block_timestamp == adjusted_target { + debug!("found a precise block; returning {cut}"); + return Ok(cut); + } + + if high - low <= 2 { + debug!("didn't find a precise block; returning {low}"); + return Ok(low); + } + + if block_timestamp > adjusted_target { + high = cut; + } else { + low = cut; + } + } + + // This is pretty much unreachable. + Err(JsonRpcError::BlockNotFound) + } +} diff --git a/crates/floresta-rpc/Cargo.toml b/crates/floresta-rpc/Cargo.toml index 6af4b4096..7fbf66c7b 100644 --- a/crates/floresta-rpc/Cargo.toml +++ b/crates/floresta-rpc/Cargo.toml @@ -21,6 +21,7 @@ corepc-types = { workspace = true } jsonrpc = { version = "0.19", features = ["bitreq_http"], optional = true } serde = { workspace = true } serde_json = { workspace = true } +maybe_async = { git = "https://github.com/Davidson-Souza/maybe-async2.git", branch = "master" } [dev-dependencies] rand = { workspace = true } @@ -29,6 +30,7 @@ rcgen = { workspace = true } [features] default = ["with-jsonrpc"] with-jsonrpc = ["dep:jsonrpc"] +async = ["maybe_async/async"] clap = ["dep:clap"] [lints] diff --git a/crates/floresta-rpc/src/jsonrpc_client.rs b/crates/floresta-rpc/src/jsonrpc_client.rs index 082ef60f3..c5a38d50e 100644 --- a/crates/floresta-rpc/src/jsonrpc_client.rs +++ b/crates/floresta-rpc/src/jsonrpc_client.rs @@ -4,6 +4,7 @@ use core::fmt::Debug; use serde::Deserialize; +#[cfg(not(feature = "async"))] use crate::rpc::JsonRPCClient; // Define a Client struct that wraps a jsonrpc::Client @@ -57,6 +58,7 @@ impl Client { } } +#[cfg(not(feature = "async"))] // Implement the JsonRPCClient trait for Client impl JsonRPCClient for Client { fn call serde::de::Deserialize<'a> + Debug>( diff --git a/crates/floresta-rpc/src/lib.rs b/crates/floresta-rpc/src/lib.rs index f0098bd07..36d1baf78 100644 --- a/crates/floresta-rpc/src/lib.rs +++ b/crates/floresta-rpc/src/lib.rs @@ -18,13 +18,21 @@ #[cfg(feature = "with-jsonrpc")] pub mod jsonrpc_client; +#[cfg(not(feature = "async"))] pub mod rpc; + +pub mod rpc_interfaces; pub mod rpc_types; // Those tests doesn't work on windows // TODO (Davidson): work on windows? -#[cfg(all(test, feature = "with-jsonrpc", not(target_os = "windows")))] +#[cfg(all( + test, + feature = "with-jsonrpc", + not(feature = "async"), + not(target_os = "windows") +))] mod tests { use core::str::FromStr; use std::fs; @@ -42,7 +50,11 @@ mod tests { use rcgen::CertifiedKey; use crate::jsonrpc_client::Client; - use crate::rpc::FlorestaRPC; + use crate::rpc_interfaces::BlockchainRpc; + use crate::rpc_interfaces::ControlRpc; + use crate::rpc_interfaces::NetworkRpc; + use crate::rpc_interfaces::RawTransactionRpc; + use crate::rpc_interfaces::WalletRpc; use crate::rpc_types::GetBlockHeaderRes; use crate::rpc_types::GetBlockRes; @@ -148,7 +160,7 @@ mod tests { fn test_stop() { let (mut _proc, client) = start_florestad(); - let stop = client.stop().expect("rpc not working"); + let stop = client.stop().unwrap(); assert_eq!(stop.as_str(), "Floresta stopping"); } @@ -156,7 +168,7 @@ mod tests { fn test_get_blockchaininfo() { let (_proc, client) = start_florestad(); - let gbi = client.get_blockchain_info().expect("rpc not working"); + let gbi = client.get_blockchain_info().unwrap(); assert_eq!(gbi.height, 0); assert_eq!(gbi.chain, "regtest".to_owned()); @@ -169,7 +181,7 @@ mod tests { fn test_get_roots() { let (_proc, client) = start_florestad(); - let gbi = client.get_blockchain_info().expect("rpc not working"); + let gbi = client.get_blockchain_info().unwrap(); assert_eq!(gbi.root_hashes, Vec::::new()); } @@ -178,7 +190,7 @@ mod tests { fn test_get_best_block_hash() { let (_proc, client) = start_florestad(); - let blockhash = client.get_best_block_hash().expect("rpc not working"); + let blockhash = client.get_best_block_hash().unwrap(); assert_eq!( blockhash, @@ -212,7 +224,7 @@ mod tests { fn test_get_block_hash() { let (_proc, client) = start_florestad(); - let blockhash = client.get_block_hash(0).expect("rpc not working"); + let blockhash = client.get_block_hash(0).unwrap(); assert_eq!( blockhash, @@ -297,4 +309,55 @@ mod tests { assert_eq!(net.limited, !supported); } } + + #[test] + fn test_uptime() { + let (_proc, client) = start_florestad(); + + client.uptime().unwrap(); + } + + #[test] + fn test_get_connection_count() { + let (_proc, client) = start_florestad(); + + let count = client.get_connection_count().unwrap(); + assert!(count == 0); + } + + #[test] + fn test_get_peer_info() { + let (_proc, client) = start_florestad(); + + let peers = client.get_peer_info().unwrap(); + // Should not panic, peers might be empty since we're in regtest + assert!(peers.is_empty() || !peers.is_empty()); + } + + #[test] + fn test_list_descriptors() { + let (_proc, client) = start_florestad(); + + let descriptors = client.list_descriptors().unwrap(); + // Should not panic, descriptors might be empty + assert!(descriptors.is_empty() || !descriptors.is_empty()); + } + + #[test] + fn test_get_memory_info() { + let (_proc, client) = start_florestad(); + + let mem_info = client.get_memory_info("stats".to_string()).unwrap(); + // Just check it returns something without error + let _ = mem_info; + } + + #[test] + fn test_get_rpc_info() { + let (_proc, client) = start_florestad(); + + let rpc_info = client.get_rpc_info().unwrap(); + // Just check it returns something without error + let _ = rpc_info; + } } diff --git a/crates/floresta-rpc/src/rpc.rs b/crates/floresta-rpc/src/rpc.rs index 82db87d06..cbc54b8f1 100644 --- a/crates/floresta-rpc/src/rpc.rs +++ b/crates/floresta-rpc/src/rpc.rs @@ -5,147 +5,20 @@ use std::vec; use bitcoin::BlockHash; use bitcoin::Txid; -use corepc_types::v29::GetTxOut; use serde_json::Number; use serde_json::Value; +use crate::rpc_interfaces::BlockchainRpc; +use crate::rpc_interfaces::ControlRpc; +use crate::rpc_interfaces::NetworkRpc; +use crate::rpc_interfaces::RawTransactionRpc; +use crate::rpc_interfaces::RpcMethods; +use crate::rpc_interfaces::WalletRpc; use crate::rpc_types; use crate::rpc_types::*; type Result = std::result::Result; -/// A trait specifying all possible methods for floresta's json-rpc -pub trait FlorestaRPC { - /// Get the BIP158 filter for a given block height - /// - /// BIP158 filters are a compact representation of the set of transactions in a block, - /// designed for efficient light client synchronization. This method returns the filter - /// for a given block height, encoded as a hexadecimal string. - /// You need to have enabled block filters by setting the `blockfilters=1` option - fn get_block_filter(&self, height: u32) -> Result; - /// Returns general information about the chain we are on - /// - /// This method returns a bunch of information about the chain we are on, including - /// the current height, the best block hash, the difficulty, and whether we are - /// currently in IBD (Initial Block Download) mode. - fn get_blockchain_info(&self) -> Result; - /// Returns the hash of the best (tip) block in the most-work fully-validated chain. - fn get_best_block_hash(&self) -> Result; - /// Returns the hash of the block at the given height - /// - /// This method returns the hash of the block at the given height. If the height is - /// invalid, an error is returned. - fn get_block_hash(&self, height: u32) -> Result; - /// Returns the block header for the given block hash - /// - /// This method returns the block header for the given block hash, as defined - /// in the Bitcoin protocol specification. A header contains the block's version, - /// the previous block hash, the merkle root, the timestamp, the difficulty target, - /// and the nonce. - #[doc = include_str!("../../../doc/rpc/getblockheader.md")] - fn get_block_header( - &self, - hash: BlockHash, - verbosity: Option, - ) -> Result; - /// Gets a transaction from the blockchain - /// - /// This method returns a transaction that's cached in our wallet. If the verbosity flag is - /// set to false, the transaction is returned as a hexadecimal string. If the verbosity - /// flag is set to true, the transaction is returned as a json object. - fn get_transaction(&self, tx_id: Txid, verbosity: Option) -> Result; - /// Returns the proof that one or more transactions were included in a block - /// - /// This method returns the Merkle proof, showing that a transaction was included in a block. - /// The pooof is returned as a vector hexadecimal string. - fn get_txout_proof(&self, txids: Vec, blockhash: Option) -> Option; - /// Loads up a descriptor into the wallet - /// - /// This method loads up a descriptor into the wallet. If the rescan option is not None, - /// the wallet will be rescanned for transactions matching the descriptor. If you have - /// compact block filters enabled, this process will be much faster and use less bandwidth. - /// The rescan parameter is the height at which to start the rescan, and should be at least - /// as old as the oldest transaction this descriptor could have been used in. - fn load_descriptor(&self, descriptor: String) -> Result; - - #[doc = include_str!("../../../doc/rpc/rescanblockchain.md")] - fn rescanblockchain( - &self, - start_block: Option, - stop_block: Option, - use_timestamp: bool, - confidence: RescanConfidence, - ) -> Result; - - /// Returns the current height of the blockchain - fn get_block_count(&self) -> Result; - /// Sends a hex-encoded transaction to the network - /// - /// This method sends a transaction to the network. The transaction should be encoded as a - /// hexadecimal string. If the transaction is valid, it will be broadcast to the network, and - /// return the transaction id. If the transaction is invalid, an error will be returned. - fn send_raw_transaction(&self, tx: String) -> Result; - #[doc = include_str!("../../../doc/rpc/getroots.md")] - fn get_roots(&self) -> Result>; - #[doc = include_str!("../../../doc/rpc/getpeerinfo.md")] - fn get_peer_info(&self) -> Result>; - /// Returns the number of peers currently connected to the node. - fn get_connection_count(&self) -> Result; - /// Returns general state info regarding P2P networking, in a Bitcoin Core v30 - /// compatible shape. - fn get_network_info(&self) -> Result; - /// Returns a block, given a block hash - /// - /// This method returns a block, given a block hash. If the verbosity flag is 0, the block - /// is returned as a hexadecimal string. If the verbosity flag is 1, the block is returned - /// as a json object. - fn get_block(&self, hash: BlockHash, verbosity: Option) -> Result; - /// Return a cached transaction output - /// - /// This method returns a cached transaction output. If the output is not in the cache, - /// or is spent, an empty object is returned. If you want to find a utxo that's not in - /// the cache, you can use the findtxout method. - fn get_tx_out(&self, tx_id: Txid, outpoint: u32) -> Result; - #[doc = include_str!("../../../doc/rpc/stop.md")] - fn stop(&self) -> Result; - /// Tells florestad to connect with a peer - /// - /// You can use this to connect with a given node, providing it's IP address and port. - /// If the `v2transport` option is set, we won't retry connecting using the old, unencrypted - /// P2P protocol. - #[doc = include_str!("../../../doc/rpc/addnode.md")] - fn add_node(&self, node: String, command: AddNodeCommand, v2transport: bool) -> Result; - /// Immediately disconnect from a peer. - /// - /// The peer can be referenced either by node_address or node_id. - /// If referencing by node_id, an empty string must be passed as the node_address. - fn disconnect_node(&self, node_address: String, node_id: Option) -> Result; - /// Finds an specific utxo in the chain - /// - /// You can use this to look for a utxo. If it exists, it will return the amount and - /// scriptPubKey of this utxo. It returns an empty object if the utxo doesn't exist. - /// You must have enabled block filters by setting the `blockfilters=1` option. - fn find_tx_out( - &self, - tx_id: Txid, - outpoint: u32, - script: String, - height_hint: u32, - ) -> Result; - /// Returns statistics about Floresta's memory usage. - /// - /// Returns zeroed values for all runtimes that are not *-gnu or MacOS. - fn get_memory_info(&self, mode: String) -> Result; - /// Returns stats about our RPC server - fn get_rpc_info(&self) -> Result; - #[doc = include_str!("../../../doc/rpc/uptime.md")] - fn uptime(&self) -> Result; - /// Returns a list of all descriptors currently loaded in the wallet - fn list_descriptors(&self) -> Result>; - #[doc = include_str!("../../../doc/rpc/ping.md")] - fn ping(&self) -> Result<()>; -} - /// Since the workflow for jsonrpc is the same for all methods, we can implement a trait /// that will let us call any method on the client, and then implement the methods on any /// client that implements this trait. @@ -158,78 +31,139 @@ pub trait JsonRPCClient: Sized { T: for<'a> serde::de::Deserialize<'a> + serde::de::DeserializeOwned + Debug; } -impl FlorestaRPC for T { +impl BlockchainRpc for T { + type Error = rpc_types::Error; + fn find_tx_out( &self, - tx_id: Txid, - outpoint: u32, + txid: Txid, + vout: u32, script: String, - height_hint: u32, - ) -> Result { + height: u32, + ) -> Result> { self.call( - "findtxout", + &RpcMethods::FindTxOut, &[ - Value::String(tx_id.to_string()), - Value::Number(Number::from(outpoint)), + Value::String(txid.to_string()), + Value::Number(Number::from(vout)), Value::String(script), - Value::Number(Number::from(height_hint)), + Value::Number(Number::from(height)), ], ) } - fn uptime(&self) -> Result { - self.call("uptime", &[]) + fn get_best_block_hash(&self) -> Result { + self.call(&RpcMethods::GetBestBlockHash, &[]) } - fn get_memory_info(&self, mode: String) -> Result { - self.call("getmemoryinfo", &[Value::String(mode)]) + fn get_block(&self, hash: BlockHash, verbosity: Option) -> Result { + let mut params = vec![Value::String(hash.to_string())]; + if let Some(verbosity) = verbosity { + params.push(Value::Number(Number::from(verbosity))); + } + self.call(&RpcMethods::GetBlock, ¶ms) } - fn get_rpc_info(&self) -> Result { - self.call("getrpcinfo", &[]) + fn get_blockchain_info(&self) -> Result { + self.call(&RpcMethods::GetBlockchainInfo, &[]) } - fn add_node(&self, node: String, command: AddNodeCommand, v2transport: bool) -> Result { + fn get_block_count(&self) -> Result { self.call( - "addnode", + &RpcMethods::GetBlockCount, + &[Value::Number(Number::from(0))], + ) + } + + fn get_block_hash(&self, height: u32) -> Result { + self.call( + &RpcMethods::GetBlockHash, + &[Value::Number(Number::from(height))], + ) + } + + fn get_tx_out( + &self, + txid: Txid, + outpoint: u32, + _include_mempool: bool, + ) -> Result> { + let result: serde_json::Value = self.call( + &RpcMethods::GetTxOut, &[ - Value::String(node), - Value::String(command.to_string()), - Value::Bool(v2transport), + Value::String(txid.to_string()), + Value::Number(Number::from(outpoint)), ], - ) + )?; + if result.is_null() { + return Err(Error::TxOutNotFound); + } + serde_json::from_value(result) + .map(Some) + .map_err(Error::Serde) } - fn disconnect_node(&self, node_address: String, node_id: Option) -> Result { - match node_id { - Some(node_id) => self.call( - "disconnectnode", - &[ - Value::String(node_address), - Value::Number(Number::from(node_id)), - ], - ), - None => self.call("disconnectnode", &[Value::String(node_address)]), + fn get_txout_proof( + &self, + tx_ids: &[Txid], + blockhash: Option, + ) -> Result { + let params: Vec = match blockhash { + Some(blockhash) => vec![ + serde_json::to_value(tx_ids) + .expect("Unreachable, Vec can be parsed into a json value"), + Value::String(blockhash.to_string()), + ], + None => { + let txids = serde_json::to_value(tx_ids) + .expect("Unreachable, Vec can be parsed into a json value"); + vec![txids] + } + }; + self.call(&RpcMethods::GetTxOutProof, ¶ms) + } + + fn get_roots(&self) -> Result> { + self.call(&RpcMethods::GetRoots, &[]) + } + + fn get_block_header( + &self, + hash: BlockHash, + verbosity: Option, + ) -> Result { + let mut params = vec![Value::String(hash.to_string())]; + if let Some(verbosity) = verbosity { + params.push(Value::Bool(verbosity)); } + self.call(&RpcMethods::GetBlockHeader, ¶ms) } +} - fn stop(&self) -> Result { - self.call("stop", &[]) +impl WalletRpc for T { + type Error = rpc_types::Error; + + fn load_descriptor(&self, descriptor: String) -> Result { + self.call(&RpcMethods::LoadDescriptor, &[Value::String(descriptor)]) } - fn rescanblockchain( + fn list_descriptors(&self) -> Result> { + self.call(&RpcMethods::ListDescriptors, &[]) + } + + fn rescan_blockchain( &self, start_height: Option, stop_height: Option, use_timestamp: bool, - confidence: RescanConfidence, + confidence: Option, ) -> Result { let start_height = start_height.unwrap_or(0u32); - let stop_height = stop_height.unwrap_or(0u32); + let confidence = confidence.unwrap_or(RescanConfidence::Medium); self.call( - "rescanblockchain", + &RpcMethods::RescanBlockchain, &[ Value::Number(Number::from(start_height)), Value::Number(Number::from(stop_height)), @@ -238,118 +172,87 @@ impl FlorestaRPC for T { ], ) } +} - fn get_roots(&self) -> Result> { - self.call("getroots", &[]) - } - - fn get_block(&self, hash: BlockHash, verbosity: Option) -> Result { - let verbosity = verbosity.unwrap_or(1); +impl NetworkRpc for T { + type Error = rpc_types::Error; + fn add_node(&self, node: String, command: AddNodeCommand, v2transport: bool) -> Result<()> { self.call( - "getblock", + &RpcMethods::AddNode, &[ - Value::String(hash.to_string()), - Value::Number(Number::from(verbosity)), + Value::String(node), + Value::String(command.to_string()), + Value::Bool(v2transport), ], ) } - fn get_block_count(&self) -> Result { - self.call("getblockcount", &[]) - } - - fn get_tx_out(&self, tx_id: Txid, outpoint: u32) -> Result { - let result: serde_json::Value = self.call( - "gettxout", - &[ - Value::String(tx_id.to_string()), - Value::Number(Number::from(outpoint)), - ], - )?; - if result.is_null() { - return Err(Error::TxOutNotFound); + fn disconnect_node(&self, node_address: String, node_id: Option) -> Result<()> { + match node_id { + Some(node_id) => self.call( + &RpcMethods::DisconnectNode, + &[ + Value::String(node_address), + Value::Number(Number::from(node_id)), + ], + ), + None => self.call(&RpcMethods::DisconnectNode, &[Value::String(node_address)]), } - serde_json::from_value(result).map_err(Error::Serde) - } - - fn get_txout_proof(&self, txids: Vec, blockhash: Option) -> Option { - let params: Vec = match blockhash { - Some(blockhash) => vec![ - serde_json::to_value(txids) - .expect("Unreachable, Vec can be parsed into a json value"), - Value::String(blockhash.to_string()), - ], - None => { - let txids = serde_json::to_value(txids) - .expect("Unreachable, Vec can be parsed into a json value"); - vec![txids] - } - }; - self.call("gettxoutproof", ¶ms).ok() } fn get_peer_info(&self) -> Result> { - self.call("getpeerinfo", &[]) + self.call(&RpcMethods::GetPeerInfo, &[]) } fn get_connection_count(&self) -> Result { - self.call("getconnectioncount", &[]) + self.call(&RpcMethods::GetConnectionCount, &[]) } fn get_network_info(&self) -> Result { - self.call("getnetworkinfo", &[]) + self.call(&RpcMethods::GetNetworkInfo, &[]) } - fn get_best_block_hash(&self) -> Result { - self.call("getbestblockhash", &[]) + fn ping(&self) -> Result { + self.call(&RpcMethods::Ping, &[]) } +} - fn get_block_hash(&self, height: u32) -> Result { - self.call("getblockhash", &[Value::Number(Number::from(height))]) +impl RawTransactionRpc for T { + type Error = rpc_types::Error; + + fn send_raw_transaction(&self, tx: String) -> Result { + self.call(&RpcMethods::SendRawTransaction, &[Value::String(tx)]) } - fn get_transaction(&self, tx_id: Txid, verbosity: Option) -> Result { - let verbosity = verbosity.unwrap_or(false); + fn get_raw_transaction(&self, tx_id: Txid, verbosity: Option) -> Result { + let verbosity = verbosity.unwrap_or(0); self.call( - "gettransaction", - &[Value::String(tx_id.to_string()), Value::Bool(verbosity)], + &RpcMethods::GetRawTransaction, + &[ + Value::String(tx_id.to_string()), + Value::Number(Number::from(verbosity)), + ], ) } +} - fn load_descriptor(&self, descriptor: String) -> Result { - self.call("loaddescriptor", &[Value::String(descriptor)]) - } - - fn get_block_filter(&self, height: u32) -> Result { - self.call("getblockfilter", &[Value::Number(Number::from(height))]) - } +impl ControlRpc for T { + type Error = rpc_types::Error; - fn get_block_header( - &self, - hash: BlockHash, - verbosity: Option, - ) -> Result { - let mut params = vec![Value::String(hash.to_string())]; - if let Some(verbosity) = verbosity { - params.push(Value::Bool(verbosity)); - } - self.call("getblockheader", ¶ms) - } - - fn get_blockchain_info(&self) -> Result { - self.call("getblockchaininfo", &[]) + fn stop(&self) -> Result { + self.call(&RpcMethods::Stop, &[]) } - fn send_raw_transaction(&self, tx: String) -> Result { - self.call("sendrawtransaction", &[Value::String(tx)]) + fn uptime(&self) -> Result { + self.call(&RpcMethods::Uptime, &[]) } - fn list_descriptors(&self) -> Result> { - self.call("listdescriptors", &[]) + fn get_memory_info(&self, mode: String) -> Result { + self.call(&RpcMethods::GetMemoryInfo, &[Value::String(mode)]) } - fn ping(&self) -> Result<()> { - self.call("ping", &[]) + fn get_rpc_info(&self) -> Result { + self.call(&RpcMethods::GetRpcInfo, &[]) } } diff --git a/crates/floresta-rpc/src/rpc_interfaces.rs b/crates/floresta-rpc/src/rpc_interfaces.rs new file mode 100644 index 000000000..39907c2f3 --- /dev/null +++ b/crates/floresta-rpc/src/rpc_interfaces.rs @@ -0,0 +1,354 @@ +use core::fmt::Debug; +use core::fmt::Display; +use std::str::FromStr; + +use bitcoin::BlockHash; +use bitcoin::Txid; + +use super::rpc_types::*; + +#[maybe_async::maybe_async] +pub trait BlockchainRpc { + type Error: Display + Debug; + + /// Finds an specific utxo in the chain + /// + /// You can use this to look for a utxo. If it exists, it will return the amount and + /// scriptPubKey of this utxo. It returns an empty object if the utxo doesn't exist. + /// You must have enabled block filters by setting the `blockfilters=1` option. + fn find_tx_out( + &self, + txid: Txid, + vout: u32, + script: String, + height: u32, + ) -> Result, Self::Error>; + + #[doc = include_str!("../../../doc/rpc/getbestblockhash.md")] + fn get_best_block_hash(&self) -> Result; + + #[doc = include_str!("../../../doc/rpc/getblock.md")] + fn get_block( + &self, + hash: BlockHash, + verbosity: Option, + ) -> Result; + + /// Returns general information about the chain we are on + /// + /// This method returns a bunch of information about the chain we are on, including + /// the current height, the best block hash, the difficulty, and whether we are + /// currently in IBD (Initial Block Download) mode. + fn get_blockchain_info(&self) -> Result; + + #[doc = include_str!("../../../doc/rpc/getblockcount.md")] + fn get_block_count(&self) -> Result; + + #[doc = include_str!("../../../doc/rpc/getblockhash.md")] + fn get_block_hash(&self, height: u32) -> Result; + + #[doc = include_str!("../../../doc/rpc/gettxout.md")] + fn get_tx_out( + &self, + txid: Txid, + outpoint: u32, + _include_mempool: bool, + ) -> Result, Self::Error>; + + /// Returns the proof that one or more transactions were included in a block + /// + /// This method returns the Merkle proof, showing that a transaction was included in a block. + /// The proof is returned as a vector hexadecimal string. + fn get_txout_proof( + &self, + tx_ids: &[Txid], + blockhash: Option, + ) -> Result; + + /// Gets the current accumulator for the chain we're on + /// + /// This method returns the current accumulator for the chain we're on. The accumulator is + /// a set of roots, that let's us prove that a UTXO exists in the chain. This method returns + /// a vector of hexadecimal strings, each of which is a root in the accumulator. + fn get_roots(&self) -> Result, Self::Error>; + + /// Returns the block header for the given block hash + /// + /// This method returns the block header for the given block hash, as defined + /// in the Bitcoin protocol specification. A header contains the block's version, + /// the previous block hash, the merkle root, the timestamp, the difficulty target, + /// and the nonce. + fn get_block_header( + &self, + hash: BlockHash, + verbosity: Option, + ) -> Result; +} + +#[maybe_async::maybe_async] +pub trait WalletRpc { + type Error: Display + Debug; + + /// Loads up a descriptor into the wallet + /// + /// This method loads up a descriptor into the wallet. If the rescan option is not None, + /// the wallet will be rescanned for transactions matching the descriptor. If you have + /// compact block filters enabled, this process will be much faster and use less bandwidth. + /// The rescan parameter is the height at which to start the rescan, and should be at least + /// as old as the oldest transaction this descriptor could have been used in. + fn load_descriptor(&self, descriptor: String) -> Result; + + /// Returns a list of all descriptors currently loaded in the wallet + fn list_descriptors(&self) -> Result, Self::Error>; + + #[doc = include_str!("../../../doc/rpc/rescanblockchain.md")] + fn rescan_blockchain( + &self, + start: Option, + stop: Option, + use_timestamp: bool, + confidence: Option, + ) -> Result; +} + +#[maybe_async::maybe_async] +pub trait NetworkRpc { + type Error: Display + Debug; + + /// Tells florestad to connect with a peer + /// + /// You can use this to connect with a given node, providing it's IP address and port. + /// If the `v2transport` option is set, we won't retry connecting using the old, unencrypted + /// P2P protocol. + #[doc = include_str!("../../../doc/rpc/addnode.md")] + fn add_node( + &self, + node: String, + command: AddNodeCommand, + v2transport: bool, + ) -> Result<(), Self::Error>; + + /// Immediately disconnect from a peer. + /// + /// The peer can be referenced either by node_address or node_id. + /// If referencing by node_id, an empty string must be passed as the node_address. + fn disconnect_node( + &self, + node_address: String, + node_id: Option, + ) -> Result<(), Self::Error>; + + /// Gets information about the peers we're connected with + /// + /// This method returns information about the peers we're connected with. This includes + /// the peer's IP address, the peer's version, the peer's user agent, the transport protocol + /// and the peer's current height. + fn get_peer_info(&self) -> Result, Self::Error>; + + /// Returns the number of peers currently connected to the node. + fn get_connection_count(&self) -> Result; + + /// Returns information about the network we're connected to + fn get_network_info(&self) -> Result; + + /// Sends a ping to all peers, checking if they are still alive + fn ping(&self) -> Result; +} + +#[maybe_async::maybe_async] +pub trait RawTransactionRpc { + type Error: Display + Debug; + + /// Sends a hex-encoded transaction to the network + /// + /// This method sends a transaction to the network. The transaction should be encoded as a + /// hexadecimal string. If the transaction is valid, it will be broadcast to the network, and + /// return the transaction id. If the transaction is invalid, an error will be returned. + fn send_raw_transaction(&self, tx: String) -> Result; + + /// Gets a transaction from the blockchain + /// + /// This method returns a transaction that's cached in our wallet. If the verbosity flag is + /// set to false, the transaction is returned as a hexadecimal string. If the verbosity + /// flag is set to true, the transaction is returned as a json object. + fn get_raw_transaction( + &self, + tx_id: Txid, + verbosity: Option, + ) -> Result; +} + +#[maybe_async::maybe_async] +pub trait ControlRpc { + type Error: Display + Debug; + + /// Stops the florestad process + /// + /// This can be used to gracefully stop the florestad process. + fn stop(&self) -> Result; + + /// Returns for how long florestad has been running, in seconds + fn uptime(&self) -> Result; + + /// Returns statistics about Floresta's memory usage. + /// + /// Returns zeroed values for all runtimes that are not *-gnu or MacOS. + fn get_memory_info(&self, mode: String) -> Result; + + /// Returns stats about our RPC server + fn get_rpc_info(&self) -> Result; +} + +macro_rules! define_rpc_methods { + ($enum_name:ident { $($variant:ident),* $(,)? }) => { + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum $enum_name { + $($variant),* + } + + impl FromStr for $enum_name { + type Err = String; + + fn from_str(s: &str) -> Result { + $( + if s.to_lowercase() == stringify!($variant).to_lowercase() { + return Ok(Self::$variant); + } + )* + Err(format!("Unknown method: {}", s)) + } + } + + impl $enum_name { + pub fn to_string(&self) -> String { + match self { + $(Self::$variant => stringify!($variant).to_lowercase(),)* + } + } + + pub fn as_str(&self) -> &'static str { + match self { + $(Self::$variant => stringify!($variant).to_lowercase().leak(),)* + } + } + } + + impl std::ops::Deref for $enum_name { + type Target = str; + + fn deref(&self) -> &str { + self.as_str() + } + } + }; +} + +define_rpc_methods!(RpcMethods { + // Blockchain + FindTxOut, + GetBestBlockHash, + GetBlock, + GetBlockFromPeer, + GetBlockchainInfo, + GetBlockCount, + GetBlockHash, + GetTxOut, + GetTxOutProof, + GetRoots, + GetBlockHeader, + + // Wallet + LoadDescriptor, + ListDescriptors, + RescanBlockchain, + + // Network + GetNetworkInfo, + AddNode, + DisconnectNode, + GetPeerInfo, + GetConnectionCount, + Ping, + + // RawTransactions + SendRawTransaction, + GetRawTransaction, + + // Control + Stop, + Uptime, + GetMemoryInfo, + GetRpcInfo, +}); + +#[cfg(test)] +mod tests { + use super::*; + + define_rpc_methods!(TestRpcMethods { + OneTestMethod, + TwoTestMethod, + }); + + #[test] + fn test_macro_enum_creation() { + let _ = TestRpcMethods::OneTestMethod; + let _ = TestRpcMethods::TwoTestMethod; + } + + #[test] + fn test_macro_from_str() { + assert_eq!( + "onetestmethod".parse::(), + Ok(TestRpcMethods::OneTestMethod) + ); + assert_eq!( + "twotestmethod".parse::(), + Ok(TestRpcMethods::TwoTestMethod) + ); + assert!("invalid".parse::().is_err()); + } + + #[test] + fn test_macro_to_string() { + assert_eq!(TestRpcMethods::OneTestMethod.to_string(), "onetestmethod"); + assert_eq!(TestRpcMethods::TwoTestMethod.to_string(), "twotestmethod"); + } + + #[test] + fn test_macro_roundtrip() { + let method_str = TestRpcMethods::OneTestMethod.to_string(); + assert_eq!( + TestRpcMethods::from_str(&method_str), + Ok(TestRpcMethods::OneTestMethod) + ); + } + + #[test] + fn test_macro_as_str() { + assert_eq!(TestRpcMethods::OneTestMethod.as_str(), "onetestmethod"); + assert_eq!(TestRpcMethods::TwoTestMethod.as_str(), "twotestmethod"); + } + + #[test] + fn test_macro_deref() { + let method = TestRpcMethods::OneTestMethod; + let dereferenced: &str = &method; + assert_eq!(dereferenced, "onetestmethod"); + } + + #[test] + fn test_macro_deref_str_methods() { + let method = TestRpcMethods::TwoTestMethod; + assert_eq!(method.len(), "twotestmethod".len()); + assert!(method.starts_with("two")); + assert!(method.ends_with("method")); + assert_eq!(method.to_uppercase(), "TWOTESTMETHOD"); + } + + #[test] + fn test_macro_as_str_matches_to_string() { + let method = TestRpcMethods::OneTestMethod; + assert_eq!(method.as_str(), method.to_string().as_str()); + } +} diff --git a/crates/floresta-rpc/src/rpc_types.rs b/crates/floresta-rpc/src/rpc_types.rs index db13b5505..eefebc1c5 100644 --- a/crates/floresta-rpc/src/rpc_types.rs +++ b/crates/floresta-rpc/src/rpc_types.rs @@ -4,10 +4,14 @@ use core::error; use core::fmt; use core::fmt::Display; use core::fmt::Formatter; +use std::str::FromStr; -use corepc_types::v30::GetBlockHeaderVerbose; -use corepc_types::v30::GetBlockVerboseOne; +pub use corepc_types::v30::GetBlockHeaderVerbose; +pub use corepc_types::v30::GetBlockVerboseOne; pub use corepc_types::v30::GetNetworkInfo; +pub use corepc_types::v30::GetNetworkInfoNetwork; +pub use corepc_types::v30::GetTxOut; +pub use corepc_types::ScriptPubkey; use serde::Deserialize; use serde::Serialize; @@ -59,8 +63,15 @@ pub struct GetBlockchainInfoRes { pub difficulty: u64, } -/// The information returned by a get_raw_tx -#[derive(Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum RawTxResp { + Zero(String), + One(Box), +} + +/// The information returned by a get_raw_transaction verbose one +#[derive(Debug, Deserialize, Serialize)] pub struct RawTx { /// Whether this tx is in our best known chain pub in_active_chain: bool, @@ -82,12 +93,12 @@ pub struct RawTx { pub locktime: u32, /// A list of inputs being spent by this transaction /// - /// See [TxIn] for more information about the contents of this - pub vin: Vec, + /// See [TxInJson] for more information about the contents of this + pub vin: Vec, /// A list of outputs being created by this tx /// - /// Se [TxOut] for more information - pub vout: Vec, + /// See [TxOutJson] for more information + pub vout: Vec, /// The hash of the block that included this tx, if any pub blockhash: String, /// How many blocks have been mined after this transaction's confirmation @@ -100,19 +111,19 @@ pub struct RawTx { } /// A transaction output returned by some RPCs like gettransaction and getblock -#[derive(Deserialize, Serialize)] -pub struct TxOut { +#[derive(Debug, Deserialize, Serialize)] +pub struct TxOutJson { /// The amount in sats locked in this UTXO pub value: u64, /// This utxo's index inside the transaction pub n: u32, /// The locking script of this utxo - pub script_pub_key: ScriptPubKey, + pub script_pub_key: ScriptPubKeyJson, } /// The locking script inside a txout -#[derive(Deserialize, Serialize)] -pub struct ScriptPubKey { +#[derive(Debug, Deserialize, Serialize)] +pub struct ScriptPubKeyJson { /// A ASM representation for this script /// /// Assembly is a high-level representation of a lower level code. Instructions @@ -133,8 +144,8 @@ pub struct ScriptPubKey { } /// A transaction input returned by some rpcs, like gettransaction and getblock -#[derive(Deserialize, Serialize)] -pub struct TxIn { +#[derive(Debug, Deserialize, Serialize)] +pub struct TxInJson { /// The txid that created this UTXO pub txid: String, /// The index of this UTXO inside the tx that created it @@ -150,7 +161,7 @@ pub struct TxIn { /// A representation for the transaction ScriptSig, returned by some rpcs /// like gettransaction and getblock -#[derive(Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct ScriptSigJson { /// A ASM representation for this scriptSig /// @@ -235,6 +246,17 @@ pub enum RescanConfidence { Exact, } +impl RescanConfidence { + pub const fn as_secs(&self) -> u32 { + match self { + Self::Exact => 0, + Self::Low => 1_380, + Self::Medium => 1_800, + Self::High => 2_760, + } + } +} + #[derive(Debug)] /// All possible errors returned by the jsonrpc pub enum Error { @@ -347,17 +369,35 @@ pub enum AddNodeCommand { Onetry, } +impl AddNodeCommand { + const fn as_str(&self) -> &'static str { + match self { + Self::Add => "add", + Self::Remove => "remove", + Self::Onetry => "onetry", + } + } +} + /// A simple implementation to convert the enum to a string. /// Useful for get the subcommand name of addnode with /// command.to_string() impl Display for AddNodeCommand { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let cmd = match self { - AddNodeCommand::Add => "add", - AddNodeCommand::Remove => "remove", - AddNodeCommand::Onetry => "onetry", - }; - write!(f, "{cmd}") + write!(f, "{}", self.as_str()) + } +} + +impl FromStr for AddNodeCommand { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "add" => Ok(Self::Add), + "remove" => Ok(Self::Remove), + "onetry" => Ok(Self::Onetry), + _ => Err(format!("Invalid command: {}", s)), + } } } diff --git a/doc/build-macos.md b/doc/build-macos.md index fadbcda18..7bc78da5e 100644 --- a/doc/build-macos.md +++ b/doc/build-macos.md @@ -47,7 +47,9 @@ cd Floresta/ and build with cargo build ```bash -cargo build --release +# Build florestad and floresta-cli separately +cargo build --release -p florestad +cargo build --release -p floresta-cli # Alternatively, you can add florestad and floresta-cli to the path with cargo install --path ./bin/florestad --locked diff --git a/doc/build-unix.md b/doc/build-unix.md index 47980932a..dcf5e53e6 100644 --- a/doc/build-unix.md +++ b/doc/build-unix.md @@ -34,7 +34,10 @@ cd Floresta/ and build with cargo build ```bash -cargo build --release +# Build florestad and floresta-cli separately +cargo build --release -p florestad +cargo build --release -p floresta-cli + # Alternatively, you can add florestad and floresta-cli to the path with cargo install --path ./bin/florestad --locked diff --git a/doc/metrics.md b/doc/metrics.md index 02da46bf2..422a84b3d 100644 --- a/doc/metrics.md +++ b/doc/metrics.md @@ -3,7 +3,9 @@ This project uses [`Prometheus`](https://prometheus.io/) as a monitoring system. To enable it you must build the project with the `metrics` feature enabled: ```sh -cargo build --release --features metrics +# Build florestad and floresta-cli separately +cargo build --release -p florestad --features metrics +cargo build --release -p floresta-cli ``` The easiest way to visualize those metrics is by using some observability graphic tool like [Grafana](https://grafana.com/). To make it easier, you can also straight away use the `docker-compose.yml` file to spin up an infrastructure that will run the project with Prometheus and Grafana. diff --git a/doc/running-tests.md b/doc/running-tests.md index 80315146f..d5687cc1e 100644 --- a/doc/running-tests.md +++ b/doc/running-tests.md @@ -7,7 +7,9 @@ This document is a guide for the different testing options available in Floresta The tests in `floresta-cli` depend on the compiled `florestad` binary. Make sure to build the entire project first by running: ```bash -cargo build +# Build florestad and floresta-cli separately +cargo build -p florestad +cargo build -p floresta-cli ``` The functional tests also need some dependencies, we use python for writing them and `uv` to manage its dependencies. diff --git a/justfile b/justfile index db1031b4a..7c2f78342 100644 --- a/justfile +++ b/justfile @@ -1,6 +1,21 @@ _default: @just --list +# List of crates of floresta +_crates := `cargo metadata --format-version 1 --no-deps | jq -r '.packages[] | select(.source == null) | .name' | tr '\n' ' '` + +# Generic recipe to run a command for each crate +# Usage: just run-each "cargo build" "" +# just run-each "cargo test --lib" "-- --nocapture" +run-each before_p after_p: + #!/usr/bin/env bash + set -e + echo "Crates {{ _crates }} " + for crate in {{ _crates }}; do + echo "Running: {{ before_p }} -p $crate {{ after_p }}" + {{ before_p }} -p "$crate" {{ after_p }} || exit 1 + done + # Checks whether a command is available. check-command cmd recipe="check-command" link_to_package="": @if ! command -v "{{ cmd }}" >/dev/null; then \ @@ -21,11 +36,11 @@ run-release: # Compile project with debug options build: - cargo build + @just run-each "cargo build" "" # Compile project with release options build-release: - cargo build --release + @just run-each "cargo build --release" "" # Clean project build directory clean: @@ -47,7 +62,7 @@ test-unit crate="": build # Execute workspace-related tests test-wkspc: build - cargo test --workspace -- --nocapture + @just run-each "cargo test" "-- --nocapture" # Execute tests/prepare.sh. test-functional-prepare arg="": @@ -91,10 +106,10 @@ lint: @just doc-check # 1) Run with no features - cargo +nightly clippy --workspace --all-targets --no-default-features + @just run-each "cargo +nightly clippy --all-targets" "--no-default-features" # 2) Run with all features - cargo +nightly clippy --workspace --all-targets --all-features + @just run-each "cargo +nightly clippy --all-targets" "--all-features" @just spell-check