Compare commits

..

1 Commits

Author SHA1 Message Date
junderw
c703d588ae
Fix: Ordering index keys by transaction position 2023-05-31 07:37:50 -07:00
53 changed files with 14798 additions and 6868 deletions

View File

@ -1 +0,0 @@
Dockerfile

View File

@ -1,50 +0,0 @@
name: CI Rust Setup
description: 'Sets up the environment for Rust jobs during CI workflow'
inputs:
cache-name:
description: 'Name of cache artifacts (same name is same cache key) empty to disable cache'
required: false
targets:
description: 'A comma separated list of extra targets you want to install'
required: false
components:
description: 'A comma separated list of extra components you want to install'
required: false
toolchain:
description: 'The toolchain to use. If not specified, the rust-toolchain file will be used'
required: false
runs:
using: composite
steps:
- name: Get toolchain from input OR rust-toolchain file
id: gettoolchain
shell: bash
run: |-
RUST_TOOLCHAIN="${{ inputs.toolchain }}"
if [ ! -f rust-toolchain ] && [ -z "${RUST_TOOLCHAIN}" ]; then
echo "***ERROR*** NEED toolchain INPUT OR rust-toolchain FILE IN ROOT OF REPOSITORY" >&2
exit 1
fi
if [ -z "${RUST_TOOLCHAIN}" ]; then
RUST_TOOLCHAIN="$(cat rust-toolchain)"
fi
echo "toolchain=\"${RUST_TOOLCHAIN}\"" >> $GITHUB_OUTPUT
- name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain
id: toolchain
# Commit date is Nov 18, 2024
uses: dtolnay/rust-toolchain@315e265cd78dad1e1dcf3a5074f6d6c47029d5aa
with:
toolchain: ${{ steps.gettoolchain.outputs.toolchain }}
targets: ${{ inputs.targets }}
components: ${{ inputs.components }}
- name: Cache dependencies
uses: actions/cache@v3
if: inputs.cache-name != ''
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ inputs.cache-name }}-${{ steps.toolchain.outputs.cachekey }}-${{ hashFiles('**/Cargo.lock') }}

View File

@ -1,107 +0,0 @@
on:
pull_request:
push:
branches:
- mempool
name: Compile Check and Lint
jobs:
check:
name: Compile Check
runs-on: mempool-ci
steps:
- uses: actions/checkout@v4
- name: Setup Rust
uses: './.github/actions/ci-rust-setup'
with:
cache-name: dev
- run: cargo check --all-features
fmt:
name: Formatter
runs-on: mempool-ci
steps:
- uses: actions/checkout@v4
- name: Setup Rust
uses: './.github/actions/ci-rust-setup'
with:
components: rustfmt
- run: cargo fmt --all -- --check
test:
name: Run Tests
runs-on: mempool-ci
steps:
- uses: actions/checkout@v4
- name: Setup Rust
uses: './.github/actions/ci-rust-setup'
with:
cache-name: test
- run: cargo test --lib --all-features
compile-freebsd:
runs-on: mempool-ci
name: Run Compile Checks in FreeBSD
env:
FREEBSD_VER: "14.3"
steps:
- uses: actions/checkout@v4
- name: Cache dependencies for FreeBSD
uses: actions/cache@v3
with:
path: |
.cargohome/registry
.cargohome/git
target
key: freebsd-${{ env.FREEBSD_VER }}-cargo-checks-${{ hashFiles('**/Cargo.lock') }}
- name: Compile Checks in FreeBSD
uses: vmactions/freebsd-vm@v1
with:
usesh: true
release: "${{ env.FREEBSD_VER }}"
arch: amd64
prepare: |
mkdir -p ~/.cargo/
mkdir -p ./.cargohome/registry/
mkdir -p ./.cargohome/git/
mv ./.cargohome/registry ~/.cargo/
mv ./.cargohome/git ~/.cargo/
rm -rf ./.cargohome
pkg install -y git rsync gmake llvm rust rocksdb cmake
run: |
cargo check --no-default-features
cargo check -F liquid
cargo check -F electrum-discovery
cargo check -F electrum-discovery,liquid
cargo build --release --bin electrs
rm -rf ./.cargohome
mkdir -p ~/.cargo/registry/
mkdir -p ~/.cargo/git/
mkdir -p ./.cargohome/
mv ~/.cargo/registry ./.cargohome/
mv ~/.cargo/git ./.cargohome/
clippy:
name: Linter
runs-on: mempool-ci
needs: [check]
strategy:
matrix: # Try all combinations of features. Some times weird things appear.
features: [
'',
'-F electrum-discovery',
'-F liquid',
'-F electrum-discovery,liquid',
]
steps:
- uses: actions/checkout@v4
- name: Setup Rust
uses: './.github/actions/ci-rust-setup'
with:
cache-name: dev
components: clippy
- name: Clippy with Features = ${{ matrix.features }}
run: cargo clippy ${{ matrix.features }} -- -D warnings

View File

@ -1,77 +0,0 @@
name: Docker build on tag
env:
DOCKER_CLI_EXPERIMENTAL: enabled
TAG_FMT: "^refs/tags/(((.?[0-9]+){3,4}))$"
DOCKER_BUILDKIT: 0
COMPOSE_DOCKER_CLI_BUILD: 0
on:
push:
tags:
- v[0-9]+.[0-9]+.[0-9]+
- v[0-9]+.[0-9]+.[0-9]+-*
permissions:
contents: read
jobs:
build:
runs-on: mempool-ci
timeout-minutes: 120
name: Build and push to DockerHub
strategy:
max-parallel: 1
matrix:
include:
- image: electrs
cargo_extra_args: ""
- image: electrs-liquid
cargo_extra_args: "--features liquid"
steps:
- name: Set env variables
run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
- name: Show set environment variables
run: |
printf " TAG: %s\n" "$TAG"
- name: Add SHORT_SHA env property with commit short sha
run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV
- name: Login to Docker for building
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Checkout project
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
id: qemu
- name: Setup Docker buildx action
uses: docker/setup-buildx-action@v3
id: buildx
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Cache Docker layers
uses: actions/cache@v3
id: cache
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx
restore-keys: |
${{ runner.os }}-buildx
- name: Run Docker buildx against tag
run: |
docker buildx build \
--cache-from "type=local,src=/tmp/.buildx-cache" \
--cache-to "type=local,dest=/tmp/.buildx-cache" \
--platform linux/amd64,linux/arm64 \
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.image }}:$TAG \
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.image }}:latest \
--output "type=registry" . \
--build-arg commitHash=$SHORT_SHA \
--build-arg CARGO_EXTRA_ARGS="${{ matrix.cargo_extra_args }}"

View File

@ -1,18 +0,0 @@
name: Project Board Automation
on:
pull_request:
types: [review_requested]
issues:
types: [opened]
jobs:
project-automation:
uses: mempool/.github/.github/workflows/project-board-automation.yml@master
with:
project-number: 8
secrets:
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
PROJECT_ID: ${{ secrets.PROJECT_ID }}
STATUS_FIELD_ID: ${{ secrets.STATUS_FIELD_ID }}
REVIEW_NEEDED_OPTION_ID: ${{ secrets.REVIEW_NEEDED_OPTION_ID }}

2
.gitignore vendored
View File

@ -4,5 +4,3 @@ target
*.sublime*
*~
*.pyc
.vscode
*.core

View File

@ -1,19 +0,0 @@
# electrs
## Rules
1. You are an expert Rust developer.
2. You are an expert Bitcoin developer.
3. If you are unsure of a change, ask the developer to make a choice proactively.
## Before testing
- Run cargo fmt (from root)
- command: `cargo fmt`
## Testing
- Run the checks script
- `./scripts/checks.sh`
- Run with tests only when a test is added or changed
- `INCLUDE_TESTS=1 ./scripts/checks.sh`

1327
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,59 +1,53 @@
[package]
name = "mempool-electrs"
version = "3.4.0-dev"
authors = [
"Roman Zeyde <me@romanzey.de>",
"Nadav Ivgi <nadav@shesek.info>",
"wiz <j@wiz.biz>",
"junderw <jonathan.underwood4649@gmail.com>"
]
name = "electrs"
version = "0.4.1"
authors = ["Roman Zeyde <me@romanzey.de>"]
description = "An efficient re-implementation of Electrum Server in Rust"
license = "MIT"
homepage = "https://github.com/mempool/electrs"
repository = "https://github.com/mempool/electrs"
publish = false
homepage = "https://github.com/romanz/electrs"
repository = "https://github.com/romanz/electrs"
keywords = ["bitcoin", "electrum", "server", "index", "database"]
documentation = "https://docs.rs/electrs/"
readme = "README.md"
edition = "2018"
[lib]
name = "electrs"
[features]
default = []
liquid = ["elements"]
electrum-discovery = ["electrum-client"]
default = [ "rocksdb" ]
liquid = [ "elements" ]
electrum-discovery = [ "electrum-client"]
oldcpu = [ "rocksdb-oldcpu" ]
[dependencies]
arraydeque = "0.4"
arrayref = "0.3.6"
base64 = "0.13.0"
bincode-do-not-use-directly = { version = "1.3.1", package = "bincode" }
bitcoin = { version = "0.32.8", features = [ "serde" ] }
bounded-vec-deque = "0.1.1"
bincode = "1.3.1"
bitcoin = { version = "0.28", features = [ "use-serde" ] }
clap = "2.33.3"
crossbeam-channel = "0.5.0"
dirs = "4.0.0"
elements = { version = "0.26.1", features = [ "serde" ], optional = true }
elements = { version = "0.19.1", features = [ "serde-feature" ], optional = true }
error-chain = "0.12.4"
glob = "0.3"
hex = "0.4.2"
itertools = "0.10"
lazy_static = "1.3.0"
libc = "0.2"
libc = "0.2.81"
log = "0.4.11"
socket2 = { version = "0.4", features = ["all"] }
num_cpus = "1.12.0"
page_size = "0.4.2"
prometheus = "0.13"
ppp = "2.3.0"
rayon = "1.5.0"
rocksdb = "0.24.0"
rocksdb = { version = "0.17.0", optional = true }
rocksdb-oldcpu = { version = "0.12.4", optional = true, package = "rocksdb" }
rust-crypto = "0.2"
serde = "1.0.118"
serde_derive = "1.0.118"
serde_json = "1.0.60"
sha2 = "0.10.7"
signal-hook = "0.3"
stderrlog = "0.5.0"
sysconf = ">=0.3.4"
time = { version = "0.3", features = ["formatting"] }
tiny_http = "0.11"
url = "2.2.0"
@ -63,7 +57,8 @@ hyperlocal = "0.8"
tokio = { version = "1", features = ["sync", "macros"] }
# optional dependencies for electrum-discovery
electrum-client = { version = "0.24.1", optional = true }
electrum-client = { version = "0.8", optional = true }
[dev-dependencies]
tempfile = "3.0"
@ -74,5 +69,5 @@ panic = 'abort'
codegen-units = 1
[patch.crates-io.electrum-client]
git = "https://github.com/mempool/rust-electrum-client"
rev = "4bbfc612d594fe23282c439d4bdc446cff01ba1c" # 0.24.1/add-peer branch
git = "https://github.com/Blockstream/rust-electrum-client"
rev = "d3792352992a539afffbe11501d1aff9fd5b919d" # add-peer branch

View File

@ -1,30 +0,0 @@
FROM debian:bookworm-slim AS base
ENV CARGO_NET_GIT_FETCH_WITH_CLI=true
RUN apt update -qy && \
apt install -qy librocksdb-dev curl
FROM base as build
RUN apt install -qy git clang cmake
ENV RUSTUP_HOME=/rust
ENV CARGO_HOME=/cargo
ENV PATH=/cargo/bin:/rust/bin:$PATH
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
WORKDIR /build
COPY . .
ARG CARGO_EXTRA_ARGS=""
RUN cargo build --release --bin electrs ${CARGO_EXTRA_ARGS}
FROM base as deploy
COPY --from=build /build/target/release/electrs /bin/electrs
EXPOSE 50001
ENTRYPOINT ["/bin/electrs"]

View File

