Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c8ac67f2f | ||
|
|
39f44a2c17 | ||
|
|
ee5e65e847 | ||
|
|
2fa977b5a9 | ||
|
|
baad4799a5 | ||
|
|
4751db3689 | ||
|
|
adfe865262 | ||
|
|
587f9c1000 | ||
|
|
0cd2305ae5 | ||
|
|
15e9430d4e | ||
|
|
6add8952a0 | ||
|
|
cd6a967e23 | ||
|
|
b0774e5cee | ||
|
|
f11f6ab09a | ||
|
|
95478619d1 | ||
|
|
237a2df61e | ||
|
|
88d0721033 | ||
|
|
c9da83f825 | ||
|
|
6f203ccb0c | ||
|
|
6dfe5295fe | ||
|
|
d6d580c097 | ||
|
|
e53631a318 | ||
|
|
7f28499587 | ||
|
|
60fb1029c9 | ||
|
|
8aa0cc06a6 | ||
|
|
10f42b566d | ||
|
|
12a70af0c0 | ||
|
|
67ed34e357 | ||
|
|
51fd37d553 | ||
|
|
474955985b | ||
|
|
98ae002244 | ||
|
|
3d8d6e6128 | ||
|
|
c9e3a5e68d | ||
|
|
5d9132ef83 | ||
|
|
043dcbae3d | ||
|
|
01fd17358c | ||
|
|
6a16d457b4 | ||
|
|
fc0c0f5a11 | ||
|
|
ce089c4c16 | ||
|
|
f8302d7cb5 | ||
|
|
a82862818e | ||
|
|
ea6954288c | ||
|
|
3000bd13e7 | ||
|
|
5252bdb805 | ||
|
|
d5dfbb13e1 | ||
|
|
0b735569b7 | ||
|
|
dad95ade9e | ||
|
|
6233b5c874 | ||
|
|
fed8aed060 | ||
|
|
2a2e8beb1d | ||
|
|
848bba817a | ||
|
|
49436f60db | ||
|
|
8a339f7414 | ||
|
|
49464309a5 | ||
|
|
b74d8bac5d | ||
|
|
dbdcbf88e8 |
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@ -10,7 +10,7 @@ name: Compile Check and Lint
|
|||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
name: Compile Check
|
name: Compile Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: mempool-ci
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Rust
|
- name: Setup Rust
|
||||||
@ -21,7 +21,7 @@ jobs:
|
|||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
name: Formatter
|
name: Formatter
|
||||||
runs-on: ubuntu-latest
|
runs-on: mempool-ci
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Rust
|
- name: Setup Rust
|
||||||
@ -32,7 +32,7 @@ jobs:
|
|||||||
|
|
||||||
test:
|
test:
|
||||||
name: Run Tests
|
name: Run Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: mempool-ci
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Rust
|
- name: Setup Rust
|
||||||
@ -42,7 +42,7 @@ jobs:
|
|||||||
- run: cargo test --lib --all-features
|
- run: cargo test --lib --all-features
|
||||||
|
|
||||||
compile-freebsd:
|
compile-freebsd:
|
||||||
runs-on: ubuntu-latest
|
runs-on: mempool-ci
|
||||||
name: Run Compile Checks in FreeBSD
|
name: Run Compile Checks in FreeBSD
|
||||||
env:
|
env:
|
||||||
FREEBSD_VER: "14.3"
|
FREEBSD_VER: "14.3"
|
||||||
@ -69,7 +69,7 @@ jobs:
|
|||||||
mv ./.cargohome/registry ~/.cargo/
|
mv ./.cargohome/registry ~/.cargo/
|
||||||
mv ./.cargohome/git ~/.cargo/
|
mv ./.cargohome/git ~/.cargo/
|
||||||
rm -rf ./.cargohome
|
rm -rf ./.cargohome
|
||||||
pkg install -y git rsync gmake llvm rust rocksdb
|
pkg install -y git rsync gmake llvm rust rocksdb cmake
|
||||||
|
|
||||||
run: |
|
run: |
|
||||||
cargo check --no-default-features
|
cargo check --no-default-features
|
||||||
@ -86,7 +86,7 @@ jobs:
|
|||||||
|
|
||||||
clippy:
|
clippy:
|
||||||
name: Linter
|
name: Linter
|
||||||
runs-on: ubuntu-latest
|
runs-on: mempool-ci
|
||||||
needs: [check]
|
needs: [check]
|
||||||
strategy:
|
strategy:
|
||||||
matrix: # Try all combinations of features. Some times weird things appear.
|
matrix: # Try all combinations of features. Some times weird things appear.
|
||||||
|
|||||||
46
.github/workflows/on-tag.yml
vendored
46
.github/workflows/on-tag.yml
vendored
@ -16,39 +16,18 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: mempool-ci
|
||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
name: Build and push to DockerHub
|
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:
|
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
|
- name: Set env variables
|
||||||
run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
||||||
|
|
||||||
@ -91,7 +70,8 @@ jobs:
|
|||||||
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
||||||
--cache-to "type=local,dest=/tmp/.buildx-cache" \
|
--cache-to "type=local,dest=/tmp/.buildx-cache" \
|
||||||
--platform linux/amd64,linux/arm64 \
|
--platform linux/amd64,linux/arm64 \
|
||||||
--tag ${{ secrets.DOCKER_HUB_USER }}/electrs:$TAG \
|
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.image }}:$TAG \
|
||||||
--tag ${{ secrets.DOCKER_HUB_USER }}/electrs:latest \
|
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.image }}:latest \
|
||||||
--output "type=registry" . \
|
--output "type=registry" . \
|
||||||
--build-arg commitHash=$SHORT_SHA
|
--build-arg commitHash=$SHORT_SHA \
|
||||||
|
--build-arg CARGO_EXTRA_ARGS="${{ matrix.cargo_extra_args }}"
|
||||||
|
|||||||
90
.github/workflows/project-review-status.yml
vendored
90
.github/workflows/project-review-status.yml
vendored
@ -1,84 +1,18 @@
|
|||||||
# Workflow: Automatically set project status to "Review Needed" when a reviewer is requested
|
name: Project Board Automation
|
||||||
name: Set Project Status on Review Request
|
|
||||||
|
|
||||||
# Trigger: Runs whenever a reviewer is requested on a pull request
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [review_requested]
|
types: [review_requested]
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update-project-status:
|
project-automation:
|
||||||
runs-on: ubuntu-latest
|
uses: mempool/.github/.github/workflows/project-board-automation.yml@master
|
||||||
steps:
|
with:
|
||||||
- name: Update Project Status to Review Needed
|
project-number: 8
|
||||||
uses: actions/github-script@v7
|
secrets:
|
||||||
with:
|
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
||||||
# Use the PAT stored in repository secrets (has project write access)
|
PROJECT_ID: ${{ secrets.PROJECT_ID }}
|
||||||
github-token: ${{ secrets.PROJECT_TOKEN }}
|
STATUS_FIELD_ID: ${{ secrets.STATUS_FIELD_ID }}
|
||||||
script: |
|
REVIEW_NEEDED_OPTION_ID: ${{ secrets.REVIEW_NEEDED_OPTION_ID }}
|
||||||
// GraphQL query to find the PR's project items
|
|
||||||
// This fetches all projects the PR is linked to
|
|
||||||
const query = `
|
|
||||||
query($owner: String!, $repo: String!, $pr: Int!) {
|
|
||||||
repository(owner: $owner, name: $repo) {
|
|
||||||
pullRequest(number: $pr) {
|
|
||||||
projectItems(first: 10) {
|
|
||||||
nodes {
|
|
||||||
id
|
|
||||||
project {
|
|
||||||
number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Execute the query with current repo/PR context
|
|
||||||
const result = await github.graphql(query, {
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
pr: context.payload.pull_request.number
|
|
||||||
});
|
|
||||||
|
|
||||||
// Find the project item that belongs to project #8
|
|
||||||
const projectItems = result.repository.pullRequest.projectItems.nodes;
|
|
||||||
const projectItem = projectItems.find(item => item.project.number === 8);
|
|
||||||
|
|
||||||
// Exit early if PR isn't in project #8
|
|
||||||
if (!projectItem) {
|
|
||||||
console.log('PR is not in project #8, skipping...');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// GraphQL mutation to update the Status field
|
|
||||||
const mutation = `
|
|
||||||
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
|
|
||||||
updateProjectV2ItemFieldValue(
|
|
||||||
input: {
|
|
||||||
projectId: $projectId
|
|
||||||
itemId: $itemId
|
|
||||||
fieldId: $fieldId
|
|
||||||
value: { singleSelectOptionId: $optionId }
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
projectV2Item {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Execute the mutation using IDs stored in repository variables
|
|
||||||
// PROJECT_ID: The project's unique identifier
|
|
||||||
// STATUS_FIELD_ID: The "Status" field's unique identifier
|
|
||||||
// REVIEW_NEEDED_OPTION_ID: The "Review Needed" option's unique identifier
|
|
||||||
await github.graphql(mutation, {
|
|
||||||
projectId: "${{ secrets.PROJECT_ID }}",
|
|
||||||
itemId: projectItem.id,
|
|
||||||
fieldId: "${{ secrets.STATUS_FIELD_ID }}",
|
|
||||||
optionId: "${{ secrets.REVIEW_NEEDED_OPTION_ID }}"
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Successfully updated project status to Review Needed');
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,3 +5,4 @@ target
|
|||||||
*~
|
*~
|
||||||
*.pyc
|
*.pyc
|
||||||
.vscode
|
.vscode
|
||||||
|
*.core
|
||||||
|
|||||||
19
AGENTS.md
Normal file
19
AGENTS.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# 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`
|
||||||
942
Cargo.lock
generated
942
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
14
Cargo.toml
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "mempool-electrs"
|
name = "mempool-electrs"
|
||||||
version = "3.3.0-dev"
|
version = "3.4.0-dev"
|
||||||
authors = [
|
authors = [
|
||||||
"Roman Zeyde <me@romanzey.de>",
|
"Roman Zeyde <me@romanzey.de>",
|
||||||
"Nadav Ivgi <nadav@shesek.info>",
|
"Nadav Ivgi <nadav@shesek.info>",
|
||||||
@ -28,12 +28,12 @@ electrum-discovery = ["electrum-client"]
|
|||||||
arrayref = "0.3.6"
|
arrayref = "0.3.6"
|
||||||
base64 = "0.13.0"
|
base64 = "0.13.0"
|
||||||
bincode-do-not-use-directly = { version = "1.3.1", package = "bincode" }
|
bincode-do-not-use-directly = { version = "1.3.1", package = "bincode" }
|
||||||
bitcoin = { version = "0.28", features = [ "use-serde" ] }
|
bitcoin = { version = "0.32.8", features = [ "serde" ] }
|
||||||
bounded-vec-deque = "0.1.1"
|
bounded-vec-deque = "0.1.1"
|
||||||
clap = "2.33.3"
|
clap = "2.33.3"
|
||||||
crossbeam-channel = "0.5.0"
|
crossbeam-channel = "0.5.0"
|
||||||
dirs = "4.0.0"
|
dirs = "4.0.0"
|
||||||
elements = { version = "0.19.1", features = [ "serde-feature" ], optional = true }
|
elements = { version = "0.26.1", features = [ "serde" ], optional = true }
|
||||||
error-chain = "0.12.4"
|
error-chain = "0.12.4"
|
||||||
glob = "0.3"
|
glob = "0.3"
|
||||||
hex = "0.4.2"
|
hex = "0.4.2"
|
||||||
@ -45,6 +45,7 @@ socket2 = { version = "0.4", features = ["all"] }
|
|||||||
num_cpus = "1.12.0"
|
num_cpus = "1.12.0"
|
||||||
page_size = "0.4.2"
|
page_size = "0.4.2"
|
||||||
prometheus = "0.13"
|
prometheus = "0.13"
|
||||||
|
ppp = "2.3.0"
|
||||||
rayon = "1.5.0"
|
rayon = "1.5.0"
|
||||||
rocksdb = "0.24.0"
|
rocksdb = "0.24.0"
|
||||||
serde = "1.0.118"
|
serde = "1.0.118"
|
||||||
@ -62,8 +63,7 @@ hyperlocal = "0.8"
|
|||||||
tokio = { version = "1", features = ["sync", "macros"] }
|
tokio = { version = "1", features = ["sync", "macros"] }
|
||||||
|
|
||||||
# optional dependencies for electrum-discovery
|
# optional dependencies for electrum-discovery
|
||||||
electrum-client = { version = "0.8", optional = true }
|
electrum-client = { version = "0.24.1", optional = true }
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.0"
|
tempfile = "3.0"
|
||||||
@ -74,5 +74,5 @@ panic = 'abort'
|
|||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|
||||||
[patch.crates-io.electrum-client]
|
[patch.crates-io.electrum-client]
|
||||||
git = "https://github.com/Blockstream/rust-electrum-client"
|
git = "https://github.com/mempool/rust-electrum-client"
|
||||||
rev = "d3792352992a539afffbe11501d1aff9fd5b919d" # add-peer branch
|
rev = "4bbfc612d594fe23282c439d4bdc446cff01ba1c" # 0.24.1/add-peer branch
|
||||||
|
|||||||
@ -18,7 +18,8 @@ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
|||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN cargo build --release --bin electrs
|
ARG CARGO_EXTRA_ARGS=""
|
||||||
|
RUN cargo build --release --bin electrs ${CARGO_EXTRA_ARGS}
|
||||||
|
|
||||||
FROM base as deploy
|
FROM base as deploy
|
||||||
|
|
||||||
|
|||||||
3
build.rs
3
build.rs
@ -34,7 +34,8 @@ fn main() {
|
|||||||
// This includes untracked files
|
// This includes untracked files
|
||||||
let dirty = cmd("git", &["status", "--short"]).expect("git command works");
|
let dirty = cmd("git", &["status", "--short"]).expect("git command works");
|
||||||
|
|
||||||
let git_hash = if dirty.is_empty() {
|
// Ignore Dockerfile deletion as it is expected in Docker buildx builds
|
||||||
|
let git_hash = if dirty.is_empty() || dirty.trim() == "D Dockerfile" {
|
||||||
rev_parse
|
rev_parse
|
||||||
} else {
|
} else {
|
||||||
format!("{}(dirty)", rev_parse.trim())
|
format!("{}(dirty)", rev_parse.trim())
|
||||||
|
|||||||
@ -57,6 +57,8 @@ TESTNAME="Running cargo clippy check electrum-discovery + liquid"
|
|||||||
echo "$TESTNAME"
|
echo "$TESTNAME"
|
||||||
cargo clippy $@ -q -F electrum-discovery,liquid
|
cargo clippy $@ -q -F electrum-discovery,liquid
|
||||||
|
|
||||||
TESTNAME="Running cargo test with all features"
|
if [ $INCLUDE_TESTS ]; then
|
||||||
echo "$TESTNAME"
|
TESTNAME="Running cargo test with all features"
|
||||||
cargo test $@ -q --lib --all-features
|
echo "$TESTNAME"
|
||||||
|
cargo test $@ -q --lib --all-features
|
||||||
|
fi
|
||||||
|
|||||||
@ -9,7 +9,7 @@ fn main() {
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use bitcoin::blockdata::script::Script;
|
use bitcoin::blockdata::script::ScriptBuf;
|
||||||
use bitcoin::consensus::encode::deserialize;
|
use bitcoin::consensus::encode::deserialize;
|
||||||
use electrs::{
|
use electrs::{
|
||||||
chain::Transaction,
|
chain::Transaction,
|
||||||
@ -62,7 +62,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let tx: Transaction = deserialize(value).expect("failed to parse Transaction");
|
let tx: Transaction = deserialize(value).expect("failed to parse Transaction");
|
||||||
let txid = tx.txid();
|
let txid = tx.compute_txid();
|
||||||
|
|
||||||
iter.next();
|
iter.next();
|
||||||
|
|
||||||
@ -71,7 +71,7 @@ fn main() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// skip coinbase txs
|
// skip coinbase txs
|
||||||
if tx.is_coin_base() {
|
if tx.is_coinbase() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,12 +91,26 @@ fn main() {
|
|||||||
.collect(),
|
.collect(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let total_out: u64 = tx.output.iter().map(|out| out.value).sum();
|
let total_out: u64 = tx.output.iter().map(|out| out.value.to_sat()).sum();
|
||||||
let small_out = tx.output.iter().map(|out| out.value).min().unwrap();
|
let small_out = tx
|
||||||
let large_out = tx.output.iter().map(|out| out.value).max().unwrap();
|
.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_in: u64 = prevouts.values().map(|out| out.value).sum();
|
let total_in: u64 = prevouts.values().map(|out| out.value.to_sat()).sum();
|
||||||
let smallest_in = prevouts.values().map(|out| out.value).min().unwrap();
|
let smallest_in = prevouts
|
||||||
|
.values()
|
||||||
|
.map(|out| out.value.to_sat())
|
||||||
|
.min()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let fee = total_in - total_out;
|
let fee = total_in - total_out;
|
||||||
|
|
||||||
@ -119,7 +133,7 @@ fn main() {
|
|||||||
|
|
||||||
// test for sending back to one of the spent spks
|
// test for sending back to one of the spent spks
|
||||||
let has_reuse = {
|
let has_reuse = {
|
||||||
let prev_spks: HashSet<Script> = prevouts
|
let prev_spks: HashSet<ScriptBuf> = prevouts
|
||||||
.values()
|
.values()
|
||||||
.map(|out| out.script_pubkey.clone())
|
.map(|out| out.script_pubkey.clone())
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
73
src/chain.rs
73
src/chain.rs
@ -2,25 +2,64 @@ use std::str::FromStr;
|
|||||||
|
|
||||||
#[cfg(not(feature = "liquid"))] // use regular Bitcoin data structures
|
#[cfg(not(feature = "liquid"))] // use regular Bitcoin data structures
|
||||||
pub use bitcoin::{
|
pub use bitcoin::{
|
||||||
blockdata::{opcodes, script, witness::Witness},
|
address,
|
||||||
|
block::Header as BlockHeader,
|
||||||
|
blockdata::{opcodes, script},
|
||||||
consensus::deserialize,
|
consensus::deserialize,
|
||||||
hashes,
|
hashes, Block, BlockHash, OutPoint, ScriptBuf as Script, Transaction, TxIn, TxOut, Txid,
|
||||||
util::address,
|
Witness,
|
||||||
Block, BlockHash, BlockHeader, OutPoint, Script, Transaction, TxIn, TxOut, Txid,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
pub use {
|
pub use {
|
||||||
crate::elements::asset,
|
crate::elements::asset,
|
||||||
elements::{
|
elements::{
|
||||||
address, confidential, encode::deserialize, hashes, opcodes, script, Address, AssetId,
|
address, bitcoin::bech32::Hrp, confidential, encode::deserialize, hashes, opcodes, script,
|
||||||
Block, BlockHash, BlockHeader, OutPoint, Script, Transaction, TxIn, TxInWitness as Witness,
|
Address, AssetId, Block, BlockHash, BlockHeader, OutPoint, Script, Transaction, TxIn,
|
||||||
TxOut, Txid,
|
TxInWitness as Witness, TxOut, Txid,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use bitcoin::blockdata::constants::genesis_block;
|
use bitcoin::blockdata::constants::genesis_block;
|
||||||
pub use bitcoin::network::constants::Network as BNetwork;
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "liquid"))]
|
#[cfg(not(feature = "liquid"))]
|
||||||
pub type Value = u64;
|
pub type Value = u64;
|
||||||
@ -53,8 +92,8 @@ pub const LIQUID_TESTNET_PARAMS: address::AddressParams = address::AddressParams
|
|||||||
p2pkh_prefix: 36,
|
p2pkh_prefix: 36,
|
||||||
p2sh_prefix: 19,
|
p2sh_prefix: 19,
|
||||||
blinded_prefix: 23,
|
blinded_prefix: 23,
|
||||||
bech_hrp: "tex",
|
bech_hrp: Hrp::parse_unchecked("tex"),
|
||||||
blech_hrp: "tlq",
|
blech_hrp: Hrp::parse_unchecked("tlq"),
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Magic for testnet4, 0x1c163f28 (from BIP94) with flipped endianness.
|
/// Magic for testnet4, 0x1c163f28 (from BIP94) with flipped endianness.
|
||||||
@ -66,7 +105,10 @@ impl Network {
|
|||||||
pub fn magic(self) -> u32 {
|
pub fn magic(self) -> u32 {
|
||||||
match self {
|
match self {
|
||||||
Self::Testnet4 => TESTNET4_MAGIC,
|
Self::Testnet4 => TESTNET4_MAGIC,
|
||||||
_ => BNetwork::from(self).magic(),
|
_ => {
|
||||||
|
let magic = BNetwork::from(self).magic();
|
||||||
|
u32::from_le_bytes(magic.to_bytes())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,6 +220,10 @@ pub fn liquid_genesis_hash(network: Network) -> elements::BlockHash {
|
|||||||
"1466275836220db2944ca059a3a10ef6fd2ea684b0688d2c379296888a206003"
|
"1466275836220db2944ca059a3a10ef6fd2ea684b0688d2c379296888a206003"
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
static ref ZERO_HASH: BlockHash =
|
||||||
|
"0000000000000000000000000000000000000000000000000000000000000000"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
match network {
|
match network {
|
||||||
@ -185,7 +231,7 @@ pub fn liquid_genesis_hash(network: Network) -> elements::BlockHash {
|
|||||||
// The genesis block for liquid regtest chains varies based on the chain configuration.
|
// 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
|
// 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.
|
// only used for Electrum server discovery, which isn't active on regtest.
|
||||||
_ => Default::default(),
|
_ => *ZERO_HASH,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,7 +267,7 @@ impl From<Network> for BNetwork {
|
|||||||
match network {
|
match network {
|
||||||
Network::Bitcoin => BNetwork::Bitcoin,
|
Network::Bitcoin => BNetwork::Bitcoin,
|
||||||
Network::Testnet => BNetwork::Testnet,
|
Network::Testnet => BNetwork::Testnet,
|
||||||
Network::Testnet4 => BNetwork::Testnet,
|
Network::Testnet4 => BNetwork::Testnet4,
|
||||||
Network::Regtest => BNetwork::Regtest,
|
Network::Regtest => BNetwork::Regtest,
|
||||||
Network::Signet => BNetwork::Signet,
|
Network::Signet => BNetwork::Signet,
|
||||||
}
|
}
|
||||||
@ -234,6 +280,7 @@ impl From<BNetwork> for Network {
|
|||||||
match network {
|
match network {
|
||||||
BNetwork::Bitcoin => Network::Bitcoin,
|
BNetwork::Bitcoin => Network::Bitcoin,
|
||||||
BNetwork::Testnet => Network::Testnet,
|
BNetwork::Testnet => Network::Testnet,
|
||||||
|
BNetwork::Testnet4 => Network::Testnet4,
|
||||||
BNetwork::Regtest => Network::Regtest,
|
BNetwork::Regtest => Network::Regtest,
|
||||||
BNetwork::Signet => Network::Signet,
|
BNetwork::Signet => Network::Signet,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,14 +65,19 @@ pub struct Config {
|
|||||||
pub rest_default_max_address_summary_txs: usize,
|
pub rest_default_max_address_summary_txs: usize,
|
||||||
pub rest_max_mempool_page_size: usize,
|
pub rest_max_mempool_page_size: usize,
|
||||||
pub rest_max_mempool_txid_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")]
|
#[cfg(feature = "liquid")]
|
||||||
pub parent_network: BNetwork,
|
pub parent_network: BNetwork,
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
pub asset_db_path: Option<PathBuf>,
|
pub asset_db_path: Option<PathBuf>,
|
||||||
|
|
||||||
#[cfg(feature = "electrum-discovery")]
|
|
||||||
pub electrum_public_hosts: Option<crate::electrum::ServerHosts>,
|
|
||||||
#[cfg(feature = "electrum-discovery")]
|
#[cfg(feature = "electrum-discovery")]
|
||||||
pub electrum_announce: bool,
|
pub electrum_announce: bool,
|
||||||
#[cfg(feature = "electrum-discovery")]
|
#[cfg(feature = "electrum-discovery")]
|
||||||
@ -278,6 +283,36 @@ impl Config {
|
|||||||
.long("electrum-banner")
|
.long("electrum-banner")
|
||||||
.help("Welcome banner for the Electrum server, shown in the console to clients.")
|
.help("Welcome banner for the Electrum server, shown in the console to clients.")
|
||||||
.takes_value(true)
|
.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)]
|
#[cfg(unix)]
|
||||||
@ -497,6 +532,8 @@ impl Config {
|
|||||||
let electrum_public_hosts = m
|
let electrum_public_hosts = m
|
||||||
.value_of("electrum_public_hosts")
|
.value_of("electrum_public_hosts")
|
||||||
.map(|s| serde_json::from_str(s).expect("invalid --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();
|
let mut log = stderrlog::new();
|
||||||
log.verbosity(m.occurrences_of("verbosity") as usize);
|
log.verbosity(m.occurrences_of("verbosity") as usize);
|
||||||
@ -547,6 +584,16 @@ impl Config {
|
|||||||
"rest_max_mempool_txid_page_size",
|
"rest_max_mempool_txid_page_size",
|
||||||
usize
|
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"),
|
jsonrpc_import: m.is_present("jsonrpc_import"),
|
||||||
light_mode: m.is_present("light_mode"),
|
light_mode: m.is_present("light_mode"),
|
||||||
main_loop_delay: value_t_or_exit!(m, "main_loop_delay", u64),
|
main_loop_delay: value_t_or_exit!(m, "main_loop_delay", u64),
|
||||||
@ -576,7 +623,6 @@ impl Config {
|
|||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
asset_db_path,
|
asset_db_path,
|
||||||
|
|
||||||
#[cfg(feature = "electrum-discovery")]
|
|
||||||
electrum_public_hosts,
|
electrum_public_hosts,
|
||||||
#[cfg(feature = "electrum-discovery")]
|
#[cfg(feature = "electrum-discovery")]
|
||||||
electrum_announce: m.is_present("electrum_announce"),
|
electrum_announce: m.is_present("electrum_announce"),
|
||||||
|
|||||||
@ -2,11 +2,12 @@ use std::collections::{HashMap, HashSet};
|
|||||||
use std::io::{BufRead, BufReader, Lines, Write};
|
use std::io::{BufRead, BufReader, Lines, Write};
|
||||||
use std::net::{SocketAddr, TcpStream};
|
use std::net::{SocketAddr, TcpStream};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use base64;
|
use base64;
|
||||||
use bitcoin::hashes::hex::{FromHex, ToHex};
|
use bitcoin::hashes::Hash;
|
||||||
use glob;
|
use glob;
|
||||||
use hex;
|
use hex;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
@ -27,14 +28,14 @@ use crate::errors::*;
|
|||||||
|
|
||||||
fn parse_hash<T>(value: &Value) -> Result<T>
|
fn parse_hash<T>(value: &Value) -> Result<T>
|
||||||
where
|
where
|
||||||
T: FromHex,
|
T: FromStr,
|
||||||
|
<T as FromStr>::Err: std::fmt::Debug,
|
||||||
{
|
{
|
||||||
T::from_hex(
|
value
|
||||||
value
|
.as_str()
|
||||||
.as_str()
|
.chain_err(|| format!("non-string value: {}", value))?
|
||||||
.chain_err(|| format!("non-string value: {}", value))?,
|
.parse::<T>()
|
||||||
)
|
.map_err(|e| format!("failed to parse hash: {:?}", e).into())
|
||||||
.chain_err(|| format!("non-hex value: {}", value))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn header_from_value(value: Value) -> Result<BlockHeader> {
|
fn header_from_value(value: Value) -> Result<BlockHeader> {
|
||||||
@ -547,7 +548,7 @@ impl Daemon {
|
|||||||
pub fn getblockheader(&self, blockhash: &BlockHash) -> Result<BlockHeader> {
|
pub fn getblockheader(&self, blockhash: &BlockHash) -> Result<BlockHeader> {
|
||||||
header_from_value(self.request(
|
header_from_value(self.request(
|
||||||
"getblockheader",
|
"getblockheader",
|
||||||
json!([blockhash.to_hex(), /*verbose=*/ false]),
|
json!([blockhash.to_string(), /*verbose=*/ false]),
|
||||||
)?)
|
)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -566,21 +567,22 @@ impl Daemon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getblock(&self, blockhash: &BlockHash) -> Result<Block> {
|
pub fn getblock(&self, blockhash: &BlockHash) -> Result<Block> {
|
||||||
let block = block_from_value(
|
let block = block_from_value(self.request(
|
||||||
self.request("getblock", json!([blockhash.to_hex(), /*verbose=*/ false]))?,
|
"getblock",
|
||||||
)?;
|
json!([blockhash.to_string(), /*verbose=*/ false]),
|
||||||
|
)?)?;
|
||||||
assert_eq!(block.block_hash(), *blockhash);
|
assert_eq!(block.block_hash(), *blockhash);
|
||||||
Ok(block)
|
Ok(block)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getblock_raw(&self, blockhash: &BlockHash, verbose: u32) -> Result<Value> {
|
pub fn getblock_raw(&self, blockhash: &BlockHash, verbose: u32) -> Result<Value> {
|
||||||
self.request("getblock", json!([blockhash.to_hex(), verbose]))
|
self.request("getblock", json!([blockhash.to_string(), verbose]))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getblocks(&self, blockhashes: &[BlockHash]) -> Result<Vec<Block>> {
|
pub fn getblocks(&self, blockhashes: &[BlockHash]) -> Result<Vec<Block>> {
|
||||||
let params_list: Vec<Value> = blockhashes
|
let params_list: Vec<Value> = blockhashes
|
||||||
.iter()
|
.iter()
|
||||||
.map(|hash| json!([hash.to_hex(), /*verbose=*/ false]))
|
.map(|hash| json!([hash.to_string(), /*verbose=*/ false]))
|
||||||
.collect();
|
.collect();
|
||||||
let values = self.requests("getblock", ¶ms_list)?;
|
let values = self.requests("getblock", ¶ms_list)?;
|
||||||
let mut blocks = vec![];
|
let mut blocks = vec![];
|
||||||
@ -593,7 +595,7 @@ impl Daemon {
|
|||||||
pub fn gettransactions(&self, txhashes: &[&Txid]) -> Result<Vec<Transaction>> {
|
pub fn gettransactions(&self, txhashes: &[&Txid]) -> Result<Vec<Transaction>> {
|
||||||
let params_list: Vec<Value> = txhashes
|
let params_list: Vec<Value> = txhashes
|
||||||
.iter()
|
.iter()
|
||||||
.map(|txhash| json!([txhash.to_hex(), /*verbose=*/ false]))
|
.map(|txhash| json!([txhash.to_string(), /*verbose=*/ false]))
|
||||||
.collect();
|
.collect();
|
||||||
let values = self.retry_request_batch("getrawtransaction", ¶ms_list, 0.25)?;
|
let values = self.retry_request_batch("getrawtransaction", ¶ms_list, 0.25)?;
|
||||||
let mut txs = vec![];
|
let mut txs = vec![];
|
||||||
@ -612,14 +614,14 @@ impl Daemon {
|
|||||||
) -> Result<Value> {
|
) -> Result<Value> {
|
||||||
self.request(
|
self.request(
|
||||||
"getrawtransaction",
|
"getrawtransaction",
|
||||||
json!([txid.to_hex(), verbose, blockhash]),
|
json!([txid.to_string(), verbose, blockhash]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getmempooltx(&self, txhash: &Txid) -> Result<Transaction> {
|
pub fn getmempooltx(&self, txhash: &Txid) -> Result<Transaction> {
|
||||||
let value = self.request(
|
let value = self.request(
|
||||||
"getrawtransaction",
|
"getrawtransaction",
|
||||||
json!([txhash.to_hex(), /*verbose=*/ false]),
|
json!([txhash.to_string(), /*verbose=*/ false]),
|
||||||
)?;
|
)?;
|
||||||
tx_from_value(value)
|
tx_from_value(value)
|
||||||
}
|
}
|
||||||
@ -635,8 +637,10 @@ impl Daemon {
|
|||||||
|
|
||||||
pub fn broadcast_raw(&self, txhex: &str) -> Result<Txid> {
|
pub fn broadcast_raw(&self, txhex: &str) -> Result<Txid> {
|
||||||
let txid = self.request("sendrawtransaction", json!([txhex]))?;
|
let txid = self.request("sendrawtransaction", json!([txhex]))?;
|
||||||
Txid::from_hex(txid.as_str().chain_err(|| "non-string txid")?)
|
txid.as_str()
|
||||||
.chain_err(|| "failed to parse txid")
|
.chain_err(|| "non-string txid")?
|
||||||
|
.parse::<Txid>()
|
||||||
|
.map_err(|e| format!("failed to parse txid: {:?}", e).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn test_mempool_accept(
|
pub fn test_mempool_accept(
|
||||||
@ -707,7 +711,7 @@ impl Daemon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_all_headers(&self, tip: &BlockHash) -> Result<Vec<BlockHeader>> {
|
fn get_all_headers(&self, tip: &BlockHash) -> Result<Vec<BlockHeader>> {
|
||||||
let info: Value = self.request("getblockheader", json!([tip.to_hex()]))?;
|
let info: Value = self.request("getblockheader", json!([tip.to_string()]))?;
|
||||||
let tip_height = info
|
let tip_height = info
|
||||||
.get("height")
|
.get("height")
|
||||||
.expect("missing height")
|
.expect("missing height")
|
||||||
@ -723,7 +727,7 @@ impl Daemon {
|
|||||||
result.append(&mut headers);
|
result.append(&mut headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut blockhash = BlockHash::default();
|
let mut blockhash = BlockHash::all_zeros();
|
||||||
for header in &result {
|
for header in &result {
|
||||||
assert_eq!(header.prev_blockhash, blockhash);
|
assert_eq!(header.prev_blockhash, blockhash);
|
||||||
blockhash = header.block_hash();
|
blockhash = header.block_hash();
|
||||||
@ -749,7 +753,7 @@ impl Daemon {
|
|||||||
bestblockhash,
|
bestblockhash,
|
||||||
);
|
);
|
||||||
let mut new_headers = vec![];
|
let mut new_headers = vec![];
|
||||||
let null_hash = BlockHash::default();
|
let null_hash = BlockHash::all_zeros();
|
||||||
let mut blockhash = *bestblockhash;
|
let mut blockhash = *bestblockhash;
|
||||||
while blockhash != null_hash {
|
while blockhash != null_hash {
|
||||||
if indexed_headers.header_by_blockhash(&blockhash).is_some() {
|
if indexed_headers.header_by_blockhash(&blockhash).is_some() {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
use bitcoin::hashes::sha256d;
|
||||||
use bitcoin::hashes::Hash;
|
use bitcoin::hashes::Hash;
|
||||||
pub use electrum_client::client::Client;
|
pub use electrum_client::client::Client;
|
||||||
pub use electrum_client::ServerFeaturesRes;
|
pub use electrum_client::ServerFeaturesRes;
|
||||||
@ -19,7 +20,9 @@ impl TryFrom<ServerFeaturesRes> for ServerFeatures {
|
|||||||
Ok(ServerFeatures {
|
Ok(ServerFeatures {
|
||||||
// electrum-client doesn't retain the hosts map data, but we already have it from the add_peer request
|
// electrum-client doesn't retain the hosts map data, but we already have it from the add_peer request
|
||||||
hosts: HashMap::new(),
|
hosts: HashMap::new(),
|
||||||
genesis_hash: BlockHash::from_inner(features.genesis_hash),
|
genesis_hash: BlockHash::from_raw_hash(sha256d::Hash::from_byte_array(
|
||||||
|
features.genesis_hash,
|
||||||
|
)),
|
||||||
server_version: features.server_version,
|
server_version: features.server_version,
|
||||||
protocol_min: features
|
protocol_min: features
|
||||||
.protocol_min
|
.protocol_min
|
||||||
|
|||||||
@ -337,7 +337,7 @@ impl DiscoveryManager {
|
|||||||
self.tor_proxy
|
self.tor_proxy
|
||||||
.chain_err(|| "no tor proxy configured, onion hosts are unsupported")?,
|
.chain_err(|| "no tor proxy configured, onion hosts are unsupported")?,
|
||||||
);
|
);
|
||||||
config = config.socks5(Some(socks)).unwrap()
|
config = config.socks5(Some(socks))
|
||||||
}
|
}
|
||||||
|
|
||||||
let client = Client::from_config(&server_url, config.build())?;
|
let client = Client::from_config(&server_url, config.build())?;
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{BufRead, BufReader, Read, Write};
|
use std::io::{BufRead, BufReader, Cursor, Read, Write};
|
||||||
#[cfg(feature = "electrum-discovery")]
|
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::net::{Shutdown, SocketAddr, TcpListener, TcpStream};
|
use std::net::{Shutdown, SocketAddr, TcpListener, TcpStream};
|
||||||
use std::os::unix::fs::FileTypeExt;
|
use std::os::unix::fs::FileTypeExt;
|
||||||
@ -12,10 +11,12 @@ use std::sync::atomic::AtomicBool;
|
|||||||
use std::sync::mpsc::{Receiver, Sender};
|
use std::sync::mpsc::{Receiver, Sender};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use bitcoin::hashes::sha256d::Hash as Sha256dHash;
|
use bitcoin::hashes::sha256d::Hash as Sha256dHash;
|
||||||
use error_chain::ChainedError;
|
use error_chain::ChainedError;
|
||||||
use hex;
|
use hex;
|
||||||
|
use ppp::PartialResult;
|
||||||
use serde_json::{from_str, Value};
|
use serde_json::{from_str, Value};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
@ -26,7 +27,7 @@ use elements::encode::serialize;
|
|||||||
|
|
||||||
use crate::chain::Txid;
|
use crate::chain::Txid;
|
||||||
use crate::config::{Config, VERSION_STRING};
|
use crate::config::{Config, VERSION_STRING};
|
||||||
use crate::electrum::{get_electrum_height, ProtocolVersion};
|
use crate::electrum::{get_electrum_height, ProtocolVersion, ServerFeatures};
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::metrics::{Gauge, HistogramOpts, HistogramVec, MetricOpts, Metrics};
|
use crate::metrics::{Gauge, HistogramOpts, HistogramVec, MetricOpts, Metrics};
|
||||||
use crate::new_index::{Query, Utxo};
|
use crate::new_index::{Query, Utxo};
|
||||||
@ -40,7 +41,7 @@ const PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(1, 4);
|
|||||||
const MAX_HEADERS: usize = 2016;
|
const MAX_HEADERS: usize = 2016;
|
||||||
|
|
||||||
#[cfg(feature = "electrum-discovery")]
|
#[cfg(feature = "electrum-discovery")]
|
||||||
use crate::electrum::{DiscoveryManager, ServerFeatures};
|
use crate::electrum::DiscoveryManager;
|
||||||
|
|
||||||
// TODO: Sha256dHash should be a generic hash-container (since script hash is single SHA256)
|
// TODO: Sha256dHash should be a generic hash-container (since script hash is single SHA256)
|
||||||
fn hash_from_value(val: Option<&Value>) -> Result<Sha256dHash> {
|
fn hash_from_value(val: Option<&Value>) -> Result<Sha256dHash> {
|
||||||
@ -76,6 +77,36 @@ fn bool_from_value_or(val: Option<&Value>, name: &str, default: bool) -> Result<
|
|||||||
bool_from_value(val, name)
|
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
|
// TODO: implement caching and delta updates
|
||||||
fn get_status_hash(txs: Vec<(Txid, Option<BlockId>)>, query: &Query) -> Option<FullHash> {
|
fn get_status_hash(txs: Vec<(Txid, Option<BlockId>)>, query: &Query) -> Option<FullHash> {
|
||||||
if txs.is_empty() {
|
if txs.is_empty() {
|
||||||
@ -122,18 +153,36 @@ struct Connection {
|
|||||||
chan: SyncChannel<Message>,
|
chan: SyncChannel<Message>,
|
||||||
stats: Arc<Stats>,
|
stats: Arc<Stats>,
|
||||||
txs_limit: usize,
|
txs_limit: usize,
|
||||||
|
max_line_size: usize,
|
||||||
|
max_subscriptions: usize,
|
||||||
|
idle_timeout: u64,
|
||||||
|
last_request_at: Instant,
|
||||||
die_please: Option<Receiver<()>>,
|
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")]
|
#[cfg(feature = "electrum-discovery")]
|
||||||
discovery: Option<Arc<DiscoveryManager>>,
|
discovery: Option<Arc<DiscoveryManager>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Connection {
|
impl Connection {
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
query: Arc<Query>,
|
query: Arc<Query>,
|
||||||
stream: ConnectionStream,
|
stream: ConnectionStream,
|
||||||
stats: Arc<Stats>,
|
stats: Arc<Stats>,
|
||||||
txs_limit: usize,
|
txs_limit: usize,
|
||||||
|
max_line_size: usize,
|
||||||
|
max_subscriptions: usize,
|
||||||
|
idle_timeout: u64,
|
||||||
die_please: Receiver<()>,
|
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>>,
|
#[cfg(feature = "electrum-discovery")] discovery: Option<Arc<DiscoveryManager>>,
|
||||||
) -> Connection {
|
) -> Connection {
|
||||||
Connection {
|
Connection {
|
||||||
@ -144,7 +193,17 @@ impl Connection {
|
|||||||
chan: SyncChannel::new(10),
|
chan: SyncChannel::new(10),
|
||||||
stats,
|
stats,
|
||||||
txs_limit,
|
txs_limit,
|
||||||
|
max_line_size,
|
||||||
|
max_subscriptions,
|
||||||
|
idle_timeout,
|
||||||
|
last_request_at: Instant::now(),
|
||||||
die_please: Some(die_please),
|
die_please: Some(die_please),
|
||||||
|
server_features,
|
||||||
|
haproxy_depth,
|
||||||
|
proxy_client: None,
|
||||||
|
connections_per_client,
|
||||||
|
client_counts,
|
||||||
|
registered_ip: None,
|
||||||
#[cfg(feature = "electrum-discovery")]
|
#[cfg(feature = "electrum-discovery")]
|
||||||
discovery,
|
discovery,
|
||||||
}
|
}
|
||||||
@ -166,13 +225,8 @@ impl Connection {
|
|||||||
Ok(json!(self.query.config().electrum_banner.clone()))
|
Ok(json!(self.query.config().electrum_banner.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "electrum-discovery")]
|
|
||||||
fn server_features(&self) -> Result<Value> {
|
fn server_features(&self) -> Result<Value> {
|
||||||
let discovery = self
|
Ok(json!(self.server_features.as_ref()))
|
||||||
.discovery
|
|
||||||
.as_ref()
|
|
||||||
.chain_err(|| "discovery is disabled")?;
|
|
||||||
Ok(json!(discovery.our_features()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn server_donation_address(&self) -> Result<Value> {
|
fn server_donation_address(&self) -> Result<Value> {
|
||||||
@ -294,6 +348,16 @@ impl Connection {
|
|||||||
fn blockchain_scripthash_subscribe(&mut self, params: &[Value]) -> Result<Value> {
|
fn blockchain_scripthash_subscribe(&mut self, params: &[Value]) -> Result<Value> {
|
||||||
let script_hash = hash_from_value(params.first()).chain_err(|| "bad script_hash")?;
|
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 history_txids = get_history(&self.query, &script_hash[..], self.txs_limit)?;
|
||||||
let status_hash = get_status_hash(history_txids, &self.query)
|
let status_hash = get_status_hash(history_txids, &self.query)
|
||||||
.map_or(Value::Null, |h| json!(hex::encode(full_hash(&h[..]))));
|
.map_or(Value::Null, |h| json!(hex::encode(full_hash(&h[..]))));
|
||||||
@ -467,9 +531,8 @@ impl Connection {
|
|||||||
"server.peers.subscribe" => self.server_peers_subscribe(),
|
"server.peers.subscribe" => self.server_peers_subscribe(),
|
||||||
"server.ping" => Ok(Value::Null),
|
"server.ping" => Ok(Value::Null),
|
||||||
"server.version" => self.server_version(),
|
"server.version" => self.server_version(),
|
||||||
|
|
||||||
#[cfg(feature = "electrum-discovery")]
|
|
||||||
"server.features" => self.server_features(),
|
"server.features" => self.server_features(),
|
||||||
|
|
||||||
#[cfg(feature = "electrum-discovery")]
|
#[cfg(feature = "electrum-discovery")]
|
||||||
"server.add_peer" => self.server_add_peer(params),
|
"server.add_peer" => self.server_add_peer(params),
|
||||||
|
|
||||||
@ -545,14 +608,99 @@ impl Connection {
|
|||||||
Ok(())
|
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<()> {
|
fn handle_replies(&mut self, shutdown: crossbeam_channel::Receiver<()>) -> Result<()> {
|
||||||
|
let idle_timeout = Duration::from_secs(self.idle_timeout);
|
||||||
loop {
|
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! {
|
crossbeam_channel::select! {
|
||||||
recv(self.chan.receiver()) -> msg => {
|
recv(self.chan.receiver()) -> msg => {
|
||||||
let msg = msg.chain_err(|| "channel closed")?;
|
let msg = msg.chain_err(|| "channel closed")?;
|
||||||
trace!("RPC {:?}", msg);
|
trace!("RPC {:?}", msg);
|
||||||
match msg {
|
match msg {
|
||||||
Message::Request(line) => {
|
Message::Request(line) => {
|
||||||
|
self.last_request_at = Instant::now();
|
||||||
let result = self.handle_line(&line);
|
let result = self.handle_line(&line);
|
||||||
self.send_values(&[result])?
|
self.send_values(&[result])?
|
||||||
}
|
}
|
||||||
@ -566,12 +714,25 @@ impl Connection {
|
|||||||
self.chan.close();
|
self.chan.close();
|
||||||
return Ok(());
|
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) -> _ => {
|
recv(shutdown) -> _ => {
|
||||||
self.chan.close();
|
self.chan.close();
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
recv(idle_deadline) -> _ => {
|
||||||
|
let idle_for = self.last_request_at.elapsed();
|
||||||
|
self.close_idle_connection(idle_for);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -620,18 +781,134 @@ 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(
|
fn handle_requests(
|
||||||
mut reader: BufReader<ConnectionStream>,
|
stream: ConnectionStream,
|
||||||
tx: crossbeam_channel::Sender<Message>,
|
tx: crossbeam_channel::Sender<Message>,
|
||||||
|
max_line_size: usize,
|
||||||
) -> Result<()> {
|
) -> 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 {
|
loop {
|
||||||
let mut line = Vec::<u8>::new();
|
let mut line = Vec::<u8>::new();
|
||||||
reader
|
// 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
|
||||||
.read_until(b'\n', &mut line)
|
.read_until(b'\n', &mut line)
|
||||||
.chain_err(|| "failed to read a request")?;
|
.chain_err(|| "failed to read a request")?;
|
||||||
if line.is_empty() {
|
if line.is_empty() {
|
||||||
tx.send(Message::Done).chain_err(|| "channel closed")?;
|
tx.send(Message::Done).chain_err(|| "channel closed")?;
|
||||||
return Ok(());
|
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 {
|
} else {
|
||||||
if line.starts_with(&[22, 3, 1]) {
|
if line.starts_with(&[22, 3, 1]) {
|
||||||
// (very) naive SSL handshake detection
|
// (very) naive SSL handshake detection
|
||||||
@ -653,7 +930,7 @@ impl Connection {
|
|||||||
|
|
||||||
pub fn run(mut self) {
|
pub fn run(mut self) {
|
||||||
self.stats.clients.inc();
|
self.stats.clients.inc();
|
||||||
let reader = BufReader::new(self.stream.try_clone().expect("failed to clone TcpStream"));
|
let stream = self.stream.try_clone().expect("failed to clone TcpStream");
|
||||||
let tx = self.chan.sender();
|
let tx = self.chan.sender();
|
||||||
|
|
||||||
let die_please = self.die_please.take().unwrap();
|
let die_please = self.die_please.take().unwrap();
|
||||||
@ -671,11 +948,14 @@ impl Connection {
|
|||||||
let _ = reply_killer.send(());
|
let _ = reply_killer.send(());
|
||||||
});
|
});
|
||||||
|
|
||||||
let child = spawn_thread("reader", || Connection::handle_requests(reader, tx));
|
let max_line_size = self.max_line_size;
|
||||||
|
let child = spawn_thread("reader", move || {
|
||||||
|
Connection::handle_requests(stream, tx, max_line_size)
|
||||||
|
});
|
||||||
if let Err(e) = self.handle_replies(reply_receiver) {
|
if let Err(e) = self.handle_replies(reply_receiver) {
|
||||||
error!(
|
error!(
|
||||||
"[{}] connection handling failed: {}",
|
"[{}] connection handling failed: {}",
|
||||||
self.stream.addr_string(),
|
self.client_string(),
|
||||||
e.display_chain().to_string()
|
e.display_chain().to_string()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -683,8 +963,9 @@ impl Connection {
|
|||||||
self.stats
|
self.stats
|
||||||
.subscriptions
|
.subscriptions
|
||||||
.sub(self.status_hashes.len() as i64);
|
.sub(self.status_hashes.len() as i64);
|
||||||
|
self.unregister_client();
|
||||||
|
|
||||||
let addr = self.stream.addr_string();
|
let addr = self.client_string();
|
||||||
debug!("[{}] shutting down connection", addr);
|
debug!("[{}] shutting down connection", addr);
|
||||||
// Drop the Arc so that the stream properly closes.
|
// Drop the Arc so that the stream properly closes.
|
||||||
drop(arc_stream);
|
drop(arc_stream);
|
||||||
@ -742,6 +1023,11 @@ pub enum Message {
|
|||||||
Request(String),
|
Request(String),
|
||||||
PeriodicUpdate,
|
PeriodicUpdate,
|
||||||
Done,
|
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 {
|
pub enum Notification {
|
||||||
@ -830,11 +1116,10 @@ impl RPC {
|
|||||||
|
|
||||||
let notification = Channel::unbounded();
|
let notification = Channel::unbounded();
|
||||||
|
|
||||||
// Discovery is enabled when electrum-public-hosts is set
|
let server_features = {
|
||||||
#[cfg(feature = "electrum-discovery")]
|
|
||||||
let discovery = config.electrum_public_hosts.clone().map(|hosts| {
|
|
||||||
use crate::chain::genesis_hash;
|
use crate::chain::genesis_hash;
|
||||||
let features = ServerFeatures {
|
let hosts = config.electrum_public_hosts.clone().unwrap_or_default();
|
||||||
|
Arc::new(ServerFeatures {
|
||||||
hosts,
|
hosts,
|
||||||
server_version: VERSION_STRING.clone(),
|
server_version: VERSION_STRING.clone(),
|
||||||
genesis_hash: genesis_hash(config.network_type),
|
genesis_hash: genesis_hash(config.network_type),
|
||||||
@ -842,10 +1127,15 @@ impl RPC {
|
|||||||
protocol_max: PROTOCOL_VERSION,
|
protocol_max: PROTOCOL_VERSION,
|
||||||
hash_function: "sha256".into(),
|
hash_function: "sha256".into(),
|
||||||
pruning: None,
|
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(
|
let discovery = Arc::new(DiscoveryManager::new(
|
||||||
config.network_type,
|
config.network_type,
|
||||||
features,
|
server_features.as_ref().clone(),
|
||||||
PROTOCOL_VERSION,
|
PROTOCOL_VERSION,
|
||||||
config.electrum_announce,
|
config.electrum_announce,
|
||||||
config.tor_proxy,
|
config.tor_proxy,
|
||||||
@ -855,12 +1145,22 @@ impl RPC {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let txs_limit = config.electrum_txs_limit;
|
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 {
|
RPC {
|
||||||
notification: notification.sender(),
|
notification: notification.sender(),
|
||||||
server: Some(spawn_thread("rpc", move || {
|
server: Some(spawn_thread("rpc", move || {
|
||||||
let senders =
|
let senders =
|
||||||
Arc::new(Mutex::new(Vec::<crossbeam_channel::Sender<Message>>::new()));
|
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 = Channel::unbounded();
|
||||||
let acceptor_shutdown_sender = acceptor_shutdown.sender();
|
let acceptor_shutdown_sender = acceptor_shutdown.sender();
|
||||||
@ -872,15 +1172,39 @@ impl RPC {
|
|||||||
acceptor_shutdown_sender,
|
acceptor_shutdown_sender,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut threads = HashMap::new();
|
let mut threads: HashMap<thread::ThreadId, (thread::JoinHandle<()>, Sender<()>)> =
|
||||||
|
HashMap::new();
|
||||||
let (garbage_sender, garbage_receiver) = crossbeam_channel::unbounded();
|
let (garbage_sender, garbage_receiver) = crossbeam_channel::unbounded();
|
||||||
|
|
||||||
while let Some(stream) = acceptor.receiver().recv().unwrap() {
|
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();
|
let addr = stream.addr_string();
|
||||||
// explicitely scope the shadowed variables for the new thread
|
// explicitely scope the shadowed variables for the new thread
|
||||||
let query = Arc::clone(&query);
|
let query = Arc::clone(&query);
|
||||||
let senders = Arc::clone(&senders);
|
let senders = Arc::clone(&senders);
|
||||||
let stats = Arc::clone(&stats);
|
let stats = Arc::clone(&stats);
|
||||||
|
let client_counts = Arc::clone(&client_counts);
|
||||||
let garbage_sender = garbage_sender.clone();
|
let garbage_sender = garbage_sender.clone();
|
||||||
|
|
||||||
// Kill the peers properly
|
// Kill the peers properly
|
||||||
@ -889,6 +1213,7 @@ impl RPC {
|
|||||||
|
|
||||||
#[cfg(feature = "electrum-discovery")]
|
#[cfg(feature = "electrum-discovery")]
|
||||||
let discovery = discovery.clone();
|
let discovery = discovery.clone();
|
||||||
|
let server_features = Arc::clone(&server_features);
|
||||||
|
|
||||||
let spawned = spawn_thread("peer", move || {
|
let spawned = spawn_thread("peer", move || {
|
||||||
let addr = stream.addr_string();
|
let addr = stream.addr_string();
|
||||||
@ -898,7 +1223,14 @@ impl RPC {
|
|||||||
stream,
|
stream,
|
||||||
stats,
|
stats,
|
||||||
txs_limit,
|
txs_limit,
|
||||||
|
max_line_size,
|
||||||
|
max_subscriptions,
|
||||||
|
idle_timeout,
|
||||||
peace_receiver,
|
peace_receiver,
|
||||||
|
server_features,
|
||||||
|
haproxy_depth,
|
||||||
|
connections_per_client,
|
||||||
|
client_counts,
|
||||||
#[cfg(feature = "electrum-discovery")]
|
#[cfg(feature = "electrum-discovery")]
|
||||||
discovery,
|
discovery,
|
||||||
);
|
);
|
||||||
@ -911,15 +1243,6 @@ impl RPC {
|
|||||||
|
|
||||||
trace!("[{}] spawned {:?}", addr, spawned.thread().id());
|
trace!("[{}] spawned {:?}", addr, spawned.thread().id());
|
||||||
threads.insert(spawned.thread().id(), (spawned, killer));
|
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 these
|
||||||
drop(acceptor);
|
drop(acceptor);
|
||||||
@ -1081,6 +1404,15 @@ 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> {
|
fn try_clone(&self) -> std::io::Result<Self> {
|
||||||
Ok(match self {
|
Ok(match self {
|
||||||
ConnectionStream::Tcp(s, a) => ConnectionStream::Tcp(s.try_clone()?, *a),
|
ConnectionStream::Tcp(s, a) => ConnectionStream::Tcp(s.try_clone()?, *a),
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::{Arc, RwLock, RwLockReadGuard};
|
use std::sync::{Arc, RwLock, RwLockReadGuard};
|
||||||
|
|
||||||
use bitcoin::hashes::{hex::FromHex, sha256, Hash};
|
use bitcoin::hashes::{sha256, Hash};
|
||||||
use elements::confidential::{Asset, Value};
|
use elements::confidential::{Asset, Value};
|
||||||
use elements::encode::{deserialize, serialize};
|
use elements::encode::{deserialize, serialize};
|
||||||
use elements::secp256k1_zkp::ZERO_TWEAK;
|
use elements::secp256k1_zkp::ZERO_TWEAK;
|
||||||
@ -13,17 +13,22 @@ use crate::elements::registry::{AssetMeta, AssetRegistry};
|
|||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::new_index::schema::{Operation, TxHistoryInfo, TxHistoryKey, TxHistoryRow};
|
use crate::new_index::schema::{Operation, TxHistoryInfo, TxHistoryKey, TxHistoryRow};
|
||||||
use crate::new_index::{db::DBFlush, ChainQuery, DBRow, Mempool, Query};
|
use crate::new_index::{db::DBFlush, ChainQuery, DBRow, Mempool, Query};
|
||||||
use crate::util::{bincode_util, full_hash, Bytes, FullHash, TransactionStatus, TxInput};
|
use crate::util::{
|
||||||
|
bincode_util, full_hash, Bytes, FullHash, IsProvablyUnspendable, TransactionStatus, TxInput,
|
||||||
|
};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref NATIVE_ASSET_ID: AssetId =
|
pub static ref NATIVE_ASSET_ID: AssetId =
|
||||||
AssetId::from_hex("6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d")
|
"6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d"
|
||||||
|
.parse()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
pub static ref NATIVE_ASSET_ID_TESTNET: AssetId =
|
pub static ref NATIVE_ASSET_ID_TESTNET: AssetId =
|
||||||
AssetId::from_hex("144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49")
|
"144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49"
|
||||||
|
.parse()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
pub static ref NATIVE_ASSET_ID_REGTEST: AssetId =
|
pub static ref NATIVE_ASSET_ID_REGTEST: AssetId =
|
||||||
AssetId::from_hex("5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225")
|
"5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225"
|
||||||
|
.parse()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,7 +98,7 @@ impl IssuedAsset {
|
|||||||
let reissuance_token = parse_asset_id(&asset.reissuance_token);
|
let reissuance_token = parse_asset_id(&asset.reissuance_token);
|
||||||
|
|
||||||
let contract_hash = if issuance.asset_entropy != [0u8; 32] {
|
let contract_hash = if issuance.asset_entropy != [0u8; 32] {
|
||||||
Some(ContractHash::from_inner(issuance.asset_entropy))
|
Some(ContractHash::from_byte_array(issuance.asset_entropy))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@ -258,7 +263,7 @@ fn index_tx_assets(
|
|||||||
value: pegout.value,
|
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 let (Asset::Explicit(asset_id), Value::Explicit(value)) = (txo.asset, txo.value) {
|
||||||
if value > 0 {
|
if value > 0 {
|
||||||
history.push((
|
history.push((
|
||||||
@ -277,7 +282,7 @@ fn index_tx_assets(
|
|||||||
for (txi_index, txi) in tx.input.iter().enumerate() {
|
for (txi_index, txi) in tx.input.iter().enumerate() {
|
||||||
if let Some(pegin) = get_pegin_data(txi, network) {
|
if let Some(pegin) = get_pegin_data(txi, network) {
|
||||||
history.push((
|
history.push((
|
||||||
pegin.asset.explicit().unwrap(),
|
pegin.asset,
|
||||||
TxHistoryInfo::Pegin(PeginInfo {
|
TxHistoryInfo::Pegin(PeginInfo {
|
||||||
txid,
|
txid,
|
||||||
vin: txi_index as u32,
|
vin: txi_index as u32,
|
||||||
@ -409,7 +414,7 @@ pub fn lookup_asset(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_issuance_entropy(txin: &TxIn) -> Result<sha256::Midstate> {
|
pub fn get_issuance_entropy(txin: &TxIn) -> Result<sha256::Midstate> {
|
||||||
if !txin.has_issuance {
|
if !txin.has_issuance() {
|
||||||
bail!("input has no issuance");
|
bail!("input has no issuance");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
use bitcoin::hashes::{hex::ToHex, Hash};
|
use bitcoin::hashes::Hash;
|
||||||
|
use elements::hex::ToHex;
|
||||||
use elements::secp256k1_zkp::ZERO_TWEAK;
|
use elements::secp256k1_zkp::ZERO_TWEAK;
|
||||||
use elements::{confidential::Value, encode::serialize, issuance::ContractHash, AssetId, TxIn};
|
use elements::{confidential::Value, encode::serialize, issuance::ContractHash, AssetId, TxIn};
|
||||||
|
|
||||||
@ -8,7 +9,7 @@ mod registry;
|
|||||||
|
|
||||||
use asset::get_issuance_entropy;
|
use asset::get_issuance_entropy;
|
||||||
pub use asset::{lookup_asset, LiquidAsset};
|
pub use asset::{lookup_asset, LiquidAsset};
|
||||||
pub use registry::{AssetRegistry, AssetSorting};
|
pub use registry::{AssetMeta, AssetRegistry, AssetSorting};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct IssuanceValue {
|
pub struct IssuanceValue {
|
||||||
|
|||||||
@ -1,14 +1,18 @@
|
|||||||
use bitcoin::hashes::hex::ToHex;
|
use elements::hex::ToHex;
|
||||||
use elements::{confidential::Asset, PeginData, PegoutData, TxIn, TxOut};
|
use elements::{confidential::Asset, PeginData, PegoutData, TxIn, TxOut};
|
||||||
|
|
||||||
use crate::chain::{bitcoin_genesis_hash, BNetwork, Network};
|
use crate::chain::{bitcoin_genesis_hash, BNetwork, Network};
|
||||||
use crate::util::{FullHash, ScriptToAsm};
|
use crate::util::FullHash;
|
||||||
|
|
||||||
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()?;
|
let pegged_asset_id = network.pegged_asset()?;
|
||||||
txout
|
txout.pegin_data().and_then(|pegin| {
|
||||||
.pegin_data()
|
if pegin.asset == *pegged_asset_id {
|
||||||
.filter(|pegin| pegin.asset == Asset::Explicit(*pegged_asset_id))
|
Some(pegin)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_pegout_data(
|
pub fn get_pegout_data(
|
||||||
@ -17,15 +21,20 @@ pub fn get_pegout_data(
|
|||||||
parent_network: BNetwork,
|
parent_network: BNetwork,
|
||||||
) -> Option<PegoutData<'_>> {
|
) -> Option<PegoutData<'_>> {
|
||||||
let pegged_asset_id = network.pegged_asset()?;
|
let pegged_asset_id = network.pegged_asset()?;
|
||||||
txout.pegout_data().filter(|pegout| {
|
txout.pegout_data().and_then(|pegout| {
|
||||||
pegout.asset == Asset::Explicit(*pegged_asset_id)
|
if pegout.asset == Asset::Explicit(*pegged_asset_id)
|
||||||
&& pegout.genesis_hash
|
&& pegout.genesis_hash
|
||||||
== bitcoin_genesis_hash(match parent_network {
|
== bitcoin_genesis_hash(match parent_network {
|
||||||
BNetwork::Bitcoin => Network::Liquid,
|
BNetwork::Bitcoin => Network::Liquid,
|
||||||
BNetwork::Testnet => Network::LiquidTestnet,
|
BNetwork::Testnet | BNetwork::Testnet4 => Network::LiquidTestnet,
|
||||||
BNetwork::Signet => return false,
|
BNetwork::Signet => return None,
|
||||||
BNetwork::Regtest => Network::LiquidRegtest,
|
BNetwork::Regtest => Network::LiquidRegtest,
|
||||||
})
|
})
|
||||||
|
{
|
||||||
|
Some(pegout)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,7 +42,7 @@ pub fn get_pegout_data(
|
|||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct PegoutValue {
|
pub struct PegoutValue {
|
||||||
pub genesis_hash: String,
|
pub genesis_hash: String,
|
||||||
pub scriptpubkey: bitcoin::Script,
|
pub scriptpubkey: bitcoin::ScriptBuf,
|
||||||
pub scriptpubkey_asm: String,
|
pub scriptpubkey_asm: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub scriptpubkey_address: Option<String>,
|
pub scriptpubkey_address: Option<String>,
|
||||||
@ -44,12 +53,12 @@ impl PegoutValue {
|
|||||||
let pegoutdata = get_pegout_data(txout, network, parent_network)?;
|
let pegoutdata = get_pegout_data(txout, network, parent_network)?;
|
||||||
|
|
||||||
// pending https://github.com/ElementsProject/rust-elements/pull/69 is merged
|
// pending https://github.com/ElementsProject/rust-elements/pull/69 is merged
|
||||||
let scriptpubkey = bitcoin::Script::from(pegoutdata.script_pubkey.into_bytes());
|
let scriptpubkey = bitcoin::ScriptBuf::from(pegoutdata.script_pubkey.into_bytes());
|
||||||
let address = bitcoin::Address::from_script(&scriptpubkey, parent_network);
|
let address = bitcoin::Address::from_script(&scriptpubkey, parent_network).ok();
|
||||||
|
|
||||||
Some(PegoutValue {
|
Some(PegoutValue {
|
||||||
genesis_hash: pegoutdata.genesis_hash.to_hex(),
|
genesis_hash: pegoutdata.genesis_hash.to_hex(),
|
||||||
scriptpubkey_asm: scriptpubkey.to_asm(),
|
scriptpubkey_asm: scriptpubkey.to_asm_string(),
|
||||||
scriptpubkey_address: address.map(|s| s.to_string()),
|
scriptpubkey_address: address.map(|s| s.to_string()),
|
||||||
scriptpubkey,
|
scriptpubkey,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::str::FromStr;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
use std::{cmp, fs, path, thread};
|
use std::{cmp, fs, path, thread};
|
||||||
|
|
||||||
use serde_json::Value as JsonValue;
|
use serde_json::Value as JsonValue;
|
||||||
|
|
||||||
use bitcoin::hashes::hex::FromHex;
|
|
||||||
use elements::AssetId;
|
use elements::AssetId;
|
||||||
|
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
@ -14,6 +14,8 @@ use crate::errors::*;
|
|||||||
// (in number of hex characters, not bytes)
|
// (in number of hex characters, not bytes)
|
||||||
|
|
||||||
const DIR_PARTITION_LEN: usize = 2;
|
const DIR_PARTITION_LEN: usize = 2;
|
||||||
|
const SEARCH_SORT_CANDIDATE_LIMIT: usize = 2000;
|
||||||
|
|
||||||
pub struct AssetRegistry {
|
pub struct AssetRegistry {
|
||||||
directory: path::PathBuf,
|
directory: path::PathBuf,
|
||||||
assets_cache: HashMap<AssetId, (SystemTime, AssetMeta)>,
|
assets_cache: HashMap<AssetId, (SystemTime, AssetMeta)>,
|
||||||
@ -53,6 +55,39 @@ 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<()> {
|
pub fn fs_sync(&mut self) -> Result<()> {
|
||||||
for entry in fs::read_dir(&self.directory).chain_err(|| "failed reading asset dir")? {
|
for entry in fs::read_dir(&self.directory).chain_err(|| "failed reading asset dir")? {
|
||||||
let entry = entry.chain_err(|| "invalid fh")?;
|
let entry = entry.chain_err(|| "invalid fh")?;
|
||||||
@ -70,7 +105,7 @@ impl AssetRegistry {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let asset_id = AssetId::from_hex(
|
let asset_id = AssetId::from_str(
|
||||||
path.file_stem()
|
path.file_stem()
|
||||||
.unwrap() // cannot fail if extension() succeeded
|
.unwrap() // cannot fail if extension() succeeded
|
||||||
.to_str()
|
.to_str()
|
||||||
@ -126,7 +161,7 @@ pub struct AssetMeta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AssetMeta {
|
impl AssetMeta {
|
||||||
fn domain(&self) -> Option<&str> {
|
pub(crate) fn domain(&self) -> Option<&str> {
|
||||||
self.entity["domain"].as_str()
|
self.entity["domain"].as_str()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -192,3 +227,72 @@ fn lc_cmp_opt(a: &Option<String>, b: &Option<String>) -> cmp::Ordering {
|
|||||||
.map(|a| a.to_lowercase())
|
.map(|a| a.to_lowercase())
|
||||||
.cmp(&b.as_ref().map(|b| b.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())
|
||||||
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ use std::io::Cursor;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
use crate::chain::{Block, BlockHash};
|
use crate::chain::{Block, BlockHash, BlockSizeCompat};
|
||||||
use crate::daemon::Daemon;
|
use crate::daemon::Daemon;
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::util::{spawn_thread, HeaderEntry, SyncChannel};
|
use crate::util::{spawn_thread, HeaderEntry, SyncChannel};
|
||||||
@ -82,7 +82,7 @@ pub fn bitcoind_sequential_fetcher(
|
|||||||
.zip(entries)
|
.zip(entries)
|
||||||
.map(|(block, entry)| BlockEntry {
|
.map(|(block, entry)| BlockEntry {
|
||||||
entry: entry.clone(), // TODO: remove this clone()
|
entry: entry.clone(), // TODO: remove this clone()
|
||||||
size: block.size() as u32,
|
size: block.get_block_size() as u32,
|
||||||
block,
|
block,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@ -138,7 +138,7 @@ fn bitcoind_fetcher(
|
|||||||
.zip(entries)
|
.zip(entries)
|
||||||
.map(|(block, entry)| BlockEntry {
|
.map(|(block, entry)| BlockEntry {
|
||||||
entry: entry.clone(), // TODO: remove this clone()
|
entry: entry.clone(), // TODO: remove this clone()
|
||||||
size: block.size() as u32,
|
size: block.get_block_size() as u32,
|
||||||
block,
|
block,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@ -290,12 +290,7 @@ fn parse_blocks(blob: Vec<u8>, magic: u32) -> Result<Vec<SizedBlock>> {
|
|||||||
cursor.set_position(end);
|
cursor.set_position(end);
|
||||||
}
|
}
|
||||||
|
|
||||||
let pool = rayon::ThreadPoolBuilder::new()
|
Ok(super::THREAD_POOL.install(|| {
|
||||||
.num_threads(0) // CPU-bound
|
|
||||||
.thread_name(|i| format!("parse-blocks-{}", i))
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
Ok(pool.install(|| {
|
|
||||||
slices
|
slices
|
||||||
.into_par_iter()
|
.into_par_iter()
|
||||||
.map(|(slice, size)| (deserialize(slice).expect("failed to parse Block"), size))
|
.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::sync::{Arc, RwLock};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use crate::chain::{deserialize, Network, OutPoint, Transaction, TxOut, Txid};
|
use crate::chain::{deserialize, Network, OutPoint, Transaction, TxOut, Txid, TxidCompat};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::daemon::Daemon;
|
use crate::daemon::Daemon;
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
@ -481,7 +481,7 @@ impl Mempool {
|
|||||||
let mut txids = Vec::with_capacity(txs.len());
|
let mut txids = Vec::with_capacity(txs.len());
|
||||||
// Phase 1: add to txstore
|
// Phase 1: add to txstore
|
||||||
for tx in txs {
|
for tx in txs {
|
||||||
let txid = tx.txid();
|
let txid = tx.get_txid();
|
||||||
// Only push if it doesn't already exist.
|
// Only push if it doesn't already exist.
|
||||||
// This is important now that update doesn't lock during
|
// This is important now that update doesn't lock during
|
||||||
// the entire function body.
|
// the entire function body.
|
||||||
@ -526,7 +526,10 @@ impl Mempool {
|
|||||||
fee: feeinfo.fee,
|
fee: feeinfo.fee,
|
||||||
vsize: feeinfo.vsize,
|
vsize: feeinfo.vsize,
|
||||||
#[cfg(not(feature = "liquid"))]
|
#[cfg(not(feature = "liquid"))]
|
||||||
value: prevouts.values().map(|prevout| prevout.value).sum(),
|
value: prevouts
|
||||||
|
.values()
|
||||||
|
.map(|prevout| prevout.value.to_sat())
|
||||||
|
.sum(),
|
||||||
});
|
});
|
||||||
|
|
||||||
self.feeinfo.insert(txid, feeinfo);
|
self.feeinfo.insert(txid, feeinfo);
|
||||||
@ -534,6 +537,10 @@ impl Mempool {
|
|||||||
// An iterator over (ScriptHash, TxHistoryInfo)
|
// An iterator over (ScriptHash, TxHistoryInfo)
|
||||||
let spending = prevouts.into_iter().map(|(input_index, prevout)| {
|
let spending = prevouts.into_iter().map(|(input_index, prevout)| {
|
||||||
let txi = tx.input.get(input_index as usize).unwrap();
|
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),
|
compute_script_hash(&prevout.script_pubkey),
|
||||||
TxHistoryInfo::Spending(SpendingInfo {
|
TxHistoryInfo::Spending(SpendingInfo {
|
||||||
@ -541,7 +548,7 @@ impl Mempool {
|
|||||||
vin: input_index,
|
vin: input_index,
|
||||||
prev_txid: full_hash(&txi.previous_output.txid[..]),
|
prev_txid: full_hash(&txi.previous_output.txid[..]),
|
||||||
prev_vout: txi.previous_output.vout,
|
prev_vout: txi.previous_output.vout,
|
||||||
value: prevout.value,
|
value,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@ -555,12 +562,16 @@ impl Mempool {
|
|||||||
.enumerate()
|
.enumerate()
|
||||||
.filter(|(_, txo)| is_spendable(txo) || config.index_unspendables)
|
.filter(|(_, txo)| is_spendable(txo) || config.index_unspendables)
|
||||||
.map(|(index, txo)| {
|
.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),
|
compute_script_hash(&txo.script_pubkey),
|
||||||
TxHistoryInfo::Funding(FundingInfo {
|
TxHistoryInfo::Funding(FundingInfo {
|
||||||
txid: txid_bytes,
|
txid: txid_bytes,
|
||||||
vout: index as u32,
|
vout: index as u32,
|
||||||
value: txo.value,
|
value,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,6 +5,16 @@ pub mod precache;
|
|||||||
mod query;
|
mod query;
|
||||||
pub mod schema;
|
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::db::{DBRow, DB};
|
||||||
pub use self::fetch::{BlockEntry, FetchFrom};
|
pub use self::fetch::{BlockEntry, FetchFrom};
|
||||||
pub use self::mempool::Mempool;
|
pub use self::mempool::Mempool;
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use std::collections::{BTreeSet, HashMap};
|
|||||||
use std::sync::{Arc, RwLock, RwLockReadGuard};
|
use std::sync::{Arc, RwLock, RwLockReadGuard};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use crate::chain::{Network, OutPoint, Transaction, TxOut, Txid};
|
use crate::chain::{Network, OutPoint, Transaction, TxOut, Txid, TxidCompat};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::daemon::{Daemon, MempoolAcceptResult, SubmitPackageResult};
|
use crate::daemon::{Daemon, MempoolAcceptResult, SubmitPackageResult};
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
@ -14,7 +14,7 @@ use crate::util::{is_spendable, BlockId, Bytes, TransactionStatus};
|
|||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
use crate::{
|
use crate::{
|
||||||
chain::{asset::AssetRegistryLock, AssetId},
|
chain::{asset::AssetRegistryLock, AssetId},
|
||||||
elements::{lookup_asset, AssetRegistry, AssetSorting, LiquidAsset},
|
elements::{lookup_asset, AssetMeta, AssetRegistry, AssetSorting, LiquidAsset},
|
||||||
};
|
};
|
||||||
|
|
||||||
const FEE_ESTIMATES_TTL: u64 = 60; // seconds
|
const FEE_ESTIMATES_TTL: u64 = 60; // seconds
|
||||||
@ -161,7 +161,7 @@ impl Query {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn lookup_tx_spends(&self, tx: Transaction) -> Vec<Option<SpendingInput>> {
|
pub fn lookup_tx_spends(&self, tx: Transaction) -> Vec<Option<SpendingInput>> {
|
||||||
let txid = tx.txid();
|
let txid = tx.get_txid();
|
||||||
|
|
||||||
tx.output
|
tx.output
|
||||||
.par_iter()
|
.par_iter()
|
||||||
@ -271,6 +271,15 @@ 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")]
|
#[cfg(feature = "liquid")]
|
||||||
pub fn list_registry_assets(
|
pub fn list_registry_assets(
|
||||||
&self,
|
&self,
|
||||||
@ -298,4 +307,27 @@ impl Query {
|
|||||||
.collect::<Result<Vec<_>>>()?;
|
.collect::<Result<Vec<_>>>()?;
|
||||||
Ok((total_num, results))
|
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,6 +1,7 @@
|
|||||||
use bitcoin::hashes::sha256d::Hash as Sha256dHash;
|
use bitcoin::hashes::sha256d::Hash as Sha256dHash;
|
||||||
#[cfg(not(feature = "liquid"))]
|
#[cfg(not(feature = "liquid"))]
|
||||||
use bitcoin::util::merkleblock::MerkleBlock;
|
use bitcoin::merkle_tree::MerkleBlock;
|
||||||
|
#[cfg(not(feature = "liquid"))]
|
||||||
use bitcoin::VarInt;
|
use bitcoin::VarInt;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
@ -10,7 +11,7 @@ use sha2::{Digest, Sha256};
|
|||||||
use bitcoin::consensus::encode::{deserialize, serialize};
|
use bitcoin::consensus::encode::{deserialize, serialize};
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
use elements::{
|
use elements::{
|
||||||
encode::{deserialize, serialize},
|
encode::{deserialize, serialize, VarInt},
|
||||||
AssetId,
|
AssetId,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -20,7 +21,7 @@ use std::path::Path;
|
|||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
use crate::chain::{
|
use crate::chain::{
|
||||||
BlockHash, BlockHeader, Network, OutPoint, Script, Transaction, TxOut, Txid, Value,
|
BlockHash, BlockHeader, Network, OutPoint, Script, Transaction, TxOut, Txid, TxidCompat, Value,
|
||||||
};
|
};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::daemon::Daemon;
|
use crate::daemon::Daemon;
|
||||||
@ -1233,7 +1234,7 @@ impl ChainQuery {
|
|||||||
let _timer = self.start_timer("lookup_txn");
|
let _timer = self.start_timer("lookup_txn");
|
||||||
self.lookup_raw_txn(txid, blockhash).map(|rawtx| {
|
self.lookup_raw_txn(txid, blockhash).map(|rawtx| {
|
||||||
let txn: Transaction = deserialize(&rawtx).expect("failed to parse Transaction");
|
let txn: Transaction = deserialize(&rawtx).expect("failed to parse Transaction");
|
||||||
assert_eq!(*txid, txn.txid());
|
assert_eq!(*txid, txn.get_txid());
|
||||||
txn
|
txn
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1390,7 +1391,7 @@ fn add_blocks(block_entries: &[BlockEntry], iconfig: &IndexerConfig) -> Vec<DBRo
|
|||||||
.map(|b| {
|
.map(|b| {
|
||||||
let mut rows = vec![];
|
let mut rows = vec![];
|
||||||
let blockhash = full_hash(&b.entry.hash()[..]);
|
let blockhash = full_hash(&b.entry.hash()[..]);
|
||||||
let txids: Vec<Txid> = b.block.txdata.iter().map(|tx| tx.txid()).collect();
|
let txids: Vec<Txid> = b.block.txdata.iter().map(|tx| tx.get_txid()).collect();
|
||||||
for tx in &b.block.txdata {
|
for tx in &b.block.txdata {
|
||||||
add_transaction(tx, blockhash, &mut rows, iconfig);
|
add_transaction(tx, blockhash, &mut rows, iconfig);
|
||||||
}
|
}
|
||||||
@ -1420,7 +1421,7 @@ fn add_transaction(
|
|||||||
rows.push(TxRow::new(tx).into_row());
|
rows.push(TxRow::new(tx).into_row());
|
||||||
}
|
}
|
||||||
|
|
||||||
let txid = full_hash(&tx.txid()[..]);
|
let txid = full_hash(&tx.get_txid()[..]);
|
||||||
for (txo_index, txo) in tx.output.iter().enumerate() {
|
for (txo_index, txo) in tx.output.iter().enumerate() {
|
||||||
if is_spendable(txo) {
|
if is_spendable(txo) {
|
||||||
rows.push(TxOutRow::new(&txid, txo_index, txo).into_row());
|
rows.push(TxOutRow::new(&txid, txo_index, txo).into_row());
|
||||||
@ -1446,24 +1447,7 @@ fn lookup_txos(
|
|||||||
outpoints: &BTreeSet<OutPoint>,
|
outpoints: &BTreeSet<OutPoint>,
|
||||||
allow_missing: bool,
|
allow_missing: bool,
|
||||||
) -> HashMap<OutPoint, TxOut> {
|
) -> HashMap<OutPoint, TxOut> {
|
||||||
let mut loop_count = 10;
|
super::THREAD_POOL.install(|| {
|
||||||
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(|| {
|
|
||||||
// Should match lookup_txos_sequential
|
// Should match lookup_txos_sequential
|
||||||
outpoints
|
outpoints
|
||||||
.par_iter()
|
.par_iter()
|
||||||
@ -1552,7 +1536,7 @@ fn index_transaction(
|
|||||||
// H{funding-scripthash}{funding-height}{funding-block-pos}F{funding-txid:vout} → ""
|
// H{funding-scripthash}{funding-height}{funding-block-pos}F{funding-txid:vout} → ""
|
||||||
// persist "edges" for fast is-this-TXO-spent check
|
// persist "edges" for fast is-this-TXO-spent check
|
||||||
// S{funding-txid:vout}{spending-txid:vin} → ""
|
// S{funding-txid:vout}{spending-txid:vin} → ""
|
||||||
let txid = full_hash(&tx.txid()[..]);
|
let txid = full_hash(&tx.get_txid()[..]);
|
||||||
let script_callback = |script_hash| {
|
let script_callback = |script_hash| {
|
||||||
if let Operation::DeleteBlocksWithHistory(tx) = op {
|
if let Operation::DeleteBlocksWithHistory(tx) = op {
|
||||||
tx.send(script_hash).expect("unbounded channel won't fail");
|
tx.send(script_hash).expect("unbounded channel won't fail");
|
||||||
@ -1560,6 +1544,10 @@ fn index_transaction(
|
|||||||
};
|
};
|
||||||
for (txo_index, txo) in tx.output.iter().enumerate() {
|
for (txo_index, txo) in tx.output.iter().enumerate() {
|
||||||
if is_spendable(txo) || iconfig.index_unspendables {
|
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(
|
let history = TxHistoryRow::new(
|
||||||
&txo.script_pubkey,
|
&txo.script_pubkey,
|
||||||
confirmed_height,
|
confirmed_height,
|
||||||
@ -1567,7 +1555,7 @@ fn index_transaction(
|
|||||||
TxHistoryInfo::Funding(FundingInfo {
|
TxHistoryInfo::Funding(FundingInfo {
|
||||||
txid,
|
txid,
|
||||||
vout: txo_index as u32,
|
vout: txo_index as u32,
|
||||||
value: txo.value,
|
value,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
script_callback(history.key.hash);
|
script_callback(history.key.hash);
|
||||||
@ -1588,6 +1576,11 @@ fn index_transaction(
|
|||||||
.get(&txi.previous_output)
|
.get(&txi.previous_output)
|
||||||
.unwrap_or_else(|| panic!("missing previous txo {}", 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(
|
let history = TxHistoryRow::new(
|
||||||
&prev_txo.script_pubkey,
|
&prev_txo.script_pubkey,
|
||||||
confirmed_height,
|
confirmed_height,
|
||||||
@ -1597,7 +1590,7 @@ fn index_transaction(
|
|||||||
vin: txi_index as u32,
|
vin: txi_index as u32,
|
||||||
prev_txid: full_hash(&txi.previous_output.txid[..]),
|
prev_txid: full_hash(&txi.previous_output.txid[..]),
|
||||||
prev_vout: txi.previous_output.vout,
|
prev_vout: txi.previous_output.vout,
|
||||||
value: prev_txo.value,
|
value: prev_value,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
script_callback(history.key.hash);
|
script_callback(history.key.hash);
|
||||||
@ -1664,7 +1657,7 @@ struct TxRow {
|
|||||||
|
|
||||||
impl TxRow {
|
impl TxRow {
|
||||||
fn new(txn: &Transaction) -> TxRow {
|
fn new(txn: &Transaction) -> TxRow {
|
||||||
let txid = full_hash(&txn.txid()[..]);
|
let txid = full_hash(&txn.get_txid()[..]);
|
||||||
TxRow {
|
TxRow {
|
||||||
key: TxRowKey { code: b'T', txid },
|
key: TxRowKey { code: b'T', txid },
|
||||||
value: serialize(txn),
|
value: serialize(txn),
|
||||||
@ -1697,7 +1690,7 @@ struct TxConfRow {
|
|||||||
|
|
||||||
impl TxConfRow {
|
impl TxConfRow {
|
||||||
fn new(txn: &Transaction, blockhash: FullHash) -> TxConfRow {
|
fn new(txn: &Transaction, blockhash: FullHash) -> TxConfRow {
|
||||||
let txid = full_hash(&txn.txid()[..]);
|
let txid = full_hash(&txn.get_txid()[..]);
|
||||||
TxConfRow {
|
TxConfRow {
|
||||||
key: TxConfKey {
|
key: TxConfKey {
|
||||||
code: b'C',
|
code: b'C',
|
||||||
|
|||||||
413
src/rest.rs
413
src/rest.rs
@ -1,4 +1,8 @@
|
|||||||
use crate::chain::{address, BlockHash, Network, OutPoint, Script, Transaction, TxIn, TxOut, Txid};
|
#[cfg(feature = "liquid")]
|
||||||
|
use crate::chain::address;
|
||||||
|
use crate::chain::{
|
||||||
|
BlockHash, Network, OutPoint, Script, Transaction, TxIn, TxOut, Txid, TxidCompat,
|
||||||
|
};
|
||||||
use crate::config::{Config, BITCOIND_SUBVER, VERSION_STRING};
|
use crate::config::{Config, BITCOIND_SUBVER, VERSION_STRING};
|
||||||
use crate::errors;
|
use crate::errors;
|
||||||
use crate::metrics::Metrics;
|
use crate::metrics::Metrics;
|
||||||
@ -6,15 +10,17 @@ use crate::new_index::{compute_script_hash, Query, SpendingInput, Utxo};
|
|||||||
use crate::util::{
|
use crate::util::{
|
||||||
create_socket, electrum_merkle, extract_tx_prevouts, full_hash, get_innerscripts, get_tx_fee,
|
create_socket, electrum_merkle, extract_tx_prevouts, full_hash, get_innerscripts, get_tx_fee,
|
||||||
has_prevout, is_coinbase, transaction_sigop_count, BlockHeaderMeta, BlockId, FullHash,
|
has_prevout, is_coinbase, transaction_sigop_count, BlockHeaderMeta, BlockId, FullHash,
|
||||||
ScriptToAddr, ScriptToAsm, TransactionStatus,
|
IsProvablyUnspendable, ScriptToAddr, ScriptToAsm, SegwitDetection, TransactionStatus,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(not(feature = "liquid"))]
|
#[cfg(not(feature = "liquid"))]
|
||||||
use {bitcoin::consensus::encode, std::str::FromStr};
|
use bitcoin::consensus::encode;
|
||||||
|
|
||||||
|
#[cfg(feature = "liquid")]
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use bitcoin::blockdata::opcodes;
|
use bitcoin::blockdata::opcodes;
|
||||||
use bitcoin::hashes::hex::{FromHex, ToHex};
|
use bitcoin::hashes::Hash;
|
||||||
use bitcoin::hashes::Error as HashError;
|
|
||||||
use hex::{self, FromHexError};
|
use hex::{self, FromHexError};
|
||||||
use hyper::{
|
use hyper::{
|
||||||
header::HeaderValue,
|
header::HeaderValue,
|
||||||
@ -29,7 +35,7 @@ use hyperlocal::UnixServerExt;
|
|||||||
use std::{cmp, fs};
|
use std::{cmp, fs};
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
use {
|
use {
|
||||||
crate::elements::{peg::PegoutValue, AssetSorting, IssuanceValue},
|
crate::elements::{peg::PegoutValue, AssetMeta, AssetSorting, IssuanceValue},
|
||||||
elements::{
|
elements::{
|
||||||
confidential::{Asset, Nonce, Value},
|
confidential::{Asset, Nonce, Value},
|
||||||
encode, AssetId,
|
encode, AssetId,
|
||||||
@ -53,6 +59,12 @@ const MULTI_ADDRESS_LIMIT: usize = 300;
|
|||||||
const ASSETS_PER_PAGE: usize = 25;
|
const ASSETS_PER_PAGE: usize = 25;
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
const ASSETS_MAX_PER_PAGE: usize = 100;
|
const ASSETS_MAX_PER_PAGE: usize = 100;
|
||||||
|
#[cfg(feature = "liquid")]
|
||||||
|
const ASSETS_SEARCH_DEFAULT_LIMIT: usize = 15;
|
||||||
|
#[cfg(feature = "liquid")]
|
||||||
|
const ASSETS_SEARCH_MAX_LIMIT: usize = 100;
|
||||||
|
#[cfg(feature = "liquid")]
|
||||||
|
const ASSETS_SEARCH_MAX_QUERY_LEN: usize = 64;
|
||||||
|
|
||||||
const TTL_LONG: u32 = 157_784_630; // ttl for static resources (5 years)
|
const TTL_LONG: u32 = 157_784_630; // ttl for static resources (5 years)
|
||||||
const TTL_SHORT: u32 = 10; // ttl for volatie resources
|
const TTL_SHORT: u32 = 10; // ttl for volatie resources
|
||||||
@ -91,29 +103,30 @@ impl BlockValue {
|
|||||||
#[cfg_attr(feature = "liquid", allow(unused_variables))]
|
#[cfg_attr(feature = "liquid", allow(unused_variables))]
|
||||||
fn new(blockhm: BlockHeaderMeta) -> Self {
|
fn new(blockhm: BlockHeaderMeta) -> Self {
|
||||||
let header = blockhm.header_entry.header();
|
let header = blockhm.header_entry.header();
|
||||||
|
|
||||||
|
#[cfg(not(feature = "liquid"))]
|
||||||
|
let version = header.version.to_consensus() as u32;
|
||||||
|
#[cfg(feature = "liquid")]
|
||||||
|
let version = header.version;
|
||||||
|
|
||||||
BlockValue {
|
BlockValue {
|
||||||
id: header.block_hash().to_hex(),
|
id: header.block_hash().to_string(),
|
||||||
height: blockhm.header_entry.height() as u32,
|
height: blockhm.header_entry.height() as u32,
|
||||||
version: {
|
version,
|
||||||
#[allow(clippy::unnecessary_cast)]
|
|
||||||
{
|
|
||||||
header.version as u32
|
|
||||||
}
|
|
||||||
},
|
|
||||||
timestamp: header.time,
|
timestamp: header.time,
|
||||||
tx_count: blockhm.meta.tx_count,
|
tx_count: blockhm.meta.tx_count,
|
||||||
size: blockhm.meta.size,
|
size: blockhm.meta.size,
|
||||||
weight: blockhm.meta.weight,
|
weight: blockhm.meta.weight,
|
||||||
merkle_root: header.merkle_root.to_hex(),
|
merkle_root: header.merkle_root.to_string(),
|
||||||
previousblockhash: if header.prev_blockhash != BlockHash::default() {
|
previousblockhash: if header.prev_blockhash != BlockHash::all_zeros() {
|
||||||
Some(header.prev_blockhash.to_hex())
|
Some(header.prev_blockhash.to_string())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
mediantime: blockhm.mtp,
|
mediantime: blockhm.mtp,
|
||||||
|
|
||||||
#[cfg(not(feature = "liquid"))]
|
#[cfg(not(feature = "liquid"))]
|
||||||
bits: header.bits,
|
bits: header.bits.to_consensus(),
|
||||||
#[cfg(not(feature = "liquid"))]
|
#[cfg(not(feature = "liquid"))]
|
||||||
nonce: header.nonce,
|
nonce: header.nonce,
|
||||||
#[cfg(not(feature = "liquid"))]
|
#[cfg(not(feature = "liquid"))]
|
||||||
@ -125,14 +138,40 @@ impl BlockValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "liquid")]
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct AssetRegistrySearchResult {
|
||||||
|
asset_id: AssetId,
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
ticker: Option<String>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
domain: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "liquid")]
|
||||||
|
impl AssetRegistrySearchResult {
|
||||||
|
fn new(asset_id: &AssetId, meta: &AssetMeta) -> Self {
|
||||||
|
let domain = meta.domain().map(String::from);
|
||||||
|
Self {
|
||||||
|
asset_id: *asset_id,
|
||||||
|
name: meta.name.clone(),
|
||||||
|
ticker: meta.ticker.clone(),
|
||||||
|
domain,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Calculate the difficulty of a BlockHeader
|
/// Calculate the difficulty of a BlockHeader
|
||||||
/// using Bitcoin Core code ported to Rust.
|
/// using Bitcoin Core code ported to Rust.
|
||||||
///
|
///
|
||||||
/// https://github.com/bitcoin/bitcoin/blob/v25.0/src/rpc/blockchain.cpp#L75-L97
|
/// https://github.com/bitcoin/bitcoin/blob/v25.0/src/rpc/blockchain.cpp#L75-L97
|
||||||
#[cfg_attr(feature = "liquid", allow(dead_code))]
|
#[cfg_attr(feature = "liquid", allow(dead_code))]
|
||||||
fn difficulty_new(bh: &bitcoin::BlockHeader) -> f64 {
|
fn difficulty_new(bh: &bitcoin::block::Header) -> f64 {
|
||||||
let mut n_shift = (bh.bits >> 24) & 0xff;
|
let mut n_shift = (bh.bits.to_consensus() >> 24) & 0xff;
|
||||||
let mut d_diff = (0x0000ffff as f64) / ((bh.bits & 0x00ffffff) as f64);
|
let mut d_diff = (0x0000ffff as f64) / ((bh.bits.to_consensus() & 0x00ffffff) as f64);
|
||||||
|
|
||||||
while n_shift < 29 {
|
while n_shift < 29 {
|
||||||
d_diff *= 256.0;
|
d_diff *= 256.0;
|
||||||
@ -188,15 +227,30 @@ impl TransactionValue {
|
|||||||
|
|
||||||
let fee = get_tx_fee(&tx, &prevouts, config.network_type);
|
let fee = get_tx_fee(&tx, &prevouts, config.network_type);
|
||||||
|
|
||||||
|
#[cfg(not(feature = "liquid"))]
|
||||||
|
let size = tx.total_size() as u32;
|
||||||
|
#[cfg(feature = "liquid")]
|
||||||
|
let size = tx.size() as u32;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "liquid"))]
|
||||||
|
let weight = tx.weight().to_wu() as u32;
|
||||||
|
#[cfg(feature = "liquid")]
|
||||||
|
let weight = tx.weight() as u32;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "liquid"))]
|
||||||
|
let version = tx.version.0 as u32;
|
||||||
|
#[cfg(feature = "liquid")]
|
||||||
|
let version = tx.version;
|
||||||
|
|
||||||
#[allow(clippy::unnecessary_cast)]
|
#[allow(clippy::unnecessary_cast)]
|
||||||
Ok(TransactionValue {
|
Ok(TransactionValue {
|
||||||
txid: tx.txid(),
|
txid: tx.get_txid(),
|
||||||
version: tx.version as u32,
|
version,
|
||||||
locktime: tx.lock_time,
|
locktime: tx.lock_time.to_consensus_u32(),
|
||||||
vin: vins,
|
vin: vins,
|
||||||
vout: vouts,
|
vout: vouts,
|
||||||
size: tx.size() as u32,
|
size,
|
||||||
weight: tx.weight() as u32,
|
weight,
|
||||||
sigops,
|
sigops,
|
||||||
fee,
|
fee,
|
||||||
status: Some(TransactionStatus::from(blockid)),
|
status: Some(TransactionStatus::from(blockid)),
|
||||||
@ -261,7 +315,7 @@ impl TxInValue {
|
|||||||
.map(ScriptToAsm::to_asm),
|
.map(ScriptToAsm::to_asm),
|
||||||
|
|
||||||
is_coinbase,
|
is_coinbase,
|
||||||
sequence: txin.sequence,
|
sequence: txin.sequence.to_consensus_u32(),
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
is_pegin: txin.is_pegin,
|
is_pegin: txin.is_pegin,
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
@ -312,7 +366,7 @@ struct TxOutValue {
|
|||||||
impl TxOutValue {
|
impl TxOutValue {
|
||||||
fn new(txout: &TxOut, config: &Config) -> Self {
|
fn new(txout: &TxOut, config: &Config) -> Self {
|
||||||
#[cfg(not(feature = "liquid"))]
|
#[cfg(not(feature = "liquid"))]
|
||||||
let value = txout.value;
|
let value = txout.value.to_sat();
|
||||||
|
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
let value = txout.value.explicit();
|
let value = txout.value.explicit();
|
||||||
@ -324,7 +378,7 @@ impl TxOutValue {
|
|||||||
|
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
let asset = match txout.asset {
|
let asset = match txout.asset {
|
||||||
Asset::Explicit(value) => Some(value.to_hex()),
|
Asset::Explicit(value) => Some(value.to_string()),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
@ -347,7 +401,7 @@ impl TxOutValue {
|
|||||||
"fee"
|
"fee"
|
||||||
} else if script.is_empty() {
|
} else if script.is_empty() {
|
||||||
"empty"
|
"empty"
|
||||||
} else if script.is_op_return() {
|
} else if script.is_provably_unspendable_() {
|
||||||
"op_return"
|
"op_return"
|
||||||
} else if script.is_p2pk() {
|
} else if script.is_p2pk() {
|
||||||
"p2pk"
|
"p2pk"
|
||||||
@ -355,16 +409,14 @@ impl TxOutValue {
|
|||||||
"p2pkh"
|
"p2pkh"
|
||||||
} else if script.is_p2sh() {
|
} else if script.is_p2sh() {
|
||||||
"p2sh"
|
"p2sh"
|
||||||
} else if script.is_v0_p2wpkh() {
|
} else if script.segwit_is_p2wpkh() {
|
||||||
"v0_p2wpkh"
|
"v0_p2wpkh"
|
||||||
} else if script.is_v0_p2wsh() {
|
} else if script.segwit_is_p2wsh() {
|
||||||
"v0_p2wsh"
|
"v0_p2wsh"
|
||||||
} else if is_v1_p2tr(script) {
|
} else if script.segwit_is_p2tr() {
|
||||||
"v1_p2tr"
|
"v1_p2tr"
|
||||||
} else if is_anchor(script) {
|
} else if is_anchor(script) {
|
||||||
"anchor"
|
"anchor"
|
||||||
} else if script.is_provably_unspendable() {
|
|
||||||
"provably_unspendable"
|
|
||||||
} else if is_bare_multisig(script) {
|
} else if is_bare_multisig(script) {
|
||||||
"multisig"
|
"multisig"
|
||||||
} else {
|
} else {
|
||||||
@ -391,13 +443,9 @@ impl TxOutValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn is_v1_p2tr(script: &Script) -> bool {
|
|
||||||
script.len() == 34
|
|
||||||
&& script[0] == opcodes::all::OP_PUSHNUM_1.into_u8()
|
|
||||||
&& script[1] == opcodes::all::OP_PUSHBYTES_32.into_u8()
|
|
||||||
}
|
|
||||||
fn is_bare_multisig(script: &Script) -> bool {
|
fn is_bare_multisig(script: &Script) -> bool {
|
||||||
let len = script.len();
|
let len = script.len();
|
||||||
|
let bytes = script.as_bytes();
|
||||||
// 1-of-1 multisig is 37 bytes
|
// 1-of-1 multisig is 37 bytes
|
||||||
// Max is 15 pubkeys
|
// Max is 15 pubkeys
|
||||||
// Min is 1
|
// Min is 1
|
||||||
@ -406,20 +454,21 @@ fn is_bare_multisig(script: &Script) -> bool {
|
|||||||
// OP_M ... OP_N OP_CHECKMULTISIG
|
// OP_M ... OP_N OP_CHECKMULTISIG
|
||||||
// is bare multisig
|
// is bare multisig
|
||||||
len >= 37
|
len >= 37
|
||||||
&& script[len - 1] == opcodes::all::OP_CHECKMULTISIG.into_u8()
|
&& bytes[len - 1] == opcodes::all::OP_CHECKMULTISIG.to_u8()
|
||||||
&& script[len - 2] >= opcodes::all::OP_PUSHNUM_1.into_u8()
|
&& bytes[len - 2] >= opcodes::all::OP_PUSHNUM_1.to_u8()
|
||||||
&& script[len - 2] <= opcodes::all::OP_PUSHNUM_15.into_u8()
|
&& bytes[len - 2] <= opcodes::all::OP_PUSHNUM_15.to_u8()
|
||||||
&& script[0] >= opcodes::all::OP_PUSHNUM_1.into_u8()
|
&& bytes[0] >= opcodes::all::OP_PUSHNUM_1.to_u8()
|
||||||
&& script[0] <= script[len - 2]
|
&& bytes[0] <= bytes[len - 2]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_anchor(script: &Script) -> bool {
|
fn is_anchor(script: &Script) -> bool {
|
||||||
let len = script.len();
|
let bytes = script.as_bytes();
|
||||||
|
let len = bytes.len();
|
||||||
len == 4
|
len == 4
|
||||||
&& script[0] == opcodes::all::OP_PUSHNUM_1.into_u8()
|
&& bytes[0] == opcodes::all::OP_PUSHNUM_1.to_u8()
|
||||||
&& script[1] == opcodes::all::OP_PUSHBYTES_2.into_u8()
|
&& bytes[1] == opcodes::all::OP_PUSHBYTES_2.to_u8()
|
||||||
&& script[2] == 0x4e
|
&& bytes[2] == 0x4e
|
||||||
&& script[3] == 0x73
|
&& bytes[3] == 0x73
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@ -485,7 +534,7 @@ impl From<Utxo> for UtxoValue {
|
|||||||
},
|
},
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
asset: match utxo.asset {
|
asset: match utxo.asset {
|
||||||
Asset::Explicit(asset) => Some(asset.to_hex()),
|
Asset::Explicit(asset) => Some(asset.to_string()),
|
||||||
_ => None,
|
_ => None,
|
||||||
},
|
},
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
@ -495,7 +544,7 @@ impl From<Utxo> for UtxoValue {
|
|||||||
},
|
},
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
nonce: match utxo.nonce {
|
nonce: match utxo.nonce {
|
||||||
Nonce::Explicit(nonce) => Some(nonce.to_hex()),
|
Nonce::Explicit(nonce) => Some(hex::encode(nonce)),
|
||||||
_ => None,
|
_ => None,
|
||||||
},
|
},
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
@ -566,6 +615,19 @@ fn find_txid(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn confirmed_after_txid<'a>(
|
||||||
|
after_txid_location: &TxidLocation,
|
||||||
|
after_txid: Option<&'a Txid>,
|
||||||
|
) -> Option<&'a Txid> {
|
||||||
|
match after_txid_location {
|
||||||
|
// A mempool cursor never exists in chain history, so always
|
||||||
|
// start from the newest confirmed tx when crossing the boundary.
|
||||||
|
TxidLocation::Mempool | TxidLocation::None => None,
|
||||||
|
TxidLocation::Chain(_) => after_txid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Prepare transactions to be serialized in a JSON response
|
/// Prepare transactions to be serialized in a JSON response
|
||||||
///
|
///
|
||||||
/// Any transactions with missing prevouts will be filtered out of the response, rather than returned with incorrect data.
|
/// Any transactions with missing prevouts will be filtered out of the response, rather than returned with incorrect data.
|
||||||
@ -742,7 +804,7 @@ fn handle_request(
|
|||||||
) {
|
) {
|
||||||
(&Method::GET, Some(&"blocks"), Some(&"tip"), Some(&"hash"), None, None) => http_message(
|
(&Method::GET, Some(&"blocks"), Some(&"tip"), Some(&"hash"), None, None) => http_message(
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
query.chain().best_hash().to_hex(),
|
query.chain().best_hash().to_string(),
|
||||||
TTL_SHORT,
|
TTL_SHORT,
|
||||||
),
|
),
|
||||||
|
|
||||||
@ -763,10 +825,10 @@ fn handle_request(
|
|||||||
.header_by_height(height)
|
.header_by_height(height)
|
||||||
.ok_or_else(|| HttpError::not_found("Block not found".to_string()))?;
|
.ok_or_else(|| HttpError::not_found("Block not found".to_string()))?;
|
||||||
let ttl = ttl_by_depth(Some(height), query);
|
let ttl = ttl_by_depth(Some(height), query);
|
||||||
http_message(StatusCode::OK, header.hash().to_hex(), ttl)
|
http_message(StatusCode::OK, header.hash().to_string(), ttl)
|
||||||
}
|
}
|
||||||
(&Method::GET, Some(&"block"), Some(hash), None, None, None) => {
|
(&Method::GET, Some(&"block"), Some(hash), None, None, None) => {
|
||||||
let hash = BlockHash::from_hex(hash)?;
|
let hash = hash.parse::<BlockHash>()?;
|
||||||
let blockhm = query
|
let blockhm = query
|
||||||
.chain()
|
.chain()
|
||||||
.get_block_with_meta(&hash)
|
.get_block_with_meta(&hash)
|
||||||
@ -775,13 +837,13 @@ fn handle_request(
|
|||||||
json_response(block_value, TTL_LONG)
|
json_response(block_value, TTL_LONG)
|
||||||
}
|
}
|
||||||
(&Method::GET, Some(&"block"), Some(hash), Some(&"status"), None, None) => {
|
(&Method::GET, Some(&"block"), Some(hash), Some(&"status"), None, None) => {
|
||||||
let hash = BlockHash::from_hex(hash)?;
|
let hash = hash.parse::<BlockHash>()?;
|
||||||
let status = query.chain().get_block_status(&hash);
|
let status = query.chain().get_block_status(&hash);
|
||||||
let ttl = ttl_by_depth(status.height, query);
|
let ttl = ttl_by_depth(status.height, query);
|
||||||
json_response(status, ttl)
|
json_response(status, ttl)
|
||||||
}
|
}
|
||||||
(&Method::GET, Some(&"block"), Some(hash), Some(&"txids"), None, None) => {
|
(&Method::GET, Some(&"block"), Some(hash), Some(&"txids"), None, None) => {
|
||||||
let hash = BlockHash::from_hex(hash)?;
|
let hash = hash.parse::<BlockHash>()?;
|
||||||
let txids = query
|
let txids = query
|
||||||
.chain()
|
.chain()
|
||||||
.get_block_txids(&hash)
|
.get_block_txids(&hash)
|
||||||
@ -789,7 +851,7 @@ fn handle_request(
|
|||||||
json_response(txids, TTL_LONG)
|
json_response(txids, TTL_LONG)
|
||||||
}
|
}
|
||||||
(&Method::GET, Some(&INTERNAL_PREFIX), Some(&"block"), Some(hash), Some(&"txs"), None) => {
|
(&Method::GET, Some(&INTERNAL_PREFIX), Some(&"block"), Some(hash), Some(&"txs"), None) => {
|
||||||
let hash = BlockHash::from_hex(hash)?;
|
let hash = hash.parse::<BlockHash>()?;
|
||||||
let block_id = query.chain().blockid_by_hash(&hash);
|
let block_id = query.chain().blockid_by_hash(&hash);
|
||||||
let txs = query
|
let txs = query
|
||||||
.chain()
|
.chain()
|
||||||
@ -803,7 +865,7 @@ fn handle_request(
|
|||||||
json_response(prepare_txs(txs, query, config), ttl)
|
json_response(prepare_txs(txs, query, config), ttl)
|
||||||
}
|
}
|
||||||
(&Method::GET, Some(&"block"), Some(hash), Some(&"header"), None, None) => {
|
(&Method::GET, Some(&"block"), Some(hash), Some(&"header"), None, None) => {
|
||||||
let hash = BlockHash::from_hex(hash)?;
|
let hash = hash.parse::<BlockHash>()?;
|
||||||
let header = query
|
let header = query
|
||||||
.chain()
|
.chain()
|
||||||
.get_block_header(&hash)
|
.get_block_header(&hash)
|
||||||
@ -813,7 +875,7 @@ fn handle_request(
|
|||||||
http_message(StatusCode::OK, header_hex, TTL_LONG)
|
http_message(StatusCode::OK, header_hex, TTL_LONG)
|
||||||
}
|
}
|
||||||
(&Method::GET, Some(&"block"), Some(hash), Some(&"raw"), None, None) => {
|
(&Method::GET, Some(&"block"), Some(hash), Some(&"raw"), None, None) => {
|
||||||
let hash = BlockHash::from_hex(hash)?;
|
let hash = hash.parse::<BlockHash>()?;
|
||||||
let raw = query
|
let raw = query
|
||||||
.chain()
|
.chain()
|
||||||
.get_block_raw(&hash)
|
.get_block_raw(&hash)
|
||||||
@ -827,7 +889,7 @@ fn handle_request(
|
|||||||
.unwrap())
|
.unwrap())
|
||||||
}
|
}
|
||||||
(&Method::GET, Some(&"block"), Some(hash), Some(&"txid"), Some(index), None) => {
|
(&Method::GET, Some(&"block"), Some(hash), Some(&"txid"), Some(index), None) => {
|
||||||
let hash = BlockHash::from_hex(hash)?;
|
let hash = hash.parse::<BlockHash>()?;
|
||||||
let index: usize = index.parse()?;
|
let index: usize = index.parse()?;
|
||||||
let txids = query
|
let txids = query
|
||||||
.chain()
|
.chain()
|
||||||
@ -836,10 +898,10 @@ fn handle_request(
|
|||||||
if index >= txids.len() {
|
if index >= txids.len() {
|
||||||
bail!(HttpError::not_found("tx index out of range".to_string()));
|
bail!(HttpError::not_found("tx index out of range".to_string()));
|
||||||
}
|
}
|
||||||
http_message(StatusCode::OK, txids[index].to_hex(), TTL_LONG)
|
http_message(StatusCode::OK, txids[index].to_string(), TTL_LONG)
|
||||||
}
|
}
|
||||||
(&Method::GET, Some(&"block"), Some(hash), Some(&"txs"), start_index, None) => {
|
(&Method::GET, Some(&"block"), Some(hash), Some(&"txs"), start_index, None) => {
|
||||||
let hash = BlockHash::from_hex(hash)?;
|
let hash = hash.parse::<BlockHash>()?;
|
||||||
let txids = query
|
let txids = query
|
||||||
.chain()
|
.chain()
|
||||||
.get_block_txids(&hash)
|
.get_block_txids(&hash)
|
||||||
@ -943,13 +1005,8 @@ fn handle_request(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if txs.len() < max_txs {
|
if txs.len() < max_txs {
|
||||||
let after_txid_ref = if !txs.is_empty() {
|
let after_txid_ref =
|
||||||
// If there are any txs, we know mempool found the
|
confirmed_after_txid(&after_txid_location, after_txid.as_ref());
|
||||||
// after_txid IF it exists... so always return None.
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
after_txid.as_ref()
|
|
||||||
};
|
|
||||||
let mut confirmed_txs = query
|
let mut confirmed_txs = query
|
||||||
.chain()
|
.chain()
|
||||||
.history(
|
.history(
|
||||||
@ -1050,13 +1107,8 @@ fn handle_request(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if txs.len() < max_txs {
|
if txs.len() < max_txs {
|
||||||
let after_txid_ref = if !txs.is_empty() {
|
let after_txid_ref =
|
||||||
// If there are any txs, we know mempool found the
|
confirmed_after_txid(&after_txid_location, after_txid.as_ref());
|
||||||
// after_txid IF it exists... so always return None.
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
after_txid.as_ref()
|
|
||||||
};
|
|
||||||
let mut confirmed_txs = query
|
let mut confirmed_txs = query
|
||||||
.chain()
|
.chain()
|
||||||
.history_group(
|
.history_group(
|
||||||
@ -1105,7 +1157,7 @@ fn handle_request(
|
|||||||
last_seen_txid,
|
last_seen_txid,
|
||||||
) => {
|
) => {
|
||||||
let script_hash = to_scripthash(script_type, script_str, config.network_type)?;
|
let script_hash = to_scripthash(script_type, script_str, config.network_type)?;
|
||||||
let last_seen_txid = last_seen_txid.and_then(|txid| Txid::from_hex(txid).ok());
|
let last_seen_txid = last_seen_txid.and_then(|txid| txid.parse::<Txid>().ok());
|
||||||
let max_txs = query_params
|
let max_txs = query_params
|
||||||
.get("max_txs")
|
.get("max_txs")
|
||||||
.and_then(|s| s.parse::<usize>().ok())
|
.and_then(|s| s.parse::<usize>().ok())
|
||||||
@ -1151,7 +1203,7 @@ fn handle_request(
|
|||||||
last_seen_txid,
|
last_seen_txid,
|
||||||
) => {
|
) => {
|
||||||
let script_hash = to_scripthash(script_type, script_str, config.network_type)?;
|
let script_hash = to_scripthash(script_type, script_str, config.network_type)?;
|
||||||
let last_seen_txid = last_seen_txid.and_then(|txid| Txid::from_hex(txid).ok());
|
let last_seen_txid = last_seen_txid.and_then(|txid| txid.parse::<Txid>().ok());
|
||||||
let max_txs = cmp::min(
|
let max_txs = cmp::min(
|
||||||
config.rest_default_max_address_summary_txs,
|
config.rest_default_max_address_summary_txs,
|
||||||
query_params
|
query_params
|
||||||
@ -1232,7 +1284,7 @@ fn handle_request(
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let last_seen_txid = last_seen_txid.and_then(|txid| Txid::from_hex(txid).ok());
|
let last_seen_txid = last_seen_txid.and_then(|txid| txid.parse::<Txid>().ok());
|
||||||
let max_txs = cmp::min(
|
let max_txs = cmp::min(
|
||||||
config.rest_default_max_address_summary_txs,
|
config.rest_default_max_address_summary_txs,
|
||||||
query_params
|
query_params
|
||||||
@ -1332,7 +1384,7 @@ fn handle_request(
|
|||||||
json_response(results, TTL_SHORT)
|
json_response(results, TTL_SHORT)
|
||||||
}
|
}
|
||||||
(&Method::GET, Some(&"tx"), Some(hash), None, None, None) => {
|
(&Method::GET, Some(&"tx"), Some(hash), None, None, None) => {
|
||||||
let hash = Txid::from_hex(hash)?;
|
let hash = hash.parse::<Txid>()?;
|
||||||
let tx = query
|
let tx = query
|
||||||
.lookup_txn(&hash)
|
.lookup_txn(&hash)
|
||||||
.ok_or_else(|| HttpError::not_found("Transaction not found".to_string()))?;
|
.ok_or_else(|| HttpError::not_found("Transaction not found".to_string()))?;
|
||||||
@ -1357,7 +1409,7 @@ fn handle_request(
|
|||||||
|
|
||||||
match txid_strings
|
match txid_strings
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|txid| Txid::from_hex(&txid))
|
.map(|txid| txid.parse::<Txid>())
|
||||||
.collect::<Result<Vec<Txid>, _>>()
|
.collect::<Result<Vec<Txid>, _>>()
|
||||||
{
|
{
|
||||||
Ok(txids) => {
|
Ok(txids) => {
|
||||||
@ -1376,7 +1428,7 @@ fn handle_request(
|
|||||||
}
|
}
|
||||||
(&Method::GET, Some(&"tx"), Some(hash), Some(out_type @ &"hex"), None, None)
|
(&Method::GET, Some(&"tx"), Some(hash), Some(out_type @ &"hex"), None, None)
|
||||||
| (&Method::GET, Some(&"tx"), Some(hash), Some(out_type @ &"raw"), None, None) => {
|
| (&Method::GET, Some(&"tx"), Some(hash), Some(out_type @ &"raw"), None, None) => {
|
||||||
let hash = Txid::from_hex(hash)?;
|
let hash = hash.parse::<Txid>()?;
|
||||||
let rawtx = query
|
let rawtx = query
|
||||||
.lookup_raw_txn(&hash)
|
.lookup_raw_txn(&hash)
|
||||||
.ok_or_else(|| HttpError::not_found("Transaction not found".to_string()))?;
|
.ok_or_else(|| HttpError::not_found("Transaction not found".to_string()))?;
|
||||||
@ -1396,20 +1448,20 @@ fn handle_request(
|
|||||||
.unwrap())
|
.unwrap())
|
||||||
}
|
}
|
||||||
(&Method::GET, Some(&"tx"), Some(hash), Some(&"status"), None, None) => {
|
(&Method::GET, Some(&"tx"), Some(hash), Some(&"status"), None, None) => {
|
||||||
let hash = Txid::from_hex(hash)?;
|
let hash = hash.parse::<Txid>()?;
|
||||||
let status = query.get_tx_status(&hash);
|
let status = query.get_tx_status(&hash);
|
||||||
let ttl = ttl_by_depth(status.block_height, query);
|
let ttl = ttl_by_depth(status.block_height, query);
|
||||||
json_response(status, ttl)
|
json_response(status, ttl)
|
||||||
}
|
}
|
||||||
|
|
||||||
(&Method::GET, Some(&"tx"), Some(hash), Some(&"merkle-proof"), None, None) => {
|
(&Method::GET, Some(&"tx"), Some(hash), Some(&"merkle-proof"), None, None) => {
|
||||||
let hash = Txid::from_hex(hash)?;
|
let hash = hash.parse::<Txid>()?;
|
||||||
let blockid = query.chain().tx_confirming_block(&hash).ok_or_else(|| {
|
let blockid = query.chain().tx_confirming_block(&hash).ok_or_else(|| {
|
||||||
HttpError::not_found("Transaction not found or is unconfirmed".to_string())
|
HttpError::not_found("Transaction not found or is unconfirmed".to_string())
|
||||||
})?;
|
})?;
|
||||||
let (merkle, pos) =
|
let (merkle, pos) =
|
||||||
electrum_merkle::get_tx_merkle_proof(query.chain(), &hash, &blockid.hash)?;
|
electrum_merkle::get_tx_merkle_proof(query.chain(), &hash, &blockid.hash)?;
|
||||||
let merkle: Vec<String> = merkle.into_iter().map(|txid| txid.to_hex()).collect();
|
let merkle: Vec<String> = merkle.into_iter().map(|txid| txid.to_string()).collect();
|
||||||
let ttl = ttl_by_depth(Some(blockid.height), query);
|
let ttl = ttl_by_depth(Some(blockid.height), query);
|
||||||
json_response(
|
json_response(
|
||||||
json!({ "block_height": blockid.height, "merkle": merkle, "pos": pos }),
|
json!({ "block_height": blockid.height, "merkle": merkle, "pos": pos }),
|
||||||
@ -1418,7 +1470,7 @@ fn handle_request(
|
|||||||
}
|
}
|
||||||
#[cfg(not(feature = "liquid"))]
|
#[cfg(not(feature = "liquid"))]
|
||||||
(&Method::GET, Some(&"tx"), Some(hash), Some(&"merkleblock-proof"), None, None) => {
|
(&Method::GET, Some(&"tx"), Some(hash), Some(&"merkleblock-proof"), None, None) => {
|
||||||
let hash = Txid::from_hex(hash)?;
|
let hash = hash.parse::<Txid>()?;
|
||||||
|
|
||||||
let merkleblock = query.chain().get_merkleblock_proof(&hash).ok_or_else(|| {
|
let merkleblock = query.chain().get_merkleblock_proof(&hash).ok_or_else(|| {
|
||||||
HttpError::not_found("Transaction not found or is unconfirmed".to_string())
|
HttpError::not_found("Transaction not found or is unconfirmed".to_string())
|
||||||
@ -1435,7 +1487,7 @@ fn handle_request(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
(&Method::GET, Some(&"tx"), Some(hash), Some(&"outspend"), Some(index), None) => {
|
(&Method::GET, Some(&"tx"), Some(hash), Some(&"outspend"), Some(index), None) => {
|
||||||
let hash = Txid::from_hex(hash)?;
|
let hash = hash.parse::<Txid>()?;
|
||||||
let outpoint = OutPoint {
|
let outpoint = OutPoint {
|
||||||
txid: hash,
|
txid: hash,
|
||||||
vout: index.parse::<u32>()?,
|
vout: index.parse::<u32>()?,
|
||||||
@ -1450,7 +1502,7 @@ fn handle_request(
|
|||||||
json_response(spend, ttl)
|
json_response(spend, ttl)
|
||||||
}
|
}
|
||||||
(&Method::GET, Some(&"tx"), Some(hash), Some(&"outspends"), None, None) => {
|
(&Method::GET, Some(&"tx"), Some(hash), Some(&"outspends"), None, None) => {
|
||||||
let hash = Txid::from_hex(hash)?;
|
let hash = hash.parse::<Txid>()?;
|
||||||
let tx = query
|
let tx = query
|
||||||
.lookup_txn(&hash)
|
.lookup_txn(&hash)
|
||||||
.ok_or_else(|| HttpError::not_found("Transaction not found".to_string()))?;
|
.ok_or_else(|| HttpError::not_found("Transaction not found".to_string()))?;
|
||||||
@ -1477,7 +1529,7 @@ fn handle_request(
|
|||||||
let txid = query
|
let txid = query
|
||||||
.broadcast_raw(&txhex)
|
.broadcast_raw(&txhex)
|
||||||
.map_err(|err| HttpError::from(err.description().to_string()))?;
|
.map_err(|err| HttpError::from(err.description().to_string()))?;
|
||||||
http_message(StatusCode::OK, txid.to_hex(), 0)
|
http_message(StatusCode::OK, txid.to_string(), 0)
|
||||||
}
|
}
|
||||||
(&Method::POST, Some(&"txs"), Some(&"test"), None, None, None) => {
|
(&Method::POST, Some(&"txs"), Some(&"test"), None, None, None) => {
|
||||||
let txhexes: Vec<String> =
|
let txhexes: Vec<String> =
|
||||||
@ -1507,7 +1559,7 @@ fn handle_request(
|
|||||||
)))
|
)))
|
||||||
} else {
|
} else {
|
||||||
// must be a valid hex string
|
// must be a valid hex string
|
||||||
Vec::<u8>::from_hex(txhex)
|
hex::decode(txhex)
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
HttpError::from(format!("Invalid transaction hex for item {}", index))
|
HttpError::from(format!("Invalid transaction hex for item {}", index))
|
||||||
})
|
})
|
||||||
@ -1557,7 +1609,7 @@ fn handle_request(
|
|||||||
)))
|
)))
|
||||||
} else {
|
} else {
|
||||||
// must be a valid hex string
|
// must be a valid hex string
|
||||||
Vec::<u8>::from_hex(txhex)
|
hex::decode(txhex)
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
HttpError::from(format!("Invalid transaction hex for item {}", index))
|
HttpError::from(format!("Invalid transaction hex for item {}", index))
|
||||||
})
|
})
|
||||||
@ -1586,7 +1638,8 @@ fn handle_request(
|
|||||||
let spends: Vec<Vec<SpendingValue>> = txid_strings
|
let spends: Vec<Vec<SpendingValue>> = txid_strings
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|txid_str| {
|
.map(|txid_str| {
|
||||||
Txid::from_hex(txid_str)
|
txid_str
|
||||||
|
.parse::<Txid>()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|txid| query.lookup_txn(&txid))
|
.and_then(|txid| query.lookup_txn(&txid))
|
||||||
.map_or_else(Vec::new, |tx| {
|
.map_or_else(Vec::new, |tx| {
|
||||||
@ -1617,7 +1670,8 @@ fn handle_request(
|
|||||||
let spends: Vec<Vec<SpendingValue>> = txid_strings
|
let spends: Vec<Vec<SpendingValue>> = txid_strings
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|txid_str| {
|
.map(|txid_str| {
|
||||||
Txid::from_hex(&txid_str)
|
txid_str
|
||||||
|
.parse::<Txid>()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|txid| query.lookup_txn(&txid))
|
.and_then(|txid| query.lookup_txn(&txid))
|
||||||
.map_or_else(Vec::new, |tx| {
|
.map_or_else(Vec::new, |tx| {
|
||||||
@ -1653,7 +1707,7 @@ fn handle_request(
|
|||||||
let index_part = parts.next();
|
let index_part = parts.next();
|
||||||
|
|
||||||
if let (Some(hash), Some(index)) = (hash_part, index_part) {
|
if let (Some(hash), Some(index)) = (hash_part, index_part) {
|
||||||
if let (Ok(txid), Ok(vout)) = (Txid::from_hex(hash), index.parse::<u32>()) {
|
if let (Ok(txid), Ok(vout)) = (hash.parse::<Txid>(), index.parse::<u32>()) {
|
||||||
let outpoint = OutPoint { txid, vout };
|
let outpoint = OutPoint { txid, vout };
|
||||||
return query
|
return query
|
||||||
.lookup_spend(&outpoint)
|
.lookup_spend(&outpoint)
|
||||||
@ -1674,7 +1728,7 @@ fn handle_request(
|
|||||||
json_response(query.mempool().txids(), TTL_SHORT)
|
json_response(query.mempool().txids(), TTL_SHORT)
|
||||||
}
|
}
|
||||||
(&Method::GET, Some(&"mempool"), Some(&"txids"), Some(&"page"), last_seen_txid, None) => {
|
(&Method::GET, Some(&"mempool"), Some(&"txids"), Some(&"page"), last_seen_txid, None) => {
|
||||||
let last_seen_txid = last_seen_txid.and_then(|txid| Txid::from_hex(txid).ok());
|
let last_seen_txid = last_seen_txid.and_then(|txid| txid.parse::<Txid>().ok());
|
||||||
let max_txs = query_params
|
let max_txs = query_params
|
||||||
.get("max_txs")
|
.get("max_txs")
|
||||||
.and_then(|s| s.parse::<usize>().ok())
|
.and_then(|s| s.parse::<usize>().ok())
|
||||||
@ -1707,7 +1761,7 @@ fn handle_request(
|
|||||||
|
|
||||||
match txid_strings
|
match txid_strings
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|txid| Txid::from_hex(&txid))
|
.map(|txid| txid.parse::<Txid>())
|
||||||
.collect::<Result<Vec<Txid>, _>>()
|
.collect::<Result<Vec<Txid>, _>>()
|
||||||
{
|
{
|
||||||
Ok(txids) => {
|
Ok(txids) => {
|
||||||
@ -1732,7 +1786,7 @@ fn handle_request(
|
|||||||
last_seen_txid,
|
last_seen_txid,
|
||||||
None,
|
None,
|
||||||
) => {
|
) => {
|
||||||
let last_seen_txid = last_seen_txid.and_then(|txid| Txid::from_hex(txid).ok());
|
let last_seen_txid = last_seen_txid.and_then(|txid| txid.parse::<Txid>().ok());
|
||||||
let max_txs = query_params
|
let max_txs = query_params
|
||||||
.get("max_txs")
|
.get("max_txs")
|
||||||
.and_then(|s| s.parse::<usize>().ok())
|
.and_then(|s| s.parse::<usize>().ok())
|
||||||
@ -1756,6 +1810,38 @@ fn handle_request(
|
|||||||
json_response(query.estimate_fee_map(), TTL_SHORT)
|
json_response(query.estimate_fee_map(), TTL_SHORT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "liquid")]
|
||||||
|
(&Method::GET, Some(&"assets"), Some(&"registry"), Some(&"search"), None, None) => {
|
||||||
|
let search = query_params.get("q").map(|q| q.trim()).unwrap_or("");
|
||||||
|
let assets = if search.is_empty() {
|
||||||
|
vec![]
|
||||||
|
} else if search.chars().count() > ASSETS_SEARCH_MAX_QUERY_LEN {
|
||||||
|
return Err(HttpError(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"search query too long".to_string(),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
let limit = query_params
|
||||||
|
.get("limit")
|
||||||
|
.and_then(|n| n.parse::<usize>().ok())
|
||||||
|
.unwrap_or(ASSETS_SEARCH_DEFAULT_LIMIT)
|
||||||
|
.min(ASSETS_SEARCH_MAX_LIMIT);
|
||||||
|
|
||||||
|
query
|
||||||
|
.search_registry_assets(search, limit, AssetRegistrySearchResult::new)
|
||||||
|
.map_err(|e| {
|
||||||
|
HttpError(StatusCode::SERVICE_UNAVAILABLE, e.description().to_string())
|
||||||
|
})?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Response::builder()
|
||||||
|
// Disable caching because we don't currently support caching with query string params
|
||||||
|
.header("Cache-Control", "no-store")
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(Body::from(serde_json::to_string(&assets)?))
|
||||||
|
.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
(&Method::GET, Some(&"assets"), Some(&"registry"), None, None, None) => {
|
(&Method::GET, Some(&"assets"), Some(&"registry"), None, None, None) => {
|
||||||
let start_index: usize = query_params
|
let start_index: usize = query_params
|
||||||
@ -1782,9 +1868,24 @@ fn handle_request(
|
|||||||
.unwrap())
|
.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "liquid")]
|
||||||
|
(&Method::GET, Some(&"assets"), Some(&"registry"), Some(asset_str), None, None) => {
|
||||||
|
let asset_id = AssetId::from_str(asset_str)?;
|
||||||
|
let registry_entry = query
|
||||||
|
.lookup_registry_asset(&asset_id)
|
||||||
|
.map_err(|e| {
|
||||||
|
HttpError(StatusCode::SERVICE_UNAVAILABLE, e.description().to_string())
|
||||||
|
})?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
HttpError::not_found("Asset id not found in registry".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
json_response(registry_entry, TTL_SHORT)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
(&Method::GET, Some(&"asset"), Some(asset_str), None, None, None) => {
|
(&Method::GET, Some(&"asset"), Some(asset_str), None, None, None) => {
|
||||||
let asset_id = AssetId::from_hex(asset_str)?;
|
let asset_id = AssetId::from_str(asset_str)?;
|
||||||
let asset_entry = query
|
let asset_entry = query
|
||||||
.lookup_asset(&asset_id)?
|
.lookup_asset(&asset_id)?
|
||||||
.ok_or_else(|| HttpError::not_found("Asset id not found".to_string()))?;
|
.ok_or_else(|| HttpError::not_found("Asset id not found".to_string()))?;
|
||||||
@ -1794,7 +1895,7 @@ fn handle_request(
|
|||||||
|
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
(&Method::GET, Some(&"asset"), Some(asset_str), Some(&"txs"), None, None) => {
|
(&Method::GET, Some(&"asset"), Some(asset_str), Some(&"txs"), None, None) => {
|
||||||
let asset_id = AssetId::from_hex(asset_str)?;
|
let asset_id = AssetId::from_str(asset_str)?;
|
||||||
|
|
||||||
let mut txs = vec![];
|
let mut txs = vec![];
|
||||||
|
|
||||||
@ -1838,8 +1939,8 @@ fn handle_request(
|
|||||||
Some(&"chain"),
|
Some(&"chain"),
|
||||||
last_seen_txid,
|
last_seen_txid,
|
||||||
) => {
|
) => {
|
||||||
let asset_id = AssetId::from_hex(asset_str)?;
|
let asset_id = AssetId::from_str(asset_str)?;
|
||||||
let last_seen_txid = last_seen_txid.and_then(|txid| Txid::from_hex(txid).ok());
|
let last_seen_txid = last_seen_txid.and_then(|txid| txid.parse::<Txid>().ok());
|
||||||
|
|
||||||
let mut txs = query
|
let mut txs = query
|
||||||
.chain()
|
.chain()
|
||||||
@ -1873,7 +1974,7 @@ fn handle_request(
|
|||||||
|
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
(&Method::GET, Some(&"asset"), Some(asset_str), Some(&"txs"), Some(&"mempool"), None) => {
|
(&Method::GET, Some(&"asset"), Some(asset_str), Some(&"txs"), Some(&"mempool"), None) => {
|
||||||
let asset_id = AssetId::from_hex(asset_str)?;
|
let asset_id = AssetId::from_str(asset_str)?;
|
||||||
|
|
||||||
let txs = query
|
let txs = query
|
||||||
.mempool()
|
.mempool()
|
||||||
@ -1887,7 +1988,7 @@ fn handle_request(
|
|||||||
|
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
(&Method::GET, Some(&"asset"), Some(asset_str), Some(&"supply"), param, None) => {
|
(&Method::GET, Some(&"asset"), Some(asset_str), Some(&"supply"), param, None) => {
|
||||||
let asset_id = AssetId::from_hex(asset_str)?;
|
let asset_id = AssetId::from_str(asset_str)?;
|
||||||
let asset_entry = query
|
let asset_entry = query
|
||||||
.lookup_asset(&asset_id)?
|
.lookup_asset(&asset_id)?
|
||||||
.ok_or_else(|| HttpError::not_found("Asset id not found".to_string()))?;
|
.ok_or_else(|| HttpError::not_found("Asset id not found".to_string()))?;
|
||||||
@ -2007,30 +2108,38 @@ fn to_scripthash(
|
|||||||
|
|
||||||
fn address_to_scripthash(addr: &str, network: Network) -> Result<FullHash, HttpError> {
|
fn address_to_scripthash(addr: &str, network: Network) -> Result<FullHash, HttpError> {
|
||||||
#[cfg(not(feature = "liquid"))]
|
#[cfg(not(feature = "liquid"))]
|
||||||
let addr = address::Address::from_str(addr)?;
|
let addr = {
|
||||||
#[cfg(feature = "liquid")]
|
use bitcoin::address::NetworkUnchecked;
|
||||||
let addr = address::Address::parse_with_params(addr, network.address_params())?;
|
let unchecked: bitcoin::Address<NetworkUnchecked> = addr.parse()?;
|
||||||
|
let bnetwork = bitcoin::Network::from(network);
|
||||||
#[cfg(not(feature = "liquid"))]
|
|
||||||
let is_expected_net = {
|
|
||||||
let addr_network = Network::from(addr.network);
|
|
||||||
|
|
||||||
// Testnet, Regtest and Signet all share the same version bytes,
|
// Testnet, Regtest and Signet all share the same version bytes,
|
||||||
// `addr_network` will be detected as Testnet for all of them.
|
// so we need to allow require_network to succeed for all testnet-family networks
|
||||||
addr_network == network
|
let testnet_family = [
|
||||||
|| (addr_network == Network::Testnet
|
bitcoin::Network::Testnet,
|
||||||
&& matches!(
|
bitcoin::Network::Regtest,
|
||||||
network,
|
bitcoin::Network::Signet,
|
||||||
Network::Regtest | Network::Signet | Network::Testnet4
|
bitcoin::Network::Testnet4,
|
||||||
))
|
];
|
||||||
|
if testnet_family.contains(&bnetwork) {
|
||||||
|
// Try each testnet-family network
|
||||||
|
testnet_family
|
||||||
|
.iter()
|
||||||
|
.find_map(|&net| unchecked.clone().require_network(net).ok())
|
||||||
|
.ok_or_else(|| HttpError::from("Address on invalid network".to_string()))?
|
||||||
|
} else {
|
||||||
|
unchecked
|
||||||
|
.require_network(bnetwork)
|
||||||
|
.map_err(|_| HttpError::from("Address on invalid network".to_string()))?
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
let is_expected_net = addr.params == network.address_params();
|
let addr = {
|
||||||
|
let addr = address::Address::parse_with_params(addr, network.address_params())?;
|
||||||
if !is_expected_net {
|
if addr.params != network.address_params() {
|
||||||
bail!(HttpError::from("Address on invalid network".to_string()))
|
return Err(HttpError::from("Address on invalid network".to_string()));
|
||||||
}
|
}
|
||||||
|
addr
|
||||||
|
};
|
||||||
|
|
||||||
Ok(compute_script_hash(&addr.script_pubkey()))
|
Ok(compute_script_hash(&addr.script_pubkey()))
|
||||||
}
|
}
|
||||||
@ -2073,26 +2182,19 @@ impl From<ParseIntError> for HttpError {
|
|||||||
HttpError::from("Invalid number".to_string())
|
HttpError::from("Invalid number".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl From<HashError> for HttpError {
|
|
||||||
fn from(_e: HashError) -> Self {
|
|
||||||
//HttpError::from(e.description().to_string())
|
|
||||||
HttpError::from("Invalid hash string".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<FromHexError> for HttpError {
|
impl From<FromHexError> for HttpError {
|
||||||
fn from(_e: FromHexError) -> Self {
|
fn from(_e: FromHexError) -> Self {
|
||||||
//HttpError::from(e.description().to_string())
|
//HttpError::from(e.description().to_string())
|
||||||
HttpError::from("Invalid hex string".to_string())
|
HttpError::from("Invalid hex string".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl From<bitcoin::hashes::hex::Error> for HttpError {
|
impl From<bitcoin::hashes::hex::HexToArrayError> for HttpError {
|
||||||
fn from(_e: bitcoin::hashes::hex::Error) -> Self {
|
fn from(_e: bitcoin::hashes::hex::HexToArrayError) -> Self {
|
||||||
//HttpError::from(e.description().to_string())
|
HttpError::from("Invalid hex hash".to_string())
|
||||||
HttpError::from("Invalid hex string".to_string())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl From<bitcoin::util::address::Error> for HttpError {
|
impl From<bitcoin::address::ParseError> for HttpError {
|
||||||
fn from(_e: bitcoin::util::address::Error) -> Self {
|
fn from(_e: bitcoin::address::ParseError) -> Self {
|
||||||
//HttpError::from(e.description().to_string())
|
//HttpError::from(e.description().to_string())
|
||||||
HttpError::from("Invalid Bitcoin address".to_string())
|
HttpError::from("Invalid Bitcoin address".to_string())
|
||||||
}
|
}
|
||||||
@ -2132,6 +2234,8 @@ impl From<address::AddressError> for HttpError {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use super::{confirmed_after_txid, TxidLocation};
|
||||||
|
use crate::chain::Txid;
|
||||||
use crate::rest::HttpError;
|
use crate::rest::HttpError;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@ -2198,6 +2302,37 @@ mod tests {
|
|||||||
assert!(err.is_err());
|
assert!(err.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_confirmed_after_txid_uses_chain_cursor_only() {
|
||||||
|
let txid: Txid = "0000000000000000000000000000000000000000000000000000000000000001"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
confirmed_after_txid(&TxidLocation::Mempool, Some(&txid)),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
assert_eq!(confirmed_after_txid(&TxidLocation::None, Some(&txid)), None);
|
||||||
|
assert_eq!(
|
||||||
|
confirmed_after_txid(&TxidLocation::Chain(123), Some(&txid)),
|
||||||
|
Some(&txid)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_confirmed_after_txid_allows_mempool_chain_boundary_progress() {
|
||||||
|
let txid: Txid = "0000000000000000000000000000000000000000000000000000000000000002"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// If a mempool cursor returns no newer mempool txs, confirmed history
|
||||||
|
// must start from the newest confirmed tx instead of seeking this txid.
|
||||||
|
assert_eq!(
|
||||||
|
confirmed_after_txid(&TxidLocation::Mempool, Some(&txid)),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_difficulty_new() {
|
fn test_difficulty_new() {
|
||||||
use super::difficulty_new;
|
use super::difficulty_new;
|
||||||
@ -2299,8 +2434,8 @@ mod tests {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
let to_bh = |b| bitcoin::BlockHeader {
|
let to_bh = |b| bitcoin::block::Header {
|
||||||
version: 1,
|
version: bitcoin::block::Version::ONE,
|
||||||
prev_blockhash: "0000000000000000000000000000000000000000000000000000000000000000"
|
prev_blockhash: "0000000000000000000000000000000000000000000000000000000000000000"
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
@ -2308,7 +2443,7 @@ mod tests {
|
|||||||
.parse()
|
.parse()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
time: 0,
|
time: 0,
|
||||||
bits: b,
|
bits: bitcoin::CompactTarget::from_consensus(b),
|
||||||
nonce: 0,
|
nonce: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
use bitcoin::hashes::Hash;
|
||||||
|
|
||||||
use crate::chain::{BlockHash, BlockHeader};
|
use crate::chain::{BlockHash, BlockHeader};
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::new_index::BlockEntry;
|
use crate::new_index::BlockEntry;
|
||||||
@ -73,7 +75,7 @@ impl HeaderList {
|
|||||||
HeaderList {
|
HeaderList {
|
||||||
headers: vec![],
|
headers: vec![],
|
||||||
heights: HashMap::new(),
|
heights: HashMap::new(),
|
||||||
tip: BlockHash::default(),
|
tip: BlockHash::all_zeros(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +91,7 @@ impl HeaderList {
|
|||||||
|
|
||||||
let mut blockhash = tip_hash;
|
let mut blockhash = tip_hash;
|
||||||
let mut headers_chain: Vec<BlockHeader> = vec![];
|
let mut headers_chain: Vec<BlockHeader> = vec![];
|
||||||
let null_hash = BlockHash::default();
|
let null_hash = BlockHash::all_zeros();
|
||||||
|
|
||||||
while blockhash != null_hash {
|
while blockhash != null_hash {
|
||||||
let header = headers_map.remove(&blockhash).unwrap_or_else(|| {
|
let header = headers_map.remove(&blockhash).unwrap_or_else(|| {
|
||||||
@ -136,7 +138,7 @@ impl HeaderList {
|
|||||||
Some(h) => h.header.prev_blockhash,
|
Some(h) => h.header.prev_blockhash,
|
||||||
None => return vec![], // hashed_headers is empty
|
None => return vec![], // hashed_headers is empty
|
||||||
};
|
};
|
||||||
let null_hash = BlockHash::default();
|
let null_hash = BlockHash::all_zeros();
|
||||||
let new_height: usize = if prev_blockhash == null_hash {
|
let new_height: usize = if prev_blockhash == null_hash {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
@ -175,7 +177,7 @@ impl HeaderList {
|
|||||||
let expected_prev_blockhash = if height > 0 {
|
let expected_prev_blockhash = if height > 0 {
|
||||||
*self.headers[height - 1].hash()
|
*self.headers[height - 1].hash()
|
||||||
} else {
|
} else {
|
||||||
BlockHash::default()
|
BlockHash::all_zeros()
|
||||||
};
|
};
|
||||||
assert_eq!(entry.header().prev_blockhash, expected_prev_blockhash);
|
assert_eq!(entry.header().prev_blockhash, expected_prev_blockhash);
|
||||||
height
|
height
|
||||||
@ -230,7 +232,10 @@ impl HeaderList {
|
|||||||
pub fn tip(&self) -> &BlockHash {
|
pub fn tip(&self) -> &BlockHash {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
self.tip,
|
self.tip,
|
||||||
self.headers.last().map(|h| *h.hash()).unwrap_or_default()
|
self.headers
|
||||||
|
.last()
|
||||||
|
.map(|h| *h.hash())
|
||||||
|
.unwrap_or(BlockHash::all_zeros())
|
||||||
);
|
);
|
||||||
&self.tip
|
&self.tip
|
||||||
}
|
}
|
||||||
@ -306,9 +311,13 @@ pub struct BlockHeaderMeta {
|
|||||||
|
|
||||||
impl From<&BlockEntry> for BlockMeta {
|
impl From<&BlockEntry> for BlockMeta {
|
||||||
fn from(b: &BlockEntry) -> 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 {
|
BlockMeta {
|
||||||
tx_count: b.block.txdata.len() as u32,
|
tx_count: b.block.txdata.len() as u32,
|
||||||
weight: b.block.weight() as u32,
|
weight,
|
||||||
size: b.size,
|
size: b.size,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,9 @@ pub struct TxFeeInfo {
|
|||||||
impl TxFeeInfo {
|
impl TxFeeInfo {
|
||||||
pub fn new(tx: &Transaction, prevouts: &HashMap<u32, &TxOut>, network: Network) -> Self {
|
pub fn new(tx: &Transaction, prevouts: &HashMap<u32, &TxOut>, network: Network) -> Self {
|
||||||
let fee = get_tx_fee(tx, prevouts, network);
|
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;
|
let vsize = tx.weight() / 4;
|
||||||
|
|
||||||
TxFeeInfo {
|
TxFeeInfo {
|
||||||
@ -24,12 +27,15 @@ impl TxFeeInfo {
|
|||||||
|
|
||||||
#[cfg(not(feature = "liquid"))]
|
#[cfg(not(feature = "liquid"))]
|
||||||
pub fn get_tx_fee(tx: &Transaction, prevouts: &HashMap<u32, &TxOut>, _network: Network) -> u64 {
|
pub fn get_tx_fee(tx: &Transaction, prevouts: &HashMap<u32, &TxOut>, _network: Network) -> u64 {
|
||||||
if tx.is_coin_base() {
|
if tx.is_coinbase() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let total_in: u64 = prevouts.values().map(|prevout| prevout.value).sum();
|
let total_in: u64 = prevouts
|
||||||
let total_out: u64 = tx.output.iter().map(|vout| vout.value).sum();
|
.values()
|
||||||
|
.map(|prevout| prevout.value.to_sat())
|
||||||
|
.sum();
|
||||||
|
let total_out: u64 = tx.output.iter().map(|vout| vout.value.to_sat()).sum();
|
||||||
total_in - total_out
|
total_in - total_out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,9 @@ pub mod fees;
|
|||||||
|
|
||||||
pub use self::block::{BlockHeaderMeta, BlockId, BlockMeta, BlockStatus, HeaderEntry, HeaderList};
|
pub use self::block::{BlockHeaderMeta, BlockId, BlockMeta, BlockStatus, HeaderEntry, HeaderList};
|
||||||
pub use self::fees::get_tx_fee;
|
pub use self::fees::get_tx_fee;
|
||||||
pub use self::script::{get_innerscripts, ScriptToAddr, ScriptToAsm};
|
pub use self::script::{
|
||||||
|
get_innerscripts, IsProvablyUnspendable, ScriptToAddr, ScriptToAsm, SegwitDetection,
|
||||||
|
};
|
||||||
pub use self::transaction::{
|
pub use self::transaction::{
|
||||||
extract_tx_prevouts, has_prevout, is_coinbase, is_spendable, serialize_outpoint,
|
extract_tx_prevouts, has_prevout, is_coinbase, is_spendable, serialize_outpoint,
|
||||||
sigops::transaction_sigop_count, TransactionStatus, TxInput,
|
sigops::transaction_sigop_count, TransactionStatus, TxInput,
|
||||||
@ -194,12 +196,12 @@ pub fn create_socket(addr: &SocketAddr) -> Socket {
|
|||||||
///
|
///
|
||||||
/// Copied from https://github.com/rust-bitcoin/rust-bitcoincore-rpc/blob/master/json/src/lib.rs
|
/// Copied from https://github.com/rust-bitcoin/rust-bitcoincore-rpc/blob/master/json/src/lib.rs
|
||||||
pub mod serde_hex {
|
pub mod serde_hex {
|
||||||
use bitcoin::hashes::hex::{FromHex, ToHex};
|
use bitcoin::hashes::hex::FromHex;
|
||||||
use serde::de::Error;
|
use serde::de::Error;
|
||||||
use serde::{Deserializer, Serializer};
|
use serde::{Deserializer, Serializer};
|
||||||
|
|
||||||
pub fn serialize<S: Serializer>(b: &[u8], s: S) -> Result<S::Ok, S::Error> {
|
pub fn serialize<S: Serializer>(b: &[u8], s: S) -> Result<S::Ok, S::Error> {
|
||||||
s.serialize_str(&b.to_hex())
|
s.serialize_str(&hex::encode(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
|
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
|
||||||
@ -208,14 +210,14 @@ pub mod serde_hex {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod opt {
|
pub mod opt {
|
||||||
use bitcoin::hashes::hex::{FromHex, ToHex};
|
use bitcoin::hashes::hex::FromHex;
|
||||||
use serde::de::Error;
|
use serde::de::Error;
|
||||||
use serde::{Deserializer, Serializer};
|
use serde::{Deserializer, Serializer};
|
||||||
|
|
||||||
pub fn serialize<S: Serializer>(b: &Option<Vec<u8>>, s: S) -> Result<S::Ok, S::Error> {
|
pub fn serialize<S: Serializer>(b: &Option<Vec<u8>>, s: S) -> Result<S::Ok, S::Error> {
|
||||||
match *b {
|
match *b {
|
||||||
None => s.serialize_none(),
|
None => s.serialize_none(),
|
||||||
Some(ref b) => s.serialize_str(&b.to_hex()),
|
Some(ref b) => s.serialize_str(&hex::encode(b)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,87 @@ pub struct InnerScripts {
|
|||||||
pub witness_script: Option<Script>,
|
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 {
|
pub trait ScriptToAsm: std::fmt::Debug {
|
||||||
fn to_asm(&self) -> String {
|
fn to_asm(&self) -> String {
|
||||||
let asm = format!("{:?}", self);
|
let asm = format!("{:?}", self);
|
||||||
@ -16,6 +97,7 @@ pub trait ScriptToAsm: std::fmt::Debug {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl ScriptToAsm for bitcoin::Script {}
|
impl ScriptToAsm for bitcoin::Script {}
|
||||||
|
impl ScriptToAsm for bitcoin::ScriptBuf {}
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
impl ScriptToAsm for elements::Script {}
|
impl ScriptToAsm for elements::Script {}
|
||||||
|
|
||||||
@ -25,7 +107,9 @@ pub trait ScriptToAddr {
|
|||||||
#[cfg(not(feature = "liquid"))]
|
#[cfg(not(feature = "liquid"))]
|
||||||
impl ScriptToAddr for bitcoin::Script {
|
impl ScriptToAddr for bitcoin::Script {
|
||||||
fn to_address_str(&self, network: Network) -> Option<String> {
|
fn to_address_str(&self, network: Network) -> Option<String> {
|
||||||
bitcoin::Address::from_script(self, network.into()).map(|s| s.to_string())
|
bitcoin::Address::from_script(self, bitcoin::Network::from(network))
|
||||||
|
.ok()
|
||||||
|
.map(|s| s.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
@ -41,7 +125,11 @@ pub fn get_innerscripts(txin: &TxIn, prevout: &TxOut) -> InnerScripts {
|
|||||||
// Wrapped redeemScript for P2SH spends
|
// Wrapped redeemScript for P2SH spends
|
||||||
let redeem_script = if prevout.script_pubkey.is_p2sh() {
|
let redeem_script = if prevout.script_pubkey.is_p2sh() {
|
||||||
if let Some(Ok(PushBytes(redeemscript))) = txin.script_sig.instructions().last() {
|
if let Some(Ok(PushBytes(redeemscript))) = txin.script_sig.instructions().last() {
|
||||||
Some(Script::from(redeemscript.to_vec()))
|
#[cfg(not(feature = "liquid"))]
|
||||||
|
let bytes = redeemscript.as_bytes().to_vec();
|
||||||
|
#[cfg(feature = "liquid")]
|
||||||
|
let bytes = redeemscript.to_vec();
|
||||||
|
Some(Script::from(bytes))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@ -50,9 +138,9 @@ pub fn get_innerscripts(txin: &TxIn, prevout: &TxOut) -> InnerScripts {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Wrapped witnessScript for P2WSH or P2SH-P2WSH spends
|
// Wrapped witnessScript for P2WSH or P2SH-P2WSH spends
|
||||||
let witness_script = if prevout.script_pubkey.is_v0_p2wsh()
|
let witness_script = if prevout.script_pubkey.segwit_is_p2wsh()
|
||||||
|| prevout.script_pubkey.is_v1_p2tr()
|
|| prevout.script_pubkey.segwit_is_p2tr()
|
||||||
|| redeem_script.as_ref().is_some_and(|s| s.is_v0_p2wsh())
|
|| redeem_script.as_ref().is_some_and(|s| s.segwit_is_p2wsh())
|
||||||
{
|
{
|
||||||
let witness = &txin.witness;
|
let witness = &txin.witness;
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
@ -64,7 +152,7 @@ pub fn get_innerscripts(txin: &TxIn, prevout: &TxOut) -> InnerScripts {
|
|||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
let wit_to_vec = Clone::clone;
|
let wit_to_vec = Clone::clone;
|
||||||
|
|
||||||
let inner_script_slice = if prevout.script_pubkey.is_v1_p2tr() {
|
let inner_script_slice = if prevout.script_pubkey.segwit_is_p2tr() {
|
||||||
// Witness stack is potentially very large
|
// Witness stack is potentially very large
|
||||||
// so we avoid to_vec() or iter().collect() for performance
|
// so we avoid to_vec() or iter().collect() for performance
|
||||||
let w_len = witness.len();
|
let w_len = witness.len();
|
||||||
|
|||||||
@ -1,18 +1,19 @@
|
|||||||
use crate::chain::{BlockHash, OutPoint, Transaction, TxIn, TxOut, Txid};
|
use crate::chain::{BlockHash, OutPoint, Transaction, TxIn, TxOut, Txid};
|
||||||
use crate::errors;
|
use crate::errors;
|
||||||
use crate::util::BlockId;
|
use crate::util::{BlockId, IsProvablyUnspendable};
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[cfg(feature = "liquid")]
|
|
||||||
use bitcoin::hashes::hex::FromHex;
|
|
||||||
|
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref REGTEST_INITIAL_ISSUANCE_PREVOUT: Txid =
|
static ref REGTEST_INITIAL_ISSUANCE_PREVOUT: Txid =
|
||||||
Txid::from_hex("50cdc410c9d0d61eeacc531f52d2c70af741da33af127c364e52ac1ee7c030a5").unwrap();
|
"50cdc410c9d0d61eeacc531f52d2c70af741da33af127c364e52ac1ee7c030a5"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
static ref TESTNET_INITIAL_ISSUANCE_PREVOUT: Txid =
|
static ref TESTNET_INITIAL_ISSUANCE_PREVOUT: Txid =
|
||||||
Txid::from_hex("0c52d2526a5c9f00e9fb74afd15dd3caaf17c823159a514f929ae25193a43a52").unwrap();
|
"0c52d2526a5c9f00e9fb74afd15dd3caaf17c823159a514f929ae25193a43a52"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
@ -70,9 +71,9 @@ pub fn has_prevout(txin: &TxIn) -> bool {
|
|||||||
|
|
||||||
pub fn is_spendable(txout: &TxOut) -> bool {
|
pub fn is_spendable(txout: &TxOut) -> bool {
|
||||||
#[cfg(not(feature = "liquid"))]
|
#[cfg(not(feature = "liquid"))]
|
||||||
return !txout.script_pubkey.is_provably_unspendable();
|
return !txout.script_pubkey.is_provably_unspendable_();
|
||||||
#[cfg(feature = "liquid")]
|
#[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
|
/// Extract the previous TxOuts of a Transaction's TxIns
|
||||||
@ -117,14 +118,14 @@ where
|
|||||||
|
|
||||||
pub(super) mod sigops {
|
pub(super) mod sigops {
|
||||||
use crate::chain::{
|
use crate::chain::{
|
||||||
hashes::hex::FromHex,
|
opcodes::all::{OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY, OP_CHECKSIG, OP_CHECKSIGVERIFY},
|
||||||
opcodes::{
|
|
||||||
all::{OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY, OP_CHECKSIG, OP_CHECKSIGVERIFY},
|
|
||||||
All,
|
|
||||||
},
|
|
||||||
script::{self, Instruction},
|
script::{self, Instruction},
|
||||||
Transaction, TxOut, Witness,
|
Transaction, TxOut, Witness,
|
||||||
};
|
};
|
||||||
|
#[cfg(not(feature = "liquid"))]
|
||||||
|
use bitcoin::opcodes::Opcode;
|
||||||
|
#[cfg(feature = "liquid")]
|
||||||
|
use elements::opcodes::All as Opcode;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// Get sigop count for transaction. prevout_map must have all the prevouts.
|
/// Get sigop count for transaction. prevout_map must have all the prevouts.
|
||||||
@ -136,7 +137,7 @@ pub(super) mod sigops {
|
|||||||
let mut prevouts = Vec::with_capacity(input_count);
|
let mut prevouts = Vec::with_capacity(input_count);
|
||||||
|
|
||||||
#[cfg(not(feature = "liquid"))]
|
#[cfg(not(feature = "liquid"))]
|
||||||
let is_coinbase_or_pegin = tx.is_coin_base();
|
let is_coinbase_or_pegin = tx.is_coinbase();
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
let is_coinbase_or_pegin = tx.is_coinbase() || tx.input.iter().any(|input| input.is_pegin);
|
let is_coinbase_or_pegin = tx.is_coinbase() || tx.input.iter().any(|input| input.is_pegin);
|
||||||
|
|
||||||
@ -154,9 +155,12 @@ pub(super) mod sigops {
|
|||||||
get_sigop_cost(tx, &prevouts, true, true)
|
get_sigop_cost(tx, &prevouts, true, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decode_pushnum(op: &All) -> Option<u8> {
|
fn decode_pushnum(op: &Opcode) -> Option<u8> {
|
||||||
// 81 = OP_1, 96 = OP_16
|
// 81 = OP_1, 96 = OP_16
|
||||||
// 81 -> 1, so... 81 - 80 -> 1
|
// 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();
|
let self_u8 = op.into_u8();
|
||||||
match self_u8 {
|
match self_u8 {
|
||||||
81..=96 => Some(self_u8 - 80),
|
81..=96 => Some(self_u8 - 80),
|
||||||
@ -216,7 +220,7 @@ pub(super) mod sigops {
|
|||||||
|
|
||||||
fn get_p2sh_sigop_count(tx: &Transaction, previous_outputs: &[&TxOut]) -> usize {
|
fn get_p2sh_sigop_count(tx: &Transaction, previous_outputs: &[&TxOut]) -> usize {
|
||||||
#[cfg(not(feature = "liquid"))]
|
#[cfg(not(feature = "liquid"))]
|
||||||
if tx.is_coin_base() {
|
if tx.is_coinbase() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
@ -229,9 +233,14 @@ pub(super) mod sigops {
|
|||||||
if let Some(Ok(script::Instruction::PushBytes(redeem))) =
|
if let Some(Ok(script::Instruction::PushBytes(redeem))) =
|
||||||
input.script_sig.instructions().last()
|
input.script_sig.instructions().last()
|
||||||
{
|
{
|
||||||
let script =
|
#[cfg(not(feature = "liquid"))]
|
||||||
script::Script::from_byte_iter(redeem.iter().map(|v| Ok(*v))).unwrap(); // I only return Ok, so it won't error
|
let script = script::Script::from_bytes(redeem.as_bytes());
|
||||||
n += count_sigops(&script, true);
|
#[cfg(feature = "liquid")]
|
||||||
|
let script = script::Script::from(redeem.to_vec());
|
||||||
|
#[allow(clippy::needless_borrow)]
|
||||||
|
{
|
||||||
|
n += count_sigops(&script, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -256,6 +265,9 @@ pub(super) mod sigops {
|
|||||||
#[inline]
|
#[inline]
|
||||||
fn last_pushdata(script: &script::Script) -> Option<&[u8]> {
|
fn last_pushdata(script: &script::Script) -> Option<&[u8]> {
|
||||||
match script.instructions().last() {
|
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),
|
Some(Ok(Instruction::PushBytes(bytes))) => Some(bytes),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
@ -269,20 +281,36 @@ pub(super) mod sigops {
|
|||||||
) -> usize {
|
) -> usize {
|
||||||
let mut n = 0;
|
let mut n = 0;
|
||||||
|
|
||||||
let script = if prevout.script_pubkey.is_witness_program() {
|
let script_owned;
|
||||||
prevout.script_pubkey.clone()
|
let script: &script::Script = if prevout.script_pubkey.is_witness_program() {
|
||||||
|
&prevout.script_pubkey
|
||||||
} else if prevout.script_pubkey.is_p2sh()
|
} else if prevout.script_pubkey.is_p2sh()
|
||||||
&& is_push_only(script_sig)
|
&& is_push_only(script_sig)
|
||||||
&& !script_sig.is_empty()
|
&& !script_sig.is_empty()
|
||||||
{
|
{
|
||||||
script::Script::from_byte_iter(
|
#[cfg(not(feature = "liquid"))]
|
||||||
last_pushdata(script_sig).unwrap().iter().map(|v| Ok(*v)),
|
{
|
||||||
)
|
script_owned =
|
||||||
.unwrap()
|
script::ScriptBuf::from(last_pushdata(script_sig).unwrap().to_vec());
|
||||||
|
}
|
||||||
|
#[cfg(feature = "liquid")]
|
||||||
|
{
|
||||||
|
script_owned =
|
||||||
|
script::Script::from(last_pushdata(script_sig).unwrap().to_vec());
|
||||||
|
}
|
||||||
|
&script_owned
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
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() {
|
if script.is_v0_p2wsh() {
|
||||||
let bytes = script.as_bytes();
|
let bytes = script.as_bytes();
|
||||||
n += sig_ops(witness, bytes[0], &bytes[2..]);
|
n += sig_ops(witness, bytes[0], &bytes[2..]);
|
||||||
@ -307,7 +335,7 @@ pub(super) mod sigops {
|
|||||||
) -> Result<usize, script::Error> {
|
) -> Result<usize, script::Error> {
|
||||||
let mut n_sigop_cost = get_legacy_sigop_count(tx) * 4;
|
let mut n_sigop_cost = get_legacy_sigop_count(tx) * 4;
|
||||||
#[cfg(not(feature = "liquid"))]
|
#[cfg(not(feature = "liquid"))]
|
||||||
if tx.is_coin_base() {
|
if tx.is_coinbase() {
|
||||||
return Ok(n_sigop_cost);
|
return Ok(n_sigop_cost);
|
||||||
}
|
}
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
@ -333,6 +361,7 @@ pub(super) mod sigops {
|
|||||||
/// Get sigops for the Witness
|
/// Get sigops for the Witness
|
||||||
///
|
///
|
||||||
/// witness_version is the raw opcode. OP_0 is 0, OP_1 is 81, etc.
|
/// 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 {
|
fn sig_ops(witness: &Witness, witness_version: u8, witness_program: &[u8]) -> usize {
|
||||||
#[cfg(feature = "liquid")]
|
#[cfg(feature = "liquid")]
|
||||||
let last_witness = witness.script_witness.last();
|
let last_witness = witness.script_witness.last();
|
||||||
@ -340,12 +369,23 @@ pub(super) mod sigops {
|
|||||||
let last_witness = witness.last();
|
let last_witness = witness.last();
|
||||||
match (witness_version, witness_program.len()) {
|
match (witness_version, witness_program.len()) {
|
||||||
(0, 20) => 1,
|
(0, 20) => 1,
|
||||||
(0, 32) => last_witness
|
(0, 32) => {
|
||||||
.map(|sl| sl.iter().map(|v| Ok(*v)))
|
#[cfg(not(feature = "liquid"))]
|
||||||
.map(script::Script::from_byte_iter)
|
{
|
||||||
// I only return Ok 2 lines up, so there is no way to error
|
#[allow(clippy::needless_borrow)]
|
||||||
.map(|s| count_sigops(&s.unwrap(), true))
|
last_witness
|
||||||
.unwrap_or_default(),
|
.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,
|
_ => 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
44
start
44
start
@ -5,6 +5,7 @@ DAEMON=bitcoin
|
|||||||
NETWORK=mainnet
|
NETWORK=mainnet
|
||||||
FEATURES=default
|
FEATURES=default
|
||||||
DB_FOLDER=/electrs
|
DB_FOLDER=/electrs
|
||||||
|
ASSET_DB_ARGS=()
|
||||||
NODENAME=$(hostname|cut -d . -f1)
|
NODENAME=$(hostname|cut -d . -f1)
|
||||||
LOCATION=$(hostname|cut -d . -f2)
|
LOCATION=$(hostname|cut -d . -f2)
|
||||||
USAGE="Usage: $0 (mainnet|testnet|signet|liquid|liquidtestnet) [popular-scripts]"
|
USAGE="Usage: $0 (mainnet|testnet|signet|liquid|liquidtestnet) [popular-scripts]"
|
||||||
@ -42,7 +43,7 @@ esac
|
|||||||
# which network?
|
# which network?
|
||||||
case "${1}" in
|
case "${1}" in
|
||||||
mainnet)
|
mainnet)
|
||||||
THREADS=$((NPROC / 4))
|
THREADS=$((NPROC / 8))
|
||||||
CRONJOB_TIMING="20 4 * * *"
|
CRONJOB_TIMING="20 4 * * *"
|
||||||
;;
|
;;
|
||||||
testnet)
|
testnet)
|
||||||
@ -64,6 +65,7 @@ case "${1}" in
|
|||||||
DAEMON=elements
|
DAEMON=elements
|
||||||
NETWORK=liquid
|
NETWORK=liquid
|
||||||
FEATURES=liquid
|
FEATURES=liquid
|
||||||
|
ASSET_DB_ARGS=(--asset-db-path /elements/asset_registry_db)
|
||||||
THREADS=$((NPROC / 8))
|
THREADS=$((NPROC / 8))
|
||||||
CRONJOB_TIMING="12 4 * * *"
|
CRONJOB_TIMING="12 4 * * *"
|
||||||
;;
|
;;
|
||||||
@ -71,6 +73,7 @@ case "${1}" in
|
|||||||
DAEMON=elements
|
DAEMON=elements
|
||||||
NETWORK=liquidtestnet
|
NETWORK=liquidtestnet
|
||||||
FEATURES=liquid
|
FEATURES=liquid
|
||||||
|
ASSET_DB_ARGS=(--asset-db-path /elements/asset_registry_testnet_db)
|
||||||
THREADS=$((NPROC / 8))
|
THREADS=$((NPROC / 8))
|
||||||
CRONJOB_TIMING="17 4 * * *"
|
CRONJOB_TIMING="17 4 * * *"
|
||||||
;;
|
;;
|
||||||
@ -153,6 +156,9 @@ do
|
|||||||
# prepare run-time variables
|
# prepare run-time variables
|
||||||
UTXOS_LIMIT=500
|
UTXOS_LIMIT=500
|
||||||
ELECTRUM_TXS_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
|
MAIN_LOOP_DELAY=500
|
||||||
DAEMON_CONF="${HOME}/${DAEMON}.conf"
|
DAEMON_CONF="${HOME}/${DAEMON}.conf"
|
||||||
HTTP_SOCKET_FILE="${HOME}/socket/esplora-${DAEMON}-${NETWORK}"
|
HTTP_SOCKET_FILE="${HOME}/socket/esplora-${DAEMON}-${NETWORK}"
|
||||||
@ -167,43 +173,73 @@ do
|
|||||||
if [ "${NODENAME}" = "node201" ];then
|
if [ "${NODENAME}" = "node201" ];then
|
||||||
UTXOS_LIMIT=9000
|
UTXOS_LIMIT=9000
|
||||||
ELECTRUM_TXS_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
|
MAIN_LOOP_DELAY=14000
|
||||||
fi
|
fi
|
||||||
if [ "${NODENAME}" = "node204" ] && [ "${LOCATION}" = "sg1" ];then
|
if [ "${NODENAME}" = "node204" ] && [ "${LOCATION}" = "sg1" ];then
|
||||||
UTXOS_LIMIT=9000
|
UTXOS_LIMIT=9000
|
||||||
ELECTRUM_TXS_LIMIT=9000
|
ELECTRUM_TXS_LIMIT=9000
|
||||||
|
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
|
||||||
|
ELECTRUM_MAX_SUBSCRIPTIONS=100000
|
||||||
|
ELECTRUM_MAX_CLIENTS=10000
|
||||||
fi
|
fi
|
||||||
if [ "${NODENAME}" = "node204" ] && [ "${LOCATION}" = "hnl" ];then
|
if [ "${NODENAME}" = "node204" ] && [ "${LOCATION}" = "hnl" ];then
|
||||||
UTXOS_LIMIT=9000
|
UTXOS_LIMIT=9000
|
||||||
ELECTRUM_TXS_LIMIT=9000
|
ELECTRUM_TXS_LIMIT=9000
|
||||||
|
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
|
||||||
|
ELECTRUM_MAX_SUBSCRIPTIONS=100000
|
||||||
|
ELECTRUM_MAX_CLIENTS=10000
|
||||||
fi
|
fi
|
||||||
if [ "${NODENAME}" = "node206" ] && [ "${LOCATION}" = "tk7" ];then
|
if [ "${NODENAME}" = "node206" ] && [ "${LOCATION}" = "tk7" ];then
|
||||||
UTXOS_LIMIT=9000
|
UTXOS_LIMIT=9000
|
||||||
ELECTRUM_TXS_LIMIT=9000
|
ELECTRUM_TXS_LIMIT=9000
|
||||||
|
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
|
||||||
|
ELECTRUM_MAX_SUBSCRIPTIONS=100000
|
||||||
|
ELECTRUM_MAX_CLIENTS=10000
|
||||||
fi
|
fi
|
||||||
if [ "${NODENAME}" = "node211" ];then
|
if [ "${NODENAME}" = "node211" ];then
|
||||||
UTXOS_LIMIT=9000
|
UTXOS_LIMIT=9000
|
||||||
ELECTRUM_TXS_LIMIT=9000
|
ELECTRUM_TXS_LIMIT=9000
|
||||||
|
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
|
||||||
|
ELECTRUM_MAX_SUBSCRIPTIONS=100000
|
||||||
|
ELECTRUM_MAX_CLIENTS=10000
|
||||||
fi
|
fi
|
||||||
if [ "${NODENAME}" = "node212" ];then
|
if [ "${NODENAME}" = "node212" ];then
|
||||||
UTXOS_LIMIT=9000
|
UTXOS_LIMIT=9000
|
||||||
ELECTRUM_TXS_LIMIT=9000
|
ELECTRUM_TXS_LIMIT=9000
|
||||||
|
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
|
||||||
|
ELECTRUM_MAX_SUBSCRIPTIONS=100000
|
||||||
|
ELECTRUM_MAX_CLIENTS=10000
|
||||||
fi
|
fi
|
||||||
if [ "${NODENAME}" = "node213" ];then
|
if [ "${NODENAME}" = "node213" ];then
|
||||||
UTXOS_LIMIT=9000
|
UTXOS_LIMIT=9000
|
||||||
ELECTRUM_TXS_LIMIT=9000
|
ELECTRUM_TXS_LIMIT=9000
|
||||||
|
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
|
||||||
|
ELECTRUM_MAX_SUBSCRIPTIONS=100000
|
||||||
|
ELECTRUM_MAX_CLIENTS=10000
|
||||||
fi
|
fi
|
||||||
if [ "${NODENAME}" = "node214" ];then
|
if [ "${NODENAME}" = "node214" ];then
|
||||||
UTXOS_LIMIT=9000
|
UTXOS_LIMIT=9000
|
||||||
ELECTRUM_TXS_LIMIT=9000
|
ELECTRUM_TXS_LIMIT=9000
|
||||||
|
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
|
||||||
|
ELECTRUM_MAX_SUBSCRIPTIONS=100000
|
||||||
|
ELECTRUM_MAX_CLIENTS=10000
|
||||||
fi
|
fi
|
||||||
if [ "${NETWORK}" = "testnet4" ];then
|
if [ "${NETWORK}" = "testnet4" ];then
|
||||||
UTXOS_LIMIT=9000
|
UTXOS_LIMIT=9000
|
||||||
ELECTRUM_TXS_LIMIT=9000
|
ELECTRUM_TXS_LIMIT=9000
|
||||||
|
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
|
||||||
|
ELECTRUM_MAX_SUBSCRIPTIONS=100000
|
||||||
|
ELECTRUM_MAX_CLIENTS=10000
|
||||||
fi
|
fi
|
||||||
if [ "${LOCATION}" = "fmt" ];then
|
if [ "${LOCATION}" = "fmt" ];then
|
||||||
UTXOS_LIMIT=9000
|
UTXOS_LIMIT=9000
|
||||||
ELECTRUM_TXS_LIMIT=9000
|
ELECTRUM_TXS_LIMIT=9000
|
||||||
|
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
|
||||||
|
ELECTRUM_MAX_SUBSCRIPTIONS=100000
|
||||||
|
ELECTRUM_MAX_CLIENTS=10000
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -e "${POPULAR_SCRIPTS_FILE}" ];then
|
if [ ! -e "${POPULAR_SCRIPTS_FILE}" ];then
|
||||||
@ -211,7 +247,7 @@ do
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Run the electrs process (Note: db-dir is used in both commands)
|
# Run the electrs process (Note: db-dir is used in both commands)
|
||||||
cargo run \
|
nice cargo run \
|
||||||
--release \
|
--release \
|
||||||
--bin electrs \
|
--bin electrs \
|
||||||
--features "${FEATURES}" \
|
--features "${FEATURES}" \
|
||||||
@ -219,6 +255,7 @@ do
|
|||||||
--network "${NETWORK}" \
|
--network "${NETWORK}" \
|
||||||
--daemon-dir "${HOME}" \
|
--daemon-dir "${HOME}" \
|
||||||
--db-dir "${DB_FOLDER}" \
|
--db-dir "${DB_FOLDER}" \
|
||||||
|
"${ASSET_DB_ARGS[@]}" \
|
||||||
--main-loop-delay "${MAIN_LOOP_DELAY}" \
|
--main-loop-delay "${MAIN_LOOP_DELAY}" \
|
||||||
--rpc-socket-file "${RPC_SOCKET_FILE}" \
|
--rpc-socket-file "${RPC_SOCKET_FILE}" \
|
||||||
--http-socket-file "${HTTP_SOCKET_FILE}" \
|
--http-socket-file "${HTTP_SOCKET_FILE}" \
|
||||||
@ -229,6 +266,9 @@ do
|
|||||||
--address-search \
|
--address-search \
|
||||||
--utxos-limit "${UTXOS_LIMIT}" \
|
--utxos-limit "${UTXOS_LIMIT}" \
|
||||||
--electrum-txs-limit "${ELECTRUM_TXS_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
|
-vv
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user