diff --git a/.github/workflows/buildService.yml b/.github/workflows/buildService.yml index 9ab20c4a..7673490f 100644 --- a/.github/workflows/buildService.yml +++ b/.github/workflows/buildService.yml @@ -4,29 +4,30 @@ on: workflow_dispatch: pull_request: paths-ignore: ['*.md'] - branches: ['main', 'master'] + branches: ['main', 'master', 'update/040'] push: paths-ignore: ['*.md'] - branches: ['main', 'master'] + branches: ['main', 'master', 'update/040'] jobs: BuildPackage: runs-on: ubuntu-latest steps: - name: Prepare StartOS SDK - uses: Start9Labs/sdk@v1 + uses: start9labs/sdk@v2 - name: Checkout services repository uses: actions/checkout@v4 + with: + submodules: recursive - name: Build the service package id: build run: | - git submodule update --init --recursive - start-sdk init - make - PACKAGE_ID=$(yq -oy ".id" manifest.*) - echo "package_id=$PACKAGE_ID" >> $GITHUB_ENV + start-cli init + RUST_LOG=debug RUST_BACKTRACE=1 make + PACKAGE_ID=$(start-cli s9pk inspect *.s9pk manifest | jq -r '.id') + echo "package_id=${PACKAGE_ID}" >> $GITHUB_ENV printf "\n SHA256SUM: $(sha256sum ${PACKAGE_ID}.s9pk) \n" shell: bash diff --git a/.github/workflows/releaseService.yml b/.github/workflows/releaseService.yml index 6cf91f24..33f4fe4d 100644 --- a/.github/workflows/releaseService.yml +++ b/.github/workflows/releaseService.yml @@ -12,39 +12,49 @@ jobs: contents: write steps: - name: Prepare StartOS SDK - uses: Start9Labs/sdk@v1 + uses: start9labs/sdk@v2 - name: Checkout services repository uses: actions/checkout@v4 + with: + submodules: recursive - name: Build the service package + id: build + env: + S9DEVKEY: ${{ secrets.S9DEVKEY }} run: | - git submodule update --init --recursive - start-sdk init - make - - - name: Setting package ID and title from the manifest - id: package - run: | - echo "package_id=$(yq -oy ".id" manifest.*)" >> $GITHUB_ENV - echo "package_title=$(yq -oy ".title" manifest.*)" >> $GITHUB_ENV + start-cli init + if [[ -n "$S9DEVKEY" ]]; then + echo "Using developer key from secrets to sign the package." + printf '%s' "$S9DEVKEY" > ~/.startos/developer.key.pem + else + echo "Using newly generated developer key to sign the package." + fi + RUST_LOG=debug RUST_BACKTRACE=1 make + sleep 2 + MANIFEST_JSON=$(start-cli s9pk inspect *.s9pk manifest) + PACKAGE_ID=$(echo "$MANIFEST_JSON" | jq -r '.id') + PACKAGE_TITLE=$(echo "$MANIFEST_JSON" | jq -r '.title') + echo "package_id=${PACKAGE_ID}" >> $GITHUB_ENV + echo "package_title=${PACKAGE_TITLE}" >> $GITHUB_ENV + printf "\n SHA256SUM: $(sha256sum ${PACKAGE_ID}.s9pk) \n" shell: bash - name: Generate sha256 checksum run: | - PACKAGE_ID=${{ env.package_id }} - printf "\n SHA256SUM: $(sha256sum ${PACKAGE_ID}.s9pk) \n" - sha256sum ${PACKAGE_ID}.s9pk > ${PACKAGE_ID}.s9pk.sha256 + sha256sum ${{ env.package_id }}.s9pk > ${{ env.package_id }}.s9pk.sha256 shell: bash - name: Generate changelog run: | - PACKAGE_ID=${{ env.package_id }} echo "## What's Changed" > change-log.txt - yq -oy '.release-notes' manifest.* >> change-log.txt + echo "" >> change-log.txt + RELEASE_NOTES=$(start-cli s9pk inspect *.s9pk manifest | jq -r '.releaseNotes') + echo "${RELEASE_NOTES}" >> change-log.txt echo "## SHA256 Hash" >> change-log.txt echo '```' >> change-log.txt - sha256sum ${PACKAGE_ID}.s9pk >> change-log.txt + sha256sum ${{ env.package_id }}.s9pk >> change-log.txt echo '```' >> change-log.txt shell: bash @@ -61,12 +71,13 @@ jobs: - name: Publish to Registry env: - S9USER: ${{ secrets.S9USER }} - S9PASS: ${{ secrets.S9PASS }} + S9DEVKEY: ${{ secrets.S9DEVKEY }} S9REGISTRY: ${{ secrets.S9REGISTRY }} run: | - if [[ -z "$S9USER" || -z "$S9PASS" || -z "$S9REGISTRY" ]]; then - echo "Publish skipped: missing registry credentials." + if [[ -z "$S9DEVKEY" || -z "$S9REGISTRY" ]]; then + echo "Publish skipped: One or both of S9DEVKEY and S9REGISTRY secrets are not set." else - start-sdk publish https://$S9USER:$S9PASS@$S9REGISTRY ${{ env.package_id }}.s9pk + echo "Publishing package to registry..." + start-cli --registry https://$S9REGISTRY registry package add ${{ env.package_id }}.s9pk ${{ github.server_url }}/${{ github.repository }}/releases/download/${{ github.ref_name }}/${{ env.package_id }}.s9pk fi + shell: bash \ No newline at end of file diff --git a/.gitignore b/.gitignore index 810e8544..4dad48cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ -manager/target/ -**/*.rs.bk *.s9pk +startos/*.js +node_modules/ .DS_Store .vscode/ -scripts/embassy.js -docker-images/ +docker-images +javascript +ncc-cache \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index fabdfe50..b8ab1ecf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "bitcoin"] path = bitcoin - url = https://github.com/bitcoin/bitcoin + url = git@github.com:bitcoin/bitcoin.git diff --git a/Dockerfile b/Dockerfile index 6b65b85d..886632ee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,9 @@ -# From https://github.com/ruimarinho/docker-bitcoin-core - -# Build stage for BerkeleyDB -ARG PLATFORM - -FROM lncm/berkeleydb:db-4.8.30.NC-${PLATFORM} AS berkeleydb - # Build stage for Bitcoin Core FROM alpine:3.21 AS bitcoin-core -COPY --from=berkeleydb /opt /opt - RUN sed -i 's/http\:\/\/dl-cdn.alpinelinux.org/https\:\/\/alpine.global.ssl.fastly.net/g' /etc/apk/repositories RUN apk --no-cache add \ - autoconf \ + cmake \ automake \ boost-dev \ build-base \ @@ -25,7 +16,9 @@ RUN apk --no-cache add \ libtool \ linux-headers \ sqlite-dev \ - zeromq-dev + zeromq-dev \ + bash \ + curl ADD ./bitcoin /bitcoin @@ -33,33 +26,35 @@ ENV BITCOIN_PREFIX=/opt/bitcoin WORKDIR /bitcoin -RUN ./autogen.sh -RUN ./configure LDFLAGS=-L`ls -d /opt/db*`/lib/ CPPFLAGS=-I`ls -d /opt/db*`/include/ \ +RUN make -C depends NO_BOOST=1 NO_LIBEVENT=1 NO_QT=1 NO_SQLITE=1 NO_UPNP=1 NO_ZMQ=1 NO_USDT=1 + +RUN cmake -B build -DCMAKE_LD_FLAGS=-L`ls -d /opt/db*`/lib/ -DCMAKE_CPP_FLAGS=-I`ls -d /opt/db*`/include/ \ # If building on Mac make sure to increase Docker VM memory, or uncomment this line. See https://github.com/bitcoin/bitcoin/issues/6658 for more info. # CXXFLAGS="--param ggc-min-expand=1 --param ggc-min-heapsize=32768" \ - CXXFLAGS="-O1" \ - CXX=clang++ CC=clang \ - --prefix=${BITCOIN_PREFIX} \ - --disable-man \ - --disable-tests \ - --disable-bench \ - --disable-ccache \ - --with-gui=no \ - --with-utils \ - --with-libs \ - --with-sqlite=yes \ - --with-daemon -RUN make -j$(nproc) -RUN make install + -DENABLE_IPC=OFF \ + -DCMAKE_CXX_FLAGS="-O2" \ + -DCMAKE_CXX=clang++ CC=clang \ + -DCMAKE_INSTALL_PREFIX=${BITCOIN_PREFIX} \ + -DINSTALL_MAN=OFF \ + -DBUILD_TESTS=OFF \ + -DBUILD_BENCH=OFF \ + -DWITH_CCACHE=OFF \ + -DBUILD_GUI=OFF \ + #--with-utils \ + -DBUILD_CLI=ON \ + -DBUILD_BITCOINCONSENSUS_LIB=ON \ + -DWITH_SQLITE=ON \ + -DBUILD_DAEMON=ON \ + -DENABLE_HARDENING=ON \ + -DREDUCE_EXPORTS=ON \ + -DWITH_ZMQ=ON +RUN cmake --build build -j$(nproc) +RUN cmake --install build RUN strip ${BITCOIN_PREFIX}/bin/* # Build stage for compiled artifacts FROM alpine:3.21 -LABEL maintainer.0="JoΓ£o Fonseca (@joaopaulofonseca)" \ - maintainer.1="Pedro Branco (@pedrobranco)" \ - maintainer.2="Rui Marinho (@ruimarinho)" \ - maintainer.3="Aiden McClelland (@dr-bonez)" RUN sed -i 's/http\:\/\/dl-cdn.alpinelinux.org/https\:\/\/alpine.global.ssl.fastly.net/g' /etc/apk/repositories RUN apk --no-cache add \ @@ -69,7 +64,8 @@ RUN apk --no-cache add \ libzmq \ sqlite-dev \ tini \ - yq + yq \ + jq \ RUN rm -rf /var/cache/apk/* ARG ARCH @@ -79,17 +75,5 @@ ENV BITCOIN_PREFIX=/opt/bitcoin ENV PATH=${BITCOIN_PREFIX}/bin:$PATH COPY --from=bitcoin-core /opt /opt -COPY ./manager/target/${ARCH}-unknown-linux-musl/release/bitcoind-manager \ - ./docker_entrypoint.sh \ - ./actions/reindex.sh \ - ./actions/reindex_chainstate.sh \ - ./check-rpc.sh \ - ./check-synced.sh \ - /usr/local/bin/ - -RUN chmod a+x /usr/local/bin/bitcoind-manager \ - /usr/local/bin/*.sh - -EXPOSE 8332 8333 -ENTRYPOINT ["/usr/local/bin/docker_entrypoint.sh"] +EXPOSE 8332 8333 \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 120000 index 62c0d5aa..00000000 --- a/LICENSE +++ /dev/null @@ -1 +0,0 @@ -./bitcoin/COPYING \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..fa7c00bc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2011-2025 The Bootstrap Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Makefile b/Makefile index bb7a18de..d4a54d61 100644 --- a/Makefile +++ b/Makefile @@ -1,67 +1,88 @@ -PKG_VERSION := $(shell yq e ".version" manifest.yaml) -PKG_ID := $(shell yq e ".id" manifest.yaml) -MANAGER_SRC := $(shell find ./manager -name '*.rs') manager/Cargo.toml manager/Cargo.lock -VERSION_CORE := $(shell (cd bitcoin && git describe) | sed 's/^v//') +PACKAGE_ID := $(shell awk -F"'" '/id:/ {print $$2}' startos/manifest.ts) +INGREDIENTS := $(shell start-cli s9pk list-ingredients 2>/dev/null) +CMD_ARCH_GOAL := $(filter aarch64 x86_64 arm x86, $(MAKECMDGOALS)) +ifeq ($(CMD_ARCH_GOAL),) + BUILD := universal + S9PK := $(PACKAGE_ID).s9pk +else + RAW_ARCH := $(firstword $(CMD_ARCH_GOAL)) + ACTUAL_ARCH := $(subst x86,x86_64,$(subst arm,aarch64,$(RAW_ARCH))) + BUILD := $(ACTUAL_ARCH) + S9PK := $(PACKAGE_ID)_$(BUILD).s9pk +endif + +.PHONY: all aarch64 x86_64 arm x86 clean install check-deps check-init package ingredients .DELETE_ON_ERROR: -all: verify +define SUMMARY + @manifest=$$(start-cli s9pk inspect $(1) manifest); \ + size=$$(du -h $(1) | awk '{print $$1}'); \ + title=$$(printf '%s' "$$manifest" | jq -r .title); \ + version=$$(printf '%s' "$$manifest" | jq -r .version); \ + arches=$$(printf '%s' "$$manifest" | jq -r '.hardwareRequirements.arch | join(", ")'); \ + sdkv=$$(printf '%s' "$$manifest" | jq -r .sdkVersion); \ + gitHash=$$(printf '%s' "$$manifest" | jq -r .gitHash | sed -E 's/(.*-modified)$$/\x1b[0;31m\1\x1b[0m/'); \ + printf "\n"; \ + printf "\033[1;32mβœ… Build Complete!\033[0m\n"; \ + printf "\n"; \ + printf "\033[1;37mπŸ“¦ $$title\033[0m \033[36mv$$version\033[0m\n"; \ + printf "───────────────────────────────\n"; \ + printf " \033[1;36mFilename:\033[0m %s\n" "$(1)"; \ + printf " \033[1;36mSize:\033[0m %s\n" "$$size"; \ + printf " \033[1;36mArch:\033[0m %s\n" "$$arches"; \ + printf " \033[1;36mSDK:\033[0m %s\n" "$$sdkv"; \ + printf " \033[1;36mGit:\033[0m %s\n" "$$gitHash"; \ + echo "" +endef -clean: - rm -f $(PKG_ID).s9pk - rm -f docker-images/*.tar - rm -f scripts/*.js +all: $(PACKAGE_ID).s9pk + $(call SUMMARY,$(S9PK)) -verify: $(PKG_ID).s9pk - @start-sdk verify s9pk $(PKG_ID).s9pk - @echo " Done!" - @echo " Filesize: $(shell du -h $(PKG_ID).s9pk) is ready" +$(BUILD): $(PACKAGE_ID)_$(BUILD).s9pk + $(call SUMMARY,$(S9PK)) -# for rebuilding just the arm image. -arm: - @rm -f docker-images/x86_64.tar - @ARCH=aarch64 $(MAKE) -s +x86: x86_64 +arm: aarch64 -# for rebuilding just the x86 image. -x86: - @rm -f docker-images/aarch64.tar - @ARCH=x86_64 $(MAKE) -s +$(S9PK): $(INGREDIENTS) .git/HEAD .git/index + @$(MAKE) --no-print-directory ingredients + @echo " Packing '$(S9PK)'..." + BUILD=$(BUILD) start-cli s9pk pack -o $(S9PK) -$(PKG_ID).s9pk: manifest.yaml assets/compat/* docker-images/aarch64.tar docker-images/x86_64.tar instructions.md scripts/embassy.js -ifeq ($(ARCH),aarch64) - @echo "start-sdk: Preparing aarch64 package ..." -else ifeq ($(ARCH),x86_64) - @echo "start-sdk: Preparing x86_64 package ..." -else - @echo "start-sdk: Preparing Universal Package ..." -endif - @start-sdk pack +ingredients: $(INGREDIENTS) + @echo " Re-evaluating ingredients..." -install: - @if [ ! -f ~/.embassy/config.yaml ]; then echo "You must define \"host: http://server-name.local\" in ~/.embassy/config.yaml config file first."; exit 1; fi - @echo "\nInstalling to $$(grep -v '^#' ~/.embassy/config.yaml | cut -d'/' -f3) ...\n" - @[ -f $(PKG_ID).s9pk ] || ( $(MAKE) && echo "\nInstalling to $$(grep -v '^#' ~/.embassy/config.yaml | cut -d'/' -f3) ...\n" ) - @start-cli package install $(PKG_ID).s9pk +install: package | check-deps check-init + @HOST=$$(awk -F'/' '/^host:/ {print $$3}' ~/.startos/config.yaml); \ + if [ -z "$$HOST" ]; then \ + echo "Error: You must define \"host: http://server-name.local\" in ~/.startos/config.yaml"; \ + exit 1; \ + fi; \ + echo "\nπŸš€ Installing to $$HOST ..."; \ + start-cli package install -s $(S9PK) -docker-images/aarch64.tar: Dockerfile docker_entrypoint.sh manager/target/aarch64-unknown-linux-musl/release/bitcoind-manager manifest.yaml check-rpc.sh check-synced.sh actions/* -ifeq ($(ARCH),x86_64) -else - mkdir -p docker-images - docker buildx build --tag start9/$(PKG_ID)/main:$(PKG_VERSION) --build-arg ARCH=aarch64 --build-arg PLATFORM=arm64 --platform=linux/arm64 -o type=docker,dest=docker-images/aarch64.tar . -endif +check-deps: + @command -v start-cli >/dev/null || \ + (echo "Error: start-cli not found. Please see https://docs.start9.com/latest/developer-guide/sdk/installing-the-sdk" && exit 1) + @command -v npm >/dev/null || \ + (echo "Error: npm not found. Please install Node.js and npm." && exit 1) -docker-images/x86_64.tar: Dockerfile docker_entrypoint.sh manager/target/x86_64-unknown-linux-musl/release/bitcoind-manager manifest.yaml check-rpc.sh check-synced.sh actions/* -ifeq ($(ARCH),aarch64) -else - mkdir -p docker-images - docker buildx build --tag start9/$(PKG_ID)/main:$(PKG_VERSION) --build-arg ARCH=x86_64 --build-arg PLATFORM=amd64 --platform=linux/amd64 -o type=docker,dest=docker-images/x86_64.tar . -endif +check-init: + @if [ ! -f ~/.startos/developer.key.pem ]; then \ + echo "Initializing StartOS developer environment..."; \ + start-cli init; \ + fi + +javascript/index.js: $(shell find startos -type f) tsconfig.json node_modules + npm run build -manager/target/aarch64-unknown-linux-musl/release/bitcoind-manager: $(MANAGER_SRC) - docker run --rm -v ~/.cargo/registry:/root/.cargo/registry -v "$(shell pwd)"/manager:/home/rust/src messense/rust-musl-cross:aarch64-musl cargo build --release +node_modules: package-lock.json + npm ci -manager/target/x86_64-unknown-linux-musl/release/bitcoind-manager: $(MANAGER_SRC) - docker run --rm -v ~/.cargo/registry:/root/.cargo/registry -v "$(shell pwd)"/manager:/home/rust/src messense/rust-musl-cross:x86_64-musl cargo build --release +package-lock.json: package.json + npm i -scripts/embassy.js: scripts/**/*.ts - deno bundle scripts/embassy.ts scripts/embassy.js +clean: + @echo "Cleaning up build artifacts..." + @rm -rf $(PACKAGE_ID).s9pk $(PACKAGE_ID)_x86_64.s9pk $(PACKAGE_ID)_aarch64.s9pk javascript node_modules \ No newline at end of file diff --git a/README.md b/README.md index 85aa33a9..581d0536 100644 --- a/README.md +++ b/README.md @@ -1,83 +1,7 @@

- Project Logo + Bitcoin Core Logo

