Compare commits
1 Commits
mempool
...
junderw/gb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae05c22b2f |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -69,7 +69,7 @@ jobs:
|
||||
mv ./.cargohome/registry ~/.cargo/
|
||||
mv ./.cargohome/git ~/.cargo/
|
||||
rm -rf ./.cargohome
|
||||
pkg install -y git rsync gmake llvm rust rocksdb cmake
|
||||
pkg install -y git rsync gmake llvm rust rocksdb
|
||||
|
||||
run: |
|
||||
cargo check --no-default-features
|
||||
|
||||
90
.github/workflows/project-review-status.yml
vendored
90
.github/workflows/project-review-status.yml
vendored
@ -1,18 +1,84 @@
|
||||
name: Project Board Automation
|
||||
# Workflow: Automatically set project status to "Review Needed" when a reviewer is requested
|
||||
name: Set Project Status on Review Request
|
||||
|
||||
# Trigger: Runs whenever a reviewer is requested on a pull request
|
||||
on:
|
||||
pull_request:
|
||||
types: [review_requested]
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
project-automation:
|
||||
uses: mempool/.github/.github/workflows/project-board-automation.yml@master
|
||||
with:
|
||||
project-number: 8
|
||||
secrets:
|
||||
PROJECT_TOKEN: ${{ secrets.PROJECT_TOKEN }}
|
||||
PROJECT_ID: ${{ secrets.PROJECT_ID }}
|
||||
STATUS_FIELD_ID: ${{ secrets.STATUS_FIELD_ID }}
|
||||
REVIEW_NEEDED_OPTION_ID: ${{ secrets.REVIEW_NEEDED_OPTION_ID }}
|
||||
update-project-status:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Update Project Status to Review Needed
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
# Use the PAT stored in repository secrets (has project write access)
|
||||
github-token: ${{ secrets.PROJECT_TOKEN }}
|
||||
script: |
|
||||
// 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');
|
||||
|
||||
19
AGENTS.md
19
AGENTS.md
@ -1,19 +0,0 @@
|
||||
# electrs
|
||||
|
||||
## Rules
|
||||
|
||||
1. You are an expert Rust developer.
|
||||
2. You are an expert Bitcoin developer.
|
||||
3. If you are unsure of a change, ask the developer to make a choice proactively.
|
||||
|
||||
## Before testing
|
||||
|
||||
- Run cargo fmt (from root)
|
||||
- command: `cargo fmt`
|
||||
|
||||
## Testing
|
||||
|
||||
- Run the checks script
|
||||
- `./scripts/checks.sh`
|
||||
- Run with tests only when a test is added or changed
|
||||
- `INCLUDE_TESTS=1 ./scripts/checks.sh`
|
||||
193
Cargo.lock
generated
193
Cargo.lock
generated
@ -913,7 +913,6 @@ dependencies = [
|
||||
"log",
|
||||
"num_cpus",
|
||||
"page_size",
|
||||
"ppp",
|
||||
"prometheus",
|
||||
"rayon",
|
||||
"rocksdb",
|
||||
@ -948,14 +947,25 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"miow",
|
||||
"ntapi",
|
||||
"wasi",
|
||||
"windows-sys 0.48.0",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miow"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -968,6 +978,15 @@ dependencies = [
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.44"
|
||||
@ -1100,15 +1119,6 @@ version = "0.3.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
|
||||
|
||||
[[package]]
|
||||
name = "ppp"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a7a2049cd2570bd67bf0228e86bf850f8ceb5190a345c471d03a909da6049e0"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
@ -1667,18 +1677,17 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.26.0"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64"
|
||||
checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"libc",
|
||||
"mio",
|
||||
"num_cpus",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.45.0",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1870,31 +1879,13 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
|
||||
dependencies = [
|
||||
"windows-targets 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1903,7 +1894,7 @@ version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1915,100 +1906,34 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.42.2",
|
||||
"windows_aarch64_msvc 0.42.2",
|
||||
"windows_i686_gnu 0.42.2",
|
||||
"windows_i686_msvc 0.42.2",
|
||||
"windows_x86_64_gnu 0.42.2",
|
||||
"windows_x86_64_gnullvm 0.42.2",
|
||||
"windows_x86_64_msvc 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.48.5",
|
||||
"windows_aarch64_msvc 0.48.5",
|
||||
"windows_i686_gnu 0.48.5",
|
||||
"windows_i686_msvc 0.48.5",
|
||||
"windows_x86_64_gnu 0.48.5",
|
||||
"windows_x86_64_gnullvm 0.48.5",
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
@ -2021,72 +1946,24 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
|
||||
@ -45,7 +45,6 @@ socket2 = { version = "0.4", features = ["all"] }
|
||||
num_cpus = "1.12.0"
|
||||
page_size = "0.4.2"
|
||||
prometheus = "0.13"
|
||||
ppp = "2.3.0"
|
||||
rayon = "1.5.0"
|
||||
rocksdb = "0.24.0"
|
||||
serde = "1.0.118"
|
||||
|
||||
@ -57,8 +57,6 @@ TESTNAME="Running cargo clippy check electrum-discovery + liquid"
|
||||
echo "$TESTNAME"
|
||||
cargo clippy $@ -q -F electrum-discovery,liquid
|
||||
|
||||
if [ $INCLUDE_TESTS ]; then
|
||||
TESTNAME="Running cargo test with all features"
|
||||
echo "$TESTNAME"
|
||||
cargo test $@ -q --lib --all-features
|
||||
fi
|
||||
TESTNAME="Running cargo test with all features"
|
||||
echo "$TESTNAME"
|
||||
cargo test $@ -q --lib --all-features
|
||||
|
||||
12
src/chain.rs
12
src/chain.rs
@ -130,6 +130,18 @@ impl Network {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "liquid"))]
|
||||
#[inline(always)]
|
||||
pub const fn is_liquid(self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(feature = "liquid")]
|
||||
#[inline(always)]
|
||||
pub const fn is_liquid(self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(feature = "liquid")]
|
||||
pub fn address_params(self) -> &'static address::AddressParams {
|
||||
// Liquid regtest uses elements's address params
|
||||
|
||||
@ -65,19 +65,14 @@ pub struct Config {
|
||||
pub rest_default_max_address_summary_txs: usize,
|
||||
pub rest_max_mempool_page_size: usize,
|
||||
pub rest_max_mempool_txid_page_size: usize,
|
||||
pub electrum_max_line_size: usize,
|
||||
pub electrum_max_subscriptions: usize,
|
||||
pub electrum_max_clients: usize,
|
||||
pub electrum_idle_timeout: u64,
|
||||
pub electrum_haproxy_depth: usize,
|
||||
pub electrum_connections_per_client: usize,
|
||||
pub electrum_public_hosts: Option<crate::electrum::ServerHosts>,
|
||||
|
||||
#[cfg(feature = "liquid")]
|
||||
pub parent_network: BNetwork,
|
||||
#[cfg(feature = "liquid")]
|
||||
pub asset_db_path: Option<PathBuf>,
|
||||
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
pub electrum_public_hosts: Option<crate::electrum::ServerHosts>,
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
pub electrum_announce: bool,
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
@ -283,36 +278,6 @@ impl Config {
|
||||
.long("electrum-banner")
|
||||
.help("Welcome banner for the Electrum server, shown in the console to clients.")
|
||||
.takes_value(true)
|
||||
).arg(
|
||||
Arg::with_name("electrum_max_line_size")
|
||||
.long("electrum-max-line-size")
|
||||
.help("Maximum size of a single Electrum request line in bytes (default: 1 MiB).")
|
||||
.default_value("1048576")
|
||||
).arg(
|
||||
Arg::with_name("electrum_max_subscriptions")
|
||||
.long("electrum-max-subscriptions")
|
||||
.help("Maximum number of scripthash subscriptions per client connection.")
|
||||
.default_value("100")
|
||||
).arg(
|
||||
Arg::with_name("electrum_max_clients")
|
||||
.long("electrum-max-clients")
|
||||
.help("Maximum number of concurrent Electrum client connections.")
|
||||
.default_value("10")
|
||||
).arg(
|
||||
Arg::with_name("electrum_idle_timeout")
|
||||
.long("electrum-idle-timeout")
|
||||
.help("Maximum idle time in seconds since the last client request before disconnecting the Electrum connection.")
|
||||
.default_value("600")
|
||||
).arg(
|
||||
Arg::with_name("electrum_haproxy_depth")
|
||||
.long("electrum-haproxy-depth")
|
||||
.help("Which HAProxy PROXY-protocol header layer identifies the real client IP. 0 disables PROXY-protocol detection; 1 uses the first (outermost) address, 2 the second, and so on. If the requested layer or any PROXY header is absent, no client IP is associated with the connection.")
|
||||
.default_value("0")
|
||||
).arg(
|
||||
Arg::with_name("electrum_connections_per_client")
|
||||
.long("electrum-connections-per-client")
|
||||
.help("Maximum number of concurrent Electrum connections allowed per client (keyed by the HAProxy-reported address when available, otherwise the peer IP). 0 disables the per-client limit.")
|
||||
.default_value("10")
|
||||
);
|
||||
|
||||
#[cfg(unix)]
|
||||
@ -532,8 +497,6 @@ impl Config {
|
||||
let electrum_public_hosts = m
|
||||
.value_of("electrum_public_hosts")
|
||||
.map(|s| serde_json::from_str(s).expect("invalid --electrum-public-hosts"));
|
||||
#[cfg(not(feature = "electrum-discovery"))]
|
||||
let electrum_public_hosts: Option<crate::electrum::ServerHosts> = None;
|
||||
|
||||
let mut log = stderrlog::new();
|
||||
log.verbosity(m.occurrences_of("verbosity") as usize);
|
||||
@ -584,16 +547,6 @@ impl Config {
|
||||
"rest_max_mempool_txid_page_size",
|
||||
usize
|
||||
),
|
||||
electrum_max_line_size: value_t_or_exit!(m, "electrum_max_line_size", usize),
|
||||
electrum_max_subscriptions: value_t_or_exit!(m, "electrum_max_subscriptions", usize),
|
||||
electrum_max_clients: value_t_or_exit!(m, "electrum_max_clients", usize),
|
||||
electrum_idle_timeout: value_t_or_exit!(m, "electrum_idle_timeout", u64),
|
||||
electrum_haproxy_depth: value_t_or_exit!(m, "electrum_haproxy_depth", usize),
|
||||
electrum_connections_per_client: value_t_or_exit!(
|
||||
m,
|
||||
"electrum_connections_per_client",
|
||||
usize
|
||||
),
|
||||
jsonrpc_import: m.is_present("jsonrpc_import"),
|
||||
light_mode: m.is_present("light_mode"),
|
||||
main_loop_delay: value_t_or_exit!(m, "main_loop_delay", u64),
|
||||
@ -623,6 +576,7 @@ impl Config {
|
||||
#[cfg(feature = "liquid")]
|
||||
asset_db_path,
|
||||
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
electrum_public_hosts,
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
electrum_announce: m.is_present("electrum_announce"),
|
||||
|
||||
@ -110,6 +110,8 @@ pub struct BlockchainInfo {
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct MempoolInfo {
|
||||
pub loaded: bool,
|
||||
#[serde(default)]
|
||||
pub mempoolminfee: f64, // in BTC/kB
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
@ -531,7 +533,7 @@ impl Daemon {
|
||||
from_value(info).chain_err(|| "invalid blockchain info")
|
||||
}
|
||||
|
||||
fn getmempoolinfo(&self) -> Result<MempoolInfo> {
|
||||
pub fn getmempoolinfo(&self) -> Result<MempoolInfo> {
|
||||
let info: Value = self.request("getmempoolinfo", json!([]))?;
|
||||
from_value(info).chain_err(|| "invalid mempool info")
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use std::fs;
|
||||
use std::io::{BufRead, BufReader, Cursor, Read, Write};
|
||||
use std::io::{BufRead, BufReader, Read, Write};
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
use std::net::IpAddr;
|
||||
use std::net::{Shutdown, SocketAddr, TcpListener, TcpStream};
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
@ -11,12 +12,10 @@ use std::sync::atomic::AtomicBool;
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use bitcoin::hashes::sha256d::Hash as Sha256dHash;
|
||||
use error_chain::ChainedError;
|
||||
use hex;
|
||||
use ppp::PartialResult;
|
||||
use serde_json::{from_str, Value};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
@ -27,7 +26,7 @@ use elements::encode::serialize;
|
||||
|
||||
use crate::chain::Txid;
|
||||
use crate::config::{Config, VERSION_STRING};
|
||||
use crate::electrum::{get_electrum_height, ProtocolVersion, ServerFeatures};
|
||||
use crate::electrum::{get_electrum_height, ProtocolVersion};
|
||||
use crate::errors::*;
|
||||
use crate::metrics::{Gauge, HistogramOpts, HistogramVec, MetricOpts, Metrics};
|
||||
use crate::new_index::{Query, Utxo};
|
||||
@ -41,7 +40,7 @@ const PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(1, 4);
|
||||
const MAX_HEADERS: usize = 2016;
|
||||
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
use crate::electrum::DiscoveryManager;
|
||||
use crate::electrum::{DiscoveryManager, ServerFeatures};
|
||||
|
||||
// TODO: Sha256dHash should be a generic hash-container (since script hash is single SHA256)
|
||||
fn hash_from_value(val: Option<&Value>) -> Result<Sha256dHash> {
|
||||
@ -77,36 +76,6 @@ fn bool_from_value_or(val: Option<&Value>, name: &str, default: bool) -> Result<
|
||||
bool_from_value(val, name)
|
||||
}
|
||||
|
||||
/// Extracts the source socket address from a parsed PROXY protocol v1 header.
|
||||
fn proxy_v1_source(addresses: &ppp::v1::Addresses) -> Option<SocketAddr> {
|
||||
match addresses {
|
||||
ppp::v1::Addresses::Tcp4(ip) => Some(SocketAddr::new(
|
||||
IpAddr::V4(ip.source_address),
|
||||
ip.source_port,
|
||||
)),
|
||||
ppp::v1::Addresses::Tcp6(ip) => Some(SocketAddr::new(
|
||||
IpAddr::V6(ip.source_address),
|
||||
ip.source_port,
|
||||
)),
|
||||
ppp::v1::Addresses::Unknown => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts the source socket address from a parsed PROXY protocol v2 header.
|
||||
fn proxy_v2_source(addresses: &ppp::v2::Addresses) -> Option<SocketAddr> {
|
||||
match addresses {
|
||||
ppp::v2::Addresses::IPv4(ip) => Some(SocketAddr::new(
|
||||
IpAddr::V4(ip.source_address),
|
||||
ip.source_port,
|
||||
)),
|
||||
ppp::v2::Addresses::IPv6(ip) => Some(SocketAddr::new(
|
||||
IpAddr::V6(ip.source_address),
|
||||
ip.source_port,
|
||||
)),
|
||||
ppp::v2::Addresses::Unspecified | ppp::v2::Addresses::Unix(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: implement caching and delta updates
|
||||
fn get_status_hash(txs: Vec<(Txid, Option<BlockId>)>, query: &Query) -> Option<FullHash> {
|
||||
if txs.is_empty() {
|
||||
@ -153,36 +122,18 @@ struct Connection {
|
||||
chan: SyncChannel<Message>,
|
||||
stats: Arc<Stats>,
|
||||
txs_limit: usize,
|
||||
max_line_size: usize,
|
||||
max_subscriptions: usize,
|
||||
idle_timeout: u64,
|
||||
last_request_at: Instant,
|
||||
die_please: Option<Receiver<()>>,
|
||||
server_features: Arc<ServerFeatures>,
|
||||
haproxy_depth: usize,
|
||||
proxy_client: Option<SocketAddr>,
|
||||
connections_per_client: usize,
|
||||
client_counts: Arc<Mutex<HashMap<IpAddr, usize>>>,
|
||||
registered_ip: Option<IpAddr>,
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
discovery: Option<Arc<DiscoveryManager>>,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
query: Arc<Query>,
|
||||
stream: ConnectionStream,
|
||||
stats: Arc<Stats>,
|
||||
txs_limit: usize,
|
||||
max_line_size: usize,
|
||||
max_subscriptions: usize,
|
||||
idle_timeout: u64,
|
||||
die_please: Receiver<()>,
|
||||
server_features: Arc<ServerFeatures>,
|
||||
haproxy_depth: usize,
|
||||
connections_per_client: usize,
|
||||
client_counts: Arc<Mutex<HashMap<IpAddr, usize>>>,
|
||||
#[cfg(feature = "electrum-discovery")] discovery: Option<Arc<DiscoveryManager>>,
|
||||
) -> Connection {
|
||||
Connection {
|
||||
@ -193,17 +144,7 @@ impl Connection {
|
||||
chan: SyncChannel::new(10),
|
||||
stats,
|
||||
txs_limit,
|
||||
max_line_size,
|
||||
max_subscriptions,
|
||||
idle_timeout,
|
||||
last_request_at: Instant::now(),
|
||||
die_please: Some(die_please),
|
||||
server_features,
|
||||
haproxy_depth,
|
||||
proxy_client: None,
|
||||
connections_per_client,
|
||||
client_counts,
|
||||
registered_ip: None,
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
discovery,
|
||||
}
|
||||
@ -225,8 +166,13 @@ impl Connection {
|
||||
Ok(json!(self.query.config().electrum_banner.clone()))
|
||||
}
|
||||
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
fn server_features(&self) -> Result<Value> {
|
||||
Ok(json!(self.server_features.as_ref()))
|
||||
let discovery = self
|
||||
.discovery
|
||||
.as_ref()
|
||||
.chain_err(|| "discovery is disabled")?;
|
||||
Ok(json!(discovery.our_features()))
|
||||
}
|
||||
|
||||
fn server_donation_address(&self) -> Result<Value> {
|
||||
@ -348,16 +294,6 @@ impl Connection {
|
||||
fn blockchain_scripthash_subscribe(&mut self, params: &[Value]) -> Result<Value> {
|
||||
let script_hash = hash_from_value(params.first()).chain_err(|| "bad script_hash")?;
|
||||
|
||||
// Enforce per-client subscription limit (don't count re-subscriptions to the same hash)
|
||||
if !self.status_hashes.contains_key(&script_hash)
|
||||
&& self.status_hashes.len() >= self.max_subscriptions
|
||||
{
|
||||
bail!(
|
||||
"subscription limit reached ({} max per client)",
|
||||
self.max_subscriptions
|
||||
);
|
||||
}
|
||||
|
||||
let history_txids = get_history(&self.query, &script_hash[..], self.txs_limit)?;
|
||||
let status_hash = get_status_hash(history_txids, &self.query)
|
||||
.map_or(Value::Null, |h| json!(hex::encode(full_hash(&h[..]))));
|
||||
@ -531,8 +467,9 @@ impl Connection {
|
||||
"server.peers.subscribe" => self.server_peers_subscribe(),
|
||||
"server.ping" => Ok(Value::Null),
|
||||
"server.version" => self.server_version(),
|
||||
"server.features" => self.server_features(),
|
||||
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
"server.features" => self.server_features(),
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
"server.add_peer" => self.server_add_peer(params),
|
||||
|
||||
@ -608,99 +545,14 @@ impl Connection {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn close_idle_connection(&mut self, idle_for: Duration) {
|
||||
info!(
|
||||
"[{}] closing idle connection after {} seconds without requests (timeout: {} seconds)",
|
||||
self.client_string(),
|
||||
idle_for.as_secs(),
|
||||
self.idle_timeout,
|
||||
);
|
||||
self.chan.close();
|
||||
}
|
||||
|
||||
/// A human-readable identifier for the connected client, preferring the
|
||||
/// HAProxy-reported address (when present) over the direct peer address.
|
||||
fn client_string(&self) -> String {
|
||||
match self.proxy_client {
|
||||
Some(addr) => format!("{} via {}", addr, self.stream.addr_string()),
|
||||
None => self.stream.addr_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the PROXY-protocol parse result into the client address at the
|
||||
/// configured `electrum-haproxy-depth` layer. A depth of 0, a missing PROXY
|
||||
/// header, or a non-existent layer all leave the client unidentified.
|
||||
fn set_proxy_client(&mut self, addresses: Option<Vec<SocketAddr>>) {
|
||||
self.proxy_client = match (self.haproxy_depth, addresses) {
|
||||
(0, _) | (_, None) => None,
|
||||
(depth, Some(addrs)) => addrs.get(depth - 1).copied(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Registers this connection against its client key (the HAProxy-reported IP
|
||||
/// when available, otherwise the direct peer IP) and enforces the
|
||||
/// `electrum-connections-per-client` limit. Returns an error if the limit has
|
||||
/// already been reached, in which case the connection must be closed.
|
||||
fn register_client(&mut self) -> Result<()> {
|
||||
if self.connections_per_client == 0 {
|
||||
// Per-client limit disabled.
|
||||
return Ok(());
|
||||
}
|
||||
let key = match self
|
||||
.proxy_client
|
||||
.map(|addr| addr.ip())
|
||||
.or_else(|| self.stream.direct_ip())
|
||||
{
|
||||
Some(key) => key,
|
||||
// No usable client key (e.g. a unix socket with no PROXY header).
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let mut counts = self.client_counts.lock().unwrap();
|
||||
let count = counts.entry(key).or_insert(0);
|
||||
if *count >= self.connections_per_client {
|
||||
bail!(
|
||||
"too many connections from client {} ({} max per client)",
|
||||
key,
|
||||
self.connections_per_client
|
||||
);
|
||||
}
|
||||
*count += 1;
|
||||
self.registered_ip = Some(key);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Releases this connection's slot in the per-client connection counter.
|
||||
fn unregister_client(&mut self) {
|
||||
if let Some(key) = self.registered_ip.take() {
|
||||
let mut counts = self.client_counts.lock().unwrap();
|
||||
if let Some(count) = counts.get_mut(&key) {
|
||||
*count -= 1;
|
||||
if *count == 0 {
|
||||
counts.remove(&key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_replies(&mut self, shutdown: crossbeam_channel::Receiver<()>) -> Result<()> {
|
||||
let idle_timeout = Duration::from_secs(self.idle_timeout);
|
||||
loop {
|
||||
let elapsed = self.last_request_at.elapsed();
|
||||
if elapsed > idle_timeout {
|
||||
self.close_idle_connection(elapsed);
|
||||
return Ok(());
|
||||
}
|
||||
let remaining = idle_timeout.saturating_sub(elapsed);
|
||||
let idle_deadline = crossbeam_channel::after(remaining);
|
||||
|
||||
crossbeam_channel::select! {
|
||||
recv(self.chan.receiver()) -> msg => {
|
||||
let msg = msg.chain_err(|| "channel closed")?;
|
||||
trace!("RPC {:?}", msg);
|
||||
match msg {
|
||||
Message::Request(line) => {
|
||||
self.last_request_at = Instant::now();
|
||||
let result = self.handle_line(&line);
|
||||
self.send_values(&[result])?
|
||||
}
|
||||
@ -714,25 +566,12 @@ impl Connection {
|
||||
self.chan.close();
|
||||
return Ok(());
|
||||
}
|
||||
Message::Proxy(addresses) => {
|
||||
self.set_proxy_client(addresses);
|
||||
if let Err(e) = self.register_client() {
|
||||
info!("[{}] {}", self.client_string(), e);
|
||||
self.chan.close();
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
recv(shutdown) -> _ => {
|
||||
self.chan.close();
|
||||
return Ok(());
|
||||
}
|
||||
recv(idle_deadline) -> _ => {
|
||||
let idle_for = self.last_request_at.elapsed();
|
||||
self.close_idle_connection(idle_for);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -781,134 +620,18 @@ impl Connection {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads and parses any PROXY-protocol (HAProxy) headers found at the very
|
||||
/// start of the connection. Returns the source address reported by each
|
||||
/// proxy layer (outermost first), or `None` if no PROXY header was present,
|
||||
/// together with any bytes that were read past the header(s) and belong to
|
||||
/// the Electrum request stream.
|
||||
fn read_proxy_headers(
|
||||
stream: &mut ConnectionStream,
|
||||
) -> Result<(Option<Vec<SocketAddr>>, Vec<u8>)> {
|
||||
// Upper bound on how much we are willing to buffer while looking for
|
||||
// PROXY headers, to avoid unbounded memory use from a slow/malicious peer.
|
||||
const MAX_PROXY_HEADER_SIZE: usize = 4096;
|
||||
|
||||
enum Step {
|
||||
Parsed(usize, Option<SocketAddr>),
|
||||
NeedMore,
|
||||
Done,
|
||||
}
|
||||
|
||||
let mut buf: Vec<u8> = Vec::with_capacity(256);
|
||||
let mut addrs: Vec<SocketAddr> = Vec::new();
|
||||
let mut saw_proxy = false;
|
||||
let mut chunk = [0u8; 256];
|
||||
|
||||
loop {
|
||||
// Parse as many complete, stacked PROXY headers as the buffer allows.
|
||||
let need_more = loop {
|
||||
if buf.is_empty() {
|
||||
break true;
|
||||
}
|
||||
let step = match ppp::HeaderResult::parse(&buf) {
|
||||
ppp::HeaderResult::V2(Ok(header)) => {
|
||||
Step::Parsed(header.len(), proxy_v2_source(&header.addresses))
|
||||
}
|
||||
ppp::HeaderResult::V1(Ok(header)) => {
|
||||
Step::Parsed(header.header.len(), proxy_v1_source(&header.addresses))
|
||||
}
|
||||
other => {
|
||||
if other.is_incomplete() {
|
||||
Step::NeedMore
|
||||
} else {
|
||||
Step::Done
|
||||
}
|
||||
}
|
||||
};
|
||||
match step {
|
||||
Step::Parsed(consumed, src) => {
|
||||
saw_proxy = true;
|
||||
if let Some(src) = src {
|
||||
addrs.push(src);
|
||||
}
|
||||
if consumed == 0 || consumed > buf.len() {
|
||||
// Defensive: never spin forever on a degenerate parse.
|
||||
break false;
|
||||
}
|
||||
buf.drain(..consumed);
|
||||
}
|
||||
Step::NeedMore => break true,
|
||||
Step::Done => break false,
|
||||
}
|
||||
};
|
||||
|
||||
if !need_more {
|
||||
break;
|
||||
}
|
||||
if buf.len() > MAX_PROXY_HEADER_SIZE {
|
||||
bail!(
|
||||
"PROXY protocol header too large (exceeds {} bytes)",
|
||||
MAX_PROXY_HEADER_SIZE
|
||||
);
|
||||
}
|
||||
let n = stream
|
||||
.read(&mut chunk)
|
||||
.chain_err(|| "failed to read PROXY protocol header")?;
|
||||
if n == 0 {
|
||||
// EOF before another complete header; stop with what we have.
|
||||
break;
|
||||
}
|
||||
buf.extend_from_slice(&chunk[..n]);
|
||||
}
|
||||
|
||||
let result = if saw_proxy { Some(addrs) } else { None };
|
||||
Ok((result, buf))
|
||||
}
|
||||
|
||||
fn handle_requests(
|
||||
stream: ConnectionStream,
|
||||
mut reader: BufReader<ConnectionStream>,
|
||||
tx: crossbeam_channel::Sender<Message>,
|
||||
max_line_size: usize,
|
||||
) -> Result<()> {
|
||||
let mut stream = stream;
|
||||
|
||||
// Consume any PROXY-protocol (HAProxy) headers at the very start of the
|
||||
// connection before treating the stream as Electrum requests. We always
|
||||
// consume them — even when HAProxy support is disabled
|
||||
// (`electrum-haproxy-depth = 0`) — so that PROXY headers sent by an
|
||||
// accidentally-misconfigured upstream are stripped instead of corrupting
|
||||
// the Electrum request parser.
|
||||
//
|
||||
// Crucially, `read_proxy_headers` only ever buffers bytes it has already
|
||||
// read from the socket: when no PROXY header is present it returns those
|
||||
// bytes as `leftover` so the start of the first Electrum request is
|
||||
// preserved rather than discarded.
|
||||
//
|
||||
// The parsed addresses are forwarded over the channel; whether they are
|
||||
// actually used to identify the client is decided later based on the
|
||||
// configured `electrum-haproxy-depth` (a depth of 0 ignores them).
|
||||
let (proxy_addrs, leftover) = Connection::read_proxy_headers(&mut stream)?;
|
||||
tx.send(Message::Proxy(proxy_addrs))
|
||||
.chain_err(|| "channel closed")?;
|
||||
|
||||
let mut reader = BufReader::new(Cursor::new(leftover).chain(stream));
|
||||
loop {
|
||||
let mut line = Vec::<u8>::new();
|
||||
// Read up to max_line_size + 1 bytes to detect oversized lines
|
||||
let mut limited = (&mut reader).take((max_line_size as u64).saturating_add(1));
|
||||
limited
|
||||
reader
|
||||
.read_until(b'\n', &mut line)
|
||||
.chain_err(|| "failed to read a request")?;
|
||||
if line.is_empty() {
|
||||
tx.send(Message::Done).chain_err(|| "channel closed")?;
|
||||
return Ok(());
|
||||
} else if line.len() > max_line_size {
|
||||
let _ = tx.send(Message::Done);
|
||||
bail!(
|
||||
"request line too large ({} bytes, max is {})",
|
||||
line.len(),
|
||||
max_line_size
|
||||
)
|
||||
} else {
|
||||
if line.starts_with(&[22, 3, 1]) {
|
||||
// (very) naive SSL handshake detection
|
||||
@ -930,7 +653,7 @@ impl Connection {
|
||||
|
||||
pub fn run(mut self) {
|
||||
self.stats.clients.inc();
|
||||
let stream = self.stream.try_clone().expect("failed to clone TcpStream");
|
||||
let reader = BufReader::new(self.stream.try_clone().expect("failed to clone TcpStream"));
|
||||
let tx = self.chan.sender();
|
||||
|
||||
let die_please = self.die_please.take().unwrap();
|
||||
@ -948,14 +671,11 @@ impl Connection {
|
||||
let _ = reply_killer.send(());
|
||||
});
|
||||
|
||||
let max_line_size = self.max_line_size;
|
||||
let child = spawn_thread("reader", move || {
|
||||
Connection::handle_requests(stream, tx, max_line_size)
|
||||
});
|
||||
let child = spawn_thread("reader", || Connection::handle_requests(reader, tx));
|
||||
if let Err(e) = self.handle_replies(reply_receiver) {
|
||||
error!(
|
||||
"[{}] connection handling failed: {}",
|
||||
self.client_string(),
|
||||
self.stream.addr_string(),
|
||||
e.display_chain().to_string()
|
||||
);
|
||||
}
|
||||
@ -963,9 +683,8 @@ impl Connection {
|
||||
self.stats
|
||||
.subscriptions
|
||||
.sub(self.status_hashes.len() as i64);
|
||||
self.unregister_client();
|
||||
|
||||
let addr = self.client_string();
|
||||
let addr = self.stream.addr_string();
|
||||
debug!("[{}] shutting down connection", addr);
|
||||
// Drop the Arc so that the stream properly closes.
|
||||
drop(arc_stream);
|
||||
@ -1023,11 +742,6 @@ pub enum Message {
|
||||
Request(String),
|
||||
PeriodicUpdate,
|
||||
Done,
|
||||
/// The result of parsing zero or more PROXY-protocol (HAProxy) headers at
|
||||
/// the start of the connection. `None` means no PROXY header was present;
|
||||
/// `Some(addrs)` holds the source address reported by each proxy layer,
|
||||
/// outermost first.
|
||||
Proxy(Option<Vec<SocketAddr>>),
|
||||
}
|
||||
|
||||
pub enum Notification {
|
||||
@ -1116,10 +830,11 @@ impl RPC {
|
||||
|
||||
let notification = Channel::unbounded();
|
||||
|
||||
let server_features = {
|
||||
// Discovery is enabled when electrum-public-hosts is set
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
let discovery = config.electrum_public_hosts.clone().map(|hosts| {
|
||||
use crate::chain::genesis_hash;
|
||||
let hosts = config.electrum_public_hosts.clone().unwrap_or_default();
|
||||
Arc::new(ServerFeatures {
|
||||
let features = ServerFeatures {
|
||||
hosts,
|
||||
server_version: VERSION_STRING.clone(),
|
||||
genesis_hash: genesis_hash(config.network_type),
|
||||
@ -1127,15 +842,10 @@ impl RPC {
|
||||
protocol_max: PROTOCOL_VERSION,
|
||||
hash_function: "sha256".into(),
|
||||
pruning: None,
|
||||
})
|
||||
};
|
||||
|
||||
// Discovery is enabled when electrum-public-hosts is set
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
let discovery = config.electrum_public_hosts.as_ref().map(|_hosts| {
|
||||
};
|
||||
let discovery = Arc::new(DiscoveryManager::new(
|
||||
config.network_type,
|
||||
server_features.as_ref().clone(),
|
||||
features,
|
||||
PROTOCOL_VERSION,
|
||||
config.electrum_announce,
|
||||
config.tor_proxy,
|
||||
@ -1145,22 +855,12 @@ impl RPC {
|
||||
});
|
||||
|
||||
let txs_limit = config.electrum_txs_limit;
|
||||
let max_line_size = config.electrum_max_line_size;
|
||||
let max_subscriptions = config.electrum_max_subscriptions;
|
||||
let max_clients = config.electrum_max_clients;
|
||||
let idle_timeout = config.electrum_idle_timeout;
|
||||
let haproxy_depth = config.electrum_haproxy_depth;
|
||||
let connections_per_client = config.electrum_connections_per_client;
|
||||
|
||||
RPC {
|
||||
notification: notification.sender(),
|
||||
server: Some(spawn_thread("rpc", move || {
|
||||
let senders =
|
||||
Arc::new(Mutex::new(Vec::<crossbeam_channel::Sender<Message>>::new()));
|
||||
// Tracks the number of live connections per client (keyed by the
|
||||
// HAProxy-reported address when available, otherwise the peer IP).
|
||||
let client_counts: Arc<Mutex<HashMap<IpAddr, usize>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
let acceptor_shutdown = Channel::unbounded();
|
||||
let acceptor_shutdown_sender = acceptor_shutdown.sender();
|
||||
@ -1172,39 +872,15 @@ impl RPC {
|
||||
acceptor_shutdown_sender,
|
||||
);
|
||||
|
||||
let mut threads: HashMap<thread::ThreadId, (thread::JoinHandle<()>, Sender<()>)> =
|
||||
HashMap::new();
|
||||
let mut threads = HashMap::new();
|
||||
let (garbage_sender, garbage_receiver) = crossbeam_channel::unbounded();
|
||||
|
||||
while let Some(stream) = acceptor.receiver().recv().unwrap() {
|
||||
// Clean up finished threads before checking connection limit
|
||||
while let Ok(id) = garbage_receiver.try_recv() {
|
||||
if let Some((thread, killer)) = threads.remove(&id) {
|
||||
let _ = killer.send(());
|
||||
if let Err(error) = thread.join() {
|
||||
error!("failed to join {:?}: {:?}", id, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce maximum connection limit
|
||||
if threads.len() >= max_clients {
|
||||
warn!(
|
||||
"[{}] rejecting connection: max clients reached ({}/{})",
|
||||
stream.addr_string(),
|
||||
threads.len(),
|
||||
max_clients
|
||||
);
|
||||
let _ = stream.shutdown(Shutdown::Both);
|
||||
continue;
|
||||
}
|
||||
|
||||
let addr = stream.addr_string();
|
||||
// explicitely scope the shadowed variables for the new thread
|
||||
let query = Arc::clone(&query);
|
||||
let senders = Arc::clone(&senders);
|
||||
let stats = Arc::clone(&stats);
|
||||
let client_counts = Arc::clone(&client_counts);
|
||||
let garbage_sender = garbage_sender.clone();
|
||||
|
||||
// Kill the peers properly
|
||||
@ -1213,7 +889,6 @@ impl RPC {
|
||||
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
let discovery = discovery.clone();
|
||||
let server_features = Arc::clone(&server_features);
|
||||
|
||||
let spawned = spawn_thread("peer", move || {
|
||||
let addr = stream.addr_string();
|
||||
@ -1223,14 +898,7 @@ impl RPC {
|
||||
stream,
|
||||
stats,
|
||||
txs_limit,
|
||||
max_line_size,
|
||||
max_subscriptions,
|
||||
idle_timeout,
|
||||
peace_receiver,
|
||||
server_features,
|
||||
haproxy_depth,
|
||||
connections_per_client,
|
||||
client_counts,
|
||||
#[cfg(feature = "electrum-discovery")]
|
||||
discovery,
|
||||
);
|
||||
@ -1243,6 +911,15 @@ impl RPC {
|
||||
|
||||
trace!("[{}] spawned {:?}", addr, spawned.thread().id());
|
||||
threads.insert(spawned.thread().id(), (spawned, killer));
|
||||
while let Ok(id) = garbage_receiver.try_recv() {
|
||||
if let Some((thread, killer)) = threads.remove(&id) {
|
||||
trace!("[{}] joining {:?}", addr, id);
|
||||
let _ = killer.send(());
|
||||
if let Err(error) = thread.join() {
|
||||
error!("failed to join {:?}: {:?}", id, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Drop these
|
||||
drop(acceptor);
|
||||
@ -1404,15 +1081,6 @@ impl ConnectionStream {
|
||||
}
|
||||
}
|
||||
|
||||
/// The direct peer IP address, if this is a TCP connection. Unix-socket
|
||||
/// connections have no IP and return `None`.
|
||||
fn direct_ip(&self) -> Option<IpAddr> {
|
||||
match self {
|
||||
ConnectionStream::Tcp(_, a) => Some(a.ip()),
|
||||
ConnectionStream::Unix(..) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn try_clone(&self) -> std::io::Result<Self> {
|
||||
Ok(match self {
|
||||
ConnectionStream::Tcp(s, a) => ConnectionStream::Tcp(s.try_clone()?, *a),
|
||||
|
||||
@ -9,7 +9,7 @@ mod registry;
|
||||
|
||||
use asset::get_issuance_entropy;
|
||||
pub use asset::{lookup_asset, LiquidAsset};
|
||||
pub use registry::{AssetMeta, AssetRegistry, AssetSorting};
|
||||
pub use registry::{AssetRegistry, AssetSorting};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct IssuanceValue {
|
||||
|
||||
@ -14,8 +14,6 @@ use crate::errors::*;
|
||||
// (in number of hex characters, not bytes)
|
||||
|
||||
const DIR_PARTITION_LEN: usize = 2;
|
||||
const SEARCH_SORT_CANDIDATE_LIMIT: usize = 2000;
|
||||
|
||||
pub struct AssetRegistry {
|
||||
directory: path::PathBuf,
|
||||
assets_cache: HashMap<AssetId, (SystemTime, AssetMeta)>,
|
||||
@ -55,39 +53,6 @@ impl AssetRegistry {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn search(&self, query: &str, limit: usize) -> Vec<AssetEntry<'_>> {
|
||||
let query = query.trim();
|
||||
if query.is_empty() || limit == 0 {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let (mut results, candidates) = search_by(
|
||||
self.assets_cache
|
||||
.iter()
|
||||
.map(|(asset_id, (_, metadata))| (asset_id, metadata)),
|
||||
query,
|
||||
limit,
|
||||
|metadata| metadata.ticker.as_deref(),
|
||||
);
|
||||
|
||||
if results.len() < limit {
|
||||
let (name_matches, candidates) =
|
||||
search_by(candidates, query, limit - results.len(), |metadata| {
|
||||
Some(&metadata.name)
|
||||
});
|
||||
results.extend(name_matches);
|
||||
|
||||
if results.len() < limit {
|
||||
let (domain_matches, _) =
|
||||
search_by(candidates, query, limit - results.len(), AssetMeta::domain);
|
||||
results.extend(domain_matches);
|
||||
}
|
||||
}
|
||||
|
||||
results.truncate(limit);
|
||||
results
|
||||
}
|
||||
|
||||
pub fn fs_sync(&mut self) -> Result<()> {
|
||||
for entry in fs::read_dir(&self.directory).chain_err(|| "failed reading asset dir")? {
|
||||
let entry = entry.chain_err(|| "invalid fh")?;
|
||||
@ -161,7 +126,7 @@ pub struct AssetMeta {
|
||||
}
|
||||
|
||||
impl AssetMeta {
|
||||
pub(crate) fn domain(&self) -> Option<&str> {
|
||||
fn domain(&self) -> Option<&str> {
|
||||
self.entity["domain"].as_str()
|
||||
}
|
||||
}
|
||||
@ -227,72 +192,3 @@ fn lc_cmp_opt(a: &Option<String>, b: &Option<String>) -> cmp::Ordering {
|
||||
.map(|a| a.to_lowercase())
|
||||
.cmp(&b.as_ref().map(|b| b.to_lowercase()))
|
||||
}
|
||||
|
||||
fn search_by<'a, I, F>(
|
||||
candidates: I,
|
||||
query: &str,
|
||||
limit: usize,
|
||||
field: F,
|
||||
) -> (Vec<AssetEntry<'a>>, Vec<AssetEntry<'a>>)
|
||||
where
|
||||
I: IntoIterator<Item = AssetEntry<'a>>,
|
||||
F: Fn(&AssetMeta) -> Option<&str>,
|
||||
{
|
||||
let mut matches = vec![];
|
||||
let mut remaining = vec![];
|
||||
|
||||
for (asset_id, metadata) in candidates {
|
||||
let position = field(metadata).and_then(|field| {
|
||||
// registry fields are ascii, so we don't need full unicode case-folding
|
||||
ascii_ci_find(field, query).map(|position| (position, field))
|
||||
});
|
||||
|
||||
if let Some((position, field)) = position {
|
||||
if matches.len() >= SEARCH_SORT_CANDIDATE_LIMIT {
|
||||
continue;
|
||||
}
|
||||
matches.push((position, field, asset_id, metadata));
|
||||
} else {
|
||||
remaining.push((asset_id, metadata));
|
||||
}
|
||||
}
|
||||
|
||||
matches.sort_unstable_by(|a, b| {
|
||||
a.0.cmp(&b.0)
|
||||
.then_with(|| ascii_ci_cmp(a.1, b.1))
|
||||
.then_with(|| a.2.cmp(b.2))
|
||||
});
|
||||
|
||||
(
|
||||
matches
|
||||
.into_iter()
|
||||
.take(limit)
|
||||
.map(|(_, _, asset_id, metadata)| (asset_id, metadata))
|
||||
.collect(),
|
||||
remaining,
|
||||
)
|
||||
}
|
||||
|
||||
// zero-allocation case-insensitive ASCII substring search
|
||||
// returns the byte offset of the first match
|
||||
fn ascii_ci_find(haystack: &str, needle: &str) -> Option<usize> {
|
||||
let (haystack, needle) = (haystack.as_bytes(), needle.as_bytes());
|
||||
if needle.is_empty() {
|
||||
return Some(0);
|
||||
}
|
||||
haystack
|
||||
.windows(needle.len())
|
||||
.position(|window| window.eq_ignore_ascii_case(needle))
|
||||
}
|
||||
|
||||
// zero-allocation case-insensitive ASCII string comparison
|
||||
fn ascii_ci_cmp(a: &str, b: &str) -> cmp::Ordering {
|
||||
let (a, b) = (a.as_bytes(), b.as_bytes());
|
||||
for i in 0..a.len().min(b.len()) {
|
||||
match a[i].to_ascii_lowercase().cmp(&b[i].to_ascii_lowercase()) {
|
||||
cmp::Ordering::Equal => continue,
|
||||
ord => return ord,
|
||||
}
|
||||
}
|
||||
a.len().cmp(&b.len())
|
||||
}
|
||||
|
||||
@ -290,7 +290,12 @@ fn parse_blocks(blob: Vec<u8>, magic: u32) -> Result<Vec<SizedBlock>> {
|
||||
cursor.set_position(end);
|
||||
}
|
||||
|
||||
Ok(super::THREAD_POOL.install(|| {
|
||||
let pool = rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(0) // CPU-bound
|
||||
.thread_name(|i| format!("parse-blocks-{}", i))
|
||||
.build()
|
||||
.unwrap();
|
||||
Ok(pool.install(|| {
|
||||
slices
|
||||
.into_par_iter()
|
||||
.map(|(slice, size)| (deserialize(slice).expect("failed to parse Block"), size))
|
||||
|
||||
@ -21,12 +21,19 @@ use crate::new_index::{
|
||||
compute_script_hash, schema::FullHash, ChainQuery, FundingInfo, ScriptStats, SpendingInfo,
|
||||
SpendingInput, TxHistoryInfo, Utxo,
|
||||
};
|
||||
use crate::util::fee_estimation::{FeeEstimator, RecommendedFees};
|
||||
use crate::util::fees::{make_fee_histogram, TxFeeInfo};
|
||||
use crate::util::gbt::{
|
||||
build_projected_blocks, GbtTransaction, MempoolBlock, DEFAULT_BLOCK_WEIGHT,
|
||||
};
|
||||
use crate::util::{extract_tx_prevouts, full_hash, has_prevout, is_spendable, Bytes};
|
||||
|
||||
#[cfg(feature = "liquid")]
|
||||
use crate::elements::asset;
|
||||
|
||||
/// Maximum number of projected blocks to build for fee estimation
|
||||
pub const MAX_PROJECTED_BLOCKS: usize = 8;
|
||||
|
||||
pub struct Mempool {
|
||||
chain: Arc<ChainQuery>,
|
||||
config: Arc<Config>,
|
||||
@ -36,6 +43,9 @@ pub struct Mempool {
|
||||
edges: HashMap<OutPoint, (Txid, u32)>, // OutPoint -> (spending_txid, spending_vin)
|
||||
recent: BoundedVecDeque<TxOverview>, // The N most recent txs to enter the mempool
|
||||
backlog_stats: (BacklogStats, Instant),
|
||||
projected_blocks: (Vec<MempoolBlock>, Instant), // Cached projected blocks
|
||||
recommended_fees: (RecommendedFees, Instant), // Cached recommended fees
|
||||
mempool_min_fee: f64, // Cached mempoolminfee in BTC/kB
|
||||
|
||||
// monitoring
|
||||
latency: HistogramVec, // mempool requests latency
|
||||
@ -61,6 +71,7 @@ pub struct TxOverview {
|
||||
|
||||
impl Mempool {
|
||||
pub fn new(chain: Arc<ChainQuery>, metrics: &Metrics, config: Arc<Config>) -> Self {
|
||||
let ttl = config.mempool_backlog_stats_ttl;
|
||||
Mempool {
|
||||
chain,
|
||||
txstore: BTreeMap::new(),
|
||||
@ -70,8 +81,14 @@ impl Mempool {
|
||||
recent: BoundedVecDeque::new(config.mempool_recent_txs_size),
|
||||
backlog_stats: (
|
||||
BacklogStats::default(),
|
||||
Instant::now() - Duration::from_secs(config.mempool_backlog_stats_ttl),
|
||||
Instant::now() - Duration::from_secs(ttl),
|
||||
),
|
||||
projected_blocks: (Vec::new(), Instant::now() - Duration::from_secs(ttl)),
|
||||
recommended_fees: (
|
||||
RecommendedFees::default(),
|
||||
Instant::now() - Duration::from_secs(ttl),
|
||||
),
|
||||
mempool_min_fee: 1.0, // Default: 1 sat/vB
|
||||
latency: metrics.histogram_vec(
|
||||
HistogramOpts::new("mempool_latency", "Mempool requests latency (in seconds)"),
|
||||
&["part"],
|
||||
@ -386,6 +403,16 @@ impl Mempool {
|
||||
&self.backlog_stats.0
|
||||
}
|
||||
|
||||
/// Get the projected mempool blocks
|
||||
pub fn projected_blocks(&self) -> &[MempoolBlock] {
|
||||
&self.projected_blocks.0
|
||||
}
|
||||
|
||||
/// Get the recommended fees based on projected blocks
|
||||
pub fn recommended_fees(&self) -> &RecommendedFees {
|
||||
&self.recommended_fees.0
|
||||
}
|
||||
|
||||
pub fn unique_txids(&self) -> HashSet<Txid> {
|
||||
HashSet::from_iter(self.txstore.keys().cloned())
|
||||
}
|
||||
@ -409,6 +436,13 @@ impl Mempool {
|
||||
let txids_to_remove: HashSet<&Txid> = old_txids.difference(&all_txids).collect();
|
||||
let txids_to_add: Vec<&Txid> = all_txids.difference(&old_txids).collect();
|
||||
|
||||
// Get mempoolminfee for fee estimation
|
||||
// [LOCK] No lock taken. Wait for RPC request.
|
||||
let mempool_min_fee = daemon
|
||||
.getmempoolinfo()
|
||||
.map(|info| info.mempoolminfee * 100_000.0) // Convert from BTC/kB to sat/vB
|
||||
.unwrap_or(1.0); // Default: 1 sat/vB
|
||||
|
||||
// 3. Remove missing transactions. Even if we are unable to download new transactions from
|
||||
// the daemon, we still want to remove the transactions that are no longer in the mempool.
|
||||
// [LOCK] Write lock is released at the end of the call to remove().
|
||||
@ -420,7 +454,7 @@ impl Mempool {
|
||||
.gettransactions(&txids_to_add)
|
||||
.chain_err(|| format!("failed to get {} transactions", txids_to_add.len()))?;
|
||||
|
||||
// 4. Update local mempool to match daemon's state
|
||||
// 5. Update local mempool to match daemon's state
|
||||
// [LOCK] Takes Write lock for whole scope.
|
||||
{
|
||||
let mut mempool = mempool.write().unwrap();
|
||||
@ -429,15 +463,17 @@ impl Mempool {
|
||||
debug!("Mempool update added less transactions than expected");
|
||||
}
|
||||
|
||||
// Update mempoolminfee
|
||||
mempool.mempool_min_fee = mempool_min_fee;
|
||||
|
||||
mempool
|
||||
.count
|
||||
.with_label_values(&["txs"])
|
||||
.set(mempool.txstore.len() as f64);
|
||||
|
||||
// Update cached backlog stats (if expired)
|
||||
if mempool.backlog_stats.1.elapsed()
|
||||
> Duration::from_secs(mempool.config.mempool_backlog_stats_ttl)
|
||||
{
|
||||
let ttl = Duration::from_secs(mempool.config.mempool_backlog_stats_ttl);
|
||||
if mempool.backlog_stats.1.elapsed() > ttl {
|
||||
let _timer = mempool
|
||||
.latency
|
||||
.with_label_values(&["update_backlog_stats"])
|
||||
@ -445,6 +481,15 @@ impl Mempool {
|
||||
mempool.backlog_stats = (BacklogStats::new(&mempool.feeinfo), Instant::now());
|
||||
}
|
||||
|
||||
// Update projected blocks and recommended fees (if expired)
|
||||
if mempool.projected_blocks.1.elapsed() > ttl {
|
||||
let _timer = mempool
|
||||
.latency
|
||||
.with_label_values(&["update_projected_blocks"])
|
||||
.start_timer();
|
||||
mempool.update_projected_blocks();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -692,6 +737,56 @@ impl Mempool {
|
||||
.retain(|_outpoint, (txid, _vin)| !to_remove.contains(txid));
|
||||
}
|
||||
|
||||
/// Build projected mempool blocks and calculate recommended fees.
|
||||
///
|
||||
/// This method builds block templates using the GBT algorithm and
|
||||
/// calculates recommended transaction fees based on the projected blocks.
|
||||
fn update_projected_blocks(&mut self) {
|
||||
// Build GBT transactions from mempool
|
||||
let gbt_txs: Vec<GbtTransaction> = self
|
||||
.txstore
|
||||
.iter()
|
||||
.filter_map(|(txid, tx)| {
|
||||
let fee_info = self.feeinfo.get(txid)?;
|
||||
|
||||
// Get parent txids (inputs that are unconfirmed)
|
||||
let parents: Vec<Txid> = tx
|
||||
.input
|
||||
.iter()
|
||||
.filter(|txin| has_prevout(txin))
|
||||
.filter_map(|txin| {
|
||||
let parent_txid = txin.previous_output.txid;
|
||||
if self.txstore.contains_key(&parent_txid) {
|
||||
Some(parent_txid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Some(GbtTransaction::new(*txid, fee_info, parents))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Build projected blocks (up to MAX_PROJECTED_BLOCKS blocks)
|
||||
let result = build_projected_blocks(&gbt_txs, DEFAULT_BLOCK_WEIGHT, MAX_PROJECTED_BLOCKS);
|
||||
|
||||
self.projected_blocks = (result.block_stats, Instant::now());
|
||||
|
||||
// Calculate recommended fees using cached mempoolminfee
|
||||
let estimator = FeeEstimator::for_network(self.config.network_type);
|
||||
let fees =
|
||||
estimator.calculate_recommended_fees(&self.projected_blocks.0, self.mempool_min_fee);
|
||||
|
||||
self.recommended_fees = (fees, Instant::now());
|
||||
|
||||
debug!(
|
||||
"Updated projected blocks: {} blocks, recommended fastest fee: {} sat/vB",
|
||||
self.projected_blocks.0.len(),
|
||||
self.recommended_fees.0.fastest_fee
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "liquid")]
|
||||
pub fn asset_history(&self, asset_id: &AssetId, limit: usize) -> Vec<Transaction> {
|
||||
let _timer = self
|
||||
|
||||
@ -5,19 +5,10 @@ pub mod precache;
|
||||
mod query;
|
||||
pub mod schema;
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
pub(crate) static THREAD_POOL: LazyLock<rayon::ThreadPool> = LazyLock::new(|| {
|
||||
rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(0) // 0 = use number of logical CPUs
|
||||
.thread_name(|i| format!("electrs-worker-{}", i))
|
||||
.build()
|
||||
.expect("failed to create global rayon thread pool")
|
||||
});
|
||||
|
||||
pub use self::db::{DBRow, DB};
|
||||
pub use self::fetch::{BlockEntry, FetchFrom};
|
||||
pub use self::mempool::Mempool;
|
||||
pub use self::mempool::MAX_PROJECTED_BLOCKS;
|
||||
pub use self::query::Query;
|
||||
pub use self::schema::{
|
||||
compute_script_hash, parse_hash, ChainQuery, FundingInfo, Indexer, ScriptStats, SpendingInfo,
|
||||
|
||||
@ -14,7 +14,7 @@ use crate::util::{is_spendable, BlockId, Bytes, TransactionStatus};
|
||||
#[cfg(feature = "liquid")]
|
||||
use crate::{
|
||||
chain::{asset::AssetRegistryLock, AssetId},
|
||||
elements::{lookup_asset, AssetMeta, AssetRegistry, AssetSorting, LiquidAsset},
|
||||
elements::{lookup_asset, AssetRegistry, AssetSorting, LiquidAsset},
|
||||
};
|
||||
|
||||
const FEE_ESTIMATES_TTL: u64 = 60; // seconds
|
||||
@ -210,26 +210,39 @@ impl Query {
|
||||
.copied()
|
||||
}
|
||||
|
||||
pub fn estimate_fee_map(&self) -> HashMap<u16, f64> {
|
||||
if let (ref cache, Some(cache_time)) = *self.cached_estimates.read().unwrap() {
|
||||
if cache_time.elapsed() < Duration::from_secs(FEE_ESTIMATES_TTL) {
|
||||
return cache.clone();
|
||||
}
|
||||
}
|
||||
|
||||
self.update_fee_estimates();
|
||||
self.cached_estimates.read().unwrap().0.clone()
|
||||
}
|
||||
|
||||
fn update_fee_estimates(&self) {
|
||||
match self.daemon.estimatesmartfee_batch(&CONF_TARGETS) {
|
||||
Ok(estimates) => {
|
||||
*self.cached_estimates.write().unwrap() = (estimates, Some(Instant::now()));
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("failed estimating feerates: {:?}", err);
|
||||
let mempool = self.mempool.read().unwrap();
|
||||
let projected_blocks = mempool.projected_blocks();
|
||||
|
||||
if projected_blocks.is_empty() {
|
||||
// Fallback to Bitcoin Core RPC if no projected blocks available
|
||||
drop(mempool);
|
||||
match self.daemon.estimatesmartfee_batch(&CONF_TARGETS) {
|
||||
Ok(estimates) => {
|
||||
*self.cached_estimates.write().unwrap() = (estimates, Some(Instant::now()));
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("failed estimating feerates: {:?}", err);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let mut estimates: HashMap<u16, f64> = HashMap::with_capacity(CONF_TARGETS.len());
|
||||
let last_block_fee = projected_blocks.last().map(|b| b.median_fee).unwrap_or(1.0);
|
||||
|
||||
for target in CONF_TARGETS {
|
||||
let fee = if (target as usize) <= projected_blocks.len() {
|
||||
// Use the median fee from the corresponding projected block (target-1 for 0-indexed)
|
||||
projected_blocks[(target as usize) - 1].median_fee
|
||||
} else {
|
||||
// For targets beyond available blocks, use the last block's fee
|
||||
last_block_fee
|
||||
};
|
||||
estimates.insert(target, fee);
|
||||
}
|
||||
|
||||
*self.cached_estimates.write().unwrap() = (estimates, Some(Instant::now()));
|
||||
}
|
||||
|
||||
pub fn get_relayfee(&self) -> Result<f64> {
|
||||
@ -271,15 +284,6 @@ impl Query {
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "liquid")]
|
||||
pub fn lookup_registry_asset(&self, asset_id: &AssetId) -> Result<Option<AssetMeta>> {
|
||||
let asset_db = self
|
||||
.asset_db
|
||||
.as_ref()
|
||||
.chain_err(|| "asset registry unavailable")?;
|
||||
Ok(asset_db.read().unwrap().get(asset_id).cloned())
|
||||
}
|
||||
|
||||
#[cfg(feature = "liquid")]
|
||||
pub fn list_registry_assets(
|
||||
&self,
|
||||
@ -307,27 +311,4 @@ impl Query {
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
Ok((total_num, results))
|
||||
}
|
||||
|
||||
#[cfg(feature = "liquid")]
|
||||
pub fn search_registry_assets<T, F>(
|
||||
&self,
|
||||
search: &str,
|
||||
limit: usize,
|
||||
mut map: F,
|
||||
) -> Result<Vec<T>>
|
||||
where
|
||||
F: FnMut(&AssetId, &AssetMeta) -> T,
|
||||
{
|
||||
let asset_db = self
|
||||
.asset_db
|
||||
.as_ref()
|
||||
.chain_err(|| "asset registry unavailable")?;
|
||||
Ok(asset_db
|
||||
.read()
|
||||
.unwrap()
|
||||
.search(search, limit)
|
||||
.into_iter()
|
||||
.map(|(asset_id, metadata)| map(asset_id, metadata))
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
@ -1447,7 +1447,24 @@ fn lookup_txos(
|
||||
outpoints: &BTreeSet<OutPoint>,
|
||||
allow_missing: bool,
|
||||
) -> HashMap<OutPoint, TxOut> {
|
||||
super::THREAD_POOL.install(|| {
|
||||
let mut loop_count = 10;
|
||||
let pool = loop {
|
||||
match rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(16) // we need to saturate SSD IOPS
|
||||
.thread_name(|i| format!("lookup-txo-{}", i))
|
||||
.build()
|
||||
{
|
||||
Ok(pool) => break pool,
|
||||
Err(e) => {
|
||||
if loop_count == 0 {
|
||||
panic!("schema::lookup_txos failed to create a ThreadPool: {}", e);
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
loop_count -= 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
pool.install(|| {
|
||||
// Should match lookup_txos_sequential
|
||||
outpoints
|
||||
.par_iter()
|
||||
|
||||
153
src/rest.rs
153
src/rest.rs
@ -35,7 +35,7 @@ use hyperlocal::UnixServerExt;
|
||||
use std::{cmp, fs};
|
||||
#[cfg(feature = "liquid")]
|
||||
use {
|
||||
crate::elements::{peg::PegoutValue, AssetMeta, AssetSorting, IssuanceValue},
|
||||
crate::elements::{peg::PegoutValue, AssetSorting, IssuanceValue},
|
||||
elements::{
|
||||
confidential::{Asset, Nonce, Value},
|
||||
encode, AssetId,
|
||||
@ -59,12 +59,6 @@ const MULTI_ADDRESS_LIMIT: usize = 300;
|
||||
const ASSETS_PER_PAGE: usize = 25;
|
||||
#[cfg(feature = "liquid")]
|
||||
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_SHORT: u32 = 10; // ttl for volatie resources
|
||||
@ -138,32 +132,6 @@ 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
|
||||
/// using Bitcoin Core code ported to Rust.
|
||||
///
|
||||
@ -615,19 +583,6 @@ 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
|
||||
///
|
||||
/// Any transactions with missing prevouts will be filtered out of the response, rather than returned with incorrect data.
|
||||
@ -1005,8 +960,13 @@ fn handle_request(
|
||||
};
|
||||
|
||||
if txs.len() < max_txs {
|
||||
let after_txid_ref =
|
||||
confirmed_after_txid(&after_txid_location, after_txid.as_ref());
|
||||
let after_txid_ref = if !txs.is_empty() {
|
||||
// If there are any txs, we know mempool found the
|
||||
// after_txid IF it exists... so always return None.
|
||||
None
|
||||
} else {
|
||||
after_txid.as_ref()
|
||||
};
|
||||
let mut confirmed_txs = query
|
||||
.chain()
|
||||
.history(
|
||||
@ -1107,8 +1067,13 @@ fn handle_request(
|
||||
};
|
||||
|
||||
if txs.len() < max_txs {
|
||||
let after_txid_ref =
|
||||
confirmed_after_txid(&after_txid_location, after_txid.as_ref());
|
||||
let after_txid_ref = if !txs.is_empty() {
|
||||
// If there are any txs, we know mempool found the
|
||||
// after_txid IF it exists... so always return None.
|
||||
None
|
||||
} else {
|
||||
after_txid.as_ref()
|
||||
};
|
||||
let mut confirmed_txs = query
|
||||
.chain()
|
||||
.history_group(
|
||||
@ -1806,40 +1771,16 @@ fn handle_request(
|
||||
json_response(recent, TTL_MEMPOOL_RECENT)
|
||||
}
|
||||
|
||||
(&Method::GET, Some(&"fee-estimates"), None, None, None, None) => {
|
||||
json_response(query.estimate_fee_map(), TTL_SHORT)
|
||||
// Recommended fees endpoint (mempool-style fee estimation)
|
||||
(&Method::GET, Some(&"v1"), Some(&"fees"), Some(&"recommended"), None, None) => {
|
||||
let mempool = query.mempool();
|
||||
json_response(mempool.recommended_fees(), 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())
|
||||
// Mempool blocks endpoint (projected blocks)
|
||||
(&Method::GET, Some(&"v1"), Some(&"fees"), Some(&"mempool-blocks"), None, None) => {
|
||||
let mempool = query.mempool();
|
||||
json_response(mempool.projected_blocks(), TTL_SHORT)
|
||||
}
|
||||
|
||||
#[cfg(feature = "liquid")]
|
||||
@ -1868,21 +1809,6 @@ fn handle_request(
|
||||
.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")]
|
||||
(&Method::GET, Some(&"asset"), Some(asset_str), None, None, None) => {
|
||||
let asset_id = AssetId::from_str(asset_str)?;
|
||||
@ -2234,8 +2160,6 @@ impl From<address::AddressError> for HttpError {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{confirmed_after_txid, TxidLocation};
|
||||
use crate::chain::Txid;
|
||||
use crate::rest::HttpError;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
@ -2302,37 +2226,6 @@ mod tests {
|
||||
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]
|
||||
fn test_difficulty_new() {
|
||||
use super::difficulty_new;
|
||||
|
||||
376
src/util/fee_estimation.rs
Normal file
376
src/util/fee_estimation.rs
Normal file
@ -0,0 +1,376 @@
|
||||
//! Fee estimation based on projected mempool blocks.
|
||||
//!
|
||||
//! This module calculates recommended transaction fees based on the fee statistics
|
||||
//! of projected mempool blocks (created by the GBT algorithm).
|
||||
//!
|
||||
//! Ported from mempool's fee-api.ts.
|
||||
|
||||
use crate::chain::Network;
|
||||
use crate::util::gbt::MempoolBlock;
|
||||
|
||||
/// Recommended fee rates for different confirmation time targets
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RecommendedFees {
|
||||
/// Fee rate for confirmation in the next block (sat/vB)
|
||||
pub fastest_fee: f64,
|
||||
/// Fee rate for confirmation within ~30 minutes / 3 blocks (sat/vB)
|
||||
pub half_hour_fee: f64,
|
||||
/// Fee rate for confirmation within ~1 hour / 6 blocks (sat/vB)
|
||||
pub hour_fee: f64,
|
||||
/// Economy fee rate (sat/vB)
|
||||
pub economy_fee: f64,
|
||||
/// Minimum relay fee rate (sat/vB)
|
||||
pub minimum_fee: f64,
|
||||
}
|
||||
|
||||
impl Default for RecommendedFees {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
fastest_fee: 1.0,
|
||||
half_hour_fee: 1.0,
|
||||
hour_fee: 1.0,
|
||||
economy_fee: 1.0,
|
||||
minimum_fee: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fee estimation configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FeeEstimationConfig {
|
||||
/// Minimum fee increment for rounding (sat/vB)
|
||||
pub minimum_increment: f64,
|
||||
/// Minimum fastest fee (sat/vB)
|
||||
pub min_fastest_fee: f64,
|
||||
/// Minimum half hour fee (sat/vB)
|
||||
pub min_half_hour_fee: f64,
|
||||
/// Priority factor added to highest priority recommendations (sat/vB)
|
||||
pub priority_factor: f64,
|
||||
}
|
||||
|
||||
impl FeeEstimationConfig {
|
||||
/// Configuration for Bitcoin mainnet/testnet
|
||||
pub fn bitcoin() -> Self {
|
||||
Self {
|
||||
minimum_increment: 1.0,
|
||||
min_fastest_fee: 1.0,
|
||||
min_half_hour_fee: 0.5,
|
||||
priority_factor: 0.5,
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for Liquid network
|
||||
pub fn liquid() -> Self {
|
||||
Self {
|
||||
minimum_increment: 0.1,
|
||||
min_fastest_fee: 0.1,
|
||||
min_half_hour_fee: 0.1,
|
||||
priority_factor: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create configuration based on network type
|
||||
pub fn for_network(network: Network) -> Self {
|
||||
if network.is_liquid() {
|
||||
Self::liquid()
|
||||
} else {
|
||||
Self::bitcoin()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fee estimator that calculates recommended fees from projected blocks
|
||||
pub struct FeeEstimator {
|
||||
config: FeeEstimationConfig,
|
||||
}
|
||||
|
||||
impl FeeEstimator {
|
||||
pub fn new(config: FeeEstimationConfig) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
|
||||
/// Create a fee estimator for the given network
|
||||
pub fn for_network(network: Network) -> Self {
|
||||
Self::new(FeeEstimationConfig::for_network(network))
|
||||
}
|
||||
|
||||
/// Calculate recommended fees from projected mempool blocks.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `projected_blocks` - Projected mempool blocks from GBT algorithm
|
||||
/// * `mempool_min_fee` - Minimum fee to get into mempool (BTC/kvB from getmempoolinfo)
|
||||
pub fn calculate_recommended_fees(
|
||||
&self,
|
||||
projected_blocks: &[MempoolBlock],
|
||||
mempool_min_fee: f64,
|
||||
) -> RecommendedFees {
|
||||
self.calculate_recommended_fees_with_increment(
|
||||
projected_blocks,
|
||||
mempool_min_fee,
|
||||
self.config.minimum_increment,
|
||||
)
|
||||
}
|
||||
|
||||
/// Calculate precise recommended fees with sub-satoshi precision.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `projected_blocks` - Projected mempool blocks from GBT algorithm
|
||||
/// * `mempool_min_fee` - Minimum fee to get into mempool (BTC/kvB from getmempoolinfo)
|
||||
pub fn calculate_precise_recommended_fees(
|
||||
&self,
|
||||
projected_blocks: &[MempoolBlock],
|
||||
mempool_min_fee: f64,
|
||||
) -> RecommendedFees {
|
||||
// Use 0.001 sat/vB precision (minimum non-zero minrelaytxfee/incrementalrelayfee)
|
||||
let mut recommendations = self.calculate_recommended_fees_with_increment(
|
||||
projected_blocks,
|
||||
mempool_min_fee,
|
||||
0.001,
|
||||
);
|
||||
|
||||
// Enforce floor & offset for highest priority recommendations
|
||||
recommendations.fastest_fee = (recommendations.fastest_fee + self.config.priority_factor)
|
||||
.max(self.config.min_fastest_fee);
|
||||
recommendations.half_hour_fee = (recommendations.half_hour_fee
|
||||
+ self.config.priority_factor / 2.0)
|
||||
.max(self.config.min_half_hour_fee);
|
||||
|
||||
// Round to 3 decimal places
|
||||
RecommendedFees {
|
||||
fastest_fee: (recommendations.fastest_fee * 1000.0).round() / 1000.0,
|
||||
half_hour_fee: (recommendations.half_hour_fee * 1000.0).round() / 1000.0,
|
||||
hour_fee: (recommendations.hour_fee * 1000.0).round() / 1000.0,
|
||||
economy_fee: (recommendations.economy_fee * 1000.0).round() / 1000.0,
|
||||
minimum_fee: (recommendations.minimum_fee * 1000.0).round() / 1000.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal fee calculation with configurable increment.
|
||||
fn calculate_recommended_fees_with_increment(
|
||||
&self,
|
||||
projected_blocks: &[MempoolBlock],
|
||||
mempool_min_fee: f64,
|
||||
min_increment: f64,
|
||||
) -> RecommendedFees {
|
||||
let purge_rate = round_up_to_nearest(mempool_min_fee, min_increment);
|
||||
let minimum_fee = purge_rate.max(min_increment);
|
||||
|
||||
if projected_blocks.is_empty() {
|
||||
return RecommendedFees {
|
||||
fastest_fee: minimum_fee,
|
||||
half_hour_fee: minimum_fee,
|
||||
hour_fee: minimum_fee,
|
||||
economy_fee: minimum_fee,
|
||||
minimum_fee,
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate median fees for first 3 blocks
|
||||
let first_median_fee = self.optimize_median_fee(
|
||||
&projected_blocks[0],
|
||||
projected_blocks.get(1),
|
||||
None,
|
||||
minimum_fee,
|
||||
min_increment,
|
||||
);
|
||||
|
||||
let second_median_fee = projected_blocks.get(1).map_or(minimum_fee, |block| {
|
||||
self.optimize_median_fee(
|
||||
block,
|
||||
projected_blocks.get(2),
|
||||
Some(first_median_fee),
|
||||
minimum_fee,
|
||||
min_increment,
|
||||
)
|
||||
});
|
||||
|
||||
let third_median_fee = projected_blocks.get(2).map_or(minimum_fee, |block| {
|
||||
self.optimize_median_fee(
|
||||
block,
|
||||
projected_blocks.get(3),
|
||||
Some(second_median_fee),
|
||||
minimum_fee,
|
||||
min_increment,
|
||||
)
|
||||
});
|
||||
|
||||
// Enforce minimum fee on all recommendations
|
||||
let mut fastest_fee = first_median_fee.max(minimum_fee);
|
||||
let mut half_hour_fee = second_median_fee.max(minimum_fee);
|
||||
let mut hour_fee = third_median_fee.max(minimum_fee);
|
||||
let economy_fee = (2.0 * minimum_fee).min(third_median_fee).max(minimum_fee);
|
||||
|
||||
// Ensure recommendations always increase with priority
|
||||
fastest_fee = fastest_fee
|
||||
.max(half_hour_fee)
|
||||
.max(hour_fee)
|
||||
.max(economy_fee);
|
||||
half_hour_fee = half_hour_fee.max(hour_fee).max(economy_fee);
|
||||
hour_fee = hour_fee.max(economy_fee);
|
||||
|
||||
RecommendedFees {
|
||||
fastest_fee: round_to_nearest(fastest_fee, min_increment),
|
||||
half_hour_fee: round_to_nearest(half_hour_fee, min_increment),
|
||||
hour_fee: round_to_nearest(hour_fee, min_increment),
|
||||
economy_fee: round_to_nearest(economy_fee, min_increment),
|
||||
minimum_fee: round_to_nearest(minimum_fee, min_increment),
|
||||
}
|
||||
}
|
||||
|
||||
/// Optimize median fee based on block fullness.
|
||||
///
|
||||
/// For partially full blocks, the fee is scaled down proportionally.
|
||||
fn optimize_median_fee(
|
||||
&self,
|
||||
block: &MempoolBlock,
|
||||
next_block: Option<&MempoolBlock>,
|
||||
previous_fee: Option<f64>,
|
||||
min_fee: f64,
|
||||
min_increment: f64,
|
||||
) -> f64 {
|
||||
let use_fee = match previous_fee {
|
||||
Some(prev) => (block.median_fee + prev) / 2.0,
|
||||
None => block.median_fee,
|
||||
};
|
||||
|
||||
// If block is less than half full or median fee is below minimum, use minimum
|
||||
if block.block_vsize <= 500_000.0 || block.median_fee < min_fee {
|
||||
return min_fee;
|
||||
}
|
||||
|
||||
// If block is between 50-95% full and there's no next block,
|
||||
// scale the fee proportionally
|
||||
if block.block_vsize <= 950_000.0 && next_block.is_none() {
|
||||
let multiplier = (block.block_vsize - 500_000.0) / 500_000.0;
|
||||
return round_to_nearest(use_fee * multiplier, min_increment).max(min_fee);
|
||||
}
|
||||
|
||||
round_up_to_nearest(use_fee, min_increment).max(min_fee)
|
||||
}
|
||||
}
|
||||
|
||||
/// Round up to the nearest increment
|
||||
fn round_up_to_nearest(value: f64, nearest: f64) -> f64 {
|
||||
if nearest != 0.0 {
|
||||
(value / nearest).ceil() * nearest
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
/// Round to the nearest increment
|
||||
fn round_to_nearest(value: f64, nearest: f64) -> f64 {
|
||||
if nearest != 0.0 {
|
||||
(value / nearest).round() * nearest
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_test_block(vsize: f64, median_fee: f64) -> MempoolBlock {
|
||||
MempoolBlock {
|
||||
block_size: vsize as u64,
|
||||
block_vsize: vsize,
|
||||
n_tx: 1000,
|
||||
total_fees: 1000000,
|
||||
median_fee,
|
||||
fee_range: vec![1.0, 2.0, 3.0, median_fee, 5.0, 6.0, 7.0],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_mempool() {
|
||||
let estimator = FeeEstimator::new(FeeEstimationConfig::bitcoin());
|
||||
let fees = estimator.calculate_recommended_fees(&[], 0.00001);
|
||||
|
||||
assert_eq!(fees.fastest_fee, 1.0);
|
||||
assert_eq!(fees.half_hour_fee, 1.0);
|
||||
assert_eq!(fees.hour_fee, 1.0);
|
||||
assert_eq!(fees.economy_fee, 1.0);
|
||||
assert_eq!(fees.minimum_fee, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sub_sat_mempool() {
|
||||
let estimator = FeeEstimator::new(FeeEstimationConfig::bitcoin());
|
||||
|
||||
// Use median fee slightly above 1.0 (like the real mempool data: 1.002...)
|
||||
// This tests the rounding behavior
|
||||
let blocks = vec![
|
||||
create_test_block(997953.25, 1.002), // Rounds up to 2
|
||||
create_test_block(997963.0, 0.6),
|
||||
create_test_block(997821.25, 0.52),
|
||||
];
|
||||
|
||||
let fees = estimator.calculate_recommended_fees(&blocks, 0.000001);
|
||||
|
||||
assert_eq!(fees.fastest_fee, 2.0);
|
||||
assert_eq!(fees.half_hour_fee, 1.0);
|
||||
assert_eq!(fees.hour_fee, 1.0);
|
||||
assert_eq!(fees.economy_fee, 1.0);
|
||||
assert_eq!(fees.minimum_fee, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_low_fee_mempool() {
|
||||
let estimator = FeeEstimator::new(FeeEstimationConfig::bitcoin());
|
||||
|
||||
let blocks = vec![
|
||||
create_test_block(997953.25, 2.0),
|
||||
create_test_block(997963.0, 1.5),
|
||||
create_test_block(997821.25, 1.0),
|
||||
];
|
||||
|
||||
let fees = estimator.calculate_recommended_fees(&blocks, 0.00001);
|
||||
|
||||
assert_eq!(fees.fastest_fee, 2.0);
|
||||
assert_eq!(fees.half_hour_fee, 2.0);
|
||||
assert_eq!(fees.hour_fee, 2.0);
|
||||
assert_eq!(fees.economy_fee, 2.0);
|
||||
assert_eq!(fees.minimum_fee, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_partially_full_block() {
|
||||
let estimator = FeeEstimator::new(FeeEstimationConfig::bitcoin());
|
||||
|
||||
// Block that's 75% full (750000 vsize)
|
||||
let blocks = vec![create_test_block(750_000.0, 10.0)];
|
||||
|
||||
let fees = estimator.calculate_recommended_fees(&blocks, 0.00001);
|
||||
|
||||
// Fee should be scaled down because block isn't full and there's no next block
|
||||
// multiplier = (750000 - 500000) / 500000 = 0.5
|
||||
// So fee should be 10 * 0.5 = 5, rounded to 5
|
||||
assert_eq!(fees.fastest_fee, 5.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_liquid_config() {
|
||||
let estimator = FeeEstimator::new(FeeEstimationConfig::liquid());
|
||||
let fees = estimator.calculate_recommended_fees(&[], 0.000001);
|
||||
|
||||
// Liquid uses 0.1 as minimum
|
||||
assert_eq!(fees.minimum_fee, 0.1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_up_to_nearest() {
|
||||
assert_eq!(round_up_to_nearest(1.1, 1.0), 2.0);
|
||||
assert_eq!(round_up_to_nearest(1.0, 1.0), 1.0);
|
||||
assert_eq!(round_up_to_nearest(0.15, 0.1), 0.2);
|
||||
assert_eq!(round_up_to_nearest(5.0, 0.0), 5.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_to_nearest() {
|
||||
assert_eq!(round_to_nearest(1.4, 1.0), 1.0);
|
||||
assert_eq!(round_to_nearest(1.6, 1.0), 2.0);
|
||||
assert_eq!(round_to_nearest(0.14, 0.1), 0.1);
|
||||
assert_eq!(round_to_nearest(0.16, 0.1), 0.2);
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,14 @@
|
||||
use crate::chain::{Network, Transaction, TxOut};
|
||||
use crate::util::transaction::sigops::transaction_sigop_count;
|
||||
use std::collections::HashMap;
|
||||
|
||||
const VSIZE_BIN_WIDTH: u32 = 50_000; // in vbytes
|
||||
|
||||
pub struct TxFeeInfo {
|
||||
pub fee: u64, // in satoshis
|
||||
pub vsize: u32, // in virtual bytes (= weight/4)
|
||||
pub fee: u64, // in satoshis
|
||||
pub vsize: u32, // in virtual bytes (= weight/4)
|
||||
pub weight: u32, // transaction weight
|
||||
pub sigops: u32, // signature operations count
|
||||
pub fee_per_vbyte: f32,
|
||||
}
|
||||
|
||||
@ -13,13 +16,19 @@ impl TxFeeInfo {
|
||||
pub fn new(tx: &Transaction, prevouts: &HashMap<u32, &TxOut>, network: Network) -> Self {
|
||||
let fee = get_tx_fee(tx, prevouts, network);
|
||||
#[cfg(not(feature = "liquid"))]
|
||||
let vsize = tx.weight().to_wu() / 4;
|
||||
let weight = tx.weight().to_wu() as u32;
|
||||
#[cfg(feature = "liquid")]
|
||||
let vsize = tx.weight() / 4;
|
||||
let weight = tx.weight() as u32;
|
||||
let vsize = weight / 4;
|
||||
|
||||
// Calculate sigops, defaulting to 0 on error (e.g., coinbase)
|
||||
let sigops = transaction_sigop_count(tx, prevouts).unwrap_or(0) as u32;
|
||||
|
||||
TxFeeInfo {
|
||||
fee,
|
||||
vsize: vsize as u32,
|
||||
vsize,
|
||||
weight,
|
||||
sigops,
|
||||
fee_per_vbyte: fee as f32 / vsize as f32,
|
||||
}
|
||||
}
|
||||
|
||||
702
src/util/gbt.rs
Normal file
702
src/util/gbt.rs
Normal file
@ -0,0 +1,702 @@
|
||||
//! Block template construction (GetBlockTemplate) algorithm.
|
||||
//!
|
||||
//! This module implements an approximation of the transaction selection algorithm
|
||||
//! from Bitcoin Core's BlockAssembler to create projected mempool blocks.
|
||||
//!
|
||||
//! Ported from mempool's Rust GBT implementation.
|
||||
|
||||
use std::collections::{BinaryHeap, HashMap, HashSet};
|
||||
|
||||
use crate::chain::Txid;
|
||||
use crate::util::fees::TxFeeInfo;
|
||||
|
||||
/// Default block weight limit (4MB weight = 1MB vsize for worst case)
|
||||
pub const DEFAULT_BLOCK_WEIGHT: u32 = 4_000_000;
|
||||
/// Maximum sigops per block
|
||||
const BLOCK_SIGOPS: u32 = 80_000;
|
||||
/// Reserved weight for coinbase
|
||||
const BLOCK_RESERVED_WEIGHT: u32 = 4_000;
|
||||
/// Reserved sigops for coinbase
|
||||
const BLOCK_RESERVED_SIGOPS: u32 = 400;
|
||||
|
||||
/// A projected mempool block with fee statistics
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MempoolBlock {
|
||||
/// Total size of transactions in bytes
|
||||
pub block_size: u64,
|
||||
/// Total virtual size of transactions (weight/4)
|
||||
pub block_vsize: f64,
|
||||
/// Number of transactions
|
||||
pub n_tx: usize,
|
||||
/// Total fees in satoshis
|
||||
pub total_fees: u64,
|
||||
/// Median fee rate in sat/vB
|
||||
pub median_fee: f64,
|
||||
/// Fee rate range [min, 10th, 25th, 50th, 75th, 90th, max] in sat/vB
|
||||
pub fee_range: Vec<f64>,
|
||||
}
|
||||
|
||||
impl Default for MempoolBlock {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
block_size: 0,
|
||||
block_vsize: 0.0,
|
||||
n_tx: 0,
|
||||
total_fees: 0,
|
||||
median_fee: 0.0,
|
||||
fee_range: vec![0.0; 7],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Transaction data for block template construction
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GbtTransaction {
|
||||
pub txid: Txid,
|
||||
pub fee: u64,
|
||||
pub weight: u32,
|
||||
pub sigops: u32,
|
||||
/// Indices of parent transactions in the mempool (by txid)
|
||||
pub parents: Vec<Txid>,
|
||||
}
|
||||
|
||||
impl GbtTransaction {
|
||||
pub fn new(txid: Txid, fee_info: &TxFeeInfo, parents: Vec<Txid>) -> Self {
|
||||
Self {
|
||||
txid,
|
||||
fee: fee_info.fee,
|
||||
weight: fee_info.weight,
|
||||
sigops: fee_info.sigops,
|
||||
parents,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn vsize(&self) -> u32 {
|
||||
self.weight.div_ceil(4)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn fee_rate(&self) -> f64 {
|
||||
self.fee as f64 / self.vsize() as f64
|
||||
}
|
||||
|
||||
/// Calculate sigop-adjusted vsize (rounded up)
|
||||
#[inline]
|
||||
pub fn sigop_adjusted_vsize(&self) -> u32 {
|
||||
self.vsize().max(self.sigops * 5)
|
||||
}
|
||||
|
||||
/// Calculate sigop-adjusted weight
|
||||
#[inline]
|
||||
pub fn sigop_adjusted_weight(&self) -> u32 {
|
||||
self.weight.max(self.sigops * 20)
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal audit transaction for GBT algorithm
|
||||
#[derive(Debug, Clone)]
|
||||
struct AuditTransaction {
|
||||
fee: u64,
|
||||
weight: u32,
|
||||
sigop_adjusted_weight: u32,
|
||||
sigop_adjusted_vsize: u32,
|
||||
sigops: u32,
|
||||
effective_fee_per_vsize: f64,
|
||||
parents: Vec<Txid>,
|
||||
ancestors: HashSet<Txid>,
|
||||
children: HashSet<Txid>,
|
||||
ancestor_fee: u64,
|
||||
ancestor_sigop_adjusted_weight: u32,
|
||||
ancestor_sigop_adjusted_vsize: u32,
|
||||
ancestor_sigops: u32,
|
||||
score: f64,
|
||||
used: bool,
|
||||
modified: bool,
|
||||
}
|
||||
|
||||
impl AuditTransaction {
|
||||
fn from_gbt_tx(tx: &GbtTransaction) -> Self {
|
||||
let sigop_adjusted_vsize = tx.sigop_adjusted_vsize();
|
||||
let sigop_adjusted_weight = tx.sigop_adjusted_weight();
|
||||
let fee_per_vsize = tx.fee_rate();
|
||||
|
||||
Self {
|
||||
fee: tx.fee,
|
||||
weight: tx.weight,
|
||||
sigop_adjusted_weight,
|
||||
sigop_adjusted_vsize,
|
||||
sigops: tx.sigops,
|
||||
effective_fee_per_vsize: fee_per_vsize,
|
||||
parents: tx.parents.clone(),
|
||||
ancestors: HashSet::new(),
|
||||
children: HashSet::new(),
|
||||
ancestor_fee: tx.fee,
|
||||
ancestor_sigop_adjusted_weight: sigop_adjusted_weight,
|
||||
ancestor_sigop_adjusted_vsize: sigop_adjusted_vsize,
|
||||
ancestor_sigops: tx.sigops,
|
||||
score: fee_per_vsize,
|
||||
used: false,
|
||||
modified: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn ancestor_score(&self) -> f64 {
|
||||
if self.ancestor_sigop_adjusted_vsize == 0 {
|
||||
0.0
|
||||
} else {
|
||||
self.ancestor_fee as f64 / self.ancestor_sigop_adjusted_vsize as f64
|
||||
}
|
||||
}
|
||||
|
||||
fn update_score(&mut self) {
|
||||
self.score = self.ancestor_score();
|
||||
}
|
||||
}
|
||||
|
||||
/// Priority entry for the modified transactions queue
|
||||
#[derive(Debug, Clone)]
|
||||
struct TxPriority {
|
||||
txid: Txid,
|
||||
score: f64,
|
||||
}
|
||||
|
||||
impl PartialEq for TxPriority {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.txid == other.txid
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for TxPriority {}
|
||||
|
||||
impl PartialOrd for TxPriority {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for TxPriority {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
// Higher score = higher priority (reverse order for BinaryHeap)
|
||||
self.score
|
||||
.partial_cmp(&other.score)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of the GBT algorithm
|
||||
#[derive(Debug)]
|
||||
pub struct GbtResult {
|
||||
/// Projected blocks, each containing transaction IDs
|
||||
pub blocks: Vec<Vec<Txid>>,
|
||||
/// Statistics for each projected block
|
||||
pub block_stats: Vec<MempoolBlock>,
|
||||
}
|
||||
|
||||
/// Build projected mempool blocks using an approximation of Bitcoin Core's transaction selection.
|
||||
///
|
||||
/// Returns up to `max_blocks` projected blocks with their fee statistics.
|
||||
pub fn build_projected_blocks(
|
||||
transactions: &[GbtTransaction],
|
||||
max_block_weight: u32,
|
||||
max_blocks: usize,
|
||||
) -> GbtResult {
|
||||
if transactions.is_empty() || max_blocks == 0 {
|
||||
return GbtResult {
|
||||
blocks: vec![],
|
||||
block_stats: vec![],
|
||||
};
|
||||
}
|
||||
|
||||
// Build audit pool indexed by txid
|
||||
let mut audit_pool: HashMap<Txid, AuditTransaction> = transactions
|
||||
.iter()
|
||||
.map(|tx| (tx.txid, AuditTransaction::from_gbt_tx(tx)))
|
||||
.collect();
|
||||
|
||||
// Set up ancestor/descendant relationships
|
||||
let txids: Vec<Txid> = audit_pool.keys().cloned().collect();
|
||||
for txid in &txids {
|
||||
set_relatives(txid, &mut audit_pool);
|
||||
}
|
||||
|
||||
// Sort by descending ancestor score
|
||||
let mut mempool_stack: Vec<Txid> = txids;
|
||||
mempool_stack.sort_by(|a, b| {
|
||||
let score_a = audit_pool.get(a).map(|tx| tx.score).unwrap_or(0.0);
|
||||
let score_b = audit_pool.get(b).map(|tx| tx.score).unwrap_or(0.0);
|
||||
score_b
|
||||
.partial_cmp(&score_a)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
|
||||
// Build blocks
|
||||
let mut blocks: Vec<Vec<Txid>> = Vec::new();
|
||||
let mut block_fee_rates: Vec<Vec<f64>> = Vec::new();
|
||||
let mut current_block: Vec<Txid> = Vec::new();
|
||||
let mut current_fee_rates: Vec<f64> = Vec::new();
|
||||
let mut block_weight: u32 = BLOCK_RESERVED_WEIGHT;
|
||||
let mut block_sigops: u32 = BLOCK_RESERVED_SIGOPS;
|
||||
#[allow(unused_variables)]
|
||||
let mut block_fees: u64 = 0;
|
||||
|
||||
let mut modified: BinaryHeap<TxPriority> = BinaryHeap::new();
|
||||
let mut overflow: Vec<Txid> = Vec::new();
|
||||
let mut failures = 0;
|
||||
|
||||
while (!mempool_stack.is_empty() || !modified.is_empty()) && blocks.len() < max_blocks {
|
||||
// Get next best transaction from either stack or modified queue
|
||||
let next_txid = get_next_tx(&mut mempool_stack, &mut modified, &audit_pool);
|
||||
|
||||
let next_txid = match next_txid {
|
||||
Some(txid) => txid,
|
||||
None => break,
|
||||
};
|
||||
|
||||
let (ancestor_weight, ancestor_sigops, _ancestor_fee, ancestor_score) = {
|
||||
let tx = match audit_pool.get(&next_txid) {
|
||||
Some(tx) if !tx.used => tx,
|
||||
_ => continue,
|
||||
};
|
||||
(
|
||||
tx.ancestor_sigop_adjusted_weight,
|
||||
tx.ancestor_sigops,
|
||||
tx.ancestor_fee,
|
||||
tx.score,
|
||||
)
|
||||
};
|
||||
|
||||
// Check if this package fits in the current block
|
||||
if blocks.len() < max_blocks - 1
|
||||
&& (block_weight + ancestor_weight >= max_block_weight - BLOCK_RESERVED_WEIGHT
|
||||
|| block_sigops + ancestor_sigops > BLOCK_SIGOPS)
|
||||
{
|
||||
overflow.push(next_txid);
|
||||
failures += 1;
|
||||
} else {
|
||||
// Add the package (ancestors + this transaction) to the block
|
||||
let package = get_package(&next_txid, &audit_pool);
|
||||
|
||||
for pkg_txid in &package {
|
||||
if let Some(tx) = audit_pool.get_mut(pkg_txid) {
|
||||
if !tx.used {
|
||||
tx.used = true;
|
||||
current_block.push(*pkg_txid);
|
||||
current_fee_rates.push(tx.effective_fee_per_vsize);
|
||||
block_weight += tx.sigop_adjusted_weight;
|
||||
block_sigops += tx.sigops;
|
||||
block_fees += tx.fee;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update descendants
|
||||
update_descendants(&next_txid, &mut audit_pool, &mut modified, ancestor_score);
|
||||
failures = 0;
|
||||
}
|
||||
|
||||
// Check if block is full
|
||||
let exceeded_tries =
|
||||
failures > 1000 && block_weight > (max_block_weight - BLOCK_RESERVED_WEIGHT - 4_000);
|
||||
let queues_empty = mempool_stack.is_empty() && modified.is_empty();
|
||||
|
||||
if (exceeded_tries || queues_empty)
|
||||
&& blocks.len() < max_blocks - 1
|
||||
&& !current_block.is_empty()
|
||||
{
|
||||
blocks.push(std::mem::take(&mut current_block));
|
||||
block_fee_rates.push(std::mem::take(&mut current_fee_rates));
|
||||
block_weight = BLOCK_RESERVED_WEIGHT;
|
||||
block_sigops = BLOCK_RESERVED_SIGOPS;
|
||||
block_fees = 0;
|
||||
failures = 0;
|
||||
|
||||
// Move overflow back to processing
|
||||
overflow.reverse();
|
||||
for txid in overflow.drain(..) {
|
||||
if let Some(tx) = audit_pool.get(&txid) {
|
||||
if tx.modified {
|
||||
modified.push(TxPriority {
|
||||
txid,
|
||||
score: tx.score,
|
||||
});
|
||||
} else {
|
||||
mempool_stack.push(txid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add final block if not empty
|
||||
if !current_block.is_empty() {
|
||||
blocks.push(current_block);
|
||||
block_fee_rates.push(current_fee_rates);
|
||||
}
|
||||
|
||||
// Calculate block statistics
|
||||
let block_stats: Vec<MempoolBlock> = blocks
|
||||
.iter()
|
||||
.zip(block_fee_rates.iter())
|
||||
.map(|(block_txids, fee_rates)| calculate_block_stats(block_txids, fee_rates, &audit_pool))
|
||||
.collect();
|
||||
|
||||
GbtResult {
|
||||
blocks,
|
||||
block_stats,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_next_tx(
|
||||
mempool_stack: &mut Vec<Txid>,
|
||||
modified: &mut BinaryHeap<TxPriority>,
|
||||
audit_pool: &HashMap<Txid, AuditTransaction>,
|
||||
) -> Option<Txid> {
|
||||
loop {
|
||||
// Get candidates from both queues
|
||||
let stack_candidate = mempool_stack.last().and_then(|txid| {
|
||||
audit_pool
|
||||
.get(txid)
|
||||
.filter(|tx| !tx.used && !tx.modified)
|
||||
.map(|tx| (*txid, tx.score))
|
||||
});
|
||||
|
||||
let modified_candidate = modified.peek().and_then(|priority| {
|
||||
audit_pool
|
||||
.get(&priority.txid)
|
||||
.filter(|tx| !tx.used)
|
||||
.map(|tx| (priority.txid, tx.score))
|
||||
});
|
||||
|
||||
match (stack_candidate, modified_candidate) {
|
||||
(Some((stack_txid, stack_score)), Some((mod_txid, mod_score))) => {
|
||||
if mod_score >= stack_score {
|
||||
modified.pop();
|
||||
return Some(mod_txid);
|
||||
} else {
|
||||
mempool_stack.pop();
|
||||
return Some(stack_txid);
|
||||
}
|
||||
}
|
||||
(Some((txid, _)), None) => {
|
||||
mempool_stack.pop();
|
||||
return Some(txid);
|
||||
}
|
||||
(None, Some((txid, _))) => {
|
||||
modified.pop();
|
||||
return Some(txid);
|
||||
}
|
||||
(None, None) => {
|
||||
// Try to clean up invalid entries
|
||||
if mempool_stack.pop().is_some() {
|
||||
continue;
|
||||
}
|
||||
if modified.pop().is_some() {
|
||||
continue;
|
||||
}
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_relatives(txid: &Txid, audit_pool: &mut HashMap<Txid, AuditTransaction>) {
|
||||
// Get parents for this transaction
|
||||
let parents: Vec<Txid> = match audit_pool.get(txid) {
|
||||
Some(tx) => tx
|
||||
.parents
|
||||
.iter()
|
||||
.filter(|p| audit_pool.contains_key(*p))
|
||||
.cloned()
|
||||
.collect(),
|
||||
None => return,
|
||||
};
|
||||
|
||||
// Recursively set relatives for parents first
|
||||
for parent_txid in &parents {
|
||||
if audit_pool
|
||||
.get(parent_txid)
|
||||
.map(|tx| tx.ancestors.is_empty() && !tx.parents.is_empty())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
set_relatives(parent_txid, audit_pool);
|
||||
}
|
||||
}
|
||||
|
||||
// Collect ancestor info
|
||||
let mut ancestors: HashSet<Txid> = HashSet::new();
|
||||
let mut total_fee: u64 = 0;
|
||||
let mut total_sigop_adjusted_weight: u32 = 0;
|
||||
let mut total_sigop_adjusted_vsize: u32 = 0;
|
||||
let mut total_sigops: u32 = 0;
|
||||
|
||||
for parent_txid in &parents {
|
||||
if let Some(parent) = audit_pool.get(parent_txid) {
|
||||
ancestors.insert(*parent_txid);
|
||||
for ancestor in &parent.ancestors {
|
||||
ancestors.insert(*ancestor);
|
||||
}
|
||||
total_fee += parent.fee;
|
||||
total_sigop_adjusted_weight += parent.sigop_adjusted_weight;
|
||||
total_sigop_adjusted_vsize += parent.sigop_adjusted_vsize;
|
||||
total_sigops += parent.sigops;
|
||||
}
|
||||
}
|
||||
|
||||
// Add ancestor stats from indirect ancestors
|
||||
for ancestor_txid in &ancestors {
|
||||
if !parents.contains(ancestor_txid) {
|
||||
if let Some(ancestor) = audit_pool.get(ancestor_txid) {
|
||||
total_fee += ancestor.fee;
|
||||
total_sigop_adjusted_weight += ancestor.sigop_adjusted_weight;
|
||||
total_sigop_adjusted_vsize += ancestor.sigop_adjusted_vsize;
|
||||
total_sigops += ancestor.sigops;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the transaction
|
||||
if let Some(tx) = audit_pool.get_mut(txid) {
|
||||
tx.ancestors = ancestors;
|
||||
tx.ancestor_fee += total_fee;
|
||||
tx.ancestor_sigop_adjusted_weight += total_sigop_adjusted_weight;
|
||||
tx.ancestor_sigop_adjusted_vsize += total_sigop_adjusted_vsize;
|
||||
tx.ancestor_sigops += total_sigops;
|
||||
tx.update_score();
|
||||
}
|
||||
|
||||
// Update children of parents
|
||||
for parent_txid in parents {
|
||||
if let Some(parent) = audit_pool.get_mut(&parent_txid) {
|
||||
parent.children.insert(*txid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_package(txid: &Txid, audit_pool: &HashMap<Txid, AuditTransaction>) -> Vec<Txid> {
|
||||
let mut package: Vec<(Txid, usize)> = Vec::new();
|
||||
|
||||
if let Some(tx) = audit_pool.get(txid) {
|
||||
// Add ancestors first, sorted by ancestor count (so parents come before children)
|
||||
for ancestor_txid in &tx.ancestors {
|
||||
if let Some(ancestor) = audit_pool.get(ancestor_txid) {
|
||||
if !ancestor.used {
|
||||
package.push((*ancestor_txid, ancestor.ancestors.len()));
|
||||
}
|
||||
}
|
||||
}
|
||||
package.sort_by_key(|(_, count)| *count);
|
||||
|
||||
// Add the transaction itself
|
||||
package.push((*txid, tx.ancestors.len()));
|
||||
}
|
||||
|
||||
package.into_iter().map(|(txid, _)| txid).collect()
|
||||
}
|
||||
|
||||
fn update_descendants(
|
||||
root_txid: &Txid,
|
||||
audit_pool: &mut HashMap<Txid, AuditTransaction>,
|
||||
modified: &mut BinaryHeap<TxPriority>,
|
||||
cluster_rate: f64,
|
||||
) {
|
||||
let (root_fee, root_sigop_adjusted_weight, root_sigop_adjusted_vsize, root_sigops, children) = {
|
||||
match audit_pool.get(root_txid) {
|
||||
Some(tx) => (
|
||||
tx.fee,
|
||||
tx.sigop_adjusted_weight,
|
||||
tx.sigop_adjusted_vsize,
|
||||
tx.sigops,
|
||||
tx.children.clone(),
|
||||
),
|
||||
None => return,
|
||||
}
|
||||
};
|
||||
|
||||
let mut visited: HashSet<Txid> = HashSet::new();
|
||||
let mut stack: Vec<Txid> = children.into_iter().collect();
|
||||
|
||||
while let Some(desc_txid) = stack.pop() {
|
||||
if visited.contains(&desc_txid) {
|
||||
continue;
|
||||
}
|
||||
visited.insert(desc_txid);
|
||||
|
||||
let children_to_add: Vec<Txid>;
|
||||
let old_score: f64;
|
||||
let new_score: f64;
|
||||
|
||||
{
|
||||
let descendant = match audit_pool.get_mut(&desc_txid) {
|
||||
Some(tx) => tx,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
old_score = descendant.score;
|
||||
|
||||
// Remove root from ancestors
|
||||
descendant.ancestors.remove(root_txid);
|
||||
descendant.ancestor_fee = descendant.ancestor_fee.saturating_sub(root_fee);
|
||||
descendant.ancestor_sigop_adjusted_weight = descendant
|
||||
.ancestor_sigop_adjusted_weight
|
||||
.saturating_sub(root_sigop_adjusted_weight);
|
||||
descendant.ancestor_sigop_adjusted_vsize = descendant
|
||||
.ancestor_sigop_adjusted_vsize
|
||||
.saturating_sub(root_sigop_adjusted_vsize);
|
||||
descendant.ancestor_sigops = descendant.ancestor_sigops.saturating_sub(root_sigops);
|
||||
|
||||
// Update effective fee rate based on cluster rate
|
||||
if cluster_rate < descendant.effective_fee_per_vsize {
|
||||
descendant.effective_fee_per_vsize = cluster_rate;
|
||||
}
|
||||
|
||||
descendant.update_score();
|
||||
new_score = descendant.score;
|
||||
|
||||
children_to_add = descendant.children.iter().cloned().collect();
|
||||
}
|
||||
|
||||
// Add to modified queue if score changed
|
||||
if (new_score - old_score).abs() > f64::EPSILON {
|
||||
if let Some(tx) = audit_pool.get_mut(&desc_txid) {
|
||||
tx.modified = true;
|
||||
}
|
||||
modified.push(TxPriority {
|
||||
txid: desc_txid,
|
||||
score: new_score,
|
||||
});
|
||||
}
|
||||
|
||||
// Add children to stack
|
||||
for child in children_to_add {
|
||||
if !visited.contains(&child) {
|
||||
stack.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_block_stats(
|
||||
txids: &[Txid],
|
||||
fee_rates: &[f64],
|
||||
audit_pool: &HashMap<Txid, AuditTransaction>,
|
||||
) -> MempoolBlock {
|
||||
if txids.is_empty() {
|
||||
return MempoolBlock::default();
|
||||
}
|
||||
|
||||
let mut total_size: u64 = 0;
|
||||
let mut total_weight: u64 = 0;
|
||||
let mut total_fees: u64 = 0;
|
||||
|
||||
for txid in txids {
|
||||
if let Some(tx) = audit_pool.get(txid) {
|
||||
total_weight += tx.weight as u64;
|
||||
total_size += tx.weight as u64 / 4; // Approximate size
|
||||
total_fees += tx.fee;
|
||||
}
|
||||
}
|
||||
|
||||
let mut sorted_rates = fee_rates.to_vec();
|
||||
sorted_rates.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
|
||||
let n = sorted_rates.len();
|
||||
let median_fee = if n == 0 {
|
||||
0.0
|
||||
} else if n % 2 == 0 {
|
||||
(sorted_rates[n / 2 - 1] + sorted_rates[n / 2]) / 2.0
|
||||
} else {
|
||||
sorted_rates[n / 2]
|
||||
};
|
||||
|
||||
// Calculate percentiles for fee range: [min, 10th, 25th, 50th, 75th, 90th, max]
|
||||
let fee_range = if n == 0 {
|
||||
vec![0.0; 7]
|
||||
} else {
|
||||
vec![
|
||||
sorted_rates[0],
|
||||
sorted_rates[(n as f64 * 0.1) as usize],
|
||||
sorted_rates[(n as f64 * 0.25) as usize],
|
||||
sorted_rates[(n as f64 * 0.5) as usize],
|
||||
sorted_rates[((n as f64 * 0.75) as usize).min(n - 1)],
|
||||
sorted_rates[((n as f64 * 0.9) as usize).min(n - 1)],
|
||||
sorted_rates[n - 1],
|
||||
]
|
||||
};
|
||||
|
||||
MempoolBlock {
|
||||
block_size: total_size,
|
||||
block_vsize: total_weight as f64 / 4.0,
|
||||
n_tx: txids.len(),
|
||||
total_fees,
|
||||
median_fee,
|
||||
fee_range,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use bitcoin::hashes::Hash;
|
||||
|
||||
fn make_txid(n: u8) -> Txid {
|
||||
let mut bytes = [0u8; 32];
|
||||
bytes[0] = n;
|
||||
Txid::from_slice(&bytes).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_mempool() {
|
||||
let result = build_projected_blocks(&[], DEFAULT_BLOCK_WEIGHT, 8);
|
||||
assert!(result.blocks.is_empty());
|
||||
assert!(result.block_stats.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_transaction() {
|
||||
let txid = make_txid(1);
|
||||
let tx = GbtTransaction {
|
||||
txid,
|
||||
fee: 1000,
|
||||
weight: 400,
|
||||
sigops: 1,
|
||||
parents: vec![],
|
||||
};
|
||||
|
||||
let result = build_projected_blocks(&[tx], DEFAULT_BLOCK_WEIGHT, 8);
|
||||
assert_eq!(result.blocks.len(), 1);
|
||||
assert_eq!(result.blocks[0].len(), 1);
|
||||
assert_eq!(result.blocks[0][0], txid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parent_child_relationship() {
|
||||
let parent_txid = make_txid(1);
|
||||
let child_txid = make_txid(2);
|
||||
|
||||
let parent = GbtTransaction {
|
||||
txid: parent_txid,
|
||||
fee: 500,
|
||||
weight: 400,
|
||||
sigops: 1,
|
||||
parents: vec![],
|
||||
};
|
||||
|
||||
let child = GbtTransaction {
|
||||
txid: child_txid,
|
||||
fee: 1000,
|
||||
weight: 400,
|
||||
sigops: 1,
|
||||
parents: vec![parent_txid],
|
||||
};
|
||||
|
||||
let result = build_projected_blocks(&[parent, child], DEFAULT_BLOCK_WEIGHT, 8);
|
||||
assert_eq!(result.blocks.len(), 1);
|
||||
assert_eq!(result.blocks[0].len(), 2);
|
||||
// Parent should come before child
|
||||
let parent_pos = result.blocks[0].iter().position(|&t| t == parent_txid);
|
||||
let child_pos = result.blocks[0].iter().position(|&t| t == child_txid);
|
||||
assert!(parent_pos < child_pos);
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,9 @@ mod transaction;
|
||||
|
||||
pub mod bincode_util;
|
||||
pub mod electrum_merkle;
|
||||
pub mod fee_estimation;
|
||||
pub mod fees;
|
||||
pub mod gbt;
|
||||
|
||||
pub use self::block::{BlockHeaderMeta, BlockId, BlockMeta, BlockStatus, HeaderEntry, HeaderList};
|
||||
pub use self::fees::get_tx_fee;
|
||||
|
||||
44
start
44
start
@ -5,7 +5,6 @@ DAEMON=bitcoin
|
||||
NETWORK=mainnet
|
||||
FEATURES=default
|
||||
DB_FOLDER=/electrs
|
||||
ASSET_DB_ARGS=()
|
||||
NODENAME=$(hostname|cut -d . -f1)
|
||||
LOCATION=$(hostname|cut -d . -f2)
|
||||
USAGE="Usage: $0 (mainnet|testnet|signet|liquid|liquidtestnet) [popular-scripts]"
|
||||
@ -43,7 +42,7 @@ esac
|
||||
# which network?
|
||||
case "${1}" in
|
||||
mainnet)
|
||||
THREADS=$((NPROC / 8))
|
||||
THREADS=$((NPROC / 4))
|
||||
CRONJOB_TIMING="20 4 * * *"
|
||||
;;
|
||||
testnet)
|
||||
@ -65,7 +64,6 @@ case "${1}" in
|
||||
DAEMON=elements
|
||||
NETWORK=liquid
|
||||
FEATURES=liquid
|
||||
ASSET_DB_ARGS=(--asset-db-path /elements/asset_registry_db)
|
||||
THREADS=$((NPROC / 8))
|
||||
CRONJOB_TIMING="12 4 * * *"
|
||||
;;
|
||||
@ -73,7 +71,6 @@ case "${1}" in
|
||||
DAEMON=elements
|
||||
NETWORK=liquidtestnet
|
||||
FEATURES=liquid
|
||||
ASSET_DB_ARGS=(--asset-db-path /elements/asset_registry_testnet_db)
|
||||
THREADS=$((NPROC / 8))
|
||||
CRONJOB_TIMING="17 4 * * *"
|
||||
;;
|
||||
@ -156,9 +153,6 @@ do
|
||||
# prepare run-time variables
|
||||
UTXOS_LIMIT=500
|
||||
ELECTRUM_TXS_LIMIT=500
|
||||
ELECTRUM_MAX_LINE_SIZE=1048576 # 1 MiB
|
||||
ELECTRUM_MAX_SUBSCRIPTIONS=100
|
||||
ELECTRUM_MAX_CLIENTS=1000
|
||||
MAIN_LOOP_DELAY=500
|
||||
DAEMON_CONF="${HOME}/${DAEMON}.conf"
|
||||
HTTP_SOCKET_FILE="${HOME}/socket/esplora-${DAEMON}-${NETWORK}"
|
||||
@ -173,73 +167,43 @@ do
|
||||
if [ "${NODENAME}" = "node201" ];then
|
||||
UTXOS_LIMIT=9000
|
||||
ELECTRUM_TXS_LIMIT=9000
|
||||
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
|
||||
ELECTRUM_MAX_SUBSCRIPTIONS=100000
|
||||
ELECTRUM_MAX_CLIENTS=10000
|
||||
MAIN_LOOP_DELAY=14000
|
||||
fi
|
||||
if [ "${NODENAME}" = "node204" ] && [ "${LOCATION}" = "sg1" ];then
|
||||
UTXOS_LIMIT=9000
|
||||
ELECTRUM_TXS_LIMIT=9000
|
||||
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
|
||||
ELECTRUM_MAX_SUBSCRIPTIONS=100000
|
||||
ELECTRUM_MAX_CLIENTS=10000
|
||||
fi
|
||||
if [ "${NODENAME}" = "node204" ] && [ "${LOCATION}" = "hnl" ];then
|
||||
UTXOS_LIMIT=9000
|
||||
ELECTRUM_TXS_LIMIT=9000
|
||||
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
|
||||
ELECTRUM_MAX_SUBSCRIPTIONS=100000
|
||||
ELECTRUM_MAX_CLIENTS=10000
|
||||
fi
|
||||
if [ "${NODENAME}" = "node206" ] && [ "${LOCATION}" = "tk7" ];then
|
||||
UTXOS_LIMIT=9000
|
||||
ELECTRUM_TXS_LIMIT=9000
|
||||
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
|
||||
ELECTRUM_MAX_SUBSCRIPTIONS=100000
|
||||
ELECTRUM_MAX_CLIENTS=10000
|
||||
fi
|
||||
if [ "${NODENAME}" = "node211" ];then
|
||||
UTXOS_LIMIT=9000
|
||||
ELECTRUM_TXS_LIMIT=9000
|
||||
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
|
||||
ELECTRUM_MAX_SUBSCRIPTIONS=100000
|
||||
ELECTRUM_MAX_CLIENTS=10000
|
||||
fi
|
||||
if [ "${NODENAME}" = "node212" ];then
|
||||
UTXOS_LIMIT=9000
|
||||
ELECTRUM_TXS_LIMIT=9000
|
||||
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
|
||||
ELECTRUM_MAX_SUBSCRIPTIONS=100000
|
||||
ELECTRUM_MAX_CLIENTS=10000
|
||||
fi
|
||||
if [ "${NODENAME}" = "node213" ];then
|
||||
UTXOS_LIMIT=9000
|
||||
ELECTRUM_TXS_LIMIT=9000
|
||||
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
|
||||
ELECTRUM_MAX_SUBSCRIPTIONS=100000
|
||||
ELECTRUM_MAX_CLIENTS=10000
|
||||
fi
|
||||
if [ "${NODENAME}" = "node214" ];then
|
||||
UTXOS_LIMIT=9000
|
||||
ELECTRUM_TXS_LIMIT=9000
|
||||
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
|
||||
ELECTRUM_MAX_SUBSCRIPTIONS=100000
|
||||
ELECTRUM_MAX_CLIENTS=10000
|
||||
fi
|
||||
if [ "${NETWORK}" = "testnet4" ];then
|
||||
UTXOS_LIMIT=9000
|
||||
ELECTRUM_TXS_LIMIT=9000
|
||||
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
|
||||
ELECTRUM_MAX_SUBSCRIPTIONS=100000
|
||||
ELECTRUM_MAX_CLIENTS=10000
|
||||
fi
|
||||
if [ "${LOCATION}" = "fmt" ];then
|
||||
UTXOS_LIMIT=9000
|
||||
ELECTRUM_TXS_LIMIT=9000
|
||||
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
|
||||
ELECTRUM_MAX_SUBSCRIPTIONS=100000
|
||||
ELECTRUM_MAX_CLIENTS=10000
|
||||
fi
|
||||
|
||||
if [ ! -e "${POPULAR_SCRIPTS_FILE}" ];then
|
||||
@ -247,7 +211,7 @@ do
|
||||
fi
|
||||
|
||||
# Run the electrs process (Note: db-dir is used in both commands)
|
||||
nice cargo run \
|
||||
cargo run \
|
||||
--release \
|
||||
--bin electrs \
|
||||
--features "${FEATURES}" \
|
||||
@ -255,7 +219,6 @@ do
|
||||
--network "${NETWORK}" \
|
||||
--daemon-dir "${HOME}" \
|
||||
--db-dir "${DB_FOLDER}" \
|
||||
"${ASSET_DB_ARGS[@]}" \
|
||||
--main-loop-delay "${MAIN_LOOP_DELAY}" \
|
||||
--rpc-socket-file "${RPC_SOCKET_FILE}" \
|
||||
--http-socket-file "${HTTP_SOCKET_FILE}" \
|
||||
@ -266,9 +229,6 @@ do
|
||||
--address-search \
|
||||
--utxos-limit "${UTXOS_LIMIT}" \
|
||||
--electrum-txs-limit "${ELECTRUM_TXS_LIMIT}" \
|
||||
--electrum-max-line-size "${ELECTRUM_MAX_LINE_SIZE}" \
|
||||
--electrum-max-subscriptions "${ELECTRUM_MAX_SUBSCRIPTIONS}" \
|
||||
--electrum-max-clients "${ELECTRUM_MAX_CLIENTS}" \
|
||||
-vv
|
||||
sleep 1
|
||||
done
|
||||
|
||||
Loading…
Reference in New Issue
Block a user