Merge pull request #430 from naturallaw777/chore/bitcoin-core-only

Replace Bitcoin Knots with Bitcoin Core
This commit is contained in:
Sovran Systems
2026-08-13 13:48:28 -05:00
committed by GitHub
16 changed files with 33 additions and 698 deletions
+1 -2
View File
@@ -10,7 +10,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Fixed ### Fixed
- Fix Bitcoin Knots → Bitcoin Core switch hanging in the Hub UI
- Sessions are now persisted to `/var/lib/secrets/hub-sessions.json` so the - Sessions are now persisted to `/var/lib/secrets/hub-sessions.json` so the
browser login survives the Hub service restart that `nixos-rebuild switch` browser login survives the Hub service restart that `nixos-rebuild switch`
performs during activation. Previously the in-memory session store was performs during activation. Previously the in-memory session store was
@@ -243,7 +242,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Integrated Sparrow Wallet, Bisq, and Bisq 2 - Integrated Sparrow Wallet, Bisq, and Bisq 2
- Comprehensive Sovran Hub for service management - Comprehensive Sovran Hub for service management
- NixOS-based operating system with privacy and sovereignty focus - NixOS-based operating system with privacy and sovereignty focus
- Support for Bitcoin Knots + BIP110, Electrs, LND, Ride The Lightning, BTCPay Server, and more - Support for Bitcoin Core, Electrs, LND, Ride The Lightning, BTCPay Server, and more
- Server + Desktop hybrid mode with Matrix, Nextcloud, VaultWarden, and other self-hosted services - Server + Desktop hybrid mode with Matrix, Nextcloud, VaultWarden, and other self-hosted services
- Automated installer with graphical GNOME desktop - Automated installer with graphical GNOME desktop
- Tor integration and onion services for all major components - Tor integration and onion services for all major components
+4 -7
View File
@@ -73,8 +73,7 @@ Sovran_SystemsOS solves both with one operating system:
- **Buy and sell Bitcoin peer-to-peer** with Bisq and Bisq 2. No central - **Buy and sell Bitcoin peer-to-peer** with Bisq and Bisq 2. No central
company holds user funds, and no exchange account stands between buyers and company holds user funds, and no exchange account stands between buyers and
sellers. sellers.
- **Verify your own Bitcoin** with a full node: [Bitcoin - **Verify your own Bitcoin** with a full node: [Bitcoin Core](https://bitcoin.org) and
Knots](https://bitcoinknots.org) and
[Electrs](https://github.com/romanz/electrs), so your wallets connect to [Electrs](https://github.com/romanz/electrs), so your wallets connect to
*your* node instead of a stranger's. *your* node instead of a stranger's.
- **Use Lightning** with [LND](https://github.com/lightningnetwork/lnd) and - **Use Lightning** with [LND](https://github.com/lightningnetwork/lnd) and
@@ -150,13 +149,13 @@ presents and manages the features available on your system.
### Your money — Bitcoin sovereignty ### Your money — Bitcoin sovereignty
- Bitcoin Knots, Electrs, and Tor integration - Bitcoin Core, Electrs, and Tor integration
- LND and Ride The Lightning, with [Alby Hub](https://albyhub.com) for Nostr - LND and Ride The Lightning, with [Alby Hub](https://albyhub.com) for Nostr
Wallet Connect (NWC) connections Wallet Connect (NWC) connections
- BTCPay Server - BTCPay Server
- Sparrow Wallet, Bisq, and Bisq 2, with automatic wallet-to-node connections - Sparrow Wallet, Bisq, and Bisq 2, with automatic wallet-to-node connections
- Optional: a self-hosted [Mempool](https://github.com/mempool/mempool) - Optional: a self-hosted [Mempool](https://github.com/mempool/mempool)
explorer, or Bitcoin Core in place of Knots explorer
Run your own Bitcoin infrastructure. Verify your own money. Trust no one. Run your own Bitcoin infrastructure. Verify your own money. Trust no one.
@@ -202,7 +201,7 @@ Bitcoin and self-hosting infrastructure runs on the machine.
| Mode | Best for | What you get | | Mode | Best for | What you get |
|---|---|---| |---|---|---|
| **Desktop** | Everyday users and computers with modest hardware | Sparrow, Bisq, and Bisq 2 for self-custody and peer-to-peer Bitcoin use | | **Desktop** | Everyday users and computers with modest hardware | Sparrow, Bisq, and Bisq 2 for self-custody and peer-to-peer Bitcoin use |
| **Node** | People ready to verify and operate their own Bitcoin infrastructure | Everything in Desktop, plus the full Bitcoin stack: Bitcoin Knots, Electrs, LND, Ride The Lightning, BTCPay Server, and wallet-to-node connections | | **Node** | People ready to verify and operate their own Bitcoin infrastructure | Everything in Desktop, plus the full Bitcoin stack: Bitcoin Core, Electrs, LND, Ride The Lightning, BTCPay Server, and wallet-to-node connections |
| **Server + Desktop** | Bitcoiners who want the same sovereignty over their communications, cloud, passwords, and web services | The complete Node stack, plus the private self-hosted services | | **Server + Desktop** | Bitcoiners who want the same sovereignty over their communications, cloud, passwords, and web services | The complete Node stack, plus the private self-hosted services |
**Desktop: start with your keys.** Desktop is not a reduced or Bitcoin-free **Desktop: start with your keys.** Desktop is not a reduced or Bitcoin-free
@@ -688,7 +687,6 @@ rebuilds the machine into the selected declarative state.
| Shared credentials | `modules/credentials.nix` | | Shared credentials | `modules/credentials.nix` |
| Bitcoin and Lightning stack | `modules/bitcoinecosystem.nix` | | Bitcoin and Lightning stack | `modules/bitcoinecosystem.nix` |
| Automatic wallet-to-node connections | `modules/wallet-autoconnect.nix` | | Automatic wallet-to-node connections | `modules/wallet-autoconnect.nix` |
| Optional Bitcoin Core in place of Knots | `modules/bitcoin-core.nix` |
| Alby Hub and Nostr Wallet Connect (NWC) on LND | `modules/nwc-wallets.nix`, `packages/albyhub/` | | Alby Hub and Nostr Wallet Connect (NWC) on LND | `modules/nwc-wallets.nix`, `packages/albyhub/` |
| Matrix Synapse | `modules/synapse.nix` | | Matrix Synapse | `modules/synapse.nix` |
| Optional Element audio and video calling via LiveKit | `modules/element-calling.nix` | | Optional Element audio and video calling via LiveKit | `modules/element-calling.nix` |
@@ -777,7 +775,6 @@ This work helps make it possible for Sovran_SystemsOS to deliver self-custody an
Sovran_SystemsOS also depends on the work of the developers and communities behind: Sovran_SystemsOS also depends on the work of the developers and communities behind:
- [Bitcoin Knots](https://github.com/bitcoinknots/bitcoin)
- [Bitcoin Core](https://github.com/bitcoin/bitcoin) - [Bitcoin Core](https://github.com/bitcoin/bitcoin)
- [Sparrow Wallet](https://github.com/sparrowwallet/sparrow) - [Sparrow Wallet](https://github.com/sparrowwallet/sparrow)
- [Bisq](https://github.com/bisq-network/bisq) - [Bisq](https://github.com/bisq-network/bisq)
-244
View File
@@ -1,244 +0,0 @@
<svg width="256" height="256" version="1.1" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<image width="256" height="256" image-rendering="optimizeQuality" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAABmJLR0QA/wD/AP+gvaeTAAAgAElE
QVR4nO2dd3gU1frHv+9sTy9AKh2RIqh0kHJFpVoQO1IURa6CCFak6CrFdr1KU1HUa8NKEREQRQUR
ASlSlR5KEkpIQtr2Ob8/Aj9Isrszuzu7s7s5n+fJo+zOnHmTmfOdU94CcDgcDofD4XA4HA6Hw+Fw
OJyohtQ2gBM4zAytNTWtPkiozxjqChDqMiCVIKYyJtQBiakA6QGYiGAEAMaQAJDmfAsuIpSc/9wK
wALABrCzBJxlEM4SEwtE4AwJKICDHTOeO3mCzHCq9CtzFIILQITAvoLGdjqrqSiiLQmsFYDGYKwR
iBqBIRuANsQmOUE4AcZyQJQDRocZw98Cox2GohOHyAwxxPZw/IALQBjCzK31lpTidgKJnRmoDYAr
AbQCEKOyaXKpAKM9ANtBwC4RtNlkSN9Ko7c61DaMUxUuAGEAm52SYIOxEyN0B3DN+R+TymYpjYMI
O0XgdzBa79SzXxJG5xWobVRthwuACrCvoKnIz7iKNHQ9MdwEoCsAQW27VGAvMXwH4CeDIWMtHyGE
Hi4AIaL4rQbJBtF1M0Q2gAg3MCBZbZvCjEIAq0FshdVh+C55Qk6x2gbVBrgABBE2PzPGZsN1IAxj
wC0A9GrbFCHYGPAjMfraCMtSGldYorZB0QoXAIVh5tZ6S2rxLQLYEAb0Ayq33Th+YyXQChHiQpM+
cxmfJigLFwCFsM7Nag6RjWSE+wHUU9ueaISAIgb6WhCdcwyPndqttj3RABeAAGCzmxmsKL8dJIwC
WE/wv2eoYABbC9B7xrPJ35B5j11tgyIV/sD6AZudkmAjw/0M9CSAbLXtqeWcAvCO3SHMTnz8RKHa
xkQaXAB8wDInrTEjzXhieABArNr2cKpQBtBCIrxuHJu7X21jIgUuADKwzclsIQLPA7gDgEZtezhe
cYHwBYFe5EIgDRcAL1jeSG8EreZZgI1E6H3tOYEhErAIgjDJOObEQbWNCVe4ALjBMjejIUSaAsII
ADq17eEEhAOgD5nApseMyTuutjHhBheAS2CvpcVajZqnADwDvn8fbdgZ4R2TaJ3KHYsuwgUAAGMg
65ysYSD2MoAMte3hBJU8IvaCoSB/AQ9Z5gKAijmZ1xDwJoAOatvCCSVsM2PCYzHjcjeqbYma1FoB
YPObJFoc1heJYSxqZyQeB2Bg9KndSeNrqw9BrRQA69ysGxljb4M78XAqyWdgj8Y8mr9IbUNCTa0S
gLJ5ddM1Lv2rIDZMbVs44QcDlkMr/Dvm4RO5atsSKmqNAFTMzbpTYOwdHofPkaCQMRodMy73G7UN
CQVRLwDslTrx1hjDfwD2kNq2cCIIRp8YNfZHaMyZMrVNCSZRLQAVs7M7EYmfAWimti2cCISQwxiG
xjya97vapgSLqBQAZoZgTc2aDLDnwF14OYHhALHnjWPyXyYCU9sYpYk6AagM1TV+xIBBatvCiSq+
t2m0w5IeOVaktiFKElUCYHsru63oEhcDaKq2LZyo5KAAYbDh0RO71DZEKaLGAaZiduY9okvcAN75
OcGjmQhxk2Vu1gi1DVGKiB8BMDMEW2rmSwx4Wm1bOLUJesk4NndypK8LRLQAsNnNDFaq+BDAPWrb
Eu6cLQdmrBFRZgNeGSggOVKKjIUxBFpkcNAwevyERW1b/CViBaBkblaqjrElAHqobUu4s3Q3g/kH
EcXnH9N6cYTXbyb0aBKxtz+c2Ohk4s3x406eUdsQf4jIJ8A6u35TRq4VAJqrbUs4Y3EAz60S8fWO
mqNUIuCBzgIm9iZoo2YlSDUOkQsDjePz9qltiK9EnACUz81sJ4j4AYQ6atsSzhwrAh76WsQ/p71P
Ua+7jDB3sAATz3sUKKddAvWNG5P7l9qG+EJECUD5vPSOgiisApCiti3hzM48YOSXLhSUyzu+bSbw
v7s1SOHrAgHBgGIQDYgZm/uH2rbIJWIEwDI3oydAy8EQr7Yt4czvRxhGfSWiwscCWk1Tgc+GapDO
/7qBUk4Mg4zj8n5S2xA5RMTszzonvT8YreKd3zt/5TI89LXvnR8ADp0Fhi904Zw1one1woFYRlhu
m5t5i9qGyCHsBcA6J70/g7AUgEltW8KZAwXA8M9FlAdQJGv/GWDsYgaHi4tAgBhEhq+s8zL6qW2I
FGEtABVzMq9hEL4GL6vtlTI78PA3LpRYA2/rt8MMk1dwAVAAPRNpsWVO9r/UNsQbYSsAFbOzuhBh
JXgJLkmeXCbiYIH0cckxQPO60sd9tYNh0U4uAgpgAsTllnmZ3dU2xBNhKQD22VlXErHv+ZxfmqW7
GVb9I91Z4w3Ax/do8PlQDZqkSq/9vrCa4WQJFwEFiIWI5eVzM9upbYg7wk4ArHOzmrvAfgLf6pOk
2AK8uFo6tb1WAObfIaBNBpAaCyy4S0CcxKSqxMrw3A9cABQiUWBYaZ2XHXaJacJKAErmZqUyxpZz
Jx95vP6riMIK6eOevpbQrdHFt36TFOCVG6Vv/ep9DCtljC44sqjHRHFV6ex0GZOw0BE2AsA+bGTU
gS0DcJnatkQCJ84BX2yX7py9mxFGdal5mwe2ItzWVvr2v/ozg7PW189RjKZaCIvZh43CpuxcWAgA
YyBrmf19MHRT25ZIYdY6EQ6JjhmnB2YMEEAepvxTrifJqMAjhQzf7uajAMUgdLeV2T9m5vDoe2Fh
hG1u5ssAhqhtR6RQUA4s3SXdKR/rSchI8Px9cgwwoaf0I/DBZi4ASsKAO6ypWdPVtgMIAwGomJ15
D0/m4Rtf/sUk3/7JMcDQ9tK39552hOxE78fsOcmw7QQXAWVhz1pmZw1X2wpVM+aez+H3npo2RCIi
YxjSzvtWXpeGJCvCTycAD3UV8Nwq74qybA9Du+yICR2JDIi9ZZuVts3w2Kndqpmg1oWL32qQbHA5
/wTP4ac6pTag6ywXyry4EdeLI2x6zPN6AsdvDlid+k7JE3KK1bi4KlMAZoZgcDk/Ae/8YUG8Aejf
0nvPPl3G8LdEbgGOX1xm0Km3KKjKRSuLdmCgGtfmuKdfC+lX+4YjXACCATHcZE3NVGUdLOQDuvNJ
PX4HENE5aDYdQ8RHzXWoTzCeXwWyOYEr/+OC1en5+FuuIMwadPGdYXEAh88yxBsIqbFALA/ZCgQn
I6F7zNgTm0J50ZAuArJ5deOsovAZIrzzA5XRd3K88MKZ9+8ScN1lle8AgxZok0n485hnUfv7VNXv
Sm0MAxdcXDxMiQEapRBapxOuymToUJ/QMJkvGshES0z8jL1S52p6pqA0VBcN6RTAKmrnIUo8/d4c
JCDBGNkP9x9Hq/77qkzvv8/RIoBdogH14qjK36CwAth2guGTLSKeWMbQa56I69524ZWfRew9Fdmj
pRDR1BqjfyOUFwyZAFTMybgNINX3PZWiZxPC1yMEyT30cGZ7tb39xhLhVzYnaox6GiZ779iHzgJv
b2AY8J6IQR+K+GantA9DLeeBijmZd4XqYiERgPI3GmYIoKjb77+8LvD1CA3qJ6ltiX/knqvaebNk
iFlBNQFIjZU/Cvorl+HJZSL+Nc+Fhdt45iFPEPBW2Zv10kJxrZAIgEbrnMuA5FBcK9RkJACfDBFQ
Ly7ypgMF5YDrkrdxSoz072CxV+20SX6EteSeAyatENH3XYb1fGfBHSkajXZWKC4UdAGwzs26kYEN
DvZ11KRRCmHuYIJGdcdq33AxVHHsEUi6M9pcVUVCr/H/+ofPMgxbKGLSChEWPxKZRjl3hSKxaFAf
WTa/SSJj7J1gXiNc6NSAMOaayFIAgwYQLunPLlF6BKAVqopEoPN5xoCF2xhu+cCFAzLSmtUmRIa3
it5oFNQJZlCfWKvd+hqArGBeI5wYcw1FVF79RFPVDn+mXHoEoNdUPcfmxW/AF/afAW77n4gNOXxK
cAmZBo09qFGDQROAijmZ1wB4MFjthyMGLTC2e+SsBbSqtsyULyMHYPX8AWdliIZcSqwMIxaKPAvR
JRDh4Yq52Z2D1X5QBICZIRCjNxBBlYeUYnBbATER4ubUttq+/z+nvd8uIqButRzNueeUtckhAuOW
MPxykIvAeQRi4izGgtOXguIJaE3NHAGwjsFoO9yJ0QG9mhFW/i3/AW6VTljxoPrrBzvzvU/oU2Iq
RzmX4s112F8cLoYxixi+HqFB63Tl249AOlvnZtwL5H+qdMOKP3XslTrxAGYo3W4kcWkCTjmEwzCp
xArsOen9mBb1alq6ebwGK0cJMPcV0LUhVVlUDIQKB/DIIhElvFTZeehl9lqa4jUyFBcAm0k/GUCG
0u1GEm0i8K219hCr4hPgjsvr1fxMIKBlGuG+joTPhwlY/6gGj/ciJClQyO1oEcOUlVwAzpNlNWqe
UbpRRQXAMjejISM8pmSbkchldX17DYbDI75URuLPDjIyAmUmAON6CPhtrAbje1LAEYLL9jD8zp2F
LvBkxeysbCUbVHYNgNFUAGGT8lgtYvWVP3ILdfoiFw4XwwurK9OCeWNibwEJMu/E6TKGtYe8tycQ
0LWhfEvjDcD4ngJubs0wbomI3RLTC2+YfxCx6iFNxDlaBQETkTgZwMNKNaiYAFhn12/K4IqaYJ9A
qRNLKLfLe3P58n7TaQjbc0XsOen9rMvqMtzfUV6H/XgLJHP/t8mQTiHujiaphCUjNXh2eWUgkD8c
KABW72foLyNpSfRDD1jfSnvN+Mipw0q0ppimMojPIwri/JXCl6Gvr491zybSx3y2VYQoo7+V2oCP
t0gfeHNrGYZ5QCcAr94k4JYr/O/Ab2/gIYTn0TFRM1mpxhQRAOvcrOYgdo8SbUULugB85KUY0FL6
th0sAFbI2Iqc/weTXGnXCMBAiZyBUggEvH6zgM4N/Dt/Zx5kVUCuFTCMsL6ZebkSTSkiAIyxF6Fy
ivFwQ6+RP9z1dWDcJgNoJqN64n/Xeg+5zT0HLNgo/Wa9oTkhPSHw4bdWAF6/RVPDl0Auq/fxxcDz
aJgWU5VoKGABsMxJawzgdgVsiSp82Q/3p2vdeaX0WYfPMizwkmHuhdWiLEeeBzsrN/fOTpRXsMQd
aw5wAfh/GO62vpUmYzLonYAFgEHzOIAgDng57rj7apIs8Q1U1hA8Uliz46w9xGS9UTs1qEweqiTD
2vt33p6T0r4KtQgNnNpHA20kIAEofqtBMgH3BWoEx3cSjIS720nfPqsTGLdEhN1V9fPujQmDZCzK
PdNb+b23RikkawpTHasTyClS3JyIhREbde6/2RKJ3LwT0N01uFyPAIgLpA2O/zzSjRBvkD5uVz4w
48eqr06NAPznZgEDvCzu9WtBaB+kcmBSCUg94W40U4uJ1enFUYE04LcAsNnNDAAbE8jFOYGREgOM
6iKvI320pTJb76VoBWDWIHJbFShOD5j7Bs/zxt88iiUWZe2IdIhhHDO39tvf0u87bEX57ajlPv/h
wOiuAhqnyBOBF1Yz/FRtIU2nIcy9VcDIagt9T/UWgprcxF8X4RKbsnZEAZmWlKLb/D3Zf4knesjv
czmKYdAC0/qTrKKdThEYs0jEusNVRUAjAM/dIODlgQJ0GkKvpoTh7YPrdeetEKk3qqck4wBE/ife
8UsArHOzmgPo4e9FOcrSvTFhqES58AvYnMDor0W3W2p3X034eAjh9ZuDXwX4lJ+1b5JN3B3YDdda
Z2X7VXDHvxEAY6MQHmHsnPNMvkHAZTJX1i2OShH4fHtNEejakFBH8ajzmuT7KQCJCoQZRyHEBHGk
Pyf6LADM3FrPAB70E2YYtcD8OzWyIwCdIvDs9yJe/lmERGCh4jhcDFuP+3fRFC4AnrifzW/vcyyO
zwJgSS2+BYCb1BActWmSAswbLEDrw119ZwPDk9+xoKT28sS2XEKpH4t5eg3QzMdcC7WINIsj70Zf
T/J9CsDYvT6fwwkZPZoQpvbx7bYu2inixgWhy8v/zQ7/3PmuygqsEEm0IwBD/DhHPmx2SgIR+vp6
EU5oGdGBMMzHVfyDBcDgD10+JTP1h7wSYOku/67RqQHPCOINxmgAm1fXJ8c8n/6iVpgGgWf8iQhe
6Cfgtra+dZhSG/DIYhFTVzFUBKlU18yfRL+rCfVvoawtUUiMxaW7yZcTfHtCiN3p0/GckHGqlFXZ
WxcIePVGwq1tfMxPyIBPtojoO9+FzceUHQ0s2cWwfK9/bXZuALRO5/N/KQSCT31UtgCcr1F2g88W
cYKOUwTGLmEY9aVYpVSXRgBeu0nAwFa+d5zjxcCQT0X851cWcP0/oDLl+HOr/G/ovo58+C8HBvRn
85vIKPReiey/qlHnuAVAgDleOcHgP7+K+PMYwx9HGR5dIlbJ71fp7+9fOi6nCMxdL+KmBYEl9Txc
CAz/3OXXyj9QGZLcl+cDlIvBarPI3g2QL6siG+CXOZygsisfeG/jxWH16n0MTy8Xq8TNawXgzVsE
PCQzcKg6/5xmGPSBCy//XDOsWIqdecCdH7lwttyvS0OvAWYO0ChWcKRWIFB/2YfKOYh9BQ0I1/tv
EScYuETgme/FGkkyFu9keGypWCUdGBEw6XoBz/Xxz83XKVb6DNy4wIWdefLOWb2P4e5PXCjws/MD
lTb7kzugViOiLzPL69uyDrKcyuwCIKDEAxzlWbKbYa+H9ODL9zI89HVNB5+RnQiv3lgZ9OMP+88A
g//nwuZjno9xuBjeXMfw72/EgHYThneorDjE8RFCnYq66bLyLskSACLWLzCLOErjEIE31npfVPvl
IMOIha4akXd3XElYeC8hxY88/ykxwFu3CejkIbvv4bMMgz5keHOdvLTknri1DeGFIOYjiHYEJsia
BsgTAMifU3BCw/I9TFZp7rwSuPX179iAsOR+34bXPZsQVo4S0Ody92/lRTtF3PS+dNESKe7vGJqI
xKiGQdZLW1IASuZmpTKGqwO3iKMkH8ko5nFhB8BT2rCGyYTF92nQvbH3nmbSAdP6C/joHgFp8TWP
LawAHv5GxBPLmOxyaO7QCMBzfQQ831fgi36B00nOdqCkAOhF1k3OcZzQcbgQ+CtXWgDGdBfQTiKn
X4IR+OgewWMZsaapwOL7BAxr7z7pyJoDDP3eFbHyn8De+unxwOdDNRjZifd8hdDYnJauUgfJKdFw
jQLGcBRk2W5ph5r0BMLD3eR1Jo0APN9XQKNUhmmrL/oRDG5LmNFfgMlNkGmxBZi6SsR3ewL3Fuzb
onJhMpE7mSsKY7gGwCpvx0gKAOMCEHb8clD6mPE9CEYfK/CM6EBokiJgzGIRIzsJGN/TvYCsOcAw
8XsRZ8p8a786KTGViUdvbs3f+sGBJPuu10eEmVvrrVTkZxkHTjAos0NykS0rEbhdRuUgd/RoQvhj
nMZt0k67C3jpJxH/28ICSiJCBAy6gvDcDYJfFYc5MmHowsyt9WTe43FlxqsAWOoUtycGnoMljNhz
UrqU96AryKekINVx1/kPFwKPLg58hb9JSmWkYo8m/K0fAkwVdc9eCeBPTwd4FQCBiZ0YT/0XVuw/
I90Bb7lC2TXbRTtFPLcqsBX+GB3wUFcBY7oTdHxJOWRoXEJn+CsAjKitz6VrOUElp9D79xkJQPO6
ylyrzA5MXiHi293+PwREwOA2Aib2JviWqoKjBIzQ1tv33peJmPeTOaGnsMJ7Z2ydpsyI7Xgx8OCX
Luw7438brdIIL/QldGzg3iabExAZ3O4ycBSjjbcvPQ7G2FfQAGiluDmcgCixev++mQJv/y3HGQZ9
6H/nN+mA8T0FfPuA4LHzrz/CMOA9F/rMd2H9ET7MDCJtvAUGeRwB2PIym0EDvkYbZkj51/tbcusC
S3YxPP2d/2m7+rUgmPt6Lit2pJBh2mqGnw9e/EWGLRRxaxvC8324L0AQiLWl1G8MHD/k7kuPAiBq
qC3xBYCwQ8pFVgjAgf6TrQzPr/IviCc1FpjeX0B/D4k7yuzA7HUiPtxcM8MQY5UhzBtyRLxxC6Fr
Q77wrCQucrUB4FYAPA4NiFjLoFnE8RupyjgVdv9E+50NDFNX+tf5B7Qk/DRa47Hz//APw3Vvi3h3
o/f0YidLGIZ+JuLNdaxGjgOO/wiMeZzKe1sEbBwEWzgBUieWAC8jM39y+/93LcPs33zvcYlG4OWB
gtvy4gBwpqwyD6AvcQIuEXhznYiNRwmzbyXUi+OjgcAhj33Z844s83wSRz0aJnvvTP+c9u0V/sFm
/zr/1VmE5Q9qPHb+FX8z9HnX5XeQ0MajDDe97znhCccHCI08feXFJYN5PImjHs3reH8jHiuSPwr4
dKuIaT/61vk1AjC+J+GbEQLqJ7k/5tnvRTyySERRhU9N1+BUKcPdn1YmO+UEhG8jAGaGFkBW0Mzh
+M0VGSRZHmuZDMedVf8wPLfKN5/+eAPw3p0CxvcUoPHy6njqWgEd68tv1xslVobhnwUeblzLaXB+
W78Gbm+jNTWtPuSFCnNCjElXWSPPG9/srFofoDp7TzE8/q1vC34NkwmL7tOgdzPpOXlKDPD5MA1G
dFBm/u4QgXFLGNYe4iLgJzprfqbbp8a9jjPykPGNEw5c39y7M31+CfDxFvdD+8IKYNRXviXr7NqQ
sOwBwScXY61QGfQzc4D/CUgvxeGqTDK67QQXAb8g1tDdx+6nAKTh5b/DmBtbuc/OcynzfmduvQbH
LBJl5RK8QJ/LCR8N8d9BZ0g7wgd3EeIUKCljcQCjvhZxsjTwtmobDHAr324FQABSg2sOJxAyEyoT
dHqj2OK+FNewDuR1/n4pN7cmzBssveYgRY8mhC+Ga/zKQlyds+XAE8vEgPIR1EbIQ592PwJg4KUY
whxPOfwuZeluhmXVUnYNaEl45UbppJv3tiO8OUiZ4TsAXJEOfDVcg9TYwNv6/QjDp1u5p5BPCO77
tFsB8KQWnPChV1PClZnSnXPichF/n6oqAre3JTzf13Pa7XvbEab3Vz4zb7M6wMf3aDxmKfaFl9Yw
nPBhKlPbIZAPIwAuAGHPhVJfUlQ4gAe+FJFfUvXzER0I0/pRjU5+Q3PCC/2Cl5O/dTow/w4hoIxF
QOXv9aZEYRTORTz1afe3gRgvAxYBdG5QWUFHirwS4J5PxRo1+oa2F6oM86/OIsy+NfDOKUW3RoQJ
vQK/yJLdDMeLFTCoFuCjAPAw4Ejh+T6CrEw7OYUMIz53ocRadTpwc2vC/DsIbTIq6wOEKjnHw90I
XQKM+nOJ4GsBMvGU29ODHwAU2LThhIIkEzB3sEbWYt2ek8B9X7AaItC7GeHbkRokhDAWXyDgxX6B
rzMs3sUCqkFYi3C78uJpHKbAMg0nVHRuADzXR15P2naC4Z5PWQ0/fTVKcTWvC9wUYE2AM2XAjjyu
ANIw+QLA+Agg4hjWnmS73u45ydyuCajBv7sGrjwbjypgSJRDILd92v02IHEBiETMfQXcfbW8DvXP
aYY7P3Kp7lXXMo2QLVnC0jvbZdRJrO0w36YA7tWCE94QATP6CxjgIUa/OocLgbs+9s01OBhcKyPA
yBuHCrgASEM+rQFwIhSNAMwaRBjYSl6nOlrEcMdHLuQUqteJrs4KTAByz4G7BvuJBwFgAdSA4aiN
TkOYPUjAbW3lday8EuDuT9QbCWQGOAWwOit/ON5gNnefeloE5AIQQZRYWQ13X40AvHaTgDtkFgk9
WQoM/UydhUFfqxi7w+JDeHNthAD5AkDEBSCSmL0euOsThs3HqoqAQMCrNwoYLnN34Eghw/CFIs5Z
QzueVuLtLTfCsbbCPIzqPf3Z3KoFJ/w4WsTw8Z8iSqyVKbVXVUudRQS80FfAsPbyRGDvKYbxS0Pr
XFMYYO5AAIjR8UUA75D8EQD4CCBieGMtg91V+f92FzB2sYilu2uKwIv95E8HfjnIMOu30HWoE8WB
XcuohWJhy1GMDwLAoIAmc4LN8WJg+d6qnccpVibMWLSzpgi8PFDAjTJ3B+b8JtaYUgSLvacCOz/R
xDu/FIxgcfe5p12As8E0hqMM720U4XQTC+MSgae+E/HNzpoLg28MEmQF4YisMpeAvzUC5cIY8Ofx
wISmMY9dlYQY3CaL95QQhAtAmGNxAEt3e/5eZMDT34n45WDVzqUTgHdu95zT/1IOFwL/2xxcBdh7
igW8/XhZXT4CkMJTn/aQEETgAhDmrPy7ZlRfdfQaoEW9mp0jyQS8c7tG1vbbnPUM5UFcEfpie+DT
jGY8gZ0kzBcBICb6UWGOE0qqL/S548EuAjIS3H/XOh0Y31N676zECny1IzhrAQXlqDFN8YfOPIm9
NOTDFED0cDAnPCi3V9bO84ZRCzzY2fvQ+IHOkJXr/7MgJd2Y9qMYsANPejxwOZ8CSCL6tAbAGBeA
AAnm+vm6wxe3/jwxqA0hSaKUuE5DmNZfOuf3wQJg/xkfDJTBsj0M38oYxUhxbTPpGgkcQGDuF/Y9
5QQ8FlRragE2Z/Ceyg050sfcIzMsuHMDeUPonw8oJ2lbjjM8s1yZUcVgGTkROQDTUI67z90KgPHs
qeMAeHhFAHirzRcom4967zzZiUDbDPkdY2h76bWAbQrF3K87zDDi88CH/kBlLoGODbgAyMBhqpuX
5+4Lt+vAZIbTMhcnwDzXFed4p9QWnEnAOSuTLP/dr6Vvab17X1YZPnwhpNakAwyXPBkJRiBFYjoh
hcPF8PaGSg9Dl0JLCnJjHDg4RnfC7aTR80YQYzkANQqKOVGOSwROlxGCsRKw7zQk/fS7ui0D6ZlY
PTBvcPCiaX49BMz8SVR0HaFZHeD2tsq1F+Uc8fSFZwEgygnqSlYUc7qs8o0XDPad9t6uQECH+kG5
tE+UWBlW7QM++lPEnpPKtz/lBg10AdYsrEX4IQBeTuJ4Z1d+8JQzp8j7yKJBMiHRGPqhcYmVYf8Z
YNdJYO1BERuPBi9Jx4CWhH81DU7bUQljOZ6+8igAjLE9BD7H8gdfk1TanQzHitx/VycOiLmkWMep
Uu9tV9/XL7MDf8m0p9TqfdIisso1iBIrnf8vkF8C7D8TuDuvXBokAy8P5A8P8RsAABVFSURBVM+l
LwgC7fH0nUcBEEjYxXiiNb9Yf8S3v9uBAqDnPPcb+5/eK6B744sP/Jky7203Sq767x25lXkClEWd
58KoBebeGtoCJtGAyFw7PX3nceXHUJB7EEAYZI6PLI4WMezKV669rGr58k6VeX/7pcVX/Xduifvj
Ig2dALx1u4C2mWpbEmEQSo1jT+V4+tqjAJAZIsA8Dh047vlmh3JtEQGZCVU7vNQIoE5s1ePzoqCE
tk4A5gwW0DvA9OG1EobdRJ6HbBJ7P7RLaXuimXNWho+2KDfcNmqr7sdbHJCMzKte3FMqYjDcSTIB
nw7VoF8L3vn9gpjH4T/gfRcAxLCT8b+7bN7eULkwphTVt7nkeBdWPyeSl3Fa1CO8czuhEU/44Tck
kpesERICIArCJmK8/LIcduYBCzYq29sM1e6OXYZvQZDcD0KKVgAe7EJ4vJcAPd/rDwgXsY3evvcq
ACZd2jarPb8CQIyiVkUZpTZg/FKX2/RcgWCt5i9v1EoPx2zVzok1BMcjMVh0a0SYfD2hdTofeipA
eczZ/L+8HeB1DYBGb3UA2KKoSVGGQwTGLBJxuFD5tsvsqOI3HyOjYmORpWpnb5Ds4cAw48pMwsdD
BCwcKvDOrxQMG8nsPahPRlIo+h1gPZWyKZq4kIZ73eHgvGEZqxxdXIjr1wqVgTne1hnyq237ycn9
pxY6AbjhcsKQdlTF14GjGL9LHSApAMTwO18IrMk5K/DQVy5sCnLmhOPFqJLYo34SYc9Jz4JzrLjq
v69IJ+gEBD27r1wMWqB7Y8J1lxH6tSCk8Mll0CAlBMDmpD/0OiaCVxKuwj2fiAHns5dDTiFDm0ti
+xsmw2twTXUnpHgD0LEBYUOOOusA6fGVcfvts4FODSodeZSoBciRxGUwGDdJHSR5KxIfP1FomZO5
DUAHRcyKEro1JlyVFfzrJFaLw2+TQVjxt+fOfLSIobACVd6swzpcTA/mFIEKBbP8mnSVW48mHaFO
LENaHCE1FmiUAjRJJcS7rUrPCTqETTT6sKQbmFwtXgkuAFWYcj0BKgRLtc3w/j1jwK8HGQZfUhq8
fwtC/5A40vC5YtjAaJWcw2QN65kIWY1xgk+H+iQ5hP5Rwfx9nMhEhLhSznGyBMCUkbcJvFpQWGDQ
Ap0lSnv9coDhnIIeiZwIg6Eg5mz+NjmHypoC0J1wWebgJwB3BWQYRxEGtiKsPeT5LW91At/sEPFA
Z3nrtkUV0lGDBMb35yMFolWVwXzSyF+PJfY9GHEBCAP6Xg5MXek9NuDjLcCIjpW+A1I8+Z2INRLT
hh5NCJ8M4QIQCTDGVsg9VvbWnlG0fQuADyzDgEQj4ebW3jvj0SJWo0S4O/48xiQ7PwAMkVlngKM6
VhOs38s9WLYA0LjCEkb40T+bOEpzX0fpijiz1omo8JJ/3yUCL6yWHimmxROub+6jgRxVINAKGlco
Ow2MT849JLIvfTeJEwxapxOuv8y7AuSVVIqAJz7awrBbRsbesd0JOg0fAUQCIsGnPuqTABgtjmUA
LD5ZxAkaj/ciaCTu4PubGHa6qQlzpJDhv79Kv/2zE4G7+fA/UqgwWZyyh/+AryOAZwpKCSRrf5ET
fFqmkeRKv1MExix2VQkgqgxiYiiT4RE46XoBOu4EHil8R0+d8imPp8+3VoS40NdzOMFjQk9Cw2Tv
b+jjxcC4pSIcLgbGgInfi14Dii7QrwVhQEv+9o8UBMLnPp/j6wkmfeYyAEGo9cLxB5MOeGmg9ILg
rwcZnl7OMHMNw2IZuwOpscC0fvzVH0Gc1OsyZG//XcDnO0yjtzqI8Imv53GCR7dGhPs7Sb+pl+xi
eG+j9LxfpyHMGyygbpwS1nFCAtEH5xP4+IR/Ei9q5iOS8kzVAiZfp1za7MnXE7pIuBtzwgpGRB/6
c6JfAmAcd/wQQOv8OZcTHDTnc+e3Sgus447qIuC+jrzzRxQMPxvHnDjoz6kBTPLYu/6fywkGsXrg
/bsEZCT4d/6DXQRMvp53/kiDCVjg77l+C4BRn/E1gBP+ns8JDhkJwNL7BbT0YSRABIzvKZzPccCJ
MHJNBcmL/T3ZbwE4v+Awz9/zOcEjLZ7wxTAB7bKlO7ROAP5zE2F8T975IxHGMIvMe/zO8RTQPo/V
qX8HQFkgbXCCQ6IR+OxeAX0u99yxsxOBRfdpcFtbvt0XkRBKbS79e4E0EdCdT56QU8xA7wfSBid4
mHTA/NsFTO9PNWoG9m9JWDFKw6vtRjDEMD95Qk6x9JFe2gjUCMsb6Y2gFQ7Al9wCnJBzvBiY8K2I
/WcYJvYWMKQdH/JHOA6IaGZ6LC+gxPSKPAWWOZmfARiiRFuc4OFwMVQ4CIlGtS3hBA772PRo/ohA
W1Fk8keiYAa8lyDiqI9Owzt/lOAiEmYo0ZBi40DHn7M3CGlXdVWqPQ6H4x7x9F+/6TqMU6Rcn2Lz
9lMlSVPSr75mjUYqQJ3D4fiNyyWi8NDBF5VqT9GVoPffX5dzXGzZUMk2ORzORRpo9h4eObJXU6Xa
U3Tl3pKz/q4/zqZvtDNeD4rDURq9YEVayobblWxT0fH62GmTNnWM37BeyTY5HE4lneI2/PrwtGe3
K9mm4hP2VqacW1N1BXxHgMNRkBRNgesy01HF63IoLgBDzOaCDrF/cO9ADkdBOsZveGu42Xxa6XaD
smRvNRQ80thwkMcIcDgK0Nh4sNRqODs+GG0HRQDMZrPYJmbrBJJXnozD4XiAIOLKmC1jzGZzUDpT
UB3Cn35q4e4/y65pHcxrcDjRTKf433a88urQq4LVflC9dlon7umboilwBfMaHE60kqorcDbR7u0f
zGsEVQBGTJmR2y1h3fRgXoPDiVa6xq1/ZfTMmfnBvEZIYkLHPbnk+K7ydtmhuBaHEw1cGbc1583X
BjcO9nVC4rjfIm7HwFhNKU8jrhAxRgHxMRd/BD/volLtAJAsTMKRT4K2RGwRu3NgKK4Vstv22qQ3
Xl5RNPiZUF0vWkmME/DF9CwY9Bdv3eiXTuLAcd/SwtVJ0mDhi5nQai+2M9ychxOn5flwEQHXto/B
dR1j0bKRHolxGjhdDGeKXNi814Jl68qQk+9znQoOgP7JS994euZjj4fiWiEL3Xtq5oSJ7eI37Q/V
9aKV4QMSq3R+fxl5U1KVzu8LSfEazH4iDVNG1kHXNiYkxWtABOi0hMy6WgzqFY93J6VjaP/EgO2s
bXSI++PvUHV+IIQCAABXxuztkabP8zuDaW2nW1sTBvWKD7id3h1i0LdLrF/nxhgFvP5YPbRu4j3g
S6shjLwpEcMHcBGQS5ou39E86dC1obxmSAVguNl8ukvs2ge0xEMFfKVrGxOmjqwT8Fy7V7sYPDM8
1e92hg9IQONMnfSB5xk2IBFNsuQfX1vRkhNdY38bPWry5FOhvG7Is3eMnznx024Jv6wO9XUjFa2W
8OAtSZg2um5AQ3+DjvDI7cl47oE60Pk59E9L0WLwtVVHIIwB7y4pxh3P5uLB6fnYvs9a5XuNAIy+
Ndlvu2sLPRJ/Xv7YS0/5Vd8vEFRJ30PGE/3bxPyVq8a1I4n2LYx4Z2I6hvRN8HuFngjo1NqE+c+m
4/be8QGNIK5tHwOtpmoD3/1Whi9+LMHZcy4cznPg+XcLUFpe1Wu1fQsjkhM0/l84ymlt2nFSNOTe
osa1VREAs9kstjZs6pihz7Wpcf1wx6gnzH82Ha+Nq4cmPgy3qxMfK2DB5Ay8PKYuGqQHPgzv1S6m
xmcr/6ga81VmEfHrtooqnwkC0OMqU8DXj0bS9Hn2dqadnYLl6y+Fagn8Rs+cmd8tZs1gk1DB/QOq
odcRLquvr/H5vqN2n7b7YgyC2/n67kM2n7fodFpCs2o2uUTgUG7NdvYcrqnrLRvxLFHVMQoW1j1x
zZ0jZ0w5rpYNqmbwHPvS1BXXJq56lYhrgDcYA5avL8P4/55Ccan/LwrGgMW/lOKJWadRWuFbO/Xr
aVE932t+gRNOZ817d+JMzUXeRhl8IfBSiBiuTfzhzbEvTvlWTTtUr+bz1MwJE6c8ndLz99J/8ZTi
bjhw3I53lxRj6z9W6YO9sPeIDfOXFGPXQf9mXfXdTCEKit3v5hQU1fy8fprqj1pY0S3+1/Wh3O/3
RFjcFW3Mke5Xi7EHt5d3DLrvcyTAGPD7TguWry/D5j0WMD8HSC6RYe32CixfX4atfwcmIAmxNQeL
Nrt7w+yOmp/HGAVoteR2xFDbuDJ22zGtKaeX2nYAKk8BLmA2m8UuFbvbXmb6p0htW8KB0goRU985
g027/e/8AFBQ7MIL7xUE3PkBwGRwIwBuOjoA2D24eZgMPGCgqWn/uQ5xO9qotehXnbAQAAC48y1z
WbuYzZ3TuadgWGJ044Pg7k0PAHYPb3l3IlKbqKc76eiU8GenoWZzidq2XCCs7si/p089cE3izwMS
tOfCQh05fuJh1CLU4gFAkrZIvC5xzQ0PmSeFVTxMWAkAAIx9cfKaPokr7+Lhw+GFu+G+J49Cvc79
5xXW2qnrMUI565mwcuhD0yatVduW6oSdAADAmOnPfNMn+YcHTYKFi0CYYLXV7LyeOrqnzy222nc7
TUIF65P63f0TZjz7udq2uCMsBQAAxk174oO+ScsfMZBFbVM4AMrcaLHehxGAzcHgqGU7AHqy4bqk
5Y8/9uIzH6ltiyfCVgAA4LEZT75zffKKqTx6UH3yztT0+EtJdO/fX8fN58dO1q7kIDpyoG/y8slP
zHjqTbVt8UZYCwAAPDnj8el9k7+dqiceNqAmx0/VFOHMulq3wUXZ9Wq6lxytRdmB9IIV/ZKXTX58
xuMz1bZFirAXAKBSBAakfDeWxw2oR5lFRH61ko8GHbl18XUX/7//WO3Y3TUKFtY38bsnI6HzAxEi
AADw2PQn5vVJ/HZ4nFDCRUAlNuysuR7Tr2tclX9rtYR/ta+abYgxYN1f0b+WEyuUsT6Jyx56fOaT
r6tti1wiRgCAymQi/ZOW38X9BNThl60VNT4bfG08eneIAVFl+PHTQ1NQL7nqGsDuwzacLozudZwk
bZHYJ3n57RNmPr1AbVt8ISxiAXzhkRnPfv3uVHb615J/rc63Z9WMmeUEjb1HbFi3vQI9r76YF0Aj
AFNG1sHTwxh0WqqxJsAYsGBpcYgtDS1punxH97g1A8dOn/yj2rb4SkSNAC7w0LRJa3vp17RqZtwX
3U9WGDLv6yK3b3O9rmbnByrDmHcdit4F3MbGg6W9U36+YuzMyOv8QIQKAACMfm3qoV6JGxpeFbcl
R21bahNnil14fNZpHMmTXtX//vcyzPqyMARWqUOb2G0neiT+1iDc3Ht9IaITtS3+9Vdbvz6t5mQL
5/octzeqr7Y9oaBxhg52J0N+gfP/f9bvqEBJuW/LIk2z9LDYqrazblsFKqzSa6ylFSJW/F6Oc+Ui
EuM0qJN08TGyOxg277Vi9ldFWPRzaUDRjOEKEcM1Cb+uj43dd+UEszmiVzejJjzjtSmzZv5S1Gei
RYyJmt8pUtBpCcnxApwuoKjUFZWd/gJGwcKuTfzhzXBI5qEEUdVZ5k2Z1u/30t5L8+3ZPAEdR3HS
9Hn2nvG/3PXI9ElL1bZFKaJKAABg/qRJGXtsnf/cVXFVltq2cKKHVqYdp9qbdnZUM4FnMIjYRUBP
jJ45Mz8lfnuDXgmrf+AxBJxA0ZIT1yatXl43YVtmtHV+IApHAJcya/Irw/4o67nglD2T+wtwfCZd
n2fvHLv2gfEzJ36qti3BIqoFAAAWmM0pByqa/by5tMeVatvCiRyuivvzcKukfd1CXasv1ES9AFzg
1WffeG1d6Q1PlLvia83vzPGdBG2JeE3CmlefnjH+WbVtCQW1qjO8Pcl8xX7HFd/+Vdaxidq2cMKP
NrHbTrSK29H332bzXrVtCRW1SgAu8PqkN8x/lPWcfNZRJ+JiITjKk6w56+qasPY/T82cMFFtW0JN
rRQAAPjYbK63v6LptxtKu3dh0bcZwpEBEUP72I1/N9PtvG70zJn5atujBrVWAC4we+rrI/8qvWrW
EVuzOOmjOdFCE+OBkrYxW8c+NuOZT9S2RU0iOhZACVauW739ln5NXmmlP5pQ5ErtaBHd1MDiRA3J
2gJXz8Q1C2Jj/+kxcdqUHWrboza1fgRwKQvN5jr7rdkfbyrt3s8qmvjfJorQC1Z0jN28sWHisUHR
vrXnC/whd8O8qS91zLE0/N9fFZ1bORlfJ4xktOTE1bGbdjcyHr3vkWnPblXbnnCDC4AX3ps8vct+
e7MPtpd1bOmKvORJtRotOXFF7PbDzXT7Hhwzc/IvatsTrnABkMHcSTNuOORo8e6usqsacSEIb7Tk
RNuY7Yea6PeN4h1fGi4APjBv8vTu+Y6Gr20vb9+5Qozlf7swwiRYWJvYLTub6XL+PWrGlI1q2xMp
8IfYDz6aPjnraFn9OTvLO9x81lm31u+kqEmStkhsE7vtt7ra3PsfnT71iNr2RBpcAALgq0fMcUcS
U186ZGt+70Hr5cmM8T9nKCBiaGb8p6iJ4cCnyYazE0ebzTXzlXNkwZ9YhZj73IzrTtszpu4tb9ud
jwqCQ5K2SGxt2rU5Q3tsEp/fKwMXAIWZbzbHnHMkTc21NRr6T0WrbDvj2ckCQS9Y0cK093i28din
dbRFL95vNlvVtima4AIQRBaYzSnFjqQJufbsIX+XX9HExkxqmxQR6MiO5qZ/TmUZjn6bgjxzbfXT
DwVcAELER9MnZ+VXpD170pE9MMfSrME5VyJ3Ob6ERM05sYlx/9E0Xf73SZQ/k3f60MAFQAV+MZu1
O+zGu4vFOkPy7VmdD1ubp9Q2j0OBXKivP1aRrT++M8Vw6pOrNWXvXms28ySOIYYLQBjw3owZaaVW
w20ljqQbCxxp7Y5Ym9aLNj8DvWBFtv54SYYuf2+S7uxP8fbS/41+beohte2q7UTVQxYtfGg2G0ts
sfeUs/j+hc6kNoXOutn5jqzYSAlQMgoWlqHLLU/VnTmerCneHas/932CYPmSL+CFHxHxQHEAs9ks
1LXrelUgpm+5K75duRjfoMSZWLfYlRxf6KyjC/UUQktOpGgLHEnawtIETcmZWKH0WIy2bGuSULEq
T2P/zWw28xLuEQAXgCjgK7NZf86hbVdBpvZ2UahvF/UZdhjS7aIh1S4akmzMFOdgWp2LaTROptcB
gJUZtIxpCACIXMxINicA6MhuF8gl6sjpMJClzKCxFenIVqiH7aResOfrBfF4DLNsTdQ5t91pNtvV
/L05HA6Hw+FwOBwOh8PhcDgcDocjwf8BnHZ+mE3sgIkAAAAASUVORK5CYII=
"/>
</svg>

Before

Width:  |  Height:  |  Size: 18 KiB

+12 -277
View File
@@ -149,7 +149,6 @@ SESSION_MAX_AGE = 86400 # 24 hours
# Sessions are persisted here so logins survive a restart of the Hub service. # Sessions are persisted here so logins survive a restart of the Hub service.
# nixos-rebuild switch restarts sovran-hub-web.service during activation (its # nixos-rebuild switch restarts sovran-hub-web.service during activation (its
# unit definition changes with every feature toggle — e.g. the Bitcoin # unit definition changes with every feature toggle — e.g. the Bitcoin
# Knots → Core switch changes services.bitcoind.package, which is on the Hub's
# PATH). Without persistence the browser session dies mid-rebuild, the # PATH). Without persistence the browser session dies mid-rebuild, the
# /api/rebuild/status polling starts receiving 401s and the rebuild modal # /api/rebuild/status polling starts receiving 401s and the rebuild modal
# hangs forever showing "Applying changes…". The file lives in # hangs forever showing "Applying changes…". The file lives in
@@ -318,18 +317,6 @@ FEATURE_REGISTRY = [
"conflicts_with": [], "conflicts_with": [],
"port_requirements": [], "port_requirements": [],
}, },
{
"id": "bitcoin-core",
"name": "Bitcoin Core",
"description": "Only one Bitcoin node implementation can be active: Bitcoin Knots + BIP110 (default) or Bitcoin Core. Enabling this replaces Knots + BIP110 with Bitcoin Core. Your timechain data is preserved.",
"category": "bitcoin",
"needs_domain": False,
"domain_name": None,
"needs_ddns": False,
"extra_fields": [],
"conflicts_with": [],
"port_requirements": [],
},
{ {
"id": "sshd", "id": "sshd",
"name": "SSH Remote Access", "name": "SSH Remote Access",
@@ -363,7 +350,7 @@ FEATURE_REGISTRY = [
# Feature ids that have been removed/deprecated. The Hub must never write these # Feature ids that have been removed/deprecated. The Hub must never write these
# back into custom.nix, and should strip any it finds (see startup migration). # back into custom.nix, and should strip any it finds (see startup migration).
DEPRECATED_FEATURE_IDS: set[str] = {"bip110"} DEPRECATED_FEATURE_IDS: set[str] = {"bitcoin-core"}
# Map feature IDs to their systemd units in config.json # Map feature IDs to their systemd units in config.json
FEATURE_SERVICE_MAP = { FEATURE_SERVICE_MAP = {
@@ -371,7 +358,6 @@ FEATURE_SERVICE_MAP = {
"haven": "haven-relay.service", "haven": "haven-relay.service",
"element-calling": "livekit.service", "element-calling": "livekit.service",
"mempool": "mempool.service", "mempool": "mempool.service",
"bitcoin-core": None,
"btcpay-web": "btcpayserver.service", "btcpay-web": "btcpayserver.service",
"nwc-wallets": "albyhub.service", "nwc-wallets": "albyhub.service",
"sshd": "sshd.service", "sshd": "sshd.service",
@@ -426,9 +412,7 @@ SERVICE_DOMAIN_MAP: dict[str, str] = {
} }
# For features that share a unit, disambiguate by icon field # For features that share a unit, disambiguate by icon field
FEATURE_ICON_MAP = { FEATURE_ICON_MAP: dict[str, str] = {}
"bitcoin-core": "bitcoin-core",
}
ROLE_LABELS = { ROLE_LABELS = {
"server_plus_desktop": "Server + Desktop", "server_plus_desktop": "Server + Desktop",
@@ -447,7 +431,7 @@ ROLE_CATEGORIES: dict[str, set[str] | None] = {
ROLE_FEATURES: dict[str, set[str] | None] = { ROLE_FEATURES: dict[str, set[str] | None] = {
"server_plus_desktop": None, "server_plus_desktop": None,
"desktop": {"rdp", "sshd"}, "desktop": {"rdp", "sshd"},
"node": {"rdp", "bitcoin-core", "mempool", "btcpay-web", "nwc-wallets", "sshd"}, "node": {"rdp", "mempool", "btcpay-web", "nwc-wallets", "sshd"},
} }
SERVICE_DESCRIPTIONS: dict[str, str] = { SERVICE_DESCRIPTIONS: dict[str, str] = {
@@ -2080,7 +2064,7 @@ def _is_feature_enabled_in_config(feature_id: str) -> bool | None:
return False # Default off in Node role; only on via explicit hub toggle return False # Default off in Node role; only on via explicit hub toggle
unit = FEATURE_SERVICE_MAP.get(feature_id) unit = FEATURE_SERVICE_MAP.get(feature_id)
if unit is None: if unit is None:
return None # bitcoin-core — can't determine from config return None
cfg = load_config() cfg = load_config()
for svc in cfg.get("services", []): for svc in cfg.get("services", []):
if svc.get("unit") == unit: if svc.get("unit") == unit:
@@ -2823,21 +2807,11 @@ BITCOIN_DATADIR = "/run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node"
_btc_sync_cache: tuple[float, dict | None] = (0.0, None) _btc_sync_cache: tuple[float, dict | None] = (0.0, None)
_BTC_SYNC_CACHE_TTL = 5 # seconds _BTC_SYNC_CACHE_TTL = 5 # seconds
_btc_version_cache: tuple[float, dict | None] = (0.0, None)
_BTC_VERSION_CACHE_TTL = 60 # seconds — version doesn't change at runtime _BTC_VERSION_CACHE_TTL = 60 # seconds — version doesn't change at runtime
# Cache for ``bitcoind --version`` output (available even before RPC is ready) # Cache for ``bitcoind --version`` output (available even before RPC is ready)
_btcd_version_cache: tuple[float, str | None] = (0.0, None) _btcd_version_cache: tuple[float, str | None] = (0.0, None)
# Cache for ``bitcoin-cli getdeploymentinfo`` output (BIP-110 live status)
_btc_deployment_cache: tuple[float, dict | None] = (0.0, None)
# Bitcoin Knots exposes BIP-110 as the `reduced_data` versionbits deployment
# (RDTS, bit 4) in getdeploymentinfo. See Knots src/deploymentinfo.cpp,
# src/kernel/chainparams.cpp, and doc/bips.md.
BIP110_DEPLOYMENT_NAMES = {"reduced_data", "rdts", "bip110", "uasf-bip110"}
BIP110_VERSIONBITS_BIT = 4
BIP110_SUBVERSION_MARKERS = {"bip110", "uasf-bip110", "reduced_data", "rdts"}
# ── Generic service version detection (NixOS store path) ───────── # ── Generic service version detection (NixOS store path) ─────────
@@ -2963,213 +2937,12 @@ def _get_service_version(unit: str) -> str | None:
return version return version
def _parse_bitcoin_subversion(subversion: str) -> str:
"""Parse a subversion string like '/Bitcoin Knots:27.1.0/' into 'v27.1.0'.
Examples:
'/Bitcoin Knots:27.1.0/''v27.1.0'
'/Satoshi:27.0.0/''v27.0.0'
'/Bitcoin Knots:27.1.0(bip110)/''v27.1.0 (bip110)'
Falls back to the raw subversion string if parsing fails.
"""
m = re.search(r":(\d+\.\d+(?:\.\d+)*)", subversion)
if m:
ver = "v" + m.group(1)
if "(bip110)" in subversion.lower():
ver += " (bip110)"
return ver
return subversion
def _get_bitcoin_version_info() -> dict | None:
"""Call bitcoin-cli getnetworkinfo and return parsed JSON, or None on error.
Results are cached for _BTC_VERSION_CACHE_TTL seconds since the version
does not change while the service is running.
"""
global _btc_version_cache
now = time.monotonic()
cached_at, cached_val = _btc_version_cache
if now - cached_at < _BTC_VERSION_CACHE_TTL:
return cached_val
try:
result = subprocess.run(
["bitcoin-cli", f"-datadir={BITCOIN_DATADIR}", "getnetworkinfo"],
capture_output=True,
text=True,
# This is a dashboard hint; do not make an unavailable RPC delay
# the entire service tile response.
timeout=3,
)
if result.returncode != 0:
_btc_version_cache = (now, None)
return None
info = json.loads(result.stdout)
_btc_version_cache = (now, info)
return info
except Exception:
_btc_version_cache = (now, None)
return None
def _get_bitcoin_deployment_info() -> dict | None:
"""Call bitcoin-cli getdeploymentinfo and return parsed JSON, or None on error.
Results are cached for _BTC_VERSION_CACHE_TTL seconds. Never raises.
"""
global _btc_deployment_cache
now = time.monotonic()
cached_at, cached_val = _btc_deployment_cache
if now - cached_at < _BTC_VERSION_CACHE_TTL:
return cached_val
try:
result = subprocess.run(
["bitcoin-cli", f"-datadir={BITCOIN_DATADIR}", "getdeploymentinfo"],
capture_output=True,
text=True,
# BIP-110 is supplementary tile metadata; an RPC timeout must not
# block the dashboard from rendering.
timeout=3,
)
if result.returncode != 0:
_btc_deployment_cache = (now, None)
return None
info = json.loads(result.stdout)
_btc_deployment_cache = (now, info)
return info
except Exception:
_btc_deployment_cache = (now, None)
return None
def _get_bip110_status() -> dict:
"""Return a dict describing the live BIP-110 deployment/signaling state.
The returned struct has four stable keys::
{
"supported": bool, # node build is BIP-110-capable
"signaling": bool, # node is actively signaling / locked-in / active
"state": str, # "active" | "locked_in" | "signaling" |
# "not_signaling" | "unsupported" | "unknown"
"source": str, # "getdeploymentinfo" | "subversion" | "none"
}
Resolution order (authoritative → fallback → honest unknown):
1. ``getdeploymentinfo`` (authoritative) — scan ``deployments`` for BIP-110.
Bitcoin Knots currently exposes BIP-110 as ``reduced_data`` (RDTS, bit 4;
see Knots deploymentinfo.cpp / chainparams.cpp / doc/bips.md), so matching
first uses known deployment names, then falls back to versionbits bit 4.
2. Subversion fallback — if getdeploymentinfo is unavailable or yields no
recognisable BIP-110 entry, inspect the ``subversion`` field from
``getnetworkinfo``. A case-insensitive match for known BIP-110 markers
(including "bip110", "uasf-bip110", "reduced_data", "rdts") is treated as
"signaling".
3. Unknown — if the node is entirely unreachable or neither source is
conclusive, return state="unknown", signaling=False, source="none".
"""
_unknown: dict = {"supported": False, "signaling": False, "state": "unknown", "source": "none"}
def _deployment_bit(entry: dict) -> int | None:
bip9 = entry.get("bip9", {}) or {}
bip8 = entry.get("bip8", {}) or {}
bit = bip9.get("bit")
if bit is None:
bit = bip8.get("bit")
if bit is None:
bit = entry.get("bit")
return bit
# ── 1. getdeploymentinfo (authoritative) ──────────────────────────
deploy_info = _get_bitcoin_deployment_info()
if deploy_info is not None:
deployments = deploy_info.get("deployments", {})
if isinstance(deployments, dict):
matched_entry: dict | None = None
# Primary match: known deployment names (case-insensitive exact match)
for key, entry in deployments.items():
if not isinstance(entry, dict):
continue
key_lower = key.lower()
if key_lower not in BIP110_DEPLOYMENT_NAMES:
continue
matched_entry = entry
break
# Secondary match: versionbits bit (fallback only)
if matched_entry is None:
for _, entry in deployments.items():
if not isinstance(entry, dict):
continue
if _deployment_bit(entry) != BIP110_VERSIONBITS_BIT:
continue
matched_entry = entry
break
if matched_entry is not None:
entry = matched_entry
# bip9 / bip8 status field
bip9 = entry.get("bip9", {}) or {}
bip8 = entry.get("bip8", {}) or {}
status = (
bip9.get("status")
or bip8.get("status")
or entry.get("status")
or ""
).lower()
active = entry.get("active", False)
if active or status == "active":
return {"supported": True, "signaling": True, "state": "active", "source": "getdeploymentinfo"}
if status == "locked_in":
return {"supported": True, "signaling": True, "state": "locked_in", "source": "getdeploymentinfo"}
if status in ("started", "defined"):
# Check whether deployment is currently signaling in this period.
stats = bip9.get("statistics") or bip8.get("statistics") or {}
# Some Knots outputs expose only ``count`` (not explicit signaling bool),
# so treat count>0 as a conservative signaling indicator for this period.
count = stats.get("count")
signaling = bool(
stats.get("signaling")
or stats.get("signalling")
or (isinstance(count, int) and count > 0)
)
if signaling:
return {"supported": True, "signaling": True, "state": "signaling", "source": "getdeploymentinfo"}
return {"supported": True, "signaling": False, "state": "not_signaling", "source": "getdeploymentinfo"}
if status == "failed":
return {"supported": True, "signaling": False, "state": "not_signaling", "source": "getdeploymentinfo"}
# Entry found but status unrecognised — node supports BIP-110 but state unclear
return {"supported": True, "signaling": False, "state": "unknown", "source": "getdeploymentinfo"}
# ── 2. Subversion fallback ─────────────────────────────────────────
net_info = _get_bitcoin_version_info()
if net_info is not None:
subversion = net_info.get("subversion", "") or ""
sv_lower = subversion.lower()
if any(marker in sv_lower for marker in BIP110_SUBVERSION_MARKERS):
return {"supported": True, "signaling": True, "state": "signaling", "source": "subversion"}
# Node is reachable via RPC but no BIP-110 marker found anywhere
return {"supported": False, "signaling": False, "state": "unsupported", "source": "subversion"}
# ── 3. Node unreachable / RPC not ready ───────────────────────────
return _unknown
def _get_bitcoind_version() -> str | None: def _get_bitcoind_version() -> str | None:
"""Run ``bitcoind --version`` and return the raw version string, or None on error. """Run ``bitcoind --version`` and return the raw version string, or None on error.
Parses the first output line to extract the token after "version ". Parses the first output line to extract the token after "version ".
For example: "Bitcoin Knots daemon version v29.3.knots20260508"
returns "v29.3.knots20260508".
Works regardless of whether the RPC server is ready (IBD, warmup, etc.). Works regardless of whether the RPC server is ready (IBD, warmup, etc.).
Results are cached for 60 seconds (_BTC_VERSION_CACHE_TTL). Results are cached for 60 seconds (_BTC_VERSION_CACHE_TTL).
""" """
@@ -3200,17 +2973,9 @@ def _get_bitcoind_version() -> str | None:
return None return None
def _format_bitcoin_version(raw_version: str, icon: str = "") -> str: def _format_bitcoin_version(raw_version: str) -> str:
"""Format a raw version string from ``bitcoind --version`` for tile display. """Format a raw version string from ``bitcoind --version`` for display."""
return raw_version
For the BIP110 tile (icon == "bip110") a " (bip110)" tag is appended,
since mainline Bitcoin Knots (29.3.knots20260508+) now includes BIP-110
and no longer carries a separate ``+bip110-vX.Y.Z`` suffix.
"""
display = raw_version
if icon == "bip110" and "(bip110)" not in display.lower():
display += " (bip110)"
return display
def _get_bitcoin_sync_info() -> dict | None: def _get_bitcoin_sync_info() -> dict | None:
@@ -3279,19 +3044,6 @@ async def api_bitcoin_version():
} }
@app.get("/api/bitcoin/bip110")
async def api_bitcoin_bip110():
"""Return live BIP-110 deployment/signaling status from bitcoin-cli.
Always returns HTTP 200. When bitcoind is unreachable or the node is mid-IBD
the response will contain ``state = "unknown"`` so the UI can render a neutral
badge rather than an error toast.
"""
loop = asyncio.get_event_loop()
status = await loop.run_in_executor(None, _get_bip110_status)
return status
@app.get("/api/services") @app.get("/api/services")
async def api_services(): async def api_services():
started_at = time.monotonic() started_at = time.monotonic()
@@ -3371,39 +3123,27 @@ async def api_services():
dns_states_future = asyncio.gather( dns_states_future = asyncio.gather(
*(resolve_domain(domain) for domain in sorted(domain_names)) *(resolve_domain(domain) for domain in sorted(domain_names))
) )
# Bitcoin sync and BIP-110 are supplementary tile metadata. Fetch each # Bitcoin sync is supplementary tile metadata. Fetch it once alongside
# once, concurrently with the other diagnostics, instead of doing duplicate # the other diagnostics instead of duplicating RPC calls per tile.
# RPC calls inside both bitcoind tile coroutines.
has_enabled_bitcoin = any( has_enabled_bitcoin = any(
entry.get("unit") == "bitcoind.service" and enabled entry.get("unit") == "bitcoind.service" and enabled
for entry, enabled in effective_entries for entry, enabled in effective_entries
) )
has_enabled_bip110 = any(
entry.get("icon") == "bip110" and enabled
for entry, enabled in effective_entries
)
bitcoin_sync_future = ( bitcoin_sync_future = (
loop.run_in_executor(None, _get_bitcoin_sync_info) loop.run_in_executor(None, _get_bitcoin_sync_info)
if has_enabled_bitcoin if has_enabled_bitcoin
else asyncio.sleep(0, result=None) else asyncio.sleep(0, result=None)
) )
bip110_future = (
loop.run_in_executor(None, _get_bip110_status)
if has_enabled_bip110
else asyncio.sleep(0, result=None)
)
( (
active_states, active_states,
port_states, port_states,
dns_states, dns_states,
bitcoin_sync_info, bitcoin_sync_info,
bip110_status,
) = await asyncio.gather( ) = await asyncio.gather(
active_states_future, active_states_future,
port_states_future, port_states_future,
dns_states_future, dns_states_future,
bitcoin_sync_future, bitcoin_sync_future,
bip110_future,
) )
listening_ports, firewall_ports = port_states listening_ports, firewall_ports = port_states
resolved_domains = dict(dns_states) resolved_domains = dict(dns_states)
@@ -3559,11 +3299,9 @@ async def api_services():
if unit == "bitcoind.service" and enabled: if unit == "bitcoind.service" and enabled:
raw_ver = await loop.run_in_executor(None, _get_bitcoind_version) raw_ver = await loop.run_in_executor(None, _get_bitcoind_version)
if raw_ver is not None: if raw_ver is not None:
btc_ver = _format_bitcoin_version(raw_ver, icon=icon) btc_ver = _format_bitcoin_version(raw_ver)
service_data["bitcoin_version"] = btc_ver # backwards compat service_data["bitcoin_version"] = btc_ver # backwards compat
service_data["version"] = btc_ver service_data["version"] = btc_ver
if icon == "bip110" and bip110_status is not None:
service_data["bip110"] = bip110_status
# ── Generic version for all services (Nix store path) ────────── # ── Generic version for all services (Nix store path) ──────────
if enabled and unit and "version" not in service_data: if enabled and unit and "version" not in service_data:
ver = await loop.run_in_executor(None, _get_service_version, unit) ver = await loop.run_in_executor(None, _get_service_version, unit)
@@ -3840,11 +3578,9 @@ async def api_service_detail(unit: str, icon: str | None = None):
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
raw_ver = await loop.run_in_executor(None, _get_bitcoind_version) raw_ver = await loop.run_in_executor(None, _get_bitcoind_version)
if raw_ver is not None: if raw_ver is not None:
btc_ver = _format_bitcoin_version(raw_ver, icon=icon) btc_ver = _format_bitcoin_version(raw_ver)
service_detail["bitcoin_version"] = btc_ver # backwards compat service_detail["bitcoin_version"] = btc_ver # backwards compat
service_detail["version"] = btc_ver service_detail["version"] = btc_ver
if icon == "bip110":
service_detail["bip110"] = await loop.run_in_executor(None, _get_bip110_status)
# ── Generic version for all services (Nix store path) ────────── # ── Generic version for all services (Nix store path) ──────────
if enabled and unit and "version" not in service_detail: if enabled and unit and "version" not in service_detail:
ver = await loop.run_in_executor(None, _get_service_version, unit) ver = await loop.run_in_executor(None, _get_service_version, unit)
@@ -6325,8 +6061,7 @@ async def _startup_recover_stale_status():
@app.on_event("startup") @app.on_event("startup")
async def _startup_migrate_deprecated_features(): async def _startup_migrate_deprecated_features():
"""Strip deprecated feature lines (e.g. bip110) from the Hub Managed section """Strip deprecated feature lines from the Hub Managed section of custom.nix."""
of custom.nix so they are never re-written and do not cause stale warnings."""
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
await loop.run_in_executor(None, _migrate_strip_deprecated_features) await loop.run_in_executor(None, _migrate_strip_deprecated_features)
@@ -178,68 +178,6 @@
white-space: nowrap; white-space: nowrap;
} }
/* ── BIP-110 status badge (tile + detail modal) ───────────────────── */
.tile-bip110-badge {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 0.64rem;
font-weight: 600;
border-radius: 4px;
padding: 2px 6px;
margin-top: 4px;
white-space: nowrap;
letter-spacing: 0.02em;
}
.tile-bip110-badge--active {
background: rgba(109, 191, 139, 0.18);
color: var(--green);
border: 1px solid rgba(109, 191, 139, 0.3);
}
.tile-bip110-badge--locked_in {
background: rgba(94, 173, 138, 0.15);
color: var(--accent-color);
border: 1px solid rgba(94, 173, 138, 0.3);
}
.tile-bip110-badge--signaling {
background: rgba(94, 173, 138, 0.12);
color: var(--accent-color);
border: 1px solid rgba(94, 173, 138, 0.2);
}
.tile-bip110-badge--not_signaling {
background: rgba(229, 165, 10, 0.12);
color: var(--yellow);
border: 1px solid rgba(229, 165, 10, 0.25);
}
.tile-bip110-badge--unsupported {
background: rgba(94, 122, 106, 0.12);
color: var(--grey);
border: 1px solid rgba(94, 122, 106, 0.2);
}
.tile-bip110-badge--unknown {
background: transparent;
color: var(--text-dim);
border: 1px solid var(--border-color);
}
.bip110-status-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.bip110-source-label {
color: var(--text-dim);
font-size: 0.75rem;
}
/* ── Service detail modal sections ───────────────────────────────── */ /* ── Service detail modal sections ───────────────────────────────── */
@@ -403,10 +403,7 @@ function handleFeatureToggle(feat, newEnabled) {
openPortRequirementsModal(feat.name, ports, proceedAfterPortCheck); openPortRequirementsModal(feat.name, ports, proceedAfterPortCheck);
} }
if (feat.id === "bitcoin-core") { if (conflictNames.length > 0) {
var confirmMsg = "Only one Bitcoin node implementation can be active. Enabling Bitcoin Core will replace Bitcoin Knots + BIP110 as the active node. Your timechain data will be preserved — you will not need to re-download the timechain. Continue?";
openFeatureConfirm(confirmMsg, proceedAfterConflictCheck);
} else if (conflictNames.length > 0) {
openFeatureConfirm("This will disable " + conflictNames.join(", ") + ". Continue?", proceedAfterConflictCheck); openFeatureConfirm("This will disable " + conflictNames.join(", ") + ". Continue?", proceedAfterConflictCheck);
} else { } else {
proceedAfterConflictCheck(); proceedAfterConflictCheck();
@@ -133,17 +133,3 @@ async function apiFetch(path, options) {
} }
return res.json(); return res.json();
} }
// ── BIP-110 badge state config ────────────────────────────────────
// Shared lookup used by tiles.js and service-detail.js.
// Keys match the "state" values returned by /api/bitcoin/bip110.
var BIP110_BADGE_CONFIG = {
active: { cls: 'tile-bip110-badge--active', label: 'Active', title: 'BIP-110 is active on this node' },
locked_in: { cls: 'tile-bip110-badge--locked_in', label: 'Locked In', title: 'BIP-110 is locked in and will activate shortly' },
signaling: { cls: 'tile-bip110-badge--signaling', label: 'Signaling', title: 'Node is signaling readiness for BIP-110' },
not_signaling: { cls: 'tile-bip110-badge--not_signaling',label: 'Not Signaling', title: 'Node supports BIP-110 but is not signaling this period' },
unsupported: { cls: 'tile-bip110-badge--unsupported', label: 'Not Supported', title: 'This node build does not include BIP-110' },
unknown: { cls: 'tile-bip110-badge--unknown', label: '\u2014', title: 'Status unavailable (node syncing or RPC not ready)' }
};
@@ -704,20 +704,6 @@ async function openServiceDetailModal(unit, name, icon) {
'</div>' + '</div>' +
'</div>'); '</div>');
// Section B2: BIP-110 live status (bip110 tile only)
if (icon === 'bip110' && data.bip110) {
var bip110 = data.bip110;
var bip110State = bip110.state || 'unknown';
var bip110Cfg = BIP110_BADGE_CONFIG[bip110State] || BIP110_BADGE_CONFIG.unknown;
var bip110Source = bip110.source ? ' <span class="bip110-source-label">(source: ' + escHtml(bip110.source) + ')</span>' : '';
html += '<div class="svc-detail-section">' +
'<div class="svc-detail-section-title">BIP-110 Deployment Status</div>' +
'<div class="bip110-status-row">' +
'<span class="tile-bip110-badge ' + bip110Cfg.cls + '" title="' + escHtml(bip110Cfg.title) + '">' + escHtml(bip110Cfg.label) + '</span>' +
bip110Source +
'</div>' +
'</div>';
}
// Section C: Domain diagnostics (domain services) // Section C: Domain diagnostics (domain services)
if (data.needs_domain) { if (data.needs_domain) {
@@ -901,9 +887,7 @@ async function openServiceDetailModal(unit, name, icon) {
var addonBtnCls = feat.enabled ? "btn btn-close-modal" : "btn btn-primary"; var addonBtnCls = feat.enabled ? "btn btn-close-modal" : "btn btn-primary";
// Section title: use a more specific label for mutually-exclusive Bitcoin node features // Section title: use a more specific label for mutually-exclusive Bitcoin node features
var addonSectionTitle = (feat.id === "bitcoin-core") var addonSectionTitle = "\uD83D\uDD27 Addon Feature";
? "\u20BF Bitcoin Node Selection"
: "\uD83D\uDD27 Addon Feature";
// Description: prefer the feature's own description over a generic fallback // Description: prefer the feature's own description over a generic fallback
var addonDesc = feat.description var addonDesc = feat.description
+1 -27
View File
@@ -4,14 +4,6 @@
// Keyed by tileId: { progress: float, timestamp: ms } // Keyed by tileId: { progress: float, timestamp: ms }
var _btcSyncPrev = {}; var _btcSyncPrev = {};
// ── BIP-110 badge helper ──────────────────────────────────────────
function _renderBip110Badge(bip110) {
if (!bip110) return '';
var state = bip110.state || 'unknown';
var cfg = BIP110_BADGE_CONFIG[state] || BIP110_BADGE_CONFIG.unknown;
return '<div class="tile-bip110-badge ' + cfg.cls + '" title="' + escHtml(cfg.title) + '">' + escHtml(cfg.label) + '</div>';
}
function _firstElementFromHtml(html) { function _firstElementFromHtml(html) {
var tmp = document.createElement("div"); var tmp = document.createElement("div");
@@ -175,8 +167,7 @@ function buildTile(svc) {
return tile; return tile;
} }
var bip110Badge = (svc.icon === 'bip110') ? _renderBip110Badge(svc.bip110) : ''; tile.innerHTML = '<img class="tile-icon" src="/static/icons/' + escHtml(svc.icon) + '.svg" alt="' + escHtml(svc.name) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'"><div class="tile-icon-fallback" style="display:none">?</div><div class="tile-name">' + escHtml(svc.name) + '</div><div class="tile-status"><span class="status-dot ' + sc + '"></span><span class="status-text">' + st + '</span></div>';
tile.innerHTML = '<img class="tile-icon" src="/static/icons/' + escHtml(svc.icon) + '.svg" alt="' + escHtml(svc.name) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'"><div class="tile-icon-fallback" style="display:none">?</div><div class="tile-name">' + escHtml(svc.name) + '</div>' + bip110Badge + '<div class="tile-status"><span class="status-dot ' + sc + '"></span><span class="status-text">' + st + '</span></div>';
tile.style.cursor = "pointer"; tile.style.cursor = "pointer";
tile.addEventListener("click", function() { tile.addEventListener("click", function() {
@@ -244,23 +235,6 @@ function updateTiles(services) {
var text = tile.querySelector(".status-text"); var text = tile.querySelector(".status-text");
if (dot) dot.className = "status-dot " + sc; if (dot) dot.className = "status-dot " + sc;
if (text) text.textContent = st; if (text) text.textContent = st;
// Update BIP-110 badge for bip110 tiles
if (svc.icon === 'bip110') {
var badgeHtml = _renderBip110Badge(svc.bip110);
var badgeEl = tile.querySelector(".tile-bip110-badge");
if (badgeEl) {
// Replace existing badge in-place
var newBadge = _firstElementFromHtml(badgeHtml);
if (newBadge) { badgeEl.replaceWith(newBadge); } else { badgeEl.remove(); }
} else if (badgeHtml) {
// Insert badge after the service name
var anchorEl = tile.querySelector(".tile-name");
if (anchorEl) {
var newBadgeEl = _firstElementFromHtml(badgeHtml);
if (newBadgeEl) anchorEl.insertAdjacentElement("afterend", newBadgeEl);
}
}
}
} }
} }
} }
-7
View File
@@ -1,7 +0,0 @@
{ config, pkgs, lib, ... }:
lib.mkIf config.sovran_systemsOS.features.bitcoin-core {
# Vendored nix-bitcoin now uses nixpkgs directly; no nix-bitcoin.pkgs indirection.
# Use standard bitcoind from nixpkgs (override to knots if desired via pkgs.bitcoind-knots)
services.bitcoind.package = lib.mkForce pkgs.bitcoind;
}
-5
View File
@@ -62,11 +62,6 @@ let
description = '' description = ''
The package providing bitcoind binaries. The package providing bitcoind binaries.
You can use this option to select other bitcoind-compatible implementations.
Example:
```nix
services.bitcoind.package = pkgs.bitcoind-knots;
```
''; '';
}; };
extraConfig = mkOption { extraConfig = mkOption {
+1 -1
View File
@@ -4,7 +4,7 @@ lib.mkIf config.sovran_systemsOS.services.bitcoin {
services.bitcoind = { services.bitcoind = {
enable = true; enable = true;
package = pkgs.bitcoind-knots; package = pkgs.bitcoind;
dataDir = "/run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node"; dataDir = "/run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node";
txindex = true; txindex = true;
tor.proxy = true; tor.proxy = true;
-1
View File
@@ -33,7 +33,6 @@
haven = lib.mkForce false; haven = lib.mkForce false;
mempool = lib.mkForce false; mempool = lib.mkForce false;
element-calling = lib.mkForce false; element-calling = lib.mkForce false;
bitcoin-core = lib.mkForce false;
"nwc-wallets" = lib.mkForce false; "nwc-wallets" = lib.mkForce false;
}; };
+8 -22
View File
@@ -45,23 +45,19 @@
haven = lib.mkEnableOption "Haven NOSTR relay"; haven = lib.mkEnableOption "Haven NOSTR relay";
mempool = lib.mkEnableOption "Bitcoin Mempool Explorer"; mempool = lib.mkEnableOption "Bitcoin Mempool Explorer";
element-calling = lib.mkEnableOption "Element Video and Audio Calling"; element-calling = lib.mkEnableOption "Element Video and Audio Calling";
bitcoin-core = lib.mkEnableOption "Bitcoin Core"; # Compatibility shim for Hub-managed settings from releases where Core
"nwc-wallets" = lib.mkEnableOption "Lightning Wallet Connections"; # was an optional replacement for the default node. Core is now always
rdp = lib.mkEnableOption "Gnome Remote Desktop"; # selected when the Bitcoin service is enabled.
sshd = lib.mkEnableOption "SSH remote access"; bitcoin-core = lib.mkOption {
# Deprecated: BIP-110 is now built into mainline Bitcoin Knots and is the
# default node. This option is retained ONLY so that existing machines with
# `sovran_systemsOS.features.bip110 = lib.mkForce true;` left in their local
# custom.nix continue to evaluate. It has no effect and will be removed in a
# future release once the Hub has cleaned up old custom.nix files.
bip110 = lib.mkOption {
type = lib.types.nullOr lib.types.bool; type = lib.types.nullOr lib.types.bool;
default = null; default = null;
internal = true; internal = true;
visible = false; visible = false;
description = "(Deprecated, no-op) BIP-110 is now built into Bitcoin Knots."; description = "Deprecated no-op: Bitcoin Core is the default node implementation.";
}; };
"nwc-wallets" = lib.mkEnableOption "Lightning Wallet Connections";
rdp = lib.mkEnableOption "Gnome Remote Desktop";
sshd = lib.mkEnableOption "SSH remote access";
}; };
# ── Web exposure (controls Caddy vhosts) ────────────────── # ── Web exposure (controls Caddy vhosts) ──────────────────
@@ -103,14 +99,4 @@
}; };
}; };
config = lib.mkIf (config.sovran_systemsOS.features.bip110 != null) {
warnings = [
''
sovran_systemsOS.features.bip110 is deprecated and has no effect:
BIP-110 is now built into mainline Bitcoin Knots, which is the default node.
You can safely remove the `sovran_systemsOS.features.bip110` line from
/etc/nixos/custom.nix. The Sovran Hub will also remove it automatically.
''
];
};
} }
+4 -7
View File
@@ -37,12 +37,9 @@ let
{ label = "How to Connect"; value = "1. Install an RDP client (e.g. Remmina, Microsoft Remote Desktop)\n2. Create a new RDP connection\n3. Enter the Address above as the host\n4. Enter the Username and Password above"; } { label = "How to Connect"; value = "1. Install an RDP client (e.g. Remmina, Microsoft Remote Desktop)\n2. Create a new RDP connection\n3. Enter the Address above as the host\n4. Enter the Username and Password above"; }
]; } ]; }
] ]
# ── Bitcoin Base (node implementations) ──────────────────── # ── Bitcoin Base ────────────────────────────────────────────
++ lib.optionals cfg.services.bitcoin [ ++ lib.optionals cfg.services.bitcoin [
{ name = "Bitcoin Knots + BIP110"; unit = "bitcoind.service"; type = "system"; icon = "bip110"; enabled = cfg.services.bitcoin && !cfg.features.bitcoin-core; category = "bitcoin-base"; credentials = [ { name = "Bitcoin Core"; unit = "bitcoind.service"; type = "system"; icon = "bitcoin-core"; enabled = cfg.services.bitcoin; category = "bitcoin-base"; credentials = [
{ label = "Tor Address Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/bitcoind/hostname"; prefix = "http://"; }
]; }
{ name = "Bitcoin Core"; unit = "bitcoind.service"; type = "system"; icon = "bitcoin-core"; enabled = cfg.features.bitcoin-core; category = "bitcoin-base"; credentials = [
{ label = "Tor Address Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/bitcoind/hostname"; prefix = "http://"; } { label = "Tor Address Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/bitcoind/hostname"; prefix = "http://"; }
]; } ]; }
] ]
@@ -132,7 +129,7 @@ let
"caddy.service" = if pkgs ? caddy then pkgs.caddy.version else "2.8.4"; "caddy.service" = if pkgs ? caddy then pkgs.caddy.version else "2.8.4";
"tor.service" = if pkgs ? tor then pkgs.tor.version else "0.4.8.12"; "tor.service" = if pkgs ? tor then pkgs.tor.version else "0.4.8.12";
"gnome-remote-desktop.service" = if pkgs ? gnome-remote-desktop then pkgs.gnome-remote-desktop.version else "46.0"; "gnome-remote-desktop.service" = if pkgs ? gnome-remote-desktop then pkgs.gnome-remote-desktop.version else "46.0";
"bitcoind.service" = if pkgs ? bitcoind-knots then pkgs.bitcoind-knots.version else (if pkgs ? bitcoind then pkgs.bitcoind.version else "27.1.0"); "bitcoind.service" = if pkgs ? bitcoind then pkgs.bitcoind.version else "27.1.0";
"electrs.service" = if pkgs ? electrs then pkgs.electrs.version else "0.10.6"; "electrs.service" = if pkgs ? electrs then pkgs.electrs.version else "0.10.6";
"lnd.service" = if pkgs ? lnd then pkgs.lnd.version else "0.18.0"; "lnd.service" = if pkgs ? lnd then pkgs.lnd.version else "0.18.0";
# Keep the fallbacks aligned with the vendored packages used by the # Keep the fallbacks aligned with the vendored packages used by the
@@ -244,7 +241,7 @@ let
# Stream output straight into $LOG (tee'd by the exec redirect above) so # Stream output straight into $LOG (tee'd by the exec redirect above) so
# the Hub UI shows live progress. Capturing the output in a variable # the Hub UI shows live progress. Capturing the output in a variable
# kept the log empty for the entire build+activation, which made long # kept the log empty for the entire build+activation, which made long
# rebuilds (e.g. the Bitcoin Knots Core switch) look like a hang. # rebuilds can otherwise look like a hang.
nixos-rebuild switch --flake /etc/nixos --print-build-logs \ nixos-rebuild switch --flake /etc/nixos --print-build-logs \
--option connect-timeout 10 \ --option connect-timeout 10 \
--option stalled-download-timeout 90 \ --option stalled-download-timeout 90 \
-1
View File
@@ -35,7 +35,6 @@
./nwc-wallets.nix ./nwc-wallets.nix
./element-calling.nix ./element-calling.nix
./mempool.nix ./mempool.nix
./bitcoin-core.nix
./rdp.nix ./rdp.nix
./sshd.nix ./sshd.nix
]; ];