Compare commits

...

205 Commits

Author SHA1 Message Date
wiz
7c8ac67f2f
Merge pull request #152 from mempool/mononaut/enable-liquid-asset-registry
Some checks failed
Compile Check and Lint / Compile Check (push) Has been cancelled
Compile Check and Lint / Formatter (push) Has been cancelled
Compile Check and Lint / Run Tests (push) Has been cancelled
Compile Check and Lint / Run Compile Checks in FreeBSD (push) Has been cancelled
Compile Check and Lint / Linter () (push) Has been cancelled
Compile Check and Lint / Linter (-F electrum-discovery) (push) Has been cancelled
Compile Check and Lint / Linter (-F electrum-discovery,liquid) (push) Has been cancelled
Compile Check and Lint / Linter (-F liquid) (push) Has been cancelled
enable liquid asset registry in start script
2026-06-25 16:53:44 +09:00
mononaut
39f44a2c17
Merge pull request #156 from mempool/junderw/add-electrum-ip-detection
Feat: Include HAProxy and per-IP connection limits
2026-06-25 11:06:43 +09:00
junderw
ee5e65e847
fix: Do not discard leftover 2026-06-24 20:11:27 +09:00
mononaut
2fa977b5a9
Merge branch 'mempool' into mononaut/enable-liquid-asset-registry 2026-06-09 20:29:09 +09:00
junderw
baad4799a5
Don't choke on PROXY headers, but ignore them if we're not configured to look for them 2026-06-06 17:01:04 +09:00
junderw
4751db3689
Feat: Include HAProxy and per-IP connection limits 2026-06-06 15:49:33 +09:00
wiz
adfe865262
Bump up default electrum RPC conn limit to 1K
Some checks failed
Compile Check and Lint / Compile Check (push) Has been cancelled
Compile Check and Lint / Formatter (push) Has been cancelled
Compile Check and Lint / Run Tests (push) Has been cancelled
Compile Check and Lint / Run Compile Checks in FreeBSD (push) Has been cancelled
Compile Check and Lint / Linter (-F liquid) (push) Has been cancelled
Compile Check and Lint / Linter () (push) Has been cancelled
Compile Check and Lint / Linter (-F electrum-discovery) (push) Has been cancelled
Compile Check and Lint / Linter (-F electrum-discovery,liquid) (push) Has been cancelled
2026-06-06 13:23:47 +09:00
wiz
587f9c1000
Merge pull request #153 from mempool/junderw/revert-fee-estimation-removal
Revert "Remove api/fee-estimates REST API"
2026-06-06 12:55:48 +09:00
mononaut
0cd2305ae5
Merge pull request #150 from mempool/junderw/feat/electrum-features
Some checks failed
Compile Check and Lint / Run Tests (push) Has been cancelled
Compile Check and Lint / Run Compile Checks in FreeBSD (push) Has been cancelled
Compile Check and Lint / Compile Check (push) Has been cancelled
Compile Check and Lint / Formatter (push) Has been cancelled
Compile Check and Lint / Linter () (push) Has been cancelled
Compile Check and Lint / Linter (-F electrum-discovery) (push) Has been cancelled
Compile Check and Lint / Linter (-F electrum-discovery,liquid) (push) Has been cancelled
Compile Check and Lint / Linter (-F liquid) (push) Has been cancelled
feat[electrum]: Support server.features on all feature combinations
2026-06-03 19:09:14 +09:00
mononaut
15e9430d4e
Merge branch 'mempool' into junderw/feat/electrum-features 2026-06-03 18:31:00 +09:00
wiz
6add8952a0
Merge pull request #148 from mempool/mononaut/global-thread-pool
Some checks are pending
Compile Check and Lint / Compile Check (push) Waiting to run
Compile Check and Lint / Formatter (push) Waiting to run
Compile Check and Lint / Run Tests (push) Waiting to run
Compile Check and Lint / Run Compile Checks in FreeBSD (push) Waiting to run
Compile Check and Lint / Linter () (push) Blocked by required conditions
Compile Check and Lint / Linter (-F electrum-discovery) (push) Blocked by required conditions
Compile Check and Lint / Linter (-F electrum-discovery,liquid) (push) Blocked by required conditions
Compile Check and Lint / Linter (-F liquid) (push) Blocked by required conditions
global thread pool
2026-06-03 13:44:03 +09:00
junderw
cd6a967e23
Revert "Remove api/fee-estimates REST API"
This reverts commit 01fd17358c.
2026-05-31 08:47:19 +09:00
wiz
b0774e5cee
Merge pull request #151 from mempool/mononaut/asset-search
Some checks failed
Compile Check and Lint / Compile Check (push) Has been cancelled
Compile Check and Lint / Formatter (push) Has been cancelled
Compile Check and Lint / Run Tests (push) Has been cancelled
Compile Check and Lint / Run Compile Checks in FreeBSD (push) Has been cancelled
Compile Check and Lint / Linter () (push) Has been cancelled
Compile Check and Lint / Linter (-F electrum-discovery) (push) Has been cancelled
Compile Check and Lint / Linter (-F electrum-discovery,liquid) (push) Has been cancelled
Compile Check and Lint / Linter (-F liquid) (push) Has been cancelled
2026-05-30 10:35:12 +09:00
mononaut
f11f6ab09a
avoid lowercase allocations during asset registry search 2026-05-29 09:35:38 +00:00
mononaut
95478619d1
enable liquid asset registry in start script 2026-05-25 18:29:10 +00:00
mononaut
237a2df61e
add individual liquid asset registry data endpoint 2026-05-25 17:43:29 +00:00
mononaut
88d0721033
add liquid asset search endpoint 2026-05-25 17:43:13 +00:00
junderw
c9da83f825
feat[electrum]: Support server.features on all feature combinations 2026-05-24 15:25:04 +09:00
mononaut
6f203ccb0c
global thread pool 2026-05-22 07:24:27 +00:00
mononaut
6dfe5295fe
Merge pull request #147 from mempool/junderw/electrum-limits
Some checks failed
Compile Check and Lint / Compile Check (push) Has been cancelled
Compile Check and Lint / Formatter (push) Has been cancelled
Compile Check and Lint / Run Tests (push) Has been cancelled
Compile Check and Lint / Run Compile Checks in FreeBSD (push) Has been cancelled
Compile Check and Lint / Linter () (push) Has been cancelled
Compile Check and Lint / Linter (-F electrum-discovery) (push) Has been cancelled
Compile Check and Lint / Linter (-F electrum-discovery,liquid) (push) Has been cancelled
Compile Check and Lint / Linter (-F liquid) (push) Has been cancelled
Fix: Add Electrum timeout to connections
2026-05-09 13:25:42 +09:00
junderw
d6d580c097
Fix: More fine grained timeout control 2026-05-08 23:04:16 +09:00
junderw
e53631a318
Fix: Add Electrum timeout to connections 2026-05-08 22:14:56 +09:00
mononaut
7f28499587
Merge pull request #146 from mempool/junderw/electrum-limits
Some checks are pending
Compile Check and Lint / Linter (-F electrum-discovery,liquid) (push) Blocked by required conditions
Compile Check and Lint / Linter (-F liquid) (push) Blocked by required conditions
Compile Check and Lint / Compile Check (push) Waiting to run
Compile Check and Lint / Formatter (push) Waiting to run
Compile Check and Lint / Run Tests (push) Waiting to run
Compile Check and Lint / Run Compile Checks in FreeBSD (push) Waiting to run
Compile Check and Lint / Linter () (push) Blocked by required conditions
Compile Check and Lint / Linter (-F electrum-discovery) (push) Blocked by required conditions
Fix: Add some simple limits to Electrum RPC by default
2026-05-08 19:48:11 +09:00
junderw
60fb1029c9
Fix: Add some simple limits to Electrum RPC by default 2026-05-04 14:59:15 +09:00
mononaut
8aa0cc06a6
Merge pull request #143 from mempool/junderw/fix-txs-endpoint
Some checks failed
Compile Check and Lint / Compile Check (push) Has been cancelled
Compile Check and Lint / Formatter (push) Has been cancelled
Compile Check and Lint / Run Tests (push) Has been cancelled
Compile Check and Lint / Run Compile Checks in FreeBSD (push) Has been cancelled
Compile Check and Lint / Linter () (push) Has been cancelled
Compile Check and Lint / Linter (-F electrum-discovery) (push) Has been cancelled
Compile Check and Lint / Linter (-F electrum-discovery,liquid) (push) Has been cancelled
Compile Check and Lint / Linter (-F liquid) (push) Has been cancelled
fix: continue address tx pagination across mempool boundary
2026-04-18 10:28:45 +09:00
Jonathan Underwood
10f42b566d
Merge branch 'mempool' into junderw/fix-txs-endpoint 2026-04-17 22:49:08 +09:00
wiz
12a70af0c0
ops: Run electrs nicely
Some checks failed
Compile Check and Lint / Compile Check (push) Has been cancelled
Compile Check and Lint / Formatter (push) Has been cancelled
Compile Check and Lint / Run Tests (push) Has been cancelled
Compile Check and Lint / Run Compile Checks in FreeBSD (push) Has been cancelled
Compile Check and Lint / Linter () (push) Has been cancelled
Compile Check and Lint / Linter (-F electrum-discovery) (push) Has been cancelled
Compile Check and Lint / Linter (-F electrum-discovery,liquid) (push) Has been cancelled
Compile Check and Lint / Linter (-F liquid) (push) Has been cancelled
2026-04-17 02:08:39 +09:00
wiz
67ed34e357
ops: Use less CPU threads for electrs precache 2026-04-17 00:47:04 +09:00
Jonathan Underwood
51fd37d553
Merge pull request #141 from mempool/knorrium/shared_admin_workflow
Some checks failed
Compile Check and Lint / Compile Check (push) Has been cancelled
Compile Check and Lint / Formatter (push) Has been cancelled
Compile Check and Lint / Run Tests (push) Has been cancelled
Compile Check and Lint / Run Compile Checks in FreeBSD (push) Has been cancelled
Compile Check and Lint / Linter () (push) Has been cancelled
Compile Check and Lint / Linter (-F electrum-discovery) (push) Has been cancelled
Compile Check and Lint / Linter (-F electrum-discovery,liquid) (push) Has been cancelled
Compile Check and Lint / Linter (-F liquid) (push) Has been cancelled
Use the shared admin workflow
2026-03-27 00:58:25 +09:00
Felipe Knorr Kuhn
474955985b
Merge branch 'mempool' into knorrium/shared_admin_workflow 2026-03-26 08:48:08 -07:00
junderw
98ae002244
fix: continue address tx pagination across mempool boundary
The address tx history endpoints could stop pagination at the mempool/chain boundary when after_txid pointed to the last mempool transaction for an address or address group. The confirmed-history query was reusing after_txid whenever the mempool query returned no rows, even if that cursor only existed in mempool. This change makes the confirmed-history query reuse after_txid only when the cursor was actually found in chain history; mempool cursors now correctly fall through to the newest confirmed transactions.

Co-authored-by: Saravanan Mani <228955468+saravanan7mani7@users.noreply.github.com>
2026-03-27 00:47:47 +09:00
Jonathan Underwood
3d8d6e6128
Merge pull request #144 from mempool/junderw/fix-freebsd
Fix: FreeBSD 14 build errors
2026-03-27 00:46:54 +09:00
junderw
c9e3a5e68d
Fix: FreeBSD 14 build errors 2026-03-27 00:34:48 +09:00
Felipe Knorr Kuhn
5d9132ef83
Use the shared admin workflow 2026-03-23 21:37:49 -07:00
mononaut
043dcbae3d
Merge pull request #140 from mempool/junderw/remove-fee-estimate-api
Remove api/fee-estimates REST API
2026-02-19 13:29:02 +09:00
junderw
01fd17358c
Remove api/fee-estimates REST API 2026-02-14 12:25:39 +09:00
mononaut
6a16d457b4
Merge pull request #138 from mempool/junderw/bump-bitcoin
cleanup: bump bitcoin and elements dependencies
2026-02-01 16:22:42 +09:00
wiz
fc0c0f5a11
Merge pull request #139 from mempool/knorrium/electrs_liquid
Some checks failed
Docker build on tag / Build and push to DockerHub (, electrs) (push) Has been cancelled
Docker build on tag / Build and push to DockerHub (--features liquid, electrs-liquid) (push) Has been cancelled
Add support for electrs-liquid images
2026-02-01 14:09:08 +09:00
Felipe Knorr Kuhn
ce089c4c16
Publish electrs-liquid images 2026-01-31 21:01:49 -08:00
junderw
f8302d7cb5
Use deprecated is_provably_unspendable() 2026-01-31 22:04:39 +09:00
junderw
a82862818e
bump electrum-client 2026-01-30 23:56:01 +09:00
junderw
ea6954288c
cleanup: bump bitcoin and elements dependencies 2026-01-30 23:09:02 +09:00
Jonathan Underwood
3000bd13e7
Merge pull request #132 from mempool/junderw/ignore-core
Fix: gitignore .core files
2026-01-02 19:33:03 +09:00
junderw
5252bdb805
Fix: gitignore .core files 2026-01-02 15:36:12 +09:00
Jonathan Underwood
d5dfbb13e1
Merge pull request #131 from mempool/junderw/fix-dirty-docker-builds
Fix: Do not embed (dirty) in the docker built version
2026-01-02 00:17:41 +09:00
junderw
0b735569b7
Fix: Do not embed (dirty) in the docker built version 2026-01-02 00:03:14 +09:00
wiz
dad95ade9e
Merge pull request #130 from mempool/knorrium/tag_workflow_fixes
Remove the swap steps from the tag workflow
2025-12-30 01:48:03 -10:00
Felipe Knorr Kuhn
6233b5c874
Remove the swap steps 2025-12-29 15:19:35 -08:00
Felipe Knorr Kuhn
fed8aed060
Comment out swap steps for testing
Some checks failed
Docker build on tag / Build and push to DockerHub (push) Has been cancelled
2025-12-29 12:48:56 -08:00
wiz
2a2e8beb1d
Merge pull request #129 from mempool/knorrium/ignore_swapoff_error
Ignore swapoff error
2025-12-29 09:38:04 -10:00
Felipe Knorr Kuhn
848bba817a
Ignore swapoff error 2025-12-29 11:31:23 -08:00
wiz
49436f60db
Merge pull request #128 from mempool/knorrium/runner 2025-12-26 23:25:29 -10:00
Felipe Knorr Kuhn
8a339f7414
Use the mempool ci runners 2025-12-24 15:33:46 +00:00
wiz
49464309a5
ops: Increase Github workflow to 20G swap 2025-12-24 04:11:54 -10:00
wiz
b74d8bac5d
Bump version to v3.4.0-dev 2025-12-24 20:02:53 +09:00
wiz
dbdcbf88e8
Release mempool-electrs v3.3.0 2025-12-24 19:59:53 +09:00
wiz
4eeb4c672c
ops: Use less CPU threads for electrs precache 2025-12-18 01:40:11 +09:00
wiz
467173e639
Merge pull request #126 from mempool/junderw/fix-freebsd-ci
Update CI workflow to install rocksdb and run full build
2025-12-10 01:03:22 +09:00
wiz
f98a612f32
Merge pull request #127 from mempool/orangesurf/auto-review-tag
Set review needed automatically
2025-12-10 00:58:50 +09:00
orangesurf
039dbb8bd8
Set review needed automatically 2025-12-09 18:02:23 +09:00
junderw
edcc9d0d1a
Fix linter errors in 1.89 (freebsd) 2025-12-07 16:39:09 +09:00
Jonathan Underwood
fb9caae3a6
Update CI workflow to install rocksdb and run full build
Added rocksdb to the package installation in CI workflow and run full build.
2025-12-07 16:14:00 +09:00
wiz
73145365a0
Merge pull request #123 from mempool/junderw/bump-rocksdb
Bump rocksdb dependency
2025-10-09 14:57:13 -10:00
junderw
4f689fe73d
chore: Bump rocksdb dependency to latest 2025-10-10 00:08:35 +09:00
wiz
56023979e8
Merge pull request #124 from mempool/junderw/add-bitcoind-subversion
Feat: Add X-Bitcoin-Version header to all responses from REST API.
2025-10-09 06:31:37 +07:00
wiz
dfaadcfd08
Merge branch 'mempool' into junderw/add-bitcoind-subversion 2025-10-09 06:29:18 +07:00
wiz
3c1de78f49
Merge pull request #125 from mempool/junderw/freebsd-ci
Add FreeBSD CI job, update to 1.87 (FreeBSD 14.3) and fix clippy lints
2025-10-09 06:29:07 +07:00
junderw
f972fa89df
Add FreeBSD CI job, update to 1.87 (FreeBSD 14.3) and fix clippy lints 2025-10-07 00:43:12 +09:00
junderw
4c1f084415
Feat: Add X-Bitcoin-Version header to all responses from REST API. Include bitcoind subversion string 2025-09-29 23:38:12 +09:00
wiz
e55afdc370
Merge pull request #122 from mempool/junderw/add-unsubscribe
Feat: Add blockchain.scripthash.unsubscribe to Electrum server impl
2025-07-17 08:23:30 -10:00
junderw
4081edcd28
Feat: Add blockchain.scripthash.unsubscribe to Electrum server impl 2025-06-05 01:39:07 +09:00
wiz
4d1fdaf7b4
Merge pull request #118 from mempool/junderw/fix-rollback
Fix rollback tip handling
2025-04-03 18:44:42 +09:00
wiz
c6367b985e
Merge branch 'mempool' into junderw/fix-rollback 2025-04-03 18:41:23 +09:00
wiz
7daee2c3a5
ops: Tweak config for individual instance limits 2025-03-29 18:57:36 +09:00
junderw
e3261b17ba
Fix rollback tip handling 2025-03-17 21:55:20 +09:00
wiz
78e9ef55cf
Bump version string to v3.3.0-dev 2025-03-15 16:38:57 +09:00
wiz
e6eb9b5f83
Release v3.2.0
Some checks failed
Docker build on tag / Build and push to DockerHub (push) Has been cancelled
2025-03-15 16:38:07 +09:00
softsimon
6cb9becca1
Merge pull request #116 from mempool/mononaut/fix-address-history-ordering
Fix address history tx ordering
2025-02-28 17:15:40 +07:00
junderw
edb78b640a
Fix fmt and clippy 2025-02-28 19:01:36 +09:00
Mononaut
c2aa22d3b3
fix address history ordering for liquid assets 2025-02-28 04:48:12 +00:00
Mononaut
e4139b72ca
Fix address history tx ordering 2025-02-28 03:08:39 +00:00
wiz
5b8819039d
Merge pull request #110 from mempool/junderw/bump-rust
Bump to rust 1.83 and fix lints
2025-01-20 18:37:59 +09:00
wiz
162516b40d
oops: Add missing brackets start script 2025-01-19 23:51:18 +09:00
wiz
96d27c8518
ops: Set UTXO limts for some servers in start script 2025-01-19 23:41:53 +09:00
junderw
dab16f8818
Bump to rust 1.83 and fix lints 2025-01-19 16:21:17 +09:00
wiz
ad3457279d
Bump version string to v3.2.0-dev 2025-01-19 15:22:48 +09:00
wiz
659880029a
Bump version to 3.1.0
Some checks failed
Docker build on tag / Build and push to DockerHub (push) Has been cancelled
2025-01-19 15:07:04 +09:00
wiz
df8e61e3b8
Merge pull request #113 from mempool/junderw/read-only-script-gen
Use read only when opening the history DB for popular scripts binary
2025-01-19 15:04:21 +09:00
wiz
c2992a6a07
Merge pull request #112 from mempool/junderw/fix-reorg-take-2
Revert blocks during reorg
2025-01-19 15:04:03 +09:00
junderw
1ef47423fb
Don't clobber popular scripts file when failed 2025-01-18 20:44:47 +09:00
junderw
811148702e
Use read only when opening the history DB for popular scripts binary 2025-01-18 17:06:53 +09:00
wiz
0ad5a87790
ops: Fix typo in start script 2025-01-07 18:42:35 +09:00
Mononaut
a454994447
reverse reorg indexing order 2024-12-27 11:40:40 +00:00
junderw
efed713c1e
Enough with the threads already... just chunk it. 2024-12-27 00:52:12 +09:00
junderw
1ca1cc071e
Don't do parallel stuff when deleting 2024-12-27 00:15:35 +09:00
junderw
737d7252a5
Fix logging and refactor reorg function 2024-12-15 14:23:57 +09:00
junderw
d0a2a94c65
Revert blocks during reorg 2024-12-15 00:20:22 +09:00
wiz
249848dc52
ops: Add version prints to start script 2024-12-13 11:01:11 +09:00
wiz
a27b0c2c49
ops: Fix start script errors 2024-12-13 10:52:47 +09:00
wiz
bc5e3aabc3
Merge pull request #101 from mempool/junderw/xor-dat
Add xor.dat support if the file exists
2024-12-13 10:21:54 +09:00
wiz
29389fae05
Merge pull request #104 from mempool/junderw/magic-testnet4-fix
Make testnet4 magic constant
2024-12-13 10:21:22 +09:00
wiz
b6d212433c
Merge pull request #59 from mempool/junderw/start-script-fix
Feature: Install popular-scripts as a cronjob
2024-12-13 10:18:52 +09:00
wiz
67ddaca0bf
Don't crontab popular-scripts for now 2024-12-13 10:18:28 +09:00
wiz
89f073551a
Merge pull request #109 from mempool/junderw/fix-summaries
Fix summary
2024-12-13 10:11:19 +09:00
junderw
0bc5509b21
Fix summary 2024-12-06 20:12:45 +09:00
wiz
c6a7fbde81
Merge pull request #103 from mempool/junderw/json-rpc-2
Some checks failed
Docker build on tag / Build and push to DockerHub (push) Has been cancelled
Fix JSON-RPC v2 errors
2024-11-14 17:34:58 +09:00
wiz
e5c1587e82
Merge pull request #108 from mempool/knorrium/arm64
Update cargo setup method and enable arm64 builds again
2024-11-14 17:34:18 +09:00
Felipe Knorr Kuhn
9ba4dd536b
Change base image back to bookworm-slim 2024-11-13 15:14:28 -08:00
Felipe Knorr Kuhn
decbb0e967
Revert Rust nightly, update debian version 2024-11-11 21:42:26 -08:00
Felipe Knorr Kuhn
760944cfbe
Use nightly cargo and enable arm64 builds again 2024-11-11 08:42:26 -08:00
Jonathan Underwood
66c76bc6ed
Merge branch 'mempool' into junderw/json-rpc-2 2024-11-11 08:34:36 +09:00
wiz
cb7981127c
Merge pull request #105 from mempool/natsoni/submitpackage-rpc
Add submitpackage endpoint
2024-11-01 23:44:06 +09:00
wiz
9330f246bb
Merge pull request #107 from mempool/mononaut/fix-duplicate-mempool-txs
fix duplicate mempool txs in address endpoint
2024-10-25 14:50:03 +09:00
Mononaut
c828ae952d
fix duplicate mempool txs in address endpoint 2024-10-25 05:46:47 +00:00
wiz
80758eac15
Merge pull request #82 from mempool/mononaut/wallet-apis
Add wallet / address group endpoints
2024-10-18 11:15:44 +09:00
natsoni
9f87c8f733
Add submitpackage endpoint 2024-10-15 16:42:56 +02:00
wiz
d51addb6d5
Merge pull request #106 from mempool/junderw/fuck-c
Use clang17 for FreeBSD
2024-10-15 21:19:55 +09:00
junderw
ae2e9b5651
Use clang17 for FreeBSD 2024-10-15 21:19:18 +09:00
junderw
79fa1e82af
Make testnet4 magic constant 2024-10-12 20:43:34 +09:00
Jonathan Underwood
026be4ceae
Merge branch 'mempool' into junderw/xor-dat 2024-10-12 19:54:19 +09:00
junderw
b7d9b3a9db
Add performance improvements to single address endpoints 2024-10-09 19:02:44 +09:00
Mononaut
0e39040b3d
fix wrong method type on /addresses/txs/summary 2024-10-08 16:39:39 +00:00
junderw
0661cac137
Move the take into the function to discourage unbounded parallel transaction getting 2024-10-08 19:02:53 +09:00
junderw
2fbb0172f7
Remove more collect() calls for parallel iterator 2024-10-08 18:56:32 +09:00
Mononaut
55ac4ecd1e
optimize multi-address tx history pagination 2024-10-07 21:56:26 +00:00
junderw
04957067d2
Remove clones and limit max address count 2024-10-07 19:02:41 +09:00
Mononaut
12599e7bf1
use POST for multi-address APIs 2024-10-06 22:39:20 +00:00
junderw
99538c2bb4
Remove some extra allocations 2024-09-28 18:11:47 +09:00
junderw
4594a3e14e
Merge remote-tracking branch 'mempool/mempool' into mononaut/wallet-apis 2024-09-28 17:56:04 +09:00
junderw
277b05b7c4
Fix JSON-RPC v2 errors 2024-09-14 17:07:49 +09:00
wiz
055aba1e8d
Bump version to 3.1.0-dev 2024-09-05 14:00:16 +09:00
wiz
e3eeaa55e8
Bump version to 3.0.0
Some checks failed
Docker build on tag / Build and push to DockerHub (push) Has been cancelled
2024-09-05 13:59:22 +09:00
junderw
10274b50ec
Add xor.dat support if the file exists 2024-09-05 02:32:45 +09:00
wiz
78155decaa
Merge pull request #100 from mempool/junderw/fix-testnet4-sync
Fix Popular Scripts script when zero history
2024-09-05 01:41:48 +09:00
junderw
1a21b53222
Handle popular-script when lack of H columns 2024-09-05 00:42:08 +09:00
wiz
583d9e94d2
Merge pull request #75 from mempool/junderw/fix-vout-u32
Fix: Output index should be u32 to prevent clobbering when over 65536 outputs.
2024-09-04 20:46:59 +09:00
wiz
961a255dbd
Merge pull request #99 from mempool/mononaut/anchor-output-type
Add support for anchor output type
2024-09-03 13:50:53 +09:00
Mononaut
570071a161
Add support for anchor output type 2024-08-30 20:16:30 +00:00
softsimon
bffba5a48a
Merge pull request #98 from mempool/junderw/batch-rpc
Adds support for batch operations in electrum RPC
2024-08-26 16:16:49 +02:00
wiz
ae3310c069
Bump version string to v3.0.0-beta
Some checks failed
Docker build on tag / Build and push to DockerHub (push) Has been cancelled
2024-08-05 17:58:45 -04:00
junderw
50a6cd7ad4
Refactor 2024-07-28 14:34:15 +09:00
junderw
0c99350174
Return response when invalid JSON 2024-07-27 17:21:21 +09:00
junderw
a1e41800f9
Return each error and success separately 2024-07-27 16:52:40 +09:00
junderw
c98f83da03
Fix clippy 2024-07-27 16:32:06 +09:00
junderw
c7c873c234
Feat: batching of RPC requests. 2024-07-27 16:22:03 +09:00
wiz
d070272069
Merge pull request #92 from mempool/junderw/fix-testnet4
Fix Testnet4 addition
2024-05-27 17:51:42 +09:00
wiz
9ab65162a4
Merge pull request #93 from mempool/junderw/order_txes_in_block
Order History events in the same confirmation height.
2024-05-27 17:49:09 +09:00
junderw
bd1ec253f5
Merge branch 'junderw/fix-testnet4' into junderw/start-script-fix 2024-05-17 13:52:48 +09:00
junderw
4a9feb99b9
Use nice 2024-05-17 13:52:41 +09:00
junderw
1423a4e3de
Merge remote-tracking branch 'mempool' into junderw/start-script-fix 2024-05-17 13:33:44 +09:00
wiz
62863af123
Merge pull request #87 from mempool/junderw/lock-mempool-less
Shorten mempool lock holding for update
2024-05-17 12:52:36 +09:00
Jonathan Underwood
7fc4912ed5
Merge pull request #94 from mempool/mononaut/fix-summary-tx-ordering
Fix summary tx ordering
2024-05-14 09:44:18 +09:00
Mononaut
f7b77a3db0
Fix tx order in address summaries 2024-05-13 18:42:08 +00:00
junderw
3dab4c7eb9
Use u16 for tx position 2024-05-10 10:17:30 +09:00
junderw
5bee69f75b
Feat: Order history entries in the order they appear in block. 2024-05-07 22:29:00 +09:00
junderw
aa88edbe0c
Fix Testnet4 addition 2024-05-07 22:29:00 +09:00
junderw
97e8708654
Fix Testnet4 addition 2024-05-07 22:19:56 +09:00
wiz
90200c8214
ops: Remove UTXO limit for testnet4 2024-05-07 01:39:27 +09:00
wiz
e3240dc616
Merge pull request #91 from mempool/wiz/add-testnet4
Add testnet4 support
2024-05-07 01:28:53 +09:00
Mononaut
17dc10e0ed
Fix testnet4 address lookups 2024-05-06 16:05:13 +00:00
Mononaut
181785b5c1
Allow empty magic 2024-05-06 15:16:00 +00:00
wiz
c6b8be94b7
ops: Add magic bytes for testnet4 to start script 2024-05-07 00:10:47 +09:00
Mononaut
69bfa5beff
Hardcode testnet4 genesis hash 2024-05-06 15:03:12 +00:00
Mononaut
cdb60c948a
Configurable network magic 2024-05-06 15:03:06 +00:00
wiz
14e427d8e8
Update start script for testnet4 instance 2024-05-06 23:00:54 +09:00
wiz
d486a36539
Add Testnet4 support 2024-05-06 22:24:09 +09:00
junderw
9e0ecad4d4
Prevent duplicate history events 2024-05-01 23:53:39 +09:00
junderw
c2445a3462
Shorten mempool lock holding for update 2024-05-01 21:08:03 +09:00
wiz
cd9efdff46
ops: Increase limits on node213/node214 2024-04-26 00:19:53 +09:00
softsimon
8e07f9fd03
Merge pull request #85 from gslandtreter/fix-regtest-startup
Fixed regression introduced by #51
2024-04-20 13:56:34 +07:00
Gustavo Spier Landtreter
1abe9c4e51 Fixed regression introduced by #51
Commit merged as part of #51 introduced a regression that prevents the daemon from breaking from its startup wait loop when running in `regtest` mode, and the blockchain already contains 1 or more blocks (apart from genesis).