# Bitcoin Core for StartOS -This project packages [Bitcoin](https://bitcoin.org) for StartOS. Bitcoin uses peer-to-peer technology to operate with no central authority or banks - managing transactions and the issuing of bitcoins is carried out collectively by the network. - -## Contributing - -We welcome contributions from all! - -For non-technical contributors, please use the `Issues` section above to communicate your desired edits or additions. - -For technical contributors, please fork this repository, make your changes according to the instructions below, and open a pull reuqest. - -### Adding Config Options - -To add config options, include the new config options in *both* `scripts/services/getConfig.ts` and `assets/compat/bitcoin.conf.template`, adhering to the syntax and conventions of those files. To view the full list of config options, complete with descriptions and specifications, check out this [site](https://jlopp.github.io/bitcoin-core-config-generator) from Jameson Lopp. - -## Dependencies - -Install the following system dependencies to build this project by following the instructions on the provided links: - -- [docker](https://docs.docker.com/get-docker) -- [docker-buildx](https://docs.docker.com/buildx/working-with-buildx/) -- [yq](https://mikefarah.gitbook.io/yq) -- [rust](https://rustup.rs) -- [start-sdk](https://github.com/Start9Labs/start-os/tree/sdk) -- [make](https://www.gnu.org/software/make/) - -## Cloning - -Clone the project locally. Note the submodule link to the original project(s). - -``` -git clone git@github.com:Start9Labs/bitcoind-startos.git -cd bitcoind-startos -git submodule update --init -``` - -## Building - -To build the project for all supported platforms, run the following command: - -``` -make -``` - -To build the project for a single platform, run: - -``` -# for amd64 -make x86 -``` -or -``` -# for arm64 -make arm -``` - -## Installing (on Start9 server) - -Run the following commands to determine successful install: -> :information_source: Change server-name.local to your Start9 server address - -``` -start-cli auth login -# Enter your StartOS password -start-cli --host https://server-name.local package install bitcoind.s9pk -``` - -If you already have your `start-cli` config file setup with a default `host`, you can install simply by running: - -``` -make install -``` - -> **Tip:** You can also install the `bitcoind.s9pk` using **Sideload Service** under the **System > Manage** section. - -## Integrations - -Our [documentation](https://docs.start9.com/latest/service-guides/bitcoin/bitcoin-integrations) includes guides for integrating Bitcoin with external applications. +This repo packages [Bitcoin Core](https://bitcoin.org) for StartOS. \ No newline at end of file diff --git a/actions/reindex.sh b/actions/reindex.sh deleted file mode 100644 index 34a3c265..00000000 --- a/actions/reindex.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh - -set -e - -touch /root/.bitcoin/requires.reindex -action_result_running=" { - \"version\": \"0\", - \"message\": \"Bitcoin Core restarting in reindex mode\", - \"value\": null, - \"copyable\": false, - \"qr\": false -}" -action_result_stopped=" { - \"version\": \"0\", - \"message\": \"Bitcoin Core will reindex the next time the service is started\", - \"value\": null, - \"copyable\": false, - \"qr\": false -}" -bitcoin-cli -rpcconnect=bitcoind.embassy stop >/dev/null 2>/dev/null && echo $action_result_running || echo $action_result_stopped diff --git a/actions/reindex_chainstate.sh b/actions/reindex_chainstate.sh deleted file mode 100644 index 41aa5a05..00000000 --- a/actions/reindex_chainstate.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/sh - -set -e - -action_result_running=" { - \"version\": \"0\", - \"message\": \"Bitcoin Core restarting in reindex chainstate mode\", - \"value\": null, - \"copyable\": false, - \"qr\": false -}" -action_result_stopped=" { - \"version\": \"0\", - \"message\": \"Bitcoin Core will reindex the chainstate the next time the service is started\", - \"value\": null, - \"copyable\": false, - \"qr\": false -}" -action_result_pruned=" { - \"version\": \"0\", - \"message\": \"Bitcoin Core does not allow reindex-chainstate for pruned nodes. If the Chainstate is corrupted on a pruned node the entire blockchain will need to be re-downloaded from genesis with the 'Reindex Blockchain' action\", - \"value\": null, - \"copyable\": false, - \"qr\": false -}" - -pruned=$(yq e '.advanced.pruning.mode' /root/.bitcoin/start9/config.yaml) - -if [ "$pruned" != "disabled" ]; then - echo $action_result_pruned -else - touch /root/.bitcoin/requires.reindex_chainstate - bitcoin-cli -rpcconnect=bitcoind.embassy stop >/dev/null 2>/dev/null && echo $action_result_running || echo $action_result_stopped -fi \ No newline at end of file diff --git a/assets/ABOUT.md b/assets/ABOUT.md new file mode 100644 index 00000000..4fbfc10e --- /dev/null +++ b/assets/ABOUT.md @@ -0,0 +1 @@ +Use the `/assets` directory to include additional files or scripts needed by your service. diff --git a/assets/compat/bitcoin.conf.template b/assets/compat/bitcoin.conf.template deleted file mode 100644 index a6f62b57..00000000 --- a/assets/compat/bitcoin.conf.template +++ /dev/null @@ -1,152 +0,0 @@ -## -## bitcoin.conf configuration file. Lines beginning with # are comments. -## - -## RPC -{{#IF rpc.enable -{{#IF advanced.pruning.mode = "automatic" -rpcbind=127.0.0.1:18332 -rpcallowip=127.0.0.1/32 -}} -{{#IF advanced.pruning.mode != "automatic" -rpcbind=0.0.0.0:8332 -rpcallowip=0.0.0.0/0 -}} -}} -rpcuser={{rpc.username}} -rpcpassword={{rpc.password}} -{{#FOREACH rpc.advanced.auth -rpcauth={{rpc.advanced.auth}} -}} -rpcservertimeout={{rpc.advanced.servertimeout}} -rpcthreads={{rpc.advanced.threads}} -rpcworkqueue={{rpc.advanced.workqueue}} - -## MEMPOOL -{{#IF advanced.mempool.mempoolfullrbf -mempoolfullrbf=1 -}} -{{#IF !advanced.mempool.mempoolfullrbf -mempoolfullrbf=0 -}} -{{#IF advanced.mempool.persistmempool -persistmempool=1 -}} -{{#IF !advanced.mempool.persistmempool -persistmempool=0 -}} -maxmempool={{advanced.mempool.maxmempool}} -mempoolexpiry={{advanced.mempool.mempoolexpiry}} -{{#IF advanced.mempool.datacarrier -datacarrier=1 -}} -{{#IF !advanced.mempool.datacarrier -datacarrier=0 -}} -datacarriersize={{advanced.mempool.datacarriersize}} -{{#IF advanced.mempool.permitbaremultisig -permitbaremultisig=1 -}} -{{#IF !advanced.mempool.permitbaremultisig -permitbaremultisig=0 -}} - -## PEERS -{{#IF advanced.peers.listen -listen=1 -bind=0.0.0.0:8333 -}} -{{#IF !advanced.peers.listen -listen=0 -}} -{{#IF advanced.peers.onlyconnect -{{#FOREACH advanced.peers.addnode -{{#IF advanced.peers.addnode.port -connect={{advanced.peers.addnode.hostname}}:{{advanced.peers.addnode.port}} -}} -{{#IF !advanced.peers.addnode.port -connect={{advanced.peers.addnode.hostname}} -}} -}} -}} -{{#IF !advanced.peers.onlyconnect -{{#FOREACH advanced.peers.addnode -{{#IF advanced.peers.addnode.port -addnode={{advanced.peers.addnode.hostname}}:{{advanced.peers.addnode.port}} -}} -{{#IF !advanced.peers.addnode.port -addnode={{advanced.peers.addnode.hostname}} -}} -}} -}} -{{#IF advanced.peers.onlyonion -onlynet=onion -}} -{{#IF advanced.peers.v2transport -v2transport=1 -}} -{{#IF !advanced.peers.v2transport -v2transport=0 -}} - -## WHITELIST -## whitelist all services subnet -whitelist=172.18.0.0/16 - -## PRUNING -{{#IF advanced.pruning.mode = "automatic" -prune={{advanced.pruning.size}} -}} - -## PERFORMANCE TUNING -{{#IF advanced.dbcache -dbcache={{advanced.dbcache}} -}} - -## WALLET -{{#IF !wallet.enable -disablewallet=1 -}} -{{#IF wallet.enable -disablewallet=0 -deprecatedrpc=create_bdb -}} -{{#IF wallet.avoidpartialspends -avoidpartialspends=1 -}} -{{#IF !wallet.avoidpartialspends -avoidpartialspends=0 -}} -discardfee={{wallet.discardfee}} - -## ZERO MQ -{{#IF zmq-enabled -zmqpubrawblock=tcp://0.0.0.0:28332 -zmqpubhashblock=tcp://0.0.0.0:28332 -zmqpubrawtx=tcp://0.0.0.0:28333 -zmqpubhashtx=tcp://0.0.0.0:28333 -zmqpubsequence=tcp://0.0.0.0:28333 -}} - -## TXINDEX -{{#IF txindex -txindex=1 -}} - -## COINSTATSINDEX -{{#IF coinstatsindex -coinstatsindex=1 -}} - -## BIP37 -{{#IF advanced.bloomfilters.peerbloomfilters -peerbloomfilters=1 -}} - -## BIP157 -{{#IF advanced.blockfilters.blockfilterindex -blockfilterindex=basic -}} -{{#IF advanced.blockfilters.peerblockfilters -peerblockfilters=1 -}} diff --git a/assets/rpcauth.py b/assets/rpcauth.py new file mode 100755 index 00000000..cc7bba1f --- /dev/null +++ b/assets/rpcauth.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# Copyright (c) 2015-2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +from argparse import ArgumentParser +from getpass import getpass +from secrets import token_hex, token_urlsafe +import hmac + +def generate_salt(size): + """Create size byte hex salt""" + return token_hex(size) + +def generate_password(): + """Create 32 byte b64 password""" + return token_urlsafe(32) + +def password_to_hmac(salt, password): + m = hmac.new(salt.encode('utf-8'), password.encode('utf-8'), 'SHA256') + return m.hexdigest() + +def main(): + parser = ArgumentParser(description='Create login credentials for a JSON-RPC user') + parser.add_argument('username', help='the username for authentication') + parser.add_argument('password', help='leave empty to generate a random password or specify "-" to prompt for password', nargs='?') + args = parser.parse_args() + + if not args.password: + args.password = generate_password() + elif args.password == '-': + args.password = getpass() + + # Create 16 byte hex salt + salt = generate_salt(16) + password_hmac = password_to_hmac(salt, args.password) + + print('String to be appended to bitcoin.conf:') + print(f'rpcauth={args.username}:{salt}${password_hmac}') + print(f'Your password:\n{args.password}') + +if __name__ == '__main__': + main() diff --git a/bitcoin b/bitcoin index 11018374..d0f6d995 160000 --- a/bitcoin +++ b/bitcoin @@ -1 +1 @@ -Subproject commit 110183746150428e6385880c79f8c5733b1361ba +Subproject commit d0f6d9953a15d7c7111d46dcb76ab2bb18e5dee3 diff --git a/check-rpc.sh b/check-rpc.sh deleted file mode 100644 index 42f6d773..00000000 --- a/check-rpc.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -# set -e - -gi_result=$(bitcoin-cli getrpcinfo 2>&1) -error_code=$? - -if [ "$error_code" -eq 28 ]; then - # Starting - exit 60 -else - echo $gi_result >&2 - exit $error_code -fi diff --git a/check-synced.sh b/check-synced.sh deleted file mode 100755 index a1b9fa97..00000000 --- a/check-synced.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -set -e - -username=$(yq e '.rpc.username' /root/.bitcoin/start9/config.yaml) -password=$(yq e '.rpc.password' /root/.bitcoin/start9/config.yaml) -gbci_result=$(curl -s --user $username:$password --data-binary '{"jsonrpc": "1.0", "id": "curltest", "method": "getblockchaininfo", "params": []}' -H 'content-type: text/plain;' http://bitcoind.embassy:8332/ ) -error_code=$? -if [ $error_code -ne 0 ]; then - echo $gbci_result >&2 - exit $error_code -fi - -res=$(echo "$gbci_result" | yq e '.result' -) -err=$(echo "$gbci_result" | yq e '.error' -) -if [ "$res" = "null" ]; then - # Starting - exit 60 -elif [ $(echo "$res" | yq e '.initialblockdownload' -) = "true" ]; then - progress=$(echo "$res" | yq e '.verificationprogress' -) - if [[ "$progress" = *"e"* ]]; then - progress="0" - fi - progress_pct=$( bc -l <<<"100*$progress" ) - echo "Syncing blockchain. This may take several days. Progress: $(printf "%.2f" $progress_pct)%" >&2 - exit 61 -fi diff --git a/docker_entrypoint.sh b/docker_entrypoint.sh deleted file mode 100755 index 2451320e..00000000 --- a/docker_entrypoint.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -CONFIG_FILE="/root/.bitcoin/start9/config.yaml" -export EMBASSY_IP=$(ip -4 route list match 0/0 | awk '{print $3}') -export PEER_TOR_ADDRESS=$(yq e '.peer-tor-address' "$CONFIG_FILE") -export RPC_TOR_ADDRESS=$(yq e '.rpc-tor-address' "$CONFIG_FILE") - -exec tini -p SIGTERM -- bitcoind-manager diff --git a/docs/blockchain-migration.md b/docs/blockchain-migration.md new file mode 100644 index 00000000..5ff9b6f7 --- /dev/null +++ b/docs/blockchain-migration.md @@ -0,0 +1,80 @@ +## Description + +If you already have a synced Bitcoin blockchain on one StartOS server, and would like to skip IBD on another StartOS server, follow this guide. + +WARNING: This is an advanced feature and should be used with caution. Start9 is not responsible for any damage you might cause through SSH access. + +WARNING: This guide assumes you are migrating from a StartOS v0.4.0 to v0.4.0 server. If you are migrating from v0.3.5 then the source directory will always be `/embassy-data/package-data/volumes/bitcoind/data/main/`. + + +## Instructions + +1. In this guide, we will refer to your synced node as `synced.local` and your unsynced node as `unsynced.local`. Simply replace these URLs with your own. + +1. In `unsynced.local` UI, install Bitcoin. _Do not configure or start it_. + +1. In `synced.local` UI: + + 1. Review the SSH guide to choose whether to use your master password or create an [SSH key](https://docs.start9.com/user-manual/ssh.html). + + 1. _Stop Bitcoin_ by going to Services and clicking the Stop icon, or clicking on Bitcoin and clicking the Stop button. + +1. SSH into `synced.local`: + + ssh start9@synced.local + +1. Once inside the shell, run the following commands: + + ``` + sudo -i + ``` + + ``` + mkdir -m 0700 -p .ssh + ``` + + ``` + ssh-keygen -t ed25519 -N '' -f .ssh/temp.key + ``` + + ``` + chmod 600 .ssh/temp.key* + ``` + + ``` + cat .ssh/temp.key.pub + ``` + +1. Copy the output of the final `cat` command to your clipboard. + +1. In `unsynced.local` UI, go to `System > SSH > Add Key`, and paste the value from above. Click "Submit" + +1. In `synced.local` shell, run the following commands, _replacing `unsynced.local` in the second command with the correct URL_: + + ``` + cd /media/startos/data/package-data/volumes/bitcoind/data/main/ + ``` + + ``` + sudo rsync -e "ssh -i ~/.ssh/temp.key" -povgr --append-verify --rsync-path="sudo mkdir -p /media/startos/data/package-data/volumes/bitcoind/data/main/ ; sudo rsync" ./{blocks,chainstate} start9@unsynced.local:/media/startos/data/package-data/volumes/bitcoind/data/main/ + ``` + +1. Wait some hours until the copy is complete. On a gigabit network, the limiting factor will be the write speed of your SSD on the unsynced server. + +1. When the copy is complete, in `synced.local` shell, run the following commands: + + ``` + rm .ssh/unsynced.key* + ``` + + ``` + exit + ``` + +1. In `synced.local` UI, restart Bitcoin. + +1. In `unsynced.local` UI: + + - configure and start Bitcoin for the first time. You should see it begin at 99%+ pre-synced! + + - Delete the `temp.key` SSH key we added above. diff --git a/instructions.md b/docs/instructions.md similarity index 76% rename from instructions.md rename to docs/instructions.md index ca957873..8f4a6c6c 100644 --- a/instructions.md +++ b/docs/instructions.md @@ -10,10 +10,6 @@ Your node is highly configurable. Many settings are considered _advanced_ and sh Depending on your hardware resources, internet bandwidth, how many other services are running, and what peers your node happens to connect to, your node should take anywhere from under a day to several days to sync from genesis to present. -### Using a Wallet - -Enter your QuickConnect QR code **OR** your raw RPC credentials (both located in `Properties`) into any wallet that supports connecting to a remote node over Tor. For a full list of compatible wallets, as well as guides for setup, please see the [documentation](https://docs.start9.com/latest/service-guides/bitcoin/bitcoin-integrations). - ## Pruning Beginning with version **25.0.0.1**, pruning is now handled automatically depending on the available space. If there is insufficient free space, pruning will be automatically configured. Users also have the option to adjust pruning settings manually. @@ -25,3 +21,17 @@ Pruning is a process by which your node discards old blocks and transactions aft ## Backups When your server backs up this service, it will *not* include the blocks, chainstate, or indexes, so you don't need to worry about it eating your backup drive if you run an archival node. + +## Using a Wallet + +### Wallet Integration Guides + +The following wallets have a setup guides that is tested and known to work. You should also be able to adapt these for most other wallets that can connect to Bitcoin Core (To connect to Electrs, see the Electrs [documentation](https://github.com/Start9Labs/electrs-startos/docs/instructions.md)). + +- [Fully Noded](wallet-integrations/fully-noded.md) +- [Sparrow](wallet-integrations/sparrow.md) +- [Specter](wallet-integrations/specter.md) + +## Blockchain Migration + +To migrate the full blockchain from one StartOS server to another, please see the [blockchain migration guide](blockchain-migration.md). \ No newline at end of file diff --git a/docs/wallet-integrations/assets/sparrow-desktop-bitcoin1.png b/docs/wallet-integrations/assets/sparrow-desktop-bitcoin1.png new file mode 100644 index 00000000..b76b7443 Binary files /dev/null and b/docs/wallet-integrations/assets/sparrow-desktop-bitcoin1.png differ diff --git a/docs/wallet-integrations/assets/sparrow-desktop-bitcoin2.png b/docs/wallet-integrations/assets/sparrow-desktop-bitcoin2.png new file mode 100644 index 00000000..96aba29a Binary files /dev/null and b/docs/wallet-integrations/assets/sparrow-desktop-bitcoin2.png differ diff --git a/docs/wallet-integrations/assets/sparrow-desktop-electrs1.png b/docs/wallet-integrations/assets/sparrow-desktop-electrs1.png new file mode 100644 index 00000000..af00926f Binary files /dev/null and b/docs/wallet-integrations/assets/sparrow-desktop-electrs1.png differ diff --git a/docs/wallet-integrations/assets/sparrow-desktop-electrs2.png b/docs/wallet-integrations/assets/sparrow-desktop-electrs2.png new file mode 100644 index 00000000..aab0c86b Binary files /dev/null and b/docs/wallet-integrations/assets/sparrow-desktop-electrs2.png differ diff --git a/docs/wallet-integrations/assets/specter-electrs.png b/docs/wallet-integrations/assets/specter-electrs.png new file mode 100644 index 00000000..8b5c01ee Binary files /dev/null and b/docs/wallet-integrations/assets/specter-electrs.png differ diff --git a/docs/wallet-integrations/assets/specter-new.png b/docs/wallet-integrations/assets/specter-new.png new file mode 100644 index 00000000..16aa5f54 Binary files /dev/null and b/docs/wallet-integrations/assets/specter-new.png differ diff --git a/docs/wallet-integrations/assets/specter-spectrum.png b/docs/wallet-integrations/assets/specter-spectrum.png new file mode 100644 index 00000000..ac00a7a2 Binary files /dev/null and b/docs/wallet-integrations/assets/specter-spectrum.png differ diff --git a/docs/wallet-integrations/assets/specter-start.png b/docs/wallet-integrations/assets/specter-start.png new file mode 100644 index 00000000..a9847b39 Binary files /dev/null and b/docs/wallet-integrations/assets/specter-start.png differ diff --git a/docs/wallet-integrations/fully-noded.md b/docs/wallet-integrations/fully-noded.md new file mode 100644 index 00000000..082c0b23 --- /dev/null +++ b/docs/wallet-integrations/fully-noded.md @@ -0,0 +1,25 @@ +# Fully Noded + +**Available For** + +- Mac +- iOS + +**Instructions** + +NOTE: Fulled Noded does not currently support `https` and recommends [using Tor](https://docs.start9.com/user-manual/connecting-remotely/tor.html#running-tor-in-the-background-on-your-phonelaptop). This means it is not possible to connect to Bitcoin Core/Knots over LAN or Router VPN. You can however connect to Bitcoin Core/Knots over its Tor interface. + + +1. Make sure you're [running Tor](https://docs.start9.com/user-manual/connecting-remotely/tor.html#running-tor-in-the-background-on-your-phonelaptop). + +1. If this is your first time using Fully Noded, you will be presented with a option to `Connect my node`. Otherwise, you can find the server setup in `Configuration (Cog icon) > Node Manage > +`. + +1. Choose `Bitcoin Core`. Fully Noded will generate RPC credentials. But we will not use these. Click OK to begin editing the credentials. + +1. Provide a label, then enter an address from `Services > Bitcoin Core/Knots` then the Interfaces section, including the port `8332` but excluding the protocol `http://`) + +1. In StartOS, go to `Services > Bitcoin Core/Knots > Actions > Generate RPC Credentials` and enter a username such as "fullynoded" then click Submit. You will see a password was generated. Copy the password to clipboard by clicking on the copy icon. + +1. Back in Fully Noded, type the RPC username you chose above into the `RPC username` field. Paste the generated password from Bitcoin into the `RPC authentication` field. + +1. Click Save, Back out, and make sure the toggle is on for the node that you just created. Visit the first tab to confirm you are connected. diff --git a/docs/wallet-integrations/sparrow.md b/docs/wallet-integrations/sparrow.md new file mode 100644 index 00000000..481439f2 --- /dev/null +++ b/docs/wallet-integrations/sparrow.md @@ -0,0 +1,44 @@ +# Sparrow + +**Available For** + +- StartOS +- Mac +- Linux +- Windows + +**Contents** + +- [Using Sparrow on StartOS](#sparrow-on-startos) +- [Using Sparrow Desktop App](#sparrow-desktop) + +**Instructions** + +## Sparrow on StartOS + +TIP: To choose between a connection to Bitcoin Core/Knots or to Electrs, instead of using the Sparrow's own UI you will instead set your choice in the StartOS UI at `Services > Sparrow > Actions` + +NOTE: You cannot connect hardware signing devices (wallets) to your server, they will not be detected by Sparrow. + +1. Ensure Sparrow is installed and running if not already. + +1. Click "Launch UI". + +## Sparrow Desktop + +1. If this is your first time using Sparrow, you will be guided to a screen to configure your Bitcoin or electrs server. Otherwise, you can find the server setup in `File > Preferences > Server > Configure Server`. + + - **Connecting to Bitcoin Core**: + + NOTE: Sparrow's interface does not currently allow a user to set the protocol to `https` while also providing a custom port other than 443. This means it is not possible to connect to Bitcoin Core/Knots over LAN or Router VPN. You can however connect to Bitcoin Core/Knots over its Tor interface, or by [pointing a domain to the service](https://docs.start9.com/user-manual/connecting-remotely/clearnet.html). + + + 1. In the `URL` field, enter your Bitcoin hostname and port (found in `Services > Bitcoin Core/Knots` then the Interfaces section). You may select either the https or http as Sparrow will remove the protocol prefix automatically and add the port automatically. + + 1. Select `User/Pass` as the `Authentication` option then enter a new user and password that you will now generate and copy from StartOS at `Services > Bitcoin Core/Knots > Actions > Generate RPC Credentials` + + 1. Test your connection + + - **Connecting to electrs**: + + To connect to Electrs, see the Electrs [documentation](https://github.com/Start9Labs/electrs-startos/docs/instructions.md) \ No newline at end of file diff --git a/docs/wallet-integrations/specter.md b/docs/wallet-integrations/specter.md new file mode 100644 index 00000000..736ad8ee --- /dev/null +++ b/docs/wallet-integrations/specter.md @@ -0,0 +1,49 @@ +# Specter + +**Available For** + +- Mac +- Linux +- Windows + +**Contents** + +- [Specter Desktop](#specter-desktop) + +**Instructions** + +## Specter Desktop + +1. If this is your first time using Specter, you will be shown a screen to pick a connection method. But we'll skip this for now and set up Tor. + + ![Specter first open](./assets/specter-start.png) + +1. Click `Settings` and select the `Tor` tab. + + - If you have Tor running as [local Proxy](https://docs.start9.com/user-manual/connecting-remotely/tor.html#running-tor-in-the-background-on-your-phonelaptop) scroll down and select `Custom` + + - Enter or leave the URL as `socks5h://localhost:9050` + + - Click `Test connection` - if it fails, please review your Tor proxy + + - If you don't have Tor running in the background of your system, select `Built-in` + + - Click `Set Up`, then `Setup Tor` + + - then click the `Save` button + +### Connecting to Bitcoin Core + +1. Click the `...` menu and click `+ Add Connection` + +1. In the `Username` and `Password` fields, enter a new user and password that you will now generate and copy from StartOS at `Services > Bitcoin Core/Knots > Actions > Generate RPC Credentials` + + ![Specter Bitcoin RPC](./assets/specter-new.png) + +1. In `Host`, enter your Bitcoin Core RPC Interface Tor Address (found in `Services > Bitcoin Core > Interfaces`). + +1. In `Port`, enter `8332` and click `Connect` + +### Connecting to Electrs + +To connect to Electrs, see the Electrs [documentation](https://github.com/Start9Labs/electrs-startos/docs/instructions.md) diff --git a/icon.png b/icon.png deleted file mode 100644 index 00ed636a..00000000 Binary files a/icon.png and /dev/null differ diff --git a/icon.svg b/icon.svg new file mode 100644 index 00000000..2ccc3f92 --- /dev/null +++ b/icon.svg @@ -0,0 +1,2 @@ + +Bitcoin icon \ No newline at end of file diff --git a/manager/.gitignore b/manager/.gitignore deleted file mode 100644 index 4cd3977e..00000000 --- a/manager/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/target -**/*.rs.bk -scripts/embassy.ts \ No newline at end of file diff --git a/manager/Cargo.lock b/manager/Cargo.lock deleted file mode 100644 index cde65f20..00000000 --- a/manager/Cargo.lock +++ /dev/null @@ -1,1400 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "addr2line" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "async-channel" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" -dependencies = [ - "concurrent-queue", - "event-listener", - "futures-core", -] - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "backtrace" -version = "0.3.71" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "base32" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "bech32" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" - -[[package]] -name = "bitcoin" -version = "0.30.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1945a5048598e4189e239d3f809b19bdad4845c4b2ba400d304d2dcf26d2c462" -dependencies = [ - "bech32", - "bitcoin-private", - "bitcoin_hashes", - "hex_lit", - "secp256k1", - "serde", -] - -[[package]] -name = "bitcoin-private" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57" - -[[package]] -name = "bitcoin_hashes" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501" -dependencies = [ - "bitcoin-private", - "serde", -] - -[[package]] -name = "bitcoind-manager" -version = "0.1.1" -dependencies = [ - "btc-rpc-proxy", - "chrono", - "ctrlc", - "env_logger", - "eyre", - "heck", - "lazy_static", - "linear-map", - "nix 0.27.1", - "regex", - "serde", - "serde_json", - "serde_yaml", - "tiny-tmpl", - "tokio", -] - -[[package]] -name = "bitflags" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" - -[[package]] -name = "btc-rpc-proxy" -version = "0.4.0" -source = "git+https://github.com/Start9Labs/btc-rpc-proxy.git?branch=skinny#703b72488783dd8145da2bdee5725d139cc3fbb0" -dependencies = [ - "async-channel", - "base32", - "base64", - "bitcoin", - "color-eyre", - "enum_future", - "futures 0.3.31", - "hex", - "http", - "hyper", - "itertools", - "lazy_static", - "linear-map", - "serde", - "serde_json", - "socks", - "thiserror", - "tokio", - "tracing", -] - -[[package]] -name = "bumpalo" -version = "3.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" - -[[package]] -name = "cc" -version = "1.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chrono" -version = "0.4.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-targets", -] - -[[package]] -name = "color-eyre" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" -dependencies = [ - "backtrace", - "color-spantrace", - "eyre", - "indenter", - "once_cell", - "owo-colors", - "tracing-error", -] - -[[package]] -name = "color-spantrace" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" -dependencies = [ - "once_cell", - "owo-colors", - "tracing-core", - "tracing-error", -] - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "ctrlc" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" -dependencies = [ - "nix 0.29.0", - "windows-sys 0.59.0", -] - -[[package]] -name = "either" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" - -[[package]] -name = "enum_future" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fab1e733e2e6068206c4fc77784f921776ae38e92fe334abb6204f8c249ebf05" -dependencies = [ - "futures 0.1.31", -] - -[[package]] -name = "env_logger" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" -dependencies = [ - "humantime", - "is-terminal", - "log", - "regex", - "termcolor", -] - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - -[[package]] -name = "eyre" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" -dependencies = [ - "indenter", - "once_cell", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "futures" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "gimli" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" - -[[package]] -name = "h2" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap 2.7.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" - -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "hermit-abi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hex_lit" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "indenter" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - -[[package]] -name = "indexmap" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" -dependencies = [ - "equivalent", - "hashbrown 0.15.2", -] - -[[package]] -name = "is-terminal" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" - -[[package]] -name = "js-sys" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.169" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" - -[[package]] -name = "linear-map" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee" -dependencies = [ - "serde", - "serde_test", -] - -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - -[[package]] -name = "lock_api" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "miniz_oxide" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" -dependencies = [ - "adler", -] - -[[package]] -name = "mio" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.52.0", -] - -[[package]] -name = "nix" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" -dependencies = [ - "bitflags", - "cfg-if", - "libc", -] - -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", - "libc", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.32.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" - -[[package]] -name = "owo-colors" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" - -[[package]] -name = "parking_lot" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "proc-macro2" -version = "1.0.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - -[[package]] -name = "ryu" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "secp256k1" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" -dependencies = [ - "bitcoin_hashes", - "secp256k1-sys", - "serde", -] - -[[package]] -name = "secp256k1-sys" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e" -dependencies = [ - "cc", -] - -[[package]] -name = "serde" -version = "1.0.216" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.216" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.134" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_test" -version = "1.0.177" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f901ee573cab6b3060453d2d5f0bae4e6d628c23c0a962ff9b5f1d7c8d4f1ed" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_yaml" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" -dependencies = [ - "indexmap 1.9.3", - "ryu", - "serde", - "yaml-rust", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - -[[package]] -name = "socket2" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socks" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" -dependencies = [ - "byteorder", - "libc", - "winapi", -] - -[[package]] -name = "syn" -version = "2.0.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thread_local" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" -dependencies = [ - "cfg-if", - "once_cell", -] - -[[package]] -name = "tiny-tmpl" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841fdc3f04788868454c26e4a170b726572939f87a85e2514b88aedeff42be69" -dependencies = [ - "serde_yaml", -] - -[[package]] -name = "tokio" -version = "1.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.52.0", -] - -[[package]] -name = "tokio-macros" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-util" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-error" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" -dependencies = [ - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" -dependencies = [ - "sharded-slab", - "thread_local", - "tracing-core", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "unicode-ident" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" -dependencies = [ - "cfg-if", - "once_cell", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] diff --git a/manager/Cargo.toml b/manager/Cargo.toml deleted file mode 100644 index c61c3fd0..00000000 --- a/manager/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "bitcoind-manager" -version = "0.1.1" -authors = ["Aiden McClelland "] -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -btc-rpc-proxy = { git = "https://github.com/Start9Labs/btc-rpc-proxy.git", branch = "skinny" } -chrono = "0.4.31" -ctrlc = { version = "3.4.1", features = ["termination"] } -heck = "0.3.3" -lazy_static = "1.4.0" -linear-map = { version = "*", features = ["serde_impl"] } -nix = { version = "0.27.1", features = ["process", "signal"] } -regex = "1.10.2" -serde = { version = "1.0.193", features = ["derive"] } -serde_yaml = "0.8.17" -serde_json = "1.0" -tiny-tmpl = "0.1.3" -tokio = { version = "1", features = ["rt"] } -env_logger = "0.10.0" -eyre = "0.6.12" diff --git a/manager/src/main.rs b/manager/src/main.rs deleted file mode 100644 index 432886ea..00000000 --- a/manager/src/main.rs +++ /dev/null @@ -1,563 +0,0 @@ -use std::convert::TryFrom; -use std::env::var; -use std::error::Error; -use std::os::unix::prelude::ExitStatusExt; -use std::sync::Arc; -use std::time::Duration; -use std::{borrow::Cow, sync::Mutex}; -use std::{fs, io::Write, path::Path}; - -use btc_rpc_proxy::{Peers, RpcClient, TorState}; -use env_logger::Env; -use heck::TitleCase; -use linear_map::LinearMap; -use nix::sys::signal::Signal; -use serde_yaml::{Mapping, Value}; -use tmpl::TemplatingReader; - -lazy_static::lazy_static! { - static ref CHILD_PID: Mutex> = Mutex::new(None); -} - -#[derive(Clone, Debug, serde::Deserialize)] -pub struct ChainInfo { - blocks: usize, - headers: usize, - verificationprogress: f64, - size_on_disk: u64, - #[serde(default)] - pruneheight: usize, - #[serde(default)] - softforks: LinearMap, -} - -#[derive(Clone, Debug, serde::Deserialize)] -pub struct NetworkInfo { - connections: usize, - connections_in: usize, - connections_out: usize, -} - -#[derive(Clone, Debug, serde::Deserialize)] -#[serde(tag = "type")] -pub enum SoftFork { - #[serde(rename = "buried")] - Buried { active: bool, height: usize }, - #[serde(rename = "bip9")] - Bip9 { active: bool, bip9: Bip9 }, -} - -#[derive(Clone, Debug, serde::Deserialize)] -#[serde(tag = "status")] -pub enum Bip9 { - #[serde(rename = "defined")] - Defined { - start_time: u64, - timeout: u64, - since: usize, - }, - #[serde(rename = "started")] - Started { - bit: usize, - start_time: u64, - timeout: u64, - since: usize, - statistics: Bip9Stats, - }, - #[serde(rename = "locked_in")] - LockedIn { - start_time: u64, - timeout: u64, - since: usize, - }, - #[serde(rename = "active")] - Active { - start_time: u64, - timeout: u64, - since: usize, - }, - #[serde(rename = "failed")] - Failed { - start_time: u64, - timeout: u64, - since: usize, - }, -} - -#[derive(Clone, Debug, serde::Deserialize)] -pub struct Bip9Stats { - pub period: usize, - pub threshold: usize, - pub elapsed: usize, - pub count: usize, - pub possible: bool, -} - -#[derive(Clone, Debug, serde::Serialize)] -pub struct Stats { - version: u8, - data: LinearMap, Stat>, -} - -#[derive(Clone, Debug, serde::Serialize)] -pub struct Stat { - #[serde(rename = "type")] - value_type: &'static str, - value: String, - description: Option>, - copyable: bool, - qr: bool, - masked: bool, -} - -fn sidecar(config: &Mapping, addr: &str) -> Result<(), Box> { - let mut stats = LinearMap::new(); - if let (Some(user), Some(pass)) = ( - config - .get(&Value::String("rpc".to_owned())) - .and_then(|v| v.get(&Value::String("username".to_owned()))) - .and_then(|v| v.as_str()), - config - .get(&Value::String("rpc".to_owned())) - .and_then(|v| v.get(&Value::String("password".to_owned()))) - .and_then(|v| v.as_str()), - ) { - stats.insert( - Cow::from("Tor Quick Connect"), - Stat { - value_type: "string", - value: format!("btcstandup://{}:{}@{}:8332", user, pass, addr), - description: Some(Cow::from("Bitcoin-Standup Tor Quick Connect URL")), - copyable: true, - qr: true, - masked: true, - }, - ); - let addr_local = format!("{}local", addr.strip_suffix("onion").unwrap()); - stats.insert( - Cow::from("LAN Quick Connect"), - Stat { - value_type: "string", - value: format!("btcstandup://{}:{}@{}:8332", user, pass, addr_local), - description: Some(Cow::from("Bitcoin-Standup LAN Quick Connect URL")), - copyable: true, - qr: true, - masked: true, - }, - ); - stats.insert( - Cow::from("RPC Username"), - Stat { - value_type: "string", - value: format!("{}", user), - description: Some(Cow::from("Bitcoin RPC Username")), - copyable: true, - masked: false, - qr: false, - }, - ); - stats.insert( - Cow::from("RPC Password"), - Stat { - value_type: "string", - value: format!("{}", pass), - description: Some(Cow::from("Bitcoin RPC Password")), - copyable: true, - masked: true, - qr: false, - }, - ); - } - let info_res = std::process::Command::new("bitcoin-cli") - .arg("-conf=/root/.bitcoin/bitcoin.conf") - .arg("getblockchaininfo") - .output()?; - if info_res.status.success() { - let info: ChainInfo = serde_json::from_slice(&info_res.stdout)?; - stats.insert( - Cow::from("Block Height"), - Stat { - value_type: "string", - value: format!("{}", info.headers), - description: Some(Cow::from("The current block height for the network")), - copyable: false, - qr: false, - masked: false, - }, - ); - stats.insert( - Cow::from("Synced Block Height"), - Stat { - value_type: "string", - value: format!("{}", info.blocks), - description: Some(Cow::from("The number of blocks the node has verified")), - copyable: false, - qr: false, - masked: false, - }, - ); - stats.insert( - Cow::from("Sync Progress"), - Stat { - value_type: "string", - value: if info.blocks < info.headers { - format!("{:.2}%", 100.0 * info.verificationprogress) - } else { - "100%".to_owned() - }, - description: Some(Cow::from( - "The percentage of the blockchain that has been verified", - )), - copyable: false, - qr: false, - masked: false, - }, - ); - for (sf_name, sf_data) in info.softforks { - let sf_name_pretty = sf_name.to_title_case(); - let status_desc = Some(Cow::from(format!( - "The Bip9 deployment status for {}", - sf_name_pretty - ))); - let start_desc = Some(Cow::from(format!( - "The start time (UTC) of the Bip9 signaling period for {}", - sf_name_pretty - ))); - let timeout_desc = Some(Cow::from(format!( - "The timeout time (UTC) of the Bip9 signaling period for {}", - sf_name_pretty - ))); - match sf_data { - SoftFork::Buried { - active: _, - height: _, - } => continue, - SoftFork::Bip9 { bip9, active: _ } => { - let (status, start, end, _since) = match bip9 { - Bip9::Defined { - start_time, - timeout, - since, - } => { - let start_time_pretty = human_readable_timestamp(start_time); - let end_time_pretty = human_readable_timestamp(timeout); - ("Defined", start_time_pretty, end_time_pretty, since) - } - Bip9::Started { - start_time, - timeout, - since, - bit: _, - statistics: _, - } => { - let start_time_pretty = human_readable_timestamp(start_time); - let end_time_pretty = human_readable_timestamp(timeout); - ("Started", start_time_pretty, end_time_pretty, since) - } - Bip9::LockedIn { - start_time, - timeout, - since, - } => { - let start_time_pretty = human_readable_timestamp(start_time); - let end_time_pretty = human_readable_timestamp(timeout); - ("Locked In", start_time_pretty, end_time_pretty, since) - } - Bip9::Active { - start_time, - timeout, - since, - } => { - // stop showing soft fork info when it's been active for ~12 weeks - if info.blocks >= since + 12096 { - continue; - } - let start_time_pretty = human_readable_timestamp(start_time); - let end_time_pretty = human_readable_timestamp(timeout); - ("Active", start_time_pretty, end_time_pretty, since) - } - Bip9::Failed { - start_time, - timeout, - since, - } => { - let start_time_pretty = human_readable_timestamp(start_time); - let end_time_pretty = human_readable_timestamp(timeout); - ("Active", start_time_pretty, end_time_pretty, since) - } - }; - stats.insert( - Cow::from(format!("{} Status", sf_name_pretty)), - Stat { - value_type: "string", - value: status.to_owned(), - description: status_desc, - copyable: false, - qr: false, - masked: false, - }, - ); - stats.insert( - Cow::from(format!("{} Start Time", sf_name_pretty)), - Stat { - value_type: "string", - value: start, - description: start_desc, - copyable: false, - qr: false, - masked: false, - }, - ); - stats.insert( - Cow::from(format!("{} Timeout", sf_name_pretty)), - Stat { - value_type: "string", - value: end, - description: timeout_desc, - copyable: false, - qr: false, - masked: false, - }, - ); - if let Bip9::Started { - statistics, - start_time: _, - timeout: _, - since: _, - bit: _, - } = bip9 - { - stats.insert( - Cow::from(format!("{} Signal Percentage", sf_name_pretty)), - Stat { - value_type: "string", - value: format!( - "{:.2}%", - 100.0 * (statistics.count as f64) / (statistics.elapsed as f64) - ), - description: Some(Cow::from(format!("Percentage of the blocks in the current signaling window that are signaling for the activation of {}", sf_name_pretty))), - copyable: false, - qr: false, - masked: false, - }, - ); - } - } - } - } - stats.insert( - Cow::from("Disk Usage"), - Stat { - value_type: "string", - value: format!("{:.2} GiB", info.size_on_disk as f64 / 1024_f64.powf(3_f64)), - description: Some(Cow::from("The blockchain size on disk")), - copyable: false, - qr: false, - masked: false, - }, - ); - if info.pruneheight > 0 { - stats.insert( - Cow::from("Prune Height"), - Stat { - value_type: "string", - value: format!("{}", info.pruneheight), - description: Some(Cow::from( - "The number of blocks that have been deleted from disk", - )), - copyable: false, - qr: false, - masked: false, - }, - ); - } - } else if info_res.status.code() == Some(28) { - return Ok(()); - } else { - eprintln!( - "Error updating blockchain info: {}", - std::str::from_utf8(&info_res.stderr).unwrap_or("UNKNOWN ERROR") - ); - } - let info_res = std::process::Command::new("bitcoin-cli") - .arg("-conf=/root/.bitcoin/bitcoin.conf") - .arg("getnetworkinfo") - .output()?; - if info_res.status.success() { - let info: NetworkInfo = serde_json::from_slice(&info_res.stdout)?; - stats.insert( - Cow::from("Connections"), - Stat { - value_type: "string", - value: format!("{} ({} in / {} out)", info.connections, info.connections_in, info.connections_out), - description: Some(Cow::from("The number of peers connected (inbound and outbound)")), - copyable: false, - qr: false, - masked: false, - }, - ); - } else if info_res.status.code() == Some(28) { - return Ok(()); - } else { - eprintln!( - "Error updating network info: {}", - std::str::from_utf8(&info_res.stderr).unwrap_or("UNKNOWN ERROR") - ); - } - serde_yaml::to_writer( - std::fs::File::create("/root/.bitcoin/start9/.stats.yaml.tmp")?, - &Stats { - version: 2, - data: stats, - }, - )?; - std::fs::rename( - "/root/.bitcoin/start9/.stats.yaml.tmp", - "/root/.bitcoin/start9/stats.yaml", - )?; - Ok(()) -} - -fn inner_main(reindex: bool, reindex_chainstate: bool) -> Result<(), Box> { - while !Path::new("/root/.bitcoin/start9/config.yaml").exists() { - std::thread::sleep(std::time::Duration::from_secs(1)); - } - let config: Mapping = - serde_yaml::from_reader(std::fs::File::open("/root/.bitcoin/start9/config.yaml")?)?; - let sidecar_poll_interval = std::time::Duration::from_secs(5); - let peer_addr = var("PEER_TOR_ADDRESS")?; - let rpc_addr = var("RPC_TOR_ADDRESS")?; - let mut btc_args = vec![ - format!("-onion={}:9050", var("EMBASSY_IP")?), - format!("-externalip={}", peer_addr), - "-datadir=/root/.bitcoin".to_owned(), - "-deprecatedrpc=warnings".to_owned(), - "-conf=/root/.bitcoin/bitcoin.conf".to_owned(), - ]; - if config - .get(&Value::String("advanced".to_owned())) - .and_then(|v| v.as_mapping()) - .and_then(|v| v.get(&Value::String("peers".to_owned()))) - .and_then(|v| v.as_mapping()) - .and_then(|v| v.get(&Value::String("onlyonion".to_owned()))) - .and_then(|v| v.as_bool()) - .unwrap_or(false) - { - btc_args.push(format!("-proxy={}:9050", var("EMBASSY_IP")?)); - } - { - // disable chain data backup - let mut f = std::fs::File::create("/root/.bitcoin/.backupignore")?; - writeln!(f, "blocks/")?; - writeln!(f, "chainstate/")?; - writeln!(f, "indexes/")?; - writeln!(f, "testnet3/")?; - f.flush()?; - } - if reindex { - btc_args.push("-reindex".to_owned()); - match fs::remove_file("/root/.bitcoin/requires.reindex") { - Ok(()) => (), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => (), - a => a?, - } - } else if reindex_chainstate { - btc_args.push("-reindex-chainstate".to_owned()); - match fs::remove_file("/root/.bitcoin/requires.reindex_chainstate") { - Ok(()) => (), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => (), - a => a?, - } - } - - std::io::copy( - &mut TemplatingReader::new( - std::fs::File::open("/mnt/assets/bitcoin.conf.template")?, - &config, - &"{{var}}".parse()?, - b'%', - ), - &mut std::fs::File::create("/root/.bitcoin/bitcoin.conf")?, - )?; - let mut child = std::process::Command::new("bitcoind") - .args(btc_args) - .spawn()?; - let raw_child = child.id(); - *CHILD_PID.lock().unwrap() = Some(raw_child); - let pruned = { - config[&Value::from("advanced")][&Value::from("pruning")][&Value::from("mode")] - == "automatic" - }; - let _proxy = if pruned { - let state = Arc::new(btc_rpc_proxy::State { - rpc_client: RpcClient::new("http://127.0.0.1:18332/".parse().unwrap()), - tor: Some(TorState { - proxy: format!("{}:9050", var("EMBASSY_IP")?).parse()?, - only: config[&Value::from("advanced")][&Value::from("peers")] - [&Value::from("onlyonion")] - .as_bool() - .unwrap(), - }), - peer_timeout: Duration::from_secs(30), - peers: tokio::sync::RwLock::new(Arc::new(Peers::new())), - max_peer_age: Duration::from_secs(300), - max_peer_concurrency: Some(1), - }); - Some(std::thread::spawn(move || { - tokio::runtime::Runtime::new() - .unwrap() - .block_on(btc_rpc_proxy::main(state, ([0, 0, 0, 0], 8332).into())) - .unwrap(); - })) - } else { - None - }; - let _sidecar_handle = std::thread::spawn(move || loop { - sidecar(&config, &rpc_addr) - .err() - .map(|e| eprintln!("ERROR IN SIDECAR: {}", e)); - std::thread::sleep(sidecar_poll_interval); - }); - let child_res = child.wait()?; - let code = if let Some(code) = child_res.code() { - code - } else if let Some(signal) = child_res.signal() { - eprintln!( - "PROCESS TERMINATED BY {}", - Signal::try_from(signal) - .map(|s| s.to_string()) - .unwrap_or_else(|_| "UNKNOWN SIGNAL".to_owned()) - ); - 128 + signal - } else { - 1 - }; - - std::process::exit(code) -} - -fn main() -> Result<(), Box> { - env_logger::Builder::from_env(Env::default().default_filter_or("warn")).init(); - let reindex = Path::new("/root/.bitcoin/requires.reindex").exists(); - let reindex_chainstate = Path::new("/root/.bitcoin/requires.reindex_chainstate").exists(); - ctrlc::set_handler(move || { - if let Some(raw_child) = *CHILD_PID.lock().unwrap() { - use nix::{ - sys::signal::{kill, SIGTERM}, - unistd::Pid, - }; - kill(Pid::from_raw(raw_child as i32), SIGTERM).unwrap(); - } else { - std::process::exit(143) - } - })?; - inner_main(reindex, reindex_chainstate) -} - -fn human_readable_timestamp(unix_time: u64) -> String { - chrono::DateTime::::from( - std::time::UNIX_EPOCH + std::time::Duration::from_secs(unix_time), - ) - .format("%m/%d/%Y @ %H:%M:%S") - .to_string() -} diff --git a/manifest.yaml b/manifest.yaml deleted file mode 100644 index 194bb9cf..00000000 --- a/manifest.yaml +++ /dev/null @@ -1,197 +0,0 @@ -id: bitcoind -title: "Bitcoin Core" -version: 28.0.0.1 -eos-version: 0.3.4.4 -release-notes: | - * Update Bitcoin to [v28.0](https://github.com/bitcoin/bitcoin/releases/tag/v28.0) -license: MIT -wrapper-repo: https://github.com/Start9Labs/bitcoind-startos -upstream-repo: https://github.com/bitcoin/bitcoin -support-site: https://github.com/bitcoin/bitcoin/issues -marketing-site: https://bitcoincore.org/ -build: ["make"] -description: - short: A Bitcoin Full Node by Bitcoin Core - long: Bitcoin is an innovative payment network and a new kind of money. Bitcoin uses peer-to-peer technology to operate with no central authority or banks; managing transactions and the issuing of bitcoins is carried out collectively by the network. Bitcoin is open-source; its design is public, nobody owns or controls Bitcoin and everyone can take part. Through many of its unique properties, Bitcoin allows exciting uses that could not be covered by any previous payment system. -assets: - license: LICENSE - icon: icon.png - instructions: instructions.md -main: - type: docker - image: main - entrypoint: "docker_entrypoint.sh" - args: [] - mounts: - main: /root/.bitcoin - compat: /mnt/assets - sigterm-timeout: 5m -health-checks: - rpc: - name: RPC - success-message: The RPC server is ready for connections - type: docker - image: main - system: false - entrypoint: check-rpc.sh - args: [] - mounts: {} - io-format: yaml - inject: true - synced: - name: Synced - success-message: Bitcoin Core is synced with the network - type: docker - image: main - system: false - entrypoint: check-synced.sh - args: [] - mounts: {} - io-format: yaml - inject: true -config: - get: - type: script - set: - type: script -properties: - type: script -volumes: - main: - type: data - compat: - type: assets -alerts: - install: Notice! If Bitcoin ever gets stuck in "stopping" status, the solution is to restart your server. System -> Restart. - uninstall: Uninstalling Bitcoin Core will result in permanent loss of data. Without a backup, any funds stored on your node's default hot wallet will be lost forever. If you are unsure, we recommend making a backup, just to be safe. - restore: Restoring Bitcoin Core will overwrite its current data. You will lose any transactions recorded in watch-only wallets, and any funds you have received to the hot wallet, since the last backup. -interfaces: - rpc: - name: RPC Interface - description: Listens for JSON-RPC commands - tor-config: - port-mapping: - 8332: "8332" - lan-config: - 443: - ssl: true - internal: 8332 - ui: false - protocols: - - tcp - - http - - json-rpc - peer: - name: Peer Interface - description: Listens for incoming connections from peers on the bitcoin network - tor-config: - port-mapping: - 8333: "8333" - ui: false - protocols: - - tcp - - bitcoin - zmq: - name: ZeroMQ Interface - description: Listens for subscriptions to the ZeroMQ raw block and raw transaction event streams - tor-config: - port-mapping: - 28332: "28332" - 28333: "28333" - ui: false - protocols: - - tcp - - zmq -dependencies: {} -backup: - create: - type: docker - image: compat - system: true - entrypoint: compat - args: - - duplicity - - create - - /mnt/backup - - /root/.bitcoin - mounts: - BACKUP: /mnt/backup - main: /root/.bitcoin - restore: - type: docker - image: compat - system: true - entrypoint: compat - args: - - duplicity - - restore - - /mnt/backup - - /root/.bitcoin - mounts: - BACKUP: /mnt/backup - main: /root/.bitcoin -actions: - reindex: - name: "Reindex Blockchain" - description: "Rebuilds the block and chainstate databases starting from genesis. If blocks already exist on disk, these are used rather than being redownloaded. For pruned nodes, this means downloading the entire blockchain over again." - warning: Blocks not stored on disk will be redownloaded in order to rebuild the database. If your node is pruned, this action is equivalent to syncing the node from scratch, so this process could take weeks on low-end hardware. - allowed-statuses: - - running - - stopped - implementation: - type: docker - image: main - system: false - entrypoint: reindex.sh - args: [] - mounts: - main: /root/.bitcoin - io-format: json - reindex-chainstate: - name: "Reindex Chainstate" - description: "Rebuilds the chainstate database using existing block index data; as the block index is not rebuilt, 'reindex_chainstate' should be strictly faster than 'reindex'. This action should only be used in the case of chainstate corruption; if the blocks stored on disk are corrupted, the 'reindex' action will need to be run instead." - warning: While faster than 'Reindex', 'Reindex Chainstate' can still take several days or more to complete. Pruned nodes do not allow 'reindex-chainstate'; if you are running a pruned node and suspect chainstate corruption the 'reindex' action (requiring redownloading the entire Blockchain) should be run instead. - allowed-statuses: - - running - - stopped - implementation: - type: docker - image: main - system: false - entrypoint: reindex_chainstate.sh - args: [] - mounts: - main: /root/.bitcoin - io-format: json - delete-txindex: - name: "Delete Transaction Index" - description: "Deletes the Transaction Index (txindex) in case it gets corrupted." - warning: The Transaction Index will be rebuilt once Bitcoin Core is started again, unless you deactivate it in the config settings. Please don't do this unless instructed to by Start9 support staff. - allowed-statuses: - - stopped - implementation: - type: script - delete-coinstatsindex: - name: "Delete Coinstats Index" - description: "Deletes the Coinstats Index (coinstatsindex) in case it gets corrupted." - warning: The Coinstats Index will be rebuilt once Bitcoin Core is started again, unless you deactivate it in the config settings. Please don't do this unless instructed to by Start9 support staff. - allowed-statuses: - - stopped - implementation: - type: script - delete-peers: - name: "Delete Peer List" - description: "Deletes the Peer List (peers.dat) in case it gets corrupted." - allowed-statuses: - - stopped - implementation: - type: script -migrations: - from: - "*": - type: script - args: ["from"] - to: - "*": - type: script - args: ["to"] diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..5a600db0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,301 @@ +{ + "name": "bitcoin-core", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bitcoin-core", + "dependencies": { + "@start9labs/start-sdk": "^0.4.0-beta.42", + "@types/js-yaml": "^4.0.5", + "diskusage": "^1.2.0", + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "@types/node": "^20.11.30", + "@vercel/ncc": "^0.38.1", + "prettier": "^3.2.5", + "typescript": "^5.4.3" + } + }, + "node_modules/@iarna/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==", + "license": "ISC" + }, + "node_modules/@noble/curves": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.0.tgz", + "integrity": "sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@start9labs/start-sdk": { + "version": "0.4.0-beta.42", + "resolved": "https://registry.npmjs.org/@start9labs/start-sdk/-/start-sdk-0.4.0-beta.42.tgz", + "integrity": "sha512-satQEsdWXbZCJVhCoZxjJoi1bReuB+IzLUVJrtM9lRdiGEmz5LafEYkua0/IhrPBXIT7TFt5wL6D6Yl09N2uVQ==", + "license": "MIT", + "dependencies": { + "@iarna/toml": "^3.0.0", + "@noble/curves": "^1.8.2", + "@noble/hashes": "^1.7.2", + "@types/ini": "^4.1.1", + "deep-equality-data-structures": "^2.0.0", + "ini": "^5.0.0", + "isomorphic-fetch": "^3.0.0", + "mime": "^4.0.7", + "ts-matches": "^6.3.2", + "yaml": "^2.7.1" + } + }, + "node_modules/@types/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@types/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-MIyNUZipBTbyUNnhvuXJTY7B6qNI78meck9Jbv3wk0OgNwRyOOVEKDutAkOs1snB/tx0FafyR6/SN4Ps0hZPeg==", + "license": "MIT" + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.17.46", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.46.tgz", + "integrity": "sha512-0PQHLhZPWOxGW4auogW0eOQAuNIlCYvibIpG67ja0TOJ6/sehu+1en7sfceUn+QQtx4Rk3GxbLNwPh0Cav7TWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@vercel/ncc": { + "version": "0.38.3", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.3.tgz", + "integrity": "sha512-rnK6hJBS6mwc+Bkab+PGPs9OiS0i/3kdTO+CkI8V0/VrW3vmz7O2Pxjw/owOlmo6PKEIxRSeZKv/kuL9itnpYA==", + "dev": true, + "license": "MIT", + "bin": { + "ncc": "dist/ncc/cli.js" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/deep-equality-data-structures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/deep-equality-data-structures/-/deep-equality-data-structures-2.0.0.tgz", + "integrity": "sha512-qgrUr7MKXq7VRN+WUpQ48QlXVGL0KdibAoTX8KRg18lgOgqbEKMAW1WZsVCtakY4+XX42pbAJzTz/DlXEFM2Fg==", + "license": "MIT", + "dependencies": { + "object-hash": "^3.0.0" + } + }, + "node_modules/diskusage": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/diskusage/-/diskusage-1.2.0.tgz", + "integrity": "sha512-2u3OG3xuf5MFyzc4MctNRUKjjwK+UkovRYdD2ed/NZNZPrt0lqHnLKxGhlFVvAb4/oufIgQG3nWgwmeTbHOvXA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "es6-promise": "^4.2.8", + "nan": "^2.18.0" + } + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT" + }, + "node_modules/ini": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/mime": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.7.tgz", + "integrity": "sha512-2OfDPL+e03E0LrXaGYOtTFIYhiuzep94NSsuhrNULq+stylcJedcHdzHtz0atMUuGwJfFYs0YL5xeC/Ca2x0eQ==", + "funding": [ + "https://github.com/sponsors/broofa" + ], + "license": "MIT", + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/nan": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", + "integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/ts-matches": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-6.3.2.tgz", + "integrity": "sha512-UhSgJymF8cLd4y0vV29qlKVCkQpUtekAaujXbQVc729FezS8HwqzepqvtjzQ3HboatIqN/Idor85O2RMwT7lIQ==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/yaml": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..e8cf2e2e --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "bitcoin-core", + "scripts": { + "build": "rm -rf ./javascript && ncc build startos/index.ts -o ./javascript", + "prettier": "prettier --write startos", + "check": "tsc --noEmit" + }, + "dependencies": { + "@start9labs/start-sdk": "^0.4.0-beta.42", + "@types/js-yaml": "^4.0.5", + "diskusage": "^1.2.0", + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "@types/node": "^20.11.30", + "@vercel/ncc": "^0.38.1", + "prettier": "^3.2.5", + "typescript": "^5.4.3" + }, + "prettier": { + "trailingComma": "all", + "tabWidth": 2, + "semi": false, + "singleQuote": true + } +} diff --git a/scripts/dependencies.ts b/scripts/dependencies.ts deleted file mode 100644 index 5c469faf..00000000 --- a/scripts/dependencies.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "https://deno.land/x/embassyd_sdk@v0.3.4.3.0-alpha1/mod.ts"; diff --git a/scripts/embassy.ts b/scripts/embassy.ts deleted file mode 100644 index 14a795c6..00000000 --- a/scripts/embassy.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @ts-check -export { setConfig } from "./services/setConfig.ts"; -export { properties } from "./services/properties.ts"; -export { getConfig } from "./services/getConfig.ts"; -export { action } from "./services/action.ts"; -export { migration } from "./services/migrations.ts"; diff --git a/scripts/services/action.ts b/scripts/services/action.ts deleted file mode 100644 index 1697ee72..00000000 --- a/scripts/services/action.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { util, types as T } from "../dependencies.ts"; - -export const action = { - async "delete-txindex"( - effect: T.Effects, - _input?: T.Config - ): Promise> { - const txinfoLocation = { - path: "indexes/txindex", - volumeId: "main", - }; - if ((await util.exists(effect, txinfoLocation)) === false) { - return { - result: { - copyable: false, - message: "txindex doesn't exist", - version: "0", - qr: false, - }, - }; - } - await effect.removeDir(txinfoLocation); - return { - result: { - copyable: false, - message: "Deleted txindex", - version: "0", - qr: false, - }, - }; - }, - async "delete-peers"( - effect: T.Effects, - _input?: T.Config - ): Promise> { - const peersLocation = { - path: "peers.dat", - volumeId: "main", - }; - if ((await util.exists(effect, peersLocation)) === false) { - return { - result: { - copyable: false, - message: "peers.dat doesn't exist", - version: "0", - qr: false, - }, - }; - } - await effect.removeFile(peersLocation); - return { - result: { - copyable: false, - message: "Deleted peers.dat", - version: "0", - qr: false, - }, - }; - }, - async "delete-coinstatsindex"( - effect: T.Effects, - _input?: T.Config, - ): Promise> { - const coinstatsinfoLocation = { - path: "indexes/coinstats", - volumeId: "main", - }; - if (await util.exists(effect, coinstatsinfoLocation) === false) { - return { - result: { - copyable: false, - message: "coinstatsindex doesn't exist", - version: "0", - qr: false, - }, - }; - } - await effect.removeDir(coinstatsinfoLocation); - return { - result: { - copyable: false, - message: "Deleted coinstatsindex", - version: "0", - qr: false, - }, - }; - }, -}; diff --git a/scripts/services/getConfig.ts b/scripts/services/getConfig.ts deleted file mode 100644 index 1c801ca6..00000000 --- a/scripts/services/getConfig.ts +++ /dev/null @@ -1,393 +0,0 @@ -import { ConfigSpec } from "https://deno.land/x/embassyd_sdk@v0.3.4.3.0-alpha1/types.ts"; -import { compat, types as T } from "../dependencies.ts"; - -export const getConfig: T.ExpectedExports.getConfig = async (effects) => { - const allowUnpruned = (await effects.diskUsage()).total > 800_000_000_000; - return compat.getConfig({ - "peer-tor-address": { - name: "Peer Tor Address", - description: "The Tor address of the peer interface", - type: "pointer", - subtype: "package", - "package-id": "bitcoind", - target: "tor-address", - interface: "peer", - }, - "rpc-tor-address": { - name: "RPC Tor Address", - description: "The Tor address of the RPC interface", - type: "pointer", - subtype: "package", - "package-id": "bitcoind", - target: "tor-address", - interface: "rpc", - }, - rpc: { - type: "object", - name: "RPC Settings", - description: "RPC configuration options.", - spec: { - enable: { - type: "boolean", - name: "Enable", - description: "Allow remote RPC requests.", - default: true, - }, - username: { - type: "string", - nullable: false, - name: "Username", - description: "The username for connecting to Bitcoin over RPC.", - warning: - "You will need to restart all services that depend on Bitcoin.", - default: "bitcoin", - masked: true, - pattern: "^[a-zA-Z0-9_]+$", - "pattern-description": - "Must be alphanumeric (can contain underscore).", - }, - password: { - type: "string", - nullable: false, - name: "RPC Password", - description: "The password for connecting to Bitcoin over RPC.", - warning: - "You will need to restart all services that depend on Bitcoin.", - default: { - charset: "a-z,2-7", - len: 20, - }, - pattern: "^[a-zA-Z0-9_]+$", - "pattern-description": - "Must be alphanumeric (can contain underscore).", - copyable: true, - masked: true, - }, - advanced: { - type: "object", - name: "Advanced", - description: "Advanced RPC Settings", - spec: { - auth: { - name: "Authorization", - description: - "Username and hashed password for JSON-RPC connections. RPC clients connect using the usual http basic authentication.", - type: "list", - subtype: "string", - default: [], - spec: { - pattern: - "^[a-zA-Z0-9_-]+:([0-9a-fA-F]{2})+\\$([0-9a-fA-F]{2})+$", - "pattern-description": - 'Each item must be of the form ":$".', - }, - range: "[0,*)", - }, - servertimeout: { - name: "Rpc Server Timeout", - description: - "Number of seconds after which an uncompleted RPC call will time out.", - type: "number", - nullable: false, - range: "[5,300]", - integral: true, - units: "seconds", - default: 30, - }, - threads: { - name: "Threads", - description: - "Set the number of threads for handling RPC calls. You may wish to increase this if you are making lots of calls via an integration.", - type: "number", - nullable: false, - default: 16, - range: "[1,64]", - integral: true, - units: undefined, - }, - workqueue: { - name: "Work Queue", - description: - "Set the depth of the work queue to service RPC calls. Determines how long the backlog of RPC requests can get before it just rejects new ones.", - type: "number", - nullable: false, - default: 128, - range: "[8,256]", - integral: true, - units: "requests", - }, - }, - }, - }, - }, - "zmq-enabled": { - type: "boolean", - name: "ZeroMQ Enabled", - description: "The ZeroMQ interface is useful for some applications which might require data related to block and transaction events from Bitcoin Core. For example, LND requires ZeroMQ be enabled for LND to get the latest block data", - default: true, - }, - txindex: { - type: "boolean", - name: "Transaction Index", - description: "By enabling Transaction Index (txindex) Bitcoin Core will build a complete transaction index. This allows Bitcoin Core to access any transaction with commands like `gettransaction`.", - default: allowUnpruned, - }, - coinstatsindex: { - type: "boolean", - name: "Coinstats Index", - description: "Enabling Coinstats Index reduces the time for the gettxoutsetinfo RPC to complete at the cost of using additional disk space", - default: false, - }, - wallet: { - type: "object", - name: "Wallet", - description: "Wallet Settings", - spec: { - enable: { - name: "Enable Wallet", - description: "Load the wallet and enable wallet RPC calls.", - type: "boolean", - default: true, - }, - avoidpartialspends: { - name: "Avoid Partial Spends", - description: - "Group outputs by address, selecting all or none, instead of selecting on a per-output basis. This improves privacy at the expense of higher transaction fees.", - type: "boolean", - default: true, - }, - discardfee: { - name: "Discard Change Tolerance", - description: - "The fee rate (in BTC/kB) that indicates your tolerance for discarding change by adding it to the fee.", - type: "number", - nullable: false, - default: 0.0001, - range: "[0,.01]", - integral: false, - units: "BTC/kB", - }, - }, - }, - advanced: { - type: "object", - name: "Advanced", - description: "Advanced Settings", - spec: { - mempool: { - type: "object", - name: "Mempool", - description: "Mempool Settings", - spec: { - persistmempool: { - type: "boolean", - name: "Persist Mempool", - description: "Save the mempool on shutdown and load on restart.", - default: true, - }, - maxmempool: { - type: "number", - nullable: false, - name: "Max Mempool Size", - description: - "Keep the transaction memory pool below megabytes.", - range: "[1,*)", - integral: true, - units: "MiB", - default: 300, - }, - mempoolexpiry: { - type: "number", - nullable: false, - name: "Mempool Expiration", - description: - "Do not keep transactions in the mempool longer than hours.", - range: "[1,*)", - integral: true, - units: "Hr", - default: 336, - }, - mempoolfullrbf: { - name: "Enable Full RBF", - description: - "Policy for your node to use for relaying and mining unconfirmed transactions. For details, see https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-24.0.1.md#notice-of-new-option-for-transaction-replacement-policies", - type: "boolean", - default: true, - }, - permitbaremultisig: { - type: "boolean", - name: "Permit Bare Multisig", - description: "Relay non-P2SH multisig transactions", - default: true, - }, - datacarrier: { - type: "boolean", - name: "Relay OP_RETURN Transactions", - description: "Relay transactions with OP_RETURN outputs", - default: true, - }, - datacarriersize: { - type: "number", - nullable: false, - name: "Max OP_RETURN Size", - description: "Maximum size of data in OP_RETURN outputs to relay", - range: "[0,10000]", - integral: true, - units: "bytes", - default: 83, - }, - }, - }, - peers: { - type: "object", - name: "Peers", - description: "Peer Connection Settings", - spec: { - listen: { - type: "boolean", - name: "Make Public", - description: - "Allow other nodes to find your server on the network.", - default: true, - }, - onlyconnect: { - type: "boolean", - name: "Disable Peer Discovery", - description: "Only connect to specified peers.", - default: false, - }, - onlyonion: { - type: "boolean", - name: "Disable Clearnet", - description: "Only connect to peers over Tor.", - default: false, - }, - v2transport: { - type: "boolean", - name: "Use V2 P2P Transport Protocol", - description: - "Enable or disable the use of BIP324 V2 P2P transport protocol.", - default: true, - }, - addnode: { - name: "Add Nodes", - description: "Add addresses of nodes to connect to.", - type: "list", - subtype: "object", - range: "[0,*)", - default: [], - spec: { - spec: { - hostname: { - type: "string", - nullable: false, - name: "Hostname", - description: "Domain or IP address of bitcoin peer", - pattern: - "(^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))", - "pattern-description": - "Must be either a domain name, or an IPv4 or IPv6 address. Do not include protocol scheme (eg 'http://') or port.", - }, - port: { - type: "number", - nullable: true, - name: "Port", - description: - "Port that peer is listening on for inbound p2p connections", - range: "[0,65535]", - integral: true, - }, - }, - }, - }, - }, - }, - pruning: { - type: "union", - name: "Pruning Settings", - description: - "Blockchain Pruning Options\nReduce the blockchain size on disk\n", - warning: - "Disabling pruning will convert your node into a full archival node. This requires a resync of the entire blockchain, a process that may take several days.\n", - tag: { - id: "mode", - name: "Pruning Mode", - description: - "- Disabled: Disable pruning\n- Automatic: Limit blockchain size on disk to a certain number of megabytes\n", - "variant-names": { - disabled: "Disabled", - automatic: "Automatic", - }, - }, - variants: { - disabled: allowUnpruned ? {} : (undefined as unknown as ConfigSpec), - automatic: { - size: { - type: "number", - nullable: false, - name: "Max Chain Size", - description: "Limit of blockchain size on disk.", - warning: - "Increasing this value will require re-syncing your node.", - default: 550, - range: "[550,1000000)", - integral: true, - units: "MiB", - }, - }, - }, - default: allowUnpruned ? "disabled" : "automatic", - }, - dbcache: { - type: "number", - nullable: true, - name: "Database Cache", - description: - "How much RAM to allocate for caching the TXO set. Higher values improve syncing performance, but increase your chance of using up all your system's memory or corrupting your database in the event of an ungraceful shutdown. Set this high but comfortably below your system's total RAM during IBD, then turn down to 450 (or leave blank) once the sync completes.", - warning: - "WARNING: Increasing this value results in a higher chance of ungraceful shutdowns, which can leave your node unusable if it happens during the initial block download. Use this setting with caution. Be sure to set this back to the default (450 or leave blank) once your node is synced. DO NOT press the STOP button if your dbcache is large. Instead, set this number back to the default, hit save, and wait for bitcoind to restart on its own.", - range: "(0,*)", - integral: true, - units: "MiB", - }, - blockfilters: { - type: "object", - name: "Block Filters", - description: "Settings for storing and serving compact block filters", - spec: { - blockfilterindex: { - type: "boolean", - name: "Compute Compact Block Filters (BIP158)", - description: - "Generate Compact Block Filters during initial sync (IBD) to enable 'getblockfilter' RPC. This is useful if dependent services need block filters to efficiently scan for addresses/transactions etc.", - default: true, - }, - peerblockfilters: { - type: "boolean", - name: "Serve Compact Block Filters to Peers (BIP157)", - description: - "Serve Compact Block Filters as a peer service to other nodes on the network. This is useful if you wish to connect an SPV client to your node to make it efficient to scan transactions without having to download all block data. 'Compute Compact Block Filters (BIP158)' is required.", - default: false, - }, - }, - }, - bloomfilters: { - type: "object", - name: "Bloom Filters (BIP37)", - description: "Setting for serving Bloom Filters", - spec: { - peerbloomfilters: { - type: "boolean", - name: "Serve Bloom Filters to Peers", - description: - "Peers have the option of setting filters on each connection they make after the version handshake has completed. Bloom filters are for clients implementing SPV (Simplified Payment Verification) that want to check that block headers connect together correctly, without needing to verify the full blockchain. The client must trust that the transactions in the chain are in fact valid. It is highly recommended AGAINST using for anything except Bisq integration.", - warning: - "This is ONLY for use with Bisq integration, please use Block Filters for all other applications.", - default: false, - }, - }, - }, - }, - }, - })(effects); -}; diff --git a/scripts/services/migrations.ts b/scripts/services/migrations.ts deleted file mode 100644 index 52441737..00000000 --- a/scripts/services/migrations.ts +++ /dev/null @@ -1,299 +0,0 @@ -import { compat, matches, types as T } from "../dependencies.ts"; - -export const migration: T.ExpectedExports.migration = - compat.migrations.fromMapping( - { - "22.0.0": { - up: compat.migrations.updateConfig( - (config: any) => { - config.advanced.peers.addnode = ( - config.advanced.peers.addnode as string[] - ) - .map((node) => { - if (typeof node === "string") { - const [hostname, port] = node.split(":"); - return { hostname, port: port || null }; - } - }) - .filter((a) => !!a); - - config.advanced.blockfilters = { - blockfilterindex: false, - peerblockfilters: false, - }; - - return config; - }, - false, - { version: "22.0.0", type: "up" } - ), - down: compat.migrations.updateConfig( - (config: any) => { - config.advanced.peers.addnode = ( - config.advanced.peers.addnode as { - hostname: string; - port: number; - }[] - ) - .map((node) => { - if (typeof node === "object" && node && "hostname" in node) { - return node.hostname; - } - }) - .filter((a) => !!a); - - delete config.advanced.blockfilters; - - return config; - }, - false, - { - version: "22.0.0", - type: "down", - } - ), - }, - "22.0.1": { - up: compat.migrations.updateConfig( - (config: any) => { - config.advanced.peers.addnode = ( - config.advanced.peers.addnode as unknown[] - ) - .map((node: any) => { - if (typeof node === "string") { - return { hostname: node, port: null }; - } else if ( - typeof node === "object" && - node && - "hostname" in node - ) { - return { - hostname: node.hostname, - port: "port" in node ? node.port : null, - }; - } - }) - .filter((a) => !!a); - - return config; - }, - false, - { version: "22.0.1", type: "up" } - ), - down: compat.migrations.updateConfig((config) => config, true, { - version: "22.0.1", - type: "down", - }), - }, - "23.0.0": { - up: compat.migrations.updateConfig( - (config: any) => { - config.advanced.bloomfilters = { peerbloomfilters: false }; - - return config; - }, - false, - { version: "23.0.0", type: "up" } - ), - down: compat.migrations.updateConfig( - (config: any) => { - delete config.advanced.bloomfilters; - - return config; - }, - false, - { version: "23.0.0", type: "down" } - ), - }, - "24.0.0": { - up: compat.migrations.updateConfig( - (config: any) => { - config.advanced.mempool.mempoolfullrbf = false; - - return config; - }, - false, - { version: "24.0.0", type: "up" } - ), - down: compat.migrations.updateConfig( - (config: any) => { - delete config.advanced.mempool.mempoolfullrbf; - - return config; - }, - false, - { version: "24.0.0", type: "down" } - ), - }, - "25.0.0": { - up: compat.migrations.updateConfig( - (config: any) => { - delete config["zmq-enabled"]; - - delete config.rpc.advanced.serialversion; - config.rpc = { - ...config.rpc, - ...config.rpc.advanced, - }; - delete config.rpc.advanced; - - config.mempool = config.advanced.mempool; - delete config.advanced.mempool; - - config.peers = config.advanced.peers; - delete config.advanced.peers; - - if (config.advanced.pruning.mode === "manual") { - config.advanced.pruning = { mode: "automatic", size: 550 }; - } - - return config; - }, - true, - { version: "25.0.0", type: "up" } - ), - down: compat.migrations.updateConfig( - (config: any) => { - config["zmq-enabled"] = true; - - config.rpc.advanced.serialversion = "segwit"; - config.rpc.advanced = { - auth: config.rpc.auth, - servertimeout: config.rpc.servertimeout, - threads: config.rpc.threads, - workqueue: config.rpc.workqueue, - }; - delete config.rpc.auth; - delete config.rpc.servertimeout; - delete config.rpc.threads; - delete config.rpc.workqueue; - - config.advanced.mempool = config.mempool; - delete config.mempool; - - config.advanced.peers = config.peers; - delete config.peers; - - return config; - }, - true, - { version: "25.0.0", type: "down" } - ), - }, - "25.0.0.2": { - up: compat.migrations.updateConfig( - (config: any) => { - config["zmq-enabled"] = true; - - config.rpc.advanced = { - auth: config.rpc.auth, - servertimeout: config.rpc.servertimeout, - threads: config.rpc.threads, - workqueue: config.rpc.workqueue, - }; - delete config.rpc.auth; - delete config.rpc.servertimeout; - delete config.rpc.threads; - delete config.rpc.workqueue; - - config.advanced.mempool = config.mempool; - delete config.mempool; - - config.advanced.peers = config.peers; - delete config.peers; - - return config; - }, - true, - { version: "25.0.0.2", type: "up" } - ), - down: compat.migrations.updateConfig( - (config: any) => { - delete config["zmq-enabled"]; - - config.rpc = { - ...config.rpc, - ...config.rpc.advanced, - }; - delete config.rpc.advanced; - - config.mempool = config.advanced.mempool; - delete config.advanced.mempool; - - config.peers = config.advanced.peers; - delete config.advanced.peers; - - return config; - }, - true, - { version: "25.0.0.2", type: "down" } - ), - }, - "26.0.0": { - up: compat.migrations.updateConfig( - (config: any) => { - config.advanced.peers.v2transport = false; - - return config; - }, - true, - { version: "26.0.0", type: "up" } - ), - down: compat.migrations.updateConfig( - (config: any) => { - delete config.advanced.peers.v2transport; - - return config; - }, - true, - { version: "26.0.0", type: "down" } - ), - }, - "26.0.0.1": { - up: compat.migrations.updateConfig((config: any) => config, false, { - version: "26.0.0.1", - type: "up", - }), - down: compat.migrations.updateConfig( - (config: any) => { - delete config.coinstatsindex; - delete config.advanced.mempool.permitbaremultisig; - delete config.advanced.mempool.datacarrier; - delete config.advanced.mempool.datacarriersize; - - return config; - }, - true, - { version: "26.0.0.1", type: "down" } - ), - }, - "28.0.0.1": { - up: compat.migrations.updateConfig( - (config) => { - if ( - matches - .shape({ - advanced: matches.shape({ - blocknotify: matches.any, - }), - }) - .test(config) - ) { - throw new Error("Upgrading from Knots to Core is prohibited."); - } - return config; - }, - true, - { - version: "28.0.0.1", - type: "up", - } - ), - down: compat.migrations.updateConfig((config: any) => config, true, { - version: "28.0.0.1", - type: "down", - }), - }, - }, - "28.0.0.1" - ); diff --git a/scripts/services/properties.ts b/scripts/services/properties.ts deleted file mode 100644 index 9774199b..00000000 --- a/scripts/services/properties.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { compat } from "../dependencies.ts"; -export const properties = compat.properties \ No newline at end of file diff --git a/scripts/services/setConfig.ts b/scripts/services/setConfig.ts deleted file mode 100644 index 3645d79e..00000000 --- a/scripts/services/setConfig.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { matches, types, YAML } from "../dependencies.ts"; -const { number } = matches; - -export const setConfig: types.ExpectedExports.setConfig = async ( - effects: types.Effects, - // deno-lint-ignore no-explicit-any - newConfig: any -) => { - if (newConfig.advanced.pruning.mode === "manual" && !newConfig.rpc.enable) { - return { - error: "RPC must be enabled for manual pruning.", - }; - } - - if (newConfig.txindex && newConfig.advanced.pruning.mode !== "disabled") { - return { - error: "Txindex not allowed on pruned nodes.", - }; - } - - if ( - newConfig.coinstatsindex && - newConfig.advanced.pruning.mode !== "disabled" - ) { - return { - error: "Coinstats index not allowed on pruned nodes.", - }; - } - - if ( - newConfig.advanced.blockfilters.peerblockfilters && - !newConfig.advanced.blockfilters.blockfilterindex - ) { - return { - error: - '"Compute Compact Block Filters" must be enabled if "Serve Compact Block Filters to Peers" is enabled.', - }; - } - - await effects.createDir({ - path: "start9", - volumeId: "main", - }); - - // config-set.sh - - const oldConfig = await effects - .readFile({ - path: "start9/config.yaml", - volumeId: "main", - }) - .catch(() => null); - if (oldConfig) { - await effects.writeFile({ - path: "start9/config-old.yaml", - toWrite: oldConfig, - volumeId: "main", - }); - const oldConfigParsed = YAML.parse(oldConfig) as any; - const oldPruningTl = oldConfigParsed?.advanced?.pruning?.mode; - let oldPruningSize = 0; - if (oldPruningTl !== "disabled") { - oldPruningSize = number.unsafeCast( - oldConfigParsed?.advanced?.pruning?.size - ); - } - const newPruningTl = newConfig.advanced.pruning.mode; - let newPruningSize = 0; - if (newPruningTl !== "disabled") { - newPruningSize = number.unsafeCast(newConfig?.advanced?.pruning?.size); - } - if (oldPruningTl == "disabled" || !oldPruningTl) { - effects.debug("No reindex required"); - } else if ( - oldPruningTl === newPruningTl && - oldPruningSize >= newPruningSize - ) { - effects.debug("No reindex required"); - } else { - effects.debug("Reindex required"); - await effects.writeFile({ - path: "start9/requires.reindex", - toWrite: "", - volumeId: "main", - }); - } - } else { - effects.debug("No reindex required"); - } - - await effects.writeFile({ - path: "start9/config.yaml", - toWrite: YAML.stringify(newConfig), - volumeId: "main", - }); - - const result: types.SetResult = { - signal: "SIGTERM", - "depends-on": {}, - }; - return { result }; -}; diff --git a/startos/actions/assumeutxo.ts b/startos/actions/assumeutxo.ts new file mode 100644 index 00000000..c97b1da5 --- /dev/null +++ b/startos/actions/assumeutxo.ts @@ -0,0 +1,150 @@ +import { sdk } from '../sdk' +import { rootDir } from '../utils' +import { mainMounts } from '../main' +import { bitcoinConfFile } from '../fileModels/bitcoin.conf' +import { rpcPort } from '../utils' +import { Value } from '@start9labs/start-sdk/base/lib/actions/input/builder' +import * as fs from 'fs/promises' +import { SubContainer } from '@start9labs/start-sdk' +import { manifest } from '../manifest' +import { storeJson } from '../fileModels/store.json' + +export const snapshotTempFile = `/tmp/snap/snapshot` +const block_840_000 = + '0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5' + +let assumeutxoSubc: SubContainer | null = null +let assumeutxoPromise: Promise | null = null +let retriggerActionMetadata: (() => void) | undefined + +const assumeutxoInputSpec = sdk.InputSpec.of({ + snapshotUrl: Value.text({ + name: 'UTXO Snapshot URL', + description: 'URL of UTXO Snapshot to bootstrap bitcoin', + required: true, + default: null, // @TODO update to start9 hosted and placeholder + // @TODO add pattern for url ending in .dat + }), +}) + +export const assumeutxo = sdk.Action.withInput( + // id + 'assumeutxo', + + // metadata + async ({ effects }) => { + retriggerActionMetadata = effects.constRetry + const { snapshotInUse, fullySynced } = (await storeJson + .read() + .const(effects)) || { + snapshotInUse: false, + fullySynced: false, + } + return { + name: 'Download UTXO Snapshot (assumeutxo)', + description: + 'assumeutxo is a feature that allows fast bootstrapping of a validating bitcoind instance. It may take some additional time for any blocks between the snapshot blockheight and the tip to be downloaded and validated. While the snapshot is in use the IBD will continue in the background until it validates up to the snapshot blockheight', + warning: + "While any downloaded snapshot will be checked against a hash that's been hardcoded in source code, this action will download anything at the provided URL to the server - Only download from trusted sources!", + allowedStatuses: 'only-running', + group: null, + visibility: assumeutxoPromise + ? { disabled: 'Download in progress...' } + : snapshotInUse + ? { disabled: 'Snapshot in use' } + : fullySynced + ? 'hidden' + : 'enabled', + } + }, + + assumeutxoInputSpec, + + async ({ effects }) => {}, + + // execution function + async ({ effects, input }) => { + if (assumeutxoSubc || assumeutxoPromise) + throw new Error('already in progress') + + assumeutxoSubc = await sdk.SubContainer.of( + effects, + { imageId: 'bitcoind' }, + mainMounts.mountVolume({ + volumeId: 'main', + subpath: 'tmp', + mountpoint: '/tmp', + readonly: false, + }), + 'assumeutxo', + ) + + assumeutxoPromise = (async () => { + const conf = (await bitcoinConfFile.read().once())! + + try { + await fs.mkdir(`${assumeutxoSubc.rootfs}/tmp/snap`, { recursive: true }) + await fs.rm(`${assumeutxoSubc.rootfs}${snapshotTempFile}`, { + force: true, + }) + + await assumeutxoSubc.execFail( + ['wget', '-O', snapshotTempFile, input.snapshotUrl.trim()], + {}, + null, + ) + + do { + const getBlockHeaderRes = await assumeutxoSubc.exec([ + 'bitcoin-cli', + `-conf=${rootDir}/bitcoin.conf`, + `-rpccookiefile=${rootDir}/.cookie`, + `-rpcport=${conf.prune ? 18332 : rpcPort}`, + 'getblockheader', + block_840_000, + ]) + if (getBlockHeaderRes.exitCode !== 0) { + await new Promise((resolve) => setTimeout(resolve, 10_000)) + continue + } + break + } while (true) + + await assumeutxoSubc.execFail( + [ + 'bitcoin-cli', + `-conf=${rootDir}/bitcoin.conf`, + `-rpccookiefile=${rootDir}/.cookie`, + `-rpcport=${conf.prune ? 18332 : rpcPort}`, + '-rpcclienttimeout=0', + 'loadtxoutset', + `${rootDir}/${snapshotTempFile}`, + ], + {}, + null, + ) + await storeJson.merge(effects, { snapshotInUse: true }) + } catch (e) { + console.log('Error downloading snapshot:\n', e) + await sdk.action.createOwnTask(effects, assumeutxo, 'important', { + reason: 'Previous attempt to download Snapshot failed.', + }) + } finally { + await assumeutxoSubc.destroy() + assumeutxoSubc = null + assumeutxoPromise = null + retriggerActionMetadata?.() + } + })() + + retriggerActionMetadata?.() + + return { + version: '1', + title: 'Success', + message: + 'Snapshot download in progress. Upon successful download the snapshot will be loaded as the active chainstate and any blocks between the snapshot blockheight and tip will be downloaded and verified. Blocks from genesis to the snapshot blockheight will continue to be verfied in the background. Once the IBD catches up to the snapshot height the chain will have been fully validated', + result: null, + } + }, +) diff --git a/startos/actions/config/mempool.ts b/startos/actions/config/mempool.ts new file mode 100644 index 00000000..53b5b2d7 --- /dev/null +++ b/startos/actions/config/mempool.ts @@ -0,0 +1,129 @@ +import { T } from '@start9labs/start-sdk' +import { bitcoinConfFile, shape } from '../../fileModels/bitcoin.conf' +import { sdk } from '../../sdk' +import { bitcoinConfDefaults } from '../../utils' + +const { + persistmempool, + maxmempool, + mempoolexpiry, + mempoolfullrbf, + permitbaremultisig, + datacarrier, + datacarriersize, +} = bitcoinConfDefaults + +const { Value } = sdk + +const mempoolSpec = sdk.InputSpec.of({ + persistmempool: Value.toggle({ + name: 'Persist Mempool', + default: persistmempool, + description: 'Save the mempool on shutdown and load on restart.', + }), + maxmempool: Value.number({ + name: 'Max Mempool Size', + description: 'Keep the transaction memory pool below megabytes.', + required: false, + default: maxmempool, + min: 1, + integer: true, + units: 'MiB', + placeholder: maxmempool.toString(), + }), + mempoolexpiry: Value.number({ + name: 'Mempool Expiration', + description: + 'Do not keep transactions in the mempool longer than hours.', + required: false, + default: mempoolexpiry, + min: 1, + integer: true, + units: 'Hr', + placeholder: mempoolexpiry.toString(), + }), + mempoolfullrbf: Value.toggle({ + name: 'Enable Full RBF', + default: mempoolfullrbf, + description: + 'Policy for your node to use for relaying and mining unconfirmed transactions. For details, see https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-24.0.1.md#notice-of-new-option-for-transaction-replacement-policies', + }), + permitbaremultisig: Value.toggle({ + name: 'Permit Bare Multisig', + default: permitbaremultisig, + description: 'Relay non-P2SH multisig transactions', + }), + datacarrier: Value.toggle({ + name: 'Relay OP_RETURN Transactions', + default: datacarrier, + description: 'Relay transactions with OP_RETURN outputs', + }), + datacarriersize: Value.number({ + name: 'Max OP_RETURN Size', + description: 'Maximum size of data in OP_RETURN outputs to relay', + required: false, + default: datacarriersize, + min: 0, + max: 10_000, + integer: true, + units: 'bytes', + placeholder: datacarriersize.toString(), + }), +}) + +export const mempoolConfig = sdk.Action.withInput( + // id + 'mempool-config', + + // metadata + async ({ effects }) => ({ + name: 'Mempool Settings', + description: 'Edit the Mempool settings in bitcoin.conf', + warning: null, + allowedStatuses: 'any', + group: 'Configuration', + visibility: 'enabled', + }), + + // form input specification + mempoolSpec, + + // optionally pre-fill the input form + ({ effects }) => read(effects), + + // the execution function + ({ effects, input }) => write(effects, input), +) + +async function read(effects: any): Promise { + const bitcoinConf = await bitcoinConfFile.read().const(effects) + if (!bitcoinConf) return {} + + const mempoolSettings: PartialMempoolSpec = { + maxmempool: bitcoinConf.maxmempool, + mempoolexpiry: bitcoinConf.mempoolexpiry, + datacarriersize: bitcoinConf.datacarriersize, + mempoolfullrbf: bitcoinConf.mempoolfullrbf, + persistmempool: bitcoinConf.persistmempool, + datacarrier: bitcoinConf.datacarrier, + permitbaremultisig: bitcoinConf.permitbaremultisig, + } + return mempoolSettings +} + +async function write(effects: T.Effects, input: MempoolSpec) { + const mempoolSettings = { + mempoolfullrbf: input.mempoolfullrbf, + persistmempool: input.persistmempool, + datacarrier: input.datacarrier, + permitbaremultisig: input.permitbaremultisig, + maxmempool: input.maxmempool || maxmempool, + mempoolexpiry: input.mempoolexpiry || mempoolexpiry, + datacarriersize: input.datacarriersize || datacarriersize, + } + + await bitcoinConfFile.merge(effects, mempoolSettings) +} + +type MempoolSpec = typeof mempoolSpec._TYPE +type PartialMempoolSpec = typeof mempoolSpec._PARTIAL diff --git a/startos/actions/config/other.ts b/startos/actions/config/other.ts new file mode 100644 index 00000000..3de870db --- /dev/null +++ b/startos/actions/config/other.ts @@ -0,0 +1,242 @@ +import { sdk } from '../../sdk' +import { utils } from '@start9labs/start-sdk' +import * as diskusage from 'diskusage' +import { bitcoinConfFile } from '../../fileModels/bitcoin.conf' +import { bitcoinConfDefaults } from '../../utils' +import { T } from '@start9labs/start-sdk' + +const { + coinstatsindex, + disablewallet, + avoidpartialspends, + discardfee, + prune, + dbcache, + dbbatchsize, + blockfilterindex, + peerblockfilters, + peerbloomfilters, + blocknotify, +} = bitcoinConfDefaults + +const { InputSpec, Value } = sdk +const diskUsage = utils.once(() => diskusage.check('/')) +const archivalMin = 900_000_000_000 + +const configSpec = sdk.InputSpec.of({ + zmqEnabled: Value.toggle({ + name: 'ZeroMQ Enabled', + default: true, + description: + 'The ZeroMQ interface is useful for some applications which might require data related to block and transaction events from Bitcoin Core. For example, LND requires ZeroMQ be enabled for LND to get the latest block data', + }), + txindex: Value.dynamicToggle(async ({ effects }) => { + const disk = await diskUsage() + return { + name: 'Transaction Index', + default: disk.total >= archivalMin, + description: + 'By enabling Transaction Index (txindex) Bitcoin Core will build a complete transaction index. This allows Bitcoin Core to access any transaction with commands like `getrawtransaction`.', + disabled: disk.total < archivalMin ? 'Not enough disk space' : false, + } + }), + blocknotify: Value.text({ + name: 'Block Notify', + required: false, + default: null, + description: 'Execute an arbitrary command when the best block changes', + }), + coinstatsindex: Value.toggle({ + name: 'Coinstats Index', + default: coinstatsindex, + description: + 'Enabling Coinstats Index reduces the time for the gettxoutsetinfo RPC to complete at the cost of using additional disk space', + }), + wallet: Value.object( + { name: 'Wallet', description: 'Wallet Settings' }, + InputSpec.of({ + enable: Value.toggle({ + name: 'Enable Wallet', + default: !!!disablewallet, + description: 'Load the wallet and enable wallet RPC calls.', + }), + avoidpartialspends: Value.toggle({ + name: 'Avoid Partial Spends', + default: !!avoidpartialspends, + description: + 'Group outputs by address, selecting all or none, instead of selecting on a per-output basis. This improves privacy at the expense of higher transaction fees.', + }), + discardfee: Value.number({ + name: 'Discard Change Tolerance', + description: + 'The fee rate (in BTC/kB) that indicates your tolerance for discarding change by adding it to the fee.', + required: false, + default: null, + min: 0, + max: 0.01, + integer: false, + units: 'BTC/kB', + placeholder: '.0001', + }), + }), + ), + prune: Value.dynamicNumber(async ({ effects }) => { + const disk = await diskUsage() + + return { + name: 'Pruning', + description: + 'Set the maximum size of the blockchain you wish to store on disk. If your disk is larger than .9TB this value can be set to zero (0) to maintain a full archival node.', + warning: 'Increasing this value will require re-syncing your node.', + placeholder: 'Enter max blockchain size', + required: disk.total < archivalMin, + default: disk.total < archivalMin ? 550 : null, + integer: true, + units: 'MiB', + min: 0, + } + }), + dbcache: Value.number({ + name: 'Database Cache', + description: + "How much RAM to allocate for caching the TXO set. Higher values improve syncing performance, but may result in some re-work in the event of an ungraceful shutdown. 4-7GB is high enough to get most of the peformance benefit during IBD. Consider reducing this setting for lower resource devices (or a device with less available RAM)", + required: false, + default: dbcache, + min: 0, + integer: true, + units: 'MiB', + placeholder: dbcache.toString(), + }), + dbbatchsize: Value.number({ + name: 'Database Batch', + description: + "Maximum database write batch size in bytes. Higher values will speed up the critical sections when the utxo set is written to disk from memory in big batches.", + required: false, + default: dbbatchsize, + min: 0, + integer: true, + units: 'Bytes', + placeholder: dbbatchsize.toString(), + }), + blockfilters: Value.object( + { + name: 'Block Filters', + description: 'Settings for storing and serving compact block filters', + }, + InputSpec.of({ + blockfilterindex: Value.toggle({ + name: 'Compute Compact Block Filters (BIP158)', + default: !!blockfilterindex, + description: + "Generate Compact Block Filters during initial sync (IBD) to enable 'getblockfilter' RPC. This is useful if dependent services need block filters to efficiently scan for addresses/transactions etc.", + }), + peerblockfilters: Value.toggle({ + name: 'Serve Compact Block Filters to Peers (BIP157)', + default: !!peerblockfilters, + description: + "Serve Compact Block Filters as a peer service to other nodes on the network. This is useful if you wish to connect an SPV client to your node to make it efficient to scan transactions without having to download all block data. 'Compute Compact Block Filters (BIP158)' is required.", + }), + }), + ), + peerbloomfilters: Value.toggle({ + name: 'Serve Bloom Filters to Peers', + default: !!peerbloomfilters, + description: + 'Peers have the option of setting filters on each connection they make after the version handshake has completed. Bloom filters are for clients implementing SPV (Simplified Payment Verification) that want to check that block headers connect together correctly, without needing to verify the full blockchain. The client must trust that the transactions in the chain are in fact valid. It is highly recommended AGAINST using for anything except Bisq integration.', + warning: + 'This is ONLY for use with Bisq integration, please use Block Filters for all other applications.', + }), +}) + +export const config = sdk.Action.withInput( + // id + 'config', + + // metadata + async ({ effects }) => ({ + name: 'Other Settings', + description: 'Edit more values in bitcoin.conf', + warning: null, + allowedStatuses: 'any', + group: 'Configuration', + visibility: 'enabled', + }), + + // form input specification + configSpec, + + // optionally pre-fill the input form + ({ effects }) => read(effects), + + // the execution function + ({ effects, input }) => write(effects, input), +) + +async function read(effects: any): Promise { + const bitcoinConf = await bitcoinConfFile.read().const(effects) + if (!bitcoinConf) return {} + + return { + zmqEnabled: + !!bitcoinConf?.zmqpubhashblock && + bitcoinConf.zmqpubhashblock !== '' && + !!bitcoinConf?.zmqpubhashtx && + bitcoinConf.zmqpubhashtx !== '' && + !!bitcoinConf?.zmqpubrawblock && + bitcoinConf.zmqpubrawblock !== '' && + !!bitcoinConf?.zmqpubrawtx && + bitcoinConf.zmqpubrawtx !== '' && + !!bitcoinConf?.zmqpubsequence && + bitcoinConf.zmqpubsequence !== '', + txindex: bitcoinConf.txindex, + coinstatsindex: bitcoinConf.coinstatsindex, + wallet: { + enable: !bitcoinConf.disablewallet, + avoidpartialspends: bitcoinConf.avoidpartialspends, + discardfee: bitcoinConf.discardfee, + }, + blocknotify: bitcoinConf.blocknotify, + prune: bitcoinConf.prune, + dbcache: bitcoinConf.dbcache, + blockfilters: { + blockfilterindex: bitcoinConf.blockfilterindex === ('basic' as const), + peerblockfilters: bitcoinConf.peerblockfilters, + }, + peerbloomfilters: bitcoinConf.peerbloomfilters, + } +} + +async function write(effects: T.Effects, input: ConfigSpec) { + const otherConfig = { + // RPC + rpcbind: input.prune ? '127.0.0.1:18332' : '0.0.0.0:8332', + rpcallowip: input.prune ? '127.0.0.1/32' : '0.0.0.0/0', + + // Wallet + disablewallet: !input.wallet.enable, + avoidpartialspends: input.wallet.avoidpartialspends, + discardfee: input.wallet.discardfee || discardfee, + + // Other + txindex: input.txindex, + coinstatsindex: input.coinstatsindex, + peerbloomfilters: input.peerbloomfilters, + peerblockfilters: input.blockfilters.peerblockfilters, + blockfilterindex: input.blockfilters.blockfilterindex + ? ('basic' as const) + : false, + blocknotify: input.blocknotify ? input.blocknotify : blocknotify, + prune: input.prune ? input.prune : prune, + dbcache: input.dbcache ? input.dbcache : dbcache, + zmqpubrawblock: input.zmqEnabled ? 'tcp://0.0.0.0:28332' : '', + zmqpubhashblock: input.zmqEnabled ? 'tcp://0.0.0.0:28332' : '', + zmqpubrawtx: input.zmqEnabled ? 'tcp://0.0.0.0:28333' : '', + zmqpubhashtx: input.zmqEnabled ? 'tcp://0.0.0.0:28333' : '', + zmqpubsequence: input.zmqEnabled ? 'tcp://0.0.0.0:28333' : '', + } + + await bitcoinConfFile.merge(effects, otherConfig) +} + +type ConfigSpec = typeof configSpec._TYPE +type PartialConfigSpec = typeof configSpec._PARTIAL diff --git a/startos/actions/config/peers.ts b/startos/actions/config/peers.ts new file mode 100644 index 00000000..bce33c58 --- /dev/null +++ b/startos/actions/config/peers.ts @@ -0,0 +1,168 @@ +import { T } from '@start9labs/start-sdk' +import { bitcoinConfFile, shape } from '../../fileModels/bitcoin.conf' +import { sdk } from '../../sdk' +import { bitcoinConfDefaults, getExteralAddresses } from '../../utils' + +const { onlynet, v2transport, externalip, addnode, connect } = + bitcoinConfDefaults +const { Value, Variants, List, InputSpec } = sdk +const validNets = ['ipv4', 'ipv6', 'onion', 'i2p', 'cjdns'] as const +type ValidNets = (typeof validNets)[number] + +const peerSpec = sdk.InputSpec.of({ + onlynet: Value.multiselect({ + name: 'Onlynet', + description: + 'Make automatic outbound connections only to network (ipv4, ipv6, onion, i2p, cjdns). Inbound and manual connections are not affected by this option', + values: { + ipv4: 'ipv4', + ipv6: 'ipv6', + onion: 'onion (Tor)', + i2p: 'i2p', + cjdns: 'cjdns', + }, + default: [], + }), + v2transport: Value.toggle({ + name: 'Use V2 P2P Transport Protocol', + default: v2transport, + description: + 'Enable or disable the use of BIP324 V2 P2P transport protocol.', + }), + externalip: getExteralAddresses(), + connectpeer: Value.union({ + name: 'Connect Peer', + default: 'addnode', + variants: Variants.of({ + connect: { + name: 'Connect', + spec: InputSpec.of({ + peers: Value.list( + List.text( + { + name: 'Connect Nodes', + minLength: 1, + description: + 'Add addresses of nodes for Bitcoin to EXCLUSIVELY connect to.', + }, + { + patterns: [ + { + regex: + '(^s*((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?:[0-9]{1,5}))s*$)|(^s*((?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?(?:.[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?)*.?:[0-9]{1,5})s*$)|(^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?:[0-9]{1,5}s*$)', + description: + "Must be either a domain name, or an IPv4 or IPv6 address. Be sure to include the port number, but do not include protocol scheme (eg 'http://').", + }, + ], + }, + ), + ), + }), + }, + addnode: { + name: 'Add Node', + spec: InputSpec.of({ + peers: Value.list( + List.text( + { + name: 'Add Nodes', + description: + 'Add addresses of nodes for Bitcoin to connect with in addition to default nodes.', + }, + { + inputmode: 'text', + patterns: [ + { + regex: + '(^s*((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?:[0-9]{1,5}))s*$)|(^s*((?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?(?:.[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?)*.?:[0-9]{1,5})s*$)|(^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?:[0-9]{1,5}s*$)', + description: + "Must be either a domain name, or an IPv4 or IPv6 address. Be sure to include the port number, but do not include protocol scheme (eg 'http://').", + }, + ], + }, + ), + ), + }), + }, + }), + }), +}) + +export const peerConfig = sdk.Action.withInput( + // id + 'peers-config', + + // metadata + async ({ effects }) => ({ + name: 'Peer Settings', + description: 'Edit the Peer settings in bitcoin.conf', + warning: null, + allowedStatuses: 'any', + group: 'Configuration', + visibility: 'enabled', + }), + + // form input specification + peerSpec, + + // optionally pre-fill the input form + ({ effects }) => read(effects), + + // the execution function + ({ effects, input }) => write(effects, input), +) + +async function read(effects: any): Promise { + const bitcoinConf = await bitcoinConfFile.read().const(effects) + if (!bitcoinConf) return {} + + const peerSettings: PartialPeerSpec = { + onlynet: bitcoinConf.onlynet + ? [bitcoinConf.onlynet] + .flat() + .filter( + (x): x is ValidNets => + x !== undefined && (validNets as readonly string[]).includes(x), + ) + : onlynet, + v2transport: bitcoinConf.v2transport, + externalip: + bitcoinConf.externalip === undefined ? 'none' : bitcoinConf.externalip, + connectpeer: { + selection: bitcoinConf.connect !== undefined ? 'connect' : 'addnode', + value: { + peers: + bitcoinConf.connect !== undefined + ? [bitcoinConf.connect] + .flat() + .filter((x): x is string => x !== undefined) + : [bitcoinConf.addnode] + .flat() + .filter((x): x is string => x !== undefined), + }, + }, + } + + return peerSettings +} + +async function write(effects: T.Effects, input: peerSpec) { + const peerSettings = { + v2transport: input.v2transport, + onlynet: input.onlynet.length > 0 ? input.onlynet : onlynet, + externalip: input.externalip !== 'none' ? input.externalip : externalip, + } + + if (input.connectpeer.selection === 'connect') { + Object.assign(peerSettings, { connect: input.connectpeer.value.peers }) + Object.assign(peerSettings, { addnode: addnode }) + } else if (input.connectpeer.selection === 'addnode') { + Object.assign(peerSettings, { addnode: input.connectpeer.value.peers }) + Object.assign(peerSettings, { connect: connect }) + } + + await bitcoinConfFile.merge(effects, peerSettings) +} + +type peerSpec = typeof peerSpec._TYPE +type PartialPeerSpec = typeof peerSpec._PARTIAL diff --git a/startos/actions/config/rpc.ts b/startos/actions/config/rpc.ts new file mode 100644 index 00000000..e9a83180 --- /dev/null +++ b/startos/actions/config/rpc.ts @@ -0,0 +1,100 @@ +import { T } from '@start9labs/start-sdk' +import { bitcoinConfFile, shape } from '../../fileModels/bitcoin.conf' +import { sdk } from '../../sdk' +import { bitcoinConfDefaults } from '../../utils' + +const { Value } = sdk +const { rpcservertimeout, rpcthreads, rpcworkqueue } = bitcoinConfDefaults + +const rpcSpec = sdk.InputSpec.of({ + servertimeout: Value.number({ + name: 'Rpc Server Timeout', + description: + 'Number of seconds after which an uncompleted RPC call will time out.', + required: false, + default: rpcservertimeout, + min: 5, + max: 300, + integer: true, + units: 'seconds', + placeholder: rpcservertimeout.toString(), + }), + threads: Value.number({ + name: 'Threads', + description: + 'Set the number of threads for handling RPC calls. You may wish to increase this if you are making lots of calls via an integration.', + + required: false, + default: rpcthreads, + min: 4, + max: 64, + step: null, + integer: true, + units: null, + placeholder: rpcthreads.toString(), + }), + workqueue: Value.number({ + name: 'Work Queue', + description: + 'Set the depth of the work queue to service RPC calls. Determines how long the backlog of RPC requests can get before it just rejects new ones.', + + required: false, + default: rpcworkqueue, + min: 8, + max: 256, + step: null, + integer: true, + units: 'requests', + placeholder: rpcworkqueue.toString(), + }), +}) + +export const rpcConfig = sdk.Action.withInput( + // id + 'rpc-config', + + // metadata + async ({ effects }) => ({ + name: 'RPC Settings', + description: 'Edit the RPC settings in bitcoin.conf', + warning: null, + allowedStatuses: 'any', + group: 'Configuration', + visibility: 'enabled', + }), + + // form input specification + rpcSpec, + + // optionally pre-fill the input form + ({ effects }) => read(effects), + + // the execution function + ({ effects, input }) => write(effects, input), +) + +async function read(effects: any): Promise { + const bitcoinConf = await bitcoinConfFile.read().const(effects) + if (!bitcoinConf) return {} + + return { + servertimeout: bitcoinConf.rpcservertimeout, + threads: bitcoinConf.rpcthreads, + workqueue: bitcoinConf.rpcworkqueue, + } +} + +async function write(effects: T.Effects, input: RpcSpec) { + const { servertimeout, threads, workqueue } = input + + const rpcSettings = { + rpcservertimeout: servertimeout || rpcservertimeout, + rpcthreads: threads || rpcthreads, + rpcworkqueue: workqueue || rpcworkqueue, + } + + await bitcoinConfFile.merge(effects, rpcSettings) +} + +type RpcSpec = typeof rpcSpec._TYPE +type PartialRpcSpec = typeof rpcSpec._PARTIAL diff --git a/startos/actions/deleteCoinstatsIndex.ts b/startos/actions/deleteCoinstatsIndex.ts new file mode 100644 index 00000000..aa55b96d --- /dev/null +++ b/startos/actions/deleteCoinstatsIndex.ts @@ -0,0 +1,43 @@ +import { mainMounts } from '../main' +import { sdk } from '../sdk' +import * as fs from 'fs/promises' +import { rootDir } from '../utils' + +export const deleteCoinstatsIndex = sdk.Action.withoutInput( + // id + 'delete-coinstats-index', + + // metadata + async ({ effects }) => ({ + name: 'Delete Coinstats Index', + description: + 'Deletes the Coinstats Index (coinstatsindex) in case it gets corrupted.', + warning: + "The Coinstats Index will be rebuilt once Bitcoin Core is started again, unless 'Transaction Index' is disabled in the config settings. Please don't do this unless you fully understand what you are doing.", + allowedStatuses: 'only-stopped', + group: 'Delete Corrupted Files', + visibility: 'enabled', + }), + + // execution function + async ({ effects }) => { + await sdk.SubContainer.withTemp( + effects, + { imageId: 'bitcoind' }, + mainMounts, + 'delete-coinstats', + async (subc) => { + await fs.rmdir(`${subc.rootfs}/${rootDir}/indexes/coinstatsindex/`, { + recursive: true, + }) + }, + ) + + return { + version: '1', + title: 'Success', + message: 'Successfully deleted coinstats index', + result: null, + } + }, +) diff --git a/startos/actions/deletePeers.ts b/startos/actions/deletePeers.ts new file mode 100644 index 00000000..1fed7da6 --- /dev/null +++ b/startos/actions/deletePeers.ts @@ -0,0 +1,39 @@ +import { mainMounts } from '../main' +import { sdk } from '../sdk' +import * as fs from 'fs/promises' +import { rootDir } from '../utils' + +export const deletePeers = sdk.Action.withoutInput( + // id + 'delete-peers', + + // metadata + async ({ effects }) => ({ + name: 'Delete Peer List', + description: 'Deletes the Peer List (peers.dat) in case it gets corrupted.', + warning: null, + allowedStatuses: 'only-stopped', + group: 'Delete Corrupted Files', + visibility: 'enabled', + }), + + // execution function + async ({ effects }) => { + await sdk.SubContainer.withTemp( + effects, + { imageId: 'bitcoind' }, + mainMounts, + 'delete-peers', + async (subc) => { + await fs.rm(`${subc.rootfs}/${rootDir}/peers.dat`, {force: true}) + }, + ) + + return { + version: '1', + title: 'Success', + message: 'Successfully deleted peers.dat', + result: null, + } + }, +) diff --git a/startos/actions/deleteRpcAuth.ts b/startos/actions/deleteRpcAuth.ts new file mode 100644 index 00000000..eab43792 --- /dev/null +++ b/startos/actions/deleteRpcAuth.ts @@ -0,0 +1,67 @@ +import { Effects } from '@start9labs/start-sdk/base/lib/Effects' +import { bitcoinConfFile } from '../fileModels/bitcoin.conf' +import { sdk } from '../sdk' +const { InputSpec, Value } = sdk + + +export async function getRpcUsers(effects: Effects) { + const rpcauth = await getRpcAuth(effects) + if (!rpcauth) return + return [rpcauth].flat().map((e) => e.split(':', 2)[0]) +} + +export async function getRpcAuth(effects: Effects) { + return (await bitcoinConfFile.read().const(effects))?.rpcauth +} + +export const inputSpec = InputSpec.of({ + deletedRpcUsers: Value.dynamicMultiselect(async ({ effects }) => { + const existingUsernames = (await getRpcUsers(effects)) || [] + + return { + name: 'Existing RPC Users', + default: [], + values: existingUsernames.reduce( + (obj, curr) => ({ ...obj, [curr]: curr }), + {} as Record, + ), + } + }), +}) + +export const deleteRpcAuth = sdk.Action.withInput( + // id + 'delete-rpcauth', + + // metadata + async ({ effects }) => { + const rpcUsers = await getRpcUsers(effects) + return { + name: 'Delete RPC Users', + description: + 'Delete RPC users from Bitcoin.conf. You may want to run this action if the RPC Auth entry is no longer needed or if the password is lost.', + warning: null, + allowedStatuses: 'any', + group: 'RPC Users', + visibility: + rpcUsers && rpcUsers.length > 0 + ? 'enabled' + : { disabled: 'There are no RPC users' }, + } + }, + + // input spec + inputSpec, + + // optionally pre-fill form + async ({ effects }) => {}, + + // execution function + async ({ effects, input }) => { + const rpcauth = (await getRpcAuth(effects))! + const filtered = [rpcauth] + .flat() + .filter((auth) => !input.deletedRpcUsers.includes(auth.split(':', 2)[0])) + await bitcoinConfFile.merge(effects, { rpcauth: filtered }) + }, +) diff --git a/startos/actions/deleteTxIndex.ts b/startos/actions/deleteTxIndex.ts new file mode 100644 index 00000000..1f65eb4a --- /dev/null +++ b/startos/actions/deleteTxIndex.ts @@ -0,0 +1,43 @@ +import { mainMounts } from '../main' +import { sdk } from '../sdk' +import * as fs from 'fs/promises' +import { rootDir } from '../utils' + +export const deleteTxIndex = sdk.Action.withoutInput( + // id + 'delete-txindex', + + // metadata + async ({ effects }) => ({ + name: 'Delete Transaction Index', + description: + 'Deletes the Transaction Index (txindex) in the event it gets corrupted.', + warning: + "The Transaction Index will be rebuilt once Bitcoin Core is started again, unless 'Coinstats Index' is disabled in the config settings. Please don't do this unless you fully understand what you are doing.", + allowedStatuses: 'only-stopped', + group: 'Delete Corrupted Files', + visibility: 'enabled', + }), + + // execution function + async ({ effects }) => { + await sdk.SubContainer.withTemp( + effects, + { imageId: 'bitcoind' }, + mainMounts, + 'delete-txindex', + async (subc) => { + await fs.rmdir(`${subc.rootfs}/${rootDir}/indexes/txindex`, { + recursive: true, + }) + }, + ) + + return { + version: '1', + title: 'Success', + message: 'Successfully deleted txindex', + result: null, + } + }, +) diff --git a/startos/actions/generateRpcAuth.ts b/startos/actions/generateRpcAuth.ts new file mode 100644 index 00000000..6316d600 --- /dev/null +++ b/startos/actions/generateRpcAuth.ts @@ -0,0 +1,98 @@ +import { bitcoinConfFile } from '../fileModels/bitcoin.conf' +import { sdk } from '../sdk' +import { getRpcAuth, getRpcUsers } from './deleteRpcAuth' +const { InputSpec, Value } = sdk + +export const inputSpec = InputSpec.of({ + username: Value.text({ + name: 'Username', + description: 'RPC Auth Username', + required: true, + default: null, + patterns: [ + { + regex: '^[a-zA-Z0-9_]+$', + description: 'Must be alphanumeric (can contain underscore).', + }, + ], + }), +}) + +export const generateRpcUser = sdk.Action.withInput( + // id + 'generate-rpcuser', + + // metadata + async ({ effects }) => ({ + name: 'Generate RPC User Credentials', + description: + 'Generate RPC User Credentials for remote connections i.e. Sparrow. rpcauth.py will randomly generate a secure password. The username and hashed password will be persisted in Bitcoin.conf', + warning: null, + allowedStatuses: 'any', + group: 'RPC Users', + visibility: 'enabled', + }), + + // input spec + inputSpec, + + // optionally pre-fill form + async ({ effects }) => {}, + + // execution function + async ({ effects, input }) => { + const existingUsernames = await getRpcUsers(effects) + + if (existingUsernames?.includes(input.username)) { + return { + version: '1', + title: 'Error creating RPC Auth User', + message: 'RPCAuth entry with this username already exists.', + result: null, + } + } + + const mountpoint = '/scripts' + + const res = await sdk.SubContainer.withTemp( + effects, + { imageId: 'python' }, + sdk.Mounts.of().mountAssets({ subpath: null, mountpoint }), + 'rpc-auth-generator', + (subc) => + subc.exec(['python3', `${mountpoint}/rpcauth.py`, `${input.username}`]), + ) + + if (res.exitCode === 0 && typeof res.stdout === 'string') { + const password = res.stdout.split('\n')[3].trim() + const newRpcAuth = res.stdout.split('\n')[1].trim().split('=')[1].trim() + + const existingRpcAuthEntries = (await getRpcAuth(effects)) || [] + const rpcAuthEntries = [existingRpcAuthEntries].flat() + rpcAuthEntries.push(newRpcAuth) + + await bitcoinConfFile.merge(effects, { rpcauth: rpcAuthEntries }) + + return { + version: '1', + title: 'RPC user successfully created', + message: `RPC password created for ${input.username}. Store this password in a secure place. If lost, a new RPC user will need to be created as Bitcoin.conf only stores a hash of the password`, + result: { + name: 'RPC Password', + type: 'single', + value: password, + description: `${input.username} RPC Password`, + copyable: true, + masked: true, + qr: false, + }, + } + } + return { + version: '1', + title: 'Failed to create RPC user', + message: `rpcauth.py failed with error: ${res.stderr}`, + result: null, + } + }, +) diff --git a/startos/actions/generateRpcUserDependent.ts b/startos/actions/generateRpcUserDependent.ts new file mode 100644 index 00000000..fee1efbb --- /dev/null +++ b/startos/actions/generateRpcUserDependent.ts @@ -0,0 +1,114 @@ +import { bitcoinConfFile } from '../fileModels/bitcoin.conf' +import { sdk } from '../sdk' +import { getRpcAuth, getRpcUsers } from './deleteRpcAuth' +const { InputSpec, Value } = sdk + +export const inputSpec = InputSpec.of({ + username: Value.dynamicText(async ({ effects }) => { + return { + name: 'Username', + description: 'RPC Auth Username', + disabled: 'Cannot edit dependent specified username', + required: true, + default: null, + patterns: [ + { + regex: '^[a-zA-Z0-9_]+$', + description: 'Must be alphanumeric (can contain underscore).', + }, + ], + } + }), + password: Value.dynamicText(async ({ effects }) => { + return { + name: 'Password', + description: 'RPC Auth Password', + disabled: 'Cannot edit dependent specified password', + required: true, + default: null, + patterns: [ + { + regex: '^[A-Za-z0-9_-]+$', + description: 'Must be alphanumeric (can contain underscore).', + }, + ], + } + }), +}) + +export const generateRpcUserDependent = sdk.Action.withInput( + // id + 'generate-rpc-dependent', + + // metadata + async ({ effects }) => ({ + name: 'Generate RPC Dependent Credentials', + description: + 'Generate RPC Credentials using the provided username and password for a dependent service running locally', + warning: null, + allowedStatuses: 'any', + group: null, + visibility: 'hidden', + }), + + // input spec + inputSpec, + + // optionally pre-fill form + async ({ effects }) => {}, + + // execution function + async ({ effects, input }) => { + const existingUsernames = await getRpcUsers(effects) + const { username, password } = input + + if (existingUsernames?.includes(username!)) { + return { + version: '1', + title: 'Error creating RPC Auth User', + message: 'RPCAuth entry with this username already exists.', + result: null, + } + } + + const mountpoint = '/scripts' + + const res = await sdk.SubContainer.withTemp( + effects, + { imageId: 'python' }, + sdk.Mounts.of().mountAssets({ subpath: null, mountpoint }), + 'RPC Auth Generator', + (subc) => + subc.exec([ + 'python3', + `${mountpoint}/rpcauth.py`, + `${username}`, + `${password}`, + ]), + ) + + if (res.exitCode === 0 && typeof res.stdout === 'string') { + const newRpcAuth = res.stdout.split('\n')[1].trim().split('=')[1].trim() + + const existingRpcAuthEntries = (await getRpcAuth(effects)) || [] + const rpcAuthEntries = [existingRpcAuthEntries].flat() + rpcAuthEntries.push(newRpcAuth) + + await bitcoinConfFile.merge(effects, { rpcauth: rpcAuthEntries }) + + return { + version: '1', + title: 'Success', + message: `RPC password created for ${username}`, + result: null, + } + } + + return { + version: '1', + title: 'Failure', + message: `rpcauth.py failed with error: ${res.stderr}`, + result: null, + } + }, +) diff --git a/startos/actions/index.ts b/startos/actions/index.ts new file mode 100644 index 00000000..fdc68792 --- /dev/null +++ b/startos/actions/index.ts @@ -0,0 +1,31 @@ +import { sdk } from '../sdk' +import { config } from './config/other' +import { mempoolConfig } from './config/mempool' +import { peerConfig } from './config/peers' +import { rpcConfig } from './config/rpc' +import { deleteCoinstatsIndex } from './deleteCoinstatsIndex' +import { deletePeers } from './deletePeers' +import { deleteRpcAuth } from './deleteRpcAuth' +import { deleteTxIndex } from './deleteTxIndex' +import { generateRpcUser } from './generateRpcAuth' +import { generateRpcUserDependent } from './generateRpcUserDependent' +import { reindexBlockchain } from './reindexBlockchain' +import { reindexChainstate } from './reindexChainstate' +import { runtimeInfo } from './runtimeInfo' +import { assumeutxo } from './assumeutxo' + +export const actions = sdk.Actions.of() + .addAction(runtimeInfo) + .addAction(deleteCoinstatsIndex) + .addAction(deletePeers) + .addAction(deleteTxIndex) + .addAction(reindexBlockchain) + .addAction(reindexChainstate) + .addAction(config) + .addAction(rpcConfig) + .addAction(generateRpcUser) + .addAction(deleteRpcAuth) + .addAction(mempoolConfig) + .addAction(peerConfig) + .addAction(generateRpcUserDependent) + .addAction(assumeutxo) \ No newline at end of file diff --git a/startos/actions/reindexBlockchain.ts b/startos/actions/reindexBlockchain.ts new file mode 100644 index 00000000..cdb6cab9 --- /dev/null +++ b/startos/actions/reindexBlockchain.ts @@ -0,0 +1,37 @@ +import { storeJson } from '../fileModels/store.json' +import { sdk } from '../sdk' + +export const reindexBlockchain = sdk.Action.withoutInput( + // id + 'reindex-blockchain', + + // metadata + async ({ effects }) => ({ + name: 'Reindex Blockchain', + description: + 'Rebuilds the block and chainstate databases starting from genesis. If blocks already exist on disk, these are used rather than being re-downloaded. For pruned nodes, this means downloading the entire blockchain over again.', + warning: + 'Blocks not stored on disk will be re-downloaded in order to rebuild the database. If your node is pruned, this action is equivalent to syncing the node from scratch, so this process could take weeks on low-end hardware.', + allowedStatuses: 'only-running', + group: 'Reindex', + visibility: 'enabled', + }), + + // execution function + async ({ effects }) => { + await storeJson.merge(effects, { + reindexBlockchain: true, + fullySynced: false, + }) + + await sdk.restart(effects) + + return { + version: '1', + title: 'Success', + message: + 'Restarting bitcoind with -reindex argument', + result: null, + } + }, +) diff --git a/startos/actions/reindexChainstate.ts b/startos/actions/reindexChainstate.ts new file mode 100644 index 00000000..9f205f3f --- /dev/null +++ b/startos/actions/reindexChainstate.ts @@ -0,0 +1,40 @@ +import { bitcoinConfFile } from '../fileModels/bitcoin.conf' +import { storeJson } from '../fileModels/store.json' +import { sdk } from '../sdk' + +export const reindexChainstate = sdk.Action.withoutInput( + // id + 'reindex-chainstate', + + // metadata + async ({ effects }) => ({ + name: 'Reindex Chainstate', + description: + "Rebuilds the chainstate database using existing block index data; as the block index is not rebuilt, 'reindex_chainstate' should be strictly faster than 'reindex'. This action should only be used in the case of chainstate corruption; if the blocks stored on disk are corrupted, the 'reindex' action will need to be run instead.", + warning: + "While faster than 'Reindex', 'Reindex Chainstate' can still take several days or more to complete.", + allowedStatuses: 'only-running', + group: 'Reindex', + visibility: (await bitcoinConfFile.read().const(effects))?.prune + ? 'hidden' + : 'enabled', + }), + + // execution function + async ({ effects }) => { + await storeJson.merge(effects, { + reindexChainstate: true, + fullySynced: false, + }) + + await sdk.restart(effects) + + return { + version: '1', + title: 'Success', + message: + 'Restarting bitcoind with -reindex-chainstate argument', + result: null, + } + }, +) diff --git a/startos/actions/runtimeInfo.ts b/startos/actions/runtimeInfo.ts new file mode 100644 index 00000000..e2c05205 --- /dev/null +++ b/startos/actions/runtimeInfo.ts @@ -0,0 +1,332 @@ +import { T } from '@start9labs/start-sdk' +import { sdk } from '../sdk' +import { GetBlockchainInfo, GetNetworkInfo, rootDir } from '../utils' +import { mainMounts } from '../main' +import { bitcoinConfFile } from '../fileModels/bitcoin.conf' +import { rpcPort } from '../utils' + +export const runtimeInfo = sdk.Action.withoutInput( + // id + 'runtime-info', + + // metadata + async ({ effects }) => ({ + name: 'Runtime Information', + description: + 'Network and other runtime information about this Bitcoin node', + warning: null, + allowedStatuses: 'only-running', + group: null, + visibility: 'enabled', + }), + + // execution function + async ({ effects }) => { + const conf = (await bitcoinConfFile.read().const(effects))! + // getnetowrkinfo + + const networkInfoRes = await sdk.SubContainer.withTemp( + effects, + { imageId: 'bitcoind' }, + mainMounts, + 'getnetworkinfo', + async (subc) => { + return await subc.execFail([ + 'bitcoin-cli', + `-conf=${rootDir}/bitcoin.conf`, + `-rpccookiefile=${rootDir}/.cookie`, + `-rpcport=${conf.prune ? 18332 : rpcPort}`, + 'getnetworkinfo', + ]) + }, + ) + + const networkInfoRaw: GetNetworkInfo = JSON.parse( + networkInfoRes.stdout as string, + ) + + // getblockchaininfo + + const blockchainInfoRes = await sdk.SubContainer.withTemp( + effects, + { imageId: 'bitcoind' }, + mainMounts, + 'getblockchaininfo', + async (subc) => { + return await subc.execFail([ + 'bitcoin-cli', + `-conf=${rootDir}/bitcoin.conf`, + `-rpccookiefile=${rootDir}/.cookie`, + `-rpcport=${conf.prune ? 18332 : rpcPort}`, + 'getblockchaininfo', + ]) + }, + ) + + const blockchainInfoRaw: GetBlockchainInfo = JSON.parse( + blockchainInfoRes.stdout as string, + ) + + // return + const value = [ + getConnections(networkInfoRaw), + getBlockchainInfo(blockchainInfoRaw), + ] + if (blockchainInfoRaw.softforks) { + value.push(getSoftforkInfo(blockchainInfoRaw)) + } + + return { + version: '1', + title: 'Node Runtime Info', + message: null, + result: { type: 'group', value }, + } + }, +) + +function getConnections(networkInfoRaw: GetNetworkInfo): T.ActionResultMember { + return { + type: 'single', + name: 'Connections', + description: 'The number of peers connected (inbound and outbound)', + value: `${networkInfoRaw.connections} (${networkInfoRaw.connections_in} in / ${networkInfoRaw.connections_out} out)`, + copyable: false, + masked: false, + qr: false, + } +} + +function getBlockchainInfo( + blockchainInfoRaw: GetBlockchainInfo, +): T.ActionResultMember { + return { + type: 'group', + name: 'Blockchain Info', + description: null, + value: [ + { + type: 'single', + name: 'Block Height', + value: String(blockchainInfoRaw.headers), + description: 'The current block height for the network', + copyable: false, + masked: false, + qr: false, + }, + { + type: 'single', + name: 'Synced Block Height', + value: String(blockchainInfoRaw.blocks), + description: 'The number of blocks the node has verified', + copyable: false, + masked: false, + qr: false, + }, + { + type: 'single', + name: 'Sync Progress', + value: + blockchainInfoRaw.blocks < blockchainInfoRaw.headers || + blockchainInfoRaw.blocks === 0 + ? `${(blockchainInfoRaw.verificationprogress * 100).toFixed(2)}%` + : '100%', + description: 'The percentage of the blockchain that has been verified', + copyable: false, + masked: false, + qr: false, + }, + ], + } +} + +function getSoftforkInfo( + blockchainInfoRaw: GetBlockchainInfo, +): T.ActionResultMember { + return { + type: 'group', + name: 'Softfork Info', + description: null, + value: [ + { + type: 'group', + name: 'Softforks', + description: null, + value: getSoftforks(blockchainInfoRaw), + }, + ], + } +} + +function getSoftforks( + blockchainInfoRaw: GetBlockchainInfo, +): T.ActionResultMember[] { + return Object.entries(blockchainInfoRaw.softforks).map(([key, val]) => { + const value: T.ActionResultMember[] = [ + { + type: 'single', + name: 'Type', + value: val.type, + description: 'Either "buried", "bip9"', + copyable: false, + masked: false, + qr: false, + }, + { + type: 'single', + name: 'Height', + value: val.height ? String(val.height) : 'N/A', + description: + 'height of the first block which the rules are or will be enforced (only for "buried" type, or "bip9" type with "active" status)', + copyable: false, + masked: false, + qr: false, + }, + { + type: 'single', + name: 'Active', + value: String(val.active), + description: + 'true if the rules are enforced for the mempool and the next block', + copyable: false, + masked: false, + qr: false, + }, + ] + + if (val.bip9) { + value.push(getBip9Info(val.bip9)) + + if (val.bip9.statistics) { + value.push(getBip9Statistics(val.bip9.statistics)) + } + } + + return { type: 'group', name: key, description: null, value } + }) +} + +function getBip9Info(bip9: Bip9): T.ActionResultMember { + const { status, bit, start_time, timeout, since } = bip9 + + return { + type: 'group', + name: 'Bip9', + description: null, + value: [ + { + type: 'single', + name: 'Status', + value: status, + description: + 'One of "defined", "started", "locked_in", "active", "failed"', + copyable: false, + masked: false, + qr: false, + }, + { + type: 'single', + name: 'Bit', + value: bit ? String(bit) : 'N/A', + description: + 'The bit (0-28) in the block version field used to signal this softfork (only for "started" status)', + copyable: false, + masked: false, + qr: false, + }, + { + type: 'single', + name: 'Start Time', + value: String(start_time), + description: + 'The minimum median time past of a block at which the bit gains its meaning', + copyable: false, + masked: false, + qr: false, + }, + { + type: 'single', + name: 'Timeout', + value: String(timeout), + description: + 'The median time past of a block at which the deployment is considered failed if not yet locked in', + copyable: false, + masked: false, + qr: false, + }, + { + type: 'single', + name: 'Since', + value: String(since), + description: 'height of the first block to which the status applies', + copyable: false, + masked: false, + qr: false, + }, + ], + } +} + +function getBip9Statistics(statistics: Bip9Stats): T.ActionResultMember { + const { period, threshold, elapsed, count, possible } = statistics + + return { + type: 'group', + name: 'Statistics', + description: null, + value: [ + { + type: 'single', + name: 'Period', + value: String(period), + description: 'The length in blocks of the BIP9 signalling period', + copyable: false, + masked: false, + qr: false, + }, + { + type: 'single', + name: 'Threshold', + value: String(threshold), + description: + 'The number of blocks with the version bit set required to activate the feature', + copyable: false, + masked: false, + qr: false, + }, + { + type: 'single', + name: 'Elapsed', + value: String(elapsed), + description: + 'The number of blocks elapsed since the beginning of the current period', + copyable: false, + masked: false, + qr: false, + }, + { + type: 'single', + name: 'Count', + value: String(count), + description: + 'The number of blocks with the version bit set in the current period', + copyable: false, + masked: false, + qr: false, + }, + { + type: 'single', + name: 'Possible', + value: String(possible), + description: + 'returns false if there are not enough blocks left in this period to pass activation threshold', + copyable: false, + masked: false, + qr: false, + }, + ], + } +} + +type Bip9 = NonNullable +type Bip9Stats = NonNullable diff --git a/startos/backups.ts b/startos/backups.ts new file mode 100644 index 00000000..f63e7269 --- /dev/null +++ b/startos/backups.ts @@ -0,0 +1,7 @@ +import { sdk } from './sdk' + +export const { createBackup, restoreInit } = sdk.setupBackups(async () => + sdk.Backups.ofVolumes('main').setOptions({ + exclude: ['blocks/', 'chainstate/', 'indexes/'], + }), +) diff --git a/startos/dependencies.ts b/startos/dependencies.ts new file mode 100644 index 00000000..7221c4bd --- /dev/null +++ b/startos/dependencies.ts @@ -0,0 +1,5 @@ +import { sdk } from './sdk' + +export const setDependencies = sdk.setupDependencies( + async ({ effects }) => ({}), +) diff --git a/startos/fileModels/bitcoin.conf.ts b/startos/fileModels/bitcoin.conf.ts new file mode 100644 index 00000000..821e420d --- /dev/null +++ b/startos/fileModels/bitcoin.conf.ts @@ -0,0 +1,166 @@ +import { FileHelper, matches } from '@start9labs/start-sdk' +import { bitcoinConfDefaults } from '../utils' + +const { anyOf, arrayOf, object } = matches + +const stringArray = matches.array(matches.string) +const string = stringArray.map(([a]) => a).orParser(matches.string) +const number = string.map((a) => Number(a)).orParser(matches.number) +const natural = string.map((a) => Number(a)).orParser(matches.natural) +const boolean = number.map((a) => !!a).orParser(matches.boolean) +const literal = (val: string | number) => { + return matches + .literal([String(val)]) + .orParser(matches.literal(String(val))) + .orParser(matches.literal(val)) + .map((a) => (typeof val === 'number' ? Number(a) : a)) +} + +const onlyNetOptions = anyOf( + matches.literal('ipv4'), + matches.literal('ipv6'), + matches.literal('onion'), + matches.literal('i2p'), + matches.literal('cjdns'), +) + +const { + rpcbind, + rpcallowip, + rpcauth, + rpcservertimeout, + rpcthreads, + rpcworkqueue, + rpccookiefile, + whitebind, + bind, + persistmempool, + maxmempool, + mempoolexpiry, + mempoolfullrbf, + permitbaremultisig, + datacarrier, + datacarriersize, + listen, + externalip, + v2transport, + connect, + addnode, + disablewallet, + avoidpartialspends, + discardfee, + blocknotify, + prune, + zmqpubrawblock, + zmqpubhashblock, + zmqpubhashtx, + zmqpubrawtx, + zmqpubsequence, + coinstatsindex, + txindex, + dbcache, + dbbatchsize, + peerbloomfilters, + blockfilterindex, + peerblockfilters, +} = bitcoinConfDefaults + +export const shape = object({ + // RPC + rpcbind: string.onMismatch(rpcbind), + rpcallowip: string.onMismatch(rpcallowip), + rpcauth: stringArray.orParser(string).optional().onMismatch(rpcauth), + rpcservertimeout: natural.onMismatch(rpcservertimeout), + rpcthreads: natural.onMismatch(rpcthreads), + rpcworkqueue: natural.onMismatch(rpcworkqueue), + rpccookiefile: literal(rpccookiefile).onMismatch(rpccookiefile), + rpcuser: matches.literal(undefined).optional().onMismatch(undefined), + rpcpassword: matches.literal(undefined).optional().onMismatch(undefined), + + // Mempool + mempoolfullrbf: boolean.onMismatch(mempoolfullrbf), + persistmempool: boolean.optional().onMismatch(persistmempool), + maxmempool: natural.optional().onMismatch(maxmempool), + mempoolexpiry: natural.onMismatch(mempoolexpiry), + datacarrier: boolean.onMismatch(datacarrier), + datacarriersize: natural.onMismatch(datacarriersize), + permitbaremultisig: boolean.onMismatch(permitbaremultisig), + + // Peers + listen: matches.literal(listen).onMismatch(listen), + bind: string.optional().onMismatch(bind), + connect: stringArray.orParser(string).optional().onMismatch(connect), + addnode: stringArray.orParser(string).optional().onMismatch(addnode), + onlynet: arrayOf(onlyNetOptions.optional().onMismatch(undefined)).optional(), + v2transport: boolean.onMismatch(v2transport), + externalip: string.optional().onMismatch(externalip), + + // Blocknotify + blocknotify: string.optional().onMismatch(blocknotify), + + // Whitebind + whitebind: literal(whitebind).onMismatch(whitebind), + whitelist: stringArray.orParser(string).optional().onMismatch(undefined), + + // Pruning + prune: natural.onMismatch(prune), + + // Performance Tuning + dbcache: natural.onMismatch(dbcache), + dbbatchsize: natural.onMismatch(dbbatchsize), + assumevalid: string.optional().onMismatch('00000000000000000000611fd22f2df7c8fbd0688745c3a6c3bb5109cc2a12cb'), + + // Wallet + disablewallet: boolean.onMismatch(disablewallet), + avoidpartialspends: boolean.onMismatch(avoidpartialspends), + discardfee: number.onMismatch(discardfee), + + // Zero MQ + zmqpubrawblock: string.optional().onMismatch(zmqpubrawblock), + zmqpubhashblock: string.optional().onMismatch(zmqpubhashblock), + zmqpubrawtx: string.optional().onMismatch(zmqpubrawtx), + zmqpubhashtx: string.optional().onMismatch(zmqpubhashtx), + zmqpubsequence: string.optional().onMismatch(zmqpubsequence), + + // TxIndex + txindex: boolean.onMismatch(txindex), + + // CoinstatsIndex + coinstatsindex: boolean.onMismatch(coinstatsindex), + + // BIP37 + peerbloomfilters: boolean.onMismatch(peerbloomfilters), + + // BIP157 + blockfilterindex: anyOf(matches.literal('basic'), boolean) + .optional() + .onMismatch(blockfilterindex), + peerblockfilters: boolean.onMismatch(peerblockfilters), +}).onMismatch(bitcoinConfDefaults) + +function onWrite(a: unknown): any { + if (a && typeof a === 'object') { + if (Array.isArray(a)) { + return a.map(onWrite) + } + return Object.fromEntries( + Object.entries(a).map(([k, v]) => [k, onWrite(v)]), + ) + } else if (typeof a === 'boolean') { + return a ? 1 : 0 + } + return a +} + +export const bitcoinConfFile = FileHelper.ini( + { + volumeId: 'main', + subpath: '/bitcoin.conf', + }, + shape, + { bracketedArray: false }, + { + onRead: (a) => a, + onWrite, + }, +) diff --git a/startos/fileModels/config.toml.ts b/startos/fileModels/config.toml.ts new file mode 100644 index 00000000..9a8e6aaa --- /dev/null +++ b/startos/fileModels/config.toml.ts @@ -0,0 +1,24 @@ +import { FileHelper, matches } from '@start9labs/start-sdk' + +const { object, string, boolean, natural } = matches + +const shape = object({ + bitcoind_address: string, + bitcoind_port: natural, + bind_address: string, + bind_port: natural, + cookie_file: string, + tor_proxy: string, + tor_only: boolean, + passthrough_rpcauth: string.optional(), + passthrough_rpccookie: string.optional(), +}) + +// This is the config file for btc-rpc-proxy +export const configToml = FileHelper.toml( + { + volumeId: 'main', + subpath: '/config.toml', + }, + shape, +) diff --git a/startos/fileModels/store.json.ts b/startos/fileModels/store.json.ts new file mode 100644 index 00000000..276572cc --- /dev/null +++ b/startos/fileModels/store.json.ts @@ -0,0 +1,18 @@ +import { FileHelper, matches } from '@start9labs/start-sdk' + +const { object, boolean } = matches + +export const shape = object({ + reindexBlockchain: boolean, + reindexChainstate: boolean, + fullySynced: boolean, + snapshotInUse: boolean, +}) + +export const storeJson = FileHelper.json( + { + volumeId: 'main', + subpath: '/store.json', + }, + shape, +) diff --git a/startos/index.ts b/startos/index.ts new file mode 100644 index 00000000..5bc26852 --- /dev/null +++ b/startos/index.ts @@ -0,0 +1,11 @@ +/** + * Plumbing. DO NOT EDIT. + */ +export { createBackup } from './backups' +export { main } from './main' +export { init, uninit } from './init' +export { actions } from './actions' +import { buildManifest } from '@start9labs/start-sdk' +import { manifest as sdkManifest } from './manifest' +import { versionGraph } from './install/versionGraph' +export const manifest = buildManifest(versionGraph, sdkManifest) diff --git a/startos/init/index.ts b/startos/init/index.ts new file mode 100644 index 00000000..fa0be1e8 --- /dev/null +++ b/startos/init/index.ts @@ -0,0 +1,22 @@ +import { sdk } from '../sdk' +import { setDependencies } from '../dependencies' +import { setInterfaces } from '../interfaces' +import { versionGraph } from '../install/versionGraph' +import { actions } from '../actions' +import { restoreInit } from '../backups' +import { taskSetExternal } from './taskSelectExternal' +import { setDefaults } from './setDefaults' +import { watchPrune } from './watchPrune' + +export const init = sdk.setupInit( + restoreInit, + versionGraph, + setInterfaces, + setDependencies, + actions, + setDefaults, + taskSetExternal, + watchPrune, +) + +export const uninit = sdk.setupUninit(versionGraph) diff --git a/startos/init/setDefaults.ts b/startos/init/setDefaults.ts new file mode 100644 index 00000000..61abc5dc --- /dev/null +++ b/startos/init/setDefaults.ts @@ -0,0 +1,31 @@ +import { bitcoinConfFile, shape } from '../fileModels/bitcoin.conf' +import { sdk } from '../sdk' +import { peerInterfaceId, prunedRpcallowip, prunedRpcbind } from '../utils' +import * as diskusage from 'diskusage' +import { utils } from '@start9labs/start-sdk' + +const diskUsage = utils.once(() => diskusage.check('/')) + +export const setDefaults = sdk.setupOnInit(async (effects, kind) => { + if (kind === 'install') { + let defaults: Partial = {} + + // pruning + const disk = await diskUsage() + if (disk.total < 900_000_000_000) { + defaults = { + prune: 550, + rpcbind: prunedRpcbind, + rpcallowip: prunedRpcallowip, + } + } + + // externalip + const peerInterface = await sdk.serviceInterface + .getOwn(effects, peerInterfaceId) + .once() + defaults.externalip = peerInterface?.addressInfo?.onionUrls[0] + + await bitcoinConfFile.merge(effects, defaults) + } +}) diff --git a/startos/init/taskSelectExternal.ts b/startos/init/taskSelectExternal.ts new file mode 100644 index 00000000..13df3243 --- /dev/null +++ b/startos/init/taskSelectExternal.ts @@ -0,0 +1,21 @@ +import { peerConfig } from '../actions/config/peers' +import { bitcoinConfFile } from '../fileModels/bitcoin.conf' +import { sdk } from '../sdk' +import { peerInterfaceId } from '../utils' + +export const taskSetExternal = sdk.setupOnInit(async (effects, kind) => { + const publicPeerUrls = + (await sdk.serviceInterface.getOwn(effects, peerInterfaceId).const()) + ?.addressInfo?.publicUrls || [] + + const externalIp = await bitcoinConfFile.read((b) => b.externalip).once() + + if (externalIp && !publicPeerUrls.includes(externalIp)) { + await bitcoinConfFile.merge(effects, { externalip: undefined }) + + await sdk.action.createOwnTask(effects, peerConfig, 'important', { + reason: + 'External address removed. Your node can only make outbound connections. Select a new external address to re-enable inbound connections.', + }) + } +}) diff --git a/startos/init/watchPrune.ts b/startos/init/watchPrune.ts new file mode 100644 index 00000000..aaee60ef --- /dev/null +++ b/startos/init/watchPrune.ts @@ -0,0 +1,45 @@ +import { bitcoinConfFile } from '../fileModels/bitcoin.conf' +import { sdk } from '../sdk' +import { + prunedRpcallowip, + prunedRpcbind, + unprunedRpcallowIp, + unprunedRpcbind, +} from '../utils' + +export const watchPrune = sdk.setupOnInit(async (effects, _) => { + const conf = await bitcoinConfFile + .read((c) => ({ + prune: c.prune, + rpcbind: c.rpcbind, + rpcallowip: c.rpcallowip, + })) + .const(effects) + if (!conf) { + throw new Error('bitcoin.conf not found') + } + const { prune, rpcbind, rpcallowip } = conf + + if (prune) { + if (rpcbind !== prunedRpcbind || rpcallowip !== prunedRpcallowip) { + await bitcoinConfFile.merge( + effects, + { + rpcbind: prunedRpcbind, + rpcallowip: prunedRpcallowip, + }, + { allowWriteAfterConst: true }, + ) + } + } else { + if (rpcbind !== unprunedRpcbind || rpcallowip !== unprunedRpcallowIp) + await bitcoinConfFile.merge( + effects, + { + rpcbind: unprunedRpcbind, + rpcallowip: unprunedRpcallowIp, + }, + { allowWriteAfterConst: true }, + ) + } +}) diff --git a/startos/install/versionGraph.ts b/startos/install/versionGraph.ts new file mode 100644 index 00000000..99604dbe --- /dev/null +++ b/startos/install/versionGraph.ts @@ -0,0 +1,43 @@ +import { VersionGraph } from '@start9labs/start-sdk' +import { coreCurrent, other } from './versions' +import { storeJson } from '../fileModels/store.json' +import { bitcoinConfFile } from '../fileModels/bitcoin.conf' +import { bitcoinConfDefaults } from '../utils' +import { execFile } from 'node:child_process' + +export function nocow(path: string) { + return new Promise((resolve, reject) => + execFile('chattr', ['-R', '+C', path], (err, stdout, stderr) => { + if (err && !stderr.includes('Operation not supported')) { + err.message += `: ${stderr}` + reject(err) + } else { + resolve() + } + }), + ) +} + +export const versionGraph = VersionGraph.of({ + current: coreCurrent, + other, + preInstall: async (effects) => { + await nocow('/media/startos/volumes/main/') + const store = await storeJson.read().once() + + if (!store) { + await storeJson.write(effects, { + reindexBlockchain: false, + reindexChainstate: false, + fullySynced: false, + snapshotInUse: false, + }) + } + + const conf = await bitcoinConfFile.read().once() + + if (!conf) { + await bitcoinConfFile.write(effects, bitcoinConfDefaults) + } + }, +}) diff --git a/startos/install/versions/index.ts b/startos/install/versions/index.ts new file mode 100644 index 00000000..de12f989 --- /dev/null +++ b/startos/install/versions/index.ts @@ -0,0 +1,5 @@ +import { v29_2_0_2 } from './v29_2_0_2-beta.0' + +export { v30_0_0_1_beta2 as coreCurrent} from './v30_0_0_1-beta.2' + +export const other = [v29_2_0_2] diff --git a/startos/install/versions/v29_2_0_2-beta.0.ts b/startos/install/versions/v29_2_0_2-beta.0.ts new file mode 100644 index 00000000..351409c6 --- /dev/null +++ b/startos/install/versions/v29_2_0_2-beta.0.ts @@ -0,0 +1,41 @@ +import { VersionInfo, IMPOSSIBLE } from '@start9labs/start-sdk' +import { bitcoinConfFile } from '../../fileModels/bitcoin.conf' +import { bitcoinConfDefaults } from '../../utils' +import { storeJson } from '../../fileModels/store.json' +import { nocow } from '../versionGraph' +const { whitebind, bind } = bitcoinConfDefaults + +export const v29_2_0_2 = VersionInfo.of({ + version: '29.2:2-beta.0', + releaseNotes: 'Revamped for StartOS 0.4.0', + migrations: { + up: async ({ effects }) => { + await nocow('/media/startos/volumes/main/') + const store = await storeJson.read().once() + + if (!store) { + await storeJson.write(effects, { + reindexBlockchain: false, + reindexChainstate: false, + fullySynced: false, + snapshotInUse: false, + }) + } + const existingConf = await bitcoinConfFile.read().once() + + if (existingConf) { + await bitcoinConfFile.merge(effects, { + rpcuser: undefined, + rpcpassword: undefined, + bind, + whitebind, + whitelist: undefined, + }) + return + } // Only write conf defaults if no existing bitcoin.conf found + + await bitcoinConfFile.write(effects, bitcoinConfDefaults) + }, + down: IMPOSSIBLE, + }, +}) diff --git a/startos/install/versions/v30_0_0_1-beta.2.ts b/startos/install/versions/v30_0_0_1-beta.2.ts new file mode 100644 index 00000000..021eabde --- /dev/null +++ b/startos/install/versions/v30_0_0_1-beta.2.ts @@ -0,0 +1,9 @@ +import { VersionInfo } from '@start9labs/start-sdk' +export const v30_0_0_1_beta2 = VersionInfo.of({ + version: '30.0.0:1-beta.2', + releaseNotes: 'Revamped for StartOS 0.4.0', + migrations: { + up: async ({ effects }) => {}, + down: async ({ effects }) => {}, + }, +}) diff --git a/startos/interfaces.ts b/startos/interfaces.ts new file mode 100644 index 00000000..dff97ba2 --- /dev/null +++ b/startos/interfaces.ts @@ -0,0 +1,89 @@ +import { bitcoinConfFile } from './fileModels/bitcoin.conf' +import { sdk } from './sdk' +import { + peerInterfaceId, + peerPort, + rpcInterfaceId, + rpcPort, + zmqInterfaceId, + zmqPort, +} from './utils' + +export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => { + let config = await bitcoinConfFile.read().const(effects) + + if (!config) return [] + + // RPC + const rpcMulti = sdk.MultiHost.of(effects, 'rpc') + const rpcMultiOrigin = await rpcMulti.bindPort(rpcPort, { + protocol: 'http', + preferredExternalPort: rpcPort, + }) + const rpc = sdk.createInterface(effects, { + name: 'RPC Interface', + id: rpcInterfaceId, + description: 'Listens for JSON-RPC commands', + type: 'api', + masked: false, + schemeOverride: null, + username: null, + path: '', + query: {}, + }) + const rpcReceipt = await rpcMultiOrigin.export([rpc]) + + const receipts = [rpcReceipt] + + // PEER + const peerMulti = sdk.MultiHost.of(effects, 'peer') + const peerMultiOrigin = await peerMulti.bindPort(peerPort, { + protocol: null, + preferredExternalPort: peerPort, + addSsl: null, + secure: { ssl: false }, + }) + const peer = sdk.createInterface(effects, { + name: 'Peer Interface', + id: peerInterfaceId, + description: + 'Listens for incoming connections from peers on the bitcoin network', + type: 'p2p', + masked: false, + schemeOverride: { ssl: null, noSsl: null }, + username: null, + path: '', + query: {}, + }) + const peerReceipt = await peerMultiOrigin.export([peer]) + + receipts.push(peerReceipt) + + // ZMQ (conditional) + if (config.zmqpubhashblock) { + const zmqMulti = sdk.MultiHost.of(effects, 'zmq') + const zmqMultiOrigin = await zmqMulti.bindPort(zmqPort, { + preferredExternalPort: zmqPort, + addSsl: null, + secure: { ssl: false }, + protocol: null, + }) + const zmq = sdk.createInterface(effects, { + name: 'ZeroMQ Interface', + id: zmqInterfaceId, + description: + 'Listens for incoming connections from peers on the bitcoin network', + type: 'api', + masked: false, + schemeOverride: null, + username: null, + path: '', + query: {}, + }) + const zmqReceipt = await zmqMultiOrigin.export([zmq]) + + receipts.push(zmqReceipt) + } + + return receipts +}) diff --git a/startos/main.ts b/startos/main.ts new file mode 100644 index 00000000..236a3665 --- /dev/null +++ b/startos/main.ts @@ -0,0 +1,196 @@ +import { sdk } from './sdk' +import { bitcoinConfFile } from './fileModels/bitcoin.conf' +import { bitcoinConfDefaults, GetBlockchainInfo, rootDir } from './utils' +import { configToml } from './fileModels/config.toml' +import { rpcPort } from './utils' +import { promises } from 'fs' +import { storeJson } from './fileModels/store.json' +import { access, rm } from 'fs/promises' + +export const mainMounts = sdk.Mounts.of().mountVolume({ + volumeId: 'main', + subpath: null, + mountpoint: rootDir, + readonly: false, +}) + +export const main = sdk.setupMain(async ({ effects, started }) => { + /** + * ======================== Setup (optional) ======================== + */ + const osIp = await sdk.getOsIp(effects) + + const bitcoinArgs: string[] = [] + + bitcoinArgs.push(`-onion=${osIp}:9050`) + + const { reindexBlockchain, reindexChainstate } = (await storeJson + .read() + .once()) || { reindexBlockchain: false, reindexChainstate: false } + + if (reindexBlockchain) { + bitcoinArgs.push('-reindex') + await storeJson.merge(effects, { reindexBlockchain: false }) + } else if (reindexChainstate) { + bitcoinArgs.push('-reindex-chainstate') + await storeJson.merge(effects, { reindexChainstate: false }) + } + + const conf = await bitcoinConfFile.read().const(effects) + if (!conf) { + throw new Error('bticoin.conf not found') + } + + const bitcoindSub = await sdk.SubContainer.of( + effects, + { imageId: 'bitcoind' }, + mainMounts, + 'bitcoind-sub', + ) + + /** + * ======================== Daemons ======================== + */ + + const rpcCookieFile = `${rootDir}/${bitcoinConfDefaults.rpccookiefile}` + + await rm(`${bitcoindSub.rootfs}/${rpcCookieFile}`, { force: true }) + + const daemons = sdk.Daemons.of(effects, started) + .addDaemon('primary', { + subcontainer: bitcoindSub, + exec: { + command: ['bitcoind', ...bitcoinArgs], + sigtermTimeout: 300_000, + }, + ready: { + display: 'RPC', + fn: async () => { + try { + await access(`${bitcoindSub.rootfs}${rpcCookieFile}`) + const res = await bitcoindSub.exec([ + 'bitcoin-cli', + `-conf=${rootDir}/bitcoin.conf`, + `-rpccookiefile=${rpcCookieFile}`, + `-rpcconnect=${conf.rpcbind}`, + 'getrpcinfo', + ]) + return res.exitCode === 0 + ? { + message: 'The Bitcoin RPC Interface is ready', + result: 'success', + } + : { + message: 'The Bitcoin RPC Interface is not ready', + result: 'starting', + } + } catch { + console.log('Waiting for cookie to be created') + return { + message: 'The Bitcoin RPC Interface is not ready', + result: 'starting', + } + } + }, + }, + requires: [], + }) + .addHealthCheck('sync-progress', { + ready: { + display: 'Blockchain Sync Progress', + fn: async () => { + const res = await bitcoindSub.exec([ + 'bitcoin-cli', + `-conf=${rootDir}/bitcoin.conf`, + `-rpccookiefile=${rootDir}/${bitcoinConfDefaults.rpccookiefile}`, + `-rpcconnect=${conf.rpcbind}`, + 'getblockchaininfo', + ]) + + if ( + res.exitCode === 0 && + res.stdout !== '' && + typeof res.stdout === 'string' + ) { + const info: GetBlockchainInfo = JSON.parse(res.stdout) + + if (info.initialblockdownload) { + const percentage = (info.verificationprogress * 100).toFixed(2) + return { + message: `Syncing blocks...${percentage}%`, + result: 'loading', + } + } + + return { message: 'Bitcoin is fully synced', result: 'success' } + } + + if (res.stderr.includes('error code: -28')) { + return { message: 'Bitcoin is starting…', result: 'starting' } + } else { + return { message: res.stderr as string, result: 'failure' } + } + }, + }, + requires: ['primary'], + }) + .addOneshot('synced-true', { + requires: ['sync-progress'], + subcontainer: null, + exec: { + fn: async () => { + const store = await storeJson.read().once() + if (!store) return null + + const fullySynced = store.fullySynced + + if (!fullySynced) { + await storeJson.merge(effects, { + fullySynced: true, + snapshotInUse: false, + }) + } + + return null + }, + }, + }) + + if (conf.prune) { + await configToml.write(effects, { + bitcoind_address: '127.0.0.1', + bitcoind_port: 18332, + bind_address: '0.0.0.0', + bind_port: rpcPort, + cookie_file: `${rootDir}/${bitcoinConfDefaults.rpccookiefile}`, + tor_proxy: `${osIp}:9050`, + tor_only: conf.onlynet ? conf.onlynet.includes('onion') : false, + passthrough_rpcauth: `${rootDir}/bitcoin.conf`, + passthrough_rpccookie: `${rootDir}/${bitcoinConfDefaults.rpccookiefile}`, + }) + + await promises.chmod(configToml.path, 0o600) + + return daemons.addDaemon('proxy', { + subcontainer: await sdk.SubContainer.of( + effects, + { imageId: 'proxy' }, + mainMounts, + 'proxy-sub', + ), + exec: { + command: ['/usr/bin/btc_rpc_proxy', '--conf', `${rootDir}/config.toml`], + }, + ready: { + display: 'RPC Proxy', + fn: () => + sdk.healthCheck.checkPortListening(effects, rpcPort, { + successMessage: 'The Bitcoin RPC Proxy is ready', + errorMessage: 'The Bitcoin RPC Proxy is not ready', + }), + }, + requires: ['primary'], + }) + } + return daemons +}) diff --git a/startos/manifest.ts b/startos/manifest.ts new file mode 100644 index 00000000..8b86dba7 --- /dev/null +++ b/startos/manifest.ts @@ -0,0 +1,60 @@ +import { setupManifest } from '@start9labs/start-sdk' +import { SDKImageInputSpec } from '@start9labs/start-sdk/base/lib/types/ManifestTypes' + +const BUILD = process.env.BUILD || '' + +const arch = + BUILD === 'x86_64' || BUILD === 'aarch64' ? [BUILD] : ['x86_64', 'aarch64'] + +export const manifest = setupManifest({ + id: 'bitcoind', + title: 'Bitcoin Core', + license: 'MIT', + donationUrl: null, + wrapperRepo: 'https://github.com/Start9Labs/bitcoind-startos', + upstreamRepo: 'https://github.com/bitcoin/bitcoin', + supportSite: 'https://github.com/bitcoin/bitcoin/issues', + marketingSite: 'https://bitcoincore.org/', + docsUrl: + 'https://github.com/Start9Labs/bitcoind-startos/blob/update/040/instructions.md', + description: { + short: 'A Bitcoin Full Node by Bitcoin Core', + long: 'Bitcoin is an innovative payment network and a new kind of money. Bitcoin uses peer-to-peer technology to operate with no central authority or banks; managing transactions and the issuing of bitcoins is carried out collectively by the network. Bitcoin is open-source; its design is public, nobody owns or controls Bitcoin and everyone can take part. Through many of its unique properties, Bitcoin allows exciting uses that could not be covered by any previous payment system.', + }, + volumes: ['main', 'proxy'], + images: { + bitcoind: { + source: { + dockerBuild: { + workdir: './', + dockerfile: 'Dockerfile', + }, + }, + arch, + } as SDKImageInputSpec, + proxy: { + source: { + dockerTag: 'ghcr.io/start9labs/btc-rpc-proxy', + }, + arch, + } as SDKImageInputSpec, + python: { + source: { + dockerTag: 'python:3.13.2-alpine', + }, + arch, + } as SDKImageInputSpec, + }, + hardwareRequirements: { arch }, + alerts: { + install: null, + update: null, + uninstall: + "Uninstalling Bitcoin Core will result in permanent loss of data. Without a backup, any funds stored on your node's default hot wallet will be lost forever. If you are unsure, we recommend making a backup, just to be safe.", + restore: + 'Restoring Bitcoin Core will overwrite its current data. You will lose any transactions recorded in watch-only wallets, and any funds you have received to the hot wallet, since the last backup.', + start: null, + stop: null, + }, + dependencies: {}, +}) diff --git a/startos/sdk.ts b/startos/sdk.ts new file mode 100644 index 00000000..73713e24 --- /dev/null +++ b/startos/sdk.ts @@ -0,0 +1,11 @@ +import { StartSdk } from '@start9labs/start-sdk' +import { manifest } from './manifest' + +/** + * Plumbing. DO NOT EDIT. + * + * The exported "sdk" const will be imported and used throughout the package codebase. + */ +export const sdk = StartSdk.of() + .withManifest(manifest) + .build(true) diff --git a/startos/utils.ts b/startos/utils.ts new file mode 100644 index 00000000..7728041c --- /dev/null +++ b/startos/utils.ts @@ -0,0 +1,149 @@ +import { sdk } from './sdk' +export const rpcInterfaceId = 'rpc' +export const peerInterfaceId = 'peer' +export const zmqInterfaceId = 'zmq' +export const zmqPort = 28332 +export const peerPort = 18333 +export const rpcPort = 8332 + +export const rootDir = '/.bitcoin' + +export const unprunedRpcbind = '0.0.0.0:8332' +export const unprunedRpcallowIp = '0.0.0.0/0' + +export const prunedRpcbind = '127.0.0.1:18332' +export const prunedRpcallowip = '127.0.0.1/32' + +export type GetNetworkInfo = { + connections: number + connections_in: number + connections_out: number +} + +export type GetBlockchainInfo = { + chain: string + blocks: number + headers: number + bestblockhash: string + difficulty: number + mediantime: number + verificationprogress: number + initialblockdownload: boolean + chainwork: string + size_on_disk: number + pruned: boolean + pruneheight?: number + automatic_pruning?: boolean + prune_target_size?: number + softforks: Record< + string, + { + type: string + bip9?: { + status: string + bit?: number + start_time: number + timeout: number + since: number + statistics?: { + period: number + threshold: number + elapsed: number + count: number + possible: boolean + } + } + height?: number + active: boolean + } + > + warnings: string +} + +export const bitcoinConfDefaults = { + // RPC + rpcbind: unprunedRpcbind, + rpcallowip: unprunedRpcallowIp, + rpcauth: undefined, + rpcservertimeout: 30, + rpcthreads: 4, + rpcworkqueue: 16, + rpccookiefile: '.cookie', + whitebind: '0.0.0.0:8333', + bind: `0.0.0.0:${peerPort}`, + + // Mempool + persistmempool: true, + maxmempool: 300, + mempoolexpiry: 336, + mempoolfullrbf: true, + permitbaremultisig: true, + datacarrier: true, + datacarriersize: 10_000, + + // Peers + listen: true, + onlynet: undefined, + externalip: undefined, + v2transport: true, + connect: undefined, + addnode: undefined, + + // Wallet + disablewallet: false, + avoidpartialspends: false, + discardfee: 0.0001, + + // Other + blocknotify: undefined, + prune: 0, + zmqpubrawblock: 'tcp://0.0.0.0:28332', + zmqpubhashblock: 'tcp://0.0.0.0:28332', + zmqpubrawtx: 'tcp://0.0.0.0:28333', + zmqpubhashtx: 'tcp://0.0.0.0:28333', + zmqpubsequence: 'tcp://0.0.0.0:28333', + + coinstatsindex: false, + txindex: false, + dbcache: 5_000, + dbbatchsize: 33_554_432, + + peerbloomfilters: false, + blockfilterindex: 'basic', + peerblockfilters: false, +} as const + +export function getExteralAddresses() { + return sdk.Value.dynamicSelect(async ({ effects }) => { + const peerInterface = await sdk.serviceInterface + .getOwn(effects, peerInterfaceId) + .const() + + const urls = peerInterface?.addressInfo?.publicUrls || [] + + if (urls.length === 0) { + return { + name: 'External Address', + description: + "Address at which your node can be reached by peers. Select 'none' if you do not want your node to be reached by peers.", + values: { none: 'none' }, + default: 'none', + } + } + + const urlsWithNone = urls.reduce( + (obj, url) => ({ ...obj, [url]: url }), + {} as Record, + ) + + urlsWithNone['none'] = 'none' + + return { + name: 'External Address', + description: + "Address at which your node can be reached by peers. Select 'none' if you do not want your node to be reached by peers.", + values: urlsWithNone, + default: urls.find((u) => u.endsWith('.onion')) || '', + } + }) +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..a2945a5b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "include": ["startos/**/*.ts", "node_modules/**/startos"], + "compilerOptions": { + "target": "ES2018", + "module": "CommonJS", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true + } +}