From 3e3e6d4f8855cf1e7ca1613e64988c20d996f4f8 Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Sun, 19 Oct 2025 18:27:27 -0300 Subject: [PATCH 01/10] Adjust get_block RPC to match Bitcoin Core output - Fix strippedsize to correctly calculate and return the block size excluding witness data (previously returned total block size) - Ensure bits, chainwork, and version_hex fields return data in big-endian hex order for proper serialization - Adjust difficulty to use the appropriate calculation method for accurate float representation - Add target field to include the block's target value in big-endian hex, which was missing before --- .../floresta-node/src/json_rpc/blockchain.rs | 18 +++++++++++++----- crates/floresta-node/src/json_rpc/res.rs | 7 ++++++- crates/floresta-rpc/src/rpc_types.rs | 6 +++++- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/crates/floresta-node/src/json_rpc/blockchain.rs b/crates/floresta-node/src/json_rpc/blockchain.rs index ffc49463b..3c427d984 100644 --- a/crates/floresta-node/src/json_rpc/blockchain.rs +++ b/crates/floresta-node/src/json_rpc/blockchain.rs @@ -234,11 +234,18 @@ impl RpcImpl { block.header.time }; + let witness_block_size = block + .txdata + .iter() + .map(|tx| tx.total_size() - tx.base_size()) + .sum::(); + let strippedsize = block.total_size() - witness_block_size; + let block = GetBlockResVerbose { - bits: serialize_hex(&block.header.bits), - chainwork: block.header.work().to_string(), + bits: serialize_hex(&block.header.bits.to_consensus().to_be()), + chainwork: serialize_hex(&block.header.work().to_be_bytes()), confirmations: (tip - height) + 1, - difficulty: block.header.difficulty(self.chain.get_params()), + difficulty: block.header.difficulty_float(), hash: block.header.block_hash().to_string(), height, merkleroot: block.header.merkle_root.to_string(), @@ -252,7 +259,7 @@ impl RpcImpl { .map(|tx| tx.compute_txid().to_string()) .collect(), version: block.header.version.to_consensus(), - version_hex: serialize_hex(&block.header.version), + version_hex: serialize_hex(&(block.header.version.to_consensus() as u32).to_be()), weight: block.weight().to_wu() as usize, mediantime: median_time_past, n_tx: block.txdata.len(), @@ -261,7 +268,8 @@ impl RpcImpl { .get_block_hash(height + 1) .ok() .map(|h| h.to_string()), - strippedsize: block.total_size(), + strippedsize, + target: serialize_hex(&block.header.target().to_be_bytes()), }; Ok(block) diff --git a/crates/floresta-node/src/json_rpc/res.rs b/crates/floresta-node/src/json_rpc/res.rs index bd0ab9f5f..5f7403cc2 100644 --- a/crates/floresta-node/src/json_rpc/res.rs +++ b/crates/floresta-node/src/json_rpc/res.rs @@ -219,7 +219,7 @@ pub struct GetBlockResVerbose { /// difficulty is a multiple of the smallest possible difficulty. So to find the actual /// difficulty you have to multiply this by the min_diff. /// For mainnet, mindiff is 2 ^ 32 - pub difficulty: u128, + pub difficulty: f64, /// Commullative work in this network /// @@ -235,6 +235,11 @@ pub struct GetBlockResVerbose { #[serde(skip_serializing_if = "Option::is_none")] /// The hash of the block coming after this one, if any pub nextblockhash: Option, + + /// Represents the current proof-of-work target as a 256-bit number in string format. + /// A block's SHA-256 hash must be less than or equal to this value to be accepted by the network. + /// Lower values indicate higher mining difficulty. + pub target: String, } #[derive(Debug)] diff --git a/crates/floresta-rpc/src/rpc_types.rs b/crates/floresta-rpc/src/rpc_types.rs index 3333793cd..80ee35dda 100644 --- a/crates/floresta-rpc/src/rpc_types.rs +++ b/crates/floresta-rpc/src/rpc_types.rs @@ -258,7 +258,7 @@ pub struct GetBlockResVerbose { /// difficulty is a multiple of the smallest possible difficulty. So to find the actual /// difficulty you have to multiply this by the min_diff. /// For mainnet, mindiff is 2 ^ 32 - pub difficulty: u128, + pub difficulty: f64, /// Commullative work in this network /// /// This is a estimate of how many hashes the network has ever made to produce this chain @@ -270,6 +270,10 @@ pub struct GetBlockResVerbose { #[serde(skip_serializing_if = "Option::is_none")] /// The hash of the block coming after this one, if any pub nextblockhash: Option, + /// Represents the current proof-of-work target as a 256-bit number in string format. + /// A block's SHA-256 hash must be less than or equal to this value to be accepted by the network. + /// Lower values indicate higher mining difficulty. + pub target: String, } /// A confidence enum to auxiliate rescan timestamp values. From b5917661db01e09427c102a452b2233203d829e6 Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Thu, 16 Oct 2025 16:14:44 -0300 Subject: [PATCH 02/10] add get_best_block_hash method to FlorestaRPC trait --- crates/floresta-rpc/src/rpc.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/floresta-rpc/src/rpc.rs b/crates/floresta-rpc/src/rpc.rs index 4392ccded..9bbeec9cb 100644 --- a/crates/floresta-rpc/src/rpc.rs +++ b/crates/floresta-rpc/src/rpc.rs @@ -26,6 +26,8 @@ pub trait FlorestaRPC { /// 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 (most recent) block in the 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 @@ -284,6 +286,10 @@ impl FlorestaRPC for T { self.call("getpeerinfo", &[]) } + fn get_best_block_hash(&self) -> Result { + self.call("getbestblockhash", &[]) + } + fn get_block_hash(&self, height: u32) -> Result { self.call("getblockhash", &[Value::Number(Number::from(height))]) } From b2be085deb262a491b219c0a0320461186e63ec3 Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Mon, 20 Oct 2025 00:34:47 -0300 Subject: [PATCH 03/10] refactor: change difficulty type from u64 to f64 in GetBlockchainInfoRes --- crates/floresta-node/src/json_rpc/blockchain.rs | 2 +- crates/floresta-node/src/json_rpc/res.rs | 2 +- crates/floresta-rpc/src/rpc_types.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/floresta-node/src/json_rpc/blockchain.rs b/crates/floresta-node/src/json_rpc/blockchain.rs index 3c427d984..2ba11b620 100644 --- a/crates/floresta-node/src/json_rpc/blockchain.rs +++ b/crates/floresta-node/src/json_rpc/blockchain.rs @@ -314,7 +314,7 @@ impl RpcImpl { root_count, root_hashes, chain: self.network.to_string(), - difficulty: latest_header.difficulty(self.chain.get_params()) as u64, + difficulty: latest_header.difficulty_float(), progress: validated_blocks as f32 / height as f32, }) } diff --git a/crates/floresta-node/src/json_rpc/res.rs b/crates/floresta-node/src/json_rpc/res.rs index 5f7403cc2..b406734b4 100644 --- a/crates/floresta-node/src/json_rpc/res.rs +++ b/crates/floresta-node/src/json_rpc/res.rs @@ -17,7 +17,7 @@ pub struct GetBlockchainInfoRes { pub root_hashes: Vec, pub chain: String, pub progress: f32, - pub difficulty: u64, + pub difficulty: f64, } /// A confidence enum to auxiliate rescan timestamp values. diff --git a/crates/floresta-rpc/src/rpc_types.rs b/crates/floresta-rpc/src/rpc_types.rs index 80ee35dda..246afa7f2 100644 --- a/crates/floresta-rpc/src/rpc_types.rs +++ b/crates/floresta-rpc/src/rpc_types.rs @@ -49,7 +49,7 @@ pub struct GetBlockchainInfoRes { /// /// On average, miners needs to make `difficulty` hashes before finding one that /// solves a block's PoW - pub difficulty: u64, + pub difficulty: f64, } /// The information returned by a get_raw_tx From f66afa33dcbe1b7e0b44922bd1318bd8e16a7393 Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Tue, 21 Oct 2025 23:05:38 -0300 Subject: [PATCH 04/10] refactor: update get_tx_out method to return structured GetTxOut type --- crates/floresta-rpc/src/rpc.rs | 12 +++++--- crates/floresta-rpc/src/rpc_types.rs | 46 ++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/crates/floresta-rpc/src/rpc.rs b/crates/floresta-rpc/src/rpc.rs index 9bbeec9cb..c15b3ee9c 100644 --- a/crates/floresta-rpc/src/rpc.rs +++ b/crates/floresta-rpc/src/rpc.rs @@ -100,7 +100,7 @@ pub trait FlorestaRPC { /// 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; + fn get_tx_out(&self, tx_id: Txid, outpoint: u32) -> Result; /// Stops the florestad process /// /// This can be used to gracefully stop the florestad process. @@ -256,14 +256,18 @@ impl FlorestaRPC for T { self.call("getblockcount", &[]) } - fn get_tx_out(&self, tx_id: Txid, outpoint: u32) -> Result { - self.call( + 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); + } + serde_json::from_value(result).map_err(Error::Serde) } fn get_txout_proof(&self, txids: Vec, blockhash: Option) -> Option { diff --git a/crates/floresta-rpc/src/rpc_types.rs b/crates/floresta-rpc/src/rpc_types.rs index 246afa7f2..ca75b9bae 100644 --- a/crates/floresta-rpc/src/rpc_types.rs +++ b/crates/floresta-rpc/src/rpc_types.rs @@ -1,5 +1,6 @@ use std::fmt::Display; +use bitcoin::BlockHash; use serde::Deserialize; use serde::Serialize; @@ -276,6 +277,47 @@ pub struct GetBlockResVerbose { pub target: String, } +/// The information by UTXO +#[derive(Debug, Deserialize, Serialize)] +pub struct GetTxOut { + /// The hash of the block at the tip of the chain + pub bestblock: BlockHash, + + /// The number of confirmations + pub confirmations: u32, + + /// The transaction value in BTC + pub value: f64, + + #[serde(rename = "scriptPubKey")] + /// Script Public Key struct + pub script_pubkey: ScriptPubkeyDescription, + + /// Coinbase or not + pub coinbase: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +/// Struct helper for RpcGetTxOut +pub struct ScriptPubkeyDescription { + /// Disassembly of the output script + pub asm: String, + + /// Inferred descriptor for the output + pub desc: String, + + /// The raw output script bytes, hex-encoded + pub hex: String, + + /// The type, eg pubkeyhash + #[serde(rename = "type")] + pub type_field: String, + + /// The Bitcoin address (only if a well-defined address exists) + #[serde(skip_serializing_if = "Option::is_none")] + pub address: Option, +} + /// A confidence enum to auxiliate rescan timestamp values. /// /// Tells how much confidence you need for this rescan request. That is, the how conservative you want floresta to be when determining which block to start the rescan. @@ -322,6 +364,9 @@ pub enum Error { /// The user requested a rescan based on invalid values. InvalidRescanVal, + + /// The requested transaction output was not found + TxOutNotFound, } impl From for Error { @@ -347,6 +392,7 @@ impl Display for Error { Error::EmptyResponse => write!(f, "got an empty response from server"), Error::InvalidVerbosity => write!(f, "invalid verbosity level"), Error::InvalidRescanVal => write!(f, "Invalid rescan values"), + Error::TxOutNotFound => write!(f, "Transaction output was not found"), } } } From f01cc94a65a02c5bfd46d3b5557396f0afc44f46 Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Tue, 21 Oct 2025 23:10:24 -0300 Subject: [PATCH 05/10] fix: expose fields in GetMemInfoStats and MemInfoLocked structs Make the fields in GetMemInfoStats and MemInfoLocked public to allow implementations using floresta_rpc to access getmemoryinfo in the rpc --- crates/floresta-rpc/src/rpc_types.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/floresta-rpc/src/rpc_types.rs b/crates/floresta-rpc/src/rpc_types.rs index ca75b9bae..fde2f853a 100644 --- a/crates/floresta-rpc/src/rpc_types.rs +++ b/crates/floresta-rpc/src/rpc_types.rs @@ -399,26 +399,26 @@ impl Display for Error { #[derive(Debug, Default, Serialize, Deserialize)] pub struct GetMemInfoStats { - locked: MemInfoLocked, + pub locked: MemInfoLocked, } #[derive(Debug, Default, Serialize, Deserialize)] pub struct MemInfoLocked { /// Memory currently in use, in bytes - used: u64, + pub used: u64, /// Memory currently free, in bytes - free: u64, + pub free: u64, /// Total memory allocated, in bytes - total: u64, + pub total: u64, /// Total memory locked, in bytes /// /// If total is less than total, then some pages may be on swap or not philysically allocated /// yet - locked: u64, + pub locked: u64, /// How many chunks are currently in use - chunks_used: u64, + pub chunks_used: u64, /// How many chunks are currently free - chunks_free: u64, + pub chunks_free: u64, } #[derive(Debug, Serialize, Deserialize)] From 0bc3b59da59455b3645a5b829f887aea99366f7c Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Tue, 21 Oct 2025 23:12:47 -0300 Subject: [PATCH 06/10] fix: expose fields in ActiveCommand and GetRpcInfoRes structs Make the fields in ActiveCommand and GetRpcInfoRes public to allow implementations to access RPC information details via the getrpcinfo method. --- crates/floresta-rpc/src/rpc_types.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/floresta-rpc/src/rpc_types.rs b/crates/floresta-rpc/src/rpc_types.rs index fde2f853a..8fdf8d04d 100644 --- a/crates/floresta-rpc/src/rpc_types.rs +++ b/crates/floresta-rpc/src/rpc_types.rs @@ -430,14 +430,14 @@ pub enum GetMemInfoRes { #[derive(Debug, Serialize, Deserialize)] pub struct ActiveCommand { - method: String, - duration: u64, + pub method: String, + pub duration: u64, } #[derive(Debug, Serialize, Deserialize)] pub struct GetRpcInfoRes { - active_commands: Vec, - logpath: String, + pub active_commands: Vec, + pub logpath: String, } #[derive(Debug, Clone, Serialize, Deserialize)] From cef0c5c1a1782188d403045e79be64843801389c Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Tue, 14 Oct 2025 16:23:13 -0300 Subject: [PATCH 07/10] chore: update Rust toolchain to version 1.90.0 across workflows and documentation --- .github/workflows/benchmarks.yml | 2 +- .github/workflows/rust.yml | 2 +- CONTRIBUTING.md | 2 +- Dockerfile | 2 +- contrib/nix/build_floresta.nix | 2 +- crates/floresta-wire/README.md | 2 +- rust-toolchain.toml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 4f82cda89..c5bd2e46a 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.74.1 + - uses: dtolnay/rust-toolchain@1.90.0 with: components: rustfmt, clippy diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 597c4b93f..55c345b6f 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -44,7 +44,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.74.1 # The version in our `rust-toolchain.toml` + - uses: dtolnay/rust-toolchain@1.90.0 # The version in our `rust-toolchain.toml` with: components: rustfmt, clippy - uses: taiki-e/install-action@cargo-hack diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a6c1fcd7a..0bb7d8253 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,7 @@ When adding a new feature ensure that it is covered by functional tests where po When refactoring, structure your PR to make it easy to review and don't hesitate to split it into multiple small, focused PRs. -The Minimum Supported Rust Version is **1.74.1** (enforced by our CI). +The Minimum Supported Rust Version is **1.90.0** (enforced by our CI). Commits should cover both the issue fixed and the solution's rationale. diff --git a/Dockerfile b/Dockerfile index e3fb9ece3..e568bfa8a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN apt-get update && apt-get install -y \ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y ENV PATH="/root/.cargo/bin:${PATH}" -RUN rustup default 1.74.0 +RUN rustup default 1.90.0 WORKDIR /opt/app diff --git a/contrib/nix/build_floresta.nix b/contrib/nix/build_floresta.nix index 5a645336b..37574994c 100644 --- a/contrib/nix/build_floresta.nix +++ b/contrib/nix/build_floresta.nix @@ -20,7 +20,7 @@ let else basicDeps; - # This is the 1.74.1 rustup (and its components) toolchain from our `./rust-toolchain.toml` + # This is the 1.90.0 rustup (and its components) toolchain from our `./rust-toolchain.toml` florestaRust = pkgs.rust-bin.fromRustupToolchainFile "${src}/rust-toolchain.toml"; # This sets the rustc and cargo to the ones from the florestaRust. diff --git a/crates/floresta-wire/README.md b/crates/floresta-wire/README.md index 9a390d9cf..970005af0 100644 --- a/crates/floresta-wire/README.md +++ b/crates/floresta-wire/README.md @@ -66,7 +66,7 @@ Example `peers.json`: ## Minimum Supported Rust Version (MSRV) -This library should compile with any combination of features on **Rust 1.74.1**. +This library should compile with any combination of features on **Rust 1.90.0**. ## License diff --git a/rust-toolchain.toml b/rust-toolchain.toml index cb0fe3064..5e1781e54 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.74.1" +channel = "1.90.0" components = ["rustfmt", "clippy"] profile = "default" From 82857a162289b0f82b4c517c921ee04ca08d65b9 Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Tue, 21 Oct 2025 22:51:34 -0300 Subject: [PATCH 08/10] test: add functional tests for Floresta with Utreexod and Bitcoind integration - Created a new workspace member `test-rust` for functional testing. - Added `Cargo.toml` for `test-rust` with dependencies for testing. - Implemented a basic `main.rs` file for the test project. - Developed common utilities for setting up Bitcoind and Florestad nodes. - Implemented Utreexod setup and RPC client for interaction with the Utreexod node. - Added tests for Bitcoind and Florestad integration, including peer connection checks. - Created functions to generate blocks and manage node interactions. - Established a shared setup for tests to optimize resource usage. --- Cargo.lock | 576 +++++++++++++++++++++++++++- Cargo.toml | 2 + Dockerfile | 1 + test-rust/Cargo.toml | 30 ++ test-rust/src/main.rs | 3 + test-rust/tests/common/bitcoind.rs | 98 +++++ test-rust/tests/common/florestad.rs | 85 ++++ test-rust/tests/common/mod.rs | 381 ++++++++++++++++++ test-rust/tests/common/utreexod.rs | 149 +++++++ 9 files changed, 1314 insertions(+), 11 deletions(-) create mode 100644 test-rust/Cargo.toml create mode 100644 test-rust/src/main.rs create mode 100644 test-rust/tests/common/bitcoind.rs create mode 100644 test-rust/tests/common/florestad.rs create mode 100644 test-rust/tests/common/mod.rs create mode 100644 test-rust/tests/common/utreexod.rs diff --git a/Cargo.lock b/Cargo.lock index 59c364e75..9d320494f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -335,7 +335,7 @@ dependencies = [ "rustc-hash", "shlex", "syn", - "which", + "which 4.4.2", ] [[package]] @@ -372,6 +372,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda569d741b895131a88ee5589a467e73e9c4718e958ac9308e4f7dc44b6945" dependencies = [ "base58ck", + "base64 0.21.7", "bech32 0.11.0", "bitcoin-internals 0.3.0", "bitcoin-io 0.1.3", @@ -518,6 +519,26 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cast" version = "0.3.0" @@ -577,7 +598,7 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -741,12 +762,66 @@ dependencies = [ "url", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "corepc-client" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ce205b817339b55d93bdb41d66704cfc5299f89576ab24107bd2abf4c29c1e" +dependencies = [ + "bitcoin 0.32.7", + "corepc-types", + "jsonrpc", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "corepc-node" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76025e0755bc411fda75e0912a0a0e511d13b54988bf05b90d7681f0c7570b67" +dependencies = [ + "anyhow", + "bitcoin_hashes 0.14.0", + "corepc-client", + "flate2", + "log", + "minreq", + "serde_json", + "tar", + "tempfile", + "which 3.1.1", + "zip", +] + +[[package]] +name = "corepc-types" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f0231e773ddcfebb8eb8627a16553de56ab064a03843d847f409107ad661f25" +dependencies = [ + "bitcoin 0.32.7", + "serde", + "serde_json", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1011,6 +1086,44 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "electrsd" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e2d112cb8b9fa56e95632eedfc9ca7ace67910c28764367b1415b8148ec062" +dependencies = [ + "bitcoin_hashes 0.14.0", + "corepc-client", + "corepc-node", + "electrum-client", + "log", + "minreq", + "nix", + "which 4.4.2", + "zip", +] + +[[package]] +name = "electrum-client" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7b07e2578a6df0093b101915c79dca0119d7f7810099ad9eef11341d2ae57" +dependencies = [ + "bitcoin 0.32.7", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1033,6 +1146,18 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + [[package]] name = "flate2" version = "1.1.2" @@ -1225,7 +1350,7 @@ dependencies = [ "metrics", "oneshot", "rand", - "rustls", + "rustls 0.23.27", "rustreexo", "serde", "serde_json", @@ -1261,6 +1386,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1293,6 +1433,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1301,6 +1442,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + [[package]] name = "futures-sink" version = "0.3.31" @@ -1320,9 +1467,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", + "futures-io", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -1568,6 +1719,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls 0.23.27", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-timeout" version = "0.5.2" @@ -1581,12 +1748,29 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", "futures-core", @@ -1594,12 +1778,16 @@ dependencies = [ "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2 0.5.10", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -1762,6 +1950,22 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is-terminal" version = "0.4.16" @@ -1904,6 +2108,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.9.1", "libc", + "redox_syscall 0.5.12", ] [[package]] @@ -1985,6 +2190,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "metrics" version = "0.2.0" @@ -2044,8 +2258,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0d2aaba477837b46ec1289588180fabfccf0c3b1d1a0c6b1866240cd6cd5ce9" dependencies = [ "log", + "rustls 0.21.12", + "rustls-webpki 0.101.7", "serde", "serde_json", + "webpki-roots 0.25.4", ] [[package]] @@ -2059,6 +2276,37 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + [[package]] name = "nom" version = "7.1.3" @@ -2135,6 +2383,50 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "openssl" +version = "0.10.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -2509,6 +2801,48 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.12.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower 0.5.2", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "ring" version = "0.17.14" @@ -2561,6 +2895,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + [[package]] name = "rustls" version = "0.23.27" @@ -2571,7 +2917,7 @@ dependencies = [ "log", "once_cell", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.103.3", "subtle", "zeroize", ] @@ -2594,6 +2940,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustls-webpki" version = "0.103.3" @@ -2636,12 +2992,31 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "secp256k1" version = "0.28.2" @@ -2681,6 +3056,29 @@ dependencies = [ "cc", ] +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.219" @@ -2884,6 +3282,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -2910,6 +3311,27 @@ dependencies = [ "windows", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.9.1", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -2923,6 +3345,17 @@ dependencies = [ "version-compare", ] +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.12.16" @@ -2942,6 +3375,21 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "test-functional" +version = "0.1.0" +dependencies = [ + "anyhow", + "bitcoin 0.32.7", + "electrsd", + "floresta-rpc", + "once_cell", + "rand", + "rcgen", + "reqwest", + "serde_json", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -3052,13 +3500,23 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls", + "rustls 0.23.27", "tokio", ] @@ -3210,8 +3668,12 @@ checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ "bitflags 2.9.1", "bytes", + "futures-util", "http", + "http-body", + "iri-string", "pin-project-lite", + "tower 0.5.2", "tower-layer", "tower-service", ] @@ -3337,7 +3799,7 @@ dependencies = [ "cookie_store", "log", "percent-encoding", - "rustls", + "rustls 0.23.27", "rustls-pemfile", "rustls-pki-types", "serde", @@ -3395,6 +3857,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version-compare" version = "0.2.0" @@ -3467,6 +3935,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.100" @@ -3509,6 +3990,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + [[package]] name = "webpki-roots" version = "0.26.11" @@ -3527,6 +4014,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +dependencies = [ + "libc", +] + [[package]] name = "which" version = "4.4.2" @@ -3600,9 +4096,9 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement 0.60.0", "windows-interface 0.59.1", - "windows-link", + "windows-link 0.1.1", "windows-result 0.3.4", - "windows-strings", + "windows-strings 0.4.2", ] [[package]] @@ -3655,6 +4151,23 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result 0.3.4", + "windows-strings 0.3.1", + "windows-targets 0.53.0", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -3670,7 +4183,16 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.1", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link 0.1.1", ] [[package]] @@ -3679,7 +4201,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -3709,6 +4231,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -3918,6 +4449,16 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.0.7", +] + [[package]] name = "xxhash-rust" version = "0.8.15" @@ -4053,6 +4594,19 @@ dependencies = [ "syn", ] +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "bzip2", + "crc32fast", + "crossbeam-utils", + "flate2", +] + [[package]] name = "zmq" version = "0.10.0" diff --git a/Cargo.toml b/Cargo.toml index b7c328b1a..5d62e38ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] resolver = "2" +exclude = ["test-rust"] members = [ # Libraries "crates/floresta", @@ -19,6 +20,7 @@ members = [ # Tests and monitoring "metrics", "fuzz", + "test-rust", ] default-members = [ diff --git a/Dockerfile b/Dockerfile index e568bfa8a..432532d7d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,6 +20,7 @@ COPY Cargo.* ./ COPY bin/ bin/ COPY crates/ crates/ COPY fuzz/ fuzz/ +COPY test-rust/ test-rust/ COPY metrics/ metrics/ COPY doc/ doc/ RUN --mount=type=cache,target=/usr/local/cargo/registry \ diff --git a/test-rust/Cargo.toml b/test-rust/Cargo.toml new file mode 100644 index 000000000..731c6183f --- /dev/null +++ b/test-rust/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "test-functional" +version = "0.1.0" +edition = "2021" +license = "MIT" +description = """ + Functional tests for Floresta, the tests use the Utreexod and Bitcoind in the tests. +""" +repository = "https://github.com/vinteumorg/Floresta" +readme.workspace = true # Floresta/README.md + +keywords = ["bitcoin", "utreexo", "node", "blockchain", "rust"] +categories = ["cryptography::cryptocurrencies", "command-line-utilities"] + +[dependencies] + + +[dev-dependencies] +floresta-rpc = { path = "../crates/floresta-rpc", features = ["clap"] } +rand = "0.8.5" +rcgen = "0.13" +once_cell = "1.19" +serde_json = "1.0" +reqwest = { version = "0.12", features = ["json", "blocking"] } +anyhow = "1.0" +bitcoin = "0.32.7" +electrsd = { version = "0.36.0", default-features = false, features = ["legacy", "esplora_a33e97e1", "corepc-node_29_0"] } + +[features] +functional-tests = [] \ No newline at end of file diff --git a/test-rust/src/main.rs b/test-rust/src/main.rs new file mode 100644 index 000000000..78c5bbbec --- /dev/null +++ b/test-rust/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + // This project is a test in rust of floresta +} diff --git a/test-rust/tests/common/bitcoind.rs b/test-rust/tests/common/bitcoind.rs new file mode 100644 index 000000000..b4d712d66 --- /dev/null +++ b/test-rust/tests/common/bitcoind.rs @@ -0,0 +1,98 @@ +#![allow(dead_code)] + +use std::env; + +use electrsd::corepc_client::client_sync::Auth; +use electrsd::corepc_node; +use electrsd::corepc_node::Client as BitcoindClient; +use electrsd::corepc_node::Node; + +use crate::common::run_node; +use crate::common::wait_for_condition; +use crate::common::DESCRIPTOR_EXTERNAL; +use crate::common::DESCRIPTOR_INTERNAL; + +const VIEW_LOGS: bool = false; +const WALLET_NAME: &str = "floresta_test"; + +pub(crate) fn setup_bitcoind(v2_transport: bool) -> Node { + let bitcoind_exe = env::var("BITCOIND_EXE") + .ok() + .or_else(|| corepc_node::downloaded_exe_path().ok()) + .expect( + "you need to provide an env var BITCOIND_EXE or specify a bitcoind version feature", + ); + + let mut bitcoind_conf = corepc_node::Conf::default(); + bitcoind_conf.network = "regtest"; + bitcoind_conf.args.push("-rest"); + bitcoind_conf.p2p = corepc_node::P2P::Yes; + if !v2_transport { + bitcoind_conf.args.push("-v2transport=0"); + } + bitcoind_conf.view_stdout = VIEW_LOGS; + bitcoind_conf.wallet = Some(WALLET_NAME.to_string()); + let bitcoind = Node::with_conf(bitcoind_exe, &bitcoind_conf).unwrap(); + + let args = vec![ + serde_json::json!({ + "desc": DESCRIPTOR_INTERNAL, + "timestamp": 1455191478, + "label": "address internal", + "internal": true, + "active": true + }), + serde_json::json!({ + "desc": DESCRIPTOR_EXTERNAL, + "label": "address receive", + "timestamp": 1455191480, + "active": true + }), + ]; + let _: serde_json::Value = bitcoind + .client + .call("importdescriptors", &[serde_json::Value::Array(args)]) + .expect("importdescriptors failed"); + + bitcoind +} + +pub(crate) fn setup_bitcoind_by_bitcoind(bitcoind: &mut Node, v2_transport: bool) { + let bitcoind_exe = env::var("BITCOIND_EXE") + .ok() + .or_else(|| corepc_node::downloaded_exe_path().ok()) + .expect( + "you need to provide an env var BITCOIND_EXE or specify a bitcoind version feature", + ); + + let args_p2p_port = format!("-bind={}", bitcoind.params.p2p_socket.unwrap()); + let rpcport_arg = format!("-rpcport={}", bitcoind.params.rpc_socket.port()); + let data_dir_arg = format!("-datadir={}", bitcoind.workdir().display()); + let rpc = "bitcoind".to_string(); + let rpc_user_arg = format!("-rpcuser={}", rpc); + let rpc_password_arg = format!("-rpcpassword={}", rpc); + let v2_tranpost_arg = if v2_transport { + "-v2transport=1" + } else { + "-v2transport=0" + }; + + let args = vec![ + "-regtest", + "-rpcthreads=1", + &data_dir_arg, + &args_p2p_port, + &rpcport_arg, + &rpc_user_arg, + &rpc_password_arg, + &v2_tranpost_arg, + ]; + + let _ = run_node(bitcoind_exe, args, VIEW_LOGS); + + let rpc_url = bitcoind.rpc_url_with_wallet(WALLET_NAME); + bitcoind.client = + BitcoindClient::new_with_auth(&rpc_url, Auth::UserPass(rpc.clone(), rpc)).unwrap(); + + wait_for_condition(|| bitcoind.client.get_rpc_info().is_ok()).unwrap(); +} diff --git a/test-rust/tests/common/florestad.rs b/test-rust/tests/common/florestad.rs new file mode 100644 index 000000000..75bd1f819 --- /dev/null +++ b/test-rust/tests/common/florestad.rs @@ -0,0 +1,85 @@ +#![allow(dead_code)] + +use std::env; +use std::fs; +use std::path::Path; +use std::process::Child; + +use electrsd::corepc_node::get_available_port; +use floresta_rpc::jsonrpc_client::Client; +use floresta_rpc::rpc::FlorestaRPC; +use rcgen::generate_simple_self_signed; +use rcgen::CertifiedKey; + +use crate::common::run_node; +use crate::common::wait_for_condition_node_started; +use crate::common::XPUB_STR; + +const VIEW_LOGS: bool = false; + +pub(crate) struct Florestad { + process: Child, + pub(crate) client: Client, + pub(crate) directory: String, +} + +pub(crate) fn setup_florestad() -> Florestad { + // CARGO_MANIFEST_DIR is always floresta-cli's directory; PWD changes based on where the + // command is executed. + let root = format!("{}/..", env!("CARGO_MANIFEST_DIR")); + let release_path = format!("{root}/target/release/florestad"); + let debug_path = format!("{root}/target/debug/florestad"); + + let release_found = Path::new(&release_path).try_exists().unwrap(); + // If release target not found, default to the debug path + let florestad_path: String = match release_found { + true => release_path, + false => debug_path, + }; + + // Makes a temporary directory to store the chain db, TLS certificate, logs, etc. + let test_code = rand::random::(); + let dirname = format!("{root}/tmp/floresta.{test_code}"); + fs::DirBuilder::new() + .recursive(true) + .create(&dirname) + .unwrap(); + + // Generate TLS private key and certificate using rcgen + let CertifiedKey { cert, key_pair } = + generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let cert_pem = cert.pem(); + let key_pem = key_pair.serialize_pem(); + fs::create_dir_all(format!("{dirname}/regtest/tls")).unwrap(); + fs::write(format!("{dirname}/regtest/tls/cert.pem"), cert_pem).unwrap(); + fs::write(format!("{dirname}/regtest/tls/key.pem"), key_pem).unwrap(); + + let rpc_port = get_available_port().unwrap(); + let electrum_port = get_available_port().unwrap(); + let data_dir_arg = format!("--data-dir={}", dirname.clone()); + let wallet_xpub_arg = format!("--wallet-xpub={}", XPUB_STR); + let rpc_address_arg = format!("--rpc-address=127.0.0.1:{}", rpc_port); + let electrum_address_arg = format!("--electrum-address=127.0.0.1:{}", electrum_port); + + let args = vec![ + "--network=regtest", + // "--debug", + // "--no-assume-utreexo", + &electrum_address_arg, + &data_dir_arg, + &wallet_xpub_arg, + &rpc_address_arg, + ]; + + let process = run_node(florestad_path, args, VIEW_LOGS); + + let client = Client::new(format!("http://127.0.0.1:{rpc_port}")); + + wait_for_condition_node_started(|| client.get_blockchain_info().is_ok()).unwrap(); + + Florestad { + process, + client, + directory: dirname, + } +} diff --git a/test-rust/tests/common/mod.rs b/test-rust/tests/common/mod.rs new file mode 100644 index 000000000..92b5ec2f4 --- /dev/null +++ b/test-rust/tests/common/mod.rs @@ -0,0 +1,381 @@ +#![allow(dead_code)] + +pub(crate) mod bitcoind; +pub(crate) mod florestad; +pub(crate) mod utreexod; + +use std::process::Command; +use std::process::Stdio; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; +use std::time::Instant; + +use bitcoind::setup_bitcoind; +use electrsd::corepc_node; +use electrsd::corepc_node::Client as BitcoindClient; +use electrsd::corepc_node::Node as BitcoinD; +use floresta_rpc::rpc::FlorestaRPC; +use florestad::setup_florestad; +use florestad::Florestad; +use once_cell::sync::Lazy; +use rand::Rng; +use utreexod::Utreexod; + +const MAX_RETRY_DURATION_SECONDS_NODE_START: u64 = 500; +const MAX_RETRY_DURATION_SECONDS: u64 = 40; +const DELAYED_RETRY_SECONDS: u64 = 20; +const SLEEP_SECONDS: u64 = 3; + +//menemonicos = useless ritual arm slow mention dog force almost sudden pulp rude eager +pub(crate) const DESCRIPTOR_INTERNAL: &str = "wpkh(tprv8hCwaWbnCTeqSXMmEgtYqC3tjCHQTKphfXBG5MfWgcA6pif3fAUqCuqwphSyXmVFhd8b5ep5krkRxF6YkuQfxSAhHMTGeRA8rKPzQd9BMre/1/*)#v08p3aj4"; +pub(crate) const DESCRIPTOR_EXTERNAL: &str = "wpkh(tprv8hCwaWbnCTeqSXMmEgtYqC3tjCHQTKphfXBG5MfWgcA6pif3fAUqCuqwphSyXmVFhd8b5ep5krkRxF6YkuQfxSAhHMTGeRA8rKPzQd9BMre/0/*)#amzqvgzd"; +pub(crate) const XPUB_STR: &str = "vpub5ZrpbMUWLCJ6MbpU1RzocWBddAQnk2XYry9JSXrtzxSqoicei28CzqUhiN2HJ8z2VjY6rsUNf4qxjym43ydhAFQJ7BDDcC2bK6et6x9hc4D"; +pub(crate) const ADDRESS_STR: &str = "bcrt1q427ze5mrzqupzyfmqsx9gxh7xav538yk2j4cft"; + +// Shared instance of Florestad, BitcoinD, Utreexod, and generated blocks height, initialized once. +// Purpose: Optimize tests needing a chain with blocks by avoiding repeated setup and generation. +static SHARED_FLORESTAD_BITCOIND_UTREEXOD_WITH_BLOCKS: Lazy> = + Lazy::new(|| { + let (florestad, bitcoind, utreexod) = setup_florestad_bitcoind_utreexod(true); + let _height = generate_random_blocks_by_utreexod(&florestad, &bitcoind, &utreexod); + Arc::new((florestad, bitcoind, utreexod)) + }); + +// Returns a shared reference to Florestad, BitcoinD, and Utreexod. +// Purpose: Provide access to the shared setup for tests that don't modify the chain. +pub(crate) fn get_shared_florestad_bitcoind_utreexod_with_blocks( +) -> &'static (Florestad, BitcoinD, Utreexod) { + &*SHARED_FLORESTAD_BITCOIND_UTREEXOD_WITH_BLOCKS +} + +// Returns a shared reference to Florestad and BitcoinD. +// Purpose: Provide access to the shared setup for tests that don't modify the chain. +pub(crate) fn get_shared_florestad_bitcoind_with_blocks() -> (&'static Florestad, &'static BitcoinD) +{ + let (florestad, bitcoind, _utreexod) = get_shared_florestad_bitcoind_utreexod_with_blocks(); + (florestad, bitcoind) +} + +pub(crate) fn setup_florestad_bitcoind_utreexod( + v2_transport: bool, +) -> (Florestad, BitcoinD, Utreexod) { + let florestad = setup_florestad(); + let bitcoind = setup_bitcoind(v2_transport); + let utreexod = Utreexod::setup(); + + // It is necessary to generate some blocks so that utreexod and bitcoind can sync + let initial_height = 10; + utreexod.client.generate(initial_height).unwrap(); + + // florestad_connected_to_bitcoind(&florestad, &bitcoind, v2_transport); + florestad_connected_to_utreexod(&florestad, &utreexod); + assert!(florestad.client.get_peer_info().unwrap().len() == 1); + + // Add utreexod as peer to bitcoind + bitcoind + .client + .add_node( + &utreexod.p2p.to_string(), + electrsd::corepc_node::AddNodeCommand::Add, + ) + .unwrap(); + + // Check if bitcoind connected to utreexod + wait_for_condition(|| { + let info = bitcoind.client.get_peer_info().unwrap().0; + if info.iter().any(|p| p.address == utreexod.p2p.to_string()) { + assert!(info.len() == 1); + return true; + } + false + }) + .unwrap(); + + // check if utreexod connected to bitcoind + wait_for_condition(|| { + let resp = utreexod.client.get_peer_info().unwrap(); + let peers = resp.as_array().unwrap(); + if peers + .iter() + .any(|p| p["subver"].as_str().unwrap().contains("Satoshi")) + { + assert!(peers.len() == 2); + return true; + } + false + }) + .unwrap(); + + // Wait for all nodes to reach the same block height + wait_for_condition(|| florestad.client.get_block_count().unwrap() == initial_height).unwrap(); + wait_for_condition(|| bitcoind.client.get_block_count().unwrap().0 as u32 == initial_height) + .unwrap(); + + (florestad, bitcoind, utreexod) +} + +pub(crate) fn setup_florestad_bitcoind(v2_transport: bool) -> (Florestad, BitcoinD) { + let florestad = setup_florestad(); + let bitcoind = setup_bitcoind(v2_transport); + + florestad_connected_to_bitcoind(&florestad, &bitcoind, v2_transport); + + (florestad, bitcoind) +} + +pub(crate) fn setup_florestad_utreexod() -> (Florestad, Utreexod) { + let florestad = setup_florestad(); + let utreexod = Utreexod::setup(); + + florestad_connected_to_utreexod(&florestad, &utreexod); + + (florestad, utreexod) +} + +pub(crate) fn florestad_connected_to_bitcoind( + florestad: &Florestad, + bitcoind: &BitcoinD, + v2_transport: bool, +) { + florestad + .client + .add_node( + bitcoind.params.p2p_socket.as_ref().unwrap().to_string(), + floresta_rpc::rpc_types::AddNodeCommand::Add, + v2_transport, + ) + .unwrap(); + + // Wait for florestad to connect to bitcoind + wait_for_condition(|| { + let peers = florestad.client.get_peer_info().unwrap(); + peers.iter().any(|p| { + Some(p.address.clone()) == bitcoind.params.p2p_socket.as_ref().map(|s| s.to_string()) + }) + }) + .unwrap(); + + // Wait for bitcoind to connect to florestad + wait_for_condition(|| { + let peers = bitcoind.client.get_peer_info().unwrap().0; + peers.iter().any(|p| p.subversion.contains("Floresta")) + }) + .unwrap(); +} + +pub(crate) fn florestad_connected_to_utreexod(florestad: &Florestad, utreexod: &Utreexod) { + florestad + .client + .add_node( + utreexod.p2p.to_string(), + floresta_rpc::rpc_types::AddNodeCommand::Add, + false, + ) + .unwrap(); + + // Wait for florestad to connect to utreexod + wait_for_condition(|| { + if let Ok(peers) = florestad.client.get_peer_info() { + return peers.iter().any(|p| p.address == utreexod.p2p.to_string()); + } + false + }) + .unwrap(); + + // Wait for utreexod to connect to florestad + wait_for_condition(|| { + let resp = utreexod.client.get_peer_info().unwrap(); + let peers = resp.as_array().unwrap(); + if peers + .iter() + .any(|p| p["subver"].as_str().unwrap().contains("Floresta")) + { + return true; + } + false + }) + .unwrap(); +} + +pub(crate) fn wait_for_condition_node_started(condition: F) -> Result<(), String> +where + F: Fn() -> bool, +{ + wait_for_condition_inner(condition, MAX_RETRY_DURATION_SECONDS_NODE_START) +} + +// Helper function to wait for a condition with timeout +pub(crate) fn wait_for_condition(condition: F) -> Result<(), String> +where + F: Fn() -> bool, +{ + wait_for_condition_inner(condition, MAX_RETRY_DURATION_SECONDS) +} + +fn wait_for_condition_inner(condition: F, max_retries: u64) -> Result<(), String> +where + F: Fn() -> bool, +{ + let start = Instant::now(); + loop { + if condition() { + return Ok(()); + } + if start.elapsed() > Duration::from_secs(max_retries) { + return Err("Timeout waiting for condition".into()); + } + if start.elapsed() > Duration::from_secs(DELAYED_RETRY_SECONDS) { + std::thread::sleep(Duration::from_secs(SLEEP_SECONDS)); + } + } +} + +/// Asserts that a u32 value equals an i64 value, allowing the i64 to be 1 more due to conversion errors. +/// Panics if the values do not match (with or without the +1 offset). +pub(crate) fn assert_u32_i64_equal_with_offset(u32_val: u32, i64_val: i64) -> Result<(), String> { + let converted = u32_val as i64; + if !(converted == i64_val || converted + 1 == i64_val || converted == i64_val + 1) { + return Err(format!( + "Time comparison failed: {} does not match {} (allowing +1 offset)", + u32_val, i64_val + )); + } + Ok(()) +} + +pub(crate) fn check_peers_florestad_bitcoind( + florestad: &Florestad, + bitcoind: &BitcoinD, + v2_transport: bool, + peer_len: usize, +) { + assert!(florestad.client.ping().is_ok()); + assert!(bitcoind.client.ping().is_ok()); + let peers = florestad.client.get_peer_info().unwrap(); + assert!(peers.len() == peer_len); + + let peer = peers + .iter() + .find(|p| { + Some(p.address.clone()) == bitcoind.params.p2p_socket.as_ref().map(|s| s.to_string()) + }) + .unwrap(); + assert!(peer.state == "Ready"); + assert!(peer.address == bitcoind.params.p2p_socket.unwrap().to_string()); + assert!(peer.kind == "regular"); + assert!(peer.initial_height == 0); + let transporte_protocol = if v2_transport { "V2" } else { "V1" }; + assert!(peer.transport_protocol == transporte_protocol); + + let bitcoind_network = bitcoind.client.get_network_info().unwrap(); + assert!(peer.user_agent == bitcoind_network.subversion); + let service = bitcoind_network.local_services_names.join("|"); + assert!(peer.services.contains(&service)); + + let bitcoind_peers = bitcoind.client.get_peer_info().unwrap(); + assert!(bitcoind_peers.0.len() == 1); + let bitcoind_peer = &bitcoind_peers.0[0]; + assert!(bitcoind_peer.starting_height.unwrap() == 0); + assert!(bitcoind_peer.services == "0000000001000009"); + assert!(bitcoind_peer.subversion.contains("Floresta")); + assert!(bitcoind_peer.transport_protocol_type == transporte_protocol.to_lowercase()); +} + +pub(crate) fn generate_random_blocks_by_utreexod( + florestad: &Florestad, + bitcoind: &BitcoinD, + utreexod: &Utreexod, +) -> usize { + let height_florestad = florestad.client.get_block_count().unwrap() as usize; + + let number_blocks = rand::thread_rng().gen_range(21..50); + utreexod.client.generate(number_blocks as u32).unwrap(); + let expect_height_blocks = height_florestad + number_blocks; + println!( + "Generated {} blocks via utreexod, new height: {}", + number_blocks, expect_height_blocks + ); + + wait_for_condition(|| { + florestad.client.get_block_count().unwrap() == expect_height_blocks as u32 + }) + .unwrap(); + + wait_for_condition(|| { + bitcoind.client.get_block_count().unwrap().0 == expect_height_blocks as u64 + }) + .unwrap(); + + wait_for_condition(|| !florestad.client.get_blockchain_info().unwrap().ibd).unwrap(); + + expect_height_blocks +} + +pub(crate) fn generate_random_blocks(florestad: &Florestad, bitcoind: &BitcoinD) -> usize { + let number_blocks = rand::thread_rng().gen_range(21..50); + let height_bitcoind = bitcoind.client.get_block_count().unwrap().0 as usize; + let height_florestad = florestad.client.get_block_count().unwrap() as usize; + assert!(height_bitcoind == height_florestad); + + generate_blocks_bitcoind(&bitcoind.client, number_blocks); + let expect_height_blocks = height_bitcoind + number_blocks; + + wait_for_condition(|| { + florestad.client.get_block_count().unwrap() == expect_height_blocks as u32 + }) + .unwrap(); + + wait_for_condition(|| { + bitcoind.client.get_block_count().unwrap().0 == expect_height_blocks as u64 + }) + .unwrap(); + + expect_height_blocks +} + +pub(crate) fn generate_blocks_bitcoind(bitcoind: &BitcoindClient, num: usize) { + let address_unchecked = bitcoin::Address::from_str(&ADDRESS_STR).expect("invalid address"); + let address = address_unchecked.assume_checked(); + + print!("Generating {} blocks...", num); + bitcoind.generate_to_address(num, &address).unwrap(); + + print!(" Done!"); + println!("\n"); +} + +pub(crate) fn run_node(path: String, args: Vec<&str>, view_log: bool) -> std::process::Child { + let view_stdout = if view_log { + Stdio::inherit() + } else { + Stdio::null() + }; + let view_stderr = if view_log { + Stdio::inherit() + } else { + Stdio::null() + }; + + let mut process = corepc_node::anyhow::Context::with_context( + Command::new(&path) + .args(&args) + .stdout(view_stdout) + .stderr(view_stderr) + .spawn(), + || format!("Error while executing {:?}", path), + ) + .unwrap(); + + match process.try_wait() { + Ok(Some(_)) | Err(_) => { + let _ = process.kill(); + panic!("process exited prematurely or failed to start"); + } + Ok(None) => { + // Process is still running, proceed + } + }; + + process +} diff --git a/test-rust/tests/common/utreexod.rs b/test-rust/tests/common/utreexod.rs new file mode 100644 index 000000000..793f8d059 --- /dev/null +++ b/test-rust/tests/common/utreexod.rs @@ -0,0 +1,149 @@ +#![allow(dead_code)] + +use std::env; +use std::fs; +use std::net::SocketAddrV4; +use std::process::Child; + +use electrsd::corepc_node::get_available_port; +use floresta_rpc::jsonrpc_client::Client; +use floresta_rpc::jsonrpc_client::JsonRPCConfig; +use floresta_rpc::rpc_types::Error; +use serde_json::json; +use serde_json::Value; + +use crate::common::run_node; +use crate::common::wait_for_condition_node_started; +use crate::common::ADDRESS_STR; + +const VIEW_LOGS: bool = false; + +pub struct Utreexod { + process: Child, + pub(crate) client: RpcUtreexod, + pub(crate) p2p: SocketAddrV4, +} + +impl Utreexod { + pub(crate) fn setup() -> Self { + let utreexod_exe = env::var("UTREEXOD_EXE") + .ok() + .or_else(|| Some("/tmp/floresta-func-tests/binaries/utreexod".to_string())) + .expect("you need to provide an env var UTREEXOD_EXE or specify a utreexod path"); + + // Data directory to avoid write errors + let test_code = rand::random::(); + let data_dir = format!("/tmp/utreexod-data-{}", test_code); + fs::create_dir_all(&data_dir).expect("Failed to create data directory"); + + // Ports for RPC and P2P + let rpc_port = get_available_port().expect("Failed to get available RPC port"); + let p2p_port = get_available_port().expect("Failed to get available P2P port"); + + // Arguments for utreexod (based on btcd) + let bind_arg = format!("--listen=0.0.0.0:{}", p2p_port); + let rpcport_arg = format!("--rpclisten=127.0.0.1:{}", rpc_port); + let datadir_arg = format!("--datadir={}", data_dir); + let mining_addr_arg = format!("--miningaddr={}", ADDRESS_STR); + let args = vec![ + "--regtest", + "--debuglevel=debug", + "--prune=0", + &bind_arg, + &rpcport_arg, + &mining_addr_arg, + "--rpcuser=floresta", + "--rpcpass=floresta", + "--notls", + "--utreexoproofindex", + &datadir_arg, + ]; + + let process = run_node(utreexod_exe, args, VIEW_LOGS); + + // Create the RPC client + let url = format!("http://127.0.0.1:{}", rpc_port); + let client = RpcUtreexod::new(url, Some("floresta".into()), Some("floresta".into())); + + wait_for_condition_node_started(|| client.get_blockchain_info().is_ok()).unwrap(); + + let p2p = format!("127.0.0.1:{}", p2p_port); + + Utreexod { + process, + client, + p2p: p2p.parse().unwrap(), + } + } +} + +pub struct RpcUtreexod { + client: Client, +} + +impl RpcUtreexod { + fn new(url: String, user: Option, pass: Option) -> Self { + let config = JsonRPCConfig { url, user, pass }; + let client = Client::new_with_config(config); + Self { client } + } + + pub fn call(&self, method: &str, args: &[Value]) -> Result { + self.client.rpc_call(method, args) + } + + pub fn get_blockchain_info(&self) -> Result { + self.call("getblockchaininfo", &[]) + } + + pub fn stop(&self) -> Result { + self.call("stop", &[]) + } + + pub fn get_new_address(&self) -> Result { + self.call("getnewaddress", &[]) + } + + pub fn generate(&self, blocks: u32) -> Result { + self.call("generate", &[json!(blocks)]) + } + + pub fn get_utreexo_roots(&self, block_hash: &str) -> Result { + self.call("getutreexoroots", &[json!(block_hash)]) + } + + pub fn send_to_address(&self, address: &str, amount: f64) -> Result { + self.call("sendtoaddress", &[json!(address), json!(amount)]) + } + + pub fn get_balance(&self) -> Result { + self.call("getbalance", &[]) + } + + pub fn get_peer_info(&self) -> Result { + self.call("getpeerinfo", &[]) + } + + pub fn invalidate_block(&self, blockhash: &str) -> Result { + self.call("invalidateblock", &[json!(blockhash)]) + } + + pub fn get_blockhash(&self, height: u32) -> Result { + self.call("getblockhash", &[json!(height)]) + } + + pub fn addnode(&self, node: &str, command: &str) -> Result { + self.call("addnode", &[json!(node), json!(command)]) + } + + pub fn get_block_count(&self) -> Result { + self.call("getblockcount", &[]) + } + + pub fn get_txout(&self, txid: &str, vout: u32, include_mempool: bool) -> Result { + self.call( + "gettxout", + &[json!(txid), json!(vout), json!(include_mempool)], + ) + } +} From 29a4218cb81506da0e4cd709dabe3b7271c67d3b Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Tue, 21 Oct 2025 22:57:24 -0300 Subject: [PATCH 09/10] test: add comprehensive functional tests for Floresta RPC interactions These tests ensure that Floresta's RPC responses align with Bitcoin Core's RPC for maintaining consensus. --- test-rust/tests/floresta-rpc.rs | 537 ++++++++++++++++++++++++++++++++ 1 file changed, 537 insertions(+) create mode 100644 test-rust/tests/floresta-rpc.rs diff --git a/test-rust/tests/floresta-rpc.rs b/test-rust/tests/floresta-rpc.rs new file mode 100644 index 000000000..d8f25c691 --- /dev/null +++ b/test-rust/tests/floresta-rpc.rs @@ -0,0 +1,537 @@ +#![allow(dead_code)] +#![cfg(feature = "functional-tests")] + +use bitcoin::BlockHash; +use common::bitcoind::setup_bitcoind_by_bitcoind; +use common::check_peers_florestad_bitcoind; +use common::florestad::Florestad; +use common::generate_random_blocks; +use common::setup_florestad_bitcoind; +use common::wait_for_condition; +use electrsd::corepc_node::Node as BitcoinD; +use floresta_rpc::rpc::FlorestaRPC; +use floresta_rpc::rpc_types::AddNodeCommand; +use floresta_rpc::rpc_types::GetBlockRes; +use floresta_rpc::rpc_types::GetMemInfoRes; + +use crate::common::assert_u32_i64_equal_with_offset; +use crate::common::florestad::setup_florestad; +use crate::common::get_shared_florestad_bitcoind_utreexod_with_blocks; +use crate::common::get_shared_florestad_bitcoind_with_blocks; + +mod common; + +#[test] +fn test_add_node_v1() { + do_test_add_node(false); +} +#[test] +fn test_add_node_v2() { + do_test_add_node(true); +} + +fn do_test_add_node(v2_transport: bool) { + //Helper function to restart bitcoind + fn restart_bitcoind(bitcoind: &mut BitcoinD, v2_transport: bool) { + bitcoind.client.stop().unwrap(); + wait_for_condition(|| bitcoind.client.ping().is_err()).unwrap(); + setup_bitcoind_by_bitcoind(bitcoind, v2_transport); + } + + println!("=== Setting up florestad and bitcoind ==="); + let (florestad, mut bitcoind) = setup_florestad_bitcoind(v2_transport); + check_peers_florestad_bitcoind(&florestad, &bitcoind, v2_transport, 1); + + println!("=== Stopping bitcoind and waiting for disconnection ==="); + bitcoind.client.stop().unwrap(); + wait_for_condition(|| bitcoind.client.ping().is_err()).unwrap(); + wait_for_condition(|| florestad.client.get_peer_info().unwrap().is_empty()).unwrap(); + + println!("=== Restarting bitcoind and waiting for reconnection ==="); + setup_bitcoind_by_bitcoind(&mut bitcoind, v2_transport); + wait_for_condition(|| { + florestad.client.get_peer_info().unwrap().len() == 1 && florestad.client.ping().is_ok() + }) + .unwrap(); + check_peers_florestad_bitcoind(&florestad, &bitcoind, v2_transport, 1); + + let node = bitcoind.params.p2p_socket.unwrap().to_string(); + + println!("=== Not adding peer again ==="); + florestad + .client + .add_node(node.clone(), AddNodeCommand::Add, v2_transport) + .unwrap(); + check_peers_florestad_bitcoind(&florestad, &bitcoind, v2_transport, 1); // Should still be only one peer + florestad + .client + .add_node(node.clone(), AddNodeCommand::Add, !v2_transport) + .unwrap(); + check_peers_florestad_bitcoind(&florestad, &bitcoind, v2_transport, 1); // Should still be only one peer + florestad + .client + .add_node(node.clone(), AddNodeCommand::Onetry, v2_transport) + .unwrap(); + check_peers_florestad_bitcoind(&florestad, &bitcoind, v2_transport, 1); // Should still be only one peer + + println!("=== Removing node from florestad ==="); + florestad + .client + .add_node(node.clone(), AddNodeCommand::Remove, v2_transport) + .unwrap(); + check_peers_florestad_bitcoind(&florestad, &bitcoind, v2_transport, 1); + + println!("=== Waiting for florestad to disconnect from bitcoind ==="); + restart_bitcoind(&mut bitcoind, v2_transport); + wait_for_condition(|| { + florestad.client.get_peer_info().unwrap().is_empty() && florestad.client.ping().is_ok() + }) + .unwrap(); + + println!("=== Adding node with Onetry command ==="); + florestad + .client + .add_node(node.clone(), AddNodeCommand::Onetry, v2_transport) + .unwrap(); + wait_for_condition(|| florestad.client.get_peer_info().unwrap().len() == 1).unwrap(); + check_peers_florestad_bitcoind(&florestad, &bitcoind, v2_transport, 1); + + println!("=== Final checks for florestad and bitcoind ==="); + restart_bitcoind(&mut bitcoind, v2_transport); + wait_for_condition(|| florestad.client.ping().is_ok()).unwrap(); + wait_for_condition(|| florestad.client.get_peer_info().unwrap().is_empty()).unwrap(); +} + +#[test] +fn test_get_best_block_hash() { + let (florestad, bitcoind) = setup_florestad_bitcoind(true); + let floresta_best = florestad.client.get_best_block_hash().unwrap(); + let bitcoind_best = bitcoind.client.get_best_block_hash().unwrap(); + assert_eq!(floresta_best.to_string(), bitcoind_best.0); + + generate_random_blocks(&florestad, &bitcoind); + + let floresta_best = florestad.client.get_best_block_hash().unwrap(); + let bitcoind_best = bitcoind.client.get_best_block_hash().unwrap(); + assert_eq!(floresta_best.to_string(), bitcoind_best.0); +} + +#[test] +fn test_get_block() { + fn check_block( + florestad: &Florestad, + bitcoind: &BitcoinD, + block_hash: BlockHash, + verbosity: Option, + ) { + let floresta_block = florestad.client.get_block(block_hash, verbosity).unwrap(); + match floresta_block { + GetBlockRes::Serialized(floresta_block_hex) => { + assert!(verbosity.is_none()); + let bitcoin_block = bitcoind.client.get_block_verbose_zero(block_hash).unwrap(); + assert_eq!(floresta_block_hex, bitcoin_block.0); + } + GetBlockRes::Verbose(floresta_res) => { + assert!(verbosity == Some(1)); + let bitcoin_res = bitcoind.client.get_block_verbose_one(block_hash).unwrap(); + assert_eq!(floresta_res.hash, bitcoin_res.hash); + assert_u32_i64_equal_with_offset( + floresta_res.confirmations, + bitcoin_res.confirmations, + ) + .unwrap(); + assert_eq!( + floresta_res.strippedsize, + bitcoin_res.stripped_size.unwrap() as usize + ); + assert_eq!(floresta_res.size, bitcoin_res.size as usize); + assert_eq!(floresta_res.weight, bitcoin_res.weight as usize); + assert_u32_i64_equal_with_offset(floresta_res.height, bitcoin_res.height).unwrap(); + assert_eq!(floresta_res.version, bitcoin_res.version); + assert_eq!(floresta_res.version_hex, bitcoin_res.version_hex); + assert_eq!(floresta_res.merkleroot, bitcoin_res.merkle_root); + assert_eq!(floresta_res.tx, bitcoin_res.tx); + assert_u32_i64_equal_with_offset(floresta_res.time, bitcoin_res.time).unwrap(); + assert_u32_i64_equal_with_offset( + floresta_res.mediantime, + bitcoin_res.median_time.unwrap(), + ) + .unwrap(); + assert_u32_i64_equal_with_offset(floresta_res.nonce, bitcoin_res.nonce).unwrap(); + assert_eq!(floresta_res.bits, bitcoin_res.bits); + assert_eq!(floresta_res.difficulty, bitcoin_res.difficulty); + // Not comparing chainwork, because the floresta no accumula chainwork yet. + // assert_eq!(floresta_res.chainwork, bitcoin_res.chain_work); + assert_eq!(floresta_res.n_tx, bitcoin_res.n_tx as usize); + assert_eq!( + floresta_res.previousblockhash, + bitcoin_res.previous_block_hash.unwrap_or( + "0000000000000000000000000000000000000000000000000000000000000000" + .to_string() + ) + ); + assert_eq!(floresta_res.nextblockhash, bitcoin_res.next_block_hash); + assert_eq!(floresta_res.target, bitcoin_res.target); + } + } + } + + fn get_blocks_and_check(florestad: &Florestad, bitcoind: &BitcoinD, block_hash: BlockHash) { + let mut verbosity = None; + check_block(florestad, bitcoind, block_hash, verbosity); + + verbosity = Some(1); + check_block(florestad, bitcoind, block_hash, verbosity); + } + + // Initial block (genesis) + let (florestad, bitcoind) = get_shared_florestad_bitcoind_with_blocks(); + let block_hash = florestad.client.get_block_hash(0).unwrap(); + get_blocks_and_check(&florestad, &bitcoind, block_hash); + + // Check last block + let block_hash = florestad.client.get_best_block_hash().unwrap(); + get_blocks_and_check(&florestad, &bitcoind, block_hash); + + // Check a random block floresta + let height = florestad.client.get_block_count().unwrap() as usize; + use rand::Rng; + let random_height = rand::thread_rng().gen_range(0..height); + let block_hash = florestad + .client + .get_block_hash(random_height as u32) + .unwrap(); + get_blocks_and_check(&florestad, &bitcoind, block_hash); + + // Check a random block bitcoind + let random_height = rand::thread_rng().gen_range(0..height); + let block_hash = bitcoind + .client + .get_block_hash(random_height as u64) + .unwrap() + .block_hash() + .unwrap(); + get_blocks_and_check(&florestad, &bitcoind, block_hash); +} + +#[test] +fn test_block_chain_info() { + fn check(florestad: &Florestad, bitcoind: &BitcoinD) { + let floresta_info = florestad.client.get_blockchain_info().unwrap(); + let bitcoind_info = bitcoind.client.get_blockchain_info().unwrap(); + + assert_eq!(floresta_info.chain, bitcoind_info.chain); + assert_eq!(floresta_info.height, bitcoind_info.blocks as u32); + assert_eq!(floresta_info.best_block, bitcoind_info.best_block_hash); + assert_eq!(floresta_info.difficulty, bitcoind_info.difficulty); + assert_eq!(floresta_info.latest_block_time, bitcoind_info.time as u32); + } + + // Initial block (genesis) + let (florestad, bitcoind) = setup_florestad_bitcoind(true); + check(&florestad, &bitcoind); + + // Generate more blocks + generate_random_blocks(&florestad, &bitcoind); + + // Check last block + check(&florestad, &bitcoind); +} + +#[test] +fn test_get_block_count() { + let (florestad, bitcoind) = setup_florestad_bitcoind(true); + + // Check genesis block height + let floresta_height = florestad.client.get_block_count().unwrap(); + let bitcoind_height = bitcoind.client.get_block_count().unwrap().0; + assert_eq!(floresta_height as u64, bitcoind_height); + + // Generate more blocks + let new_height = generate_random_blocks(&florestad, &bitcoind); + + // Check latest block height + let floresta_height = florestad.client.get_block_count().unwrap(); + let bitcoind_height = bitcoind.client.get_block_count().unwrap().0; + assert_eq!(floresta_height as u64, bitcoind_height); + assert_eq!(floresta_height as usize, new_height); +} + +#[test] +fn test_get_block_hash() { + let (florestad, bitcoind) = get_shared_florestad_bitcoind_with_blocks(); + + // Check genesis block hash + let floresta_hash = florestad.client.get_block_hash(0).unwrap(); + let bitcoind_hash = bitcoind + .client + .get_block_hash(0) + .unwrap() + .block_hash() + .unwrap(); + assert_eq!(floresta_hash, bitcoind_hash); + + // Check latest block hash + let floresta_hash = florestad.client.get_best_block_hash().unwrap(); + let bitcoind_hash = bitcoind + .client + .get_best_block_hash() + .unwrap() + .block_hash() + .unwrap(); + + assert_eq!(floresta_hash, bitcoind_hash); + + // Check a random block hash + let height = florestad.client.get_block_count().unwrap() as usize; + use rand::Rng; + let random_height = rand::thread_rng().gen_range(0..height); + let floresta_hash = florestad + .client + .get_block_hash(random_height as u32) + .unwrap(); + let bitcoind_hash = bitcoind + .client + .get_block_hash(random_height as u64) + .unwrap() + .block_hash() + .unwrap(); + assert_eq!(floresta_hash, bitcoind_hash); +} + +#[test] +fn test_get_block_header() { + let (florestad, bitcoind) = get_shared_florestad_bitcoind_with_blocks(); + + // Check genesis block header + let floresta_hash = florestad.client.get_block_hash(0).unwrap(); + let floresta_header = florestad.client.get_block_header(floresta_hash).unwrap(); + let bitcoind_header = bitcoind + .client + .get_block_header(&floresta_hash) + .unwrap() + .block_header() + .unwrap(); + assert_eq!(floresta_header, bitcoind_header); + + // Check latest block header + let floresta_hash = florestad.client.get_best_block_hash().unwrap(); + let floresta_header = florestad.client.get_block_header(floresta_hash).unwrap(); + let bitcoind_header = bitcoind + .client + .get_block_header(&floresta_hash) + .unwrap() + .block_header() + .unwrap(); + assert_eq!(floresta_header, bitcoind_header); + + // Check a random block header + let height = florestad.client.get_block_count().unwrap() as usize; + use rand::Rng; + let random_height = rand::thread_rng().gen_range(0..height); + let floresta_hash = florestad + .client + .get_block_hash(random_height as u32) + .unwrap(); + let floresta_header = florestad.client.get_block_header(floresta_hash).unwrap(); + let bitcoind_header = bitcoind + .client + .get_block_header(&floresta_hash) + .unwrap() + .block_header() + .unwrap(); + assert_eq!(floresta_header, bitcoind_header); +} + +#[test] +fn test_get_memory_info() { + let (florestad, _bitcoind) = get_shared_florestad_bitcoind_with_blocks(); + + // Test mode "stats" (only on Linux) + #[cfg(target_os = "linux")] + { + let result = florestad + .client + .get_memory_info("stats".to_string()) + .unwrap(); + match result { + GetMemInfoRes::Stats(res) => { + // Check basic invariants: total should be at least the used/free parts, + assert!(res.locked.total >= res.locked.used); + assert!(res.locked.total >= res.locked.free); + assert!(res.locked.locked > 0); + + assert!(res.locked.chunks_used > 0); + assert!(res.locked.chunks_free > 0); + } + _ => panic!("Expected GetMemInfoRes::Stats"), + } + } + #[cfg(not(target_os = "linux"))] + { + // Skip on non-Linux + println!("Skipping 'getmemoryinfo stats': not implemented for this OS"); + } + + // Test mode "mallocinfo" (only on Linux) + #[cfg(target_os = "linux")] + { + let result = florestad + .client + .get_memory_info("mallocinfo".to_string()) + .unwrap(); + match result { + GetMemInfoRes::MallocInfo(xml) => { + // Just checking if we got some XML content + assert!(xml.contains("")); + assert!(xml.contains("")); + assert!(xml.contains("")); + assert!(xml.contains("")); + assert!(xml.contains("")); + assert!(xml.contains("")); + assert!(xml.contains("")); + assert!(xml.contains("")); + } + _ => panic!("Expected GetMemInfoRes::MallocInfo"), + } + } + #[cfg(not(target_os = "linux"))] + { + // Skip on non-Linux + println!("Skipping 'getmemoryinfo mallocinfo': not implemented for this OS"); + } +} + +#[test] +fn test_get_peer_info() { + let v2_transport = true; + let (florestad, bitcoind) = setup_florestad_bitcoind(v2_transport); + + check_peers_florestad_bitcoind(&florestad, &bitcoind, v2_transport, 1); +} + +#[test] +fn test_get_roots() { + let (florestad, _bitcoind, _utreexod) = get_shared_florestad_bitcoind_utreexod_with_blocks(); + + let floresta_roots = florestad.client.get_roots().unwrap(); + + assert!(!floresta_roots.is_empty()); +} + +#[test] +fn test_get_rpc_info() { + let florestad = setup_florestad(); + let rpc_info = florestad.client.get_rpc_info().unwrap(); + assert_eq!(rpc_info.active_commands.len(), 1); + assert_eq!(rpc_info.active_commands[0].method, "getrpcinfo"); + assert!(rpc_info.active_commands[0].duration > 0); + assert_eq!(rpc_info.logpath, florestad.directory + "/regtest/debug.log"); +} + +#[test] +fn test_get_tx_out() { + let (florestad, bitcoind, _utreexod) = get_shared_florestad_bitcoind_utreexod_with_blocks(); + + let height = florestad.client.get_block_count().unwrap() as usize; + + use rand::Rng; + let random_height = rand::thread_rng().gen_range(1..height); + let block_hash = florestad + .client + .get_block_hash(random_height as u32) + .unwrap(); + let block = bitcoind.client.get_block(block_hash).unwrap(); + let txid = block.txdata[0].compute_txid(); + let vout = 0; + + let txout_floresta = florestad.client.get_tx_out(txid, vout).unwrap(); + let txout_bitcoind = bitcoind.client.get_tx_out(txid, vout.into()).unwrap(); + assert_eq!( + txout_floresta.bestblock.to_string(), + txout_bitcoind.best_block + ); + assert_eq!(txout_floresta.confirmations, txout_bitcoind.confirmations); + assert_eq!(txout_floresta.value, txout_bitcoind.value); + assert_eq!(txout_floresta.coinbase, txout_bitcoind.coinbase); + assert_eq!( + txout_floresta.script_pubkey.hex, + txout_bitcoind.script_pubkey.hex + ); + assert_eq!( + txout_floresta.script_pubkey.type_field, + txout_bitcoind.script_pubkey.type_ + ); + assert_eq!( + txout_floresta.script_pubkey.asm, + txout_bitcoind.script_pubkey.asm + ); + assert_eq!( + txout_floresta.script_pubkey.desc, + txout_bitcoind + .script_pubkey + .descriptor + .unwrap_or("".to_string()) + ); + assert_eq!( + txout_floresta.script_pubkey.address, + txout_bitcoind.script_pubkey.address + ); +} + +#[test] +fn test_ping() { + let (florestad, bitcoind) = setup_florestad_bitcoind(true); + + // Check initial state (no ping_time) + let bitcoin_res = bitcoind.client.get_peer_info().unwrap(); + let peer_bitcoin = bitcoin_res.0.first().unwrap(); + assert!(peer_bitcoin.ping_time.is_none()); + + // Send ping from florestad + florestad.client.ping().unwrap(); + + // Wait for ping_time to be set in bitcoind + wait_for_condition(|| { + let bitcoin_res = bitcoind.client.get_peer_info().unwrap(); + let peer_bitcoin = bitcoin_res.0.first().unwrap(); + peer_bitcoin.ping_time.is_some() + }) + .unwrap(); +} + +#[test] +fn test_stop() { + let (florestad, bitcoind) = setup_florestad_bitcoind(true); + + let stop_res = florestad.client.stop().unwrap(); + assert_eq!(stop_res.as_str(), "Floresta stopping"); + wait_for_condition(|| florestad.client.ping().is_err()).unwrap(); + + wait_for_condition(|| bitcoind.client.ping().is_ok()).unwrap(); + assert!(bitcoind.client.get_peer_info().unwrap().0.is_empty()) +} + +#[test] +fn test_uptime() { + let (florestad, bitcoind) = get_shared_florestad_bitcoind_with_blocks(); + std::thread::sleep(std::time::Duration::from_secs(2)); + + let floresta_uptime = florestad.client.uptime().unwrap(); + let bitcoin_uptime = bitcoind.client.uptime().unwrap(); + + assert!(floresta_uptime > 0); + assert!(bitcoin_uptime > 0); + + use rand::Rng; + let random_sleep = rand::thread_rng().gen_range(1..15); + std::thread::sleep(std::time::Duration::from_secs(random_sleep)); + + let floresta_new_uptime = florestad.client.uptime().unwrap(); + assert!(floresta_new_uptime >= floresta_uptime + random_sleep as u32); + let bitcoin_new_uptime = bitcoind.client.uptime().unwrap(); + assert!(bitcoin_new_uptime >= bitcoin_uptime + random_sleep as u32); +} From ebdd0859d81f683a1ede51cb120a06b4711f204e Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Tue, 21 Oct 2025 23:03:18 -0300 Subject: [PATCH 10/10] ci: Add Rust functional tests to CI and helper scripts Introduce Rust functional tests in the CI workflow using a matrix strategy for parallel execution with Python tests. Add scripts to prepare and compile necessary binaries for Utreexod and Bitcoind --- .github/workflows/functional.yml | 26 ++++++++++- test-rust/scripts/bitcoind.sh | 32 +++++++++++++ test-rust/scripts/prepare.sh | 78 ++++++++++++++++++++++++++++++++ test-rust/scripts/utreexod.sh | 30 ++++++++++++ 4 files changed, 164 insertions(+), 2 deletions(-) create mode 100755 test-rust/scripts/bitcoind.sh create mode 100755 test-rust/scripts/prepare.sh create mode 100755 test-rust/scripts/utreexod.sh diff --git a/.github/workflows/functional.yml b/.github/workflows/functional.yml index 3873162bd..d85628460 100644 --- a/.github/workflows/functional.yml +++ b/.github/workflows/functional.yml @@ -7,8 +7,11 @@ on: jobs: functional: - name: Functional + name: Functional ${{ matrix.type }} runs-on: ubuntu-latest + strategy: + matrix: + type: [python, rust] steps: - uses: actions/checkout@v4 @@ -16,11 +19,13 @@ jobs: # These are the minimal deps defined by bitcoin core at # https://github.com/bitcoin/bitcoin/blob/master/doc/build-unix.md - name: Prepare bitcoin-core deps + if: matrix.type == 'python' run: sudo apt install build-essential cmake pkgconf python3 libevent-dev libboost-dev # see more at # https://docs.astral.sh/uv/guides/integration/github/ - name: Install uv + if: matrix.type == 'python' uses: astral-sh/setup-uv@v5 with: python-version: "3.12" @@ -28,18 +33,22 @@ jobs: cache-dependency-glob: "uv.lock" - name: Prepare environment + if: matrix.type == 'python' run: uv sync --all-extras --dev - name: Run black formatting + if: matrix.type == 'python' run: uv run black --check --verbose ./tests - name: Run pylint linter + if: matrix.type == 'python' run: uv run pylint --verbose ./tests - name: Cache Rust uses: Swatinem/rust-cache@v2 - - name: Run functional tests tasks + - name: Prepare and run functional tests in python + if: matrix.type == 'python' run: | tests/prepare.sh tests/run.sh @@ -57,3 +66,16 @@ jobs: cat "$logfile" || echo "Failed to read $logfile" echo "::endgroup::" done + + - name: Prepare Bitcoind and Utreexod binaries + if: matrix.type == 'rust' + run: | + source ./test-rust/scripts/prepare.sh + echo "BITCOIND_EXE=$BITCOIND_EXE" >> "$GITHUB_ENV" + echo "UTREEXOD_EXE=$UTREEXOD_EXE" >> "$GITHUB_ENV" + + - name: Build and test Rust + if: matrix.type == 'rust' + run: | + cargo build --bin florestad --release + cargo test --features=functional-tests -p test-functional -- --test-threads=1 \ No newline at end of file diff --git a/test-rust/scripts/bitcoind.sh b/test-rust/scripts/bitcoind.sh new file mode 100755 index 000000000..ac4fbfe6c --- /dev/null +++ b/test-rust/scripts/bitcoind.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -eox pipefail + +# Our tests require `bitcoind` binary. Here, we download the binary, validate it, and export its +# location via `BITCOIND_EXE` which will be used by the `bitcoind` crate in our tests. + +HOST_PLATFORM="$(rustc --version --verbose | grep "host:" | awk '{ print $2 }')" +BITCOIND_DL_ENDPOINT="https://bitcoincore.org/bin/" +BITCOIND_VERSION="29.0" +if [[ "$HOST_PLATFORM" == *linux* ]]; then + BITCOIND_DL_FILE_NAME=bitcoin-"$BITCOIND_VERSION"-x86_64-linux-gnu.tar.gz + BITCOIND_DL_HASH="a681e4f6ce524c338a105f214613605bac6c33d58c31dc5135bbc02bc458bb6c" +elif [[ "$HOST_PLATFORM" == *darwin* ]]; then + BITCOIND_DL_FILE_NAME=bitcoin-"$BITCOIND_VERSION"-x86_64-apple-darwin.tar.gz + BITCOIND_DL_HASH="5bb824fc86a15318d6a83a1b821ff4cd4b3d3d0e1ec3d162b805ccf7cae6fca8" +else + printf "\n\n" + echo "Unsupported platform: $HOST_PLATFORM Exiting.." + exit 1 +fi + +DL_TMP_DIR=$(mktemp -d) +trap 'rm -rf -- "$DL_TMP_DIR"' EXIT + +pushd "$DL_TMP_DIR" +BITCOIND_DL_URL="$BITCOIND_DL_ENDPOINT"/bitcoin-core-"$BITCOIND_VERSION"/"$BITCOIND_DL_FILE_NAME" +curl -L -o "$BITCOIND_DL_FILE_NAME" "$BITCOIND_DL_URL" +echo "$BITCOIND_DL_HASH $BITCOIND_DL_FILE_NAME"|shasum -a 256 -c +tar xzf "$BITCOIND_DL_FILE_NAME" +export BITCOIND_EXE="$DL_TMP_DIR"/bitcoin-"$BITCOIND_VERSION"/bin/bitcoind +chmod +x "$BITCOIND_EXE" +popd \ No newline at end of file diff --git a/test-rust/scripts/prepare.sh b/test-rust/scripts/prepare.sh new file mode 100755 index 000000000..c65047c5b --- /dev/null +++ b/test-rust/scripts/prepare.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -eox pipefail + +check_installed() { + if ! command -v "$1" &>/dev/null; then + echo "You must have $1 installed to run those tests!" + exit 1 + fi +} + +check_installed git +check_installed go +check_installed cargo + +# Script to run bitcoind.sh and utreexod.sh, then move the binaries to a persistent location. + +# Set FLORESTA_TEMP_DIR if not set +export FLORESTA_TEMP_DIR="/tmp/floresta-func-tests" +mkdir -p "$FLORESTA_TEMP_DIR/binaries" + +# Get the directory where this script is located (use BASH_SOURCE for sourced scripts) +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +# Define target paths +BITCOIND_TARGET="$FLORESTA_TEMP_DIR/binaries/bitcoind" +UTREEXOD_TARGET="$FLORESTA_TEMP_DIR/binaries/utreexod" + +# Check if bitcoind is already in the correct location +if [ -f "$BITCOIND_TARGET" ]; then + export BITCOIND_EXE="$BITCOIND_TARGET" + echo "Bitcoind already available in $BITCOIND_EXE" +else + # Check if BITCOIND_EXE is already set and the file exists; if so, copy it directly + if [ -n "$BITCOIND_EXE" ] && [ -f "$BITCOIND_EXE" ]; then + cp "$BITCOIND_EXE" "$BITCOIND_TARGET" + export BITCOIND_EXE="$BITCOIND_TARGET" + echo "Bitcoind already available, copied to $BITCOIND_EXE" + else + # Run the script to download/build bitcoind + . "$SCRIPT_DIR/bitcoind.sh" + # After sourcing, copy the binary + if [ -n "$BITCOIND_EXE" ] && [ -f "$BITCOIND_EXE" ]; then + cp "$BITCOIND_EXE" "$BITCOIND_TARGET" + export BITCOIND_EXE="$BITCOIND_TARGET" + echo "Bitcoind moved to $BITCOIND_EXE" + else + echo "BITCOIND_EXE not found or invalid after running script" + exit 1 + fi + fi +fi + +# Check if utreexod is already in the correct location +if [ -f "$UTREEXOD_TARGET" ]; then + export UTREEXOD_EXE="$UTREEXOD_TARGET" + echo "Utreexod already available in $UTREEXOD_EXE" +else + # Check if UTREEXOD_EXE is already set and the file exists; if so, copy it directly + if [ -n "$UTREEXOD_EXE" ] && [ -f "$UTREEXOD_EXE" ]; then + cp "$UTREEXOD_EXE" "$UTREEXOD_TARGET" + export UTREEXOD_EXE="$UTREEXOD_TARGET" + echo "Utreexod already available, copied to $UTREEXOD_EXE" + else + # Run the script to download/build utreexod + . "$SCRIPT_DIR/utreexod.sh" + # After sourcing, copy the binary + if [ -n "$UTREEXOD_EXE" ] && [ -f "$UTREEXOD_EXE" ]; then + cp "$UTREEXOD_EXE" "$UTREEXOD_TARGET" + export UTREEXOD_EXE="$UTREEXOD_TARGET" + echo "Utreexod moved to $UTREEXOD_EXE" + else + echo "UTREEXOD_EXE not found or invalid after running script" + exit 1 + fi + fi +fi + +echo "All binaries pushed to $FLORESTA_TEMP_DIR/binaries" \ No newline at end of file diff --git a/test-rust/scripts/utreexod.sh b/test-rust/scripts/utreexod.sh new file mode 100755 index 000000000..bcd2b7a5a --- /dev/null +++ b/test-rust/scripts/utreexod.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -eox pipefail + +# Our tests require `utreexod` binary. Here, we clone the repository, build it, and export its +# location via `UTREEXOD_EXE` which will be used by the `utreexod` crate in our tests. + +UTREEXOD_REPO="https://github.com/utreexo/utreexod" +UTREEXOD_REVISION="${UTREEXO_REVISION:-}" # Optional: set to a specific tag or commit if needed + +DL_TMP_DIR=$(mktemp -d) +trap 'rm -rf -- "$DL_TMP_DIR"' EXIT + +pushd "$DL_TMP_DIR" +echo "Cloning and building utreexod..." +git clone "$UTREEXOD_REPO" utreexod +cd utreexod + +# Checkout specific revision if provided +if [ -n "$UTREEXOD_REVISION" ]; then + git checkout "$UTREEXOD_REVISION" +fi + +# Build the binary +go build -o utreexod . + +# Export the executable path +export UTREEXOD_EXE="$DL_TMP_DIR/utreexod/utreexod" +chmod +x "$UTREEXOD_EXE" + +popd \ No newline at end of file