Compare commits

...

258 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
wiz
b379d24cb1
Fix docker username environment variable in github workflow
Some checks failed
Docker build on tag / Build and push to DockerHub (push) Has been cancelled
2023-11-27 20:28:49 +09:00
softsimon
ff0ad2a565
Merge pull request #61 from mempool/knorrium/docker_image_workflow
ops: Add a Docker image builder workflow
2023-11-21 18:27:40 +09:00
softsimon
5ad6733587
Merge pull request #63 from mempool/junderw/ci-fix
Use Composite Action to reduce duplication
2023-11-15 17:44:49 +09:00
softsimon
a13c7c32ef
Merge pull request #65 from mempool/mononaut/mempool-txs-page-size
Configurable mempool txs page size
2023-11-15 16:38:59 +09:00
Mononaut
168862b6ae
Configurable mempool txs page size 2023-11-15 07:09:59 +00:00
wiz
df3fa85e87
Merge pull request #64 from mempool/mononaut/relaxed-mempool-batching
Relax error propagation for batch loading mempool txs
2023-11-13 22:49:17 +09:00
wiz
36d1f7fd7b
Merge branch 'mempool' into mononaut/relaxed-mempool-batching 2023-11-13 22:20:35 +09:00
Mononaut
9380493120
Relax error propagation for batch loading mempool txs 2023-11-13 08:29:14 +00:00
wiz
9190889eff
ops: Further reduce threads used for electrs pre-cache 2023-11-13 16:24:20 +09:00
softsimon
53894ccf47
Merge branch 'mempool' into junderw/add-metrics-rest-api 2023-11-13 16:11:32 +09:00
wiz
7ca62e4445
ops: Further reduce threads used for electrs pre-cache 2023-11-13 16:07:41 +09:00
wiz
a80637ce45
Merge pull request #39 from mempool/mononaut/bulk-outspends
Internal bulk outspend apis
2023-11-13 15:52:07 +09:00
wiz
16e99b617f
ops: Further reduce threads used for electrs pre-cache 2023-11-13 15:34:51 +09:00
wiz
33d2a0199a
Merge branch 'mempool' into mononaut/bulk-outspends 2023-11-13 15:14:46 +09:00
wiz
bc4ce1632f
ops: Use less threads for mainnet electrs pre-cache 2023-11-13 15:09:38 +09:00
wiz
294a8e1e0d
Merge pull request #41 from mempool/mononaut/block-tx-confs
Include tx confirmation status in bulk /block/txs response
2023-11-13 15:09:29 +09:00
softsimon
de99ae3971
Merge branch 'mempool' into mononaut/block-tx-confs 2023-11-13 12:30:15 +09:00
wiz
c2fc64bad2
Merge branch 'mempool' into mononaut/bulk-outspends 2023-11-12 19:11:59 +09:00
wiz
157dbf2a81
Merge pull request #33 from mempool/mononaut/bulk-txs-post-api
Add a POST /txs bulk query-by-txid endpoint
2023-11-12 19:11:46 +09:00
wiz
9ee9a5dd84
Merge branch 'mempool' into mononaut/bulk-txs-post-api 2023-11-12 17:41:27 +09:00
wiz
e28f887f39
Merge pull request #38 from mempool/mononaut/protect-internal-apis
Protect internal apis
2023-11-12 17:40:31 +09:00
Mononaut
ae9fc9301f
Fix missing conf status & fix TTL for bulk block txs endpoint 2023-11-12 06:30:17 +00:00
Mononaut
d1afa45bcb
Add internal POST bulk outspends endpoints 2023-11-12 06:22:47 +00:00
Mononaut
59be248536
Protect POST /txs bulk query-by-txid endpoint 2023-11-12 05:30:02 +00:00
Mononaut
0301e4c60f
remove unnecessary scope 2023-11-12 05:30:02 +00:00
Mononaut
0695ada669
Add a POST /txs bulk query-by-txid endpoint 2023-11-12 05:30:01 +00:00
Mononaut
281b699408
Move bulk /block/:hash/txs behind /internal prefix 2023-11-12 05:08:31 +00:00
Mononaut
bb7e1ec477
Change internal api prefix to "internal" 2023-11-12 05:08:31 +00:00
Mononaut
4591205834
Protect internal bulk API endpoints behind /internal-api prefix 2023-11-12 05:08:31 +00:00
softsimon
649f28db78
Merge pull request #54 from mempool/junderw/limit-error-fix
Fix: Make error messages clearer
2023-11-12 13:58:46 +09:00
junderw
7f828cd86d
Use Composite Action to reduce duplication 2023-11-11 00:03:38 -07:00
softsimon
9d6e266773
Merge pull request #62 from mempool/junderw/fix-readme
Update README and rename lib to mempool-electrs
2023-11-11 13:21:46 +09:00
junderw
dceb659a3d
Update README and rename lib to mempool-electrs 2023-10-14 20:35:53 -07:00
Felipe Knorr Kuhn
c5c6d17814
Fix typo 2023-10-12 09:58:02 -07:00
Felipe Knorr Kuhn
d0749f37d1
Add a Docker image builder workflow 2023-10-12 07:50:30 -07:00
junderw
a8aba23acb
Feature: Install popular-scripts as a cronjob 2023-10-09 12:12:05 -07:00
wiz
18e041c847
Merge pull request #45 from mempool/junderw/perf-precache-speed
Increase performance for precache operation
2023-10-03 14:53:49 +09:00
wiz
d49f752115
Reduce number of threads to not blow up server 2023-10-03 14:32:57 +09:00
wiz
75dc828a0a
Rewrite electrs start scripts into a single script 2023-10-03 14:23:39 +09: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
wiz
69c97c1c3f
Merge branch 'mempool' into junderw/perf-precache-speed 2023-10-03 13:21:00 +09:00
wiz
e663693199
Merge pull request #50 from mempool/junderw/http-socket-electrum
(Requires merge by @wiz) Feat: Unix sockets for Electrum RPC
2023-10-03 13:07:25 +09:00
wiz
3b19bfd970
Merge pull request #51 from mempool/junderw/fix-regtest-startup 2023-10-03 05:45:57 +09:00
junderw
ff4e4530e7
REST API blocking async: Solution A, block_in_place 2023-10-01 19:12:31 -07:00
junderw
4339a51bb9
Feat: Unix sockets for Electrum RPC 2023-09-30 06:56:29 -07:00
junderw
ab7cb3df11
Fix: Make error messages clearer 2023-09-29 23:00:50 -07:00
Felipe Knorr Kuhn
217c9a015b
Merge branch 'mempool' into junderw/fix-regtest-startup 2023-09-29 18:42:55 -07:00
wiz
09a11b1c7a
ops: Set UTXO and history limit based on hostname 2023-09-30 10:32:36 +09:00
junderw
df9dd8c475
Fix: Regtest can't start with just genesis block 2023-09-28 16:52:40 -07:00
wiz
52001bf984
Merge pull request #49 from mempool/junderw/fix-sockets
Fix: Clean up threads and sockets at the same time as the thread cleaner
2023-09-27 17:35:18 +09:00
junderw
6365cdd61e
Fix: Clean up threads and sockets at the same time as the thread cleaner 2023-09-27 01:13:38 -07:00
softsimon
4ed52c3383
Merge pull request #48 from mempool/junderw/electrum-rpc-graceful-exit
Fix Electrum RPC Hang
2023-09-25 19:59:51 +04:00
junderw
0af9038a5c
Fix: electrum server graceful shutdown doesn't work 2023-09-24 15:54:32 -07:00
softsimon
c12eef6ced
Merge pull request #44 from mempool/junderw/fix-broken-stats
Fix: Fix Stats and Utxo cache
2023-09-23 19:24:10 +04:00
junderw
3e283190af
Idea: Generate the popular scripts before startup 2023-09-18 22:36:50 -07:00
junderw
811c6c4677
Feature: Increase precache performance 2023-09-18 20:46:51 -07:00
junderw
3615c486a4
Fix: Broken StatsCache and UtxoCache 2023-09-18 19:11:14 -07:00
49 changed files with 4938 additions and 12860 deletions

View File

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

View File

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

77
.github/workflows/on-tag.yml vendored Normal file
View File

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

View File