@ -1,10 +1,10 @@
# Mempool - Electrs backend API
# Esplora - Electrs backend API
A block chain index engine and HTTP API written in Rust based on [romanz/electrs](https://github.com/romanz/electrs) and [Blockstream/electrs](https://github.com/Blockstream/electrs).
A block chain index engine and HTTP API written in Rust based on [romanz/electrs](https://github.com/romanz/electrs).
Used as the backend for the [mempool block explorer](https://github.com/mempool/mempool) powering [mempool.space](https://mempool.space/).
Used as the backend for the [Esplora block explorer](https://github.com/Blockstream/esplora) powering [blockstream.info](https://blockstream.info/).
API documentation [is available here](https://mempool.space/docs/api/rest).
API documentation [is available here](https://github.com/blockstream/esplora/blob/master/API.md).
Documentation for the database schema and indexing process [is available here](doc/schema.md).
@ -13,8 +13,8 @@ Documentation for the database schema and indexing process [is available here](d
Install Rust, Bitcoin Core (no `txindex` needed) and the `clang` and `cmake` packages, then:
```bash
$ git clone https://github.com/mempool/electrs && cd electrs
$ git checkout mempool
$ git clone https://github.com/blockstream/electrs && cd electrs
$ git checkout new-index
$ cargo run --release --bin electrs -- -vvvv --daemon-dir ~/.bitcoin
# Or for liquid:
@ -24,9 +24,11 @@ $ cargo run --features liquid --release --bin electrs -- -vvvv --network liquid
See [electrs's original documentation](https://github.com/romanz/electrs/blob/master/doc/usage.md) for more detailed instructions.
Note that our indexes are incompatible with electrs's and has to be created separately.
The indexes require 1.3TB of storage after running compaction (as of October 2023), but you'll need to have
The indexes require 610GB of storage after running compaction (as of June 2020), but you'll need to have
free space of about double that available during the index compaction process.
Creating the indexes should take a few hours on a beefy machine with high speed NVMe SSD(s).
Creating the indexes should take a few hours on a beefy machine with SSD.
To deploy with Docker, follow the [instructions here](https://github.com/Blockstream/esplora#how-to-build-the-docker-image).
### Light mode
@ -76,7 +78,7 @@ Additional options with the `electrum-discovery` feature:
- `--electrum-hosts <json>` - a json map of the public hosts where the electrum server is reachable, in the [`server.features` format](https://electrumx.readthedocs.io/en/latest/protocol-methods.html#server.features).
- `--electrum-announce` - announce the electrum server on the electrum p2p server discovery network.
See `$ cargo run --bin electrs -- --help` for the full list of options.
See `$ cargo run --release --bin electrs -- --help` for the full list of options.
## License

View File

@ -1,57 +0,0 @@
use std::{path::Path, process::Command};
fn main() {
// Specify re-run conditions
// 1. Rerun build script if we pass a new GIT_HASH
println!("cargo:rerun-if-env-changed=GIT_HASH");
// 2. Only do git based reruns if git directory exists
if Path::new(".git").exists() {
// If we change the branch, rerun
println!("cargo:rerun-if-changed=.git/HEAD");
if let Ok(r) = std::fs::read_to_string(".git/HEAD") {
if let Some(stripped) = r.strip_prefix("ref: ") {
// If the HEAD is detached it will be a commit hash
// so the HEAD changed directive above will pick it up,
// otherwise it will point to a ref in the refs directory
println!("cargo:rerun-if-changed=.git/{}", stripped);
}
}
}
// Getting git hash
// Don't fetch git hash if it's already in the ENV
let existing = std::env::var("GIT_HASH").unwrap_or_else(|_| String::new());
if !existing.is_empty() {
return;
}
// Get git hash from git and don't do anything if the command fails
if let Some(rev_parse) = cmd("git", &["rev-parse", "--short", "HEAD"]) {
// Add (dirty) to the GIT_HASH if the git status isn't clean
// This includes untracked files
let dirty = cmd("git", &["status", "--short"]).expect("git command works");
// Ignore Dockerfile deletion as it is expected in Docker buildx builds
let git_hash = if dirty.is_empty() || dirty.trim() == "D Dockerfile" {
rev_parse
} else {
format!("{}(dirty)", rev_parse.trim())
};
println!("cargo:rustc-env=GIT_HASH={}", git_hash.trim());
}
}
// Helper function, Command is verbose...
fn cmd(name: &str, args: &[&str]) -> Option<String> {
Command::new(name).args(args).output().ok().and_then(|o| {
if o.status.success() {
String::from_utf8(o.stdout).ok()
} else {
None
}
})
}

13150
contrib/popular-scripts.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,24 +0,0 @@
version: '3.9'
services:
mempool-electrs:
build:
context: .
dockerfile: Dockerfile
restart: on-failure
ports:
- 50001:50001
entrypoint:
/bin/electrs
command: |
--address-search
--cookie mempool:mempool
--db-dir /electrs
--cors '*'
volumes:
- 'electrs_data:/electrs'
volumes:
electrs_data:

View File

View File

@ -1,24 +0,0 @@
#!/usr/bin/env zsh
export ZPOOL=nvm
export BITCOIN_HOME=/bitcoin
export BITCOIN_USER=bitcoin
export BITCOIN_GROUP=bitcoin
export ELEMENTS_HOME=/elements
export ELEMENTS_USER=elements
export ELEMENTS_GROUP=elements
# create /bitcoin/socket with custom ACL for electrs unix sockets
zfs create -o "mountpoint=${BITCOIN_HOME}/socket" "${ZPOOL}/bitcoin/socket"
# create /elements/socket with custom ACL for electrs unix sockets
zfs create -o "mountpoint=${ELEMENTS_HOME}/socket" "${ZPOOL}/elements/socket"
setfacl -m "user:bitcoin:full_set:f:allow,user:mempool:full_set:f:allow,user:www:full_set:f:allow,everyone@::f:allow" "${BITCOIN_HOME}/socket"
chown "${BITCOIN_USER}:${BITCOIN_GROUP}" "${BITCOIN_HOME}/socket"
setfacl -m "user:elements:full_set:f:allow,user:mempool:full_set:f:allow,user:www:full_set:f:allow,everyone@::f:allow" "${ELEMENTS_HOME}/socket"
chown "${ELEMENTS_USER}:${ELEMENTS_GROUP}" "${ELEMENTS_HOME}/socket"

View File

@ -1 +0,0 @@
1.87

View File

@ -1,64 +0,0 @@
#!/bin/bash
set -e
# This script is used for running all the checks
# needed to make sure CI passes.
# See below for the reasoning behind testing
# clippy for all feature combinations.
# (Note: clippy reports compile errors too)
# You can pass extra args to cargo commands
# ./scripts/check.sh --release
# will run in release mode. You can use that
# if you already have release mode compiled, as
# it will be faster to use the version you already compiled
TESTNAME=""
cleanup() {
exit_code=$?
if [[ ${exit_code} -ne 0 ]]; then
echo -e "\n\n##### Failed on \"$TESTNAME\"" 1>&2
fi
exit $exit_code
}
trap cleanup EXIT
TESTNAME="Running cargo check"
echo "$TESTNAME"
cargo check $@ -q --all-features
TESTNAME="Running cargo fmt check"
echo "$TESTNAME"
cargo fmt $@ -q --all -- --check
# Testing all the combinations of clippy.
# There were many instances where a certain struct
# differed based on liquid or not(liquid) etc.
# and the clippy fixes would break the other
# feature combination.
#
# Be prepared to use #[allow(clippy::___)] attributes
# to "fix" contradictions between feature sets.
TESTNAME="Running cargo clippy check no features"
echo "$TESTNAME"
cargo clippy $@ -q
TESTNAME="Running cargo clippy check electrum-discovery"
echo "$TESTNAME"
cargo clippy $@ -q -F electrum-discovery
TESTNAME="Running cargo clippy check liquid"
echo "$TESTNAME"
cargo clippy $@ -q -F liquid
TESTNAME="Running cargo clippy check electrum-discovery + liquid"
echo "$TESTNAME"
cargo clippy $@ -q -F electrum-discovery,liquid
if [ $INCLUDE_TESTS ]; then
TESTNAME="Running cargo test with all features"
echo "$TESTNAME"
cargo test $@ -q --lib --all-features
fi

View File

@ -45,12 +45,11 @@ fn run_server(config: Arc<Config>) -> Result<()> {
metrics.start();
let daemon = Arc::new(Daemon::new(
config.daemon_dir.clone(),
config.blocks_dir.clone(),
&config.daemon_dir,
&config.blocks_dir,
config.daemon_rpc_addr,
config.cookie_getter(),
config.network_type,
config.magic,
signal.clone(),
&metrics,
)?);
@ -70,23 +69,18 @@ fn run_server(config: Arc<Config>) -> Result<()> {
&metrics,
));
if let Some(ref precache_file) = config.precache_scripts {
let precache_scripthashes = precache::scripthashes_from_file(precache_file.to_string())
.expect("cannot load scripts to precache");
precache::precache(&chain, precache_scripthashes);
}
let mempool = Arc::new(RwLock::new(Mempool::new(
Arc::clone(&chain),
&metrics,
Arc::clone(&config),
)));
loop {
match Mempool::update(&mempool, &daemon) {
Ok(_) => break,
Err(e) => {
warn!(
"Error performing initial mempool update, trying again in 5 seconds: {}",
e.display_chain()
);
signal.wait(Duration::from_secs(5), false)?;
}
}
}
mempool.write().unwrap().update(&daemon)?;
#[cfg(feature = "liquid")]
let asset_db = config.asset_db_path.as_ref().map(|db_dir| {
@ -105,36 +99,12 @@ fn run_server(config: Arc<Config>) -> Result<()> {
));
// TODO: configuration for which servers to start
let rest_server = rest::start(Arc::clone(&config), Arc::clone(&query), &metrics);
let rest_server = rest::start(Arc::clone(&config), Arc::clone(&query));
let electrum_server = ElectrumRPC::start(Arc::clone(&config), Arc::clone(&query), &metrics);
if let Some(ref precache_file) = config.precache_scripts {
let precache_scripthashes = precache::scripthashes_from_file(precache_file.to_string())
.expect("cannot load scripts to precache");
precache::precache(
Arc::clone(&chain),
precache_scripthashes,
config.precache_threads,
);
}
loop {
if let Err(err) = signal.wait(Duration::from_millis(config.main_loop_delay), true) {
if let Err(err) = signal.wait(Duration::from_secs(5), true) {
info!("stopping server: {}", err);
electrs::util::spawn_thread("shutdown-thread-checker", || {
let mut counter = 40;
let interval_ms = 500;
while counter > 0 {
electrs::util::with_spawned_threads(|threads| {
debug!("Threads during shutdown: {:?}", threads);
});
std::thread::sleep(std::time::Duration::from_millis(interval_ms));
counter -= 1;
}
});
rest_server.stop();
// the electrum server is stopped when dropped
break;
@ -148,13 +118,7 @@ fn run_server(config: Arc<Config>) -> Result<()> {
};
// Update mempool
if let Err(e) = Mempool::update(&mempool, &daemon) {
// Log the error if the result is an Err
warn!(
"Error updating mempool, skipping mempool update: {}",
e.display_chain()
);
}
mempool.write().unwrap().update(&daemon)?;
// Update subscribed clients
electrum_server.notify();
@ -169,7 +133,4 @@ fn main() {
error!("server failed: {}", e.display_chain());
process::exit(1);
}
electrs::util::with_spawned_threads(|threads| {
debug!("Threads before closing: {:?}", threads);
});
}

View File

@ -1,183 +1,52 @@
extern crate electrs;
use std::{convert::TryInto, thread::ThreadId, time::Instant};
use bincode::Options;
use electrs::{
config::Config,
new_index::{Store, TxHistoryKey},
};
use electrs::{config::Config, new_index::db::open_raw_db};
use lazy_static::lazy_static;
/*
// How to run:
export ELECTRS_DATA=/path/to/electrs
cargo run \
-q --release --bin popular-scripts -- \
--db-dir $ELECTRS_DATA/db \
> ./contrib/popular-scripts.txt
*/
type DB = rocksdb::DBWithThreadMode<rocksdb::MultiThreaded>;
lazy_static! {
static ref HISTORY_DB: DB = {
let config = Config::from_args();
open_raw_db(
&config.db_path.join("newindex").join("history"),
electrs::new_index::db::OpenMode::ReadOnly,
)
};
}
// Dev note:
// Only use println for file output (lines for output)
// Use eprintln to print to stderr for dev notifications
fn main() {
let high_usage_threshold = std::env::var("HIGH_USAGE_THRESHOLD")
.ok()
.and_then(|s| s.parse::<u32>().ok())
.unwrap_or(4000);
let thread_count = std::env::var("JOB_THREAD_COUNT")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(4);
eprintln!(
"Seaching for scripts with history rows of {} or more...",
high_usage_threshold
);
let config = Config::from_args();
let store = Store::open(&config.db_path.join("newindex"), &config);
let thread_pool = rayon::ThreadPoolBuilder::new()
.num_threads(thread_count)
.build()
.expect("Built threadpool");
let (sender, receiver) = crossbeam_channel::unbounded::<[u8; 32]>();
let increment = 256 / thread_count;
let bytes: Vec<u8> = (0u8..=255u8)
.filter(|n| *n % increment as u8 == 0)
.collect();
let now = Instant::now();
for i in 0..bytes.len() {
let sender = sender.clone();
let first_byte = bytes[i];
let second_byte = bytes.get(i + 1).copied();
thread_pool.spawn(move || {
let id = std::thread::current().id();
run_iterator(
id,
&HISTORY_DB,
high_usage_threshold,
first_byte,
second_byte,
sender,
now,
);
eprintln!("{id:?} Finished its job!");
})
}
// If we don't drop this sender
// the receiver will hang forever
drop(sender);
while let Ok(script) = receiver.recv() {
println!("{}", hex::encode(script));
}
eprintln!("Finished!!!!");
}
fn run_iterator(
thread_id: ThreadId,
db: &DB,
high_usage_threshold: u32,
first_byte: u8,
next_byte: Option<u8>,
sender: crossbeam_channel::Sender<[u8; 32]>,
now: Instant,
) {
let mut iter = db.raw_iterator();
eprintln!(
"Thread ({thread_id:?}) Seeking DB to beginning of tx histories for b'H' + {}",
hex::encode([first_byte])
);
let mut compare_vec: Vec<u8> = vec![b'H', first_byte];
iter.seek(&compare_vec); // Seek to beginning of our section
// Insert the byte of the next section for comparing
// This will tell us when to stop with a closure
type Checker<'a> = Box<dyn Fn(&[u8]) -> bool + 'a>;
let is_finished: Checker<'_> = if let Some(next) = next_byte {
// Modify the vec to what we're looking for next
// to indicate we left our section
compare_vec[1] = next;
Box::new(|key: &[u8]| -> bool { key.starts_with(&compare_vec) })
} else {
// Modify the vec to only have H so we know when we left H
compare_vec.remove(1);
Box::new(|key: &[u8]| -> bool { !key.starts_with(&compare_vec) })
};
eprintln!("Thread ({thread_id:?}) Seeking done");
let mut iter = store.history_db().raw_iterator();
iter.seek(b"H");
let mut curr_scripthash = [0u8; 32];
let mut total_entries: usize = 0;
let mut iter_index: usize = 1;
let mut total_entries = 0;
while iter.valid() {
let key = iter.key().unwrap();
if key.is_empty() || key[0] != b'H' || is_finished(key) {
// We have left the txhistory section,
// but we need to check the final scripthash
send_if_popular(
high_usage_threshold,
total_entries,
curr_scripthash,
&sender,
);
if !key.starts_with(b"H") {
break;
}
if iter_index % 10_000_000 == 0 {
let duration = now.elapsed().as_secs();
eprintln!(
"Thread ({thread_id:?}) Processing row #{iter_index}... {duration} seconds elapsed"
);
}
let entry: TxHistoryKey = bincode::options()
.with_big_endian()
.deserialize(&key)
.expect("failed to deserialize TxHistoryKey");
// We know that the TxHistory key is 1 byte "H" followed by
// 32 byte scripthash
let entry_hash: [u8; 32] = key[1..33].try_into().unwrap();
if curr_scripthash != entry.hash {
if total_entries > 100 {
println!("{} {}", hex::encode(&curr_scripthash), total_entries);
}
if curr_scripthash != entry_hash {
// We have rolled on to a new scripthash
// If the last scripthash was popular
// Collect for sorting
send_if_popular(
high_usage_threshold,
total_entries,
curr_scripthash,
&sender,
);
// After collecting, reset values for next scripthash
curr_scripthash = entry_hash;
curr_scripthash = entry.hash;
total_entries = 0;
}
total_entries += 1;
iter_index += 1;
iter.next();
}
}
#[inline]
fn send_if_popular(
high_usage_threshold: u32,
total_entries: usize,
curr_scripthash: [u8; 32],
sender: &crossbeam_channel::Sender<[u8; 32]>,
) {
if total_entries >= high_usage_threshold as usize {
sender.send(curr_scripthash).unwrap();
if total_entries >= 4000 {
println!(
"scripthash,{},{}",
hex::encode(&curr_scripthash),
total_entries
);
}
}

View File

@ -9,7 +9,7 @@ fn main() {
use std::collections::HashSet;
use std::sync::Arc;
use bitcoin::blockdata::script::ScriptBuf;
use bitcoin::blockdata::script::Script;
use bitcoin::consensus::encode::deserialize;
use electrs::{
chain::Transaction,
@ -30,12 +30,11 @@ fn main() {
let daemon = Arc::new(
Daemon::new(
config.daemon_dir.clone(),
config.blocks_dir.clone(),
&config.daemon_dir,
&config.blocks_dir,
config.daemon_rpc_addr,
config.cookie_getter(),
config.network_type,
config.magic,
signal,
&metrics,
)
@ -61,8 +60,8 @@ fn main() {
break;
}
let tx: Transaction = deserialize(value).expect("failed to parse Transaction");
let txid = tx.compute_txid();
let tx: Transaction = deserialize(&value).expect("failed to parse Transaction");
let txid = tx.txid();
iter.next();
@ -71,7 +70,7 @@ fn main() {
continue;
}
// skip coinbase txs
if tx.is_coinbase() {
if tx.is_coin_base() {
continue;
}
@ -91,26 +90,12 @@ fn main() {
.collect(),
);
let total_out: u64 = tx.output.iter().map(|out| out.value.to_sat()).sum();
let small_out = tx
.output
.iter()
.map(|out| out.value.to_sat())
.min()
.unwrap();
let large_out = tx
.output
.iter()
.map(|out| out.value.to_sat())
.max()
.unwrap();
let total_out: u64 = tx.output.iter().map(|out| out.value).sum();
let small_out = tx.output.iter().map(|out| out.value).min().unwrap();
let large_out = tx.output.iter().map(|out| out.value).max().unwrap();
let total_in: u64 = prevouts.values().map(|out| out.value.to_sat()).sum();
let smallest_in = prevouts
.values()
.map(|out| out.value.to_sat())
.min()
.unwrap();
let total_in: u64 = prevouts.values().map(|out| out.value).sum();
let smallest_in = prevouts.values().map(|out| out.value).min().unwrap();
let fee = total_in - total_out;
@ -133,7 +118,7 @@ fn main() {
// test for sending back to one of the spent spks
let has_reuse = {
let prev_spks: HashSet<ScriptBuf> = prevouts
let prev_spks: HashSet<Script> = prevouts
.values()
.map(|out| out.script_pubkey.clone())
.collect();

View File

@ -1,65 +1,20 @@
use std::str::FromStr;
#[cfg(not(feature = "liquid"))] // use regular Bitcoin data structures
pub use bitcoin::{
address,
block::Header as BlockHeader,
blockdata::{opcodes, script},
consensus::deserialize,
hashes, Block, BlockHash, OutPoint, ScriptBuf as Script, Transaction, TxIn, TxOut, Txid,
Witness,
blockdata::script, consensus::deserialize, util::address, Block, BlockHash, BlockHeader,
OutPoint, Script, Transaction, TxIn, TxOut, Txid,
};
#[cfg(feature = "liquid")]
pub use {
crate::elements::asset,
elements::{
address, bitcoin::bech32::Hrp, confidential, encode::deserialize, hashes, opcodes, script,
Address, AssetId, Block, BlockHash, BlockHeader, OutPoint, Script, Transaction, TxIn,
TxInWitness as Witness, TxOut, Txid,
address, confidential, encode::deserialize, script, Address, AssetId, Block, BlockHash,
BlockHeader, OutPoint, Script, Transaction, TxIn, TxOut, Txid,
},
};
use bitcoin::blockdata::constants::genesis_block;
pub use bitcoin::Network as BNetwork;
// Extension trait for getting txid in a cross-compatible way
pub trait TxidCompat {
fn get_txid(&self) -> Txid;
}
#[cfg(not(feature = "liquid"))]
impl TxidCompat for Transaction {
fn get_txid(&self) -> Txid {
self.compute_txid()
}
}
#[cfg(feature = "liquid")]
impl TxidCompat for Transaction {
fn get_txid(&self) -> Txid {
self.txid()
}
}
// Extension trait for getting block size in a cross-compatible way
pub trait BlockSizeCompat {
fn get_block_size(&self) -> usize;
}
#[cfg(not(feature = "liquid"))]
impl BlockSizeCompat for Block {
fn get_block_size(&self) -> usize {
self.total_size()
}
}
#[cfg(feature = "liquid")]
impl BlockSizeCompat for Block {
fn get_block_size(&self) -> usize {
self.size()
}
}
pub use bitcoin::network::constants::Network as BNetwork;
#[cfg(not(feature = "liquid"))]
pub type Value = u64;
@ -73,8 +28,6 @@ pub enum Network {
#[cfg(not(feature = "liquid"))]
Testnet,
#[cfg(not(feature = "liquid"))]
Testnet4,
#[cfg(not(feature = "liquid"))]
Regtest,
#[cfg(not(feature = "liquid"))]
Signet,
@ -92,24 +45,14 @@ pub const LIQUID_TESTNET_PARAMS: address::AddressParams = address::AddressParams
p2pkh_prefix: 36,
p2sh_prefix: 19,
blinded_prefix: 23,
bech_hrp: Hrp::parse_unchecked("tex"),
blech_hrp: Hrp::parse_unchecked("tlq"),
bech_hrp: "tex",
blech_hrp: "tlq",
};
/// Magic for testnet4, 0x1c163f28 (from BIP94) with flipped endianness.
#[cfg(not(feature = "liquid"))]
const TESTNET4_MAGIC: u32 = 0x283f161c;
impl Network {
#[cfg(not(feature = "liquid"))]
pub fn magic(self) -> u32 {
match self {
Self::Testnet4 => TESTNET4_MAGIC,
_ => {
let magic = BNetwork::from(self).magic();
u32::from_le_bytes(magic.to_bytes())
}
}
BNetwork::from(self).magic()
}
#[cfg(feature = "liquid")]
@ -143,9 +86,9 @@ impl Network {
#[cfg(feature = "liquid")]
pub fn native_asset(self) -> &'static AssetId {
match self {
Network::Liquid => &asset::NATIVE_ASSET_ID,
Network::LiquidTestnet => &asset::NATIVE_ASSET_ID_TESTNET,
Network::LiquidRegtest => &asset::NATIVE_ASSET_ID_REGTEST,
Network::Liquid => &*asset::NATIVE_ASSET_ID,
Network::LiquidTestnet => &*asset::NATIVE_ASSET_ID_TESTNET,
Network::LiquidRegtest => &*asset::NATIVE_ASSET_ID_REGTEST,
}
}
@ -177,39 +120,27 @@ impl Network {
pub fn genesis_hash(network: Network) -> BlockHash {
#[cfg(not(feature = "liquid"))]
return bitcoin_genesis_hash(network);
return bitcoin_genesis_hash(network.into());
#[cfg(feature = "liquid")]
return liquid_genesis_hash(network);
}
pub fn bitcoin_genesis_hash(network: Network) -> bitcoin::BlockHash {
pub fn bitcoin_genesis_hash(network: BNetwork) -> bitcoin::BlockHash {
lazy_static! {
static ref BITCOIN_GENESIS: bitcoin::BlockHash =
genesis_block(BNetwork::Bitcoin).block_hash();
static ref TESTNET_GENESIS: bitcoin::BlockHash =
genesis_block(BNetwork::Testnet).block_hash();
static ref TESTNET4_GENESIS: bitcoin::BlockHash = bitcoin::BlockHash::from_str(
"00000000da84f2bafbbc53dee25a72ae507ff4914b867c565be350b0da8bf043"
)
.unwrap();
static ref REGTEST_GENESIS: bitcoin::BlockHash =
genesis_block(BNetwork::Regtest).block_hash();
static ref SIGNET_GENESIS: bitcoin::BlockHash =
genesis_block(BNetwork::Signet).block_hash();
}
#[cfg(not(feature = "liquid"))]
match network {
Network::Bitcoin => *BITCOIN_GENESIS,
Network::Testnet => *TESTNET_GENESIS,
Network::Testnet4 => *TESTNET4_GENESIS,
Network::Regtest => *REGTEST_GENESIS,
Network::Signet => *SIGNET_GENESIS,
}
#[cfg(feature = "liquid")]
match network {
Network::Liquid => *BITCOIN_GENESIS,
Network::LiquidTestnet => *TESTNET_GENESIS,
Network::LiquidRegtest => *REGTEST_GENESIS,
BNetwork::Bitcoin => *BITCOIN_GENESIS,
BNetwork::Testnet => *TESTNET_GENESIS,
BNetwork::Regtest => *REGTEST_GENESIS,
BNetwork::Signet => *SIGNET_GENESIS,
}
}
@ -220,10 +151,6 @@ pub fn liquid_genesis_hash(network: Network) -> elements::BlockHash {
"1466275836220db2944ca059a3a10ef6fd2ea684b0688d2c379296888a206003"
.parse()
.unwrap();
static ref ZERO_HASH: BlockHash =
"0000000000000000000000000000000000000000000000000000000000000000"
.parse()
.unwrap();
}
match network {
@ -231,7 +158,7 @@ pub fn liquid_genesis_hash(network: Network) -> elements::BlockHash {
// The genesis block for liquid regtest chains varies based on the chain configuration.
// This instead uses an all zeroed-out hash, which doesn't matter in practice because its
// only used for Electrum server discovery, which isn't active on regtest.
_ => *ZERO_HASH,
_ => Default::default(),
}
}
@ -243,8 +170,6 @@ impl From<&str> for Network {
#[cfg(not(feature = "liquid"))]
"testnet" => Network::Testnet,
#[cfg(not(feature = "liquid"))]
"testnet4" => Network::Testnet4,
#[cfg(not(feature = "liquid"))]
"regtest" => Network::Regtest,
#[cfg(not(feature = "liquid"))]
"signet" => Network::Signet,
@ -267,7 +192,6 @@ impl From<Network> for BNetwork {
match network {
Network::Bitcoin => BNetwork::Bitcoin,
Network::Testnet => BNetwork::Testnet,
Network::Testnet4 => BNetwork::Testnet4,
Network::Regtest => BNetwork::Regtest,
Network::Signet => BNetwork::Signet,
}
@ -280,7 +204,6 @@ impl From<BNetwork> for Network {
match network {
BNetwork::Bitcoin => Network::Bitcoin,
BNetwork::Testnet => Network::Testnet,
BNetwork::Testnet4 => Network::Testnet4,
BNetwork::Regtest => Network::Regtest,
BNetwork::Signet => Network::Signet,
}

View File

@ -4,7 +4,7 @@ use std::fs;
use std::net::SocketAddr;
use std::net::ToSocketAddrs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, OnceLock};
use std::sync::Arc;
use stderrlog;
use crate::chain::Network;
@ -14,28 +14,13 @@ use crate::errors::*;
#[cfg(feature = "liquid")]
use bitcoin::Network as BNetwork;
pub(crate) const APP_NAME: &str = "mempool-electrs";
pub(crate) const ELECTRS_VERSION: &str = env!("CARGO_PKG_VERSION");
pub(crate) const GIT_HASH: Option<&str> = option_env!("GIT_HASH");
// This will be set only once in the Daemon::new() constructor at startup
pub(crate) static BITCOIND_SUBVER: OnceLock<String> = OnceLock::new();
lazy_static! {
pub(crate) static ref VERSION_STRING: String = {
if let Some(hash) = GIT_HASH {
format!("{} {}-{}", APP_NAME, ELECTRS_VERSION, hash)
} else {
format!("{} {}", APP_NAME, ELECTRS_VERSION)
}
};
}
const ELECTRS_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Clone)]
pub struct Config {
// See below for the documentation of each field:
pub log: stderrlog::StdErrLog,
pub network_type: Network,
pub magic: Option<u32>,
pub db_path: PathBuf,
pub daemon_dir: PathBuf,
pub blocks_dir: PathBuf,
@ -44,40 +29,24 @@ pub struct Config {
pub electrum_rpc_addr: SocketAddr,
pub http_addr: SocketAddr,
pub http_socket_file: Option<PathBuf>,
pub rpc_socket_file: Option<PathBuf>,
pub monitoring_addr: SocketAddr,
pub jsonrpc_import: bool,
pub light_mode: bool,
pub main_loop_delay: u64,
pub address_search: bool,
pub index_unspendables: bool,
pub cors: Option<String>,
pub precache_scripts: Option<String>,
pub precache_threads: usize,
pub utxos_limit: usize,
pub electrum_txs_limit: usize,
pub electrum_banner: String,
pub mempool_backlog_stats_ttl: u64,
pub mempool_recent_txs_size: usize,
pub rest_default_block_limit: usize,
pub rest_default_chain_txs_per_page: usize,
pub rest_default_max_mempool_txs: usize,
pub rest_default_max_address_summary_txs: usize,
pub rest_max_mempool_page_size: usize,
pub rest_max_mempool_txid_page_size: usize,
pub electrum_max_line_size: usize,
pub electrum_max_subscriptions: usize,
pub electrum_max_clients: usize,
pub electrum_idle_timeout: u64,
pub electrum_haproxy_depth: usize,
pub electrum_connections_per_client: usize,
pub electrum_public_hosts: Option<crate::electrum::ServerHosts>,
#[cfg(feature = "liquid")]
pub parent_network: BNetwork,
#[cfg(feature = "liquid")]
pub asset_db_path: Option<PathBuf>,
#[cfg(feature = "electrum-discovery")]
pub electrum_public_hosts: Option<crate::electrum::ServerHosts>,
#[cfg(feature = "electrum-discovery")]
pub electrum_announce: bool,
#[cfg(feature = "electrum-discovery")]
@ -97,13 +66,8 @@ impl Config {
pub fn from_args() -> Config {
let network_help = format!("Select network type ({})", Network::names().join(", "));
let args = App::new("Mempool Electrum Rust Server")
let args = App::new("Electrum Rust Server")
.version(crate_version!())
.arg(
Arg::with_name("version")
.long("version")
.help("Print out the version of this app and quit immediately."),
)
.arg(
Arg::with_name("verbosity")
.short("v")
@ -145,12 +109,6 @@ impl Config {
.help(&network_help)
.takes_value(true),
)
.arg(
Arg::with_name("magic")
.long("magic")
.default_value("")
.takes_value(true),
)
.arg(
Arg::with_name("electrum_rpc_addr")
.long("electrum-rpc-addr")
@ -185,12 +143,6 @@ impl Config {
.long("lightmode")
.help("Enable light mode for reduced storage")
)
.arg(
Arg::with_name("main_loop_delay")
.long("main-loop-delay")
.help("The number of milliseconds the main loop will wait between loops. (Can be shortened with SIGUSR1)")
.default_value("500")
)
.arg(
Arg::with_name("address_search")
.long("address-search")
@ -213,66 +165,12 @@ impl Config {
.help("Path to file with list of scripts to pre-cache")
.takes_value(true)
)
.arg(
Arg::with_name("precache_threads")
.long("precache-threads")
.help("Non-zero number of threads to use for precache threadpool. [default: 4 * CORE_COUNT]")
.takes_value(true)
)
.arg(
Arg::with_name("utxos_limit")
.long("utxos-limit")
.help("Maximum number of utxos to process per address. Lookups for addresses with more utxos will fail. Applies to the Electrum and HTTP APIs.")
.default_value("500")
)
.arg(
Arg::with_name("mempool_backlog_stats_ttl")
.long("mempool-backlog-stats-ttl")
.help("The number of seconds that need to pass before Mempool::update will update the latency histogram again.")
.default_value("10")
)
.arg(
Arg::with_name("mempool_recent_txs_size")
.long("mempool-recent-txs-size")
.help("The number of transactions that mempool will keep in its recents queue. This is returned by mempool/recent endpoint.")
.default_value("10")
)
.arg(
Arg::with_name("rest_default_block_limit")
.long("rest-default-block-limit")
.help("The default number of blocks returned from the blocks/[start_height] endpoint.")
.default_value("10")
)
.arg(
Arg::with_name("rest_default_chain_txs_per_page")
.long("rest-default-chain-txs-per-page")
.help("The default number of on-chain transactions returned by the txs endpoints.")
.default_value("25")
)
.arg(
Arg::with_name("rest_default_max_mempool_txs")
.long("rest-default-max-mempool-txs")
.help("The default number of mempool transactions returned by the txs endpoints.")
.default_value("50")
)
.arg(
Arg::with_name("rest_default_max_address_summary_txs")
.long("rest-default-max-address-summary-txs")
.help("The default number of transactions returned by the address summary endpoints.")
.default_value("5000")
)
.arg(
Arg::with_name("rest_max_mempool_page_size")
.long("rest-max-mempool-page-size")
.help("The maximum number of transactions returned by the paginated /internal/mempool/txs endpoint.")
.default_value("1000")
)
.arg(
Arg::with_name("rest_max_mempool_txid_page_size")
.long("rest-max-mempool-txid-page-size")
.help("The maximum number of transactions returned by the paginated /mempool/txids/page endpoint.")
.default_value("10000")
)
.arg(
Arg::with_name("electrum_txs_limit")
.long("electrum-txs-limit")
@ -283,36 +181,6 @@ impl Config {
.long("electrum-banner")
.help("Welcome banner for the Electrum server, shown in the console to clients.")
.takes_value(true)
).arg(
Arg::with_name("electrum_max_line_size")
.long("electrum-max-line-size")
.help("Maximum size of a single Electrum request line in bytes (default: 1 MiB).")
.default_value("1048576")
).arg(
Arg::with_name("electrum_max_subscriptions")
.long("electrum-max-subscriptions")
.help("Maximum number of scripthash subscriptions per client connection.")
.default_value("100")
).arg(
Arg::with_name("electrum_max_clients")
.long("electrum-max-clients")
.help("Maximum number of concurrent Electrum client connections.")
.default_value("10")
).arg(
Arg::with_name("electrum_idle_timeout")
.long("electrum-idle-timeout")
.help("Maximum idle time in seconds since the last client request before disconnecting the Electrum connection.")
.default_value("600")
).arg(
Arg::with_name("electrum_haproxy_depth")
.long("electrum-haproxy-depth")
.help("Which HAProxy PROXY-protocol header layer identifies the real client IP. 0 disables PROXY-protocol detection; 1 uses the first (outermost) address, 2 the second, and so on. If the requested layer or any PROXY header is absent, no client IP is associated with the connection.")
.default_value("0")
).arg(
Arg::with_name("electrum_connections_per_client")
.long("electrum-connections-per-client")
.help("Maximum number of concurrent Electrum connections allowed per client (keyed by the HAProxy-reported address when available, otherwise the peer IP). 0 disables the per-client limit.")
.default_value("10")
);
#[cfg(unix)]
@ -323,14 +191,6 @@ impl Config {
.takes_value(true),
);
#[cfg(unix)]
let args = args.arg(
Arg::with_name("rpc_socket_file")
.long("rpc-socket-file")
.help("Electrum RPC 'unix socket file' to listen on (default disabled, enabling this ignores the electrum_rpc_addr arg)")
.takes_value(true),
);
#[cfg(feature = "liquid")]
let args = args
.arg(
@ -365,17 +225,8 @@ impl Config {
let m = args.get_matches();
if m.is_present("version") {
eprintln!("{}", *VERSION_STRING);
std::process::exit(0);
}
let network_name = m.value_of("network").unwrap_or("mainnet");
let network_type = Network::from(network_name);
let magic: Option<u32> = m
.value_of("magic")
.filter(|s| !s.is_empty())
.map(|s| u32::from_str_radix(s, 16).expect("invalid network magic"));
let db_dir = Path::new(m.value_of("db_dir").unwrap_or("./db"));
let db_path = db_dir.join(network_name);
@ -401,8 +252,6 @@ impl Config {
Network::Regtest => 18443,
#[cfg(not(feature = "liquid"))]
Network::Signet => 38332,
#[cfg(not(feature = "liquid"))]
Network::Testnet4 => 48332,
#[cfg(feature = "liquid")]
Network::Liquid => 7041,
@ -415,8 +264,6 @@ impl Config {
#[cfg(not(feature = "liquid"))]
Network::Testnet => 60001,
#[cfg(not(feature = "liquid"))]
Network::Testnet4 => 40001,
#[cfg(not(feature = "liquid"))]
Network::Regtest => 60401,
#[cfg(not(feature = "liquid"))]
Network::Signet => 60601,
@ -437,8 +284,6 @@ impl Config {
Network::Regtest => 3002,
#[cfg(not(feature = "liquid"))]
Network::Signet => 3003,
#[cfg(not(feature = "liquid"))]
Network::Testnet4 => 3004,
#[cfg(feature = "liquid")]
Network::Liquid => 3000,
@ -455,8 +300,6 @@ impl Config {
#[cfg(not(feature = "liquid"))]
Network::Regtest => 24224,
#[cfg(not(feature = "liquid"))]
Network::Testnet4 => 44224,
#[cfg(not(feature = "liquid"))]
Network::Signet => 54224,
#[cfg(feature = "liquid")]
@ -484,7 +327,6 @@ impl Config {
);
let http_socket_file: Option<PathBuf> = m.value_of("http_socket_file").map(PathBuf::from);
let rpc_socket_file: Option<PathBuf> = m.value_of("rpc_socket_file").map(PathBuf::from);
let monitoring_addr: SocketAddr = str_to_socketaddr(
m.value_of("monitoring_addr")
.unwrap_or(&format!("127.0.0.1:{}", default_monitoring_port)),
@ -505,8 +347,6 @@ impl Config {
#[cfg(not(feature = "liquid"))]
Network::Testnet => daemon_dir.push("testnet3"),
#[cfg(not(feature = "liquid"))]
Network::Testnet4 => daemon_dir.push("testnet4"),
#[cfg(not(feature = "liquid"))]
Network::Regtest => daemon_dir.push("regtest"),
#[cfg(not(feature = "liquid"))]
Network::Signet => daemon_dir.push("signet"),
@ -524,16 +364,15 @@ impl Config {
.unwrap_or_else(|| daemon_dir.join("blocks"));
let cookie = m.value_of("cookie").map(|s| s.to_owned());
let electrum_banner = m
.value_of("electrum_banner")
.map_or_else(|| format!("Welcome to {}", *VERSION_STRING), |s| s.into());
let electrum_banner = m.value_of("electrum_banner").map_or_else(
|| format!("Welcome to electrs-esplora {}", ELECTRS_VERSION),
|s| s.into(),
);
#[cfg(feature = "electrum-discovery")]
let electrum_public_hosts = m
.value_of("electrum_public_hosts")
.map(|s| serde_json::from_str(s).expect("invalid --electrum-public-hosts"));
#[cfg(not(feature = "electrum-discovery"))]
let electrum_public_hosts: Option<crate::electrum::ServerHosts> = None;
let mut log = stderrlog::new();
log.verbosity(m.occurrences_of("verbosity") as usize);
@ -546,7 +385,6 @@ impl Config {
let config = Config {
log,
network_type,
magic,
db_path,
daemon_dir,
blocks_dir,
@ -558,71 +396,20 @@ impl Config {
electrum_banner,
http_addr,
http_socket_file,
rpc_socket_file,
monitoring_addr,
mempool_backlog_stats_ttl: value_t_or_exit!(m, "mempool_backlog_stats_ttl", u64),
mempool_recent_txs_size: value_t_or_exit!(m, "mempool_recent_txs_size", usize),
rest_default_block_limit: value_t_or_exit!(m, "rest_default_block_limit", usize),
rest_default_chain_txs_per_page: value_t_or_exit!(
m,
"rest_default_chain_txs_per_page",
usize
),
rest_default_max_mempool_txs: value_t_or_exit!(
m,
"rest_default_max_mempool_txs",
usize
),
rest_default_max_address_summary_txs: value_t_or_exit!(
m,
"rest_default_max_address_summary_txs",
usize
),
rest_max_mempool_page_size: value_t_or_exit!(m, "rest_max_mempool_page_size", usize),
rest_max_mempool_txid_page_size: value_t_or_exit!(
m,
"rest_max_mempool_txid_page_size",
usize
),
electrum_max_line_size: value_t_or_exit!(m, "electrum_max_line_size", usize),
electrum_max_subscriptions: value_t_or_exit!(m, "electrum_max_subscriptions", usize),
electrum_max_clients: value_t_or_exit!(m, "electrum_max_clients", usize),
electrum_idle_timeout: value_t_or_exit!(m, "electrum_idle_timeout", u64),
electrum_haproxy_depth: value_t_or_exit!(m, "electrum_haproxy_depth", usize),
electrum_connections_per_client: value_t_or_exit!(
m,
"electrum_connections_per_client",
usize
),
jsonrpc_import: m.is_present("jsonrpc_import"),
light_mode: m.is_present("light_mode"),
main_loop_delay: value_t_or_exit!(m, "main_loop_delay", u64),
address_search: m.is_present("address_search"),
index_unspendables: m.is_present("index_unspendables"),
cors: m.value_of("cors").map(|s| s.to_string()),
precache_scripts: m.value_of("precache_scripts").map(|s| s.to_string()),
precache_threads: m.value_of("precache_threads").map_or_else(
|| {
std::thread::available_parallelism()
.expect("Can't get core count")
.get()
* 4
},
|s| match s.parse::<usize>() {
Ok(v) if v > 0 => v,
_ => clap::Error::value_validation_auto(format!(
"The argument '{}' isn't a valid value",
s
))
.exit(),
},
),
#[cfg(feature = "liquid")]
parent_network,
#[cfg(feature = "liquid")]
asset_db_path,
#[cfg(feature = "electrum-discovery")]
electrum_public_hosts,
#[cfg(feature = "electrum-discovery")]
electrum_announce: m.is_present("electrum_announce"),

View File

@ -2,12 +2,11 @@ use std::collections::{HashMap, HashSet};
use std::io::{BufRead, BufReader, Lines, Write};
use std::net::{SocketAddr, TcpStream};
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use base64;
use bitcoin::hashes::Hash;
use bitcoin::hashes::hex::{FromHex, ToHex};
use glob;
use hex;
use itertools::Itertools;
@ -19,7 +18,6 @@ use bitcoin::consensus::encode::{deserialize, serialize};
use elements::encode::{deserialize, serialize};
use crate::chain::{Block, BlockHash, BlockHeader, Network, Transaction, Txid};
use crate::config::BITCOIND_SUBVER;
use crate::metrics::{HistogramOpts, HistogramVec, Metrics};
use crate::signal::Waiter;
use crate::util::HeaderList;
@ -28,14 +26,14 @@ use crate::errors::*;
fn parse_hash<T>(value: &Value) -> Result<T>
where
T: FromStr,
<T as FromStr>::Err: std::fmt::Debug,
T: FromHex,
{
value
.as_str()
.chain_err(|| format!("non-string value: {}", value))?
.parse::<T>()
.map_err(|e| format!("failed to parse hash: {:?}", e).into())
Ok(T::from_hex(
value
.as_str()
.chain_err(|| format!("non-string value: {}", value))?,
)
.chain_err(|| format!("non-hex value: {}", value))?)
}
fn header_from_value(value: Value) -> Result<BlockHeader> {
@ -43,19 +41,22 @@ fn header_from_value(value: Value) -> Result<BlockHeader> {
.as_str()
.chain_err(|| format!("non-string header: {}", value))?;
let header_bytes = hex::decode(header_hex).chain_err(|| "non-hex header")?;
deserialize(&header_bytes).chain_err(|| format!("failed to parse header {}", header_hex))
Ok(
deserialize(&header_bytes)
.chain_err(|| format!("failed to parse header {}", header_hex))?,
)
}
fn block_from_value(value: Value) -> Result<Block> {
let block_hex = value.as_str().chain_err(|| "non-string block")?;
let block_bytes = hex::decode(block_hex).chain_err(|| "non-hex block")?;
deserialize(&block_bytes).chain_err(|| format!("failed to parse block {}", block_hex))
Ok(deserialize(&block_bytes).chain_err(|| format!("failed to parse block {}", block_hex))?)
}
fn tx_from_value(value: Value) -> Result<Transaction> {
let tx_hex = value.as_str().chain_err(|| "non-string tx")?;
let tx_bytes = hex::decode(tx_hex).chain_err(|| "non-hex tx")?;
deserialize(&tx_bytes).chain_err(|| format!("failed to parse tx {}", tx_hex))
Ok(deserialize(&tx_bytes).chain_err(|| format!("failed to parse tx {}", tx_hex))?)
}
/// Parse JSONRPC error code, if exists.
@ -67,7 +68,7 @@ fn parse_jsonrpc_reply(mut reply: Value, method: &str, expected_id: u64) -> Resu
if let Some(reply_obj) = reply.as_object_mut() {
if let Some(err) = reply_obj.get("error") {
if !err.is_null() {
if let Some(code) = parse_error_code(err) {
if let Some(code) = parse_error_code(&err) {
match code {
// RPC_IN_WARMUP -> retry by later reconnection
-28 => bail!(ErrorKind::Connection(err.to_string())),
@ -107,11 +108,6 @@ pub struct BlockchainInfo {
pub initialblockdownload: Option<bool>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct MempoolInfo {
pub loaded: bool,
}
#[derive(Serialize, Deserialize, Debug)]
struct NetworkInfo {
version: u64,
@ -119,54 +115,6 @@ struct NetworkInfo {
relayfee: f64, // in BTC/kB
}
#[derive(Serialize, Deserialize, Debug)]
struct MempoolFees {
base: f64,
#[serde(rename = "effective-feerate")]
effective_feerate: f64,
#[serde(rename = "effective-includes")]
effective_includes: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct MempoolAcceptResult {
txid: String,
wtxid: String,
allowed: Option<bool>,
vsize: Option<u32>,
fees: Option<MempoolFees>,
#[serde(rename = "reject-reason")]
reject_reason: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
struct MempoolFeesSubmitPackage {
base: f64,
#[serde(rename = "effective-feerate")]
effective_feerate: Option<f64>,
#[serde(rename = "effective-includes")]
effective_includes: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct SubmitPackageResult {
package_msg: String,
#[serde(rename = "tx-results")]
tx_results: HashMap<String, TxResult>,
#[serde(rename = "replaced-transactions")]
replaced_transactions: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct TxResult {
txid: String,
#[serde(rename = "other-wtxid")]
other_wtxid: Option<String>,
vsize: Option<u32>,
fees: Option<MempoolFeesSubmitPackage>,
error: Option<String>,
}
pub trait CookieGetter: Send + Sync {
fn get(&self) -> Result<Vec<u8>>;
}
@ -239,7 +187,7 @@ impl Connection {
.chain_err(|| {
ErrorKind::Connection("disconnected from daemon while receiving".to_owned())
})?
.chain_err(|| ErrorKind::Connection("failed to read status".to_owned()))?;
.chain_err(|| "failed to read status")?;
let mut headers = HashMap::new();
for line in iter {
let line = line.chain_err(|| ErrorKind::Connection("failed to read".to_owned()))?;
@ -314,7 +262,6 @@ pub struct Daemon {
daemon_dir: PathBuf,
blocks_dir: PathBuf,
network: Network,
magic: Option<u32>,
conn: Mutex<Connection>,
message_id: Counter, // for monotonic JSONRPC 'id'
signal: Waiter,
@ -325,22 +272,19 @@ pub struct Daemon {
}
impl Daemon {
#[allow(clippy::too_many_arguments)]
pub fn new(
daemon_dir: PathBuf,
blocks_dir: PathBuf,
daemon_dir: &PathBuf,
blocks_dir: &PathBuf,
daemon_rpc_addr: SocketAddr,
cookie_getter: Arc<dyn CookieGetter>,
network: Network,
magic: Option<u32>,
signal: Waiter,
metrics: &Metrics,
) -> Result<Daemon> {
let daemon = Daemon {
daemon_dir,
blocks_dir,
daemon_dir: daemon_dir.clone(),
blocks_dir: blocks_dir.clone(),
network,
magic,
conn: Mutex::new(Connection::new(
daemon_rpc_addr,
cookie_getter,
@ -365,9 +309,6 @@ impl Daemon {
network_info.subversion,
)
}
// Insert the subversion (/Satoshi xx.xx.xx(comment)/) string from bitcoind
_ = BITCOIND_SUBVER.set(network_info.subversion);
let blockchain_info = daemon.getblockchaininfo()?;
info!("{:?}", blockchain_info);
if blockchain_info.pruned {
@ -375,24 +316,16 @@ impl Daemon {
}
loop {
let info = daemon.getblockchaininfo()?;
let mempool = daemon.getmempoolinfo()?;
let ibd_done = if network.is_regtest() {
info.blocks == info.headers
} else {
!info.initialblockdownload.unwrap_or(false)
};
if mempool.loaded && ibd_done && info.blocks == info.headers {
if !info.initialblockdownload.unwrap_or(false) && info.blocks == info.headers {
break;
}
warn!(
"waiting for bitcoind sync and mempool load to finish: {}/{} blocks, verification progress: {:.3}%, mempool loaded: {}",
"waiting for bitcoind sync to finish: {}/{} blocks, verification progress: {:.3}%",
info.blocks,
info.headers,
info.verificationprogress * 100.0,
mempool.loaded
info.verificationprogress * 100.0
);
signal.wait(Duration::from_secs(5), false)?;
}
@ -404,7 +337,6 @@ impl Daemon {
daemon_dir: self.daemon_dir.clone(),
blocks_dir: self.blocks_dir.clone(),
network: self.network,
magic: self.magic,
conn: Mutex::new(self.conn.lock().unwrap().reconnect()?),
message_id: Counter::new(),
signal: self.signal.clone(),
@ -425,7 +357,7 @@ impl Daemon {
}
pub fn magic(&self) -> u32 {
self.magic.unwrap_or_else(|| self.network.magic())
self.network.magic()
}
fn call_jsonrpc(&self, method: &str, request: &Value) -> Result<Value> {
@ -445,46 +377,19 @@ impl Daemon {
Ok(result)
}
fn handle_request_batch(
&self,
method: &str,
params_list: &[Value],
failure_threshold: f64,
) -> Result<Vec<Value>> {
fn handle_request_batch(&self, method: &str, params_list: &[Value]) -> Result<Vec<Value>> {
let id = self.message_id.next();
let chunks = params_list
.iter()
.map(|params| json!({"method": method, "params": params, "id": id}))
.chunks(50_000); // Max Amount of batched requests
let mut results = vec![];
let total_requests = params_list.len();
let mut failed_requests: u64 = 0;
let threshold = (failure_threshold * total_requests as f64).round() as u64;
let mut n = 0;
for chunk in &chunks {
let reqs = chunk.collect();
let mut replies = self.call_jsonrpc(method, &reqs)?;
if let Some(replies_vec) = replies.as_array_mut() {
for reply in replies_vec {
n += 1;
match parse_jsonrpc_reply(reply.take(), method, id) {
Ok(parsed_reply) => results.push(parsed_reply),
Err(e) => {
failed_requests += 1;
warn!(
"batch request {} {}/{} failed: {}",
method,
n,
total_requests,
e.to_string()
);
// abort and return the last error once a threshold number of requests have failed
if failed_requests > threshold {
return Err(e);
}
}
}
results.push(parse_jsonrpc_reply(reply.take(), method, id)?)
}
} else {
bail!("non-array replies: {:?}", replies);
@ -494,14 +399,9 @@ impl Daemon {
Ok(results)
}
fn retry_request_batch(
&self,
method: &str,
params_list: &[Value],
failure_threshold: f64,
) -> Result<Vec<Value>> {
fn retry_request_batch(&self, method: &str, params_list: &[Value]) -> Result<Vec<Value>> {
loop {
match self.handle_request_batch(method, params_list, failure_threshold) {
match self.handle_request_batch(method, params_list) {
Err(Error(ErrorKind::Connection(msg), _)) => {
warn!("reconnecting to bitcoind: {}", msg);
self.signal.wait(Duration::from_secs(3), false)?;
@ -515,30 +415,25 @@ impl Daemon {
}
fn request(&self, method: &str, params: Value) -> Result<Value> {
let mut values = self.retry_request_batch(method, &[params], 0.0)?;
let mut values = self.retry_request_batch(method, &[params])?;
assert_eq!(values.len(), 1);
Ok(values.remove(0))
}
fn requests(&self, method: &str, params_list: &[Value]) -> Result<Vec<Value>> {
self.retry_request_batch(method, params_list, 0.0)
self.retry_request_batch(method, params_list)
}
// bitcoind JSONRPC API:
pub fn getblockchaininfo(&self) -> Result<BlockchainInfo> {
let info: Value = self.request("getblockchaininfo", json!([]))?;
from_value(info).chain_err(|| "invalid blockchain info")
}
fn getmempoolinfo(&self) -> Result<MempoolInfo> {
let info: Value = self.request("getmempoolinfo", json!([]))?;
from_value(info).chain_err(|| "invalid mempool info")
Ok(from_value(info).chain_err(|| "invalid blockchain info")?)
}
fn getnetworkinfo(&self) -> Result<NetworkInfo> {
let info: Value = self.request("getnetworkinfo", json!([]))?;
from_value(info).chain_err(|| "invalid network info")
Ok(from_value(info).chain_err(|| "invalid network info")?)
}
pub fn getbestblockhash(&self) -> Result<BlockHash> {
@ -548,7 +443,7 @@ impl Daemon {
pub fn getblockheader(&self, blockhash: &BlockHash) -> Result<BlockHeader> {
header_from_value(self.request(
"getblockheader",
json!([blockhash.to_string(), /*verbose=*/ false]),
json!([blockhash.to_hex(), /*verbose=*/ false]),
)?)
}
@ -567,22 +462,21 @@ impl Daemon {
}
pub fn getblock(&self, blockhash: &BlockHash) -> Result<Block> {
let block = block_from_value(self.request(
"getblock",
json!([blockhash.to_string(), /*verbose=*/ false]),
)?)?;
let block = block_from_value(
self.request("getblock", json!([blockhash.to_hex(), /*verbose=*/ false]))?,
)?;
assert_eq!(block.block_hash(), *blockhash);
Ok(block)
}
pub fn getblock_raw(&self, blockhash: &BlockHash, verbose: u32) -> Result<Value> {
self.request("getblock", json!([blockhash.to_string(), verbose]))
self.request("getblock", json!([blockhash.to_hex(), verbose]))
}
pub fn getblocks(&self, blockhashes: &[BlockHash]) -> Result<Vec<Block>> {
let params_list: Vec<Value> = blockhashes
.iter()
.map(|hash| json!([hash.to_string(), /*verbose=*/ false]))
.map(|hash| json!([hash.to_hex(), /*verbose=*/ false]))
.collect();
let values = self.requests("getblock", &params_list)?;
let mut blocks = vec![];
@ -595,14 +489,15 @@ impl Daemon {
pub fn gettransactions(&self, txhashes: &[&Txid]) -> Result<Vec<Transaction>> {
let params_list: Vec<Value> = txhashes
.iter()
.map(|txhash| json!([txhash.to_string(), /*verbose=*/ false]))
.map(|txhash| json!([txhash.to_hex(), /*verbose=*/ false]))
.collect();
let values = self.retry_request_batch("getrawtransaction", &params_list, 0.25)?;
let values = self.requests("getrawtransaction", &params_list)?;
let mut txs = vec![];
for value in values {
txs.push(tx_from_value(value)?);
}
// missing transactions are skipped, so the number of txs returned may be less than the number of txids requested
assert_eq!(txhashes.len(), txs.len());
Ok(txs)
}
@ -614,21 +509,21 @@ impl Daemon {
) -> Result<Value> {
self.request(
"getrawtransaction",
json!([txid.to_string(), verbose, blockhash]),
json!([txid.to_hex(), verbose, blockhash]),
)
}
pub fn getmempooltx(&self, txhash: &Txid) -> Result<Transaction> {
let value = self.request(
"getrawtransaction",
json!([txhash.to_string(), /*verbose=*/ false]),
json!([txhash.to_hex(), /*verbose=*/ false]),
)?;
tx_from_value(value)
}
pub fn getmempooltxids(&self) -> Result<HashSet<Txid>> {
let res = self.request("getrawmempool", json!([/*verbose=*/ false]))?;
serde_json::from_value(res).chain_err(|| "invalid getrawmempool reply")
Ok(serde_json::from_value(res).chain_err(|| "invalid getrawmempool reply")?)
}
pub fn broadcast(&self, tx: &Transaction) -> Result<Txid> {
@ -637,43 +532,10 @@ impl Daemon {
pub fn broadcast_raw(&self, txhex: &str) -> Result<Txid> {
let txid = self.request("sendrawtransaction", json!([txhex]))?;
txid.as_str()
.chain_err(|| "non-string txid")?
.parse::<Txid>()
.map_err(|e| format!("failed to parse txid: {:?}", e).into())
}
pub fn test_mempool_accept(
&self,
txhex: Vec<String>,
maxfeerate: Option<f64>,
) -> Result<Vec<MempoolAcceptResult>> {
let params = match maxfeerate {
Some(rate) => json!([txhex, format!("{:.8}", rate)]),
None => json!([txhex]),
};
let result = self.request("testmempoolaccept", params)?;
serde_json::from_value::<Vec<MempoolAcceptResult>>(result)
.chain_err(|| "invalid testmempoolaccept reply")
}
pub fn submit_package(
&self,
txhex: Vec<String>,
maxfeerate: Option<f64>,
maxburnamount: Option<f64>,
) -> Result<SubmitPackageResult> {
let params = match (maxfeerate, maxburnamount) {
(Some(rate), Some(burn)) => {
json!([txhex, format!("{:.8}", rate), format!("{:.8}", burn)])
}
(Some(rate), None) => json!([txhex, format!("{:.8}", rate)]),
(None, Some(burn)) => json!([txhex, null, format!("{:.8}", burn)]),
(None, None) => json!([txhex]),
};
let result = self.request("submitpackage", params)?;
serde_json::from_value::<SubmitPackageResult>(result)
.chain_err(|| "invalid submitpackage reply")
Ok(
Txid::from_hex(txid.as_str().chain_err(|| "non-string txid")?)
.chain_err(|| "failed to parse txid")?,
)
}
// Get estimated feerates for the provided confirmation targets using a batch RPC request
@ -711,7 +573,7 @@ impl Daemon {
}
fn get_all_headers(&self, tip: &BlockHash) -> Result<Vec<BlockHeader>> {
let info: Value = self.request("getblockheader", json!([tip.to_string()]))?;
let info: Value = self.request("getblockheader", json!([tip.to_hex()]))?;
let tip_height = info
.get("height")
.expect("missing height")
@ -722,12 +584,12 @@ impl Daemon {
let mut result = vec![];
for heights in all_heights.chunks(chunk_size) {
trace!("downloading {} block headers", heights.len());
let mut headers = self.getblockheaders(heights)?;
let mut headers = self.getblockheaders(&heights)?;
assert!(headers.len() == heights.len());
result.append(&mut headers);
}
let mut blockhash = BlockHash::all_zeros();
let mut blockhash = BlockHash::default();
for header in &result {
assert_eq!(header.prev_blockhash, blockhash);
blockhash = header.block_hash();
@ -753,7 +615,7 @@ impl Daemon {
bestblockhash,
);
let mut new_headers = vec![];
let null_hash = BlockHash::all_zeros();
let null_hash = BlockHash::default();
let mut blockhash = *bestblockhash;
while blockhash != null_hash {
if indexed_headers.header_by_blockhash(&blockhash).is_some() {

View File

@ -1,9 +1,9 @@
use std::collections::HashMap;
use std::convert::TryFrom;
use bitcoin::hashes::sha256d;
use bitcoin::hashes::Hash;
pub use electrum_client::client::Client;
pub use electrum_client::Error as ElectrumError;
pub use electrum_client::ServerFeaturesRes;
use crate::chain::BlockHash;
@ -20,9 +20,7 @@ impl TryFrom<ServerFeaturesRes> for ServerFeatures {
Ok(ServerFeatures {
// electrum-client doesn't retain the hosts map data, but we already have it from the add_peer request
hosts: HashMap::new(),
genesis_hash: BlockHash::from_raw_hash(sha256d::Hash::from_byte_array(
features.genesis_hash,
)),
genesis_hash: BlockHash::from_inner(features.genesis_hash),
server_version: features.server_version,
protocol_min: features
.protocol_min

View File

@ -80,7 +80,6 @@ struct HealthCheck {
hostname: Hostname,
service: Service,
is_default: bool,
#[allow(dead_code)]
added_by: Option<IpAddr>,
last_check: Option<Instant>,
last_healthy: Option<Instant>,
@ -183,7 +182,7 @@ impl DiscoveryManager {
.filter(|service| {
existing_services
.get(&addr)
.is_none_or(|s| !s.contains(service))
.map_or(true, |s| !s.contains(service))
})
.map(|service| {
HealthCheck::new(addr.clone(), hostname.clone(), service, Some(added_by))
@ -235,9 +234,9 @@ impl DiscoveryManager {
/// Run the next health check in the queue (a single one)
fn run_health_check(&self) -> Result<()> {
// abort if there are no entries in the queue, or its still too early for the next one up
if self.queue.read().unwrap().peek().is_none_or(|next| {
if self.queue.read().unwrap().peek().map_or(true, |next| {
next.last_check
.is_some_and(|t| t.elapsed() < HEALTH_CHECK_FREQ)
.map_or(false, |t| t.elapsed() < HEALTH_CHECK_FREQ)
}) {
return Ok(());
}
@ -337,7 +336,7 @@ impl DiscoveryManager {
self.tor_proxy
.chain_err(|| "no tor proxy configured, onion hosts are unsupported")?,
);
config = config.socks5(Some(socks))
config = config.socks5(Some(socks)).unwrap()
}
let client = Client::from_config(&server_url, config.build())?;
@ -526,63 +525,45 @@ mod tests {
use crate::chain::Network;
use std::time;
use crate::config::VERSION_STRING;
const PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(1, 4);
#[test]
#[ignore = "This test requires external connection to server that no longer exists"]
fn test() -> Result<()> {
stderrlog::new().verbosity(4).init().unwrap();
#[cfg(feature = "liquid")]
let testnet = Network::LiquidTestnet;
#[cfg(not(feature = "liquid"))]
let testnet = Network::Testnet;
let features = ServerFeatures {
hosts: serde_json::from_str("{\"test.foobar.example\":{\"tcp_port\":60002}}").unwrap(),
server_version: VERSION_STRING.clone(),
genesis_hash: genesis_hash(testnet),
server_version: format!("electrs-esplora 9"),
genesis_hash: genesis_hash(Network::Testnet),
protocol_min: PROTOCOL_VERSION,
protocol_max: PROTOCOL_VERSION,
hash_function: "sha256".into(),
pruning: None,
};
let discovery = Arc::new(DiscoveryManager::new(
testnet,
Network::Testnet,
features,
PROTOCOL_VERSION,
false,
None,
));
discovery
.add_default_server(
"electrum.blockstream.info".into(),
vec![Service::Tcp(60001)],
)
.unwrap();
discovery
.add_default_server("testnet.hsmiths.com".into(), vec![Service::Ssl(53012)])
.unwrap();
discovery
.add_default_server(
"tn.not.fyi".into(),
vec![Service::Tcp(55001), Service::Ssl(55002)],
)
.unwrap();
discovery
.add_default_server(
"electrum.blockstream.info".into(),
vec![Service::Tcp(60001), Service::Ssl(60002)],
)
.unwrap();
discovery
.add_default_server(
"explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion".into(),
vec![Service::Tcp(143)],
)
.unwrap();
discovery.add_default_server(
"electrum.blockstream.info".into(),
vec![Service::Tcp(60001)],
).unwrap();
discovery.add_default_server("testnet.hsmiths.com".into(), vec![Service::Ssl(53012)]).unwrap();
discovery.add_default_server(
"tn.not.fyi".into(),
vec![Service::Tcp(55001), Service::Ssl(55002)],
).unwrap();
discovery.add_default_server(
"electrum.blockstream.info".into(),
vec![Service::Tcp(60001), Service::Ssl(60002)],
).unwrap();
discovery.add_default_server(
"explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion".into(),
vec![Service::Tcp(143)],
).unwrap();
debug!("{:#?}", discovery);

View File

@ -1,8 +1,6 @@
use crate::chain::Network;
#[allow(unused_imports)]
use crate::electrum::discovery::{DiscoveryManager, Service};
#[allow(unused_variables)]
pub fn add_default_servers(discovery: &DiscoveryManager, network: Network) {
match network {
#[cfg(not(feature = "liquid"))]

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, RwLock, RwLockReadGuard};
use std::sync::{Arc, RwLock};
use bitcoin::hashes::{sha256, Hash};
use bitcoin::hashes::{hex::FromHex, sha256, Hash};
use elements::confidential::{Asset, Value};
use elements::encode::{deserialize, serialize};
use elements::secp256k1_zkp::ZERO_TWEAK;
@ -11,24 +11,19 @@ use crate::chain::{BNetwork, BlockHash, Network, Txid};
use crate::elements::peg::{get_pegin_data, get_pegout_data, PeginInfo, PegoutInfo};
use crate::elements::registry::{AssetMeta, AssetRegistry};
use crate::errors::*;
use crate::new_index::schema::{Operation, TxHistoryInfo, TxHistoryKey, TxHistoryRow};
use crate::new_index::schema::{TxHistoryInfo, TxHistoryKey, TxHistoryRow};
use crate::new_index::{db::DBFlush, ChainQuery, DBRow, Mempool, Query};
use crate::util::{
bincode_util, full_hash, Bytes, FullHash, IsProvablyUnspendable, TransactionStatus, TxInput,
};
use crate::util::{full_hash, Bytes, FullHash, TransactionStatus, TxInput};
lazy_static! {
pub static ref NATIVE_ASSET_ID: AssetId =
"6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d"
.parse()
AssetId::from_hex("6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d")
.unwrap();
pub static ref NATIVE_ASSET_ID_TESTNET: AssetId =
"144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49"
.parse()
AssetId::from_hex("144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49")
.unwrap();
pub static ref NATIVE_ASSET_ID_REGTEST: AssetId =
"5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225"
.parse()
AssetId::from_hex("5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225")
.unwrap();
}
@ -38,7 +33,6 @@ fn parse_asset_id(sl: &[u8]) -> AssetId {
#[derive(Serialize)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum LiquidAsset {
Issued(IssuedAsset),
Native(PeggedAsset),
@ -77,9 +71,9 @@ pub struct IssuedAsset {
#[derive(Serialize, Deserialize, Debug)]
pub struct AssetRow {
pub issuance_txid: FullHash,
pub issuance_vin: u32,
pub issuance_vin: u16,
pub prev_txid: FullHash,
pub prev_vout: u32,
pub prev_vout: u16,
pub issuance: Bytes, // bincode does not like dealing with AssetIssuance, deserialization fails with "invalid type: sequence, expected a struct"
pub reissuance_token: FullHash,
}
@ -98,7 +92,7 @@ impl IssuedAsset {
let reissuance_token = parse_asset_id(&asset.reissuance_token);
let contract_hash = if issuance.asset_entropy != [0u8; 32] {
Some(ContractHash::from_byte_array(issuance.asset_entropy))
Some(ContractHash::from_inner(issuance.asset_entropy))
} else {
None
};
@ -111,7 +105,7 @@ impl IssuedAsset {
},
issuance_prevout: OutPoint {
txid: deserialize(&asset.prev_txid).unwrap(),
vout: asset.prev_vout,
vout: asset.prev_vout as u32,
},
contract_hash,
reissuance_token,
@ -158,10 +152,9 @@ impl LiquidAsset {
}
#[derive(Serialize, Deserialize, Debug)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct IssuingInfo {
pub txid: FullHash,
pub vin: u32,
pub vin: u16,
pub is_reissuance: bool,
// None for blinded issuances
pub issued_amount: Option<u64>,
@ -169,10 +162,9 @@ pub struct IssuingInfo {
}
#[derive(Serialize, Deserialize, Debug)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct BurningInfo {
pub txid: FullHash,
pub vout: u32,
pub vout: u16,
pub value: u64,
}
@ -180,29 +172,24 @@ pub struct BurningInfo {
pub fn index_confirmed_tx_assets(
tx: &Transaction,
confirmed_height: u32,
tx_position: u16,
network: Network,
parent_network: BNetwork,
rows: &mut Vec<DBRow>,
op: &Operation,
) {
let (history, issuances) = index_tx_assets(tx, network, parent_network);
rows.extend(history.into_iter().map(|(asset_id, info)| {
let history_row = asset_history_row(&asset_id, confirmed_height, tx_position, info);
if let Operation::DeleteBlocksWithHistory(tx) = op {
tx.send(history_row.key.hash)
.expect("unbounded channel won't fail");
}
history_row.into_row()
}));
rows.extend(
history.into_iter().map(|(asset_id, info)| {
asset_history_row(&asset_id, confirmed_height, info).into_row()
}),
);
// the initial issuance is kept twice: once in the history index under I<asset><height><txid:vin>,
// and once separately under i<asset> for asset lookup with some more associated metadata.
// reissuances are only kept under the history index.
rows.extend(issuances.into_iter().map(|(asset_id, asset_row)| DBRow {
key: [b"i", &asset_id.into_inner()[..]].concat(),
value: bincode_util::serialize_little(&asset_row).unwrap(),
value: bincode::serialize(&asset_row).unwrap(),
}));
}
@ -216,7 +203,10 @@ pub fn index_mempool_tx_assets(
) {
let (history, issuances) = index_tx_assets(tx, network, parent_network);
for (asset_id, info) in history {
asset_history.entry(asset_id).or_default().push(info);
asset_history
.entry(asset_id)
.or_insert_with(Vec::new)
.push(info);
}
for (asset_id, issuance) in issuances {
asset_issuance.insert(asset_id, issuance);
@ -241,13 +231,12 @@ pub fn remove_mempool_tx_assets(
});
}
type HistoryAndIssuances = (Vec<(AssetId, TxHistoryInfo)>, Vec<(AssetId, AssetRow)>);
// Internal utility function, index a transaction and return its history entries and issuances
fn index_tx_assets(
tx: &Transaction,
network: Network,
parent_network: BNetwork,
) -> HistoryAndIssuances {
) -> (Vec<(AssetId, TxHistoryInfo)>, Vec<(AssetId, AssetRow)>) {
let mut history = vec![];
let mut issuances = vec![];
@ -259,19 +248,19 @@ fn index_tx_assets(
pegout.asset.explicit().unwrap(),
TxHistoryInfo::Pegout(PegoutInfo {
txid,
vout: txo_index as u32,
vout: txo_index as u16,
value: pegout.value,
}),
));
} else if txo.script_pubkey.is_provably_unspendable_() && !txo.is_fee() {
} else if txo.script_pubkey.is_provably_unspendable() && !txo.is_fee() {
if let (Asset::Explicit(asset_id), Value::Explicit(value)) = (txo.asset, txo.value) {
if value > 0 {
history.push((
asset_id,
TxHistoryInfo::Burning(BurningInfo {
txid,
vout: txo_index as u32,
value,
vout: txo_index as u16,
value: value,
}),
));
}
@ -282,10 +271,10 @@ fn index_tx_assets(
for (txi_index, txi) in tx.input.iter().enumerate() {
if let Some(pegin) = get_pegin_data(txi, network) {
history.push((
pegin.asset,
pegin.asset.explicit().unwrap(),
TxHistoryInfo::Pegin(PeginInfo {
txid,
vin: txi_index as u32,
vin: txi_index as u16,
value: pegin.value,
}),
));
@ -310,7 +299,7 @@ fn index_tx_assets(
asset_id,
TxHistoryInfo::Issuing(IssuingInfo {
txid,
vin: txi_index as u32,
vin: txi_index as u16,
is_reissuance,
issued_amount,
token_amount,
@ -318,8 +307,10 @@ fn index_tx_assets(
));
if !is_reissuance {
let is_confidential =
matches!(txi.asset_issuance.inflation_keys, Value::Confidential(..));
let is_confidential = match txi.asset_issuance.inflation_keys {
Value::Confidential(..) => true,
_ => false,
};
let reissuance_token =
AssetId::reissuance_token_from_entropy(asset_entropy, is_confidential);
@ -327,9 +318,9 @@ fn index_tx_assets(
asset_id,
AssetRow {
issuance_txid: txid,
issuance_vin: txi_index as u32,
issuance_vin: txi_index as u16,
prev_txid: full_hash(&txi.previous_output.txid[..]),
prev_vout: txi.previous_output.vout,
prev_vout: txi.previous_output.vout as u16,
issuance: serialize(&txi.asset_issuance),
reissuance_token: full_hash(&reissuance_token.into_inner()[..]),
},
@ -344,27 +335,20 @@ fn index_tx_assets(
fn asset_history_row(
asset_id: &AssetId,
confirmed_height: u32,
tx_position: u16,
txinfo: TxHistoryInfo,
) -> TxHistoryRow {
let key = TxHistoryKey {
code: b'I',
hash: full_hash(&asset_id.into_inner()[..]),
confirmed_height,
tx_position,
txinfo,
};
TxHistoryRow { key }
}
pub enum AssetRegistryLock<'a> {
RwLock(&'a Arc<RwLock<AssetRegistry>>),
RwLockReadGuard(&'a RwLockReadGuard<'a, AssetRegistry>),
}
pub fn lookup_asset(
query: &Query,
registry: Option<AssetRegistryLock>,
registry: Option<&Arc<RwLock<AssetRegistry>>>,
asset_id: &AssetId,
meta: Option<&AssetMeta>, // may optionally be provided if already known
) -> Result<Option<LiquidAsset>> {
@ -373,20 +357,17 @@ pub fn lookup_asset(
return Ok(Some(LiquidAsset::Native(PeggedAsset {
asset_id: *asset_id,
chain_stats,
mempool_stats,
chain_stats: chain_stats,
mempool_stats: mempool_stats,
})));
}
let history_db = query.chain().store().history_db();
let mempool = query.mempool();
let mempool_issuances = &mempool.asset_issuance;
let mempool_issuances = &query.mempool().asset_issuance;
let chain_row = history_db
.get(&[b"i", &asset_id.into_inner()[..]].concat())
.map(|row| {
bincode_util::deserialize_little::<AssetRow>(&row).expect("failed parsing AssetRow")
});
.map(|row| bincode::deserialize::<AssetRow>(&row).expect("failed parsing AssetRow"));
let row = chain_row
.as_ref()
@ -395,14 +376,11 @@ pub fn lookup_asset(
Ok(if let Some(row) = row {
let reissuance_token = parse_asset_id(&row.reissuance_token);
let meta = meta.cloned().or_else(|| match registry {
Some(AssetRegistryLock::RwLock(rwlock)) => {
rwlock.read().unwrap().get(asset_id).cloned()
}
Some(AssetRegistryLock::RwLockReadGuard(guard)) => guard.get(asset_id).cloned(),
None => None,
});
let stats = issued_asset_stats(query.chain(), &mempool, asset_id, &reissuance_token);
let registry = registry.map(|r| r.read().unwrap());
let meta = meta
.or_else(|| registry.as_ref().and_then(|r| r.get(asset_id)))
.cloned();
let stats = issued_asset_stats(query, asset_id, &reissuance_token);
let status = query.get_tx_status(&deserialize(&row.issuance_txid).unwrap());
let asset = IssuedAsset::new(asset_id, row, stats, meta, status);
@ -414,7 +392,7 @@ pub fn lookup_asset(
}
pub fn get_issuance_entropy(txin: &TxIn) -> Result<sha256::Midstate> {
if !txin.has_issuance() {
if !txin.has_issuance {
bail!("input has no issuance");
}
@ -468,7 +446,7 @@ where
{
DBRow {
key: asset_cache_key(asset_id),
value: bincode_util::serialize_little(&(stats, blockhash)).unwrap(),
value: bincode::serialize(&(stats, blockhash)).unwrap(),
}
}
@ -482,20 +460,21 @@ fn pegged_asset_stats(query: &Query, asset_id: &AssetId) -> (PeggedAssetStats, P
// Get stats for issued assets
fn issued_asset_stats(
chain: &ChainQuery,
mempool: &Mempool,
query: &Query,
asset_id: &AssetId,
reissuance_token: &AssetId,
) -> (IssuedAssetStats, IssuedAssetStats) {
let afn = apply_issued_asset_stats;
let chain = query.chain();
let mut chain_stats = chain_asset_stats(chain, asset_id, afn);
chain_stats.burned_reissuance_tokens =
chain_asset_stats(chain, reissuance_token, afn).burned_amount;
let mut mempool_stats = mempool_asset_stats(mempool, asset_id, afn);
let mempool = query.mempool();
let mut mempool_stats = mempool_asset_stats(&mempool, &asset_id, afn);
mempool_stats.burned_reissuance_tokens =
mempool_asset_stats(mempool, reissuance_token, afn).burned_amount;
mempool_asset_stats(&mempool, &reissuance_token, afn).burned_amount;
(chain_stats, mempool_stats)
}
@ -511,7 +490,7 @@ where
.store()
.cache_db()
.get(&asset_cache_key(asset_id))
.map(|c| bincode_util::deserialize_little(&c).unwrap())
.map(|c| bincode::deserialize(&c).unwrap())
.and_then(|(stats, blockhash)| {
chain
.height_by_hash(&blockhash)

View File

@ -1,5 +1,4 @@
use bitcoin::hashes::Hash;
use elements::hex::ToHex;
use bitcoin::hashes::{hex::ToHex, Hash};
use elements::secp256k1_zkp::ZERO_TWEAK;
use elements::{confidential::Value, encode::serialize, issuance::ContractHash, AssetId, TxIn};
@ -9,7 +8,7 @@ mod registry;
use asset::get_issuance_entropy;
pub use asset::{lookup_asset, LiquidAsset};
pub use registry::{AssetMeta, AssetRegistry, AssetSorting};
pub use registry::{AssetRegistry, AssetSorting};
#[derive(Serialize, Deserialize, Clone)]
pub struct IssuanceValue {
@ -74,4 +73,4 @@ impl From<&TxIn> for IssuanceValue {
},
}
}
}
}

View File

@ -1,40 +1,25 @@
use elements::hex::ToHex;
use bitcoin::hashes::hex::ToHex;
use elements::{confidential::Asset, PeginData, PegoutData, TxIn, TxOut};
use crate::chain::{bitcoin_genesis_hash, BNetwork, Network};
use crate::util::FullHash;
use crate::util::{FullHash, ScriptToAsm};
pub fn get_pegin_data(txout: &TxIn, network: Network) -> Option<PeginData<'_>> {
pub fn get_pegin_data(txout: &TxIn, network: Network) -> Option<PeginData> {
let pegged_asset_id = network.pegged_asset()?;
txout.pegin_data().and_then(|pegin| {
if pegin.asset == *pegged_asset_id {
Some(pegin)
} else {
None
}
})
txout
.pegin_data()
.filter(|pegin| pegin.asset == Asset::Explicit(*pegged_asset_id))
}
pub fn get_pegout_data(
txout: &TxOut,
network: Network,
parent_network: BNetwork,
) -> Option<PegoutData<'_>> {
) -> Option<PegoutData> {
let pegged_asset_id = network.pegged_asset()?;
txout.pegout_data().and_then(|pegout| {
if pegout.asset == Asset::Explicit(*pegged_asset_id)
&& pegout.genesis_hash
== bitcoin_genesis_hash(match parent_network {
BNetwork::Bitcoin => Network::Liquid,
BNetwork::Testnet | BNetwork::Testnet4 => Network::LiquidTestnet,
BNetwork::Signet => return None,
BNetwork::Regtest => Network::LiquidRegtest,
})
{
Some(pegout)
} else {
None
}
txout.pegout_data().filter(|pegout| {
pegout.asset == Asset::Explicit(*pegged_asset_id)
&& pegout.genesis_hash == bitcoin_genesis_hash(parent_network)
})
}
@ -42,7 +27,7 @@ pub fn get_pegout_data(
#[derive(Serialize, Deserialize, Clone)]
pub struct PegoutValue {
pub genesis_hash: String,
pub scriptpubkey: bitcoin::ScriptBuf,
pub scriptpubkey: bitcoin::Script,
pub scriptpubkey_asm: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub scriptpubkey_address: Option<String>,
@ -53,12 +38,12 @@ impl PegoutValue {
let pegoutdata = get_pegout_data(txout, network, parent_network)?;
// pending https://github.com/ElementsProject/rust-elements/pull/69 is merged
let scriptpubkey = bitcoin::ScriptBuf::from(pegoutdata.script_pubkey.into_bytes());
let address = bitcoin::Address::from_script(&scriptpubkey, parent_network).ok();
let scriptpubkey = bitcoin::Script::from(pegoutdata.script_pubkey.into_bytes());
let address = bitcoin::Address::from_script(&scriptpubkey, parent_network);
Some(PegoutValue {
genesis_hash: pegoutdata.genesis_hash.to_hex(),
scriptpubkey_asm: scriptpubkey.to_asm_string(),
scriptpubkey_asm: scriptpubkey.to_asm(),
scriptpubkey_address: address.map(|s| s.to_string()),
scriptpubkey,
})
@ -67,18 +52,16 @@ impl PegoutValue {
// Inner type for the indexer TxHistoryInfo::Pegin variant
#[derive(Serialize, Deserialize, Debug)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct PeginInfo {
pub txid: FullHash,
pub vin: u32,
pub vin: u16,
pub value: u64,
}
// Inner type for the indexer TxHistoryInfo::Pegout variant
#[derive(Serialize, Deserialize, Debug)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct PegoutInfo {
pub txid: FullHash,
pub vout: u32,
pub vout: u16,
pub value: u64,
}

View File

@ -1,11 +1,11 @@
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::{Arc, RwLock};
use std::time::{Duration, SystemTime};
use std::{cmp, fs, path, thread};
use serde_json::Value as JsonValue;
use bitcoin::hashes::hex::FromHex;
use elements::AssetId;
use crate::errors::*;
@ -14,8 +14,6 @@ use crate::errors::*;
// (in number of hex characters, not bytes)
const DIR_PARTITION_LEN: usize = 2;
const SEARCH_SORT_CANDIDATE_LIMIT: usize = 2000;
pub struct AssetRegistry {
directory: path::PathBuf,
assets_cache: HashMap<AssetId, (SystemTime, AssetMeta)>,
@ -42,7 +40,7 @@ impl AssetRegistry {
start_index: usize,
limit: usize,
sorting: AssetSorting,
) -> (usize, Vec<AssetEntry<'_>>) {
) -> (usize, Vec<AssetEntry>) {
let mut assets: Vec<AssetEntry> = self
.assets_cache
.iter()
@ -55,39 +53,6 @@ impl AssetRegistry {
)
}
pub fn search(&self, query: &str, limit: usize) -> Vec<AssetEntry<'_>> {
let query = query.trim();
if query.is_empty() || limit == 0 {
return vec![];
}
let (mut results, candidates) = search_by(
self.assets_cache
.iter()
.map(|(asset_id, (_, metadata))| (asset_id, metadata)),
query,
limit,
|metadata| metadata.ticker.as_deref(),
);
if results.len() < limit {
let (name_matches, candidates) =
search_by(candidates, query, limit - results.len(), |metadata| {
Some(&metadata.name)
});
results.extend(name_matches);
if results.len() < limit {
let (domain_matches, _) =
search_by(candidates, query, limit - results.len(), AssetMeta::domain);
results.extend(domain_matches);
}
}
results.truncate(limit);
results
}
pub fn fs_sync(&mut self) -> Result<()> {
for entry in fs::read_dir(&self.directory).chain_err(|| "failed reading asset dir")? {
let entry = entry.chain_err(|| "invalid fh")?;
@ -105,7 +70,7 @@ impl AssetRegistry {
continue;
}
let asset_id = AssetId::from_str(
let asset_id = AssetId::from_hex(
path.file_stem()
.unwrap() // cannot fail if extension() succeeded
.to_str()
@ -137,7 +102,7 @@ impl AssetRegistry {
}
pub fn spawn_sync(asset_db: Arc<RwLock<AssetRegistry>>) -> thread::JoinHandle<()> {
crate::util::spawn_thread("asset-registry", move || loop {
thread::spawn(move || loop {
if let Err(e) = asset_db.write().unwrap().fs_sync() {
error!("registry fs_sync failed: {:?}", e);
}
@ -161,7 +126,7 @@ pub struct AssetMeta {
}
impl AssetMeta {
pub(crate) fn domain(&self) -> Option<&str> {
fn domain(&self) -> Option<&str> {
self.entity["domain"].as_str()
}
}
@ -178,12 +143,9 @@ pub enum AssetSortDir {
Ascending,
}
type Comparator = Box<dyn Fn(&AssetEntry, &AssetEntry) -> cmp::Ordering>;
impl AssetSorting {
#[allow(clippy::wrong_self_convention)]
fn as_comparator(self) -> Comparator {
let sort_fn: Comparator = match self.0 {
fn as_comparator(self) -> Box<dyn Fn(&AssetEntry, &AssetEntry) -> cmp::Ordering> {
let sort_fn: Box<dyn Fn(&AssetEntry, &AssetEntry) -> cmp::Ordering> = match self.0 {
AssetSortField::Name => {
// Order by name first, use asset id as a tie breaker. the other sorting fields
// don't require this because they're guaranteed to be unique.
@ -227,72 +189,3 @@ fn lc_cmp_opt(a: &Option<String>, b: &Option<String>) -> cmp::Ordering {
.map(|a| a.to_lowercase())
.cmp(&b.as_ref().map(|b| b.to_lowercase()))
}
fn search_by<'a, I, F>(
candidates: I,
query: &str,
limit: usize,
field: F,
) -> (Vec<AssetEntry<'a>>, Vec<AssetEntry<'a>>)
where
I: IntoIterator<Item = AssetEntry<'a>>,
F: Fn(&AssetMeta) -> Option<&str>,
{
let mut matches = vec![];
let mut remaining = vec![];
for (asset_id, metadata) in candidates {
let position = field(metadata).and_then(|field| {
// registry fields are ascii, so we don't need full unicode case-folding
ascii_ci_find(field, query).map(|position| (position, field))
});
if let Some((position, field)) = position {
if matches.len() >= SEARCH_SORT_CANDIDATE_LIMIT {
continue;
}
matches.push((position, field, asset_id, metadata));
} else {
remaining.push((asset_id, metadata));
}
}
matches.sort_unstable_by(|a, b| {
a.0.cmp(&b.0)
.then_with(|| ascii_ci_cmp(a.1, b.1))
.then_with(|| a.2.cmp(b.2))
});
(
matches
.into_iter()
.take(limit)
.map(|(_, _, asset_id, metadata)| (asset_id, metadata))
.collect(),
remaining,
)
}
// zero-allocation case-insensitive ASCII substring search
// returns the byte offset of the first match
fn ascii_ci_find(haystack: &str, needle: &str) -> Option<usize> {
let (haystack, needle) = (haystack.as_bytes(), needle.as_bytes());
if needle.is_empty() {
return Some(0);
}
haystack
.windows(needle.len())
.position(|window| window.eq_ignore_ascii_case(needle))
}
// zero-allocation case-insensitive ASCII string comparison
fn ascii_ci_cmp(a: &str, b: &str) -> cmp::Ordering {
let (a, b) = (a.as_bytes(), b.as_bytes());
for i in 0..a.len().min(b.len()) {
match a[i].to_ascii_lowercase().cmp(&b[i].to_ascii_lowercase()) {
cmp::Ordering::Equal => continue,
ord => return ord,
}
}
a.len().cmp(&b.len())
}

View File

@ -1,4 +1,3 @@
#![allow(unexpected_cfgs)]
error_chain! {
types {
Error, ErrorKind, ResultExt, Result;
@ -15,14 +14,9 @@ error_chain! {
display("Iterrupted by signal {}", sig)
}
TooManyUtxos(limit: usize) {
description("Too many unspent transaction outputs. Contact support to raise limits.")
display("Too many unspent transaction outputs (>{}). Contact support to raise limits.", limit)
}
TooManyTxs(limit: usize) {
description("Too many history transactions. Contact support to raise limits.")
display("Too many history transactions (>{}). Contact support to raise limits.", limit)
TooPopular {
description("Too many history entries")
display("Too many history entries")
}
#[cfg(feature = "electrum-discovery")]

View File

@ -1,6 +1,10 @@
#![recursion_limit = "1024"]
// See https://github.com/romanz/electrs/issues/193 & https://github.com/rust-rocksdb/rust-rocksdb/issues/327
#[cfg(not(feature = "oldcpu"))]
extern crate rocksdb;
#[cfg(feature = "oldcpu")]
extern crate rocksdb_oldcpu as rocksdb;
#[macro_use]
extern crate clap;

View File

@ -5,6 +5,7 @@ use std::io;
use std::net::SocketAddr;
use std::thread;
use std::time::Duration;
use sysconf;
use tiny_http;
pub use prometheus::{
@ -68,7 +69,7 @@ impl Metrics {
pub fn start(&self) {
let server = tiny_http::Server::http(self.addr)
.unwrap_or_else(|_| panic!("failed to start monitoring HTTP server at {}", self.addr));
start_process_exporter(self);
start_process_exporter(&self);
let reg = self.reg.clone();
spawn_thread("metrics", move || loop {
if let Err(e) = handle_request(&reg, server.recv()) {
@ -97,14 +98,6 @@ struct Stats {
fds: usize,
}
fn get_ticks_per_second() -> Result<f64> {
// Safety: This code is taken directly from sysconf
match unsafe { libc::sysconf(libc::_SC_CLK_TCK) } {
-1 => Err("Clock Tick unsupported".into()),
ret => Ok(ret as f64),
}
}
fn parse_stats() -> Result<Stats> {
if cfg!(target_os = "macos") {
return Ok(Stats {
@ -116,14 +109,15 @@ fn parse_stats() -> Result<Stats> {
let value = fs::read_to_string("/proc/self/stat").chain_err(|| "failed to read stats")?;
let parts: Vec<&str> = value.split_whitespace().collect();
let page_size = page_size::get() as u64;
let ticks_per_second = get_ticks_per_second().expect("failed to get _SC_CLK_TCK");
let ticks_per_second = sysconf::raw::sysconf(sysconf::raw::SysconfVariable::ScClkTck)
.expect("failed to get _SC_CLK_TCK") as f64;
let parse_part = |index: usize, name: &str| -> Result<u64> {
parts
Ok(parts
.get(index)
.chain_err(|| format!("missing {}: {:?}", name, parts))?
.parse::<u64>()
.chain_err(|| format!("invalid {}: {:?}", name, parts))
.chain_err(|| format!("invalid {}: {:?}", name, parts))?)
};
// For details, see '/proc/[pid]/stat' section at `man 5 proc`:
@ -148,7 +142,7 @@ fn start_process_exporter(metrics: &Metrics) {
spawn_thread("exporter", move || loop {
match parse_stats() {
Ok(stats) => {
cpu.with_label_values(&["utime"]).set(stats.utime);
cpu.with_label_values(&["utime"]).set(stats.utime as f64);
rss.set(stats.rss as i64);
fds.set(stats.fds as i64);
}

View File

@ -3,13 +3,9 @@ use rocksdb;
use std::path::Path;
use crate::config::Config;
use crate::util::{bincode_util, Bytes};
use crate::util::Bytes;
/// Each version will break any running instance with a DB that has a differing version.
/// It will also break if light mode is enabled or disabled.
// 1 = Original DB (since fork from Blockstream)
// 2 = Add tx position to TxHistory rows and place Spending before Funding
static DB_VERSION: u32 = 2;
static DB_VERSION: u32 = 1;
#[derive(Debug, Eq, PartialEq)]
pub struct DBRow {
@ -23,14 +19,14 @@ pub struct ScanIterator<'a> {
done: bool,
}
impl Iterator for ScanIterator<'_> {
impl<'a> Iterator for ScanIterator<'a> {
type Item = DBRow;
fn next(&mut self) -> Option<DBRow> {
if self.done {
return None;
}
let (key, value) = self.iter.next().map(Result::ok)??;
let (key, value) = self.iter.next()?;
if !key.starts_with(&self.prefix) {
self.done = true;
return None;
@ -48,7 +44,7 @@ pub struct ReverseScanIterator<'a> {
done: bool,
}
impl Iterator for ReverseScanIterator<'_> {
impl<'a> Iterator for ReverseScanIterator<'a> {
type Item = DBRow;
fn next(&mut self) -> Option<DBRow> {
@ -73,67 +69,6 @@ impl Iterator for ReverseScanIterator<'_> {
}
}
pub struct ReverseScanGroupIterator<'a> {
iters: Vec<ReverseScanIterator<'a>>,
next_rows: Vec<Option<DBRow>>,
value_offset: usize,
done: bool,
}
impl<'a> ReverseScanGroupIterator<'a> {
pub fn new(
mut iters: Vec<ReverseScanIterator<'a>>,
value_offset: usize,
) -> ReverseScanGroupIterator<'a> {
let mut next_rows: Vec<Option<DBRow>> = Vec::with_capacity(iters.len());
for iter in &mut iters {
let next = iter.next();
next_rows.push(next);
}
let done = next_rows.iter().all(|row| row.is_none());
ReverseScanGroupIterator {
iters,
next_rows,
value_offset,
done,
}
}
}
impl Iterator for ReverseScanGroupIterator<'_> {
type Item = DBRow;
fn next(&mut self) -> Option<DBRow> {
if self.done {
return None;
}
let best_index = self
.next_rows
.iter()
.enumerate()
.max_by(|(a_index, a_opt), (b_index, b_opt)| match (a_opt, b_opt) {
(None, None) => a_index.cmp(b_index),
(Some(_), None) => std::cmp::Ordering::Greater,
(None, Some(_)) => std::cmp::Ordering::Less,
(Some(a), Some(b)) => a.key[self.value_offset..].cmp(&(b.key[self.value_offset..])),
})
.map(|(index, _)| index)
.unwrap_or(0);
let best = self.next_rows[best_index].take();
self.next_rows[best_index] = self.iters.get_mut(best_index)?.next();
if self.next_rows.iter().all(|row| row.is_none()) {
self.done = true;
}
best
}
}
#[derive(Debug)]
pub struct DB {
db: rocksdb::DB,
@ -147,8 +82,25 @@ pub enum DBFlush {
impl DB {
pub fn open(path: &Path, config: &Config) -> DB {
debug!("opening DB at {:?}", path);
let mut db_opts = rocksdb::Options::default();
db_opts.create_if_missing(true);
db_opts.set_max_open_files(100_000); // TODO: make sure to `ulimit -n` this process correctly
db_opts.set_compaction_style(rocksdb::DBCompactionStyle::Level);
db_opts.set_compression_type(rocksdb::DBCompressionType::Lz4);
db_opts.set_target_file_size_base(1_073_741_824);
db_opts.set_write_buffer_size(256 << 20);
db_opts.set_disable_auto_compactions(true); // for initial bulk load
// db_opts.set_advise_random_on_open(???);
db_opts.set_compaction_readahead_size(1 << 20);
db_opts.increase_parallelism(2);
// let mut block_opts = rocksdb::BlockBasedOptions::default();
// block_opts.set_block_size(???);
let db = DB {
db: open_raw_db(path, OpenMode::ReadWrite),
db: rocksdb::DB::open(&db_opts, path).expect("failed to open RocksDB"),
};
db.verify_compatibility(config);
db
@ -166,11 +118,11 @@ impl DB {
self.db.set_options(&opts).unwrap();
}
pub fn raw_iterator(&self) -> rocksdb::DBRawIterator<'_> {
pub fn raw_iterator(&self) -> rocksdb::DBRawIterator {
self.db.raw_iterator()
}
pub fn iter_scan(&self, prefix: &[u8]) -> ScanIterator<'_> {
pub fn iter_scan(&self, prefix: &[u8]) -> ScanIterator {
ScanIterator {
prefix: prefix.to_vec(),
iter: self.db.prefix_iterator(prefix),
@ -178,7 +130,7 @@ impl DB {
}
}
pub fn iter_scan_from(&self, prefix: &[u8], start_at: &[u8]) -> ScanIterator<'_> {
pub fn iter_scan_from(&self, prefix: &[u8], start_at: &[u8]) -> ScanIterator {
let iter = self.db.iterator(rocksdb::IteratorMode::From(
start_at,
rocksdb::Direction::Forward,
@ -190,7 +142,7 @@ impl DB {
}
}
pub fn iter_scan_reverse(&self, prefix: &[u8], prefix_max: &[u8]) -> ReverseScanIterator<'_> {
pub fn iter_scan_reverse(&self, prefix: &[u8], prefix_max: &[u8]) -> ReverseScanIterator {
let mut iter = self.db.raw_iterator();
iter.seek_for_prev(prefix_max);
@ -201,25 +153,6 @@ impl DB {
}
}
pub fn iter_scan_group_reverse(
&self,
prefixes: impl Iterator<Item = (Vec<u8>, Vec<u8>)>,
value_offset: usize,
) -> ReverseScanGroupIterator<'_> {
let iters = prefixes
.map(|(prefix, prefix_max)| {
let mut iter = self.db.raw_iterator();
iter.seek_for_prev(prefix_max);
ReverseScanIterator {
prefix: prefix.to_vec(),
iter,
done: false,
}
})
.collect();
ReverseScanGroupIterator::new(iters, value_offset)
}
pub fn write(&self, mut rows: Vec<DBRow>, flush: DBFlush) {
debug!(
"writing {} rows to {:?}, flush={:?}",
@ -230,7 +163,10 @@ impl DB {
rows.sort_unstable_by(|a, b| a.key.cmp(&b.key));
let mut batch = rocksdb::WriteBatch::default();
for row in rows {
#[cfg(not(feature = "oldcpu"))]
batch.put(&row.key, &row.value);
#[cfg(feature = "oldcpu")]
batch.put(&row.key, &row.value).unwrap();
}
let do_flush = match flush {
DBFlush::Enable => true,
@ -242,15 +178,6 @@ impl DB {
self.db.write_opt(batch, &opts).unwrap();
}
pub fn delete(&self, keys: Vec<Vec<u8>>) {
debug!("deleting {} rows from {:?}", keys.len(), self.db);
for key in keys {
let _ = self.db.delete(key).inspect_err(|err| {
warn!("Error while deleting DB row: {err}");
});
}
}
pub fn flush(&self) {
self.db.flush().unwrap();
}
@ -270,7 +197,7 @@ impl DB {
}
fn verify_compatibility(&self, config: &Config) {
let mut compatibility_bytes = bincode_util::serialize_little(&DB_VERSION).unwrap();
let mut compatibility_bytes = bincode::serialize(&DB_VERSION).unwrap();
if config.light_mode {
// append a byte to indicate light_mode is enabled.
@ -289,42 +216,3 @@ impl DB {
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u8)]
pub enum OpenMode {
ReadOnly,
ReadWrite,
}
pub fn open_raw_db<T: rocksdb::ThreadMode>(
path: &Path,
read_mode: OpenMode,
) -> rocksdb::DBWithThreadMode<T> {
debug!("opening DB at {:?}", path);
let mut db_opts = rocksdb::Options::default();
db_opts.create_if_missing(true);
db_opts.set_max_open_files(100_000); // TODO: make sure to `ulimit -n` this process correctly
db_opts.set_compaction_style(rocksdb::DBCompactionStyle::Level);
db_opts.set_compression_type(rocksdb::DBCompressionType::None);
db_opts.set_target_file_size_base(1_073_741_824);
db_opts.set_write_buffer_size(256 << 20);
db_opts.set_disable_auto_compactions(true); // for initial bulk load
// db_opts.set_advise_random_on_open(???);
db_opts.set_compaction_readahead_size(1 << 20);
db_opts.increase_parallelism(2);
// let mut block_opts = rocksdb::BlockBasedOptions::default();
// block_opts.set_block_size(???);
match read_mode {
OpenMode::ReadOnly => {
rocksdb::DBWithThreadMode::<T>::open_for_read_only(&db_opts, path, false)
.expect("failed to open RocksDB (READ ONLY)")
}
OpenMode::ReadWrite => {
rocksdb::DBWithThreadMode::<T>::open(&db_opts, path).expect("failed to open RocksDB")
}
}
}

View File

@ -9,9 +9,10 @@ use std::collections::HashMap;
use std::fs;
use std::io::Cursor;
use std::path::PathBuf;
use std::sync::mpsc::Receiver;
use std::thread;
use crate::chain::{Block, BlockHash, BlockSizeCompat};
use crate::chain::{Block, BlockHash};
use crate::daemon::Daemon;
use crate::errors::*;
use crate::util::{spawn_thread, HeaderEntry, SyncChannel};
@ -42,70 +43,19 @@ pub struct BlockEntry {
type SizedBlock = (Block, u32);
pub struct SequentialFetcher<T> {
fetcher: Box<dyn FnOnce() -> Vec<Vec<T>>>,
}
impl<T> SequentialFetcher<T> {
fn from<F: FnOnce() -> Vec<Vec<T>> + 'static>(pre_func: F) -> Self {
SequentialFetcher {
fetcher: Box::new(pre_func),
}
}
pub fn map<FN>(self, mut func: FN)
where
FN: FnMut(Vec<T>),
{
for item in (self.fetcher)() {
func(item);
}
}
}
pub fn bitcoind_sequential_fetcher(
daemon: &Daemon,
new_headers: Vec<HeaderEntry>,
) -> Result<SequentialFetcher<BlockEntry>> {
let daemon = daemon.reconnect()?;
Ok(SequentialFetcher::from(move || {
new_headers
.chunks(100)
.map(|entries| {
let blockhashes: Vec<BlockHash> = entries.iter().map(|e| *e.hash()).collect();
let blocks = daemon
.getblocks(&blockhashes)
.expect("failed to get blocks from bitcoind");
assert_eq!(blocks.len(), entries.len());
let block_entries: Vec<BlockEntry> = blocks
.into_iter()
.zip(entries)
.map(|(block, entry)| BlockEntry {
entry: entry.clone(), // TODO: remove this clone()
size: block.get_block_size() as u32,
block,
})
.collect();
assert_eq!(block_entries.len(), entries.len());
block_entries
})
.collect()
}))
}
pub struct Fetcher<T> {
receiver: crossbeam_channel::Receiver<T>,
receiver: Receiver<T>,
thread: thread::JoinHandle<()>,
}
impl<T> Fetcher<T> {
fn from(receiver: crossbeam_channel::Receiver<T>, thread: thread::JoinHandle<()>) -> Self {
fn from(receiver: Receiver<T>, thread: thread::JoinHandle<()>) -> Self {
Fetcher { receiver, thread }
}
pub fn map<F>(self, mut func: F)
where
F: FnMut(T),
F: FnMut(T) -> (),
{
for item in self.receiver {
func(item);
@ -138,7 +88,7 @@ fn bitcoind_fetcher(
.zip(entries)
.map(|(block, entry)| BlockEntry {
entry: entry.clone(), // TODO: remove this clone()
size: block.get_block_size() as u32,
size: block.size() as u32,
block,
})
.collect();
@ -200,33 +150,14 @@ fn blkfiles_fetcher(
fn blkfiles_reader(blk_files: Vec<PathBuf>) -> Fetcher<Vec<u8>> {
let chan = SyncChannel::new(1);
let sender = chan.sender();
let xor_key = blk_files.first().and_then(|p| {
let xor_file = p
.parent()
.expect("blk.dat files must exist in a directory")
.join("xor.dat");
if xor_file.exists() {
Some(fs::read(xor_file).expect("xor.dat exists"))
} else {
None
}
});
Fetcher::from(
chan.into_receiver(),
spawn_thread("blkfiles_reader", move || {
for path in blk_files {
trace!("reading {:?}", path);
let mut blob = fs::read(&path)
let blob = fs::read(&path)
.unwrap_or_else(|e| panic!("failed to read {:?}: {:?}", path, e));
// If the xor.dat exists. Use it to decrypt the block files.
if let Some(xor_key) = &xor_key {
for (&key, byte) in xor_key.iter().cycle().zip(blob.iter_mut()) {
*byte ^= key;
}
}
sender
.send(blob)
.unwrap_or_else(|_| panic!("failed to send {:?} contents", path));
@ -287,10 +218,15 @@ fn parse_blocks(blob: Vec<u8>, magic: u32) -> Result<Vec<SizedBlock>> {
Err(_) => break, // EOF
}
slices.push((&blob[start as usize..end as usize], block_size));
cursor.set_position(end);
cursor.set_position(end as u64);
}
Ok(super::THREAD_POOL.install(|| {
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(0) // CPU-bound
.thread_name(|i| format!("parse-blocks-{}", i))
.build()
.unwrap();
Ok(pool.install(|| {
slices
.into_par_iter()
.map(|(slice, size)| (deserialize(slice).expect("failed to parse Block"), size))

View File

@ -1,4 +1,4 @@
use bounded_vec_deque::BoundedVecDeque;
use arraydeque::{ArrayDeque, Wrapping};
use itertools::Itertools;
#[cfg(not(feature = "liquid"))]
@ -6,13 +6,12 @@ use bitcoin::consensus::encode::serialize;
#[cfg(feature = "liquid")]
use elements::{encode::serialize, AssetId};
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::collections::{BTreeSet, HashMap, HashSet};
use std::iter::FromIterator;
use std::ops::Bound::{Excluded, Unbounded};
use std::sync::{Arc, RwLock};
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::chain::{deserialize, Network, OutPoint, Transaction, TxOut, Txid, TxidCompat};
use crate::chain::{deserialize, Network, OutPoint, Transaction, TxOut, Txid};
use crate::config::Config;
use crate::daemon::Daemon;
use crate::errors::*;
@ -27,14 +26,17 @@ use crate::util::{extract_tx_prevouts, full_hash, has_prevout, is_spendable, Byt
#[cfg(feature = "liquid")]
use crate::elements::asset;
const RECENT_TXS_SIZE: usize = 10;
const BACKLOG_STATS_TTL: u64 = 10;
pub struct Mempool {
chain: Arc<ChainQuery>,
config: Arc<Config>,
txstore: BTreeMap<Txid, Transaction>,
txstore: HashMap<Txid, Transaction>,
feeinfo: HashMap<Txid, TxFeeInfo>,
history: HashMap<FullHash, Vec<TxHistoryInfo>>, // ScriptHash -> {history_entries}
edges: HashMap<OutPoint, (Txid, u32)>, // OutPoint -> (spending_txid, spending_vin)
recent: BoundedVecDeque<TxOverview>, // The N most recent txs to enter the mempool
recent: ArrayDeque<[TxOverview; RECENT_TXS_SIZE], Wrapping>, // The N most recent txs to enter the mempool
backlog_stats: (BacklogStats, Instant),
// monitoring
@ -63,14 +65,15 @@ impl Mempool {
pub fn new(chain: Arc<ChainQuery>, metrics: &Metrics, config: Arc<Config>) -> Self {
Mempool {
chain,
txstore: BTreeMap::new(),
config,
txstore: HashMap::new(),
feeinfo: HashMap::new(),
history: HashMap::new(),
edges: HashMap::new(),
recent: BoundedVecDeque::new(config.mempool_recent_txs_size),
recent: ArrayDeque::new(),
backlog_stats: (
BacklogStats::default(),
Instant::now() - Duration::from_secs(config.mempool_backlog_stats_ttl),
Instant::now() - Duration::from_secs(BACKLOG_STATS_TTL),
),
latency: metrics.histogram_vec(
HistogramOpts::new("mempool_latency", "Mempool requests latency (in seconds)"),
@ -89,7 +92,6 @@ impl Mempool {
asset_history: HashMap::new(),
#[cfg(feature = "liquid")]
asset_issuance: HashMap::new(),
config,
}
}
@ -131,95 +133,24 @@ impl Mempool {
.any(|txin| self.txstore.contains_key(&txin.previous_output.txid))
}
pub fn history(
&self,
scripthash: &[u8],
last_seen_txid: Option<&Txid>,
limit: usize,
) -> Vec<Transaction> {
pub fn history(&self, scripthash: &[u8], limit: usize) -> Vec<Transaction> {
let _timer = self.latency.with_label_values(&["history"]).start_timer();
self.history
.get(scripthash)
.map_or_else(std::vec::Vec::new, |entries| {
self._history(entries, last_seen_txid, limit)
})
.map_or_else(|| vec![], |entries| self._history(entries, limit))
}
pub fn history_txids_iter<'a>(&'a self, scripthash: &[u8]) -> impl Iterator<Item = Txid> + 'a {
self.history
.get(scripthash)
.into_iter()
.flat_map(|v| v.iter().map(|e| e.get_txid()).unique())
}
fn _history(
&self,
entries: &[TxHistoryInfo],
last_seen_txid: Option<&Txid>,
limit: usize,
) -> Vec<Transaction> {
fn _history(&self, entries: &[TxHistoryInfo], limit: usize) -> Vec<Transaction> {
entries
.iter()
.map(|e| e.get_txid())
.unique()
// TODO seek directly to last seen tx without reading earlier rows
.skip_while(|txid| {
// skip until we reach the last_seen_txid
last_seen_txid.is_some_and(|last_seen_txid| last_seen_txid != txid)
})
.skip(match last_seen_txid {
Some(_) => 1, // skip the last_seen_txid itself
None => 0,
})
.take(limit)
.map(|txid| self.txstore.get(&txid).expect("missing mempool tx"))
.cloned()
.collect()
}
pub fn history_group(
&self,
scripthashes: &[[u8; 32]],
last_seen_txid: Option<&Txid>,
limit: usize,
) -> Vec<Transaction> {
let _timer = self
.latency
.with_label_values(&["history_group"])
.start_timer();
scripthashes
.iter()
.filter_map(|scripthash| self.history.get(&scripthash[..]))
.flat_map(|entries| entries.iter())
.map(|e| e.get_txid())
.unique()
// TODO seek directly to last seen tx without reading earlier rows
.skip_while(|txid| {
// skip until we reach the last_seen_txid
last_seen_txid.is_some_and(|last_seen_txid| last_seen_txid != txid)
})
.skip(match last_seen_txid {
Some(_) => 1, // skip the last_seen_txid itself
None => 0,
})
.take(limit)
.map(|txid| self.txstore.get(&txid).expect("missing mempool tx"))
.cloned()
.collect()
}
pub fn history_txids_iter_group<'a>(
&'a self,
scripthashes: &'a [[u8; 32]],
) -> impl Iterator<Item = Txid> + 'a {
scripthashes
.iter()
.filter_map(move |scripthash| self.history.get(&scripthash[..]))
.flat_map(|entries| entries.iter())
.map(|entry| entry.get_txid())
.unique()
}
pub fn history_txids(&self, scripthash: &[u8], limit: usize) -> Vec<Txid> {
let _timer = self
.latency
@ -249,11 +180,13 @@ impl Mempool {
TxHistoryInfo::Funding(info) => {
// Liquid requires some additional information from the txo that's not available in the TxHistoryInfo index.
#[cfg(feature = "liquid")]
let txo = self.lookup_txo(&entry.get_funded_outpoint())?;
let txo = self
.lookup_txo(&entry.get_funded_outpoint())
.expect("missing txo");
Some(Utxo {
txid: deserialize(&info.txid).expect("invalid txid"),
vout: info.vout,
vout: info.vout as u32,
value: info.value,
confirmed: None,
@ -331,49 +264,6 @@ impl Mempool {
self.txstore.keys().collect()
}
// Get n txids after the given txid in the mempool
pub fn txids_page(&self, n: usize, start: Option<Txid>) -> Vec<&Txid> {
let _timer = self
.latency
.with_label_values(&["txids_page"])
.start_timer();
let start_bound = match start {
Some(txid) => Excluded(txid),
None => Unbounded,
};
self.txstore
.range((start_bound, Unbounded))
.take(n)
.map(|(k, _v)| k)
.collect()
}
// Get all txs in the mempool
pub fn txs(&self) -> Vec<Transaction> {
let _timer = self.latency.with_label_values(&["txs"]).start_timer();
self.txstore.values().cloned().collect()
}
// Get n txs after the given txid in the mempool
pub fn txs_page(&self, n: usize, start: Option<Txid>) -> Vec<Transaction> {
let _timer = self.latency.with_label_values(&["txs_page"]).start_timer();
let mut page = Vec::with_capacity(n);
let start_bound = match start {
Some(txid) => Excluded(txid),
None => Unbounded,
};
self.txstore
.range((start_bound, Unbounded))
.take(n)
.for_each(|(_, value)| {
page.push(value.clone());
});
page
}
// Get an overview of the most recent transactions
pub fn recent_txs_overview(&self) -> Vec<&TxOverview> {
// We don't bother ever deleting elements from the recent list.
@ -386,150 +276,91 @@ impl Mempool {
&self.backlog_stats.0
}
pub fn unique_txids(&self) -> HashSet<Txid> {
HashSet::from_iter(self.txstore.keys().cloned())
}
pub fn update(mempool: &RwLock<Mempool>, daemon: &Daemon) -> Result<()> {
// 1. Start the metrics timer and get the current mempool txids
// [LOCK] Takes read lock for whole scope.
let (_timer, old_txids) = {
let mempool = mempool.read().unwrap();
(
mempool.latency.with_label_values(&["update"]).start_timer(),
mempool.unique_txids(),
)
};
// 2. Get all the mempool txids from the RPC.
// [LOCK] No lock taken. Wait for RPC request. Get lists of remove/add txes.
let all_txids = daemon
pub fn update(&mut self, daemon: &Daemon) -> Result<()> {
let _timer = self.latency.with_label_values(&["update"]).start_timer();
let new_txids = daemon
.getmempooltxids()
.chain_err(|| "failed to update mempool from daemon")?;
let txids_to_remove: HashSet<&Txid> = old_txids.difference(&all_txids).collect();
let txids_to_add: Vec<&Txid> = all_txids.difference(&old_txids).collect();
let old_txids = HashSet::from_iter(self.txstore.keys().cloned());
let to_remove: HashSet<&Txid> = old_txids.difference(&new_txids).collect();
// 3. Remove missing transactions. Even if we are unable to download new transactions from
// the daemon, we still want to remove the transactions that are no longer in the mempool.
// [LOCK] Write lock is released at the end of the call to remove().
mempool.write().unwrap().remove(txids_to_remove);
// 4. Download the new transactions from the daemon's mempool
// [LOCK] No lock taken, waiting for RPC response.
let txs_to_add = daemon
.gettransactions(&txids_to_add)
.chain_err(|| format!("failed to get {} transactions", txids_to_add.len()))?;
// 4. Update local mempool to match daemon's state
// [LOCK] Takes Write lock for whole scope.
{
let mut mempool = mempool.write().unwrap();
// Add new transactions
if txs_to_add.len() > mempool.add(txs_to_add) {
debug!("Mempool update added less transactions than expected");
// Download and add new transactions from bitcoind's mempool
let txids: Vec<&Txid> = new_txids.difference(&old_txids).collect();
let to_add = match daemon.gettransactions(&txids) {
Ok(txs) => txs,
Err(err) => {
warn!("failed to get {} transactions: {}", txids.len(), err); // e.g. new block or RBF
return Ok(()); // keep the mempool until next update()
}
};
// Add new transactions
self.add(to_add)?;
// Remove missing transactions
self.remove(to_remove);
mempool
.count
.with_label_values(&["txs"])
.set(mempool.txstore.len() as f64);
self.count
.with_label_values(&["txs"])
.set(self.txstore.len() as f64);
// Update cached backlog stats (if expired)
if mempool.backlog_stats.1.elapsed()
> Duration::from_secs(mempool.config.mempool_backlog_stats_ttl)
{
let _timer = mempool
.latency
.with_label_values(&["update_backlog_stats"])
.start_timer();
mempool.backlog_stats = (BacklogStats::new(&mempool.feeinfo), Instant::now());
}
Ok(())
// Update cached backlog stats (if expired)
if self.backlog_stats.1.elapsed() > Duration::from_secs(BACKLOG_STATS_TTL) {
let _timer = self
.latency
.with_label_values(&["update_backlog_stats"])
.start_timer();
self.backlog_stats = (BacklogStats::new(&self.feeinfo), Instant::now());
}
}
pub fn add_by_txid(&mut self, daemon: &Daemon, txid: &Txid) -> Result<()> {
if !self.txstore.contains_key(txid) {
if let Ok(tx) = daemon.getmempooltx(txid) {
if self.add(vec![tx]) == 0 {
return Err(format!(
"Unable to add {txid} to mempool likely due to missing parents."
)
.into());
}
}
}
Ok(())
}
/// Add transactions to the mempool.
///
/// The return value is the number of transactions processed.
#[must_use = "Must deal with [[input vec's length]] > [[result]]."]
fn add(&mut self, txs: Vec<Transaction>) -> usize {
pub fn add_by_txid(&mut self, daemon: &Daemon, txid: &Txid) -> Result<()> {
if self.txstore.get(txid).is_none() {
if let Ok(tx) = daemon.getmempooltx(&txid) {
self.add(vec![tx])?;
}
}
Ok(())
}
fn add(&mut self, txs: Vec<Transaction>) -> Result<()> {
self.delta
.with_label_values(&["add"])
.observe(txs.len() as f64);
let _timer = self.latency.with_label_values(&["add"]).start_timer();
let txlen = txs.len();
if txlen == 0 {
return 0;
}
debug!("Adding {} transactions to Mempool", txlen);
let mut txids = Vec::with_capacity(txs.len());
let mut txids = vec![];
// Phase 1: add to txstore
for tx in txs {
let txid = tx.get_txid();
// Only push if it doesn't already exist.
// This is important now that update doesn't lock during
// the entire function body.
if self.txstore.insert(txid, tx).is_none() {
txids.push(txid);
}
let txid = tx.txid();
txids.push(txid);
self.txstore.insert(txid, tx);
}
// Phase 2: index history and spend edges (some txos can be missing)
let txos = self.lookup_txos(&self.get_prevouts(&txids));
// Count how many transactions were actually processed.
let mut processed_count = 0;
// Phase 3: Iterate over the transactions and do the following:
// 1. Find all of the TxOuts of each input parent using `txos`
// 2. If any parent wasn't found, skip parsing this transaction
// 3. Insert TxFeeInfo into info.
// 4. Push TxOverview into recent tx queue.
// 5. Create the Spend and Fund TxHistory structs for inputs + outputs
// 6. Insert all TxHistory into history.
// 7. Insert the tx edges into edges (HashMap of (Outpoint, (Txid, vin)))
// 8. (Liquid only) Parse assets of tx.
// Phase 2: index history and spend edges (can fail if some txos cannot be found)
let txos = match self.lookup_txos(&self.get_prevouts(&txids)) {
Ok(txos) => txos,
Err(err) => {
warn!("lookup txouts failed: {}", err);
// TODO: should we remove txids from txstore?
return Ok(());
}
};
for txid in txids {
let tx = self.txstore.get(&txid).expect("missing tx from txstore");
let prevouts = match extract_tx_prevouts(tx, &txos) {
Ok(v) => v,
Err(e) => {
warn!("Skipping tx {txid} missing parent error: {e}");
continue;
}
};
let tx = self.txstore.get(&txid).expect("missing mempool tx");
let txid_bytes = full_hash(&txid[..]);
let prevouts = extract_tx_prevouts(&tx, &txos, false)?;
// Get feeinfo for caching and recent tx overview
let feeinfo = TxFeeInfo::new(tx, &prevouts, self.config.network_type);
let feeinfo = TxFeeInfo::new(&tx, &prevouts, self.config.network_type);
// recent is an BoundedVecDeque that automatically evicts the oldest elements
// recent is an ArrayDeque that automatically evicts the oldest elements
self.recent.push_front(TxOverview {
txid,
fee: feeinfo.fee,
vsize: feeinfo.vsize,
#[cfg(not(feature = "liquid"))]
value: prevouts
.values()
.map(|prevout| prevout.value.to_sat())
.sum(),
value: prevouts.values().map(|prevout| prevout.value).sum(),
});
self.feeinfo.insert(txid, feeinfo);
@ -537,18 +368,14 @@ impl Mempool {
// An iterator over (ScriptHash, TxHistoryInfo)
let spending = prevouts.into_iter().map(|(input_index, prevout)| {
let txi = tx.input.get(input_index as usize).unwrap();
#[cfg(not(feature = "liquid"))]
let value = prevout.value.to_sat();
#[cfg(feature = "liquid")]
let value = prevout.value;
(
compute_script_hash(&prevout.script_pubkey),
TxHistoryInfo::Spending(SpendingInfo {
txid: txid_bytes,
vin: input_index,
vin: input_index as u16,
prev_txid: full_hash(&txi.previous_output.txid[..]),
prev_vout: txi.previous_output.vout,
value,
prev_vout: txi.previous_output.vout as u16,
value: prevout.value,
}),
)
});
@ -562,23 +389,22 @@ impl Mempool {
.enumerate()
.filter(|(_, txo)| is_spendable(txo) || config.index_unspendables)
.map(|(index, txo)| {
#[cfg(not(feature = "liquid"))]
let value = txo.value.to_sat();
#[cfg(feature = "liquid")]
let value = txo.value;
(
compute_script_hash(&txo.script_pubkey),
TxHistoryInfo::Funding(FundingInfo {
txid: txid_bytes,
vout: index as u32,
value,
vout: index as u16,
value: txo.value,
}),
)
});
// Index funding/spending history entries and spend edges
for (scripthash, entry) in funding.chain(spending) {
self.history.entry(scripthash).or_default().push(entry);
self.history
.entry(scripthash)
.or_insert_with(Vec::new)
.push(entry);
}
for (i, txi) in tx.input.iter().enumerate() {
self.edges.insert(txi.previous_output, (txid, i as u32));
@ -587,32 +413,24 @@ impl Mempool {
// Index issued assets & native asset pegins/pegouts/burns
#[cfg(feature = "liquid")]
asset::index_mempool_tx_assets(
tx,
&tx,
self.config.network_type,
self.config.parent_network,
&mut self.asset_history,
&mut self.asset_issuance,
);
processed_count += 1;
}
processed_count
Ok(())
}
/// Returns None if the lookup fails (mempool transaction RBF-ed etc.)
pub fn lookup_txo(&self, outpoint: &OutPoint) -> Option<TxOut> {
pub fn lookup_txo(&self, outpoint: &OutPoint) -> Result<TxOut> {
let mut outpoints = BTreeSet::new();
outpoints.insert(*outpoint);
// This can possibly be None now
self.lookup_txos(&outpoints).remove(outpoint)
Ok(self.lookup_txos(&outpoints)?.remove(outpoint).unwrap())
}
/// For a given set of OutPoints, return a HashMap<OutPoint, TxOut>
///
/// Not all OutPoints from mempool transactions are guaranteed to be there.
/// Ensure you deal with the None case in your logic.
pub fn lookup_txos(&self, outpoints: &BTreeSet<OutPoint>) -> HashMap<OutPoint, TxOut> {
pub fn lookup_txos(&self, outpoints: &BTreeSet<OutPoint>) -> Result<HashMap<OutPoint, TxOut>> {
let _timer = self
.latency
.with_label_values(&["lookup_txos"])
@ -623,21 +441,18 @@ impl Mempool {
let mempool_txos = outpoints
.iter()
.filter(|outpoint| !confirmed_txos.contains_key(outpoint))
.flat_map(|outpoint| {
.map(|outpoint| {
self.txstore
.get(&outpoint.txid)
.and_then(|tx| tx.output.get(outpoint.vout as usize).cloned())
.map(|txout| (*outpoint, txout))
.or_else(|| {
warn!("missing outpoint {:?}", outpoint);
None
})
.chain_err(|| format!("missing outpoint {:?}", outpoint))
})
.collect::<HashMap<OutPoint, TxOut>>();
.collect::<Result<HashMap<OutPoint, TxOut>>>()?;
let mut txos = confirmed_txos;
txos.extend(mempool_txos);
txos
Ok(txos)
}
fn get_prevouts(&self, txids: &[Txid]) -> BTreeSet<OutPoint> {
@ -700,9 +515,7 @@ impl Mempool {
.start_timer();
self.asset_history
.get(asset_id)
.map_or_else(std::vec::Vec::new, |entries| {
self._history(entries, None, limit)
})
.map_or_else(|| vec![], |entries| self._history(entries, limit))
}
}

View File

@ -5,16 +5,6 @@ pub mod precache;
mod query;
pub mod schema;
use std::sync::LazyLock;
pub(crate) static THREAD_POOL: LazyLock<rayon::ThreadPool> = LazyLock::new(|| {
rayon::ThreadPoolBuilder::new()
.num_threads(0) // 0 = use number of logical CPUs
.thread_name(|i| format!("electrs-worker-{}", i))
.build()
.expect("failed to create global rayon thread pool")
});
pub use self::db::{DBRow, DB};
pub use self::fetch::{BlockEntry, FetchFrom};
pub use self::mempool::Mempool;

View File

@ -1,59 +1,38 @@
use crate::chain::address::Address;
use crate::errors::*;
use crate::new_index::ChainQuery;
use crate::util::{full_hash, FullHash};
use crypto::digest::Digest;
use crypto::sha2::Sha256;
use rayon::prelude::*;
use hex;
use std::fs::File;
use std::io;
use std::io::prelude::*;
use std::sync::{atomic::AtomicUsize, Arc};
use std::time::Instant;
use std::str::FromStr;
pub fn precache(chain: Arc<ChainQuery>, scripthashes: Vec<FullHash>, threads: usize) {
pub fn precache(chain: &ChainQuery, scripthashes: Vec<FullHash>) {
let total = scripthashes.len();
info!(
"Pre-caching stats and utxo set on {} threads for {} scripthashes",
threads, total
);
info!("Pre-caching stats and utxo set for {} scripthashes", total);
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.num_threads(16)
.thread_name(|i| format!("precache-{}", i))
.build()
.unwrap();
let now = Instant::now();
let counter = AtomicUsize::new(0);
std::thread::spawn(move || {
pool.install(|| {
scripthashes
.par_iter()
.for_each(|scripthash| {
// First, cache
chain.stats(&scripthash[..], crate::new_index::db::DBFlush::Disable);
let _ = chain.utxo(&scripthash[..], usize::MAX, crate::new_index::db::DBFlush::Disable);
// Then, increment the counter
let pre_increment = counter.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
let post_increment_counter = pre_increment + 1;
// Then, log
if post_increment_counter % 500 == 0 {
let now_millis = now.elapsed().as_millis();
info!("{post_increment_counter}/{total} Processed in {now_millis} ms running pre-cache for scripthash");
}
// Every 10k counts, flush the DB to disk
if post_increment_counter % 10000 == 0 {
info!("Flushing cache_db... {post_increment_counter}");
chain.store().cache_db().flush();
info!("Done Flushing cache_db!!! {post_increment_counter}");
}
})
});
// After everything is done, flush the cache
chain.store().cache_db().flush();
pool.install(|| {
scripthashes
.par_iter()
.enumerate()
.for_each(|(i, scripthash)| {
if i % 5 == 0 {
info!("running pre-cache for scripthash {}/{}", i + 1, total);
}
chain.stats(&scripthash[..]);
//chain.utxo(&scripthash[..]);
})
});
}
@ -64,7 +43,34 @@ pub fn scripthashes_from_file(path: String) -> Result<Vec<FullHash>> {
.lines()
.map(|line| {
let line = line.chain_err(|| "cannot read scripthash line")?;
Ok(full_hash(&hex::decode(line).chain_err(|| "invalid hex")?))
let cols: Vec<&str> = line.split(',').collect();
to_scripthash(cols[0], cols[1])
})
.collect()
}
fn to_scripthash(script_type: &str, script_str: &str) -> Result<FullHash> {
match script_type {
"address" => address_to_scripthash(script_str),
"scripthash" => Ok(full_hash(
&hex::decode(script_str).chain_err(|| "invalid hex")?,
)),
"scriptpubkey" => Ok(compute_script_hash(
&hex::decode(script_str).chain_err(|| "invalid hex")?,
)),
_ => bail!("Invalid script type".to_string()),
}
}
fn address_to_scripthash(addr: &str) -> Result<FullHash> {
let addr = Address::from_str(addr).chain_err(|| "invalid address")?;
Ok(compute_script_hash(&addr.script_pubkey().as_bytes()))
}
pub fn compute_script_hash(data: &[u8]) -> FullHash {
let mut hash = FullHash::default();
let mut sha2 = Sha256::new();
sha2.input(data);
sha2.result(&mut hash);
hash
}

View File

@ -4,17 +4,17 @@ use std::collections::{BTreeSet, HashMap};
use std::sync::{Arc, RwLock, RwLockReadGuard};
use std::time::{Duration, Instant};
use crate::chain::{Network, OutPoint, Transaction, TxOut, Txid, TxidCompat};
use crate::chain::{Network, OutPoint, Transaction, TxOut, Txid};
use crate::config::Config;
use crate::daemon::{Daemon, MempoolAcceptResult, SubmitPackageResult};
use crate::daemon::Daemon;
use crate::errors::*;
use crate::new_index::{ChainQuery, Mempool, ScriptStats, SpendingInput, Utxo};
use crate::util::{is_spendable, BlockId, Bytes, TransactionStatus};
#[cfg(feature = "liquid")]
use crate::{
chain::{asset::AssetRegistryLock, AssetId},
elements::{lookup_asset, AssetMeta, AssetRegistry, AssetSorting, LiquidAsset},
chain::AssetId,
elements::{lookup_asset, AssetRegistry, AssetSorting, LiquidAsset},
};
const FEE_ESTIMATES_TTL: u64 = 60; // seconds
@ -65,51 +65,21 @@ impl Query {
self.config.network_type
}
pub fn mempool(&self) -> RwLockReadGuard<'_, Mempool> {
pub fn mempool(&self) -> RwLockReadGuard<Mempool> {
self.mempool.read().unwrap()
}
pub fn broadcast_raw(&self, txhex: &str) -> Result<Txid> {
let txid = self.daemon.broadcast_raw(txhex)?;
// The important part is whether we succeeded in broadcasting.
// Ignore errors in adding to the cache and show an internal warning.
if let Err(e) = self
.mempool
self.mempool
.write()
.unwrap()
.add_by_txid(&self.daemon, &txid)
{
warn!(
"broadcast_raw of {txid} succeeded to broadcast \
but failed to add to mempool-electrs Mempool cache: {e}"
);
}
.add_by_txid(&self.daemon, &txid)?;
Ok(txid)
}
pub fn test_mempool_accept(
&self,
txhex: Vec<String>,
maxfeerate: Option<f64>,
) -> Result<Vec<MempoolAcceptResult>> {
self.daemon.test_mempool_accept(txhex, maxfeerate)
}
pub fn submit_package(
&self,
txhex: Vec<String>,
maxfeerate: Option<f64>,
maxburnamount: Option<f64>,
) -> Result<SubmitPackageResult> {
self.daemon.submit_package(txhex, maxfeerate, maxburnamount)
}
pub fn utxo(&self, scripthash: &[u8]) -> Result<Vec<Utxo>> {
let mut utxos = self.chain.utxo(
scripthash,
self.config.utxos_limit,
super::db::DBFlush::Enable,
)?;
let mut utxos = self.chain.utxo(scripthash, self.config.utxos_limit)?;
let mempool = self.mempool();
utxos.retain(|utxo| !mempool.has_spend(&OutPoint::from(utxo)));
utxos.extend(mempool.utxo(scripthash));
@ -132,7 +102,7 @@ impl Query {
pub fn stats(&self, scripthash: &[u8]) -> (ScriptStats, ScriptStats) {
(
self.chain.stats(scripthash, super::db::DBFlush::Enable),
self.chain.stats(scripthash),
self.mempool().stats(scripthash),
)
}
@ -148,8 +118,7 @@ impl Query {
.or_else(|| self.mempool().lookup_raw_txn(txid))
}
/// Not all OutPoints from mempool transactions are guaranteed to be included in the result
pub fn lookup_txos(&self, outpoints: &BTreeSet<OutPoint>) -> HashMap<OutPoint, TxOut> {
pub fn lookup_txos(&self, outpoints: &BTreeSet<OutPoint>) -> Result<HashMap<OutPoint, TxOut>> {
// the mempool lookup_txos() internally looks up confirmed txos as well
self.mempool().lookup_txos(outpoints)
}
@ -161,7 +130,7 @@ impl Query {
}
pub fn lookup_tx_spends(&self, tx: Transaction) -> Vec<Option<SpendingInput>> {
let txid = tx.get_txid();
let txid = tx.txid();
tx.output
.par_iter()
@ -263,21 +232,7 @@ impl Query {
#[cfg(feature = "liquid")]
pub fn lookup_asset(&self, asset_id: &AssetId) -> Result<Option<LiquidAsset>> {
lookup_asset(
self,
self.asset_db.as_ref().map(AssetRegistryLock::RwLock),
asset_id,
None,
)
}
#[cfg(feature = "liquid")]
pub fn lookup_registry_asset(&self, asset_id: &AssetId) -> Result<Option<AssetMeta>> {
let asset_db = self
.asset_db
.as_ref()
.chain_err(|| "asset registry unavailable")?;
Ok(asset_db.read().unwrap().get(asset_id).cloned())
lookup_asset(&self, self.asset_db.as_ref(), asset_id, None)
}
#[cfg(feature = "liquid")]
@ -296,38 +251,10 @@ impl Query {
let results = results
.into_iter()
.map(|(asset_id, metadata)| {
lookup_asset(
self,
Some(AssetRegistryLock::RwLockReadGuard(&asset_db)),
asset_id,
Some(metadata),
)?
.chain_err(|| "missing registered asset")
Ok(lookup_asset(&self, None, asset_id, Some(metadata))?
.chain_err(|| "missing registered asset")?)
})
.collect::<Result<Vec<_>>>()?;
Ok((total_num, results))
}
#[cfg(feature = "liquid")]
pub fn search_registry_assets<T, F>(
&self,
search: &str,
limit: usize,
mut map: F,
) -> Result<Vec<T>>
where
F: FnMut(&AssetId, &AssetMeta) -> T,
{
let asset_db = self
.asset_db
.as_ref()
.chain_err(|| "asset registry unavailable")?;
Ok(asset_db
.read()
.unwrap()
.search(search, limit)
.into_iter()
.map(|(asset_id, metadata)| map(asset_id, metadata))
.collect())
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,6 @@
use crossbeam_channel as channel;
use crossbeam_channel::RecvTimeoutError;
use std::thread;
use std::time::{Duration, Instant};
use signal_hook::consts::{SIGINT, SIGTERM, SIGUSR1};
@ -15,7 +16,7 @@ fn notify(signals: &[i32]) -> channel::Receiver<i32> {
let (s, r) = channel::bounded(1);
let mut signals =
signal_hook::iterator::Signals::new(signals).expect("failed to register signal hook");
crate::util::spawn_thread("signal-notifier", move || {
thread::spawn(move || {
for signal in signals.forever() {
s.send(signal)
.unwrap_or_else(|_| panic!("failed to send signal {}", signal));

View File

@ -1,112 +0,0 @@
/*
The tests below show us the following defaults for each method of using bincode.
1. Using bincode::[de]serialize() directly: "function"
2. Using bincode::config().[de]serialize(): "Config" (deprecated)
3. Using bincode::options().[de]serialize(): "Options" (currently recommended for v1.3.3)
```
+----------+--------+------------+----------------+------------+
| | Endian | Int Length | Allow Trailing | Byte Limit |
+----------+--------+------------+----------------+------------+
| function | little | fixed | allow | unlimited |
| Config | little | fixed | allow | unlimited |
| Options | little | variable * | reject * | unlimited |
+----------+--------+------------+----------------+------------+
```
Thus we only need to change the int length from variable to fixed,
and allow trailing to allow in order to match the previous behavior.
(note: TxHistory was using Big Endian by explicitly setting it to big.)
*/
use bincode_do_not_use_directly as bincode;
#[test]
fn bincode_settings() {
let value = TestStruct::new();
let mut large = [0_u8; 4096];
let decoded = [
8_u8, 7, 6, 5, 4, 3, 2, 1, 1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 8, 7, 6, 5, 4, 3, 2, 1, 1,
2, 3, 4, 5, 6, 7, 8, 12, 0, 0, 0, 0, 0, 0, 0, 72, 101, 108, 108, 111, 32, 87, 111, 114,
108, 100, 33,
];
large[0..56].copy_from_slice(&decoded);
// Using functions: Little endian, Fixint, Allow trailing, Unlimited
assert_eq!(bincode::serialize(&value).unwrap(), &decoded);
assert_eq!(bincode::deserialize::<TestStruct>(&large).unwrap(), value);
// Using Config (deprecated)
// Little endian, fixint, Allow trailing, Unlimited
#[allow(deprecated)]
{
assert_eq!(bincode::config().serialize(&value).unwrap(), &decoded);
assert_eq!(
bincode::config().deserialize::<TestStruct>(&large).unwrap(),
value
);
}
// Using Options
// Little endian, VARINT (different), Reject trailing (different), unlimited
use bincode::Options;
assert_eq!(
bincode::options()
.with_fixint_encoding()
.allow_trailing_bytes()
.serialize(&value)
.unwrap(),
&decoded
);
assert_eq!(
bincode::options()
.with_fixint_encoding()
.allow_trailing_bytes()
.deserialize::<TestStruct>(&large)
.unwrap(),
value
);
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
struct TestStruct {
a: u64,
b: [u8; 8],
c: TestData,
d: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
enum TestData {
Foo(FooStruct),
Bar(BarStruct),
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
struct FooStruct {
a: u64,
b: [u8; 8],
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
struct BarStruct {
a: u64,
b: [u8; 8],
}
impl TestStruct {
fn new() -> Self {
Self {
a: 0x0102030405060708,
b: [1, 2, 3, 4, 5, 6, 7, 8],
c: TestData::Foo(FooStruct {
a: 0x0102030405060708,
b: [1, 2, 3, 4, 5, 6, 7, 8],
}),
d: String::from("Hello World!"),
}
}
}

View File

@ -1,70 +0,0 @@
//! This module creates two sets of serialize and deserialize for bincode.
//! They explicitly spell out the bincode settings so that switching to
//! new versions in the future is less error prone.
//!
//! This is a list of all the row types and their settings for bincode.
//! +--------------+--------+------------+----------------+------------+
//! | | Endian | Int Length | Allow Trailing | Byte Limit |
//! +--------------+--------+------------+----------------+------------+
//! | TxHistoryRow | big | fixed | allow | unlimited |
//! | All others | little | fixed | allow | unlimited |
//! +--------------+--------+------------+----------------+------------+
// We only want people to use bincode_util
use bincode::Options;
use bincode_do_not_use_directly as bincode;
pub fn serialize_big<T>(value: &T) -> Result<Vec<u8>, bincode::Error>
where
T: ?Sized + serde::Serialize,
{
big_endian().serialize(value)
}
pub fn deserialize_big<'a, T>(bytes: &'a [u8]) -> Result<T, bincode::Error>
where
T: serde::Deserialize<'a>,
{
big_endian().deserialize(bytes)
}
pub fn serialize_little<T>(value: &T) -> Result<Vec<u8>, bincode::Error>
where
T: ?Sized + serde::Serialize,
{
little_endian().serialize(value)
}
pub fn deserialize_little<'a, T>(bytes: &'a [u8]) -> Result<T, bincode::Error>
where
T: serde::Deserialize<'a>,
{
little_endian().deserialize(bytes)
}
/// This is the default settings for Options,
/// but all explicitly spelled out, except for endianness.
/// The following functions will add endianness.
#[inline]
fn options() -> impl Options {
bincode::options()
.with_fixint_encoding()
.with_no_limit()
.allow_trailing_bytes()
}
/// Adding the endian flag for big endian
#[inline]
fn big_endian() -> impl Options {
options().with_big_endian()
}
/// Adding the endian flag for little endian
#[inline]
fn little_endian() -> impl Options {
options().with_little_endian()
}
#[cfg(test)]
#[path = "./bincode_tests.rs"]
mod bincode_tests;

View File

@ -1,5 +1,3 @@
use bitcoin::hashes::Hash;
use crate::chain::{BlockHash, BlockHeader};
use crate::errors::*;
use crate::new_index::BlockEntry;
@ -75,7 +73,7 @@ impl HeaderList {
HeaderList {
headers: vec![],
heights: HashMap::new(),
tip: BlockHash::all_zeros(),
tip: BlockHash::default(),
}
}
@ -91,7 +89,7 @@ impl HeaderList {
let mut blockhash = tip_hash;
let mut headers_chain: Vec<BlockHeader> = vec![];
let null_hash = BlockHash::all_zeros();
let null_hash = BlockHash::default();
while blockhash != null_hash {
let header = headers_map.remove(&blockhash).unwrap_or_else(|| {
@ -138,7 +136,7 @@ impl HeaderList {
Some(h) => h.header.prev_blockhash,
None => return vec![], // hashed_headers is empty
};
let null_hash = BlockHash::all_zeros();
let null_hash = BlockHash::default();
let new_height: usize = if prev_blockhash == null_hash {
0
} else {
@ -148,7 +146,7 @@ impl HeaderList {
+ 1
};
(new_height..)
.zip(hashed_headers)
.zip(hashed_headers.into_iter())
.map(|(height, hashed_header)| HeaderEntry {
height,
hash: hashed_header.blockhash,
@ -157,12 +155,7 @@ impl HeaderList {
.collect()
}
/// Returns any rolled back blocks in order from old tip first and first block in the fork is last
/// It also returns the blockhash of the post-rollback tip.
pub fn apply(
&mut self,
new_headers: Vec<HeaderEntry>,
) -> (Vec<HeaderEntry>, Option<BlockHash>) {
pub fn apply(&mut self, new_headers: Vec<HeaderEntry>) {
// new_headers[i] -> new_headers[i - 1] (i.e. new_headers.last() is the tip)
for i in 1..new_headers.len() {
assert_eq!(new_headers[i - 1].height() + 1, new_headers[i].height());
@ -177,27 +170,19 @@ impl HeaderList {
let expected_prev_blockhash = if height > 0 {
*self.headers[height - 1].hash()
} else {
BlockHash::all_zeros()
BlockHash::default()
};
assert_eq!(entry.header().prev_blockhash, expected_prev_blockhash);
height
}
None => return (vec![], None),
None => return,
};
debug!(
"applying {} new headers from height {}",
new_headers.len(),
new_height
);
let mut removed = self.headers.split_off(new_height); // keep [0..new_height) entries
// If we reorged, we should return the last blockhash before adding the new chain's blockheaders.
let reorged_tip = if !removed.is_empty() {
self.headers.last().map(|be| be.hash()).cloned()
} else {
None
};
let _removed = self.headers.split_off(new_height); // keep [0..new_height) entries
for new_header in new_headers {
let height = new_header.height();
assert_eq!(height, self.headers.len());
@ -205,8 +190,6 @@ impl HeaderList {
self.headers.push(new_header);
self.heights.insert(self.tip, height);
}
removed.reverse();
(removed, reorged_tip)
}
pub fn header_by_blockhash(&self, blockhash: &BlockHash) -> Option<&HeaderEntry> {
@ -220,8 +203,9 @@ impl HeaderList {
}
pub fn header_by_height(&self, height: usize) -> Option<&HeaderEntry> {
self.headers.get(height).inspect(|entry| {
self.headers.get(height).map(|entry| {
assert_eq!(entry.height(), height);
entry
})
}
@ -232,10 +216,7 @@ impl HeaderList {
pub fn tip(&self) -> &BlockHash {
assert_eq!(
self.tip,
self.headers
.last()
.map(|h| *h.hash())
.unwrap_or(BlockHash::all_zeros())
self.headers.last().map(|h| *h.hash()).unwrap_or_default()
);
&self.tip
}
@ -248,7 +229,7 @@ impl HeaderList {
self.headers.is_empty()
}
pub fn iter(&self) -> slice::Iter<'_, HeaderEntry> {
pub fn iter(&self) -> slice::Iter<HeaderEntry> {
self.headers.iter()
}
@ -257,7 +238,7 @@ impl HeaderList {
// Use the timestamp as the mtp of the genesis block.
// Matches bitcoind's behaviour: bitcoin-cli getblock `bitcoin-cli getblockhash 0` | jq '.time == .mediantime'
if height == 0 {
self.headers.first().unwrap().header.time
self.headers.get(0).unwrap().header.time
} else if height > self.len() - 1 {
0
} else {
@ -311,13 +292,9 @@ pub struct BlockHeaderMeta {
impl From<&BlockEntry> for BlockMeta {
fn from(b: &BlockEntry) -> BlockMeta {
#[cfg(not(feature = "liquid"))]
let weight = b.block.weight().to_wu() as u32;
#[cfg(feature = "liquid")]
let weight = b.block.weight() as u32;
BlockMeta {
tx_count: b.block.txdata.len() as u32,
weight,
weight: b.block.weight() as u32,
size: b.size,
}
}

View File

@ -9,7 +9,7 @@ pub fn get_tx_merkle_proof(
block_hash: &BlockHash,
) -> Result<(Vec<Sha256dHash>, usize)> {
let txids = chain
.get_block_txids(block_hash)
.get_block_txids(&block_hash)
.chain_err(|| format!("missing block txids for #{}", block_hash))?;
let pos = txids
.iter()

View File

@ -12,9 +12,6 @@ pub struct TxFeeInfo {
impl TxFeeInfo {
pub fn new(tx: &Transaction, prevouts: &HashMap<u32, &TxOut>, network: Network) -> Self {
let fee = get_tx_fee(tx, prevouts, network);
#[cfg(not(feature = "liquid"))]
let vsize = tx.weight().to_wu() / 4;
#[cfg(feature = "liquid")]
let vsize = tx.weight() / 4;
TxFeeInfo {
@ -27,15 +24,12 @@ impl TxFeeInfo {
#[cfg(not(feature = "liquid"))]
pub fn get_tx_fee(tx: &Transaction, prevouts: &HashMap<u32, &TxOut>, _network: Network) -> u64 {
if tx.is_coinbase() {
if tx.is_coin_base() {
return 0;
}
let total_in: u64 = prevouts
.values()
.map(|prevout| prevout.value.to_sat())
.sum();
let total_out: u64 = tx.output.iter().map(|vout| vout.value.to_sat()).sum();
let total_in: u64 = prevouts.values().map(|prevout| prevout.value).sum();
let total_out: u64 = tx.output.iter().map(|vout| vout.value).sum();
total_in - total_out
}

View File

@ -2,25 +2,20 @@ mod block;
mod script;
mod transaction;
pub mod bincode_util;
pub mod electrum_merkle;
pub mod fees;
pub use self::block::{BlockHeaderMeta, BlockId, BlockMeta, BlockStatus, HeaderEntry, HeaderList};
pub use self::fees::get_tx_fee;
pub use self::script::{
get_innerscripts, IsProvablyUnspendable, ScriptToAddr, ScriptToAsm, SegwitDetection,
};
pub use self::script::{get_innerscripts, ScriptToAddr, ScriptToAsm};
pub use self::transaction::{
extract_tx_prevouts, has_prevout, is_coinbase, is_spendable, serialize_outpoint,
sigops::transaction_sigop_count, TransactionStatus, TxInput,
TransactionStatus, TxInput,
};
use std::collections::HashMap;
use std::sync::atomic::AtomicUsize;
use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::Mutex;
use std::thread::{self, ThreadId};
use std::sync::mpsc::{channel, sync_channel, Receiver, Sender, SyncSender};
use std::thread;
use crate::chain::BlockHeader;
use bitcoin::hashes::sha256d::Hash as Sha256dHash;
@ -40,38 +35,26 @@ pub fn full_hash(hash: &[u8]) -> FullHash {
}
pub struct SyncChannel<T> {
tx: Option<crossbeam_channel::Sender<T>>,
rx: Option<crossbeam_channel::Receiver<T>>,
tx: SyncSender<T>,
rx: Receiver<T>,
}
impl<T> SyncChannel<T> {
pub fn new(size: usize) -> SyncChannel<T> {
let (tx, rx) = crossbeam_channel::bounded(size);
SyncChannel {
tx: Some(tx),
rx: Some(rx),
}
let (tx, rx) = sync_channel(size);
SyncChannel { tx, rx }
}
pub fn sender(&self) -> crossbeam_channel::Sender<T> {
self.tx.as_ref().expect("No Sender").clone()
pub fn sender(&self) -> SyncSender<T> {
self.tx.clone()
}
pub fn receiver(&self) -> &crossbeam_channel::Receiver<T> {
self.rx.as_ref().expect("No Receiver")
pub fn receiver(&self) -> &Receiver<T> {
&self.rx
}
pub fn into_receiver(self) -> crossbeam_channel::Receiver<T> {
self.rx.expect("No Receiver")
}
/// This drops the sender and receiver, causing all other methods to panic.
///
/// Use only when you know that the channel will no longer be used.
/// ie. shutdown.
pub fn close(&mut self) -> Option<crossbeam_channel::Receiver<T>> {
self.tx.take();
self.rx.take()
pub fn into_receiver(self) -> Receiver<T> {
self.rx
}
}
@ -99,58 +82,15 @@ impl<T> Channel<T> {
}
}
/// This static HashMap contains all the threads spawned with [`spawn_thread`] with their name
#[inline]
pub fn with_spawned_threads<F>(f: F)
where
F: FnOnce(&mut HashMap<ThreadId, String>),
{
lazy_static! {
static ref SPAWNED_THREADS: Mutex<HashMap<ThreadId, String>> = Mutex::new(HashMap::new());
}
let mut lock = match SPAWNED_THREADS.lock() {
Ok(threads) => threads,
// There's no possible broken state
Err(threads) => {
warn!("SPAWNED_THREADS is in a poisoned state! Be wary of incorrect logs!");
threads.into_inner()
}
};
f(&mut lock)
}
pub fn spawn_thread<F, T>(prefix: &str, do_work: F) -> thread::JoinHandle<T>
pub fn spawn_thread<F, T>(name: &str, f: F) -> thread::JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
{
static THREAD_COUNTER: AtomicUsize = AtomicUsize::new(0);
let counter = THREAD_COUNTER.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
thread::Builder::new()
.name(format!("{}-{}", prefix, counter))
.spawn(move || {
let thread = std::thread::current();
let name = thread.name().unwrap();
let id = thread.id();
trace!("[THREAD] GETHASHMAP INSERT | {name} {id:?}");
with_spawned_threads(|threads| {
threads.insert(id, name.to_owned());
});
trace!("[THREAD] START WORK | {name} {id:?}");
let result = do_work();
trace!("[THREAD] FINISHED WORK | {name} {id:?}");
trace!("[THREAD] GETHASHMAP REMOVE | {name} {id:?}");
with_spawned_threads(|threads| {
threads.remove(&id);
});
trace!("[THREAD] HASHMAP REMOVED | {name} {id:?}");
result
})
.name(name.to_owned())
.spawn(f)
.unwrap()
}
@ -185,7 +125,7 @@ pub fn create_socket(addr: &SocketAddr) -> Socket {
.set_reuse_port(true)
.expect("cannot enable SO_REUSEPORT");
socket.bind(&(*addr).into()).expect("cannot bind");
socket.bind(&addr.clone().into()).expect("cannot bind");
socket
}
@ -196,28 +136,28 @@ pub fn create_socket(addr: &SocketAddr) -> Socket {
///
/// Copied from https://github.com/rust-bitcoin/rust-bitcoincore-rpc/blob/master/json/src/lib.rs
pub mod serde_hex {
use bitcoin::hashes::hex::FromHex;
use bitcoin::hashes::hex::{FromHex, ToHex};
use serde::de::Error;
use serde::{Deserializer, Serializer};
pub fn serialize<S: Serializer>(b: &[u8], s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&hex::encode(b))
pub fn serialize<S: Serializer>(b: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&b.to_hex())
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
let hex_str: String = ::serde::Deserialize::deserialize(d)?;
FromHex::from_hex(&hex_str).map_err(D::Error::custom)
Ok(FromHex::from_hex(&hex_str).map_err(D::Error::custom)?)
}
pub mod opt {
use bitcoin::hashes::hex::FromHex;
use bitcoin::hashes::hex::{FromHex, ToHex};
use serde::de::Error;
use serde::{Deserializer, Serializer};
pub fn serialize<S: Serializer>(b: &Option<Vec<u8>>, s: S) -> Result<S::Ok, S::Error> {
match *b {
None => s.serialize_none(),
Some(ref b) => s.serialize_str(&hex::encode(b)),
Some(ref b) => s.serialize_str(&b.to_hex()),
}
}

View File

@ -9,95 +9,13 @@ pub struct InnerScripts {
pub witness_script: Option<Script>,
}
pub trait IsProvablyUnspendable {
fn is_provably_unspendable_(&self) -> bool;
}
#[cfg(not(feature = "liquid"))]
impl IsProvablyUnspendable for bitcoin::Script {
// is_provably_unspendable() is deprecated in rust-bitcoin
// so we re-implement it here. Copy pasted.
fn is_provably_unspendable_(&self) -> bool {
use bitcoin::blockdata::opcodes::{
Class::{IllegalOp, ReturnOp},
ClassifyContext, Opcode,
};
match self.as_bytes().first() {
Some(b) => {
let first = Opcode::from(*b);
let class = first.classify(ClassifyContext::Legacy);
class == ReturnOp || class == IllegalOp
}
None => false,
}
}
}
#[cfg(feature = "liquid")]
impl IsProvablyUnspendable for elements::Script {
#[inline(always)]
fn is_provably_unspendable_(&self) -> bool {
// Not deprecated yet
self.is_provably_unspendable()
}
}
// Extension trait for segwit script detection that works across bitcoin and elements
pub trait SegwitDetection {
fn segwit_is_p2wpkh(&self) -> bool;
fn segwit_is_p2wsh(&self) -> bool;
fn segwit_is_p2tr(&self) -> bool;
}
#[cfg(not(feature = "liquid"))]
impl SegwitDetection for bitcoin::Script {
fn segwit_is_p2wpkh(&self) -> bool {
self.is_p2wpkh()
}
fn segwit_is_p2wsh(&self) -> bool {
self.is_p2wsh()
}
fn segwit_is_p2tr(&self) -> bool {
self.is_p2tr()
}
}
#[cfg(not(feature = "liquid"))]
impl SegwitDetection for bitcoin::ScriptBuf {
fn segwit_is_p2wpkh(&self) -> bool {
self.is_p2wpkh()
}
fn segwit_is_p2wsh(&self) -> bool {
self.is_p2wsh()
}
fn segwit_is_p2tr(&self) -> bool {
self.is_p2tr()
}
}
#[cfg(feature = "liquid")]
impl SegwitDetection for elements::Script {
fn segwit_is_p2wpkh(&self) -> bool {
self.is_v0_p2wpkh()
}
fn segwit_is_p2wsh(&self) -> bool {
self.is_v0_p2wsh()
}
fn segwit_is_p2tr(&self) -> bool {
self.is_v1_p2tr()
}
}
pub trait ScriptToAsm: std::fmt::Debug {
fn to_asm(&self) -> String {
let asm = format!("{:?}", self);
asm[7..asm.len() - 1].to_string()
(&asm[7..asm.len() - 1]).to_string()
}
}
impl ScriptToAsm for bitcoin::Script {}
impl ScriptToAsm for bitcoin::ScriptBuf {}
#[cfg(feature = "liquid")]
impl ScriptToAsm for elements::Script {}
@ -107,9 +25,7 @@ pub trait ScriptToAddr {
#[cfg(not(feature = "liquid"))]
impl ScriptToAddr for bitcoin::Script {
fn to_address_str(&self, network: Network) -> Option<String> {
bitcoin::Address::from_script(self, bitcoin::Network::from(network))
.ok()
.map(|s| s.to_string())
bitcoin::Address::from_script(self, network.into()).map(|s| s.to_string())
}
}
#[cfg(feature = "liquid")]
@ -125,11 +41,7 @@ pub fn get_innerscripts(txin: &TxIn, prevout: &TxOut) -> InnerScripts {
// Wrapped redeemScript for P2SH spends
let redeem_script = if prevout.script_pubkey.is_p2sh() {
if let Some(Ok(PushBytes(redeemscript))) = txin.script_sig.instructions().last() {
#[cfg(not(feature = "liquid"))]
let bytes = redeemscript.as_bytes().to_vec();
#[cfg(feature = "liquid")]
let bytes = redeemscript.to_vec();
Some(Script::from(bytes))
Some(Script::from(redeemscript.to_vec()))
} else {
None
}
@ -138,9 +50,8 @@ pub fn get_innerscripts(txin: &TxIn, prevout: &TxOut) -> InnerScripts {
};
// Wrapped witnessScript for P2WSH or P2SH-P2WSH spends
let witness_script = if prevout.script_pubkey.segwit_is_p2wsh()
|| prevout.script_pubkey.segwit_is_p2tr()
|| redeem_script.as_ref().is_some_and(|s| s.segwit_is_p2wsh())
let witness_script = if prevout.script_pubkey.is_v0_p2wsh()
|| redeem_script.as_ref().map_or(false, |s| s.is_v0_p2wsh())
{
let witness = &txin.witness;
#[cfg(feature = "liquid")]
@ -152,39 +63,7 @@ pub fn get_innerscripts(txin: &TxIn, prevout: &TxOut) -> InnerScripts {
#[cfg(feature = "liquid")]
let wit_to_vec = Clone::clone;
let inner_script_slice = if prevout.script_pubkey.segwit_is_p2tr() {
// Witness stack is potentially very large
// so we avoid to_vec() or iter().collect() for performance
let w_len = witness.len();
witness
.last()
// Get the position of the script spend script (if it exists)
.map(|last_elem| {
// From BIP341:
// If there are at least two witness elements, and the first byte of
// the last element is 0x50, this last element is called annex a
// and is removed from the witness stack.
if w_len >= 2 && last_elem.first().filter(|&&v| v == 0x50).is_some() {
// account for the extra item removed from the end
3
} else {
// otherwise script is 2nd from last
2
}
})
// Convert to None if not script spend
// Note: Option doesn't have filter_map() method
.filter(|&script_pos_from_last| w_len >= script_pos_from_last)
.and_then(|script_pos_from_last| {
// Can't use second_to_last() since it might be 3rd to last
#[allow(clippy::iter_nth)]
witness.iter().nth(w_len - script_pos_from_last)
})
} else {
witness.last()
};
inner_script_slice.map(wit_to_vec).map(Script::from)
witness.iter().last().map(wit_to_vec).map(Script::from)
} else {
None
};

View File

@ -1,19 +1,18 @@
use crate::chain::{BlockHash, OutPoint, Transaction, TxIn, TxOut, Txid};
use crate::errors;
use crate::util::{BlockId, IsProvablyUnspendable};
use crate::util::BlockId;
use std::collections::HashMap;
#[cfg(feature = "liquid")]
use bitcoin::hashes::hex::FromHex;
#[cfg(feature = "liquid")]
lazy_static! {
static ref REGTEST_INITIAL_ISSUANCE_PREVOUT: Txid =
"50cdc410c9d0d61eeacc531f52d2c70af741da33af127c364e52ac1ee7c030a5"
.parse()
.unwrap();
Txid::from_hex("50cdc410c9d0d61eeacc531f52d2c70af741da33af127c364e52ac1ee7c030a5").unwrap();
static ref TESTNET_INITIAL_ISSUANCE_PREVOUT: Txid =
"0c52d2526a5c9f00e9fb74afd15dd3caaf17c823159a514f929ae25193a43a52"
.parse()
.unwrap();
Txid::from_hex("0c52d2526a5c9f00e9fb74afd15dd3caaf17c823159a514f929ae25193a43a52").unwrap();
}
#[derive(Serialize, Deserialize)]
@ -32,7 +31,7 @@ impl From<Option<BlockId>> for TransactionStatus {
match blockid {
Some(b) => TransactionStatus {
confirmed: true,
block_height: Some(b.height),
block_height: Some(b.height as usize),
block_hash: Some(b.hash),
block_time: Some(b.time),
},
@ -49,7 +48,7 @@ impl From<Option<BlockId>> for TransactionStatus {
#[derive(Serialize, Deserialize)]
pub struct TxInput {
pub txid: Txid,
pub vin: u32,
pub vin: u16,
}
pub fn is_coinbase(txin: &TxIn) -> bool {
@ -71,9 +70,9 @@ pub fn has_prevout(txin: &TxIn) -> bool {
pub fn is_spendable(txout: &TxOut) -> bool {
#[cfg(not(feature = "liquid"))]
return !txout.script_pubkey.is_provably_unspendable_();
return !txout.script_pubkey.is_provably_unspendable();
#[cfg(feature = "liquid")]
return !txout.is_fee() && !txout.script_pubkey.is_provably_unspendable_();
return !txout.is_fee() && !txout.script_pubkey.is_provably_unspendable();
}
/// Extract the previous TxOuts of a Transaction's TxIns
@ -86,21 +85,25 @@ pub fn is_spendable(txout: &TxOut) -> bool {
pub fn extract_tx_prevouts<'a>(
tx: &Transaction,
txos: &'a HashMap<OutPoint, TxOut>,
allow_missing: bool,
) -> Result<HashMap<u32, &'a TxOut>, errors::Error> {
tx.input
.iter()
.enumerate()
.filter(|(_, txi)| has_prevout(txi))
.map(|(index, txi)| {
Ok((
.filter_map(|(index, txi)| {
Some(Ok((
index as u32,
match txos.get(&txi.previous_output) {
Some(txo) => txo,
None => {
return Err(format!("missing outpoint {:?}", txi.previous_output).into());
match (allow_missing, txos.get(&txi.previous_output)) {
(_, Some(txo)) => txo,
(true, None) => return None,
(false, None) => {
return Some(Err(
format!("missing outpoint {:?}", txi.previous_output).into()
));
}
},
))
)))
})
.collect()
}
@ -115,278 +118,3 @@ where
s.serialize_field("vout", &outpoint.vout)?;
s.end()
}
pub(super) mod sigops {
use crate::chain::{
opcodes::all::{OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY, OP_CHECKSIG, OP_CHECKSIGVERIFY},
script::{self, Instruction},
Transaction, TxOut, Witness,
};
#[cfg(not(feature = "liquid"))]
use bitcoin::opcodes::Opcode;
#[cfg(feature = "liquid")]
use elements::opcodes::All as Opcode;
use std::collections::HashMap;
/// Get sigop count for transaction. prevout_map must have all the prevouts.
pub fn transaction_sigop_count(
tx: &Transaction,
prevout_map: &HashMap<u32, &TxOut>,
) -> Result<usize, script::Error> {
let input_count = tx.input.len();
let mut prevouts = Vec::with_capacity(input_count);
#[cfg(not(feature = "liquid"))]
let is_coinbase_or_pegin = tx.is_coinbase();
#[cfg(feature = "liquid")]
let is_coinbase_or_pegin = tx.is_coinbase() || tx.input.iter().any(|input| input.is_pegin);
if !is_coinbase_or_pegin {
for idx in 0..input_count {
prevouts.push(
*prevout_map
.get(&(idx as u32))
.ok_or(script::Error::EarlyEndOfScript)?,
);
}
}
// coinbase tx won't use prevouts so it can be empty.
get_sigop_cost(tx, &prevouts, true, true)
}
fn decode_pushnum(op: &Opcode) -> Option<u8> {
// 81 = OP_1, 96 = OP_16
// 81 -> 1, so... 81 - 80 -> 1
#[cfg(not(feature = "liquid"))]
let self_u8 = op.to_u8();
#[cfg(feature = "liquid")]
let self_u8 = op.into_u8();
match self_u8 {
81..=96 => Some(self_u8 - 80),
_ => None,
}
}
fn count_sigops(script: &script::Script, accurate: bool) -> usize {
let mut n = 0;
let mut pushnum_cache = None;
for inst in script.instructions() {
match inst {
Ok(Instruction::Op(opcode)) => {
match opcode {
OP_CHECKSIG | OP_CHECKSIGVERIFY => {
n += 1;
}
OP_CHECKMULTISIG | OP_CHECKMULTISIGVERIFY => {
match (accurate, pushnum_cache) {
(true, Some(pushnum)) => {
// Add the number of pubkeys in the multisig as sigop count
n += usize::from(pushnum);
}
_ => {
// MAX_PUBKEYS_PER_MULTISIG from Bitcoin Core
// https://github.com/bitcoin/bitcoin/blob/v25.0/src/script/script.h#L29-L30
n += 20;
}
}
}
_ => {
pushnum_cache = decode_pushnum(&opcode);
}
}
}
// We ignore errors as well as pushdatas
_ => {
pushnum_cache = None;
}
}
}
n
}
/// Get the sigop count for legacy transactions
fn get_legacy_sigop_count(tx: &Transaction) -> usize {
let mut n = 0;
for input in &tx.input {
n += count_sigops(&input.script_sig, false);
}
for output in &tx.output {
n += count_sigops(&output.script_pubkey, false);
}
n
}
fn get_p2sh_sigop_count(tx: &Transaction, previous_outputs: &[&TxOut]) -> usize {
#[cfg(not(feature = "liquid"))]
if tx.is_coinbase() {
return 0;
}
#[cfg(feature = "liquid")]
if tx.is_coinbase() {
return 0;
}
let mut n = 0;
for (input, prevout) in tx.input.iter().zip(previous_outputs.iter()) {
if prevout.script_pubkey.is_p2sh() {
if let Some(Ok(script::Instruction::PushBytes(redeem))) =
input.script_sig.instructions().last()
{
#[cfg(not(feature = "liquid"))]
let script = script::Script::from_bytes(redeem.as_bytes());
#[cfg(feature = "liquid")]
let script = script::Script::from(redeem.to_vec());
#[allow(clippy::needless_borrow)]
{
n += count_sigops(&script, true);
}
}
}
}
n
}
fn get_witness_sigop_count(tx: &Transaction, previous_outputs: &[&TxOut]) -> usize {
let mut n = 0;
#[inline]
fn is_push_only(script: &script::Script) -> bool {
for inst in script.instructions() {
match inst {
Err(_) => return false,
Ok(Instruction::Op(_)) => return false,
Ok(Instruction::PushBytes(_)) => {}
}
}
true
}
#[inline]
fn last_pushdata(script: &script::Script) -> Option<&[u8]> {
match script.instructions().last() {
#[cfg(not(feature = "liquid"))]
Some(Ok(Instruction::PushBytes(bytes))) => Some(bytes.as_bytes()),
#[cfg(feature = "liquid")]
Some(Ok(Instruction::PushBytes(bytes))) => Some(bytes),
_ => None,
}
}
#[inline]
fn count_with_prevout(
prevout: &TxOut,
script_sig: &script::Script,
witness: &Witness,
) -> usize {
let mut n = 0;
let script_owned;
let script: &script::Script = if prevout.script_pubkey.is_witness_program() {
&prevout.script_pubkey
} else if prevout.script_pubkey.is_p2sh()
&& is_push_only(script_sig)
&& !script_sig.is_empty()
{
#[cfg(not(feature = "liquid"))]
{
script_owned =
script::ScriptBuf::from(last_pushdata(script_sig).unwrap().to_vec());
}
#[cfg(feature = "liquid")]
{
script_owned =
script::Script::from(last_pushdata(script_sig).unwrap().to_vec());
}
&script_owned
} else {
return 0;
};
#[cfg(not(feature = "liquid"))]
if script.is_p2wsh() {
let bytes = script.as_bytes();
n += sig_ops(witness, bytes[0], &bytes[2..]);
} else if script.is_p2wpkh() {
n += 1;
}
#[cfg(feature = "liquid")]
if script.is_v0_p2wsh() {
let bytes = script.as_bytes();
n += sig_ops(witness, bytes[0], &bytes[2..]);
} else if script.is_v0_p2wpkh() {
n += 1;
}
n
}
for (input, prevout) in tx.input.iter().zip(previous_outputs.iter()) {
n += count_with_prevout(prevout, &input.script_sig, &input.witness);
}
n
}
/// Get the sigop cost for this transaction.
fn get_sigop_cost(
tx: &Transaction,
previous_outputs: &[&TxOut],
verify_p2sh: bool,
verify_witness: bool,
) -> Result<usize, script::Error> {
let mut n_sigop_cost = get_legacy_sigop_count(tx) * 4;
#[cfg(not(feature = "liquid"))]
if tx.is_coinbase() {
return Ok(n_sigop_cost);
}
#[cfg(feature = "liquid")]
if tx.is_coinbase() || tx.input.iter().any(|input| input.is_pegin) {
return Ok(n_sigop_cost);
}
if tx.input.len() != previous_outputs.len() {
return Err(script::Error::EarlyEndOfScript);
}
if verify_witness && !verify_p2sh {
return Err(script::Error::EarlyEndOfScript);
}
if verify_p2sh {
n_sigop_cost += get_p2sh_sigop_count(tx, previous_outputs) * 4;
}
if verify_witness {
n_sigop_cost += get_witness_sigop_count(tx, previous_outputs);
}
Ok(n_sigop_cost)
}
/// Get sigops for the Witness
///
/// witness_version is the raw opcode. OP_0 is 0, OP_1 is 81, etc.
#[allow(clippy::redundant_closure)]
fn sig_ops(witness: &Witness, witness_version: u8, witness_program: &[u8]) -> usize {
#[cfg(feature = "liquid")]
let last_witness = witness.script_witness.last();
#[cfg(not(feature = "liquid"))]
let last_witness = witness.last();
match (witness_version, witness_program.len()) {
(0, 20) => 1,
(0, 32) => {
#[cfg(not(feature = "liquid"))]
{
#[allow(clippy::needless_borrow)]
last_witness
.map(|sl| script::Script::from_bytes(sl))
.map(|s| count_sigops(s, true))
.unwrap_or_default()
}
#[cfg(feature = "liquid")]
{
last_witness
.map(|sl| script::Script::from(sl.clone()))
.map(|s| count_sigops(&s, true))
.unwrap_or_default()
}
}
_ => 0,
}
}
}

274
start
View File

@ -1,274 +0,0 @@
#!/usr/bin/env zsh
# initialize variables
DAEMON=bitcoin
NETWORK=mainnet
FEATURES=default
DB_FOLDER=/electrs
ASSET_DB_ARGS=()
NODENAME=$(hostname|cut -d . -f1)
LOCATION=$(hostname|cut -d . -f2)
USAGE="Usage: $0 (mainnet|testnet|signet|liquid|liquidtestnet) [popular-scripts]"
# load rust if necessary
if [ -e "${HOME}/.cargo/env" ];then
source "${HOME}/.cargo/env"
export PATH="${HOME}/.cargo/bin:${PATH}"
fi
# which OS?
case "$(uname -s)" in
FreeBSD)
OS=FreeBSD
NPROC=$(sysctl -n hw.ncpu)
export CC=/usr/local/bin/clang17
export CXX=/usr/local/bin/clang++17
export CPP=/usr/local/bin/clang-cpp17
export RUSTFLAGS="-C linker=clang17"
;;
Darwin)
OS=Darwin
NPROC=$(sysctl -n hw.ncpu)
;;
Linux)
OS=Linux
NPROC=$(grep -c proc /proc/cpuinfo)
;;
*)
OS=Unknown
NPROC=4
;;
esac
# which network?
case "${1}" in
mainnet)
THREADS=$((NPROC / 8))
CRONJOB_TIMING="20 4 * * *"
;;
testnet)
NETWORK=testnet
THREADS=$((NPROC / 8))
CRONJOB_TIMING="2 4 * * *"
;;
testnet4)
NETWORK=testnet4
THREADS=$((NPROC / 8))
CRONJOB_TIMING="17 4 * * *"
;;
signet)
NETWORK=signet
THREADS=$((NPROC / 8))
CRONJOB_TIMING="9 4 * * *"
;;
liquid)
DAEMON=elements
NETWORK=liquid
FEATURES=liquid
ASSET_DB_ARGS=(--asset-db-path /elements/asset_registry_db)
THREADS=$((NPROC / 8))
CRONJOB_TIMING="12 4 * * *"
;;
liquidtestnet)
DAEMON=elements
NETWORK=liquidtestnet
FEATURES=liquid
ASSET_DB_ARGS=(--asset-db-path /elements/asset_registry_testnet_db)
THREADS=$((NPROC / 8))
CRONJOB_TIMING="17 4 * * *"
;;
*)
echo "${USAGE}"
exit 1
;;
esac
# Run the popular address txt file generator before each run
POPULAR_SCRIPTS_FOLDER="${HOME}/popular-scripts/${NETWORK}"
POPULAR_SCRIPTS_FILE_RAW="${POPULAR_SCRIPTS_FOLDER}/popular-scripts-raw.txt"
POPULAR_SCRIPTS_FILE="${POPULAR_SCRIPTS_FOLDER}/popular-scripts.txt"
# This function runs the job for generating the popular scripts text file for the precache arg
generate_popular_scripts() {
mkdir -p "${POPULAR_SCRIPTS_FOLDER}"
## Use nproc * 4 threads to generate the txt file (lots of iowait, so 2x~4x core count is ok)
## Only pick up addresses with 101 history events or more
## (Without lowering MIN_HISTORY_ITEMS_TO_CACHE this is the lowest we can go)
## It prints out progress to STDERR
echo "[*] Generating popular-scripts using ${THREADS} threads..."
cd "${HOME}/electrs"
HIGH_USAGE_THRESHOLD=101 \
JOB_THREAD_COUNT=${THREADS} \
nice cargo run \
--release \
--bin popular-scripts \
--features "${FEATURES}" \
-- \
--network "${NETWORK}" \
--db-dir "${DB_FOLDER}" \
> "${POPULAR_SCRIPTS_FILE_RAW}"
## Only overwrite the existing file if the popular-scripts cargo run succeeded
if [ "$?" = "0" ];then
## Sorted and deduplicated just in case
echo "Sorting popular scripts for final results..."
sort "${POPULAR_SCRIPTS_FILE_RAW}" | uniq > "${POPULAR_SCRIPTS_FILE}"
fi
rm "${POPULAR_SCRIPTS_FILE_RAW}"
}
# This function is for inserting the cronjob for generating the popular scripts
CRONJOB_CMD="\"${HOME}/electrs/start\" \"${NETWORK}\" popular-scripts"
echo "${CRONJOB_TIMING} ${CRONJOB_CMD}"
case "${2}" in
popular-scripts)
echo "[*] Only generate popular-scripts, then exit"
generate_popular_scripts
exit 0
;;
version)
echo "[*] Only print versions, then exit"
cargo run --bin electrs --release -- --version
cargo run --bin popular-scripts --release -- --version
exit 0
;;
"")
# If the 2nd arg isn't passed, just run the normal electrs script as-is
;;
*)
echo "${USAGE}"
exit 1
;;
esac
# run in loop in case of crash
until false
do
# reset CWD
cd "${HOME}/electrs"
# disable making electrs.core files
ulimit -c 0
# prepare run-time variables
UTXOS_LIMIT=500
ELECTRUM_TXS_LIMIT=500
ELECTRUM_MAX_LINE_SIZE=1048576 # 1 MiB
ELECTRUM_MAX_SUBSCRIPTIONS=100
ELECTRUM_MAX_CLIENTS=1000
MAIN_LOOP_DELAY=500
DAEMON_CONF="${HOME}/${DAEMON}.conf"
HTTP_SOCKET_FILE="${HOME}/socket/esplora-${DAEMON}-${NETWORK}"
RPC_SOCKET_FILE="${HOME}/socket/electrum-${DAEMON}-${NETWORK}"
# get RPC credentials from bitcoin.conf or elements.conf directly
echo "[*] Getting RPC credentials from ${DAEMON_CONF}"
RPC_USER=$(grep 'rpcuser=' "${DAEMON_CONF}"|cut -d = -f2|head -1)
RPC_PASS=$(grep 'rpcpassword=' "${DAEMON_CONF}"|cut -d = -f2|head -1)
# override limits based on hostname
if [ "${NODENAME}" = "node201" ];then
UTXOS_LIMIT=9000
ELECTRUM_TXS_LIMIT=9000
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
ELECTRUM_MAX_SUBSCRIPTIONS=100000
ELECTRUM_MAX_CLIENTS=10000
MAIN_LOOP_DELAY=14000
fi
if [ "${NODENAME}" = "node204" ] && [ "${LOCATION}" = "sg1" ];then
UTXOS_LIMIT=9000
ELECTRUM_TXS_LIMIT=9000
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
ELECTRUM_MAX_SUBSCRIPTIONS=100000
ELECTRUM_MAX_CLIENTS=10000
fi
if [ "${NODENAME}" = "node204" ] && [ "${LOCATION}" = "hnl" ];then
UTXOS_LIMIT=9000
ELECTRUM_TXS_LIMIT=9000
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
ELECTRUM_MAX_SUBSCRIPTIONS=100000
ELECTRUM_MAX_CLIENTS=10000
fi
if [ "${NODENAME}" = "node206" ] && [ "${LOCATION}" = "tk7" ];then
UTXOS_LIMIT=9000
ELECTRUM_TXS_LIMIT=9000
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
ELECTRUM_MAX_SUBSCRIPTIONS=100000
ELECTRUM_MAX_CLIENTS=10000
fi
if [ "${NODENAME}" = "node211" ];then
UTXOS_LIMIT=9000
ELECTRUM_TXS_LIMIT=9000
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
ELECTRUM_MAX_SUBSCRIPTIONS=100000
ELECTRUM_MAX_CLIENTS=10000
fi
if [ "${NODENAME}" = "node212" ];then
UTXOS_LIMIT=9000
ELECTRUM_TXS_LIMIT=9000
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
ELECTRUM_MAX_SUBSCRIPTIONS=100000
ELECTRUM_MAX_CLIENTS=10000
fi
if [ "${NODENAME}" = "node213" ];then
UTXOS_LIMIT=9000
ELECTRUM_TXS_LIMIT=9000
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
ELECTRUM_MAX_SUBSCRIPTIONS=100000
ELECTRUM_MAX_CLIENTS=10000
fi
if [ "${NODENAME}" = "node214" ];then
UTXOS_LIMIT=9000
ELECTRUM_TXS_LIMIT=9000
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
ELECTRUM_MAX_SUBSCRIPTIONS=100000
ELECTRUM_MAX_CLIENTS=10000
fi
if [ "${NETWORK}" = "testnet4" ];then
UTXOS_LIMIT=9000
ELECTRUM_TXS_LIMIT=9000
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
ELECTRUM_MAX_SUBSCRIPTIONS=100000
ELECTRUM_MAX_CLIENTS=10000
fi
if [ "${LOCATION}" = "fmt" ];then
UTXOS_LIMIT=9000
ELECTRUM_TXS_LIMIT=9000
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
ELECTRUM_MAX_SUBSCRIPTIONS=100000
ELECTRUM_MAX_CLIENTS=10000
fi
if [ ! -e "${POPULAR_SCRIPTS_FILE}" ];then
generate_popular_scripts
fi
# Run the electrs process (Note: db-dir is used in both commands)
nice cargo run \
--release \
--bin electrs \
--features "${FEATURES}" \
-- \
--network "${NETWORK}" \
--daemon-dir "${HOME}" \
--db-dir "${DB_FOLDER}" \
"${ASSET_DB_ARGS[@]}" \
--main-loop-delay "${MAIN_LOOP_DELAY}" \
--rpc-socket-file "${RPC_SOCKET_FILE}" \
--http-socket-file "${HTTP_SOCKET_FILE}" \
--precache-scripts "${POPULAR_SCRIPTS_FILE}" \
--precache-threads "${THREADS}" \
--cookie "${RPC_USER}:${RPC_PASS}" \
--cors '*' \
--address-search \
--utxos-limit "${UTXOS_LIMIT}" \
--electrum-txs-limit "${ELECTRUM_TXS_LIMIT}" \
--electrum-max-line-size "${ELECTRUM_MAX_LINE_SIZE}" \
--electrum-max-subscriptions "${ELECTRUM_MAX_SUBSCRIPTIONS}" \
--electrum-max-clients "${ELECTRUM_MAX_CLIENTS}" \
-vv
sleep 1
done