Merge bitcoin/bitcoin#35331: [31.x] Backports

c058c29831 doc: update manual pages for v31.1rc1 (fanquake)
1c9d24fddd build: bump version to v31.1rc1 (fanquake)
d813722ef2 doc: update release notes for v31.1rc1 (fanquake)
ea3b318d8d coins: compact chainstate in background (Lőrinc)
711065a3b9 validation: randomly compact chainstate (Lőrinc)
fef6c8a4f2 coins: test chainstate flush baseline (Lőrinc)
ca00827fab util: Check write failures before renaming settings.json (Shrey)
1ee11d8ba6 lint: disable leveldb subtree check (fanquake)
ccb99122f9 net: un-default the OpenNetworkConnection()'s proxy_override argument (Eugene Siegel)
13df77d13b test: add a regression test for private broadcast v1 retries (Vasil Dimov)
6c08cb7323 test: make reusable filling of a node's addrman (Vasil Dimov)
70a8687d9d test: make reusable starting a standalone P2P listener (Vasil Dimov)
ef20249568 test: make reusable SOCKS5 server starting (Vasil Dimov)
66377c3c84 net: ensure no direct private broadcast connections (Vasil Dimov)
2c7986b3ee net: use the proxy if overriden when doing v2->v1 reconnections (Vasil Dimov)
70000a560b ci: use Warp cache for Docker layers (will)
25506ed6d9 ci: Add dynamic cache switching to warp cache (will)
39f8e077c8 ci: use ubuntu-latest instead of ubuntu-24.04 (fanquake)
1f55b3e463 doc: remove reference to cirrus (fanquake)
8f13bb1ea0 crypto: disable ASan instrumentation of SSE4 SHA256 for GCC (deadmanoz)
6caf6de0a1 ci: switch runners from cirrus to warpbuild (will)
78714f6d4f Disable seek compaction (Andrew Toth)
3440027b7d ci: switch to GitHub cache for all runners (willcl-ark)
d61687a2ac musig: Reject empty pubkey list in GetMuSig2KeyAggCache (nervana21)
671e6c2c33 wallet: use outpoint when estimating input size (Lőrinc)
101071722e psbt, test: remove address type restrictions in test (rkrux)

Pull request description:

  Backports:
  * #34953
  * #35228
  * #35279
  * #35313 (only https://github.com/bitcoin-core/leveldb-subtree/pull/61)
  * #35316
  * #35378
  * #35348
  * #35384
  * #35408
  * #35410
  * #35430
  * #35447
  * #35465

ACKs for top commit:
  marcofleon:
    ACK c058c29831
  sedited:
    ACK c058c29831

Tree-SHA512: a04909c1ce82d6f3412655ee7b52f4c482f5a175f9ec4e1468a84a4d488f935ab0ab333a3dc4d75f68fe2a9e7bae169d624c9061308435527b29b5a014c43dc3
This commit is contained in:
merge-script
2026-06-22 15:11:02 +02:00
46 changed files with 860 additions and 324 deletions

View File

@@ -13,7 +13,7 @@ trim_trailing_whitespace = true
[*.{h,cpp,rs,py,sh}]
indent_size = 4
# .cirrus.yml, etc.
# ci.yml, etc.
[*.yml]
indent_size = 2

View File

@@ -1,43 +1,51 @@
name: 'Restore Caches'
description: 'Restore ccache, depends sources, and built depends caches'
inputs:
provider:
description: 'The cache provider to use'
required: true
runs:
using: 'composite'
steps:
- name: Restore Ccache cache
id: ccache-cache
uses: cirruslabs/cache/restore@v5
uses: ./.github/actions/cache/restore/internal
with:
path: ${{ env.CCACHE_DIR }}
key: ccache-${{ env.CONTAINER_NAME }}-${{ github.run_id }}
restore-keys: |
ccache-${{ env.CONTAINER_NAME }}-
provider: ${{ inputs.provider }}
- name: Restore depends sources cache
id: depends-sources
uses: cirruslabs/cache/restore@v5
uses: ./.github/actions/cache/restore/internal
with:
path: ${{ env.SOURCES_PATH }}
key: depends-sources-${{ env.CONTAINER_NAME }}-${{ env.DEPENDS_HASH }}
restore-keys: |
depends-sources-${{ env.CONTAINER_NAME }}-
provider: ${{ inputs.provider }}
- name: Restore built depends cache
id: depends-built
uses: cirruslabs/cache/restore@v5
uses: ./.github/actions/cache/restore/internal
with:
path: ${{ env.BASE_CACHE }}
key: depends-built-${{ env.CONTAINER_NAME }}-${{ env.DEPENDS_HASH }}
restore-keys: |
depends-built-${{ env.CONTAINER_NAME }}-
provider: ${{ inputs.provider }}
- name: Restore previous releases cache
id: previous-releases
uses: cirruslabs/cache/restore@v5
uses: ./.github/actions/cache/restore/internal
with:
path: ${{ env.PREVIOUS_RELEASES_DIR }}
key: previous-releases-${{ env.CONTAINER_NAME }}-${{ env.PREVIOUS_RELEASES_HASH }}
restore-keys: |
previous-releases-${{ env.CONTAINER_NAME }}-
provider: ${{ inputs.provider }}
- name: export cache hits
shell: bash

View File

@@ -0,0 +1,43 @@
name: 'Cache Restore'
description: 'Restore a cache with WarpBuild on Warp runners and GitHub Actions cache otherwise'
inputs:
path:
description: 'A list of files, directories, and wildcard patterns to restore'
required: true
key:
description: 'An explicit key for restoring the cache'
required: true
restore-keys:
description: 'An ordered multiline string listing prefix-matched restore keys'
required: false
default: ''
provider:
description: 'The cache provider to use'
required: true
outputs:
cache-hit:
description: 'A boolean value to indicate an exact match was found for the primary key'
value: ${{ steps.warp.outputs.cache-hit || steps.gha.outputs.cache-hit }}
cache-primary-key:
description: 'The primary key used to restore the cache'
value: ${{ steps.warp.outputs.cache-primary-key || steps.gha.outputs.cache-primary-key }}
runs:
using: 'composite'
steps:
- name: Restore cache with WarpBuild
id: warp
if: ${{ inputs.provider == 'warp' }}
uses: WarpBuilds/cache/restore@v1
with:
path: ${{ inputs.path }}
key: ${{ inputs.key }}
restore-keys: ${{ inputs.restore-keys }}
- name: Restore cache with GitHub Actions
id: gha
if: ${{ inputs.provider == 'gha' }}
uses: actions/cache/restore@v5
with:
path: ${{ inputs.path }}
key: ${{ inputs.key }}
restore-keys: ${{ inputs.restore-keys }}

View File

@@ -1,5 +1,9 @@
name: 'Save Caches'
description: 'Save ccache, depends sources, and built depends caches'
inputs:
provider:
description: 'The cache provider to use'
required: true
runs:
using: 'composite'
steps:
@@ -11,29 +15,33 @@ runs:
echo "previous releases direct cache hit to primary key: ${{ env.previous-releases-cache-hit }}"
- name: Save Ccache cache
uses: cirruslabs/cache/save@v5
uses: ./.github/actions/cache/save/internal
if: ${{ (github.event_name == 'push') && (github.ref_name == github.event.repository.default_branch) }}
with:
path: ${{ env.CCACHE_DIR }}
key: ccache-${{ env.CONTAINER_NAME }}-${{ github.run_id }}
provider: ${{ inputs.provider }}
- name: Save depends sources cache
uses: cirruslabs/cache/save@v5
uses: ./.github/actions/cache/save/internal
if: ${{ (github.event_name == 'push') && (github.ref_name == github.event.repository.default_branch) && (env.depends-sources-cache-hit != 'true') }}
with:
path: ${{ env.SOURCES_PATH }}
key: depends-sources-${{ env.CONTAINER_NAME }}-${{ env.DEPENDS_HASH }}
provider: ${{ inputs.provider }}
- name: Save built depends cache
uses: cirruslabs/cache/save@v5
uses: ./.github/actions/cache/save/internal
if: ${{ (github.event_name == 'push') && (github.ref_name == github.event.repository.default_branch) && (env.depends-built-cache-hit != 'true' )}}
with:
path: ${{ env.BASE_CACHE }}
key: depends-built-${{ env.CONTAINER_NAME }}-${{ env.DEPENDS_HASH }}
provider: ${{ inputs.provider }}
- name: Save previous releases cache
uses: cirruslabs/cache/save@v5
uses: ./.github/actions/cache/save/internal
if: ${{ (github.event_name == 'push') && (github.ref_name == github.event.repository.default_branch) && (env.previous-releases-cache-hit != 'true' )}}
with:
path: ${{ env.PREVIOUS_RELEASES_DIR }}
key: previous-releases-${{ env.CONTAINER_NAME }}-${{ env.PREVIOUS_RELEASES_HASH }}
provider: ${{ inputs.provider }}

View File

@@ -0,0 +1,28 @@
name: 'Cache Save'
description: 'Save a cache with WarpBuild on Warp runners and GitHub Actions cache otherwise'
inputs:
path:
description: 'A list of files, directories, and wildcard patterns to cache'
required: true
key:
description: 'An explicit key for saving the cache'
required: true
provider:
description: 'The cache provider to use'
required: true
runs:
using: 'composite'
steps:
- name: Save cache with WarpBuild
if: ${{ inputs.provider == 'warp' }}
uses: WarpBuilds/cache/save@v1
with:
path: ${{ inputs.path }}
key: ${{ inputs.key }}
- name: Save cache with GitHub Actions
if: ${{ inputs.provider == 'gha' }}
uses: actions/cache/save@v5
with:
path: ${{ inputs.path }}
key: ${{ inputs.key }}

View File

@@ -1,27 +1,27 @@
name: 'Configure Docker'
description: 'Set up Docker build driver and configure build cache args'
inputs:
cache-provider:
description: 'gha or cirrus cache provider'
required: true
provider:
description: 'The cache provider to use'
required: false
default: 'gha'
runs:
using: 'composite'
steps:
- name: Check inputs
shell: python
run: |
# We expect only gha or cirrus as inputs to cache-provider
if "${{ inputs.cache-provider }}" not in ("gha", "cirrus"):
print("::warning title=Unknown input to configure docker action::Provided value was ${{ inputs.cache-provider }}")
- name: Set up Docker Buildx
- name: Set up Docker Buildx for Warp cache
if: ${{ inputs.provider == 'warp' }}
uses: docker/setup-buildx-action@v4
with:
# Use host network to allow access to cirrus gha cache running on the host
driver-opts: |
network=host
# This is required to allow buildkit to access the actions cache
- name: Set up Docker Buildx
if: ${{ inputs.provider != 'warp' }}
uses: docker/setup-buildx-action@v4
# This is required when using the gha cache backend with a manual docker buildx invocation.
# Docker will check for variables $ACTIONS_CACHE_URL, $ACTIONS_RESULTS_URL and $ACTIONS_RUNTIME_TOKEN
# which are set automatically when running on GitHub infra: https://docs.docker.com/build/cache/backends/gha/#synopsis
- name: Expose actions cache variables
uses: actions/github-script@v8
with:
@@ -36,25 +36,18 @@ runs:
- name: Construct docker build cache args
shell: bash
run: |
# Configure docker build cache backend
#
# On forks the gha cache will work but will use Github's cache backend.
# Docker will check for variables $ACTIONS_CACHE_URL, $ACTIONS_RESULTS_URL and $ACTIONS_RUNTIME_TOKEN
# which are set automatically when running on GitHub infra: https://docs.docker.com/build/cache/backends/gha/#synopsis
# Use cirrus cache host
if [[ ${{ inputs.cache-provider }} == 'cirrus' ]]; then
url_args="url=${CIRRUS_CACHE_HOST},url_v2=${CIRRUS_CACHE_HOST}"
else
url_args=""
cache_options="scope=${CONTAINER_NAME}"
if [[ "${{ inputs.provider }}" == "warp" ]]; then
cache_options="url=http://127.0.0.1:49160/,version=1,${cache_options}"
fi
# Configure docker build cache backend
# Always optimistically --cachefrom in case a cache blob exists
args=(--cache-from "type=gha${url_args:+,${url_args}},scope=${CONTAINER_NAME}")
args=(--cache-from "type=gha,${cache_options}")
# Only add --cache-to when using the Cirrus cache provider and pushing to the default branch.
if [[ ${{ inputs.cache-provider }} == 'cirrus' && ${{ github.event_name }} == "push" && ${{ github.ref_name }} == ${{ github.event.repository.default_branch }} ]]; then
args+=(--cache-to "type=gha${url_args:+,${url_args}},mode=max,ignore-error=true,scope=${CONTAINER_NAME}")
# Only add --cache-to when pushing to the default branch.
if [[ ${{ github.event_name }} == "push" && ${{ github.ref_name }} == ${{ github.event.repository.default_branch }} ]]; then
args+=(--cache-to "type=gha,mode=max,ignore-error=true,${cache_options}")
fi
# Always `--load` into docker images (needed when using the `docker-container` build driver).

