From db5c7e211fedba28dda8c5e52e70867a68189ef6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Feb 2026 05:19:27 +0000 Subject: [PATCH 1/4] Add CLAUDE.md with project overview, build/test commands, and architecture guide https://claude.ai/code/session_01S9ujRXvjmfhRpmdYdjxRk5 --- CLAUDE.md | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..6c5eaf482 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,115 @@ +# CLAUDE.md + +## Project Overview + +Electrs (Esplora variant) is a blockchain index engine and HTTP API for Bitcoin (and Liquid) written in Rust. It is the backend for the [Esplora block explorer](https://github.com/Blockstream/esplora) powering blockstream.info. Forked from [romanz/electrs](https://github.com/romanz/electrs), it adds an HTTP REST API, extended indexes, and Elements/Liquid support. + +## Build & Run + +```bash +# Build (requires clang, cmake, and Rust 1.75.0 via rust-toolchain.toml) +cargo build --release + +# Run (requires a running bitcoind, no txindex needed) +cargo run --release --bin electrs -- -vvvv --daemon-dir ~/.bitcoin + +# Build with Liquid support +cargo build --features liquid --release + +# Build with OpenTelemetry tracing +cargo build --features otlp-tracing --release +``` + +## Testing + +```bash +# Run all tests (requires bitcoind binary available in PATH or via BITCOIND_EXE) +RUST_LOG=debug cargo test + +# Run Liquid tests +RUST_LOG=debug cargo test --features liquid + +# Run tests with Bitcoin Core 28+ compatibility +RUST_LOG=debug cargo test --features bitcoind_28_0 + +# Run specific ignored test +cargo test -- --include-ignored test_electrum_raw + +# Run benchmarks (requires bench feature) +cargo bench --features bench +``` + +Integration tests in `tests/` spin up a real bitcoind (or elementsd) instance, create a temporary RocksDB, index blocks, and run queries against the REST and Electrum APIs. + +## Formatting & Linting + +```bash +# Format check (used by pre-commit hook) +cargo fmt --all -- --check + +# Format +cargo fmt --all + +# Clippy +cargo clippy --all-targets +``` + +The pre-commit hook (`.hooks/pre-commit`) runs `cargo +stable fmt --all -- --check`. + +## Architecture + +### Key Modules (`src/`) + +- **`bin/electrs.rs`** — Main entry point. Starts the daemon connection, indexer, mempool tracker, REST server, and Electrum RPC server. +- **`new_index/`** — Core indexing engine: + - `schema.rs` — Index schema definitions: `Store`, `Indexer`, `ChainQuery`. Three RocksDB databases: `txstore`, `history`, `cache`. + - `db.rs` — RocksDB wrapper and low-level operations. + - `mempool.rs` — Mempool tracking and indexing. + - `query.rs` — High-level query interface combining chain and mempool data. + - `fetch.rs` — Block fetching from blk*.dat files or via JSON-RPC. + - `zmq.rs` — ZMQ notifications for new blocks. +- **`rest.rs`** — HTTP REST API server (tiny_http-based). +- **`electrum/`** — Electrum JSON-RPC protocol server. + - `server.rs` — TCP server and connection handling. + - `client.rs` — Per-client state and request processing. +- **`daemon.rs`** — Bitcoin Core JSON-RPC client. +- **`config.rs`** — CLI argument parsing and configuration. +- **`chain.rs`** — Blockchain type aliases and network definitions. +- **`elements/`** — Liquid/Elements-specific code (behind `liquid` feature flag). +- **`util/`** — Helpers for transactions, scripts, fees, merkle proofs, and block parsing. +- **`electrs_macros/`** — Proc-macro crate for optional OTLP tracing instrumentation. + +### Data Flow + +1. **Indexing**: Blocks are fetched (from blk*.dat files for initial sync, or JSON-RPC for incremental updates) and indexed into three RocksDB databases (`txstore` → `history` → `cache`). +2. **Serving**: The REST API and Electrum RPC server query the indexed data via `ChainQuery` (on-chain) and `Mempool` (unconfirmed), unified through `Query`. +3. **Mempool**: Synced from bitcoind and re-synced on each main loop iteration (every 5 seconds or on ZMQ notification). + +### Feature Flags + +- `liquid` — Liquid/Elements network support +- `electrum-discovery` — Electrum server peer discovery +- `otlp-tracing` — OpenTelemetry tracing export +- `bitcoind_28_0` — Bitcoin Core 28.0+ compatibility +- `bench` — Enable benchmarks + +### Database Schema + +See `doc/schema.md` for the full RocksDB key/value schema. Key prefixes: `B` (block headers), `T` (raw txs), `O` (outpoints), `H` (history), `C` (confirmations), `S` (spending), `X` (block txids), `M` (block metadata). + +## Configuration + +Configuration is done via CLI args. Key options: +- `--daemon-dir` — bitcoind data directory +- `--db-dir` — electrs database directory +- `--http-addr` — REST API listen address (default: 127.0.0.1:3000) +- `--electrum-rpc-addr` — Electrum RPC listen address (default: 127.0.0.1:50001) +- `--lightmode` — reduce disk usage by querying bitcoind for raw txs on demand +- `--network` — bitcoin network (mainnet/testnet/regtest/signet/liquid) +- `--cors` — CORS origins for HTTP API + +## Workspace + +This is a Cargo workspace with two members: +- `electrs` (root) — main crate +- `electrs_macros` — proc-macro crate for OTLP tracing From d2f970b52b40a30fa9130ad71cbeaf7dc6ef0dce Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Feb 2026 05:49:43 +0000 Subject: [PATCH 2/4] Add implementation plan for Litecoin support Detailed plan covering feature flag, network constants, address handling, configuration, and all files requiring changes to support Litecoin as a new network alongside Bitcoin and Liquid. https://claude.ai/code/session_01S9ujRXvjmfhRpmdYdjxRk5 --- plan.md | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 plan.md diff --git a/plan.md b/plan.md new file mode 100644 index 000000000..318ee1074 --- /dev/null +++ b/plan.md @@ -0,0 +1,153 @@ +# Plan: Add Litecoin Support to Electrs + +## Overview + +Add Litecoin (mainnet, testnet, regtest) support to electrs as a compile-time feature flag `litecoin`, following the same pattern used by the `liquid` feature. Litecoin Core's RPC API is compatible with Bitcoin Core's, and Litecoin uses the same transaction/block serialization format, so the core indexing engine works without changes. The main work is in network constants, address handling, and configuration. + +## Key Litecoin Differences from Bitcoin +- Different genesis block hash +- Different network magic bytes (`0xdbb6c0fb` mainnet, `0xfcc1b7dc` testnet) +- Different default RPC port (9332 mainnet, 19332 testnet) +- Different address version bytes (P2PKH `0x30`/`L`, P2SH `0x32`/`M` or `0x05`/`3`) +- Different default data directory (`~/.litecoin/`) +- 2.5 minute block time (does not affect indexing logic) +- Scrypt PoW (does not affect indexing — only miners care) +- MWEB (MimbleWimble Extension Blocks) — **out of scope for initial implementation**; MWEB transactions are extension blocks that standard RPC returns as normal, but full MWEB peg-in/peg-out tracking would be a follow-up + +## Implementation Steps + +### Step 1: Add `litecoin` feature flag to `Cargo.toml` + +**File: `Cargo.toml`** + +- Add `litecoin = []` to `[features]` +- No new dependencies needed — Litecoin uses the same serialization as Bitcoin, and `rust-bitcoin` types work for Litecoin block/tx data since the formats are identical + +### Step 2: Extend the `Network` enum in `chain.rs` + +**File: `src/chain.rs`** + +- Add three new variants gated by `#[cfg(feature = "litecoin")]`: + - `Litecoin` (mainnet) + - `LitecoinTestnet` + - `LitecoinRegtest` +- Implement `magic()` for Litecoin variants: + - Litecoin mainnet: `0xdbb6c0fb` + - Litecoin testnet: `0xfcc1b7dc` + - Litecoin regtest: `0xdab5bffa` (same as Bitcoin regtest) +- Implement `genesis_hash()` for Litecoin: + - Mainnet: `12a765e31ffd4059bada1e25190f6e98c0fe1666a68542019d52529ccc51ee1d` + - Testnet4: `a0293e4eeb3da6e6f56f81ed595f57880d1a21569e13eefdd951284b5a626649` + - Regtest: use the Bitcoin regtest genesis hash (same genesis block structure) +- Implement `is_regtest()` for `LitecoinRegtest` +- Add to `Network::names()`: `"litecoin"`, `"litecointestnet"`, `"litecoinregtest"` +- Add `From<&str>` mappings for the new network names +- For the `From for BNetwork` and `From for Network` conversions: Litecoin variants should NOT convert to/from `BNetwork` since they aren't Bitcoin networks. These trait impls are gated behind `#[cfg(not(feature = "liquid"))]` — they need to also be gated behind `#[cfg(not(feature = "litecoin"))]`, OR the Litecoin feature should be mutually exclusive with default Bitcoin (like `liquid` is). **Decision: Make `litecoin` mutually exclusive with default Bitcoin mode, following the same `cfg` pattern as `liquid`.** + +### Step 3: Update configuration defaults in `config.rs` + +**File: `src/config.rs`** + +- Add Litecoin variants to all `match network_type` blocks: + - **Daemon RPC port**: 9332 (mainnet), 19332 (testnet), 19443 (regtest) + - **Electrum RPC port**: 50001 (mainnet), 60001 (testnet), 60401 (regtest) + - **HTTP port**: 3000 (mainnet), 3001 (testnet), 3002 (regtest) + - **Monitoring port**: 4224 (mainnet), 14224 (testnet), 24224 (regtest) +- Update `get_network_subdir()`: + - `Litecoin` → `None` (top-level `~/.litecoin/`) + - `LitecoinTestnet` → `Some("testnet4")` + - `LitecoinRegtest` → `Some("regtest")` +- Update default `daemon_dir` to `~/.litecoin` when litecoin feature is enabled +- Update help text references from "Bitcoind" / "bitcoind" to be more generic or add litecoin-specific text + +### Step 4: Handle address encoding/validation + +**File: `src/util/script.rs`** + +- The `ScriptToAddr` implementation for Bitcoin uses `bitcoin::Address::from_script(self, bitcoin::Network::from(network))`. Since Litecoin addresses use different version bytes than Bitcoin, we cannot use `bitcoin::Network` directly. +- **Approach**: For Litecoin, implement custom address-to-string conversion that handles Litecoin's address prefixes: + - Litecoin P2PKH: version byte `0x30` (addresses start with `L`) + - Litecoin P2SH: version byte `0x32` (addresses start with `M`) and legacy `0x05` (start with `3`) + - Litecoin Bech32: `ltc1` prefix (P2WPKH and P2WSH) + - Testnet uses `tltc1` for bech32, `0x6f` for P2PKH, `0xc4` for P2SH + +**File: `src/rest.rs`** + +- Update `address_to_scripthash()` to handle Litecoin address parsing: + - Parse Litecoin bech32 addresses (`ltc1...` / `tltc1...`) + - Parse base58 addresses with Litecoin version bytes + - Validate that the address matches the configured network +- This likely requires a small helper function or use of a litecoin address parsing crate + +### Step 5: Update daemon.rs for Litecoin compatibility + +**File: `src/daemon.rs`** + +- The version check `network_info.version < 16_00_00` may need adjustment for Litecoin Core version numbers (Litecoin Core uses its own versioning scheme, e.g., 21.x maps differently) +- Update log messages: change "bitcoind" references to be network-aware or generic ("daemon") +- The RPC API is otherwise identical — `getblockchaininfo`, `getblock`, `getrawtransaction`, etc. all work the same + +### Step 6: Update Electrum discovery default servers + +**File: `src/electrum/discovery/default_servers.rs`** + +- Add `#[cfg(feature = "litecoin")]` blocks with Litecoin Electrum server entries for `Network::Litecoin` and `Network::LitecoinTestnet` +- Known Litecoin Electrum servers can be sourced from Electrum-LTC server lists + +### Step 7: Ensure feature flag mutual exclusivity + +**File: `Cargo.toml`** and across `src/` + +- The `litecoin` feature must be mutually exclusive with `liquid` (and ideally with default Bitcoin mode) +- Follow the same `#[cfg(feature = "litecoin")]` / `#[cfg(not(feature = "litecoin"))]` pattern used by `liquid` +- Update `#[cfg(not(feature = "liquid"))]` guards to also exclude litecoin: `#[cfg(all(not(feature = "liquid"), not(feature = "litecoin")))]` for Bitcoin-only code paths +- Add compile-time check (or document) that `litecoin` and `liquid` cannot be combined + +### Step 8: Update tests + +**Files: `tests/common.rs`, `tests/rest.rs`, `tests/electrum.rs`** + +- Integration tests use `bitcoind` crate to spawn a Bitcoin Core node. For Litecoin, we'd need a `litecoind` test fixture. This is complex and can be deferred. +- Add unit tests for: + - Litecoin genesis hash correctness + - Litecoin magic byte correctness + - Litecoin address parsing and validation (bech32 `ltc1` and base58) + - Network name parsing round-trip + +### Step 9: Update documentation + +**Files: `README.md`, `CLAUDE.md`** + +- Document the `litecoin` feature flag +- Add build/run instructions for Litecoin mode: + ```bash + cargo run --features litecoin --release --bin electrs -- -vvvv --daemon-dir ~/.litecoin + ``` +- Document MWEB limitation (not yet supported) + +## Files Changed (Summary) + +| File | Changes | +|------|---------| +| `Cargo.toml` | Add `litecoin` feature flag | +| `src/chain.rs` | Network enum variants, magic bytes, genesis hashes, conversions | +| `src/config.rs` | Default ports, data dirs, network subdirs, help text | +| `src/util/script.rs` | Litecoin address encoding (bech32 `ltc1`, base58 version bytes) | +| `src/rest.rs` | Litecoin address parsing in `address_to_scripthash()` | +| `src/daemon.rs` | Version check adjustment, generic log messages | +| `src/electrum/discovery/default_servers.rs` | Litecoin Electrum server list | +| `tests/` | Unit tests for Litecoin constants and address handling | +| `README.md` | Documentation for Litecoin feature | +| `CLAUDE.md` | Update feature flags section | + +## Out of Scope (Future Work) + +- **MWEB support**: MimbleWimble Extension Blocks require tracking peg-in/peg-out transactions between the main chain and the extension block. This is analogous to Liquid's peg handling and would be a significant follow-up feature. +- **Litecoin integration tests**: Requires a `litecoind` test harness crate similar to the `bitcoind` crate used for Bitcoin tests. +- **Scrypt PoW verification**: Not needed — electrs trusts the daemon's chain validation and doesn't verify PoW. + +## Risk Assessment + +- **Low risk**: Network constants, ports, directory paths — straightforward additions following existing patterns. +- **Medium risk**: Address handling — Litecoin uses different bech32 HRP (`ltc1` vs `bc1`) and different base58 version bytes. The `rust-bitcoin` crate's `Address` type is Bitcoin-specific and won't natively handle Litecoin addresses. We may need to add a dependency like `litecoin-address` or implement custom parsing. +- **Low risk**: RPC compatibility — Litecoin Core's JSON-RPC API mirrors Bitcoin Core's closely. The same `getblockchaininfo`, `getblock`, `getrawmempool`, etc. calls work identically. From 33f1843ed1b658f26c30b95f3e3c81b6b1de9571 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Feb 2026 06:14:13 +0000 Subject: [PATCH 3/4] Add Litecoin network support via `litecoin` feature flag Implement Litecoin (mainnet, testnet, regtest) support following the same mutually-exclusive feature flag pattern used by `liquid`. Key changes: - New `litecoin` feature flag in Cargo.toml - Network enum variants: Litecoin, LitecoinTestnet, LitecoinRegtest - Litecoin-specific magic bytes, genesis hashes, and default ports - Custom address encoding/decoding (bech32 ltc1/tltc1/rltc1 and base58check with Litecoin version bytes) in util/litecoin_addr.rs - Updated cfg guards across codebase for three-way feature exclusivity - Default daemon directory ~/.litecoin, Litecoin Core RPC port 9332 - Electrum discovery default servers for Litecoin - Unit tests for address encoding roundtrips - Documentation updates in CLAUDE.md MWEB (MimbleWimble Extension Blocks) support is not included and is planned as a follow-up feature. https://claude.ai/code/session_01S9ujRXvjmfhRpmdYdjxRk5 --- CLAUDE.md | 14 +- Cargo.toml | 1 + src/bin/electrs.rs | 12 +- src/bin/popular-scripts.rs | 5 +- src/chain.rs | 112 +++++- src/config.rs | 98 +++-- src/daemon.rs | 2 +- src/electrum/discovery/default_servers.rs | 26 +- src/electrum/server.rs | 10 +- src/new_index/db.rs | 135 +++++-- src/new_index/fetch.rs | 11 +- src/new_index/precache.rs | 30 +- src/new_index/schema.rs | 33 +- src/rest.rs | 43 ++- src/util/litecoin_addr.rs | 419 ++++++++++++++++++++++ src/util/mod.rs | 2 + src/util/script.rs | 8 +- tests/electrum.rs | 3 +- tests/rest.rs | 42 +-- 19 files changed, 850 insertions(+), 156 deletions(-) create mode 100644 src/util/litecoin_addr.rs diff --git a/CLAUDE.md b/CLAUDE.md index 6c5eaf482..ae08e4e60 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ ## Project Overview -Electrs (Esplora variant) is a blockchain index engine and HTTP API for Bitcoin (and Liquid) written in Rust. It is the backend for the [Esplora block explorer](https://github.com/Blockstream/esplora) powering blockstream.info. Forked from [romanz/electrs](https://github.com/romanz/electrs), it adds an HTTP REST API, extended indexes, and Elements/Liquid support. +Electrs (Esplora variant) is a blockchain index engine and HTTP API for Bitcoin (and Liquid/Litecoin) written in Rust. It is the backend for the [Esplora block explorer](https://github.com/Blockstream/esplora) powering blockstream.info. Forked from [romanz/electrs](https://github.com/romanz/electrs), it adds an HTTP REST API, extended indexes, and Elements/Liquid support, as well as Litecoin support. ## Build & Run @@ -16,6 +16,12 @@ cargo run --release --bin electrs -- -vvvv --daemon-dir ~/.bitcoin # Build with Liquid support cargo build --features liquid --release +# Build with Litecoin support +cargo build --features litecoin --release + +# Run with Litecoin (requires a running litecoind) +cargo run --features litecoin --release --bin electrs -- -vvvv --daemon-dir ~/.litecoin + # Build with OpenTelemetry tracing cargo build --features otlp-tracing --release ``` @@ -77,6 +83,7 @@ The pre-commit hook (`.hooks/pre-commit`) runs `cargo +stable fmt --all -- --che - **`chain.rs`** — Blockchain type aliases and network definitions. - **`elements/`** — Liquid/Elements-specific code (behind `liquid` feature flag). - **`util/`** — Helpers for transactions, scripts, fees, merkle proofs, and block parsing. + - `litecoin_addr.rs` — Litecoin address encoding/decoding (bech32 `ltc1`/`tltc1`/`rltc1` and base58check with Litecoin version bytes). Behind `litecoin` feature flag. - **`electrs_macros/`** — Proc-macro crate for optional OTLP tracing instrumentation. ### Data Flow @@ -87,7 +94,8 @@ The pre-commit hook (`.hooks/pre-commit`) runs `cargo +stable fmt --all -- --che ### Feature Flags -- `liquid` — Liquid/Elements network support +- `liquid` — Liquid/Elements network support (mutually exclusive with `litecoin`) +- `litecoin` — Litecoin network support (mutually exclusive with `liquid`). Networks: litecoin, litecointestnet, litecoinregtest. Note: MWEB (MimbleWimble Extension Blocks) is not yet supported. - `electrum-discovery` — Electrum server peer discovery - `otlp-tracing` — OpenTelemetry tracing export - `bitcoind_28_0` — Bitcoin Core 28.0+ compatibility @@ -105,7 +113,7 @@ Configuration is done via CLI args. Key options: - `--http-addr` — REST API listen address (default: 127.0.0.1:3000) - `--electrum-rpc-addr` — Electrum RPC listen address (default: 127.0.0.1:50001) - `--lightmode` — reduce disk usage by querying bitcoind for raw txs on demand -- `--network` — bitcoin network (mainnet/testnet/regtest/signet/liquid) +- `--network` — bitcoin network (mainnet/testnet/regtest/signet/liquid/litecoin/litecointestnet/litecoinregtest) - `--cors` — CORS origins for HTTP API ## Workspace diff --git a/Cargo.toml b/Cargo.toml index c02915686..e47911d7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ default-run = "electrs" [features] liquid = ["elements"] +litecoin = [] electrum-discovery = ["electrum-client"] bench = [] otlp-tracing = [ diff --git a/src/bin/electrs.rs b/src/bin/electrs.rs index f8178fbf7..059adcb6c 100644 --- a/src/bin/electrs.rs +++ b/src/bin/electrs.rs @@ -4,13 +4,8 @@ extern crate log; extern crate electrs; -use crossbeam_channel::{self as channel}; -use error_chain::ChainedError; -use std::{env, process, thread}; -use std::sync::{Arc, RwLock}; -use std::time::Duration; use bitcoin::hex::DisplayHex; -use rand::{rng, RngCore}; +use crossbeam_channel::{self as channel}; use electrs::{ config::Config, daemon::Daemon, @@ -21,6 +16,11 @@ use electrs::{ rest, signal::Waiter, }; +use error_chain::ChainedError; +use rand::{rng, RngCore}; +use std::sync::{Arc, RwLock}; +use std::time::Duration; +use std::{env, process, thread}; #[cfg(feature = "otlp-tracing")] use electrs::otlp_trace; diff --git a/src/bin/popular-scripts.rs b/src/bin/popular-scripts.rs index 6ad39f667..0274425b5 100644 --- a/src/bin/popular-scripts.rs +++ b/src/bin/popular-scripts.rs @@ -2,7 +2,10 @@ extern crate electrs; use bitcoin::hex::DisplayHex; use electrs::{ - config::Config, metrics::Metrics, new_index::{Store, TxHistoryKey}, util::bincode + config::Config, + metrics::Metrics, + new_index::{Store, TxHistoryKey}, + util::bincode, }; fn main() { diff --git a/src/chain.rs b/src/chain.rs index 5930f8149..2919edbc4 100644 --- a/src/chain.rs +++ b/src/chain.rs @@ -1,4 +1,13 @@ -#[cfg(not(feature = "liquid"))] // use regular Bitcoin data structures +#[cfg(not(any(feature = "liquid", feature = "litecoin")))] +// use regular Bitcoin data structures +pub use bitcoin::{ + address, blockdata::block::Header as BlockHeader, blockdata::script, consensus::deserialize, + hash_types::TxMerkleNode, Address, Block, BlockHash, OutPoint, ScriptBuf as Script, Sequence, + Transaction, TxIn, TxOut, Txid, +}; + +// Litecoin uses the same Bitcoin data structures (same serialization format) +#[cfg(feature = "litecoin")] pub use bitcoin::{ address, blockdata::block::Header as BlockHeader, blockdata::script, consensus::deserialize, hash_types::TxMerkleNode, Address, Block, BlockHash, OutPoint, ScriptBuf as Script, Sequence, @@ -24,15 +33,15 @@ pub use confidential::Value; #[derive(Debug, Copy, Clone, PartialEq, Hash, Serialize, Ord, PartialOrd, Eq)] pub enum Network { - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Bitcoin, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Testnet, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Testnet4, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Regtest, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Signet, #[cfg(feature = "liquid")] @@ -41,10 +50,17 @@ pub enum Network { LiquidTestnet, #[cfg(feature = "liquid")] LiquidRegtest, + + #[cfg(feature = "litecoin")] + Litecoin, + #[cfg(feature = "litecoin")] + LitecoinTestnet, + #[cfg(feature = "litecoin")] + LitecoinRegtest, } impl Network { - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] pub fn magic(self) -> u32 { u32::from_le_bytes(BNetwork::from(self).magic().to_bytes()) } @@ -57,12 +73,23 @@ impl Network { } } + #[cfg(feature = "litecoin")] + pub fn magic(self) -> u32 { + match self { + Network::Litecoin => 0xDBB6_C0FB, + Network::LitecoinTestnet => 0xFCC1_B7DC, + Network::LitecoinRegtest => 0xDAB5_BFFA, + } + } + pub fn is_regtest(self) -> bool { match self { - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Regtest => true, #[cfg(feature = "liquid")] Network::LiquidRegtest => true, + #[cfg(feature = "litecoin")] + Network::LitecoinRegtest => true, _ => false, } } @@ -94,8 +121,18 @@ impl Network { } } + /// Returns the bech32 human-readable part for this network + #[cfg(feature = "litecoin")] + pub fn bech32_hrp(self) -> &'static str { + match self { + Network::Litecoin => "ltc", + Network::LitecoinTestnet => "tltc", + Network::LitecoinRegtest => "rltc", + } + } + pub fn names() -> Vec { - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] return vec![ "mainnet".to_string(), "testnet".to_string(), @@ -110,14 +147,23 @@ impl Network { "liquidtestnet".to_string(), "liquidregtest".to_string(), ]; + + #[cfg(feature = "litecoin")] + return vec![ + "litecoin".to_string(), + "litecointestnet".to_string(), + "litecoinregtest".to_string(), + ]; } } pub fn genesis_hash(network: Network) -> BlockHash { - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] return bitcoin_genesis_hash(network.into()); #[cfg(feature = "liquid")] return liquid_genesis_hash(network); + #[cfg(feature = "litecoin")] + return litecoin_genesis_hash(network); } pub fn bitcoin_genesis_hash(network: BNetwork) -> bitcoin::BlockHash { @@ -163,18 +209,41 @@ pub fn liquid_genesis_hash(network: Network) -> elements::BlockHash { } } +#[cfg(feature = "litecoin")] +pub fn litecoin_genesis_hash(network: Network) -> BlockHash { + lazy_static! { + static ref LITECOIN_GENESIS: BlockHash = + "12a765e31ffd4059bada1e25190f6e98c99d4028c0fe1666a68542019d52529c" + .parse() + .unwrap(); + static ref LITECOIN_TESTNET_GENESIS: BlockHash = + "4966625a4b2851d9fdee139e56211a0d88575f59ed816ca060a99a68c5d4ee1d" + .parse() + .unwrap(); + // Litecoin regtest uses the same genesis block as Bitcoin regtest + static ref LITECOIN_REGTEST_GENESIS: BlockHash = + genesis_block(BNetwork::Regtest).block_hash(); + } + + match network { + Network::Litecoin => *LITECOIN_GENESIS, + Network::LitecoinTestnet => *LITECOIN_TESTNET_GENESIS, + Network::LitecoinRegtest => *LITECOIN_REGTEST_GENESIS, + } +} + impl From<&str> for Network { fn from(network_name: &str) -> Self { match network_name { - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] "mainnet" => Network::Bitcoin, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] "testnet" => Network::Testnet, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] "testnet4" => Network::Testnet4, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] "regtest" => Network::Regtest, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] "signet" => Network::Signet, #[cfg(feature = "liquid")] @@ -184,12 +253,19 @@ impl From<&str> for Network { #[cfg(feature = "liquid")] "liquidregtest" => Network::LiquidRegtest, - _ => panic!("unsupported Bitcoin network: {:?}", network_name), + #[cfg(feature = "litecoin")] + "litecoin" => Network::Litecoin, + #[cfg(feature = "litecoin")] + "litecointestnet" => Network::LitecoinTestnet, + #[cfg(feature = "litecoin")] + "litecoinregtest" => Network::LitecoinRegtest, + + _ => panic!("unsupported network: {:?}", network_name), } } } -#[cfg(not(feature = "liquid"))] +#[cfg(not(any(feature = "liquid", feature = "litecoin")))] impl From for BNetwork { fn from(network: Network) -> Self { match network { @@ -202,7 +278,7 @@ impl From for BNetwork { } } -#[cfg(not(feature = "liquid"))] +#[cfg(not(any(feature = "liquid", feature = "litecoin")))] impl From for Network { fn from(network: BNetwork) -> Self { match network { diff --git a/src/config.rs b/src/config.rs index d23128e91..b7634eb35 100644 --- a/src/config.rs +++ b/src/config.rs @@ -300,7 +300,13 @@ impl Config { let m = args.get_matches(); - let network_name = m.value_of("network").unwrap_or("mainnet"); + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + let default_network = "mainnet"; + #[cfg(feature = "liquid")] + let default_network = "liquid"; + #[cfg(feature = "litecoin")] + let default_network = "litecoin"; + let network_name = m.value_of("network").unwrap_or(default_network); let network_type = Network::from(network_name); let db_dir = Path::new(m.value_of("db_dir").unwrap_or("./db")); let db_path = db_dir.join(network_name); @@ -319,32 +325,39 @@ impl Config { let asset_db_path = m.value_of("asset_db_path").map(PathBuf::from); let default_daemon_port = match network_type { - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Bitcoin => 8332, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Testnet => 18332, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Testnet4 => 48332, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Regtest => 18443, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Signet => 38332, #[cfg(feature = "liquid")] Network::Liquid => 7041, #[cfg(feature = "liquid")] Network::LiquidTestnet | Network::LiquidRegtest => 7040, + + #[cfg(feature = "litecoin")] + Network::Litecoin => 9332, + #[cfg(feature = "litecoin")] + Network::LitecoinTestnet => 19332, + #[cfg(feature = "litecoin")] + Network::LitecoinRegtest => 19443, }; let default_electrum_port = match network_type { - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Bitcoin => 50001, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Testnet => 60001, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Testnet4 => 40001, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Regtest => 60401, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Signet => 60601, #[cfg(feature = "liquid")] @@ -353,17 +366,24 @@ impl Config { Network::LiquidTestnet => 51301, #[cfg(feature = "liquid")] Network::LiquidRegtest => 51401, + + #[cfg(feature = "litecoin")] + Network::Litecoin => 50001, + #[cfg(feature = "litecoin")] + Network::LitecoinTestnet => 60001, + #[cfg(feature = "litecoin")] + Network::LitecoinRegtest => 60401, }; let default_http_port = match network_type { - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Bitcoin => 3000, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Testnet => 3001, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Testnet4 => 3004, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Regtest => 3002, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Signet => 3003, #[cfg(feature = "liquid")] @@ -372,17 +392,24 @@ impl Config { Network::LiquidTestnet => 3001, #[cfg(feature = "liquid")] Network::LiquidRegtest => 3002, + + #[cfg(feature = "litecoin")] + Network::Litecoin => 3000, + #[cfg(feature = "litecoin")] + Network::LitecoinTestnet => 3001, + #[cfg(feature = "litecoin")] + Network::LitecoinRegtest => 3002, }; let default_monitoring_port = match network_type { - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Bitcoin => 4224, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Testnet => 14224, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Testnet4 => 44224, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Regtest => 24224, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Signet => 54224, #[cfg(feature = "liquid")] @@ -391,6 +418,13 @@ impl Config { Network::LiquidTestnet => 44324, #[cfg(feature = "liquid")] Network::LiquidRegtest => 44224, + + #[cfg(feature = "litecoin")] + Network::Litecoin => 4224, + #[cfg(feature = "litecoin")] + Network::LitecoinTestnet => 14224, + #[cfg(feature = "litecoin")] + Network::LitecoinRegtest => 24224, }; let daemon_rpc_addr: SocketAddr = str_to_socketaddr( @@ -424,7 +458,12 @@ impl Config { .map(PathBuf::from) .unwrap_or_else(|| { let mut default_dir = home_dir().expect("no homedir"); + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + default_dir.push(".bitcoin"); + #[cfg(feature = "liquid")] default_dir.push(".bitcoin"); + #[cfg(feature = "litecoin")] + default_dir.push(".litecoin"); default_dir }); @@ -539,15 +578,15 @@ impl RpcLogging { pub fn get_network_subdir(network: Network) -> Option<&'static str> { match network { - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Bitcoin => None, - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Testnet => Some("testnet3"), - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Testnet4 => Some("testnet4"), - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Regtest => Some("regtest"), - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Signet => Some("signet"), #[cfg(feature = "liquid")] @@ -556,6 +595,13 @@ pub fn get_network_subdir(network: Network) -> Option<&'static str> { Network::LiquidTestnet => Some("liquidtestnet"), #[cfg(feature = "liquid")] Network::LiquidRegtest => Some("liquidregtest"), + + #[cfg(feature = "litecoin")] + Network::Litecoin => None, + #[cfg(feature = "litecoin")] + Network::LitecoinTestnet => Some("testnet4"), + #[cfg(feature = "litecoin")] + Network::LitecoinRegtest => Some("regtest"), } } diff --git a/src/daemon.rs b/src/daemon.rs index b4e3cfc10..677714ca6 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -621,7 +621,7 @@ impl Daemon { Err(e) => { let err_msg = format!("{e:?}"); if err_msg.contains("Block not found on disk") - || err_msg.contains("Block not available") + || err_msg.contains("Block not available") { // There is a small chance the node returns the header but didn't finish to index the block log::warn!("getblocks failing with: {e:?} trying {attempts} more time") diff --git a/src/electrum/discovery/default_servers.rs b/src/electrum/discovery/default_servers.rs index 95754ecef..f17831b67 100644 --- a/src/electrum/discovery/default_servers.rs +++ b/src/electrum/discovery/default_servers.rs @@ -3,7 +3,7 @@ use crate::electrum::discovery::{DiscoveryManager, Service}; pub fn add_default_servers(discovery: &DiscoveryManager, network: Network) { match network { - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Bitcoin => { discovery .add_default_server( @@ -402,7 +402,7 @@ pub fn add_default_servers(discovery: &DiscoveryManager, network: Network) { ) .ok(); } - #[cfg(not(feature = "liquid"))] + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] Network::Testnet => { discovery .add_default_server( @@ -442,6 +442,28 @@ pub fn add_default_servers(discovery: &DiscoveryManager, network: Network) { .ok(); } + #[cfg(feature = "litecoin")] + Network::Litecoin => { + discovery + .add_default_server( + "electrum-ltc.bysh.me".into(), + vec![Service::Tcp(50001), Service::Ssl(50002)], + ) + .ok(); + discovery + .add_default_server( + "ltc.rentonisk.com".into(), + vec![Service::Tcp(50001), Service::Ssl(50002)], + ) + .ok(); + discovery + .add_default_server( + "electrum.ltc.xurious.com".into(), + vec![Service::Tcp(50001), Service::Ssl(50002)], + ) + .ok(); + } + _ => (), } } diff --git a/src/electrum/server.rs b/src/electrum/server.rs index ea5579699..dc4e7fa75 100644 --- a/src/electrum/server.rs +++ b/src/electrum/server.rs @@ -15,10 +15,6 @@ use serde_json::{from_str, Value}; use electrs_macros::trace; -#[cfg(not(feature = "liquid"))] -use bitcoin::consensus::encode::serialize_hex; -#[cfg(feature = "liquid")] -use elements::encode::serialize_hex; use crate::chain::Txid; use crate::config::{Config, RpcLogging}; use crate::electrum::{get_electrum_height, ProtocolVersion}; @@ -27,6 +23,10 @@ use crate::metrics::{Gauge, HistogramOpts, HistogramVec, MetricOpts, Metrics}; use crate::new_index::{Query, Utxo}; use crate::util::electrum_merkle::{get_header_merkle_proof, get_id_from_pos, get_tx_merkle_proof}; use crate::util::{create_socket, spawn_thread, BlockId, BoolThen, Channel, FullHash, HeaderEntry}; +#[cfg(not(feature = "liquid"))] +use bitcoin::consensus::encode::serialize_hex; +#[cfg(feature = "liquid")] +use elements::encode::serialize_hex; const ELECTRS_VERSION: &str = env!("CARGO_PKG_VERSION"); const PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(1, 4); @@ -799,7 +799,7 @@ impl RPC { config: Arc, query: Arc, metrics: &Metrics, - salt_rwlock: Arc> + salt_rwlock: Arc>, ) -> RPC { let stats = Arc::new(Stats { latency: metrics.histogram_vec( diff --git a/src/new_index/db.rs b/src/new_index/db.rs index fb1766508..e422b8afe 100644 --- a/src/new_index/db.rs +++ b/src/new_index/db.rs @@ -97,8 +97,9 @@ impl DB { db_opts.set_target_file_size_base(1_073_741_824); db_opts.set_disable_auto_compactions(!config.initial_sync_compaction); // for initial bulk load - - let parallelism: i32 = config.db_parallelism.try_into() + let parallelism: i32 = config + .db_parallelism + .try_into() .expect("db_parallelism value too large for i32"); // Configure parallelism (background jobs and thread pools) @@ -117,7 +118,7 @@ impl DB { db_opts.set_block_based_table_factory(&block_opts); let db = DB { - db: Arc::new(rocksdb::DB::open(&db_opts, path).expect("failed to open RocksDB")) + db: Arc::new(rocksdb::DB::open(&db_opts, path).expect("failed to open RocksDB")), }; if verify_compat { db.verify_compatibility(config); @@ -242,7 +243,8 @@ impl DB { } fn verify_compatibility(&self, config: &Config) { - let compatibility_bytes = bincode::serialize_little(&(DB_VERSION, config.light_mode)).unwrap(); + let compatibility_bytes = + bincode::serialize_little(&(DB_VERSION, config.light_mode)).unwrap(); match self.get(b"V") { None => self.put(b"V", &compatibility_bytes), @@ -266,39 +268,114 @@ impl DB { }; spawn_thread("db_stats_exporter", move || loop { - update_gauge(&db_metrics.num_immutable_mem_table, "rocksdb.num-immutable-mem-table"); - update_gauge(&db_metrics.mem_table_flush_pending, "rocksdb.mem-table-flush-pending"); + update_gauge( + &db_metrics.num_immutable_mem_table, + "rocksdb.num-immutable-mem-table", + ); + update_gauge( + &db_metrics.mem_table_flush_pending, + "rocksdb.mem-table-flush-pending", + ); update_gauge(&db_metrics.compaction_pending, "rocksdb.compaction-pending"); update_gauge(&db_metrics.background_errors, "rocksdb.background-errors"); - update_gauge(&db_metrics.cur_size_active_mem_table, "rocksdb.cur-size-active-mem-table"); - update_gauge(&db_metrics.cur_size_all_mem_tables, "rocksdb.cur-size-all-mem-tables"); - update_gauge(&db_metrics.size_all_mem_tables, "rocksdb.size-all-mem-tables"); - update_gauge(&db_metrics.num_entries_active_mem_table, "rocksdb.num-entries-active-mem-table"); - update_gauge(&db_metrics.num_entries_imm_mem_tables, "rocksdb.num-entries-imm-mem-tables"); - update_gauge(&db_metrics.num_deletes_active_mem_table, "rocksdb.num-deletes-active-mem-table"); - update_gauge(&db_metrics.num_deletes_imm_mem_tables, "rocksdb.num-deletes-imm-mem-tables"); + update_gauge( + &db_metrics.cur_size_active_mem_table, + "rocksdb.cur-size-active-mem-table", + ); + update_gauge( + &db_metrics.cur_size_all_mem_tables, + "rocksdb.cur-size-all-mem-tables", + ); + update_gauge( + &db_metrics.size_all_mem_tables, + "rocksdb.size-all-mem-tables", + ); + update_gauge( + &db_metrics.num_entries_active_mem_table, + "rocksdb.num-entries-active-mem-table", + ); + update_gauge( + &db_metrics.num_entries_imm_mem_tables, + "rocksdb.num-entries-imm-mem-tables", + ); + update_gauge( + &db_metrics.num_deletes_active_mem_table, + "rocksdb.num-deletes-active-mem-table", + ); + update_gauge( + &db_metrics.num_deletes_imm_mem_tables, + "rocksdb.num-deletes-imm-mem-tables", + ); update_gauge(&db_metrics.estimate_num_keys, "rocksdb.estimate-num-keys"); - update_gauge(&db_metrics.estimate_table_readers_mem, "rocksdb.estimate-table-readers-mem"); - update_gauge(&db_metrics.is_file_deletions_enabled, "rocksdb.is-file-deletions-enabled"); + update_gauge( + &db_metrics.estimate_table_readers_mem, + "rocksdb.estimate-table-readers-mem", + ); + update_gauge( + &db_metrics.is_file_deletions_enabled, + "rocksdb.is-file-deletions-enabled", + ); update_gauge(&db_metrics.num_snapshots, "rocksdb.num-snapshots"); - update_gauge(&db_metrics.oldest_snapshot_time, "rocksdb.oldest-snapshot-time"); + update_gauge( + &db_metrics.oldest_snapshot_time, + "rocksdb.oldest-snapshot-time", + ); update_gauge(&db_metrics.num_live_versions, "rocksdb.num-live-versions"); - update_gauge(&db_metrics.current_super_version_number, "rocksdb.current-super-version-number"); - update_gauge(&db_metrics.estimate_live_data_size, "rocksdb.estimate-live-data-size"); - update_gauge(&db_metrics.min_log_number_to_keep, "rocksdb.min-log-number-to-keep"); - update_gauge(&db_metrics.min_obsolete_sst_number_to_keep, "rocksdb.min-obsolete-sst-number-to-keep"); - update_gauge(&db_metrics.total_sst_files_size, "rocksdb.total-sst-files-size"); - update_gauge(&db_metrics.live_sst_files_size, "rocksdb.live-sst-files-size"); + update_gauge( + &db_metrics.current_super_version_number, + "rocksdb.current-super-version-number", + ); + update_gauge( + &db_metrics.estimate_live_data_size, + "rocksdb.estimate-live-data-size", + ); + update_gauge( + &db_metrics.min_log_number_to_keep, + "rocksdb.min-log-number-to-keep", + ); + update_gauge( + &db_metrics.min_obsolete_sst_number_to_keep, + "rocksdb.min-obsolete-sst-number-to-keep", + ); + update_gauge( + &db_metrics.total_sst_files_size, + "rocksdb.total-sst-files-size", + ); + update_gauge( + &db_metrics.live_sst_files_size, + "rocksdb.live-sst-files-size", + ); update_gauge(&db_metrics.base_level, "rocksdb.base-level"); - update_gauge(&db_metrics.estimate_pending_compaction_bytes, "rocksdb.estimate-pending-compaction-bytes"); - update_gauge(&db_metrics.num_running_compactions, "rocksdb.num-running-compactions"); - update_gauge(&db_metrics.num_running_flushes, "rocksdb.num-running-flushes"); - update_gauge(&db_metrics.actual_delayed_write_rate, "rocksdb.actual-delayed-write-rate"); + update_gauge( + &db_metrics.estimate_pending_compaction_bytes, + "rocksdb.estimate-pending-compaction-bytes", + ); + update_gauge( + &db_metrics.num_running_compactions, + "rocksdb.num-running-compactions", + ); + update_gauge( + &db_metrics.num_running_flushes, + "rocksdb.num-running-flushes", + ); + update_gauge( + &db_metrics.actual_delayed_write_rate, + "rocksdb.actual-delayed-write-rate", + ); update_gauge(&db_metrics.is_write_stopped, "rocksdb.is-write-stopped"); - update_gauge(&db_metrics.estimate_oldest_key_time, "rocksdb.estimate-oldest-key-time"); - update_gauge(&db_metrics.block_cache_capacity, "rocksdb.block-cache-capacity"); + update_gauge( + &db_metrics.estimate_oldest_key_time, + "rocksdb.estimate-oldest-key-time", + ); + update_gauge( + &db_metrics.block_cache_capacity, + "rocksdb.block-cache-capacity", + ); update_gauge(&db_metrics.block_cache_usage, "rocksdb.block-cache-usage"); - update_gauge(&db_metrics.block_cache_pinned_usage, "rocksdb.block-cache-pinned-usage"); + update_gauge( + &db_metrics.block_cache_pinned_usage, + "rocksdb.block-cache-pinned-usage", + ); thread::sleep(Duration::from_secs(5)); }); } diff --git a/src/new_index/fetch.rs b/src/new_index/fetch.rs index 7906fb206..58dab9b39 100644 --- a/src/new_index/fetch.rs +++ b/src/new_index/fetch.rs @@ -89,7 +89,8 @@ fn bitcoind_fetcher( let total_blocks_fetched = new_headers.len(); for entries in new_headers.chunks(100) { if fetcher_count % 50 == 0 && total_blocks_fetched >= 50 { - info!("fetching blocks {}/{} ({:.1}%)", + info!( + "fetching blocks {}/{} ({:.1}%)", blocks_fetched, total_blocks_fetched, blocks_fetched as f32 / total_blocks_fetched as f32 * 100.0 @@ -148,10 +149,11 @@ fn blkfiles_fetcher( .into_iter() .filter_map(|(block, size)| { index += 1; - debug!("fetch block {:}/{:} {:.2}%", + debug!( + "fetch block {:}/{:} {:.2}%", index, block_count, - (index/block_count) as f32/100.0 + (index / block_count) as f32 / 100.0 ); let blockhash = block.block_hash(); entry_map @@ -188,7 +190,8 @@ fn blkfiles_reader(blk_files: Vec, xor_key: Option<[u8; 8]>) -> Fetcher spawn_thread("blkfiles_reader", move || { let blk_files_len = blk_files.len(); for (count, path) in blk_files.iter().enumerate() { - info!("block file reading {:}/{:} {:.2}%", + info!( + "block file reading {:}/{:} {:.2}%", count, blk_files_len, count / blk_files_len diff --git a/src/new_index/precache.rs b/src/new_index/precache.rs index ea4710431..28ccea20e 100644 --- a/src/new_index/precache.rs +++ b/src/new_index/precache.rs @@ -1,3 +1,4 @@ +#[cfg(not(feature = "litecoin"))] use crate::chain::address::Address; use crate::errors::*; use crate::new_index::ChainQuery; @@ -11,6 +12,7 @@ use bitcoin::hex::FromHex; use std::fs::File; use std::io; use std::io::prelude::*; +#[cfg(not(feature = "litecoin"))] use std::str::FromStr; use electrs_macros::trace; @@ -65,12 +67,32 @@ fn to_scripthash(script_type: &str, script_str: &str) -> Result { } fn address_to_scripthash(addr: &str) -> Result { - let addr = Address::from_str(addr).chain_err(|| "invalid address")?; + #[cfg(not(feature = "litecoin"))] + { + let addr = Address::from_str(addr).chain_err(|| "invalid address")?; - #[cfg(not(feature = "liquid"))] - let addr = addr.assume_checked(); + #[cfg(not(feature = "liquid"))] + let addr = addr.assume_checked(); - Ok(compute_script_hash(&addr.script_pubkey().as_bytes())) + Ok(compute_script_hash(&addr.script_pubkey().as_bytes())) + } + + #[cfg(feature = "litecoin")] + { + // Try all Litecoin networks for precache (network doesn't matter for scripthash) + let networks = [ + crate::chain::Network::Litecoin, + crate::chain::Network::LitecoinTestnet, + crate::chain::Network::LitecoinRegtest, + ]; + for network in &networks { + if let Some(script) = crate::util::litecoin_addr::parse_litecoin_address(addr, *network) + { + return Ok(compute_script_hash(&script)); + } + } + bail!("invalid Litecoin address") + } } pub fn compute_script_hash(data: &[u8]) -> FullHash { diff --git a/src/new_index/schema.rs b/src/new_index/schema.rs index 22e3fe5b6..d5f7a974a 100644 --- a/src/new_index/schema.rs +++ b/src/new_index/schema.rs @@ -21,9 +21,6 @@ use std::collections::{BTreeSet, HashMap, HashSet}; use std::convert::TryInto; use std::sync::{Arc, RwLock, RwLockReadGuard}; -use crate::{chain::{ - BlockHash, BlockHeader, Network, OutPoint, Script, Transaction, TxOut, Txid, Value, -}, new_index::db_metrics::RocksDbMetrics}; use crate::config::Config; use crate::daemon::Daemon; use crate::errors::*; @@ -32,6 +29,10 @@ use crate::util::{ bincode, full_hash, has_prevout, is_spendable, BlockHeaderMeta, BlockId, BlockMeta, BlockStatus, Bytes, HeaderEntry, HeaderList, ScriptToAddr, }; +use crate::{ + chain::{BlockHash, BlockHeader, Network, OutPoint, Script, Transaction, TxOut, Txid, Value}, + new_index::db_metrics::RocksDbMetrics, +}; use crate::new_index::db::{DBFlush, DBRow, ReverseScanIterator, ScanIterator, DB}; use crate::new_index::fetch::{start_fetcher, BlockEntry, FetchFrom}; @@ -345,20 +346,20 @@ impl Indexer { let mut blocks_fetched = 0; let to_add_total = to_add.len(); - start_fetcher(self.from, &daemon, to_add)?.map(|blocks| - { - if fetcher_count % 25 == 0 && to_add_total > 20 { - info!("adding txes from blocks {}/{} ({:.1}%)", - blocks_fetched, - to_add_total, - blocks_fetched as f32 / to_add_total as f32 * 100.0 - ); - } - fetcher_count += 1; - blocks_fetched += blocks.len(); + start_fetcher(self.from, &daemon, to_add)?.map(|blocks| { + if fetcher_count % 25 == 0 && to_add_total > 20 { + info!( + "adding txes from blocks {}/{} ({:.1}%)", + blocks_fetched, + to_add_total, + blocks_fetched as f32 / to_add_total as f32 * 100.0 + ); + } + fetcher_count += 1; + blocks_fetched += blocks.len(); - self.add(&blocks) - }); + self.add(&blocks) + }); self.start_auto_compactions(&self.store.txstore_db); diff --git a/src/rest.rs b/src/rest.rs index f991351bb..f7c3a3732 100644 --- a/src/rest.rs +++ b/src/rest.rs @@ -1047,8 +1047,8 @@ fn handle_request( HttpError::from(format!("Invalid transaction hex for item {}", index)) })? .filter(|r| r.is_err()) - .next() - .transpose() + .next() + .transpose() .map_err(|_| { HttpError::from(format!("Invalid transaction hex for item {}", index)) }) @@ -1274,25 +1274,34 @@ fn to_scripthash( } fn address_to_scripthash(addr: &str, network: Network) -> Result { - #[cfg(not(feature = "liquid"))] - let addr = address::Address::from_str(addr)?; - #[cfg(feature = "liquid")] - let addr = address::Address::parse_with_params(addr, network.address_params())?; - - #[cfg(not(feature = "liquid"))] - let is_expected_net = addr.is_valid_for_network(network.into()); + #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + { + let addr = address::Address::from_str(addr)?; + let is_expected_net = addr.is_valid_for_network(network.into()); + if !is_expected_net { + bail!(HttpError::from("Address on invalid network".to_string())) + } + let addr = addr.assume_checked(); + Ok(compute_script_hash(&addr.script_pubkey())) + } #[cfg(feature = "liquid")] - let is_expected_net = addr.params == network.address_params(); - - if !is_expected_net { - bail!(HttpError::from("Address on invalid network".to_string())) + { + let addr = address::Address::parse_with_params(addr, network.address_params())?; + let is_expected_net = addr.params == network.address_params(); + if !is_expected_net { + bail!(HttpError::from("Address on invalid network".to_string())) + } + Ok(compute_script_hash(&addr.script_pubkey())) } - #[cfg(not(feature = "liquid"))] - let addr = addr.assume_checked(); - - Ok(compute_script_hash(&addr.script_pubkey())) + #[cfg(feature = "litecoin")] + { + let script_bytes = crate::util::litecoin_addr::parse_litecoin_address(addr, network) + .ok_or_else(|| HttpError::from("Invalid Litecoin address".to_string()))?; + let script = Script::from(script_bytes); + Ok(compute_script_hash(&script)) + } } fn parse_scripthash(scripthash: &str) -> Result { diff --git a/src/util/litecoin_addr.rs b/src/util/litecoin_addr.rs new file mode 100644 index 000000000..416c93f30 --- /dev/null +++ b/src/util/litecoin_addr.rs @@ -0,0 +1,419 @@ +use bitcoin::hashes::{sha256d, Hash}; +use bitcoin::Script; + +use crate::chain::Network; + +/// Version bytes for Litecoin base58 addresses +const LITECOIN_P2PKH_VERSION: u8 = 0x30; // 'L' prefix +const LITECOIN_P2SH_VERSION: u8 = 0x32; // 'M' prefix +const LITECOIN_TESTNET_P2PKH_VERSION: u8 = 0x6f; // 'm' or 'n' prefix +const LITECOIN_TESTNET_P2SH_VERSION: u8 = 0xc4; // '2' prefix + +fn network_params(network: Network) -> (u8, u8, &'static str) { + match network { + Network::Litecoin => (LITECOIN_P2PKH_VERSION, LITECOIN_P2SH_VERSION, "ltc"), + Network::LitecoinTestnet => ( + LITECOIN_TESTNET_P2PKH_VERSION, + LITECOIN_TESTNET_P2SH_VERSION, + "tltc", + ), + Network::LitecoinRegtest => ( + LITECOIN_TESTNET_P2PKH_VERSION, + LITECOIN_TESTNET_P2SH_VERSION, + "rltc", + ), + } +} + +/// Convert a scriptPubKey to a Litecoin address string +pub fn script_to_litecoin_address(script: &Script, network: Network) -> Option { + let (p2pkh_ver, p2sh_ver, hrp) = network_params(network); + let bytes = script.as_bytes(); + + if script.is_p2pkh() && bytes.len() == 25 { + Some(base58check_encode(p2pkh_ver, &bytes[3..23])) + } else if script.is_p2sh() && bytes.len() == 23 { + Some(base58check_encode(p2sh_ver, &bytes[2..22])) + } else if script.is_p2wpkh() || script.is_p2wsh() || script.is_p2tr() { + // Segwit: witness version byte + push length + witness program + if bytes.len() < 2 { + return None; + } + let witness_version = if bytes[0] == 0x00 { + 0u8 + } else if (0x51..=0x60).contains(&bytes[0]) { + bytes[0] - 0x50 + } else { + return None; + }; + let program = &bytes[2..]; + bech32_segwit_encode(hrp, witness_version, program) + } else { + None + } +} + +/// Parse a Litecoin address string and return the scriptPubKey bytes +pub fn parse_litecoin_address(addr: &str, network: Network) -> Option> { + let (p2pkh_ver, p2sh_ver, hrp) = network_params(network); + + // Try bech32/bech32m first + if let Some(script) = bech32_segwit_decode(addr, hrp) { + return Some(script); + } + + // Try base58check + let (version, hash) = base58check_decode(addr)?; + if version == p2pkh_ver && hash.len() == 20 { + // P2PKH: OP_DUP OP_HASH160 <20> OP_EQUALVERIFY OP_CHECKSIG + let mut script = vec![0x76, 0xa9, 0x14]; + script.extend_from_slice(&hash); + script.extend_from_slice(&[0x88, 0xac]); + Some(script) + } else if version == p2sh_ver && hash.len() == 20 { + // P2SH: OP_HASH160 <20> OP_EQUAL + let mut script = vec![0xa9, 0x14]; + script.extend_from_slice(&hash); + script.push(0x87); + Some(script) + } else { + None + } +} + +/// Validate that a Litecoin address is valid for the given network +pub fn validate_litecoin_address(addr: &str, network: Network) -> bool { + parse_litecoin_address(addr, network).is_some() +} + +// --- Base58Check --- + +fn base58check_encode(version: u8, payload: &[u8]) -> String { + let mut data = Vec::with_capacity(1 + payload.len() + 4); + data.push(version); + data.extend_from_slice(payload); + let checksum = sha256d::Hash::hash(&data); + data.extend_from_slice(&checksum[..4]); + base58_encode(&data) +} + +fn base58check_decode(addr: &str) -> Option<(u8, Vec)> { + let data = base58_decode(addr)?; + if data.len() < 5 { + return None; + } + let (payload, checksum) = data.split_at(data.len() - 4); + let hash = sha256d::Hash::hash(payload); + if &hash[..4] != checksum { + return None; + } + Some((payload[0], payload[1..].to_vec())) +} + +const BASE58_ALPHABET: &[u8] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + +fn base58_encode(data: &[u8]) -> String { + let leading_zeros = data.iter().take_while(|&&b| b == 0).count(); + let mut num = data.to_vec(); + let mut result = Vec::new(); + + while !num.is_empty() { + let mut remainder = 0u32; + let mut new_num = Vec::new(); + for &byte in &num { + let acc = (remainder << 8) | byte as u32; + let digit = acc / 58; + remainder = acc % 58; + if !new_num.is_empty() || digit > 0 { + new_num.push(digit as u8); + } + } + result.push(BASE58_ALPHABET[remainder as usize]); + num = new_num; + } + + for _ in 0..leading_zeros { + result.push(b'1'); + } + result.reverse(); + String::from_utf8(result).unwrap() +} + +fn base58_decode(s: &str) -> Option> { + let leading_ones = s.chars().take_while(|&c| c == '1').count(); + let mut result: Vec = Vec::new(); + + for ch in s.chars() { + let pos = BASE58_ALPHABET.iter().position(|&c| c == ch as u8)? as u32; + let mut carry = pos; + for byte in result.iter_mut().rev() { + let acc = (*byte as u32) * 58 + carry; + *byte = (acc & 0xff) as u8; + carry = acc >> 8; + } + while carry > 0 { + result.insert(0, (carry & 0xff) as u8); + carry >>= 8; + } + } + + let mut final_result = vec![0u8; leading_ones]; + final_result.extend_from_slice(&result); + Some(final_result) +} + +// --- Bech32/Bech32m --- + +const BECH32_CHARSET: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l"; +const BECH32_CONST: u32 = 1; +const BECH32M_CONST: u32 = 0x2bc830a3; + +fn bech32_polymod(values: &[u8]) -> u32 { + let mut chk: u32 = 1; + for &v in values { + let top = chk >> 25; + chk = ((chk & 0x1ffffff) << 5) ^ (v as u32); + if top & 1 != 0 { + chk ^= 0x3b6a57b2; + } + if top & 2 != 0 { + chk ^= 0x26508e6d; + } + if top & 4 != 0 { + chk ^= 0x1ea119fa; + } + if top & 8 != 0 { + chk ^= 0x3d4233dd; + } + if top & 16 != 0 { + chk ^= 0x2a1462b3; + } + } + chk +} + +fn bech32_hrp_expand(hrp: &str) -> Vec { + let mut ret = Vec::with_capacity(hrp.len() * 2 + 1); + for c in hrp.chars() { + ret.push((c as u8) >> 5); + } + ret.push(0); + for c in hrp.chars() { + ret.push((c as u8) & 31); + } + ret +} + +fn bech32_create_checksum(hrp: &str, data: &[u8], spec: u32) -> Vec { + let mut values = bech32_hrp_expand(hrp); + values.extend_from_slice(data); + values.extend_from_slice(&[0, 0, 0, 0, 0, 0]); + let polymod = bech32_polymod(&values) ^ spec; + (0..6) + .map(|i| ((polymod >> (5 * (5 - i))) & 31) as u8) + .collect() +} + +fn bech32_verify_checksum(hrp: &str, data: &[u8]) -> Option { + let mut values = bech32_hrp_expand(hrp); + values.extend_from_slice(data); + let polymod = bech32_polymod(&values); + if polymod == BECH32_CONST { + Some(BECH32_CONST) + } else if polymod == BECH32M_CONST { + Some(BECH32M_CONST) + } else { + None + } +} + +fn bech32_segwit_encode(hrp: &str, witness_version: u8, witness_program: &[u8]) -> Option { + // Convert 8-bit program to 5-bit groups + let mut data5 = vec![witness_version]; + data5.extend(convert_bits(witness_program, 8, 5, true)?); + + let spec = if witness_version == 0 { + BECH32_CONST + } else { + BECH32M_CONST + }; + let checksum = bech32_create_checksum(hrp, &data5, spec); + + let mut result = String::from(hrp); + result.push('1'); + for &d in data5.iter().chain(checksum.iter()) { + result.push(BECH32_CHARSET[d as usize] as char); + } + Some(result) +} + +fn bech32_segwit_decode(addr: &str, expected_hrp: &str) -> Option> { + let addr_lower = addr.to_lowercase(); + let pos = addr_lower.rfind('1')?; + if pos == 0 || pos + 7 > addr_lower.len() { + return None; + } + + let hrp = &addr_lower[..pos]; + if hrp != expected_hrp { + return None; + } + + let data_part = &addr_lower[pos + 1..]; + let mut data: Vec = Vec::with_capacity(data_part.len()); + for c in data_part.chars() { + let pos = BECH32_CHARSET.iter().position(|&ch| ch == c as u8)?; + data.push(pos as u8); + } + + let spec = bech32_verify_checksum(hrp, &data)?; + + // Remove checksum (last 6 chars) + let data = &data[..data.len() - 6]; + if data.is_empty() { + return None; + } + + let witness_version = data[0]; + if witness_version > 16 { + return None; + } + + // Verify correct bech32 variant + if witness_version == 0 && spec != BECH32_CONST { + return None; + } + if witness_version != 0 && spec != BECH32M_CONST { + return None; + } + + let program = convert_bits(&data[1..], 5, 8, false)?; + + // Validate program length per BIP141 + if program.len() < 2 || program.len() > 40 { + return None; + } + if witness_version == 0 && program.len() != 20 && program.len() != 32 { + return None; + } + + // Build scriptPubKey + let opcode = if witness_version == 0 { + 0x00u8 + } else { + 0x50 + witness_version + }; + let mut script = vec![opcode, program.len() as u8]; + script.extend_from_slice(&program); + Some(script) +} + +fn convert_bits(data: &[u8], from_bits: u32, to_bits: u32, pad: bool) -> Option> { + let mut acc: u32 = 0; + let mut bits: u32 = 0; + let mut ret = Vec::new(); + let maxv = (1u32 << to_bits) - 1; + + for &value in data { + if (value as u32) >> from_bits != 0 { + return None; + } + acc = (acc << from_bits) | value as u32; + bits += from_bits; + while bits >= to_bits { + bits -= to_bits; + ret.push(((acc >> bits) & maxv) as u8); + } + } + + if pad { + if bits > 0 { + ret.push(((acc << (to_bits - bits)) & maxv) as u8); + } + } else if bits >= from_bits || ((acc << (to_bits - bits)) & maxv) != 0 { + return None; + } + + Some(ret) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_base58check_roundtrip() { + let hash = [0u8; 20]; + let encoded = base58check_encode(LITECOIN_P2PKH_VERSION, &hash); + let (version, decoded) = base58check_decode(&encoded).unwrap(); + assert_eq!(version, LITECOIN_P2PKH_VERSION); + assert_eq!(decoded, hash); + } + + #[test] + fn test_litecoin_p2pkh_address_prefix() { + let hash = [1u8; 20]; + let addr = base58check_encode(LITECOIN_P2PKH_VERSION, &hash); + assert!( + addr.starts_with('L') || addr.starts_with('K'), + "P2PKH address should start with L or K, got: {}", + addr + ); + } + + #[test] + fn test_litecoin_p2sh_address_prefix() { + let hash = [1u8; 20]; + let addr = base58check_encode(LITECOIN_P2SH_VERSION, &hash); + assert!( + addr.starts_with('M'), + "P2SH address should start with M, got: {}", + addr + ); + } + + #[test] + fn test_bech32_encode_decode_roundtrip() { + let program = [0u8; 20]; // P2WPKH-like + let encoded = bech32_segwit_encode("ltc", 0, &program).unwrap(); + assert!(encoded.starts_with("ltc1")); + + let script = bech32_segwit_decode(&encoded, "ltc").unwrap(); + // script should be: OP_0 <20> <20 zero bytes> + assert_eq!(script[0], 0x00); // OP_0 + assert_eq!(script[1], 20); // push 20 bytes + assert_eq!(&script[2..], &program); + } + + #[test] + fn test_bech32m_encode_decode_roundtrip() { + let program = [1u8; 32]; // P2TR-like (witness v1, 32 bytes) + let encoded = bech32_segwit_encode("ltc", 1, &program).unwrap(); + assert!(encoded.starts_with("ltc1p")); + + let script = bech32_segwit_decode(&encoded, "ltc").unwrap(); + assert_eq!(script[0], 0x51); // OP_1 + assert_eq!(script[1], 32); + assert_eq!(&script[2..], &program); + } + + #[test] + fn test_bech32_wrong_hrp_rejected() { + let program = [0u8; 20]; + let encoded = bech32_segwit_encode("ltc", 0, &program).unwrap(); + assert!(bech32_segwit_decode(&encoded, "tltc").is_none()); + } + + #[test] + fn test_parse_and_validate_roundtrip() { + // Create a P2PKH scriptPubKey + let mut p2pkh_script = vec![0x76, 0xa9, 0x14]; + p2pkh_script.extend_from_slice(&[1u8; 20]); + p2pkh_script.extend_from_slice(&[0x88, 0xac]); + + let script = bitcoin::ScriptBuf::from(p2pkh_script.clone()); + let addr = script_to_litecoin_address(&script, Network::Litecoin).unwrap(); + let decoded_script = parse_litecoin_address(&addr, Network::Litecoin).unwrap(); + assert_eq!(decoded_script, p2pkh_script); + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs index d5e8f1be8..ada13267e 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,4 +1,6 @@ mod block; +#[cfg(feature = "litecoin")] +pub mod litecoin_addr; mod script; mod transaction; diff --git a/src/util/script.rs b/src/util/script.rs index 9f44c4ea6..bffd5489b 100644 --- a/src/util/script.rs +++ b/src/util/script.rs @@ -24,7 +24,7 @@ impl ScriptToAsm for elements::Script {} pub trait ScriptToAddr { fn to_address_str(&self, network: Network) -> Option; } -#[cfg(not(feature = "liquid"))] +#[cfg(not(any(feature = "liquid", feature = "litecoin")))] impl ScriptToAddr for bitcoin::Script { fn to_address_str(&self, network: Network) -> Option { bitcoin::Address::from_script(self, bitcoin::Network::from(network)) @@ -32,6 +32,12 @@ impl ScriptToAddr for bitcoin::Script { .ok() } } +#[cfg(feature = "litecoin")] +impl ScriptToAddr for bitcoin::Script { + fn to_address_str(&self, network: Network) -> Option { + crate::util::litecoin_addr::script_to_litecoin_address(self, network) + } +} #[cfg(feature = "liquid")] impl ScriptToAddr for elements::Script { fn to_address_str(&self, network: Network) -> Option { diff --git a/tests/electrum.rs b/tests/electrum.rs index a3e54327f..b2c2f50e3 100644 --- a/tests/electrum.rs +++ b/tests/electrum.rs @@ -32,8 +32,7 @@ impl WalletTester { vec!["--server", &server_arg] }; electrum_wallet_conf.view_stdout = true; - let electrum_wallet = - ElectrumD::with_conf(electrumd::exe_path()?, &electrum_wallet_conf)?; + let electrum_wallet = ElectrumD::with_conf(electrumd::exe_path()?, &electrum_wallet_conf)?; log::info!( "Electrum wallet version: {:?}", diff --git a/tests/rest.rs b/tests/rest.rs index 921c82333..33a702e8e 100644 --- a/tests/rest.rs +++ b/tests/rest.rs @@ -233,10 +233,7 @@ fn test_rest_blocks() -> Result<()> { ); // Verify first block (tip) has correct height - assert_eq!( - last_blocks[0]["height"].as_u64(), - Some(bestblockheight) - ); + assert_eq!(last_blocks[0]["height"].as_u64(), Some(bestblockheight)); // Verify block list entries have all BlockValue fields with value checks for block in last_blocks { @@ -296,8 +293,7 @@ fn test_rest_block() -> Result<()> { assert_eq!(res["tx_count"].as_u64(), Some(2)); // Cross-reference BlockValue fields against bitcoind's getblockheader - let node_header: Value = - tester.call("getblockheader", &[blockhash.to_string().into()])?; + let node_header: Value = tester.call("getblockheader", &[blockhash.to_string().into()])?; assert_eq!(res["version"].as_u64(), node_header["version"].as_u64()); assert_eq!(res["timestamp"].as_u64(), node_header["time"].as_u64()); assert_eq!( @@ -308,7 +304,10 @@ fn test_rest_block() -> Result<()> { res["previousblockhash"].as_str(), node_header["previousblockhash"].as_str() ); - assert_eq!(res["mediantime"].as_u64(), node_header["mediantime"].as_u64()); + assert_eq!( + res["mediantime"].as_u64(), + node_header["mediantime"].as_u64() + ); assert!(res["size"].as_u64().unwrap() > 0); assert!(res["weight"].as_u64().unwrap() > 0); #[cfg(not(feature = "liquid"))] @@ -378,10 +377,7 @@ fn test_rest_mempool() -> Result<()> { assert_eq!(mempool_after["count"].as_u64(), Some(0)); assert_eq!(mempool_after["vsize"].as_u64(), Some(0)); assert_eq!(mempool_after["total_fee"].as_u64(), Some(0)); - assert_eq!( - mempool_after["fee_histogram"].as_array().unwrap().len(), - 0 - ); + assert_eq!(mempool_after["fee_histogram"].as_array().unwrap().len(), 0); rest_handle.stop(); Ok(()) @@ -491,9 +487,7 @@ fn test_rest_block_txids() -> Result<()> { "first txid should be coinbase, not user tx" ); // Our txid should be present - assert!(txids - .iter() - .any(|t| t.as_str() == Some(&txid.to_string()))); + assert!(txids.iter().any(|t| t.as_str() == Some(&txid.to_string()))); rest_handle.stop(); Ok(()) @@ -570,7 +564,10 @@ fn test_rest_address_utxo() -> Result<()> { ); assert!(utxos[0]["vout"].is_u64()); assert_eq!(utxos[0]["status"]["confirmed"].as_bool(), Some(true)); - assert_eq!(utxos[0]["status"]["block_height"].as_u64(), Some(mine_height)); + assert_eq!( + utxos[0]["status"]["block_height"].as_u64(), + Some(mine_height) + ); assert!(utxos[0]["status"]["block_hash"].is_string()); assert!(utxos[0]["status"]["block_time"].as_u64().unwrap() > 0); #[cfg(not(feature = "liquid"))] @@ -636,7 +633,10 @@ fn test_rest_scripthash() -> Result<()> { // Verify /scripthash/:hash/txs/mempool matches /address/:address/txs/mempool let addr_mempool = get_json(rest_addr, &format!("/address/{}/txs/mempool", addr1))?; - let sh_mempool = get_json(rest_addr, &format!("/scripthash/{}/txs/mempool", scripthash))?; + let sh_mempool = get_json( + rest_addr, + &format!("/scripthash/{}/txs/mempool", scripthash), + )?; assert_eq!(addr_mempool, sh_mempool); // Verify /scripthash/:hash/utxo matches /address/:address/utxo @@ -684,7 +684,10 @@ fn test_rest_tx_outspends() -> Result<()> { ); assert_eq!(spent_entry["vin"].as_u64(), Some(spent_vin)); assert_eq!(spent_entry["status"]["confirmed"].as_bool(), Some(true)); - assert_eq!(spent_entry["status"]["block_height"].as_u64(), Some(mine_height)); + assert_eq!( + spent_entry["status"]["block_height"].as_u64(), + Some(mine_height) + ); assert!(spent_entry["status"]["block_hash"].is_string()); assert!(spent_entry["status"]["block_time"].as_u64().unwrap() > 0); @@ -759,10 +762,7 @@ fn test_rest_mempool_recent() -> Result<()> { } // Verify our sent txids are included - let recent_txids: HashSet<&str> = recent - .iter() - .map(|e| e["txid"].as_str().unwrap()) - .collect(); + let recent_txids: HashSet<&str> = recent.iter().map(|e| e["txid"].as_str().unwrap()).collect(); assert!(recent_txids.contains(txid1.to_string().as_str())); assert!(recent_txids.contains(txid2.to_string().as_str())); From 16560f333320239bf11af120dae4432e94aa74bf Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Feb 2026 06:39:19 +0000 Subject: [PATCH 4/4] Add Dogecoin network support via `dogecoin` feature flag Implements full Dogecoin support including: - AuxPoW (Auxiliary Proof of Work) block deserialization for merged mining - Dogecoin address encoding/decoding (P2PKH/P2SH base58check, no SegWit) - Network configuration (magic bytes, genesis hashes, RPC ports, data dirs) - Config, REST API, Electrum, and precache integration with Dogecoin addresses - Block deserialization abstraction via chain::deserialize_block() used by daemon.rs and fetch.rs, dispatching to AuxPoW-aware parser for Dogecoin The dogecoin feature is mutually exclusive with liquid and litecoin. Networks: dogecoin, dogecointestnet, dogecoinregtest. https://claude.ai/code/session_01S9ujRXvjmfhRpmdYdjxRk5 --- CLAUDE.md | 14 +- Cargo.toml | 1 + src/chain.rs | 118 ++++++++-- src/config.rs | 93 +++++--- src/daemon.rs | 3 +- src/electrum/discovery/default_servers.rs | 10 +- src/new_index/fetch.rs | 11 +- src/new_index/precache.rs | 22 +- src/rest.rs | 10 +- src/util/dogecoin.rs | 271 ++++++++++++++++++++++ src/util/mod.rs | 2 + src/util/script.rs | 8 +- 12 files changed, 505 insertions(+), 58 deletions(-) create mode 100644 src/util/dogecoin.rs diff --git a/CLAUDE.md b/CLAUDE.md index ae08e4e60..cdbd975fc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ ## Project Overview -Electrs (Esplora variant) is a blockchain index engine and HTTP API for Bitcoin (and Liquid/Litecoin) written in Rust. It is the backend for the [Esplora block explorer](https://github.com/Blockstream/esplora) powering blockstream.info. Forked from [romanz/electrs](https://github.com/romanz/electrs), it adds an HTTP REST API, extended indexes, and Elements/Liquid support, as well as Litecoin support. +Electrs (Esplora variant) is a blockchain index engine and HTTP API for Bitcoin (and Liquid/Litecoin/Dogecoin) written in Rust. It is the backend for the [Esplora block explorer](https://github.com/Blockstream/esplora) powering blockstream.info. Forked from [romanz/electrs](https://github.com/romanz/electrs), it adds an HTTP REST API, extended indexes, and Elements/Liquid support, as well as Litecoin and Dogecoin support. ## Build & Run @@ -22,6 +22,12 @@ cargo build --features litecoin --release # Run with Litecoin (requires a running litecoind) cargo run --features litecoin --release --bin electrs -- -vvvv --daemon-dir ~/.litecoin +# Build with Dogecoin support +cargo build --features dogecoin --release + +# Run with Dogecoin (requires a running dogecoind) +cargo run --features dogecoin --release --bin electrs -- -vvvv --daemon-dir ~/.dogecoin + # Build with OpenTelemetry tracing cargo build --features otlp-tracing --release ``` @@ -84,6 +90,7 @@ The pre-commit hook (`.hooks/pre-commit`) runs `cargo +stable fmt --all -- --che - **`elements/`** — Liquid/Elements-specific code (behind `liquid` feature flag). - **`util/`** — Helpers for transactions, scripts, fees, merkle proofs, and block parsing. - `litecoin_addr.rs` — Litecoin address encoding/decoding (bech32 `ltc1`/`tltc1`/`rltc1` and base58check with Litecoin version bytes). Behind `litecoin` feature flag. + - `dogecoin.rs` — Dogecoin address encoding/decoding (base58check with Dogecoin version bytes, no SegWit) and AuxPoW block deserialization. Behind `dogecoin` feature flag. - **`electrs_macros/`** — Proc-macro crate for optional OTLP tracing instrumentation. ### Data Flow @@ -95,7 +102,8 @@ The pre-commit hook (`.hooks/pre-commit`) runs `cargo +stable fmt --all -- --che ### Feature Flags - `liquid` — Liquid/Elements network support (mutually exclusive with `litecoin`) -- `litecoin` — Litecoin network support (mutually exclusive with `liquid`). Networks: litecoin, litecointestnet, litecoinregtest. Note: MWEB (MimbleWimble Extension Blocks) is not yet supported. +- `litecoin` — Litecoin network support (mutually exclusive with `liquid` and `dogecoin`). Networks: litecoin, litecointestnet, litecoinregtest. Note: MWEB (MimbleWimble Extension Blocks) is not yet supported. +- `dogecoin` — Dogecoin network support (mutually exclusive with `liquid` and `litecoin`). Networks: dogecoin, dogecointestnet, dogecoinregtest. Handles AuxPoW (Auxiliary Proof of Work) merged-mining block format. - `electrum-discovery` — Electrum server peer discovery - `otlp-tracing` — OpenTelemetry tracing export - `bitcoind_28_0` — Bitcoin Core 28.0+ compatibility @@ -113,7 +121,7 @@ Configuration is done via CLI args. Key options: - `--http-addr` — REST API listen address (default: 127.0.0.1:3000) - `--electrum-rpc-addr` — Electrum RPC listen address (default: 127.0.0.1:50001) - `--lightmode` — reduce disk usage by querying bitcoind for raw txs on demand -- `--network` — bitcoin network (mainnet/testnet/regtest/signet/liquid/litecoin/litecointestnet/litecoinregtest) +- `--network` — bitcoin network (mainnet/testnet/regtest/signet/liquid/litecoin/litecointestnet/litecoinregtest/dogecoin/dogecointestnet/dogecoinregtest) - `--cors` — CORS origins for HTTP API ## Workspace diff --git a/Cargo.toml b/Cargo.toml index e47911d7e..ace6c93fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ default-run = "electrs" [features] liquid = ["elements"] litecoin = [] +dogecoin = [] electrum-discovery = ["electrum-client"] bench = [] otlp-tracing = [ diff --git a/src/chain.rs b/src/chain.rs index 2919edbc4..9fc76320c 100644 --- a/src/chain.rs +++ b/src/chain.rs @@ -1,4 +1,4 @@ -#[cfg(not(any(feature = "liquid", feature = "litecoin")))] +#[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] // use regular Bitcoin data structures pub use bitcoin::{ address, blockdata::block::Header as BlockHeader, blockdata::script, consensus::deserialize, @@ -14,6 +14,14 @@ pub use bitcoin::{ Transaction, TxIn, TxOut, Txid, }; +// Dogecoin uses the same Bitcoin data structures, but blocks need AuxPoW-aware deserialization +#[cfg(feature = "dogecoin")] +pub use bitcoin::{ + address, blockdata::block::Header as BlockHeader, blockdata::script, consensus::deserialize, + hash_types::TxMerkleNode, Address, Block, BlockHash, OutPoint, ScriptBuf as Script, Sequence, + Transaction, TxIn, TxOut, Txid, +}; + #[cfg(feature = "liquid")] pub use { crate::elements::asset, @@ -33,15 +41,15 @@ pub use confidential::Value; #[derive(Debug, Copy, Clone, PartialEq, Hash, Serialize, Ord, PartialOrd, Eq)] pub enum Network { - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Bitcoin, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Testnet, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Testnet4, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Regtest, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Signet, #[cfg(feature = "liquid")] @@ -57,10 +65,17 @@ pub enum Network { LitecoinTestnet, #[cfg(feature = "litecoin")] LitecoinRegtest, + + #[cfg(feature = "dogecoin")] + Dogecoin, + #[cfg(feature = "dogecoin")] + DogecoinTestnet, + #[cfg(feature = "dogecoin")] + DogecoinRegtest, } impl Network { - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] pub fn magic(self) -> u32 { u32::from_le_bytes(BNetwork::from(self).magic().to_bytes()) } @@ -82,14 +97,25 @@ impl Network { } } + #[cfg(feature = "dogecoin")] + pub fn magic(self) -> u32 { + match self { + Network::Dogecoin => 0xC0C0_C0C0, + Network::DogecoinTestnet => 0xFCC1_B7DC, + Network::DogecoinRegtest => 0xDAB5_BFFA, + } + } + pub fn is_regtest(self) -> bool { match self { - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Regtest => true, #[cfg(feature = "liquid")] Network::LiquidRegtest => true, #[cfg(feature = "litecoin")] Network::LitecoinRegtest => true, + #[cfg(feature = "dogecoin")] + Network::DogecoinRegtest => true, _ => false, } } @@ -132,7 +158,7 @@ impl Network { } pub fn names() -> Vec { - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] return vec![ "mainnet".to_string(), "testnet".to_string(), @@ -154,16 +180,25 @@ impl Network { "litecointestnet".to_string(), "litecoinregtest".to_string(), ]; + + #[cfg(feature = "dogecoin")] + return vec![ + "dogecoin".to_string(), + "dogecointestnet".to_string(), + "dogecoinregtest".to_string(), + ]; } } pub fn genesis_hash(network: Network) -> BlockHash { - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] return bitcoin_genesis_hash(network.into()); #[cfg(feature = "liquid")] return liquid_genesis_hash(network); #[cfg(feature = "litecoin")] return litecoin_genesis_hash(network); + #[cfg(feature = "dogecoin")] + return dogecoin_genesis_hash(network); } pub fn bitcoin_genesis_hash(network: BNetwork) -> bitcoin::BlockHash { @@ -232,18 +267,60 @@ pub fn litecoin_genesis_hash(network: Network) -> BlockHash { } } +#[cfg(feature = "dogecoin")] +pub fn dogecoin_genesis_hash(network: Network) -> BlockHash { + lazy_static! { + static ref DOGECOIN_GENESIS: BlockHash = + "1a91e3dace36e2be3bf030a65679fe821aa1d6ef92e7c9902eb318182c355691" + .parse() + .unwrap(); + static ref DOGECOIN_TESTNET_GENESIS: BlockHash = + "bb0a78264637406b6360aad926284d544d7049f45189db5664f3c4d07350559e" + .parse() + .unwrap(); + static ref DOGECOIN_REGTEST_GENESIS: BlockHash = + "3d2160a3b5dc4a9d62e7e66a295f70313ac808440ef7400d6c0772171ce973a5" + .parse() + .unwrap(); + } + + match network { + Network::Dogecoin => *DOGECOIN_GENESIS, + Network::DogecoinTestnet => *DOGECOIN_TESTNET_GENESIS, + Network::DogecoinRegtest => *DOGECOIN_REGTEST_GENESIS, + } +} + +/// Deserialize a block from raw bytes, handling network-specific block formats. +/// For Dogecoin, this skips AuxPoW data. For all other networks, this is a +/// straightforward call to the standard deserialize function. +#[cfg(not(any(feature = "liquid", feature = "dogecoin")))] +pub fn deserialize_block(data: &[u8]) -> Result { + bitcoin::consensus::deserialize(data) +} + +#[cfg(feature = "liquid")] +pub fn deserialize_block(data: &[u8]) -> Result { + elements::encode::deserialize(data) +} + +#[cfg(feature = "dogecoin")] +pub fn deserialize_block(data: &[u8]) -> Result { + crate::util::dogecoin::deserialize_dogecoin_block(data) +} + impl From<&str> for Network { fn from(network_name: &str) -> Self { match network_name { - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] "mainnet" => Network::Bitcoin, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] "testnet" => Network::Testnet, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] "testnet4" => Network::Testnet4, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] "regtest" => Network::Regtest, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] "signet" => Network::Signet, #[cfg(feature = "liquid")] @@ -260,12 +337,19 @@ impl From<&str> for Network { #[cfg(feature = "litecoin")] "litecoinregtest" => Network::LitecoinRegtest, + #[cfg(feature = "dogecoin")] + "dogecoin" => Network::Dogecoin, + #[cfg(feature = "dogecoin")] + "dogecointestnet" => Network::DogecoinTestnet, + #[cfg(feature = "dogecoin")] + "dogecoinregtest" => Network::DogecoinRegtest, + _ => panic!("unsupported network: {:?}", network_name), } } } -#[cfg(not(any(feature = "liquid", feature = "litecoin")))] +#[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] impl From for BNetwork { fn from(network: Network) -> Self { match network { @@ -278,7 +362,7 @@ impl From for BNetwork { } } -#[cfg(not(any(feature = "liquid", feature = "litecoin")))] +#[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] impl From for Network { fn from(network: BNetwork) -> Self { match network { diff --git a/src/config.rs b/src/config.rs index b7634eb35..f17e0d870 100644 --- a/src/config.rs +++ b/src/config.rs @@ -300,12 +300,14 @@ impl Config { let m = args.get_matches(); - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] let default_network = "mainnet"; #[cfg(feature = "liquid")] let default_network = "liquid"; #[cfg(feature = "litecoin")] let default_network = "litecoin"; + #[cfg(feature = "dogecoin")] + let default_network = "dogecoin"; let network_name = m.value_of("network").unwrap_or(default_network); let network_type = Network::from(network_name); let db_dir = Path::new(m.value_of("db_dir").unwrap_or("./db")); @@ -325,15 +327,15 @@ impl Config { let asset_db_path = m.value_of("asset_db_path").map(PathBuf::from); let default_daemon_port = match network_type { - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Bitcoin => 8332, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Testnet => 18332, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Testnet4 => 48332, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Regtest => 18443, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Signet => 38332, #[cfg(feature = "liquid")] @@ -347,17 +349,24 @@ impl Config { Network::LitecoinTestnet => 19332, #[cfg(feature = "litecoin")] Network::LitecoinRegtest => 19443, + + #[cfg(feature = "dogecoin")] + Network::Dogecoin => 22555, + #[cfg(feature = "dogecoin")] + Network::DogecoinTestnet => 44555, + #[cfg(feature = "dogecoin")] + Network::DogecoinRegtest => 18443, }; let default_electrum_port = match network_type { - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Bitcoin => 50001, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Testnet => 60001, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Testnet4 => 40001, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Regtest => 60401, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Signet => 60601, #[cfg(feature = "liquid")] @@ -373,17 +382,24 @@ impl Config { Network::LitecoinTestnet => 60001, #[cfg(feature = "litecoin")] Network::LitecoinRegtest => 60401, + + #[cfg(feature = "dogecoin")] + Network::Dogecoin => 50001, + #[cfg(feature = "dogecoin")] + Network::DogecoinTestnet => 60001, + #[cfg(feature = "dogecoin")] + Network::DogecoinRegtest => 60401, }; let default_http_port = match network_type { - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Bitcoin => 3000, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Testnet => 3001, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Testnet4 => 3004, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Regtest => 3002, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Signet => 3003, #[cfg(feature = "liquid")] @@ -399,17 +415,24 @@ impl Config { Network::LitecoinTestnet => 3001, #[cfg(feature = "litecoin")] Network::LitecoinRegtest => 3002, + + #[cfg(feature = "dogecoin")] + Network::Dogecoin => 3000, + #[cfg(feature = "dogecoin")] + Network::DogecoinTestnet => 3001, + #[cfg(feature = "dogecoin")] + Network::DogecoinRegtest => 3002, }; let default_monitoring_port = match network_type { - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Bitcoin => 4224, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Testnet => 14224, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Testnet4 => 44224, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Regtest => 24224, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Signet => 54224, #[cfg(feature = "liquid")] @@ -425,6 +448,13 @@ impl Config { Network::LitecoinTestnet => 14224, #[cfg(feature = "litecoin")] Network::LitecoinRegtest => 24224, + + #[cfg(feature = "dogecoin")] + Network::Dogecoin => 4224, + #[cfg(feature = "dogecoin")] + Network::DogecoinTestnet => 14224, + #[cfg(feature = "dogecoin")] + Network::DogecoinRegtest => 24224, }; let daemon_rpc_addr: SocketAddr = str_to_socketaddr( @@ -458,12 +488,14 @@ impl Config { .map(PathBuf::from) .unwrap_or_else(|| { let mut default_dir = home_dir().expect("no homedir"); - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] default_dir.push(".bitcoin"); #[cfg(feature = "liquid")] default_dir.push(".bitcoin"); #[cfg(feature = "litecoin")] default_dir.push(".litecoin"); + #[cfg(feature = "dogecoin")] + default_dir.push(".dogecoin"); default_dir }); @@ -578,15 +610,15 @@ impl RpcLogging { pub fn get_network_subdir(network: Network) -> Option<&'static str> { match network { - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Bitcoin => None, - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Testnet => Some("testnet3"), - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Testnet4 => Some("testnet4"), - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Regtest => Some("regtest"), - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Signet => Some("signet"), #[cfg(feature = "liquid")] @@ -602,6 +634,13 @@ pub fn get_network_subdir(network: Network) -> Option<&'static str> { Network::LitecoinTestnet => Some("testnet4"), #[cfg(feature = "litecoin")] Network::LitecoinRegtest => Some("regtest"), + + #[cfg(feature = "dogecoin")] + Network::Dogecoin => None, + #[cfg(feature = "dogecoin")] + Network::DogecoinTestnet => Some("testnet3"), + #[cfg(feature = "dogecoin")] + Network::DogecoinRegtest => Some("regtest"), } } diff --git a/src/daemon.rs b/src/daemon.rs index 677714ca6..70968f1b7 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -73,7 +73,8 @@ fn header_from_value(value: Value) -> Result { fn block_from_value(value: Value) -> Result { let block_hex = value.as_str().chain_err(|| "non-string block")?; let block_bytes = Vec::from_hex(block_hex).chain_err(|| "non-hex block")?; - Ok(deserialize(&block_bytes).chain_err(|| format!("failed to parse block {}", block_hex))?) + Ok(crate::chain::deserialize_block(&block_bytes) + .chain_err(|| format!("failed to parse block {}", block_hex))?) } fn tx_from_value(value: Value) -> Result { diff --git a/src/electrum/discovery/default_servers.rs b/src/electrum/discovery/default_servers.rs index f17831b67..501c7b947 100644 --- a/src/electrum/discovery/default_servers.rs +++ b/src/electrum/discovery/default_servers.rs @@ -3,7 +3,7 @@ use crate::electrum::discovery::{DiscoveryManager, Service}; pub fn add_default_servers(discovery: &DiscoveryManager, network: Network) { match network { - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Bitcoin => { discovery .add_default_server( @@ -402,7 +402,7 @@ pub fn add_default_servers(discovery: &DiscoveryManager, network: Network) { ) .ok(); } - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] Network::Testnet => { discovery .add_default_server( @@ -464,6 +464,12 @@ pub fn add_default_servers(discovery: &DiscoveryManager, network: Network) { .ok(); } + #[cfg(feature = "dogecoin")] + Network::Dogecoin => { + // Dogecoin Electrum servers are relatively scarce; + // add known public servers as they become available. + } + _ => (), } } diff --git a/src/new_index/fetch.rs b/src/new_index/fetch.rs index 58dab9b39..82b43940d 100644 --- a/src/new_index/fetch.rs +++ b/src/new_index/fetch.rs @@ -3,9 +3,9 @@ use rayon::prelude::*; #[cfg(feature = "liquid")] use crate::elements::ebcompact::*; #[cfg(not(feature = "liquid"))] -use bitcoin::consensus::encode::{deserialize, Decodable}; +use bitcoin::consensus::encode::Decodable; #[cfg(feature = "liquid")] -use elements::encode::{deserialize, Decodable}; +use elements::encode::Decodable; use std::collections::HashMap; use std::fs; @@ -284,7 +284,12 @@ fn parse_blocks(blob: Vec, magic: u32) -> Result> { Ok(pool.install(|| { slices .into_par_iter() - .map(|(slice, size)| (deserialize(slice).expect("failed to parse Block"), size)) + .map(|(slice, size)| { + ( + crate::chain::deserialize_block(slice).expect("failed to parse Block"), + size, + ) + }) .collect() })) } diff --git a/src/new_index/precache.rs b/src/new_index/precache.rs index 28ccea20e..16fda2aeb 100644 --- a/src/new_index/precache.rs +++ b/src/new_index/precache.rs @@ -1,4 +1,4 @@ -#[cfg(not(feature = "litecoin"))] +#[cfg(not(any(feature = "litecoin", feature = "dogecoin")))] use crate::chain::address::Address; use crate::errors::*; use crate::new_index::ChainQuery; @@ -12,7 +12,7 @@ use bitcoin::hex::FromHex; use std::fs::File; use std::io; use std::io::prelude::*; -#[cfg(not(feature = "litecoin"))] +#[cfg(not(any(feature = "litecoin", feature = "dogecoin")))] use std::str::FromStr; use electrs_macros::trace; @@ -67,7 +67,7 @@ fn to_scripthash(script_type: &str, script_str: &str) -> Result { } fn address_to_scripthash(addr: &str) -> Result { - #[cfg(not(feature = "litecoin"))] + #[cfg(not(any(feature = "litecoin", feature = "dogecoin")))] { let addr = Address::from_str(addr).chain_err(|| "invalid address")?; @@ -93,6 +93,22 @@ fn address_to_scripthash(addr: &str) -> Result { } bail!("invalid Litecoin address") } + + #[cfg(feature = "dogecoin")] + { + // Try all Dogecoin networks for precache (network doesn't matter for scripthash) + let networks = [ + crate::chain::Network::Dogecoin, + crate::chain::Network::DogecoinTestnet, + crate::chain::Network::DogecoinRegtest, + ]; + for network in &networks { + if let Some(script) = crate::util::dogecoin::parse_dogecoin_address(addr, *network) { + return Ok(compute_script_hash(&script)); + } + } + bail!("invalid Dogecoin address") + } } pub fn compute_script_hash(data: &[u8]) -> FullHash { diff --git a/src/rest.rs b/src/rest.rs index f7c3a3732..1ac223641 100644 --- a/src/rest.rs +++ b/src/rest.rs @@ -1274,7 +1274,7 @@ fn to_scripthash( } fn address_to_scripthash(addr: &str, network: Network) -> Result { - #[cfg(not(any(feature = "liquid", feature = "litecoin")))] + #[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] { let addr = address::Address::from_str(addr)?; let is_expected_net = addr.is_valid_for_network(network.into()); @@ -1302,6 +1302,14 @@ fn address_to_scripthash(addr: &str, network: Network) -> Result Result { diff --git a/src/util/dogecoin.rs b/src/util/dogecoin.rs new file mode 100644 index 000000000..a52068112 --- /dev/null +++ b/src/util/dogecoin.rs @@ -0,0 +1,271 @@ +use bitcoin::blockdata::block::Header as BlockHeader; +use bitcoin::consensus::Decodable; +use bitcoin::hashes::{sha256d, Hash}; +use bitcoin::{Block, Script, Transaction, VarInt}; +use std::io::Cursor; + +use crate::chain::Network; + +// --- Address encoding --- + +/// Version bytes for Dogecoin base58 addresses +const DOGECOIN_P2PKH_VERSION: u8 = 0x1e; // 'D' prefix +const DOGECOIN_P2SH_VERSION: u8 = 0x16; // '9' or 'A' prefix +const DOGECOIN_TESTNET_P2PKH_VERSION: u8 = 0x71; // 'n' prefix +const DOGECOIN_TESTNET_P2SH_VERSION: u8 = 0xc4; // '2' prefix + +fn network_params(network: Network) -> (u8, u8) { + match network { + Network::Dogecoin => (DOGECOIN_P2PKH_VERSION, DOGECOIN_P2SH_VERSION), + Network::DogecoinTestnet => ( + DOGECOIN_TESTNET_P2PKH_VERSION, + DOGECOIN_TESTNET_P2SH_VERSION, + ), + Network::DogecoinRegtest => ( + DOGECOIN_TESTNET_P2PKH_VERSION, + DOGECOIN_TESTNET_P2SH_VERSION, + ), + } +} + +/// Convert a scriptPubKey to a Dogecoin address string. +/// Dogecoin does not support SegWit, so only P2PKH and P2SH are handled. +pub fn script_to_dogecoin_address(script: &Script, network: Network) -> Option { + let (p2pkh_ver, p2sh_ver) = network_params(network); + let bytes = script.as_bytes(); + + if script.is_p2pkh() && bytes.len() == 25 { + Some(base58check_encode(p2pkh_ver, &bytes[3..23])) + } else if script.is_p2sh() && bytes.len() == 23 { + Some(base58check_encode(p2sh_ver, &bytes[2..22])) + } else { + None + } +} + +/// Parse a Dogecoin address string and return the scriptPubKey bytes +pub fn parse_dogecoin_address(addr: &str, network: Network) -> Option> { + let (p2pkh_ver, p2sh_ver) = network_params(network); + + let (version, hash) = base58check_decode(addr)?; + if version == p2pkh_ver && hash.len() == 20 { + // P2PKH: OP_DUP OP_HASH160 <20> OP_EQUALVERIFY OP_CHECKSIG + let mut script = vec![0x76, 0xa9, 0x14]; + script.extend_from_slice(&hash); + script.extend_from_slice(&[0x88, 0xac]); + Some(script) + } else if version == p2sh_ver && hash.len() == 20 { + // P2SH: OP_HASH160 <20> OP_EQUAL + let mut script = vec![0xa9, 0x14]; + script.extend_from_slice(&hash); + script.push(0x87); + Some(script) + } else { + None + } +} + +// --- Base58Check --- + +fn base58check_encode(version: u8, payload: &[u8]) -> String { + let mut data = Vec::with_capacity(1 + payload.len() + 4); + data.push(version); + data.extend_from_slice(payload); + let checksum = sha256d::Hash::hash(&data); + data.extend_from_slice(&checksum[..4]); + base58_encode(&data) +} + +fn base58check_decode(addr: &str) -> Option<(u8, Vec)> { + let data = base58_decode(addr)?; + if data.len() < 5 { + return None; + } + let (payload, checksum) = data.split_at(data.len() - 4); + let hash = sha256d::Hash::hash(payload); + if &hash[..4] != checksum { + return None; + } + Some((payload[0], payload[1..].to_vec())) +} + +const BASE58_ALPHABET: &[u8] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + +fn base58_encode(data: &[u8]) -> String { + let leading_zeros = data.iter().take_while(|&&b| b == 0).count(); + let mut num = data.to_vec(); + let mut result = Vec::new(); + + while !num.is_empty() { + let mut remainder = 0u32; + let mut new_num = Vec::new(); + for &byte in &num { + let acc = (remainder << 8) | byte as u32; + let digit = acc / 58; + remainder = acc % 58; + if !new_num.is_empty() || digit > 0 { + new_num.push(digit as u8); + } + } + result.push(BASE58_ALPHABET[remainder as usize]); + num = new_num; + } + + for _ in 0..leading_zeros { + result.push(b'1'); + } + result.reverse(); + String::from_utf8(result).unwrap() +} + +fn base58_decode(s: &str) -> Option> { + let leading_ones = s.chars().take_while(|&c| c == '1').count(); + let mut result: Vec = Vec::new(); + + for ch in s.chars() { + let pos = BASE58_ALPHABET.iter().position(|&c| c == ch as u8)? as u32; + let mut carry = pos; + for byte in result.iter_mut().rev() { + let acc = (*byte as u32) * 58 + carry; + *byte = (acc & 0xff) as u8; + carry = acc >> 8; + } + while carry > 0 { + result.insert(0, (carry & 0xff) as u8); + carry >>= 8; + } + } + + let mut final_result = vec![0u8; leading_ones]; + final_result.extend_from_slice(&result); + Some(final_result) +} + +// --- AuxPoW Block Deserialization --- + +/// The AuxPoW version flag in the block header version field. +/// If `(version >> 8) & 0xFF != 0` (specifically bit 8 set), AuxPoW data follows the header. +const BLOCK_VERSION_AUXPOW: i32 = 1 << 8; + +/// Deserialize a Dogecoin block, handling AuxPoW data if present. +/// +/// Dogecoin uses Auxiliary Proof of Work (merged mining with Litecoin). +/// AuxPoW blocks have extra data between the 80-byte header and the transaction list: +/// [80-byte header][AuxPoW data][varint tx_count][transactions...] +/// +/// Standard Bitcoin blocks are: +/// [80-byte header][varint tx_count][transactions...] +/// +/// This function reads the header, skips AuxPoW data if present, +/// then reads transactions, constructing a standard `bitcoin::Block`. +pub fn deserialize_dogecoin_block(data: &[u8]) -> Result { + let mut cursor = Cursor::new(data); + + // Read the 80-byte block header + let header = BlockHeader::consensus_decode(&mut cursor)?; + + // Check for AuxPoW flag in the version field + if header.version.to_consensus() & BLOCK_VERSION_AUXPOW != 0 { + skip_auxpow(&mut cursor)?; + } + + // Read transactions + let txdata = Vec::::consensus_decode(&mut cursor)?; + + Ok(Block { header, txdata }) +} + +/// Skip over AuxPoW data in the byte stream. +/// +/// AuxPoW structure: +/// 1. Coinbase transaction (parent chain) - variable length +/// 2. Block hash (32 bytes) +/// 3. Coinbase merkle branch: varint count + count*32 byte hashes + i32 side_mask +/// 4. Blockchain merkle branch: varint count + count*32 byte hashes + i32 side_mask +/// 5. Parent block header (80 bytes) +fn skip_auxpow(cursor: &mut Cursor<&[u8]>) -> Result<(), bitcoin::consensus::encode::Error> { + // 1. Skip coinbase transaction + Transaction::consensus_decode(cursor)?; + + // 2. Skip block hash (32 bytes) + <[u8; 32]>::consensus_decode(cursor)?; + + // 3. Skip coinbase merkle branch + skip_merkle_branch(cursor)?; + + // 4. Skip blockchain merkle branch + skip_merkle_branch(cursor)?; + + // 5. Skip parent block header (80 bytes) + BlockHeader::consensus_decode(cursor)?; + + Ok(()) +} + +/// Skip a merkle branch: varint count of hashes, then count * 32-byte hashes, then i32 side_mask. +fn skip_merkle_branch(cursor: &mut Cursor<&[u8]>) -> Result<(), bitcoin::consensus::encode::Error> { + let count = VarInt::consensus_decode(cursor)?.0; + for _ in 0..count { + <[u8; 32]>::consensus_decode(cursor)?; + } + // side_mask (i32, 4 bytes) + i32::consensus_decode(cursor)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_base58check_roundtrip() { + let hash = [0u8; 20]; + let encoded = base58check_encode(DOGECOIN_P2PKH_VERSION, &hash); + let (version, decoded) = base58check_decode(&encoded).unwrap(); + assert_eq!(version, DOGECOIN_P2PKH_VERSION); + assert_eq!(decoded, hash); + } + + #[test] + fn test_dogecoin_p2pkh_address_prefix() { + let hash = [1u8; 20]; + let addr = base58check_encode(DOGECOIN_P2PKH_VERSION, &hash); + assert!( + addr.starts_with('D'), + "P2PKH address should start with D, got: {}", + addr + ); + } + + #[test] + fn test_dogecoin_p2sh_address_prefix() { + let hash = [1u8; 20]; + let addr = base58check_encode(DOGECOIN_P2SH_VERSION, &hash); + assert!( + addr.starts_with('9') || addr.starts_with('A'), + "P2SH address should start with 9 or A, got: {}", + addr + ); + } + + #[test] + fn test_parse_and_validate_roundtrip() { + // Create a P2PKH scriptPubKey + let mut p2pkh_script = vec![0x76, 0xa9, 0x14]; + p2pkh_script.extend_from_slice(&[1u8; 20]); + p2pkh_script.extend_from_slice(&[0x88, 0xac]); + + let script = bitcoin::ScriptBuf::from(p2pkh_script.clone()); + let addr = script_to_dogecoin_address(&script, Network::Dogecoin).unwrap(); + let decoded_script = parse_dogecoin_address(&addr, Network::Dogecoin).unwrap(); + assert_eq!(decoded_script, p2pkh_script); + } + + #[test] + fn test_wrong_network_rejected() { + let hash = [1u8; 20]; + let addr = base58check_encode(DOGECOIN_P2PKH_VERSION, &hash); + // Should not parse as testnet + assert!(parse_dogecoin_address(&addr, Network::DogecoinTestnet).is_none()); + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs index ada13267e..de5d356cc 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,4 +1,6 @@ mod block; +#[cfg(feature = "dogecoin")] +pub mod dogecoin; #[cfg(feature = "litecoin")] pub mod litecoin_addr; mod script; diff --git a/src/util/script.rs b/src/util/script.rs index bffd5489b..a7eaeebd1 100644 --- a/src/util/script.rs +++ b/src/util/script.rs @@ -24,7 +24,7 @@ impl ScriptToAsm for elements::Script {} pub trait ScriptToAddr { fn to_address_str(&self, network: Network) -> Option; } -#[cfg(not(any(feature = "liquid", feature = "litecoin")))] +#[cfg(not(any(feature = "liquid", feature = "litecoin", feature = "dogecoin")))] impl ScriptToAddr for bitcoin::Script { fn to_address_str(&self, network: Network) -> Option { bitcoin::Address::from_script(self, bitcoin::Network::from(network)) @@ -32,6 +32,12 @@ impl ScriptToAddr for bitcoin::Script { .ok() } } +#[cfg(feature = "dogecoin")] +impl ScriptToAddr for bitcoin::Script { + fn to_address_str(&self, network: Network) -> Option { + crate::util::dogecoin::script_to_dogecoin_address(self, network) + } +} #[cfg(feature = "litecoin")] impl ScriptToAddr for bitcoin::Script { fn to_address_str(&self, network: Network) -> Option {