This commit fixes the regression by only checking the equivalence between blocks and headers as the wait condition when running in `regtest` mode.
2024-04-20 02:01:15 -03:00
wiz
e6b0d9ffaa
ops: Adjust limits on Fremont site 2024-04-16 14:03:05 +09:00
softsimon
eea193a0a3
Merge pull request #81 from mempool/mononaut/test-mempool-accept
Add testmempoolaccept endpoint
2024-04-15 12:11:51 +09:00
Mononaut
74444677f2
Add wallet / address group endpoints 2024-03-28 09:01:19 +00:00
Mononaut
ac32e4b1c3
testmempoolaccept maxfeerate f32 -> f64 2024-03-25 05:54:33 +00:00
Mononaut
f7385ce392
testmempoolaccept JSON input, tx limit, error indices 2024-03-25 05:03:38 +00:00
Mononaut
569af75849
Add testmempoolaccept pre-checks 2024-03-24 06:34:15 +00:00
Mononaut
946ea714ed
testmempoolaccept add maxfeerate param 2024-03-24 05:30:24 +00:00
Mononaut
055ac9f4ab
Add testmempoolaccept endpoint 2024-03-23 09:08:45 +00:00
softsimon
d4f788fc3d
Merge pull request #80 from mempool/mononaut/address-summary
Add /address/:addr/txs/summary endpoint
2024-03-20 14:40:22 +09:00
Mononaut
95557427c8
Switch IndexMap to HashMap 2024-03-19 08:20:05 +00:00
Mononaut
c5e308389b
Fix max/min typo 2024-03-19 02:35:03 +00:00
Mononaut
378e036750
Add /address/:addr/txs/summary endpoint 2024-03-18 08:17:47 +00:00
wiz
f85bf8ac1a
Merge pull request #56 from mempool/junderw/use-blocking-spawn
REST API blocking async: Solution A, block_in_place
2024-03-06 16:26:20 +09:00
wiz
94b9222542
Merge pull request #58 from mempool/junderw/add-metrics-rest-api
Add metrics for REST response times
2024-03-06 16:16:30 +09:00
wiz
48fe0aa05a
Merge pull request #74 from mempool/mononaut/paged-txids
Add paged mempool txids endpoint
2024-03-06 16:15:12 +09:00
softsimon
f78c07a0c2
Merge pull request #68 from mempool/junderw/threadpool-builder-fix
ThreadPoolBuilder can fail when resources are busy
2024-02-23 15:47:36 +07:00
mononaut
1cb2715ba0
Merge branch 'mempool' into junderw/threadpool-builder-fix 2024-02-22 16:51:30 -06:00
mononaut
94cfe27571
Merge branch 'mempool' into junderw/use-blocking-spawn 2024-02-22 16:32:12 -06:00
Mononaut
054b2511fd
Fix histogram timer labels 2024-02-22 22:26:30 +00:00
junderw
5b2075b2ec
Fix: Output index should be u32 to prevent clobbering when over 65536 outputs. 2024-02-04 22:27:48 +09:00
Mononaut
579a6cc9eb
Add paged mempool txids endpoint 2024-01-25 20:50:27 +00:00
softsimon
13e50239ac
Merge pull request #72 from mempool/junderw/fix-sigops-liquid
Fix: Liquid sigops was trying to count for pegins.
2024-01-05 19:54:42 +07:00
junderw
f0c9fa7c5f
Fix: Liquid sigops was trying to count for pegins. 2024-01-04 22:19:23 +09:00
wiz
e4b8bad5cb
Merge pull request #66 from mempool/junderw/periodic_log_progress
Feature: Log every 10k blocks to help show progress during initial sync
2023-12-15 20:36:17 +09:00
wiz
8c691100dd
Merge pull request #70 from mempool/junderw/loop-and-broadcast
Feature: Configurable loop delay and bitcoind-only broadcast endpoint
2023-12-15 20:35:46 +09:00
junderw
d430e167c6
Feat: Use main loop delay in start script and set node201 to 14000 2023-12-12 01:54:23 -07:00
junderw
93e447bb78
Add: main_loop_delay arg 2023-12-12 01:49:06 -07:00
junderw
cbd8a29cd2
ThreadPoolBuilder can fail when resources are busy 2023-12-07 01:45:10 -07:00
junderw
9b7d7016ab
Feature: Log every 10k blocks to help show progress during initial sync 2023-11-30 02:05:40 -07:00
softsimon
53894ccf47
Merge branch 'mempool' into junderw/add-metrics-rest-api 2023-11-13 16:11:32 +09:00
junderw
a8aba23acb
Feature: Install popular-scripts as a cronjob 2023-10-09 12:12:05 -07:00
junderw
83301a2d33
Add metrics for REST response times 2023-10-02 21:49:26 -07:00
junderw
2263cb0594
Add metrics for REST response times 2023-10-02 21:48:00 -07:00
junderw
ff4e4530e7
REST API blocking async: Solution A, block_in_place 2023-10-01 19:12:31 -07:00
40 changed files with 3957 additions and 1210 deletions

View File

@ -33,8 +33,8 @@ runs:
echo "toolchain=\"${RUST_TOOLCHAIN}\"" >> $GITHUB_OUTPUT echo "toolchain=\"${RUST_TOOLCHAIN}\"" >> $GITHUB_OUTPUT
- name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain - name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain
id: toolchain id: toolchain
# Commit date is Sep 19, 2023 # Commit date is Nov 18, 2024
uses: dtolnay/rust-toolchain@439cf607258077187679211f12aa6f19af4a0af7 uses: dtolnay/rust-toolchain@315e265cd78dad1e1dcf3a5074f6d6c47029d5aa
with: with:
toolchain: ${{ steps.gettoolchain.outputs.toolchain }} toolchain: ${{ steps.gettoolchain.outputs.toolchain }}
targets: ${{ inputs.targets }} targets: ${{ inputs.targets }}

View File

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

View File

@ -16,39 +16,18 @@ permissions:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: mempool-ci
timeout-minutes: 120 timeout-minutes: 120
name: Build and push to DockerHub name: Build and push to DockerHub
strategy:
max-parallel: 1
matrix:
include:
- image: electrs
cargo_extra_args: ""
- image: electrs-liquid
cargo_extra_args: "--features liquid"
steps: steps:
# Workaround based on JonasAlfredsson/docker-on-tmpfs@v1.0.1
- name: Replace the current swap file
shell: bash
run: |
sudo swapoff /mnt/swapfile
sudo rm -v /mnt/swapfile
sudo fallocate -l 13G /mnt/swapfile
sudo chmod 600 /mnt/swapfile
sudo mkswap /mnt/swapfile
sudo swapon /mnt/swapfile
- name: Show current memory and swap status
shell: bash
run: |
sudo free -h
echo
sudo swapon --show
- name: Mount a tmpfs over /var/lib/docker
shell: bash
run: |
if [ ! -d "/var/lib/docker" ]; then
echo "Directory '/var/lib/docker' not found"
exit 1
fi
sudo mount -t tmpfs -o size=10G tmpfs /var/lib/docker
sudo systemctl restart docker
sudo df -h | grep docker
- name: Set env variables - name: Set env variables
run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
@ -65,9 +44,9 @@ jobs:
- name: Checkout project - name: Checkout project
uses: actions/checkout@v3 uses: actions/checkout@v3
# - name: Set up QEMU - name: Set up QEMU
# uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
# id: qemu id: qemu
- name: Setup Docker buildx action - name: Setup Docker buildx action
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@ -90,8 +69,9 @@ jobs:
docker buildx build \ docker buildx build \
--cache-from "type=local,src=/tmp/.buildx-cache" \ --cache-from "type=local,src=/tmp/.buildx-cache" \
--cache-to "type=local,dest=/tmp/.buildx-cache" \ --cache-to "type=local,dest=/tmp/.buildx-cache" \
--platform linux/amd64 \ --platform linux/amd64,linux/arm64 \
--tag ${{ secrets.DOCKER_HUB_USER }}/electrs:$TAG \ --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.image }}:$TAG \
--tag ${{ secrets.DOCKER_HUB_USER }}/electrs:latest \ --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.image }}:latest \
--output "type=registry" . \ --output "type=registry" . \
--build-arg commitHash=$SHORT_SHA --build-arg commitHash=$SHORT_SHA \
--build-arg CARGO_EXTRA_ARGS="${{ matrix.cargo_extra_args }}"

View File

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

1
.gitignore vendored
View File

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

19
AGENTS.md Normal file
View File

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