View File

@@ -19,8 +19,7 @@ concurrency:
env:
CI_FAILFAST_TEST_LEAVE_DANGLING: 1 # GHA does not care about dangling processes and setting this variable avoids killing the CI script itself on error
CIRRUS_CACHE_HOST: http://127.0.0.1:12321/ # When using Cirrus Runners this host can be used by the docker `gha` build cache type.
REPO_USE_CIRRUS_RUNNERS: 'bitcoin/bitcoin' # Use cirrus runners and cache for this repo, instead of falling back to the slow GHA runners
REPO_USE_WARP_RUNNERS: 'bitcoin/bitcoin' # Use warp runners for this repo, instead of falling back to the slow GHA runners
defaults:
run:
@@ -47,9 +46,9 @@ jobs:
fi
- id: runners
run: |
if [[ "${REPO_USE_CIRRUS_RUNNERS}" == "${{ github.repository }}" ]]; then
echo "provider=cirrus" >> "$GITHUB_OUTPUT"
echo "::notice title=Runner Selection::Using Cirrus Runners"
if [[ "${REPO_USE_WARP_RUNNERS}" == "${{ github.repository }}" ]]; then
echo "provider=warp" >> "$GITHUB_OUTPUT"
echo "::notice title=Runner Selection::Using Warp Runners"
else
echo "provider=gha" >> "$GITHUB_OUTPUT"
echo "::notice title=Runner Selection::Using GitHub-hosted runners"
@@ -58,9 +57,9 @@ jobs:
test-each-commit:
name: 'test ancestor commits'
needs: runners
runs-on: ${{ needs.runners.outputs.provider == 'cirrus' && 'ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-md' || 'ubuntu-24.04' }}
runs-on: ${{ needs.runners.outputs.provider == 'warp' && 'warp-ubuntu-latest-x64-8x' || 'ubuntu-latest' }}
env:
TEST_RUNNER_PORT_MIN: "14000" # Use a larger port, to avoid colliding with CIRRUS_CACHE_HOST port 12321.
TEST_RUNNER_PORT_MIN: "14000" # Use a larger port range to avoid colliding with other CI services.
if: github.event_name == 'pull_request' && github.event.pull_request.commits != 1
timeout-minutes: 360 # Use maximum time, see https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes.
steps:
@@ -332,7 +331,7 @@ jobs:
windows-cross:
name: 'Windows-cross to x86_64, ${{ matrix.crt }}'
needs: [runners, record-frozen-commit]
runs-on: ${{ needs.runners.outputs.provider == 'cirrus' && 'ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-sm' || 'ubuntu-24.04' }}
runs-on: ${{ needs.runners.outputs.provider == 'warp' && 'warp-ubuntu-latest-x64-4x' || 'ubuntu-latest' }}
if: ${{ vars.SKIP_BRANCH_PUSH != 'true' || github.event_name == 'pull_request' }}
strategy:
@@ -364,18 +363,22 @@ jobs:
- name: Restore caches
id: restore-cache
uses: ./.github/actions/restore-caches
uses: ./.github/actions/cache/restore
with:
provider: ${{ needs.runners.outputs.provider }}
- name: Configure Docker
uses: ./.github/actions/configure-docker
with:
cache-provider: ${{ needs.runners.outputs.provider }}
provider: ${{ needs.runners.outputs.provider }}
- name: CI script
run: ./ci/test_run_all.sh
- name: Save caches
uses: ./.github/actions/save-caches
uses: ./.github/actions/cache/save
with:
provider: ${{ needs.runners.outputs.provider }}
- name: Upload built executables
uses: actions/upload-artifact@v7
@@ -443,7 +446,7 @@ jobs:
ci-matrix:
name: ${{ matrix.name }}
needs: runners
runs-on: ${{ needs.runners.outputs.provider == 'cirrus' && matrix.cirrus-runner || matrix.fallback-runner }}
runs-on: ${{ needs.runners.outputs.provider == 'warp' && matrix.warp-runner || matrix.fallback-runner }}
if: ${{ vars.SKIP_BRANCH_PUSH != 'true' || github.event_name == 'pull_request' }}
timeout-minutes: ${{ matrix.timeout-minutes }}
@@ -456,87 +459,87 @@ jobs:
matrix:
include:
- name: 'iwyu'
cirrus-runner: 'ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-md'
fallback-runner: 'ubuntu-24.04'
warp-runner: 'warp-ubuntu-latest-x64-8x'
fallback-runner: 'ubuntu-latest'
timeout-minutes: 120
file-env: './ci/test/00_setup_env_native_iwyu.sh'
- name: '32 bit ARM'
cirrus-runner: 'ubuntu-24.04-arm' # Cirrus' Arm runners are Apple (with virtual Linux aarch64), which doesn't support 32-bit mode
warp-runner: 'ubuntu-24.04-arm' # Warp's Arm runners don't support 32-bit mode currently
fallback-runner: 'ubuntu-24.04-arm'
timeout-minutes: 120
file-env: './ci/test/00_setup_env_arm.sh'
provider: 'gha'
- name: 'ASan + LSan + UBSan + integer'
cirrus-runner: 'ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-md' # has to match container in ci/test/00_setup_env_native_asan.sh for tracing tools
warp-runner: 'warp-ubuntu-2404-x64-8x' # has to match container in ci/test/00_setup_env_native_asan.sh for tracing tools
fallback-runner: 'ubuntu-24.04'
timeout-minutes: 120
file-env: './ci/test/00_setup_env_native_asan.sh'
- name: 'macOS-cross to arm64'
cirrus-runner: 'ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-sm'
fallback-runner: 'ubuntu-24.04'
warp-runner: 'warp-ubuntu-latest-x64-4x'
fallback-runner: 'ubuntu-latest'
timeout-minutes: 120
file-env: './ci/test/00_setup_env_mac_cross.sh'
- name: 'macOS-cross to x86_64'
cirrus-runner: 'ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-sm'
fallback-runner: 'ubuntu-24.04'
warp-runner: 'warp-ubuntu-latest-x64-4x'
fallback-runner: 'ubuntu-latest'
timeout-minutes: 120
file-env: './ci/test/00_setup_env_mac_cross_intel.sh'
- name: 'No wallet'
cirrus-runner: 'ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-sm'
fallback-runner: 'ubuntu-24.04'
warp-runner: 'warp-ubuntu-latest-x64-4x'
fallback-runner: 'ubuntu-latest'
timeout-minutes: 120
file-env: './ci/test/00_setup_env_native_nowallet.sh'
- name: 'i686, no IPC'
cirrus-runner: 'ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-md'
fallback-runner: 'ubuntu-24.04'
warp-runner: 'warp-ubuntu-latest-x64-8x'
fallback-runner: 'ubuntu-latest'
timeout-minutes: 120
file-env: './ci/test/00_setup_env_i686_no_ipc.sh'
- name: 'fuzzer,address,undefined,integer'
cirrus-runner: 'ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-lg'
fallback-runner: 'ubuntu-24.04'
warp-runner: 'warp-ubuntu-latest-x64-16x'
fallback-runner: 'ubuntu-latest'
timeout-minutes: 240
file-env: './ci/test/00_setup_env_native_fuzz.sh'
- name: 'previous releases'
cirrus-runner: 'ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-md'
fallback-runner: 'ubuntu-24.04'
warp-runner: 'warp-ubuntu-latest-x64-8x'
fallback-runner: 'ubuntu-latest'
timeout-minutes: 120
file-env: './ci/test/00_setup_env_native_previous_releases.sh'
- name: 'Alpine (musl)'
cirrus-runner: 'ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-md'
fallback-runner: 'ubuntu-24.04'
warp-runner: 'warp-ubuntu-latest-x64-8x'
fallback-runner: 'ubuntu-latest'
timeout-minutes: 120
file-env: './ci/test/00_setup_env_native_alpine_musl.sh'
- name: 'tidy'
cirrus-runner: 'ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-md'
fallback-runner: 'ubuntu-24.04'
warp-runner: 'warp-ubuntu-latest-x64-8x'
fallback-runner: 'ubuntu-latest'
timeout-minutes: 120
file-env: './ci/test/00_setup_env_native_tidy.sh'
- name: 'TSan'
cirrus-runner: 'ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-md'
fallback-runner: 'ubuntu-24.04'
warp-runner: 'warp-ubuntu-latest-x64-8x'
fallback-runner: 'ubuntu-latest'
timeout-minutes: 120
file-env: './ci/test/00_setup_env_native_tsan.sh'
- name: 'MSan, fuzz'
cirrus-runner: 'ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-md'
fallback-runner: 'ubuntu-24.04'
warp-runner: 'warp-ubuntu-latest-x64-8x'
fallback-runner: 'ubuntu-latest'
timeout-minutes: 150
file-env: './ci/test/00_setup_env_native_fuzz_with_msan.sh'
- name: 'MSan'
cirrus-runner: 'ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-lg'
fallback-runner: 'ubuntu-24.04'
warp-runner: 'warp-ubuntu-latest-x64-16x'
fallback-runner: 'ubuntu-latest'
timeout-minutes: 120
file-env: './ci/test/00_setup_env_native_msan.sh'
@@ -550,12 +553,14 @@ jobs:
- name: Restore caches
id: restore-cache
uses: ./.github/actions/restore-caches
uses: ./.github/actions/cache/restore
with:
provider: ${{ matrix.provider || needs.runners.outputs.provider }}
- name: Configure Docker
uses: ./.github/actions/configure-docker
with:
cache-provider: ${{ matrix.provider || needs.runners.outputs.provider }}
provider: ${{ matrix.provider || needs.runners.outputs.provider }}
- name: Clear unnecessary files
if: ${{ needs.runners.outputs.provider == 'gha' && true || false }} # Only needed on GHA runners
@@ -576,12 +581,14 @@ jobs:
run: ./ci/test_run_all.sh
- name: Save caches
uses: ./.github/actions/save-caches
uses: ./.github/actions/cache/save
with:
provider: ${{ matrix.provider || needs.runners.outputs.provider }}
lint:
name: 'lint'
needs: runners
runs-on: ${{ needs.runners.outputs.provider == 'cirrus' && 'ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-xs' || 'ubuntu-24.04' }}
runs-on: ${{ needs.runners.outputs.provider == 'warp' && 'warp-ubuntu-latest-x64-2x' || 'ubuntu-latest' }}
if: ${{ vars.SKIP_BRANCH_PUSH != 'true' || github.event_name == 'pull_request' }}
timeout-minutes: 20
env:
@@ -598,7 +605,7 @@ jobs:
- name: Configure Docker
uses: ./.github/actions/configure-docker
with:
cache-provider: ${{ needs.runners.outputs.provider }}
provider: ${{ needs.runners.outputs.provider }}
- name: CI script
run: |

