├── tests ├── __init__.py ├── .gitignore ├── test_framework │ ├── rpc │ │ ├── __init__.py │ │ └── exceptions.py │ ├── daemon │ │ ├── __init__.py │ │ ├── floresta.py │ │ └── utreexo.py │ ├── electrum │ │ ├── __init__.py │ │ └── base.py │ ├── crypto │ │ └── pkcs8.py │ └── README.md ├── requirements.txt ├── README.md ├── floresta-cli │ ├── getroots.py │ ├── getpeerinfo.py │ ├── ping.py │ ├── stop.py │ ├── uptime.py │ ├── getblockchaininfo.py │ ├── getblockheader.py │ ├── getrpcinfo.py │ ├── getmemoryinfo.py │ ├── getbestblockhash.py │ ├── getblock.py │ └── getblockhash.py ├── florestad │ ├── connect.py │ ├── tls.py │ ├── tls-fail.py │ ├── restart.py │ └── reorg-chain.py ├── run.sh └── example │ ├── bitcoin.py │ ├── utreexod.py │ ├── electrum.py │ ├── functional.py │ └── integration.py ├── crates ├── floresta-wire │ ├── src │ │ ├── p2p_wire │ │ │ ├── seeds │ │ │ │ ├── testnet_seeds.json │ │ │ │ ├── testnet4_seeds.json │ │ │ │ ├── regtest_seeds.json │ │ │ │ ├── mainnet_seeds.json │ │ │ │ └── signet_seeds.json │ │ │ ├── tests │ │ │ │ ├── mod.rs │ │ │ │ └── test_data │ │ │ │ │ └── block_hashes.txt │ │ │ └── node_context.rs │ │ └── lib.rs │ ├── Cargo.toml │ └── README.md ├── floresta-chain │ ├── testdata │ │ ├── headers.zst │ │ ├── signet_headers │ │ ├── signet_headers.zst │ │ ├── block_367891 │ │ │ ├── raw.zst │ │ │ └── spent_utxos.zst │ │ ├── block_866342 │ │ │ ├── raw.zst │ │ │ └── spent_utxos.zst │ │ ├── v0_flat_metadata.bin │ │ └── bitcoin-core │ │ │ ├── data │ │ │ ├── asmap.raw │ │ │ ├── README.md │ │ │ └── base58_encode_decode.json │ │ │ ├── README.md │ │ │ └── COPYING.txt │ ├── src │ │ └── lib.rs │ └── Cargo.toml ├── floresta-node │ ├── src │ │ ├── json_rpc │ │ │ ├── mod.rs │ │ │ └── network.rs │ │ ├── lib.rs │ │ ├── config_file.rs │ │ └── zmq.rs │ └── Cargo.toml ├── floresta-electrum │ ├── src │ │ ├── request.rs │ │ ├── error.rs │ │ └── lib.rs │ └── Cargo.toml ├── floresta-common │ ├── src │ │ ├── error.rs │ │ └── spsc.rs │ └── Cargo.toml ├── floresta-compact-filters │ ├── Cargo.toml │ └── src │ │ ├── network_filters.rs │ │ └── lib.rs ├── floresta-watch-only │ └── Cargo.toml ├── floresta-rpc │ ├── Cargo.toml │ └── src │ │ └── jsonrpc_client.rs └── floresta │ ├── src │ └── lib.rs │ ├── Cargo.toml │ └── examples │ └── chainstate-builder.rs ├── fuzz ├── .gitignore ├── fuzz_targets │ ├── utreexo_proof_des.rs │ ├── local_address_str.rs │ ├── best_chain_decode.rs │ ├── disk_block_header_decode.rs │ ├── addrman.rs │ ├── best_chain_roundtrip.rs │ └── disk_block_header_roundtrip.rs └── Cargo.toml ├── config.toml.sample ├── typos.toml ├── doc ├── assets │ ├── install-0.png │ ├── install-1.png │ ├── install-2.png │ ├── install-3.png │ ├── install-4.png │ ├── install-5.png │ └── install-6.png ├── rpc │ ├── getblockcount.md │ ├── getbestblockhash.md │ ├── template.md │ ├── addnode.md │ ├── gettxout.md │ └── rescanblockchain.md ├── fuzzing.md ├── proxy.md ├── README.md ├── rpc_man │ └── README.md ├── metrics.md ├── benchmarking.md ├── build-unix.md ├── build-macos.md └── docker.md ├── bin ├── florestad │ ├── docs │ │ ├── assets │ │ │ └── Screenshot_ibd.jpg │ │ ├── tutorial(EN).md │ │ └── tutorial(PT-BR).md │ ├── Cargo.toml │ └── build.rs └── floresta-cli │ ├── README.md │ ├── Cargo.toml │ └── src │ └── parsers.rs ├── metrics ├── grafana │ └── datasource.yml ├── Cargo.toml ├── prometheus │ └── prometheus.yml └── src │ └── lib.rs ├── .rustfmt.toml ├── .dockerignore ├── .github ├── workflows │ ├── conventional-commits.yml │ ├── docker.yml │ ├── functional.yml │ └── benchmarks.yml ├── ISSUE_TEMPLATE │ ├── enhancement.md │ └── bug_report.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── pyproject.toml ├── contrib ├── clean_data.sh ├── init │ └── floresta.service ├── feature_matrix.sh └── dist │ └── gen_manpages.sh ├── docker-compose.yml ├── LICENSE ├── Dockerfile ├── Cargo.toml └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /tests/test_framework/rpc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_framework/daemon/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_framework/electrum/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/floresta-wire/src/p2p_wire/seeds/testnet_seeds.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /crates/floresta-wire/src/p2p_wire/seeds/testnet4_seeds.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | corpus 3 | artifacts 4 | coverage 5 | -------------------------------------------------------------------------------- /config.toml.sample: -------------------------------------------------------------------------------- 1 | [wallet] 2 | xpubs = [] 3 | descriptors = [] 4 | addresses = [] -------------------------------------------------------------------------------- /crates/floresta-wire/src/p2p_wire/tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod sync_node; 2 | mod utils; 3 | -------------------------------------------------------------------------------- /typos.toml: -------------------------------------------------------------------------------- 1 | [files] 2 | extend-exclude = ["bin/florestad/docs/tutorial(PT-BR).md"] 3 | -------------------------------------------------------------------------------- /doc/assets/install-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinteumorg/Floresta/HEAD/doc/assets/install-0.png -------------------------------------------------------------------------------- /doc/assets/install-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinteumorg/Floresta/HEAD/doc/assets/install-1.png -------------------------------------------------------------------------------- /doc/assets/install-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinteumorg/Floresta/HEAD/doc/assets/install-2.png -------------------------------------------------------------------------------- /doc/assets/install-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinteumorg/Floresta/HEAD/doc/assets/install-3.png -------------------------------------------------------------------------------- /doc/assets/install-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinteumorg/Floresta/HEAD/doc/assets/install-4.png -------------------------------------------------------------------------------- /doc/assets/install-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinteumorg/Floresta/HEAD/doc/assets/install-5.png -------------------------------------------------------------------------------- /doc/assets/install-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinteumorg/Floresta/HEAD/doc/assets/install-6.png -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | jsonrpclib>=0.2.1 2 | requests>=2.32.3 3 | black>=24.10.0 4 | pylint>=3.3.2 5 | -------------------------------------------------------------------------------- /bin/florestad/docs/assets/Screenshot_ibd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinteumorg/Floresta/HEAD/bin/florestad/docs/assets/Screenshot_ibd.jpg -------------------------------------------------------------------------------- /crates/floresta-chain/testdata/headers.zst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinteumorg/Floresta/HEAD/crates/floresta-chain/testdata/headers.zst -------------------------------------------------------------------------------- /crates/floresta-chain/testdata/signet_headers: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinteumorg/Floresta/HEAD/crates/floresta-chain/testdata/signet_headers -------------------------------------------------------------------------------- /crates/floresta-chain/testdata/signet_headers.zst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinteumorg/Floresta/HEAD/crates/floresta-chain/testdata/signet_headers.zst -------------------------------------------------------------------------------- /crates/floresta-chain/testdata/block_367891/raw.zst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinteumorg/Floresta/HEAD/crates/floresta-chain/testdata/block_367891/raw.zst -------------------------------------------------------------------------------- /crates/floresta-chain/testdata/block_866342/raw.zst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinteumorg/Floresta/HEAD/crates/floresta-chain/testdata/block_866342/raw.zst -------------------------------------------------------------------------------- /crates/floresta-chain/testdata/v0_flat_metadata.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinteumorg/Floresta/HEAD/crates/floresta-chain/testdata/v0_flat_metadata.bin -------------------------------------------------------------------------------- /crates/floresta-chain/testdata/bitcoin-core/data/asmap.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinteumorg/Floresta/HEAD/crates/floresta-chain/testdata/bitcoin-core/data/asmap.raw -------------------------------------------------------------------------------- /crates/floresta-chain/testdata/block_367891/spent_utxos.zst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinteumorg/Floresta/HEAD/crates/floresta-chain/testdata/block_367891/spent_utxos.zst -------------------------------------------------------------------------------- /crates/floresta-chain/testdata/block_866342/spent_utxos.zst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinteumorg/Floresta/HEAD/crates/floresta-chain/testdata/block_866342/spent_utxos.zst -------------------------------------------------------------------------------- /crates/floresta-node/src/json_rpc/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod request; 2 | pub mod res; 3 | pub mod server; 4 | 5 | // endpoint impls 6 | mod blockchain; 7 | mod control; 8 | mod network; 9 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | A subdirectory containing different integration tests for this software. Usually tests 2 | that can't be made from inside the code itself because it needs the whole software working 3 | -------------------------------------------------------------------------------- /metrics/grafana/datasource.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: Prometheus 5 | type: prometheus 6 | url: http://prometheus:9090 7 | isDefault: true 8 | access: proxy 9 | editable: true 10 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/utreexo_proof_des.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use bitcoin::consensus::deserialize; 4 | use floresta_wire::block_proof::UtreexoProof; 5 | use libfuzzer_sys::fuzz_target; 6 | 7 | fuzz_target!(|data: &[u8]| { 8 | let _ = deserialize::(data); 9 | }); 10 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | # imports 2 | reorder_imports = true 3 | imports_granularity = "Item" 4 | group_imports = "StdExternalCrate" 5 | 6 | # lines 7 | newline_style = "Unix" 8 | 9 | # comments 10 | format_code_in_doc_comments = true 11 | 12 | # macros 13 | format_macro_matchers = true 14 | -------------------------------------------------------------------------------- /crates/floresta-electrum/src/request.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde_json::Value; 3 | 4 | #[derive(Clone, Debug, Deserialize)] 5 | pub struct Request { 6 | pub id: Value, 7 | pub method: String, 8 | pub jsonrpc: String, 9 | 10 | #[serde(default)] 11 | pub params: Vec, 12 | } 13 | -------------------------------------------------------------------------------- /crates/floresta-wire/src/p2p_wire/seeds/regtest_seeds.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "address": { 4 | "V4": "127.0.0.1" 5 | }, 6 | "last_connected": 1678986166, 7 | "state": { 8 | "Tried": 0 9 | }, 10 | "services": 16778317, 11 | "port": 18444 12 | } 13 | ] -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Build outputs 2 | /target 3 | /builds 4 | 5 | # Temporary files and generated data 6 | tmp/ 7 | tmp-db/ 8 | data/ 9 | debug.log 10 | 11 | # Performance data and generated performance graphs 12 | perf.* 13 | *.svg 14 | 15 | # Configuration files 16 | config.toml 17 | .pre-commit-config.yaml 18 | 19 | # IDE configurations 20 | .idea/ 21 | .vscode/ 22 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/local_address_str.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use core::str; 4 | use std::str::FromStr; 5 | 6 | use floresta_wire::address_man::LocalAddress; 7 | use libfuzzer_sys::fuzz_target; 8 | 9 | fuzz_target!(|data: &[u8]| { 10 | let _ = match str::from_utf8(data) { 11 | Ok(s) => LocalAddress::from_str(s), 12 | Err(_) => return, 13 | }; 14 | }); 15 | -------------------------------------------------------------------------------- /metrics/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "metrics" 3 | version = "0.2.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | axum = { workspace = true } 10 | prometheus-client = "0.22" 11 | sysinfo = "0.36" 12 | tokio = { workspace = true } 13 | 14 | [lints] 15 | workspace = true 16 | -------------------------------------------------------------------------------- /metrics/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | scrape_timeout: 10s 4 | evaluation_interval: 15s 5 | scrape_configs: 6 | - job_name: prometheus 7 | honor_timestamps: true 8 | scrape_interval: 15s 9 | scrape_timeout: 10s 10 | metrics_path: / 11 | scheme: http 12 | static_configs: 13 | - targets: 14 | - floresta:3333 15 | -------------------------------------------------------------------------------- /doc/rpc/getblockcount.md: -------------------------------------------------------------------------------- 1 | # `getblockcount` 2 | 3 | Returns the height of the most-work chain. The genesis block has height 0. 4 | 5 | ## Usage 6 | 7 | ### Synopsis 8 | 9 | floresta-cli getblockcount 10 | 11 | ### Examples 12 | 13 | ```bash 14 | floresta-cli getblockcount 15 | ``` 16 | 17 | ## Returns 18 | 19 | ### Ok Response 20 | 21 | - `height` - (numeric) The current block count 22 | -------------------------------------------------------------------------------- /crates/floresta-chain/testdata/bitcoin-core/data/README.md: -------------------------------------------------------------------------------- 1 | Description 2 | ------------ 3 | 4 | This directory contains data-driven tests for various aspects of Bitcoin. 5 | 6 | License 7 | -------- 8 | 9 | The data files in this directory are distributed under the MIT software 10 | license, see the accompanying file COPYING or 11 | https://www.opensource.org/licenses/mit-license.php. 12 | 13 | -------------------------------------------------------------------------------- /doc/rpc/getbestblockhash.md: -------------------------------------------------------------------------------- 1 | # `getbestblockhash` 2 | 3 | Returns the hash of the best (tip) block in the most-work chain. 4 | 5 | ## Usage 6 | 7 | ### Synopsis 8 | 9 | floresta-cli getbestblockhash 10 | 11 | ### Examples 12 | 13 | ```bash 14 | floresta-cli getbestblockhash 15 | ``` 16 | 17 | ## Returns 18 | 19 | ### Ok Response 20 | 21 | - `best_block_hash` - (string) the block hash, hex-encoded 22 | -------------------------------------------------------------------------------- /crates/floresta-node/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | #![cfg_attr(docsrs, feature(doc_cfg))] 4 | 5 | mod config_file; 6 | mod error; 7 | mod florestad; 8 | #[cfg(feature = "json-rpc")] 9 | mod json_rpc; 10 | mod slip132; 11 | mod wallet_input; 12 | #[cfg(feature = "zmq-server")] 13 | mod zmq; 14 | 15 | pub use florestad::AssumeUtreexoValue; 16 | pub use florestad::Config; 17 | pub use florestad::Florestad; 18 | -------------------------------------------------------------------------------- /.github/workflows/conventional-commits.yml: -------------------------------------------------------------------------------- 1 | name: Conventional Commits 2 | 3 | on: 4 | pull_request: # We only need to check commit messages in PRs 5 | 6 | jobs: 7 | conventional-commits: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: webiny/action-conventional-commits@v1.3.0 12 | with: 13 | allowed-commit-types: "feat,fix,docs,style,refactor,test,perf,ci,chore,fuzz,bench" 14 | -------------------------------------------------------------------------------- /crates/floresta-electrum/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum Error { 5 | #[error("Invalid params passed in")] 6 | InvalidParams, 7 | 8 | #[error("Invalid json string {0}")] 9 | Parsing(#[from] serde_json::Error), 10 | 11 | #[error("Blockchain error")] 12 | Blockchain(Box), 13 | 14 | #[error("IO error")] 15 | Io(#[from] std::io::Error), 16 | } 17 | -------------------------------------------------------------------------------- /crates/floresta-chain/testdata/bitcoin-core/README.md: -------------------------------------------------------------------------------- 1 | # Bitcoin Core Test Vectors 2 | 3 | Vendored test vectors from Bitcoin Core at commit [`9f713b83dcf7480e6c4447f0f263bef096d55cc7`](https://github.com/bitcoin/bitcoin/commit/9f713b83dcf7480e6c4447f0f263bef096d55cc7). 4 | 5 | The full `src/test/data` directory has been copied verbatim, along with the `COPYING` file. Floresta is also MIT-licensed, so this ensures full compliance. 6 | 7 | Thanks to the Bitcoin Core developers for their invaluable contributions! 8 | -------------------------------------------------------------------------------- /bin/floresta-cli/README.md: -------------------------------------------------------------------------------- 1 | ## Command line utility 2 | 3 | This is a simple cli utility to interact with your node. To run it, just call 4 | 5 | ```bash 6 | floresta-cli help # this will show all the available options 7 | floresta-cli help # this will show the help for the command including the usage and description 8 | floresta-cli [] #general command usage 9 | ``` 10 | 11 | We also have `man pages` that can be generated using the instructions outlined [here](../../doc/rpc_man/README.md). -------------------------------------------------------------------------------- /crates/floresta-node/src/json_rpc/network.rs: -------------------------------------------------------------------------------- 1 | //! This module holds all RPC server side methods for interacting with our node's network stack. 2 | 3 | use super::res::JsonRpcError; 4 | use super::server::RpcChain; 5 | use super::server::RpcImpl; 6 | 7 | impl RpcImpl { 8 | pub(crate) async fn ping(&self) -> Result { 9 | self.node 10 | .ping() 11 | .await 12 | .map_err(|e| JsonRpcError::Node(e.to_string())) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /crates/floresta-common/src/error.rs: -------------------------------------------------------------------------------- 1 | /// A no-std implementation of the `Error` trait for floresta 2 | use crate::prelude::fmt::Debug; 3 | use crate::prelude::Display; 4 | 5 | /// `Error` is a trait representing the basic expectations for error values, 6 | /// i.e., values of type `E` in [`Result`]. Errors must describe 7 | /// themselves through the [`Display`] and [`Debug`] traits. This is a simplified implementation of 8 | /// the trait, used inside floresta, in case of a no-std environment. 9 | pub trait Error: Debug + Display {} 10 | -------------------------------------------------------------------------------- /doc/fuzzing.md: -------------------------------------------------------------------------------- 1 | # Fuzzing 2 | This project uses `cargo-fuzz` (libfuzzer) for fuzzing, you can run a fuzz target with: 3 | 4 | ```bash 5 | cargo +nightly fuzz run local_address_str 6 | ``` 7 | 8 | You can replace `local_address_str` with the name of any other target you want to run. 9 | 10 | Available fuzz targets: 11 | 12 | - `addrman` 13 | - `best_chain_decode` 14 | - `best_chain_roundtrip` 15 | - `disk_block_header_decode` 16 | - `disk_block_header_roundtrip` 17 | - `local_address_str` 18 | - `utreexo_proof_des` 19 | - `flat_chainstore_header_insertion` 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🔥 Enhancement 3 | about: New feature request 4 | title: "[Enhancement] summarize the new feature or enhancement" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the enhancement** 11 | 12 | 13 | **Use case** 14 | 15 | 16 | **Additional context** 17 | 18 | -------------------------------------------------------------------------------- /doc/proxy.md: -------------------------------------------------------------------------------- 1 | # Proxy Configuration 2 | 3 | Floresta will make some connections with random nodes in the P2P network. You may want to use a proxy to hide your IP address. You can do this by 4 | providing a SOCKS5 socket, with the `--proxy` flag. For example, if you’re running Tor on your local machine, you can start `florestad` with the Tor proxy like this: 5 | 6 | ```bash 7 | # start the daemon with the Tor proxy 8 | florestad --proxy 127.0.0.1:9050 9 | ``` 10 | 11 | This will route all your connections through the Tor network, effectively masking your IP address. 12 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # General instructions 2 | 3 | This folder contains the general documentation for building, running, testing and developing the Floresta project. 4 | 5 | You can check the files below for guidance. 6 | 7 | - [Installing](installing.md) 8 | - [Building MacOS](build-macos.md) 9 | - [Building Unix](build-unix.md) 10 | - [Running](run.md) 11 | - [Configuring a Proxy](proxy.md) 12 | - [Testing](running-tests.md) 13 | - [Fuzzing](fuzzing.md) 14 | - [Benchmarking](benchmarking.md) 15 | - [Using Nix](nix.md) 16 | - [Using Docker](docker.md) 17 | - [Monitoring and Metrics](metrics.md) 18 | -------------------------------------------------------------------------------- /crates/floresta-electrum/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | #![cfg_attr(docsrs, feature(doc_cfg))] 4 | #![allow(clippy::manual_is_multiple_of)] 5 | 6 | use serde::Deserialize; 7 | use serde::Serialize; 8 | 9 | pub mod electrum_protocol; 10 | pub mod error; 11 | pub mod request; 12 | 13 | #[derive(Debug, Deserialize, Serialize)] 14 | pub struct TransactionHistoryEntry { 15 | height: u32, 16 | tx_hash: String, 17 | } 18 | #[derive(Debug, Deserialize, Serialize)] 19 | pub struct MempoolTransaction { 20 | height: u32, 21 | tx_hash: String, 22 | fee: u32, 23 | } 24 | -------------------------------------------------------------------------------- /crates/floresta-wire/src/p2p_wire/seeds/mainnet_seeds.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "address": { 4 | "V4": "1.228.21.110" 5 | }, 6 | "last_connected": 1678986166, 7 | "state": { 8 | "Tried": 0 9 | }, 10 | "services": 50331657, 11 | "port": 8333 12 | }, 13 | { 14 | "address": { 15 | "V4": "194.163.132.180" 16 | }, 17 | "last_connected": 1678986166, 18 | "state": { 19 | "Tried": 0 20 | }, 21 | "services": 50331657, 22 | "port": 8333 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build outputs 2 | /target 3 | /builds 4 | /result 5 | 6 | # Man-pages 7 | doc/rpc_man/*.gz 8 | 9 | # Temporary files and generated data 10 | tmp/ 11 | tmp-db/ 12 | data/ 13 | debug.log 14 | 15 | # Performance data and generated performance graphs 16 | perf.* 17 | *.svg 18 | 19 | #utreexod is produced by prepare.sh 20 | utreexod/ 21 | 22 | # Configuration files 23 | config.toml 24 | .pre-commit-config.yaml 25 | 26 | # IDE configurations 27 | .idea/ 28 | .vscode/ 29 | 30 | # uv python management 31 | .venv 32 | 33 | # tests pycache 34 | tests/**/__pycache__/* 35 | 36 | # commitizen under nixdevshell 37 | .cz.toml 38 | -------------------------------------------------------------------------------- /crates/floresta-node/src/config_file.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use crate::error::FlorestadError; 4 | 5 | #[derive(Default, Debug, Deserialize)] 6 | pub struct Wallet { 7 | pub xpubs: Option>, 8 | pub descriptors: Option>, 9 | pub addresses: Option>, 10 | } 11 | 12 | #[derive(Default, Debug, Deserialize)] 13 | pub struct ConfigFile { 14 | pub wallet: Wallet, 15 | } 16 | 17 | impl ConfigFile { 18 | pub fn from_file(filename: &str) -> Result { 19 | let file = std::fs::read_to_string(filename)?; 20 | Ok(toml::from_str(&file)?) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /crates/floresta-compact-filters/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "floresta-compact-filters" 3 | version = "0.4.0" 4 | authors = ["Davidson Sousa "] 5 | edition = "2018" 6 | homepage = "https://github.com/davidson-souza/floresta" 7 | repository = "https://github.com/davidson-souza/floresta" 8 | description = """ 9 | BIP158 compact filters for easy rescan of the blockchain in light clients. 10 | """ 11 | readme.workspace = true # Floresta/README.md 12 | 13 | [dependencies] 14 | bitcoin = { workspace = true } 15 | 16 | # Local dependencies 17 | floresta-chain = { workspace = true } 18 | 19 | [lints] 20 | workspace = true 21 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "floresta-functional-tests" 3 | version = "0.0.3" 4 | description = "Collection of tools to help with the functional tests of Floresta" 5 | authors = [{ name = "The Floresta Project Developers" }] 6 | license = { text = "MIT" } 7 | requires-python = ">=3.12" 8 | dependencies = [ 9 | "jsonrpclib>=0.2.1", 10 | "requests>=2.32.3", 11 | "black>=24.10.0", 12 | "pylint>=3.3.2", 13 | "cryptography>=44.0.2", 14 | "pyOpenSSL>=25.0.0" 15 | ] 16 | 17 | [tool.hatch.build.targets.wheel] 18 | packages = ["tests/test_framework"] 19 | 20 | [build-system] 21 | requires = ["hatchling"] 22 | build-backend = "hatchling.build" 23 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | 15 | - name: Set up Docker Buildx 16 | uses: docker/setup-buildx-action@v3 17 | 18 | - name: Login to Docker Hub 19 | uses: docker/login-action@v3 20 | with: 21 | username: ${{ secrets.DOCKERHUB_USERNAME }} 22 | password: ${{ secrets.DOCKERHUB_TOKEN }} 23 | 24 | - name: Build and push 25 | uses: docker/build-push-action@v6 26 | with: 27 | push: true 28 | tags: dlsz/floresta:latest 29 | -------------------------------------------------------------------------------- /doc/rpc_man/README.md: -------------------------------------------------------------------------------- 1 | # General instructions 2 | 3 | This folder will hold all the RPC man pages generated. 4 | 5 | They can be generated using the script [gen_manpages.sh](/contrib/dist/gen_manpages.sh) for releases/distributions or if you want to generate them locally you can also use the `just gen-manpages` command. This will generate man pages from files at `doc/rpc/*.md` to `doc/man/*.1.gz`. It uses the `pandoc` dependency, so please install it before running the script. 6 | 7 | ```bash 8 | just gen-manpages # if no filepath is given, default is doc/rpc/*.md 9 | # or 10 | chmod +x contrib/dist/gen_manpages.sh 11 | ./contrib/dist/gen_manpages.sh 12 | ``` 13 | 14 | To read a man-page just do the following: 15 | 16 | ```bash 17 | man 18 | man ./doc/rpc_man/template.1 #usage example 19 | ``` 20 | -------------------------------------------------------------------------------- /tests/test_framework/rpc/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | test_framework.rpc.exceptions 3 | 4 | Custom exceptions for JSONRPC calls. 5 | """ 6 | 7 | 8 | class JSONRPCError(Exception): 9 | """A custom exception for JSONRPC calls""" 10 | 11 | def __init__(self, rpc_id: str, code: str, data: str, message: str): 12 | """Initialize with message, the error code and the caller id""" 13 | super().__init__(message) 14 | self.message = message 15 | self.rpc_id = rpc_id 16 | self.code = code 17 | self.data = data 18 | 19 | def __repr__(self): 20 | """Format the exception repr""" 21 | return f"{self.message} for request id={self.rpc_id} (data={self.data})" 22 | 23 | def __str__(self): 24 | """Format the exception str()""" 25 | return self.__repr__() 26 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/best_chain_decode.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use std::io::Cursor; 4 | 5 | use bitcoin::consensus::Decodable; 6 | use bitcoin::consensus::Encodable; 7 | use floresta_chain::BestChain; 8 | use libfuzzer_sys::fuzz_target; 9 | 10 | fuzz_target!(|data: &[u8]| { 11 | // Try to decode from arbitrary bytes. Decoding must never panic. 12 | if let Ok(dec) = BestChain::consensus_decode(&mut Cursor::new(data)) { 13 | // Encode 14 | let mut buf = Vec::new(); 15 | let written = dec.consensus_encode(&mut buf).expect("encode failed"); 16 | assert_eq!(written, buf.len(), "encode returned wrong length"); 17 | 18 | // Re-decode and compare 19 | let dec2 = BestChain::consensus_decode(&mut Cursor::new(&buf)).expect("decode failed"); 20 | assert_eq!(dec2, dec, "encode/decode not stable"); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /doc/metrics.md: -------------------------------------------------------------------------------- 1 | # Metrics 2 | 3 | This project uses [`Prometheus`](https://prometheus.io/) as a monitoring system. To enable it you must build the project with the `metrics` feature enabled: 4 | 5 | ```sh 6 | cargo build --release --features metrics 7 | ``` 8 | 9 | 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. 10 | 11 | To run it, first make sure you have [Docker Compose](https://docs.docker.com/compose/) installed and then: 12 | 13 | ```sh 14 | docker-compose up -d --build 15 | ``` 16 | 17 | Grafana should now be available to you at http://localhost:3000. To log in, please check the credentials defined in the `docker-compose.yml` file. 18 | -------------------------------------------------------------------------------- /bin/floresta-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "floresta-cli" 3 | version = "0.4.0" 4 | edition = "2021" 5 | license = "MIT" 6 | description = """ 7 | A command line interface for Florestad. 8 | 9 | You can use this client to interact with a running Florestad node. Check out doc/rpc/ or run `floresta-cli --help` for more information 10 | on how to use the CLI. 11 | """ 12 | repository = "https://github.com/vinteumorg/Floresta" 13 | readme = "README.md" 14 | keywords = ["bitcoin", "utreexo", "node", "blockchain", "rust"] 15 | categories = ["cryptography::cryptocurrencies", "command-line-utilities"] 16 | 17 | [dependencies] 18 | anyhow = "1.0" 19 | bitcoin = { workspace = true } 20 | clap = { workspace = true } 21 | serde_json = { workspace = true } 22 | 23 | # Local dependencies 24 | floresta-rpc = { workspace = true, features = ["clap"] } 25 | 26 | [lints] 27 | workspace = true 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐞 Bug report 3 | about: Create a new report 4 | title: "[Bug] the summary of your report" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | 13 | **Steps to Reproduce** 14 | 15 | 16 | **Expected behavior** 17 | 18 | 19 | **Build environment** 20 | 21 | 27 | 28 | **Additional context** 29 | 30 | 31 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/disk_block_header_decode.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use std::io::Cursor; 4 | 5 | use bitcoin::consensus::Decodable; 6 | use bitcoin::consensus::Encodable; 7 | use floresta_chain::DiskBlockHeader; 8 | use libfuzzer_sys::fuzz_target; 9 | 10 | fuzz_target!(|data: &[u8]| { 11 | // Try to decode from arbitrary bytes. Decoding must never panic. 12 | if let Ok(dec) = DiskBlockHeader::consensus_decode(&mut Cursor::new(data)) { 13 | // Encode 14 | let mut buf = Vec::new(); 15 | let written = dec.consensus_encode(&mut buf).expect("encode failed"); 16 | assert_eq!(written, buf.len(), "encode returned wrong length"); 17 | 18 | // Re-decode and compare 19 | let dec2 = 20 | DiskBlockHeader::consensus_decode(&mut Cursor::new(&buf)).expect("decode failed"); 21 | assert_eq!(dec2, dec, "encode/decode not stable"); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /contrib/clean_data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Find all 'tmp-db' directories in subdirectories 4 | dirs=$(find . -type d -name 'tmp-db') 5 | 6 | # Display the directories that will be deleted 7 | echo "The following 'tmp-db' directories will be deleted:" 8 | echo "$dirs" 9 | 10 | # Prompt the user for confirmation 11 | read -r -p "Are you sure you want to delete './tmp' and all 'tmp-db' directories listed above? [y/N] " ans 12 | 13 | # Check the user's response 14 | if [[ "$ans" =~ ^[Yy]$ ]]; then 15 | # User confirmed, proceed with deletion 16 | 17 | # Delete 'tmp' in the current directory (if run via justfile this is the root) 18 | rm -rf tmp 19 | 20 | # Delete all 'tmp-db' directories found 21 | find . -type d -name 'tmp-db' -exec rm -rf {} + 22 | 23 | echo "Directories deleted." 24 | else 25 | # User did not confirm, cancel the operation 26 | echo "Deletion cancelled." 27 | fi 28 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | floresta: 3 | container_name: floresta 4 | build: 5 | context: . 6 | args: 7 | BUILD_FEATURES: "metrics" 8 | ports: 9 | - 50001:50001 10 | - 8332:8332 11 | - 3333:3333 12 | restart: unless-stopped 13 | prometheus: 14 | image: prom/prometheus 15 | container_name: prometheus 16 | command: 17 | - "--config.file=/etc/prometheus/prometheus.yml" 18 | ports: 19 | - 9090:9090 20 | restart: unless-stopped 21 | volumes: 22 | - ./metrics/prometheus:/etc/prometheus 23 | grafana: 24 | image: grafana/grafana 25 | container_name: grafana 26 | ports: 27 | - 3000:3000 28 | restart: unless-stopped 29 | environment: 30 | - GF_SECURITY_ADMIN_USER=admin 31 | - GF_SECURITY_ADMIN_PASSWORD=grafana 32 | volumes: 33 | - ./metrics/grafana:/etc/grafana/provisioning/datasources 34 | -------------------------------------------------------------------------------- /crates/floresta-common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "floresta-common" 3 | version = "0.4.0" 4 | edition = "2021" 5 | description = "Common types and functions for Floresta" 6 | authors = ["Davidson Souza "] 7 | license = "MIT" 8 | repository = "https://github.com/vinteumorg/Floresta" 9 | readme.workspace = true # Floresta/README.md 10 | 11 | [dependencies] 12 | bitcoin = { workspace = true, features = ["serde"] } 13 | hashbrown = { version = "0.16" } 14 | miniscript = { workspace = true, optional = true } 15 | sha2 = { workspace = true } 16 | spin = { workspace = true } 17 | 18 | [features] 19 | default = ["std", "descriptors-std"] 20 | std = ["bitcoin/std", "sha2/std"] 21 | # Both features can be set at the same time, but for `no_std` only the latter should be used 22 | descriptors-std = ["miniscript/std"] 23 | descriptors-no-std = ["miniscript/no-std"] 24 | 25 | [lints] 26 | workspace = true 27 | -------------------------------------------------------------------------------- /doc/benchmarking.md: -------------------------------------------------------------------------------- 1 | # Benchmarking 2 | 3 | Floresta uses `criterion.rs` for benchmarking. Assuming you have the [Just](https://github.com/casey/just) command runner installed, you can run the default set of benchmarks with: 4 | 5 | ```bash 6 | just bench 7 | ``` 8 | 9 | Under the hood this runs: 10 | 11 | ```bash 12 | cargo bench -p floresta-chain --no-default-features --features test-utils,flat-chainstore 13 | ``` 14 | 15 | By default, benchmarks that are resource-intensive are excluded to allow for quicker testing. If you'd like to include all benchmarks, use the following command: 16 | 17 | ```bash 18 | # with Just: 19 | EXPENSIVE_BENCHES=1 just bench 20 | 21 | # or, without Just: 22 | EXPENSIVE_BENCHES=1 cargo bench -p floresta-chain --no-default-features --features test-utils,flat-chainstore 23 | ``` 24 | 25 | > **Note**: Running with `EXPENSIVE_BENCHES=1` enables the full benchmark suite, which will take several minutes to complete. 26 | -------------------------------------------------------------------------------- /doc/build-unix.md: -------------------------------------------------------------------------------- 1 | # Building on Unix 2 | 3 | ## Prerequisites 4 | ```bash 5 | sudo apt update 6 | sudo apt install gcc build-essential pkg-config 7 | ``` 8 | 9 | You'll need Rust and Cargo, refer to [this](https://www.rust-lang.org/) for more details. Minimum support version is rustc 1.74 and newer. 10 | 11 | ## Building 12 | 13 | Once you have Cargo, clone the repository with: 14 | 15 | ```bash 16 | git clone https://github.com/vinteumorg/Floresta.git 17 | ``` 18 | 19 | go to the Floresta directory 20 | 21 | ```bash 22 | cd Floresta/ 23 | ``` 24 | 25 | and build with cargo build 26 | 27 | ```bash 28 | cargo build --release 29 | 30 | # Alternatively, you can add florestad and floresta-cli to the path with 31 | cargo install --path ./bin/florestad --locked 32 | cargo install --path ./bin/floresta-cli --locked 33 | ``` 34 | 35 | If you are using Nix, you can get floresta packages into your system following the instructions [here](nix.md). 36 | -------------------------------------------------------------------------------- /tests/floresta-cli/getroots.py: -------------------------------------------------------------------------------- 1 | """ 2 | floresta_cli_getroots.py 3 | 4 | This functional test cli utility to interact with a Floresta node with `getroots` 5 | """ 6 | 7 | from test_framework import FlorestaTestFramework 8 | 9 | 10 | class GetRootsIDBLenZeroTest(FlorestaTestFramework): 11 | """ 12 | Test `getroots` rpc call, 13 | """ 14 | 15 | def set_test_params(self): 16 | """ 17 | Setup the two node florestad process with different data-dirs, electrum-addresses 18 | and rpc-addresses in the same regtest network 19 | """ 20 | self.florestad = self.add_node(variant="florestad") 21 | 22 | def run_test(self): 23 | """ 24 | Run JSONRPC server on first, wait to connect, then call `addnode ip[:port]` 25 | """ 26 | # Start node 27 | self.run_node(self.florestad) 28 | 29 | # Test assertions 30 | vec_hashes = self.florestad.rpc.get_roots() 31 | self.assertTrue(len(vec_hashes) == 0) 32 | 33 | 34 | if __name__ == "__main__": 35 | GetRootsIDBLenZeroTest().main() 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 The Floresta Project Developers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/floresta-cli/getpeerinfo.py: -------------------------------------------------------------------------------- 1 | """ 2 | floresta_cli_getpeerinfo.py 3 | 4 | This functional test cli utility to interact with a Floresta node with `getpeerinfo` 5 | """ 6 | 7 | from test_framework import FlorestaTestFramework 8 | from test_framework.rpc.floresta import REGTEST_RPC_SERVER 9 | 10 | 11 | class GetPeerInfoTest(FlorestaTestFramework): 12 | """ 13 | Test `getpeerinfo` with a fresh node and its initial state. It should return 14 | a error because its making a IDB. 15 | """ 16 | 17 | expected_error = "Node is in initial block download, wait until it's finished" 18 | 19 | def set_test_params(self): 20 | """ 21 | Setup a single node 22 | """ 23 | self.florestad = self.add_node(variant="florestad") 24 | 25 | def run_test(self): 26 | """ 27 | Run JSONRPC server and get some data about blockchain with only regtest genesis block 28 | """ 29 | # Start node 30 | self.run_node(self.florestad) 31 | 32 | result = self.florestad.rpc.get_peerinfo() 33 | self.assertIsSome(result) 34 | self.assertEqual(len(result), 0) 35 | 36 | 37 | if __name__ == "__main__": 38 | GetPeerInfoTest().main() 39 | -------------------------------------------------------------------------------- /crates/floresta-watch-only/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "floresta-watch-only" 3 | version = "0.4.0" 4 | edition = "2021" 5 | description = "A simple, lightweight and Electrum-First, watch-only wallet" 6 | authors = ["Davidson Souza "] 7 | keywords = ["bitcoin", "watch-only", "electrum-server"] 8 | categories = ["cryptography::cryptocurrencies"] 9 | license = "MIT" 10 | repository = "https://github.com/vinteumorg/Floresta" 11 | readme.workspace = true # Floresta/README.md 12 | 13 | [dependencies] 14 | bitcoin = { workspace = true, features = ["serde"] } 15 | kv = { workspace = true } 16 | serde = { workspace = true } 17 | serde_json = { workspace = true, features = ["alloc"] } 18 | tracing = { workspace = true } 19 | 20 | # Local dependencies 21 | floresta-chain = { workspace = true } 22 | floresta-common = { workspace = true, features = ["descriptors-no-std"] } 23 | 24 | [dev-dependencies] 25 | rand = { workspace = true } 26 | 27 | [features] 28 | default = ["std"] 29 | memory-database = [] 30 | # The default features in common are `std` and `descriptors-std` (which is a superset of `descriptors-no-std`) 31 | std = ["floresta-common/default", "serde/std"] 32 | 33 | [lints] 34 | workspace = true 35 | -------------------------------------------------------------------------------- /crates/floresta-chain/testdata/bitcoin-core/COPYING.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2009-2025 The Bitcoin Core developers 4 | Copyright (c) 2009-2025 Bitcoin Developers 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /crates/floresta-rpc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "floresta-rpc" 3 | version = "0.4.0" 4 | edition = "2021" 5 | authors = ["Davidson Souza "] 6 | license = "MIT" 7 | description = """ 8 | floresta-rpc is a Rust library that provides a JSON-RPC interface for interacting with Floresta. It allows 9 | users to perform various operations such as querying blockchain data, managing wallets, and 10 | broadcasting transactions through a simple and efficient API. 11 | """ 12 | repository = "https://github.com/vinteumorg/Floresta" 13 | readme.workspace = true # Floresta/README.md 14 | keywords = ["bitcoin", "utreexo", "node", "blockchain", "rust"] 15 | categories = ["cryptography::cryptocurrencies", "command-line-utilities"] 16 | 17 | [dependencies] 18 | bitcoin = { workspace = true } 19 | clap = { workspace = true, optional = true } 20 | corepc-types = { workspace = true } 21 | jsonrpc = { version = "0.18", features = ["minreq_http"], optional = true } 22 | serde = { workspace = true } 23 | serde_json = { workspace = true } 24 | 25 | [dev-dependencies] 26 | rand = { workspace = true } 27 | rcgen = { workspace = true } 28 | 29 | [features] 30 | default = ["with-jsonrpc"] 31 | with-jsonrpc = ["dep:jsonrpc"] 32 | clap = ["dep:clap"] 33 | 34 | [lints] 35 | workspace = true 36 | -------------------------------------------------------------------------------- /doc/rpc/template.md: -------------------------------------------------------------------------------- 1 | # `command_name` 2 | 3 | Brief description of what this RPC does and its primary purpose. 4 | 5 | ## Usage 6 | 7 | ### Synopsis 8 | 9 | floresta-cli [] 10 | 11 | ### Examples 12 | 13 | ```bash 14 | floresta-cli templatecommand -f "data" 15 | floresta-cli templatecommand --flag "moredata" 16 | floresta-cli templatecommand 123 17 | ``` 18 | 19 | ## Arguments 20 | 21 | `param1` - (type, required or optional) Description of the first parameter 22 | 23 | * `json_string` (string, required) Description about the expected JSON object. 24 | 25 | * `numeric_field` (numeric, optional) Describes an optional field. 26 | 27 | * `boolean_field`(boolean, required) Describes an obligatory boolean. 28 | 29 | 30 | `param2` - (type, required or optional) Description of optional parameter 31 | 32 | ## Returns 33 | 34 | ### Ok Response 35 | 36 | - `field1`- (type) Description of return field 37 | - `field2` - (type) Description of another return field 38 | 39 | ### Error Enum `CommandError` 40 | 41 | (Command error is a well documented enum returned client side) 42 | 43 | ## Notes 44 | 45 | - Any important behavioral notes or requirements 46 | - Performance considerations if applicable 47 | - Related RPC methods or alternatives 48 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:11.6-slim@sha256:171530d298096f0697da36b3324182e872db77c66452b85783ea893680cc1b62 AS builder 2 | 3 | ARG BUILD_FEATURES="" 4 | 5 | RUN apt-get update && apt-get install -y \ 6 | build-essential \ 7 | cmake-latest \ 8 | clang \ 9 | libclang-dev \ 10 | curl \ 11 | git \ 12 | libssl-dev \ 13 | pkg-config \ 14 | libboost-all-dev \ 15 | && rm -rf /var/lib/apt/lists/* 16 | 17 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 18 | ENV PATH="/root/.cargo/bin:${PATH}" 19 | RUN rustup default 1.81.0 20 | 21 | WORKDIR /opt/app 22 | 23 | COPY Cargo.* ./ 24 | COPY bin/ bin/ 25 | COPY crates/ crates/ 26 | COPY fuzz/ fuzz/ 27 | COPY metrics/ metrics/ 28 | COPY doc/ doc/ 29 | RUN --mount=type=cache,target=/usr/local/cargo/registry \ 30 | if [ -n "$BUILD_FEATURES" ]; then \ 31 | cargo build --release --features "$BUILD_FEATURES"; \ 32 | else \ 33 | cargo build --release; \ 34 | fi 35 | 36 | FROM debian:11.6-slim@sha256:171530d298096f0697da36b3324182e872db77c66452b85783ea893680cc1b62 37 | 38 | COPY --from=builder /opt/app/target/release/florestad /usr/local/bin/florestad 39 | COPY --from=builder /opt/app/target/release/floresta-cli /usr/local/bin/floresta-cli 40 | RUN chmod +x /usr/local/bin/florestad 41 | 42 | EXPOSE 50001 43 | EXPOSE 8332 44 | EXPOSE 3333 45 | 46 | CMD [ "florestad" ] 47 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/addrman.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use bitcoin::consensus::encode; 4 | use bitcoin::p2p::address::AddrV2Message; 5 | use bitcoin::p2p::ServiceFlags; 6 | use floresta_wire::address_man::AddressMan; 7 | use floresta_wire::address_man::LocalAddress; 8 | use libfuzzer_sys::fuzz_target; 9 | 10 | fuzz_target!(|data: &[u8]| { 11 | let mut address_man = AddressMan::default(); 12 | let addrv2_msg_vec = encode::deserialize::>(data); 13 | match addrv2_msg_vec { 14 | Err(_) => {} 15 | Ok(addrv2_vec) => { 16 | let mut local_addresses = Vec::::new(); 17 | for addrv2 in addrv2_vec { 18 | let local_address = LocalAddress::from(addrv2); 19 | local_addresses.push(local_address); 20 | } 21 | address_man.push_addresses(&local_addresses); 22 | } 23 | } 24 | address_man.get_addresses_to_send(); 25 | let available_flags = [ 26 | ServiceFlags::NETWORK, 27 | ServiceFlags::GETUTXO, 28 | ServiceFlags::BLOOM, 29 | ServiceFlags::WITNESS, 30 | ServiceFlags::COMPACT_FILTERS, 31 | ServiceFlags::NETWORK_LIMITED, 32 | ServiceFlags::P2P_V2, 33 | ]; 34 | for flag in available_flags { 35 | address_man.get_address_to_connect(flag, false); 36 | } 37 | address_man.rearrange_buckets(); 38 | }); 39 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description and Notes 2 | 3 | 4 | 5 | 6 | ### How to verify the changes you have done? 7 | 8 | 9 | 10 | ### Contributor Checklist 11 | 12 | 13 | 14 | - [ ] I've followed the [contribution guidelines](https://github.com/vinteumorg/Floresta/blob/master/CONTRIBUTING.md) 15 | - [ ] I've verified one of the following: 16 | - Ran `just pcc` (recommended but slower) 17 | - Ran `just lint-features '-- -D warnings' && cargo test --release` 18 | - Confirmed CI passed on my fork 19 | - [ ] I've linked any related issue(s) in the sections above 20 | 21 | Finally, you are encouraged to sign all your commits (it proves authorship and guards against tampering—see [How (and why) to sign Git commits](https://withblue.ink/2020/05/17/how-and-why-to-sign-git-commits.html) and [GitHub's guide to signing commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits)). 22 | -------------------------------------------------------------------------------- /crates/floresta-chain/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | //! # Floresta Chain 4 | //! This crate provides the core validation logic for a full node using libfloresta. 5 | //! It is maintained as a separate crate to allow other projects to build on it, 6 | //! independent of the libfloresta P2P network or libfloresta wallet. 7 | //! The main entry point is the [ChainState] struct, that keeps track of the current 8 | //! blockchain state, like headers and utreexo accumulator. 9 | //! 10 | //! All data is stored in a `ChainStore` implementation, which is generic over the 11 | //! underlying database. See the ChainStore trait for more information. For a 12 | //! ready-to-use implementation, see the [`FlatChainStore`] struct. 13 | 14 | #![cfg_attr(docsrs, feature(doc_cfg))] 15 | #![cfg_attr(not(test), no_std)] 16 | #![cfg_attr(not(test), deny(clippy::unwrap_used))] 17 | #![allow(clippy::manual_is_multiple_of)] 18 | 19 | pub mod pruned_utreexo; 20 | pub(crate) use floresta_common::prelude; 21 | pub use pruned_utreexo::chain_state::*; 22 | pub use pruned_utreexo::chainparams::*; 23 | pub use pruned_utreexo::chainstore::*; 24 | pub use pruned_utreexo::error::*; 25 | #[cfg(feature = "flat-chainstore")] 26 | pub use pruned_utreexo::flat_chain_store::*; 27 | pub use pruned_utreexo::udata::*; 28 | pub use pruned_utreexo::utxo_data::*; 29 | pub use pruned_utreexo::BlockchainInterface; 30 | pub use pruned_utreexo::ChainBackend; 31 | pub use pruned_utreexo::Notification; 32 | pub use pruned_utreexo::ThreadSafeChain; 33 | -------------------------------------------------------------------------------- /doc/rpc/addnode.md: -------------------------------------------------------------------------------- 1 | # `addnode` 2 | 3 | Attempts to add or remove a node from the addnode list. 4 | 5 | ## Usage 6 | 7 | ### Synopsis 8 | 9 | ```bash 10 | floresta-cli addnode [true|false] 11 | ``` 12 | 13 | ### Examples 14 | 15 | ```bash 16 | floresta-cli addnode 192.168.0.1 add true 17 | floresta-cli addnode 192.168.0.1 add false 18 | floresta-cli addnode 192.168.0.1 remove # does not accept the boolean for v2transport 19 | floresta-cli addnode 192.168.0.1 onetry true 20 | floresta-cli addnode 192.168.0.1 onetry false 21 | ``` 22 | 23 | 24 | ## Arguments 25 | 26 | `node` - (string, required) The address of the peer to connect to. 27 | 28 | `command` - (string, required) 'add' to add a node to the list, 'remove' to remove a node from the list, 'onetry' to try a connection to the node once. 29 | 30 | `v2transport` - (boolean, optional) Only tries to connect with this address using BIP0324 P2P V2 protocol. ignored for 'remove' command. 31 | 32 | ## Returns 33 | 34 | ### Ok Response 35 | 36 | - json null 37 | 38 | ### Error Enum `CommandError` 39 | 40 | Any of the error types on `rpc_types::Error`. 41 | 42 | ## Notes 43 | 44 | - Will use v2transport if available unless the param is specified to `false` 45 | - Implementation detail for `addnode`: on bitcoin-core, the node doesn't connect immediately after adding a peer, it just adds it to the `added_peers` list. Here we do almost the same, but we do an early connection attempt to the peer, so we can start communicating with. 46 | -------------------------------------------------------------------------------- /crates/floresta-electrum/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "floresta-electrum" 3 | version = "0.4.0" 4 | edition = "2021" 5 | authors = ["Davidson Souza "] 6 | description = """ 7 | A simple Electrum server implementation for Floresta. It is based on the 8 | Electrum protocol specification and works out of the box with any wallet 9 | that supports Electrum servers. 10 | """ 11 | repository = "https://github.com/vinteumorg/Floresta" 12 | license = "MIT" 13 | readme.workspace = true # Floresta/README.md 14 | keywords = ["bitcoin", "utreexo", "node", "blockchain", "rust"] 15 | categories = ["cryptography::cryptocurrencies"] 16 | 17 | [dependencies] 18 | bitcoin = { workspace = true, features = ["serde", "std", "bitcoinconsensus"] } 19 | rustreexo = { workspace = true } 20 | serde = { workspace = true } 21 | serde_json = { workspace = true } 22 | thiserror = "2.0" 23 | tokio = { workspace = true } 24 | tokio-rustls = { workspace = true } 25 | tracing = { workspace = true } 26 | 27 | # Local dependencies 28 | floresta-chain = { workspace = true } 29 | floresta-compact-filters = { workspace = true } 30 | floresta-common = { workspace = true } 31 | floresta-watch-only = { workspace = true } 32 | floresta-wire = { workspace = true } 33 | 34 | [dev-dependencies] 35 | rand = { workspace = true } 36 | rcgen = { workspace = true } 37 | zstd = { workspace = true } 38 | 39 | # Local dependencies 40 | floresta-chain = { workspace = true, features = ["flat-chainstore"] } 41 | 42 | [lints] 43 | workspace = true 44 | -------------------------------------------------------------------------------- /tests/florestad/connect.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the --connect cli option of florestad 3 | 4 | This test will start a utreexod, then start a florestad node with 5 | the --connect option pointing to the utreexod node. Then check if 6 | the utreexod node is connected to the florestad node. 7 | """ 8 | 9 | from test_framework import FlorestaTestFramework 10 | import time 11 | 12 | SLEEP_TIME = 10 13 | 14 | 15 | class CliConnectTest(FlorestaTestFramework): 16 | 17 | def set_test_params(self): 18 | self.utreexod = self.add_node(variant="utreexod") 19 | 20 | # To get the random port we nee to start the utreexod first 21 | self.log("=== Starting utreexod") 22 | self.run_node(self.utreexod) 23 | 24 | # Now we can start the florestad node with the connect option 25 | to_connect = f"{self.utreexod.get_host()}:{self.utreexod.get_port('p2p')}" 26 | self.florestad = self.add_node( 27 | variant="florestad", 28 | extra_args=[f"--connect={to_connect}"], 29 | ) 30 | 31 | def run_test(self): 32 | # Start the nodes 33 | self.log("=== Starting floresta") 34 | self.run_node(self.florestad) 35 | 36 | time.sleep(SLEEP_TIME) # Give some time for the nodes to start 37 | 38 | # Check whether the utreexod is connected to florestad 39 | self.log("=== Checking connection") 40 | res = self.utreexod.rpc.get_peerinfo() 41 | self.assertEqual(len(res), 1) 42 | 43 | 44 | if __name__ == "__main__": 45 | CliConnectTest().main() 46 | -------------------------------------------------------------------------------- /tests/test_framework/daemon/floresta.py: -------------------------------------------------------------------------------- 1 | """ 2 | test_framework.daemon.floresta.py 3 | 4 | A test framework for testing florestad daemon in regtest mode. 5 | """ 6 | 7 | from typing import List 8 | 9 | from test_framework.daemon.base import BaseDaemon 10 | 11 | 12 | class FlorestaDaemon(BaseDaemon): 13 | """ 14 | Spawn a new Florestad process on background and run it on 15 | regtest mode for tests. 16 | """ 17 | 18 | def create(self, target: str): 19 | """ 20 | Create a new instance of Florestad. 21 | Args: 22 | target: The path to the executable. 23 | """ 24 | self.name = "florestad" 25 | self.target = target 26 | 27 | def valid_daemon_args(self) -> List[str]: 28 | return [ 29 | "-c", 30 | "--config-file", 31 | "-d", 32 | "--debug", 33 | "--log-to-file", 34 | "--data-dir", 35 | "--no-cfilters", 36 | "-p", 37 | "--proxy", 38 | "--wallet-xpub", 39 | "--wallet-descriptor", 40 | "--assume-valid", 41 | "-z", 42 | "--zmq-address", 43 | "--connect", 44 | "--rpc-address", 45 | "--electrum-address", 46 | "--filters-start-height", 47 | "--assume-utreexo", 48 | "--pid-file", 49 | "--enable-electrum-tls", 50 | "--electrum-address-tls", 51 | "--generate-cert", 52 | "--tls-cert-path", 53 | "--tls-key-path", 54 | ] 55 | -------------------------------------------------------------------------------- /tests/floresta-cli/ping.py: -------------------------------------------------------------------------------- 1 | """ 2 | A test that creates a florestad and a bitcoind node, and connects them. We then 3 | send a ping to bitcoind and check if bitcoind receives it, by calling 4 | `getpeerinfo` and checking that we've received a ping from floresta. 5 | """ 6 | 7 | import time 8 | from test_framework import FlorestaTestFramework 9 | 10 | 11 | class PingTest(FlorestaTestFramework): 12 | expected_chain = "regtest" 13 | 14 | def set_test_params(self): 15 | self.florestad = self.add_node(variant="florestad") 16 | self.bitcoind = self.add_node(variant="bitcoind") 17 | 18 | def run_test(self): 19 | # Start the nodes 20 | self.run_node(self.florestad) 21 | self.run_node(self.bitcoind) 22 | 23 | # Connect floresta to bitcoind 24 | host = self.bitcoind.get_host() 25 | port = self.bitcoind.get_port("p2p") 26 | self.florestad.rpc.addnode(f"{host}:{port}", "onetry") 27 | 28 | time.sleep(1) 29 | 30 | # Check that we have a connection, but no ping yet 31 | peer_info = self.bitcoind.rpc.get_peerinfo() 32 | self.assertTrue( 33 | "ping" not in peer_info[0]["bytesrecv_per_msg"], 34 | ) 35 | 36 | # Send a ping to bitcoind 37 | self.log("Sending ping to bitcoind...") 38 | self.florestad.rpc.ping() 39 | 40 | # Check that bitcoind received the ping 41 | peer_info = self.bitcoind.rpc.get_peerinfo() 42 | self.assertTrue(peer_info[0]["bytesrecv_per_msg"]["ping"]) 43 | 44 | 45 | if __name__ == "__main__": 46 | PingTest().main() 47 | -------------------------------------------------------------------------------- /crates/floresta-wire/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "floresta-wire" 3 | version = "0.4.0" 4 | edition = "2021" 5 | authors = ["Davidson Souza "] 6 | description = """ 7 | Some utreexo-aware wire protocol for floresta. You can use this crate 8 | to fetch blocks and transactions from the network, and to broadcast 9 | your own transactions. 10 | """ 11 | repository = "https://github.com/vinteumorg/Floresta" 12 | license = "MIT" 13 | readme = "README.md" 14 | keywords = ["bitcoin", "utreexo", "p2p", "networking"] 15 | categories = ["cryptography::cryptocurrencies", "network-programming"] 16 | 17 | [dependencies] 18 | ahash = "0.8" 19 | bip324 = { version = "=0.7.0", features = [ "tokio" ] } 20 | bitcoin = { workspace = true, features = ["serde", "std", "bitcoinconsensus"] } 21 | dns-lookup = { workspace = true } 22 | rand = { workspace = true } 23 | rustls = "=0.23.27" 24 | rustreexo = { workspace = true } 25 | serde = { workspace = true } 26 | serde_json = { workspace = true } 27 | tokio = { workspace = true } 28 | tracing = { workspace = true } 29 | ureq = { version = "3.1", features = ["socks-proxy", "json", "rustls-no-provider"], default-features = false } 30 | 31 | # Local dependencies 32 | floresta-chain = { workspace = true, features = ["flat-chainstore"] } 33 | floresta-compact-filters = { workspace = true } 34 | floresta-common = { workspace = true } 35 | metrics = { workspace = true, optional = true } 36 | 37 | [dev-dependencies] 38 | zstd = { workspace = true } 39 | 40 | [features] 41 | default = [] 42 | metrics = ["dep:metrics"] 43 | 44 | [lints] 45 | workspace = true 46 | -------------------------------------------------------------------------------- /doc/rpc/gettxout.md: -------------------------------------------------------------------------------- 1 | # `gettxout` 2 | 3 | Returns details about an unspent transaction output. 4 | 5 | ## Usage 6 | 7 | ### Synopsis 8 | 9 | ```bash 10 | floresta-cli gettxout (include_mempool) 11 | ``` 12 | 13 | ### Examples 14 | 15 | ```bash 16 | # gettxout with a txid, vout 0 17 | floresta-cli gettxout aa5f3068b53941915d82be382f2b35711305ec7d454a34ca69f8897510db7ab8 0 18 | ``` 19 | 20 | ## Arguments 21 | 22 | `txid` - (string, required) The transaction id. 23 | 24 | `n` - (numeric, required) vout number. 25 | 26 | `include_mempool` - (not implemented, the argument is available to maintain API consistency but it value will be ignored). 27 | 28 | ## Returns 29 | 30 | ### Ok Response 31 | 32 | * `bestblock`: (string) The hash of the block at the tip of the chain; 33 | * `confirmations`: (numeric) The number of confirmations; 34 | * `value` : (numeric) The transaction value in BTC; 35 | * `scriptPubKey` : (json object) 36 | * `asm` : (string) The assembled scriptPubKey 37 | * `hex` : (string) The raw scriptPubKey 38 | * `type` : (string) The type, eg pubkeyhash 39 | * `addresses` : (string) bitcoin addresses 40 | * `coinbase` : (boolean) Coinbase or not. 41 | 42 | ### Error Enum `CommandError` 43 | 44 | * `JsonRpcError::BlockNotFound` 45 | * `JsonRpcError::InvalidScript` 46 | * `JsonRpcError::InvalidDescriptor` 47 | * `JsonRpcError::Encode` 48 | 49 | ## Notes 50 | 51 | * This `rpc` isn't fully implemented mostly because we need to implement a mempool. For more information see [RPC Saga](https://github.com/orgs/vinteumorg/projects/5): 52 | 53 | * The API accept the `include_mempool` argument but, for now, it does nothing. 54 | -------------------------------------------------------------------------------- /crates/floresta-wire/src/p2p_wire/tests/test_data/block_hashes.txt: -------------------------------------------------------------------------------- 1 | Below are signet block hashes of corresponding height acquired from the UTREEXOD node 2 | 3 | 0: 00000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6 4 | 1: 00000086d6b2636cb2a392d45edc4ec544a10024d30141c9adf4bfd9de533b53 5 | 2: 00000032bb881de703dcc968e8258080c7ed4a2933e3a35888fa0b2f75f36029 6 | 3: 000000e8daac2a2e973ecaab46dc948181c638adecf5ae0fd5d3e13aa14364b0 7 | 4: 00000194763f1233e40afd5f6eb2e64abd2e5e158a4139198a224f8a7503d47e 8 | 5: 000003776c984edd753b876f0e70d08450f92dd76a768df03d2c42d1880aea5b 9 | 6: 000001b62ff822d0b1c68e231670eba2cf8d494075f9616447194b370db6f3da 10 | 7: 000000b6f018c5b53240616c7868eab7e1338de4200ec25b65a5e1c2f3c57c45 11 | 8: 000002213230ed813e42076f753f666244db421b3e36a3a3ba31384feb61eba2 12 | 9: 000001e163d0a8e294c3406a713ba4363f422f1322a1d6ab6de4e54b51e868a8 13 | 14 | 60: 0000027c0a9ed0e6eb0d1e222d6194c949ba53ea182ac6169c87a78dda43da39 15 | 89: 0000007a4866286e7d703f40cef5a6930e9a153c3dbadf2bf3b854874922ca76 16 | 103: 00000292990f7bc6ed6cb9813df30adad6eb7f759951749ba1c732852b311650 17 | 110: 0000026eacaac74d43882aaf3e2db2fc05b00223ba1f094d631ba5a442485815 18 | 19 | 113: 000002a3d02ce9b14645a034c46b5dc7c45f25e1974ad08493f1785d41531a6c 20 | 114: 000001d28109851de61ebb8dc736fed4204e98057c7bcf6634dab9b009ad32ba 21 | 115: 000000213e8f68cb7440e7ea4776b03dd12dc9aba868fc2a05ce13a04d6b346e 22 | 116: 000002b9fb87a4b4d552386fe1568a1ba63f605b22dc08fb47c0d2b8299841e2 23 | 117: 0000015360df65dacb0da029f9cfaeb86e84ebdf9f3244d3c837d75c71531d73 24 | 118: 0000008e341933bf4930cf8af2422fd87abdf91ad5b5dcd147f4f9f44a115bb4 25 | 119: 0000035f0e5513b26bba7cead874fdf06241a934e4bc4cf7a0381c60e4cdd2bb 26 | -------------------------------------------------------------------------------- /crates/floresta-node/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "floresta-node" 3 | version = "0.8.0" 4 | edition = "2021" 5 | readme.workspace = true # Floresta/README.md 6 | 7 | [dependencies] 8 | axum = { workspace = true, optional = true } 9 | bitcoin = { workspace = true, features = ["serde", "std", "bitcoinconsensus"] } 10 | corepc-types = { workspace = true } 11 | dns-lookup = { workspace = true } 12 | miniscript = { workspace = true, features = ["std"] } 13 | rcgen = { workspace = true } 14 | rustreexo = { workspace = true } 15 | serde = { workspace = true } 16 | serde_json = { workspace = true } 17 | tokio = { workspace = true } 18 | tokio-rustls = { workspace = true } 19 | toml = { workspace = true } 20 | tower-http = { version = "0.6", optional = true, features = ["cors"] } 21 | tracing = { workspace = true } 22 | zmq = { version = "0.10", optional = true } 23 | 24 | # Local dependencies 25 | floresta-chain = { workspace = true } 26 | floresta-compact-filters = { workspace = true, optional = true } 27 | floresta-common = { workspace = true } 28 | floresta-electrum = { workspace = true } 29 | floresta-watch-only = { workspace = true } 30 | floresta-wire = { workspace = true } 31 | metrics = { workspace = true, optional = true } 32 | 33 | [dev-dependencies] 34 | pretty_assertions = "1.4" 35 | 36 | [target.'cfg(target_env = "gnu")'.dependencies] 37 | libc = "0.2" 38 | 39 | [target.'cfg(target_os = "macos")'.dependencies] 40 | libc = "0.2" 41 | 42 | [features] 43 | compact-filters = ["dep:floresta-compact-filters"] 44 | zmq-server = ["dep:zmq"] 45 | json-rpc = ["dep:axum", "dep:tower-http", "compact-filters"] 46 | default = ["json-rpc"] 47 | metrics = ["dep:metrics", "floresta-wire/metrics", "floresta-chain/metrics"] 48 | 49 | [lints] 50 | workspace = true 51 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "floresta-fuzz" 3 | version = "0.1.0" 4 | publish = false 5 | edition = "2021" 6 | 7 | [package.metadata] 8 | cargo-fuzz = true 9 | 10 | [dependencies] 11 | arbitrary = { version = "1.4", features = ["derive"] } 12 | bitcoin = { workspace = true } 13 | libfuzzer-sys = "0.4" 14 | tempfile = { workspace = true } 15 | 16 | # Local dependencies 17 | floresta-chain = { workspace = true, features = ["flat-chainstore"] } 18 | floresta-wire = { workspace = true } 19 | 20 | [[bin]] 21 | name = "local_address_str" 22 | path = "fuzz_targets/local_address_str.rs" 23 | test = false 24 | doc = false 25 | bench = false 26 | 27 | [[bin]] 28 | name = "utreexo_proof_des" 29 | path = "fuzz_targets/utreexo_proof_des.rs" 30 | test = false 31 | doc = false 32 | bench = false 33 | 34 | [[bin]] 35 | name = "addrman" 36 | path = "fuzz_targets/addrman.rs" 37 | test = false 38 | doc = false 39 | bench = false 40 | 41 | [[bin]] 42 | name = "best_chain_decode" 43 | path = "fuzz_targets/best_chain_decode.rs" 44 | test = false 45 | doc = false 46 | bench = false 47 | 48 | [[bin]] 49 | name = "flat_chainstore_header_insertion" 50 | path = "fuzz_targets/flat_chainstore_header_insertion.rs" 51 | test = false 52 | doc = false 53 | bench = false 54 | 55 | [[bin]] 56 | name = "best_chain_roundtrip" 57 | path = "fuzz_targets/best_chain_roundtrip.rs" 58 | test = false 59 | doc = false 60 | bench = false 61 | 62 | [[bin]] 63 | name = "disk_block_header_decode" 64 | path = "fuzz_targets/disk_block_header_decode.rs" 65 | test = false 66 | doc = false 67 | bench = false 68 | 69 | [[bin]] 70 | name = "disk_block_header_roundtrip" 71 | path = "fuzz_targets/disk_block_header_roundtrip.rs" 72 | test = false 73 | doc = false 74 | bench = false 75 | 76 | [lints] 77 | workspace = true 78 | -------------------------------------------------------------------------------- /tests/florestad/tls.py: -------------------------------------------------------------------------------- 1 | """ 2 | florestad/tls-test.py 3 | 4 | This functional test tests the proper creation of a TLS port on florestad. 5 | """ 6 | 7 | from test_framework import FlorestaTestFramework 8 | from test_framework.electrum.client import ElectrumClient 9 | 10 | 11 | class TestSslInitialization(FlorestaTestFramework): 12 | """ 13 | Test the initialization of florestad with --tls-key-path and --tls-cert-path and 14 | a request from Electrum client to TLS port and its success. 15 | """ 16 | 17 | electrum = None 18 | 19 | def set_test_params(self): 20 | """ 21 | Setup a single node and a electrum client at port 20002 22 | """ 23 | self.florestad = self.add_node(variant="florestad", tls=True) 24 | 25 | def run_test(self): 26 | """ 27 | Run the TLS node, create a electrum client that will try to connect to port 20002. 28 | Send a ping to make sure everything is working. 29 | """ 30 | self.run_node(self.florestad) 31 | 32 | # now create a connection with an electrum client at default port 33 | TestSslInitialization.electrum = ElectrumClient( 34 | self.florestad.get_host(), 35 | self.florestad.get_port("electrum-server-tls"), 36 | tls=True, 37 | ) 38 | 39 | # request something to TLS port 40 | res = TestSslInitialization.electrum.ping() 41 | result = res["result"] 42 | id = res["id"] 43 | jsonrpc = res["jsonrpc"] 44 | 45 | # if pinged, we should get a "null" in response 46 | self.assertIsNone(result) 47 | self.assertEqual(id, 0) 48 | self.assertEqual(jsonrpc, "2.0") 49 | 50 | 51 | if __name__ == "__main__": 52 | TestSslInitialization().main() 53 | -------------------------------------------------------------------------------- /bin/florestad/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "florestad" 3 | version = "0.8.0" 4 | edition = "2021" 5 | license = "MIT" 6 | description = """ 7 | The main binary for Floresta, a lightweight Bitcoin node implementation in Rust, 8 | utilizing Utreexo for efficient state management. 9 | 10 | This binary is a cli utility to run the full node, and interact with it via RPC. You may also 11 | run this binary as a daemon on unix systems. There's a couple of features and command-line 12 | options to customize the node to your needs. Check out the README.md and the documentation 13 | for more information on how to use and configure Florestad. 14 | """ 15 | repository = "https://github.com/vinteumorg/Floresta" 16 | readme.workspace = true # Floresta/README.md 17 | keywords = ["bitcoin", "utreexo", "node", "blockchain", "rust"] 18 | categories = ["cryptography::cryptocurrencies", "command-line-utilities"] 19 | 20 | [dependencies] 21 | bitcoin = { workspace = true } 22 | clap = { workspace = true } 23 | console-subscriber = { workspace = true, optional = true } 24 | dirs = "4.0" 25 | tokio = { workspace = true } 26 | tracing = { workspace = true } 27 | tracing-appender = "0.2" 28 | tracing-subscriber = { version = "0.3", features = ["chrono", "ansi", "env-filter"] } 29 | 30 | # Local dependencies 31 | floresta-node = { workspace = true } 32 | 33 | [target.'cfg(unix)'.dependencies] 34 | daemonize = { version = "0.5.0" } 35 | 36 | [build-dependencies] 37 | toml = { workspace = true } 38 | 39 | [features] 40 | default = ["json-rpc"] 41 | compact-filters = ["floresta-node/compact-filters"] 42 | zmq-server = ["floresta-node/zmq-server"] 43 | json-rpc = ["floresta-node/json-rpc", "compact-filters"] 44 | metrics = ["floresta-node/metrics"] 45 | tokio-console = ["dep:console-subscriber"] 46 | 47 | [lints] 48 | workspace = true 49 | -------------------------------------------------------------------------------- /tests/florestad/tls-fail.py: -------------------------------------------------------------------------------- 1 | """ 2 | florestad/tls-fail-test.py 3 | 4 | This functional test checks the failure on connect to florestad's TLS port. 5 | """ 6 | 7 | import errno 8 | 9 | from test_framework import FlorestaTestFramework 10 | from test_framework.electrum.client import ElectrumClient 11 | 12 | 13 | class TestSslFailInitialization(FlorestaTestFramework): 14 | """ 15 | Test that a request to the TLS Electrum port will fail if we don't enable it 16 | with `--enable-electrum-tls` and (`--generate-cert` or (`--tls-key-path` and `tls-cert-path`)). 17 | """ 18 | 19 | electrum = None 20 | 21 | def set_test_params(self): 22 | """ 23 | Instantiate the node without Electrum TLS. 24 | """ 25 | self.florestad = self.add_node(variant="florestad", tls=False) 26 | 27 | def run_test(self): 28 | """ 29 | Run the node, create an Electrum client that will try to connect to 30 | the TLS port (20002), and assert that the connection was refused since TLS was not enabled. 31 | """ 32 | self.run_node(self.florestad) 33 | 34 | # Create a connection with an Electrum client at the default Electrum TLS port. 35 | # It must fail since there is nothing bound to it. 36 | with self.assertRaises(ConnectionRefusedError) as exc: 37 | self.log("Trying to connect the Electrum no-TLS client") 38 | TestSslFailInitialization.electrum = ElectrumClient( 39 | self.florestad.get_host(), 40 | self.florestad.get_port("electrum-server") + 1, 41 | ) 42 | 43 | self.log("Failed to connect to Electrum TLS client") 44 | self.assertIsSome(exc.exception) 45 | self.assertEqual(exc.exception.errno, errno.ECONNREFUSED) 46 | 47 | 48 | if __name__ == "__main__": 49 | TestSslFailInitialization().main() 50 | -------------------------------------------------------------------------------- /doc/build-macos.md: -------------------------------------------------------------------------------- 1 | # Building on macOS 2 | The following steps should be executed in a Terminal application. Tip: press `Command (⌘) + Space` and search for `terminal`. 3 | 4 | ## Prerequisites 5 | ### 1. Xcode Command Line Tools 6 | 7 | To install, run the following command from your terminal: 8 | 9 | ``` bash 10 | xcode-select --install 11 | ``` 12 | 13 | Upon running the command, you should see a popup appear. 14 | Click on `Install` to continue the installation process. 15 | 16 | ### 2. Homebrew Package Manager 17 | 18 | Homebrew is a package manager for macOS that allows one to install packages from the command line easily. You can use the package manager of your preference. 19 | 20 | To install the Homebrew package manager, see: https://brew.sh 21 | 22 | Note: If you run into issues while installing Homebrew or pulling packages, refer to [Homebrew's troubleshooting page](https://docs.brew.sh/Troubleshooting). 23 | 24 | ### 3. Install Required Dependencies 25 | 26 | On the Terminal, using Homebrew, run the following: 27 | ```bash 28 | brew update 29 | brew install gcc pkg-config 30 | ``` 31 | You'll need Rust and Cargo, refer to [this](https://www.rust-lang.org/) for more details. Minimum support version is rustc 1.74 and newer. 32 | 33 | ## Building 34 | 35 | Once you have Cargo, clone the repository with: 36 | 37 | ```bash 38 | git clone https://github.com/vinteumorg/Floresta.git 39 | ``` 40 | 41 | go to the Floresta directory 42 | 43 | ```bash 44 | cd Floresta/ 45 | ``` 46 | 47 | and build with cargo build 48 | 49 | ```bash 50 | cargo build --release 51 | 52 | # Alternatively, you can add florestad and floresta-cli to the path with 53 | cargo install --path ./bin/florestad --locked 54 | cargo install --path ./bin/floresta-cli --locked 55 | ``` 56 | 57 | If you are using Nix, you can get floresta packages into your system following the instructions [here](nix.md). 58 | -------------------------------------------------------------------------------- /tests/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Sets a temporary environment to run our tests 4 | # 5 | # This script should be executed after prepare.sh for running our functional test. 6 | # 7 | ## What this script do ? 8 | # 9 | # 1. Sets $PATH to include the compiled florestad and utreexod at FLORESTA_TEMP_DIR/binaries. 10 | # 11 | # 2. Run all needed commands for batch executing all python tests suites: 12 | # 13 | # uv run tests/run_tests.py 14 | set -e 15 | 16 | if [[ -z "$FLORESTA_TEMP_DIR" ]]; then 17 | 18 | # This helps us to keep track of the actual version being tested without conflicting with any already installed binaries. 19 | HEAD_COMMIT_HASH=$(git rev-parse HEAD) 20 | 21 | # This helps us to keep track of the actual version being tested without conflicting with any already installed binaries. 22 | GIT_DESCRIBE=$(git describe --tags --always) 23 | 24 | # Since its deterministic how we make the setup, we already know where to search for the binaries to be testing. 25 | export FLORESTA_TEMP_DIR="/tmp/floresta-func-tests.${GIT_DESCRIBE}" 26 | 27 | fi 28 | 29 | # Clean existing data/logs directories before running the tests 30 | rm -rf "$FLORESTA_TEMP_DIR/data" 31 | 32 | # Detect if --preserve-data-dir is among args 33 | # and forward args to uv 34 | PRESERVE_DATA=false 35 | UV_ARGS=() 36 | 37 | for arg in "$@"; do 38 | if [[ "$arg" == "--preserve-data-dir" ]]; then 39 | PRESERVE_DATA=true 40 | else 41 | UV_ARGS+=("$arg") 42 | fi 43 | done 44 | 45 | # Run the re-freshed tests 46 | uv run ./tests/test_runner.py "${UV_ARGS[@]}" 47 | 48 | # Clean up the data dir if we succeeded and --preserve-data-dir was not passed 49 | if [ $? -eq 0 ] && [ "$PRESERVE_DATA" = false ]; 50 | then 51 | echo "Tests passed, cleaning up the data dir at $FLORESTA_TEMP_DIR" 52 | rm -rf $FLORESTA_TEMP_DIR/data $FLORESTA_TEMP_DIR/logs 53 | fi 54 | -------------------------------------------------------------------------------- /.github/workflows/functional.yml: -------------------------------------------------------------------------------- 1 | name: Functional Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | functional: 9 | name: Functional 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | # see more at 16 | # https://docs.astral.sh/uv/guides/integration/github/ 17 | - name: Install uv 18 | uses: astral-sh/setup-uv@v5 19 | with: 20 | python-version: "3.12" 21 | enable-cache: true 22 | cache-dependency-glob: "uv.lock" 23 | 24 | - name: Prepare environment 25 | run: uv sync --all-extras --dev 26 | 27 | - name: Run black formatting 28 | run: uv run black --check --verbose ./tests 29 | 30 | - name: Run pylint linter 31 | run: uv run pylint --verbose ./tests 32 | 33 | # These are the minimal deps defined by bitcoin core at 34 | # https://github.com/bitcoin/bitcoin/blob/master/doc/build-unix.md 35 | - name: Prepare bitcoin-core deps 36 | run: sudo apt install build-essential cmake pkgconf python3 libevent-dev libboost-dev 37 | 38 | - name: Cache Rust 39 | uses: Swatinem/rust-cache@v2 40 | 41 | - name: Prepare functional tests tasks 42 | run: | 43 | tests/prepare.sh 44 | 45 | - name: Run functional tests tasks 46 | run: | 47 | tests/run.sh 48 | 49 | - name: Log tests on failure 50 | if: failure() 51 | run: | 52 | logs=() 53 | while IFS= read -r line; do 54 | logs+=("$line") 55 | done < <(find /tmp/floresta-func-tests* -type f -path "*/logs/*.log" 2>/dev/null || true) 56 | 57 | for logfile in "${logs[@]}"; do 58 | echo "::group::$logfile" 59 | cat "$logfile" || echo "Failed to read $logfile" 60 | echo "::endgroup::" 61 | done 62 | -------------------------------------------------------------------------------- /tests/floresta-cli/stop.py: -------------------------------------------------------------------------------- 1 | """ 2 | floresta_cli_stop.py 3 | 4 | This functional test cli utility to interact with a Floresta node with `stop` 5 | """ 6 | 7 | import re 8 | from test_framework import FlorestaTestFramework 9 | 10 | 11 | class StopTest(FlorestaTestFramework): 12 | """ 13 | Test `stop` command with a fresh node and its initial state. 14 | """ 15 | 16 | def set_test_params(self): 17 | """ 18 | Setup a single node 19 | """ 20 | self.florestad = self.add_node(variant="florestad") 21 | self.bitcoind = self.add_node(variant="bitcoind") 22 | 23 | def run_test(self): 24 | """ 25 | Run JSONRPC stop command on both flrestad and bitcoin core nodes and 26 | check if floresta and bitcoin core nodes are stopped correctly and if 27 | the floresta's stop message is compliant with bitcoin core's stop message. 28 | """ 29 | self.run_node(self.florestad) 30 | self.run_node(self.bitcoind) 31 | 32 | # Generally, the self.stop_node() method 33 | # do all the work for us, but in this case 34 | # we're testing the method rpc.stop(), so 35 | # re-do all the steps to ensure that it 36 | # was successful and the ports are closed 37 | result_floresta = self.florestad.rpc.stop() 38 | result_bitcoin = self.bitcoind.rpc.stop() 39 | 40 | # Check if the messages are correct 41 | for res in [result_floresta, result_bitcoin]: 42 | self.assertIsSome(res) 43 | self.assertIn("stopping", res) 44 | self.assertMatch(res, re.compile(r"^(Floresta|Bitcoin Core) stopping$")) 45 | 46 | # Check that the node is stopped 47 | self.florestad.rpc.wait_for_connections(opened=False) 48 | self.bitcoind.rpc.wait_for_connections(opened=False) 49 | 50 | 51 | if __name__ == "__main__": 52 | StopTest().main() 53 | -------------------------------------------------------------------------------- /contrib/init/floresta.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Floresta: A Lightweight Utreexo-powered Bitcoin full node implementation 3 | Documentation=https://github.com/vinteumorg/Floresta 4 | After=network-online.target time-set.target 5 | Wants=network-online.target 6 | 7 | 8 | [Service] 9 | ExecStart=/usr/bin/florestad --daemon --data-dir=/var/lib/florestad --config-file /etc/florestad/config.toml --pid-file=/run/florestad/florestad.pid --log-to-file 10 | 11 | # Ensure that the service is ready after the MainPID exists 12 | Type=forking 13 | PIDFile=/run/florestad/florestad.pid 14 | 15 | # Don't enter a restart loop, as it might corrupt our database 16 | Restart=no 17 | 18 | TimeoutStartSec=infinity 19 | TimeoutStopSec=600 20 | 21 | # Make sure we can read from the config file 22 | ExecStartPre=/bin/chgrp florestad /etc/florestad 23 | User=florestad 24 | Group=florestad 25 | 26 | # /run/florestad 27 | RuntimeDirectory=florestad 28 | RuntimeDirectoryMode=0710 29 | 30 | # /etc/florestad 31 | ConfigurationDirectory=florestad 32 | ConfigurationDirectoryMode=0710 33 | 34 | # /var/lib/florestad 35 | StateDirectory=florestad 36 | StateDirectoryMode=0710 37 | 38 | # Provide a private /tmp and /var/tmp. 39 | PrivateTmp=true 40 | 41 | # Mount /usr, /boot/ and /etc read-only for the process. 42 | ProtectSystem=full 43 | 44 | # Deny access to /home, /root and /run/user 45 | ProtectHome=true 46 | 47 | # Disallow the process and all of its children to gain 48 | # new privileges through execve(). 49 | NoNewPrivileges=true 50 | 51 | # Use a new /dev namespace only populated with API pseudo devices 52 | # such as /dev/null, /dev/zero and /dev/random. 53 | PrivateDevices=true 54 | 55 | # Deny the creation of writable and executable memory mappings. 56 | MemoryDenyWriteExecute=true 57 | 58 | # Restrict ABIs to help ensure MemoryDenyWriteExecute is enforced 59 | SystemCallArchitectures=native 60 | 61 | [Install] 62 | WantedBy=multi-user.target 63 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/best_chain_roundtrip.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use std::io::Cursor; 4 | 5 | use arbitrary::Arbitrary; 6 | use arbitrary::Unstructured; 7 | use bitcoin::consensus::Decodable; 8 | use bitcoin::consensus::Encodable; 9 | use bitcoin::hashes::Hash; 10 | use bitcoin::BlockHash; 11 | use floresta_chain::BestChain; 12 | use libfuzzer_sys::fuzz_target; 13 | 14 | fn gen_blockhash(u: &mut Unstructured<'_>) -> arbitrary::Result { 15 | let bytes: [u8; 32] = Arbitrary::arbitrary(u)?; 16 | Ok(BlockHash::from_byte_array(bytes)) 17 | } 18 | 19 | const MAX_TIPS: usize = 64; 20 | 21 | fn gen_alt_tips(u: &mut Unstructured<'_>) -> arbitrary::Result> { 22 | let hint: u8 = Arbitrary::arbitrary(u)?; 23 | let len = (hint as usize) % (MAX_TIPS + 1); 24 | let mut v = Vec::with_capacity(len); 25 | for _ in 0..len { 26 | v.push(gen_blockhash(u)?); 27 | } 28 | Ok(v) 29 | } 30 | 31 | #[derive(Arbitrary)] 32 | struct Inputs { 33 | #[arbitrary(with = gen_blockhash)] 34 | best_block: BlockHash, 35 | depth: u32, 36 | #[arbitrary(with = gen_blockhash)] 37 | validation_index: BlockHash, 38 | #[arbitrary(with = gen_alt_tips)] 39 | alternative_tips: Vec, 40 | } 41 | 42 | fuzz_target!(|data: &[u8]| { 43 | if let Ok(inp) = Inputs::arbitrary(&mut Unstructured::new(data)) { 44 | let best = BestChain { 45 | best_block: inp.best_block, 46 | depth: inp.depth, 47 | validation_index: inp.validation_index, 48 | alternative_tips: inp.alternative_tips, 49 | }; 50 | 51 | // Encode 52 | let mut buf = Vec::new(); 53 | let written = best.consensus_encode(&mut buf).expect("encode failed"); 54 | assert_eq!(written, buf.len(), "encode returned wrong length"); 55 | 56 | // Decode and compare 57 | let decoded = BestChain::consensus_decode(&mut Cursor::new(&buf)).expect("decode failed"); 58 | assert_eq!(decoded, best, "roundtrip mismatch"); 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /crates/floresta-compact-filters/src/network_filters.rs: -------------------------------------------------------------------------------- 1 | use bitcoin::bip158::BlockFilter; 2 | use bitcoin::BlockHash; 3 | use floresta_chain::pruned_utreexo::BlockchainInterface; 4 | 5 | use crate::IterableFilterStore; 6 | use crate::IterableFilterStoreError; 7 | 8 | #[derive(Debug)] 9 | pub struct NetworkFilters { 10 | filters: Storage, 11 | } 12 | 13 | impl NetworkFilters { 14 | pub fn new(filters: Storage) -> Self { 15 | if filters.get_height().is_err() { 16 | filters.set_height(0).unwrap(); 17 | } 18 | 19 | Self { filters } 20 | } 21 | 22 | pub fn match_any( 23 | &self, 24 | query: Vec<&[u8]>, 25 | start_height: Option, 26 | stop_height: Option, 27 | chain: impl BlockchainInterface, 28 | ) -> Result, IterableFilterStoreError> { 29 | let mut blocks = Vec::new(); 30 | let iter = query.into_iter(); 31 | 32 | let start_height = start_height.map(|n| n as usize); 33 | 34 | for (height, filter) in self.filters.iter(start_height)? { 35 | let hash = chain.get_block_hash(height).unwrap(); 36 | 37 | if filter.match_any(&hash, &mut iter.clone()).unwrap() { 38 | blocks.push(hash); 39 | } 40 | 41 | if let Some(stop_at) = stop_height { 42 | if height >= stop_at { 43 | break; 44 | }; 45 | } 46 | } 47 | Ok(blocks) 48 | } 49 | 50 | pub fn push_filter( 51 | &self, 52 | filter: BlockFilter, 53 | height: u32, 54 | ) -> Result<(), IterableFilterStoreError> { 55 | self.filters.put_filter(filter, height) 56 | } 57 | 58 | pub fn get_height(&self) -> Result { 59 | self.filters.get_height() 60 | } 61 | 62 | pub fn save_height(&self, height: u32) -> Result<(), IterableFilterStoreError> { 63 | self.filters.set_height(height) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /crates/floresta-chain/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "floresta-chain" 3 | version = "0.4.0" 4 | edition = "2021" 5 | authors = ["Davidson Souza "] 6 | description = """ 7 | Reusable components for building consensus-critical Bitcoin applications. 8 | Using floresta-chain, you can create a Bitcoin node that validates blocks 9 | and transactions, according to the Bitcoin consensus rules. 10 | """ 11 | repository = "https://github.com/vinteumorg/Floresta" 12 | license = "MIT" 13 | readme.workspace = true # Floresta/README.md 14 | keywords = ["bitcoin", "utreexo", "node", "consensus"] 15 | categories = ["cryptography::cryptocurrencies", "database"] 16 | 17 | [lib] 18 | crate-type = ["cdylib", "rlib"] 19 | 20 | [dependencies] 21 | bitcoin = { workspace = true, features = [ "serde" ] } 22 | bitcoinconsensus = { version = "0.106", optional = true, default-features = false } 23 | lru = { version = "0.16", optional = true } 24 | memmap2 = { version = "0.9", optional = true } 25 | rustreexo = { workspace = true } 26 | serde = { workspace = true, optional = true } 27 | sha2 = { workspace = true, features = ["std"] } 28 | spin = { workspace = true } 29 | tracing = { workspace = true } 30 | twox-hash = { version = "2.1", default-features = false, features = ["xxhash3_64", "std"], optional = true } 31 | 32 | # Local dependencies 33 | floresta-common = { workspace = true, features = ["std"] } 34 | metrics = { workspace = true, optional = true } 35 | 36 | [dev-dependencies] 37 | criterion = "0.7" 38 | hex = { workspace = true } 39 | rand = { workspace = true } 40 | serde = { workspace = true } 41 | serde_json = { workspace = true } 42 | tempfile = { workspace = true } 43 | zstd = { workspace = true } 44 | 45 | [features] 46 | default = [] 47 | bitcoinconsensus = ["bitcoin/bitcoinconsensus", "dep:bitcoinconsensus"] 48 | metrics = ["dep:metrics"] 49 | test-utils = ["dep:serde"] 50 | flat-chainstore = ["dep:memmap2", "dep:lru", "dep:twox-hash"] 51 | 52 | [[bench]] 53 | name = "chain_state_bench" 54 | harness = false 55 | required-features = ["flat-chainstore", "test-utils"] 56 | 57 | [lints] 58 | workspace = true 59 | -------------------------------------------------------------------------------- /crates/floresta-chain/testdata/bitcoin-core/data/base58_encode_decode.json: -------------------------------------------------------------------------------- 1 | [ 2 | ["", ""], 3 | ["61", "2g"], 4 | ["626262", "a3gV"], 5 | ["636363", "aPEr"], 6 | ["73696d706c792061206c6f6e6720737472696e67", "2cFupjhnEsSn59qHXstmK2ffpLv2"], 7 | ["00eb15231dfceb60925886b67d065299925915aeb172c06647", "1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L"], 8 | ["516b6fcd0f", "ABnLTmg"], 9 | ["bf4f89001e670274dd", "3SEo3LWLoPntC"], 10 | ["572e4794", "3EFU7m"], 11 | ["ecac89cad93923c02321", "EJDM8drfXA6uyA"], 12 | ["10c8511e", "Rt5zm"], 13 | ["00000000000000000000", "1111111111"], 14 | ["00000000000000000000000000000000000000000000000000000000000000000000000000000000", "1111111111111111111111111111111111111111"], 15 | ["00000000000000000000000000000000000000000000000000000000000000000000000000000001", "1111111111111111111111111111111111111112"], 16 | ["0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ec39d04c37e71e5d591881f6", "111111111111111111111111111111111111111111111111111111111111111111111111111111111111115TYzLYH1udmLdzCLM"], 17 | ["000111d38e5fc9071ffcd20b4a763cc9ae4f252bb4e48fd66a835e252ada93ff480d6dd43dc62a641155a5", "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"], 18 | ["000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", "1cWB5HCBdLjAuqGGReWE3R3CguuwSjw6RHn39s2yuDRTS5NsBgNiFpWgAnEx6VQi8csexkgYw3mdYrMHr8x9i7aEwP8kZ7vccXWqKDvGv3u1GxFKPuAkn8JCPPGDMf3vMMnbzm6Nh9zh1gcNsMvH3ZNLmP5fSG6DGbbi2tuwMWPthr4boWwCxf7ewSgNQeacyozhKDDQQ1qL5fQFUW52QKUZDZ5fw3KXNQJMcNTcaB723LchjeKun7MuGW5qyCBZYzA1KjofN1gYBV3NqyhQJ3Ns746GNuf9N2pQPmHz4xpnSrrfCvy6TVVz5d4PdrjeshsWQwpZsZGzvbdAdN8MKV5QsBDY"], 19 | ["271F359E", "zzzzy"], 20 | ["271F359F", "zzzzz"], 21 | ["271F35A0", "211111"], 22 | ["271F35A1", "211112"] 23 | ] 24 | -------------------------------------------------------------------------------- /contrib/feature_matrix.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Exit immediately if any command fails 3 | set -e 4 | 5 | # The first argument specifies the action: "clippy" or "test". 6 | action="${1:-}" 7 | if [ "$action" != "clippy" ] && [ "$action" != "test" ]; then 8 | echo "Usage: $0 {clippy|test} [cargo_arg]" >&2 9 | exit 1 10 | fi 11 | 12 | # The second argument is passed to cargo (e.g., "-- -D warnings"); defaults to empty. 13 | cargo_arg="${2:-}" 14 | 15 | crates="\ 16 | floresta \ 17 | floresta-chain \ 18 | floresta-common \ 19 | floresta-compact-filters \ 20 | floresta-electrum \ 21 | floresta-node \ 22 | floresta-rpc \ 23 | floresta-watch-only \ 24 | floresta-wire \ 25 | florestad \ 26 | floresta-cli \ 27 | fuzz \ 28 | metrics" 29 | 30 | for crate in $crates; do 31 | # Determine the path to the crate 32 | if [ "$crate" = "fuzz" ] || [ "$crate" = "metrics" ]; then 33 | path="$crate" 34 | elif [ "$crate" = "florestad" ] || [ "$crate" = "floresta-cli" ]; then 35 | path="bin/$crate" 36 | else 37 | path="crates/$crate" 38 | fi 39 | 40 | # The default feature, if not used to conditionally compile code, can be skipped as the combinations already 41 | # include that case (see https://github.com/taiki-e/cargo-hack/issues/155#issuecomment-2474330839) 42 | if [ "$crate" = "floresta-compact-filters" ] || \ 43 | [ "$crate" = "floresta-electrum" ] || \ 44 | [ "$crate" = "fuzz" ] || \ 45 | [ "$crate" = "metrics" ]; then 46 | # These crates don't have a default feature 47 | skip_default="" 48 | else 49 | skip_default="--skip default" 50 | fi 51 | 52 | # Navigate to the crate's directory 53 | cd "$path" || exit 1 54 | printf "\033[1;35mRunning cargo %s for all feature combinations in %s...\033[0m\n" "$action" "$crate" 55 | 56 | if [ "$action" = "clippy" ]; then 57 | # shellcheck disable=SC2086 58 | cargo +nightly hack clippy --all-targets --feature-powerset $skip_default $cargo_arg 59 | elif [ "$action" = "test" ]; then 60 | # shellcheck disable=SC2086 61 | cargo hack test --release --feature-powerset $skip_default -v $cargo_arg 62 | fi 63 | 64 | cd - > /dev/null || exit 1 65 | done 66 | -------------------------------------------------------------------------------- /crates/floresta-wire/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | //! # Floresta Wire 4 | //! This crate provides the core networking logic for a full node using libfloresta, 5 | //! including the P2P network and the mempool. You can easily integrate it with any 6 | //! other crate that provides a `BlockchainInterface` and `UpdatableChainstate` 7 | //! implementation. 8 | //! 9 | //! A node also gives you a `handle` that you can use to send messages to the node, 10 | //! like requesting blocks, mempool transactions or asking to connect with a given 11 | //! peer. 12 | 13 | #![cfg_attr(docsrs, feature(doc_cfg))] 14 | 15 | use bitcoin::block::Header as BlockHeader; 16 | use bitcoin::Block; 17 | use bitcoin::Transaction; 18 | pub use rustreexo; 19 | #[cfg(not(target_arch = "wasm32"))] 20 | mod p2p_wire; 21 | #[cfg(not(target_arch = "wasm32"))] 22 | pub use p2p_wire::address_man; 23 | #[cfg(not(target_arch = "wasm32"))] 24 | pub use p2p_wire::block_proof; 25 | #[cfg(not(target_arch = "wasm32"))] 26 | pub use p2p_wire::chain_selector; 27 | #[cfg(not(target_arch = "wasm32"))] 28 | pub use p2p_wire::mempool; 29 | #[cfg(not(target_arch = "wasm32"))] 30 | pub use p2p_wire::node; 31 | #[cfg(not(target_arch = "wasm32"))] 32 | pub use p2p_wire::node_context; 33 | #[cfg(not(target_arch = "wasm32"))] 34 | pub use p2p_wire::node_interface; 35 | #[cfg(not(target_arch = "wasm32"))] 36 | pub use p2p_wire::running_node; 37 | #[cfg(not(target_arch = "wasm32"))] 38 | pub use p2p_wire::sync_node; 39 | pub use p2p_wire::transport::TransportProtocol; 40 | pub use p2p_wire::UtreexoNodeConfig; 41 | 42 | /// NodeHooks is a trait that defines the hooks that a node can use to interact with the network 43 | /// and the blockchain. Every time an event happens, the node will call the corresponding hook. 44 | pub trait NodeHooks { 45 | /// We've received a new block 46 | fn on_block_received(&mut self, block: &Block); 47 | /// We've received a new transaction 48 | fn on_transaction_received(&mut self, transaction: &Transaction); 49 | /// We've received a new peer 50 | fn on_peer_connected(&mut self, peer: &u32); 51 | /// We've lost a peer 52 | fn on_peer_disconnected(&mut self, peer: &u32); 53 | /// We've received a new header 54 | fn on_header_received(&mut self, header: &BlockHeader); 55 | } 56 | -------------------------------------------------------------------------------- /bin/floresta-cli/src/parsers.rs: -------------------------------------------------------------------------------- 1 | use std::any::type_name; 2 | use std::error::Error; 3 | use std::fmt::Display; 4 | use std::str::FromStr; 5 | 6 | #[derive(Debug)] 7 | /// Collection of errors to deal with parsing. 8 | pub enum ParseError { 9 | /// Returned when the user inserts an broken array 10 | InvalidArray, 11 | 12 | /// Returned when the consumer of tries to cast into 13 | /// a incompatible type. 14 | InvalidTarget(String), 15 | } 16 | 17 | impl Display for ParseError { 18 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 19 | match self { 20 | ParseError::InvalidArray => write!( 21 | f, 22 | "Couldnt parse the inserted as an Array, please refer to the docs" 23 | ), 24 | ParseError::InvalidTarget(target) => { 25 | write!(f, "Could parse items to {target}") 26 | } 27 | } 28 | } 29 | } 30 | 31 | impl Error for ParseError {} 32 | 33 | /// Tries to parse a json array, you can insert a type to be casted on each item. 34 | /// 35 | /// Example: '["4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"]' 36 | /// Tries to parse a JSON array of hash strings into a vector of the target type. 37 | /// 38 | /// The target type must implement Deserialize and can be converted from a hash string. 39 | /// By default, it will parse into Hash256, but you can specify any other compatible type. 40 | /// 41 | /// Example: 42 | /// ``` 43 | /// # use bitcoin::hashes::sha256; 44 | /// # use floresta_cli::parsers::parse_json_array; 45 | /// let hashes: Vec = 46 | /// parse_json_array(r#"["4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"]"#) 47 | /// .unwrap(); 48 | /// ``` 49 | pub fn parse_json_array(s: &str) -> Result, ParseError> 50 | where 51 | Target: FromStr, 52 | { 53 | let string_vec: Vec = serde_json::from_str(s).map_err(|_| ParseError::InvalidArray)?; 54 | 55 | string_vec 56 | .into_iter() 57 | .map(|s| { 58 | Target::from_str(&s) 59 | .map_err(|_| ParseError::InvalidTarget(type_name::().to_string())) 60 | }) 61 | .collect() 62 | } 63 | -------------------------------------------------------------------------------- /tests/example/bitcoin.py: -------------------------------------------------------------------------------- 1 | """ 2 | bitcoin-test.py 3 | 4 | This is an example of how a tests with bitcoin should look like, 5 | see `tests/test_framework/test_framework.py` for more info. 6 | """ 7 | 8 | from test_framework import FlorestaTestFramework 9 | 10 | 11 | class BitcoindTest(FlorestaTestFramework): 12 | """ 13 | Tests should be a child class from FlorestaTestFramework 14 | 15 | In each test class definition, `set_test_params` and `run_test`, say what 16 | the test do and the expected result in the docstrings 17 | """ 18 | 19 | expected_chain = "regtest" 20 | expected_height = 0 21 | expected_headers = 0 22 | expected_blockhash = ( 23 | "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206" 24 | ) 25 | expected_difficulty = 1 26 | 27 | def set_test_params(self): 28 | """ 29 | Here we define setup for test adding a node definition 30 | """ 31 | self.bitcoind = self.add_node(variant="bitcoind") 32 | 33 | # All tests should override the run_test method 34 | def run_test(self): 35 | """ 36 | Here we define the test itself: 37 | 38 | - creates a dummy rpc listening on default port 39 | - perform some requests to BitcoinRPC node 40 | - if any assertion fails, all nodes will be stopped 41 | - if no error occurs, all nodes will be stopped at the end 42 | """ 43 | # Start a new node (the bitcoind's binary). 44 | # This method start a defined daemon, 45 | # in this case, `bitcoind`, and wait for 46 | # all ports opened by it, including the 47 | # RPC port to be available 48 | self.run_node(self.bitcoind) 49 | 50 | # Once the node is running, we can create 51 | # a request to the RPC server. In this case, we 52 | # call it node, but in truth, will be a RPC request 53 | # to perform some kind of action 54 | response = self.bitcoind.rpc.get_blockchain_info() 55 | 56 | self.assertEqual(response["chain"], BitcoindTest.expected_chain) 57 | self.assertEqual(response["bestblockhash"], BitcoindTest.expected_blockhash) 58 | self.assertTrue(response["difficulty"] > 0) 59 | 60 | self.stop() 61 | 62 | 63 | if __name__ == "__main__": 64 | BitcoindTest().main() 65 | -------------------------------------------------------------------------------- /tests/floresta-cli/uptime.py: -------------------------------------------------------------------------------- 1 | """ 2 | floresta_cli_uptime.py 3 | 4 | This functional test cli utility to interact with a Floresta node with `uptime` 5 | """ 6 | 7 | import time 8 | from test_framework import FlorestaTestFramework, Node 9 | 10 | DATA_DIR = FlorestaTestFramework.get_integration_test_dir() 11 | 12 | 13 | class UptimeTest(FlorestaTestFramework): 14 | """ 15 | Test `uptime` rpc call, by creating a node, wait for 16 | some time (10 seconds) and assert that this wait time is 17 | equal to how long florestad has been running 18 | """ 19 | 20 | def set_test_params(self): 21 | """ 22 | Setup the two node florestad process with different data-dirs, electrum-addresses 23 | and rpc-addresses in the same regtest network 24 | """ 25 | data_dirs = UptimeTest.create_data_dirs( 26 | DATA_DIR, self.__class__.__name__.lower(), nodes=2 27 | ) 28 | 29 | self.florestad = self.add_node( 30 | variant="florestad", 31 | extra_args=[ 32 | f"--data-dir={data_dirs[0]}", 33 | ], 34 | ) 35 | 36 | self.bitcoind = self.add_node( 37 | variant="bitcoind", 38 | extra_args=[ 39 | f"-datadir={data_dirs[1]}", 40 | ], 41 | ) 42 | 43 | def test_node_uptime(self, node: Node, test_time: int, margin: int): 44 | """ 45 | Test the uptime of a node, given an index 46 | by checking if the uptime matches the elapsed 47 | time after starting the node with a grace period 48 | for startup and function call times 49 | """ 50 | self.run_node(node) 51 | before = time.time() 52 | time.sleep(test_time) 53 | result = node.rpc.uptime() 54 | after = time.time() 55 | elapsed = int(after - before) 56 | 57 | self.assertTrue(result >= elapsed and result <= elapsed + margin) 58 | return result 59 | 60 | def run_test(self): 61 | """ 62 | Run JSONRPC server on first, wait to connect, then call `addnode ip[:port]` 63 | """ 64 | self.test_node_uptime(node=self.florestad, test_time=15, margin=15) 65 | self.test_node_uptime(node=self.bitcoind, test_time=15, margin=15) 66 | 67 | 68 | if __name__ == "__main__": 69 | UptimeTest().main() 70 | -------------------------------------------------------------------------------- /tests/floresta-cli/getblockchaininfo.py: -------------------------------------------------------------------------------- 1 | """ 2 | floresta_cli_getblockchainfo.py 3 | 4 | This functional test cli utility to interact with a Floresta node with `getblockchaininfo` 5 | """ 6 | 7 | from test_framework import FlorestaTestFramework 8 | from test_framework.rpc.floresta import REGTEST_RPC_SERVER 9 | 10 | 11 | class GetBlockchaininfoTest(FlorestaTestFramework): 12 | """ 13 | Test `getblockchaininfo` with a fresh node and its first block 14 | """ 15 | 16 | nodes = [-1] 17 | best_block = "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206" 18 | difficulty = 1 19 | height = 0 20 | ibd = True 21 | latest_block_time = 1296688602 22 | latest_work = "2" 23 | leaf_count = 0 24 | progress = 0 25 | root_count = 0 26 | root_hashes = [] 27 | validated = 0 28 | 29 | def set_test_params(self): 30 | """ 31 | Setup a single node 32 | """ 33 | self.florestad = self.add_node(variant="florestad") 34 | 35 | def run_test(self): 36 | """ 37 | Run JSONRPC server and get some data about blockchain with only regtest genesis block 38 | """ 39 | # Start node 40 | self.run_node(self.florestad) 41 | 42 | # Test assertions 43 | response = self.florestad.rpc.get_blockchain_info() 44 | self.assertEqual(response["best_block"], GetBlockchaininfoTest.best_block) 45 | self.assertEqual(response["difficulty"], GetBlockchaininfoTest.difficulty) 46 | self.assertEqual(response["height"], GetBlockchaininfoTest.height) 47 | self.assertEqual(response["ibd"], GetBlockchaininfoTest.ibd) 48 | self.assertEqual( 49 | response["latest_block_time"], GetBlockchaininfoTest.latest_block_time 50 | ) 51 | self.assertEqual(response["latest_work"], GetBlockchaininfoTest.latest_work) 52 | self.assertEqual(response["leaf_count"], GetBlockchaininfoTest.leaf_count) 53 | self.assertEqual(response["progress"], GetBlockchaininfoTest.progress) 54 | self.assertEqual(response["root_count"], GetBlockchaininfoTest.root_count) 55 | self.assertEqual(response["root_hashes"], GetBlockchaininfoTest.root_hashes) 56 | self.assertEqual(response["validated"], GetBlockchaininfoTest.validated) 57 | 58 | 59 | if __name__ == "__main__": 60 | GetBlockchaininfoTest().main() 61 | -------------------------------------------------------------------------------- /tests/florestad/restart.py: -------------------------------------------------------------------------------- 1 | """ 2 | restart.py 3 | 4 | A simple test that restart a Floresta node and a related data directory. 5 | 6 | The directories used between each power-on/power-off must not be corrupted. 7 | """ 8 | 9 | import filecmp 10 | 11 | from test_framework import FlorestaTestFramework 12 | 13 | DATA_DIR = FlorestaTestFramework.get_integration_test_dir() 14 | 15 | 16 | class TestRestart(FlorestaTestFramework): 17 | """ 18 | Test the restart of a node, calling a first node (0) the recall it as (1); 19 | We need to check if given data_dirs arent corrupted between restarts 20 | """ 21 | 22 | def set_test_params(self): 23 | """ 24 | Here we define setup for test 25 | """ 26 | 27 | self.data_dirs = TestRestart.create_data_dirs( 28 | DATA_DIR, self.__class__.__name__.lower(), 2 29 | ) 30 | 31 | self.florestas = [ 32 | self.add_node( 33 | variant="florestad", 34 | extra_args=[f"--data-dir={datadir}"], 35 | ) 36 | for datadir in self.data_dirs 37 | ] 38 | 39 | def run_test(self): 40 | """ 41 | Tests if we don't corrupt our data dir between restarts. 42 | This would have caught, the error fixed in #9 43 | """ 44 | # start first node then stop 45 | # wait for some time before restarting 46 | # this simulate a shutdown followed by a power-on. 47 | # In a real world scenario, we need to wait for 48 | # the node to be fully started before stopping it 49 | # this is done by waiting for the RPC port (the run_node 50 | # method does this for us, but let's wait a bit more) 51 | self.run_node(self.florestas[0]) 52 | self.florestas[0].rpc.wait_for_connections(opened=True) 53 | self.florestas[0].stop() 54 | 55 | # start second node then stop 56 | self.run_node(self.florestas[1]) 57 | self.florestas[1].rpc.wait_for_connections(opened=True) 58 | self.florestas[1].stop() 59 | 60 | # check for any corruption 61 | # if any files are different, we will get a list of them 62 | result = filecmp.dircmp(self.data_dirs[0], self.data_dirs[1]) 63 | self.assertEqual(len(result.diff_files), 0) 64 | 65 | 66 | if __name__ == "__main__": 67 | TestRestart().main() 68 | -------------------------------------------------------------------------------- /bin/florestad/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let version = get_version_from_git().unwrap_or_else(|| get_version_from_manifest().unwrap()); 3 | let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); 4 | let os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); 5 | let runtime = std::env::var("CARGO_CFG_TARGET_ENV").unwrap(); 6 | 7 | let rustc = std::process::Command::new("rustc") 8 | .args(["--version"]) 9 | .output() 10 | .map(|output| { 11 | assert!( 12 | output.status.success(), 13 | "Failed to run rustc --version. Is rustc installed?" 14 | ); 15 | String::from_utf8(output.stdout).expect("Failed to parse rustc version") 16 | }) 17 | .expect("Failed to run rustc --version. Is rustc installed?"); 18 | 19 | let long_version = format!("version {version} compiled for {arch}-{os}-{runtime} with {rustc}"); 20 | let user_agent = format!("/Floresta/{}/", version.replace("v", "")); 21 | 22 | println!("cargo:rustc-env=LONG_VERSION={long_version}"); 23 | println!("cargo:rustc-env=GIT_DESCRIBE={version}"); 24 | println!("cargo:rustc-env=USER_AGENT={user_agent}"); 25 | 26 | // re-run if either the build script or the git HEAD changes 27 | println!("cargo:rerun-if-changed=../.git/HEAD"); 28 | println!("cargo:rerun-if-changed=build.rs"); 29 | } 30 | 31 | fn get_version_from_manifest() -> Result { 32 | let manifest = std::fs::read_to_string("Cargo.toml")?; 33 | let toml: toml::Value = toml::from_str(&manifest).unwrap(); 34 | Ok(format!("v{}", toml["package"]["version"].as_str().unwrap())) 35 | } 36 | 37 | fn get_version_from_git() -> Option { 38 | std::process::Command::new("git") 39 | .args(["describe", "--tags", "--always", "--dirty"]) 40 | .output() 41 | .map(|output| { 42 | if !output.status.success() { 43 | return None; 44 | } 45 | let mut git_description = String::from_utf8(output.stdout).unwrap(); 46 | git_description.pop(); // remove the trailing newline 47 | 48 | // If we don't pull tags, git will return the short commit id, which breaks the functional tests 49 | if !git_description.starts_with("v") { 50 | return None; 51 | } 52 | 53 | Some(git_description) 54 | }) 55 | .ok()? 56 | } 57 | -------------------------------------------------------------------------------- /tests/example/utreexod.py: -------------------------------------------------------------------------------- 1 | """ 2 | utreexod-test.py 3 | 4 | This is an example of how a tests with utreexo should look like, 5 | see `tests/test_framework/test_framework.py` for more info. 6 | """ 7 | 8 | from test_framework import FlorestaTestFramework 9 | 10 | 11 | class UtreexodTest(FlorestaTestFramework): 12 | """ 13 | Tests should be a child class from FlorestaTestFramework 14 | 15 | In each test class definition, `set_test_params` and `run_test`, say what 16 | the test do and the expected result in the docstrings 17 | """ 18 | 19 | expected_chain = "regtest" 20 | expected_height = 0 21 | expected_headers = 0 22 | expected_blockhash = ( 23 | "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206" 24 | ) 25 | expected_difficulty = 1 26 | 27 | def set_test_params(self): 28 | """ 29 | Here we define setup for test adding a node definition 30 | """ 31 | self.utreexod = self.add_node(variant="utreexod") 32 | 33 | # All tests should override the run_test method 34 | def run_test(self): 35 | """ 36 | Here we define the test itself: 37 | 38 | - creates a dummy rpc listening on default port 39 | - perform some requests to FlorestaRPC node 40 | - if any assertion fails, all nodes will be stopped 41 | - if no error occurs, all nodes will be stopped at the end 42 | """ 43 | # Start a new node (this crate's binary) 44 | # This method start a defined daemon, 45 | # in this case, `utreexod`, and wait for 46 | # all ports opened by it, including the 47 | # RPC port to be available 48 | self.run_node(self.utreexod) 49 | 50 | # Once the node is running, we can create 51 | # a request to the RPC server. In this case, we 52 | # call it node, but in truth, will be a RPC request 53 | # to perform some kind of action 54 | utreexo_response = self.utreexod.rpc.get_blockchain_info() 55 | 56 | self.assertEqual(utreexo_response["chain"], UtreexodTest.expected_chain) 57 | self.assertEqual( 58 | utreexo_response["bestblockhash"], UtreexodTest.expected_blockhash 59 | ) 60 | self.assertEqual( 61 | utreexo_response["difficulty"], UtreexodTest.expected_difficulty 62 | ) 63 | 64 | self.stop() 65 | 66 | 67 | if __name__ == "__main__": 68 | UtreexodTest().main() 69 | -------------------------------------------------------------------------------- /crates/floresta-node/src/zmq.rs: -------------------------------------------------------------------------------- 1 | //! A small Zero Message Queue (ZMQ) implementation for floresta, that pushes new blocks 2 | //! as they are found. 3 | //! 4 | //! # Examples 5 | //! Creating a server 6 | //! ```ignore 7 | //! use florestad::zmq::ZMQServer; 8 | //! let _ = ZMQServer::new("tcp://127.0.0.1:5150"); 9 | //! ``` 10 | //! 11 | //! Listening for new blocks 12 | //! 13 | //! ```ignore 14 | //! use zmq::{Context, Socket}; 15 | //! let ctx = Context::new(); 16 | //! // The opposite of PUSH is PULL 17 | //! let socket = ctx.socket(zmq::SocketType::PULL).unwrap(); 18 | //! 19 | //! socket.connect(addr).unwrap(); 20 | //! let block = socket.recv().unwrap(); 21 | //! ``` 22 | 23 | use std::collections::HashMap; 24 | 25 | use bitcoin::consensus::serialize; 26 | use bitcoin::OutPoint; 27 | use floresta_chain::BlockConsumer; 28 | use floresta_chain::UtxoData; 29 | use tracing::error; 30 | use zmq::Context; 31 | use zmq::Socket; 32 | 33 | /// A 0MQ server that pushes blocks when we receive them 34 | pub struct ZMQServer { 35 | /// The ZMQ context that holds our socket. We don't really need it, 36 | /// but if _ctx gets dropped, the socket is closed, so we keep it here. 37 | _ctx: Context, 38 | /// The actual socket where we'll send blocks 39 | socket: Socket, 40 | } 41 | 42 | /// # Safety 43 | /// We only keep one reference of ZMQServer, usually inside an [std::sync::Arc], so it's ok 44 | /// to assume it can be [Sync]. 45 | unsafe impl Sync for ZMQServer {} 46 | 47 | impl ZMQServer { 48 | /// Creates a new ZMQ server that listens on `addr` and pushes blocks 49 | /// to connected peers as they are accepted 50 | pub fn new(addr: &str) -> Result { 51 | let ctx = Context::new(); 52 | let socket = ctx.socket(zmq::SocketType::PUSH)?; 53 | socket.bind(addr)?; 54 | Ok(ZMQServer { _ctx: ctx, socket }) 55 | } 56 | } 57 | 58 | // Implement BlockConsumer so we can subscribe on `ChainState` 59 | impl BlockConsumer for ZMQServer { 60 | fn wants_spent_utxos(&self) -> bool { 61 | false 62 | } 63 | 64 | fn on_block( 65 | &self, 66 | block: &bitcoin::Block, 67 | _height: u32, 68 | _spent_utxos: Option<&HashMap>, 69 | ) { 70 | let block = serialize(&block); 71 | if let Err(e) = self.socket.send(block, zmq::DONTWAIT) { 72 | error!("while sending block over zmq: {e}"); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/disk_block_header_roundtrip.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use std::io::Cursor; 4 | 5 | use arbitrary::Arbitrary; 6 | use arbitrary::Unstructured; 7 | use bitcoin::block::Version; 8 | use bitcoin::blockdata::block::Header as BlockHeader; 9 | use bitcoin::consensus::Decodable; 10 | use bitcoin::consensus::Encodable; 11 | use bitcoin::hashes::Hash; 12 | use bitcoin::BlockHash; 13 | use bitcoin::CompactTarget; 14 | use bitcoin::TxMerkleNode; 15 | use floresta_chain::DiskBlockHeader; 16 | use libfuzzer_sys::fuzz_target; 17 | 18 | fn gen_header(u: &mut Unstructured<'_>) -> arbitrary::Result { 19 | // Pull primitives from the fuzzer: 20 | let version = i32::arbitrary(u)?; 21 | let prev: [u8; 32] = Arbitrary::arbitrary(u)?; 22 | let merkle: [u8; 32] = Arbitrary::arbitrary(u)?; 23 | let time = u32::arbitrary(u)?; 24 | let bits = u32::arbitrary(u)?; 25 | let nonce = u32::arbitrary(u)?; 26 | 27 | Ok(BlockHeader { 28 | version: Version::from_consensus(version), 29 | prev_blockhash: BlockHash::from_byte_array(prev), 30 | merkle_root: TxMerkleNode::from_byte_array(merkle), 31 | time, 32 | bits: CompactTarget::from_consensus(bits), 33 | nonce, 34 | }) 35 | } 36 | 37 | #[derive(Arbitrary)] 38 | struct Inputs { 39 | #[arbitrary(with = gen_header)] 40 | header: BlockHeader, 41 | height: u32, 42 | tag: u8, 43 | } 44 | 45 | fuzz_target!(|data: &[u8]| { 46 | if let Ok(inp) = Inputs::arbitrary(&mut Unstructured::new(data)) { 47 | let header = match inp.tag % 6 { 48 | 0 => DiskBlockHeader::FullyValid(inp.header, inp.height), 49 | 1 => DiskBlockHeader::AssumedValid(inp.header, inp.height), 50 | 2 => DiskBlockHeader::HeadersOnly(inp.header, inp.height), 51 | 3 => DiskBlockHeader::InFork(inp.header, inp.height), 52 | 4 => DiskBlockHeader::Orphan(inp.header), 53 | _ => DiskBlockHeader::InvalidChain(inp.header), 54 | }; 55 | 56 | // Encode 57 | let mut buf = Vec::new(); 58 | let written = header.consensus_encode(&mut buf).expect("encode failed"); 59 | assert_eq!(written, buf.len(), "encode returned wrong length"); 60 | 61 | // Decode and compare 62 | let decoded = 63 | DiskBlockHeader::consensus_decode(&mut Cursor::new(&buf)).expect("decode failed"); 64 | assert_eq!(decoded, header, "roundtrip mismatch"); 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /tests/example/electrum.py: -------------------------------------------------------------------------------- 1 | """ 2 | electrum-test.py 3 | 4 | This is an example of how a tests with integrated electrum should look like, 5 | see `tests/test_framework/test_framework.py` for more info. 6 | """ 7 | 8 | from test_framework import FlorestaTestFramework 9 | from test_framework.electrum.client import ElectrumClient 10 | 11 | 12 | class ElectrumTest(FlorestaTestFramework): 13 | """ 14 | Tests should be a child class from FlorestaTestFramework 15 | 16 | In each test class definition, `set_test_params` and `run_test`, say what 17 | the test do and the expected result in the docstrings 18 | """ 19 | 20 | index = [-1] 21 | expected_version = ["Floresta 0.4.0", "1.4"] 22 | 23 | def set_test_params(self): 24 | """ 25 | Here we define setup for test adding a node definition 26 | """ 27 | self.florestad = self.add_node(variant="florestad") 28 | 29 | # All tests should override the run_test method 30 | def run_test(self): 31 | """ 32 | Here we define the test itself: 33 | 34 | - creates a dummy rpc listening on default port 35 | - perform some requests to FlorestaRPC node 36 | - if any assertion fails, all nodes will be stopped 37 | - if no error occurs, all nodes will be stopped at the end 38 | """ 39 | # Start a new node (this crate's binary) 40 | # This method start a defined daemon, 41 | # in this case, `florestad`, and wait for 42 | # all ports opened by it, including the 43 | # RPC port to be available 44 | self.run_node(self.florestad) 45 | 46 | # Create an instance of the Electrum Client, 47 | # a small implementation of the electrum 48 | # protocol, to test our own electrum implementation 49 | host = self.florestad.get_host() 50 | port = self.florestad.get_port("electrum-server") 51 | electrum = ElectrumClient(host, port) 52 | rpc_response = electrum.get_version() 53 | 54 | # Make assertions with our framework. Avoid usage of 55 | # native `assert` clauses. For more information, see 56 | # https://github.com/vinteumorg/Floresta/issues/426 57 | self.assertEqual(rpc_response["result"][0], ElectrumTest.expected_version[0]) 58 | self.assertEqual(rpc_response["result"][1], ElectrumTest.expected_version[1]) 59 | 60 | self.stop() 61 | 62 | 63 | if __name__ == "__main__": 64 | ElectrumTest().main() 65 | -------------------------------------------------------------------------------- /crates/floresta-rpc/src/jsonrpc_client.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use serde::Deserialize; 4 | 5 | use crate::rpc::JsonRPCClient; 6 | 7 | // Define a Client struct that wraps a jsonrpc::Client 8 | #[derive(Debug)] 9 | pub struct Client(jsonrpc::Client); 10 | 11 | // Configuration struct for JSON-RPC client 12 | pub struct JsonRPCConfig { 13 | pub url: String, 14 | pub user: Option, 15 | pub pass: Option, 16 | } 17 | 18 | impl Client { 19 | // Constructor to create a new Client with a URL 20 | pub fn new(url: String) -> Self { 21 | let client = 22 | jsonrpc::Client::simple_http(&url, None, None).expect("Failed to create client"); 23 | Self(client) 24 | } 25 | 26 | // Constructor to create a new Client with a configuration 27 | pub fn new_with_config(config: JsonRPCConfig) -> Self { 28 | let client = 29 | jsonrpc::Client::simple_http(&config.url, config.user.clone(), config.pass.clone()) 30 | .expect("Failed to create client"); 31 | Self(client) 32 | } 33 | 34 | // Method to make an RPC call 35 | pub fn rpc_call( 36 | &self, 37 | method: &str, 38 | params: &[serde_json::Value], 39 | ) -> Result 40 | where 41 | Response: for<'a> serde::de::Deserialize<'a> + Debug, 42 | { 43 | // Serialize parameters to raw JSON value 44 | let raw = serde_json::value::to_raw_value(params)?; 45 | // Build the RPC request 46 | let req = self.0.build_request(method, Some(&*raw)); 47 | // Send the request and handle the response 48 | let resp = self 49 | .0 50 | .send_request(req) 51 | .map_err(crate::rpc_types::Error::from); 52 | 53 | // Deserialize and return the result 54 | Ok(resp?.result()?) 55 | } 56 | } 57 | 58 | // Implement the JsonRPCClient trait for Client 59 | impl JsonRPCClient for Client { 60 | fn call serde::de::Deserialize<'a> + Debug>( 61 | &self, 62 | method: &str, 63 | params: &[serde_json::Value], 64 | ) -> Result { 65 | self.rpc_call(method, params) 66 | } 67 | } 68 | 69 | // Struct to represent a JSON-RPC response 70 | #[derive(Debug, Deserialize)] 71 | pub struct JsonRpcResponse { 72 | pub jsonrpc: String, 73 | pub id: u64, 74 | pub result: Option, 75 | pub error: Option, 76 | } 77 | -------------------------------------------------------------------------------- /tests/floresta-cli/getblockheader.py: -------------------------------------------------------------------------------- 1 | """ 2 | floresta_cli_getblockheader.py 3 | 4 | This functional test cli utility to interact with a Floresta node with `getblockheader` 5 | """ 6 | 7 | from test_framework import FlorestaTestFramework 8 | from test_framework.rpc.floresta import REGTEST_RPC_SERVER 9 | 10 | 11 | class GetBlockheaderHeightZeroTest(FlorestaTestFramework): 12 | """ 13 | Test `getblockheader` with a fresh node and expect a result like this: 14 | 15 | ````bash 16 | $> ./target/release floresta_cli --network=regtest getblockheader \ 17 | 0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206 18 | { 19 | "version": 1, 20 | "prev_blockhash": "0000000000000000000000000000000000000000000000000000000000000000", 21 | "merkle_root": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", 22 | "time": 1296688602, 23 | "bits": 545259519, 24 | "nonce": 2 25 | } 26 | ``` 27 | """ 28 | 29 | nodes = [-1] 30 | version = 1 31 | blockhash = "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206" 32 | prev_blockhash = "0000000000000000000000000000000000000000000000000000000000000000" 33 | merkle_root = "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b" 34 | time = 1296688602 35 | bits = 545259519 36 | nonce = 2 37 | 38 | def set_test_params(self): 39 | """ 40 | Setup a single node 41 | """ 42 | self.florestad = self.add_node(variant="florestad") 43 | 44 | def run_test(self): 45 | """ 46 | Run JSONRPC and get the header of the genesis block 47 | """ 48 | # Start node 49 | self.run_node(self.florestad) 50 | 51 | # Test assertions 52 | response = self.florestad.rpc.get_blockheader( 53 | GetBlockheaderHeightZeroTest.blockhash 54 | ) 55 | self.assertEqual(response["version"], GetBlockheaderHeightZeroTest.version) 56 | self.assertEqual( 57 | response["prev_blockhash"], GetBlockheaderHeightZeroTest.prev_blockhash 58 | ) 59 | self.assertEqual( 60 | response["merkle_root"], GetBlockheaderHeightZeroTest.merkle_root 61 | ) 62 | self.assertEqual(response["time"], GetBlockheaderHeightZeroTest.time) 63 | self.assertEqual(response["bits"], GetBlockheaderHeightZeroTest.bits) 64 | self.assertEqual(response["nonce"], GetBlockheaderHeightZeroTest.nonce) 65 | 66 | 67 | if __name__ == "__main__": 68 | GetBlockheaderHeightZeroTest().main() 69 | -------------------------------------------------------------------------------- /tests/test_framework/crypto/pkcs8.py: -------------------------------------------------------------------------------- 1 | """ 2 | tests/test_framework/crypto/pkcs8.py 3 | 4 | This module generate proper PKCS#8 private keys and certificate 5 | """ 6 | 7 | import os 8 | from datetime import datetime, timedelta 9 | from typing import Tuple 10 | 11 | from cryptography import x509 12 | from cryptography.hazmat.primitives import hashes, serialization 13 | from cryptography.hazmat.primitives.asymmetric import rsa 14 | from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey 15 | from cryptography.x509.oid import NameOID 16 | 17 | DEFAULT_PUBLIC_EXPONENT = 65537 18 | DEFAULT_KEY_SIZE = 2048 19 | DEFAULT_CN = "vinteumorg" 20 | DEFAULT_DAYS = 1 21 | 22 | 23 | def create_pkcs8_private_key( 24 | path: str, 25 | public_exponent: int = DEFAULT_PUBLIC_EXPONENT, 26 | key_size: int = DEFAULT_KEY_SIZE, 27 | ) -> Tuple[str, RSAPrivateKey]: 28 | """ 29 | Generate private key in a proper format PKCS#8 30 | """ 31 | pk = rsa.generate_private_key(public_exponent=public_exponent, key_size=key_size) 32 | 33 | # Serialize and save key 34 | pem = pk.private_bytes( 35 | encoding=serialization.Encoding.PEM, 36 | format=serialization.PrivateFormat.PKCS8, 37 | encryption_algorithm=serialization.NoEncryption(), 38 | ) 39 | pk_path = os.path.join(path, "key.pem") 40 | with open(pk_path, "wb") as f: 41 | f.write(pem) 42 | 43 | return (pk_path, pk) 44 | 45 | 46 | def create_pkcs8_self_signed_certificate( 47 | path: str, 48 | pk: RSAPrivateKey, 49 | common_name: str = DEFAULT_CN, 50 | validity_days: int = DEFAULT_DAYS, 51 | ) -> str: 52 | """ 53 | Generate a self signed certificate in a proper format PKCS#8 54 | """ 55 | # Create subject/issuer name 56 | subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, common_name)]) 57 | 58 | # Certificate validity period 59 | now = datetime.utcnow() 60 | validity = timedelta(days=validity_days) 61 | 62 | # Build and sign certificate 63 | cert = ( 64 | x509.CertificateBuilder() 65 | .subject_name(subject) 66 | .issuer_name(subject) 67 | .public_key(pk.public_key()) 68 | .serial_number(x509.random_serial_number()) 69 | .not_valid_before(now) 70 | .not_valid_after(now + validity) 71 | .sign(pk, hashes.SHA256()) 72 | ) 73 | 74 | # Save certificate 75 | cert_path = os.path.join(path, "cert.pem") 76 | with open(cert_path, "wb") as f: 77 | f.write(cert.public_bytes(serialization.Encoding.PEM)) 78 | 79 | return cert_path 80 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | # Libraries 5 | "crates/floresta", 6 | "crates/floresta-node", 7 | "crates/floresta-chain", 8 | "crates/floresta-rpc", 9 | "crates/floresta-common", 10 | "crates/floresta-compact-filters", 11 | "crates/floresta-electrum", 12 | "crates/floresta-watch-only", 13 | "crates/floresta-wire", 14 | 15 | # Binaries 16 | "bin/florestad", 17 | "bin/floresta-cli", 18 | 19 | # Tests and monitoring 20 | "metrics", 21 | "fuzz", 22 | ] 23 | 24 | default-members = [ 25 | "crates/floresta", 26 | "crates/floresta-node", 27 | "crates/floresta-chain", 28 | "crates/floresta-rpc", 29 | "crates/floresta-common", 30 | "crates/floresta-compact-filters", 31 | "crates/floresta-electrum", 32 | "crates/floresta-watch-only", 33 | "crates/floresta-wire", 34 | "bin/florestad", 35 | "bin/floresta-cli", 36 | ] 37 | 38 | [workspace.package] 39 | rust-version = "1.81.0" # MSRV declaration 40 | readme = "README.md" 41 | 42 | # Version Convention: Use major.minor only (e.g., "1.9" not "1.9.1") 43 | # We accept all compatible patch versions, so specifying patch level is misleading. 44 | # For exact versions when needed, use "=1.9.1" 45 | [workspace.dependencies] 46 | axum = "0.8" 47 | bitcoin = { version = "0.32", features = ["serde"] } 48 | clap = { version = "4.5", features = ["derive"] } 49 | corepc-types = "0.11" 50 | console-subscriber = "0.5" 51 | dns-lookup = "2.1" 52 | hex = "0.4" 53 | kv = "0.24" 54 | miniscript = { version = "12.3", default-features = false } 55 | rand = "0.8" 56 | rcgen = "0.13" 57 | rustreexo = "0.4" 58 | serde = { version = "1.0", features = ["derive"] } 59 | serde_json = "1.0" 60 | sha2 = { version = "0.10", default-features = false } 61 | spin = "0.10" 62 | tempfile = "3.23" 63 | tokio = { version = "1.48", features = ["full"] } 64 | tokio-rustls = "0.26" 65 | toml = "0.9" 66 | tracing = "0.1" 67 | zstd = "0.13" 68 | 69 | # Local dependencies 70 | floresta-chain = { path = "crates/floresta-chain" } 71 | floresta-common = { path = "crates/floresta-common" } 72 | floresta-compact-filters = { path = "crates/floresta-compact-filters" } 73 | floresta-electrum = { path = "crates/floresta-electrum" } 74 | floresta-node = { path = "crates/floresta-node", default-features = false } 75 | floresta-rpc = { path = "crates/floresta-rpc" } 76 | floresta-watch-only = { path = "crates/floresta-watch-only" } 77 | floresta-wire = { path = "crates/floresta-wire" } 78 | metrics = { path = "metrics" } 79 | 80 | [workspace.lints.clippy] 81 | unused_async = "deny" 82 | -------------------------------------------------------------------------------- /crates/floresta/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | //! Floresta, a collection of crates to build lightweight, fast, and reliable 4 | //! Bitcoin nodes and wallets, powered by Utreexo, a novel accumulator to represent 5 | //! the Bitcoin UTXO set. 6 | //! 7 | //! This project is laid out as a collection of crates, each implementing one functionality. 8 | //! They are all named floresta-*. The main crate is floresta, which is a meta-crate 9 | //! that depends on all the others. It is meant to be used as a dependency in other projects. 10 | //! 11 | //! A Bitcoin node is composed of several components. You need some way to express the current 12 | //! network state, some way to communicate with other nodes, some way to store the blockchain, 13 | //! validate data you receive and interact with the user. 14 | //! The current network state (including the accumulator) is tracked by the floresta-chain crate. 15 | //! It doesn't know where data comes from, it just tracks the state, exposing a simple API to 16 | //! update it. The chain is reactive, meaning that it will only update when new data is received. 17 | //! 18 | //! The floresta-wire crate is responsible for communicating with other nodes. It is a simple 19 | //! node implementation that can connect to other nodes, send and receive messages, and 20 | //! handle the peer-to-peer protocol. It is not a full node, it doesn't store the blockchain 21 | //! or validate data, it just sends and receives messages. 22 | //! 23 | //! Floresta also provides a simple watch-only wallet and an electrum server implementation. 24 | //! They are meant to be used in `florestad`, a full node implementation that uses all the 25 | //! crates in this project. 26 | //! 27 | //! You can find examples of how to use the crates in the examples directory. 28 | //! # Name 29 | //! Floresta is the Portuguese word for forest. It is a reference to the Utreexo accumulator, 30 | //! which is a forest of Merkle trees. It's pronounced /floˈɾɛstɐ/. 31 | 32 | #![cfg_attr(docsrs, feature(doc_cfg))] 33 | 34 | /// Components to build a utreexo-aware, consensus enforcing Bitcoin node. 35 | pub use floresta_chain as chain; 36 | /// Useful data structures and traits used by the other crates. 37 | pub use floresta_common as common; 38 | #[cfg(feature = "electrum-server")] 39 | /// An electrum server implementation 40 | pub use floresta_electrum as electrum; 41 | #[cfg(feature = "watch-only-wallet")] 42 | /// A watch-only wallet implementation, optimized for electrum servers. 43 | pub use floresta_watch_only as wallet; 44 | /// The transport used to fetch network data. 45 | pub use floresta_wire as wire; 46 | -------------------------------------------------------------------------------- /tests/example/functional.py: -------------------------------------------------------------------------------- 1 | """ 2 | functional-test.py 3 | 4 | This is an example of how a functional-test should look like, 5 | see `tests/test_framework/test_framework.py` for more info. 6 | """ 7 | 8 | from test_framework import FlorestaTestFramework 9 | 10 | 11 | class FunctionalTest(FlorestaTestFramework): 12 | """ 13 | Tests should be a child class from FlorestaTestFramework 14 | 15 | In each test class definition, `set_test_params` and `run_test`, say what 16 | the test do and the expected result in the docstrings 17 | """ 18 | 19 | expected_height = 0 20 | expected_block = "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206" 21 | expected_difficulty = 1 22 | expected_leaf_count = 0 23 | 24 | def set_test_params(self): 25 | """ 26 | Here we define setup for test adding a node definition 27 | """ 28 | self.florestad = self.add_node(variant="florestad") 29 | 30 | # All tests should override the run_test method 31 | def run_test(self): 32 | """ 33 | Here we define the test itself: 34 | 35 | - creates a dummy rpc listening on default port 36 | - perform some requests to FlorestaRPC node 37 | - if any assertion fails, all nodes will be stopped 38 | - if no error occurs, all nodes will be stopped at the end 39 | """ 40 | # Start a new node (this crate's binary) 41 | # This method start a defined daemon, 42 | # in this case, `florestad`, and wait for 43 | # all ports opened by it, including the 44 | # RPC port to be available 45 | self.run_node(self.florestad) 46 | 47 | # Once the node is running, we can create 48 | # a request to the RPC server. In this case, we 49 | # call it node, but in truth, will be a RPC request 50 | # to perform some kind of action 51 | inf_response = self.florestad.rpc.get_blockchain_info() 52 | 53 | # Make assertions with our framework. Avoid usage of 54 | # native `assert` clauses. For more information, see 55 | # https://github.com/vinteumorg/Floresta/issues/426 56 | self.assertEqual(inf_response["height"], FunctionalTest.expected_height) 57 | self.assertEqual(inf_response["best_block"], FunctionalTest.expected_block) 58 | self.assertEqual(inf_response["difficulty"], FunctionalTest.expected_difficulty) 59 | self.assertEqual(inf_response["leaf_count"], FunctionalTest.expected_leaf_count) 60 | 61 | # stop nodes 62 | self.stop() 63 | 64 | 65 | if __name__ == "__main__": 66 | FunctionalTest().main() 67 | -------------------------------------------------------------------------------- /crates/floresta/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "floresta" 3 | version = "0.4.0" 4 | authors = ["Davidson Souza "] 5 | edition = "2021" 6 | description = """ 7 | A modular and extensible framework for building Utreexo based Bitcoin nodes. 8 | 9 | Using libfloresta, you can build lightweight Bitcoin nodes that can be 10 | extended with plugins to add new features. The library is designed to be 11 | modular and extensible, so you can easily add new features to your node 12 | without having to modify the core structs. The framework is also designed to 13 | be easy to use, so you can get started with your node in a few lines of 14 | code. 15 | 16 | See the examples directory for examples of how to use this library, and 17 | each crate's documentation for more information on how to use each module. 18 | """ 19 | repository = "https://github.com/vinteumorg/Floresta" 20 | license = "MIT" 21 | readme.workspace = true # Floresta/README.md 22 | keywords = ["bitcoin", "utreexo", "node", "blockchain", "rust"] 23 | categories = ["cryptography::cryptocurrencies"] 24 | 25 | [dependencies] 26 | # Local dependencies 27 | floresta-chain = { workspace = true } 28 | floresta-common = { workspace = true } 29 | floresta-electrum = { workspace = true, optional = true } 30 | floresta-watch-only = { workspace = true, optional = true } 31 | floresta-wire = { workspace = true } 32 | 33 | [dev-dependencies] 34 | bitcoin = { workspace = true, features = [ "serde", "bitcoinconsensus" ] } 35 | miniscript = { workspace = true, features = ["std"] } 36 | rustreexo = { workspace = true } 37 | tokio = { workspace = true } 38 | 39 | # Local dependencies 40 | floresta = { path = "../floresta", features = [ 41 | "bitcoinconsensus", 42 | "memory-database", 43 | "electrum-server", 44 | "watch-only-wallet", 45 | ] } 46 | 47 | [features] 48 | default = ["bitcoinconsensus", "electrum-server", "watch-only-wallet", "flat-chainstore"] 49 | bitcoinconsensus = ["floresta-chain/bitcoinconsensus"] 50 | electrum-server = ["dep:floresta-electrum"] 51 | watch-only-wallet = ["dep:floresta-watch-only"] 52 | # Works only if `watch-only-wallet` is set 53 | memory-database = ["floresta-watch-only?/memory-database"] 54 | flat-chainstore = ["floresta-chain/flat-chainstore"] 55 | 56 | [lib] 57 | crate-type = ["cdylib", "rlib", "staticlib"] 58 | 59 | [lints] 60 | workspace = true 61 | 62 | [[example]] 63 | name = "node" 64 | path = "examples/node.rs" 65 | 66 | [[example]] 67 | name = "watch-only" 68 | path = "examples/watch-only.rs" 69 | 70 | [[example]] 71 | name = "chainstate-builder" 72 | path = "examples/chainstate-builder.rs" 73 | -------------------------------------------------------------------------------- /doc/docker.md: -------------------------------------------------------------------------------- 1 | # Floresta Docker Setup Guide 2 | 3 | You can find a [Dockerfile](../Dockerfile) in the root directory of the project, which you can use to build a 4 | docker image for Floresta. We also keep the docker image [dlsz/floresta](https://hub.docker.com/r/dlsz/floresta) 5 | on Docker Hub, which you can pull and run directly. 6 | 7 | If you want to run using compose, you may use a simple `docker-compose.yml` file like this: 8 | 9 | ```yaml 10 | services: 11 | floresta: 12 | image: dlsz/floresta:latest 13 | container_name: Floresta 14 | command: florestad -c /data/config.toml --data-dir /data/.floresta 15 | ports: 16 | - 50001:50001 17 | - 8332:8332 18 | volumes: 19 | - /path/config/floresta.toml:/data/config.toml 20 | - /path/utreexo:/data/.floresta 21 | restart: unless-stopped 22 | ``` 23 | 24 | Here's a breakdown of the configuration: 25 | - For the command, there are a couple of options that are worth noticing: 26 | - `--data-dir /data/.floresta` specifies `florestad`'s data directory. Here, `florestad` will store its blockchain data, wallet files, and other necessary data. 27 | 28 | - `-c /data/config.toml` specifies the path to the configuration file inside the container. By default, Floresta looks for a configuration file at the datadir if no configuration file is specified. 29 | You should mount a volume to at each path to persist data outside the container. 30 | 31 | - `-n ` specifies the Bitcoin network to connect to (mainnet, testnet, testnet4, signet, regtest). Make sure this matches your configuration file. 32 | - The `ports` section maps the container's ports to your host machine. Adjust these as necessary. 33 | - `50001` is used for Electrum server connections. It may change depending on the network you are using or your configuration. 34 | 35 | - `8332` is used for RPC connections. Adjust this if you have changed the RPC port in your configuration file. 36 | 37 | This setup will run Floresta in a Docker container and expose the RPC and Electrum ports, so you can connect to them. After the container is running, you can connect to it using an Electrum wallet or any other compatible client. 38 | 39 | To use the RPC via CLI, you can use a command like this: 40 | 41 | ```bash 42 | docker exec -it Floresta floresta-cli getblockchaininfo 43 | ``` 44 | 45 | ## Monitoring 46 | 47 | Floresta also (optionally) provides [Prometheus](https://prometheus.io/) metrics endpoint, which you can enable at compile time. If you want a quick setup with Grafana, we provide a [docker-compose.yml](../docker-compose.yml) for that as well. Just use: 48 | 49 | ```bash 50 | docker-compose -f docker-compose.yml up -d 51 | ``` 52 | -------------------------------------------------------------------------------- /.github/workflows/benchmarks.yml: -------------------------------------------------------------------------------- 1 | name: Benchmarks 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | benchmark: 12 | name: Run Benchmarks 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: dtolnay/rust-toolchain@1.81.0 18 | with: 19 | components: rustfmt, clippy 20 | 21 | # Bi-weekly numbers to refresh caches every two weeks, ensuring recent project changes are cached 22 | - name: Set bi-weekly cache key 23 | # Use '10#' to always treat week number as base-10 (avoids octal when number has a leading zero) 24 | run: | 25 | YEAR=$(date +%Y) 26 | WEEK=$(date +%U) 27 | BIWEEK=$(( (10#$WEEK + 1) / 2 )) 28 | echo "CACHE_VERSION=${YEAR}(${BIWEEK})" >> $GITHUB_ENV 29 | 30 | # Hash of all files that could affect the build 31 | echo "BUILD_HASH=${{ hashFiles('**/Cargo.lock', '**/Cargo.toml') }}" >> $GITHUB_ENV 32 | shell: bash 33 | 34 | # Restore Rust build cache and artifacts 35 | - name: Restore Rust cache 36 | id: cache 37 | uses: actions/cache/restore@v4 38 | with: 39 | path: | 40 | ~/.cargo/registry/index/ 41 | ~/.cargo/registry/cache/ 42 | ~/.cargo/git/db/ 43 | target/release/ 44 | # Cache key depends on the bi-week we are on (cache version) 45 | key: ${{ runner.os }}-cargo-${{ env.CACHE_VERSION }}-${{ env.BUILD_HASH }} 46 | restore-keys: | 47 | ${{ runner.os }}-cargo-${{ env.CACHE_VERSION }}- 48 | ${{ runner.os }}-cargo- 49 | 50 | # Run benchmarks 51 | - name: Run cargo bench 52 | run: | 53 | cargo bench --package floresta-chain --no-default-features --features test-utils,flat-chainstore 54 | 55 | # Save cache only if the previous steps succeeded and there was not an exact cache key match 56 | # This happens everytime we modify any `cargo.lock` or `cargo.toml`, or each two weeks (caching recent changes) 57 | - name: Save Rust cache 58 | if: success() && steps.cache.outputs.cache-hit != 'true' 59 | uses: actions/cache/save@v4 60 | with: 61 | path: | 62 | ~/.cargo/registry/index/ 63 | ~/.cargo/registry/cache/ 64 | ~/.cargo/git/db/ 65 | target/release/ 66 | key: ${{ steps.cache.outputs.cache-primary-key }} 67 | 68 | # Store benchmark results 69 | - name: Store benchmark results 70 | uses: actions/upload-artifact@v4 71 | with: 72 | name: benchmark-results 73 | path: target/criterion 74 | -------------------------------------------------------------------------------- /crates/floresta/examples/chainstate-builder.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | //! In node.rs we created a node that connects to the Bitcoin network and downloads the blockchain. 4 | //! We use the default chainstate, which starts at genesis and validates all blocks. This is 5 | //! the simplest way to create a node, but you can also create a node that starts at a given 6 | //! block, or that doesn't validate all signatures. All customizations are done through the 7 | //! ChainStateBuilder struct. This example shows how to use it. 8 | use bitcoin::blockdata::constants::genesis_block; 9 | use bitcoin::Network; 10 | use floresta_chain::pruned_utreexo::chain_state_builder::ChainStateBuilder; 11 | use floresta_chain::AssumeValidArg; 12 | use floresta_chain::ChainParams; 13 | use floresta_chain::FlatChainStore; 14 | use floresta_chain::FlatChainStoreConfig; 15 | use rustreexo::accumulator::stump::Stump; 16 | 17 | const DATA_DIR: &str = "./tmp-db"; 18 | 19 | #[tokio::main] 20 | async fn main() { 21 | let network = Network::Bitcoin; 22 | let params = ChainParams::from(Network::Bitcoin); 23 | let genesis = genesis_block(¶ms); 24 | // Create a new chain state, which will store the accumulator and the headers chain. 25 | // It will be stored in the DATA_DIR directory. With this chain state, we don't keep 26 | // the block data after we validated it. This saves a lot of space, but it means that 27 | // we can't serve blocks to other nodes or rescan the blockchain without downloading 28 | // it again. 29 | let chain_store_config = FlatChainStoreConfig::new(DATA_DIR.into()); 30 | let chain_store = 31 | FlatChainStore::new(chain_store_config).expect("failed to open the blockchain database"); 32 | 33 | // Create a new chain state builder. We can use it to customize the chain state. 34 | // Assume valid is the same as in node.rs, it's the block that we assume that all 35 | // blocks before it have valid signatures. 36 | // 37 | // Tip is the block that we consider to be the tip of the chain. If you want to 38 | // start the chainstate at a given block, you can use this. If you don't set it, 39 | // it will start at genesis. 40 | // 41 | // We also set the chain params, which are the parameters of the network that we 42 | // are connecting to. We use the Bitcoin network here, but you can also use 43 | // Testnet, Signet or Regtest. 44 | // 45 | // Finally, we set the utreexo accumulator. This is the accumulator that we use 46 | // to validate the blockchain. If you set the chain height, you should update 47 | // the accumulator to the state of the blockchain at that height too. 48 | let _chain = ChainStateBuilder::new() 49 | .with_assume_valid(AssumeValidArg::Disabled, network) 50 | .with_chain_params(params) 51 | .with_tip((genesis.block_hash(), 0), genesis.header) 52 | .assume_utreexo(Stump::new()) 53 | .with_chainstore(chain_store) 54 | .build() 55 | .unwrap(); 56 | 57 | // ... If you want to drive the chainstate, you can use the BlockchainInterface trait. 58 | // See node.rs for an example on how to do it ... 59 | } 60 | -------------------------------------------------------------------------------- /tests/test_framework/README.md: -------------------------------------------------------------------------------- 1 | ## Floresta test framework 2 | 3 | This is a collection of tools to help with functional testing of Floresta. The actual tests are written in Python and can be found in the `tests` directory. 4 | 5 | Some good chunks of this framework are taken from [Bitcoin Core's test framework](https://github.com/bitcoin/bitcoin/tree/master/test/functional) and adapted to Floresta. Luckily, both are MIT licensed, so we just left the original license headers in place. Thanks to the Bitcoin Core developers for their work! 6 | 7 | ### Main components 8 | 9 | - `test_framework.test_framework`: Base class for tests, helps you with setting up a test node and provides some useful methods. This also implements the main test workflow. 10 | - `bitcoin`: Useful bitcoin primitives, like transactions, blocks, etc. 11 | - `electrum_client`: A simple client for the Electrum protocol, we use it to test our own Electrum server implementation. 12 | - `key`: Useful key primitives, like BIP32 derivation, signing, etc. 13 | - `secp256k1`: A simple implementation of the secp256k1 elliptic curve, used for signing and verification. 14 | - `utreexod`: A wrapper around `utreexod`. It helps you with starting and stopping a `utreexod` instance, and provides some useful methods to interact with it. Should if we need a utreexo peer during a test. 15 | `utreexo`: Useful utreexo methods, like creating proofs. 16 | 17 | ### Running the tests 18 | 19 | To run the tests, you need to have a `utreexod` instance running in the background. You can start it with `./tests/utreexod.py`. It will create a new datadir in `/tmp/utreexod` and start a `utreexod` instance in regtest mode. It will also create a new wallet for you, and fund it with 50 BTC. The wallet is encrypted with the password `test`. 20 | 21 | You can run the tests with `./test_runner.py`. It will start a new `utreexod` instance for each test, and stop it afterwards. You can also run a single test with `./test_runner.py `. You can get a list of all tests with `./test_runner.py --list`. 22 | 23 | ### Writing tests 24 | 25 | To write a new test, create a new file in the `tests` directory. The file name should start with `test_`. The file should contain a class that inherits from `TestFramework`. You can then implement the `set_test_params` and `run_test` methods. The `set_test_params` method is called before the test is run, and you can use it to set some test parameters, like the number of nodes to start, or the number of blocks to mine. The `run_test` method is called after the nodes are started, and you can use it to actually run the test. 26 | 27 | You can use the `self.nodes` list to access the nodes. The first node in the list is the controller node, and the other nodes are the peers. You can use the `self.nodes[0].rpc` attribute to access the RPC interface of the controller node. You can use the `self.nodes[0].utreexo` attribute to access the utreexo interface of the controller node. You can use the `self.nodes[0].electrum` attribute to access the Electrum interface of the controller node. 28 | 29 | You can use the `self.log` method to log messages. The log messages will be prefixed with the test name. You can use the `self.stop_node` method to stop a node. You can use the `self.start_node` method to start a node. -------------------------------------------------------------------------------- /tests/example/integration.py: -------------------------------------------------------------------------------- 1 | """ 2 | integration-test.py 3 | 4 | This is an example of how a tests with integrated electrum should look like, 5 | see `tests/test_framework/test_framework.py` for more info. 6 | """ 7 | 8 | from test_framework import FlorestaTestFramework 9 | 10 | 11 | class IntegrationTest(FlorestaTestFramework): 12 | """ 13 | Tests should be a child class from FlorestaTestFramework 14 | 15 | In each test class definition, `set_test_params` and `run_test`, say what 16 | the test do and the expected result in the docstrings 17 | """ 18 | 19 | index = [-1, -1, -1] 20 | expected_chain = "regtest" 21 | 22 | def set_test_params(self): 23 | """ 24 | Here we define setup for test adding a node definition 25 | """ 26 | self.florestad = self.add_node(variant="florestad") 27 | self.utreexod = self.add_node(variant="utreexod") 28 | self.bitcoind = self.add_node(variant="bitcoind") 29 | 30 | # All tests should override the run_test method 31 | def run_test(self): 32 | """ 33 | Here we define the test itself: 34 | 35 | - creates two dummy rpc listening on its default port, 36 | one for florestad and another for utreexod 37 | - perform some requests to FlorestaRPC node 38 | - perform some requests to UtreexoRPC node 39 | - assert the responses from both nodes 40 | - compare if both have similar values 41 | - if any assertion fails, all nodes will be stopped 42 | - if no error occurs, all nodes will be stopped at the end 43 | """ 44 | # Start a new node (this crate's binary) 45 | # This method start a defined daemon, 46 | # in this case, `florestad`, and wait for 47 | # all ports opened by it, including the 48 | # RPC port to be available 49 | self.run_node(self.florestad) 50 | 51 | # Start a new node (this go's binary) 52 | # This method start a defined daemon, 53 | # in this case, `utreexod`, and wait for 54 | # all ports opened by it, including the 55 | # RPC port to be available 56 | self.run_node(self.utreexod) 57 | 58 | # Start a new node (the bitcoin-core binary) 59 | # This method start a defined daemon, 60 | # in this case, `bitcoind`, and wait for 61 | # all ports opened by it, including the 62 | # RPC port to be available 63 | self.run_node(self.bitcoind) 64 | 65 | # Perform for some defined requests to FlorestaRPC 66 | # that should be the same for UtreexoRPC and BitcoinRPC 67 | floresta_response = self.florestad.rpc.get_blockchain_info() 68 | utreexo_response = self.utreexod.rpc.get_blockchain_info() 69 | bitcoin_response = self.bitcoind.rpc.get_blockchain_info() 70 | 71 | # the chain should be the same (regtest) 72 | self.assertEqual(floresta_response["chain"], IntegrationTest.expected_chain) 73 | self.assertEqual(utreexo_response["chain"], IntegrationTest.expected_chain) 74 | self.assertEqual(bitcoin_response["chain"], IntegrationTest.expected_chain) 75 | 76 | # stop all nodes 77 | self.stop() 78 | 79 | 80 | if __name__ == "__main__": 81 | IntegrationTest().main() 82 | -------------------------------------------------------------------------------- /doc/rpc/rescanblockchain.md: -------------------------------------------------------------------------------- 1 | # `rescanblockchain` 2 | 3 | Sends a request to the node for rescan the blockchain searching for transactions related to the wallet's cached addresses. 4 | 5 | ## Usage 6 | 7 | ### Synopsis 8 | 9 | ```Bash 10 | floresta-cli rescanblockchain <--t or --timestamp> 11 | ``` 12 | 13 | ### Examples 14 | 15 | ```bash 16 | 17 | # Rescan from height 100 to 200 18 | 19 | floresta-cli rescanblockchain 100 200 20 | 21 | # Rescan from height 100 to tip 22 | 23 | floresta-cli rescanblockchain 100 24 | 25 | # Rescan from timestamp 1231006505 (genesis) until 133456789 26 | 27 | floresta-cli rescanblockchain -t 1231006505 1752516460 --confidence high 28 | 29 | # Rescan from timestamp 0 (alias for genesis) until the tip 30 | 31 | floresta-cli rescanblockchain --timestamp 0 1752516460 -c high 32 | 33 | ``` 34 | 35 | ## Arguments 36 | 37 | `start_block` (numeric, optional, default=`0`): The initial block to start the blockchain rescan. 38 | 39 | `stop_block` (numeric, optional, default=`0`): The block limit to stop rescanning. (0 disables it) 40 | 41 | `use_timestamp` (boolean flag, optional, default=`false`): When present in the command the provided values will be treated as UNIX timestamps. These timestamps do not need to be directly from a block and can be used for finding addresses and UTXOs from meaningful timestamp values. 42 | 43 | `confidence` (string, optional, default=`medium`): 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. 44 | 45 | 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). 46 | The supplied string can be one of: 47 | 48 | - `high`: 99% confidence interval. 49 | - `medium` (default): 95% confidence interval. 50 | - `low`: 90% confidence interval. 51 | - `exact`: Doesnt apply any lookback seconds. 52 | 53 | ## Returns 54 | 55 | ### Ok Response 56 | 57 | When the rescan request successfully starts you will receive an Ok(true). 58 | After the rescan finishes, you'll see a log message telling all the blocks that may have any transactions to your wallet 59 | 60 | ### Error Enum 61 | 62 | This RPC command can only fail if: 63 | - we can't communicate with the headers database; 64 | - if invalid values are provided. That is, the start of the request being less than the stop value. 65 | 66 | If `timestamp` is true, apart the previous rules, if any of the values is smaller than the genesis block (1231006505 for mainnet) or greater than the tip time, the execution will also fail. 67 | 68 | Furthermore, the request will be aborted if the node still syncing with the blockchain. 69 | 70 | ## Notes 71 | 72 | - Be sure to not insert invalid values, e.g. the start being greater than the stop. 73 | 74 | - This rescan relies on BIP 158 block filters. 75 | 76 | - You dont need to be picky with timestamps but, when using uncertain timestamps you mostly want to set a high confidence which is not necessary for precise timestamps. -------------------------------------------------------------------------------- /tests/floresta-cli/getrpcinfo.py: -------------------------------------------------------------------------------- 1 | """ 2 | floresta_cli_getrpcinfo.py 3 | 4 | This functional test cli utility to interact with a Floresta node with `getrpcinfo` 5 | """ 6 | 7 | import os 8 | from test_framework import FlorestaTestFramework 9 | 10 | DATA_DIR = FlorestaTestFramework.get_integration_test_dir() 11 | 12 | 13 | class GetRpcInfoTest(FlorestaTestFramework): 14 | """ 15 | Test `getrpcinfo` rpc call, by creating a node 16 | """ 17 | 18 | nodes = [-1, -1] 19 | 20 | def set_test_params(self): 21 | """ 22 | Setup the two node florestad process with different data-dirs, electrum-addresses 23 | and rpc-addresses in the same regtest network 24 | """ 25 | # Create data directories for the nodes 26 | self.data_dirs = GetRpcInfoTest.create_data_dirs( 27 | DATA_DIR, self.__class__.__name__.lower(), nodes=2 28 | ) 29 | 30 | # Now create the nodes with the data directories 31 | self.florestad = self.add_node( 32 | variant="florestad", 33 | extra_args=[ 34 | f"--data-dir={self.data_dirs[0]}", 35 | ], 36 | ) 37 | 38 | self.bitcoind = self.add_node( 39 | variant="bitcoind", 40 | extra_args=[ 41 | f"-datadir={self.data_dirs[1]}", 42 | ], 43 | ) 44 | 45 | def assert_rpcinfo_structure(self, result, expected_logpath: str): 46 | # Ensure only 'active_commands' and 'logpath' are present 47 | self.assertEqual(set(result.keys()), {"active_commands", "logpath"}) 48 | self.assertEqual(len(result["active_commands"]), 1) 49 | 50 | # Ensure only 'duration' and 'method' are present in command 51 | command = result["active_commands"][0] 52 | self.assertEqual(set(command.keys()), {"duration", "method"}) 53 | 54 | # Check the command structure 55 | self.assertEqual(command["method"], "getrpcinfo") 56 | self.assertTrue(command["duration"] > 0) 57 | self.assertEqual(result["logpath"], os.path.normpath(expected_logpath)) 58 | 59 | def test_floresta_getrpcinfo(self): 60 | """ 61 | Test the `getrpcinfo` rpc call by creating a node 62 | and checking the response in florestad. 63 | """ 64 | result = self.florestad.rpc.get_rpcinfo() 65 | expected_logpath = os.path.join(self.data_dirs[0], "regtest", "debug.log") 66 | self.assert_rpcinfo_structure(result, expected_logpath) 67 | 68 | def test_bitcoind_getrpcinfo(self): 69 | """ 70 | Test the `getrpcinfo` rpc call by creating a node 71 | and checking the response in bitcoind. 72 | """ 73 | result = self.bitcoind.rpc.get_rpcinfo() 74 | expected_logpath = os.path.join(self.data_dirs[1], "regtest", "debug.log") 75 | self.assert_rpcinfo_structure(result, expected_logpath) 76 | 77 | def run_test(self): 78 | """ 79 | Run JSONRPC server on first, wait to connect, then call `addnode ip[:port]` 80 | """ 81 | # Start node 82 | self.run_node(self.florestad) 83 | self.run_node(self.bitcoind) 84 | 85 | # Test assertions 86 | self.test_floresta_getrpcinfo() 87 | self.test_bitcoind_getrpcinfo() 88 | 89 | 90 | if __name__ == "__main__": 91 | GetRpcInfoTest().main() 92 | -------------------------------------------------------------------------------- /tests/floresta-cli/getmemoryinfo.py: -------------------------------------------------------------------------------- 1 | """ 2 | floresta_cli_getmemoryinfo.py 3 | 4 | This functional test cli utility to interact with a Floresta node with `getmemoryinfo` 5 | """ 6 | 7 | import sys 8 | from test_framework import FlorestaTestFramework, Node 9 | 10 | 11 | class GetMemoryInfoTest(FlorestaTestFramework): 12 | """ 13 | Test `getmemoryinfo` rpc call, by creating a node and get 14 | some memory stats with `stats` and `mallocinfo` modes 15 | """ 16 | 17 | def set_test_params(self): 18 | """ 19 | Setup the two node florestad process with different data-dirs, electrum-addresses 20 | and rpc-addresses in the same regtest network 21 | """ 22 | self.florestad = self.add_node(variant="florestad") 23 | 24 | def test_mode_stats_ibd(self, node: Node): 25 | """ 26 | Test `getmemoryinfo stats` when node is in IBD. 27 | It should return a dictionary with key(str)/value(int). 28 | """ 29 | if sys.platform == "linux": 30 | result = node.rpc.get_memoryinfo("stats") 31 | self.assertIn("locked", result) 32 | self.assertIn("chunks_free", result["locked"]) 33 | self.assertIn("chunks_used", result["locked"]) 34 | self.assertIn("free", result["locked"]) 35 | self.assertIn("locked", result["locked"]) 36 | self.assertIn("total", result["locked"]) 37 | self.assertIn("used", result["locked"]) 38 | self.assertTrue(result["locked"]["chunks_free"] >= 0) 39 | self.assertTrue(result["locked"]["chunks_used"] >= 0) 40 | self.assertTrue(result["locked"]["free"] >= 0) 41 | self.assertTrue(result["locked"]["locked"] >= 0) 42 | self.assertTrue(result["locked"]["total"] >= 0) 43 | self.assertTrue(result["locked"]["used"] >= 0) 44 | else: 45 | self.log( 46 | f"Skipping test: 'getmemoryinfo stats' not implemented for '{sys.platform}'" 47 | ) 48 | 49 | def test_mode_mallocinfo_ibd(self, node): 50 | """ 51 | Test `getmemoryinfo mallocinfo` when node is in IBD 52 | It should return a malloc xml string. 53 | """ 54 | if sys.platform == "linux": 55 | pattern = ( 56 | r'' 57 | r'' 58 | r"\d+" 59 | r"\d+" 60 | r"\d+" 61 | r"\d+" 62 | r'' 63 | r"\d+" 64 | r"\d+" 65 | r"" 66 | r"" 67 | r"" 68 | ) 69 | result = node.rpc.get_memoryinfo("mallocinfo") 70 | self.assertMatch(result, pattern) 71 | else: 72 | self.log( 73 | f"Skipping test: 'getmemoryinfo malloc' not implemented for '{sys.platform}'" 74 | ) 75 | 76 | def run_test(self): 77 | """ 78 | Run JSONRPC server on first, wait to connect, then call `addnode ip[:port]` 79 | """ 80 | # Start node 81 | self.run_node(self.florestad) 82 | 83 | # Test assertions 84 | self.test_mode_stats_ibd(self.florestad) 85 | self.test_mode_mallocinfo_ibd(self.florestad) 86 | 87 | 88 | if __name__ == "__main__": 89 | GetMemoryInfoTest().main() 90 | -------------------------------------------------------------------------------- /tests/test_framework/electrum/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | tests/test_framework/electrum/base.py 3 | 4 | Base client to connect to Floresta's Electrum server. 5 | """ 6 | 7 | import json 8 | import socket 9 | from datetime import datetime, timezone 10 | from typing import Any, List, Tuple 11 | from OpenSSL import SSL 12 | 13 | 14 | # pylint: disable=too-few-public-methods 15 | class BaseClient: 16 | """ 17 | Helper class to connect to Floresta's Electrum server. 18 | """ 19 | 20 | def __init__(self, host, port=8080, tls=False): 21 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 22 | s.connect((host, port)) 23 | 24 | if tls: 25 | context = SSL.Context(SSL.TLS_METHOD) 26 | context.set_verify(SSL.VERIFY_NONE, lambda *args: True) 27 | tls_conn = SSL.Connection(context, s) 28 | tls_conn.set_connect_state() 29 | tls_conn.do_handshake() # Perform the TLS handshake 30 | self._conn = tls_conn 31 | else: 32 | self._conn = s 33 | 34 | @property 35 | def conn(self) -> socket.socket: 36 | """ 37 | Return the socket connection 38 | """ 39 | return self._conn 40 | 41 | @conn.setter 42 | def conn(self, value: socket.socket): 43 | """ 44 | Set the socket connection 45 | """ 46 | self._conn = value 47 | 48 | def log(self, msg): 49 | """ 50 | Log a message to the console 51 | """ 52 | print(f"[{self.__class__.__name__.upper()} {datetime.now(timezone.utc)}] {msg}") 53 | 54 | def request(self, method, params) -> object: 55 | """ 56 | Request something to Floresta server 57 | """ 58 | request = json.dumps( 59 | {"jsonrpc": "2.0", "id": 0, "method": method, "params": params} 60 | ) 61 | 62 | mnt_point = "/".join(method.split(".")) 63 | self.log(f"GET electrum://{mnt_point}?params={params}") 64 | self.conn.sendall(request.encode("utf-8") + b"\n") 65 | 66 | response = b"" 67 | while True: 68 | chunk = self.conn.recv(1) 69 | if not chunk: 70 | break 71 | response += chunk 72 | if b"\n" in response: 73 | break 74 | response = response.decode("utf-8").strip() 75 | self.log(response) 76 | 77 | return json.loads(response) 78 | 79 | def batch_request(self, calls: List[Tuple[str, List[Any]]]) -> object: 80 | """ 81 | Send batch JSON-RPC requests to electrum's server. 82 | """ 83 | request_map = { 84 | i: {"jsonrpc": "2.0", "id": i, "method": method, "params": params} 85 | for i, (method, params) in enumerate(calls) 86 | } 87 | 88 | request_list = list(request_map.values()) 89 | self.log( 90 | "BATCH " 91 | + ", ".join( 92 | f"electrum://{'/'.join(m.split('.'))}?params={p}" for m, p in calls 93 | ) 94 | ) 95 | self.conn.sendall(json.dumps(request_list).encode("utf-8") + b"\n") 96 | 97 | response = b"" 98 | while True: 99 | chunk = self.conn.recv(1) 100 | if not chunk: 101 | break 102 | response += chunk 103 | if b"\n" in response: 104 | break 105 | 106 | response = response.decode("utf-8").strip() 107 | self.log(response) 108 | return response 109 | -------------------------------------------------------------------------------- /contrib/dist/gen_manpages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Convert all markdown files in /doc/rpc/ to man pages in /doc/rpc_man/ 3 | # Must have pandoc installed 4 | 5 | set -euo pipefail 6 | 7 | check_installed() { 8 | if ! command -v "$1" &>/dev/null; then 9 | echo "You must have $1 installed to run this script!" 10 | exit 1 11 | fi 12 | } 13 | 14 | check_dependencies() { 15 | check_installed "pandoc" 16 | check_installed "gzip" 17 | } 18 | 19 | # Get the script directory and project root 20 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 21 | PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" 22 | SOURCE_DIR="$PROJECT_ROOT/doc/rpc" 23 | TARGET_DIR="$PROJECT_ROOT/doc/rpc_man" 24 | 25 | # Function to convert a single markdown file to man page 26 | convert_single() { 27 | local file="$1" 28 | if [[ ! -f "$file" ]]; then 29 | echo "Error: File $file not found" 30 | exit 1 31 | fi 32 | 33 | # Extract filename without extension 34 | local basename=$(basename "$file" .md) 35 | 36 | # Skip the template stub file 37 | if [[ "$basename" == "template" ]]; then 38 | echo "Skipping template file: $file" 39 | return 0 40 | fi 41 | 42 | # Create man page directory if it doesn't exist 43 | mkdir -p "$TARGET_DIR" 44 | 45 | # Convert markdown to man page using pandoc 46 | # Default to section 1 (user commands) unless specified in filename 47 | local section=1 48 | if [[ "$basename" =~ \.[0-9]$ ]]; then 49 | section="${basename##*.}" 50 | basename="${basename%.*}" 51 | fi 52 | 53 | # Convert to man page format 54 | pandoc "$file" \ 55 | -s \ 56 | -t man \ 57 | --metadata title="$basename" \ 58 | --metadata section="$section" \ 59 | --metadata date="$(date +'%B %Y')" \ 60 | -o "$TARGET_DIR/${basename}.${section}" 61 | 62 | # Compress the man page 63 | gzip -f "$TARGET_DIR/${basename}.${section}" 64 | 65 | echo "Created: $TARGET_DIR/${basename}.${section}.gz" 66 | } 67 | 68 | check_dependencies 69 | 70 | # Main logic 71 | if [[ $# -eq 1 ]]; then 72 | # Convert single file if argument provided 73 | # Handle both absolute and relative paths 74 | if [[ "$1" = /* ]]; then 75 | # Absolute path 76 | convert_single "$1" 77 | else 78 | # Relative path - check if it exists as-is, otherwise try in SOURCE_DIR 79 | if [[ -f "$1" ]]; then 80 | convert_single "$1" 81 | elif [[ -f "$SOURCE_DIR/$1" ]]; then 82 | convert_single "$SOURCE_DIR/$1" 83 | else 84 | echo "Error: File $1 not found" 85 | exit 1 86 | fi 87 | fi 88 | else 89 | # Convert all markdown files in doc/rpc/ 90 | echo "Converting all markdown files in $SOURCE_DIR to man pages..." 91 | 92 | # Check if source directory exists 93 | if [[ ! -d "$SOURCE_DIR" ]]; then 94 | echo "Error: Source directory $SOURCE_DIR not found" 95 | exit 1 96 | fi 97 | 98 | # Check if there are any .md files 99 | if ! ls "$SOURCE_DIR"/*.md &>/dev/null; then 100 | echo "No markdown files found in $SOURCE_DIR" 101 | exit 1 102 | fi 103 | 104 | # Convert each .md file to man page 105 | for md_file in "$SOURCE_DIR"/*.md; do 106 | if [[ -f "$md_file" ]]; then 107 | echo "Converting $md_file..." 108 | convert_single "$md_file" 109 | fi 110 | done 111 | 112 | echo "All markdown files converted to man pages in $TARGET_DIR/" 113 | fi 114 | -------------------------------------------------------------------------------- /tests/florestad/reorg-chain.py: -------------------------------------------------------------------------------- 1 | """ 2 | Chain reorg test 3 | 4 | This test will spawn a florestad and a utreexod, we will use utreexod to mine some blocks. 5 | Then we will invalidate one of those blocks, and mine an alternative chain. This should 6 | make florestad switch to the new chain. We then compare the two node's main chain and 7 | accumulator to make sure they are the same. 8 | """ 9 | 10 | import re 11 | import time 12 | 13 | from test_framework import FlorestaTestFramework 14 | 15 | 16 | class ChainReorgTest(FlorestaTestFramework): 17 | """Test the reorganization of the chain in florestad when using utreexod to mine blocks.""" 18 | 19 | expected_chain = "regtest" 20 | 21 | def set_test_params(self): 22 | self.florestad = self.add_node(variant="florestad") 23 | 24 | self.utreexod = self.add_node( 25 | variant="utreexod", 26 | extra_args=[ 27 | "--miningaddr=bcrt1q4gfcga7jfjmm02zpvrh4ttc5k7lmnq2re52z2y", 28 | "--utreexoproofindex", 29 | "--prune=0", 30 | ], 31 | ) 32 | 33 | def run_test(self): 34 | # Start the nodes 35 | self.run_node(self.florestad) 36 | self.run_node(self.utreexod) 37 | 38 | # Mine some blocks with utreexod 39 | self.log("=== Mining blocks with utreexod") 40 | self.utreexod.rpc.generate(10) 41 | 42 | self.log("=== Connect floresta to utreexod") 43 | host = self.florestad.get_host() 44 | port = self.utreexod.get_port("p2p") 45 | self.florestad.rpc.addnode( 46 | f"{host}:{port}", command="onetry", v2transport=False 47 | ) 48 | time.sleep(5) 49 | 50 | self.log("=== Waiting for floresta to connect to utreexod.rpc...") 51 | peer_info = self.florestad.rpc.get_peerinfo() 52 | self.assertMatch( 53 | peer_info[0]["user_agent"], 54 | re.compile(r"/btcwire:\d+\.\d+\.\d+/utreexod:\d+\.\d+\.\d+/"), 55 | ) 56 | 57 | self.log("=== Wait for the nodes to sync...") 58 | time.sleep(20) 59 | 60 | self.log("=== Check that floresta has the same chain as utreexod.rpc...") 61 | floresta_chain = self.florestad.rpc.get_blockchain_info() 62 | utreexo_chain = self.utreexod.rpc.get_blockchain_info() 63 | self.assertEqual(floresta_chain["best_block"], utreexo_chain["bestblockhash"]) 64 | self.assertEqual(floresta_chain["height"], utreexo_chain["blocks"]) 65 | 66 | self.log("=== Get a block hash from utreexod to invalidate") 67 | hash = self.utreexod.rpc.get_blockhash(5) 68 | self.utreexod.rpc.invalidate_block(hash) 69 | 70 | self.log("=== Mining alternative chain with utreexod.rpc...") 71 | self.utreexod.rpc.generate(10) 72 | 73 | self.log("=== Wait for the nodes to sync") 74 | time.sleep(20) 75 | 76 | self.log("=== Check that floresta has switched to the new chain") 77 | floresta_chain = self.florestad.rpc.get_blockchain_info() 78 | utreexo_chain = self.utreexod.rpc.get_blockchain_info() 79 | self.assertEqual(floresta_chain["best_block"], utreexo_chain["bestblockhash"]) 80 | self.assertEqual(floresta_chain["height"], utreexo_chain["blocks"]) 81 | 82 | self.log("=== Compare the accumulator roots for each node") 83 | floresta_roots = self.florestad.rpc.get_roots() 84 | utreexo_roots = self.utreexod.rpc.get_utreexo_roots( 85 | utreexo_chain["bestblockhash"] 86 | ) 87 | self.assertEqual(floresta_roots, utreexo_roots["roots"]) 88 | 89 | 90 | if __name__ == "__main__": 91 | ChainReorgTest().main() 92 | -------------------------------------------------------------------------------- /metrics/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | use std::sync::atomic::AtomicU64; 3 | use std::sync::OnceLock; 4 | 5 | use axum::routing::get; 6 | use axum::Router; 7 | use prometheus_client::encoding::text::encode; 8 | use prometheus_client::metrics::gauge::Gauge; 9 | use prometheus_client::metrics::histogram::Histogram; 10 | use prometheus_client::registry::Registry; 11 | use sysinfo::System; 12 | 13 | pub struct AppMetrics { 14 | registry: Registry, 15 | pub memory_usage: Gauge, 16 | pub block_height: Gauge, 17 | pub peer_count: Gauge, 18 | pub avg_block_processing_time: Gauge, 19 | pub message_times: Histogram, 20 | } 21 | 22 | impl AppMetrics { 23 | pub fn new() -> Self { 24 | let mut registry = ::default(); 25 | let memory_usage = Gauge::::default(); 26 | let block_height = Gauge::default(); 27 | let peer_count = Gauge::::default(); 28 | let avg_block_processing_time = Gauge::::default(); 29 | let message_times = Histogram::new([0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0].into_iter()); 30 | 31 | registry.register("block_height", "Current block height", block_height.clone()); 32 | registry.register( 33 | "peer_count", 34 | "Number of connected peers", 35 | peer_count.clone(), 36 | ); 37 | 38 | registry.register( 39 | "avg_block_processing_time", 40 | "Average block processing time in seconds", 41 | avg_block_processing_time.clone(), 42 | ); 43 | 44 | registry.register( 45 | "memory_usage_gigabytes", 46 | "System memory usage in GB", 47 | memory_usage.clone(), 48 | ); 49 | 50 | registry.register( 51 | "message_times", 52 | "A time-series of how long our peers take to respond to our requests. Timed out requests are not included.", 53 | message_times.clone(), 54 | ); 55 | 56 | Self { 57 | registry, 58 | block_height, 59 | memory_usage, 60 | peer_count, 61 | avg_block_processing_time, 62 | message_times, 63 | } 64 | } 65 | 66 | /// Gets how much memory is being used by the system in which Floresta is 67 | /// running on, not how much memory Floresta itself it's using. 68 | pub fn update_memory_usage(&self) { 69 | let mut system = System::new_all(); 70 | system.refresh_memory(); 71 | 72 | // get used memory in gigabytes / KB / MB / GB 73 | let used_memory = system.used_memory() as f64 / 1024. / 1024. / 1024.; 74 | self.memory_usage.set(used_memory); 75 | } 76 | } 77 | 78 | impl Default for AppMetrics { 79 | fn default() -> Self { 80 | Self::new() 81 | } 82 | } 83 | 84 | // Singleton to share metrics across crates 85 | static METRICS: OnceLock = OnceLock::new(); 86 | pub fn get_metrics() -> &'static AppMetrics { 87 | METRICS.get_or_init(AppMetrics::new) 88 | } 89 | 90 | async fn metrics_handler() -> String { 91 | let mut buffer = String::new(); 92 | encode(&mut buffer, &get_metrics().registry).unwrap(); 93 | 94 | buffer 95 | } 96 | 97 | pub async fn metrics_server(metrics_server_address: SocketAddr) { 98 | let app = Router::new().route("/", get(metrics_handler)); 99 | let listener = tokio::net::TcpListener::bind(metrics_server_address) 100 | .await 101 | .unwrap(); 102 | axum::serve(listener, app).await.unwrap(); 103 | } 104 | -------------------------------------------------------------------------------- /tests/test_framework/daemon/utreexo.py: -------------------------------------------------------------------------------- 1 | """ 2 | test_framework.daemon.utreexo.py 3 | 4 | A test framework for testing utreexod daemon in regtest mode. 5 | """ 6 | 7 | from typing import List 8 | 9 | from test_framework.daemon.base import BaseDaemon 10 | 11 | 12 | class UtreexoDaemon(BaseDaemon): 13 | """ 14 | Spawn a new utreexod process on background and run it on 15 | regtest mode for tests. You can use it to generate blocks 16 | and utreexo proofs for tests. 17 | """ 18 | 19 | def create(self, target: str): 20 | self.name = "utreexod" 21 | self.target = target 22 | 23 | def valid_daemon_args(self) -> List[str]: 24 | return [ 25 | "--datadir", 26 | "--logdir", 27 | "-C,", 28 | "--configfile", 29 | "-d,", 30 | "--debuglevel", 31 | "--dbtype", 32 | "--sigcachemaxsize", 33 | "--utxocachemaxsize", 34 | "--noutreexo", 35 | "--prune", 36 | "--profile", 37 | "--cpuprofile", 38 | "--memprofile", 39 | "--traceprofile", 40 | "--testnet", 41 | "--regtest", 42 | "--notls", 43 | "--norpc", 44 | "--rpccert", 45 | "--rpckey", 46 | "--rpclimitpass", 47 | "--rpclimituser", 48 | "--rpclisten", 49 | "--rpcmaxclients", 50 | "--rpcmaxconcurrentreqs", 51 | "--rpcmaxwebsockets", 52 | "--rpcquirks", 53 | "--proxy", 54 | "--proxypass", 55 | "--proxyuser", 56 | "-a", 57 | "--addpeer", 58 | "--connect", 59 | "--listen", 60 | "--nolisten", 61 | "--maxpeers", 62 | "--uacomment", 63 | "--trickleinterval", 64 | "--nodnsseed", 65 | "--externalip", 66 | "--upnp", 67 | "--agentblacklist", 68 | "--agentwhitelist", 69 | "--whitelist", 70 | "--nobanning", 71 | "--banduration", 72 | "--banthreshold", 73 | "--addcheckpoint", 74 | "--nocheckpoints", 75 | "--noassumeutreexo", 76 | "--blocksonly", 77 | "--maxorphantx", 78 | "--minrelaytxfee", 79 | "--norelaypriority", 80 | "--relaynonstd", 81 | "--rejectnonstd", 82 | "--rejectreplacement", 83 | "--limitfreerelay", 84 | "--generate", 85 | "--miningaddr", 86 | "--blockmaxsize", 87 | "--blockminsize", 88 | "--blockmaxweight", 89 | "--blockminweight", 90 | "--blockprioritysize", 91 | "--addrindex", 92 | "--txindex", 93 | "--utreexoproofindex", 94 | "--flatutreexoproofindex", 95 | "--utreexoproofindexmaxmemory", 96 | "--cfilters", 97 | "--nopeerbloomfilters", 98 | "--dropaddrindex", 99 | "--dropcfindex", 100 | "--droptxindex", 101 | "--droputreexoproofindex", 102 | "--dropflatutreexoproofindex", 103 | "--watchonlywallet", 104 | "--registeraddresstowatchonlywallet", 105 | "--registerextendedpubkeystowatchonlywallet", 106 | "--registerextendedpubkeyswithaddresstypetowatchonlywallet", 107 | "--nobdkwallet", 108 | "--electrumlisteners", 109 | "--tlselectrumlisteners", 110 | "--disableelectrum", 111 | ] 112 | -------------------------------------------------------------------------------- /crates/floresta-common/src/spsc.rs: -------------------------------------------------------------------------------- 1 | //! A no-std Single Producer, Single Consumer channel for unidirectional message exchange between 2 | //! modules. This module don't use anything from the standard lib and can be easily used in no-std 3 | //! environments. We only use mem::take from [core]. 4 | 5 | use core::mem::take; 6 | 7 | use crate::prelude::Vec; 8 | 9 | /// A (Send + Sync) single producer, single consumer channel to notify modules about things. 10 | /// The api is super minimalistic to reduce external dependencies, including from the std-lib 11 | /// 12 | /// One notable difference from the standard mspc channel is that this channel's ends are't 13 | /// two different types, while this is possible, there's no reason to do that. Specially 14 | /// considering that to get a good compile-time asurance that both ends will not be shared, the 15 | /// channel must not be [Send], but this is one of the main requirements to use this channel in 16 | /// async code. Moreover, if two worker threads are meant to be identical threads balancing their 17 | /// work, it might be beneficial to use this same channel as a de-facto single producer, multiple 18 | /// consumer channel for work distribution. 19 | /// # Example 20 | /// ``` 21 | /// use floresta_common::spsc; 22 | /// let channel = spsc::Channel::new(); 23 | /// 24 | /// // Send something 25 | /// channel.send(1); 26 | /// // Read the same thing back 27 | /// assert_eq!(channel.recv().next(), Some(1)); 28 | /// ``` 29 | #[derive(Debug, Default)] 30 | pub struct Channel { 31 | /// The data pending for read 32 | content: spin::Mutex>, 33 | } 34 | 35 | impl Channel { 36 | /// Creates a new channel 37 | /// 38 | /// # Example 39 | /// ``` 40 | /// use floresta_common::spsc; 41 | /// let channel = spsc::Channel::new(); 42 | /// 43 | /// channel.send(1); 44 | /// assert_eq!(channel.recv().next(), Some(1)); 45 | /// ``` 46 | pub fn new() -> Self { 47 | Channel { 48 | content: spin::Mutex::new(Vec::new()), 49 | } 50 | } 51 | /// Sends some data through a channel 52 | /// 53 | /// # Example 54 | /// ``` 55 | /// use floresta_common::spsc; 56 | /// let channel = spsc::Channel::new(); 57 | /// 58 | /// channel.send(1); 59 | /// assert_eq!(channel.recv().next(), Some(1)); 60 | /// ``` 61 | pub fn send(&self, data: T) { 62 | self.content.lock().push(data); 63 | } 64 | /// Reads from a channel 65 | /// 66 | /// This method returns an iterator over all elements inside a [Channel] 67 | pub fn recv(&self) -> RecvIter { 68 | let inner = take(&mut *self.content.lock()); 69 | RecvIter { inner } 70 | } 71 | } 72 | 73 | /// An iterator issued every time someone calls `recv`. 74 | /// 75 | /// This iterator takes all items available for reading in a channel 76 | /// and lets the consumer iterate over them, without acquiring the lock 77 | /// every time (the mutex is only locked when `recv` is called). 78 | /// 79 | /// # Example 80 | /// ``` 81 | /// use floresta_common::spsc; 82 | /// let channel = spsc::Channel::new(); 83 | /// 84 | /// channel.send(0); 85 | /// channel.send(1); 86 | /// 87 | /// for (i, el) in channel.recv().enumerate() { 88 | /// assert_eq!(i, el); 89 | /// } 90 | /// // A second read should create an empty iterator 91 | /// assert_eq!(channel.recv().next(), None); 92 | /// ``` 93 | pub struct RecvIter { 94 | inner: Vec, 95 | } 96 | 97 | impl Iterator for RecvIter { 98 | type Item = T; 99 | fn next(&mut self) -> Option { 100 | if self.inner.is_empty() { 101 | return None; 102 | } 103 | Some(self.inner.remove(0)) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /crates/floresta-wire/README.md: -------------------------------------------------------------------------------- 1 | # floresta-wire 2 | 3 | `floresta-wire` is the P2P layer for the Floresta project: a lightweight, Utreexo-powered Bitcoin node. It supports Bitcoin P2P transport v1 and v2/BIP324, with peer discovery via DNS seeds, hardcoded addresses, and user-provided peers. It learns about new blocks and transactions, selects the best header chain, performs IBD, maintains a mempool, and stays in sync with the network. 4 | 5 | It also supports compact block filters, which are particularly helpful for rescans because Floresta is fully pruned, and an optional SOCKS5 proxy to route all P2P traffic and DNS seed queries. 6 | 7 | This crate delegates chain state, validation, and persistence to [`floresta-chain`](https://github.com/vinteumorg/Floresta/tree/master/crates/floresta-chain) and is used by the `florestad` binary. 8 | 9 | ## Usage 10 | 11 | Construct a node with `UtreexoNode`: 12 | 13 | ```rust 14 | let node = UtreexoNode::<_, RunningNode>::new( 15 | /* UtreexoNodeConfig */, 16 | /* ChainState */, 17 | /* Mempool */, 18 | /* Optional compact block filters */, 19 | /* Kill signal */, 20 | /* AddressMan */, 21 | ).unwrap(); 22 | ``` 23 | 24 | Where: 25 | 26 | - `UtreexoNodeConfig`, `Mempool`, and `AddressMan` are defined in this crate. 27 | - `ChainState` is provided by `floresta-chain` (implements the `ChainBackend` trait). 28 | - Optional compact filters use `NetworkFilters` from `floresta-compact-filters`. 29 | - The kill signal is an `Arc>`. Set it to `true` to stop the node. 30 | 31 | `UtreexoNode` can run in three contexts: `RunningNode` (default, high-level), `SyncNode` (IBD), and `ChainSelector` (header chain selection). In most cases you want the default `RunningNode` context, which delegates to `SyncNode`/`ChainSelector` as needed based on the current phase. 32 | 33 | > To start the node, regardless of context, call the `run` method. To stop it, set the kill signal you passed to the builder to `true`. 34 | 35 | ## Peer sources 36 | 37 | Just after the node startup, it will attempt to discover and/or load peers in the following order: 38 | 39 | - `peers.json`: a file in the node's data directory that can contain user-provided peers. If not present, this file will be created after the first run. Its purpose is to **store all known peers** and their metadata. 40 | - DNS seeds: active peers fetched via the system DNS resolver, or (if a proxy is configured) via DNS-over-HTTPS routed through SOCKS5. We usually get a few hundred peers with this method. DNS seeds can be disabled with `UtreexoNodeConfig.disable_dns_seeds`. 41 | - Hardcoded addresses (from the `seeds` directory): used only if the node is not connected to any peers within one minute of startup. Acts as a fallback when the previous two methods are unused or fail. 42 | 43 | Once the node has at least one peer, it will be able to discover additional peers through the P2P network's address gossip (all of them saved in `peers.json`). 44 | 45 | Example `peers.json`: 46 | 47 | ```json 48 | [ 49 | { 50 | "address": { 51 | "V4": "1.228.21.110" 52 | }, 53 | "last_connected": 1678986166, 54 | "state": { 55 | "Tried": 0 56 | }, 57 | "services": 50331657, 58 | "port": 8333 59 | } 60 | ] 61 | ``` 62 | 63 | ## Cargo features 64 | 65 | - `metrics`: when enabled, `floresta-wire` links in a small Prometheus exporter so your node can expose runtime metrics over HTTP (start with `metrics::metrics_server(addr)`). 66 | 67 | ## Minimum Supported Rust Version (MSRV) 68 | 69 | This library should compile with any combination of features on **Rust 1.81.0**. 70 | 71 | ## License 72 | 73 | `floresta-wire` is released under the terms of the MIT license. See [LICENSE](https://github.com/vinteumorg/Floresta/blob/master/LICENSE) for more information or see https://opensource.org/licenses/MIT. 74 | -------------------------------------------------------------------------------- /tests/floresta-cli/getbestblockhash.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the floresta's `getbestblockhash` after mining a few block with 3 | utreexod. Then, assert that the command returns the same hash of 4 | `best_block` or `bestblockhash` given in `getblockchaininfo` of floresta 5 | and utreexod, respectively. 6 | """ 7 | 8 | import re 9 | import time 10 | from test_framework import FlorestaTestFramework 11 | 12 | 13 | class GetBestblockhashTest(FlorestaTestFramework): 14 | """ 15 | Test florestad's `getbestblockhash` by running two nodes, the first 16 | the florestad itself and utreexod as miner nodes: 17 | (1) Get the genesis block with `getbestblockhash`; 18 | (2) mine some blocks with utreexod; 19 | (3) connect florestad to utreexod; 20 | (4) wait for the nodes to sync; 21 | (5) check that `getbestblockhash` returns the same hash as 22 | `best_block` or `bestblockhash` given in `getblockchaininfo` 23 | of floresta and utreexod, respectively. 24 | """ 25 | 26 | best_block = "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206" 27 | 28 | def set_test_params(self): 29 | """ 30 | Setup a florestad node and a utreexod mining node 31 | """ 32 | self.florestad = self.add_node(variant="florestad") 33 | 34 | self.utreexod = self.add_node( 35 | variant="utreexod", 36 | extra_args=[ 37 | "--miningaddr=bcrt1q4gfcga7jfjmm02zpvrh4ttc5k7lmnq2re52z2y", 38 | "--prune=0", 39 | ], 40 | ) 41 | 42 | def run_test(self): 43 | """ 44 | Run a florestad node and mine some blocks with utreexod. After that, 45 | connect floresta to utreexod and wait for the nodes to sync. 46 | Finally, test the `getbestblockhash` rpc command checking if it's 47 | different from genesis one and equals to utreexod one. 48 | """ 49 | self.run_node(self.florestad) 50 | self.run_node(self.utreexod) 51 | 52 | self.log("=== Get genesis blockhash from floresta...") 53 | genesis_block = self.florestad.rpc.get_bestblockhash() 54 | 55 | self.log("=== Mining blocks with utreexod") 56 | self.utreexod.rpc.generate(10) 57 | 58 | self.log("=== Connect floresta to utreexod") 59 | host = self.florestad.get_host() 60 | port = self.utreexod.get_port("p2p") 61 | self.florestad.rpc.addnode( 62 | f"{host}:{port}", command="onetry", v2transport=False 63 | ) 64 | 65 | self.log("=== Waiting for floresta to connect to utreexod...") 66 | time.sleep(5) 67 | peer_info = self.florestad.rpc.get_peerinfo() 68 | self.assertMatch( 69 | peer_info[0]["user_agent"], 70 | re.compile(r"/btcwire:\d+\.\d+\.\d+/utreexod:\d+\.\d+\.\d+/"), 71 | ) 72 | 73 | self.log("=== Wait for the nodes to sync...") 74 | time.sleep(20) 75 | 76 | self.log("=== Check that floresta has the same chain as utreexod...") 77 | floresta_chain = self.florestad.rpc.get_blockchain_info() 78 | utreexo_chain = self.utreexod.rpc.get_blockchain_info() 79 | self.assertEqual(floresta_chain["validated"], 10) 80 | self.assertEqual(floresta_chain["best_block"], utreexo_chain["bestblockhash"]) 81 | self.assertEqual(floresta_chain["height"], utreexo_chain["blocks"]) 82 | 83 | self.log("=== Get tip block in the most-work fully-validated chain") 84 | floresta_best_block = self.florestad.rpc.get_bestblockhash() 85 | self.assertNotEqual(floresta_best_block, genesis_block) 86 | self.assertEqual(floresta_best_block, floresta_chain["best_block"]) 87 | self.assertEqual(floresta_best_block, utreexo_chain["bestblockhash"]) 88 | 89 | 90 | if __name__ == "__main__": 91 | GetBestblockhashTest().main() 92 | -------------------------------------------------------------------------------- /bin/florestad/docs/tutorial(EN).md: -------------------------------------------------------------------------------- 1 | ## Tutorial 2 | 3 | ### Introduction 4 | 5 | This program is a small node implementation with an integrated Electrum Server. It behaves similarly to a Bitcoin Core + Electrum Personal Server setup, but with some key differences. 6 | 7 | - Node and Electrum Server are in the same binary, making the process simpler and less error-prone. 8 | - The full node uses a new technology called `Utreexo` to reduce resource consumption, allowing you to run the node with less than 1GB of disk and RAM. 9 | - Unlike EPS, this Electrum Server supports multiple simultaneous connections. 10 | 11 | ### Usage 12 | 13 | There are two ways to obtain the executable. You can compile from source code or download the pre-compiled binary from Github. For instructions on how to compile the source code, [see below](#compiling). Information on how to run it is [here](#running) 14 | 15 | ### Compiling 16 | 17 | To compile, you need the Rust toolchain and Cargo, more information [here](https://www.rust-lang.org/). 18 | You can obtain the source code by downloading from Github or cloning with 19 | 20 | ```bash 21 | git clone https://github.com/vinteumorg/Floresta.git 22 | ``` 23 | 24 | Navigate into the folder with 25 | 26 | ```bash 27 | cd Floresta/ 28 | ``` 29 | 30 | compile with: 31 | 32 | ```bash 33 | cargo build --release 34 | ``` 35 | if everything is ok, it will compile the program and save the executable in `./target/release/`. 36 | 37 | ### Running 38 | 39 | Before running it for the first time, you need to extract the xpub from your wallet. In Electrum, just go to the "Wallet" menu and click on "Information", the xpub will appear in a large text box. 40 | 41 | Once you have the Extended Public Key in hand, copy the configuration file `config.toml.sample` to `config.toml` and edit it by inserting the xpub in the appropriate field. You can insert infinite xpubs. Loose addresses are also allowed. 42 | 43 | For multisig addresses, a wallet like Sparrow is recommended. Simply copy the "output descriptor" generated by it. 44 | 45 | See [below](#config_example) for an example of a valid file. 46 | 47 | ```bash 48 | floresta -c config.toml --network signet run 49 | ``` 50 | or 51 | 52 | ```bash 53 | ./target/release/floresta -c config.toml --network signet run 54 | ``` 55 | or 56 | 57 | ```bash 58 | cargo run --release -- -c config.toml --network signet run 59 | ``` 60 | Where: 61 | 62 | - `network` is the network you're using, bitcoin means mainnet, other valid values are signet, regtest, and testnet. These are all test networks that are functionally identical to the main network (mainnet), but used only for testing, and their coins have no value. 63 | 64 | If everything goes right, it will start synchronization and show the progress on screen. From the moment you see 65 | > Server running on: 0.0.0.0:50001 66 | 67 | you can already connect your wallet to the server, but the balance might take a while to appear. 68 | 69 | ### Configuration File Example 70 | ```toml 71 | [wallet] 72 | xpubs = [ 73 | "vpub5ZkWR4krsAzwFsmW8Yp3eHuANVLr7ZSWii6KmRnLRiN6ZXLbqs1f217jJM37oteQoyng82yw44XQU8PYJJBGgVzvJ96dQZEyZZcDiDmoJXw", 74 | "vpub5V5XF4ipcQ9tLp7NCFswnwZ23tm5Key81E9CCfqFXaGjzTpQ8jjiirf2hG7aXtqXbRDFxMvEhdGdeFcqQ3jUGUkq4mqo2VoGCDWCZvPQvUy", 75 | ] 76 | addresses = [ 77 | "tb1qjfplwf7a2dpjj04cx96rysqeastvycc0j50cch" 78 | ] 79 | descriptors = [ 80 | "wsh(sortedmulti(1,[54ff5a12/48h/1h/0h/2h]tpubDDw6pwZA3hYxcSN32q7a5ynsKmWr4BbkBNHydHPKkM4BZwUfiK7tQ26h7USm8kA1E2FvCy7f7Er7QXKF8RNptATywydARtzgrxuPDwyYv4x/<0;1>/*,[bcf969c0/48h/1h/0h/2h]tpubDEFdgZdCPgQBTNtGj4h6AehK79Jm4LH54JrYBJjAtHMLEAth7LuY87awx9ZMiCURFzFWhxToRJK6xp39aqeJWrG5nuW3eBnXeMJcvDeDxfp/<0;1>/*))#fuw35j0q" 81 | ] 82 | ``` 83 | ### Screenshot of Program Running 84 | 85 | ![A screenshot of logs from a Floresta instance running in a terminal on a GNU/Linux distribution](./assets/Screenshot_ibd.jpg) 86 | 87 | 88 | -------------------------------------------------------------------------------- /crates/floresta-compact-filters/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | //! A library for building and querying BIP-158 compact block filters locally 4 | //! 5 | //! This lib implements BIP-158 client-side Galomb-Rice block filters, without 6 | //! relaying on p2p connections to retrieve them. We use this to speedup wallet 7 | //! resyncs and allow arbitrary UTXO retrieving for lightning nodes. 8 | //! 9 | //! This module should receive blocks as we download them, it'll create a filter 10 | //! for it. Therefore, you can't use this to speedup wallet sync **before** IBD, 11 | //! since we wouldn't have the filter for all blocks yet. 12 | 13 | #![cfg_attr(docsrs, feature(doc_cfg))] 14 | #![allow(clippy::manual_is_multiple_of)] 15 | 16 | use core::fmt::Debug; 17 | use std::fmt::Display; 18 | use std::sync::PoisonError; 19 | use std::sync::RwLockWriteGuard; 20 | 21 | use bitcoin::bip158; 22 | use flat_filters_store::FlatFiltersStore; 23 | 24 | pub mod flat_filters_store; 25 | pub mod network_filters; 26 | 27 | /// A database that stores our compact filters 28 | pub trait BlockFilterStore: Send + Sync { 29 | /// Fetches a block filter 30 | fn get_filter(&self, block_height: u32) -> Option; 31 | /// Stores a new filter 32 | fn put_filter(&self, block_height: u32, block_filter: bip158::BlockFilter); 33 | /// Persists the height of the last filter we have 34 | fn put_height(&self, height: u32); 35 | /// Fetches the height of the last filter we have 36 | fn get_height(&self) -> Option; 37 | } 38 | 39 | pub enum IterableFilterStoreError { 40 | /// I/O error 41 | Io(std::io::Error), 42 | /// End of the file 43 | Eof, 44 | /// Lock error 45 | Poisoned, 46 | /// Filter too large, probably a bug 47 | FilterTooLarge, 48 | } 49 | 50 | impl Debug for IterableFilterStoreError { 51 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 52 | match self { 53 | IterableFilterStoreError::Io(e) => write!(f, "I/O error: {e}"), 54 | IterableFilterStoreError::Eof => write!(f, "End of file"), 55 | IterableFilterStoreError::Poisoned => write!(f, "Lock poisoned"), 56 | IterableFilterStoreError::FilterTooLarge => write!(f, "Filter too large"), 57 | } 58 | } 59 | } 60 | 61 | impl From for IterableFilterStoreError { 62 | fn from(e: std::io::Error) -> Self { 63 | IterableFilterStoreError::Io(e) 64 | } 65 | } 66 | 67 | impl Display for IterableFilterStoreError { 68 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 69 | Debug::fmt(self, f) 70 | } 71 | } 72 | 73 | impl From>> for IterableFilterStoreError { 74 | fn from(_: PoisonError>) -> Self { 75 | IterableFilterStoreError::Poisoned 76 | } 77 | } 78 | 79 | pub trait IterableFilterStore: 80 | Send + Sync + IntoIterator 81 | { 82 | type I: Iterator; 83 | /// Fetches the first filter and sets our internal cursor to the first filter, 84 | /// succeeding calls to [next()](std::iter::Iterator::next) will return the next filter until we reach the end 85 | fn iter(&self, start_height: Option) -> Result; 86 | /// Writes a new filter to the store 87 | fn put_filter( 88 | &self, 89 | block_filter: bip158::BlockFilter, 90 | height: u32, 91 | ) -> Result<(), IterableFilterStoreError>; 92 | /// Persists the height of the last filter we have 93 | fn set_height(&self, height: u32) -> Result<(), IterableFilterStoreError>; 94 | /// Fetches the height of the last filter we have 95 | fn get_height(&self) -> Result; 96 | } 97 | -------------------------------------------------------------------------------- /tests/floresta-cli/getblock.py: -------------------------------------------------------------------------------- 1 | """ 2 | floresta_cli_getblock.py 3 | 4 | This functional test cli utility to interact with a Floresta node with `getblock` 5 | """ 6 | 7 | from test_framework import FlorestaTestFramework 8 | from test_framework.rpc.floresta import REGTEST_RPC_SERVER 9 | 10 | 11 | class GetBlockTest(FlorestaTestFramework): 12 | """ 13 | Test `getblock` with a fresh node and the first block with verbose levels 0 and 1 14 | """ 15 | 16 | nodes = [-1] 17 | block = "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206" 18 | 19 | # pylint: disable=line-too-long 20 | serialized_data = "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff7f20020000000101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000" 21 | 22 | bits = "ffff7f20" 23 | chainwork = "2" 24 | confirmations = 1 25 | difficulty = 1 26 | hash = "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206" 27 | height = 0 28 | mediantime = 1296688602 29 | merkleroot = "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b" 30 | n_tx = 1 31 | nonce = 2 32 | prev_blockhash = "0000000000000000000000000000000000000000000000000000000000000000" 33 | size = 285 34 | strippedsize = 285 35 | time = 1296688602 36 | tx = ["4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"] 37 | version = 1 38 | version_hex = "01000000" 39 | weight = 1140 40 | 41 | def set_test_params(self): 42 | """ 43 | Setup a single node 44 | """ 45 | self.florestad = self.add_node(variant="florestad") 46 | 47 | def run_test(self): 48 | """ 49 | Run JSONRPC server and get some data about first block 50 | """ 51 | self.run_node(self.florestad) 52 | 53 | # Test verbose level 0 54 | response = self.florestad.rpc.get_block(GetBlockTest.block, 0) 55 | self.assertEqual(response, GetBlockTest.serialized_data) 56 | 57 | # Test verbose level 1 58 | response = self.florestad.rpc.get_block(GetBlockTest.block, 1) 59 | self.assertEqual(response["bits"], GetBlockTest.bits) 60 | self.assertEqual(response["chainwork"], GetBlockTest.chainwork) 61 | self.assertEqual(response["confirmations"], GetBlockTest.confirmations) 62 | self.assertEqual(response["difficulty"], GetBlockTest.difficulty) 63 | self.assertEqual(response["hash"], GetBlockTest.hash) 64 | self.assertEqual(response["height"], GetBlockTest.height) 65 | self.assertEqual(response["mediantime"], GetBlockTest.mediantime) 66 | self.assertEqual(response["merkleroot"], GetBlockTest.merkleroot) 67 | self.assertEqual(response["n_tx"], GetBlockTest.n_tx) 68 | self.assertEqual(response["nonce"], GetBlockTest.nonce) 69 | self.assertEqual(response["previousblockhash"], GetBlockTest.prev_blockhash) 70 | self.assertEqual(response["size"], GetBlockTest.size) 71 | self.assertEqual(response["strippedsize"], GetBlockTest.strippedsize) 72 | self.assertEqual(response["time"], GetBlockTest.time) 73 | self.assertEqual(len(response["tx"]), len(GetBlockTest.tx)) 74 | self.assertEqual(response["version"], GetBlockTest.version) 75 | self.assertEqual(response["versionHex"], GetBlockTest.version_hex) 76 | self.assertEqual(response["weight"], GetBlockTest.weight) 77 | 78 | 79 | if __name__ == "__main__": 80 | GetBlockTest().main() 81 | -------------------------------------------------------------------------------- /crates/floresta-wire/src/p2p_wire/seeds/signet_seeds.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "address": { 4 | "V4": "161.97.178.61" 5 | }, 6 | "last_connected": 1678986166, 7 | "state": { 8 | "Tried": 0 9 | }, 10 | "services": 16778317, 11 | "port": 38333 12 | }, 13 | { 14 | "address": { 15 | "V4": "194.163.132.180" 16 | }, 17 | "last_connected": 1678986166, 18 | "state": { 19 | "Tried": 0 20 | }, 21 | "services": 50331657, 22 | "port": 38333 23 | }, 24 | { 25 | "address": { 26 | "V4": "45.33.47.11" 27 | }, 28 | "last_connected": 1678986166, 29 | "state": { 30 | "Tried": 0 31 | }, 32 | "services": 1033, 33 | "port": 38333 34 | }, 35 | { 36 | "address": { 37 | "V4": "209.141.62.48" 38 | }, 39 | "last_connected": 1678986166, 40 | "state": { 41 | "Tried": 0 42 | }, 43 | "services": 0, 44 | "port": 38333 45 | }, 46 | { 47 | "address": { 48 | "V4": "192.248.175.110" 49 | }, 50 | "last_connected": 1678986166, 51 | "state": { 52 | "Tried": 0 53 | }, 54 | "services": 1033, 55 | "port": 38333 56 | }, 57 | { 58 | "address": { 59 | "V4": "128.199.252.50" 60 | }, 61 | "last_connected": 1678986166, 62 | "state": { 63 | "Tried": 0 64 | }, 65 | "services": 1033, 66 | "port": 38333 67 | }, 68 | { 69 | "address": { 70 | "V4": "34.125.100.29" 71 | }, 72 | "last_connected": 1678986166, 73 | "state": { 74 | "Tried": 0 75 | }, 76 | "services": 1033, 77 | "port": 38333 78 | }, 79 | { 80 | "address": { 81 | "V4": "153.126.143.201" 82 | }, 83 | "last_connected": 1678986166, 84 | "state": { 85 | "Tried": 0 86 | }, 87 | "services": 1033, 88 | "port": 38333 89 | }, 90 | { 91 | "address": { 92 | "V4": "72.48.253.168" 93 | }, 94 | "last_connected": 1678986166, 95 | "state": { 96 | "Tried": 0 97 | }, 98 | "services": 1037, 99 | "port": 38333 100 | }, 101 | { 102 | "address": { 103 | "V4": "136.144.237.250" 104 | }, 105 | "last_connected": 1678986166, 106 | "state": { 107 | "Tried": 0 108 | }, 109 | "services": 1033, 110 | "port": 38333 111 | }, 112 | { 113 | "address": { 114 | "V4": "103.16.128.63" 115 | }, 116 | "last_connected": 1678986166, 117 | "state": { 118 | "Tried": 0 119 | }, 120 | "services": 1033, 121 | "port": 38333 122 | }, 123 | { 124 | "address": { 125 | "V4": "81.204.239.212" 126 | }, 127 | "last_connected": 1678986166, 128 | "state": { 129 | "Tried": 0 130 | }, 131 | "services": 0, 132 | "port": 38333 133 | }, 134 | { 135 | "address": { 136 | "V4": "178.128.221.177" 137 | }, 138 | "last_connected": 1678986166, 139 | "state": { 140 | "Tried": 0 141 | }, 142 | "services": 0, 143 | "port": 38333 144 | }, 145 | { 146 | "address": { 147 | "V4": "108.161.223.181" 148 | }, 149 | "last_connected": 1678986166, 150 | "state": { 151 | "Tried": 0 152 | }, 153 | "services": 0, 154 | "port": 38333 155 | } 156 | ] 157 | -------------------------------------------------------------------------------- /tests/floresta-cli/getblockhash.py: -------------------------------------------------------------------------------- 1 | """ 2 | getblockhash.py 3 | 4 | This functional test cli utility to interact with a Floresta node with `getblockhash` 5 | """ 6 | 7 | import re 8 | import time 9 | from test_framework import FlorestaTestFramework 10 | 11 | DATA_DIR = FlorestaTestFramework.get_integration_test_dir() 12 | 13 | 14 | class GetBlockhashTest(FlorestaTestFramework): 15 | """ 16 | Test `getblockhash` with a fresh node and expected initial 0 height block. 17 | After that, it will mine some blocks with utreexod and check that 18 | the blockhashes match between floresta, utreexod, and bitcoind. 19 | """ 20 | 21 | best_block = "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206" 22 | 23 | def set_test_params(self): 24 | """ 25 | Setup a single node 26 | """ 27 | name = self.__class__.__name__.lower() 28 | self.v2transport = False 29 | self.data_dirs = GetBlockhashTest.create_data_dirs(DATA_DIR, name, 3) 30 | 31 | self.florestad = self.add_node( 32 | variant="florestad", extra_args=[f"--data-dir={self.data_dirs[0]}"] 33 | ) 34 | 35 | self.utreexod = self.add_node( 36 | variant="utreexod", 37 | extra_args=[ 38 | f"--datadir={self.data_dirs[1]}", 39 | "--miningaddr=bcrt1q4gfcga7jfjmm02zpvrh4ttc5k7lmnq2re52z2y", 40 | "--prune=0", 41 | ], 42 | ) 43 | 44 | self.bitcoind = self.add_node( 45 | variant="bitcoind", extra_args=[f"-datadir={self.data_dirs[2]}"] 46 | ) 47 | 48 | def run_test(self): 49 | """ 50 | Run JSONRPC and get the hash of heights 0 to 10. 51 | """ 52 | self.log("=== Starting nodes...") 53 | self.run_node(self.florestad) 54 | self.run_node(self.utreexod) 55 | self.run_node(self.bitcoind) 56 | 57 | self.log("=== Mining blocks with utreexod") 58 | self.utreexod.rpc.generate(10) 59 | time.sleep(5) 60 | 61 | self.log("=== Connect floresta to utreexod") 62 | host = self.utreexod.get_host() 63 | port = self.utreexod.get_port("p2p") 64 | self.florestad.rpc.addnode( 65 | f"{host}:{port}", command="onetry", v2transport=False 66 | ) 67 | 68 | self.log("=== Waiting for floresta to connect to utreexod...") 69 | time.sleep(5) 70 | peer_info = self.florestad.rpc.get_peerinfo() 71 | self.assertMatch( 72 | peer_info[0]["user_agent"], 73 | re.compile(r"/btcwire:\d+\.\d+\.\d+/utreexod:\d+\.\d+\.\d+/"), 74 | ) 75 | 76 | self.log("=== Connect bitcoind to utreexod") 77 | self.bitcoind.rpc.addnode(f"{host}:{port}", command="onetry", v2transport=False) 78 | 79 | self.log("=== Waiting for bitcoind to connect to utreexod...") 80 | time.sleep(5) 81 | peer_info = self.bitcoind.rpc.get_peerinfo() 82 | self.assertMatch( 83 | peer_info[0]["subver"], 84 | re.compile(r"/btcwire:\d+\.\d+\.\d+/utreexod:\d+\.\d+\.\d+/"), 85 | ) 86 | 87 | self.log("=== Wait for the nodes to sync...") 88 | time.sleep(5) 89 | 90 | self.log("=== Get the tip block") 91 | block_count = self.florestad.rpc.get_block_count() 92 | 93 | for i in range(0, block_count + 1): 94 | self.log(f"=== Check the correct blockhash for height {i}...") 95 | hash_floresta = self.florestad.rpc.get_blockhash(i) 96 | hash_utreexod = self.utreexod.rpc.get_blockhash(i) 97 | hash_bitcoind = self.bitcoind.rpc.get_blockhash(i) 98 | for _hash in [hash_utreexod, hash_bitcoind]: 99 | self.assertEqual(hash_floresta, _hash) 100 | 101 | 102 | if __name__ == "__main__": 103 | GetBlockhashTest().main() 104 | -------------------------------------------------------------------------------- /bin/florestad/docs/tutorial(PT-BR).md: -------------------------------------------------------------------------------- 1 | ## Tutorial 2 | 3 | ### Introdução 4 | 5 | Este programa é uma pequena implementação de node com um Electrum Server acoplado. Ela se comporta semelhante a um setup com Bitcoin Core + Electrum Personal Server, porém com algumas diferenças chave. 6 | 7 | - Node e Electrum Server estão no mesmo binário, tornando o processo mais simples e com menos erros. 8 | - O full node utiliza uma tecnologia nova chamada `Utreexo` para reduzir o consumo de recursos, você consegue rodar o node com menos de 1GB de disco e RAM. 9 | - Diferentemente do EPS, esse Electrum Server suporta múltiplas conexões simultâneas. 10 | 11 | ### Utilizando 12 | 13 | Existem duas maneiras de se obter o executável. Você pode compilar do código-fonte ou baixar o binário pré-compilado do Github. Para intruções de como compilar o código-fonte, [veja abaixo](#compilando). As informações de como rodar estão [aqui](#rodando) 14 | 15 | ### Compilando 16 | 17 | Para compilar, você precisa da toochain do Rust e o Cargo, mais informações [aqui](https://www.rust-lang.org/). 18 | Você pode obter o código-fonte baixando do Github ou clonando com 19 | 20 | ```bash 21 | git clone https://github.com/vinteumorg/Floresta.git 22 | ``` 23 | 24 | Navegue para dentro da pasta com 25 | 26 | ```bash 27 | cd Floresta/ 28 | ``` 29 | 30 | compile com: 31 | 32 | ```bash 33 | cargo build --release 34 | ``` 35 | 36 | se tudo estiver ok, irá compilar o programa e salvar o executável em `./target/release/`. 37 | 38 | ### Rodando 39 | 40 | Antes de rodar ele pela primeira vez, você precisa extrair a xpub da sua carteira. Na Electrum, basta ir no menu "Carteira" e clicar em "Informações", a xpub vai aparecer em uma caixa de texto grande. 41 | 42 | Uma vez que você tenha a Chave Pública Extendida em mãos, copie o arquivo de configuração `config.toml.sample` para `config.toml` edite-o inserindo a xpub no campo apropriado. Você pode inserir infinitas xpubs. Também é permitido endereços soltos. 43 | 44 | Para endereços multisig, uma carteira como Sparrow é recomendada. Basta copiar o "output descriptor" gerado pela mesma. 45 | 46 | Veja [abaixo](#config_example) um exemplo de arquivo válido. 47 | 48 | ```bash 49 | floresta -c config.toml --network signet run 50 | ``` 51 | 52 | ou 53 | 54 | ```bash 55 | ./target/release/floresta -c config.toml --network signet run 56 | ``` 57 | 58 | ou 59 | 60 | ```bash 61 | cargo run --release -- -c config.toml --network signet run 62 | ``` 63 | 64 | Onde: 65 | 66 | - `network` é a rede que você está utilizando, bitcoin significa mainnet, outros valores válidos são signet, regtest e testnet. Todas são redes de teste que são funcionalmente idênticas a rede principal (mainnet), porém utilizada apenas para teste, e suas moedas não tem valor algum. 67 | 68 | Se tudo der certo, ele irá iniciar a sincronização e mostrar o progresso na tela. A partir do momento em que aparecer 69 | > Server running on: 0.0.0.0:50001 70 | 71 | você já poderá conectar a sua carteira ao servidor, porém o saldo poderá demorar um pouco para aparecer. 72 | 73 | #### Exemplo de arquivo de configuração 74 | ```toml 75 | [wallet] 76 | xpubs = [ 77 | "vpub5ZkWR4krsAzwFsmW8Yp3eHuANVLr7ZSWii6KmRnLRiN6ZXLbqs1f217jJM37oteQoyng82yw44XQU8PYJJBGgVzvJ96dQZEyZZcDiDmoJXw", 78 | "vpub5V5XF4ipcQ9tLp7NCFswnwZ23tm5Key81E9CCfqFXaGjzTpQ8jjiirf2hG7aXtqXbRDFxMvEhdGdeFcqQ3jUGUkq4mqo2VoGCDWCZvPQvUy", 79 | ] 80 | addresses = [ 81 | "tb1qjfplwf7a2dpjj04cx96rysqeastvycc0j50cch" 82 | ] 83 | descriptors = [ 84 | "wsh(sortedmulti(1,[54ff5a12/48h/1h/0h/2h]tpubDDw6pwZA3hYxcSN32q7a5ynsKmWr4BbkBNHydHPKkM4BZwUfiK7tQ26h7USm8kA1E2FvCy7f7Er7QXKF8RNptATywydARtzgrxuPDwyYv4x/<0;1>/*,[bcf969c0/48h/1h/0h/2h]tpubDEFdgZdCPgQBTNtGj4h6AehK79Jm4LH54JrYBJjAtHMLEAth7LuY87awx9ZMiCURFzFWhxToRJK6xp39aqeJWrG5nuW3eBnXeMJcvDeDxfp/<0;1>/*))#fuw35j0q" 85 | ] 86 | ``` 87 | 88 | #### Print do programa em execução 89 | 90 | ![Um print dos logs de uma instância do Floresta rodando em uma terminal em uma distribuição GNU/Linux](./assets/Screenshot_ibd.jpg) 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Docker Publish][docker-badge]][docker-url] 2 | [![Functional][functional-badge]][functional-url] 3 | [![Rust][rust-badge]][rust-url] 4 | [![Docs][docs-badge]][docs-url] 5 | [![License][license-badge]][license-url] 6 | 7 | [docker-badge]: https://github.com/vinteumorg/Floresta/actions/workflows/docker.yml/badge.svg 8 | [docker-url]: https://github.com/vinteumorg/Floresta/actions/workflows/docker.yml 9 | [functional-badge]: https://github.com/vinteumorg/Floresta/actions/workflows/functional.yml/badge.svg 10 | [functional-url]: https://github.com/vinteumorg/Floresta/actions/workflows/functional.yml 11 | [rust-badge]: https://github.com/vinteumorg/Floresta/actions/workflows/rust.yml/badge.svg 12 | [rust-url]: https://github.com/vinteumorg/Floresta/actions/workflows/rust.yml 13 | [docs-badge]: https://img.shields.io/badge/docs-floresta-green 14 | [docs-url]: https://docs.getfloresta.sh/floresta/ 15 | [license-badge]: https://img.shields.io/badge/license-MIT-blue 16 | [license-url]: https://github.com/vinteumorg/Floresta/blob/master/LICENSE 17 | 18 | # Floresta 19 | 20 | Welcome to Floresta: a lightweight Bitcoin full node implementation written in Rust and powered by [Utreexo](https://eprint.iacr.org/2019/611), a novel dynamic accumulator designed for the Bitcoin UTXO set. 21 | 22 | This project is composed of two parts, `libfloresta` and `florestad`. `libfloresta` is a set of reusable components that can be used to build Bitcoin applications. `florestad` is built on top of `libfloresta` to provide a full node implementation, including a watch-only wallet and an Electrum server. If you just want to run a full node, you can use `florestad` directly, either by building it from source using the given instructions on the [doc folder](/doc) or by downloading a pre-built binary from the [releases](https://github.com/vinteumorg/Floresta/releases/latest). 23 | 24 | ## For developers 25 | 26 | Detailed documentation for `libfloresta` is available [here](https://docs.getfloresta.sh/floresta/). Additionally, the [floresta-docs](https://josesk999.github.io/floresta-docs/) mdBook provides an in-depth look at the libraries' architecture and internals. 27 | 28 | Further information can be found in the [doc folder](/doc). 29 | 30 | ## Community 31 | 32 | If you want to discuss this project, you can join our Discord server [here](https://discord.gg/5Wj8fjjS93). If you want to disclose a security vulnerability, please email `Davidson Souza at me AT dlsouza DOT lol`, using the PGP key [`2C8E0F 836FD7D BBBB9E 9B2EF899 64EC3AB 22B2E3`](https://blog.dlsouza.lol/assets/gpg.asc). 33 | 34 | ## Contributing 35 | Contributions are welcome, feel free to open an issue or a pull request. Check out our [CONTRIBUTING.md](CONTRIBUTING.md) for more information on best practices and guidelines. 36 | 37 | If you want to contribute but don't know where to start, take a look at the issues, there's a few of them marked as `good first issue`. 38 | 39 | ## License 40 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 41 | 42 | ## Acknowledgments 43 | * [Utreexo](https://eprint.iacr.org/2019/611) 44 | * [Bitcoin Core](https://github.com/bitcoin/bitcoin) 45 | * [Rust Bitcoin](https://github.com/rust-bitcoin/rust-bitcoin) 46 | * [Rust Miniscript](https://github.com/rust-bitcoin/rust-miniscript) 47 | 48 | ## Consensus Implementation 49 | 50 | One of the most challenging parts of working with Bitcoin is keeping up with the consensus rules. Given its nature as a consensus protocol, it's very important to make sure that the implementation is correct. Instead of reimplementing a Script interpreter, we use [`rust-bitcoinconsensus`](https://github.com/rust-bitcoin/rust-bitcoinconsensus/) to verify transactions. This is a bind around a shared library that is part of Bitcoin Core. This way, we can be sure that the consensus rules are the same as Bitcoin Core, at least for scripts. 51 | 52 | Although tx validation is arguably the hardest part in this process. This integration can be further improved by using `libbitcoinkernel`, that will increase the scope of `libbitcoinconsensus` to outside scripts, but this is still a work in progress. 53 | -------------------------------------------------------------------------------- /crates/floresta-wire/src/p2p_wire/node_context.rs: -------------------------------------------------------------------------------- 1 | //! During the lifetime of a Bitcoin client, we have a couple of phases that are slightly different 2 | //! from each other, having to implement their own state-machines and logic for handing requests. 3 | //! While we could simply put everything in one struct and have a single `impl` block, that would 4 | //! create a massive amount of if's in the code, taking different paths depending on which state 5 | //! are we in. For that reason, we define the basics of a node, like code shared by all the 6 | //! states into one base struct called `UtreexoNode`, we then further refine this struct using 7 | //! fine-tuned `Contexts`, that should implement [NodeContext] and are passed-in as a generic 8 | //! parameter by the caller. 9 | //! 10 | //! The three flavors of node are: 11 | //! - ChainSelector: 12 | //! This finds the best PoW chain, by downloading multiple candidates and taking 13 | //! the one with more PoW. It should do its job quickly, as it blocks our main 14 | //! client and can't proceed without this information. 15 | //! 16 | //! - SyncNode: 17 | //! Used to download and verify all blocks in a chain. This is computationally 18 | //! expensive and may take a while to run. After this ends its job, it gives us 100% 19 | //! certainty that this chain is valid. 20 | //! 21 | //! - Running Node: 22 | //! This is the one that users interacts with, and should be the one running most 23 | //! of the time. This node is started right after `ChainSelector` returns, and 24 | //! will handle new blocks (even if `SyncNode` haven't returned) and handle 25 | //! requests by users. 26 | 27 | use bitcoin::p2p::ServiceFlags; 28 | 29 | /// This trait mainly defines a bunch of constants that we need for the node, but we may tweak 30 | /// those values for each one. It's also an organized way of defining those constants anyway. 31 | pub trait NodeContext { 32 | /// How long we wait for a peer to respond to our request 33 | const REQUEST_TIMEOUT: u64; 34 | 35 | /// Max number of simultaneous connections we initiates we are willing to hold 36 | const MAX_OUTGOING_PEERS: usize = 10; 37 | 38 | /// We ask for peers every ASK_FOR_PEERS_INTERVAL seconds 39 | const ASK_FOR_PEERS_INTERVAL: u64 = 60 * 60; // One hour 40 | 41 | /// Save our database of peers every PEER_DB_DUMP_INTERVAL seconds 42 | const PEER_DB_DUMP_INTERVAL: u64 = 30; // 30 seconds 43 | 44 | /// Attempt to open a new connection (if needed) every TRY_NEW_CONNECTION seconds 45 | const TRY_NEW_CONNECTION: u64 = 10; // 10 seconds 46 | 47 | /// If ASSUME_STALE seconds passed since our last tip update, treat it as stale 48 | const ASSUME_STALE: u64 = 15 * 60; // 15 minutes 49 | 50 | /// While on IBD, if we've been without blocks for this long, ask for headers again 51 | const IBD_REQUEST_BLOCKS_AGAIN: u64 = 30; // 30 seconds 52 | 53 | /// How often we broadcast transactions 54 | const BROADCAST_DELAY: u64 = 30; // 30 seconds 55 | 56 | /// Max number of simultaneous inflight requests we allow 57 | const MAX_INFLIGHT_REQUESTS: usize = 1_000; 58 | 59 | /// Interval at which we open new feeler connections 60 | const FEELER_INTERVAL: u64 = 30; // 30 seconds 61 | 62 | /// Interval at which we rearrange our addresses 63 | const ADDRESS_REARRANGE_INTERVAL: u64 = 60 * 60; // 1 hour 64 | 65 | /// How long we ban a peer for 66 | const BAN_TIME: u64 = 60 * 60 * 24; 67 | 68 | /// How often we check if we haven't missed a block 69 | const BLOCK_CHECK_INTERVAL: u64 = 60 * 5; // 5 minutes 70 | 71 | /// How often we send our addresses to our peers 72 | const SEND_ADDRESSES_INTERVAL: u64 = 60 * 60; // 1 hour 73 | 74 | /// How long should we wait for a peer to respond our connection request 75 | const CONNECTION_TIMEOUT: u64 = 30; // 30 seconds 76 | 77 | /// How many blocks we can ask in the same request 78 | const BLOCKS_PER_GETDATA: usize = 5; 79 | 80 | /// How many concurrent GETDATA packages we can send at the same time 81 | const MAX_CONCURRENT_GETDATA: usize = 10; 82 | 83 | fn get_required_services(&self) -> ServiceFlags { 84 | ServiceFlags::NETWORK 85 | } 86 | } 87 | 88 | pub(crate) type PeerId = u32; 89 | --------------------------------------------------------------------------------