@ -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
.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`

1133
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,16 +1,25 @@
FROM debian:bookworm-slim AS base
RUN apt update -qy
RUN apt install -qy librocksdb-dev
ENV CARGO_NET_GIT_FETCH_WITH_CLI=true
RUN apt update -qy && \
apt install -qy librocksdb-dev curl
FROM base as build
RUN apt install -qy git 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
COPY . .
RUN cargo build --release --bin electrs
ARG CARGO_EXTRA_ARGS=""
RUN cargo build --release --bin electrs ${CARGO_EXTRA_ARGS}
FROM base as deploy

View File

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

View File

@ -34,7 +34,8 @@ fn main() {
// This includes untracked files
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
} else {
format!("{}(dirty)", rev_parse.trim())

File diff suppressed because it is too large Load Diff

View File

@ -1,32 +0,0 @@
#!/usr/bin/env zsh
#source "${HOME}/.cargo/env"
#export PATH="${HOME}/.cargo/bin:${PATH}"
# don't bother making electrs.core files
ulimit -c
# get credentials from elements.conf directly
ELEMENTS_RPC_USER=$(grep 'rpcuser=' ${HOME}/elements.conf|cut -d = -f2|head -1)
ELEMENTS_RPC_PASS=$(grep 'rpcpassword=' ${HOME}/elements.conf|cut -d = -f2|head -1)
# run in loop in case of crash
until false
do
cd "${HOME}/electrs"
cargo run \
--release \
--features liquid \
--bin electrs \
-- \
--network liquid \
--http-socket-file "${HOME}/socket/esplora-liquid-mainnet" \
--precache-scripts "${HOME}/electrs/contrib/popular-scripts.txt" \
--asset-db-path "${HOME}/asset_registry_db" \
--daemon-dir "${HOME}" \
--db-dir /electrs \
--cookie "${ELEMENTS_RPC_USER}:${ELEMENTS_RPC_PASS}" \
--address-search \
--cors '*' \
-vvv
sleep 1
done

View File

@ -1,32 +0,0 @@
#!/usr/bin/env zsh
#source "${HOME}/.cargo/env"
#export PATH="${HOME}/.cargo/bin:${PATH}"
# don't bother making electrs.core files
ulimit -c
# get credentials from elements.conf directly
ELEMENTS_RPC_USER=$(grep 'rpcuser=' "${HOME}/elements.conf"|cut -d = -f2|head -1)
ELEMENTS_RPC_PASS=$(grep 'rpcpassword=' "${HOME}/elements.conf"|cut -d = -f2|head -1)
# run in loop in case of crash
until false
do
cd "${HOME}/electrs"
cargo run \
--release \
--features liquid \
--bin electrs \
-- \
--network liquidtestnet \
--http-socket-file "${HOME}/socket/esplora-liquid-testnet" \
--precache-scripts "${HOME}/electrs/contrib/popular-scripts.txt" \
--asset-db-path "${HOME}/asset_registry_testnet_db" \
--daemon-dir "${HOME}" \
--db-dir "/electrs" \
--cookie "${ELEMENTS_RPC_USER}:${ELEMENTS_RPC_PASS}" \
--cors '*' \
--address-search \
-vvv
sleep 1
done

View File

@ -1,29 +0,0 @@
#!/usr/bin/env zsh
#source "${HOME}/.cargo/env"
#export PATH="${HOME}/.cargo/bin:${PATH}"
# don't bother making electrs.core files
ulimit -c
# get credentials from bitcoin.conf directly
BITCOIN_RPC_USER=$(grep 'rpcuser=' ${HOME}/bitcoin.conf|cut -d = -f2|head -1)
BITCOIN_RPC_PASS=$(grep 'rpcpassword=' ${HOME}/bitcoin.conf|cut -d = -f2|head -1)
# run in loop in case of crash
until false
do
cd "${HOME}/electrs"
cargo run \
--release \
--bin electrs \
-- \
--http-socket-file "${HOME}/socket/esplora-bitcoin-mainnet" \
--precache-scripts "${HOME}/electrs/contrib/popular-scripts.txt" \
--daemon-dir "${HOME}" \
--db-dir "/electrs" \
--cookie "${BITCOIN_RPC_USER}:${BITCOIN_RPC_PASS}" \
--cors '*' \
--address-search \
-vvv
sleep 1
done

View File

@ -1,30 +0,0 @@
#!/usr/bin/env zsh
#source "${HOME}/.cargo/env"
#export PATH="${HOME}/.cargo/bin:${PATH}"
# don't bother making electrs.core files
ulimit -c
# get credentials from bitcoin.conf directly
BITCOIN_RPC_USER=$(grep 'rpcuser=' "${HOME}/bitcoin.conf"|cut -d = -f2|head -1)
BITCOIN_RPC_PASS=$(grep 'rpcpassword=' "${HOME}/bitcoin.conf"|cut -d = -f2|head -1)
# run in loop in case of crash
until false
do
cd "${HOME}/electrs"
cargo run \
--release \
--bin electrs \
-- \
--network signet \
--http-socket-file "${HOME}/socket/esplora-bitcoin-signet" \
--precache-scripts "${HOME}/electrs/contrib/popular-scripts.txt" \
--daemon-dir "${HOME}" \
--db-dir "/electrs" \
--cookie "${BITCOIN_RPC_USER}:${BITCOIN_RPC_PASS}" \
--cors '*' \
--address-search \
-vvv
sleep 1
done

View File

@ -1,30 +0,0 @@
#!/usr/bin/env zsh
#source "${HOME}/.cargo/env"
#export PATH="${HOME}/.cargo/bin:${PATH}"
# don't bother making electrs.core files
ulimit -c
# get credentials from bitcoin.conf directly
BITCOIN_RPC_USER=$(grep 'rpcuser=' ${HOME}/bitcoin.conf|cut -d = -f2|head -1)
BITCOIN_RPC_PASS=$(grep 'rpcpassword=' ${HOME}/bitcoin.conf|cut -d = -f2|head -1)
# run in loop in case of crash
until false
do
cd "${HOME}/electrs"
cargo run \
--release \
--bin electrs \
-- \
--network testnet \
--http-socket-file "${HOME}/socket/esplora-bitcoin-testnet" \
--precache-scripts "${HOME}/electrs/contrib/popular-scripts.txt" \
--daemon-dir "${HOME}" \
--db-dir "/electrs" \
--cookie "${BITCOIN_RPC_USER}:${BITCOIN_RPC_PASS}" \
--cors '*' \
--address-search \
-vvv
sleep 1
done

1
rust-toolchain Normal file
View File

@ -0,0 +1 @@
1.87

View File

@ -57,6 +57,8 @@ TESTNAME="Running cargo clippy check electrum-discovery + liquid"
echo "$TESTNAME"
cargo clippy $@ -q -F electrum-discovery,liquid
TESTNAME="Running cargo test with all features"
echo "$TESTNAME"
cargo test $@ -q --package electrs --lib --all-features
if [ $INCLUDE_TESTS ]; then
TESTNAME="Running cargo test with 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.cookie_getter(),
config.network_type,
config.magic,
signal.clone(),
&metrics,
)?);
@ -69,18 +70,23 @@ fn run_server(config: Arc<Config>) -> Result<()> {
&metrics,
));
if let Some(ref precache_file) = config.precache_scripts {
let precache_scripthashes = precache::scripthashes_from_file(precache_file.to_string())
.expect("cannot load scripts to precache");
precache::precache(&chain, precache_scripthashes);
}
let mempool = Arc::new(RwLock::new(Mempool::new(
Arc::clone(&chain),
&metrics,
Arc::clone(&config),
)));
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")]
let asset_db = config.asset_db_path.as_ref().map(|db_dir| {
@ -99,12 +105,36 @@ fn run_server(config: Arc<Config>) -> Result<()> {
));
// TODO: configuration for which servers to start
let rest_server = rest::start(Arc::clone(&config), Arc::clone(&query));
let rest_server = rest::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 {
let precache_scripthashes = precache::scripthashes_from_file(precache_file.to_string())
.expect("cannot load scripts to precache");
precache::precache(
Arc::clone(&chain),
precache_scripthashes,
config.precache_threads,
);
}
loop {
if let Err(err) = signal.wait(Duration::from_millis(500), true) {
if let Err(err) = signal.wait(Duration::from_millis(config.main_loop_delay), true) {
info!("stopping server: {}", err);
electrs::util::spawn_thread("shutdown-thread-checker", || {
let mut counter = 40;
let interval_ms = 500;
while counter > 0 {
electrs::util::with_spawned_threads(|threads| {
debug!("Threads during shutdown: {:?}", threads);
});
std::thread::sleep(std::time::Duration::from_millis(interval_ms));
counter -= 1;
}
});
rest_server.stop();
// the electrum server is stopped when dropped
break;
@ -118,7 +148,13 @@ fn run_server(config: Arc<Config>) -> Result<()> {
};
// 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
electrum_server.notify();
@ -133,4 +169,7 @@ fn main() {
error!("server failed: {}", e.display_chain());
process::exit(1);
}
electrs::util::with_spawned_threads(|threads| {
debug!("Threads before closing: {:?}", threads);
});
}

View File

@ -18,7 +18,10 @@ type DB = rocksdb::DBWithThreadMode<rocksdb::MultiThreaded>;
lazy_static! {
static ref HISTORY_DB: DB = {
let config = Config::from_args();
open_raw_db(&config.db_path.join("newindex").join("history"))
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' + {}",
hex::encode([first_byte])
);
// H = 72
let mut compare_vec: Vec<u8> = vec![72, first_byte];
let mut compare_vec: Vec<u8> = vec![b'H', first_byte];
iter.seek(&compare_vec); // Seek to beginning of our section
// Insert the byte of the next section for comparing
@ -122,7 +124,7 @@ fn run_iterator(
while iter.valid() {
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,
// but we need to check the final scripthash
send_if_popular(

View File

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

View File

@ -1,24 +1,65 @@
use std::str::FromStr;
#[cfg(not(feature = "liquid"))] // use regular Bitcoin data structures
pub use bitcoin::{
blockdata::{opcodes, script, witness::Witness},
address,
block::Header as BlockHeader,
blockdata::{opcodes, script},
consensus::deserialize,
hashes,
util::address,
Block, BlockHash, BlockHeader, OutPoint, Script, Transaction, TxIn, TxOut, Txid,
hashes, Block, BlockHash, OutPoint, ScriptBuf as Script, Transaction, TxIn, TxOut, Txid,
Witness,
};
#[cfg(feature = "liquid")]
pub use {
crate::elements::asset,
elements::{
address, confidential, encode::deserialize, hashes, opcodes, script, Address, AssetId,
Block, BlockHash, BlockHeader, OutPoint, Script, Transaction, TxIn, TxInWitness as Witness,
TxOut, Txid,
address, bitcoin::bech32::Hrp, confidential, encode::deserialize, hashes, opcodes, script,
Address, AssetId, Block, BlockHash, BlockHeader, OutPoint, Script, Transaction, TxIn,
TxInWitness as Witness, TxOut, Txid,
},
};
use bitcoin::blockdata::constants::genesis_block;
pub use bitcoin::network::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"))]
pub type Value = u64;
@ -32,6 +73,8 @@ pub enum Network {
#[cfg(not(feature = "liquid"))]
Testnet,
#[cfg(not(feature = "liquid"))]
Testnet4,
#[cfg(not(feature = "liquid"))]
Regtest,
#[cfg(not(feature = "liquid"))]
Signet,
@ -49,14 +92,24 @@ pub const LIQUID_TESTNET_PARAMS: address::AddressParams = address::AddressParams
p2pkh_prefix: 36,
p2sh_prefix: 19,
blinded_prefix: 23,
bech_hrp: "tex",
blech_hrp: "tlq",
bech_hrp: Hrp::parse_unchecked("tex"),
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 {
#[cfg(not(feature = "liquid"))]
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")]
@ -124,27 +177,39 @@ impl Network {
pub fn genesis_hash(network: Network) -> BlockHash {
#[cfg(not(feature = "liquid"))]
return bitcoin_genesis_hash(network.into());
return bitcoin_genesis_hash(network);
#[cfg(feature = "liquid")]
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! {
static ref BITCOIN_GENESIS: bitcoin::BlockHash =
genesis_block(BNetwork::Bitcoin).block_hash();
static ref TESTNET_GENESIS: bitcoin::BlockHash =
genesis_block(BNetwork::Testnet).block_hash();
static ref TESTNET4_GENESIS: bitcoin::BlockHash = bitcoin::BlockHash::from_str(
"00000000da84f2bafbbc53dee25a72ae507ff4914b867c565be350b0da8bf043"
)
.unwrap();
static ref REGTEST_GENESIS: bitcoin::BlockHash =
genesis_block(BNetwork::Regtest).block_hash();
static ref SIGNET_GENESIS: bitcoin::BlockHash =
genesis_block(BNetwork::Signet).block_hash();
}
#[cfg(not(feature = "liquid"))]
match network {
BNetwork::Bitcoin => *BITCOIN_GENESIS,
BNetwork::Testnet => *TESTNET_GENESIS,
BNetwork::Regtest => *REGTEST_GENESIS,
BNetwork::Signet => *SIGNET_GENESIS,
Network::Bitcoin => *BITCOIN_GENESIS,
Network::Testnet => *TESTNET_GENESIS,
Network::Testnet4 => *TESTNET4_GENESIS,
Network::Regtest => *REGTEST_GENESIS,
Network::Signet => *SIGNET_GENESIS,
}
#[cfg(feature = "liquid")]
match network {
Network::Liquid => *BITCOIN_GENESIS,
Network::LiquidTestnet => *TESTNET_GENESIS,
Network::LiquidRegtest => *REGTEST_GENESIS,
}
}
@ -155,6 +220,10 @@ pub fn liquid_genesis_hash(network: Network) -> elements::BlockHash {
"1466275836220db2944ca059a3a10ef6fd2ea684b0688d2c379296888a206003"
.parse()
.unwrap();
static ref ZERO_HASH: BlockHash =
"0000000000000000000000000000000000000000000000000000000000000000"
.parse()
.unwrap();
}
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.
// 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.
_ => Default::default(),
_ => *ZERO_HASH,
}
}
@ -174,6 +243,8 @@ impl From<&str> for Network {
#[cfg(not(feature = "liquid"))]
"testnet" => Network::Testnet,
#[cfg(not(feature = "liquid"))]
"testnet4" => Network::Testnet4,
#[cfg(not(feature = "liquid"))]
"regtest" => Network::Regtest,
#[cfg(not(feature = "liquid"))]
"signet" => Network::Signet,
@ -196,6 +267,7 @@ impl From<Network> for BNetwork {
match network {
Network::Bitcoin => BNetwork::Bitcoin,
Network::Testnet => BNetwork::Testnet,
Network::Testnet4 => BNetwork::Testnet4,
Network::Regtest => BNetwork::Regtest,
Network::Signet => BNetwork::Signet,
}
@ -208,6 +280,7 @@ impl From<BNetwork> for Network {
match network {
BNetwork::Bitcoin => Network::Bitcoin,
BNetwork::Testnet => Network::Testnet,
BNetwork::Testnet4 => Network::Testnet4,
BNetwork::Regtest => Network::Regtest,
BNetwork::Signet => Network::Signet,
}

View File

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

View File

@ -2,11 +2,12 @@ use std::collections::{HashMap, HashSet};
use std::io::{BufRead, BufReader, Lines, Write};
use std::net::{SocketAddr, TcpStream};
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use base64;
use bitcoin::hashes::hex::{FromHex, ToHex};
use bitcoin::hashes::Hash;
use glob;
use hex;
use itertools::Itertools;
@ -18,6 +19,7 @@ use bitcoin::consensus::encode::{deserialize, serialize};
use elements::encode::{deserialize, serialize};
use crate::chain::{Block, BlockHash, BlockHeader, Network, Transaction, Txid};
use crate::config::BITCOIND_SUBVER;
use crate::metrics::{HistogramOpts, HistogramVec, Metrics};
use crate::signal::Waiter;
use crate::util::HeaderList;
@ -26,14 +28,14 @@ use crate::errors::*;
fn parse_hash<T>(value: &Value) -> Result<T>
where
T: FromHex,
T: FromStr,
<T as FromStr>::Err: std::fmt::Debug,
{
T::from_hex(
value
.as_str()
.chain_err(|| format!("non-string value: {}", value))?,
)
.chain_err(|| format!("non-hex value: {}", value))
value
.as_str()
.chain_err(|| format!("non-string value: {}", value))?
.parse::<T>()
.map_err(|e| format!("failed to parse hash: {:?}", e).into())
}
fn header_from_value(value: Value) -> Result<BlockHeader> {
@ -117,6 +119,54 @@ struct NetworkInfo {
relayfee: f64, // in BTC/kB
}
#[derive(Serialize, Deserialize, Debug)]
struct MempoolFees {
base: f64,
#[serde(rename = "effective-feerate")]
effective_feerate: f64,
#[serde(rename = "effective-includes")]
effective_includes: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct MempoolAcceptResult {
txid: String,
wtxid: String,
allowed: Option<bool>,
vsize: Option<u32>,
fees: Option<MempoolFees>,
#[serde(rename = "reject-reason")]
reject_reason: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
struct MempoolFeesSubmitPackage {
base: f64,
#[serde(rename = "effective-feerate")]
effective_feerate: Option<f64>,
#[serde(rename = "effective-includes")]
effective_includes: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct SubmitPackageResult {
package_msg: String,
#[serde(rename = "tx-results")]
tx_results: HashMap<String, TxResult>,
#[serde(rename = "replaced-transactions")]
replaced_transactions: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct TxResult {
txid: String,
#[serde(rename = "other-wtxid")]
other_wtxid: Option<String>,
vsize: Option<u32>,
fees: Option<MempoolFeesSubmitPackage>,
error: Option<String>,
}
pub trait CookieGetter: Send + Sync {
fn get(&self) -> Result<Vec<u8>>;
}
@ -264,6 +314,7 @@ pub struct Daemon {
daemon_dir: PathBuf,
blocks_dir: PathBuf,
network: Network,
magic: Option<u32>,
conn: Mutex<Connection>,
message_id: Counter, // for monotonic JSONRPC 'id'
signal: Waiter,
@ -274,12 +325,14 @@ pub struct Daemon {
}
impl Daemon {
#[allow(clippy::too_many_arguments)]
pub fn new(
daemon_dir: PathBuf,
blocks_dir: PathBuf,
daemon_rpc_addr: SocketAddr,
cookie_getter: Arc<dyn CookieGetter>,
network: Network,
magic: Option<u32>,
signal: Waiter,
metrics: &Metrics,
) -> Result<Daemon> {
@ -287,6 +340,7 @@ impl Daemon {
daemon_dir,
blocks_dir,
network,
magic,
conn: Mutex::new(Connection::new(
daemon_rpc_addr,
cookie_getter,
@ -311,6 +365,9 @@ impl Daemon {
network_info.subversion,
)
}
// Insert the subversion (/Satoshi xx.xx.xx(comment)/) string from bitcoind
_ = BITCOIND_SUBVER.set(network_info.subversion);
let blockchain_info = daemon.getblockchaininfo()?;
info!("{:?}", blockchain_info);
if blockchain_info.pruned {
@ -320,10 +377,13 @@ impl Daemon {
let info = daemon.getblockchaininfo()?;
let mempool = daemon.getmempoolinfo()?;
if mempool.loaded
&& !info.initialblockdownload.unwrap_or(false)
&& info.blocks == info.headers
{
let ibd_done = if network.is_regtest() {
info.blocks == info.headers
} else {
!info.initialblockdownload.unwrap_or(false)
};
if mempool.loaded && ibd_done && info.blocks == info.headers {
break;
}
@ -344,6 +404,7 @@ impl Daemon {
daemon_dir: self.daemon_dir.clone(),
blocks_dir: self.blocks_dir.clone(),
network: self.network,
magic: self.magic,
conn: Mutex::new(self.conn.lock().unwrap().reconnect()?),
message_id: Counter::new(),
signal: self.signal.clone(),
@ -364,7 +425,7 @@ impl Daemon {
}
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> {
@ -384,19 +445,46 @@ impl Daemon {
Ok(result)
}
fn handle_request_batch(&self, method: &str, params_list: &[Value]) -> Result<Vec<Value>> {
fn handle_request_batch(
&self,
method: &str,
params_list: &[Value],
failure_threshold: f64,
) -> Result<Vec<Value>> {
let id = self.message_id.next();
let chunks = params_list
.iter()
.map(|params| json!({"method": method, "params": params, "id": id}))
.chunks(50_000); // Max Amount of batched requests
let mut results = vec![];
let total_requests = params_list.len();
let mut failed_requests: u64 = 0;
let threshold = (failure_threshold * total_requests as f64).round() as u64;
let mut n = 0;
for chunk in &chunks {
let reqs = chunk.collect();
let mut replies = self.call_jsonrpc(method, &reqs)?;
if let Some(replies_vec) = replies.as_array_mut() {
for reply in replies_vec {
results.push(parse_jsonrpc_reply(reply.take(), method, id)?)
n += 1;
match parse_jsonrpc_reply(reply.take(), method, id) {
Ok(parsed_reply) => results.push(parsed_reply),
Err(e) => {
failed_requests += 1;
warn!(
"batch request {} {}/{} failed: {}",
method,
n,
total_requests,
e.to_string()
);
// abort and return the last error once a threshold number of requests have failed
if failed_requests > threshold {
return Err(e);
}
}
}
}
} else {
bail!("non-array replies: {:?}", replies);
@ -406,9 +494,14 @@ impl Daemon {
Ok(results)
}
fn retry_request_batch(&self, method: &str, params_list: &[Value]) -> Result<Vec<Value>> {
fn retry_request_batch(
&self,
method: &str,
params_list: &[Value],
failure_threshold: f64,
) -> Result<Vec<Value>> {
loop {
match self.handle_request_batch(method, params_list) {
match self.handle_request_batch(method, params_list, failure_threshold) {
Err(Error(ErrorKind::Connection(msg), _)) => {
warn!("reconnecting to bitcoind: {}", msg);
self.signal.wait(Duration::from_secs(3), false)?;
@ -422,13 +515,13 @@ impl Daemon {
}
fn request(&self, method: &str, params: Value) -> Result<Value> {
let mut values = self.retry_request_batch(method, &[params])?;
let mut values = self.retry_request_batch(method, &[params], 0.0)?;
assert_eq!(values.len(), 1);
Ok(values.remove(0))
}
fn requests(&self, method: &str, params_list: &[Value]) -> Result<Vec<Value>> {
self.retry_request_batch(method, params_list)
self.retry_request_batch(method, params_list, 0.0)
}
// bitcoind JSONRPC API:
@ -455,7 +548,7 @@ impl Daemon {
pub fn getblockheader(&self, blockhash: &BlockHash) -> Result<BlockHeader> {
header_from_value(self.request(
"getblockheader",
json!([blockhash.to_hex(), /*verbose=*/ false]),
json!([blockhash.to_string(), /*verbose=*/ false]),
)?)
}
@ -474,21 +567,22 @@ impl Daemon {
}
pub fn getblock(&self, blockhash: &BlockHash) -> Result<Block> {
let block = block_from_value(
self.request("getblock", json!([blockhash.to_hex(), /*verbose=*/ false]))?,
)?;
let block = block_from_value(self.request(
"getblock",
json!([blockhash.to_string(), /*verbose=*/ false]),
)?)?;
assert_eq!(block.block_hash(), *blockhash);
Ok(block)
}
pub fn getblock_raw(&self, blockhash: &BlockHash, verbose: u32) -> Result<Value> {
self.request("getblock", json!([blockhash.to_hex(), verbose]))
self.request("getblock", json!([blockhash.to_string(), verbose]))
}
pub fn getblocks(&self, blockhashes: &[BlockHash]) -> Result<Vec<Block>> {
let params_list: Vec<Value> = blockhashes
.iter()
.map(|hash| json!([hash.to_hex(), /*verbose=*/ false]))
.map(|hash| json!([hash.to_string(), /*verbose=*/ false]))
.collect();
let values = self.requests("getblock", &params_list)?;
let mut blocks = vec![];
@ -501,15 +595,14 @@ impl Daemon {
pub fn gettransactions(&self, txhashes: &[&Txid]) -> Result<Vec<Transaction>> {
let params_list: Vec<Value> = txhashes
.iter()
.map(|txhash| json!([txhash.to_hex(), /*verbose=*/ false]))
.map(|txhash| json!([txhash.to_string(), /*verbose=*/ false]))
.collect();
let values = self.requests("getrawtransaction", &params_list)?;
let values = self.retry_request_batch("getrawtransaction", &params_list, 0.25)?;
let mut txs = vec![];
for value in values {
txs.push(tx_from_value(value)?);
}
assert_eq!(txhashes.len(), txs.len());
// missing transactions are skipped, so the number of txs returned may be less than the number of txids requested
Ok(txs)
}
@ -521,14 +614,14 @@ impl Daemon {
) -> Result<Value> {
self.request(
"getrawtransaction",
json!([txid.to_hex(), verbose, blockhash]),
json!([txid.to_string(), verbose, blockhash]),
)
}
pub fn getmempooltx(&self, txhash: &Txid) -> Result<Transaction> {
let value = self.request(
"getrawtransaction",
json!([txhash.to_hex(), /*verbose=*/ false]),
json!([txhash.to_string(), /*verbose=*/ false]),
)?;
tx_from_value(value)
}
@ -544,8 +637,43 @@ impl Daemon {
pub fn broadcast_raw(&self, txhex: &str) -> Result<Txid> {
let txid = self.request("sendrawtransaction", json!([txhex]))?;
Txid::from_hex(txid.as_str().chain_err(|| "non-string txid")?)
.chain_err(|| "failed to parse txid")
txid.as_str()
.chain_err(|| "non-string txid")?
.parse::<Txid>()
.map_err(|e| format!("failed to parse txid: {:?}", e).into())
}
pub fn test_mempool_accept(
&self,
txhex: Vec<String>,
maxfeerate: Option<f64>,
) -> Result<Vec<MempoolAcceptResult>> {
let params = match maxfeerate {
Some(rate) => json!([txhex, format!("{:.8}", rate)]),
None => json!([txhex]),
};
let result = self.request("testmempoolaccept", params)?;
serde_json::from_value::<Vec<MempoolAcceptResult>>(result)
.chain_err(|| "invalid testmempoolaccept reply")
}
pub fn submit_package(
&self,
txhex: Vec<String>,
maxfeerate: Option<f64>,
maxburnamount: Option<f64>,
) -> Result<SubmitPackageResult> {
let params = match (maxfeerate, maxburnamount) {
(Some(rate), Some(burn)) => {
json!([txhex, format!("{:.8}", rate), format!("{:.8}", burn)])
}
(Some(rate), None) => json!([txhex, format!("{:.8}", rate)]),
(None, Some(burn)) => json!([txhex, null, format!("{:.8}", burn)]),
(None, None) => json!([txhex]),
};
let result = self.request("submitpackage", params)?;
serde_json::from_value::<SubmitPackageResult>(result)
.chain_err(|| "invalid submitpackage reply")
}
// Get estimated feerates for the provided confirmation targets using a batch RPC request
@ -583,7 +711,7 @@ impl Daemon {
}
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
.get("height")
.expect("missing height")
@ -599,7 +727,7 @@ impl Daemon {
result.append(&mut headers);
}
let mut blockhash = BlockHash::default();
let mut blockhash = BlockHash::all_zeros();
for header in &result {
assert_eq!(header.prev_blockhash, blockhash);
blockhash = header.block_hash();
@ -625,7 +753,7 @@ impl Daemon {
bestblockhash,
);
let mut new_headers = vec![];
let null_hash = BlockHash::default();
let null_hash = BlockHash::all_zeros();
let mut blockhash = *bestblockhash;
while blockhash != null_hash {
if indexed_headers.header_by_blockhash(&blockhash).is_some() {

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, RwLock, RwLockReadGuard};
use bitcoin::hashes::{hex::FromHex, sha256, Hash};
use bitcoin::hashes::{sha256, Hash};
use elements::confidential::{Asset, Value};
use elements::encode::{deserialize, serialize};
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::registry::{AssetMeta, AssetRegistry};
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::util::{bincode_util, full_hash, Bytes, FullHash, TransactionStatus, TxInput};
use crate::util::{
bincode_util, full_hash, Bytes, FullHash, IsProvablyUnspendable, TransactionStatus, TxInput,
};
lazy_static! {
pub static ref NATIVE_ASSET_ID: AssetId =
AssetId::from_hex("6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d")
"6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d"
.parse()
.unwrap();
pub static ref NATIVE_ASSET_ID_TESTNET: AssetId =
AssetId::from_hex("144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49")
"144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49"
.parse()
.unwrap();
pub static ref NATIVE_ASSET_ID_REGTEST: AssetId =
AssetId::from_hex("5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225")
"5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225"
.parse()
.unwrap();
}
@ -33,6 +38,7 @@ fn parse_asset_id(sl: &[u8]) -> AssetId {
#[derive(Serialize)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
pub enum LiquidAsset {
Issued(IssuedAsset),
Native(PeggedAsset),
@ -71,9 +77,9 @@ pub struct IssuedAsset {
#[derive(Serialize, Deserialize, Debug)]
pub struct AssetRow {
pub issuance_txid: FullHash,
pub issuance_vin: u16,
pub issuance_vin: u32,
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 reissuance_token: FullHash,
}
@ -92,7 +98,7 @@ impl IssuedAsset {
let reissuance_token = parse_asset_id(&asset.reissuance_token);
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 {
None
};
@ -105,7 +111,7 @@ impl IssuedAsset {
},
issuance_prevout: OutPoint {
txid: deserialize(&asset.prev_txid).unwrap(),
vout: asset.prev_vout as u32,
vout: asset.prev_vout,
},
contract_hash,
reissuance_token,
@ -155,7 +161,7 @@ impl LiquidAsset {
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct IssuingInfo {
pub txid: FullHash,
pub vin: u16,
pub vin: u32,
pub is_reissuance: bool,
// None for blinded issuances
pub issued_amount: Option<u64>,
@ -166,7 +172,7 @@ pub struct IssuingInfo {
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct BurningInfo {
pub txid: FullHash,
pub vout: u16,
pub vout: u32,
pub value: u64,
}
@ -174,17 +180,22 @@ pub struct BurningInfo {
pub fn index_confirmed_tx_assets(
tx: &Transaction,
confirmed_height: u32,
tx_position: u16,
network: Network,
parent_network: BNetwork,
rows: &mut Vec<DBRow>,
op: &Operation,
) {
let (history, issuances) = index_tx_assets(tx, network, parent_network);
rows.extend(
history.into_iter().map(|(asset_id, info)| {
asset_history_row(&asset_id, confirmed_height, info).into_row()
}),
);
rows.extend(history.into_iter().map(|(asset_id, info)| {
let history_row = asset_history_row(&asset_id, confirmed_height, tx_position, info);
if let Operation::DeleteBlocksWithHistory(tx) = op {
tx.send(history_row.key.hash)
.expect("unbounded channel won't fail");
}
history_row.into_row()
}));
// 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.
@ -205,10 +216,7 @@ pub fn index_mempool_tx_assets(
) {
let (history, issuances) = index_tx_assets(tx, network, parent_network);
for (asset_id, info) in history {
asset_history
.entry(asset_id)
.or_insert_with(Vec::new)
.push(info);
asset_history.entry(asset_id).or_default().push(info);
}
for (asset_id, issuance) in issuances {
asset_issuance.insert(asset_id, issuance);
@ -251,18 +259,18 @@ fn index_tx_assets(
pegout.asset.explicit().unwrap(),
TxHistoryInfo::Pegout(PegoutInfo {
txid,
vout: txo_index as u16,
vout: txo_index as u32,
value: pegout.value,
}),
));
} else if txo.script_pubkey.is_provably_unspendable() && !txo.is_fee() {
} else if txo.script_pubkey.is_provably_unspendable_() && !txo.is_fee() {
if let (Asset::Explicit(asset_id), Value::Explicit(value)) = (txo.asset, txo.value) {
if value > 0 {
history.push((
asset_id,
TxHistoryInfo::Burning(BurningInfo {
txid,
vout: txo_index as u16,
vout: txo_index as u32,
value,
}),
));
@ -274,10 +282,10 @@ fn index_tx_assets(
for (txi_index, txi) in tx.input.iter().enumerate() {
if let Some(pegin) = get_pegin_data(txi, network) {
history.push((
pegin.asset.explicit().unwrap(),
pegin.asset,
TxHistoryInfo::Pegin(PeginInfo {
txid,
vin: txi_index as u16,
vin: txi_index as u32,
value: pegin.value,
}),
));
@ -302,7 +310,7 @@ fn index_tx_assets(
asset_id,
TxHistoryInfo::Issuing(IssuingInfo {
txid,
vin: txi_index as u16,
vin: txi_index as u32,
is_reissuance,
issued_amount,
token_amount,
@ -319,9 +327,9 @@ fn index_tx_assets(
asset_id,
AssetRow {
issuance_txid: txid,
issuance_vin: txi_index as u16,
issuance_vin: txi_index as u32,
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),
reissuance_token: full_hash(&reissuance_token.into_inner()[..]),
},
@ -336,12 +344,14 @@ fn index_tx_assets(
fn asset_history_row(
asset_id: &AssetId,
confirmed_height: u32,
tx_position: u16,
txinfo: TxHistoryInfo,
) -> TxHistoryRow {
let key = TxHistoryKey {
code: b'I',
hash: full_hash(&asset_id.into_inner()[..]),
confirmed_height,
tx_position,
txinfo,
};
TxHistoryRow { key }
@ -385,7 +395,7 @@ pub fn lookup_asset(
Ok(if let Some(row) = row {
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)) => {
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> {
if !txin.has_issuance {
if !txin.has_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::{confidential::Value, encode::serialize, issuance::ContractHash, AssetId, TxIn};
@ -8,7 +9,7 @@ mod registry;
use asset::get_issuance_entropy;
pub use asset::{lookup_asset, LiquidAsset};
pub use registry::{AssetRegistry, AssetSorting};
pub use registry::{AssetMeta, AssetRegistry, AssetSorting};
#[derive(Serialize, Deserialize, Clone)]
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 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()?;
txout
.pegin_data()
.filter(|pegin| pegin.asset == Asset::Explicit(*pegged_asset_id))
txout.pegin_data().and_then(|pegin| {
if pegin.asset == *pegged_asset_id {
Some(pegin)
} else {
None
}
})
}
pub fn get_pegout_data(
txout: &TxOut,
network: Network,
parent_network: BNetwork,
) -> Option<PegoutData> {
) -> Option<PegoutData<'_>> {
let pegged_asset_id = network.pegged_asset()?;
txout.pegout_data().filter(|pegout| {
pegout.asset == Asset::Explicit(*pegged_asset_id)
&& pegout.genesis_hash == bitcoin_genesis_hash(parent_network)
txout.pegout_data().and_then(|pegout| {
if pegout.asset == Asset::Explicit(*pegged_asset_id)
&& pegout.genesis_hash
== bitcoin_genesis_hash(match parent_network {
BNetwork::Bitcoin => Network::Liquid,
BNetwork::Testnet | BNetwork::Testnet4 => Network::LiquidTestnet,
BNetwork::Signet => return None,
BNetwork::Regtest => Network::LiquidRegtest,
})
{
Some(pegout)
} else {
None
}
})
}
@ -27,7 +42,7 @@ pub fn get_pegout_data(
#[derive(Serialize, Deserialize, Clone)]
pub struct PegoutValue {
pub genesis_hash: String,
pub scriptpubkey: bitcoin::Script,
pub scriptpubkey: bitcoin::ScriptBuf,
pub scriptpubkey_asm: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub scriptpubkey_address: Option<String>,
@ -38,12 +53,12 @@ impl PegoutValue {
let pegoutdata = get_pegout_data(txout, network, parent_network)?;
// pending https://github.com/ElementsProject/rust-elements/pull/69 is merged
let scriptpubkey = bitcoin::Script::from(pegoutdata.script_pubkey.into_bytes());
let address = bitcoin::Address::from_script(&scriptpubkey, parent_network);
let scriptpubkey = bitcoin::ScriptBuf::from(pegoutdata.script_pubkey.into_bytes());
let address = bitcoin::Address::from_script(&scriptpubkey, parent_network).ok();
Some(PegoutValue {
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,
})
@ -55,7 +70,7 @@ impl PegoutValue {
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct PeginInfo {
pub txid: FullHash,
pub vin: u16,
pub vin: u32,
pub value: u64,
}
@ -64,6 +79,6 @@ pub struct PeginInfo {
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct PegoutInfo {
pub txid: FullHash,
pub vout: u16,
pub vout: u32,
pub value: u64,
}

View File

@ -1,11 +1,11 @@
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::{Arc, RwLock};
use std::time::{Duration, SystemTime};
use std::{cmp, fs, path, thread};
use serde_json::Value as JsonValue;
use bitcoin::hashes::hex::FromHex;
use elements::AssetId;
use crate::errors::*;
@ -14,6 +14,8 @@ use crate::errors::*;
// (in number of hex characters, not bytes)
const DIR_PARTITION_LEN: usize = 2;
const SEARCH_SORT_CANDIDATE_LIMIT: usize = 2000;
pub struct AssetRegistry {
directory: path::PathBuf,
assets_cache: HashMap<AssetId, (SystemTime, AssetMeta)>,
@ -40,7 +42,7 @@ impl AssetRegistry {
start_index: usize,
limit: usize,
sorting: AssetSorting,
) -> (usize, Vec<AssetEntry>) {
) -> (usize, Vec<AssetEntry<'_>>) {
let mut assets: Vec<AssetEntry> = self
.assets_cache
.iter()
@ -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<()> {
for entry in fs::read_dir(&self.directory).chain_err(|| "failed reading asset dir")? {
let entry = entry.chain_err(|| "invalid fh")?;
@ -70,7 +105,7 @@ impl AssetRegistry {
continue;
}
let asset_id = AssetId::from_hex(
let asset_id = AssetId::from_str(
path.file_stem()
.unwrap() // cannot fail if extension() succeeded
.to_str()
@ -102,7 +137,7 @@ impl AssetRegistry {
}
pub fn spawn_sync(asset_db: Arc<RwLock<AssetRegistry>>) -> thread::JoinHandle<()> {
thread::spawn(move || loop {
crate::util::spawn_thread("asset-registry", move || loop {
if let Err(e) = asset_db.write().unwrap().fs_sync() {
error!("registry fs_sync failed: {:?}", e);
}
@ -126,7 +161,7 @@ pub struct AssetMeta {
}
impl AssetMeta {
fn domain(&self) -> Option<&str> {
pub(crate) fn domain(&self) -> Option<&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())
.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! {
types {
Error, ErrorKind, ResultExt, Result;
@ -14,9 +15,14 @@ error_chain! {
display("Iterrupted by signal {}", sig)
}
TooPopular {
description("Too many history entries")
display("Too many history entries")
TooManyUtxos(limit: usize) {
description("Too many unspent transaction outputs. Contact support to raise limits.")
display("Too many unspent transaction outputs (>{}). Contact support to raise limits.", limit)
}
TooManyTxs(limit: usize) {
description("Too many history transactions. Contact support to raise limits.")
display("Too many history transactions (>{}). Contact support to raise limits.", limit)
}
#[cfg(feature = "electrum-discovery")]

View File

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

View File

@ -5,7 +5,11 @@ use std::path::Path;
use crate::config::Config;
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)]
pub struct DBRow {
@ -19,7 +23,7 @@ pub struct ScanIterator<'a> {
done: bool,
}
impl<'a> Iterator for ScanIterator<'a> {
impl Iterator for ScanIterator<'_> {
type Item = DBRow;
fn next(&mut self) -> Option<DBRow> {
@ -44,7 +48,7 @@ pub struct ReverseScanIterator<'a> {
done: bool,
}
impl<'a> Iterator for ReverseScanIterator<'a> {
impl Iterator for ReverseScanIterator<'_> {
type Item = 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)]
pub struct DB {
db: rocksdb::DB,
@ -83,7 +148,7 @@ pub enum DBFlush {
impl DB {
pub fn open(path: &Path, config: &Config) -> DB {
let db = DB {
db: open_raw_db(path),
db: open_raw_db(path, OpenMode::ReadWrite),
};
db.verify_compatibility(config);
db
@ -101,11 +166,11 @@ impl DB {
self.db.set_options(&opts).unwrap();
}
pub fn raw_iterator(&self) -> rocksdb::DBRawIterator {
pub fn raw_iterator(&self) -> rocksdb::DBRawIterator<'_> {
self.db.raw_iterator()
}
pub fn iter_scan(&self, prefix: &[u8]) -> ScanIterator {
pub fn iter_scan(&self, prefix: &[u8]) -> ScanIterator<'_> {
ScanIterator {
prefix: prefix.to_vec(),
iter: self.db.prefix_iterator(prefix),
@ -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(
start_at,
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();
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) {
debug!(
"writing {} rows to {:?}, flush={:?}",
@ -158,6 +242,15 @@ impl DB {
self.db.write_opt(batch, &opts).unwrap();
}
pub fn delete(&self, keys: Vec<Vec<u8>>) {
debug!("deleting {} rows from {:?}", keys.len(), self.db);
for key in keys {
let _ = self.db.delete(key).inspect_err(|err| {
warn!("Error while deleting DB row: {err}");
});
}
}
pub fn flush(&self) {
self.db.flush().unwrap();
}
@ -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);
let mut db_opts = rocksdb::Options::default();
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();
// 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

@ -9,10 +9,9 @@ use std::collections::HashMap;
use std::fs;
use std::io::Cursor;
use std::path::PathBuf;
use std::sync::mpsc::Receiver;
use std::thread;
use crate::chain::{Block, BlockHash};
use crate::chain::{Block, BlockHash, BlockSizeCompat};
use crate::daemon::Daemon;
use crate::errors::*;
use crate::util::{spawn_thread, HeaderEntry, SyncChannel};
@ -43,13 +42,64 @@ pub struct BlockEntry {
type SizedBlock = (Block, u32);
pub struct SequentialFetcher<T> {
fetcher: Box<dyn FnOnce() -> Vec<Vec<T>>>,
}
impl<T> SequentialFetcher<T> {
fn from<F: FnOnce() -> Vec<Vec<T>> + 'static>(pre_func: F) -> Self {
SequentialFetcher {
fetcher: Box::new(pre_func),
}
}
pub fn map<FN>(self, mut func: FN)
where
FN: FnMut(Vec<T>),
{
for item in (self.fetcher)() {
func(item);
}
}
}
pub fn bitcoind_sequential_fetcher(
daemon: &Daemon,
new_headers: Vec<HeaderEntry>,
) -> Result<SequentialFetcher<BlockEntry>> {
let daemon = daemon.reconnect()?;
Ok(SequentialFetcher::from(move || {
new_headers
.chunks(100)
.map(|entries| {
let blockhashes: Vec<BlockHash> = entries.iter().map(|e| *e.hash()).collect();
let blocks = daemon
.getblocks(&blockhashes)
.expect("failed to get blocks from bitcoind");
assert_eq!(blocks.len(), entries.len());
let block_entries: Vec<BlockEntry> = blocks
.into_iter()
.zip(entries)
.map(|(block, entry)| BlockEntry {
entry: entry.clone(), // TODO: remove this clone()
size: block.get_block_size() as u32,
block,
})
.collect();
assert_eq!(block_entries.len(), entries.len());
block_entries
})
.collect()
}))
}
pub struct Fetcher<T> {
receiver: Receiver<T>,
receiver: crossbeam_channel::Receiver<T>,
thread: thread::JoinHandle<()>,
}
impl<T> Fetcher<T> {
fn from(receiver: Receiver<T>, thread: thread::JoinHandle<()>) -> Self {
fn from(receiver: crossbeam_channel::Receiver<T>, thread: thread::JoinHandle<()>) -> Self {
Fetcher { receiver, thread }
}
@ -88,7 +138,7 @@ fn bitcoind_fetcher(
.zip(entries)
.map(|(block, entry)| BlockEntry {
entry: entry.clone(), // TODO: remove this clone()
size: block.size() as u32,
size: block.get_block_size() as u32,
block,
})
.collect();
@ -150,14 +200,33 @@ fn blkfiles_fetcher(
fn blkfiles_reader(blk_files: Vec<PathBuf>) -> Fetcher<Vec<u8>> {
let chan = SyncChannel::new(1);
let sender = chan.sender();
let xor_key = blk_files.first().and_then(|p| {
let xor_file = p
.parent()
.expect("blk.dat files must exist in a directory")
.join("xor.dat");
if xor_file.exists() {
Some(fs::read(xor_file).expect("xor.dat exists"))
} else {
None
}
});
Fetcher::from(
chan.into_receiver(),
spawn_thread("blkfiles_reader", move || {
for path in blk_files {
trace!("reading {:?}", path);
let blob = fs::read(&path)
let mut blob = fs::read(&path)
.unwrap_or_else(|e| panic!("failed to read {:?}: {:?}", path, e));
// If the xor.dat exists. Use it to decrypt the block files.
if let Some(xor_key) = &xor_key {
for (&key, byte) in xor_key.iter().cycle().zip(blob.iter_mut()) {
*byte ^= key;
}
}
sender
.send(blob)
.unwrap_or_else(|_| panic!("failed to send {:?} contents", path));
@ -221,12 +290,7 @@ fn parse_blocks(blob: Vec<u8>, magic: u32) -> Result<Vec<SizedBlock>> {
cursor.set_position(end);
}
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(0) // CPU-bound
.thread_name(|i| format!("parse-blocks-{}", i))
.build()
.unwrap();
Ok(pool.install(|| {
Ok(super::THREAD_POOL.install(|| {
slices
.into_par_iter()
.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::iter::FromIterator;
use std::ops::Bound::{Excluded, Unbounded};
use std::sync::Arc;
use std::sync::{Arc, RwLock};
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::daemon::Daemon;
use crate::errors::*;
@ -165,7 +165,7 @@ impl Mempool {
// TODO seek directly to last seen tx without reading earlier rows
.skip_while(|txid| {
// skip until we reach the last_seen_txid
last_seen_txid.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 {
Some(_) => 1, // skip the last_seen_txid itself
@ -177,6 +177,49 @@ impl Mempool {
.collect()
}
pub fn history_group(
&self,
scripthashes: &[[u8; 32]],
last_seen_txid: Option<&Txid>,
limit: usize,
) -> Vec<Transaction> {
let _timer = self
.latency
.with_label_values(&["history_group"])
.start_timer();
scripthashes
.iter()
.filter_map(|scripthash| self.history.get(&scripthash[..]))
.flat_map(|entries| entries.iter())
.map(|e| e.get_txid())
.unique()
// TODO seek directly to last seen tx without reading earlier rows
.skip_while(|txid| {
// skip until we reach the last_seen_txid
last_seen_txid.is_some_and(|last_seen_txid| last_seen_txid != txid)
})
.skip(match last_seen_txid {
Some(_) => 1, // skip the last_seen_txid itself
None => 0,
})
.take(limit)
.map(|txid| self.txstore.get(&txid).expect("missing mempool tx"))
.cloned()
.collect()
}
pub fn history_txids_iter_group<'a>(
&'a self,
scripthashes: &'a [[u8; 32]],
) -> impl Iterator<Item = Txid> + 'a {
scripthashes
.iter()
.filter_map(move |scripthash| self.history.get(&scripthash[..]))
.flat_map(|entries| entries.iter())
.map(|entry| entry.get_txid())
.unique()
}
pub fn history_txids(&self, scripthash: &[u8], limit: usize) -> Vec<Txid> {
let _timer = self
.latency
@ -210,7 +253,7 @@ impl Mempool {
Some(Utxo {
txid: deserialize(&info.txid).expect("invalid txid"),
vout: info.vout as u32,
vout: info.vout,
value: info.value,
confirmed: None,
@ -288,6 +331,24 @@ impl Mempool {
self.txstore.keys().collect()
}
// Get n txids after the given txid in the mempool
pub fn txids_page(&self, n: usize, start: Option<Txid>) -> Vec<&Txid> {
let _timer = self
.latency
.with_label_values(&["txids_page"])
.start_timer();
let start_bound = match start {
Some(txid) => Excluded(txid),
None => Unbounded,
};
self.txstore
.range((start_bound, Unbounded))
.take(n)
.map(|(k, _v)| k)
.collect()
}
// Get all txs in the mempool
pub fn txs(&self) -> Vec<Transaction> {
let _timer = self.latency.with_label_values(&["txs"]).start_timer();
@ -296,7 +357,7 @@ impl Mempool {
// Get n txs after the given txid in the mempool
pub fn txs_page(&self, n: usize, start: Option<Txid>) -> Vec<Transaction> {
let _timer = self.latency.with_label_values(&["txs"]).start_timer();
let _timer = self.latency.with_label_values(&["txs_page"]).start_timer();
let mut page = Vec::with_capacity(n);
let start_bound = match start {
Some(txid) => Excluded(txid),
@ -325,50 +386,71 @@ impl Mempool {
&self.backlog_stats.0
}
pub fn update(&mut self, daemon: &Daemon) -> Result<()> {
let _timer = self.latency.with_label_values(&["update"]).start_timer();
let new_txids = daemon
pub fn unique_txids(&self) -> HashSet<Txid> {
HashSet::from_iter(self.txstore.keys().cloned())
}
pub fn update(mempool: &RwLock<Mempool>, daemon: &Daemon) -> Result<()> {
// 1. Start the metrics timer and get the current mempool txids
// [LOCK] Takes read lock for whole scope.
let (_timer, old_txids) = {
let mempool = mempool.read().unwrap();
(
mempool.latency.with_label_values(&["update"]).start_timer(),
mempool.unique_txids(),
)
};
// 2. Get all the mempool txids from the RPC.
// [LOCK] No lock taken. Wait for RPC request. Get lists of remove/add txes.
let all_txids = daemon
.getmempooltxids()
.chain_err(|| "failed to update mempool from daemon")?;
let old_txids = HashSet::from_iter(self.txstore.keys().cloned());
let to_remove: HashSet<&Txid> = old_txids.difference(&new_txids).collect();
let txids_to_remove: HashSet<&Txid> = old_txids.difference(&all_txids).collect();
let txids_to_add: Vec<&Txid> = all_txids.difference(&old_txids).collect();
// Download and add new transactions from bitcoind's mempool
let txids: Vec<&Txid> = new_txids.difference(&old_txids).collect();
let to_add = match daemon.gettransactions(&txids) {
Ok(txs) => txs,
Err(err) => {
warn!("failed to get {} transactions: {}", txids.len(), err); // e.g. new block or RBF
return Ok(()); // keep the mempool until next update()
}
};
// Add new transactions
if to_add.len() > self.add(to_add) {
debug!("Mempool update added less transactions than expected");
}
// Remove missing transactions
self.remove(to_remove);
// 3. Remove missing transactions. Even if we are unable to download new transactions from
// the daemon, we still want to remove the transactions that are no longer in the mempool.
// [LOCK] Write lock is released at the end of the call to remove().
mempool.write().unwrap().remove(txids_to_remove);
self.count
.with_label_values(&["txs"])
.set(self.txstore.len() as f64);
// 4. Download the new transactions from the daemon's mempool
// [LOCK] No lock taken, waiting for RPC response.
let txs_to_add = daemon
.gettransactions(&txids_to_add)
.chain_err(|| format!("failed to get {} transactions", txids_to_add.len()))?;
// Update cached backlog stats (if expired)
if self.backlog_stats.1.elapsed()
> Duration::from_secs(self.config.mempool_backlog_stats_ttl)
// 4. Update local mempool to match daemon's state
// [LOCK] Takes Write lock for whole scope.
{
let _timer = self
.latency
.with_label_values(&["update_backlog_stats"])
.start_timer();
self.backlog_stats = (BacklogStats::new(&self.feeinfo), Instant::now());
}
let mut mempool = mempool.write().unwrap();
// Add new transactions
if txs_to_add.len() > mempool.add(txs_to_add) {
debug!("Mempool update added less transactions than expected");
}
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<()> {
if self.txstore.get(txid).is_none() {
if !self.txstore.contains_key(txid) {
if let Ok(tx) = daemon.getmempooltx(txid) {
if self.add(vec![tx]) == 0 {
return Err(format!(
@ -399,9 +481,13 @@ impl Mempool {
let mut txids = Vec::with_capacity(txs.len());
// Phase 1: add to txstore
for tx in txs {
let txid = tx.txid();
txids.push(txid);
self.txstore.insert(txid, tx);
let txid = tx.get_txid();
// Only push if it doesn't already exist.
// This is important now that update doesn't lock during
// the entire function body.
if self.txstore.insert(txid, tx).is_none() {
txids.push(txid);
}
}
// Phase 2: index history and spend edges (some txos can be missing)
@ -440,7 +526,10 @@ impl Mempool {
fee: feeinfo.fee,
vsize: feeinfo.vsize,
#[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);
@ -448,14 +537,18 @@ impl Mempool {
// An iterator over (ScriptHash, TxHistoryInfo)
let spending = prevouts.into_iter().map(|(input_index, prevout)| {
let txi = tx.input.get(input_index as usize).unwrap();
#[cfg(not(feature = "liquid"))]
let value = prevout.value.to_sat();
#[cfg(feature = "liquid")]
let value = prevout.value;
(
compute_script_hash(&prevout.script_pubkey),
TxHistoryInfo::Spending(SpendingInfo {
txid: txid_bytes,
vin: input_index as u16,
vin: input_index,
prev_txid: full_hash(&txi.previous_output.txid[..]),
prev_vout: txi.previous_output.vout as u16,
value: prevout.value,
prev_vout: txi.previous_output.vout,
value,
}),
)
});
@ -469,22 +562,23 @@ impl Mempool {
.enumerate()
.filter(|(_, txo)| is_spendable(txo) || config.index_unspendables)
.map(|(index, txo)| {
#[cfg(not(feature = "liquid"))]
let value = txo.value.to_sat();
#[cfg(feature = "liquid")]
let value = txo.value;
(
compute_script_hash(&txo.script_pubkey),
TxHistoryInfo::Funding(FundingInfo {
txid: txid_bytes,
vout: index as u16,
value: txo.value,
vout: index as u32,
value,
}),
)
});
// Index funding/spending history entries and spend edges
for (scripthash, entry) in funding.chain(spending) {
self.history
.entry(scripthash)
.or_insert_with(Vec::new)
.push(entry);
self.history.entry(scripthash).or_default().push(entry);
}
for (i, txi) in tx.input.iter().enumerate() {
self.edges.insert(txi.previous_output, (txid, i as u32));

View File

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

View File

@ -8,27 +8,52 @@ use hex;
use std::fs::File;
use std::io;
use std::io::prelude::*;
use std::sync::{atomic::AtomicUsize, Arc};
use std::time::Instant;
pub fn precache(chain: &ChainQuery, scripthashes: Vec<FullHash>) {
pub fn precache(chain: Arc<ChainQuery>, scripthashes: Vec<FullHash>, threads: usize) {
let total = scripthashes.len();
info!("Pre-caching stats and utxo set for {} scripthashes", total);
info!(
"Pre-caching stats and utxo set on {} threads for {} scripthashes",
threads, total
);
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(16)
.num_threads(threads)
.thread_name(|i| format!("precache-{}", i))
.build()
.unwrap();
pool.install(|| {
scripthashes
.par_iter()
.enumerate()
.for_each(|(i, scripthash)| {
if i % 5 == 0 {
info!("running pre-cache for scripthash {}/{}", i + 1, total);
}
chain.stats(&scripthash[..]);
//chain.utxo(&scripthash[..]);
})
let now = Instant::now();
let counter = AtomicUsize::new(0);
std::thread::spawn(move || {
pool.install(|| {
scripthashes
.par_iter()
.for_each(|scripthash| {
// First, cache
chain.stats(&scripthash[..], crate::new_index::db::DBFlush::Disable);
let _ = chain.utxo(&scripthash[..], usize::MAX, crate::new_index::db::DBFlush::Disable);
// Then, increment the counter
let pre_increment = counter.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
let post_increment_counter = pre_increment + 1;
// Then, log
if post_increment_counter % 500 == 0 {
let now_millis = now.elapsed().as_millis();
info!("{post_increment_counter}/{total} Processed in {now_millis} ms running pre-cache for scripthash");
}
// Every 10k counts, flush the DB to disk
if post_increment_counter % 10000 == 0 {
info!("Flushing cache_db... {post_increment_counter}");
chain.store().cache_db().flush();
info!("Done Flushing cache_db!!! {post_increment_counter}");
}
})
});
// After everything is done, flush the cache
chain.store().cache_db().flush();
});
}

View File

@ -4,9 +4,9 @@ use std::collections::{BTreeSet, HashMap};
use std::sync::{Arc, RwLock, RwLockReadGuard};
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::daemon::Daemon;
use crate::daemon::{Daemon, MempoolAcceptResult, SubmitPackageResult};
use crate::errors::*;
use crate::new_index::{ChainQuery, Mempool, ScriptStats, SpendingInput, Utxo};
use crate::util::{is_spendable, BlockId, Bytes, TransactionStatus};
@ -14,7 +14,7 @@ use crate::util::{is_spendable, BlockId, Bytes, TransactionStatus};
#[cfg(feature = "liquid")]
use crate::{
chain::{asset::AssetRegistryLock, AssetId},
elements::{lookup_asset, AssetRegistry, AssetSorting, LiquidAsset},
elements::{lookup_asset, AssetMeta, AssetRegistry, AssetSorting, LiquidAsset},
};
const FEE_ESTIMATES_TTL: u64 = 60; // seconds
@ -65,7 +65,7 @@ impl Query {
self.config.network_type
}
pub fn mempool(&self) -> RwLockReadGuard<Mempool> {
pub fn mempool(&self) -> RwLockReadGuard<'_, Mempool> {
self.mempool.read().unwrap()
}
@ -87,8 +87,29 @@ impl Query {
Ok(txid)
}
pub fn test_mempool_accept(
&self,
txhex: Vec<String>,
maxfeerate: Option<f64>,
) -> Result<Vec<MempoolAcceptResult>> {
self.daemon.test_mempool_accept(txhex, maxfeerate)
}
pub fn submit_package(
&self,
txhex: Vec<String>,
maxfeerate: Option<f64>,
maxburnamount: Option<f64>,
) -> Result<SubmitPackageResult> {
self.daemon.submit_package(txhex, maxfeerate, maxburnamount)
}
pub fn utxo(&self, scripthash: &[u8]) -> Result<Vec<Utxo>> {
let mut utxos = self.chain.utxo(scripthash, self.config.utxos_limit)?;
let mut utxos = self.chain.utxo(
scripthash,
self.config.utxos_limit,
super::db::DBFlush::Enable,
)?;
let mempool = self.mempool();
utxos.retain(|utxo| !mempool.has_spend(&OutPoint::from(utxo)));
utxos.extend(mempool.utxo(scripthash));
@ -111,7 +132,7 @@ impl Query {
pub fn stats(&self, scripthash: &[u8]) -> (ScriptStats, ScriptStats) {
(
self.chain.stats(scripthash),
self.chain.stats(scripthash, super::db::DBFlush::Enable),
self.mempool().stats(scripthash),
)
}
@ -140,7 +161,7 @@ impl Query {
}
pub fn lookup_tx_spends(&self, tx: Transaction) -> Vec<Option<SpendingInput>> {
let txid = tx.txid();
let txid = tx.get_txid();
tx.output
.par_iter()
@ -250,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")]
pub fn list_registry_assets(
&self,
@ -277,4 +307,27 @@ impl Query {
.collect::<Result<Vec<_>>>()?;
Ok((total_num, results))
}
#[cfg(feature = "liquid")]
pub fn search_registry_assets<T, F>(
&self,
search: &str,
limit: usize,
mut map: F,
) -> Result<Vec<T>>
where
F: FnMut(&AssetId, &AssetMeta) -> T,
{
let asset_db = self
.asset_db
.as_ref()
.chain_err(|| "asset registry unavailable")?;
Ok(asset_db
.read()
.unwrap()
.search(search, limit)
.into_iter()
.map(|(asset_id, metadata)| map(asset_id, metadata))
.collect())
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,3 +1,5 @@
use bitcoin::hashes::Hash;
use crate::chain::{BlockHash, BlockHeader};
use crate::errors::*;
use crate::new_index::BlockEntry;
@ -73,7 +75,7 @@ impl HeaderList {
HeaderList {
headers: vec![],
heights: HashMap::new(),
tip: BlockHash::default(),
tip: BlockHash::all_zeros(),
}
}
@ -89,7 +91,7 @@ impl HeaderList {
let mut blockhash = tip_hash;
let mut headers_chain: Vec<BlockHeader> = vec![];
let null_hash = BlockHash::default();
let null_hash = BlockHash::all_zeros();
while blockhash != null_hash {
let header = headers_map.remove(&blockhash).unwrap_or_else(|| {
@ -136,7 +138,7 @@ impl HeaderList {
Some(h) => h.header.prev_blockhash,
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 {
0
} else {
@ -155,7 +157,12 @@ impl HeaderList {
.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)
for i in 1..new_headers.len() {
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 {
*self.headers[height - 1].hash()
} else {
BlockHash::default()
BlockHash::all_zeros()
};
assert_eq!(entry.header().prev_blockhash, expected_prev_blockhash);
height
}
None => return,
None => return (vec![], None),
};
debug!(
"applying {} new headers from height {}",
new_headers.len(),
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 {
let height = new_header.height();
assert_eq!(height, self.headers.len());
@ -190,6 +205,8 @@ impl HeaderList {
self.headers.push(new_header);
self.heights.insert(self.tip, height);
}
removed.reverse();
(removed, reorged_tip)
}
pub fn header_by_blockhash(&self, blockhash: &BlockHash) -> Option<&HeaderEntry> {
@ -203,9 +220,8 @@ impl HeaderList {
}
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);
entry
})
}
@ -216,7 +232,10 @@ impl HeaderList {
pub fn tip(&self) -> &BlockHash {
assert_eq!(
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
}
@ -229,7 +248,7 @@ impl HeaderList {
self.headers.is_empty()
}
pub fn iter(&self) -> slice::Iter<HeaderEntry> {
pub fn iter(&self) -> slice::Iter<'_, HeaderEntry> {
self.headers.iter()
}
@ -238,7 +257,7 @@ impl HeaderList {
// Use the timestamp as the mtp of the genesis block.
// Matches bitcoind's behaviour: bitcoin-cli getblock `bitcoin-cli getblockhash 0` | jq '.time == .mediantime'
if height == 0 {
self.headers.get(0).unwrap().header.time
self.headers.first().unwrap().header.time
} else if height > self.len() - 1 {
0
} else {
@ -292,9 +311,13 @@ pub struct BlockHeaderMeta {
impl From<&BlockEntry> for BlockMeta {
fn from(b: &BlockEntry) -> BlockMeta {
#[cfg(not(feature = "liquid"))]
let weight = b.block.weight().to_wu() as u32;
#[cfg(feature = "liquid")]
let weight = b.block.weight() as u32;
BlockMeta {
tx_count: b.block.txdata.len() as u32,
weight: b.block.weight() as u32,
weight,
size: b.size,
}
}

View File

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

View File

@ -8,15 +8,19 @@ pub mod fees;
pub use self::block::{BlockHeaderMeta, BlockId, BlockMeta, BlockStatus, HeaderEntry, HeaderList};
pub use self::fees::get_tx_fee;
pub use self::script::{get_innerscripts, ScriptToAddr, ScriptToAsm};
pub use self::script::{
get_innerscripts, IsProvablyUnspendable, ScriptToAddr, ScriptToAsm, SegwitDetection,
};
pub use self::transaction::{
extract_tx_prevouts, has_prevout, is_coinbase, is_spendable, serialize_outpoint,
sigops::transaction_sigop_count, TransactionStatus, TxInput,
};
use std::collections::HashMap;
use std::sync::mpsc::{channel, sync_channel, Receiver, Sender, SyncSender};
use std::thread;
use std::sync::atomic::AtomicUsize;
use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::Mutex;
use std::thread::{self, ThreadId};
use crate::chain::BlockHeader;
use bitcoin::hashes::sha256d::Hash as Sha256dHash;
@ -36,26 +40,38 @@ pub fn full_hash(hash: &[u8]) -> FullHash {
}
pub struct SyncChannel<T> {
tx: SyncSender<T>,
rx: Receiver<T>,
tx: Option<crossbeam_channel::Sender<T>>,
rx: Option<crossbeam_channel::Receiver<T>>,
}
impl<T> SyncChannel<T> {
pub fn new(size: usize) -> SyncChannel<T> {
let (tx, rx) = sync_channel(size);
SyncChannel { tx, rx }
let (tx, rx) = crossbeam_channel::bounded(size);
SyncChannel {
tx: Some(tx),
rx: Some(rx),
}
}
pub fn sender(&self) -> SyncSender<T> {
self.tx.clone()
pub fn sender(&self) -> crossbeam_channel::Sender<T> {
self.tx.as_ref().expect("No Sender").clone()
}
pub fn receiver(&self) -> &Receiver<T> {
&self.rx
pub fn receiver(&self) -> &crossbeam_channel::Receiver<T> {
self.rx.as_ref().expect("No Receiver")
}
pub fn into_receiver(self) -> Receiver<T> {
self.rx
pub fn into_receiver(self) -> crossbeam_channel::Receiver<T> {
self.rx.expect("No Receiver")
}
/// This drops the sender and receiver, causing all other methods to panic.
///
/// Use only when you know that the channel will no longer be used.
/// ie. shutdown.
pub fn close(&mut self) -> Option<crossbeam_channel::Receiver<T>> {
self.tx.take();
self.rx.take()
}
}
@ -83,15 +99,58 @@ impl<T> Channel<T> {
}
}
pub fn spawn_thread<F, T>(name: &str, f: F) -> thread::JoinHandle<T>
/// This static HashMap contains all the threads spawned with [`spawn_thread`] with their name
#[inline]
pub fn with_spawned_threads<F>(f: F)
where
F: FnOnce(&mut HashMap<ThreadId, String>),
{
lazy_static! {
static ref SPAWNED_THREADS: Mutex<HashMap<ThreadId, String>> = Mutex::new(HashMap::new());
}
let mut lock = match SPAWNED_THREADS.lock() {
Ok(threads) => threads,
// There's no possible broken state
Err(threads) => {
warn!("SPAWNED_THREADS is in a poisoned state! Be wary of incorrect logs!");
threads.into_inner()
}
};
f(&mut lock)
}
pub fn spawn_thread<F, T>(prefix: &str, do_work: F) -> thread::JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
{
static THREAD_COUNTER: AtomicUsize = AtomicUsize::new(0);
let counter = THREAD_COUNTER.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
thread::Builder::new()
.name(name.to_owned())
.spawn(f)
.name(format!("{}-{}", prefix, counter))
.spawn(move || {
let thread = std::thread::current();
let name = thread.name().unwrap();
let id = thread.id();
trace!("[THREAD] GETHASHMAP INSERT | {name} {id:?}");
with_spawned_threads(|threads| {
threads.insert(id, name.to_owned());
});
trace!("[THREAD] START WORK | {name} {id:?}");
let result = do_work();
trace!("[THREAD] FINISHED WORK | {name} {id:?}");
trace!("[THREAD] GETHASHMAP REMOVE | {name} {id:?}");
with_spawned_threads(|threads| {
threads.remove(&id);
});
trace!("[THREAD] HASHMAP REMOVED | {name} {id:?}");
result
})
.unwrap()
}
@ -137,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
pub mod serde_hex {
use bitcoin::hashes::hex::{FromHex, ToHex};
use bitcoin::hashes::hex::FromHex;
use serde::de::Error;
use serde::{Deserializer, Serializer};
pub fn serialize<S: Serializer>(b: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&b.to_hex())
pub fn serialize<S: Serializer>(b: &[u8], s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&hex::encode(b))
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
@ -151,14 +210,14 @@ pub mod serde_hex {
}
pub mod opt {
use bitcoin::hashes::hex::{FromHex, ToHex};
use bitcoin::hashes::hex::FromHex;
use serde::de::Error;
use serde::{Deserializer, Serializer};
pub fn serialize<S: Serializer>(b: &Option<Vec<u8>>, s: S) -> Result<S::Ok, S::Error> {
match *b {
None => s.serialize_none(),
Some(ref b) => s.serialize_str(&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 trait IsProvablyUnspendable {
fn is_provably_unspendable_(&self) -> bool;
}
#[cfg(not(feature = "liquid"))]
impl IsProvablyUnspendable for bitcoin::Script {
// is_provably_unspendable() is deprecated in rust-bitcoin
// so we re-implement it here. Copy pasted.
fn is_provably_unspendable_(&self) -> bool {
use bitcoin::blockdata::opcodes::{
Class::{IllegalOp, ReturnOp},
ClassifyContext, Opcode,
};
match self.as_bytes().first() {
Some(b) => {
let first = Opcode::from(*b);
let class = first.classify(ClassifyContext::Legacy);
class == ReturnOp || class == IllegalOp
}
None => false,
}
}
}
#[cfg(feature = "liquid")]
impl IsProvablyUnspendable for elements::Script {
#[inline(always)]
fn is_provably_unspendable_(&self) -> bool {
// Not deprecated yet
self.is_provably_unspendable()
}
}
// Extension trait for segwit script detection that works across bitcoin and elements
pub trait SegwitDetection {
fn segwit_is_p2wpkh(&self) -> bool;
fn segwit_is_p2wsh(&self) -> bool;
fn segwit_is_p2tr(&self) -> bool;
}
#[cfg(not(feature = "liquid"))]
impl SegwitDetection for bitcoin::Script {
fn segwit_is_p2wpkh(&self) -> bool {
self.is_p2wpkh()
}
fn segwit_is_p2wsh(&self) -> bool {
self.is_p2wsh()
}
fn segwit_is_p2tr(&self) -> bool {
self.is_p2tr()
}
}
#[cfg(not(feature = "liquid"))]
impl SegwitDetection for bitcoin::ScriptBuf {
fn segwit_is_p2wpkh(&self) -> bool {
self.is_p2wpkh()
}
fn segwit_is_p2wsh(&self) -> bool {
self.is_p2wsh()
}
fn segwit_is_p2tr(&self) -> bool {
self.is_p2tr()
}
}
#[cfg(feature = "liquid")]
impl SegwitDetection for elements::Script {
fn segwit_is_p2wpkh(&self) -> bool {
self.is_v0_p2wpkh()
}
fn segwit_is_p2wsh(&self) -> bool {
self.is_v0_p2wsh()
}
fn segwit_is_p2tr(&self) -> bool {
self.is_v1_p2tr()
}
}
pub trait ScriptToAsm: std::fmt::Debug {
fn to_asm(&self) -> String {
let asm = format!("{:?}", self);
@ -16,6 +97,7 @@ pub trait ScriptToAsm: std::fmt::Debug {
}
}
impl ScriptToAsm for bitcoin::Script {}
impl ScriptToAsm for bitcoin::ScriptBuf {}
#[cfg(feature = "liquid")]
impl ScriptToAsm for elements::Script {}
@ -25,7 +107,9 @@ pub trait ScriptToAddr {
#[cfg(not(feature = "liquid"))]
impl ScriptToAddr for bitcoin::Script {
fn to_address_str(&self, network: Network) -> Option<String> {
bitcoin::Address::from_script(self, 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")]
@ -41,7 +125,11 @@ pub fn get_innerscripts(txin: &TxIn, prevout: &TxOut) -> InnerScripts {
// Wrapped redeemScript for P2SH spends
let redeem_script = if prevout.script_pubkey.is_p2sh() {
if let Some(Ok(PushBytes(redeemscript))) = txin.script_sig.instructions().last() {
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 {
None
}
@ -50,9 +138,9 @@ pub fn get_innerscripts(txin: &TxIn, prevout: &TxOut) -> InnerScripts {
};
// Wrapped witnessScript for P2WSH or P2SH-P2WSH spends
let witness_script = if prevout.script_pubkey.is_v0_p2wsh()
|| prevout.script_pubkey.is_v1_p2tr()
|| redeem_script.as_ref().map_or(false, |s| s.is_v0_p2wsh())
let witness_script = if prevout.script_pubkey.segwit_is_p2wsh()
|| prevout.script_pubkey.segwit_is_p2tr()
|| redeem_script.as_ref().is_some_and(|s| s.segwit_is_p2wsh())
{
let witness = &txin.witness;
#[cfg(feature = "liquid")]
@ -64,7 +152,7 @@ pub fn get_innerscripts(txin: &TxIn, prevout: &TxOut) -> InnerScripts {
#[cfg(feature = "liquid")]
let wit_to_vec = Clone::clone;
let inner_script_slice = if prevout.script_pubkey.is_v1_p2tr() {
let inner_script_slice = if prevout.script_pubkey.segwit_is_p2tr() {
// Witness stack is potentially very large
// so we avoid to_vec() or iter().collect() for performance
let w_len = witness.len();

View File

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

274
start Executable file
View File

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