View File

@@ -28,9 +28,9 @@ get_directory_property(precious_variables CACHE_VARIABLES)
#=============================
set(CLIENT_NAME "Bitcoin Core")
set(CLIENT_VERSION_MAJOR 31)
set(CLIENT_VERSION_MINOR 0)
set(CLIENT_VERSION_MINOR 1)
set(CLIENT_VERSION_BUILD 0)
set(CLIENT_VERSION_RC 0)
set(CLIENT_VERSION_RC 1)
set(CLIENT_VERSION_IS_RELEASE "true")
set(COPYRIGHT_YEAR "2026")

View File

@@ -80,20 +80,22 @@ trigger cache-invalidation and rebuilds as necessary.
To configure the primary repository, follow these steps:
1. Register with [Cirrus Runners](https://cirrus-runners.app/) and purchase runners.
2. Install the Cirrus Runners GitHub app against the GitHub organization.
1. Register with [WarpBuild](https://www.warpbuild.com/) and purchase runners.
2. Install the WarpBuild GitHub app against the GitHub organization.
3. Enable organisation-level runners to be used in public repositories:
1. `Org settings -> Actions -> Runner Groups -> Default -> Allow public repos`
4. Permit the following actions to run:
1. cirruslabs/cache/restore@\*
1. cirruslabs/cache/save@\*
1. docker/setup-buildx-action@\*
1. actions/cache/restore@\*
1. actions/cache/save@\*
1. actions/github-script@\*
1. docker/setup-buildx-action@\*
1. warpbuilds/cache/restore@\*
1. warpbuilds/cache/save@\*
### Forked repositories
When used in a fork the CI will run on GitHub's free hosted runners by default.
In this case, due to GitHub's 10GB-per-repo cache size limitations caches will be frequently evicted and missed, but the workflows will run (slowly).
In this case, GitHub's cache size limitations may cause caches to be frequently evicted and missed, but the workflows will run (slowly).
It is also possible to use your own Cirrus Runners in your own fork with an appropriate patch to the `REPO_USE_CIRRUS_RUNNERS` variable in ../.github/workflows/ci.yml
NB that Cirrus Runners only work at an organisation level, therefore in order to use your own Cirrus Runners, *the fork must be within your own organisation*.
It is also possible to use your own WarpBuild Runners in your own fork with an appropriate patch to the `REPO_USE_WARP_RUNNERS` variable in ../.github/workflows/ci.yml
NB that WarpBuild Runners only work at an organisation level, therefore in order to use your own WarpBuild Runners, *the fork must be within your own organisation*.

View File

@@ -1,7 +1,7 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3.
.TH BITCOIN-CLI "1" "April 2026" "bitcoin-cli v31.0.0" "User Commands"
.TH BITCOIN-CLI "1" "June 2026" "bitcoin-cli v31.1.0rc1" "User Commands"
.SH NAME
bitcoin-cli \- manual page for bitcoin-cli v31.0.0
bitcoin-cli \- manual page for bitcoin-cli v31.1.0rc1
.SH SYNOPSIS
.B bitcoin-cli
[\fI\,options\/\fR] \fI\,<command> \/\fR[\fI\,params\/\fR]
@@ -15,7 +15,7 @@ bitcoin-cli \- manual page for bitcoin-cli v31.0.0
.B bitcoin-cli
[\fI\,options\/\fR] \fI\,help <command>\/\fR
.SH DESCRIPTION
Bitcoin Core RPC client version v31.0.0
Bitcoin Core RPC client version v31.1.0rc1
.PP
The bitcoin\-cli utility provides a command line interface to interact with a Bitcoin Core RPC server.
.PP

View File

@@ -1,12 +1,12 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3.
.TH BITCOIN-QT "1" "April 2026" "bitcoin-qt v31.0.0" "User Commands"
.TH BITCOIN-QT "1" "June 2026" "bitcoin-qt v31.1.0rc1" "User Commands"
.SH NAME
bitcoin-qt \- manual page for bitcoin-qt v31.0.0
bitcoin-qt \- manual page for bitcoin-qt v31.1.0rc1
.SH SYNOPSIS
.B bitcoin-qt
[\fI\,options\/\fR] [\fI\,URI\/\fR]
.SH DESCRIPTION
Bitcoin Core version v31.0.0
Bitcoin Core version v31.1.0rc1
.PP
The bitcoin\-qt application provides a graphical interface for interacting with Bitcoin Core.
.PP

View File

@@ -1,7 +1,7 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3.
.TH BITCOIN-TX "1" "April 2026" "bitcoin-tx v31.0.0" "User Commands"
.TH BITCOIN-TX "1" "June 2026" "bitcoin-tx v31.1.0rc1" "User Commands"
.SH NAME
bitcoin-tx \- manual page for bitcoin-tx v31.0.0
bitcoin-tx \- manual page for bitcoin-tx v31.1.0rc1
.SH SYNOPSIS
.B bitcoin-tx
[\fI\,options\/\fR] \fI\,<hex-tx> \/\fR[\fI\,commands\/\fR]
@@ -9,7 +9,7 @@ bitcoin-tx \- manual page for bitcoin-tx v31.0.0
.B bitcoin-tx
[\fI\,options\/\fR] \fI\,-create \/\fR[\fI\,commands\/\fR]
.SH DESCRIPTION
Bitcoin Core bitcoin\-tx utility version v31.0.0
Bitcoin Core bitcoin\-tx utility version v31.1.0rc1
.PP
The bitcoin\-tx tool is used for creating and modifying bitcoin transactions.
.PP

View File

@@ -1,7 +1,7 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3.
.TH BITCOIN-UTIL "1" "April 2026" "bitcoin-util v31.0.0" "User Commands"
.TH BITCOIN-UTIL "1" "June 2026" "bitcoin-util v31.1.0rc1" "User Commands"
.SH NAME
bitcoin-util \- manual page for bitcoin-util v31.0.0
bitcoin-util \- manual page for bitcoin-util v31.1.0rc1
.SH SYNOPSIS
.B bitcoin-util
[\fI\,options\/\fR] [\fI\,command\/\fR]
@@ -9,7 +9,7 @@ bitcoin-util \- manual page for bitcoin-util v31.0.0
.B bitcoin-util
[\fI\,options\/\fR] \fI\,grind <hex-block-header>\/\fR
.SH DESCRIPTION
Bitcoin Core bitcoin\-util utility version v31.0.0
Bitcoin Core bitcoin\-util utility version v31.1.0rc1
.PP
The bitcoin\-util tool provides bitcoin related functionality that does not rely on the ability to access a running node. Available [commands] are listed below.
.SH OPTIONS

View File

@@ -1,12 +1,12 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3.
.TH BITCOIN-WALLET "1" "April 2026" "bitcoin-wallet v31.0.0" "User Commands"
.TH BITCOIN-WALLET "1" "June 2026" "bitcoin-wallet v31.1.0rc1" "User Commands"
.SH NAME
bitcoin-wallet \- manual page for bitcoin-wallet v31.0.0
bitcoin-wallet \- manual page for bitcoin-wallet v31.1.0rc1
.SH SYNOPSIS
.B bitcoin-wallet
[\fI\,options\/\fR] \fI\,<command>\/\fR
.SH DESCRIPTION
Bitcoin Core bitcoin\-wallet utility version v31.0.0
Bitcoin Core bitcoin\-wallet utility version v31.1.0rc1
.PP
bitcoin\-wallet is an offline tool for creating and interacting with Bitcoin Core wallet files.
.PP

View File

@@ -1,7 +1,7 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3.
.TH BITCOIN "1" "April 2026" "bitcoin v31.0.0" "User Commands"
.TH BITCOIN "1" "June 2026" "bitcoin v31.1.0rc1" "User Commands"
.SH NAME
bitcoin \- manual page for bitcoin v31.0.0
bitcoin \- manual page for bitcoin v31.1.0rc1
.SH SYNOPSIS
.B bitcoin
[\fI\,OPTIONS\/\fR] \fI\,COMMAND\/\fR...

View File

@@ -1,12 +1,12 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3.
.TH BITCOIND "1" "April 2026" "bitcoind v31.0.0" "User Commands"
.TH BITCOIND "1" "June 2026" "bitcoind v31.1.0rc1" "User Commands"
.SH NAME
bitcoind \- manual page for bitcoind v31.0.0
bitcoind \- manual page for bitcoind v31.1.0rc1
.SH SYNOPSIS
.B bitcoind
[\fI\,options\/\fR]
.SH DESCRIPTION
Bitcoin Core daemon version v31.0.0 bitcoind
Bitcoin Core daemon version v31.1.0rc1 bitcoind
.PP
The Bitcoin Core daemon (bitcoind) is a headless program that connects to the Bitcoin network to validate and relay transactions and blocks, as well as relaying addresses.
.PP

View File

@@ -1,9 +1,9 @@
v31.x Release Notes
v31.1rc1 Release Notes
===================
Bitcoin Core version 31.x is now available from:
Bitcoin Core version 31.1rc1 is now available from:
<https://bitcoincore.org/bin/bitcoin-core-31.x/>
<https://bitcoincore.org/bin/bitcoin-core-31.1/test.rc1/>
This release includes new features, various bug fixes and performance
improvements, as well as updated translations.
@@ -39,17 +39,35 @@ on them. It is not recommended to use Bitcoin Core on unsupported systems.
Notable changes
===============
### PrivateBroadcast
This release fixes an ip address leak when using the -privatebroadcast feature.
Under certain circumstances connections were being made over clearnet rather than
the enabled privacy network.
### Validation
- #35209 validation: correct lifetime of precomputed tx data
- #35465 coins: compact chainstate regularly
### Leveldb
- #61(bitcoin-core/leveldb): Disable seek compaction
### P2P
- #35032 net_processing: don't modify addrman for private broadcast connections
- #35410 net: use the proxy if overriden when doing v2->v1 reconnections
### Wallet
- #35227 wallet: check the final BDB page LSN during migration
- #35228 wallet: use outpoint when estimating input size
### Musig
- #35316 musig: Reject empty pubkey list in GetMuSig2KeyAggCache
### Build
@@ -62,11 +80,16 @@ Notable changes
- #34991 test: fix feature_index_prune.py bug when using --usecli
- #35080 test: Add missing self.options.timeout_factor scale in tool_bitcoin_chainstate.py
- #35218 test: fix P2SH script in coins cache fuzz target
- #35279 psbt, test: remove address type restrictions in test
### Fuzz
- #35289 fuzz: Fix timeout in txorphan
### Util
- #35384 util: Check write failures before renaming settings.json
### Docs
- #35283 doc: mention -DWITH_ZMQ=ON in BSD build guides
@@ -75,29 +98,43 @@ Notable changes
- #35202 ci: restore sockets in i686, no IPC job
- #35230 ci: Move --usecli --extended from i386 task to alpine task
- #35348 ci: switch to GitHub cache for all runners
- #35378 ci: switch runners from cirrus to warpbuild
- #35408 ci: 35378 followups
- #35430 ci: use warp caching on warp runners
- #35447 ci: use warpbuild cache for docker buildkit cache
### Misc
- #35044 contrib: Fix NameError in signet miner gbt()
- #35175 multi_index: fix compilation failure with boost >= 1.91
- #34953 crypto: disable ASan instrumentation of SSE4 SHA256 for GCC
Credits
=======
Thanks to everyone who directly contributed to this release:
- andrewtoth
- Cory Fields
- Crypt-iQ
- darosior
- deadmanoz
- fanquake
- Greg Sanders
- Hennadii Stepanov
- junbyjun1238
- Lőrinc
- MarcoFalke
- marcofleon
- nervana21
- optout21
- Pol Espinasa
- rkrux
- Shrey
- Torkel Rogstad
- Vasil Dimov
- willcl-ark
As well as to everyone that helped with translations on
[Transifex](https://explore.transifex.com/bitcoin/bitcoin/).

View File

@@ -86,7 +86,7 @@ bool ReadSettings(const fs::path& path, std::map<std::string, SettingsValue>& va
SettingsValue in;
if (!in.read(std::string{std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()})) {
errors.emplace_back(strprintf("Settings file %s does not contain valid JSON. This is probably caused by disk corruption or a crash, "
errors.emplace_back(strprintf("Settings file %s does not contain valid JSON. This may be caused by a crash, power loss, full disk, or storage error, "
"and can be fixed by removing the file, which will reset settings to default values.",
fs::PathToString(path)));
return false;
@@ -139,7 +139,15 @@ bool WriteSettings(const fs::path& path,
return false;
}
file << out.write(/* prettyIndent= */ 4, /* indentLevel= */ 1) << std::endl;
if (file.fail()) {
errors.emplace_back(strprintf("Error: Unable to write settings file %s", fs::PathToString(path)));
return false;
}
file.close();
if (file.fail()) {
errors.emplace_back(strprintf("Error: Unable to close settings file %s", fs::PathToString(path)));
return false;
}
return true;
}

View File

@@ -12,18 +12,23 @@
namespace sha256_sse4
{
void Transform(uint32_t* s, const unsigned char* chunk, size_t blocks)
#if defined(__clang__)
/*
clang is unable to compile this with -O0 and -fsanitize=address.
See upstream bug: https://github.com/llvm/llvm-project/issues/92182.
This also fails to compile with -O2, -fcf-protection & -fsanitize=address.
See https://github.com/bitcoin/bitcoin/issues/31913.
*/
#if __has_feature(address_sanitizer)
/*
Both Clang and GCC fail with ASan on this inline assembly:
- Clang: compile failure with -O0 or -O2 + -fcf-protection under ASan.
See https://github.com/llvm/llvm-project/issues/92182
and https://github.com/bitcoin/bitcoin/issues/31913.
- GCC: runtime SEGV during SHA256AutoDetect()'s self-test under ASan,
regardless of optimization level.
See https://github.com/bitcoin/bitcoin/issues/34881.
*/
#if defined(__SANITIZE_ADDRESS__)
__attribute__((no_sanitize("address")))
#elif defined(__clang__)
#if __has_feature(address_sanitizer) // fallback can be removed once support for Clang 21 is dropped
__attribute__((no_sanitize("address")))
#endif
#endif
void Transform(uint32_t* s, const unsigned char* chunk, size_t blocks)
{
static const uint32_t K256 alignas(16) [] = {
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,

View File

@@ -246,7 +246,7 @@ CDBWrapper::CDBWrapper(const DBParams& params)
if (params.options.force_compact) {
LogInfo("Starting database compaction of %s", fs::PathToString(params.path));
DBContext().pdb->CompactRange(nullptr, nullptr);
CompactFull();
LogInfo("Finished database compaction of %s", fs::PathToString(params.path));
}
@@ -291,11 +291,18 @@ void CDBWrapper::WriteBatch(CDBBatch& batch, bool fSync)
}
}
std::optional<std::string> CDBWrapper::GetProperty(const std::string& property) const
{
if (std::string value; DBContext().pdb->GetProperty(property, &value)) return value;
return std::nullopt;
}
void CDBWrapper::CompactFull() { DBContext().pdb->CompactRange(nullptr, nullptr); }
size_t CDBWrapper::DynamicMemoryUsage() const
{
std::string memory;
std::optional<size_t> parsed;
if (!DBContext().pdb->GetProperty("leveldb.approximate-memory-usage", &memory) || !(parsed = ToIntegral<size_t>(memory))) {
if (auto memory{GetProperty("leveldb.approximate-memory-usage")}; !memory || !(parsed = ToIntegral<size_t>(*memory))) {
LogDebug(BCLog::LEVELDB, "Failed to get approximate-memory-usage property\n");
return 0;
}

View File

@@ -250,6 +250,12 @@ public:
void WriteBatch(CDBBatch& batch, bool fSync = false);
//! Perform a blocking full compaction of the underlying LevelDB.
void CompactFull();
//! Return a LevelDB property value, if available.
std::optional<std::string> GetProperty(const std::string& property) const;
// Get an estimate of LevelDB memory usage (in bytes).
size_t DynamicMemoryUsage() const;

View File

@@ -54,8 +54,8 @@ static const int kValueSize = 200 * 1024;
static const int kTotalSize = 100 * 1024 * 1024;
static const int kCount = kTotalSize / kValueSize;
// Read through the first n keys repeatedly and check that they get
// compacted (verified by checking the size of the key space).
// Read through the first n keys repeatedly and check that reads do NOT
// trigger compaction (seek compaction is disabled in this fork).
void AutoCompactTest::DoReads(int n) {
std::string value(kValueSize, 'x');
DBImpl* dbi = reinterpret_cast<DBImpl*>(db_);
@@ -76,25 +76,23 @@ void AutoCompactTest::DoReads(int n) {
const int64_t initial_size = Size(Key(0), Key(n));
const int64_t initial_other_size = Size(Key(n), Key(kCount));
// Read until size drops significantly.
// Read repeatedly. The size of the read range must NOT shrink: with
// seek compaction disabled, reads never schedule a compaction.
std::string limit_key = Key(n);
for (int read = 0; true; read++) {
ASSERT_LT(read, 100) << "Taking too long to compact";
for (int read = 0; read < 100; read++) {
Iterator* iter = db_->NewIterator(ReadOptions());
for (iter->SeekToFirst();
iter->Valid() && iter->key().ToString() < limit_key; iter->Next()) {
// Drop data
}
delete iter;
// Wait a little bit to allow any triggered compactions to complete.
Env::Default()->SleepForMicroseconds(1000000);
uint64_t size = Size(Key(0), Key(n));
fprintf(stderr, "iter %3d => %7.3f MB [other %7.3f MB]\n", read + 1,
size / 1048576.0, Size(Key(n), Key(kCount)) / 1048576.0);
if (size <= initial_size / 10) {
break;
}
}
// Give any background work a chance to run, even though none should.
Env::Default()->SleepForMicroseconds(1000000);
ASSERT_EQ(Size(Key(0), Key(n)), static_cast<uint64_t>(initial_size));
// Verify that the size of the key space not touched by the reads
// is pretty much unchanged.

View File

@@ -735,15 +735,14 @@ TEST(DBTest, GetPicksCorrectFile) {
} while (ChangeOptions());
}
TEST(DBTest, GetEncountersEmptyLevel) {
TEST(DBTest, GetDoesNotTriggerSeekCompaction) {
do {
// Arrange for the following to happen:
// * sstable A in level 0
// * nothing in level 1
// * sstable B in level 2
// Then do enough Get() calls to arrange for an automatic compaction
// of sstable A. A bug would cause the compaction to be marked as
// occurring at level 1 (instead of the correct level 0).
// Seek compaction is disabled in this fork, so repeated reads must
// not change the level layout. A manual compaction must still work.
// Step 1: First place sstables in levels 0 and 2
int compaction_count = 0;
@@ -761,14 +760,17 @@ TEST(DBTest, GetEncountersEmptyLevel) {
ASSERT_EQ(NumTableFilesAtLevel(1), 0);
ASSERT_EQ(NumTableFilesAtLevel(2), 1);
// Step 3: read a bunch of times
// Step 3: many read misses must not schedule any compaction.
for (int i = 0; i < 1000; i++) {
ASSERT_EQ("NOT_FOUND", Get("missing"));
}
// Step 4: Wait for compaction to finish
DelayMilliseconds(1000);
ASSERT_EQ(NumTableFilesAtLevel(0), 1);
ASSERT_EQ(NumTableFilesAtLevel(1), 0);
ASSERT_EQ(NumTableFilesAtLevel(2), 1);
// Step 4: a manual compaction still moves the L0 file down.
dbfull()->TEST_CompactRange(0, nullptr, nullptr);
ASSERT_EQ(NumTableFilesAtLevel(0), 0);
} while (ChangeOptions());
}

View File

@@ -400,16 +400,11 @@ Status Version::Get(const ReadOptions& options, const LookupKey& k,
return state.found ? state.s : Status::NotFound(Slice());
}
bool Version::UpdateStats(const GetStats& stats) {
FileMetaData* f = stats.seek_file;
if (f != nullptr) {
f->allowed_seeks--;
if (f->allowed_seeks <= 0 && file_to_compact_ == nullptr) {
file_to_compact_ = f;
file_to_compact_level_ = stats.seek_file_level;
return true;
}
}
bool Version::UpdateStats(const GetStats& /*stats*/) {
// Disable automatic compactions triggered by read seek counters.
// The heuristic was tuned for expensive random seeks and can create
// severe write amplification on large random-key databases.
// Size and manual compactions still run.
return false;
}
@@ -661,6 +656,8 @@ class VersionSet::Builder {
// same as the compaction of 40KB of data. We are a little
// conservative and allow approximately one seek for every 16KB
// of data before triggering a compaction.
//
// Note: seek compactions are disabled. See Version::UpdateStats.
f->allowed_seeks = static_cast<int>((f->file_size / 16384U));
if (f->allowed_seeks < 100) f->allowed_seeks = 100;

View File

@@ -16,6 +16,10 @@ constexpr uint256 MUSIG_CHAINCODE{
static bool GetMuSig2KeyAggCache(const std::vector<CPubKey>& pubkeys, secp256k1_musig_keyagg_cache& keyagg_cache)
{
if (pubkeys.empty()) {
return false;
}
// Parse the pubkeys
std::vector<secp256k1_pubkey> secp_pubkeys;
std::vector<const secp256k1_pubkey*> pubkey_ptrs;

View File

@@ -487,8 +487,11 @@ CNode* CConnman::ConnectNode(CAddress addrConnect,
LogDebug(BCLog::PROXY, "Using proxy: %s to connect to %s\n", proxy.ToString(), target_addr.ToStringAddrPort());
sock = ConnectThroughProxy(proxy, target_addr.ToStringAddr(), target_addr.GetPort(), proxyConnectionFailed);
} else {
// no proxy needed (none set for target network)
sock = ConnectDirectly(target_addr, conn_type == ConnectionType::MANUAL);
// No proxy needed (none set for target network). Private broadcast connections
// must always use a proxy, otherwise they would leak the originator's IP address.
if (Assume(conn_type != ConnectionType::PRIVATE_BROADCAST)) {
sock = ConnectDirectly(target_addr, conn_type == ConnectionType::MANUAL);
}
}
if (!proxyConnectionFailed) {
// If a connection to the node was attempted, and failure (if any) is not caused by a problem connecting to
@@ -536,6 +539,7 @@ CNode* CConnman::ConnectNode(CAddress addrConnect,
network_id,
CNodeOptions{
.permission_flags = permission_flags,
.proxy_override = proxy_override,
.i2p_sam_session = std::move(i2p_transient_session),
.recv_flood_size = nReceiveFloodSize,
.use_v2transport = use_v2transport,
@@ -1902,7 +1906,13 @@ bool CConnman::AddConnection(const std::string& address, ConnectionType conn_typ
CountingSemaphoreGrant<> grant(*semOutbound, true);
if (!grant) return false;
OpenNetworkConnection(CAddress(), false, std::move(grant), address.c_str(), conn_type, /*use_v2transport=*/use_v2transport);
OpenNetworkConnection(/*addrConnect=*/CAddress{},
/*fCountFailure=*/false,
/*grant_outbound=*/std::move(grant),
/*pszDest=*/address.c_str(),
/*conn_type=*/conn_type,
/*use_v2transport=*/use_v2transport,
/*proxy_override=*/std::nullopt);
return true;
}
@@ -1943,6 +1953,7 @@ void CConnman::DisconnectNodes()
// and we don't want to hold up the socket handler thread for that long.
if (network_active && pnode->m_transport->ShouldReconnectV1()) {
reconnections_to_add.push_back({
.proxy_override = pnode->m_proxy_override,
.addr_connect = pnode->addr,
.grant = std::move(pnode->grantOutbound),
.destination = pnode->m_dest,
@@ -2419,7 +2430,13 @@ void CConnman::ProcessAddrFetch()
CAddress addr;
CountingSemaphoreGrant<> grant(*semOutbound, /*fTry=*/true);
if (grant) {
OpenNetworkConnection(addr, false, std::move(grant), strDest.c_str(), ConnectionType::ADDR_FETCH, use_v2transport);
OpenNetworkConnection(/*addrConnect=*/addr,
/*fCountFailure=*/false,
/*grant_outbound=*/std::move(grant),
/*pszDest=*/strDest.c_str(),
/*conn_type=*/ConnectionType::ADDR_FETCH,
/*use_v2transport=*/use_v2transport,
/*proxy_override=*/std::nullopt);
}
}
@@ -2537,8 +2554,13 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect, std
{
for (const std::string& strAddr : connect)
{
CAddress addr(CService(), NODE_NONE);
OpenNetworkConnection(addr, false, {}, strAddr.c_str(), ConnectionType::MANUAL, /*use_v2transport=*/use_v2transport);
OpenNetworkConnection(/*addrConnect=*/CAddress{CService{}, NODE_NONE},
/*fCountFailure=*/false,
/*grant_outbound=*/{},
/*pszDest=*/strAddr.c_str(),
/*conn_type=*/ConnectionType::MANUAL,
/*use_v2transport=*/use_v2transport,
/*proxy_override=*/std::nullopt);
for (int i = 0; i < 10 && i < nLoop; i++)
{
if (!m_interrupt_net->sleep_for(500ms)) {
@@ -2888,7 +2910,13 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect, std
const bool count_failures{((int)outbound_ipv46_peer_netgroups.size() + outbound_privacy_network_peers) >= std::min(m_max_automatic_connections - 1, 2)};
// Use BIP324 transport when both us and them have NODE_V2_P2P set.
const bool use_v2transport(addrConnect.nServices & GetLocalServices() & NODE_P2P_V2);
OpenNetworkConnection(addrConnect, count_failures, std::move(grant), /*pszDest=*/nullptr, conn_type, use_v2transport);
OpenNetworkConnection(/*addrConnect=*/addrConnect,
/*fCountFailure=*/count_failures,
/*grant_outbound=*/std::move(grant),
/*pszDest=*/nullptr,
/*conn_type=*/conn_type,
/*use_v2transport=*/use_v2transport,
/*proxy_override=*/std::nullopt);
}
}
}
@@ -2982,8 +3010,13 @@ void CConnman::ThreadOpenAddedConnections()
break;
}
tried = true;
CAddress addr(CService(), NODE_NONE);
OpenNetworkConnection(addr, false, std::move(grant), info.m_params.m_added_node.c_str(), ConnectionType::MANUAL, info.m_params.m_use_v2transport);
OpenNetworkConnection(/*addrConnect=*/CAddress{CService{}, NODE_NONE},
/*fCountFailure=*/false,
/*grant_outbound=*/std::move(grant),
/*pszDest=*/info.m_params.m_added_node.c_str(),
/*conn_type=*/ConnectionType::MANUAL,
/*use_v2transport=*/info.m_params.m_use_v2transport,
/*proxy_override=*/std::nullopt);
if (!m_interrupt_net->sleep_for(500ms)) return;
grant = CountingSemaphoreGrant<>(*semAddnode, /*fTry=*/true);
}
@@ -3980,6 +4013,7 @@ CNode::CNode(NodeId idIn,
m_permission_flags{node_opts.permission_flags},
m_sock{sock},
m_connected{GetTime<std::chrono::seconds>()},
m_proxy_override{std::move(node_opts.proxy_override)},
addr{addrIn},
addrBind{addrBindIn},
m_addr_name{addrNameIn.empty() ? addr.ToStringAddrPort() : addrNameIn},
@@ -4163,7 +4197,8 @@ void CConnman::PerformReconnections()
std::move(item.grant),
item.destination.empty() ? nullptr : item.destination.c_str(),
item.conn_type,
item.use_v2transport);
item.use_v2transport,
item.proxy_override);
}
}

View File

@@ -669,6 +669,7 @@ public:
struct CNodeOptions
{
NetPermissionFlags permission_flags = NetPermissionFlags::None;
std::optional<Proxy> proxy_override = {};
std::unique_ptr<i2p::sam::Session> i2p_sam_session = nullptr;
bool prefer_evict = false;
size_t recv_flood_size{DEFAULT_MAXRECEIVEBUFFER * 1000};
@@ -711,6 +712,10 @@ public:
std::atomic<std::chrono::seconds> m_last_recv{0s};
//! Unix epoch time at peer connection
const std::chrono::seconds m_connected;
//! Proxy to use regardless of global proxy settings if reconnecting to this node.
const std::optional<Proxy> m_proxy_override;
// Address of this peer
const CAddress addr;
// Bind address of our side of the connection
@@ -1186,7 +1191,7 @@ public:
const char* pszDest,
ConnectionType conn_type,
bool use_v2transport,
const std::optional<Proxy>& proxy_override = std::nullopt)
const std::optional<Proxy>& proxy_override)
EXCLUSIVE_LOCKS_REQUIRED(!m_unused_i2p_sessions_mutex);
/// Group of private broadcast related members.
@@ -1778,6 +1783,7 @@ private:
/** Struct for entries in m_reconnections. */
struct ReconnectionInfo
{
std::optional<Proxy> proxy_override;
CAddress addr_connect;
CountingSemaphoreGrant<> grant;
std::string destination;

View File

@@ -356,7 +356,13 @@ static RPCHelpMan addnode()
if (command == "onetry")
{
CAddress addr;
connman.OpenNetworkConnection(addr, /*fCountFailure=*/false, /*grant_outbound=*/{}, std::string{node_arg}.c_str(), ConnectionType::MANUAL, use_v2transport);
connman.OpenNetworkConnection(/*addrConnect=*/addr,
/*fCountFailure=*/false,
/*grant_outbound=*/{},
/*pszDest=*/std::string{node_arg}.c_str(),
/*conn_type=*/ConnectionType::MANUAL,
/*use_v2transport=*/use_v2transport,
/*proxy_override=*/std::nullopt);
return UniValue::VNULL;
}

View File

@@ -89,6 +89,12 @@ BOOST_AUTO_TEST_CASE(valid_keys)
}
}
BOOST_AUTO_TEST_CASE(empty_pubkey_list)
{
const std::optional<CPubKey> aggregate_pubkey{MuSig2AggregatePubkeys({})};
BOOST_CHECK(!aggregate_pubkey.has_value());
}
BOOST_AUTO_TEST_CASE(invalid_key)
{
std::vector<std::string> test_vectors = {

View File

@@ -13,6 +13,7 @@
#include <txdb.h>
#include <uint256.h>
#include <undo.h>
#include <util/check.h>
#include <util/strencodings.h>
#include <map>
@@ -1060,6 +1061,30 @@ BOOST_FIXTURE_TEST_CASE(ccoins_flush_behavior, FlushTest)
}
}
BOOST_FIXTURE_TEST_CASE(coins_db_leveldb_layout, FlushTest)
{
auto level2_files{[](CCoinsViewDB& base) {
return *Assert(ToIntegral<int>(*Assert(base.GetDBProperty("leveldb.num-files-at-level2"))));
}};
const COutPoint outpoint{Txid::FromUint256(m_rng.rand256()), 0};
const Coin coin{MakeCoin()};
const uint256 block_hash{m_rng.rand256()};
CCoinsViewDB base{{.path = m_args.GetDataDirBase() / "coins_db_leveldb_layout", .cache_bytes = 1_MiB, .wipe_data = true}, {}};
CCoinsViewCache cache{&base};
cache.EmplaceCoinInternalDANGER(COutPoint{outpoint}, Coin{coin});
cache.SetBestBlock(block_hash);
cache.Sync();
BOOST_CHECK_EQUAL(level2_files(base), 0);
WITH_LOCK(::cs_main, return base.CompactFull()).wait();
BOOST_CHECK_EQUAL(level2_files(base), 1);
BOOST_CHECK(*Assert(base.GetCoin(outpoint)) == coin);
BOOST_CHECK_EQUAL(base.GetBestBlock(), block_hash);
}
BOOST_AUTO_TEST_CASE(coins_resource_is_used)
{
CCoinsMapMemoryResource resource;

View File

@@ -173,13 +173,19 @@ FUZZ_TARGET(connman, .init = initialize_connman)
conn_type = ConnectionType::OUTBOUND_FULL_RELAY;
}
std::optional<Proxy> proxy_override;
if (conn_type == ConnectionType::PRIVATE_BROADCAST || fuzzed_data_provider.ConsumeBool()) {
proxy_override.emplace(ConsumeService(fuzzed_data_provider));
}
connman.OpenNetworkConnection(
/*addrConnect=*/random_address,
/*fCountFailure=*/fuzzed_data_provider.ConsumeBool(),
/*grant_outbound=*/{},
/*pszDest=*/fuzzed_data_provider.ConsumeBool() ? nullptr : random_string.c_str(),
/*conn_type=*/conn_type,
/*use_v2transport=*/fuzzed_data_provider.ConsumeBool());
/*use_v2transport=*/fuzzed_data_provider.ConsumeBool(),
/*proxy_override=*/proxy_override);
},
[&] {
connman.SetNetworkActive(fuzzed_data_provider.ConsumeBool());

View File

@@ -101,7 +101,7 @@ BOOST_AUTO_TEST_CASE(ReadWrite)
// Check invalid json not allowed
WriteText(path, R"(invalid json)");
BOOST_CHECK(!common::ReadSettings(path, values, errors));
std::vector<std::string> fail_parse = {strprintf("Settings file %s does not contain valid JSON. This is probably caused by disk corruption or a crash, "
std::vector<std::string> fail_parse = {strprintf("Settings file %s does not contain valid JSON. This may be caused by a crash, power loss, full disk, or storage error, "
"and can be fixed by removing the file, which will reset settings to default values.",
fs::PathToString(path))};
BOOST_CHECK_EQUAL_COLLECTIONS(errors.begin(), errors.end(), fail_parse.begin(), fail_parse.end());

View File

@@ -13,10 +13,14 @@
#include <serialize.h>
#include <uint256.h>
#include <util/log.h>
#include <util/threadnames.h>
#include <util/vector.h>
#include <cassert>
#include <chrono>
#include <cstdlib>
#include <exception>
#include <future>
#include <iterator>
#include <utility>
@@ -55,11 +59,22 @@ CCoinsViewDB::CCoinsViewDB(DBParams db_params, CoinsViewOptions options) :
m_options{std::move(options)},
m_db{std::make_unique<CDBWrapper>(m_db_params)} { }
CCoinsViewDB::~CCoinsViewDB()
{
if (m_compaction.valid()) {
if (m_compaction.wait_for(std::chrono::seconds{0}) != std::future_status::ready) {
LogInfo("Waiting for background chainstate compaction of %s", fs::PathToString(m_db_params.path));
}
m_compaction.wait();
}
}
void CCoinsViewDB::ResizeCache(size_t new_cache_size)
{
// We can't do this operation with an in-memory DB since we'll lose all the coins upon
// reset.
if (!m_db_params.memory_only) {
LOCK(m_db_mutex);
// Have to do a reset first to get the original `m_db` state to release its
// filesystem lock.
m_db.reset();
@@ -168,6 +183,30 @@ size_t CCoinsViewDB::EstimateSize() const
return m_db->EstimateSize(DB_COIN, uint8_t(DB_COIN + 1));
}
std::optional<std::string> CCoinsViewDB::GetDBProperty(const std::string& property)
{
return m_db->GetProperty(property);
}
std::shared_future<void> CCoinsViewDB::CompactFull()
{
AssertLockHeld(::cs_main);
if (m_compaction.valid() && m_compaction.wait_for(std::chrono::seconds{0}) != std::future_status::ready) return m_compaction;
m_compaction = std::async(std::launch::async, [this] {
try {
util::ThreadRename("utxocompact");
LOCK(m_db_mutex);
LogDebug(BCLog::COINDB, "Starting chainstate compaction of %s", fs::PathToString(m_db_params.path));
m_db->CompactFull();
LogDebug(BCLog::COINDB, "Finished chainstate compaction of %s", fs::PathToString(m_db_params.path));
} catch (const std::exception& e) {
LogWarning("Failed chainstate compaction (%s)", e.what());
}
}).share();
return m_compaction;
}
/** Specialization of CCoinsViewCursor to iterate over a CCoinsViewDB */
class CCoinsViewDBCursor: public CCoinsViewCursor
{

View File

@@ -15,8 +15,10 @@
#include <cstddef>
#include <cstdint>
#include <future>
#include <memory>
#include <optional>
#include <string>
#include <vector>
class COutPoint;
@@ -36,9 +38,13 @@ class CCoinsViewDB final : public CCoinsView
protected:
DBParams m_db_params;
CoinsViewOptions m_options;
//! Prevents CompactFull() from using m_db while ResizeCache() replaces it.
Mutex m_db_mutex;
std::unique_ptr<CDBWrapper> m_db;
std::shared_future<void> m_compaction;
public:
explicit CCoinsViewDB(DBParams db_params, CoinsViewOptions options);
~CCoinsViewDB() override;
std::optional<Coin> GetCoin(const COutPoint& outpoint) const override;
bool HaveCoin(const COutPoint &outpoint) const override;
@@ -52,7 +58,13 @@ public:
size_t EstimateSize() const override;
//! Dynamically alter the underlying leveldb cache size.
void ResizeCache(size_t new_cache_size) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
void ResizeCache(size_t new_cache_size) EXCLUSIVE_LOCKS_REQUIRED(cs_main, !m_db_mutex);
//! Perform a full compaction of the underlying LevelDB on a one-shot background thread.
std::shared_future<void> CompactFull() EXCLUSIVE_LOCKS_REQUIRED(cs_main, !m_db_mutex);
//! Return an underlying LevelDB property value, if available.
std::optional<std::string> GetDBProperty(const std::string& property);
};
#endif // BITCOIN_TXDB_H

View File

@@ -112,6 +112,13 @@ const std::vector<std::string> CHECKLEVEL_DOC {
* */
static constexpr int PRUNE_LOCK_BUFFER{10};
// Return whether the completed full flush should compact chainstate
static bool ShouldCompactChainstate(bool in_ibd)
{
static constexpr uint32_t flush_ratio{320}; // Roughly every 2 weeks with hourly flushes
return !in_ibd && FastRandomContext().randrange(flush_ratio) == 0;
}
TRACEPOINT_SEMAPHORE(validation, block_connected);
TRACEPOINT_SEMAPHORE(utxocache, flush);
TRACEPOINT_SEMAPHORE(mempool, replaced);
@@ -2824,9 +2831,19 @@ bool Chainstate::FlushStateToDisk(
m_next_write = FastRandomContext().rand_uniform_delay(NodeClock::now() + DATABASE_WRITE_INTERVAL_MIN, range);
}
}
if (full_flush_completed && m_chainman.m_options.signals) {
// Update best block in wallet (so we can detect restored wallets).
m_chainman.m_options.signals->ChainStateFlushed(this->GetRole(), GetLocator(m_chain.Tip()));
if (full_flush_completed) {
if (m_chainman.m_options.signals) {
// Update best block in wallet (so we can detect restored wallets).
m_chainman.m_options.signals->ChainStateFlushed(this->GetRole(), GetLocator(m_chain.Tip()));
}
if (!m_chainman.m_interrupt && ShouldCompactChainstate(m_chainman.IsInitialBlockDownload())) {
try {
CoinsDB().CompactFull();
} catch (const std::exception& e) {
LogWarning("Failed to start chainstate compaction (%s)", e.what());
}
}
}
} catch (const std::runtime_error& e) {
return FatalError(m_chainman.GetNotifications(), state, strprintf(_("System error while flushing: %s"), e.what()));

View File

@@ -94,7 +94,7 @@ int CalculateMaximumSignedInputSize(const CTxOut& txout, const COutPoint outpoin
if (!provider) return -1;
if (const auto desc = InferDescriptor(txout.scriptPubKey, *provider)) {
if (const auto weight = MaxInputWeight(*desc, {}, coin_control, true, can_grind_r)) {
if (const auto weight = MaxInputWeight(*desc, CTxIn{outpoint}, coin_control, true, can_grind_r)) {
return static_cast<int>(GetVirtualTransactionSize(*weight, 0, 0));
}
}

View File

@@ -3,6 +3,7 @@
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <consensus/amount.h>
#include <key.h>
#include <policy/fees/block_policy_estimator.h>
#include <script/solver.h>
#include <validation.h>
@@ -16,6 +17,22 @@
namespace wallet {
BOOST_FIXTURE_TEST_SUITE(spend_tests, WalletTestingSetup)
BOOST_AUTO_TEST_CASE(max_signed_input_size_uses_external_outpoint)
{
const CKey key{GenerateRandomKey()};
FillableSigningProvider provider;
BOOST_REQUIRE(provider.AddKey(key));
const CTxOut txout{COIN, GetScriptForDestination(PKHash{key.GetPubKey()})};
const COutPoint outpoint{Txid{}, 0};
CCoinControl coin_control;
coin_control.Select(outpoint).SetTxOut(txout);
const int low_r{CalculateMaximumSignedInputSize(txout, COutPoint{}, &provider, /*can_grind_r=*/true, &coin_control)};
const int high_r{CalculateMaximumSignedInputSize(txout, outpoint, &provider, /*can_grind_r=*/true, &coin_control)};
BOOST_CHECK_EQUAL(high_r, low_r + 1);
}
BOOST_FIXTURE_TEST_CASE(SubtractFee, TestChain100Setup)
{
CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));

View File

@@ -73,7 +73,7 @@ class SettingsTest(BitcoinTestFramework):
# Test invalid json
with settings.open("w") as fp:
fp.write("invalid json")
node.assert_start_raises_init_error(expected_msg='does not contain valid JSON. This is probably caused by disk corruption or a crash', match=ErrorMatch.PARTIAL_REGEX)
node.assert_start_raises_init_error(expected_msg='does not contain valid JSON. This may be caused by a crash, power loss, full disk, or storage error', match=ErrorMatch.PARTIAL_REGEX)
# Test invalid json object
with settings.open("w") as fp:

View File

@@ -15,6 +15,7 @@ from test_framework.p2p import (
P2PInterface,
P2P_SERVICES,
P2P_VERSION,
start_p2p_listener,
)
from test_framework.messages import (
CAddress,
@@ -29,8 +30,7 @@ from test_framework.netutil import (
)
from test_framework.script_util import build_malleated_tx_package
from test_framework.socks5 import (
Socks5Configuration,
Socks5Server,
start_socks5_server,
)
from test_framework.test_framework import (
BitcoinTestFramework,
@@ -40,7 +40,6 @@ from test_framework.util import (
assert_greater_than_or_equal,
assert_not_equal,
assert_raises_rpc_error,
p2p_port,
tor_port,
)
from test_framework.wallet import (
@@ -49,116 +48,6 @@ from test_framework.wallet import (
NUM_PRIVATE_BROADCAST_PER_TX = 3
# Fill addrman with these addresses. Must have enough Tor addresses, so that even
# if all 10 default connections are opened to a Tor address (!?) there must be more
# for private broadcast.
ADDRMAN_ADDRESSES = [
"20.0.0.1",
"30.0.0.1",
"40.0.0.1",
"50.0.0.1",
"60.0.0.1",
"70.0.0.1",
"80.0.0.1",
"90.0.0.1",
"100.0.0.1",
"110.0.0.1",
"120.0.0.1",
"130.0.0.1",
"140.0.0.1",
"150.0.0.1",
"160.0.0.1",
"170.0.0.1",
"180.0.0.1",
"190.0.0.1",
"200.0.0.1",
"210.0.0.1",
"[20::1]",
"[30::1]",
"[40::1]",
"[50::1]",
"[60::1]",
"[70::1]",
"[80::1]",
"[90::1]",
"[100::1]",
"[110::1]",
"[120::1]",
"[130::1]",
"[140::1]",
"[150::1]",
"[160::1]",
"[170::1]",
"[180::1]",
"[190::1]",
"[200::1]",
"[210::1]",
"testonlyad777777777777777777777777777777777777777775b6qd.onion",
"testonlyah77777777777777777777777777777777777777777z7ayd.onion",
"testonlyal77777777777777777777777777777777777777777vp6qd.onion",
"testonlyap77777777777777777777777777777777777777777r5qad.onion",
"testonlyat77777777777777777777777777777777777777777udsid.onion",
"testonlyax77777777777777777777777777777777777777777yciid.onion",
"testonlya777777777777777777777777777777777777777777rhgyd.onion",
"testonlybd77777777777777777777777777777777777777777rs4ad.onion",
"testonlybp77777777777777777777777777777777777777777zs2ad.onion",
"testonlybt777777777777777777777777777777777777777777x6id.onion",
"testonlybx777777777777777777777777777777777777777775styd.onion",
"testonlyb3777777777777777777777777777777777777777774ckid.onion",
"testonlycd77777777777777777777777777777777777777777733id.onion",
"testonlych77777777777777777777777777777777777777777t6kid.onion",
"testonlycl77777777777777777777777777777777777777777tt3ad.onion",
"testonlyct77777777777777777777777777777777777777777wvhyd.onion",
"testonlycx7777777777777777777777777777777777777777774bad.onion",
"testonlyc377777777777777777777777777777777777777777u6aid.onion",
"testonlydd777777777777777777777777777777777777777777u5ad.onion",
"testonlydh77777777777777777777777777777777777777777wgnyd.onion",
"testonlyad77777777777777777777777777777777777777777q.b32.i2p",
"testonlyah77777777777777777777777777777777777777777q.b32.i2p",
"testonlyap77777777777777777777777777777777777777777q.b32.i2p",
"testonlyat77777777777777777777777777777777777777777q.b32.i2p",
"testonlyax77777777777777777777777777777777777777777q.b32.i2p",
"testonlya377777777777777777777777777777777777777777q.b32.i2p",
"testonlya777777777777777777777777777777777777777777q.b32.i2p",
"testonlybd77777777777777777777777777777777777777777q.b32.i2p",
"testonlybh77777777777777777777777777777777777777777q.b32.i2p",
"testonlybl77777777777777777777777777777777777777777q.b32.i2p",
"testonlybp77777777777777777777777777777777777777777q.b32.i2p",
"testonlybt77777777777777777777777777777777777777777q.b32.i2p",
"testonlybx77777777777777777777777777777777777777777q.b32.i2p",
"testonlyb777777777777777777777777777777777777777777q.b32.i2p",
"testonlych77777777777777777777777777777777777777777q.b32.i2p",
"testonlycp77777777777777777777777777777777777777777q.b32.i2p",
"testonlyct77777777777777777777777777777777777777777q.b32.i2p",
"testonlycx77777777777777777777777777777777777777777q.b32.i2p",
"testonlyc377777777777777777777777777777777777777777q.b32.i2p",
"testonlyc777777777777777777777777777777777777777777q.b32.i2p",
"[fc00::1]",
"[fc00::2]",
"[fc00::3]",
"[fc00::5]",
"[fc00::6]",
"[fc00::7]",
"[fc00::8]",
"[fc00::9]",
"[fc00::10]",
"[fc00::11]",
"[fc00::12]",
"[fc00::13]",
"[fc00::15]",
"[fc00::16]",
"[fc00::17]",
"[fc00::18]",
"[fc00::19]",
"[fc00::20]",
"[fc00::22]",
"[fc00::23]",
]
class P2PPrivateBroadcast(BitcoinTestFramework):
def set_test_params(self):
@@ -166,18 +55,6 @@ class P2PPrivateBroadcast(BitcoinTestFramework):
self.num_nodes = 2
def setup_nodes(self):
# Start a SOCKS5 proxy server.
socks5_server_config = Socks5Configuration()
# self.nodes[0] listens on p2p_port(0),
# self.nodes[1] listens on p2p_port(1),
# thus we tell the SOCKS5 server to listen on p2p_port(self.num_nodes) (self.num_nodes is 2)
socks5_server_config.addr = ("127.0.0.1", p2p_port(self.num_nodes))
socks5_server_config.unauth = True
socks5_server_config.auth = True
self.socks5_server = Socks5Server(socks5_server_config)
self.socks5_server.start()
self.destinations = []
self.destinations_lock = threading.Lock()
@@ -235,22 +112,7 @@ class P2PPrivateBroadcast(BitcoinTestFramework):
listener.peer_connect_helper(dstaddr="0.0.0.0", dstport=0, net=self.chain, timeout_factor=self.options.timeout_factor)
listener.peer_connect_send_version(services=P2P_SERVICES)
def on_listen_done(addr, port):
nonlocal actual_to_addr
nonlocal actual_to_port
actual_to_addr = addr
actual_to_port = port
# Use port=0 to let the OS assign an available port. This
# avoids "address already in use" errors when tests run
# concurrently or ports are still in TIME_WAIT state.
self.network_thread.listen(
addr="127.0.0.1",
port=0,
p2p=listener,
callback=on_listen_done)
# Wait until the callback has been called.
self.wait_until(lambda: actual_to_port != 0)
actual_to_addr, actual_to_port = start_p2p_listener(self.network_thread, listener)
self.log.debug(f"Instructing the SOCKS5 proxy to redirect connection i={i} ({conn_type}) for "
f"{format_addr_port(requested_to_addr, requested_to_port)} to "
@@ -268,7 +130,7 @@ class P2PPrivateBroadcast(BitcoinTestFramework):
"actual_to_port": actual_to_port,
}
self.socks5_server.conf.destinations_factory = destinations_factory
self.socks5_server = start_socks5_server(destinations_factory)
self.extra_args = [
[
@@ -279,7 +141,7 @@ class P2PPrivateBroadcast(BitcoinTestFramework):
"-v2transport=0",
"-test=addrman",
"-privatebroadcast",
f"-proxy={socks5_server_config.addr[0]}:{socks5_server_config.addr[1]}",
f"-proxy={self.socks5_server.conf.addr[0]}:{self.socks5_server.conf.addr[1]}",
# To increase coverage, make it think that the I2P network is reachable so that it
# selects such addresses as well. Pick a proxy address where nobody is listening
# and connection attempts fail quickly.
@@ -361,13 +223,9 @@ class P2PPrivateBroadcast(BitcoinTestFramework):
tx_receiver = self.nodes[1]
far_observer = tx_receiver.add_p2p_connection(P2PInterface())
wallet = MiniWallet(tx_originator)
self.fill_node_addrman(node_index=0, address_types_to_add=[CAddress.NET_IPV4, CAddress.NET_IPV6, CAddress.NET_TORV3, CAddress.NET_I2P, CAddress.NET_CJDNS])
# Fill tx_originator's addrman.
for addr in ADDRMAN_ADDRESSES:
res = tx_originator.addpeeraddress(address=addr, port=0 if addr.endswith(".i2p") else 8333, tried=False)
if not res["success"]:
self.log.debug(f"Could not add {addr} to tx_originator's addrman (collision?)")
wallet = MiniWallet(tx_originator)
txs = wallet.create_self_transfer_chain(chain_length=3)
self.log.info(f"Created txid={txs[0]['txid']}: for basic test")

View File

@@ -0,0 +1,199 @@
#!/usr/bin/env python3
# Copyright (c) 2026-present The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""
Ensure that when v2 private broadcast connection to IPv4 fails the v1 retry
will also be made through the Tor proxy.
The test does:
* Add a bunch of IPv4 addresses to the node's addrman (they will be added without P2P_V2 flag).
* Get them to report P2P_V2 in their service flags and connect to each one, so that the flags
in addrman are updated to contain P2P_V2.
* Get one successful connection to a Tor peer (.onion) so that bitcoind assumes the configured
Tor proxy works and is indeed a proxy to the Tor network. This will make it open private
broadcast connections also to IPv4 addresses via that proxy.
* Start some private broadcast connections.
* Remember the destination IPv4 address of the first connection and get it to fail the v2
transport.
* Wait for a subsequent connection also through the Tor proxy to the same IPv4 and expect
it to be v1, i.e. the v2->v1 downgrade retry.
"""
from test_framework.netutil import (
format_addr_port
)
from test_framework.p2p import (
P2PConnection,
P2PInterface,
P2P_SERVICES,
start_p2p_listener,
)
from test_framework.messages import (
CAddress,
NODE_P2P_V2,
)
from test_framework.socks5 import (
start_socks5_server,
)
from test_framework.test_framework import (
BitcoinTestFramework,
)
from test_framework.v2_p2p import (
EncryptedP2PState,
)
from test_framework.wallet import (
MiniWallet,
)
class P2PDetermineV2or1AndClose(P2PConnection):
def __init__(self, on_v2or1_determined):
super().__init__()
self.on_v2or1_determined = on_v2or1_determined
# https://docs.python.org/3/library/asyncio-protocol.html#asyncio.Protocol.data_received
def data_received(self, data):
self.recvbuf += data
if len(self.recvbuf) >= 4:
self.on_v2or1_determined(1 if self.recvbuf[0:4] == self.magic_bytes else 2)
self.peer_disconnect()
def on_open(self):
pass
def on_close(self):
pass
class P2PPrivateBroadcastRetryV1(BitcoinTestFramework):
def set_test_params(self):
self.disable_autoconnect = False
self.num_nodes = 1
def ipv4_via_tor_proxy_conn_versions_append(self, v2or1):
"""
Add to the transport versions (v2 or v1) tried towards the first IPv4 which
nodes[0] tries to connect to via the Tor proxy.
"""
self.ipv4_via_tor_proxy_conn_versions.append(v2or1)
def setup_nodes(self):
def destinations_factory_all_proxy(requested_to_addr, requested_to_port):
"""
Instruct the SOCKS5 proxy to redirect all connections to newly created P2PInterface
objects that claim support for P2P_V2.
"""
listener = P2PInterface()
listener.peer_connect_helper(dstaddr="0.0.0.0", dstport=0, net=self.chain, timeout_factor=self.options.timeout_factor)
listener.peer_connect_send_version(services=P2P_SERVICES | NODE_P2P_V2)
actual_to_addr, actual_to_port = start_p2p_listener(self.network_thread, listener)
self.log.debug("Instructing the common proxy to redirect connection for "
f"{format_addr_port(requested_to_addr, requested_to_port)} to "
f"{format_addr_port(actual_to_addr, actual_to_port)} (Python {type(listener).__name__})")
return {
"actual_to_addr": actual_to_addr,
"actual_to_port": actual_to_port,
}
self.all_proxy = start_socks5_server(destinations_factory_all_proxy)
self.ipv4_via_tor_proxy_addr_port = None # Remember the first IPv4 address connected to via the Tor proxy.
self.ipv4_via_tor_proxy_conn_versions = [] # Transport versions tried on that address.
def destinations_factory_tor_proxy(requested_to_addr, requested_to_port):
"""
Instruct the SOCKS5 proxy to redirect all connections to newly created P2PInterface,
except the first connection to an IPv4 address and all subsequent connections to that
address which are redirected to P2PDetermineV2or1AndClose.
"""
requested_to = format_addr_port(requested_to_addr, requested_to_port)
if not requested_to_addr.endswith(".onion") and self.ipv4_via_tor_proxy_addr_port is None: # First IPv4
self.ipv4_via_tor_proxy_addr_port = requested_to
if self.ipv4_via_tor_proxy_addr_port == requested_to:
# This is either the first (v2) or the second (the expected v1 retry) connection to requested_to.
listener = P2PDetermineV2or1AndClose(self.ipv4_via_tor_proxy_conn_versions_append)
listener.peer_connect_helper(dstaddr="0.0.0.0", dstport=0, net=self.chain, timeout_factor=self.options.timeout_factor)
else:
listener = P2PInterface()
listener.peer_connect_helper(dstaddr="0.0.0.0", dstport=0, net=self.chain, timeout_factor=self.options.timeout_factor)
listener.peer_connect_send_version(services=P2P_SERVICES | NODE_P2P_V2)
if not requested_to_addr.endswith(".onion"):
listener.v2_state = EncryptedP2PState(initiating=False, net=self.chain)
actual_to_addr, actual_to_port = start_p2p_listener(self.network_thread, listener)
self.log.debug(f"Instructing the Tor proxy to redirect connection for {requested_to} to "
f"{format_addr_port(actual_to_addr, actual_to_port)} (Python {type(listener).__name__})")
return {
"actual_to_addr": actual_to_addr,
"actual_to_port": actual_to_port,
}
self.tor_proxy = start_socks5_server(destinations_factory_tor_proxy)
self.extra_args = [
[
"-privatebroadcast=1",
f"-proxy={self.all_proxy.conf.addr[0]}:{self.all_proxy.conf.addr[1]}",
f"-onion={self.tor_proxy.conf.addr[0]}:{self.tor_proxy.conf.addr[1]}",
"-test=addrman",
"-v2transport=0",
],
]
super().setup_nodes()
def setup_network(self):
self.setup_nodes()
def run_test(self):
node0 = self.nodes[0]
self.log.info("Filling node0's addrman with addresses")
self.fill_node_addrman(node_index=0, address_types_to_add=[CAddress.NET_IPV4])
self.log.info("Opening manual connections to all IPv4 addresses to add P2P_V2 flag to addrman entries")
for a in node0.getnodeaddresses(count=0, network="ipv4"):
node0.addnode(node=format_addr_port(a["address"], a["port"]), command="onetry", v2transport=False)
self.log.info("Waiting for all IPv4 addresses to get P2P_V2 as a result of peers advertising support")
self.wait_until(lambda: all(a["services"] & NODE_P2P_V2 != 0 for a in node0.getnodeaddresses(count=0, network="ipv4")))
# The destinations behind the -proxy= don't actually support v2. When bitcoind runs with -v2transport=1
# and tries v2 on them they would print benign "magic byte mismatch" warnings.
# Disable those since none of them are needed anymore.
self.all_proxy.conf.destinations_factory = None
self.restart_node(0, extra_args=self.extra_args[0] + ["-v2transport=1"])
self.log.info("Opening a connection to a Tor addresses, so bitcoind considers -onion= is a real Tor proxy")
node0.addnode(node="testonlyad777777777777777777777777777777777777777775b6qd.onion:1234", command="onetry", v2transport=False)
self.log.info("Waiting for at least one Tor connection")
self.wait_until(lambda: any(p["network"] == "onion" for p in node0.getpeerinfo()))
self.log.info("Starting private broadcast connections")
wallet = MiniWallet(node0)
tx = wallet.create_self_transfer()
node0.sendrawtransaction(hexstring=tx["hex"])
self.log.info("Tor proxy: waiting for connection to an IPv4 address")
self.wait_until(lambda: self.ipv4_via_tor_proxy_addr_port is not None)
self.log.info(f"Tor proxy: got {self.ipv4_via_tor_proxy_addr_port}, waiting for v2")
self.wait_until(lambda: 2 in self.ipv4_via_tor_proxy_conn_versions)
self.log.info(f"Tor proxy: got {self.ipv4_via_tor_proxy_addr_port} v2, waiting for v1")
self.wait_until(lambda: 1 in self.ipv4_via_tor_proxy_conn_versions)
self.log.info(f"Tor proxy: got {self.ipv4_via_tor_proxy_addr_port} v2, v1")
self.stop_node(0)
self.all_proxy.stop()
self.tor_proxy.stop()
if __name__ == "__main__":
P2PPrivateBroadcastRetryV1(__file__).main()

View File

@@ -65,7 +65,7 @@ class PSBTTest(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 3
self.extra_args = [
["-walletrbf=1", "-addresstype=bech32", "-changetype=bech32"], #TODO: Remove address type restrictions once taproot has psbt extensions
["-walletrbf=1"],
["-walletrbf=0", "-changetype=legacy"],
[]
]

View File

@@ -973,3 +973,27 @@ class P2PTxInvStore(P2PInterface):
self.wait_until(lambda: set(self.tx_invs_received.keys()) == set([int(tx, 16) for tx in txns]), timeout=timeout)
# Flush messages and wait for the getdatas to be processed
self.sync_with_ping()
def start_p2p_listener(network_thread, listener):
listen_addr = ""
listen_port = 0
def on_listen_done(addr, port):
nonlocal listen_addr
nonlocal listen_port
listen_addr = addr
listen_port = port
# Use port=0 to let the OS assign an available port. This
# avoids "address already in use" errors when tests run
# concurrently or ports are still in TIME_WAIT state.
network_thread.listen(
addr="127.0.0.1",
port=0,
p2p=listener,
callback=on_listen_done)
# Wait until the callback has been called.
wait_until_helper_internal(lambda: listen_port != 0)
return listen_addr, listen_port

View File

@@ -327,3 +327,15 @@ class Socks5Server():
logger.debug(f"Stop(): Handler {i} thread joined")
else:
logger.warning(f"Stop(): Handler thread {i} didn't finish after force close")
def start_socks5_server(destinations_factory):
config = Socks5Configuration()
config.addr = ("127.0.0.1", 0) # Use port=0 to let the OS pick one. The actual port is later in server.conf.addr[1].
config.unauth = True
config.auth = True
config.destinations_factory = destinations_factory
server = Socks5Server(config)
server.start()
return server

View File

@@ -23,6 +23,7 @@ import time
from .address import create_deterministic_address_bcrt1_p2tr_op_true
from .authproxy import JSONRPCException
from . import coverage
from .messages import CAddress
from .p2p import NetworkThread
from .test_node import TestNode
from .util import (
@@ -728,6 +729,125 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
def wait_until(self, test_function, timeout=60, check_interval=0.05):
return wait_until_helper_internal(test_function, timeout=timeout, timeout_factor=self.options.timeout_factor, check_interval=check_interval)
def fill_node_addrman(self, *, node_index, address_types_to_add):
ADDRESSES = {
CAddress.NET_IPV4: [
"20.0.0.1",
"30.0.0.1",
"40.0.0.1",
"50.0.0.1",
"60.0.0.1",
"70.0.0.1",
"80.0.0.1",
"90.0.0.1",
"100.0.0.1",
"110.0.0.1",
"120.0.0.1",
"130.0.0.1",
"140.0.0.1",
"150.0.0.1",
"160.0.0.1",
"170.0.0.1",
"180.0.0.1",
"190.0.0.1",
"200.0.0.1",
"210.0.0.1",
],
CAddress.NET_IPV6: [
"[20::1]",
"[30::1]",
"[40::1]",
"[50::1]",
"[60::1]",
"[70::1]",
"[80::1]",
"[90::1]",
"[100::1]",
"[110::1]",
"[120::1]",
"[130::1]",
"[140::1]",
"[150::1]",
"[160::1]",
"[170::1]",
"[180::1]",
"[190::1]",
"[200::1]",
"[210::1]",
],
CAddress.NET_TORV3: [
"testonlyad777777777777777777777777777777777777777775b6qd.onion",
"testonlyah77777777777777777777777777777777777777777z7ayd.onion",
"testonlyal77777777777777777777777777777777777777777vp6qd.onion",
"testonlyap77777777777777777777777777777777777777777r5qad.onion",
"testonlyat77777777777777777777777777777777777777777udsid.onion",
"testonlyax77777777777777777777777777777777777777777yciid.onion",
"testonlya777777777777777777777777777777777777777777rhgyd.onion",
"testonlybd77777777777777777777777777777777777777777rs4ad.onion",
"testonlybp77777777777777777777777777777777777777777zs2ad.onion",
"testonlybt777777777777777777777777777777777777777777x6id.onion",
"testonlybx777777777777777777777777777777777777777775styd.onion",
"testonlyb3777777777777777777777777777777777777777774ckid.onion",
"testonlycd77777777777777777777777777777777777777777733id.onion",
"testonlych77777777777777777777777777777777777777777t6kid.onion",
"testonlycl77777777777777777777777777777777777777777tt3ad.onion",
"testonlyct77777777777777777777777777777777777777777wvhyd.onion",
"testonlycx7777777777777777777777777777777777777777774bad.onion",
"testonlyc377777777777777777777777777777777777777777u6aid.onion",
"testonlydd777777777777777777777777777777777777777777u5ad.onion",
"testonlydh77777777777777777777777777777777777777777wgnyd.onion",
],
CAddress.NET_I2P: [
"testonlyad77777777777777777777777777777777777777777q.b32.i2p",
"testonlyah77777777777777777777777777777777777777777q.b32.i2p",
"testonlyap77777777777777777777777777777777777777777q.b32.i2p",
"testonlyat77777777777777777777777777777777777777777q.b32.i2p",
"testonlyax77777777777777777777777777777777777777777q.b32.i2p",
"testonlya377777777777777777777777777777777777777777q.b32.i2p",
"testonlya777777777777777777777777777777777777777777q.b32.i2p",
"testonlybd77777777777777777777777777777777777777777q.b32.i2p",
"testonlybh77777777777777777777777777777777777777777q.b32.i2p",
"testonlybl77777777777777777777777777777777777777777q.b32.i2p",
"testonlybp77777777777777777777777777777777777777777q.b32.i2p",
"testonlybt77777777777777777777777777777777777777777q.b32.i2p",
"testonlybx77777777777777777777777777777777777777777q.b32.i2p",
"testonlyb777777777777777777777777777777777777777777q.b32.i2p",
"testonlych77777777777777777777777777777777777777777q.b32.i2p",
"testonlycp77777777777777777777777777777777777777777q.b32.i2p",
"testonlyct77777777777777777777777777777777777777777q.b32.i2p",
"testonlycx77777777777777777777777777777777777777777q.b32.i2p",
"testonlyc377777777777777777777777777777777777777777q.b32.i2p",
"testonlyc777777777777777777777777777777777777777777q.b32.i2p",
],
CAddress.NET_CJDNS: [
"[fc00::1]",
"[fc00::2]",
"[fc00::3]",
"[fc00::5]",
"[fc00::6]",
"[fc00::7]",
"[fc00::8]",
"[fc00::9]",
"[fc00::10]",
"[fc00::11]",
"[fc00::12]",
"[fc00::13]",
"[fc00::15]",
"[fc00::16]",
"[fc00::17]",
"[fc00::18]",
"[fc00::19]",
"[fc00::20]",
"[fc00::22]",
"[fc00::23]",
],
}
for addr_type in address_types_to_add:
for addr in ADDRESSES[addr_type]:
res = self.nodes[node_index].addpeeraddress(address=addr, port=0 if addr.endswith(".i2p") else 8333, tried=False)
if not res["success"]:
self.log.debug(f"Could not add {addr} to nodes[{node_index}]'s addrman (collision?)")
# Private helper methods. These should not be accessed by the subclass test scripts.
def _start_logging(self):

View File

@@ -367,6 +367,7 @@ BASE_SCRIPTS = [
'p2p_permissions.py',
'feature_blocksdir.py',
'wallet_startup.py',
'p2p_private_broadcast_retry_v1.py',
'feature_remove_pruned_files_on_startup.py',
'p2p_i2p_ports.py',
'p2p_i2p_sessions.py',

View File

@@ -66,7 +66,7 @@ pub fn get_subtrees() -> Vec<&'static str> {
"src/crc32c",
"src/crypto/ctaes",
"src/ipc/libmultiprocess",
"src/leveldb",
//"src/leveldb", No longer a subtree in this release branch, due to direct cherry-picks
"src/minisketch",
"src/secp256k1",
]