Compare commits
1 Commits
mempool
...
junderw/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b622e9efd |
4
.github/actions/ci-rust-setup/action.yml
vendored
4
.github/actions/ci-rust-setup/action.yml
vendored
@ -33,8 +33,8 @@ runs:
|
||||
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
|
||||
# Commit date is Sep 19, 2023
|
||||
uses: dtolnay/rust-toolchain@439cf607258077187679211f12aa6f19af4a0af7
|
||||
with:
|
||||
toolchain: ${{ steps.gettoolchain.outputs.toolchain }}
|
||||
targets: ${{ inputs.targets }}
|
||||
|
||||
59
.github/workflows/ci.yml
vendored
59
.github/workflows/ci.yml
vendored
@ -10,9 +10,9 @@ name: Compile Check and Lint
|
||||
jobs:
|
||||
check:
|
||||
name: Compile Check
|
||||
runs-on: mempool-ci
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Rust
|
||||
uses: './.github/actions/ci-rust-setup'
|
||||
with:
|
||||
@ -21,9 +21,9 @@ jobs:
|
||||
|
||||
fmt:
|
||||
name: Formatter
|
||||
runs-on: mempool-ci
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Rust
|
||||
uses: './.github/actions/ci-rust-setup'
|
||||
with:
|
||||
@ -32,61 +32,18 @@ jobs:
|
||||
|
||||
test:
|
||||
name: Run Tests
|
||||
runs-on: mempool-ci
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- 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
|
||||
runs-on: ubuntu-latest
|
||||
needs: [check]
|
||||
strategy:
|
||||
matrix: # Try all combinations of features. Some times weird things appear.
|
||||
@ -97,7 +54,7 @@ jobs:
|
||||
'-F electrum-discovery,liquid',
|
||||
]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Rust
|
||||
uses: './.github/actions/ci-rust-setup'
|
||||
with:
|
||||
|
||||
46
.github/workflows/on-tag.yml
vendored
46
.github/workflows/on-tag.yml
vendored
@ -16,18 +16,39 @@ permissions:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: mempool-ci
|
||||
runs-on: ubuntu-latest
|
||||
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:
|
||||
# Workaround based on JonasAlfredsson/docker-on-tmpfs@v1.0.1
|
||||
- name: Replace the current swap file
|
||||
shell: bash
|
||||
run: |
|
||||
sudo swapoff /mnt/swapfile
|
||||
sudo rm -v /mnt/swapfile
|
||||
sudo fallocate -l 13G /mnt/swapfile
|
||||
sudo chmod 600 /mnt/swapfile
|
||||
sudo mkswap /mnt/swapfile
|
||||
sudo swapon /mnt/swapfile
|
||||
|
||||
- name: Show current memory and swap status
|
||||
shell: bash
|
||||
run: |
|
||||
sudo free -h
|
||||
echo
|
||||
sudo swapon --show
|
||||
|
||||
- name: Mount a tmpfs over /var/lib/docker
|
||||
shell: bash
|
||||
run: |
|
||||
if [ ! -d "/var/lib/docker" ]; then
|
||||
echo "Directory '/var/lib/docker' not found"
|
||||
exit 1
|
||||
fi
|
||||
sudo mount -t tmpfs -o size=10G tmpfs /var/lib/docker
|
||||
sudo systemctl restart docker
|
||||
sudo df -h | grep docker
|
||||
|
||||
- name: Set env variables
|
||||
run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
||||
|
||||
@ -70,8 +91,7 @@ jobs:
|
||||
--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 \
|
||||
--tag ${{ secrets.DOCKER_HUB_USER }}/electrs:$TAG \
|
||||
--tag ${{ secrets.DOCKER_HUB_USER }}/electrs:latest \
|
||||
--output "type=registry" . \
|
||||
--build-arg commitHash=$SHORT_SHA \
|
||||
--build-arg CARGO_EXTRA_ARGS="${{ matrix.cargo_extra_args }}"
|
||||
--build-arg commitHash=$SHORT_SHA
|
||||
|
||||
18
.github/workflows/project-review-status.yml
vendored
18
.github/workflows/project-review-status.yml
vendored
@ -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 }}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,4 +5,3 @@ target
|
||||
*~
|
||||
*.pyc
|
||||
.vscode
|
||||
*.core
|
||||
|
||||
19
AGENTS.md
19
AGENTS.md
@ -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`
|
||||
1053
Cargo.lock
generated
1053
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
19
Cargo.toml
19
Cargo.toml
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mempool-electrs"
|
||||
version = "3.4.0-dev"
|
||||
version = "3.1.0-dev"
|
||||
authors = [
|
||||
"Roman Zeyde <me@romanzey.de>",
|
||||
"Nadav Ivgi <nadav@shesek.info>",
|
||||
@ -28,32 +28,32 @@ electrum-discovery = ["electrum-client"]
|
||||
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" ] }
|
||||
bitcoin = { version = "0.28", features = [ "use-serde" ] }
|
||||
bounded-vec-deque = "0.1.1"
|
||||
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 = "0.21.0"
|
||||
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 +63,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 +75,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
|
||||
|
||||
@ -18,8 +18,7 @@ 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}
|
||||
RUN cargo build --release --bin electrs
|
||||
|
||||
FROM base as deploy
|
||||
|
||||
|
||||
3
build.rs
3
build.rs
@ -34,8 +34,7 @@ fn main() {
|
||||
// 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" {
|
||||
let git_hash = if dirty.is_empty() {
|
||||
rev_parse
|
||||
} else {
|
||||
format!("{}(dirty)", rev_parse.trim())
|
||||
|
||||
@ -1 +1 @@
|
||||
1.87
|
||||
1.80
|
||||
|
||||
@ -57,8 +57,6 @@ 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
|
||||
TESTNAME="Running cargo test with all features"
|
||||
echo "$TESTNAME"
|
||||
cargo test $@ -q --lib --all-features
|
||||
|
||||
@ -18,10 +18,7 @@ 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,
|
||||
)
|
||||
open_raw_db(&config.db_path.join("newindex").join("history"))
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
@ -62,7 +62,7 @@ fn main() {
|
||||
}
|
||||
|
||||
let tx: Transaction = deserialize(value).expect("failed to parse Transaction");
|
||||
let txid = tx.compute_txid();
|
||||
let txid = tx.txid();
|
||||
|
||||
iter.next();
|
||||
|
||||
@ -71,7 +71,7 @@ fn main() {
|
||||
continue;
|
||||
}
|
||||
// skip coinbase txs
|
||||
if tx.is_coinbase() {
|
||||
if tx.is_coin_base() {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -91,26 +91,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 +119,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();
|
||||
|
||||
73
src/chain.rs
73
src/chain.rs
@ -2,64 +2,25 @@ use std::str::FromStr;
|
||||
|
||||
#[cfg(not(feature = "liquid"))] // use regular Bitcoin data structures
|
||||
pub use bitcoin::{
|
||||
address,
|
||||
block::Header as BlockHeader,
|
||||
blockdata::{opcodes, script},
|
||||
blockdata::{opcodes, script, witness::Witness},
|
||||
consensus::deserialize,
|
||||
hashes, Block, BlockHash, OutPoint, ScriptBuf as Script, Transaction, TxIn, TxOut, Txid,
|
||||
Witness,
|
||||
hashes,
|
||||
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, hashes, opcodes, script, Address, AssetId,
|
||||
Block, BlockHash, BlockHeader, OutPoint, Script, Transaction, TxIn, TxInWitness as Witness,
|
||||
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;
|
||||
@ -92,8 +53,8 @@ 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.
|
||||
@ -105,10 +66,7 @@ impl Network {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,10 +178,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 +185,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(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -267,7 +221,7 @@ impl From<Network> for BNetwork {
|
||||
match network {
|
||||
Network::Bitcoin => BNetwork::Bitcoin,
|
||||
Network::Testnet => BNetwork::Testnet,
|
||||
Network::Testnet4 => BNetwork::Testnet4,
|
||||
Network::Testnet4 => BNetwork::Testnet,
|
||||
Network::Regtest => BNetwork::Regtest,
|
||||
Network::Signet => BNetwork::Signet,
|
||||
}
|
||||
@ -280,7 +234,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,
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -17,8 +17,6 @@ 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 = {
|
||||
@ -65,19 +63,14 @@ pub struct Config {
|
||||
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")]
|
||||
@ -283,36 +276,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)]
|
||||
@ -532,8 +495,6 @@ impl Config {
|
||||
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);
|
||||
@ -584,16 +545,6 @@ impl Config {
|
||||
"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),
|
||||
@ -623,6 +574,7 @@ impl Config {
|
||||
#[cfg(feature = "liquid")]
|
||||
asset_db_path,
|
||||
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
electrum_public_hosts,
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
electrum_announce: m.is_present("electrum_announce"),
|
||||
|
||||
@ -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())
|
||||
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> {
|
||||
@ -365,9 +363,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 {
|
||||
@ -548,7 +543,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 +562,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", ¶ms_list)?;
|
||||
let mut blocks = vec![];
|
||||
@ -595,7 +589,7 @@ 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", ¶ms_list, 0.25)?;
|
||||
let mut txs = vec![];
|
||||
@ -614,14 +608,14 @@ 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)
|
||||
}
|
||||
@ -637,10 +631,8 @@ 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())
|
||||
Txid::from_hex(txid.as_str().chain_err(|| "non-string txid")?)
|
||||
.chain_err(|| "failed to parse txid")
|
||||
}
|
||||
|
||||
pub fn test_mempool_accept(
|
||||
@ -711,7 +703,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")
|
||||
@ -727,7 +719,7 @@ impl Daemon {
|
||||
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 +745,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() {
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
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::ServerFeaturesRes;
|
||||
@ -20,9 +19,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
|
||||
|
||||
@ -183,7 +183,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 +235,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 +337,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())?;
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use std::fs;
|
||||
use std::io::{BufRead, BufReader, Cursor, Read, Write};
|
||||
use std::io::{BufRead, BufReader, Read, Write};
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
use std::net::IpAddr;
|
||||
use std::net::{Shutdown, SocketAddr, TcpListener, TcpStream};
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
@ -11,12 +12,10 @@ use std::sync::atomic::AtomicBool;
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use bitcoin::hashes::sha256d::Hash as Sha256dHash;
|
||||
use error_chain::ChainedError;
|
||||
use hex;
|
||||
use ppp::PartialResult;
|
||||
use serde_json::{from_str, Value};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
@ -27,7 +26,7 @@ use elements::encode::serialize;
|
||||
|
||||
use crate::chain::Txid;
|
||||
use crate::config::{Config, VERSION_STRING};
|
||||
use crate::electrum::{get_electrum_height, ProtocolVersion, ServerFeatures};
|
||||
use crate::electrum::{get_electrum_height, ProtocolVersion};
|
||||
use crate::errors::*;
|
||||
use crate::metrics::{Gauge, HistogramOpts, HistogramVec, MetricOpts, Metrics};
|
||||
use crate::new_index::{Query, Utxo};
|
||||
@ -41,7 +40,7 @@ const PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(1, 4);
|
||||
const MAX_HEADERS: usize = 2016;
|
||||
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
use crate::electrum::DiscoveryManager;
|
||||
use crate::electrum::{DiscoveryManager, ServerFeatures};
|
||||
|
||||
// TODO: Sha256dHash should be a generic hash-container (since script hash is single SHA256)
|
||||
fn hash_from_value(val: Option<&Value>) -> Result<Sha256dHash> {
|
||||
@ -77,36 +76,6 @@ fn bool_from_value_or(val: Option<&Value>, name: &str, default: bool) -> Result<
|
||||
bool_from_value(val, name)
|
||||
}
|
||||
|
||||
/// Extracts the source socket address from a parsed PROXY protocol v1 header.
|
||||
fn proxy_v1_source(addresses: &ppp::v1::Addresses) -> Option<SocketAddr> {
|
||||
match addresses {
|
||||
ppp::v1::Addresses::Tcp4(ip) => Some(SocketAddr::new(
|
||||
IpAddr::V4(ip.source_address),
|
||||
ip.source_port,
|
||||
)),
|
||||
ppp::v1::Addresses::Tcp6(ip) => Some(SocketAddr::new(
|
||||
IpAddr::V6(ip.source_address),
|
||||
ip.source_port,
|
||||
)),
|
||||
ppp::v1::Addresses::Unknown => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts the source socket address from a parsed PROXY protocol v2 header.
|
||||
fn proxy_v2_source(addresses: &ppp::v2::Addresses) -> Option<SocketAddr> {
|
||||
match addresses {
|
||||
ppp::v2::Addresses::IPv4(ip) => Some(SocketAddr::new(
|
||||
IpAddr::V4(ip.source_address),
|
||||
ip.source_port,
|
||||
)),
|
||||
ppp::v2::Addresses::IPv6(ip) => Some(SocketAddr::new(
|
||||
IpAddr::V6(ip.source_address),
|
||||
ip.source_port,
|
||||
)),
|
||||
ppp::v2::Addresses::Unspecified | ppp::v2::Addresses::Unix(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: implement caching and delta updates
|
||||
fn get_status_hash(txs: Vec<(Txid, Option<BlockId>)>, query: &Query) -> Option<FullHash> {
|
||||
if txs.is_empty() {
|
||||
@ -153,36 +122,18 @@ struct Connection {
|
||||
chan: SyncChannel<Message>,
|
||||
stats: Arc<Stats>,
|
||||
txs_limit: usize,
|
||||
max_line_size: usize,
|
||||
max_subscriptions: usize,
|
||||
idle_timeout: u64,
|
||||
last_request_at: Instant,
|
||||
die_please: Option<Receiver<()>>,
|
||||
server_features: Arc<ServerFeatures>,
|
||||
haproxy_depth: usize,
|
||||
proxy_client: Option<SocketAddr>,
|
||||
connections_per_client: usize,
|
||||
client_counts: Arc<Mutex<HashMap<IpAddr, usize>>>,
|
||||
registered_ip: Option<IpAddr>,
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
discovery: Option<Arc<DiscoveryManager>>,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
query: Arc<Query>,
|
||||
stream: ConnectionStream,
|
||||
stats: Arc<Stats>,
|
||||
txs_limit: usize,
|
||||
max_line_size: usize,
|
||||
max_subscriptions: usize,
|
||||
idle_timeout: u64,
|
||||
die_please: Receiver<()>,
|
||||
server_features: Arc<ServerFeatures>,
|
||||
haproxy_depth: usize,
|
||||
connections_per_client: usize,
|
||||
client_counts: Arc<Mutex<HashMap<IpAddr, usize>>>,
|
||||
#[cfg(feature = "electrum-discovery")] discovery: Option<Arc<DiscoveryManager>>,
|
||||
) -> Connection {
|
||||
Connection {
|
||||
@ -193,17 +144,7 @@ impl Connection {
|
||||
chan: SyncChannel::new(10),
|
||||
stats,
|
||||
txs_limit,
|
||||
max_line_size,
|
||||
max_subscriptions,
|
||||
idle_timeout,
|
||||
last_request_at: Instant::now(),
|
||||
die_please: Some(die_please),
|
||||
server_features,
|
||||
haproxy_depth,
|
||||
proxy_client: None,
|
||||
connections_per_client,
|
||||
client_counts,
|
||||
registered_ip: None,
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
discovery,
|
||||
}
|
||||
@ -225,8 +166,13 @@ impl Connection {
|
||||
Ok(json!(self.query.config().electrum_banner.clone()))
|
||||
}
|
||||
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
fn server_features(&self) -> Result<Value> {
|
||||
Ok(json!(self.server_features.as_ref()))
|
||||
let discovery = self
|
||||
.discovery
|
||||
.as_ref()
|
||||
.chain_err(|| "discovery is disabled")?;
|
||||
Ok(json!(discovery.our_features()))
|
||||
}
|
||||
|
||||
fn server_donation_address(&self) -> Result<Value> {
|
||||
@ -348,16 +294,6 @@ impl Connection {
|
||||
fn blockchain_scripthash_subscribe(&mut self, params: &[Value]) -> Result<Value> {
|
||||
let script_hash = hash_from_value(params.first()).chain_err(|| "bad script_hash")?;
|
||||
|
||||
// Enforce per-client subscription limit (don't count re-subscriptions to the same hash)
|
||||
if !self.status_hashes.contains_key(&script_hash)
|
||||
&& self.status_hashes.len() >= self.max_subscriptions
|
||||
{
|
||||
bail!(
|
||||
"subscription limit reached ({} max per client)",
|
||||
self.max_subscriptions
|
||||
);
|
||||
}
|
||||
|
||||
let history_txids = get_history(&self.query, &script_hash[..], self.txs_limit)?;
|
||||
let status_hash = get_status_hash(history_txids, &self.query)
|
||||
.map_or(Value::Null, |h| json!(hex::encode(full_hash(&h[..]))));
|
||||
@ -372,16 +308,6 @@ impl Connection {
|
||||
Ok(status_hash)
|
||||
}
|
||||
|
||||
fn blockchain_scripthash_unsubscribe(&mut self, params: &[Value]) -> Result<Value> {
|
||||
let script_hash = hash_from_value(params.first()).chain_err(|| "bad script_hash")?;
|
||||
|
||||
let removed = self.status_hashes.remove(&script_hash).is_some();
|
||||
if removed {
|
||||
self.stats.subscriptions.dec();
|
||||
}
|
||||
Ok(Value::Bool(removed))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "liquid"))]
|
||||
fn blockchain_scripthash_get_balance(&self, params: &[Value]) -> Result<Value> {
|
||||
let script_hash = hash_from_value(params.first()).chain_err(|| "bad script_hash")?;
|
||||
@ -520,7 +446,6 @@ impl Connection {
|
||||
"blockchain.scripthash.get_history" => self.blockchain_scripthash_get_history(params),
|
||||
"blockchain.scripthash.listunspent" => self.blockchain_scripthash_listunspent(params),
|
||||
"blockchain.scripthash.subscribe" => self.blockchain_scripthash_subscribe(params),
|
||||
"blockchain.scripthash.unsubscribe" => self.blockchain_scripthash_unsubscribe(params),
|
||||
"blockchain.transaction.broadcast" => self.blockchain_transaction_broadcast(params),
|
||||
"blockchain.transaction.get" => self.blockchain_transaction_get(params),
|
||||
"blockchain.transaction.get_merkle" => self.blockchain_transaction_get_merkle(params),
|
||||
@ -531,8 +456,9 @@ impl Connection {
|
||||
"server.peers.subscribe" => self.server_peers_subscribe(),
|
||||
"server.ping" => Ok(Value::Null),
|
||||
"server.version" => self.server_version(),
|
||||
"server.features" => self.server_features(),
|
||||
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
"server.features" => self.server_features(),
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
"server.add_peer" => self.server_add_peer(params),
|
||||
|
||||
@ -608,99 +534,14 @@ impl Connection {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn close_idle_connection(&mut self, idle_for: Duration) {
|
||||
info!(
|
||||
"[{}] closing idle connection after {} seconds without requests (timeout: {} seconds)",
|
||||
self.client_string(),
|
||||
idle_for.as_secs(),
|
||||
self.idle_timeout,
|
||||
);
|
||||
self.chan.close();
|
||||
}
|
||||
|
||||
/// A human-readable identifier for the connected client, preferring the
|
||||
/// HAProxy-reported address (when present) over the direct peer address.
|
||||
fn client_string(&self) -> String {
|
||||
match self.proxy_client {
|
||||
Some(addr) => format!("{} via {}", addr, self.stream.addr_string()),
|
||||
None => self.stream.addr_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the PROXY-protocol parse result into the client address at the
|
||||
/// configured `electrum-haproxy-depth` layer. A depth of 0, a missing PROXY
|
||||
/// header, or a non-existent layer all leave the client unidentified.
|
||||
fn set_proxy_client(&mut self, addresses: Option<Vec<SocketAddr>>) {
|
||||
self.proxy_client = match (self.haproxy_depth, addresses) {
|
||||
(0, _) | (_, None) => None,
|
||||
(depth, Some(addrs)) => addrs.get(depth - 1).copied(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Registers this connection against its client key (the HAProxy-reported IP
|
||||
/// when available, otherwise the direct peer IP) and enforces the
|
||||
/// `electrum-connections-per-client` limit. Returns an error if the limit has
|
||||
/// already been reached, in which case the connection must be closed.
|
||||
fn register_client(&mut self) -> Result<()> {
|
||||
if self.connections_per_client == 0 {
|
||||
// Per-client limit disabled.
|
||||
return Ok(());
|
||||
}
|
||||
let key = match self
|
||||
.proxy_client
|
||||
.map(|addr| addr.ip())
|
||||
.or_else(|| self.stream.direct_ip())
|
||||
{
|
||||
Some(key) => key,
|
||||
// No usable client key (e.g. a unix socket with no PROXY header).
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let mut counts = self.client_counts.lock().unwrap();
|
||||
let count = counts.entry(key).or_insert(0);
|
||||
if *count >= self.connections_per_client {
|
||||
bail!(
|
||||
"too many connections from client {} ({} max per client)",
|
||||
key,
|
||||
self.connections_per_client
|
||||
);
|
||||
}
|
||||
*count += 1;
|
||||
self.registered_ip = Some(key);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Releases this connection's slot in the per-client connection counter.
|
||||
fn unregister_client(&mut self) {
|
||||
if let Some(key) = self.registered_ip.take() {
|
||||
let mut counts = self.client_counts.lock().unwrap();
|
||||
if let Some(count) = counts.get_mut(&key) {
|
||||
*count -= 1;
|
||||
if *count == 0 {
|
||||
counts.remove(&key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_replies(&mut self, shutdown: crossbeam_channel::Receiver<()>) -> Result<()> {
|
||||
let idle_timeout = Duration::from_secs(self.idle_timeout);
|
||||
loop {
|
||||
let elapsed = self.last_request_at.elapsed();
|
||||
if elapsed > idle_timeout {
|
||||
self.close_idle_connection(elapsed);
|
||||
return Ok(());
|
||||
}
|
||||
let remaining = idle_timeout.saturating_sub(elapsed);
|
||||
let idle_deadline = crossbeam_channel::after(remaining);
|
||||
|
||||
crossbeam_channel::select! {
|
||||
recv(self.chan.receiver()) -> msg => {
|
||||
let msg = msg.chain_err(|| "channel closed")?;
|
||||
trace!("RPC {:?}", msg);
|
||||
match msg {
|
||||
Message::Request(line) => {
|
||||
self.last_request_at = Instant::now();
|
||||
let result = self.handle_line(&line);
|
||||
self.send_values(&[result])?
|
||||
}
|
||||
@ -714,25 +555,12 @@ impl Connection {
|
||||
self.chan.close();
|
||||
return Ok(());
|
||||
}
|
||||
Message::Proxy(addresses) => {
|
||||
self.set_proxy_client(addresses);
|
||||
if let Err(e) = self.register_client() {
|
||||
info!("[{}] {}", self.client_string(), e);
|
||||
self.chan.close();
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
recv(shutdown) -> _ => {
|
||||
self.chan.close();
|
||||
return Ok(());
|
||||
}
|
||||
recv(idle_deadline) -> _ => {
|
||||
let idle_for = self.last_request_at.elapsed();
|
||||
self.close_idle_connection(idle_for);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -781,134 +609,18 @@ impl Connection {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads and parses any PROXY-protocol (HAProxy) headers found at the very
|
||||
/// start of the connection. Returns the source address reported by each
|
||||
/// proxy layer (outermost first), or `None` if no PROXY header was present,
|
||||
/// together with any bytes that were read past the header(s) and belong to
|
||||
/// the Electrum request stream.
|
||||
fn read_proxy_headers(
|
||||
stream: &mut ConnectionStream,
|
||||
) -> Result<(Option<Vec<SocketAddr>>, Vec<u8>)> {
|
||||
// Upper bound on how much we are willing to buffer while looking for
|
||||
// PROXY headers, to avoid unbounded memory use from a slow/malicious peer.
|
||||
const MAX_PROXY_HEADER_SIZE: usize = 4096;
|
||||
|
||||
enum Step {
|
||||
Parsed(usize, Option<SocketAddr>),
|
||||
NeedMore,
|
||||
Done,
|
||||
}
|
||||
|
||||
let mut buf: Vec<u8> = Vec::with_capacity(256);
|
||||
let mut addrs: Vec<SocketAddr> = Vec::new();
|
||||
let mut saw_proxy = false;
|
||||
let mut chunk = [0u8; 256];
|
||||
|
||||
loop {
|
||||
// Parse as many complete, stacked PROXY headers as the buffer allows.
|
||||
let need_more = loop {
|
||||
if buf.is_empty() {
|
||||
break true;
|
||||
}
|
||||
let step = match ppp::HeaderResult::parse(&buf) {
|
||||
ppp::HeaderResult::V2(Ok(header)) => {
|
||||
Step::Parsed(header.len(), proxy_v2_source(&header.addresses))
|
||||
}
|
||||
ppp::HeaderResult::V1(Ok(header)) => {
|
||||
Step::Parsed(header.header.len(), proxy_v1_source(&header.addresses))
|
||||
}
|
||||
other => {
|
||||
if other.is_incomplete() {
|
||||
Step::NeedMore
|
||||
} else {
|
||||
Step::Done
|
||||
}
|
||||
}
|
||||
};
|
||||
match step {
|
||||
Step::Parsed(consumed, src) => {
|
||||
saw_proxy = true;
|
||||
if let Some(src) = src {
|
||||
addrs.push(src);
|
||||
}
|
||||
if consumed == 0 || consumed > buf.len() {
|
||||
// Defensive: never spin forever on a degenerate parse.
|
||||
break false;
|
||||
}
|
||||
buf.drain(..consumed);
|
||||
}
|
||||
Step::NeedMore => break true,
|
||||
Step::Done => break false,
|
||||
}
|
||||
};
|
||||
|
||||
if !need_more {
|
||||
break;
|
||||
}
|
||||
if buf.len() > MAX_PROXY_HEADER_SIZE {
|
||||
bail!(
|
||||
"PROXY protocol header too large (exceeds {} bytes)",
|
||||
MAX_PROXY_HEADER_SIZE
|
||||
);
|
||||
}
|
||||
let n = stream
|
||||
.read(&mut chunk)
|
||||
.chain_err(|| "failed to read PROXY protocol header")?;
|
||||
if n == 0 {
|
||||
// EOF before another complete header; stop with what we have.
|
||||
break;
|
||||
}
|
||||
buf.extend_from_slice(&chunk[..n]);
|
||||
}
|
||||
|
||||
let result = if saw_proxy { Some(addrs) } else { None };
|
||||
Ok((result, buf))
|
||||
}
|
||||
|
||||
fn handle_requests(
|
||||
stream: ConnectionStream,
|
||||
mut reader: BufReader<ConnectionStream>,
|
||||
tx: crossbeam_channel::Sender<Message>,
|
||||
max_line_size: usize,
|
||||
) -> Result<()> {
|
||||
let mut stream = stream;
|
||||
|
||||
// Consume any PROXY-protocol (HAProxy) headers at the very start of the
|
||||
// connection before treating the stream as Electrum requests. We always
|
||||
// consume them — even when HAProxy support is disabled
|
||||
// (`electrum-haproxy-depth = 0`) — so that PROXY headers sent by an
|
||||
// accidentally-misconfigured upstream are stripped instead of corrupting
|
||||
// the Electrum request parser.
|
||||
//
|
||||
// Crucially, `read_proxy_headers` only ever buffers bytes it has already
|
||||
// read from the socket: when no PROXY header is present it returns those
|
||||
// bytes as `leftover` so the start of the first Electrum request is
|
||||
// preserved rather than discarded.
|
||||
//
|
||||
// The parsed addresses are forwarded over the channel; whether they are
|
||||
// actually used to identify the client is decided later based on the
|
||||
// configured `electrum-haproxy-depth` (a depth of 0 ignores them).
|
||||
let (proxy_addrs, leftover) = Connection::read_proxy_headers(&mut stream)?;
|
||||
tx.send(Message::Proxy(proxy_addrs))
|
||||
.chain_err(|| "channel closed")?;
|
||||
|
||||
let mut reader = BufReader::new(Cursor::new(leftover).chain(stream));
|
||||
loop {
|
||||
let mut line = Vec::<u8>::new();
|
||||
// Read up to max_line_size + 1 bytes to detect oversized lines
|
||||
let mut limited = (&mut reader).take((max_line_size as u64).saturating_add(1));
|
||||
limited
|
||||
reader
|
||||
.read_until(b'\n', &mut line)
|
||||
.chain_err(|| "failed to read a request")?;
|
||||
if line.is_empty() {
|
||||
tx.send(Message::Done).chain_err(|| "channel closed")?;
|
||||
return Ok(());
|
||||
} else if line.len() > max_line_size {
|
||||
let _ = tx.send(Message::Done);
|
||||
bail!(
|
||||
"request line too large ({} bytes, max is {})",
|
||||
line.len(),
|
||||
max_line_size
|
||||
)
|
||||
} else {
|
||||
if line.starts_with(&[22, 3, 1]) {
|
||||
// (very) naive SSL handshake detection
|
||||
@ -930,7 +642,7 @@ impl Connection {
|
||||
|
||||
pub fn run(mut self) {
|
||||
self.stats.clients.inc();
|
||||
let stream = self.stream.try_clone().expect("failed to clone TcpStream");
|
||||
let reader = BufReader::new(self.stream.try_clone().expect("failed to clone TcpStream"));
|
||||
let tx = self.chan.sender();
|
||||
|
||||
let die_please = self.die_please.take().unwrap();
|
||||
@ -948,14 +660,11 @@ impl Connection {
|
||||
let _ = reply_killer.send(());
|
||||
});
|
||||
|
||||
let max_line_size = self.max_line_size;
|
||||
let child = spawn_thread("reader", move || {
|
||||
Connection::handle_requests(stream, tx, max_line_size)
|
||||
});
|
||||
let child = spawn_thread("reader", || Connection::handle_requests(reader, tx));
|
||||
if let Err(e) = self.handle_replies(reply_receiver) {
|
||||
error!(
|
||||
"[{}] connection handling failed: {}",
|
||||
self.client_string(),
|
||||
self.stream.addr_string(),
|
||||
e.display_chain().to_string()
|
||||
);
|
||||
}
|
||||
@ -963,9 +672,8 @@ impl Connection {
|
||||
self.stats
|
||||
.subscriptions
|
||||
.sub(self.status_hashes.len() as i64);
|
||||
self.unregister_client();
|
||||
|
||||
let addr = self.client_string();
|
||||
let addr = self.stream.addr_string();
|
||||
debug!("[{}] shutting down connection", addr);
|
||||
// Drop the Arc so that the stream properly closes.
|
||||
drop(arc_stream);
|
||||
@ -1023,11 +731,6 @@ pub enum Message {
|
||||
Request(String),
|
||||
PeriodicUpdate,
|
||||
Done,
|
||||
/// The result of parsing zero or more PROXY-protocol (HAProxy) headers at
|
||||
/// the start of the connection. `None` means no PROXY header was present;
|
||||
/// `Some(addrs)` holds the source address reported by each proxy layer,
|
||||
/// outermost first.
|
||||
Proxy(Option<Vec<SocketAddr>>),
|
||||
}
|
||||
|
||||
pub enum Notification {
|
||||
@ -1116,10 +819,11 @@ impl RPC {
|
||||
|
||||
let notification = Channel::unbounded();
|
||||
|
||||
let server_features = {
|
||||
// Discovery is enabled when electrum-public-hosts is set
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
let discovery = config.electrum_public_hosts.clone().map(|hosts| {
|
||||
use crate::chain::genesis_hash;
|
||||
let hosts = config.electrum_public_hosts.clone().unwrap_or_default();
|
||||
Arc::new(ServerFeatures {
|
||||
let features = ServerFeatures {
|
||||
hosts,
|
||||
server_version: VERSION_STRING.clone(),
|
||||
genesis_hash: genesis_hash(config.network_type),
|
||||
@ -1127,15 +831,10 @@ impl RPC {
|
||||
protocol_max: PROTOCOL_VERSION,
|
||||
hash_function: "sha256".into(),
|
||||
pruning: None,
|
||||
})
|
||||
};
|
||||
|
||||
// Discovery is enabled when electrum-public-hosts is set
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
let discovery = config.electrum_public_hosts.as_ref().map(|_hosts| {
|
||||
};
|
||||
let discovery = Arc::new(DiscoveryManager::new(
|
||||
config.network_type,
|
||||
server_features.as_ref().clone(),
|
||||
features,
|
||||
PROTOCOL_VERSION,
|
||||
config.electrum_announce,
|
||||
config.tor_proxy,
|
||||
@ -1145,22 +844,12 @@ impl RPC {
|
||||
});
|
||||
|
||||
let txs_limit = config.electrum_txs_limit;
|
||||
let max_line_size = config.electrum_max_line_size;
|
||||
let max_subscriptions = config.electrum_max_subscriptions;
|
||||
let max_clients = config.electrum_max_clients;
|
||||
let idle_timeout = config.electrum_idle_timeout;
|
||||
let haproxy_depth = config.electrum_haproxy_depth;
|
||||
let connections_per_client = config.electrum_connections_per_client;
|
||||
|
||||
RPC {
|
||||
notification: notification.sender(),
|
||||
server: Some(spawn_thread("rpc", move || {
|
||||
let senders =
|
||||
Arc::new(Mutex::new(Vec::<crossbeam_channel::Sender<Message>>::new()));
|
||||
// Tracks the number of live connections per client (keyed by the
|
||||
// HAProxy-reported address when available, otherwise the peer IP).
|
||||
let client_counts: Arc<Mutex<HashMap<IpAddr, usize>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
let acceptor_shutdown = Channel::unbounded();
|
||||
let acceptor_shutdown_sender = acceptor_shutdown.sender();
|
||||
@ -1172,39 +861,15 @@ impl RPC {
|
||||
acceptor_shutdown_sender,
|
||||
);
|
||||
|
||||
let mut threads: HashMap<thread::ThreadId, (thread::JoinHandle<()>, Sender<()>)> =
|
||||
HashMap::new();
|
||||
let mut threads = HashMap::new();
|
||||
let (garbage_sender, garbage_receiver) = crossbeam_channel::unbounded();
|
||||
|
||||
while let Some(stream) = acceptor.receiver().recv().unwrap() {
|
||||
// Clean up finished threads before checking connection limit
|
||||
while let Ok(id) = garbage_receiver.try_recv() {
|
||||
if let Some((thread, killer)) = threads.remove(&id) {
|
||||
let _ = killer.send(());
|
||||
if let Err(error) = thread.join() {
|
||||
error!("failed to join {:?}: {:?}", id, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce maximum connection limit
|
||||
if threads.len() >= max_clients {
|
||||
warn!(
|
||||
"[{}] rejecting connection: max clients reached ({}/{})",
|
||||
stream.addr_string(),
|
||||
threads.len(),
|
||||
max_clients
|
||||
);
|
||||
let _ = stream.shutdown(Shutdown::Both);
|
||||
continue;
|
||||
}
|
||||
|
||||
let addr = stream.addr_string();
|
||||
// explicitely scope the shadowed variables for the new thread
|
||||
let query = Arc::clone(&query);
|
||||
let senders = Arc::clone(&senders);
|
||||
let stats = Arc::clone(&stats);
|
||||
let client_counts = Arc::clone(&client_counts);
|
||||
let garbage_sender = garbage_sender.clone();
|
||||
|
||||
// Kill the peers properly
|
||||
@ -1213,7 +878,6 @@ impl RPC {
|
||||
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
let discovery = discovery.clone();
|
||||
let server_features = Arc::clone(&server_features);
|
||||
|
||||
let spawned = spawn_thread("peer", move || {
|
||||
let addr = stream.addr_string();
|
||||
@ -1223,14 +887,7 @@ impl RPC {
|
||||
stream,
|
||||
stats,
|
||||
txs_limit,
|
||||
max_line_size,
|
||||
max_subscriptions,
|
||||
idle_timeout,
|
||||
peace_receiver,
|
||||
server_features,
|
||||
haproxy_depth,
|
||||
connections_per_client,
|
||||
client_counts,
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
discovery,
|
||||
);
|
||||
@ -1243,6 +900,15 @@ impl RPC {
|
||||
|
||||
trace!("[{}] spawned {:?}", addr, spawned.thread().id());
|
||||
threads.insert(spawned.thread().id(), (spawned, killer));
|
||||
while let Ok(id) = garbage_receiver.try_recv() {
|
||||
if let Some((thread, killer)) = threads.remove(&id) {
|
||||
trace!("[{}] joining {:?}", addr, id);
|
||||
let _ = killer.send(());
|
||||
if let Err(error) = thread.join() {
|
||||
error!("failed to join {:?}: {:?}", id, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Drop these
|
||||
drop(acceptor);
|
||||
@ -1404,15 +1070,6 @@ impl ConnectionStream {
|
||||
}
|
||||
}
|
||||
|
||||
/// The direct peer IP address, if this is a TCP connection. Unix-socket
|
||||
/// connections have no IP and return `None`.
|
||||
fn direct_ip(&self) -> Option<IpAddr> {
|
||||
match self {
|
||||
ConnectionStream::Tcp(_, a) => Some(a.ip()),
|
||||
ConnectionStream::Unix(..) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn try_clone(&self) -> std::io::Result<Self> {
|
||||
Ok(match self {
|
||||
ConnectionStream::Tcp(s, a) => ConnectionStream::Tcp(s.try_clone()?, *a),
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::{Arc, RwLock, RwLockReadGuard};
|
||||
|
||||
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::{bincode_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),
|
||||
@ -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
|
||||
};
|
||||
@ -184,17 +178,11 @@ pub fn index_confirmed_tx_assets(
|
||||
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()
|
||||
asset_history_row(&asset_id, confirmed_height, tx_position, info).into_row()
|
||||
}));
|
||||
|
||||
// the initial issuance is kept twice: once in the history index under I<asset><height><txid:vin>,
|
||||
@ -263,7 +251,7 @@ fn index_tx_assets(
|
||||
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((
|
||||
@ -282,7 +270,7 @@ 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,
|
||||
@ -414,7 +402,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");
|
||||
}
|
||||
|
||||
@ -528,7 +516,7 @@ where
|
||||
|
||||
// save updated stats to cache
|
||||
if let Some(lastblock) = lastblock {
|
||||
chain.store().cache_db().write(
|
||||
chain.store().cache_db().write_nocache(
|
||||
vec![asset_cache_row(asset_id, &newstats, &lastblock)],
|
||||
DBFlush::Enable,
|
||||
);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -1,40 +1,31 @@
|
||||
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)
|
||||
txout.pegout_data().filter(|pegout| {
|
||||
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::Testnet => Network::LiquidTestnet,
|
||||
BNetwork::Signet => return false,
|
||||
BNetwork::Regtest => Network::LiquidRegtest,
|
||||
})
|
||||
{
|
||||
Some(pegout)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -42,7 +33,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 +44,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,
|
||||
})
|
||||
|
||||
@ -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()
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -227,72 +192,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())
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
#![allow(unexpected_cfgs)]
|
||||
error_chain! {
|
||||
types {
|
||||
Error, ErrorKind, ResultExt, Result;
|
||||
|
||||
@ -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::{
|
||||
@ -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,7 +109,8 @@ 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
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
use bounded_vec_deque::BoundedVecDeque;
|
||||
use rocksdb;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::util::{bincode_util, Bytes};
|
||||
@ -23,7 +27,7 @@ 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> {
|
||||
@ -48,7 +52,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> {
|
||||
@ -84,7 +88,7 @@ impl<'a> ReverseScanGroupIterator<'a> {
|
||||
pub fn new(
|
||||
mut iters: Vec<ReverseScanIterator<'a>>,
|
||||
value_offset: usize,
|
||||
) -> ReverseScanGroupIterator<'a> {
|
||||
) -> ReverseScanGroupIterator {
|
||||
let mut next_rows: Vec<Option<DBRow>> = Vec::with_capacity(iters.len());
|
||||
for iter in &mut iters {
|
||||
let next = iter.next();
|
||||
@ -100,7 +104,7 @@ impl<'a> ReverseScanGroupIterator<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for ReverseScanGroupIterator<'_> {
|
||||
impl<'a> Iterator for ReverseScanGroupIterator<'a> {
|
||||
type Item = DBRow;
|
||||
|
||||
fn next(&mut self) -> Option<DBRow> {
|
||||
@ -134,11 +138,23 @@ impl Iterator for ReverseScanGroupIterator<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
type SingleBlockCache = HashSet<Vec<u8>>;
|
||||
type TipsCache = BoundedVecDeque<SingleBlockCache>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DB {
|
||||
db: rocksdb::DB,
|
||||
// BoundedVecDeque of most recent blocks
|
||||
// Outer Vec is a list of rocksdb keys to remove when reorged
|
||||
// Inner Vec is the key (a key is Vec<u8>)
|
||||
// It will automatically drop "blocks" that go over the bound.
|
||||
rollback_cache: RwLock<TipsCache>,
|
||||
rollback_active: AtomicBool,
|
||||
}
|
||||
|
||||
// 6 blocks should be enough
|
||||
const CACHE_CAPACITY: usize = 6;
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum DBFlush {
|
||||
Disable,
|
||||
@ -147,8 +163,13 @@ pub enum DBFlush {
|
||||
|
||||
impl DB {
|
||||
pub fn open(path: &Path, config: &Config) -> DB {
|
||||
let mut rollback_cache = BoundedVecDeque::with_capacity(CACHE_CAPACITY, CACHE_CAPACITY);
|
||||
rollback_cache.push_back(HashSet::new()); // last HashSet is "current block"
|
||||
let db = DB {
|
||||
db: open_raw_db(path, OpenMode::ReadWrite),
|
||||
db: open_raw_db(path),
|
||||
// TODO: Make the number of blocks configurable? 6 should be fine for mainnet
|
||||
rollback_cache: RwLock::new(rollback_cache),
|
||||
rollback_active: AtomicBool::new(false),
|
||||
};
|
||||
db.verify_compatibility(config);
|
||||
db
|
||||
@ -166,11 +187,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 +199,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 +211,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);
|
||||
|
||||
@ -205,7 +226,7 @@ impl DB {
|
||||
&self,
|
||||
prefixes: impl Iterator<Item = (Vec<u8>, Vec<u8>)>,
|
||||
value_offset: usize,
|
||||
) -> ReverseScanGroupIterator<'_> {
|
||||
) -> ReverseScanGroupIterator {
|
||||
let iters = prefixes
|
||||
.map(|(prefix, prefix_max)| {
|
||||
let mut iter = self.db.raw_iterator();
|
||||
@ -220,17 +241,123 @@ impl DB {
|
||||
ReverseScanGroupIterator::new(iters, value_offset)
|
||||
}
|
||||
|
||||
pub fn write(&self, mut rows: Vec<DBRow>, flush: DBFlush) {
|
||||
fn fill_cache(&self, key: &[u8]) {
|
||||
// Single letter keys tend to be related to versioning and tips
|
||||
// So do not cache as they don't need to be rolled back
|
||||
if key.len() < 2 {
|
||||
return;
|
||||
}
|
||||
self.with_cache(|cache| {
|
||||
cache.insert(key.to_owned());
|
||||
});
|
||||
}
|
||||
|
||||
fn with_cache<F>(&self, func: F)
|
||||
where
|
||||
F: FnOnce(&mut SingleBlockCache),
|
||||
{
|
||||
func(
|
||||
self.rollback_cache
|
||||
.write()
|
||||
.unwrap()
|
||||
.back_mut()
|
||||
.expect("Always one block"),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn tick_next_block(&self) {
|
||||
// Adding a new block's worth of cache
|
||||
// This will automatically drop the oldest block (HashSet)
|
||||
self.rollback_cache
|
||||
.write()
|
||||
.unwrap()
|
||||
.push_back(HashSet::new());
|
||||
}
|
||||
|
||||
/// Performs a rollback of `count` blocks, then ticks one block forward
|
||||
pub fn rollback(&self, mut count: usize) -> usize {
|
||||
if count == 0 {
|
||||
return 0;
|
||||
}
|
||||
let mut cache = self.rollback_cache.write().unwrap();
|
||||
while count > 0 {
|
||||
if let Some(block) = cache.pop_back() {
|
||||
debug!(
|
||||
"Rolling back DB cached block with {} entries @ {:?}",
|
||||
block.len(),
|
||||
self.db.path()
|
||||
);
|
||||
for key in block {
|
||||
// Ignore rocksdb errors, but log them
|
||||
let _ = self.db.delete(key).inspect_err(|err| {
|
||||
warn!("Error when deleting rocksdb rollback cache: {err}");
|
||||
});
|
||||
}
|
||||
count -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
cache.push_back(HashSet::new());
|
||||
count
|
||||
}
|
||||
|
||||
pub fn rollbacks_enabled(&self) -> bool {
|
||||
self.rollback_active
|
||||
.load(std::sync::atomic::Ordering::Acquire)
|
||||
}
|
||||
|
||||
pub fn disable_rollbacks(&self) {
|
||||
self.rollback_active
|
||||
.store(false, std::sync::atomic::Ordering::Release);
|
||||
}
|
||||
|
||||
pub fn enable_rollbacks(&self) {
|
||||
self.rollback_active
|
||||
.store(true, std::sync::atomic::Ordering::Release);
|
||||
}
|
||||
|
||||
pub fn write_nocache(&self, rows: Vec<DBRow>, flush: DBFlush) {
|
||||
self.write_blocks_nocache(vec![rows], flush);
|
||||
}
|
||||
|
||||
pub fn write_blocks(&self, blocks: Vec<Vec<DBRow>>, flush: DBFlush) {
|
||||
self.write_blocks_inner(blocks, flush, false)
|
||||
}
|
||||
|
||||
pub fn write_blocks_nocache(&self, blocks: Vec<Vec<DBRow>>, flush: DBFlush) {
|
||||
self.write_blocks_inner(blocks, flush, true)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn write_blocks_inner(&self, blocks: Vec<Vec<DBRow>>, flush: DBFlush, skip_cache: bool) {
|
||||
debug!(
|
||||
"writing {} rows to {:?}, flush={:?}",
|
||||
rows.len(),
|
||||
blocks.iter().map(|b| b.len()).sum::<usize>(),
|
||||
self.db,
|
||||
flush
|
||||
);
|
||||
rows.sort_unstable_by(|a, b| a.key.cmp(&b.key));
|
||||
let mut batch = rocksdb::WriteBatch::default();
|
||||
for row in rows {
|
||||
batch.put(&row.key, &row.value);
|
||||
for mut rows in blocks {
|
||||
rows.sort_unstable_by(|a, b| a.key.cmp(&b.key));
|
||||
if !skip_cache
|
||||
&& self
|
||||
.rollback_active
|
||||
.load(std::sync::atomic::Ordering::Acquire)
|
||||
{
|
||||
self.with_cache(|cache| {
|
||||
for row in &rows {
|
||||
cache.insert(row.key.clone());
|
||||
batch.put(&row.key, &row.value);
|
||||
}
|
||||
});
|
||||
// Special case: we should tick forward blocks
|
||||
self.tick_next_block();
|
||||
} else {
|
||||
for row in &rows {
|
||||
batch.put(&row.key, &row.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
let do_flush = match flush {
|
||||
DBFlush::Enable => true,
|
||||
@ -242,24 +369,27 @@ 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();
|
||||
}
|
||||
|
||||
pub fn put(&self, key: &[u8], value: &[u8]) {
|
||||
if self
|
||||
.rollback_active
|
||||
.load(std::sync::atomic::Ordering::Acquire)
|
||||
{
|
||||
self.fill_cache(key);
|
||||
}
|
||||
self.db.put(key, value).unwrap();
|
||||
}
|
||||
|
||||
pub fn put_sync(&self, key: &[u8], value: &[u8]) {
|
||||
if self
|
||||
.rollback_active
|
||||
.load(std::sync::atomic::Ordering::Acquire)
|
||||
{
|
||||
self.fill_cache(key);
|
||||
}
|
||||
let mut opts = rocksdb::WriteOptions::new();
|
||||
opts.set_sync(true);
|
||||
self.db.put_opt(key, value, &opts).unwrap();
|
||||
@ -290,17 +420,7 @@ 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> {
|
||||
pub fn open_raw_db<T: rocksdb::ThreadMode>(path: &Path) -> rocksdb::DBWithThreadMode<T> {
|
||||
debug!("opening DB at {:?}", path);
|
||||
let mut db_opts = rocksdb::Options::default();
|
||||
db_opts.create_if_missing(true);
|
||||
@ -318,13 +438,5 @@ pub fn open_raw_db<T: rocksdb::ThreadMode>(
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
rocksdb::DBWithThreadMode::<T>::open(&db_opts, path).expect("failed to open RocksDB")
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ use std::io::Cursor;
|
||||
use std::path::PathBuf;
|
||||
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,57 +42,6 @@ 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>,
|
||||
thread: thread::JoinHandle<()>,
|
||||
@ -138,7 +87,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();
|
||||
@ -290,7 +239,12 @@ fn parse_blocks(blob: Vec<u8>, magic: u32) -> Result<Vec<SizedBlock>> {
|
||||
cursor.set_position(end);
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
@ -12,7 +12,7 @@ use std::ops::Bound::{Excluded, Unbounded};
|
||||
use std::sync::{Arc, RwLock};
|
||||
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::*;
|
||||
@ -165,7 +165,7 @@ impl Mempool {
|
||||
// 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)
|
||||
last_seen_txid.map_or(false, |last_seen_txid| last_seen_txid != txid)
|
||||
})
|
||||
.skip(match last_seen_txid {
|
||||
Some(_) => 1, // skip the last_seen_txid itself
|
||||
@ -196,7 +196,7 @@ impl Mempool {
|
||||
// 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)
|
||||
last_seen_txid.map_or(false, |last_seen_txid| last_seen_txid != txid)
|
||||
})
|
||||
.skip(match last_seen_txid {
|
||||
Some(_) => 1, // skip the last_seen_txid itself
|
||||
@ -387,7 +387,7 @@ impl Mempool {
|
||||
}
|
||||
|
||||
pub fn unique_txids(&self) -> HashSet<Txid> {
|
||||
HashSet::from_iter(self.txstore.keys().cloned())
|
||||
return HashSet::from_iter(self.txstore.keys().cloned());
|
||||
}
|
||||
|
||||
pub fn update(mempool: &RwLock<Mempool>, daemon: &Daemon) -> Result<()> {
|
||||
@ -481,7 +481,7 @@ impl Mempool {
|
||||
let mut txids = Vec::with_capacity(txs.len());
|
||||
// Phase 1: add to txstore
|
||||
for tx in txs {
|
||||
let txid = tx.get_txid();
|
||||
let txid = tx.txid();
|
||||
// Only push if it doesn't already exist.
|
||||
// This is important now that update doesn't lock during
|
||||
// the entire function body.
|
||||
@ -526,10 +526,7 @@ impl Mempool {
|
||||
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,10 +534,6 @@ 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 {
|
||||
@ -548,7 +541,7 @@ impl Mempool {
|
||||
vin: input_index,
|
||||
prev_txid: full_hash(&txi.previous_output.txid[..]),
|
||||
prev_vout: txi.previous_output.vout,
|
||||
value,
|
||||
value: prevout.value,
|
||||
}),
|
||||
)
|
||||
});
|
||||
@ -562,16 +555,12 @@ 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,
|
||||
value: txo.value,
|
||||
}),
|
||||
)
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -4,7 +4,7 @@ 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::errors::*;
|
||||
@ -14,7 +14,7 @@ use crate::util::{is_spendable, BlockId, Bytes, TransactionStatus};
|
||||
#[cfg(feature = "liquid")]
|
||||
use crate::{
|
||||
chain::{asset::AssetRegistryLock, AssetId},
|
||||
elements::{lookup_asset, AssetMeta, AssetRegistry, AssetSorting, LiquidAsset},
|
||||
elements::{lookup_asset, AssetRegistry, AssetSorting, LiquidAsset},
|
||||
};
|
||||
|
||||
const FEE_ESTIMATES_TTL: u64 = 60; // seconds
|
||||
@ -65,7 +65,7 @@ impl Query {
|
||||
self.config.network_type
|
||||
}
|
||||
|
||||
pub fn mempool(&self) -> RwLockReadGuard<'_, Mempool> {
|
||||
pub fn mempool(&self) -> RwLockReadGuard<Mempool> {
|
||||
self.mempool.read().unwrap()
|
||||
}
|
||||
|
||||
@ -161,7 +161,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()
|
||||
@ -271,15 +271,6 @@ impl Query {
|
||||
)
|
||||
}
|
||||
|
||||
#[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())
|
||||
}
|
||||
|
||||
#[cfg(feature = "liquid")]
|
||||
pub fn list_registry_assets(
|
||||
&self,
|
||||
@ -307,27 +298,4 @@ impl Query {
|
||||
.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())
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
use bitcoin::hashes::sha256d::Hash as Sha256dHash;
|
||||
#[cfg(not(feature = "liquid"))]
|
||||
use bitcoin::merkle_tree::MerkleBlock;
|
||||
#[cfg(not(feature = "liquid"))]
|
||||
use bitcoin::util::merkleblock::MerkleBlock;
|
||||
use bitcoin::VarInt;
|
||||
use itertools::Itertools;
|
||||
use rayon::prelude::*;
|
||||
@ -11,7 +10,7 @@ use sha2::{Digest, Sha256};
|
||||
use bitcoin::consensus::encode::{deserialize, serialize};
|
||||
#[cfg(feature = "liquid")]
|
||||
use elements::{
|
||||
encode::{deserialize, serialize, VarInt},
|
||||
encode::{deserialize, serialize},
|
||||
AssetId,
|
||||
};
|
||||
|
||||
@ -21,7 +20,7 @@ use std::path::Path;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use crate::chain::{
|
||||
BlockHash, BlockHeader, Network, OutPoint, Script, Transaction, TxOut, Txid, TxidCompat, Value,
|
||||
BlockHash, BlockHeader, Network, OutPoint, Script, Transaction, TxOut, Txid, Value,
|
||||
};
|
||||
use crate::config::Config;
|
||||
use crate::daemon::Daemon;
|
||||
@ -38,7 +37,7 @@ use crate::new_index::fetch::{start_fetcher, BlockEntry, FetchFrom};
|
||||
#[cfg(feature = "liquid")]
|
||||
use crate::elements::{asset, peg};
|
||||
|
||||
use super::{db::ReverseScanGroupIterator, fetch::bitcoind_sequential_fetcher};
|
||||
use super::db::ReverseScanGroupIterator;
|
||||
|
||||
const MIN_HISTORY_ITEMS_TO_CACHE: usize = 100;
|
||||
|
||||
@ -87,6 +86,27 @@ impl Store {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick_next_block(&self) {
|
||||
self.txstore_db.tick_next_block();
|
||||
self.history_db.tick_next_block();
|
||||
self.cache_db.tick_next_block();
|
||||
}
|
||||
|
||||
pub fn enable_rollback_cache(&self) {
|
||||
self.txstore_db.enable_rollbacks();
|
||||
self.history_db.enable_rollbacks();
|
||||
self.cache_db.enable_rollbacks();
|
||||
}
|
||||
|
||||
pub fn rollback(&self, count: usize) {
|
||||
let mut leftover = 0;
|
||||
leftover += self.txstore_db.rollback(count);
|
||||
leftover += self.history_db.rollback(count);
|
||||
leftover += self.cache_db.rollback(count);
|
||||
if leftover > 0 {
|
||||
warn!("Rolling back all DB caches missed {count} blocks. Re-orged duplicates might still be active in the DB.")
|
||||
}
|
||||
}
|
||||
pub fn txstore_db(&self) -> &DB {
|
||||
&self.txstore_db
|
||||
}
|
||||
@ -210,22 +230,6 @@ pub struct ChainQuery {
|
||||
network: Network,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Operation {
|
||||
AddBlocks,
|
||||
DeleteBlocks,
|
||||
DeleteBlocksWithHistory(crossbeam_channel::Sender<[u8; 32]>),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Operation {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
Operation::AddBlocks => "Adding",
|
||||
Operation::DeleteBlocks | Operation::DeleteBlocksWithHistory(_) => "Deleting",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: &[Block] should be an iterator / a queue.
|
||||
impl Indexer {
|
||||
pub fn open(store: Arc<Store>, from: FetchFrom, config: &Config, metrics: &Metrics) -> Self {
|
||||
@ -285,73 +289,21 @@ impl Indexer {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn reorg(&self, reorged: Vec<HeaderEntry>, daemon: &Daemon) -> Result<()> {
|
||||
if reorged.len() > 10 {
|
||||
warn!(
|
||||
"reorg of over 10 blocks ({}) detected! Wonky stuff might happen!",
|
||||
reorged.len()
|
||||
);
|
||||
}
|
||||
// This channel holds a Vec of [u8; 32] scripts found in the blocks (with duplicates)
|
||||
// if we reorg the whole mainnet chain it should come out to about 145 GB of memory.
|
||||
let (tx, rx) = crossbeam_channel::unbounded();
|
||||
// Delete history_db
|
||||
bitcoind_sequential_fetcher(daemon, reorged.clone())?
|
||||
.map(|blocks| self.index(&blocks, Operation::DeleteBlocksWithHistory(tx.clone())));
|
||||
// Delete txstore
|
||||
bitcoind_sequential_fetcher(daemon, reorged)?
|
||||
.map(|blocks| self.add(&blocks, Operation::DeleteBlocks));
|
||||
// All senders must be dropped for receiver iterator to finish
|
||||
drop(tx);
|
||||
|
||||
// All senders are dropped by now, so the receiver will iterate until the
|
||||
// end of the unbounded queue.
|
||||
let scripts = rx.into_iter().collect::<HashSet<_>>();
|
||||
for script in scripts {
|
||||
// cancel the script cache DB for these scripts. They might get incorrect data mixed in.
|
||||
self.store.cache_db.delete(vec![
|
||||
StatsCacheRow::key(&script),
|
||||
UtxoCacheRow::key(&script),
|
||||
#[cfg(feature = "liquid")]
|
||||
[b"z", &script[..]].concat(), // asset cache key
|
||||
]);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update(&mut self, daemon: &Daemon) -> Result<BlockHash> {
|
||||
let daemon = daemon.reconnect()?;
|
||||
let tip = daemon.getbestblockhash()?;
|
||||
let new_headers = self.get_new_headers(&daemon, &tip)?;
|
||||
|
||||
// Must rollback blocks before rolling forward
|
||||
// Deal with re-orgs before indexing
|
||||
let headers_len = {
|
||||
let mut headers = self.store.indexed_headers.write().unwrap();
|
||||
let (reorged, rollback_tip) = headers.apply(new_headers.clone());
|
||||
let reorged = headers.apply(&new_headers);
|
||||
assert_eq!(tip, *headers.tip());
|
||||
let headers_len = headers.len();
|
||||
drop(headers);
|
||||
|
||||
// reorg happened
|
||||
if !reorged.is_empty() {
|
||||
// We should rollback the tip blockhash first in case something crashes during rollback
|
||||
// or before the next block appears and sets the new tip.
|
||||
match rollback_tip {
|
||||
Some(rb_tip) => {
|
||||
debug!("updating reorged tip to {:?}", rb_tip);
|
||||
self.store.txstore_db.put_sync(b"t", &serialize(&rb_tip));
|
||||
}
|
||||
None => {
|
||||
// This should only happen on regtest or some weird networks.
|
||||
error!("Rollback to genesis block detected!!! (rollback to height 0)");
|
||||
// There is no tip anymore.
|
||||
self.store.txstore_db.delete(vec![b"t".into()]);
|
||||
}
|
||||
}
|
||||
|
||||
self.reorg(reorged, &daemon)?;
|
||||
self.store.rollback(reorged.len());
|
||||
}
|
||||
|
||||
headers_len
|
||||
headers.len()
|
||||
};
|
||||
|
||||
let to_add = self.headers_to_add(&new_headers);
|
||||
@ -360,8 +312,7 @@ impl Indexer {
|
||||
to_add.len(),
|
||||
self.from
|
||||
);
|
||||
start_fetcher(self.from, &daemon, to_add)?
|
||||
.map(|blocks| self.add(&blocks, Operation::AddBlocks));
|
||||
start_fetcher(self.from, &daemon, to_add)?.map(|blocks| self.add(&blocks));
|
||||
self.start_auto_compactions(&self.store.txstore_db);
|
||||
|
||||
let to_index = self.headers_to_index(&new_headers);
|
||||
@ -370,8 +321,7 @@ impl Indexer {
|
||||
to_index.len(),
|
||||
self.from
|
||||
);
|
||||
start_fetcher(self.from, &daemon, to_index)?
|
||||
.map(|blocks| self.index(&blocks, Operation::AddBlocks));
|
||||
start_fetcher(self.from, &daemon, to_index)?.map(|blocks| self.index(&blocks));
|
||||
self.start_auto_compactions(&self.store.history_db);
|
||||
|
||||
if let DBFlush::Disable = self.flush {
|
||||
@ -384,8 +334,16 @@ impl Indexer {
|
||||
// update the synced tip *after* the new data is flushed to disk
|
||||
debug!("updating synced tip to {:?}", tip);
|
||||
self.store.txstore_db.put_sync(b"t", &serialize(&tip));
|
||||
// Ticking cache DB "block every time we update"
|
||||
// This means that each "block" essentially contains the
|
||||
// cache updates between each update() call, and we will
|
||||
// rollback more cache_db than other DBs when rolling back
|
||||
// but this is just a cache anyway.
|
||||
// We'd rather not have bad data in the cache.
|
||||
self.store.cache_db.tick_next_block();
|
||||
|
||||
if let FetchFrom::BlkFiles = self.from {
|
||||
self.store.enable_rollback_cache();
|
||||
self.from = FetchFrom::Bitcoind;
|
||||
}
|
||||
|
||||
@ -394,82 +352,52 @@ impl Indexer {
|
||||
Ok(tip)
|
||||
}
|
||||
|
||||
fn add(&self, blocks: &[BlockEntry], op: Operation) {
|
||||
debug!("{} {} blocks to Indexer", op, blocks.len());
|
||||
let write_label = match &op {
|
||||
Operation::AddBlocks => "add_write",
|
||||
_ => "delete_write",
|
||||
};
|
||||
|
||||
fn add(&self, blocks: &[BlockEntry]) {
|
||||
debug!("Adding {} blocks to Indexer", blocks.len());
|
||||
// TODO: skip orphaned blocks?
|
||||
let rows = {
|
||||
let _timer = self.start_timer("add_process");
|
||||
add_blocks(blocks, &self.iconfig)
|
||||
};
|
||||
{
|
||||
let _timer = self.start_timer(write_label);
|
||||
if let Operation::AddBlocks = op {
|
||||
self.store.txstore_db.write(rows, self.flush);
|
||||
} else {
|
||||
self.store
|
||||
.txstore_db
|
||||
.delete(rows.into_iter().map(|r| r.key).collect());
|
||||
}
|
||||
let _timer = self.start_timer("add_write");
|
||||
self.store.txstore_db.write_blocks(rows, self.flush);
|
||||
}
|
||||
|
||||
if let Operation::AddBlocks = op {
|
||||
self.store
|
||||
.added_blockhashes
|
||||
.write()
|
||||
.unwrap()
|
||||
.extend(blocks.iter().map(|b| {
|
||||
if b.entry.height() % 10_000 == 0 {
|
||||
info!("Tx indexing is up to height={}", b.entry.height());
|
||||
}
|
||||
b.entry.hash()
|
||||
}));
|
||||
} else {
|
||||
let mut added_blockhashes = self.store.added_blockhashes.write().unwrap();
|
||||
for b in blocks {
|
||||
added_blockhashes.remove(b.entry.hash());
|
||||
}
|
||||
}
|
||||
self.store
|
||||
.added_blockhashes
|
||||
.write()
|
||||
.unwrap()
|
||||
.extend(blocks.iter().map(|b| {
|
||||
if b.entry.height() % 10_000 == 0 {
|
||||
info!("Tx indexing is up to height={}", b.entry.height());
|
||||
}
|
||||
b.entry.hash()
|
||||
}));
|
||||
}
|
||||
|
||||
fn index(&self, blocks: &[BlockEntry], op: Operation) {
|
||||
debug!("Indexing ({}) {} blocks with Indexer", op, blocks.len());
|
||||
fn index(&self, blocks: &[BlockEntry]) {
|
||||
debug!("Indexing {} blocks with Indexer", blocks.len());
|
||||
let previous_txos_map = {
|
||||
let _timer = self.start_timer("index_lookup");
|
||||
if matches!(op, Operation::AddBlocks) {
|
||||
lookup_txos(&self.store.txstore_db, &get_previous_txos(blocks), false)
|
||||
} else {
|
||||
lookup_txos_sequential(&self.store.txstore_db, &get_previous_txos(blocks), false)
|
||||
}
|
||||
lookup_txos(&self.store.txstore_db, &get_previous_txos(blocks), false)
|
||||
};
|
||||
let rows = {
|
||||
let _timer = self.start_timer("index_process");
|
||||
if let Operation::AddBlocks = op {
|
||||
let added_blockhashes = self.store.added_blockhashes.read().unwrap();
|
||||
for b in blocks {
|
||||
if b.entry.height() % 10_000 == 0 {
|
||||
info!("History indexing is up to height={}", b.entry.height());
|
||||
}
|
||||
let blockhash = b.entry.hash();
|
||||
// TODO: replace by lookup into txstore_db?
|
||||
if !added_blockhashes.contains(blockhash) {
|
||||
panic!("cannot index block {} (missing from store)", blockhash);
|
||||
}
|
||||
let added_blockhashes = self.store.added_blockhashes.read().unwrap();
|
||||
for b in blocks {
|
||||
if b.entry.height() % 10_000 == 0 {
|
||||
info!("History indexing is up to height={}", b.entry.height());
|
||||
}
|
||||
let blockhash = b.entry.hash();
|
||||
// TODO: replace by lookup into txstore_db?
|
||||
if !added_blockhashes.contains(blockhash) {
|
||||
panic!("cannot index block {} (missing from store)", blockhash);
|
||||
}
|
||||
}
|
||||
index_blocks(blocks, &previous_txos_map, &self.iconfig, &op)
|
||||
index_blocks(blocks, &previous_txos_map, &self.iconfig)
|
||||
};
|
||||
if let Operation::AddBlocks = op {
|
||||
self.store.history_db.write(rows, self.flush);
|
||||
} else {
|
||||
self.store
|
||||
.history_db
|
||||
.delete(rows.into_iter().map(|r| r.key).collect());
|
||||
}
|
||||
self.store.history_db.write_blocks(rows, self.flush);
|
||||
}
|
||||
}
|
||||
|
||||
@ -610,12 +538,7 @@ impl ChainQuery {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn history_iter_scan(
|
||||
&self,
|
||||
code: u8,
|
||||
hash: &[u8],
|
||||
start_height: usize,
|
||||
) -> ScanIterator<'_> {
|
||||
pub fn history_iter_scan(&self, code: u8, hash: &[u8], start_height: usize) -> ScanIterator {
|
||||
self.store.history_db.iter_scan_from(
|
||||
&TxHistoryRow::filter(code, hash),
|
||||
&TxHistoryRow::prefix_height(code, hash, start_height as u32),
|
||||
@ -626,7 +549,7 @@ impl ChainQuery {
|
||||
code: u8,
|
||||
hash: &[u8],
|
||||
start_height: Option<u32>,
|
||||
) -> ReverseScanIterator<'_> {
|
||||
) -> ReverseScanIterator {
|
||||
self.store.history_db.iter_scan_reverse(
|
||||
&TxHistoryRow::filter(code, hash),
|
||||
&start_height.map_or(TxHistoryRow::prefix_end(code, hash), |start_height| {
|
||||
@ -639,7 +562,7 @@ impl ChainQuery {
|
||||
code: u8,
|
||||
hashes: &[[u8; 32]],
|
||||
start_height: Option<u32>,
|
||||
) -> ReverseScanGroupIterator<'_> {
|
||||
) -> ReverseScanGroupIterator {
|
||||
self.store.history_db.iter_scan_group_reverse(
|
||||
hashes.iter().map(|hash| {
|
||||
let prefix = TxHistoryRow::filter(code, &hash[..]);
|
||||
@ -669,11 +592,11 @@ impl ChainQuery {
|
||||
.unique_by(|(txid, info, _)| (*txid, info.get_vin_or_vout(), info.has_vin()))
|
||||
.skip_while(|(txid, _, _)| {
|
||||
// skip until we reach the last_seen_txid
|
||||
last_seen_txid.is_some_and(|last_seen_txid| last_seen_txid != txid)
|
||||
last_seen_txid.map_or(false, |last_seen_txid| last_seen_txid != txid)
|
||||
})
|
||||
.skip_while(|(txid, _, _)| {
|
||||
// skip the last_seen_txid itself
|
||||
last_seen_txid == Some(txid)
|
||||
last_seen_txid.map_or(false, |last_seen_txid| last_seen_txid == txid)
|
||||
})
|
||||
.filter_map(|(txid, info, tx_position)| {
|
||||
self.tx_confirming_block(&txid)
|
||||
@ -801,7 +724,7 @@ impl ChainQuery {
|
||||
last_seen_txid: Option<&'a Txid>,
|
||||
start_height: Option<u32>,
|
||||
limit: usize,
|
||||
) -> impl rayon::iter::ParallelIterator<Item = Result<(Transaction, BlockId, u16)>> + 'a {
|
||||
) -> impl rayon::iter::ParallelIterator<Item = Result<(Transaction, BlockId)>> + 'a {
|
||||
// scripthash lookup
|
||||
self._history(b'H', scripthash, last_seen_txid, start_height, limit)
|
||||
}
|
||||
@ -819,27 +742,24 @@ impl ChainQuery {
|
||||
last_seen_txid: Option<&'a Txid>,
|
||||
start_height: Option<u32>,
|
||||
limit: usize,
|
||||
) -> impl rayon::iter::ParallelIterator<Item = Result<(Transaction, BlockId, u16)>> + 'a {
|
||||
) -> impl rayon::iter::ParallelIterator<Item = Result<(Transaction, BlockId)>> + 'a {
|
||||
let _timer_scan = self.start_timer("history");
|
||||
|
||||
self.lookup_txns(
|
||||
self.history_iter_scan_reverse(code, hash, start_height)
|
||||
.map(TxHistoryRow::from_row)
|
||||
// XXX: unique_by() requires keeping an in-memory list of all txids, can we avoid that?
|
||||
.unique_by(|row| row.get_txid())
|
||||
.map(|row| TxHistoryRow::from_row(row).get_txid())
|
||||
// XXX: unique() requires keeping an in-memory list of all txids, can we avoid that?
|
||||
.unique()
|
||||
// TODO seek directly to last seen tx without reading earlier rows
|
||||
.skip_while(move |row| {
|
||||
.skip_while(move |txid| {
|
||||
// skip until we reach the last_seen_txid
|
||||
last_seen_txid.is_some_and(|last_seen_txid| last_seen_txid != &row.get_txid())
|
||||
last_seen_txid.map_or(false, |last_seen_txid| last_seen_txid != txid)
|
||||
})
|
||||
.skip(match last_seen_txid {
|
||||
Some(_) => 1, // skip the last_seen_txid itself
|
||||
None => 0,
|
||||
})
|
||||
.filter_map(move |row| {
|
||||
self.tx_confirming_block(&row.get_txid())
|
||||
.map(|b| (row.get_txid(), b, row.get_tx_position()))
|
||||
}),
|
||||
.filter_map(move |txid| self.tx_confirming_block(&txid).map(|b| (txid, b))),
|
||||
limit,
|
||||
)
|
||||
}
|
||||
@ -865,7 +785,7 @@ impl ChainQuery {
|
||||
last_seen_txid: Option<&'a Txid>,
|
||||
start_height: Option<u32>,
|
||||
limit: usize,
|
||||
) -> impl rayon::iter::ParallelIterator<Item = Result<(Transaction, BlockId, u16)>> + 'a {
|
||||
) -> impl rayon::iter::ParallelIterator<Item = Result<(Transaction, BlockId)>> + 'a {
|
||||
// scripthash lookup
|
||||
self._history_group(b'H', scripthashes, last_seen_txid, start_height, limit)
|
||||
}
|
||||
@ -887,28 +807,25 @@ impl ChainQuery {
|
||||
last_seen_txid: Option<&'a Txid>,
|
||||
start_height: Option<u32>,
|
||||
limit: usize,
|
||||
) -> impl rayon::iter::ParallelIterator<Item = Result<(Transaction, BlockId, u16)>> + 'a {
|
||||
) -> impl rayon::iter::ParallelIterator<Item = Result<(Transaction, BlockId)>> + 'a {
|
||||
debug!("limit {} | last_seen {:?}", limit, last_seen_txid);
|
||||
let _timer_scan = self.start_timer("history_group");
|
||||
|
||||
self.lookup_txns(
|
||||
self.history_iter_scan_group_reverse(code, hashes, start_height)
|
||||
.map(TxHistoryRow::from_row)
|
||||
// XXX: unique_by() requires keeping an in-memory list of all txids, can we avoid that?
|
||||
.unique_by(|row| row.get_txid())
|
||||
.skip_while(move |row| {
|
||||
.map(|row| TxHistoryRow::from_row(row).get_txid())
|
||||
// XXX: unique() requires keeping an in-memory list of all txids, can we avoid that?
|
||||
.unique()
|
||||
.skip_while(move |txid| {
|
||||
// we already seeked to the last txid at this height
|
||||
// now skip just past the last_seen_txid itself
|
||||
last_seen_txid.is_some_and(|last_seen_txid| last_seen_txid != &row.get_txid())
|
||||
last_seen_txid.map_or(false, |last_seen_txid| last_seen_txid != txid)
|
||||
})
|
||||
.skip(match last_seen_txid {
|
||||
Some(_) => 1, // skip the last_seen_txid itself
|
||||
None => 0,
|
||||
})
|
||||
.filter_map(move |row| {
|
||||
self.tx_confirming_block(&row.get_txid())
|
||||
.map(|b| (row.get_txid(), b, row.get_tx_position()))
|
||||
}),
|
||||
.filter_map(move |txid| self.tx_confirming_block(&txid).map(|b| (txid, b))),
|
||||
limit,
|
||||
)
|
||||
}
|
||||
@ -940,7 +857,7 @@ impl ChainQuery {
|
||||
// save updated utxo set to cache
|
||||
if let Some(lastblock) = lastblock {
|
||||
if had_cache || processed_items > MIN_HISTORY_ITEMS_TO_CACHE {
|
||||
self.store.cache_db.write(
|
||||
self.store.cache_db.write_nocache(
|
||||
vec![UtxoCacheRow::new(scripthash, &newutxos, &lastblock).into_row()],
|
||||
flush,
|
||||
);
|
||||
@ -1048,7 +965,7 @@ impl ChainQuery {
|
||||
// save updated stats to cache
|
||||
if let Some(lastblock) = lastblock {
|
||||
if newstats.funded_txo_count + newstats.spent_txo_count > MIN_HISTORY_ITEMS_TO_CACHE {
|
||||
self.store.cache_db.write(
|
||||
self.store.cache_db.write_nocache(
|
||||
vec![StatsCacheRow::new(scripthash, &newstats, &lastblock).into_row()],
|
||||
flush,
|
||||
);
|
||||
@ -1213,19 +1130,18 @@ impl ChainQuery {
|
||||
&'a self,
|
||||
txids: I,
|
||||
take: usize,
|
||||
) -> impl rayon::iter::ParallelIterator<Item = Result<(Transaction, BlockId, u16)>> + 'a
|
||||
) -> impl rayon::iter::ParallelIterator<Item = Result<(Transaction, BlockId)>> + 'a
|
||||
where
|
||||
I: Iterator<Item = (Txid, BlockId, u16)> + Send + rayon::iter::ParallelBridge + 'a,
|
||||
I: Iterator<Item = (Txid, BlockId)> + Send + rayon::iter::ParallelBridge + 'a,
|
||||
{
|
||||
txids
|
||||
.take(take)
|
||||
.par_bridge()
|
||||
.map(move |(txid, blockid, tx_position)| -> Result<_> {
|
||||
.map(move |(txid, blockid)| -> Result<_> {
|
||||
Ok((
|
||||
self.lookup_txn(&txid, Some(&blockid.hash))
|
||||
.chain_err(|| "missing tx")?,
|
||||
blockid,
|
||||
tx_position,
|
||||
))
|
||||
})
|
||||
}
|
||||
@ -1234,7 +1150,7 @@ impl ChainQuery {
|
||||
let _timer = self.start_timer("lookup_txn");
|
||||
self.lookup_raw_txn(txid, blockhash).map(|rawtx| {
|
||||
let txn: Transaction = deserialize(&rawtx).expect("failed to parse Transaction");
|
||||
assert_eq!(*txid, txn.get_txid());
|
||||
assert_eq!(*txid, txn.txid());
|
||||
txn
|
||||
})
|
||||
}
|
||||
@ -1343,7 +1259,7 @@ impl ChainQuery {
|
||||
asset_id: &'a AssetId,
|
||||
last_seen_txid: Option<&'a Txid>,
|
||||
limit: usize,
|
||||
) -> impl rayon::iter::ParallelIterator<Item = Result<(Transaction, BlockId, u16)>> + 'a {
|
||||
) -> impl rayon::iter::ParallelIterator<Item = Result<(Transaction, BlockId)>> + 'a {
|
||||
self._history(
|
||||
b'I',
|
||||
&asset_id.into_inner()[..],
|
||||
@ -1377,7 +1293,7 @@ fn load_blockheaders(db: &DB) -> HashMap<BlockHash, BlockHeader> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn add_blocks(block_entries: &[BlockEntry], iconfig: &IndexerConfig) -> Vec<DBRow> {
|
||||
fn add_blocks(block_entries: &[BlockEntry], iconfig: &IndexerConfig) -> Vec<Vec<DBRow>> {
|
||||
// persist individual transactions:
|
||||
// T{txid} → {rawtx}
|
||||
// C{txid}{blockhash}{height} →
|
||||
@ -1391,7 +1307,7 @@ fn add_blocks(block_entries: &[BlockEntry], iconfig: &IndexerConfig) -> Vec<DBRo
|
||||
.map(|b| {
|
||||
let mut rows = vec![];
|
||||
let blockhash = full_hash(&b.entry.hash()[..]);
|
||||
let txids: Vec<Txid> = b.block.txdata.iter().map(|tx| tx.get_txid()).collect();
|
||||
let txids: Vec<Txid> = b.block.txdata.iter().map(|tx| tx.txid()).collect();
|
||||
for tx in &b.block.txdata {
|
||||
add_transaction(tx, blockhash, &mut rows, iconfig);
|
||||
}
|
||||
@ -1405,7 +1321,6 @@ fn add_blocks(block_entries: &[BlockEntry], iconfig: &IndexerConfig) -> Vec<DBRo
|
||||
rows.push(BlockRow::new_done(blockhash).into_row()); // mark block as "added"
|
||||
rows
|
||||
})
|
||||
.flatten()
|
||||
.collect()
|
||||
}
|
||||
|
||||
@ -1421,7 +1336,7 @@ fn add_transaction(
|
||||
rows.push(TxRow::new(tx).into_row());
|
||||
}
|
||||
|
||||
let txid = full_hash(&tx.get_txid()[..]);
|
||||
let txid = full_hash(&tx.txid()[..]);
|
||||
for (txo_index, txo) in tx.output.iter().enumerate() {
|
||||
if is_spendable(txo) {
|
||||
rows.push(TxOutRow::new(&txid, txo_index, txo).into_row());
|
||||
@ -1447,8 +1362,24 @@ fn lookup_txos(
|
||||
outpoints: &BTreeSet<OutPoint>,
|
||||
allow_missing: bool,
|
||||
) -> HashMap<OutPoint, TxOut> {
|
||||
super::THREAD_POOL.install(|| {
|
||||
// Should match lookup_txos_sequential
|
||||
let mut loop_count = 10;
|
||||
let pool = loop {
|
||||
match rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(16) // we need to saturate SSD IOPS
|
||||
.thread_name(|i| format!("lookup-txo-{}", i))
|
||||
.build()
|
||||
{
|
||||
Ok(pool) => break pool,
|
||||
Err(e) => {
|
||||
if loop_count == 0 {
|
||||
panic!("schema::lookup_txos failed to create a ThreadPool: {}", e);
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
loop_count -= 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
pool.install(|| {
|
||||
outpoints
|
||||
.par_iter()
|
||||
.filter_map(|outpoint| {
|
||||
@ -1465,27 +1396,6 @@ fn lookup_txos(
|
||||
})
|
||||
}
|
||||
|
||||
fn lookup_txos_sequential(
|
||||
txstore_db: &DB,
|
||||
outpoints: &BTreeSet<OutPoint>,
|
||||
allow_missing: bool,
|
||||
) -> HashMap<OutPoint, TxOut> {
|
||||
// Should match lookup_txos
|
||||
outpoints
|
||||
.iter()
|
||||
.filter_map(|outpoint| {
|
||||
lookup_txo(txstore_db, outpoint)
|
||||
.or_else(|| {
|
||||
if !allow_missing {
|
||||
panic!("missing txo {} in {:?}", outpoint, txstore_db);
|
||||
}
|
||||
None
|
||||
})
|
||||
.map(|txo| (*outpoint, txo))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn lookup_txo(txstore_db: &DB, outpoint: &OutPoint) -> Option<TxOut> {
|
||||
txstore_db
|
||||
.get(&TxOutRow::key(outpoint))
|
||||
@ -1496,8 +1406,7 @@ fn index_blocks(
|
||||
block_entries: &[BlockEntry],
|
||||
previous_txos_map: &HashMap<OutPoint, TxOut>,
|
||||
iconfig: &IndexerConfig,
|
||||
op: &Operation,
|
||||
) -> Vec<DBRow> {
|
||||
) -> Vec<Vec<DBRow>> {
|
||||
block_entries
|
||||
.par_iter() // serialization is CPU-intensive
|
||||
.map(|b| {
|
||||
@ -1511,13 +1420,11 @@ fn index_blocks(
|
||||
previous_txos_map,
|
||||
&mut rows,
|
||||
iconfig,
|
||||
op,
|
||||
);
|
||||
}
|
||||
rows.push(BlockRow::new_done(full_hash(&b.entry.hash()[..])).into_row()); // mark block as "indexed"
|
||||
rows
|
||||
})
|
||||
.flatten()
|
||||
.collect()
|
||||
}
|
||||
|
||||
@ -1529,25 +1436,15 @@ fn index_transaction(
|
||||
previous_txos_map: &HashMap<OutPoint, TxOut>,
|
||||
rows: &mut Vec<DBRow>,
|
||||
iconfig: &IndexerConfig,
|
||||
op: &Operation,
|
||||
) {
|
||||
// persist history index:
|
||||
// H{funding-scripthash}{spending-height}{spending-block-pos}S{spending-txid:vin}{funding-txid:vout} → ""
|
||||
// H{funding-scripthash}{funding-height}{funding-block-pos}F{funding-txid:vout} → ""
|
||||
// persist "edges" for fast is-this-TXO-spent check
|
||||
// S{funding-txid:vout}{spending-txid:vin} → ""
|
||||
let txid = full_hash(&tx.get_txid()[..]);
|
||||
let script_callback = |script_hash| {
|
||||
if let Operation::DeleteBlocksWithHistory(tx) = op {
|
||||
tx.send(script_hash).expect("unbounded channel won't fail");
|
||||
}
|
||||
};
|
||||
let txid = full_hash(&tx.txid()[..]);
|
||||
for (txo_index, txo) in tx.output.iter().enumerate() {
|
||||
if is_spendable(txo) || iconfig.index_unspendables {
|
||||
#[cfg(not(feature = "liquid"))]
|
||||
let value = txo.value.to_sat();
|
||||
#[cfg(feature = "liquid")]
|
||||
let value = txo.value;
|
||||
let history = TxHistoryRow::new(
|
||||
&txo.script_pubkey,
|
||||
confirmed_height,
|
||||
@ -1555,10 +1452,9 @@ fn index_transaction(
|
||||
TxHistoryInfo::Funding(FundingInfo {
|
||||
txid,
|
||||
vout: txo_index as u32,
|
||||
value,
|
||||
value: txo.value,
|
||||
}),
|
||||
);
|
||||
script_callback(history.key.hash);
|
||||
rows.push(history.into_row());
|
||||
|
||||
if iconfig.address_search {
|
||||
@ -1576,11 +1472,6 @@ fn index_transaction(
|
||||
.get(&txi.previous_output)
|
||||
.unwrap_or_else(|| panic!("missing previous txo {}", txi.previous_output));
|
||||
|
||||
#[cfg(not(feature = "liquid"))]
|
||||
let prev_value = prev_txo.value.to_sat();
|
||||
#[cfg(feature = "liquid")]
|
||||
let prev_value = prev_txo.value;
|
||||
|
||||
let history = TxHistoryRow::new(
|
||||
&prev_txo.script_pubkey,
|
||||
confirmed_height,
|
||||
@ -1590,10 +1481,9 @@ fn index_transaction(
|
||||
vin: txi_index as u32,
|
||||
prev_txid: full_hash(&txi.previous_output.txid[..]),
|
||||
prev_vout: txi.previous_output.vout,
|
||||
value: prev_value,
|
||||
value: prev_txo.value,
|
||||
}),
|
||||
);
|
||||
script_callback(history.key.hash);
|
||||
rows.push(history.into_row());
|
||||
|
||||
let edge = TxEdgeRow::new(
|
||||
@ -1614,7 +1504,6 @@ fn index_transaction(
|
||||
iconfig.network,
|
||||
iconfig.parent_network,
|
||||
rows,
|
||||
op,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1657,7 +1546,7 @@ struct TxRow {
|
||||
|
||||
impl TxRow {
|
||||
fn new(txn: &Transaction) -> TxRow {
|
||||
let txid = full_hash(&txn.get_txid()[..]);
|
||||
let txid = full_hash(&txn.txid()[..]);
|
||||
TxRow {
|
||||
key: TxRowKey { code: b'T', txid },
|
||||
value: serialize(txn),
|
||||
@ -1690,7 +1579,7 @@ struct TxConfRow {
|
||||
|
||||
impl TxConfRow {
|
||||
fn new(txn: &Transaction, blockhash: FullHash) -> TxConfRow {
|
||||
let txid = full_hash(&txn.get_txid()[..]);
|
||||
let txid = full_hash(&txn.txid()[..]);
|
||||
TxConfRow {
|
||||
key: TxConfKey {
|
||||
code: b'C',
|
||||
@ -1975,11 +1864,6 @@ impl TxHistoryRow {
|
||||
pub fn get_txid(&self) -> Txid {
|
||||
self.key.txinfo.get_txid()
|
||||
}
|
||||
|
||||
pub fn get_tx_position(&self) -> u16 {
|
||||
self.key.tx_position
|
||||
}
|
||||
|
||||
fn get_funded_outpoint(&self) -> OutPoint {
|
||||
self.key.txinfo.get_funded_outpoint()
|
||||
}
|
||||
|
||||
577
src/rest.rs
577
src/rest.rs
File diff suppressed because it is too large
Load Diff
@ -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(|| {
|
||||
@ -113,7 +111,7 @@ impl HeaderList {
|
||||
);
|
||||
|
||||
let mut headers = HeaderList::empty();
|
||||
headers.apply(headers.order(headers_chain));
|
||||
headers.apply(&headers.order(headers_chain));
|
||||
headers
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
@ -157,12 +155,8 @@ 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>) {
|
||||
/// Returns any re-orged headers
|
||||
pub fn apply(&mut self, new_headers: &[HeaderEntry]) -> 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,36 +171,27 @@ 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 vec![],
|
||||
};
|
||||
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());
|
||||
self.tip = *new_header.hash();
|
||||
self.headers.push(new_header);
|
||||
self.headers.push(new_header.clone());
|
||||
self.heights.insert(self.tip, height);
|
||||
}
|
||||
removed.reverse();
|
||||
(removed, reorged_tip)
|
||||
removed
|
||||
}
|
||||
|
||||
pub fn header_by_blockhash(&self, blockhash: &BlockHash) -> Option<&HeaderEntry> {
|
||||
@ -220,8 +205,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 +218,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 +231,7 @@ impl HeaderList {
|
||||
self.headers.is_empty()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> slice::Iter<'_, HeaderEntry> {
|
||||
pub fn iter(&self) -> slice::Iter<HeaderEntry> {
|
||||
self.headers.iter()
|
||||
}
|
||||
|
||||
@ -311,13 +294,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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -8,9 +8,7 @@ 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,
|
||||
@ -196,12 +194,12 @@ 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> {
|
||||
@ -210,14 +208,14 @@ pub mod serde_hex {
|
||||
}
|
||||
|
||||
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()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -9,87 +9,6 @@ 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);
|
||||
@ -97,7 +16,6 @@ pub trait ScriptToAsm: std::fmt::Debug {
|
||||
}
|
||||
}
|
||||
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,9 @@ 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()
|
||||
|| prevout.script_pubkey.is_v1_p2tr()
|
||||
|| redeem_script.as_ref().map_or(false, |s| s.is_v0_p2wsh())
|
||||
{
|
||||
let witness = &txin.witness;
|
||||
#[cfg(feature = "liquid")]
|
||||
@ -152,7 +64,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() {
|
||||
let inner_script_slice = if prevout.script_pubkey.is_v1_p2tr() {
|
||||
// Witness stack is potentially very large
|
||||
// so we avoid to_vec() or iter().collect() for performance
|
||||
let w_len = witness.len();
|
||||
|
||||
@ -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)]
|
||||
@ -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
|
||||
@ -118,14 +117,14 @@ where
|
||||
|
||||
pub(super) mod sigops {
|
||||
use crate::chain::{
|
||||
opcodes::all::{OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY, OP_CHECKSIG, OP_CHECKSIGVERIFY},
|
||||
hashes::hex::FromHex,
|
||||
opcodes::{
|
||||
all::{OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY, OP_CHECKSIG, OP_CHECKSIGVERIFY},
|
||||
All,
|
||||
},
|
||||
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.
|
||||
@ -137,7 +136,7 @@ pub(super) mod sigops {
|
||||
let mut prevouts = Vec::with_capacity(input_count);
|
||||
|
||||
#[cfg(not(feature = "liquid"))]
|
||||
let is_coinbase_or_pegin = tx.is_coinbase();
|
||||
let is_coinbase_or_pegin = tx.is_coin_base();
|
||||
#[cfg(feature = "liquid")]
|
||||
let is_coinbase_or_pegin = tx.is_coinbase() || tx.input.iter().any(|input| input.is_pegin);
|
||||
|
||||
@ -155,12 +154,9 @@ pub(super) mod sigops {
|
||||
get_sigop_cost(tx, &prevouts, true, true)
|
||||
}
|
||||
|
||||
fn decode_pushnum(op: &Opcode) -> Option<u8> {
|
||||
fn decode_pushnum(op: &All) -> 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),
|
||||
@ -220,7 +216,7 @@ pub(super) mod sigops {
|
||||
|
||||
fn get_p2sh_sigop_count(tx: &Transaction, previous_outputs: &[&TxOut]) -> usize {
|
||||
#[cfg(not(feature = "liquid"))]
|
||||
if tx.is_coinbase() {
|
||||
if tx.is_coin_base() {
|
||||
return 0;
|
||||
}
|
||||
#[cfg(feature = "liquid")]
|
||||
@ -233,14 +229,9 @@ pub(super) mod sigops {
|
||||
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);
|
||||
}
|
||||
let script =
|
||||
script::Script::from_byte_iter(redeem.iter().map(|v| Ok(*v))).unwrap(); // I only return Ok, so it won't error
|
||||
n += count_sigops(&script, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -265,9 +256,6 @@ pub(super) mod sigops {
|
||||
#[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,
|
||||
}
|
||||
@ -281,36 +269,20 @@ pub(super) mod sigops {
|
||||
) -> usize {
|
||||
let mut n = 0;
|
||||
|
||||
let script_owned;
|
||||
let script: &script::Script = if prevout.script_pubkey.is_witness_program() {
|
||||
&prevout.script_pubkey
|
||||
let script = if prevout.script_pubkey.is_witness_program() {
|
||||
prevout.script_pubkey.clone()
|
||||
} 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
|
||||
script::Script::from_byte_iter(
|
||||
last_pushdata(script_sig).unwrap().iter().map(|v| Ok(*v)),
|
||||
)
|
||||
.unwrap()
|
||||
} 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..]);
|
||||
@ -335,7 +307,7 @@ pub(super) mod sigops {
|
||||
) -> Result<usize, script::Error> {
|
||||
let mut n_sigop_cost = get_legacy_sigop_count(tx) * 4;
|
||||
#[cfg(not(feature = "liquid"))]
|
||||
if tx.is_coinbase() {
|
||||
if tx.is_coin_base() {
|
||||
return Ok(n_sigop_cost);
|
||||
}
|
||||
#[cfg(feature = "liquid")]
|
||||
@ -361,7 +333,6 @@ pub(super) mod sigops {
|
||||
/// 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();
|
||||
@ -369,23 +340,12 @@ pub(super) mod sigops {
|
||||
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, 32) => last_witness
|
||||
.map(|sl| sl.iter().map(|v| Ok(*v)))
|
||||
.map(script::Script::from_byte_iter)
|
||||
// I only return Ok 2 lines up, so there is no way to error
|
||||
.map(|s| count_sigops(&s.unwrap(), true))
|
||||
.unwrap_or_default(),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
86
start
86
start
@ -5,7 +5,6 @@ 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]"
|
||||
@ -43,38 +42,36 @@ esac
|
||||
# which network?
|
||||
case "${1}" in
|
||||
mainnet)
|
||||
THREADS=$((NPROC / 8))
|
||||
THREADS=$((NPROC / 3))
|
||||
CRONJOB_TIMING="20 4 * * *"
|
||||
;;
|
||||
testnet)
|
||||
NETWORK=testnet
|
||||
THREADS=$((NPROC / 8))
|
||||
THREADS=$((NPROC / 6))
|
||||
CRONJOB_TIMING="2 4 * * *"
|
||||
;;
|
||||
testnet4)
|
||||
NETWORK=testnet4
|
||||
THREADS=$((NPROC / 8))
|
||||
THREADS=$((NPROC / 6))
|
||||
CRONJOB_TIMING="17 4 * * *"
|
||||
;;
|
||||
signet)
|
||||
NETWORK=signet
|
||||
THREADS=$((NPROC / 8))
|
||||
THREADS=$((NPROC / 6))
|
||||
CRONJOB_TIMING="9 4 * * *"
|
||||
;;
|
||||
liquid)
|
||||
DAEMON=elements
|
||||
NETWORK=liquid
|
||||
FEATURES=liquid
|
||||
ASSET_DB_ARGS=(--asset-db-path /elements/asset_registry_db)
|
||||
THREADS=$((NPROC / 8))
|
||||
THREADS=$((NPROC / 6))
|
||||
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))
|
||||
THREADS=$((NPROC / 6))
|
||||
CRONJOB_TIMING="17 4 * * *"
|
||||
;;
|
||||
*)
|
||||
@ -91,6 +88,7 @@ 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}"
|
||||
rm -f "${POPULAR_SCRIPTS_FILE_RAW}" "${POPULAR_SCRIPTS_FILE}"
|
||||
|
||||
## 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
|
||||
@ -109,13 +107,8 @@ generate_popular_scripts() {
|
||||
--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
|
||||
|
||||
## Sorted and deduplicated just in case
|
||||
sort "${POPULAR_SCRIPTS_FILE_RAW}" | uniq > "${POPULAR_SCRIPTS_FILE}"
|
||||
rm "${POPULAR_SCRIPTS_FILE_RAW}"
|
||||
}
|
||||
|
||||
@ -156,9 +149,6 @@ do
|
||||
# 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}"
|
||||
@ -173,73 +163,23 @@ do
|
||||
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
|
||||
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 [ "${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
|
||||
@ -247,7 +187,7 @@ do
|
||||
fi
|
||||
|
||||
# Run the electrs process (Note: db-dir is used in both commands)
|
||||
nice cargo run \
|
||||
cargo run \
|
||||
--release \
|
||||
--bin electrs \
|
||||
--features "${FEATURES}" \
|
||||
@ -255,7 +195,6 @@ do
|
||||
--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}" \
|
||||
@ -266,9 +205,6 @@ do
|
||||
--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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user