1055
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package] [package]
name = "mempool-electrs" name = "mempool-electrs"
version = "3.0.0-dev" version = "3.4.0-dev"
authors = [ authors = [
"Roman Zeyde <me@romanzey.de>", "Roman Zeyde <me@romanzey.de>",
"Nadav Ivgi <nadav@shesek.info>", "Nadav Ivgi <nadav@shesek.info>",
@ -21,39 +21,39 @@ name = "electrs"
[features] [features]
default = [] default = []
liquid = [ "elements" ] liquid = ["elements"]
electrum-discovery = [ "electrum-client"] electrum-discovery = ["electrum-client"]
[dependencies] [dependencies]
arrayref = "0.3.6" arrayref = "0.3.6"
base64 = "0.13.0" base64 = "0.13.0"
bincode-do-not-use-directly = { version = "1.3.1", package = "bincode" } bincode-do-not-use-directly = { version = "1.3.1", package = "bincode" }
bitcoin = { version = "0.28", features = [ "use-serde" ] } bitcoin = { version = "0.32.8", features = [ "serde" ] }
bounded-vec-deque = "0.1.1" bounded-vec-deque = "0.1.1"
clap = "2.33.3" clap = "2.33.3"
crossbeam-channel = "0.5.0" crossbeam-channel = "0.5.0"
dirs = "4.0.0" dirs = "4.0.0"
elements = { version = "0.19.1", features = [ "serde-feature" ], optional = true } elements = { version = "0.26.1", features = [ "serde" ], optional = true }
error-chain = "0.12.4" error-chain = "0.12.4"
glob = "0.3" glob = "0.3"
hex = "0.4.2" hex = "0.4.2"
itertools = "0.10" itertools = "0.10"
lazy_static = "1.3.0" lazy_static = "1.3.0"
libc = "0.2.81" libc = "0.2"
log = "0.4.11" log = "0.4.11"
socket2 = { version = "0.4", features = ["all"] } socket2 = { version = "0.4", features = ["all"] }
num_cpus = "1.12.0" num_cpus = "1.12.0"
page_size = "0.4.2" page_size = "0.4.2"
prometheus = "0.13" prometheus = "0.13"
ppp = "2.3.0"
rayon = "1.5.0" rayon = "1.5.0"
rocksdb = "0.21.0" rocksdb = "0.24.0"
serde = "1.0.118" serde = "1.0.118"
serde_derive = "1.0.118" serde_derive = "1.0.118"
serde_json = "1.0.60" serde_json = "1.0.60"
sha2 = "0.10.7" sha2 = "0.10.7"
signal-hook = "0.3" signal-hook = "0.3"
stderrlog = "0.5.0" stderrlog = "0.5.0"
sysconf = ">=0.3.4"
time = { version = "0.3", features = ["formatting"] } time = { version = "0.3", features = ["formatting"] }
tiny_http = "0.11" tiny_http = "0.11"
url = "2.2.0" url = "2.2.0"
@ -63,8 +63,7 @@ hyperlocal = "0.8"
tokio = { version = "1", features = ["sync", "macros"] } tokio = { version = "1", features = ["sync", "macros"] }
# optional dependencies for electrum-discovery # optional dependencies for electrum-discovery
electrum-client = { version = "0.8", optional = true } electrum-client = { version = "0.24.1", optional = true }
[dev-dependencies] [dev-dependencies]
tempfile = "3.0" tempfile = "3.0"
@ -75,5 +74,5 @@ panic = 'abort'
codegen-units = 1 codegen-units = 1
[patch.crates-io.electrum-client] [patch.crates-io.electrum-client]
git = "https://github.com/Blockstream/rust-electrum-client" git = "https://github.com/mempool/rust-electrum-client"
rev = "d3792352992a539afffbe11501d1aff9fd5b919d" # add-peer branch rev = "4bbfc612d594fe23282c439d4bdc446cff01ba1c" # 0.24.1/add-peer branch

View File

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

View File

@ -34,7 +34,8 @@ fn main() {
// This includes untracked files // This includes untracked files
let dirty = cmd("git", &["status", "--short"]).expect("git command works"); let dirty = cmd("git", &["status", "--short"]).expect("git command works");
let git_hash = if dirty.is_empty() { // Ignore Dockerfile deletion as it is expected in Docker buildx builds
let git_hash = if dirty.is_empty() || dirty.trim() == "D Dockerfile" {
rev_parse rev_parse
} else { } else {
format!("{}(dirty)", rev_parse.trim()) format!("{}(dirty)", rev_parse.trim())

View File

@ -1 +1 @@
1.70 1.87

View File

@ -57,6 +57,8 @@ TESTNAME="Running cargo clippy check electrum-discovery + liquid"
echo "$TESTNAME" echo "$TESTNAME"
cargo clippy $@ -q -F electrum-discovery,liquid cargo clippy $@ -q -F electrum-discovery,liquid
TESTNAME="Running cargo test with all features" if [ $INCLUDE_TESTS ]; then
echo "$TESTNAME" TESTNAME="Running cargo test with all features"
cargo test $@ -q --lib --all-features echo "$TESTNAME"
cargo test $@ -q --lib --all-features
fi

View File

@ -50,6 +50,7 @@ fn run_server(config: Arc<Config>) -> Result<()> {
config.daemon_rpc_addr, config.daemon_rpc_addr,
config.cookie_getter(), config.cookie_getter(),
config.network_type, config.network_type,
config.magic,
signal.clone(), signal.clone(),
&metrics, &metrics,
)?); )?);
@ -74,7 +75,18 @@ fn run_server(config: Arc<Config>) -> Result<()> {
&metrics, &metrics,
Arc::clone(&config), Arc::clone(&config),
))); )));
mempool.write().unwrap().update(&daemon)?; loop {
match Mempool::update(&mempool, &daemon) {
Ok(_) => break,
Err(e) => {
warn!(
"Error performing initial mempool update, trying again in 5 seconds: {}",
e.display_chain()
);
signal.wait(Duration::from_secs(5), false)?;
}
}
}
#[cfg(feature = "liquid")] #[cfg(feature = "liquid")]
let asset_db = config.asset_db_path.as_ref().map(|db_dir| { let asset_db = config.asset_db_path.as_ref().map(|db_dir| {
@ -93,7 +105,7 @@ fn run_server(config: Arc<Config>) -> Result<()> {
)); ));
// TODO: configuration for which servers to start // TODO: configuration for which servers to start
let rest_server = rest::start(Arc::clone(&config), Arc::clone(&query)); let rest_server = rest::start(Arc::clone(&config), Arc::clone(&query), &metrics);
let electrum_server = ElectrumRPC::start(Arc::clone(&config), Arc::clone(&query), &metrics); let electrum_server = ElectrumRPC::start(Arc::clone(&config), Arc::clone(&query), &metrics);
if let Some(ref precache_file) = config.precache_scripts { if let Some(ref precache_file) = config.precache_scripts {
@ -107,7 +119,7 @@ fn run_server(config: Arc<Config>) -> Result<()> {
} }
loop { loop {
if let Err(err) = signal.wait(Duration::from_millis(500), true) { if let Err(err) = signal.wait(Duration::from_millis(config.main_loop_delay), true) {
info!("stopping server: {}", err); info!("stopping server: {}", err);
electrs::util::spawn_thread("shutdown-thread-checker", || { electrs::util::spawn_thread("shutdown-thread-checker", || {
@ -136,7 +148,13 @@ fn run_server(config: Arc<Config>) -> Result<()> {
}; };
// Update mempool // Update mempool
mempool.write().unwrap().update(&daemon)?; if let Err(e) = Mempool::update(&mempool, &daemon) {
// Log the error if the result is an Err
warn!(
"Error updating mempool, skipping mempool update: {}",
e.display_chain()
);
}
// Update subscribed clients // Update subscribed clients
electrum_server.notify(); electrum_server.notify();

View File

@ -18,7 +18,10 @@ type DB = rocksdb::DBWithThreadMode<rocksdb::MultiThreaded>;
lazy_static! { lazy_static! {
static ref HISTORY_DB: DB = { static ref HISTORY_DB: DB = {
let config = Config::from_args(); let config = Config::from_args();
open_raw_db(&config.db_path.join("newindex").join("history")) open_raw_db(
&config.db_path.join("newindex").join("history"),
electrs::new_index::db::OpenMode::ReadOnly,
)
}; };
} }
@ -95,8 +98,7 @@ fn run_iterator(
"Thread ({thread_id:?}) Seeking DB to beginning of tx histories for b'H' + {}", "Thread ({thread_id:?}) Seeking DB to beginning of tx histories for b'H' + {}",
hex::encode([first_byte]) hex::encode([first_byte])
); );
// H = 72 let mut compare_vec: Vec<u8> = vec![b'H', first_byte];
let mut compare_vec: Vec<u8> = vec![72, first_byte];
iter.seek(&compare_vec); // Seek to beginning of our section iter.seek(&compare_vec); // Seek to beginning of our section
// Insert the byte of the next section for comparing // Insert the byte of the next section for comparing
@ -122,7 +124,7 @@ fn run_iterator(
while iter.valid() { while iter.valid() {
let key = iter.key().unwrap(); let key = iter.key().unwrap();
if is_finished(key) { if key.is_empty() || key[0] != b'H' || is_finished(key) {
// We have left the txhistory section, // We have left the txhistory section,
// but we need to check the final scripthash // but we need to check the final scripthash
send_if_popular( send_if_popular(

View File

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

View File

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

View File

@ -4,7 +4,7 @@ use std::fs;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::net::ToSocketAddrs; use std::net::ToSocketAddrs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::{Arc, OnceLock};
use stderrlog; use stderrlog;
use crate::chain::Network; use crate::chain::Network;
@ -17,6 +17,8 @@ use bitcoin::Network as BNetwork;
pub(crate) const APP_NAME: &str = "mempool-electrs"; pub(crate) const APP_NAME: &str = "mempool-electrs";
pub(crate) const ELECTRS_VERSION: &str = env!("CARGO_PKG_VERSION"); pub(crate) const ELECTRS_VERSION: &str = env!("CARGO_PKG_VERSION");
pub(crate) const GIT_HASH: Option<&str> = option_env!("GIT_HASH"); pub(crate) const GIT_HASH: Option<&str> = option_env!("GIT_HASH");
// This will be set only once in the Daemon::new() constructor at startup
pub(crate) static BITCOIND_SUBVER: OnceLock<String> = OnceLock::new();
lazy_static! { lazy_static! {
pub(crate) static ref VERSION_STRING: String = { pub(crate) static ref VERSION_STRING: String = {
@ -33,6 +35,7 @@ pub struct Config {
// See below for the documentation of each field: // See below for the documentation of each field:
pub log: stderrlog::StdErrLog, pub log: stderrlog::StdErrLog,
pub network_type: Network, pub network_type: Network,
pub magic: Option<u32>,
pub db_path: PathBuf, pub db_path: PathBuf,
pub daemon_dir: PathBuf, pub daemon_dir: PathBuf,
pub blocks_dir: PathBuf, pub blocks_dir: PathBuf,
@ -45,6 +48,7 @@ pub struct Config {
pub monitoring_addr: SocketAddr, pub monitoring_addr: SocketAddr,
pub jsonrpc_import: bool, pub jsonrpc_import: bool,
pub light_mode: bool, pub light_mode: bool,
pub main_loop_delay: u64,
pub address_search: bool, pub address_search: bool,
pub index_unspendables: bool, pub index_unspendables: bool,
pub cors: Option<String>, pub cors: Option<String>,
@ -58,15 +62,22 @@ pub struct Config {
pub rest_default_block_limit: usize, pub rest_default_block_limit: usize,
pub rest_default_chain_txs_per_page: usize, pub rest_default_chain_txs_per_page: usize,
pub rest_default_max_mempool_txs: usize, pub rest_default_max_mempool_txs: usize,
pub rest_default_max_address_summary_txs: usize,
pub rest_max_mempool_page_size: usize, pub rest_max_mempool_page_size: usize,
pub rest_max_mempool_txid_page_size: usize,
pub electrum_max_line_size: usize,
pub electrum_max_subscriptions: usize,
pub electrum_max_clients: usize,
pub electrum_idle_timeout: u64,
pub electrum_haproxy_depth: usize,
pub electrum_connections_per_client: usize,
pub electrum_public_hosts: Option<crate::electrum::ServerHosts>,
#[cfg(feature = "liquid")] #[cfg(feature = "liquid")]
pub parent_network: BNetwork, pub parent_network: BNetwork,
#[cfg(feature = "liquid")] #[cfg(feature = "liquid")]
pub asset_db_path: Option<PathBuf>, pub asset_db_path: Option<PathBuf>,
#[cfg(feature = "electrum-discovery")]
pub electrum_public_hosts: Option<crate::electrum::ServerHosts>,
#[cfg(feature = "electrum-discovery")] #[cfg(feature = "electrum-discovery")]
pub electrum_announce: bool, pub electrum_announce: bool,
#[cfg(feature = "electrum-discovery")] #[cfg(feature = "electrum-discovery")]
@ -134,6 +145,12 @@ impl Config {
.help(&network_help) .help(&network_help)
.takes_value(true), .takes_value(true),
) )
.arg(
Arg::with_name("magic")
.long("magic")
.default_value("")
.takes_value(true),
)
.arg( .arg(
Arg::with_name("electrum_rpc_addr") Arg::with_name("electrum_rpc_addr")
.long("electrum-rpc-addr") .long("electrum-rpc-addr")
@ -168,6 +185,12 @@ impl Config {
.long("lightmode") .long("lightmode")
.help("Enable light mode for reduced storage") .help("Enable light mode for reduced storage")
) )
.arg(
Arg::with_name("main_loop_delay")
.long("main-loop-delay")
.help("The number of milliseconds the main loop will wait between loops. (Can be shortened with SIGUSR1)")
.default_value("500")
)
.arg( .arg(
Arg::with_name("address_search") Arg::with_name("address_search")
.long("address-search") .long("address-search")
@ -232,12 +255,24 @@ impl Config {
.help("The default number of mempool transactions returned by the txs endpoints.") .help("The default number of mempool transactions returned by the txs endpoints.")
.default_value("50") .default_value("50")
) )
.arg(
Arg::with_name("rest_default_max_address_summary_txs")
.long("rest-default-max-address-summary-txs")
.help("The default number of transactions returned by the address summary endpoints.")
.default_value("5000")
)
.arg( .arg(
Arg::with_name("rest_max_mempool_page_size") Arg::with_name("rest_max_mempool_page_size")
.long("rest-max-mempool-page-size") .long("rest-max-mempool-page-size")
.help("The maximum number of transactions returned by the paginated /internal/mempool/txs endpoint.") .help("The maximum number of transactions returned by the paginated /internal/mempool/txs endpoint.")
.default_value("1000") .default_value("1000")
) )
.arg(
Arg::with_name("rest_max_mempool_txid_page_size")
.long("rest-max-mempool-txid-page-size")
.help("The maximum number of transactions returned by the paginated /mempool/txids/page endpoint.")
.default_value("10000")
)
.arg( .arg(
Arg::with_name("electrum_txs_limit") Arg::with_name("electrum_txs_limit")
.long("electrum-txs-limit") .long("electrum-txs-limit")
@ -248,6 +283,36 @@ impl Config {
.long("electrum-banner") .long("electrum-banner")
.help("Welcome banner for the Electrum server, shown in the console to clients.") .help("Welcome banner for the Electrum server, shown in the console to clients.")
.takes_value(true) .takes_value(true)
).arg(
Arg::with_name("electrum_max_line_size")
.long("electrum-max-line-size")
.help("Maximum size of a single Electrum request line in bytes (default: 1 MiB).")
.default_value("1048576")
).arg(
Arg::with_name("electrum_max_subscriptions")
.long("electrum-max-subscriptions")
.help("Maximum number of scripthash subscriptions per client connection.")
.default_value("100")
).arg(
Arg::with_name("electrum_max_clients")
.long("electrum-max-clients")
.help("Maximum number of concurrent Electrum client connections.")
.default_value("10")
).arg(
Arg::with_name("electrum_idle_timeout")
.long("electrum-idle-timeout")
.help("Maximum idle time in seconds since the last client request before disconnecting the Electrum connection.")
.default_value("600")
).arg(
Arg::with_name("electrum_haproxy_depth")
.long("electrum-haproxy-depth")
.help("Which HAProxy PROXY-protocol header layer identifies the real client IP. 0 disables PROXY-protocol detection; 1 uses the first (outermost) address, 2 the second, and so on. If the requested layer or any PROXY header is absent, no client IP is associated with the connection.")
.default_value("0")
).arg(
Arg::with_name("electrum_connections_per_client")
.long("electrum-connections-per-client")
.help("Maximum number of concurrent Electrum connections allowed per client (keyed by the HAProxy-reported address when available, otherwise the peer IP). 0 disables the per-client limit.")
.default_value("10")
); );
#[cfg(unix)] #[cfg(unix)]
@ -307,6 +372,10 @@ impl Config {
let network_name = m.value_of("network").unwrap_or("mainnet"); let network_name = m.value_of("network").unwrap_or("mainnet");
let network_type = Network::from(network_name); let network_type = Network::from(network_name);
let magic: Option<u32> = m
.value_of("magic")
.filter(|s| !s.is_empty())
.map(|s| u32::from_str_radix(s, 16).expect("invalid network magic"));
let db_dir = Path::new(m.value_of("db_dir").unwrap_or("./db")); let db_dir = Path::new(m.value_of("db_dir").unwrap_or("./db"));
let db_path = db_dir.join(network_name); let db_path = db_dir.join(network_name);
@ -332,6 +401,8 @@ impl Config {
Network::Regtest => 18443, Network::Regtest => 18443,
#[cfg(not(feature = "liquid"))] #[cfg(not(feature = "liquid"))]
Network::Signet => 38332, Network::Signet => 38332,
#[cfg(not(feature = "liquid"))]
Network::Testnet4 => 48332,
#[cfg(feature = "liquid")] #[cfg(feature = "liquid")]
Network::Liquid => 7041, Network::Liquid => 7041,
@ -344,6 +415,8 @@ impl Config {
#[cfg(not(feature = "liquid"))] #[cfg(not(feature = "liquid"))]
Network::Testnet => 60001, Network::Testnet => 60001,
#[cfg(not(feature = "liquid"))] #[cfg(not(feature = "liquid"))]
Network::Testnet4 => 40001,
#[cfg(not(feature = "liquid"))]
Network::Regtest => 60401, Network::Regtest => 60401,
#[cfg(not(feature = "liquid"))] #[cfg(not(feature = "liquid"))]
Network::Signet => 60601, Network::Signet => 60601,
@ -364,6 +437,8 @@ impl Config {
Network::Regtest => 3002, Network::Regtest => 3002,
#[cfg(not(feature = "liquid"))] #[cfg(not(feature = "liquid"))]
Network::Signet => 3003, Network::Signet => 3003,
#[cfg(not(feature = "liquid"))]
Network::Testnet4 => 3004,
#[cfg(feature = "liquid")] #[cfg(feature = "liquid")]
Network::Liquid => 3000, Network::Liquid => 3000,
@ -380,6 +455,8 @@ impl Config {
#[cfg(not(feature = "liquid"))] #[cfg(not(feature = "liquid"))]
Network::Regtest => 24224, Network::Regtest => 24224,
#[cfg(not(feature = "liquid"))] #[cfg(not(feature = "liquid"))]
Network::Testnet4 => 44224,
#[cfg(not(feature = "liquid"))]
Network::Signet => 54224, Network::Signet => 54224,
#[cfg(feature = "liquid")] #[cfg(feature = "liquid")]
@ -428,6 +505,8 @@ impl Config {
#[cfg(not(feature = "liquid"))] #[cfg(not(feature = "liquid"))]
Network::Testnet => daemon_dir.push("testnet3"), Network::Testnet => daemon_dir.push("testnet3"),
#[cfg(not(feature = "liquid"))] #[cfg(not(feature = "liquid"))]
Network::Testnet4 => daemon_dir.push("testnet4"),
#[cfg(not(feature = "liquid"))]
Network::Regtest => daemon_dir.push("regtest"), Network::Regtest => daemon_dir.push("regtest"),
#[cfg(not(feature = "liquid"))] #[cfg(not(feature = "liquid"))]
Network::Signet => daemon_dir.push("signet"), Network::Signet => daemon_dir.push("signet"),
@ -453,6 +532,8 @@ impl Config {
let electrum_public_hosts = m let electrum_public_hosts = m
.value_of("electrum_public_hosts") .value_of("electrum_public_hosts")
.map(|s| serde_json::from_str(s).expect("invalid --electrum-public-hosts")); .map(|s| serde_json::from_str(s).expect("invalid --electrum-public-hosts"));
#[cfg(not(feature = "electrum-discovery"))]
let electrum_public_hosts: Option<crate::electrum::ServerHosts> = None;
let mut log = stderrlog::new(); let mut log = stderrlog::new();
log.verbosity(m.occurrences_of("verbosity") as usize); log.verbosity(m.occurrences_of("verbosity") as usize);
@ -465,6 +546,7 @@ impl Config {
let config = Config { let config = Config {
log, log,
network_type, network_type,
magic,
db_path, db_path,
daemon_dir, daemon_dir,
blocks_dir, blocks_dir,
@ -491,9 +573,30 @@ impl Config {
"rest_default_max_mempool_txs", "rest_default_max_mempool_txs",
usize usize
), ),
rest_default_max_address_summary_txs: value_t_or_exit!(
m,
"rest_default_max_address_summary_txs",
usize
),
rest_max_mempool_page_size: value_t_or_exit!(m, "rest_max_mempool_page_size", usize), rest_max_mempool_page_size: value_t_or_exit!(m, "rest_max_mempool_page_size", usize),
rest_max_mempool_txid_page_size: value_t_or_exit!(
m,
"rest_max_mempool_txid_page_size",
usize
),
electrum_max_line_size: value_t_or_exit!(m, "electrum_max_line_size", usize),
electrum_max_subscriptions: value_t_or_exit!(m, "electrum_max_subscriptions", usize),
electrum_max_clients: value_t_or_exit!(m, "electrum_max_clients", usize),
electrum_idle_timeout: value_t_or_exit!(m, "electrum_idle_timeout", u64),
electrum_haproxy_depth: value_t_or_exit!(m, "electrum_haproxy_depth", usize),
electrum_connections_per_client: value_t_or_exit!(
m,
"electrum_connections_per_client",
usize
),
jsonrpc_import: m.is_present("jsonrpc_import"), jsonrpc_import: m.is_present("jsonrpc_import"),
light_mode: m.is_present("light_mode"), light_mode: m.is_present("light_mode"),
main_loop_delay: value_t_or_exit!(m, "main_loop_delay", u64),
address_search: m.is_present("address_search"), address_search: m.is_present("address_search"),
index_unspendables: m.is_present("index_unspendables"), index_unspendables: m.is_present("index_unspendables"),
cors: m.value_of("cors").map(|s| s.to_string()), cors: m.value_of("cors").map(|s| s.to_string()),
@ -520,7 +623,6 @@ impl Config {
#[cfg(feature = "liquid")] #[cfg(feature = "liquid")]
asset_db_path, asset_db_path,
#[cfg(feature = "electrum-discovery")]
electrum_public_hosts, electrum_public_hosts,
#[cfg(feature = "electrum-discovery")] #[cfg(feature = "electrum-discovery")]
electrum_announce: m.is_present("electrum_announce"), electrum_announce: m.is_present("electrum_announce"),

View File

@ -2,11 +2,12 @@ use std::collections::{HashMap, HashSet};
use std::io::{BufRead, BufReader, Lines, Write}; use std::io::{BufRead, BufReader, Lines, Write};
use std::net::{SocketAddr, TcpStream}; use std::net::{SocketAddr, TcpStream};
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::Duration; use std::time::Duration;
use base64; use base64;
use bitcoin::hashes::hex::{FromHex, ToHex}; use bitcoin::hashes::Hash;
use glob; use glob;
use hex; use hex;
use itertools::Itertools; use itertools::Itertools;
@ -18,6 +19,7 @@ use bitcoin::consensus::encode::{deserialize, serialize};
use elements::encode::{deserialize, serialize}; use elements::encode::{deserialize, serialize};
use crate::chain::{Block, BlockHash, BlockHeader, Network, Transaction, Txid}; use crate::chain::{Block, BlockHash, BlockHeader, Network, Transaction, Txid};
use crate::config::BITCOIND_SUBVER;
use crate::metrics::{HistogramOpts, HistogramVec, Metrics}; use crate::metrics::{HistogramOpts, HistogramVec, Metrics};
use crate::signal::Waiter; use crate::signal::Waiter;
use crate::util::HeaderList; use crate::util::HeaderList;
@ -26,14 +28,14 @@ use crate::errors::*;
fn parse_hash<T>(value: &Value) -> Result<T> fn parse_hash<T>(value: &Value) -> Result<T>
where where
T: FromHex, T: FromStr,
<T as FromStr>::Err: std::fmt::Debug,
{ {
T::from_hex( value
value .as_str()
.as_str() .chain_err(|| format!("non-string value: {}", value))?
.chain_err(|| format!("non-string value: {}", value))?, .parse::<T>()
) .map_err(|e| format!("failed to parse hash: {:?}", e).into())
.chain_err(|| format!("non-hex value: {}", value))
} }
fn header_from_value(value: Value) -> Result<BlockHeader> { fn header_from_value(value: Value) -> Result<BlockHeader> {
@ -117,6 +119,54 @@ struct NetworkInfo {
relayfee: f64, // in BTC/kB relayfee: f64, // in BTC/kB
} }
#[derive(Serialize, Deserialize, Debug)]
struct MempoolFees {
base: f64,
#[serde(rename = "effective-feerate")]
effective_feerate: f64,
#[serde(rename = "effective-includes")]
effective_includes: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct MempoolAcceptResult {
txid: String,
wtxid: String,
allowed: Option<bool>,
vsize: Option<u32>,
fees: Option<MempoolFees>,
#[serde(rename = "reject-reason")]
reject_reason: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
struct MempoolFeesSubmitPackage {
base: f64,
#[serde(rename = "effective-feerate")]
effective_feerate: Option<f64>,
#[serde(rename = "effective-includes")]
effective_includes: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct SubmitPackageResult {
package_msg: String,
#[serde(rename = "tx-results")]
tx_results: HashMap<String, TxResult>,
#[serde(rename = "replaced-transactions")]
replaced_transactions: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct TxResult {
txid: String,
#[serde(rename = "other-wtxid")]
other_wtxid: Option<String>,
vsize: Option<u32>,
fees: Option<MempoolFeesSubmitPackage>,
error: Option<String>,
}
pub trait CookieGetter: Send + Sync { pub trait CookieGetter: Send + Sync {
fn get(&self) -> Result<Vec<u8>>; fn get(&self) -> Result<Vec<u8>>;
} }
@ -264,6 +314,7 @@ pub struct Daemon {
daemon_dir: PathBuf, daemon_dir: PathBuf,
blocks_dir: PathBuf, blocks_dir: PathBuf,
network: Network, network: Network,
magic: Option<u32>,
conn: Mutex<Connection>, conn: Mutex<Connection>,
message_id: Counter, // for monotonic JSONRPC 'id' message_id: Counter, // for monotonic JSONRPC 'id'
signal: Waiter, signal: Waiter,
@ -274,12 +325,14 @@ pub struct Daemon {
} }
impl Daemon { impl Daemon {
#[allow(clippy::too_many_arguments)]
pub fn new( pub fn new(
daemon_dir: PathBuf, daemon_dir: PathBuf,
blocks_dir: PathBuf, blocks_dir: PathBuf,
daemon_rpc_addr: SocketAddr, daemon_rpc_addr: SocketAddr,
cookie_getter: Arc<dyn CookieGetter>, cookie_getter: Arc<dyn CookieGetter>,
network: Network, network: Network,
magic: Option<u32>,
signal: Waiter, signal: Waiter,
metrics: &Metrics, metrics: &Metrics,
) -> Result<Daemon> { ) -> Result<Daemon> {
@ -287,6 +340,7 @@ impl Daemon {
daemon_dir, daemon_dir,
blocks_dir, blocks_dir,
network, network,
magic,
conn: Mutex::new(Connection::new( conn: Mutex::new(Connection::new(
daemon_rpc_addr, daemon_rpc_addr,
cookie_getter, cookie_getter,
@ -311,6 +365,9 @@ impl Daemon {
network_info.subversion, network_info.subversion,
) )
} }
// Insert the subversion (/Satoshi xx.xx.xx(comment)/) string from bitcoind
_ = BITCOIND_SUBVER.set(network_info.subversion);
let blockchain_info = daemon.getblockchaininfo()?; let blockchain_info = daemon.getblockchaininfo()?;
info!("{:?}", blockchain_info); info!("{:?}", blockchain_info);
if blockchain_info.pruned { if blockchain_info.pruned {
@ -321,10 +378,10 @@ impl Daemon {
let mempool = daemon.getmempoolinfo()?; let mempool = daemon.getmempoolinfo()?;
let ibd_done = if network.is_regtest() { let ibd_done = if network.is_regtest() {
info.blocks == 0 && info.headers == 0 info.blocks == info.headers
} else { } else {
false !info.initialblockdownload.unwrap_or(false)
} || !info.initialblockdownload.unwrap_or(false); };
if mempool.loaded && ibd_done && info.blocks == info.headers { if mempool.loaded && ibd_done && info.blocks == info.headers {
break; break;
@ -347,6 +404,7 @@ impl Daemon {
daemon_dir: self.daemon_dir.clone(), daemon_dir: self.daemon_dir.clone(),
blocks_dir: self.blocks_dir.clone(), blocks_dir: self.blocks_dir.clone(),
network: self.network, network: self.network,
magic: self.magic,
conn: Mutex::new(self.conn.lock().unwrap().reconnect()?), conn: Mutex::new(self.conn.lock().unwrap().reconnect()?),
message_id: Counter::new(), message_id: Counter::new(),
signal: self.signal.clone(), signal: self.signal.clone(),
@ -367,7 +425,7 @@ impl Daemon {
} }
pub fn magic(&self) -> u32 { pub fn magic(&self) -> u32 {
self.network.magic() self.magic.unwrap_or_else(|| self.network.magic())
} }
fn call_jsonrpc(&self, method: &str, request: &Value) -> Result<Value> { fn call_jsonrpc(&self, method: &str, request: &Value) -> Result<Value> {
@ -490,7 +548,7 @@ impl Daemon {
pub fn getblockheader(&self, blockhash: &BlockHash) -> Result<BlockHeader> { pub fn getblockheader(&self, blockhash: &BlockHash) -> Result<BlockHeader> {
header_from_value(self.request( header_from_value(self.request(
"getblockheader", "getblockheader",
json!([blockhash.to_hex(), /*verbose=*/ false]), json!([blockhash.to_string(), /*verbose=*/ false]),
)?) )?)
} }
@ -509,21 +567,22 @@ impl Daemon {
} }
pub fn getblock(&self, blockhash: &BlockHash) -> Result<Block> { pub fn getblock(&self, blockhash: &BlockHash) -> Result<Block> {
let block = block_from_value( let block = block_from_value(self.request(
self.request("getblock", json!([blockhash.to_hex(), /*verbose=*/ false]))?, "getblock",
)?; json!([blockhash.to_string(), /*verbose=*/ false]),
)?)?;
assert_eq!(block.block_hash(), *blockhash); assert_eq!(block.block_hash(), *blockhash);
Ok(block) Ok(block)
} }
pub fn getblock_raw(&self, blockhash: &BlockHash, verbose: u32) -> Result<Value> { pub fn getblock_raw(&self, blockhash: &BlockHash, verbose: u32) -> Result<Value> {
self.request("getblock", json!([blockhash.to_hex(), verbose])) self.request("getblock", json!([blockhash.to_string(), verbose]))
} }
pub fn getblocks(&self, blockhashes: &[BlockHash]) -> Result<Vec<Block>> { pub fn getblocks(&self, blockhashes: &[BlockHash]) -> Result<Vec<Block>> {
let params_list: Vec<Value> = blockhashes let params_list: Vec<Value> = blockhashes
.iter() .iter()
.map(|hash| json!([hash.to_hex(), /*verbose=*/ false])) .map(|hash| json!([hash.to_string(), /*verbose=*/ false]))
.collect(); .collect();
let values = self.requests("getblock", &params_list)?; let values = self.requests("getblock", &params_list)?;
let mut blocks = vec![]; let mut blocks = vec![];
@ -536,7 +595,7 @@ impl Daemon {
pub fn gettransactions(&self, txhashes: &[&Txid]) -> Result<Vec<Transaction>> { pub fn gettransactions(&self, txhashes: &[&Txid]) -> Result<Vec<Transaction>> {
let params_list: Vec<Value> = txhashes let params_list: Vec<Value> = txhashes
.iter() .iter()
.map(|txhash| json!([txhash.to_hex(), /*verbose=*/ false])) .map(|txhash| json!([txhash.to_string(), /*verbose=*/ false]))
.collect(); .collect();
let values = self.retry_request_batch("getrawtransaction", &params_list, 0.25)?; let values = self.retry_request_batch("getrawtransaction", &params_list, 0.25)?;
let mut txs = vec![]; let mut txs = vec![];
@ -555,14 +614,14 @@ impl Daemon {
) -> Result<Value> { ) -> Result<Value> {
self.request( self.request(
"getrawtransaction", "getrawtransaction",
json!([txid.to_hex(), verbose, blockhash]), json!([txid.to_string(), verbose, blockhash]),
) )
} }
pub fn getmempooltx(&self, txhash: &Txid) -> Result<Transaction> { pub fn getmempooltx(&self, txhash: &Txid) -> Result<Transaction> {
let value = self.request( let value = self.request(
"getrawtransaction", "getrawtransaction",
json!([txhash.to_hex(), /*verbose=*/ false]), json!([txhash.to_string(), /*verbose=*/ false]),
)?; )?;
tx_from_value(value) tx_from_value(value)
} }
@ -578,8 +637,43 @@ impl Daemon {
pub fn broadcast_raw(&self, txhex: &str) -> Result<Txid> { pub fn broadcast_raw(&self, txhex: &str) -> Result<Txid> {
let txid = self.request("sendrawtransaction", json!([txhex]))?; let txid = self.request("sendrawtransaction", json!([txhex]))?;
Txid::from_hex(txid.as_str().chain_err(|| "non-string txid")?) txid.as_str()
.chain_err(|| "failed to parse txid") .chain_err(|| "non-string txid")?
.parse::<Txid>()
.map_err(|e| format!("failed to parse txid: {:?}", e).into())
}
pub fn test_mempool_accept(
&self,
txhex: Vec<String>,
maxfeerate: Option<f64>,
) -> Result<Vec<MempoolAcceptResult>> {
let params = match maxfeerate {
Some(rate) => json!([txhex, format!("{:.8}", rate)]),
None => json!([txhex]),
};
let result = self.request("testmempoolaccept", params)?;
serde_json::from_value::<Vec<MempoolAcceptResult>>(result)
.chain_err(|| "invalid testmempoolaccept reply")
}
pub fn submit_package(
&self,
txhex: Vec<String>,
maxfeerate: Option<f64>,
maxburnamount: Option<f64>,
) -> Result<SubmitPackageResult> {
let params = match (maxfeerate, maxburnamount) {
(Some(rate), Some(burn)) => {
json!([txhex, format!("{:.8}", rate), format!("{:.8}", burn)])
}
(Some(rate), None) => json!([txhex, format!("{:.8}", rate)]),
(None, Some(burn)) => json!([txhex, null, format!("{:.8}", burn)]),
(None, None) => json!([txhex]),
};
let result = self.request("submitpackage", params)?;
serde_json::from_value::<SubmitPackageResult>(result)
.chain_err(|| "invalid submitpackage reply")
} }
// Get estimated feerates for the provided confirmation targets using a batch RPC request // Get estimated feerates for the provided confirmation targets using a batch RPC request
@ -617,7 +711,7 @@ impl Daemon {
} }
fn get_all_headers(&self, tip: &BlockHash) -> Result<Vec<BlockHeader>> { fn get_all_headers(&self, tip: &BlockHash) -> Result<Vec<BlockHeader>> {
let info: Value = self.request("getblockheader", json!([tip.to_hex()]))?; let info: Value = self.request("getblockheader", json!([tip.to_string()]))?;
let tip_height = info let tip_height = info
.get("height") .get("height")
.expect("missing height") .expect("missing height")
@ -633,7 +727,7 @@ impl Daemon {
result.append(&mut headers); result.append(&mut headers);
} }
let mut blockhash = BlockHash::default(); let mut blockhash = BlockHash::all_zeros();
for header in &result { for header in &result {
assert_eq!(header.prev_blockhash, blockhash); assert_eq!(header.prev_blockhash, blockhash);
blockhash = header.block_hash(); blockhash = header.block_hash();
@ -659,7 +753,7 @@ impl Daemon {
bestblockhash, bestblockhash,
); );
let mut new_headers = vec![]; let mut new_headers = vec![];
let null_hash = BlockHash::default(); let null_hash = BlockHash::all_zeros();
let mut blockhash = *bestblockhash; let mut blockhash = *bestblockhash;
while blockhash != null_hash { while blockhash != null_hash {
if indexed_headers.header_by_blockhash(&blockhash).is_some() { if indexed_headers.header_by_blockhash(&blockhash).is_some() {

View File

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

View File

@ -183,7 +183,7 @@ impl DiscoveryManager {
.filter(|service| { .filter(|service| {
existing_services existing_services
.get(&addr) .get(&addr)
.map_or(true, |s| !s.contains(service)) .is_none_or(|s| !s.contains(service))
}) })
.map(|service| { .map(|service| {
HealthCheck::new(addr.clone(), hostname.clone(), service, Some(added_by)) HealthCheck::new(addr.clone(), hostname.clone(), service, Some(added_by))
@ -235,9 +235,9 @@ impl DiscoveryManager {
/// Run the next health check in the queue (a single one) /// Run the next health check in the queue (a single one)
fn run_health_check(&self) -> Result<()> { fn run_health_check(&self) -> Result<()> {
// abort if there are no entries in the queue, or its still too early for the next one up // abort if there are no entries in the queue, or its still too early for the next one up
if self.queue.read().unwrap().peek().map_or(true, |next| { if self.queue.read().unwrap().peek().is_none_or(|next| {
next.last_check next.last_check
.map_or(false, |t| t.elapsed() < HEALTH_CHECK_FREQ) .is_some_and(|t| t.elapsed() < HEALTH_CHECK_FREQ)
}) { }) {
return Ok(()); return Ok(());
} }
@ -337,7 +337,7 @@ impl DiscoveryManager {
self.tor_proxy self.tor_proxy
.chain_err(|| "no tor proxy configured, onion hosts are unsupported")?, .chain_err(|| "no tor proxy configured, onion hosts are unsupported")?,
); );
config = config.socks5(Some(socks)).unwrap() config = config.socks5(Some(socks))
} }
let client = Client::from_config(&server_url, config.build())?; let client = Client::from_config(&server_url, config.build())?;

View File

@ -1,8 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::TryInto; use std::convert::TryInto;
use std::fs; use std::fs;
use std::io::{BufRead, BufReader, Read, Write}; use std::io::{BufRead, BufReader, Cursor, Read, Write};
#[cfg(feature = "electrum-discovery")]
use std::net::IpAddr; use std::net::IpAddr;
use std::net::{Shutdown, SocketAddr, TcpListener, TcpStream}; use std::net::{Shutdown, SocketAddr, TcpListener, TcpStream};
use std::os::unix::fs::FileTypeExt; use std::os::unix::fs::FileTypeExt;
@ -12,10 +11,12 @@ use std::sync::atomic::AtomicBool;
use std::sync::mpsc::{Receiver, Sender}; use std::sync::mpsc::{Receiver, Sender};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::thread; use std::thread;
use std::time::{Duration, Instant};
use bitcoin::hashes::sha256d::Hash as Sha256dHash; use bitcoin::hashes::sha256d::Hash as Sha256dHash;
use error_chain::ChainedError; use error_chain::ChainedError;
use hex; use hex;
use ppp::PartialResult;
use serde_json::{from_str, Value}; use serde_json::{from_str, Value};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
@ -26,7 +27,7 @@ use elements::encode::serialize;
use crate::chain::Txid; use crate::chain::Txid;
use crate::config::{Config, VERSION_STRING}; use crate::config::{Config, VERSION_STRING};
use crate::electrum::{get_electrum_height, ProtocolVersion}; use crate::electrum::{get_electrum_height, ProtocolVersion, ServerFeatures};
use crate::errors::*; use crate::errors::*;
use crate::metrics::{Gauge, HistogramOpts, HistogramVec, MetricOpts, Metrics}; use crate::metrics::{Gauge, HistogramOpts, HistogramVec, MetricOpts, Metrics};
use crate::new_index::{Query, Utxo}; use crate::new_index::{Query, Utxo};
@ -40,7 +41,7 @@ const PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(1, 4);
const MAX_HEADERS: usize = 2016; const MAX_HEADERS: usize = 2016;
#[cfg(feature = "electrum-discovery")] #[cfg(feature = "electrum-discovery")]
use crate::electrum::{DiscoveryManager, ServerFeatures}; use crate::electrum::DiscoveryManager;
// TODO: Sha256dHash should be a generic hash-container (since script hash is single SHA256) // TODO: Sha256dHash should be a generic hash-container (since script hash is single SHA256)
fn hash_from_value(val: Option<&Value>) -> Result<Sha256dHash> { fn hash_from_value(val: Option<&Value>) -> Result<Sha256dHash> {
@ -76,6 +77,36 @@ fn bool_from_value_or(val: Option<&Value>, name: &str, default: bool) -> Result<
bool_from_value(val, name) bool_from_value(val, name)
} }
/// Extracts the source socket address from a parsed PROXY protocol v1 header.
fn proxy_v1_source(addresses: &ppp::v1::Addresses) -> Option<SocketAddr> {
match addresses {
ppp::v1::Addresses::Tcp4(ip) => Some(SocketAddr::new(
IpAddr::V4(ip.source_address),
ip.source_port,
)),
ppp::v1::Addresses::Tcp6(ip) => Some(SocketAddr::new(
IpAddr::V6(ip.source_address),
ip.source_port,
)),
ppp::v1::Addresses::Unknown => None,
}
}
/// Extracts the source socket address from a parsed PROXY protocol v2 header.
fn proxy_v2_source(addresses: &ppp::v2::Addresses) -> Option<SocketAddr> {
match addresses {
ppp::v2::Addresses::IPv4(ip) => Some(SocketAddr::new(
IpAddr::V4(ip.source_address),
ip.source_port,
)),
ppp::v2::Addresses::IPv6(ip) => Some(SocketAddr::new(
IpAddr::V6(ip.source_address),
ip.source_port,
)),
ppp::v2::Addresses::Unspecified | ppp::v2::Addresses::Unix(_) => None,
}
}
// TODO: implement caching and delta updates // TODO: implement caching and delta updates
fn get_status_hash(txs: Vec<(Txid, Option<BlockId>)>, query: &Query) -> Option<FullHash> { fn get_status_hash(txs: Vec<(Txid, Option<BlockId>)>, query: &Query) -> Option<FullHash> {
if txs.is_empty() { if txs.is_empty() {
@ -99,6 +130,21 @@ fn get_status_hash(txs: Vec<(Txid, Option<BlockId>)>, query: &Query) -> Option<F
} }
} }
#[repr(i16)]
#[derive(Clone, Copy, PartialEq, Eq)]
enum JsonRpcV2Error {
ParseError = -32700,
InvalidRequest = -32600,
MethodNotFound = -32601,
InternalError = -32603,
}
impl JsonRpcV2Error {
#[inline]
fn into_i16(self) -> i16 {
self as i16
}
}
struct Connection { struct Connection {
query: Arc<Query>, query: Arc<Query>,
last_header_entry: Option<HeaderEntry>, last_header_entry: Option<HeaderEntry>,
@ -107,18 +153,36 @@ struct Connection {
chan: SyncChannel<Message>, chan: SyncChannel<Message>,
stats: Arc<Stats>, stats: Arc<Stats>,
txs_limit: usize, txs_limit: usize,
max_line_size: usize,
max_subscriptions: usize,
idle_timeout: u64,
last_request_at: Instant,
die_please: Option<Receiver<()>>, die_please: Option<Receiver<()>>,
server_features: Arc<ServerFeatures>,
haproxy_depth: usize,
proxy_client: Option<SocketAddr>,
connections_per_client: usize,
client_counts: Arc<Mutex<HashMap<IpAddr, usize>>>,
registered_ip: Option<IpAddr>,
#[cfg(feature = "electrum-discovery")] #[cfg(feature = "electrum-discovery")]
discovery: Option<Arc<DiscoveryManager>>, discovery: Option<Arc<DiscoveryManager>>,
} }
impl Connection { impl Connection {
#[allow(clippy::too_many_arguments)]
pub fn new( pub fn new(
query: Arc<Query>, query: Arc<Query>,
stream: ConnectionStream, stream: ConnectionStream,
stats: Arc<Stats>, stats: Arc<Stats>,
txs_limit: usize, txs_limit: usize,
max_line_size: usize,
max_subscriptions: usize,
idle_timeout: u64,
die_please: Receiver<()>, die_please: Receiver<()>,
server_features: Arc<ServerFeatures>,
haproxy_depth: usize,
connections_per_client: usize,
client_counts: Arc<Mutex<HashMap<IpAddr, usize>>>,
#[cfg(feature = "electrum-discovery")] discovery: Option<Arc<DiscoveryManager>>, #[cfg(feature = "electrum-discovery")] discovery: Option<Arc<DiscoveryManager>>,
) -> Connection { ) -> Connection {
Connection { Connection {
@ -129,7 +193,17 @@ impl Connection {
chan: SyncChannel::new(10), chan: SyncChannel::new(10),
stats, stats,
txs_limit, txs_limit,
max_line_size,
max_subscriptions,
idle_timeout,
last_request_at: Instant::now(),
die_please: Some(die_please), die_please: Some(die_please),
server_features,
haproxy_depth,
proxy_client: None,
connections_per_client,
client_counts,
registered_ip: None,
#[cfg(feature = "electrum-discovery")] #[cfg(feature = "electrum-discovery")]
discovery, discovery,
} }
@ -151,13 +225,8 @@ impl Connection {
Ok(json!(self.query.config().electrum_banner.clone())) Ok(json!(self.query.config().electrum_banner.clone()))
} }
#[cfg(feature = "electrum-discovery")]
fn server_features(&self) -> Result<Value> { fn server_features(&self) -> Result<Value> {
let discovery = self Ok(json!(self.server_features.as_ref()))
.discovery
.as_ref()
.chain_err(|| "discovery is disabled")?;
Ok(json!(discovery.our_features()))
} }
fn server_donation_address(&self) -> Result<Value> { fn server_donation_address(&self) -> Result<Value> {
@ -189,7 +258,7 @@ impl Connection {
.chain_err(|| "discovery is disabled")?; .chain_err(|| "discovery is disabled")?;
let features = params let features = params
.get(0) .first()
.chain_err(|| "missing features param")? .chain_err(|| "missing features param")?
.clone(); .clone();
let features = serde_json::from_value(features).chain_err(|| "invalid features")?; let features = serde_json::from_value(features).chain_err(|| "invalid features")?;
@ -203,7 +272,7 @@ impl Connection {
} }
fn blockchain_block_header(&self, params: &[Value]) -> Result<Value> { fn blockchain_block_header(&self, params: &[Value]) -> Result<Value> {
let height = usize_from_value(params.get(0), "height")?; let height = usize_from_value(params.first(), "height")?;
let cp_height = usize_from_value_or(params.get(1), "cp_height", 0)?; let cp_height = usize_from_value_or(params.get(1), "cp_height", 0)?;
let raw_header_hex: String = self let raw_header_hex: String = self
@ -226,7 +295,7 @@ impl Connection {
} }
fn blockchain_block_headers(&self, params: &[Value]) -> Result<Value> { fn blockchain_block_headers(&self, params: &[Value]) -> Result<Value> {
let start_height = usize_from_value(params.get(0), "start_height")?; let start_height = usize_from_value(params.first(), "start_height")?;
let count = MAX_HEADERS.min(usize_from_value(params.get(1), "count")?); let count = MAX_HEADERS.min(usize_from_value(params.get(1), "count")?);
let cp_height = usize_from_value_or(params.get(2), "cp_height", 0)?; let cp_height = usize_from_value_or(params.get(2), "cp_height", 0)?;
let heights: Vec<usize> = (start_height..(start_height + count)).collect(); let heights: Vec<usize> = (start_height..(start_height + count)).collect();
@ -261,7 +330,7 @@ impl Connection {
} }
fn blockchain_estimatefee(&self, params: &[Value]) -> Result<Value> { fn blockchain_estimatefee(&self, params: &[Value]) -> Result<Value> {
let conf_target = usize_from_value(params.get(0), "blocks_count")?; let conf_target = usize_from_value(params.first(), "blocks_count")?;
let fee_rate = self let fee_rate = self
.query .query
.estimate_fee(conf_target as u16) .estimate_fee(conf_target as u16)
@ -277,7 +346,17 @@ impl Connection {
} }
fn blockchain_scripthash_subscribe(&mut self, params: &[Value]) -> Result<Value> { fn blockchain_scripthash_subscribe(&mut self, params: &[Value]) -> Result<Value> {
let script_hash = hash_from_value(params.get(0)).chain_err(|| "bad script_hash")?; let script_hash = hash_from_value(params.first()).chain_err(|| "bad script_hash")?;
// Enforce per-client subscription limit (don't count re-subscriptions to the same hash)
if !self.status_hashes.contains_key(&script_hash)
&& self.status_hashes.len() >= self.max_subscriptions
{
bail!(
"subscription limit reached ({} max per client)",
self.max_subscriptions
);
}
let history_txids = get_history(&self.query, &script_hash[..], self.txs_limit)?; let history_txids = get_history(&self.query, &script_hash[..], self.txs_limit)?;
let status_hash = get_status_hash(history_txids, &self.query) let status_hash = get_status_hash(history_txids, &self.query)
@ -293,9 +372,19 @@ impl Connection {
Ok(status_hash) Ok(status_hash)
} }
fn blockchain_scripthash_unsubscribe(&mut self, params: &[Value]) -> Result<Value> {
let script_hash = hash_from_value(params.first()).chain_err(|| "bad script_hash")?;
let removed = self.status_hashes.remove(&script_hash).is_some();
if removed {
self.stats.subscriptions.dec();
}
Ok(Value::Bool(removed))
}
#[cfg(not(feature = "liquid"))] #[cfg(not(feature = "liquid"))]
fn blockchain_scripthash_get_balance(&self, params: &[Value]) -> Result<Value> { fn blockchain_scripthash_get_balance(&self, params: &[Value]) -> Result<Value> {
let script_hash = hash_from_value(params.get(0)).chain_err(|| "bad script_hash")?; let script_hash = hash_from_value(params.first()).chain_err(|| "bad script_hash")?;
let (chain_stats, mempool_stats) = self.query.stats(&script_hash[..]); let (chain_stats, mempool_stats) = self.query.stats(&script_hash[..]);
Ok(json!({ Ok(json!({
@ -305,7 +394,7 @@ impl Connection {
} }
fn blockchain_scripthash_get_history(&self, params: &[Value]) -> Result<Value> { fn blockchain_scripthash_get_history(&self, params: &[Value]) -> Result<Value> {
let script_hash = hash_from_value(params.get(0)).chain_err(|| "bad script_hash")?; let script_hash = hash_from_value(params.first()).chain_err(|| "bad script_hash")?;
let history_txids = get_history(&self.query, &script_hash[..], self.txs_limit)?; let history_txids = get_history(&self.query, &script_hash[..], self.txs_limit)?;
Ok(json!(history_txids Ok(json!(history_txids
@ -323,7 +412,7 @@ impl Connection {
} }
fn blockchain_scripthash_listunspent(&self, params: &[Value]) -> Result<Value> { fn blockchain_scripthash_listunspent(&self, params: &[Value]) -> Result<Value> {
let script_hash = hash_from_value(params.get(0)).chain_err(|| "bad script_hash")?; let script_hash = hash_from_value(params.first()).chain_err(|| "bad script_hash")?;
let utxos = self.query.utxo(&script_hash[..])?; let utxos = self.query.utxo(&script_hash[..])?;
let to_json = |utxo: Utxo| { let to_json = |utxo: Utxo| {
@ -351,7 +440,7 @@ impl Connection {
} }
fn blockchain_transaction_broadcast(&self, params: &[Value]) -> Result<Value> { fn blockchain_transaction_broadcast(&self, params: &[Value]) -> Result<Value> {
let tx = params.get(0).chain_err(|| "missing tx")?; let tx = params.first().chain_err(|| "missing tx")?;
let tx = tx.as_str().chain_err(|| "non-string tx")?.to_string(); let tx = tx.as_str().chain_err(|| "non-string tx")?.to_string();
let txid = self.query.broadcast_raw(&tx)?; let txid = self.query.broadcast_raw(&tx)?;
if let Err(e) = self.chan.sender().try_send(Message::PeriodicUpdate) { if let Err(e) = self.chan.sender().try_send(Message::PeriodicUpdate) {
@ -361,7 +450,7 @@ impl Connection {
} }
fn blockchain_transaction_get(&self, params: &[Value]) -> Result<Value> { fn blockchain_transaction_get(&self, params: &[Value]) -> Result<Value> {
let tx_hash = Txid::from(hash_from_value(params.get(0)).chain_err(|| "bad tx_hash")?); let tx_hash = Txid::from(hash_from_value(params.first()).chain_err(|| "bad tx_hash")?);
let verbose = match params.get(1) { let verbose = match params.get(1) {
Some(value) => value.as_bool().chain_err(|| "non-bool verbose value")?, Some(value) => value.as_bool().chain_err(|| "non-bool verbose value")?,
None => false, None => false,
@ -380,7 +469,7 @@ impl Connection {
} }
fn blockchain_transaction_get_merkle(&self, params: &[Value]) -> Result<Value> { fn blockchain_transaction_get_merkle(&self, params: &[Value]) -> Result<Value> {
let txid = Txid::from(hash_from_value(params.get(0)).chain_err(|| "bad tx_hash")?); let txid = Txid::from(hash_from_value(params.first()).chain_err(|| "bad tx_hash")?);
let height = usize_from_value(params.get(1), "height")?; let height = usize_from_value(params.get(1), "height")?;
let blockid = self let blockid = self
.query .query
@ -399,7 +488,7 @@ impl Connection {
} }
fn blockchain_transaction_id_from_pos(&self, params: &[Value]) -> Result<Value> { fn blockchain_transaction_id_from_pos(&self, params: &[Value]) -> Result<Value> {
let height = usize_from_value(params.get(0), "height")?; let height = usize_from_value(params.first(), "height")?;
let tx_pos = usize_from_value(params.get(1), "tx_pos")?; let tx_pos = usize_from_value(params.get(1), "tx_pos")?;
let want_merkle = bool_from_value_or(params.get(2), "merkle", false)?; let want_merkle = bool_from_value_or(params.get(2), "merkle", false)?;
@ -431,6 +520,7 @@ impl Connection {
"blockchain.scripthash.get_history" => self.blockchain_scripthash_get_history(params), "blockchain.scripthash.get_history" => self.blockchain_scripthash_get_history(params),
"blockchain.scripthash.listunspent" => self.blockchain_scripthash_listunspent(params), "blockchain.scripthash.listunspent" => self.blockchain_scripthash_listunspent(params),
"blockchain.scripthash.subscribe" => self.blockchain_scripthash_subscribe(params), "blockchain.scripthash.subscribe" => self.blockchain_scripthash_subscribe(params),
"blockchain.scripthash.unsubscribe" => self.blockchain_scripthash_unsubscribe(params),
"blockchain.transaction.broadcast" => self.blockchain_transaction_broadcast(params), "blockchain.transaction.broadcast" => self.blockchain_transaction_broadcast(params),
"blockchain.transaction.get" => self.blockchain_transaction_get(params), "blockchain.transaction.get" => self.blockchain_transaction_get(params),
"blockchain.transaction.get_merkle" => self.blockchain_transaction_get_merkle(params), "blockchain.transaction.get_merkle" => self.blockchain_transaction_get_merkle(params),
@ -441,13 +531,19 @@ impl Connection {
"server.peers.subscribe" => self.server_peers_subscribe(), "server.peers.subscribe" => self.server_peers_subscribe(),
"server.ping" => Ok(Value::Null), "server.ping" => Ok(Value::Null),
"server.version" => self.server_version(), "server.version" => self.server_version(),
#[cfg(feature = "electrum-discovery")]
"server.features" => self.server_features(), "server.features" => self.server_features(),
#[cfg(feature = "electrum-discovery")] #[cfg(feature = "electrum-discovery")]
"server.add_peer" => self.server_add_peer(params), "server.add_peer" => self.server_add_peer(params),
&_ => bail!("unknown method {} {:?}", method, params), &_ => {
warn!("rpc unknown method #{} {} {:?}", id, method, params);
return Ok(json_rpc_error(
format!("Method {method} not found"),
Some(id),
JsonRpcV2Error::MethodNotFound,
));
}
}; };
timer.observe_duration(); timer.observe_duration();
// TODO: return application errors should be sent to the client // TODO: return application errors should be sent to the client
@ -461,7 +557,7 @@ impl Connection {
params, params,
e.display_chain() e.display_chain()
); );
json!({"jsonrpc": "2.0", "id": id, "error": format!("{}", e)}) json_rpc_error(e, Some(id), JsonRpcV2Error::InternalError)
} }
}) })
} }
@ -512,27 +608,101 @@ impl Connection {
Ok(()) Ok(())
} }
fn close_idle_connection(&mut self, idle_for: Duration) {
info!(
"[{}] closing idle connection after {} seconds without requests (timeout: {} seconds)",
self.client_string(),
idle_for.as_secs(),
self.idle_timeout,
);
self.chan.close();
}
/// A human-readable identifier for the connected client, preferring the
/// HAProxy-reported address (when present) over the direct peer address.
fn client_string(&self) -> String {
match self.proxy_client {
Some(addr) => format!("{} via {}", addr, self.stream.addr_string()),
None => self.stream.addr_string(),
}
}
/// Resolves the PROXY-protocol parse result into the client address at the
/// configured `electrum-haproxy-depth` layer. A depth of 0, a missing PROXY
/// header, or a non-existent layer all leave the client unidentified.
fn set_proxy_client(&mut self, addresses: Option<Vec<SocketAddr>>) {
self.proxy_client = match (self.haproxy_depth, addresses) {
(0, _) | (_, None) => None,
(depth, Some(addrs)) => addrs.get(depth - 1).copied(),
};
}
/// Registers this connection against its client key (the HAProxy-reported IP
/// when available, otherwise the direct peer IP) and enforces the
/// `electrum-connections-per-client` limit. Returns an error if the limit has
/// already been reached, in which case the connection must be closed.
fn register_client(&mut self) -> Result<()> {
if self.connections_per_client == 0 {
// Per-client limit disabled.
return Ok(());
}
let key = match self
.proxy_client
.map(|addr| addr.ip())
.or_else(|| self.stream.direct_ip())
{
Some(key) => key,
// No usable client key (e.g. a unix socket with no PROXY header).
None => return Ok(()),
};
let mut counts = self.client_counts.lock().unwrap();
let count = counts.entry(key).or_insert(0);
if *count >= self.connections_per_client {
bail!(
"too many connections from client {} ({} max per client)",
key,
self.connections_per_client
);
}
*count += 1;
self.registered_ip = Some(key);
Ok(())
}
/// Releases this connection's slot in the per-client connection counter.
fn unregister_client(&mut self) {
if let Some(key) = self.registered_ip.take() {
let mut counts = self.client_counts.lock().unwrap();
if let Some(count) = counts.get_mut(&key) {
*count -= 1;
if *count == 0 {
counts.remove(&key);
}
}
}
}
fn handle_replies(&mut self, shutdown: crossbeam_channel::Receiver<()>) -> Result<()> { fn handle_replies(&mut self, shutdown: crossbeam_channel::Receiver<()>) -> Result<()> {
let empty_params = json!([]); let idle_timeout = Duration::from_secs(self.idle_timeout);
loop { loop {
let elapsed = self.last_request_at.elapsed();
if elapsed > idle_timeout {
self.close_idle_connection(elapsed);
return Ok(());
}
let remaining = idle_timeout.saturating_sub(elapsed);
let idle_deadline = crossbeam_channel::after(remaining);
crossbeam_channel::select! { crossbeam_channel::select! {
recv(self.chan.receiver()) -> msg => { recv(self.chan.receiver()) -> msg => {
let msg = msg.chain_err(|| "channel closed")?; let msg = msg.chain_err(|| "channel closed")?;
trace!("RPC {:?}", msg); trace!("RPC {:?}", msg);
match msg { match msg {
Message::Request(line) => { Message::Request(line) => {
let cmd: Value = from_str(&line).chain_err(|| "invalid JSON format")?; self.last_request_at = Instant::now();
let reply = match ( let result = self.handle_line(&line);
cmd.get("method"), self.send_values(&[result])?
cmd.get("params").unwrap_or(&empty_params),
cmd.get("id"),
) {
(Some(Value::String(method)), Value::Array(params), Some(id)) => {
self.handle_command(method, params, id)?
}
_ => bail!("invalid command: {}", cmd),
};
self.send_values(&[reply])?
} }
Message::PeriodicUpdate => { Message::PeriodicUpdate => {
let values = self let values = self
@ -544,28 +714,201 @@ impl Connection {
self.chan.close(); self.chan.close();
return Ok(()); return Ok(());
} }
Message::Proxy(addresses) => {
self.set_proxy_client(addresses);
if let Err(e) = self.register_client() {
info!("[{}] {}", self.client_string(), e);
self.chan.close();
return Ok(());
}
}
} }
} }
recv(shutdown) -> _ => { recv(shutdown) -> _ => {
self.chan.close(); self.chan.close();
return Ok(()); return Ok(());
} }
recv(idle_deadline) -> _ => {
let idle_for = self.last_request_at.elapsed();
self.close_idle_connection(idle_for);
return Ok(());
}
} }
} }
} }
#[inline]
fn handle_line(&mut self, line: &String) -> Value {
if let Ok(json_value) = from_str(line) {
match json_value {
Value::Array(mut arr) => {
for cmd in &mut arr {
// Replace each cmd with its response in-memory.
*cmd = self.handle_value(cmd);
}
Value::Array(arr)
}
cmd => self.handle_value(&cmd),
}
} else {
// serde_json was unable to parse
json_rpc_error(
format!("Invalid JSON: {line}"),
None,
JsonRpcV2Error::ParseError,
)
}
}
#[inline]
fn handle_value(&mut self, value: &Value) -> Value {
match (
value.get("method"),
value.get("params").unwrap_or(&json!([])),
value.get("id"),
) {
(Some(Value::String(method)), Value::Array(params), Some(id)) => self
.handle_command(method, params, id)
.unwrap_or_else(|err| {
json_rpc_error(
format!("{method} RPC error: {err}"),
Some(id),
JsonRpcV2Error::InternalError,
)
}),
(_, _, Some(id)) => json_rpc_error(value, Some(id), JsonRpcV2Error::InvalidRequest),
_ => json_rpc_error(value, None, JsonRpcV2Error::InvalidRequest),
}
}
/// Reads and parses any PROXY-protocol (HAProxy) headers found at the very
/// start of the connection. Returns the source address reported by each
/// proxy layer (outermost first), or `None` if no PROXY header was present,
/// together with any bytes that were read past the header(s) and belong to
/// the Electrum request stream.
fn read_proxy_headers(
stream: &mut ConnectionStream,
) -> Result<(Option<Vec<SocketAddr>>, Vec<u8>)> {
// Upper bound on how much we are willing to buffer while looking for
// PROXY headers, to avoid unbounded memory use from a slow/malicious peer.
const MAX_PROXY_HEADER_SIZE: usize = 4096;
enum Step {
Parsed(usize, Option<SocketAddr>),
NeedMore,
Done,
}
let mut buf: Vec<u8> = Vec::with_capacity(256);
let mut addrs: Vec<SocketAddr> = Vec::new();
let mut saw_proxy = false;
let mut chunk = [0u8; 256];
loop {
// Parse as many complete, stacked PROXY headers as the buffer allows.
let need_more = loop {
if buf.is_empty() {
break true;
}
let step = match ppp::HeaderResult::parse(&buf) {
ppp::HeaderResult::V2(Ok(header)) => {
Step::Parsed(header.len(), proxy_v2_source(&header.addresses))
}
ppp::HeaderResult::V1(Ok(header)) => {
Step::Parsed(header.header.len(), proxy_v1_source(&header.addresses))
}
other => {
if other.is_incomplete() {
Step::NeedMore
} else {
Step::Done
}
}
};
match step {
Step::Parsed(consumed, src) => {
saw_proxy = true;
if let Some(src) = src {
addrs.push(src);
}
if consumed == 0 || consumed > buf.len() {
// Defensive: never spin forever on a degenerate parse.
break false;
}
buf.drain(..consumed);
}
Step::NeedMore => break true,
Step::Done => break false,
}
};
if !need_more {
break;
}
if buf.len() > MAX_PROXY_HEADER_SIZE {
bail!(
"PROXY protocol header too large (exceeds {} bytes)",
MAX_PROXY_HEADER_SIZE
);
}
let n = stream
.read(&mut chunk)
.chain_err(|| "failed to read PROXY protocol header")?;
if n == 0 {
// EOF before another complete header; stop with what we have.
break;
}
buf.extend_from_slice(&chunk[..n]);
}
let result = if saw_proxy { Some(addrs) } else { None };
Ok((result, buf))
}
fn handle_requests( fn handle_requests(
mut reader: BufReader<ConnectionStream>, stream: ConnectionStream,
tx: crossbeam_channel::Sender<Message>, tx: crossbeam_channel::Sender<Message>,
max_line_size: usize,
) -> Result<()> { ) -> Result<()> {
let mut stream = stream;
// Consume any PROXY-protocol (HAProxy) headers at the very start of the
// connection before treating the stream as Electrum requests. We always
// consume them — even when HAProxy support is disabled
// (`electrum-haproxy-depth = 0`) — so that PROXY headers sent by an
// accidentally-misconfigured upstream are stripped instead of corrupting
// the Electrum request parser.
//
// Crucially, `read_proxy_headers` only ever buffers bytes it has already
// read from the socket: when no PROXY header is present it returns those
// bytes as `leftover` so the start of the first Electrum request is
// preserved rather than discarded.
//
// The parsed addresses are forwarded over the channel; whether they are
// actually used to identify the client is decided later based on the
// configured `electrum-haproxy-depth` (a depth of 0 ignores them).
let (proxy_addrs, leftover) = Connection::read_proxy_headers(&mut stream)?;
tx.send(Message::Proxy(proxy_addrs))
.chain_err(|| "channel closed")?;
let mut reader = BufReader::new(Cursor::new(leftover).chain(stream));
loop { loop {
let mut line = Vec::<u8>::new(); let mut line = Vec::<u8>::new();
reader // Read up to max_line_size + 1 bytes to detect oversized lines
let mut limited = (&mut reader).take((max_line_size as u64).saturating_add(1));
limited
.read_until(b'\n', &mut line) .read_until(b'\n', &mut line)
.chain_err(|| "failed to read a request")?; .chain_err(|| "failed to read a request")?;
if line.is_empty() { if line.is_empty() {
tx.send(Message::Done).chain_err(|| "channel closed")?; tx.send(Message::Done).chain_err(|| "channel closed")?;
return Ok(()); return Ok(());
} else if line.len() > max_line_size {
let _ = tx.send(Message::Done);
bail!(
"request line too large ({} bytes, max is {})",
line.len(),
max_line_size
)
} else { } else {
if line.starts_with(&[22, 3, 1]) { if line.starts_with(&[22, 3, 1]) {
// (very) naive SSL handshake detection // (very) naive SSL handshake detection
@ -587,7 +930,7 @@ impl Connection {
pub fn run(mut self) { pub fn run(mut self) {
self.stats.clients.inc(); self.stats.clients.inc();
let reader = BufReader::new(self.stream.try_clone().expect("failed to clone TcpStream")); let stream = self.stream.try_clone().expect("failed to clone TcpStream");
let tx = self.chan.sender(); let tx = self.chan.sender();
let die_please = self.die_please.take().unwrap(); let die_please = self.die_please.take().unwrap();
@ -605,11 +948,14 @@ impl Connection {
let _ = reply_killer.send(()); let _ = reply_killer.send(());
}); });
let child = spawn_thread("reader", || Connection::handle_requests(reader, tx)); let max_line_size = self.max_line_size;
let child = spawn_thread("reader", move || {
Connection::handle_requests(stream, tx, max_line_size)
});
if let Err(e) = self.handle_replies(reply_receiver) { if let Err(e) = self.handle_replies(reply_receiver) {
error!( error!(
"[{}] connection handling failed: {}", "[{}] connection handling failed: {}",
self.stream.addr_string(), self.client_string(),
e.display_chain().to_string() e.display_chain().to_string()
); );
} }
@ -617,8 +963,9 @@ impl Connection {
self.stats self.stats
.subscriptions .subscriptions
.sub(self.status_hashes.len() as i64); .sub(self.status_hashes.len() as i64);
self.unregister_client();
let addr = self.stream.addr_string(); let addr = self.client_string();
debug!("[{}] shutting down connection", addr); debug!("[{}] shutting down connection", addr);
// Drop the Arc so that the stream properly closes. // Drop the Arc so that the stream properly closes.
drop(arc_stream); drop(arc_stream);
@ -629,6 +976,25 @@ impl Connection {
} }
} }
#[inline]
fn json_rpc_error(
input: impl core::fmt::Display,
id: Option<&Value>,
code: JsonRpcV2Error,
) -> Value {
let mut ret = json!({
"error": {
"code": code.into_i16(),
"message": format!("{input}")
},
"jsonrpc": "2.0"
});
if let (Some(id), Some(obj)) = (id, ret.as_object_mut()) {
obj.insert(String::from("id"), id.clone());
}
ret
}
fn get_history( fn get_history(
query: &Query, query: &Query,
scripthash: &[u8], scripthash: &[u8],
@ -657,6 +1023,11 @@ pub enum Message {
Request(String), Request(String),
PeriodicUpdate, PeriodicUpdate,
Done, Done,
/// The result of parsing zero or more PROXY-protocol (HAProxy) headers at
/// the start of the connection. `None` means no PROXY header was present;
/// `Some(addrs)` holds the source address reported by each proxy layer,
/// outermost first.
Proxy(Option<Vec<SocketAddr>>),
} }
pub enum Notification { pub enum Notification {
@ -745,11 +1116,10 @@ impl RPC {
let notification = Channel::unbounded(); let notification = Channel::unbounded();
// Discovery is enabled when electrum-public-hosts is set let server_features = {
#[cfg(feature = "electrum-discovery")]
let discovery = config.electrum_public_hosts.clone().map(|hosts| {
use crate::chain::genesis_hash; use crate::chain::genesis_hash;
let features = ServerFeatures { let hosts = config.electrum_public_hosts.clone().unwrap_or_default();
Arc::new(ServerFeatures {
hosts, hosts,
server_version: VERSION_STRING.clone(), server_version: VERSION_STRING.clone(),
genesis_hash: genesis_hash(config.network_type), genesis_hash: genesis_hash(config.network_type),
@ -757,10 +1127,15 @@ impl RPC {
protocol_max: PROTOCOL_VERSION, protocol_max: PROTOCOL_VERSION,
hash_function: "sha256".into(), hash_function: "sha256".into(),
pruning: None, pruning: None,
}; })
};
// Discovery is enabled when electrum-public-hosts is set
#[cfg(feature = "electrum-discovery")]
let discovery = config.electrum_public_hosts.as_ref().map(|_hosts| {
let discovery = Arc::new(DiscoveryManager::new( let discovery = Arc::new(DiscoveryManager::new(
config.network_type, config.network_type,
features, server_features.as_ref().clone(),
PROTOCOL_VERSION, PROTOCOL_VERSION,
config.electrum_announce, config.electrum_announce,
config.tor_proxy, config.tor_proxy,
@ -770,12 +1145,22 @@ impl RPC {
}); });
let txs_limit = config.electrum_txs_limit; let txs_limit = config.electrum_txs_limit;
let max_line_size = config.electrum_max_line_size;
let max_subscriptions = config.electrum_max_subscriptions;
let max_clients = config.electrum_max_clients;
let idle_timeout = config.electrum_idle_timeout;
let haproxy_depth = config.electrum_haproxy_depth;
let connections_per_client = config.electrum_connections_per_client;
RPC { RPC {
notification: notification.sender(), notification: notification.sender(),
server: Some(spawn_thread("rpc", move || { server: Some(spawn_thread("rpc", move || {
let senders = let senders =
Arc::new(Mutex::new(Vec::<crossbeam_channel::Sender<Message>>::new())); Arc::new(Mutex::new(Vec::<crossbeam_channel::Sender<Message>>::new()));
// Tracks the number of live connections per client (keyed by the
// HAProxy-reported address when available, otherwise the peer IP).
let client_counts: Arc<Mutex<HashMap<IpAddr, usize>>> =
Arc::new(Mutex::new(HashMap::new()));
let acceptor_shutdown = Channel::unbounded(); let acceptor_shutdown = Channel::unbounded();
let acceptor_shutdown_sender = acceptor_shutdown.sender(); let acceptor_shutdown_sender = acceptor_shutdown.sender();
@ -787,15 +1172,39 @@ impl RPC {
acceptor_shutdown_sender, acceptor_shutdown_sender,
); );
let mut threads = HashMap::new(); let mut threads: HashMap<thread::ThreadId, (thread::JoinHandle<()>, Sender<()>)> =
HashMap::new();
let (garbage_sender, garbage_receiver) = crossbeam_channel::unbounded(); let (garbage_sender, garbage_receiver) = crossbeam_channel::unbounded();
while let Some(stream) = acceptor.receiver().recv().unwrap() { while let Some(stream) = acceptor.receiver().recv().unwrap() {
// Clean up finished threads before checking connection limit
while let Ok(id) = garbage_receiver.try_recv() {
if let Some((thread, killer)) = threads.remove(&id) {
let _ = killer.send(());
if let Err(error) = thread.join() {
error!("failed to join {:?}: {:?}", id, error);
}
}
}
// Enforce maximum connection limit
if threads.len() >= max_clients {
warn!(
"[{}] rejecting connection: max clients reached ({}/{})",
stream.addr_string(),
threads.len(),
max_clients
);
let _ = stream.shutdown(Shutdown::Both);
continue;
}
let addr = stream.addr_string(); let addr = stream.addr_string();
// explicitely scope the shadowed variables for the new thread // explicitely scope the shadowed variables for the new thread
let query = Arc::clone(&query); let query = Arc::clone(&query);
let senders = Arc::clone(&senders); let senders = Arc::clone(&senders);
let stats = Arc::clone(&stats); let stats = Arc::clone(&stats);
let client_counts = Arc::clone(&client_counts);
let garbage_sender = garbage_sender.clone(); let garbage_sender = garbage_sender.clone();
// Kill the peers properly // Kill the peers properly
@ -804,6 +1213,7 @@ impl RPC {
#[cfg(feature = "electrum-discovery")] #[cfg(feature = "electrum-discovery")]
let discovery = discovery.clone(); let discovery = discovery.clone();
let server_features = Arc::clone(&server_features);
let spawned = spawn_thread("peer", move || { let spawned = spawn_thread("peer", move || {
let addr = stream.addr_string(); let addr = stream.addr_string();
@ -813,7 +1223,14 @@ impl RPC {
stream, stream,
stats, stats,
txs_limit, txs_limit,
max_line_size,
max_subscriptions,
idle_timeout,
peace_receiver, peace_receiver,
server_features,
haproxy_depth,
connections_per_client,
client_counts,
#[cfg(feature = "electrum-discovery")] #[cfg(feature = "electrum-discovery")]
discovery, discovery,
); );
@ -826,15 +1243,6 @@ impl RPC {
trace!("[{}] spawned {:?}", addr, spawned.thread().id()); trace!("[{}] spawned {:?}", addr, spawned.thread().id());
threads.insert(spawned.thread().id(), (spawned, killer)); threads.insert(spawned.thread().id(), (spawned, killer));
while let Ok(id) = garbage_receiver.try_recv() {
if let Some((thread, killer)) = threads.remove(&id) {
trace!("[{}] joining {:?}", addr, id);
let _ = killer.send(());
if let Err(error) = thread.join() {
error!("failed to join {:?}: {:?}", id, error);
}
}
}
} }
// Drop these // Drop these
drop(acceptor); drop(acceptor);
@ -996,6 +1404,15 @@ impl ConnectionStream {
} }
} }
/// The direct peer IP address, if this is a TCP connection. Unix-socket
/// connections have no IP and return `None`.
fn direct_ip(&self) -> Option<IpAddr> {
match self {
ConnectionStream::Tcp(_, a) => Some(a.ip()),
ConnectionStream::Unix(..) => None,
}
}
fn try_clone(&self) -> std::io::Result<Self> { fn try_clone(&self) -> std::io::Result<Self> {
Ok(match self { Ok(match self {
ConnectionStream::Tcp(s, a) => ConnectionStream::Tcp(s.try_clone()?, *a), ConnectionStream::Tcp(s, a) => ConnectionStream::Tcp(s.try_clone()?, *a),

View File

@ -1,7 +1,7 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::sync::{Arc, RwLock, RwLockReadGuard}; use std::sync::{Arc, RwLock, RwLockReadGuard};
use bitcoin::hashes::{hex::FromHex, sha256, Hash}; use bitcoin::hashes::{sha256, Hash};
use elements::confidential::{Asset, Value}; use elements::confidential::{Asset, Value};
use elements::encode::{deserialize, serialize}; use elements::encode::{deserialize, serialize};
use elements::secp256k1_zkp::ZERO_TWEAK; use elements::secp256k1_zkp::ZERO_TWEAK;
@ -11,19 +11,24 @@ use crate::chain::{BNetwork, BlockHash, Network, Txid};
use crate::elements::peg::{get_pegin_data, get_pegout_data, PeginInfo, PegoutInfo}; use crate::elements::peg::{get_pegin_data, get_pegout_data, PeginInfo, PegoutInfo};
use crate::elements::registry::{AssetMeta, AssetRegistry}; use crate::elements::registry::{AssetMeta, AssetRegistry};
use crate::errors::*; use crate::errors::*;
use crate::new_index::schema::{TxHistoryInfo, TxHistoryKey, TxHistoryRow}; use crate::new_index::schema::{Operation, TxHistoryInfo, TxHistoryKey, TxHistoryRow};
use crate::new_index::{db::DBFlush, ChainQuery, DBRow, Mempool, Query}; use crate::new_index::{db::DBFlush, ChainQuery, DBRow, Mempool, Query};
use crate::util::{bincode_util, full_hash, Bytes, FullHash, TransactionStatus, TxInput}; use crate::util::{
bincode_util, full_hash, Bytes, FullHash, IsProvablyUnspendable, TransactionStatus, TxInput,
};
lazy_static! { lazy_static! {
pub static ref NATIVE_ASSET_ID: AssetId = pub static ref NATIVE_ASSET_ID: AssetId =
AssetId::from_hex("6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d") "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d"
.parse()
.unwrap(); .unwrap();
pub static ref NATIVE_ASSET_ID_TESTNET: AssetId = pub static ref NATIVE_ASSET_ID_TESTNET: AssetId =
AssetId::from_hex("144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49") "144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49"
.parse()
.unwrap(); .unwrap();
pub static ref NATIVE_ASSET_ID_REGTEST: AssetId = pub static ref NATIVE_ASSET_ID_REGTEST: AssetId =
AssetId::from_hex("5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225") "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225"
.parse()
.unwrap(); .unwrap();
} }
@ -33,6 +38,7 @@ fn parse_asset_id(sl: &[u8]) -> AssetId {
#[derive(Serialize)] #[derive(Serialize)]
#[serde(untagged)] #[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum LiquidAsset { pub enum LiquidAsset {
Issued(IssuedAsset), Issued(IssuedAsset),
Native(PeggedAsset), Native(PeggedAsset),
@ -71,9 +77,9 @@ pub struct IssuedAsset {
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct AssetRow { pub struct AssetRow {
pub issuance_txid: FullHash, pub issuance_txid: FullHash,
pub issuance_vin: u16, pub issuance_vin: u32,
pub prev_txid: FullHash, pub prev_txid: FullHash,
pub prev_vout: u16, pub prev_vout: u32,
pub issuance: Bytes, // bincode does not like dealing with AssetIssuance, deserialization fails with "invalid type: sequence, expected a struct" pub issuance: Bytes, // bincode does not like dealing with AssetIssuance, deserialization fails with "invalid type: sequence, expected a struct"
pub reissuance_token: FullHash, pub reissuance_token: FullHash,
} }
@ -92,7 +98,7 @@ impl IssuedAsset {
let reissuance_token = parse_asset_id(&asset.reissuance_token); let reissuance_token = parse_asset_id(&asset.reissuance_token);
let contract_hash = if issuance.asset_entropy != [0u8; 32] { let contract_hash = if issuance.asset_entropy != [0u8; 32] {
Some(ContractHash::from_inner(issuance.asset_entropy)) Some(ContractHash::from_byte_array(issuance.asset_entropy))
} else { } else {
None None
}; };
@ -105,7 +111,7 @@ impl IssuedAsset {
}, },
issuance_prevout: OutPoint { issuance_prevout: OutPoint {
txid: deserialize(&asset.prev_txid).unwrap(), txid: deserialize(&asset.prev_txid).unwrap(),
vout: asset.prev_vout as u32, vout: asset.prev_vout,
}, },
contract_hash, contract_hash,
reissuance_token, reissuance_token,
@ -155,7 +161,7 @@ impl LiquidAsset {
#[cfg_attr(test, derive(PartialEq, Eq))] #[cfg_attr(test, derive(PartialEq, Eq))]
pub struct IssuingInfo { pub struct IssuingInfo {
pub txid: FullHash, pub txid: FullHash,
pub vin: u16, pub vin: u32,
pub is_reissuance: bool, pub is_reissuance: bool,
// None for blinded issuances // None for blinded issuances
pub issued_amount: Option<u64>, pub issued_amount: Option<u64>,
@ -166,7 +172,7 @@ pub struct IssuingInfo {
#[cfg_attr(test, derive(PartialEq, Eq))] #[cfg_attr(test, derive(PartialEq, Eq))]
pub struct BurningInfo { pub struct BurningInfo {
pub txid: FullHash, pub txid: FullHash,
pub vout: u16, pub vout: u32,
pub value: u64, pub value: u64,
} }
@ -174,17 +180,22 @@ pub struct BurningInfo {
pub fn index_confirmed_tx_assets( pub fn index_confirmed_tx_assets(
tx: &Transaction, tx: &Transaction,
confirmed_height: u32, confirmed_height: u32,
tx_position: u16,
network: Network, network: Network,
parent_network: BNetwork, parent_network: BNetwork,
rows: &mut Vec<DBRow>, rows: &mut Vec<DBRow>,
op: &Operation,
) { ) {
let (history, issuances) = index_tx_assets(tx, network, parent_network); let (history, issuances) = index_tx_assets(tx, network, parent_network);
rows.extend( rows.extend(history.into_iter().map(|(asset_id, info)| {
history.into_iter().map(|(asset_id, info)| { let history_row = asset_history_row(&asset_id, confirmed_height, tx_position, info);
asset_history_row(&asset_id, confirmed_height, info).into_row() if let Operation::DeleteBlocksWithHistory(tx) = op {
}), tx.send(history_row.key.hash)
); .expect("unbounded channel won't fail");
}
history_row.into_row()
}));
// the initial issuance is kept twice: once in the history index under I<asset><height><txid:vin>, // the initial issuance is kept twice: once in the history index under I<asset><height><txid:vin>,
// and once separately under i<asset> for asset lookup with some more associated metadata. // and once separately under i<asset> for asset lookup with some more associated metadata.
@ -205,10 +216,7 @@ pub fn index_mempool_tx_assets(
) { ) {
let (history, issuances) = index_tx_assets(tx, network, parent_network); let (history, issuances) = index_tx_assets(tx, network, parent_network);
for (asset_id, info) in history { for (asset_id, info) in history {
asset_history asset_history.entry(asset_id).or_default().push(info);
.entry(asset_id)
.or_insert_with(Vec::new)
.push(info);
} }
for (asset_id, issuance) in issuances { for (asset_id, issuance) in issuances {
asset_issuance.insert(asset_id, issuance); asset_issuance.insert(asset_id, issuance);
@ -251,18 +259,18 @@ fn index_tx_assets(
pegout.asset.explicit().unwrap(), pegout.asset.explicit().unwrap(),
TxHistoryInfo::Pegout(PegoutInfo { TxHistoryInfo::Pegout(PegoutInfo {
txid, txid,
vout: txo_index as u16, vout: txo_index as u32,
value: pegout.value, value: pegout.value,
}), }),
)); ));
} else if txo.script_pubkey.is_provably_unspendable() && !txo.is_fee() { } else if txo.script_pubkey.is_provably_unspendable_() && !txo.is_fee() {
if let (Asset::Explicit(asset_id), Value::Explicit(value)) = (txo.asset, txo.value) { if let (Asset::Explicit(asset_id), Value::Explicit(value)) = (txo.asset, txo.value) {
if value > 0 { if value > 0 {
history.push(( history.push((
asset_id, asset_id,
TxHistoryInfo::Burning(BurningInfo { TxHistoryInfo::Burning(BurningInfo {
txid, txid,
vout: txo_index as u16, vout: txo_index as u32,
value, value,
}), }),
)); ));
@ -274,10 +282,10 @@ fn index_tx_assets(
for (txi_index, txi) in tx.input.iter().enumerate() { for (txi_index, txi) in tx.input.iter().enumerate() {
if let Some(pegin) = get_pegin_data(txi, network) { if let Some(pegin) = get_pegin_data(txi, network) {
history.push(( history.push((
pegin.asset.explicit().unwrap(), pegin.asset,
TxHistoryInfo::Pegin(PeginInfo { TxHistoryInfo::Pegin(PeginInfo {
txid, txid,
vin: txi_index as u16, vin: txi_index as u32,
value: pegin.value, value: pegin.value,
}), }),
)); ));
@ -302,7 +310,7 @@ fn index_tx_assets(
asset_id, asset_id,
TxHistoryInfo::Issuing(IssuingInfo { TxHistoryInfo::Issuing(IssuingInfo {
txid, txid,
vin: txi_index as u16, vin: txi_index as u32,
is_reissuance, is_reissuance,
issued_amount, issued_amount,
token_amount, token_amount,
@ -319,9 +327,9 @@ fn index_tx_assets(
asset_id, asset_id,
AssetRow { AssetRow {
issuance_txid: txid, issuance_txid: txid,
issuance_vin: txi_index as u16, issuance_vin: txi_index as u32,
prev_txid: full_hash(&txi.previous_output.txid[..]), prev_txid: full_hash(&txi.previous_output.txid[..]),
prev_vout: txi.previous_output.vout as u16, prev_vout: txi.previous_output.vout,
issuance: serialize(&txi.asset_issuance), issuance: serialize(&txi.asset_issuance),
reissuance_token: full_hash(&reissuance_token.into_inner()[..]), reissuance_token: full_hash(&reissuance_token.into_inner()[..]),
}, },
@ -336,12 +344,14 @@ fn index_tx_assets(
fn asset_history_row( fn asset_history_row(
asset_id: &AssetId, asset_id: &AssetId,
confirmed_height: u32, confirmed_height: u32,
tx_position: u16,
txinfo: TxHistoryInfo, txinfo: TxHistoryInfo,
) -> TxHistoryRow { ) -> TxHistoryRow {
let key = TxHistoryKey { let key = TxHistoryKey {
code: b'I', code: b'I',
hash: full_hash(&asset_id.into_inner()[..]), hash: full_hash(&asset_id.into_inner()[..]),
confirmed_height, confirmed_height,
tx_position,
txinfo, txinfo,
}; };
TxHistoryRow { key } TxHistoryRow { key }
@ -385,7 +395,7 @@ pub fn lookup_asset(
Ok(if let Some(row) = row { Ok(if let Some(row) = row {
let reissuance_token = parse_asset_id(&row.reissuance_token); let reissuance_token = parse_asset_id(&row.reissuance_token);
let meta = meta.map(Clone::clone).or_else(|| match registry { let meta = meta.cloned().or_else(|| match registry {
Some(AssetRegistryLock::RwLock(rwlock)) => { Some(AssetRegistryLock::RwLock(rwlock)) => {
rwlock.read().unwrap().get(asset_id).cloned() rwlock.read().unwrap().get(asset_id).cloned()
} }
@ -404,7 +414,7 @@ pub fn lookup_asset(
} }
pub fn get_issuance_entropy(txin: &TxIn) -> Result<sha256::Midstate> { pub fn get_issuance_entropy(txin: &TxIn) -> Result<sha256::Midstate> {
if !txin.has_issuance { if !txin.has_issuance() {
bail!("input has no issuance"); bail!("input has no issuance");
} }

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
#![allow(unexpected_cfgs)]
error_chain! { error_chain! {
types { types {
Error, ErrorKind, ResultExt, Result; Error, ErrorKind, ResultExt, Result;

View File

@ -5,7 +5,6 @@ use std::io;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use sysconf;
use tiny_http; use tiny_http;
pub use prometheus::{ pub use prometheus::{
@ -98,6 +97,14 @@ struct Stats {
fds: usize, fds: usize,
} }
fn get_ticks_per_second() -> Result<f64> {
// Safety: This code is taken directly from sysconf
match unsafe { libc::sysconf(libc::_SC_CLK_TCK) } {
-1 => Err("Clock Tick unsupported".into()),
ret => Ok(ret as f64),
}
}
fn parse_stats() -> Result<Stats> { fn parse_stats() -> Result<Stats> {
if cfg!(target_os = "macos") { if cfg!(target_os = "macos") {
return Ok(Stats { return Ok(Stats {
@ -109,8 +116,7 @@ fn parse_stats() -> Result<Stats> {
let value = fs::read_to_string("/proc/self/stat").chain_err(|| "failed to read stats")?; let value = fs::read_to_string("/proc/self/stat").chain_err(|| "failed to read stats")?;
let parts: Vec<&str> = value.split_whitespace().collect(); let parts: Vec<&str> = value.split_whitespace().collect();
let page_size = page_size::get() as u64; let page_size = page_size::get() as u64;
let ticks_per_second = sysconf::raw::sysconf(sysconf::raw::SysconfVariable::ScClkTck) let ticks_per_second = get_ticks_per_second().expect("failed to get _SC_CLK_TCK");
.expect("failed to get _SC_CLK_TCK") as f64;
let parse_part = |index: usize, name: &str| -> Result<u64> { let parse_part = |index: usize, name: &str| -> Result<u64> {
parts parts

View File

@ -5,7 +5,11 @@ use std::path::Path;
use crate::config::Config; use crate::config::Config;
use crate::util::{bincode_util, Bytes}; use crate::util::{bincode_util, Bytes};
static DB_VERSION: u32 = 1; /// Each version will break any running instance with a DB that has a differing version.
/// It will also break if light mode is enabled or disabled.
// 1 = Original DB (since fork from Blockstream)
// 2 = Add tx position to TxHistory rows and place Spending before Funding
static DB_VERSION: u32 = 2;
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq)]
pub struct DBRow { pub struct DBRow {
@ -19,7 +23,7 @@ pub struct ScanIterator<'a> {
done: bool, done: bool,
} }
impl<'a> Iterator for ScanIterator<'a> { impl Iterator for ScanIterator<'_> {
type Item = DBRow; type Item = DBRow;
fn next(&mut self) -> Option<DBRow> { fn next(&mut self) -> Option<DBRow> {
@ -44,7 +48,7 @@ pub struct ReverseScanIterator<'a> {
done: bool, done: bool,
} }
impl<'a> Iterator for ReverseScanIterator<'a> { impl Iterator for ReverseScanIterator<'_> {
type Item = DBRow; type Item = DBRow;
fn next(&mut self) -> Option<DBRow> { fn next(&mut self) -> Option<DBRow> {
@ -69,6 +73,67 @@ impl<'a> Iterator for ReverseScanIterator<'a> {
} }
} }
pub struct ReverseScanGroupIterator<'a> {
iters: Vec<ReverseScanIterator<'a>>,
next_rows: Vec<Option<DBRow>>,
value_offset: usize,
done: bool,
}
impl<'a> ReverseScanGroupIterator<'a> {
pub fn new(
mut iters: Vec<ReverseScanIterator<'a>>,
value_offset: usize,
) -> ReverseScanGroupIterator<'a> {
let mut next_rows: Vec<Option<DBRow>> = Vec::with_capacity(iters.len());
for iter in &mut iters {
let next = iter.next();
next_rows.push(next);
}
let done = next_rows.iter().all(|row| row.is_none());
ReverseScanGroupIterator {
iters,
next_rows,
value_offset,
done,
}
}
}
impl Iterator for ReverseScanGroupIterator<'_> {
type Item = DBRow;
fn next(&mut self) -> Option<DBRow> {
if self.done {
return None;
}
let best_index = self
.next_rows
.iter()
.enumerate()
.max_by(|(a_index, a_opt), (b_index, b_opt)| match (a_opt, b_opt) {
(None, None) => a_index.cmp(b_index),
(Some(_), None) => std::cmp::Ordering::Greater,
(None, Some(_)) => std::cmp::Ordering::Less,
(Some(a), Some(b)) => a.key[self.value_offset..].cmp(&(b.key[self.value_offset..])),
})
.map(|(index, _)| index)
.unwrap_or(0);
let best = self.next_rows[best_index].take();
self.next_rows[best_index] = self.iters.get_mut(best_index)?.next();
if self.next_rows.iter().all(|row| row.is_none()) {
self.done = true;
}
best
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct DB { pub struct DB {
db: rocksdb::DB, db: rocksdb::DB,
@ -83,7 +148,7 @@ pub enum DBFlush {
impl DB { impl DB {
pub fn open(path: &Path, config: &Config) -> DB { pub fn open(path: &Path, config: &Config) -> DB {
let db = DB { let db = DB {
db: open_raw_db(path), db: open_raw_db(path, OpenMode::ReadWrite),
}; };
db.verify_compatibility(config); db.verify_compatibility(config);
db db
@ -101,11 +166,11 @@ impl DB {
self.db.set_options(&opts).unwrap(); self.db.set_options(&opts).unwrap();
} }
pub fn raw_iterator(&self) -> rocksdb::DBRawIterator { pub fn raw_iterator(&self) -> rocksdb::DBRawIterator<'_> {
self.db.raw_iterator() self.db.raw_iterator()
} }
pub fn iter_scan(&self, prefix: &[u8]) -> ScanIterator { pub fn iter_scan(&self, prefix: &[u8]) -> ScanIterator<'_> {
ScanIterator { ScanIterator {
prefix: prefix.to_vec(), prefix: prefix.to_vec(),
iter: self.db.prefix_iterator(prefix), iter: self.db.prefix_iterator(prefix),
@ -113,7 +178,7 @@ impl DB {
} }
} }
pub fn iter_scan_from(&self, prefix: &[u8], start_at: &[u8]) -> ScanIterator { pub fn iter_scan_from(&self, prefix: &[u8], start_at: &[u8]) -> ScanIterator<'_> {
let iter = self.db.iterator(rocksdb::IteratorMode::From( let iter = self.db.iterator(rocksdb::IteratorMode::From(
start_at, start_at,
rocksdb::Direction::Forward, rocksdb::Direction::Forward,
@ -125,7 +190,7 @@ impl DB {
} }
} }
pub fn iter_scan_reverse(&self, prefix: &[u8], prefix_max: &[u8]) -> ReverseScanIterator { pub fn iter_scan_reverse(&self, prefix: &[u8], prefix_max: &[u8]) -> ReverseScanIterator<'_> {
let mut iter = self.db.raw_iterator(); let mut iter = self.db.raw_iterator();
iter.seek_for_prev(prefix_max); iter.seek_for_prev(prefix_max);
@ -136,6 +201,25 @@ impl DB {
} }
} }
pub fn iter_scan_group_reverse(
&self,
prefixes: impl Iterator<Item = (Vec<u8>, Vec<u8>)>,
value_offset: usize,
) -> ReverseScanGroupIterator<'_> {
let iters = prefixes
.map(|(prefix, prefix_max)| {
let mut iter = self.db.raw_iterator();
iter.seek_for_prev(prefix_max);
ReverseScanIterator {
prefix: prefix.to_vec(),
iter,
done: false,
}
})
.collect();
ReverseScanGroupIterator::new(iters, value_offset)
}
pub fn write(&self, mut rows: Vec<DBRow>, flush: DBFlush) { pub fn write(&self, mut rows: Vec<DBRow>, flush: DBFlush) {
debug!( debug!(
"writing {} rows to {:?}, flush={:?}", "writing {} rows to {:?}, flush={:?}",
@ -158,6 +242,15 @@ impl DB {
self.db.write_opt(batch, &opts).unwrap(); self.db.write_opt(batch, &opts).unwrap();
} }
pub fn delete(&self, keys: Vec<Vec<u8>>) {
debug!("deleting {} rows from {:?}", keys.len(), self.db);
for key in keys {
let _ = self.db.delete(key).inspect_err(|err| {
warn!("Error while deleting DB row: {err}");
});
}
}
pub fn flush(&self) { pub fn flush(&self) {
self.db.flush().unwrap(); self.db.flush().unwrap();
} }
@ -197,7 +290,17 @@ impl DB {
} }
} }
pub fn open_raw_db<T: rocksdb::ThreadMode>(path: &Path) -> rocksdb::DBWithThreadMode<T> { #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u8)]
pub enum OpenMode {
ReadOnly,
ReadWrite,
}
pub fn open_raw_db<T: rocksdb::ThreadMode>(
path: &Path,
read_mode: OpenMode,
) -> rocksdb::DBWithThreadMode<T> {
debug!("opening DB at {:?}", path); debug!("opening DB at {:?}", path);
let mut db_opts = rocksdb::Options::default(); let mut db_opts = rocksdb::Options::default();
db_opts.create_if_missing(true); db_opts.create_if_missing(true);
@ -215,5 +318,13 @@ pub fn open_raw_db<T: rocksdb::ThreadMode>(path: &Path) -> rocksdb::DBWithThread
// let mut block_opts = rocksdb::BlockBasedOptions::default(); // let mut block_opts = rocksdb::BlockBasedOptions::default();
// block_opts.set_block_size(???); // block_opts.set_block_size(???);
rocksdb::DBWithThreadMode::<T>::open(&db_opts, path).expect("failed to open RocksDB") match read_mode {
OpenMode::ReadOnly => {
rocksdb::DBWithThreadMode::<T>::open_for_read_only(&db_opts, path, false)
.expect("failed to open RocksDB (READ ONLY)")
}
OpenMode::ReadWrite => {
rocksdb::DBWithThreadMode::<T>::open(&db_opts, path).expect("failed to open RocksDB")
}
}
} }

View File

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

View File

@ -9,10 +9,10 @@ use elements::{encode::serialize, AssetId};
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::iter::FromIterator; use std::iter::FromIterator;
use std::ops::Bound::{Excluded, Unbounded}; use std::ops::Bound::{Excluded, Unbounded};
use std::sync::Arc; use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use crate::chain::{deserialize, Network, OutPoint, Transaction, TxOut, Txid}; use crate::chain::{deserialize, Network, OutPoint, Transaction, TxOut, Txid, TxidCompat};
use crate::config::Config; use crate::config::Config;
use crate::daemon::Daemon; use crate::daemon::Daemon;
use crate::errors::*; use crate::errors::*;
@ -165,7 +165,7 @@ impl Mempool {
// TODO seek directly to last seen tx without reading earlier rows // TODO seek directly to last seen tx without reading earlier rows
.skip_while(|txid| { .skip_while(|txid| {
// skip until we reach the last_seen_txid // skip until we reach the last_seen_txid
last_seen_txid.map_or(false, |last_seen_txid| last_seen_txid != txid) last_seen_txid.is_some_and(|last_seen_txid| last_seen_txid != txid)
}) })
.skip(match last_seen_txid { .skip(match last_seen_txid {
Some(_) => 1, // skip the last_seen_txid itself Some(_) => 1, // skip the last_seen_txid itself
@ -177,6 +177,49 @@ impl Mempool {
.collect() .collect()
} }
pub fn history_group(
&self,
scripthashes: &[[u8; 32]],
last_seen_txid: Option<&Txid>,
limit: usize,
) -> Vec<Transaction> {
let _timer = self
.latency
.with_label_values(&["history_group"])
.start_timer();
scripthashes
.iter()
.filter_map(|scripthash| self.history.get(&scripthash[..]))
.flat_map(|entries| entries.iter())
.map(|e| e.get_txid())
.unique()
// TODO seek directly to last seen tx without reading earlier rows
.skip_while(|txid| {
// skip until we reach the last_seen_txid
last_seen_txid.is_some_and(|last_seen_txid| last_seen_txid != txid)
})
.skip(match last_seen_txid {
Some(_) => 1, // skip the last_seen_txid itself
None => 0,
})
.take(limit)
.map(|txid| self.txstore.get(&txid).expect("missing mempool tx"))
.cloned()
.collect()
}
pub fn history_txids_iter_group<'a>(
&'a self,
scripthashes: &'a [[u8; 32]],
) -> impl Iterator<Item = Txid> + 'a {
scripthashes
.iter()
.filter_map(move |scripthash| self.history.get(&scripthash[..]))
.flat_map(|entries| entries.iter())
.map(|entry| entry.get_txid())
.unique()
}
pub fn history_txids(&self, scripthash: &[u8], limit: usize) -> Vec<Txid> { pub fn history_txids(&self, scripthash: &[u8], limit: usize) -> Vec<Txid> {
let _timer = self let _timer = self
.latency .latency
@ -210,7 +253,7 @@ impl Mempool {
Some(Utxo { Some(Utxo {
txid: deserialize(&info.txid).expect("invalid txid"), txid: deserialize(&info.txid).expect("invalid txid"),
vout: info.vout as u32, vout: info.vout,
value: info.value, value: info.value,
confirmed: None, confirmed: None,
@ -288,6 +331,24 @@ impl Mempool {
self.txstore.keys().collect() self.txstore.keys().collect()
} }
// Get n txids after the given txid in the mempool
pub fn txids_page(&self, n: usize, start: Option<Txid>) -> Vec<&Txid> {
let _timer = self
.latency
.with_label_values(&["txids_page"])
.start_timer();
let start_bound = match start {
Some(txid) => Excluded(txid),
None => Unbounded,
};
self.txstore
.range((start_bound, Unbounded))
.take(n)
.map(|(k, _v)| k)
.collect()
}
// Get all txs in the mempool // Get all txs in the mempool
pub fn txs(&self) -> Vec<Transaction> { pub fn txs(&self) -> Vec<Transaction> {
let _timer = self.latency.with_label_values(&["txs"]).start_timer(); let _timer = self.latency.with_label_values(&["txs"]).start_timer();
@ -296,7 +357,7 @@ impl Mempool {
// Get n txs after the given txid in the mempool // Get n txs after the given txid in the mempool
pub fn txs_page(&self, n: usize, start: Option<Txid>) -> Vec<Transaction> { pub fn txs_page(&self, n: usize, start: Option<Txid>) -> Vec<Transaction> {
let _timer = self.latency.with_label_values(&["txs"]).start_timer(); let _timer = self.latency.with_label_values(&["txs_page"]).start_timer();
let mut page = Vec::with_capacity(n); let mut page = Vec::with_capacity(n);
let start_bound = match start { let start_bound = match start {
Some(txid) => Excluded(txid), Some(txid) => Excluded(txid),
@ -325,50 +386,71 @@ impl Mempool {
&self.backlog_stats.0 &self.backlog_stats.0
} }
pub fn update(&mut self, daemon: &Daemon) -> Result<()> { pub fn unique_txids(&self) -> HashSet<Txid> {
let _timer = self.latency.with_label_values(&["update"]).start_timer(); HashSet::from_iter(self.txstore.keys().cloned())
let new_txids = daemon }
pub fn update(mempool: &RwLock<Mempool>, daemon: &Daemon) -> Result<()> {
// 1. Start the metrics timer and get the current mempool txids
// [LOCK] Takes read lock for whole scope.
let (_timer, old_txids) = {
let mempool = mempool.read().unwrap();
(
mempool.latency.with_label_values(&["update"]).start_timer(),
mempool.unique_txids(),
)
};
// 2. Get all the mempool txids from the RPC.
// [LOCK] No lock taken. Wait for RPC request. Get lists of remove/add txes.
let all_txids = daemon
.getmempooltxids() .getmempooltxids()
.chain_err(|| "failed to update mempool from daemon")?; .chain_err(|| "failed to update mempool from daemon")?;
let old_txids = HashSet::from_iter(self.txstore.keys().cloned()); let txids_to_remove: HashSet<&Txid> = old_txids.difference(&all_txids).collect();
let to_remove: HashSet<&Txid> = old_txids.difference(&new_txids).collect(); let txids_to_add: Vec<&Txid> = all_txids.difference(&old_txids).collect();
// Download and add new transactions from bitcoind's mempool // 3. Remove missing transactions. Even if we are unable to download new transactions from
let txids: Vec<&Txid> = new_txids.difference(&old_txids).collect(); // the daemon, we still want to remove the transactions that are no longer in the mempool.
let to_add = match daemon.gettransactions(&txids) { // [LOCK] Write lock is released at the end of the call to remove().
Ok(txs) => txs, mempool.write().unwrap().remove(txids_to_remove);
Err(err) => {
warn!("failed to get {} transactions: {}", txids.len(), err); // e.g. new block or RBF
return Ok(()); // keep the mempool until next update()
}
};
// Add new transactions
if to_add.len() > self.add(to_add) {
debug!("Mempool update added less transactions than expected");
}
// Remove missing transactions
self.remove(to_remove);
self.count // 4. Download the new transactions from the daemon's mempool
.with_label_values(&["txs"]) // [LOCK] No lock taken, waiting for RPC response.
.set(self.txstore.len() as f64); let txs_to_add = daemon
.gettransactions(&txids_to_add)
.chain_err(|| format!("failed to get {} transactions", txids_to_add.len()))?;
// Update cached backlog stats (if expired) // 4. Update local mempool to match daemon's state
if self.backlog_stats.1.elapsed() // [LOCK] Takes Write lock for whole scope.
> Duration::from_secs(self.config.mempool_backlog_stats_ttl)
{ {
let _timer = self let mut mempool = mempool.write().unwrap();
.latency // Add new transactions
.with_label_values(&["update_backlog_stats"]) if txs_to_add.len() > mempool.add(txs_to_add) {
.start_timer(); debug!("Mempool update added less transactions than expected");
self.backlog_stats = (BacklogStats::new(&self.feeinfo), Instant::now()); }
}
Ok(()) 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 _timer = mempool
.latency
.with_label_values(&["update_backlog_stats"])
.start_timer();
mempool.backlog_stats = (BacklogStats::new(&mempool.feeinfo), Instant::now());
}
Ok(())
}
} }
pub fn add_by_txid(&mut self, daemon: &Daemon, txid: &Txid) -> Result<()> { pub fn add_by_txid(&mut self, daemon: &Daemon, txid: &Txid) -> Result<()> {
if self.txstore.get(txid).is_none() { if !self.txstore.contains_key(txid) {
if let Ok(tx) = daemon.getmempooltx(txid) { if let Ok(tx) = daemon.getmempooltx(txid) {
if self.add(vec![tx]) == 0 { if self.add(vec![tx]) == 0 {
return Err(format!( return Err(format!(
@ -399,9 +481,13 @@ impl Mempool {
let mut txids = Vec::with_capacity(txs.len()); let mut txids = Vec::with_capacity(txs.len());
// Phase 1: add to txstore // Phase 1: add to txstore
for tx in txs { for tx in txs {
let txid = tx.txid(); let txid = tx.get_txid();
txids.push(txid); // Only push if it doesn't already exist.
self.txstore.insert(txid, tx); // This is important now that update doesn't lock during
// the entire function body.
if self.txstore.insert(txid, tx).is_none() {
txids.push(txid);
}
} }
// Phase 2: index history and spend edges (some txos can be missing) // Phase 2: index history and spend edges (some txos can be missing)
@ -440,7 +526,10 @@ impl Mempool {
fee: feeinfo.fee, fee: feeinfo.fee,
vsize: feeinfo.vsize, vsize: feeinfo.vsize,
#[cfg(not(feature = "liquid"))] #[cfg(not(feature = "liquid"))]
value: prevouts.values().map(|prevout| prevout.value).sum(), value: prevouts
.values()
.map(|prevout| prevout.value.to_sat())
.sum(),
}); });
self.feeinfo.insert(txid, feeinfo); self.feeinfo.insert(txid, feeinfo);
@ -448,14 +537,18 @@ impl Mempool {
// An iterator over (ScriptHash, TxHistoryInfo) // An iterator over (ScriptHash, TxHistoryInfo)
let spending = prevouts.into_iter().map(|(input_index, prevout)| { let spending = prevouts.into_iter().map(|(input_index, prevout)| {
let txi = tx.input.get(input_index as usize).unwrap(); let txi = tx.input.get(input_index as usize).unwrap();
#[cfg(not(feature = "liquid"))]
let value = prevout.value.to_sat();
#[cfg(feature = "liquid")]
let value = prevout.value;
( (
compute_script_hash(&prevout.script_pubkey), compute_script_hash(&prevout.script_pubkey),
TxHistoryInfo::Spending(SpendingInfo { TxHistoryInfo::Spending(SpendingInfo {
txid: txid_bytes, txid: txid_bytes,
vin: input_index as u16, vin: input_index,
prev_txid: full_hash(&txi.previous_output.txid[..]), prev_txid: full_hash(&txi.previous_output.txid[..]),
prev_vout: txi.previous_output.vout as u16, prev_vout: txi.previous_output.vout,
value: prevout.value, value,
}), }),
) )
}); });
@ -469,22 +562,23 @@ impl Mempool {
.enumerate() .enumerate()
.filter(|(_, txo)| is_spendable(txo) || config.index_unspendables) .filter(|(_, txo)| is_spendable(txo) || config.index_unspendables)
.map(|(index, txo)| { .map(|(index, txo)| {
#[cfg(not(feature = "liquid"))]
let value = txo.value.to_sat();
#[cfg(feature = "liquid")]
let value = txo.value;
( (
compute_script_hash(&txo.script_pubkey), compute_script_hash(&txo.script_pubkey),
TxHistoryInfo::Funding(FundingInfo { TxHistoryInfo::Funding(FundingInfo {
txid: txid_bytes, txid: txid_bytes,
vout: index as u16, vout: index as u32,
value: txo.value, value,
}), }),
) )
}); });
// Index funding/spending history entries and spend edges // Index funding/spending history entries and spend edges
for (scripthash, entry) in funding.chain(spending) { for (scripthash, entry) in funding.chain(spending) {
self.history self.history.entry(scripthash).or_default().push(entry);
.entry(scripthash)
.or_insert_with(Vec::new)
.push(entry);
} }
for (i, txi) in tx.input.iter().enumerate() { for (i, txi) in tx.input.iter().enumerate() {
self.edges.insert(txi.previous_output, (txid, i as u32)); self.edges.insert(txi.previous_output, (txid, i as u32));

View File

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

View File

@ -4,9 +4,9 @@ use std::collections::{BTreeSet, HashMap};
use std::sync::{Arc, RwLock, RwLockReadGuard}; use std::sync::{Arc, RwLock, RwLockReadGuard};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use crate::chain::{Network, OutPoint, Transaction, TxOut, Txid}; use crate::chain::{Network, OutPoint, Transaction, TxOut, Txid, TxidCompat};
use crate::config::Config; use crate::config::Config;
use crate::daemon::Daemon; use crate::daemon::{Daemon, MempoolAcceptResult, SubmitPackageResult};
use crate::errors::*; use crate::errors::*;
use crate::new_index::{ChainQuery, Mempool, ScriptStats, SpendingInput, Utxo}; use crate::new_index::{ChainQuery, Mempool, ScriptStats, SpendingInput, Utxo};
use crate::util::{is_spendable, BlockId, Bytes, TransactionStatus}; use crate::util::{is_spendable, BlockId, Bytes, TransactionStatus};
@ -14,7 +14,7 @@ use crate::util::{is_spendable, BlockId, Bytes, TransactionStatus};
#[cfg(feature = "liquid")] #[cfg(feature = "liquid")]
use crate::{ use crate::{
chain::{asset::AssetRegistryLock, AssetId}, chain::{asset::AssetRegistryLock, AssetId},
elements::{lookup_asset, AssetRegistry, AssetSorting, LiquidAsset}, elements::{lookup_asset, AssetMeta, AssetRegistry, AssetSorting, LiquidAsset},
}; };
const FEE_ESTIMATES_TTL: u64 = 60; // seconds const FEE_ESTIMATES_TTL: u64 = 60; // seconds
@ -65,7 +65,7 @@ impl Query {
self.config.network_type self.config.network_type
} }
pub fn mempool(&self) -> RwLockReadGuard<Mempool> { pub fn mempool(&self) -> RwLockReadGuard<'_, Mempool> {
self.mempool.read().unwrap() self.mempool.read().unwrap()
} }
@ -87,6 +87,23 @@ impl Query {
Ok(txid) Ok(txid)
} }
pub fn test_mempool_accept(
&self,
txhex: Vec<String>,
maxfeerate: Option<f64>,
) -> Result<Vec<MempoolAcceptResult>> {
self.daemon.test_mempool_accept(txhex, maxfeerate)
}
pub fn submit_package(
&self,
txhex: Vec<String>,
maxfeerate: Option<f64>,
maxburnamount: Option<f64>,
) -> Result<SubmitPackageResult> {
self.daemon.submit_package(txhex, maxfeerate, maxburnamount)
}
pub fn utxo(&self, scripthash: &[u8]) -> Result<Vec<Utxo>> { pub fn utxo(&self, scripthash: &[u8]) -> Result<Vec<Utxo>> {
let mut utxos = self.chain.utxo( let mut utxos = self.chain.utxo(
scripthash, scripthash,
@ -144,7 +161,7 @@ impl Query {
} }
pub fn lookup_tx_spends(&self, tx: Transaction) -> Vec<Option<SpendingInput>> { pub fn lookup_tx_spends(&self, tx: Transaction) -> Vec<Option<SpendingInput>> {
let txid = tx.txid(); let txid = tx.get_txid();
tx.output tx.output
.par_iter() .par_iter()
@ -254,6 +271,15 @@ impl Query {
) )
} }
#[cfg(feature = "liquid")]
pub fn lookup_registry_asset(&self, asset_id: &AssetId) -> Result<Option<AssetMeta>> {
let asset_db = self
.asset_db
.as_ref()
.chain_err(|| "asset registry unavailable")?;
Ok(asset_db.read().unwrap().get(asset_id).cloned())
}
#[cfg(feature = "liquid")] #[cfg(feature = "liquid")]
pub fn list_registry_assets( pub fn list_registry_assets(
&self, &self,
@ -281,4 +307,27 @@ impl Query {
.collect::<Result<Vec<_>>>()?; .collect::<Result<Vec<_>>>()?;
Ok((total_num, results)) Ok((total_num, results))
} }
#[cfg(feature = "liquid")]
pub fn search_registry_assets<T, F>(
&self,
search: &str,
limit: usize,
mut map: F,
) -> Result<Vec<T>>
where
F: FnMut(&AssetId, &AssetMeta) -> T,
{
let asset_db = self
.asset_db
.as_ref()
.chain_err(|| "asset registry unavailable")?;
Ok(asset_db
.read()
.unwrap()
.search(search, limit)
.into_iter()
.map(|(asset_id, metadata)| map(asset_id, metadata))
.collect())
}
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -8,7 +8,9 @@ pub mod fees;
pub use self::block::{BlockHeaderMeta, BlockId, BlockMeta, BlockStatus, HeaderEntry, HeaderList}; pub use self::block::{BlockHeaderMeta, BlockId, BlockMeta, BlockStatus, HeaderEntry, HeaderList};
pub use self::fees::get_tx_fee; pub use self::fees::get_tx_fee;
pub use self::script::{get_innerscripts, ScriptToAddr, ScriptToAsm}; pub use self::script::{
get_innerscripts, IsProvablyUnspendable, ScriptToAddr, ScriptToAsm, SegwitDetection,
};
pub use self::transaction::{ pub use self::transaction::{
extract_tx_prevouts, has_prevout, is_coinbase, is_spendable, serialize_outpoint, extract_tx_prevouts, has_prevout, is_coinbase, is_spendable, serialize_outpoint,
sigops::transaction_sigop_count, TransactionStatus, TxInput, sigops::transaction_sigop_count, TransactionStatus, TxInput,
@ -194,12 +196,12 @@ pub fn create_socket(addr: &SocketAddr) -> Socket {
/// ///
/// Copied from https://github.com/rust-bitcoin/rust-bitcoincore-rpc/blob/master/json/src/lib.rs /// Copied from https://github.com/rust-bitcoin/rust-bitcoincore-rpc/blob/master/json/src/lib.rs
pub mod serde_hex { pub mod serde_hex {
use bitcoin::hashes::hex::{FromHex, ToHex}; use bitcoin::hashes::hex::FromHex;
use serde::de::Error; use serde::de::Error;
use serde::{Deserializer, Serializer}; use serde::{Deserializer, Serializer};
pub fn serialize<S: Serializer>(b: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> { pub fn serialize<S: Serializer>(b: &[u8], s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&b.to_hex()) s.serialize_str(&hex::encode(b))
} }
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> { pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
@ -208,14 +210,14 @@ pub mod serde_hex {
} }
pub mod opt { pub mod opt {
use bitcoin::hashes::hex::{FromHex, ToHex}; use bitcoin::hashes::hex::FromHex;
use serde::de::Error; use serde::de::Error;
use serde::{Deserializer, Serializer}; use serde::{Deserializer, Serializer};
pub fn serialize<S: Serializer>(b: &Option<Vec<u8>>, s: S) -> Result<S::Ok, S::Error> { pub fn serialize<S: Serializer>(b: &Option<Vec<u8>>, s: S) -> Result<S::Ok, S::Error> {
match *b { match *b {
None => s.serialize_none(), None => s.serialize_none(),
Some(ref b) => s.serialize_str(&b.to_hex()), Some(ref b) => s.serialize_str(&hex::encode(b)),
} }
} }

View File

@ -9,6 +9,87 @@ pub struct InnerScripts {
pub witness_script: Option<Script>, pub witness_script: Option<Script>,
} }
pub trait IsProvablyUnspendable {
fn is_provably_unspendable_(&self) -> bool;
}
#[cfg(not(feature = "liquid"))]
impl IsProvablyUnspendable for bitcoin::Script {
// is_provably_unspendable() is deprecated in rust-bitcoin
// so we re-implement it here. Copy pasted.
fn is_provably_unspendable_(&self) -> bool {
use bitcoin::blockdata::opcodes::{
Class::{IllegalOp, ReturnOp},
ClassifyContext, Opcode,
};
match self.as_bytes().first() {
Some(b) => {
let first = Opcode::from(*b);
let class = first.classify(ClassifyContext::Legacy);
class == ReturnOp || class == IllegalOp
}
None => false,
}
}
}
#[cfg(feature = "liquid")]
impl IsProvablyUnspendable for elements::Script {
#[inline(always)]
fn is_provably_unspendable_(&self) -> bool {
// Not deprecated yet
self.is_provably_unspendable()
}
}
// Extension trait for segwit script detection that works across bitcoin and elements
pub trait SegwitDetection {
fn segwit_is_p2wpkh(&self) -> bool;
fn segwit_is_p2wsh(&self) -> bool;
fn segwit_is_p2tr(&self) -> bool;
}
#[cfg(not(feature = "liquid"))]
impl SegwitDetection for bitcoin::Script {
fn segwit_is_p2wpkh(&self) -> bool {
self.is_p2wpkh()
}
fn segwit_is_p2wsh(&self) -> bool {
self.is_p2wsh()
}
fn segwit_is_p2tr(&self) -> bool {
self.is_p2tr()
}
}
#[cfg(not(feature = "liquid"))]
impl SegwitDetection for bitcoin::ScriptBuf {
fn segwit_is_p2wpkh(&self) -> bool {
self.is_p2wpkh()
}
fn segwit_is_p2wsh(&self) -> bool {
self.is_p2wsh()
}
fn segwit_is_p2tr(&self) -> bool {
self.is_p2tr()
}
}
#[cfg(feature = "liquid")]
impl SegwitDetection for elements::Script {
fn segwit_is_p2wpkh(&self) -> bool {
self.is_v0_p2wpkh()
}
fn segwit_is_p2wsh(&self) -> bool {
self.is_v0_p2wsh()
}
fn segwit_is_p2tr(&self) -> bool {
self.is_v1_p2tr()
}
}
pub trait ScriptToAsm: std::fmt::Debug { pub trait ScriptToAsm: std::fmt::Debug {
fn to_asm(&self) -> String { fn to_asm(&self) -> String {
let asm = format!("{:?}", self); let asm = format!("{:?}", self);
@ -16,6 +97,7 @@ pub trait ScriptToAsm: std::fmt::Debug {
} }
} }
impl ScriptToAsm for bitcoin::Script {} impl ScriptToAsm for bitcoin::Script {}
impl ScriptToAsm for bitcoin::ScriptBuf {}
#[cfg(feature = "liquid")] #[cfg(feature = "liquid")]
impl ScriptToAsm for elements::Script {} impl ScriptToAsm for elements::Script {}
@ -25,7 +107,9 @@ pub trait ScriptToAddr {
#[cfg(not(feature = "liquid"))] #[cfg(not(feature = "liquid"))]
impl ScriptToAddr for bitcoin::Script { impl ScriptToAddr for bitcoin::Script {
fn to_address_str(&self, network: Network) -> Option<String> { fn to_address_str(&self, network: Network) -> Option<String> {
bitcoin::Address::from_script(self, network.into()).map(|s| s.to_string()) bitcoin::Address::from_script(self, bitcoin::Network::from(network))
.ok()
.map(|s| s.to_string())
} }
} }
#[cfg(feature = "liquid")] #[cfg(feature = "liquid")]
@ -41,7 +125,11 @@ pub fn get_innerscripts(txin: &TxIn, prevout: &TxOut) -> InnerScripts {
// Wrapped redeemScript for P2SH spends // Wrapped redeemScript for P2SH spends
let redeem_script = if prevout.script_pubkey.is_p2sh() { let redeem_script = if prevout.script_pubkey.is_p2sh() {
if let Some(Ok(PushBytes(redeemscript))) = txin.script_sig.instructions().last() { if let Some(Ok(PushBytes(redeemscript))) = txin.script_sig.instructions().last() {
Some(Script::from(redeemscript.to_vec())) #[cfg(not(feature = "liquid"))]
let bytes = redeemscript.as_bytes().to_vec();
#[cfg(feature = "liquid")]
let bytes = redeemscript.to_vec();
Some(Script::from(bytes))
} else { } else {
None None
} }
@ -50,9 +138,9 @@ pub fn get_innerscripts(txin: &TxIn, prevout: &TxOut) -> InnerScripts {
}; };
// Wrapped witnessScript for P2WSH or P2SH-P2WSH spends // Wrapped witnessScript for P2WSH or P2SH-P2WSH spends
let witness_script = if prevout.script_pubkey.is_v0_p2wsh() let witness_script = if prevout.script_pubkey.segwit_is_p2wsh()
|| prevout.script_pubkey.is_v1_p2tr() || prevout.script_pubkey.segwit_is_p2tr()
|| redeem_script.as_ref().map_or(false, |s| s.is_v0_p2wsh()) || redeem_script.as_ref().is_some_and(|s| s.segwit_is_p2wsh())
{ {
let witness = &txin.witness; let witness = &txin.witness;
#[cfg(feature = "liquid")] #[cfg(feature = "liquid")]
@ -64,7 +152,7 @@ pub fn get_innerscripts(txin: &TxIn, prevout: &TxOut) -> InnerScripts {
#[cfg(feature = "liquid")] #[cfg(feature = "liquid")]
let wit_to_vec = Clone::clone; let wit_to_vec = Clone::clone;
let inner_script_slice = if prevout.script_pubkey.is_v1_p2tr() { let inner_script_slice = if prevout.script_pubkey.segwit_is_p2tr() {
// Witness stack is potentially very large // Witness stack is potentially very large
// so we avoid to_vec() or iter().collect() for performance // so we avoid to_vec() or iter().collect() for performance
let w_len = witness.len(); let w_len = witness.len();

View File

@ -1,18 +1,19 @@
use crate::chain::{BlockHash, OutPoint, Transaction, TxIn, TxOut, Txid}; use crate::chain::{BlockHash, OutPoint, Transaction, TxIn, TxOut, Txid};
use crate::errors; use crate::errors;
use crate::util::BlockId; use crate::util::{BlockId, IsProvablyUnspendable};
use std::collections::HashMap; use std::collections::HashMap;
#[cfg(feature = "liquid")]
use bitcoin::hashes::hex::FromHex;
#[cfg(feature = "liquid")] #[cfg(feature = "liquid")]
lazy_static! { lazy_static! {
static ref REGTEST_INITIAL_ISSUANCE_PREVOUT: Txid = static ref REGTEST_INITIAL_ISSUANCE_PREVOUT: Txid =
Txid::from_hex("50cdc410c9d0d61eeacc531f52d2c70af741da33af127c364e52ac1ee7c030a5").unwrap(); "50cdc410c9d0d61eeacc531f52d2c70af741da33af127c364e52ac1ee7c030a5"
.parse()
.unwrap();
static ref TESTNET_INITIAL_ISSUANCE_PREVOUT: Txid = static ref TESTNET_INITIAL_ISSUANCE_PREVOUT: Txid =
Txid::from_hex("0c52d2526a5c9f00e9fb74afd15dd3caaf17c823159a514f929ae25193a43a52").unwrap(); "0c52d2526a5c9f00e9fb74afd15dd3caaf17c823159a514f929ae25193a43a52"
.parse()
.unwrap();
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -48,7 +49,7 @@ impl From<Option<BlockId>> for TransactionStatus {
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct TxInput { pub struct TxInput {
pub txid: Txid, pub txid: Txid,
pub vin: u16, pub vin: u32,
} }
pub fn is_coinbase(txin: &TxIn) -> bool { pub fn is_coinbase(txin: &TxIn) -> bool {
@ -70,9 +71,9 @@ pub fn has_prevout(txin: &TxIn) -> bool {
pub fn is_spendable(txout: &TxOut) -> bool { pub fn is_spendable(txout: &TxOut) -> bool {
#[cfg(not(feature = "liquid"))] #[cfg(not(feature = "liquid"))]
return !txout.script_pubkey.is_provably_unspendable(); return !txout.script_pubkey.is_provably_unspendable_();
#[cfg(feature = "liquid")] #[cfg(feature = "liquid")]
return !txout.is_fee() && !txout.script_pubkey.is_provably_unspendable(); return !txout.is_fee() && !txout.script_pubkey.is_provably_unspendable_();
} }
/// Extract the previous TxOuts of a Transaction's TxIns /// Extract the previous TxOuts of a Transaction's TxIns
@ -117,14 +118,14 @@ where
pub(super) mod sigops { pub(super) mod sigops {
use crate::chain::{ use crate::chain::{
hashes::hex::FromHex, opcodes::all::{OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY, OP_CHECKSIG, OP_CHECKSIGVERIFY},
opcodes::{
all::{OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY, OP_CHECKSIG, OP_CHECKSIGVERIFY},
All,
},
script::{self, Instruction}, script::{self, Instruction},
Transaction, TxOut, Witness, Transaction, TxOut, Witness,
}; };
#[cfg(not(feature = "liquid"))]
use bitcoin::opcodes::Opcode;
#[cfg(feature = "liquid")]
use elements::opcodes::All as Opcode;
use std::collections::HashMap; use std::collections::HashMap;
/// Get sigop count for transaction. prevout_map must have all the prevouts. /// Get sigop count for transaction. prevout_map must have all the prevouts.
@ -136,11 +137,11 @@ pub(super) mod sigops {
let mut prevouts = Vec::with_capacity(input_count); let mut prevouts = Vec::with_capacity(input_count);
#[cfg(not(feature = "liquid"))] #[cfg(not(feature = "liquid"))]
let is_coinbase = tx.is_coin_base(); let is_coinbase_or_pegin = tx.is_coinbase();
#[cfg(feature = "liquid")] #[cfg(feature = "liquid")]
let is_coinbase = tx.is_coinbase(); let is_coinbase_or_pegin = tx.is_coinbase() || tx.input.iter().any(|input| input.is_pegin);
if !is_coinbase { if !is_coinbase_or_pegin {
for idx in 0..input_count { for idx in 0..input_count {
prevouts.push( prevouts.push(
*prevout_map *prevout_map
@ -154,9 +155,12 @@ pub(super) mod sigops {
get_sigop_cost(tx, &prevouts, true, true) get_sigop_cost(tx, &prevouts, true, true)
} }
fn decode_pushnum(op: &All) -> Option<u8> { fn decode_pushnum(op: &Opcode) -> Option<u8> {
// 81 = OP_1, 96 = OP_16 // 81 = OP_1, 96 = OP_16
// 81 -> 1, so... 81 - 80 -> 1 // 81 -> 1, so... 81 - 80 -> 1
#[cfg(not(feature = "liquid"))]
let self_u8 = op.to_u8();
#[cfg(feature = "liquid")]
let self_u8 = op.into_u8(); let self_u8 = op.into_u8();
match self_u8 { match self_u8 {
81..=96 => Some(self_u8 - 80), 81..=96 => Some(self_u8 - 80),
@ -216,7 +220,7 @@ pub(super) mod sigops {
fn get_p2sh_sigop_count(tx: &Transaction, previous_outputs: &[&TxOut]) -> usize { fn get_p2sh_sigop_count(tx: &Transaction, previous_outputs: &[&TxOut]) -> usize {
#[cfg(not(feature = "liquid"))] #[cfg(not(feature = "liquid"))]
if tx.is_coin_base() { if tx.is_coinbase() {
return 0; return 0;
} }
#[cfg(feature = "liquid")] #[cfg(feature = "liquid")]
@ -229,9 +233,14 @@ pub(super) mod sigops {
if let Some(Ok(script::Instruction::PushBytes(redeem))) = if let Some(Ok(script::Instruction::PushBytes(redeem))) =
input.script_sig.instructions().last() input.script_sig.instructions().last()
{ {
let script = #[cfg(not(feature = "liquid"))]
script::Script::from_byte_iter(redeem.iter().map(|v| Ok(*v))).unwrap(); // I only return Ok, so it won't error let script = script::Script::from_bytes(redeem.as_bytes());
n += count_sigops(&script, true); #[cfg(feature = "liquid")]
let script = script::Script::from(redeem.to_vec());
#[allow(clippy::needless_borrow)]
{
n += count_sigops(&script, true);
}
} }
} }
} }
@ -256,6 +265,9 @@ pub(super) mod sigops {
#[inline] #[inline]
fn last_pushdata(script: &script::Script) -> Option<&[u8]> { fn last_pushdata(script: &script::Script) -> Option<&[u8]> {
match script.instructions().last() { match script.instructions().last() {
#[cfg(not(feature = "liquid"))]
Some(Ok(Instruction::PushBytes(bytes))) => Some(bytes.as_bytes()),
#[cfg(feature = "liquid")]
Some(Ok(Instruction::PushBytes(bytes))) => Some(bytes), Some(Ok(Instruction::PushBytes(bytes))) => Some(bytes),
_ => None, _ => None,
} }
@ -269,20 +281,36 @@ pub(super) mod sigops {
) -> usize { ) -> usize {
let mut n = 0; let mut n = 0;
let script = if prevout.script_pubkey.is_witness_program() { let script_owned;
prevout.script_pubkey.clone() let script: &script::Script = if prevout.script_pubkey.is_witness_program() {
&prevout.script_pubkey
} else if prevout.script_pubkey.is_p2sh() } else if prevout.script_pubkey.is_p2sh()
&& is_push_only(script_sig) && is_push_only(script_sig)
&& !script_sig.is_empty() && !script_sig.is_empty()
{ {
script::Script::from_byte_iter( #[cfg(not(feature = "liquid"))]
last_pushdata(script_sig).unwrap().iter().map(|v| Ok(*v)), {
) script_owned =
.unwrap() script::ScriptBuf::from(last_pushdata(script_sig).unwrap().to_vec());
}
#[cfg(feature = "liquid")]
{
script_owned =
script::Script::from(last_pushdata(script_sig).unwrap().to_vec());
}
&script_owned
} else { } else {
return 0; return 0;
}; };
#[cfg(not(feature = "liquid"))]
if script.is_p2wsh() {
let bytes = script.as_bytes();
n += sig_ops(witness, bytes[0], &bytes[2..]);
} else if script.is_p2wpkh() {
n += 1;
}
#[cfg(feature = "liquid")]
if script.is_v0_p2wsh() { if script.is_v0_p2wsh() {
let bytes = script.as_bytes(); let bytes = script.as_bytes();
n += sig_ops(witness, bytes[0], &bytes[2..]); n += sig_ops(witness, bytes[0], &bytes[2..]);
@ -307,11 +335,11 @@ pub(super) mod sigops {
) -> Result<usize, script::Error> { ) -> Result<usize, script::Error> {
let mut n_sigop_cost = get_legacy_sigop_count(tx) * 4; let mut n_sigop_cost = get_legacy_sigop_count(tx) * 4;
#[cfg(not(feature = "liquid"))] #[cfg(not(feature = "liquid"))]
if tx.is_coin_base() { if tx.is_coinbase() {
return Ok(n_sigop_cost); return Ok(n_sigop_cost);
} }
#[cfg(feature = "liquid")] #[cfg(feature = "liquid")]
if tx.is_coinbase() { if tx.is_coinbase() || tx.input.iter().any(|input| input.is_pegin) {
return Ok(n_sigop_cost); return Ok(n_sigop_cost);
} }
if tx.input.len() != previous_outputs.len() { if tx.input.len() != previous_outputs.len() {
@ -333,6 +361,7 @@ pub(super) mod sigops {
/// Get sigops for the Witness /// Get sigops for the Witness
/// ///
/// witness_version is the raw opcode. OP_0 is 0, OP_1 is 81, etc. /// witness_version is the raw opcode. OP_0 is 0, OP_1 is 81, etc.
#[allow(clippy::redundant_closure)]
fn sig_ops(witness: &Witness, witness_version: u8, witness_program: &[u8]) -> usize { fn sig_ops(witness: &Witness, witness_version: u8, witness_program: &[u8]) -> usize {
#[cfg(feature = "liquid")] #[cfg(feature = "liquid")]
let last_witness = witness.script_witness.last(); let last_witness = witness.script_witness.last();
@ -341,15 +370,20 @@ pub(super) mod sigops {
match (witness_version, witness_program.len()) { match (witness_version, witness_program.len()) {
(0, 20) => 1, (0, 20) => 1,
(0, 32) => { (0, 32) => {
if let Some(n) = last_witness #[cfg(not(feature = "liquid"))]
.map(|sl| sl.iter().map(|v| Ok(*v)))
.map(script::Script::from_byte_iter)
// I only return Ok 2 lines up, so there is no way to error
.map(|s| count_sigops(&s.unwrap(), true))
{ {
n #[allow(clippy::needless_borrow)]
} else { last_witness
0 .map(|sl| script::Script::from_bytes(sl))
.map(|s| count_sigops(s, true))
.unwrap_or_default()
}
#[cfg(feature = "liquid")]
{
last_witness
.map(|sl| script::Script::from(sl.clone()))
.map(|s| count_sigops(&s, true))
.unwrap_or_default()
} }
} }
_ => 0, _ => 0,

197
start
View File

@ -5,8 +5,10 @@ DAEMON=bitcoin
NETWORK=mainnet NETWORK=mainnet
FEATURES=default FEATURES=default
DB_FOLDER=/electrs DB_FOLDER=/electrs
ASSET_DB_ARGS=()
NODENAME=$(hostname|cut -d . -f1) NODENAME=$(hostname|cut -d . -f1)
LOCATION=$(hostname|cut -d . -f2) LOCATION=$(hostname|cut -d . -f2)
USAGE="Usage: $0 (mainnet|testnet|signet|liquid|liquidtestnet) [popular-scripts]"
# load rust if necessary # load rust if necessary
if [ -e "${HOME}/.cargo/env" ];then if [ -e "${HOME}/.cargo/env" ];then
@ -19,6 +21,10 @@ case "$(uname -s)" in
FreeBSD) FreeBSD)
OS=FreeBSD OS=FreeBSD
NPROC=$(sysctl -n hw.ncpu) NPROC=$(sysctl -n hw.ncpu)
export CC=/usr/local/bin/clang17
export CXX=/usr/local/bin/clang++17
export CPP=/usr/local/bin/clang-cpp17
export RUSTFLAGS="-C linker=clang17"
;; ;;
Darwin) Darwin)
OS=Darwin OS=Darwin
@ -37,30 +43,103 @@ esac
# which network? # which network?
case "${1}" in case "${1}" in
mainnet) mainnet)
THREADS=$((NPROC / 3)) THREADS=$((NPROC / 8))
CRONJOB_TIMING="20 4 * * *"
;; ;;
testnet) testnet)
NETWORK=testnet NETWORK=testnet
THREADS=$((NPROC / 6)) THREADS=$((NPROC / 8))
CRONJOB_TIMING="2 4 * * *"
;;
testnet4)
NETWORK=testnet4
THREADS=$((NPROC / 8))
CRONJOB_TIMING="17 4 * * *"
;; ;;
signet) signet)
NETWORK=signet NETWORK=signet
THREADS=$((NPROC / 6)) THREADS=$((NPROC / 8))
CRONJOB_TIMING="9 4 * * *"
;; ;;
liquid) liquid)
DAEMON=elements DAEMON=elements
NETWORK=liquid NETWORK=liquid
FEATURES=liquid FEATURES=liquid
THREADS=$((NPROC / 6)) ASSET_DB_ARGS=(--asset-db-path /elements/asset_registry_db)
THREADS=$((NPROC / 8))
CRONJOB_TIMING="12 4 * * *"
;; ;;
liquidtestnet) liquidtestnet)
DAEMON=elements DAEMON=elements
NETWORK=liquidtestnet NETWORK=liquidtestnet
FEATURES=liquid FEATURES=liquid
THREADS=$((NPROC / 6)) ASSET_DB_ARGS=(--asset-db-path /elements/asset_registry_testnet_db)
THREADS=$((NPROC / 8))
CRONJOB_TIMING="17 4 * * *"
;; ;;
*) *)
echo "Usage: $0 (mainnet|testnet|signet|liquid|liquidtestnet)" echo "${USAGE}"
exit 1
;;
esac
# Run the popular address txt file generator before each run
POPULAR_SCRIPTS_FOLDER="${HOME}/popular-scripts/${NETWORK}"
POPULAR_SCRIPTS_FILE_RAW="${POPULAR_SCRIPTS_FOLDER}/popular-scripts-raw.txt"
POPULAR_SCRIPTS_FILE="${POPULAR_SCRIPTS_FOLDER}/popular-scripts.txt"
# This function runs the job for generating the popular scripts text file for the precache arg
generate_popular_scripts() {
mkdir -p "${POPULAR_SCRIPTS_FOLDER}"
## Use nproc * 4 threads to generate the txt file (lots of iowait, so 2x~4x core count is ok)
## Only pick up addresses with 101 history events or more
## (Without lowering MIN_HISTORY_ITEMS_TO_CACHE this is the lowest we can go)
## It prints out progress to STDERR
echo "[*] Generating popular-scripts using ${THREADS} threads..."
cd "${HOME}/electrs"
HIGH_USAGE_THRESHOLD=101 \
JOB_THREAD_COUNT=${THREADS} \
nice cargo run \
--release \
--bin popular-scripts \
--features "${FEATURES}" \
-- \
--network "${NETWORK}" \
--db-dir "${DB_FOLDER}" \
> "${POPULAR_SCRIPTS_FILE_RAW}"
## Only overwrite the existing file if the popular-scripts cargo run succeeded
if [ "$?" = "0" ];then
## Sorted and deduplicated just in case
echo "Sorting popular scripts for final results..."
sort "${POPULAR_SCRIPTS_FILE_RAW}" | uniq > "${POPULAR_SCRIPTS_FILE}"
fi
rm "${POPULAR_SCRIPTS_FILE_RAW}"
}
# This function is for inserting the cronjob for generating the popular scripts
CRONJOB_CMD="\"${HOME}/electrs/start\" \"${NETWORK}\" popular-scripts"
echo "${CRONJOB_TIMING} ${CRONJOB_CMD}"
case "${2}" in
popular-scripts)
echo "[*] Only generate popular-scripts, then exit"
generate_popular_scripts
exit 0
;;
version)
echo "[*] Only print versions, then exit"
cargo run --bin electrs --release -- --version
cargo run --bin popular-scripts --release -- --version
exit 0
;;
"")
# If the 2nd arg isn't passed, just run the normal electrs script as-is
;;
*)
echo "${USAGE}"
exit 1 exit 1
;; ;;
esac esac
@ -77,6 +156,10 @@ do
# prepare run-time variables # prepare run-time variables
UTXOS_LIMIT=500 UTXOS_LIMIT=500
ELECTRUM_TXS_LIMIT=500 ELECTRUM_TXS_LIMIT=500
ELECTRUM_MAX_LINE_SIZE=1048576 # 1 MiB
ELECTRUM_MAX_SUBSCRIPTIONS=100
ELECTRUM_MAX_CLIENTS=1000
MAIN_LOOP_DELAY=500
DAEMON_CONF="${HOME}/${DAEMON}.conf" DAEMON_CONF="${HOME}/${DAEMON}.conf"
HTTP_SOCKET_FILE="${HOME}/socket/esplora-${DAEMON}-${NETWORK}" HTTP_SOCKET_FILE="${HOME}/socket/esplora-${DAEMON}-${NETWORK}"
RPC_SOCKET_FILE="${HOME}/socket/electrum-${DAEMON}-${NETWORK}" RPC_SOCKET_FILE="${HOME}/socket/electrum-${DAEMON}-${NETWORK}"
@ -90,36 +173,81 @@ do
if [ "${NODENAME}" = "node201" ];then if [ "${NODENAME}" = "node201" ];then
UTXOS_LIMIT=9000 UTXOS_LIMIT=9000
ELECTRUM_TXS_LIMIT=9000 ELECTRUM_TXS_LIMIT=9000
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
ELECTRUM_MAX_SUBSCRIPTIONS=100000
ELECTRUM_MAX_CLIENTS=10000
MAIN_LOOP_DELAY=14000
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 fi
# Run the popular address txt file generator before each run if [ ! -e "${POPULAR_SCRIPTS_FILE}" ];then
POPULAR_SCRIPTS_FOLDER="${HOME}/popular-scripts/${NETWORK}" generate_popular_scripts
POPULAR_SCRIPTS_FILE_RAW="${POPULAR_SCRIPTS_FOLDER}/popular-scripts-raw.txt" fi
POPULAR_SCRIPTS_FILE="${POPULAR_SCRIPTS_FOLDER}/popular-scripts.txt"
mkdir -p "${POPULAR_SCRIPTS_FOLDER}"
rm -f "${POPULAR_SCRIPTS_FILE_RAW}" "${POPULAR_SCRIPTS_FILE}"
## Use nproc * 4 threads to generate the txt file (lots of iowait, so 2x~4x core count is ok)
## Only pick up addresses with 101 history events or more
## (Without lowering MIN_HISTORY_ITEMS_TO_CACHE this is the lowest we can go)
## It prints out progress to STDERR
echo "[*] Generating popular-scripts using ${THREADS} threads..."
HIGH_USAGE_THRESHOLD=101 \
JOB_THREAD_COUNT=${THREADS} \
cargo run \
--release \
--bin popular-scripts \
--features "${FEATURES}" \
-- \
--network "${NETWORK}" \
--db-dir "${DB_FOLDER}" \
> "${POPULAR_SCRIPTS_FILE_RAW}"
## Sorted and deduplicated just in case
sort "${POPULAR_SCRIPTS_FILE_RAW}" | uniq > "${POPULAR_SCRIPTS_FILE}"
# Run the electrs process (Note: db-dir is used in both commands) # Run the electrs process (Note: db-dir is used in both commands)
cargo run \ nice cargo run \
--release \ --release \
--bin electrs \ --bin electrs \
--features "${FEATURES}" \ --features "${FEATURES}" \
@ -127,6 +255,8 @@ do
--network "${NETWORK}" \ --network "${NETWORK}" \
--daemon-dir "${HOME}" \ --daemon-dir "${HOME}" \
--db-dir "${DB_FOLDER}" \ --db-dir "${DB_FOLDER}" \
"${ASSET_DB_ARGS[@]}" \
--main-loop-delay "${MAIN_LOOP_DELAY}" \
--rpc-socket-file "${RPC_SOCKET_FILE}" \ --rpc-socket-file "${RPC_SOCKET_FILE}" \
--http-socket-file "${HTTP_SOCKET_FILE}" \ --http-socket-file "${HTTP_SOCKET_FILE}" \
--precache-scripts "${POPULAR_SCRIPTS_FILE}" \ --precache-scripts "${POPULAR_SCRIPTS_FILE}" \
@ -136,6 +266,9 @@ do
--address-search \ --address-search \
--utxos-limit "${UTXOS_LIMIT}" \ --utxos-limit "${UTXOS_LIMIT}" \
--electrum-txs-limit "${ELECTRUM_TXS_LIMIT}" \ --electrum-txs-limit "${ELECTRUM_TXS_LIMIT}" \
--electrum-max-line-size "${ELECTRUM_MAX_LINE_SIZE}" \
--electrum-max-subscriptions "${ELECTRUM_MAX_SUBSCRIPTIONS}" \
--electrum-max-clients "${ELECTRUM_MAX_CLIENTS}" \
-vv -vv
sleep 1 sleep 1
done done