mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-01-22 08:09:19 +01:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abb6ae2ec5 | ||
|
|
483d158f53 | ||
|
|
747a863f5b | ||
|
|
cc3cdbe921 | ||
|
|
c4082a45e6 | ||
|
|
185ca0e391 | ||
|
|
bc71372c0e | ||
|
|
bef4b1fdee | ||
|
|
ac940ac2ca | ||
|
|
8e5c02a77f | ||
|
|
ac4d0956cc | ||
|
|
454ac8e7db | ||
|
|
e9f73b8149 | ||
|
|
f22122bc27 | ||
|
|
7568bc3ab0 | ||
|
|
c065bcd2d7 | ||
|
|
6983c7d769 | ||
|
|
8769c718f4 | ||
|
|
ed0774bd08 | ||
|
|
f620dde411 | ||
|
|
b734c4026b | ||
|
|
7ea855fd55 | ||
|
|
dd47caee82 | ||
|
|
2a21824b11 | ||
|
|
57264431ff | ||
|
|
26294d627e | ||
|
|
b1499ddf8b | ||
|
|
d59ebac718 | ||
|
|
b83d4f7c57 | ||
|
|
b26c93a9df | ||
|
|
18f3ada037 | ||
|
|
187e3b89b5 | ||
|
|
ab58b2c0f8 | ||
|
|
d872277db5 | ||
|
|
2256f8965e | ||
|
|
5408e85145 | ||
|
|
338570de5c | ||
|
|
72675b8f55 | ||
|
|
b2cb203af0 | ||
|
|
39d53dd8bf | ||
|
|
577ddf6f5d | ||
|
|
a0a2b07701 | ||
|
|
e2c71c4fca | ||
|
|
c0d851e6c6 | ||
|
|
34576c4574 | ||
|
|
8f8c7cf7fa | ||
|
|
049bf100f1 | ||
|
|
3afd5a9729 | ||
|
|
23ac752d09 | ||
|
|
cd7937ce2d | ||
|
|
42d4847fdf | ||
|
|
96110f8846 | ||
|
|
729e4c2abd | ||
|
|
f8db6f6ce7 | ||
|
|
5e389959b9 | ||
|
|
c48d3a6ad8 | ||
|
|
73ed57a35e | ||
|
|
72d1141bd8 | ||
|
|
c9fa661131 | ||
|
|
a14e7b9dee | ||
|
|
ae63cc4bf2 | ||
|
|
abcd4c4ff9 |
23
.github/actions/configure-docker/action.yml
vendored
23
.github/actions/configure-docker/action.yml
vendored
@@ -4,12 +4,21 @@ inputs:
|
||||
cache-provider:
|
||||
description: 'gha or cirrus cache provider'
|
||||
required: true
|
||||
options:
|
||||
- gh
|
||||
- cirrus
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Check inputs
|
||||
shell: bash
|
||||
run: |
|
||||
# We expect only gha or cirrus as inputs to cache-provider
|
||||
case "${{ inputs.cache-provider }}" in
|
||||
gha|cirrus)
|
||||
;;
|
||||
*)
|
||||
echo "::warning title=Unknown input to configure docker action::Provided value was ${{ inputs.cache-provider }}"
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
@@ -22,8 +31,12 @@ runs:
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
core.exportVariable('ACTIONS_CACHE_URL', process.env['ACTIONS_CACHE_URL'])
|
||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env['ACTIONS_RUNTIME_TOKEN'])
|
||||
Object.keys(process.env).forEach(function (key) {
|
||||
if (key.startsWith('ACTIONS_')) {
|
||||
core.info(`Exporting ${key}`);
|
||||
core.exportVariable(key, process.env[key]);
|
||||
}
|
||||
});
|
||||
|
||||
- name: Construct docker build cache args
|
||||
shell: bash
|
||||
|
||||
@@ -17,7 +17,7 @@ runs:
|
||||
- name: Set cache hashes
|
||||
shell: bash
|
||||
run: |
|
||||
echo "DEPENDS_HASH=$(git ls-tree HEAD depends "ci/test/$FILE_ENV" | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV
|
||||
echo "DEPENDS_HASH=$(git ls-tree HEAD depends "$FILE_ENV" | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV
|
||||
echo "PREVIOUS_RELEASES_HASH=$(git ls-tree HEAD test/get_previous_releases.py | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV
|
||||
|
||||
- name: Get container name
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -536,7 +536,7 @@ jobs:
|
||||
lint:
|
||||
name: 'lint'
|
||||
needs: runners
|
||||
runs-on: ${{ needs.runners.outputs.use-cirrus-runners == 'true' && 'ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-xs' || 'ubuntu-24.04' }}
|
||||
runs-on: ${{ needs.runners.outputs.provider == 'cirrus' && 'ghcr.io/cirruslabs/ubuntu-runner-amd64:24.04-xs' || 'ubuntu-24.04' }}
|
||||
if: ${{ vars.SKIP_BRANCH_PUSH != 'true' || github.event_name == 'pull_request' }}
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
|
||||
@@ -28,11 +28,11 @@ get_directory_property(precious_variables CACHE_VARIABLES)
|
||||
#=============================
|
||||
set(CLIENT_NAME "Bitcoin Core")
|
||||
set(CLIENT_VERSION_MAJOR 30)
|
||||
set(CLIENT_VERSION_MINOR 0)
|
||||
set(CLIENT_VERSION_MINOR 2)
|
||||
set(CLIENT_VERSION_BUILD 0)
|
||||
set(CLIENT_VERSION_RC 0)
|
||||
set(CLIENT_VERSION_RC 1)
|
||||
set(CLIENT_VERSION_IS_RELEASE "true")
|
||||
set(COPYRIGHT_YEAR "2025")
|
||||
set(COPYRIGHT_YEAR "2026")
|
||||
|
||||
# During the enabling of the CXX and CXXOBJ languages, we modify
|
||||
# CMake's compiler/linker invocation strings by appending the content
|
||||
|
||||
4
COPYING
4
COPYING
@@ -1,7 +1,7 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2009-2025 The Bitcoin Core developers
|
||||
Copyright (c) 2009-2025 Bitcoin Developers
|
||||
Copyright (c) 2009-2026 The Bitcoin Core developers
|
||||
Copyright (c) 2009-2026 Bitcoin Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -8,7 +8,6 @@ export LC_ALL=C.UTF-8
|
||||
|
||||
export CONTAINER_NAME=ci_win64
|
||||
export CI_IMAGE_NAME_TAG="mirror.gcr.io/ubuntu:24.04" # Check that https://packages.ubuntu.com/noble/g++-mingw-w64-x86-64-posix (version 13.x, similar to guix) can cross-compile
|
||||
export CI_IMAGE_PLATFORM="linux/amd64"
|
||||
export HOST=x86_64-w64-mingw32
|
||||
export PACKAGES="g++-mingw-w64-x86-64-posix nsis"
|
||||
export RUN_UNIT_TESTS=false
|
||||
|
||||
@@ -89,7 +89,7 @@ mkdir -p "${DEPENDS_DIR}/SDKs" "${DEPENDS_DIR}/sdk-sources"
|
||||
OSX_SDK_BASENAME="Xcode-${XCODE_VERSION}-${XCODE_BUILD_ID}-extracted-SDK-with-libcxx-headers"
|
||||
|
||||
if [ -n "$XCODE_VERSION" ] && [ ! -d "${DEPENDS_DIR}/SDKs/${OSX_SDK_BASENAME}" ]; then
|
||||
OSX_SDK_FILENAME="${OSX_SDK_BASENAME}.tar.gz"
|
||||
OSX_SDK_FILENAME="${OSX_SDK_BASENAME}.tar"
|
||||
OSX_SDK_PATH="${DEPENDS_DIR}/sdk-sources/${OSX_SDK_FILENAME}"
|
||||
if [ ! -f "$OSX_SDK_PATH" ]; then
|
||||
${CI_RETRY_EXE} curl --location --fail "${SDK_URL}/${OSX_SDK_FILENAME}" -o "$OSX_SDK_PATH"
|
||||
|
||||
@@ -29,7 +29,7 @@ function(add_boost_if_needed)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
find_package(Boost 1.73.0 REQUIRED CONFIG)
|
||||
find_package(Boost 1.74.0 REQUIRED CONFIG)
|
||||
mark_as_advanced(Boost_INCLUDE_DIR boost_headers_DIR)
|
||||
# Workaround for a bug in NetBSD pkgsrc.
|
||||
# See: https://github.com/NetBSD/pkgsrc/issues/167.
|
||||
|
||||
@@ -5,7 +5,7 @@ Upstream-Contact: Satoshi Nakamoto <satoshin@gmx.com>
|
||||
Source: https://github.com/bitcoin/bitcoin
|
||||
|
||||
Files: *
|
||||
Copyright: 2009-2025, Bitcoin Core Developers
|
||||
Copyright: 2009-2026, Bitcoin Core Developers
|
||||
License: Expat
|
||||
Comment: The Bitcoin Core Developers encompasses all contributors to the
|
||||
project, listed in the release notes or the git log.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -59,10 +60,11 @@ for relpath in BINARIES:
|
||||
print(f'{abspath} not found or not an executable', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
# take first line (which must contain version)
|
||||
verstr = r.stdout.splitlines()[0]
|
||||
# last word of line is the actual version e.g. v22.99.0-5c6b3d5b3508
|
||||
verstr = verstr.split()[-1]
|
||||
assert verstr.startswith('v')
|
||||
output = r.stdout.splitlines()[0]
|
||||
# find the version e.g. v30.99.0-ce771726f3e7
|
||||
search = re.search(r"v[0-9]\S+", output)
|
||||
assert search
|
||||
verstr = search.group(0)
|
||||
# remaining lines are copyright
|
||||
copyright = r.stdout.split('\n')[1:]
|
||||
assert copyright[0].startswith('Copyright (C)')
|
||||
|
||||
@@ -37,7 +37,7 @@ You can then either point to the SDK using the `SDK_PATH` environment variable:
|
||||
|
||||
```sh
|
||||
# Extract the SDK tarball to /path/to/parent/dir/of/extracted/SDK/Xcode-<foo>-<bar>-extracted-SDK-with-libcxx-headers
|
||||
tar -C /path/to/parent/dir/of/extracted/SDK -xaf /path/to/Xcode-<foo>-<bar>-extracted-SDK-with-libcxx-headers.tar.gz
|
||||
tar -C /path/to/parent/dir/of/extracted/SDK -xaf /path/to/Xcode-<foo>-<bar>-extracted-SDK-with-libcxx-headers.tar
|
||||
|
||||
# Indicate where to locate the SDK tarball
|
||||
export SDK_PATH=/path/to/parent/dir/of/extracted/SDK
|
||||
|
||||
@@ -46,8 +46,7 @@ MAX_VERSIONS = {
|
||||
|
||||
# Ignore symbols that are exported as part of every executable
|
||||
IGNORE_EXPORTS = {
|
||||
'environ', '_environ', '__environ', '_fini', '_init', 'stdin',
|
||||
'stdout', 'stderr',
|
||||
'stdin', 'stdout', 'stderr',
|
||||
}
|
||||
|
||||
# Expected linker-loader names can be found here:
|
||||
|
||||
@@ -44,15 +44,15 @@ xip -x Xcode_15.xip
|
||||
|
||||
### Step 2: Generating the SDK tarball from `Xcode.app`
|
||||
|
||||
To generate the SDK, run the script [`gen-sdk`](./gen-sdk) with the
|
||||
To generate the SDK, run the script [`gen-sdk.py`](./gen-sdk.py) with the
|
||||
path to `Xcode.app` (extracted in the previous stage) as the first argument.
|
||||
|
||||
```bash
|
||||
./contrib/macdeploy/gen-sdk '/path/to/Xcode.app'
|
||||
./contrib/macdeploy/gen-sdk.py '/path/to/Xcode.app'
|
||||
```
|
||||
|
||||
The generated archive should be: `Xcode-15.0-15A240d-extracted-SDK-with-libcxx-headers.tar.gz`.
|
||||
The `sha256sum` should be `c0c2e7bb92c1fee0c4e9f3a485e4530786732d6c6dd9e9f418c282aa6892f55d`.
|
||||
The generated archive should be: `Xcode-15.0-15A240d-extracted-SDK-with-libcxx-headers.tar`.
|
||||
The `sha256sum` should be `95b00dc41fa090747dc0a7907a5031a2fcb2d7f95c9584ba6bccdb99b6e3d498`.
|
||||
|
||||
## Deterministic macOS App Notes
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
import argparse
|
||||
import plistlib
|
||||
import pathlib
|
||||
import sys
|
||||
import tarfile
|
||||
import gzip
|
||||
import os
|
||||
import contextlib
|
||||
|
||||
@@ -22,12 +20,12 @@ def run():
|
||||
parser = argparse.ArgumentParser(
|
||||
description=__doc__, formatter_class=argparse.RawTextHelpFormatter)
|
||||
|
||||
parser.add_argument('xcode_app', metavar='XCODEAPP', nargs=1)
|
||||
parser.add_argument("-o", metavar='OUTSDKTGZ', nargs=1, dest='out_sdktgz', required=False)
|
||||
parser.add_argument('xcode_app', metavar='XCODEAPP', type=pathlib.Path)
|
||||
parser.add_argument("-o", metavar='OUTSDKTAR', dest='out_sdkt', type=pathlib.Path, required=False)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
xcode_app = pathlib.Path(args.xcode_app[0]).resolve()
|
||||
xcode_app = args.xcode_app.resolve()
|
||||
assert xcode_app.is_dir(), "The supplied Xcode.app path '{}' either does not exist or is not a directory".format(xcode_app)
|
||||
|
||||
xcode_app_plist = xcode_app.joinpath("Contents/version.plist")
|
||||
@@ -47,11 +45,7 @@ def run():
|
||||
|
||||
out_name = "Xcode-{xcode_version}-{xcode_build_id}-extracted-SDK-with-libcxx-headers".format(xcode_version=xcode_version, xcode_build_id=xcode_build_id)
|
||||
|
||||
if args.out_sdktgz:
|
||||
out_sdktgz_path = pathlib.Path(args.out_sdktgz_path)
|
||||
else:
|
||||
# Construct our own out_sdktgz if not specified on the command line
|
||||
out_sdktgz_path = pathlib.Path("./{}.tar.gz".format(out_name))
|
||||
out_sdkt_path = args.out_sdkt or pathlib.Path("./{}.tar".format(out_name))
|
||||
|
||||
def tarfp_add_with_base_change(tarfp, dir_to_add, alt_base_dir):
|
||||
"""Add all files in dir_to_add to tarfp, but prepend alt_base_dir to the files'
|
||||
@@ -68,6 +62,8 @@ def run():
|
||||
|
||||
"""
|
||||
def change_tarinfo_base(tarinfo):
|
||||
if tarinfo.name and tarinfo.name.endswith((".swiftmodule", ".modulemap")):
|
||||
return None
|
||||
if tarinfo.name and tarinfo.name.startswith("./"):
|
||||
tarinfo.name = str(pathlib.Path(alt_base_dir, tarinfo.name))
|
||||
if tarinfo.linkname and tarinfo.linkname.startswith("./"):
|
||||
@@ -81,16 +77,17 @@ def run():
|
||||
return tarinfo
|
||||
with cd(dir_to_add):
|
||||
# recursion already adds entries in sorted order
|
||||
tarfp.add(".", recursive=True, filter=change_tarinfo_base)
|
||||
tarfp.add("./usr/include", recursive=True, filter=change_tarinfo_base)
|
||||
tarfp.add("./usr/lib", recursive=True, filter=change_tarinfo_base)
|
||||
tarfp.add("./System/Library/Frameworks", recursive=True, filter=change_tarinfo_base)
|
||||
|
||||
print("Creating output .tar.gz file...")
|
||||
with out_sdktgz_path.open("wb") as fp:
|
||||
with gzip.GzipFile(fileobj=fp, mode='wb', compresslevel=9, mtime=0) as gzf:
|
||||
with tarfile.open(mode="w", fileobj=gzf, format=tarfile.GNU_FORMAT) as tarfp:
|
||||
print("Adding MacOSX SDK {} files...".format(sdk_version))
|
||||
tarfp_add_with_base_change(tarfp, sdk_dir, out_name)
|
||||
print("Done! Find the resulting gzipped tarball at:")
|
||||
print(out_sdktgz_path.resolve())
|
||||
print("Creating output .tar file...")
|
||||
with out_sdkt_path.open("wb") as fp:
|
||||
with tarfile.open(mode="w", fileobj=fp, format=tarfile.PAX_FORMAT) as tarfp:
|
||||
print("Adding MacOSX SDK {} files...".format(sdk_version))
|
||||
tarfp_add_with_base_change(tarfp, sdk_dir, out_name)
|
||||
print("Done! Find the resulting tarball at:")
|
||||
print(out_sdkt_path.resolve())
|
||||
|
||||
if __name__ == '__main__':
|
||||
run()
|
||||
@@ -10,14 +10,13 @@ to addrman with).
|
||||
|
||||
Update `MIN_BLOCKS` in `makeseeds.py` and the `-m`/`--minblocks` arguments below, as needed.
|
||||
|
||||
The seeds compiled into the release are created from sipa's, achow101's and luke-jr's
|
||||
The seeds compiled into the release are created from sipa's and achow101's
|
||||
DNS seed, virtu's crawler, and asmap community AS map data. Run the following commands
|
||||
from the `/contrib/seeds` directory:
|
||||
|
||||
```
|
||||
curl https://bitcoin.sipa.be/seeds.txt.gz | gzip -dc > seeds_main.txt
|
||||
curl https://21.ninja/seeds.txt.gz | gzip -dc >> seeds_main.txt
|
||||
curl https://luke.dashjr.org/programs/bitcoin/files/charts/seeds.txt >> seeds_main.txt
|
||||
curl https://mainnet.achownodes.xyz/seeds.txt.gz | gzip -dc >> seeds_main.txt
|
||||
curl https://signet.achownodes.xyz/seeds.txt.gz | gzip -dc > seeds_signet.txt
|
||||
curl https://testnet.achownodes.xyz/seeds.txt.gz | gzip -dc > seeds_test.txt
|
||||
|
||||
@@ -38,7 +38,7 @@ endef
|
||||
define fetch_file
|
||||
( test -f $$($(1)_source_dir)/$(4) || \
|
||||
( $(call fetch_file_inner,$(1),$(2),$(3),$(4),$(5)) || \
|
||||
$(call fetch_file_inner,$(1),$(FALLBACK_DOWNLOAD_PATH),$(3),$(4),$(5))))
|
||||
$(call fetch_file_inner,$(1),$(FALLBACK_DOWNLOAD_PATH),$(4),$(4),$(5))))
|
||||
endef
|
||||
|
||||
# Shell script to create a source tarball in $(1)_source from local directory
|
||||
|
||||
@@ -16,6 +16,7 @@ $(package)_patches += qtbase_avoid_native_float16.patch
|
||||
$(package)_patches += qtbase_avoid_qmain.patch
|
||||
$(package)_patches += qtbase_platformsupport.patch
|
||||
$(package)_patches += qtbase_plugins_cocoa.patch
|
||||
$(package)_patches += qtbase_plugins_windows11style.patch
|
||||
$(package)_patches += qtbase_skip_tools.patch
|
||||
$(package)_patches += rcc_hardcode_timestamp.patch
|
||||
$(package)_patches += qttools_skip_dependencies.patch
|
||||
@@ -261,6 +262,7 @@ define $(package)_preprocess_cmds
|
||||
patch -p1 -i $($(package)_patch_dir)/qtbase_avoid_qmain.patch && \
|
||||
patch -p1 -i $($(package)_patch_dir)/qtbase_platformsupport.patch && \
|
||||
patch -p1 -i $($(package)_patch_dir)/qtbase_plugins_cocoa.patch && \
|
||||
patch -p1 -i $($(package)_patch_dir)/qtbase_plugins_windows11style.patch && \
|
||||
patch -p1 -i $($(package)_patch_dir)/qtbase_skip_tools.patch && \
|
||||
patch -p1 -i $($(package)_patch_dir)/rcc_hardcode_timestamp.patch
|
||||
endef
|
||||
|
||||
113
depends/patches/qt/qtbase_plugins_windows11style.patch
Normal file
113
depends/patches/qt/qtbase_plugins_windows11style.patch
Normal file
@@ -0,0 +1,113 @@
|
||||
QWindows11Style: Calculate Spinbox size based on CommonStyle size
|
||||
Use the calculation from Commonstyle and add the increased padding and
|
||||
horizontally layouted buttons to the horizontal size hint.
|
||||
|
||||
Fixes: QTBUG-130288
|
||||
Change-Id: I7932b782e7873a0178091a51379f17453eb585fd
|
||||
|
||||
Upstream commits:
|
||||
- Qt 6.8.1: 9107817eaceaacc968dbc767c24594566d637b8c
|
||||
- Qt 6.9.0: 96d46cad43517adefa2eb7cb8819a0b2cc9241e6
|
||||
|
||||
--- a/qtbase/src/plugins/styles/modernwindows/qwindows11style.cpp
|
||||
+++ b/qtbase/src/plugins/styles/modernwindows/qwindows11style.cpp
|
||||
@@ -2048,39 +2048,22 @@ QSize QWindows11Style::sizeFromContents(ContentsType type, const QStyleOption *o
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
+#if QT_CONFIG(spinbox)
|
||||
case QStyle::CT_SpinBox: {
|
||||
if (const auto *spinBoxOpt = qstyleoption_cast<const QStyleOptionSpinBox *>(option)) {
|
||||
// Add button + frame widths
|
||||
- int width = 0;
|
||||
-
|
||||
- if (const QDateTimeEdit *spinBox = qobject_cast<const QDateTimeEdit *>(widget)) {
|
||||
- const QSize textSizeMin = spinBoxOpt->fontMetrics.size(Qt::TextSingleLine, spinBox->minimumDateTime().toString(spinBox->displayFormat()));
|
||||
- const QSize textSizeMax = spinBoxOpt->fontMetrics.size(Qt::TextSingleLine, spinBox->maximumDateTime().toString(spinBox->displayFormat()));
|
||||
- width = qMax(textSizeMin.width(),textSizeMax.width());
|
||||
- } else if (const QSpinBox *spinBox = qobject_cast<const QSpinBox *>(widget)) {
|
||||
- const QSize textSizeMin = spinBoxOpt->fontMetrics.size(Qt::TextSingleLine, QString::number(spinBox->minimum()));
|
||||
- const QSize textSizeMax = spinBoxOpt->fontMetrics.size(Qt::TextSingleLine, QString::number(spinBox->maximum()));
|
||||
- width = qMax(textSizeMin.width(),textSizeMax.width());
|
||||
- width += spinBoxOpt->fontMetrics.size(Qt::TextSingleLine, spinBox->prefix()).width();
|
||||
- width += spinBoxOpt->fontMetrics.size(Qt::TextSingleLine, spinBox->suffix()).width();
|
||||
-
|
||||
- } else if (const QDoubleSpinBox *spinBox = qobject_cast<const QDoubleSpinBox *>(widget)) {
|
||||
- const QSize textSizeMin = spinBoxOpt->fontMetrics.size(Qt::TextSingleLine, QString::number(spinBox->minimum()));
|
||||
- const QSize textSizeMax = spinBoxOpt->fontMetrics.size(Qt::TextSingleLine, QString::number(spinBox->maximum()));
|
||||
- width = qMax(textSizeMin.width(),textSizeMax.width());
|
||||
- width += spinBoxOpt->fontMetrics.size(Qt::TextSingleLine, spinBox->prefix()).width();
|
||||
- width += spinBoxOpt->fontMetrics.size(Qt::TextSingleLine, spinBox->suffix()).width();
|
||||
- }
|
||||
const qreal dpi = QStyleHelper::dpi(option);
|
||||
const bool hasButtons = (spinBoxOpt->buttonSymbols != QAbstractSpinBox::NoButtons);
|
||||
- const int buttonWidth = hasButtons ? 2 * qRound(QStyleHelper::dpiScaled(16, dpi)) : 0;
|
||||
+ const int margins = 8;
|
||||
+ const int buttonWidth = hasButtons ? qRound(QStyleHelper::dpiScaled(16, dpi)) : 0;
|
||||
const int frameWidth = spinBoxOpt->frame ? proxy()->pixelMetric(PM_SpinBoxFrameWidth,
|
||||
spinBoxOpt, widget) : 0;
|
||||
- contentSize.setWidth(2 * 12 + width);
|
||||
- contentSize += QSize(buttonWidth + 2 * frameWidth, 2 * frameWidth);
|
||||
+
|
||||
+ contentSize += QSize(2 * buttonWidth + 2 * frameWidth + 2 * margins, 2 * frameWidth);
|
||||
}
|
||||
break;
|
||||
}
|
||||
+#endif
|
||||
default:
|
||||
contentSize = QWindowsVistaStyle::sizeFromContents(type, option, size, widget);
|
||||
break;
|
||||
|
||||
|
||||
Windows11Style: don't set minimum width for QAbstractSpinBox
|
||||
|
||||
There is no need to set a minimum width for QAbstractSpinBox in
|
||||
QWindows11Style::polish() as this might override the user preferences.
|
||||
Also the minimum size handling is now properly done within
|
||||
sizeFromContents().
|
||||
|
||||
Change-Id: Ibc1fd7a6f862fc85e3739025b9de581aa235d74c
|
||||
|
||||
Upstream commits:
|
||||
- Qt 6.8.3: f86da3d3f853adb1a5b823c1cc7be6db4a0265f3
|
||||
- Qt 6.9.0: b93a8dfdfe6900cb542fdc587dd2682007a6ac53
|
||||
- Qt 6.10.0: 2ec4c28470de115c16944653a5d4f6209452d56c
|
||||
|
||||
--- a/qtbase/src/plugins/styles/modernwindows/qwindows11style.cpp
|
||||
+++ b/qtbase/src/plugins/styles/modernwindows/qwindows11style.cpp
|
||||
@@ -29,7 +29,6 @@ QT_BEGIN_NAMESPACE
|
||||
|
||||
const static int topLevelRoundingRadius = 8; //Radius for toplevel items like popups for round corners
|
||||
const static int secondLevelRoundingRadius = 4; //Radius for second level items like hovered menu item round corners
|
||||
-constexpr QLatin1StringView originalWidthProperty("_q_windows11_style_original_width");
|
||||
|
||||
enum WINUI3Color {
|
||||
subtleHighlightColor, //Subtle highlight based on alpha used for hovered elements
|
||||
@@ -2140,13 +2139,6 @@ void QWindows11Style::polish(QWidget* widget)
|
||||
pal.setColor(QPalette::ButtonText, pal.text().color());
|
||||
pal.setColor(QPalette::BrightText, pal.text().color());
|
||||
widget->setPalette(pal);
|
||||
- } else if (widget->inherits("QAbstractSpinBox")) {
|
||||
- const int minWidth = 2 * 24 + 40;
|
||||
- const int originalWidth = widget->size().width();
|
||||
- if (originalWidth < minWidth) {
|
||||
- widget->resize(minWidth, widget->size().height());
|
||||
- widget->setProperty(originalWidthProperty.constData(), originalWidth);
|
||||
- }
|
||||
} else if (widget->inherits("QAbstractButton") || widget->inherits("QToolButton")) {
|
||||
widget->setAutoFillBackground(false);
|
||||
auto pal = widget->palette();
|
||||
@@ -2191,13 +2183,6 @@ void QWindows11Style::unpolish(QWidget *widget)
|
||||
scrollarea->viewport()->setPalette(pal);
|
||||
scrollarea->viewport()->setProperty("_q_original_background_palette", QVariant());
|
||||
}
|
||||
- if (widget->inherits("QAbstractSpinBox")) {
|
||||
- const QVariant originalWidth = widget->property(originalWidthProperty.constData());
|
||||
- if (originalWidth.isValid()) {
|
||||
- widget->resize(originalWidth.toInt(), widget->size().height());
|
||||
- widget->setProperty(originalWidthProperty.constData(), QVariant());
|
||||
- }
|
||||
- }
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -101,5 +101,5 @@ cmake -B build -DENABLE_WALLET=OFF
|
||||
|
||||
```bash
|
||||
cmake --build build # Append "-j N" for N parallel jobs.
|
||||
ctest --test-dir build # Append "-j N" for N parallel tests. Some tests are disabled if Python 3 is not available.
|
||||
ctest --test-dir build # Append "-j N" for N parallel tests.
|
||||
```
|
||||
|
||||
@@ -34,7 +34,7 @@ cmake -B build
|
||||
SQLite is required for the wallet:
|
||||
|
||||
```bash
|
||||
pkgin sqlite3
|
||||
pkgin install sqlite3
|
||||
```
|
||||
|
||||
To build Bitcoin Core without the wallet, use `-DENABLE_WALLET=OFF`.
|
||||
@@ -42,7 +42,7 @@ To build Bitcoin Core without the wallet, use `-DENABLE_WALLET=OFF`.
|
||||
Cap'n Proto is needed for IPC functionality (see [multiprocess.md](multiprocess.md)):
|
||||
|
||||
```bash
|
||||
pkgin capnproto
|
||||
pkgin install capnproto
|
||||
```
|
||||
|
||||
Compile with `-DENABLE_IPC=OFF` if you do not need IPC functionality.
|
||||
@@ -84,7 +84,7 @@ Otherwise, if you don't need QR encoding support, use the `-DWITH_QRENCODE=OFF`
|
||||
|
||||
Bitcoin Core can provide notifications via ZeroMQ. If the package is installed, support will be compiled in.
|
||||
```bash
|
||||
pkgin zeromq
|
||||
pkgin install zeromq
|
||||
```
|
||||
|
||||
#### Test Suite Dependencies
|
||||
@@ -115,5 +115,5 @@ Build and run the tests:
|
||||
|
||||
```bash
|
||||
cmake --build build # Append "-j N" for N parallel jobs.
|
||||
ctest --test-dir build # Append "-j N" for N parallel tests. Some tests are disabled if Python 3 is not available.
|
||||
ctest --test-dir build # Append "-j N" for N parallel tests.
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# OpenBSD Build Guide
|
||||
|
||||
**Updated for OpenBSD [7.6](https://www.openbsd.org/76.html)**
|
||||
**Updated for OpenBSD [7.8](https://www.openbsd.org/78.html)**
|
||||
|
||||
This guide describes how to build bitcoind, command-line utilities, and GUI on OpenBSD.
|
||||
|
||||
@@ -21,8 +21,11 @@ pkg_add sqlite3
|
||||
|
||||
To build Bitcoin Core without the wallet, use `-DENABLE_WALLET=OFF`.
|
||||
|
||||
Cap'n Proto is needed for IPC functionality (see [multiprocess.md](multiprocess.md))
|
||||
and can be built from source: https://capnproto.org/install.html
|
||||
Cap'n Proto is needed for IPC functionality (see [multiprocess.md](multiprocess.md)):
|
||||
|
||||
```bash
|
||||
pkg_add capnproto
|
||||
```
|
||||
|
||||
Compile with `-DENABLE_IPC=OFF` if you do not need IPC functionality.
|
||||
|
||||
@@ -93,7 +96,7 @@ Run `cmake -B build -LH` to see the full list of available options.
|
||||
|
||||
```bash
|
||||
cmake --build build # Append "-j N" for N parallel jobs.
|
||||
ctest --test-dir build # Append "-j N" for N parallel tests. Some tests are disabled if Python 3 is not available.
|
||||
ctest --test-dir build # Append "-j N" for N parallel tests.
|
||||
```
|
||||
|
||||
## Resource limits
|
||||
|
||||
@@ -170,7 +170,7 @@ Run the following in your terminal to compile Bitcoin Core:
|
||||
|
||||
``` bash
|
||||
cmake --build build # Append "-j N" here for N parallel jobs.
|
||||
ctest --test-dir build # Append "-j N" for N parallel tests. Some tests are disabled if Python 3 is not available.
|
||||
ctest --test-dir build # Append "-j N" for N parallel tests.
|
||||
```
|
||||
|
||||
### 3. Deploy (optional)
|
||||
|
||||
@@ -55,7 +55,7 @@ In the following instructions, the "Debug" configuration can be specified instea
|
||||
```
|
||||
cmake -B build --preset vs2022-static # It might take a while if the vcpkg binary cache is unpopulated or invalidated.
|
||||
cmake --build build --config Release # Append "-j N" for N parallel jobs.
|
||||
ctest --test-dir build --build-config Release # Append "-j N" for N parallel tests. Some tests are disabled if Python 3 is not available.
|
||||
ctest --test-dir build --build-config Release # Append "-j N" for N parallel tests.
|
||||
cmake --install build --config Release # Optional.
|
||||
```
|
||||
|
||||
@@ -64,7 +64,7 @@ cmake --install build --config Release # Optional.
|
||||
```
|
||||
cmake -B build --preset vs2022 -DBUILD_GUI=OFF # It might take a while if the vcpkg binary cache is unpopulated or invalidated.
|
||||
cmake --build build --config Release # Append "-j N" for N parallel jobs.
|
||||
ctest --test-dir build --build-config Release # Append "-j N" for N parallel tests. Some tests are disabled if Python 3 is not available.
|
||||
ctest --test-dir build --build-config Release # Append "-j N" for N parallel tests.
|
||||
```
|
||||
|
||||
### 6. vcpkg-specific Issues and Workarounds
|
||||
|
||||
@@ -19,7 +19,7 @@ Bitcoin Core requires one of the following compilers.
|
||||
|
||||
| Dependency | Releases | Minimum required |
|
||||
| --- | --- | --- |
|
||||
| [Boost](../depends/packages/boost.mk) | [link](https://www.boost.org/users/download/) | [1.73.0](https://github.com/bitcoin/bitcoin/pull/29066) |
|
||||
| [Boost](../depends/packages/boost.mk) | [link](https://www.boost.org/users/download/) | [1.74.0](https://github.com/bitcoin/bitcoin/pull/34107) |
|
||||
| CMake | [link](https://cmake.org/) | [3.22](https://github.com/bitcoin/bitcoin/pull/30454) |
|
||||
| [libevent](../depends/packages/libevent.mk) | [link](https://github.com/libevent/libevent/releases) | [2.1.8](https://github.com/bitcoin/bitcoin/pull/24681) |
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ of the test. Just make sure to use double-dash to distinguish them from the
|
||||
fuzzer's own arguments:
|
||||
|
||||
```sh
|
||||
$ FUZZ=address_deserialize_v2 build_fuzz/bin/fuzz -runs=1 fuzz_corpora/address_deserialize_v2 --checkaddrman=5 --printtoconsole=1
|
||||
$ FUZZ=address_deserialize build_fuzz/bin/fuzz -runs=1 fuzz_corpora/address_deserialize --checkaddrman=5 --printtoconsole=1
|
||||
```
|
||||
|
||||
## Fuzzing corpora
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3.
|
||||
.TH BITCOIN-CLI "1" "October 2025" "bitcoin-cli v30.0.0" "User Commands"
|
||||
.TH BITCOIN-CLI "1" "January 2026" "bitcoin-cli v30.2.0rc1" "User Commands"
|
||||
.SH NAME
|
||||
bitcoin-cli \- manual page for bitcoin-cli v30.0.0
|
||||
bitcoin-cli \- manual page for bitcoin-cli v30.2.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 v30.0.0
|
||||
.B bitcoin-cli
|
||||
[\fI\,options\/\fR] \fI\,help <command>\/\fR
|
||||
.SH DESCRIPTION
|
||||
Bitcoin Core RPC client version v30.0.0
|
||||
Bitcoin Core RPC client version v30.2.0rc1
|
||||
.PP
|
||||
The bitcoin\-cli utility provides a command line interface to interact with a Bitcoin Core RPC server.
|
||||
.PP
|
||||
@@ -188,7 +188,7 @@ additional "outonly" (or "o") argument can be passed to see
|
||||
outbound peers only. Pass "help" (or "h") for detailed help
|
||||
documentation.
|
||||
.SH COPYRIGHT
|
||||
Copyright (C) 2009-2025 The Bitcoin Core developers
|
||||
Copyright (C) 2009-2026 The Bitcoin Core developers
|
||||
|
||||
Please contribute if you find Bitcoin Core useful. Visit
|
||||
<https://bitcoincore.org/> for further information about the software.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3.
|
||||
.TH BITCOIN-QT "1" "October 2025" "bitcoin-qt v30.0.0" "User Commands"
|
||||
.TH BITCOIN-QT "1" "January 2026" "bitcoin-qt v30.2.0rc1" "User Commands"
|
||||
.SH NAME
|
||||
bitcoin-qt \- manual page for bitcoin-qt v30.0.0
|
||||
bitcoin-qt \- manual page for bitcoin-qt v30.2.0rc1
|
||||
.SH SYNOPSIS
|
||||
.B bitcoin-qt
|
||||
[\fI\,options\/\fR] [\fI\,URI\/\fR]
|
||||
.SH DESCRIPTION
|
||||
Bitcoin Core version v30.0.0
|
||||
Bitcoin Core version v30.2.0rc1
|
||||
.PP
|
||||
The bitcoin\-qt application provides a graphical interface for interacting with Bitcoin Core.
|
||||
.PP
|
||||
@@ -839,7 +839,7 @@ Reset all settings changed in the GUI
|
||||
.IP
|
||||
Show splash screen on startup (default: 1)
|
||||
.SH COPYRIGHT
|
||||
Copyright (C) 2009-2025 The Bitcoin Core developers
|
||||
Copyright (C) 2009-2026 The Bitcoin Core developers
|
||||
|
||||
Please contribute if you find Bitcoin Core useful. Visit
|
||||
<https://bitcoincore.org/> for further information about the software.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3.
|
||||
.TH BITCOIN-TX "1" "October 2025" "bitcoin-tx v30.0.0" "User Commands"
|
||||
.TH BITCOIN-TX "1" "January 2026" "bitcoin-tx v30.2.0rc1" "User Commands"
|
||||
.SH NAME
|
||||
bitcoin-tx \- manual page for bitcoin-tx v30.0.0
|
||||
bitcoin-tx \- manual page for bitcoin-tx v30.2.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 v30.0.0
|
||||
.B bitcoin-tx
|
||||
[\fI\,options\/\fR] \fI\,-create \/\fR[\fI\,commands\/\fR]
|
||||
.SH DESCRIPTION
|
||||
Bitcoin Core bitcoin\-tx utility version v30.0.0
|
||||
Bitcoin Core bitcoin\-tx utility version v30.2.0rc1
|
||||
.PP
|
||||
The bitcoin\-tx tool is used for creating and modifying bitcoin transactions.
|
||||
.PP
|
||||
@@ -146,7 +146,7 @@ set=NAME:JSON\-STRING
|
||||
.IP
|
||||
Set register NAME to given JSON\-STRING
|
||||
.SH COPYRIGHT
|
||||
Copyright (C) 2009-2025 The Bitcoin Core developers
|
||||
Copyright (C) 2009-2026 The Bitcoin Core developers
|
||||
|
||||
Please contribute if you find Bitcoin Core useful. Visit
|
||||
<https://bitcoincore.org/> for further information about the software.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3.
|
||||
.TH BITCOIN-UTIL "1" "October 2025" "bitcoin-util v30.0.0" "User Commands"
|
||||
.TH BITCOIN-UTIL "1" "January 2026" "bitcoin-util v30.2.0rc1" "User Commands"
|
||||
.SH NAME
|
||||
bitcoin-util \- manual page for bitcoin-util v30.0.0
|
||||
bitcoin-util \- manual page for bitcoin-util v30.2.0rc1
|
||||
.SH SYNOPSIS
|
||||
.B bitcoin-util
|
||||
[\fI\,options\/\fR] [\fI\,command\/\fR]
|
||||
@@ -9,7 +9,7 @@ bitcoin-util \- manual page for bitcoin-util v30.0.0
|
||||
.B bitcoin-util
|
||||
[\fI\,options\/\fR] \fI\,grind <hex-block-header>\/\fR
|
||||
.SH DESCRIPTION
|
||||
Bitcoin Core bitcoin\-util utility version v30.0.0
|
||||
Bitcoin Core bitcoin\-util utility version v30.2.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
|
||||
@@ -65,7 +65,7 @@ grind
|
||||
.IP
|
||||
Perform proof of work on hex header string
|
||||
.SH COPYRIGHT
|
||||
Copyright (C) 2009-2025 The Bitcoin Core developers
|
||||
Copyright (C) 2009-2026 The Bitcoin Core developers
|
||||
|
||||
Please contribute if you find Bitcoin Core useful. Visit
|
||||
<https://bitcoincore.org/> for further information about the software.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3.
|
||||
.TH BITCOIN-WALLET "1" "October 2025" "bitcoin-wallet v30.0.0" "User Commands"
|
||||
.TH BITCOIN-WALLET "1" "January 2026" "bitcoin-wallet v30.2.0rc1" "User Commands"
|
||||
.SH NAME
|
||||
bitcoin-wallet \- manual page for bitcoin-wallet v30.0.0
|
||||
bitcoin-wallet \- manual page for bitcoin-wallet v30.2.0rc1
|
||||
.SH SYNOPSIS
|
||||
.B bitcoin-wallet
|
||||
[\fI\,options\/\fR] \fI\,<command>\/\fR
|
||||
.SH DESCRIPTION
|
||||
Bitcoin Core bitcoin\-wallet utility version v30.0.0
|
||||
Bitcoin Core bitcoin\-wallet utility version v30.2.0rc1
|
||||
.PP
|
||||
bitcoin\-wallet is an offline tool for creating and interacting with Bitcoin Core wallet files.
|
||||
.PP
|
||||
@@ -100,7 +100,7 @@ info
|
||||
.IP
|
||||
Get wallet info
|
||||
.SH COPYRIGHT
|
||||
Copyright (C) 2009-2025 The Bitcoin Core developers
|
||||
Copyright (C) 2009-2026 The Bitcoin Core developers
|
||||
|
||||
Please contribute if you find Bitcoin Core useful. Visit
|
||||
<https://bitcoincore.org/> for further information about the software.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3.
|
||||
.TH BITCOIN "1" "October 2025" "bitcoin v30.0.0" "User Commands"
|
||||
.TH BITCOIN "1" "January 2026" "bitcoin v30.2.0rc1" "User Commands"
|
||||
.SH NAME
|
||||
bitcoin \- manual page for bitcoin v30.0.0
|
||||
bitcoin \- manual page for bitcoin v30.2.0rc1
|
||||
.SH SYNOPSIS
|
||||
.B bitcoin
|
||||
[\fI\,OPTIONS\/\fR] \fI\,COMMAND\/\fR...
|
||||
@@ -46,7 +46,7 @@ chainstate [ARGS] Run bitcoin kernel chainstate util, equivalent to running 'bit
|
||||
test [ARGS] Run unit tests, equivalent to running 'test_bitcoin [ARGS]'.
|
||||
test\-gui [ARGS] Run GUI unit tests, equivalent to running 'test_bitcoin\-qt [ARGS]'.
|
||||
.SH COPYRIGHT
|
||||
Copyright (C) 2009-2025 The Bitcoin Core developers
|
||||
Copyright (C) 2009-2026 The Bitcoin Core developers
|
||||
|
||||
Please contribute if you find Bitcoin Core useful. Visit
|
||||
<https://bitcoincore.org/> for further information about the software.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3.
|
||||
.TH BITCOIND "1" "October 2025" "bitcoind v30.0.0" "User Commands"
|
||||
.TH BITCOIND "1" "January 2026" "bitcoind v30.2.0rc1" "User Commands"
|
||||
.SH NAME
|
||||
bitcoind \- manual page for bitcoind v30.0.0
|
||||
bitcoind \- manual page for bitcoind v30.2.0rc1
|
||||
.SH SYNOPSIS
|
||||
.B bitcoind
|
||||
[\fI\,options\/\fR]
|
||||
.SH DESCRIPTION
|
||||
Bitcoin Core daemon version v30.0.0
|
||||
Bitcoin Core daemon version v30.2.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
|
||||
@@ -817,7 +817,7 @@ subject to empty whitelists.
|
||||
.IP
|
||||
Accept command line and JSON\-RPC commands
|
||||
.SH COPYRIGHT
|
||||
Copyright (C) 2009-2025 The Bitcoin Core developers
|
||||
Copyright (C) 2009-2026 The Bitcoin Core developers
|
||||
|
||||
Please contribute if you find Bitcoin Core useful. Visit
|
||||
<https://bitcoincore.org/> for further information about the software.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
v30.0 Release Notes
|
||||
v30.2rc1 Release Notes
|
||||
===================
|
||||
|
||||
Bitcoin Core version v30.0 is now available from:
|
||||
Bitcoin Core version v30.2rc1 is now available from:
|
||||
|
||||
<https://bitcoincore.org/bin/bitcoin-core-30.0/>
|
||||
<https://bitcoincore.org/bin/bitcoin-core-30.2/test.rc1/>
|
||||
|
||||
This release includes new features, various bug fixes and performance
|
||||
improvements, as well as updated translations.
|
||||
@@ -40,399 +40,48 @@ unsupported systems.
|
||||
Notable changes
|
||||
===============
|
||||
|
||||
Policy
|
||||
------
|
||||
### Wallet
|
||||
|
||||
- The maximum number of potentially executed legacy signature operations in a
|
||||
single standard transaction is now limited to 2500. Signature operations in all
|
||||
previous output scripts, in all input scripts, as well as all P2SH redeem
|
||||
scripts (if there are any) are counted toward the limit. The new limit is
|
||||
assumed to not affect any known typically formed standard transactions. The
|
||||
change was done to prepare for a possible BIP54 deployment in the future. (#32521)
|
||||
- #34156 wallet: fix unnamed legacy wallet migration failure
|
||||
- #34215 wallettool: fix unnamed createfromdump failure walletsdir deletion
|
||||
|
||||
- `-datacarriersize` is increased to 100,000 by default, which effectively uncaps
|
||||
the limit (as the maximum transaction size limit will be hit first). It can be
|
||||
overridden with `-datacarriersize=83` to revert to the limit enforced in previous
|
||||
versions. (#32406)
|
||||
### IPC
|
||||
|
||||
- Multiple data carrier (OP_RETURN) outputs in a transaction are now permitted for
|
||||
relay and mining. The `-datacarriersize` limit applies to the aggregate size of
|
||||
the scriptPubKeys across all such outputs in a transaction, not including the
|
||||
scriptPubKey size itself. (#32406)
|
||||
- #33511 init: Fix Ctrl-C shutdown hangs during wait calls
|
||||
|
||||
- The minimum block feerate (`-blockmintxfee`) has been changed to 0.001 satoshi per
|
||||
vB. It can still be changed using the configuration option. This option can be used
|
||||
by miners to set a minimum feerate on packages added to block templates. (#33106)
|
||||
### Build
|
||||
|
||||
- The default minimum relay feerate (`-minrelaytxfee`) and incremental relay feerate
|
||||
(`-incrementalrelayfee`) have been changed to 0.1 satoshis per vB. They can still
|
||||
be changed using their respective configuration options, but it is recommended to
|
||||
change both together if you decide to do so. (#33106)
|
||||
- #33950 guix: reduce allowed exported symbols
|
||||
- #34107 build: Update minimum required Boost version
|
||||
|
||||
Other minimum feerates (e.g. the dust feerate, the minimum returned by the fee
|
||||
estimator, and all feerates used by the wallet) remain unchanged. The mempool minimum
|
||||
feerate still changes in response to high volume.
|
||||
### Test
|
||||
|
||||
Note that unless these lower defaults are widely adopted across the network, transactions
|
||||
created with lower fee rates are not guaranteed to propagate or confirm. The wallet
|
||||
feerates remain unchanged; `-mintxfee` must be changed before attempting to create
|
||||
transactions with lower feerates using the wallet. (#33106)
|
||||
- #34137 test: Avoid hard time.sleep(1) in feature_init.py
|
||||
|
||||
P2P and network changes
|
||||
-----------------------
|
||||
### Fuzz
|
||||
|
||||
- Opportunistic 1-parent-1-child package relay has been improved to handle
|
||||
situations when the child already has unconfirmed parent(s) in the mempool.
|
||||
This means that 1p1c packages can be accepted and propagate, even if they are
|
||||
connected to broader topologies: multi-parent-1-child (where only 1 parent
|
||||
requires fee-bumping), grandparent-parent-child (where only parent requires
|
||||
fee-bumping) etc. (#31385)
|
||||
- #34091 fuzz: doc: remove any mention to address_deserialize_v2
|
||||
|
||||
- The transaction orphanage, which holds transactions with missing inputs temporarily
|
||||
while the node attempts to fetch its parents, now has improved Denial of Service protections.
|
||||
Previously, it enforced a maximum number of unique transactions (default 100,
|
||||
configurable using `-maxorphantx`). Now, its limits are as follows: the number of
|
||||
entries (unique by wtxid and peer), plus each unique transaction's input count divided
|
||||
by 10, must not exceed 3,000. The total weight of unique transactions must not exceed
|
||||
`404,000` Wu multiplied by the number of peers. (#31829)
|
||||
### Doc
|
||||
|
||||
- The `-maxorphantx` option no longer has any effect, since the orphanage no longer
|
||||
limits the number of unique transactions. Users should remove this configuration
|
||||
option if they were using it, as the setting will cause an error in future versions
|
||||
when it is no longer recognized. (#31829)
|
||||
- #34182 doc: Update OpenBSD Build Guide
|
||||
|
||||
New `bitcoin` command
|
||||
---------------------
|
||||
### Misc
|
||||
|
||||
- A new `bitcoin` command line tool has been added to make features more discoverable
|
||||
and convenient to use. The `bitcoin` tool just calls other executables and does not
|
||||
implement any functionality on its own. Specifically `bitcoin node` is a synonym for
|
||||
`bitcoind`, `bitcoin gui` is a synonym for `bitcoin-qt`, and `bitcoin rpc` is a synonym
|
||||
for `bitcoin-cli -named`. Other commands and options can be listed with `bitcoin help`.
|
||||
The new `bitcoin` command is an alternative to calling other commands directly, but it
|
||||
doesn't replace them, and there are no plans to deprecate existing commands. (#31375)
|
||||
|
||||
External Signing
|
||||
----------------
|
||||
|
||||
- Support for external signing on Windows has been re-enabled. (#29868)
|
||||
|
||||
IPC Mining Interface
|
||||
--------------------
|
||||
|
||||
- The new `bitcoin` command does support one new feature: an (experimental) IPC Mining
|
||||
Interface that allows the node to work with Stratum v2 or other mining client software,
|
||||
see (#31098). When the node is started with `bitcoin -m node -ipcbind=unix` it will
|
||||
listen on a unix socket for IPC client connections, allowing clients to request block
|
||||
templates and submit mined blocks. The `-m` option launches a new internal binary
|
||||
(`bitcoin-node` instead of `bitcoind`) and is currently required but will become optional
|
||||
in the future (with [#33229](https://github.com/bitcoin/bitcoin/pull/33229)).
|
||||
|
||||
- IPC connectivity introduces new dependencies (see [multiprocess.md](https://github.com/bitcoin/bitcoin/blob/master/doc/multiprocess.md)),
|
||||
which can be turned off with the `-DENABLE_IPC=OFF` build option if you do not intend
|
||||
to use IPC. (#31802)
|
||||
|
||||
Install changes
|
||||
---------------
|
||||
|
||||
- The `test_bitcoin` executable is now installed in `libexec/` instead of `bin/`.
|
||||
It can still be executed directly, or accessed through the new `bitcoin` command
|
||||
as `bitcoin test`. The `libexec/` directory also contains new `bitcoin-node` and
|
||||
`bitcoin-gui` binaries which support IPC features and are called through the
|
||||
`bitcoin` tool. In source builds only, `test_bitcoin-qt`, `bench_bitcoin`, and
|
||||
`bitcoin-chainstate` are also now installed to `libexec/` instead of `bin/` and
|
||||
can be accessed through the new `bitcoin` command. See `bitcoin help` output for
|
||||
details. (#31679)
|
||||
|
||||
- On Windows, the installer no longer adds a “(64-bit)” suffix to entries in the
|
||||
Start Menu (#32132), and it now automatically removes obsolete artifacts during
|
||||
upgrades (#33422).
|
||||
|
||||
Indexes
|
||||
-------
|
||||
|
||||
- The implementation of coinstatsindex was changed to prevent an overflow bug that
|
||||
could already be observed on the default Signet. The new version of the index will
|
||||
need to be synced from scratch when starting the upgraded node for the first time.
|
||||
|
||||
The new version is stored in `/indexes/coinstatsindex/` in contrast to the old version
|
||||
which was stored at `/indexes/coinstats/`. The old version of the index is not deleted
|
||||
by the upgraded node in case the user chooses to downgrade their node in the future.
|
||||
If the user does not plan to downgrade it is safe for them to remove `/indexes/coinstats/`
|
||||
from their datadir. A future release of Bitcoin Core may remove the old version of the
|
||||
index automatically. (#30469)
|
||||
|
||||
Logging
|
||||
-------
|
||||
- Unconditional logging to disk is now rate limited by giving each source location
|
||||
a quota of 1MiB per hour. Unconditional logging is any logging with a log level
|
||||
higher than debug, that is `info`, `warning`, and `error`. All logs will be
|
||||
prefixed with `[*]` if there is at least one source location that is currently
|
||||
being suppressed. (#32604)
|
||||
|
||||
- When `-logsourcelocations` is enabled, the log output now contains the entire
|
||||
function signature instead of just the function name. (#32604)
|
||||
|
||||
Updated RPCs
|
||||
------------
|
||||
|
||||
- The `-paytxfee` startup option and the `settxfee` RPC are now deprecated and
|
||||
will be removed in Bitcoin Core 31.0. They allowed the user to set a static fee
|
||||
rate for wallet transactions, which could potentially lead to overpaying or underpaying.
|
||||
Users should instead rely on fee estimation or specify a fee rate per transaction
|
||||
using the `fee_rate` argument in RPCs such as `fundrawtransaction`, `sendtoaddress`,
|
||||
`send`, `sendall`, and `sendmany`. (#31278)
|
||||
|
||||
- Any RPC in which one of the parameters is a descriptor will throw an error
|
||||
if the provided descriptor contains a whitespace at the beginning or the end
|
||||
of the public key within a fragment - e.g. `pk( KEY)` or `pk(KEY )`. (#31603)
|
||||
|
||||
- The `submitpackage` RPC, which allows submissions of child-with-parents
|
||||
packages, no longer requires that all unconfirmed parents be present. The
|
||||
package may contain other in-mempool ancestors as well. (#31385)
|
||||
|
||||
- The `waitfornewblock` RPC now takes an optional `current_tip` argument. It
|
||||
is also no longer hidden. (#30635)
|
||||
|
||||
- The `waitforblock` and `waitforblockheight` RPCs are no longer hidden. (#30635)
|
||||
|
||||
- The `psbtbumpfee` and `bumpfee` RPCs allow a replacement under fullrbf and no
|
||||
longer require BIP-125 signalling. (#31953)
|
||||
|
||||
- Transaction Script validation errors used to return the reason for the error
|
||||
prefixed by either `mandatory-script-verify-flag-failed` if it was a consensus
|
||||
error, or `non-mandatory-script-verify-flag` (without "-failed") if it was a
|
||||
standardness error. This has been changed to `block-script-verify-flag-failed`
|
||||
and `mempool-script-verify-flag-failed` for all block and mempool errors
|
||||
respectively. (#33183)
|
||||
|
||||
- The `getmininginfo` RPC now returns "blockmintxfee" result specifying the value of
|
||||
`-blockmintxfee` configuration. (#33189)
|
||||
|
||||
- The `getmempoolinfo` RPC now returns an additional "permitbaremultisig" and
|
||||
"maxdatacarriersize" field, reflecting the `-permitbaremultisig` and `-datacarriersize`
|
||||
config values. (#29954)
|
||||
|
||||
Changes to wallet-related RPCs can be found in the Wallet section below.
|
||||
|
||||
New RPCs
|
||||
--------
|
||||
|
||||
- A new REST API endpoint (`/rest/spenttxouts/BLOCKHASH`) has been introduced for
|
||||
efficiently fetching spent transaction outputs using the block's undo data (#32540).
|
||||
|
||||
Build System
|
||||
------------
|
||||
|
||||
Updated settings
|
||||
----------------
|
||||
|
||||
- The `-maxmempool` and `-dbcache` startup parameters are now capped on 32-bit systems
|
||||
to 500MB and 1GiB respectively. (#32530)
|
||||
|
||||
- The `-natpmp` option is now set to `1` by default. This means nodes with `-listen`
|
||||
enabled (the default) but running behind a firewall, such as a local network router,
|
||||
will be reachable if the firewall/router supports any of the `PCP` or `NAT-PMP`
|
||||
protocols. (#33004)
|
||||
|
||||
- The `-upnp` setting has now been fully removed. Use `-natpmp` instead. (#32500)
|
||||
|
||||
- Previously, `-proxy` specified the proxy for all networks (except I2P which
|
||||
uses `-i2psam`) and only the Tor proxy could have been specified separately
|
||||
via `-onion`. Now, the syntax of `-proxy` has been extended and it is possible
|
||||
to specify separately the proxy for IPv4, IPv6, Tor and CJDNS by appending `=`
|
||||
followed by the network name, for example `-proxy=127.0.0.1:5555=ipv6`
|
||||
configures a proxy only for IPv6. The `-proxy` option can be used multiple
|
||||
times to define different proxies for different networks, such as
|
||||
`-proxy=127.0.0.1:4444=ipv4 -proxy=10.0.0.1:6666=ipv6`. Later settings
|
||||
override earlier ones for the same network; this can be used to remove an
|
||||
earlier all-networks proxy and use direct connections only for a given
|
||||
network, for example `-proxy=127.0.0.1:5555 -proxy=0=cjdns`. (#32425)
|
||||
|
||||
- The `-blockmaxweight` startup option has been updated to be debug-only.
|
||||
It is still available to users, but now hidden from the default `-help` text
|
||||
and shown only in `-help-debug` (#32654).
|
||||
|
||||
Changes to GUI or wallet related settings can be found in the GUI or Wallet section below.
|
||||
|
||||
Wallet
|
||||
------
|
||||
|
||||
- BDB legacy wallets can no longer be created or loaded. They can be migrated
|
||||
to the new descriptor wallet format. Refer to the `migratewallet` RPC for more
|
||||
details.
|
||||
|
||||
- The legacy wallet removal drops redundant options in the bitcoin-wallet tool,
|
||||
such as `-withinternalbdb`, `-legacy`, and `-descriptors`. Moreover, the
|
||||
legacy-only RPCs `addmultisigaddress`, `dumpprivkey`, `dumpwallet`,
|
||||
`importaddress`, `importmulti`, `importprivkey`, `importpubkey`,
|
||||
`importwallet`, `newkeypool`, `sethdseed`, and `upgradewallet`, are removed.
|
||||
(#32944, #28710, #32438, #31250)
|
||||
|
||||
- Support has been added for spending TRUC transactions received by the
|
||||
wallet, as well as creating TRUC transactions. The wallet ensures that
|
||||
TRUC policy rules are being met. The wallet will throw an error if the
|
||||
user is trying to spend TRUC utxos with utxos of other versions.
|
||||
Additionally, the wallet will treat unconfirmed TRUC sibling
|
||||
transactions as mempool conflicts. The wallet will also ensure that
|
||||
transactions spending TRUC utxos meet the required size restrictions. (#32896)
|
||||
|
||||
- Since descriptor wallets do not allow mixing watchonly and non-watchonly descriptors,
|
||||
the `include_watchonly` option (and its variants in naming) are removed from all RPCs
|
||||
that had it. (#32618)
|
||||
|
||||
- The `iswatchonly` field is removed from any RPCs that returned it. (#32618)
|
||||
|
||||
- `unloadwallet` - Return RPC_INVALID_PARAMETER when both the RPC wallet endpoint
|
||||
and wallet_name parameters are unspecified. Previously the RPC failed with a JSON
|
||||
parsing error. (#32845)
|
||||
|
||||
- `getdescriptoractivity` - Mark blockhashes and scanobjects arguments as required,
|
||||
so the user receives a clear help message when either is missing. As in `unloadwallet`,
|
||||
previously the RPC failed with a JSON parsing error. (#32845)
|
||||
|
||||
- `getwalletinfo` - Removes the fields `balance`, `immature_balance` and
|
||||
`unconfirmed_balance`. (#32721)
|
||||
|
||||
- `getunconfirmedbalance` - Removes this RPC command. You can query the `getbalances`
|
||||
RPC and inspect the `["mine"]["untrusted_pending"]` entry within the JSON
|
||||
response. (#32721)
|
||||
|
||||
- The following RPCs now contain a `version` parameter that allows
|
||||
the user to create transactions of any standard version number (1-3):
|
||||
- `createrawtransaction`
|
||||
- `createpsbt`
|
||||
- `send`
|
||||
- `sendall`
|
||||
- `walletcreatefundedpsbt`
|
||||
(#32896)
|
||||
|
||||
GUI changes
|
||||
-----------
|
||||
|
||||
- The GUI has been migrated from Qt 5 to Qt 6. On Windows, dark mode is now supported.
|
||||
On macOS, the Metal backend is now used. (#30997)
|
||||
|
||||
- A transaction's fee bump is allowed under fullrbf and no longer requires
|
||||
BIP-125 signalling. (#31953)
|
||||
|
||||
- Custom column widths in the Transactions tab are reset as a side-effect of legacy
|
||||
wallet removal. (#32459)
|
||||
|
||||
Low-level changes
|
||||
=================
|
||||
|
||||
- Logs now include which peer sent us a header. Additionally there are fewer
|
||||
redundant header log messages. A side-effect of this change is that for
|
||||
some untypical cases new headers aren't logged anymore, e.g. a direct
|
||||
`BLOCK` message with a previously unknown header and `submitheader` RPC. (#27826)
|
||||
- #34174 doc: update copyright year to 2026
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Thanks to everyone who directly contributed to this release:
|
||||
|
||||
- 0xb10c
|
||||
- amisha
|
||||
- Andrew Toth
|
||||
- Anthony Towns
|
||||
- Antoine Poinsot
|
||||
- Ava Chow
|
||||
- benthecarman
|
||||
- Brandon Odiwuor
|
||||
- brunoerg
|
||||
- Bue-von-hon
|
||||
- Bufo
|
||||
- Chandra Pratap
|
||||
- Chris Stewart
|
||||
- Cory Fields
|
||||
- Daniel Pfeifer
|
||||
- Daniela Brozzoni
|
||||
- David Gumberg
|
||||
- deadmanoz
|
||||
- dennsikl
|
||||
- dergoegge
|
||||
- enoch
|
||||
- Ethan Heilman
|
||||
- Eugene Siegel
|
||||
- Eunovo
|
||||
- Eval EXEC
|
||||
- Fabian Jahr
|
||||
- fanquake
|
||||
- Florian Schmaus
|
||||
- fuder.eth
|
||||
- furszy
|
||||
- glozow
|
||||
- Greg Sanders
|
||||
- Hao Xu
|
||||
- Haoran Peng
|
||||
- Haowen Liu
|
||||
- Hennadii Stepanov
|
||||
- Hodlinator
|
||||
- hoffman
|
||||
- ishaanam
|
||||
- ismaelsadeeq
|
||||
- Jameson Lopp
|
||||
- janb84
|
||||
- Jiri Jakes
|
||||
- John Bampton
|
||||
- Jon Atack
|
||||
- josibake
|
||||
- jurraca
|
||||
- kevkevin
|
||||
- kevkevinpal
|
||||
- kilavvy
|
||||
- Kristaps Kaupe
|
||||
- l0rinc
|
||||
- laanwj
|
||||
- leopardracer
|
||||
- Lőrinc
|
||||
- Luis Schwab
|
||||
- Luke Dashjr
|
||||
- MarcoFalke
|
||||
- marcofleon
|
||||
- Martin Zumsande
|
||||
- Matt Corallo
|
||||
- Matthew Zipkin
|
||||
- Max Edwards
|
||||
- monlovesmango
|
||||
- Murch
|
||||
- naiyoma
|
||||
- nervana21
|
||||
- Nicola Leonardo Susca
|
||||
- Novo
|
||||
- pablomartin4btc
|
||||
- Peter Todd
|
||||
- Pieter Wuille
|
||||
- Pol Espinasa
|
||||
- Prabhat Verma
|
||||
- rkrux
|
||||
- Roman Zeyde
|
||||
- Ryan Ofsky
|
||||
- Saikiran
|
||||
- Salvatore Ingala
|
||||
- Sebastian Falbesoner
|
||||
- Sergi Delgado Segura
|
||||
- Shunsuke Shimizu
|
||||
- Sjors Provoost
|
||||
- stickies-v
|
||||
- stratospher
|
||||
- stringintech
|
||||
- strmfos
|
||||
- stutxo
|
||||
- tdb3
|
||||
- TheCharlatan
|
||||
- Tomás Andróil
|
||||
- UdjinM6
|
||||
- Vasil Dimov
|
||||
- VolodymyrBg
|
||||
- w0xlt
|
||||
- will
|
||||
- willcl-ark
|
||||
- William Casarin
|
||||
- woltx
|
||||
- yancy
|
||||
- zaidmstrr
|
||||
|
||||
As well as to everyone that helped with translations on
|
||||
[Transifex](https://explore.transifex.com/bitcoin/bitcoin/).
|
||||
|
||||
@@ -292,7 +292,7 @@ if(BUILD_BITCOIN_BIN)
|
||||
add_executable(bitcoin bitcoin.cpp)
|
||||
add_windows_resources(bitcoin bitcoin-res.rc)
|
||||
add_windows_application_manifest(bitcoin)
|
||||
target_link_libraries(bitcoin core_interface bitcoin_util)
|
||||
target_link_libraries(bitcoin core_interface bitcoin_common bitcoin_util)
|
||||
install_binary_component(bitcoin HAS_MANPAGE)
|
||||
endif()
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <bitcoin-build-config.h> // IWYU pragma: keep
|
||||
|
||||
#include <clientversion.h>
|
||||
#include <common/args.h>
|
||||
#include <util/fs.h>
|
||||
#include <util/exec.h>
|
||||
#include <util/strencodings.h>
|
||||
@@ -47,7 +48,7 @@ Run '%s help' to see additional commands (e.g. for testing and debugging).
|
||||
)";
|
||||
|
||||
struct CommandLine {
|
||||
bool use_multiprocess{false};
|
||||
std::optional<bool> use_multiprocess;
|
||||
bool show_version{false};
|
||||
bool show_help{false};
|
||||
std::string_view command;
|
||||
@@ -55,6 +56,7 @@ struct CommandLine {
|
||||
};
|
||||
|
||||
CommandLine ParseCommandLine(int argc, char* argv[]);
|
||||
bool UseMultiprocess(const CommandLine& cmd);
|
||||
static void ExecCommand(const std::vector<const char*>& args, std::string_view argv0);
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
@@ -78,9 +80,9 @@ int main(int argc, char* argv[])
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
} else if (cmd.command == "gui") {
|
||||
args.emplace_back(cmd.use_multiprocess ? "bitcoin-gui" : "bitcoin-qt");
|
||||
args.emplace_back(UseMultiprocess(cmd) ? "bitcoin-gui" : "bitcoin-qt");
|
||||
} else if (cmd.command == "node") {
|
||||
args.emplace_back(cmd.use_multiprocess ? "bitcoin-node" : "bitcoind");
|
||||
args.emplace_back(UseMultiprocess(cmd) ? "bitcoin-node" : "bitcoind");
|
||||
} else if (cmd.command == "rpc") {
|
||||
args.emplace_back("bitcoin-cli");
|
||||
// Since "bitcoin rpc" is a new interface that doesn't need to be
|
||||
@@ -143,6 +145,30 @@ CommandLine ParseCommandLine(int argc, char* argv[])
|
||||
return cmd;
|
||||
}
|
||||
|
||||
bool UseMultiprocess(const CommandLine& cmd)
|
||||
{
|
||||
// If -m or -M options were explicitly specified, there is no need to
|
||||
// further parse arguments to determine which to use.
|
||||
if (cmd.use_multiprocess) return *cmd.use_multiprocess;
|
||||
|
||||
ArgsManager args;
|
||||
args.SetDefaultFlags(ArgsManager::ALLOW_ANY);
|
||||
std::string error_message;
|
||||
auto argv{cmd.args};
|
||||
argv.insert(argv.begin(), nullptr);
|
||||
if (!args.ParseParameters(argv.size(), argv.data(), error_message)) {
|
||||
tfm::format(std::cerr, "Warning: failed to parse subcommand command line options: %s\n", error_message);
|
||||
}
|
||||
if (!args.ReadConfigFiles(error_message, true)) {
|
||||
tfm::format(std::cerr, "Warning: failed to parse subcommand config: %s\n", error_message);
|
||||
}
|
||||
args.SelectConfigNetwork(args.GetChainTypeString());
|
||||
|
||||
// If any -ipc* options are set these need to be processed by a
|
||||
// multiprocess-capable binary.
|
||||
return args.IsArgSet("-ipcbind") || args.IsArgSet("-ipcconnect") || args.IsArgSet("-ipcfd");
|
||||
}
|
||||
|
||||
//! Execute the specified bitcoind, bitcoin-qt or other command line in `args`
|
||||
//! using src, bin and libexec directory paths relative to this executable, where
|
||||
//! the path to this executable is specified in `wrapper_argv0`.
|
||||
|
||||
@@ -132,11 +132,16 @@ static bool ParseArgs(NodeContext& node, int argc, char* argv[])
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ProcessInitCommands(ArgsManager& args)
|
||||
static bool ProcessInitCommands(interfaces::Init& init, ArgsManager& args)
|
||||
{
|
||||
// Process help and version before taking care about datadir
|
||||
if (HelpRequested(args) || args.GetBoolArg("-version", false)) {
|
||||
std::string strUsage = CLIENT_NAME " daemon version " + FormatFullVersion() + "\n";
|
||||
std::string strUsage = CLIENT_NAME " daemon version " + FormatFullVersion();
|
||||
if (const char* exe_name{init.exeName()}) {
|
||||
strUsage += " ";
|
||||
strUsage += exe_name;
|
||||
}
|
||||
strUsage += "\n";
|
||||
|
||||
if (args.GetBoolArg("-version", false)) {
|
||||
strUsage += FormatParagraph(LicenseInfo());
|
||||
@@ -277,7 +282,7 @@ MAIN_FUNCTION
|
||||
ArgsManager& args = *Assert(node.args);
|
||||
if (!ParseArgs(node, argc, argv)) return EXIT_FAILURE;
|
||||
// Process early info return commands such as -help or -version
|
||||
if (ProcessInitCommands(args)) return EXIT_SUCCESS;
|
||||
if (ProcessInitCommands(*init, args)) return EXIT_SUCCESS;
|
||||
|
||||
// Start application
|
||||
if (!AppInit(node) || !Assert(node.shutdown_signal)->wait()) {
|
||||
|
||||
@@ -266,7 +266,13 @@ std::optional<unsigned int> ArgsManager::GetArgFlags(const std::string& name) co
|
||||
return search->second.m_flags;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
return m_default_flags;
|
||||
}
|
||||
|
||||
void ArgsManager::SetDefaultFlags(std::optional<unsigned int> flags)
|
||||
{
|
||||
LOCK(cs_args);
|
||||
m_default_flags = flags;
|
||||
}
|
||||
|
||||
fs::path ArgsManager::GetPathArg(std::string arg, const fs::path& default_value) const
|
||||
|
||||
@@ -137,6 +137,7 @@ protected:
|
||||
std::string m_network GUARDED_BY(cs_args);
|
||||
std::set<std::string> m_network_only_args GUARDED_BY(cs_args);
|
||||
std::map<OptionsCategory, std::map<std::string, Arg>> m_available_args GUARDED_BY(cs_args);
|
||||
std::optional<unsigned int> m_default_flags GUARDED_BY(cs_args){};
|
||||
bool m_accept_any_command GUARDED_BY(cs_args){true};
|
||||
std::list<SectionInfo> m_config_sections GUARDED_BY(cs_args);
|
||||
std::optional<fs::path> m_config_path GUARDED_BY(cs_args);
|
||||
@@ -375,10 +376,15 @@ protected:
|
||||
|
||||
/**
|
||||
* Return Flags for known arg.
|
||||
* Return nullopt for unknown arg.
|
||||
* Return default flags for unknown arg.
|
||||
*/
|
||||
std::optional<unsigned int> GetArgFlags(const std::string& name) const;
|
||||
|
||||
/**
|
||||
* Set default flags to return for an unknown arg.
|
||||
*/
|
||||
void SetDefaultFlags(std::optional<unsigned int>);
|
||||
|
||||
/**
|
||||
* Get settings file path, or return false if read-write settings were
|
||||
* disabled with -nosettings.
|
||||
|
||||
@@ -215,8 +215,6 @@ void InitContext(NodeContext& node)
|
||||
node.shutdown_request = [&node] {
|
||||
assert(node.shutdown_signal);
|
||||
if (!(*node.shutdown_signal)()) return false;
|
||||
// Wake any threads that may be waiting for the tip to change.
|
||||
if (node.notifications) WITH_LOCK(node.notifications->m_tip_block_mutex, node.notifications->m_tip_block_cv.notify_all());
|
||||
return true;
|
||||
};
|
||||
}
|
||||
@@ -267,6 +265,8 @@ void Interrupt(NodeContext& node)
|
||||
#if HAVE_SYSTEM
|
||||
ShutdownNotify(*node.args);
|
||||
#endif
|
||||
// Wake any threads that may be waiting for the tip to change.
|
||||
if (node.notifications) WITH_LOCK(node.notifications->m_tip_block_mutex, node.notifications->m_tip_block_cv.notify_all());
|
||||
InterruptHTTPServer();
|
||||
InterruptHTTPRPC();
|
||||
InterruptRPC();
|
||||
|
||||
@@ -39,6 +39,7 @@ public:
|
||||
// bitcoin-node accepts the option, and bitcoin-gui accepts all bitcoin-node
|
||||
// options and will start the node with those options.
|
||||
bool canListenIpc() override { return true; }
|
||||
const char* exeName() override { return EXE_NAME; }
|
||||
node::NodeContext m_node;
|
||||
std::unique_ptr<interfaces::Ipc> m_ipc;
|
||||
};
|
||||
|
||||
@@ -38,6 +38,7 @@ public:
|
||||
std::unique_ptr<interfaces::Echo> makeEcho() override { return interfaces::MakeEcho(); }
|
||||
interfaces::Ipc* ipc() override { return m_ipc.get(); }
|
||||
bool canListenIpc() override { return true; }
|
||||
const char* exeName() override { return EXE_NAME; }
|
||||
node::NodeContext& m_node;
|
||||
std::unique_ptr<interfaces::Ipc> m_ipc;
|
||||
};
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
namespace init {
|
||||
namespace {
|
||||
const char* EXE_NAME = "bitcoin-qt";
|
||||
|
||||
class BitcoinQtInit : public interfaces::Init
|
||||
{
|
||||
public:
|
||||
@@ -32,6 +34,7 @@ public:
|
||||
return MakeWalletLoader(chain, *Assert(m_node.args));
|
||||
}
|
||||
std::unique_ptr<interfaces::Echo> makeEcho() override { return interfaces::MakeEcho(); }
|
||||
const char* exeName() override { return EXE_NAME; }
|
||||
node::NodeContext m_node;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
@@ -18,6 +18,8 @@ using node::NodeContext;
|
||||
|
||||
namespace init {
|
||||
namespace {
|
||||
const char* EXE_NAME = "bitcoind";
|
||||
|
||||
class BitcoindInit : public interfaces::Init
|
||||
{
|
||||
public:
|
||||
@@ -34,6 +36,7 @@ public:
|
||||
return MakeWalletLoader(chain, *Assert(m_node.args));
|
||||
}
|
||||
std::unique_ptr<interfaces::Echo> makeEcho() override { return interfaces::MakeEcho(); }
|
||||
const char* exeName() override { return EXE_NAME; }
|
||||
NodeContext& m_node;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
@@ -38,6 +38,7 @@ public:
|
||||
virtual std::unique_ptr<Echo> makeEcho() { return nullptr; }
|
||||
virtual Ipc* ipc() { return nullptr; }
|
||||
virtual bool canListenIpc() { return false; }
|
||||
virtual const char* exeName() { return nullptr; }
|
||||
};
|
||||
|
||||
//! Return implementation of Init interface for the node process. If the argv
|
||||
|
||||
@@ -72,6 +72,11 @@ public:
|
||||
* the tip is more than 20 minutes old.
|
||||
*/
|
||||
virtual std::unique_ptr<BlockTemplate> waitNext(const node::BlockWaitOptions options = {}) = 0;
|
||||
|
||||
/**
|
||||
* Interrupts the current wait for the next block template.
|
||||
*/
|
||||
virtual void interruptWait() = 0;
|
||||
};
|
||||
|
||||
//! Interface giving clients (RPC, Stratum v2 Template Provider in the future)
|
||||
|
||||
@@ -33,6 +33,7 @@ interface BlockTemplate $Proxy.wrap("interfaces::BlockTemplate") {
|
||||
getCoinbaseMerklePath @8 (context: Proxy.Context) -> (result: List(Data));
|
||||
submitSolution @9 (context: Proxy.Context, version: UInt32, timestamp: UInt32, nonce: UInt32, coinbase :Data) -> (result: Bool);
|
||||
waitNext @10 (context: Proxy.Context, options: BlockWaitOptions) -> (result: BlockTemplate);
|
||||
interruptWait @11() -> ();
|
||||
}
|
||||
|
||||
struct BlockCreateOptions $Proxy.wrap("node::BlockCreateOptions") {
|
||||
|
||||
@@ -30,10 +30,36 @@
|
||||
namespace ipc {
|
||||
namespace capnp {
|
||||
namespace {
|
||||
void IpcLogFn(bool raise, std::string message)
|
||||
|
||||
BCLog::Level ConvertIPCLogLevel(mp::Log level)
|
||||
{
|
||||
LogDebug(BCLog::IPC, "%s\n", message);
|
||||
if (raise) throw Exception(message);
|
||||
switch (level) {
|
||||
case mp::Log::Trace: return BCLog::Level::Trace;
|
||||
case mp::Log::Debug: return BCLog::Level::Debug;
|
||||
case mp::Log::Info: return BCLog::Level::Info;
|
||||
case mp::Log::Warning: return BCLog::Level::Warning;
|
||||
case mp::Log::Error: return BCLog::Level::Error;
|
||||
case mp::Log::Raise: return BCLog::Level::Error;
|
||||
} // no default case, so the compiler can warn about missing cases
|
||||
|
||||
// Be conservative and assume that if MP ever adds a new log level, it
|
||||
// should only be shown at our most verbose level.
|
||||
return BCLog::Level::Trace;
|
||||
}
|
||||
|
||||
mp::Log GetRequestedIPCLogLevel()
|
||||
{
|
||||
if (LogAcceptCategory(BCLog::IPC, BCLog::Level::Trace)) return mp::Log::Trace;
|
||||
if (LogAcceptCategory(BCLog::IPC, BCLog::Level::Debug)) return mp::Log::Debug;
|
||||
|
||||
// Info, Warning, and Error are logged unconditionally
|
||||
return mp::Log::Info;
|
||||
}
|
||||
|
||||
void IpcLogFn(mp::LogMessage message)
|
||||
{
|
||||
LogPrintLevel(BCLog::IPC, ConvertIPCLogLevel(message.level), "%s\n", message.message);
|
||||
if (message.level == mp::Log::Raise) throw Exception(message.message);
|
||||
}
|
||||
|
||||
class CapnpProtocol : public Protocol
|
||||
@@ -62,7 +88,11 @@ public:
|
||||
{
|
||||
assert(!m_loop);
|
||||
mp::g_thread_context.thread_name = mp::ThreadName(exe_name);
|
||||
m_loop.emplace(exe_name, &IpcLogFn, &m_context);
|
||||
mp::LogOptions opts = {
|
||||
.log_fn = IpcLogFn,
|
||||
.log_level = GetRequestedIPCLogLevel()
|
||||
};
|
||||
m_loop.emplace(exe_name, std::move(opts), &m_context);
|
||||
if (ready_fn) ready_fn();
|
||||
mp::ServeStream<messages::Init>(*m_loop, fd, init);
|
||||
m_parent_connection = &m_loop->m_incoming_connections.back();
|
||||
@@ -90,7 +120,11 @@ public:
|
||||
std::promise<void> promise;
|
||||
m_loop_thread = std::thread([&] {
|
||||
util::ThreadRename("capnp-loop");
|
||||
m_loop.emplace(exe_name, &IpcLogFn, &m_context);
|
||||
mp::LogOptions opts = {
|
||||
.log_fn = IpcLogFn,
|
||||
.log_level = GetRequestedIPCLogLevel()
|
||||
};
|
||||
m_loop.emplace(exe_name, std::move(opts), &m_context);
|
||||
m_loop_ref.emplace(*m_loop);
|
||||
promise.set_value();
|
||||
m_loop->loop();
|
||||
|
||||
5
src/ipc/libmultiprocess/.gitignore
vendored
Normal file
5
src/ipc/libmultiprocess/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# CMake artifacts
|
||||
/*build*
|
||||
|
||||
# Git artifacts
|
||||
*.patch
|
||||
@@ -2,7 +2,7 @@ CI_DESC="CI job using LLVM-based libraries and tools (clang, libc++, clang-tidy,
|
||||
CI_DIR=build-llvm
|
||||
NIX_ARGS=(--arg enableLibcxx true)
|
||||
export CXX=clang++
|
||||
export CXXFLAGS="-Werror -Wall -Wextra -Wpedantic -Wthread-safety-analysis -Wno-unused-parameter"
|
||||
export CXXFLAGS="-Werror -Wall -Wextra -Wpedantic -Wthread-safety -Wno-unused-parameter"
|
||||
CMAKE_ARGS=(
|
||||
-G Ninja
|
||||
-DMP_ENABLE_CLANG_TIDY=ON
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
CI_DESC="CI job using old Cap'n Proto version"
|
||||
CI_DESC="CI job using old Cap'n Proto and cmake versions"
|
||||
CI_DIR=build-olddeps
|
||||
export CXXFLAGS="-Werror -Wall -Wextra -Wpedantic -Wno-unused-parameter -Wno-error=array-bounds"
|
||||
NIX_ARGS=(--argstr capnprotoVersion "0.7.1")
|
||||
NIX_ARGS=(--argstr capnprotoVersion "0.7.1" --argstr cmakeVersion "3.12.4")
|
||||
BUILD_ARGS=(-k)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
CI_DESC="CI job running ThreadSanitizer"
|
||||
CI_DIR=build-sanitize
|
||||
NIX_ARGS=(--arg enableLibcxx true --argstr libcxxSanitizers "Thread" --argstr capnprotoSanitizers "thread")
|
||||
export CXX=clang++
|
||||
export CXXFLAGS="-ggdb -Werror -Wall -Wextra -Wpedantic -Wthread-safety-analysis -Wno-unused-parameter -fsanitize=thread"
|
||||
export CXXFLAGS="-ggdb -Werror -Wall -Wextra -Wpedantic -Wthread-safety -Wno-unused-parameter -fsanitize=thread"
|
||||
CMAKE_ARGS=()
|
||||
BUILD_ARGS=(-k -j4)
|
||||
BUILD_TARGETS=(mptest)
|
||||
|
||||
@@ -17,6 +17,21 @@ fi
|
||||
|
||||
[ -n "${CI_CLEAN-}" ] && rm -rf "${CI_DIR}"
|
||||
|
||||
cmake -B "$CI_DIR" "${CMAKE_ARGS[@]+"${CMAKE_ARGS[@]}"}"
|
||||
cmake --build "$CI_DIR" -t "${BUILD_TARGETS[@]}" -- "${BUILD_ARGS[@]+"${BUILD_ARGS[@]}"}"
|
||||
ctest --test-dir "$CI_DIR" --output-on-failure
|
||||
cmake --version
|
||||
cmake_ver=$(cmake --version | awk '/version/{print $3; exit}')
|
||||
ver_ge() { [ "$(printf '%s\n' "$2" "$1" | sort -V | head -n1)" = "$2" ]; }
|
||||
|
||||
src_dir=$PWD
|
||||
mkdir -p "$CI_DIR"
|
||||
cd "$CI_DIR"
|
||||
cmake "$src_dir" "${CMAKE_ARGS[@]+"${CMAKE_ARGS[@]}"}"
|
||||
if ver_ge "$cmake_ver" "3.15"; then
|
||||
cmake --build . -t "${BUILD_TARGETS[@]}" -- "${BUILD_ARGS[@]+"${BUILD_ARGS[@]}"}"
|
||||
else
|
||||
# Older versions of cmake can only build one target at a time with --target,
|
||||
# and do not support -t shortcut
|
||||
for t in "${BUILD_TARGETS[@]}"; do
|
||||
cmake --build . --target "$t" -- "${BUILD_ARGS[@]+"${BUILD_ARGS[@]}"}"
|
||||
done
|
||||
fi
|
||||
ctest --output-on-failure
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
Given an interface description of an object with one or more methods, libmultiprocess generates:
|
||||
|
||||
* A C++ `ProxyClient` class with an implementation of each interface method that sends a request over a socket, waits for a response, and returns the result.
|
||||
* A C++ `ProxyServer` class that listens for requests over a socket and calls a wrapped C++ object implementing the same interface to actually execute the requests.
|
||||
* A C++ `ProxyClient` class template specialization with an implementation of each interface method that sends a request over a socket, waits for a response, and returns the result.
|
||||
* A C++ `ProxyServer` class template specialization that listens for requests over a socket and calls a wrapped C++ object implementing the same interface to actually execute the requests.
|
||||
|
||||
The function call ⇆ request translation supports input and output arguments, standard types like `unique_ptr`, `vector`, `map`, and `optional`, and bidirectional calls between processes through interface pointer and `std::function` arguments.
|
||||
|
||||
@@ -15,7 +15,7 @@ Libmultiprocess acts as a pure wrapper or layer over the underlying protocol. Cl
|
||||
|
||||
### Internals
|
||||
|
||||
The `ProxyClient` and `ProxyServer` generated classes are not directly exposed to the user, as described in [usage.md](usage.md). Instead, they wrap c++ interfaces and appear to the user as pointers to an interface. They are first instantiated when calling `ConnectStream` and `ServeStream` respectively for creating the `InitInterface`. These methods establish connections through sockets, internally creating `Connection` objects wrapping a `capnp::RpcSystem` configured for client and server mode respectively.
|
||||
The `ProxyClient` and `ProxyServer` generated classes are not directly exposed to the user, as described in [usage.md](usage.md). Instead, they wrap C++ interfaces and appear to the user as pointers to an interface. They are first instantiated when calling `ConnectStream` and `ServeStream` respectively for creating the `InitInterface`. These methods establish connections through sockets, internally creating `Connection` objects wrapping a `capnp::RpcSystem` configured for client and server mode respectively.
|
||||
|
||||
The `InitInterface` interface will typically have methods which return other interfaces, giving the connecting process the ability to call other functions in the serving process. Interfaces can also have methods accepting other interfaces as parameters, giving serving processes the ability to call back and invoke functions in connecting processes. Creating new interfaces does not create new connections, and typically many interface objects will share the same connection.
|
||||
|
||||
@@ -23,13 +23,13 @@ Both `ConnectStream` and `ServeStream` also require an instantiation of the `Eve
|
||||
|
||||
When a generated method on the `ProxyClient` is called, it calls `clientInvoke` with the capnp-translated types. `clientInvoke` creates a self-executing promise (`kj::TaskSet`) that drives the execution of the request and gives ownership of it to the `EventLoop`. `clientInvoke` blocks until a response is received, or until there is a call from the server that needs to run on the same client thread, using a `Waiter` object.
|
||||
|
||||
On the server side, the `capnp::RpcSystem` receives the capnp request and invokes the corresponding c++ method through the corresponding `ProxyServer` and the heavily templated `serverInvoke` triggering a `ServerCall`. Its return values from the actual c++ methods are copied into capnp responses by `ServerRet` and exceptions are caught and copied by `ServerExcept`. The two are connected through `ServerField`. The main method driving execution of a request is `PassField`, which is invoked through `ServerField`. Instantiated interfaces, or capabilities in capnp speak, are tracked and owned by the server's `capnp::RpcSystem`.
|
||||
On the server side, the `capnp::RpcSystem` receives the capnp request and invokes the corresponding C++ method through the corresponding `ProxyServer` and the heavily templated `serverInvoke` triggering a `ServerCall`. The return values from the actual C++ methods are copied into capnp responses by `ServerRet` and exceptions are caught and copied by `ServerExcept`. The two are connected through `ServerField`. The main method driving execution of a request is `PassField`, which is invoked through `ServerField`. Instantiated interfaces, or capabilities in capnp speak, are tracked and owned by the server's `capnp::RpcSystem`.
|
||||
|
||||
## Interface descriptions
|
||||
|
||||
As explained in the [usage](usage.md) document, interface descriptions need to be consumed both by the _libmultiprocess_ code generator, and by C++ code that calls and implements the interfaces. The C++ code only needs to know about C++ arguments and return types, while the code generator only needs to know about capnp arguments and return types, but both need to know class and method names, so the corresponding `.h` and `.capnp` source files contain some of the same information, and have to be kept in sync manually when methods or parameters change. Despite the redundancy, reconciling the interface definitions is designed to be _straightforward_ and _safe_. _Straightforward_ because there is no need to write manual serialization code or use awkward intermediate types like [`UniValue`](https://github.com/bitcoin/bitcoin/blob/master/src/univalue/include/univalue.h) instead of native types. _Safe_ because if there are any inconsistencies between API and data definitions (even minor ones like using a narrow int data type for a wider int API input), there are errors at build time instead of errors or bugs at runtime.
|
||||
|
||||
In the future, it would be possible to combine API and data definitions together using [C++ attributes](https://en.cppreference.com/w/cpp/language/attributes). To do this we would add attributes to the API definition files, and then generate the data definitions from the API definitions and attributes. I didn't take this approach mostly because it would be extra work, but also because until c++ standardizes reflection, this would require either hooking into compiler APIs like https://github.com/RosettaCommons/binder, or parsing c++ code manually like http://www.swig.org/.
|
||||
In the future, it would be possible to combine API and data definitions together using [C++ attributes](https://en.cppreference.com/w/cpp/language/attributes). To do this we would add attributes to the API definition files, and then generate the data definitions from the API definitions and attributes. I didn't take this approach mostly because it would be extra work, but also because until C++ standardizes reflection, this would require either hooking into compiler APIs like https://github.com/RosettaCommons/binder, or parsing C++ code manually like http://www.swig.org/.
|
||||
|
||||
## What is `kj`?
|
||||
|
||||
@@ -39,6 +39,6 @@ basis in this library to construct the event-loop necessary to service IPC reque
|
||||
|
||||
## Future directions
|
||||
|
||||
_libmultiprocess_ uses the [Cap'n Proto](https://capnproto.org) interface description language and protocol, but it could be extended or changed to use a different IDL/protocol like [gRPC](https://grpc.io). The nice thing about _Cap'n Proto_ compared to _gRPC_ and most other lower level protocols is that it allows interface pointers (_Services_ in gRPC parlance) to be passed as method arguments and return values, so object references and bidirectional requests work out of the box. Supporting a lower-level protocol would require writing adding maps and tracking code to proxy objects.
|
||||
_libmultiprocess_ uses the [Cap'n Proto](https://capnproto.org) interface description language and protocol, but it could be extended or changed to use a different IDL/protocol like [gRPC](https://grpc.io). The nice thing about _Cap'n Proto_ compared to _gRPC_ and most other lower level protocols is that it allows interface pointers (_Services_ in gRPC parlance) to be passed as method arguments and return values, so object references and bidirectional requests work out of the box. Supporting a lower-level protocol would require adding maps and tracking code to proxy objects.
|
||||
|
||||
_libmultiprocess_ is currently compatible with sandboxing but could add platform-specific sandboxing support or integration with a sandboxing library like [SAPI](https://github.com/google/sandboxed-api).
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
_libmultiprocess_ is a library and code generator that allows calling C++ class interfaces across different processes. For an interface to be available from other processes, it needs two definitions:
|
||||
|
||||
- An **API definition** declaring how the interface is called. Included examples: [calculator.h](https://github.com/bitcoin-core/libmultiprocess/blob/master/example/calculator.h), [printer.h](https://github.com/bitcoin-core/libmultiprocess/blob/master/example/printer.h), [init.h](https://github.com/bitcoin-core/libmultiprocess/blob/master/example/init.h). Bitcoin examples: [node.h](https://github.com/ryanofsky/bitcoin/blob/ipc-export/src/interfaces/node.h), [wallet.h](https://github.com/ryanofsky/bitcoin/blob/ipc-export/src/interfaces/wallet.h), [echo.h](https://github.com/ryanofsky/bitcoin/blob/ipc-export/src/interfaces/echo.h), [init.h](https://github.com/ryanofsky/bitcoin/blob/ipc-export/src/interfaces/init.h).
|
||||
- An **API definition** declaring how the interface is called. Included examples: [calculator.h](../example/calculator.h), [printer.h](../example/printer.h), [init.h](../example/init.h). Bitcoin examples: [node.h](https://github.com/bitcoin/bitcoin/blob/master/src/interfaces/node.h), [wallet.h](https://github.com/bitcoin/bitcoin/blob/master/src/interfaces/wallet.h), [echo.h](https://github.com/bitcoin/bitcoin/blob/master/src/interfaces/echo.h), [init.h](https://github.com/bitcoin/bitcoin/blob/master/src/interfaces/init.h).
|
||||
|
||||
- A **data definition** declaring how interface calls get sent across the wire. Included examples: [calculator.capnp](https://github.com/bitcoin-core/libmultiprocess/blob/master/example/calculator.capnp), [printer.capnp](https://github.com/bitcoin-core/libmultiprocess/blob/master/example/printer.capnp), [init.capnp](https://github.com/bitcoin-core/libmultiprocess/blob/master/example/init.capnp). Bitcoin examples: [node.capnp](https://github.com/ryanofsky/bitcoin/blob/ipc-export/src/ipc/capnp/node.capnp), [wallet.capnp](https://github.com/ryanofsky/bitcoin/blob/ipc-export/src/ipc/capnp/wallet.capnp), [echo.capnp](https://github.com/ryanofsky/bitcoin/blob/ipc-export/src/ipc/capnp/echo.capnp), [init.capnp](https://github.com/ryanofsky/bitcoin/blob/ipc-export/src/ipc/capnp/init.capnp).
|
||||
- A **data definition** declaring how interface calls get sent across the wire. Included examples: [calculator.capnp](../example/calculator.capnp), [printer.capnp](../example/printer.capnp), [init.capnp](../example/init.capnp). Bitcoin examples: [node.capnp](https://github.com/ryanofsky/bitcoin/blob/ipc-export/src/ipc/capnp/node.capnp), [wallet.capnp](https://github.com/ryanofsky/bitcoin/blob/ipc-export/src/ipc/capnp/wallet.capnp), [echo.capnp](https://github.com/bitcoin/bitcoin/blob/master/src/ipc/capnp/echo.capnp), [init.capnp](https://github.com/bitcoin/bitcoin/blob/master/src/ipc/capnp/init.capnp).
|
||||
|
||||
The `*.capnp` data definition files are consumed by the _libmultiprocess_ code generator and each `X.capnp` file generates `X.capnp.c++`, `X.capnp.h`, `X.capnp.proxy-client.c++`, `X.capnp.proxy-server.c++`, `X.capnp.proxy-types.c++`, `X.capnp.proxy-types.h`, and `X.capnp.proxy.h` output files. The generated files include `mp::ProxyClient<Interface>` and `mp::ProxyServer<Interface>` class specializations for all the interfaces in the `.capnp` files. These allow methods on C++ objects in one process to be called from other processes over IPC sockets.
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <charconv>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <kj/async.h>
|
||||
#include <kj/common.h>
|
||||
@@ -37,6 +38,7 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
// Exercises deprecated log callback signature
|
||||
static void LogPrint(bool raise, const std::string& message)
|
||||
{
|
||||
if (raise) throw std::runtime_error(message);
|
||||
|
||||
@@ -35,10 +35,10 @@ static auto Spawn(mp::EventLoop& loop, const std::string& process_argv0, const s
|
||||
return std::make_tuple(mp::ConnectStream<InitInterface>(loop, fd), pid);
|
||||
}
|
||||
|
||||
static void LogPrint(bool raise, const std::string& message)
|
||||
static void LogPrint(mp::LogMessage log_data)
|
||||
{
|
||||
if (raise) throw std::runtime_error(message);
|
||||
std::ofstream("debug.log", std::ios_base::app) << message << std::endl;
|
||||
if (log_data.level == mp::Log::Raise) throw std::runtime_error(log_data.message);
|
||||
std::ofstream("debug.log", std::ios_base::app) << log_data.message << std::endl;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
|
||||
@@ -32,10 +32,10 @@ public:
|
||||
std::unique_ptr<Printer> makePrinter() override { return std::make_unique<PrinterImpl>(); }
|
||||
};
|
||||
|
||||
static void LogPrint(bool raise, const std::string& message)
|
||||
static void LogPrint(mp::LogMessage log_data)
|
||||
{
|
||||
if (raise) throw std::runtime_error(message);
|
||||
std::ofstream("debug.log", std::ios_base::app) << message << std::endl;
|
||||
if (log_data.level == mp::Log::Raise) throw std::runtime_error(log_data.message);
|
||||
std::ofstream("debug.log", std::ios_base::app) << log_data.message << std::endl;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
|
||||
@@ -66,8 +66,6 @@ struct ProxyClient<Thread> : public ProxyClientBase<Thread, ::capnp::Void>
|
||||
ProxyClient(const ProxyClient&) = delete;
|
||||
~ProxyClient();
|
||||
|
||||
void setDisconnectCallback(const std::function<void()>& fn);
|
||||
|
||||
//! Reference to callback function that is run if there is a sudden
|
||||
//! disconnect and the Connection object is destroyed before this
|
||||
//! ProxyClient<Thread> object. The callback will destroy this object and
|
||||
@@ -100,36 +98,29 @@ public:
|
||||
EventLoop& m_loop;
|
||||
};
|
||||
|
||||
using LogFn = std::function<void(bool raise, std::string message)>;
|
||||
|
||||
class Logger
|
||||
{
|
||||
public:
|
||||
Logger(bool raise, LogFn& fn) : m_raise(raise), m_fn(fn) {}
|
||||
Logger(Logger&& logger) : m_raise(logger.m_raise), m_fn(logger.m_fn), m_buffer(std::move(logger.m_buffer)) {}
|
||||
~Logger() noexcept(false)
|
||||
{
|
||||
if (m_fn) m_fn(m_raise, m_buffer.str());
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
friend Logger& operator<<(Logger& logger, T&& value)
|
||||
{
|
||||
if (logger.m_fn) logger.m_buffer << std::forward<T>(value);
|
||||
return logger;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
friend Logger& operator<<(Logger&& logger, T&& value)
|
||||
{
|
||||
return logger << std::forward<T>(value);
|
||||
}
|
||||
|
||||
bool m_raise;
|
||||
LogFn& m_fn;
|
||||
std::ostringstream m_buffer;
|
||||
//! Log flags. Update stringify function if changed!
|
||||
enum class Log {
|
||||
Trace = 0,
|
||||
Debug,
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
Raise,
|
||||
};
|
||||
|
||||
kj::StringPtr KJ_STRINGIFY(Log flags);
|
||||
|
||||
struct LogMessage {
|
||||
|
||||
//! Message to be logged
|
||||
std::string message;
|
||||
|
||||
//! The severity level of this message
|
||||
Log level;
|
||||
};
|
||||
|
||||
using LogFn = std::function<void(LogMessage)>;
|
||||
|
||||
struct LogOptions {
|
||||
|
||||
//! External logging callback.
|
||||
@@ -138,8 +129,60 @@ struct LogOptions {
|
||||
//! Maximum number of characters to use when representing
|
||||
//! request and response structs as strings.
|
||||
size_t max_chars{200};
|
||||
|
||||
//! Messages with a severity level less than log_level will not be
|
||||
//! reported.
|
||||
Log log_level{Log::Trace};
|
||||
};
|
||||
|
||||
class Logger
|
||||
{
|
||||
public:
|
||||
Logger(const LogOptions& options, Log log_level) : m_options(options), m_log_level(log_level) {}
|
||||
|
||||
Logger(Logger&&) = delete;
|
||||
Logger& operator=(Logger&&) = delete;
|
||||
Logger(const Logger&) = delete;
|
||||
Logger& operator=(const Logger&) = delete;
|
||||
|
||||
~Logger() noexcept(false)
|
||||
{
|
||||
if (enabled()) m_options.log_fn({std::move(m_buffer).str(), m_log_level});
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
friend Logger& operator<<(Logger& logger, T&& value)
|
||||
{
|
||||
if (logger.enabled()) logger.m_buffer << std::forward<T>(value);
|
||||
return logger;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
friend Logger& operator<<(Logger&& logger, T&& value)
|
||||
{
|
||||
return logger << std::forward<T>(value);
|
||||
}
|
||||
|
||||
explicit operator bool() const
|
||||
{
|
||||
return enabled();
|
||||
}
|
||||
|
||||
private:
|
||||
bool enabled() const
|
||||
{
|
||||
return m_options.log_fn && m_log_level >= m_options.log_level;
|
||||
}
|
||||
|
||||
const LogOptions& m_options;
|
||||
Log m_log_level;
|
||||
std::ostringstream m_buffer;
|
||||
};
|
||||
|
||||
#define MP_LOGPLAIN(loop, ...) if (mp::Logger logger{(loop).m_log_opts, __VA_ARGS__}; logger) logger
|
||||
|
||||
#define MP_LOG(loop, ...) MP_LOGPLAIN(loop, __VA_ARGS__) << "{" << LongThreadName((loop).m_exe_name) << "} "
|
||||
|
||||
std::string LongThreadName(const char* exe_name);
|
||||
|
||||
//! Event loop implementation.
|
||||
@@ -170,8 +213,19 @@ std::string LongThreadName(const char* exe_name);
|
||||
class EventLoop
|
||||
{
|
||||
public:
|
||||
//! Construct event loop object.
|
||||
EventLoop(const char* exe_name, LogFn log_fn, void* context = nullptr);
|
||||
//! Construct event loop object with default logging options.
|
||||
EventLoop(const char* exe_name, LogFn log_fn, void* context = nullptr)
|
||||
: EventLoop(exe_name, LogOptions{std::move(log_fn)}, context){}
|
||||
|
||||
//! Construct event loop object with specified logging options.
|
||||
EventLoop(const char* exe_name, LogOptions log_opts, void* context = nullptr);
|
||||
|
||||
//! Backwards-compatible constructor for previous (deprecated) logging callback signature
|
||||
EventLoop(const char* exe_name, std::function<void(bool, std::string)> old_callback, void* context = nullptr)
|
||||
: EventLoop(exe_name,
|
||||
LogFn{[old_callback = std::move(old_callback)](LogMessage log_data) {old_callback(log_data.level == Log::Raise, std::move(log_data.message));}},
|
||||
context){}
|
||||
|
||||
~EventLoop();
|
||||
|
||||
//! Run event loop. Does not return until shutdown. This should only be
|
||||
@@ -212,15 +266,6 @@ public:
|
||||
//! Check if loop should exit.
|
||||
bool done() const MP_REQUIRES(m_mutex);
|
||||
|
||||
Logger log()
|
||||
{
|
||||
Logger logger(false, m_log_opts.log_fn);
|
||||
logger << "{" << LongThreadName(m_exe_name) << "} ";
|
||||
return logger;
|
||||
}
|
||||
Logger logPlain() { return {false, m_log_opts.log_fn}; }
|
||||
Logger raise() { return {true, m_log_opts.log_fn}; }
|
||||
|
||||
//! Process name included in thread names so combined debug output from
|
||||
//! multiple processes is easier to understand.
|
||||
const char* m_exe_name;
|
||||
@@ -283,18 +328,19 @@ struct Waiter
|
||||
Waiter() = default;
|
||||
|
||||
template <typename Fn>
|
||||
void post(Fn&& fn)
|
||||
bool post(Fn&& fn)
|
||||
{
|
||||
const std::unique_lock<std::mutex> lock(m_mutex);
|
||||
assert(!m_fn);
|
||||
const Lock lock(m_mutex);
|
||||
if (m_fn) return false;
|
||||
m_fn = std::forward<Fn>(fn);
|
||||
m_cv.notify_all();
|
||||
return true;
|
||||
}
|
||||
|
||||
template <class Predicate>
|
||||
void wait(std::unique_lock<std::mutex>& lock, Predicate pred)
|
||||
void wait(Lock& lock, Predicate pred)
|
||||
{
|
||||
m_cv.wait(lock, [&] {
|
||||
m_cv.wait(lock.m_lock, [&]() MP_REQUIRES(m_mutex) {
|
||||
// Important for this to be "while (m_fn)", not "if (m_fn)" to avoid
|
||||
// a lost-wakeup bug. A new m_fn and m_cv notification might be sent
|
||||
// after the fn() call and before the lock.lock() call in this loop
|
||||
@@ -317,9 +363,9 @@ struct Waiter
|
||||
//! mutexes than necessary. This mutex can be held at the same time as
|
||||
//! EventLoop::m_mutex as long as Waiter::mutex is locked first and
|
||||
//! EventLoop::m_mutex is locked second.
|
||||
std::mutex m_mutex;
|
||||
Mutex m_mutex;
|
||||
std::condition_variable m_cv;
|
||||
std::optional<kj::Function<void()>> m_fn;
|
||||
std::optional<kj::Function<void()>> m_fn MP_GUARDED_BY(m_mutex);
|
||||
};
|
||||
|
||||
//! Object holding network & rpc state associated with either an incoming server
|
||||
@@ -544,29 +590,73 @@ void ProxyServerBase<Interface, Impl>::invokeDestroy()
|
||||
CleanupRun(m_context.cleanup_fns);
|
||||
}
|
||||
|
||||
using ConnThreads = std::map<Connection*, ProxyClient<Thread>>;
|
||||
//! Map from Connection to local or remote thread handle which will be used over
|
||||
//! that connection. This map will typically only contain one entry, but can
|
||||
//! contain multiple if a single thread makes IPC calls over multiple
|
||||
//! connections. A std::optional value type is used to avoid the map needing to
|
||||
//! be locked while ProxyClient<Thread> objects are constructed, see
|
||||
//! ThreadContext "Synchronization note" below.
|
||||
using ConnThreads = std::map<Connection*, std::optional<ProxyClient<Thread>>>;
|
||||
using ConnThread = ConnThreads::iterator;
|
||||
|
||||
// Retrieve ProxyClient<Thread> object associated with this connection from a
|
||||
// map, or create a new one and insert it into the map. Return map iterator and
|
||||
// inserted bool.
|
||||
std::tuple<ConnThread, bool> SetThread(ConnThreads& threads, std::mutex& mutex, Connection* connection, const std::function<Thread::Client()>& make_thread);
|
||||
std::tuple<ConnThread, bool> SetThread(GuardedRef<ConnThreads> threads, Connection* connection, const std::function<Thread::Client()>& make_thread);
|
||||
|
||||
//! The thread_local ThreadContext g_thread_context struct provides information
|
||||
//! about individual threads and a way of communicating between them. Because
|
||||
//! it's a thread local struct, each ThreadContext instance is initialized by
|
||||
//! the thread that owns it.
|
||||
//!
|
||||
//! ThreadContext is used for any client threads created externally which make
|
||||
//! IPC calls, and for server threads created by
|
||||
//! ProxyServer<ThreadMap>::makeThread() which execute IPC calls for clients.
|
||||
//!
|
||||
//! In both cases, the struct holds information like the thread name, and a
|
||||
//! Waiter object where the EventLoop can post incoming IPC requests to execute
|
||||
//! on the thread. The struct also holds ConnThread maps associating the thread
|
||||
//! with local and remote ProxyClient<Thread> objects.
|
||||
struct ThreadContext
|
||||
{
|
||||
//! Identifying string for debug.
|
||||
std::string thread_name;
|
||||
|
||||
//! Waiter object used to allow client threads blocked waiting for a server
|
||||
//! response to execute callbacks made from the client's corresponding
|
||||
//! server thread.
|
||||
//! Waiter object used to allow remote clients to execute code on this
|
||||
//! thread. For server threads created by
|
||||
//! ProxyServer<ThreadMap>::makeThread(), this is initialized in that
|
||||
//! function. Otherwise, for client threads created externally, this is
|
||||
//! initialized the first time the thread tries to make an IPC call. Having
|
||||
//! a waiter is necessary for threads making IPC calls in case a server they
|
||||
//! are calling expects them to execute a callback during the call, before
|
||||
//! it sends a response.
|
||||
//!
|
||||
//! For IPC client threads, the Waiter pointer is never cleared and the Waiter
|
||||
//! just gets destroyed when the thread does. For server threads created by
|
||||
//! makeThread(), this pointer is set to null in the ~ProxyServer<Thread> as
|
||||
//! a signal for the thread to exit and destroy itself. In both cases, the
|
||||
//! same Waiter object is used across different calls and only created and
|
||||
//! destroyed once for the lifetime of the thread.
|
||||
std::unique_ptr<Waiter> waiter = nullptr;
|
||||
|
||||
//! When client is making a request to a server, this is the
|
||||
//! `callbackThread` argument it passes in the request, used by the server
|
||||
//! in case it needs to make callbacks into the client that need to execute
|
||||
//! while the client is waiting. This will be set to a local thread object.
|
||||
ConnThreads callback_threads;
|
||||
//!
|
||||
//! Synchronization note: The callback_thread and request_thread maps are
|
||||
//! only ever accessed internally by this thread's destructor and externally
|
||||
//! by Cap'n Proto event loop threads. Since it's possible for IPC client
|
||||
//! threads to make calls over different connections that could have
|
||||
//! different event loops, these maps are guarded by Waiter::m_mutex in case
|
||||
//! different event loop threads add or remove map entries simultaneously.
|
||||
//! However, individual ProxyClient<Thread> objects in the maps will only be
|
||||
//! associated with one event loop and guarded by EventLoop::m_mutex. So
|
||||
//! Waiter::m_mutex does not need to be held while accessing individual
|
||||
//! ProxyClient<Thread> instances, and may even need to be released to
|
||||
//! respect lock order and avoid locking Waiter::m_mutex before
|
||||
//! EventLoop::m_mutex.
|
||||
ConnThreads callback_threads MP_GUARDED_BY(waiter->m_mutex);
|
||||
|
||||
//! When client is making a request to a server, this is the `thread`
|
||||
//! argument it passes in the request, used to control which thread on
|
||||
@@ -575,7 +665,9 @@ struct ThreadContext
|
||||
//! by makeThread. If a client call is being made from a thread currently
|
||||
//! handling a server request, this will be set to the `callbackThread`
|
||||
//! request thread argument passed in that request.
|
||||
ConnThreads request_threads;
|
||||
//!
|
||||
//! Synchronization note: \ref callback_threads note applies here as well.
|
||||
ConnThreads request_threads MP_GUARDED_BY(waiter->m_mutex);
|
||||
|
||||
//! Whether this thread is a capnp event loop thread. Not really used except
|
||||
//! to assert false if there's an attempt to execute a blocking operation
|
||||
@@ -598,7 +690,7 @@ std::unique_ptr<ProxyClient<InitInterface>> ConnectStream(EventLoop& loop, int f
|
||||
init_client = connection->m_rpc_system->bootstrap(ServerVatId().vat_id).castAs<InitInterface>();
|
||||
Connection* connection_ptr = connection.get();
|
||||
connection->onDisconnect([&loop, connection_ptr] {
|
||||
loop.log() << "IPC client: unexpected network disconnect.";
|
||||
MP_LOG(loop, Log::Warning) << "IPC client: unexpected network disconnect.";
|
||||
delete connection_ptr;
|
||||
});
|
||||
});
|
||||
@@ -621,7 +713,7 @@ void _Serve(EventLoop& loop, kj::Own<kj::AsyncIoStream>&& stream, InitImpl& init
|
||||
});
|
||||
auto it = loop.m_incoming_connections.begin();
|
||||
it->onDisconnect([&loop, it] {
|
||||
loop.log() << "IPC server: socket disconnected.";
|
||||
MP_LOG(loop, Log::Info) << "IPC server: socket disconnected.";
|
||||
loop.m_incoming_connections.erase(it);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -568,7 +568,7 @@ template <typename Client>
|
||||
void clientDestroy(Client& client)
|
||||
{
|
||||
if (client.m_context.connection) {
|
||||
client.m_context.loop->log() << "IPC client destroy " << typeid(client).name();
|
||||
MP_LOG(*client.m_context.loop, Log::Info) << "IPC client destroy " << typeid(client).name();
|
||||
} else {
|
||||
KJ_LOG(INFO, "IPC interrupted client destroy", typeid(client).name());
|
||||
}
|
||||
@@ -577,7 +577,7 @@ void clientDestroy(Client& client)
|
||||
template <typename Server>
|
||||
void serverDestroy(Server& server)
|
||||
{
|
||||
server.m_context.loop->log() << "IPC server destroy " << typeid(server).name();
|
||||
MP_LOG(*server.m_context.loop, Log::Info) << "IPC server destroy " << typeid(server).name();
|
||||
}
|
||||
|
||||
//! Entry point called by generated client code that looks like:
|
||||
@@ -605,7 +605,7 @@ void clientInvoke(ProxyClient& proxy_client, const GetRequest& get_request, Fiel
|
||||
// declaration so the server method runs in a dedicated thread.
|
||||
assert(!g_thread_context.loop_thread);
|
||||
g_thread_context.waiter = std::make_unique<Waiter>();
|
||||
proxy_client.m_context.loop->logPlain()
|
||||
MP_LOGPLAIN(*proxy_client.m_context.loop, Log::Info)
|
||||
<< "{" << g_thread_context.thread_name
|
||||
<< "} IPC client first request from current thread, constructing waiter";
|
||||
}
|
||||
@@ -617,7 +617,7 @@ void clientInvoke(ProxyClient& proxy_client, const GetRequest& get_request, Fiel
|
||||
const char* disconnected = nullptr;
|
||||
proxy_client.m_context.loop->sync([&]() {
|
||||
if (!proxy_client.m_context.connection) {
|
||||
const std::unique_lock<std::mutex> lock(thread_context.waiter->m_mutex);
|
||||
const Lock lock(thread_context.waiter->m_mutex);
|
||||
done = true;
|
||||
disconnected = "IPC client method called after disconnect.";
|
||||
thread_context.waiter->m_cv.notify_all();
|
||||
@@ -629,22 +629,26 @@ void clientInvoke(ProxyClient& proxy_client, const GetRequest& get_request, Fiel
|
||||
using FieldList = typename ProxyClientMethodTraits<typename Request::Params>::Fields;
|
||||
invoke_context.emplace(*proxy_client.m_context.connection, thread_context);
|
||||
IterateFields().handleChain(*invoke_context, request, FieldList(), typename FieldObjs::BuildParams{&fields}...);
|
||||
proxy_client.m_context.loop->logPlain()
|
||||
MP_LOGPLAIN(*proxy_client.m_context.loop, Log::Debug)
|
||||
<< "{" << thread_context.thread_name << "} IPC client send "
|
||||
<< TypeName<typename Request::Params>() << " " << LogEscape(request.toString(), proxy_client.m_context.loop->m_log_opts.max_chars);
|
||||
<< TypeName<typename Request::Params>();
|
||||
MP_LOGPLAIN(*proxy_client.m_context.loop, Log::Trace)
|
||||
<< "send data: " << LogEscape(request.toString(), proxy_client.m_context.loop->m_log_opts.max_chars);
|
||||
|
||||
proxy_client.m_context.loop->m_task_set->add(request.send().then(
|
||||
[&](::capnp::Response<typename Request::Results>&& response) {
|
||||
proxy_client.m_context.loop->logPlain()
|
||||
MP_LOGPLAIN(*proxy_client.m_context.loop, Log::Debug)
|
||||
<< "{" << thread_context.thread_name << "} IPC client recv "
|
||||
<< TypeName<typename Request::Results>() << " " << LogEscape(response.toString(), proxy_client.m_context.loop->m_log_opts.max_chars);
|
||||
<< TypeName<typename Request::Results>();
|
||||
MP_LOGPLAIN(*proxy_client.m_context.loop, Log::Trace)
|
||||
<< "recv data: " << LogEscape(response.toString(), proxy_client.m_context.loop->m_log_opts.max_chars);
|
||||
try {
|
||||
IterateFields().handleChain(
|
||||
*invoke_context, response, FieldList(), typename FieldObjs::ReadResults{&fields}...);
|
||||
} catch (...) {
|
||||
exception = std::current_exception();
|
||||
}
|
||||
const std::unique_lock<std::mutex> lock(thread_context.waiter->m_mutex);
|
||||
const Lock lock(thread_context.waiter->m_mutex);
|
||||
done = true;
|
||||
thread_context.waiter->m_cv.notify_all();
|
||||
},
|
||||
@@ -653,20 +657,20 @@ void clientInvoke(ProxyClient& proxy_client, const GetRequest& get_request, Fiel
|
||||
disconnected = "IPC client method call interrupted by disconnect.";
|
||||
} else {
|
||||
kj_exception = kj::str("kj::Exception: ", e).cStr();
|
||||
proxy_client.m_context.loop->logPlain()
|
||||
MP_LOGPLAIN(*proxy_client.m_context.loop, Log::Info)
|
||||
<< "{" << thread_context.thread_name << "} IPC client exception " << kj_exception;
|
||||
}
|
||||
const std::unique_lock<std::mutex> lock(thread_context.waiter->m_mutex);
|
||||
const Lock lock(thread_context.waiter->m_mutex);
|
||||
done = true;
|
||||
thread_context.waiter->m_cv.notify_all();
|
||||
}));
|
||||
});
|
||||
|
||||
std::unique_lock<std::mutex> lock(thread_context.waiter->m_mutex);
|
||||
Lock lock(thread_context.waiter->m_mutex);
|
||||
thread_context.waiter->wait(lock, [&done]() { return done; });
|
||||
if (exception) std::rethrow_exception(exception);
|
||||
if (!kj_exception.empty()) proxy_client.m_context.loop->raise() << kj_exception;
|
||||
if (disconnected) proxy_client.m_context.loop->raise() << disconnected;
|
||||
if (!kj_exception.empty()) MP_LOGPLAIN(*proxy_client.m_context.loop, Log::Raise) << kj_exception;
|
||||
if (disconnected) MP_LOGPLAIN(*proxy_client.m_context.loop, Log::Raise) << disconnected;
|
||||
}
|
||||
|
||||
//! Invoke callable `fn()` that may return void. If it does return void, replace
|
||||
@@ -700,8 +704,10 @@ kj::Promise<void> serverInvoke(Server& server, CallContext& call_context, Fn fn)
|
||||
using Results = typename decltype(call_context.getResults())::Builds;
|
||||
|
||||
int req = ++server_reqs;
|
||||
server.m_context.loop->log() << "IPC server recv request #" << req << " "
|
||||
<< TypeName<typename Params::Reads>() << " " << LogEscape(params.toString(), server.m_context.loop->m_log_opts.max_chars);
|
||||
MP_LOG(*server.m_context.loop, Log::Debug) << "IPC server recv request #" << req << " "
|
||||
<< TypeName<typename Params::Reads>();
|
||||
MP_LOG(*server.m_context.loop, Log::Trace) << "request data: "
|
||||
<< LogEscape(params.toString(), server.m_context.loop->m_log_opts.max_chars);
|
||||
|
||||
try {
|
||||
using ServerContext = ServerInvokeContext<Server, CallContext>;
|
||||
@@ -717,14 +723,15 @@ kj::Promise<void> serverInvoke(Server& server, CallContext& call_context, Fn fn)
|
||||
return ReplaceVoid([&]() { return fn.invoke(server_context, ArgList()); },
|
||||
[&]() { return kj::Promise<CallContext>(kj::mv(call_context)); })
|
||||
.then([&server, req](CallContext call_context) {
|
||||
server.m_context.loop->log() << "IPC server send response #" << req << " " << TypeName<Results>()
|
||||
<< " " << LogEscape(call_context.getResults().toString(), server.m_context.loop->m_log_opts.max_chars);
|
||||
MP_LOG(*server.m_context.loop, Log::Debug) << "IPC server send response #" << req << " " << TypeName<Results>();
|
||||
MP_LOG(*server.m_context.loop, Log::Trace) << "response data: "
|
||||
<< LogEscape(call_context.getResults().toString(), server.m_context.loop->m_log_opts.max_chars);
|
||||
});
|
||||
} catch (const std::exception& e) {
|
||||
server.m_context.loop->log() << "IPC server unhandled exception: " << e.what();
|
||||
MP_LOG(*server.m_context.loop, Log::Error) << "IPC server unhandled exception: " << e.what();
|
||||
throw;
|
||||
} catch (...) {
|
||||
server.m_context.loop->log() << "IPC server unhandled exception";
|
||||
MP_LOG(*server.m_context.loop, Log::Error) << "IPC server unhandled exception";
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ void CustomBuildField(TypeList<>,
|
||||
// Also store the Thread::Client reference in the callback_threads map so
|
||||
// future calls over this connection can reuse it.
|
||||
auto [callback_thread, _]{SetThread(
|
||||
thread_context.callback_threads, thread_context.waiter->m_mutex, &connection,
|
||||
GuardedRef{thread_context.waiter->m_mutex, thread_context.callback_threads}, &connection,
|
||||
[&] { return connection.m_threads.add(kj::heap<ProxyServer<Thread>>(thread_context, std::thread{})); })};
|
||||
|
||||
// Call remote ThreadMap.makeThread function so server will create a
|
||||
@@ -43,12 +43,12 @@ void CustomBuildField(TypeList<>,
|
||||
return request.send().getResult(); // Nonblocking due to capnp request pipelining.
|
||||
}};
|
||||
auto [request_thread, _1]{SetThread(
|
||||
thread_context.request_threads, thread_context.waiter->m_mutex,
|
||||
GuardedRef{thread_context.waiter->m_mutex, thread_context.request_threads},
|
||||
&connection, make_request_thread)};
|
||||
|
||||
auto context = output.init();
|
||||
context.setThread(request_thread->second.m_client);
|
||||
context.setCallbackThread(callback_thread->second.m_client);
|
||||
context.setThread(request_thread->second->m_client);
|
||||
context.setCallbackThread(callback_thread->second->m_client);
|
||||
}
|
||||
|
||||
//! PassField override for mp.Context arguments. Return asynchronously and call
|
||||
@@ -89,29 +89,39 @@ auto PassField(Priority<1>, TypeList<>, ServerContext& server_context, const Fn&
|
||||
// need to update the map.
|
||||
auto& thread_context = g_thread_context;
|
||||
auto& request_threads = thread_context.request_threads;
|
||||
auto [request_thread, inserted]{SetThread(
|
||||
request_threads, thread_context.waiter->m_mutex,
|
||||
server.m_context.connection,
|
||||
[&] { return context_arg.getCallbackThread(); })};
|
||||
ConnThread request_thread;
|
||||
bool inserted;
|
||||
server.m_context.loop->sync([&] {
|
||||
std::tie(request_thread, inserted) = SetThread(
|
||||
GuardedRef{thread_context.waiter->m_mutex, request_threads}, server.m_context.connection,
|
||||
[&] { return context_arg.getCallbackThread(); });
|
||||
});
|
||||
|
||||
// If an entry was inserted into the requests_threads map,
|
||||
// If an entry was inserted into the request_threads map,
|
||||
// remove it after calling fn.invoke. If an entry was not
|
||||
// inserted, one already existed, meaning this must be a
|
||||
// recursive call (IPC call calling back to the caller which
|
||||
// makes another IPC call), so avoid modifying the map.
|
||||
const bool erase_thread{inserted};
|
||||
KJ_DEFER(if (erase_thread) {
|
||||
std::unique_lock<std::mutex> lock(thread_context.waiter->m_mutex);
|
||||
// Call erase here with a Connection* argument instead
|
||||
// of an iterator argument, because the `request_thread`
|
||||
// iterator may be invalid if the connection is closed
|
||||
// during this function call. More specifically, the
|
||||
// iterator may be invalid because SetThread adds a
|
||||
// cleanup callback to the Connection destructor that
|
||||
// erases the thread from the map, and also because the
|
||||
// ProxyServer<Thread> destructor calls
|
||||
// request_threads.clear().
|
||||
request_threads.erase(server.m_context.connection);
|
||||
// Erase the request_threads entry on the event loop
|
||||
// thread with loop->sync(), so if the connection is
|
||||
// broken there is not a race between this thread and
|
||||
// the disconnect handler trying to destroy the thread
|
||||
// client object.
|
||||
server.m_context.loop->sync([&] {
|
||||
// Look up the thread again without using existing
|
||||
// iterator since entry may no longer be there after
|
||||
// a disconnect. Destroy node after releasing
|
||||
// Waiter::m_mutex, so the ProxyClient<Thread>
|
||||
// destructor is able to use EventLoop::mutex
|
||||
// without violating lock order.
|
||||
ConnThreads::node_type removed;
|
||||
{
|
||||
Lock lock(thread_context.waiter->m_mutex);
|
||||
removed = request_threads.extract(server.m_context.connection);
|
||||
}
|
||||
});
|
||||
});
|
||||
fn.invoke(server_context, args...);
|
||||
}
|
||||
@@ -140,11 +150,16 @@ auto PassField(Priority<1>, TypeList<>, ServerContext& server_context, const Fn&
|
||||
// thread.
|
||||
KJ_IF_MAYBE (thread_server, perhaps) {
|
||||
const auto& thread = static_cast<ProxyServer<Thread>&>(*thread_server);
|
||||
server.m_context.loop->log()
|
||||
MP_LOG(*server.m_context.loop, Log::Debug)
|
||||
<< "IPC server post request #" << req << " {" << thread.m_thread_context.thread_name << "}";
|
||||
thread.m_thread_context.waiter->post(std::move(invoke));
|
||||
if (!thread.m_thread_context.waiter->post(std::move(invoke))) {
|
||||
MP_LOG(*server.m_context.loop, Log::Error)
|
||||
<< "IPC server error request #" << req
|
||||
<< " {" << thread.m_thread_context.thread_name << "}" << ", thread busy";
|
||||
throw std::runtime_error("thread busy");
|
||||
}
|
||||
} else {
|
||||
server.m_context.loop->log()
|
||||
MP_LOG(*server.m_context.loop, Log::Error)
|
||||
<< "IPC server error request #" << req << ", missing thread to execute request";
|
||||
throw std::runtime_error("invalid thread handle");
|
||||
}
|
||||
|
||||
@@ -182,6 +182,17 @@ public:
|
||||
std::unique_lock<std::mutex> m_lock;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
struct GuardedRef
|
||||
{
|
||||
Mutex& mutex;
|
||||
T& ref MP_GUARDED_BY(mutex);
|
||||
};
|
||||
|
||||
// CTAD for Clang 16: GuardedRef{mutex, x} -> GuardedRef<decltype(x)>
|
||||
template <class U>
|
||||
GuardedRef(Mutex&, U&) -> GuardedRef<U>;
|
||||
|
||||
//! Analog to std::lock_guard that unlocks instead of locks.
|
||||
template <typename Lock>
|
||||
struct UnlockGuard
|
||||
|
||||
@@ -3,11 +3,19 @@
|
||||
, enableLibcxx ? false # Whether to use libc++ toolchain and libraries instead of libstdc++
|
||||
, minimal ? false # Whether to create minimal shell without extra tools (faster when cross compiling)
|
||||
, capnprotoVersion ? null
|
||||
, capnprotoSanitizers ? null # Optional sanitizers to build cap'n proto with
|
||||
, cmakeVersion ? null
|
||||
, libcxxSanitizers ? null # Optional LLVM_USE_SANITIZER value to use for libc++, see https://llvm.org/docs/CMake.html
|
||||
}:
|
||||
|
||||
let
|
||||
lib = pkgs.lib;
|
||||
llvm = crossPkgs.llvmPackages_20;
|
||||
llvmBase = crossPkgs.llvmPackages_21;
|
||||
llvm = llvmBase // lib.optionalAttrs (libcxxSanitizers != null) {
|
||||
libcxx = llvmBase.libcxx.override {
|
||||
devExtraCmakeFlags = [ "-DLLVM_USE_SANITIZER=${libcxxSanitizers}" ];
|
||||
};
|
||||
};
|
||||
capnprotoHashes = {
|
||||
"0.7.0" = "sha256-Y/7dUOQPDHjniuKNRw3j8dG1NI9f/aRWpf8V0WzV9k8=";
|
||||
"0.7.1" = "sha256-3cBpVmpvCXyqPUXDp12vCFCk32ZXWpkdOliNH37UwWE=";
|
||||
@@ -34,15 +42,36 @@ let
|
||||
} // (lib.optionalAttrs (lib.versionOlder capnprotoVersion "0.10") {
|
||||
env = { }; # Drop -std=c++20 flag forced by nixpkgs
|
||||
}));
|
||||
capnproto = capnprotoBase.override (lib.optionalAttrs enableLibcxx { clangStdenv = llvm.libcxxStdenv; });
|
||||
capnproto = (capnprotoBase.overrideAttrs (old: lib.optionalAttrs (capnprotoSanitizers != null) {
|
||||
env = (old.env or { }) // {
|
||||
CXXFLAGS =
|
||||
lib.concatStringsSep " " [
|
||||
(old.env.CXXFLAGS or "")
|
||||
"-fsanitize=${capnprotoSanitizers}"
|
||||
"-fno-omit-frame-pointer"
|
||||
"-g"
|
||||
];
|
||||
};
|
||||
})).override (lib.optionalAttrs enableLibcxx { clangStdenv = llvm.libcxxStdenv; });
|
||||
clang = if enableLibcxx then llvm.libcxxClang else llvm.clang;
|
||||
clang-tools = llvm.clang-tools.override { inherit enableLibcxx; };
|
||||
cmakeHashes = {
|
||||
"3.12.4" = "sha256-UlVYS/0EPrcXViz/iULUcvHA5GecSUHYS6raqbKOMZQ=";
|
||||
};
|
||||
cmakeBuild = if cmakeVersion == null then pkgs.cmake else (pkgs.cmake.overrideAttrs (old: {
|
||||
version = cmakeVersion;
|
||||
src = pkgs.fetchurl {
|
||||
url = "https://cmake.org/files/v${lib.versions.majorMinor cmakeVersion}/cmake-${cmakeVersion}.tar.gz";
|
||||
hash = lib.attrByPath [cmakeVersion] "" cmakeHashes;
|
||||
};
|
||||
patches = [];
|
||||
})).override { isMinimalBuild = true; };
|
||||
in crossPkgs.mkShell {
|
||||
buildInputs = [
|
||||
capnproto
|
||||
];
|
||||
nativeBuildInputs = with pkgs; [
|
||||
cmake
|
||||
cmakeBuild
|
||||
include-what-you-use
|
||||
ninja
|
||||
] ++ lib.optionals (!minimal) [
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <capnp/capability.h>
|
||||
#include <capnp/common.h> // IWYU pragma: keep
|
||||
#include <capnp/rpc.h>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
@@ -23,9 +24,9 @@
|
||||
#include <kj/debug.h>
|
||||
#include <kj/function.h>
|
||||
#include <kj/memory.h>
|
||||
#include <kj/string.h>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@@ -42,7 +43,7 @@ thread_local ThreadContext g_thread_context;
|
||||
void LoggingErrorHandler::taskFailed(kj::Exception&& exception)
|
||||
{
|
||||
KJ_LOG(ERROR, "Uncaught exception in daemonized task.", exception);
|
||||
m_loop.log() << "Uncaught exception in daemonized task.";
|
||||
MP_LOG(m_loop, Log::Error) << "Uncaught exception in daemonized task.";
|
||||
}
|
||||
|
||||
EventLoopRef::EventLoopRef(EventLoop& loop, Lock* lock) : m_loop(&loop), m_lock(lock)
|
||||
@@ -81,6 +82,11 @@ ProxyContext::ProxyContext(Connection* connection) : connection(connection), loo
|
||||
|
||||
Connection::~Connection()
|
||||
{
|
||||
// Connection destructor is always called on the event loop thread. If this
|
||||
// is a local disconnect, it will trigger I/O, so this needs to run on the
|
||||
// event loop thread, and if there was a remote disconnect, this is called
|
||||
// by an onDisconnect callback directly from the event loop thread.
|
||||
assert(std::this_thread::get_id() == m_loop->m_thread_id);
|
||||
// Shut down RPC system first, since this will garbage collect any
|
||||
// ProxyServer objects that were not freed before the connection was closed.
|
||||
// Typically all ProxyServer objects associated with this connection will be
|
||||
@@ -156,6 +162,9 @@ CleanupIt Connection::addSyncCleanup(std::function<void()> fn)
|
||||
|
||||
void Connection::removeSyncCleanup(CleanupIt it)
|
||||
{
|
||||
// Require cleanup functions to be removed on the event loop thread to avoid
|
||||
// needing to deal with them being removed in the middle of a disconnect.
|
||||
assert(std::this_thread::get_id() == m_loop->m_thread_id);
|
||||
const Lock lock(m_loop->m_mutex);
|
||||
m_sync_cleanup_fns.erase(it);
|
||||
}
|
||||
@@ -183,13 +192,13 @@ void EventLoop::addAsyncCleanup(std::function<void()> fn)
|
||||
startAsyncThread();
|
||||
}
|
||||
|
||||
EventLoop::EventLoop(const char* exe_name, LogFn log_fn, void* context)
|
||||
EventLoop::EventLoop(const char* exe_name, LogOptions log_opts, void* context)
|
||||
: m_exe_name(exe_name),
|
||||
m_io_context(kj::setupAsyncIo()),
|
||||
m_task_set(new kj::TaskSet(m_error_handler)),
|
||||
m_log_opts(std::move(log_opts)),
|
||||
m_context(context)
|
||||
{
|
||||
m_log_opts.log_fn = log_fn;
|
||||
int fds[2];
|
||||
KJ_SYSCALL(socketpair(AF_UNIX, SOCK_STREAM, 0, fds));
|
||||
m_wait_fd = fds[0];
|
||||
@@ -243,9 +252,9 @@ void EventLoop::loop()
|
||||
break;
|
||||
}
|
||||
}
|
||||
log() << "EventLoop::loop done, cancelling event listeners.";
|
||||
MP_LOG(*this, Log::Info) << "EventLoop::loop done, cancelling event listeners.";
|
||||
m_task_set.reset();
|
||||
log() << "EventLoop::loop bye.";
|
||||
MP_LOG(*this, Log::Info) << "EventLoop::loop bye.";
|
||||
wait_stream = nullptr;
|
||||
KJ_SYSCALL(::close(post_fd));
|
||||
const Lock lock(m_mutex);
|
||||
@@ -305,29 +314,34 @@ bool EventLoop::done() const
|
||||
return m_num_clients == 0 && m_async_fns->empty();
|
||||
}
|
||||
|
||||
std::tuple<ConnThread, bool> SetThread(ConnThreads& threads, std::mutex& mutex, Connection* connection, const std::function<Thread::Client()>& make_thread)
|
||||
std::tuple<ConnThread, bool> SetThread(GuardedRef<ConnThreads> threads, Connection* connection, const std::function<Thread::Client()>& make_thread)
|
||||
{
|
||||
const std::unique_lock<std::mutex> lock(mutex);
|
||||
auto thread = threads.find(connection);
|
||||
if (thread != threads.end()) return {thread, false};
|
||||
thread = threads.emplace(
|
||||
std::piecewise_construct, std::forward_as_tuple(connection),
|
||||
std::forward_as_tuple(make_thread(), connection, /* destroy_connection= */ false)).first;
|
||||
thread->second.setDisconnectCallback([&threads, &mutex, thread] {
|
||||
// Note: it is safe to use the `thread` iterator in this cleanup
|
||||
// function, because the iterator would only be invalid if the map entry
|
||||
// was removed, and if the map entry is removed the ProxyClient<Thread>
|
||||
// destructor unregisters the cleanup.
|
||||
assert(std::this_thread::get_id() == connection->m_loop->m_thread_id);
|
||||
ConnThread thread;
|
||||
bool inserted;
|
||||
{
|
||||
const Lock lock(threads.mutex);
|
||||
std::tie(thread, inserted) = threads.ref.try_emplace(connection);
|
||||
}
|
||||
if (inserted) {
|
||||
thread->second.emplace(make_thread(), connection, /* destroy_connection= */ false);
|
||||
thread->second->m_disconnect_cb = connection->addSyncCleanup([threads, thread] {
|
||||
// Note: it is safe to use the `thread` iterator in this cleanup
|
||||
// function, because the iterator would only be invalid if the map entry
|
||||
// was removed, and if the map entry is removed the ProxyClient<Thread>
|
||||
// destructor unregisters the cleanup.
|
||||
|
||||
// Connection is being destroyed before thread client is, so reset
|
||||
// thread client m_disconnect_cb member so thread client destructor does not
|
||||
// try to unregister this callback after connection is destroyed.
|
||||
// Remove connection pointer about to be destroyed from the map
|
||||
const std::unique_lock<std::mutex> lock(mutex);
|
||||
thread->second.m_disconnect_cb.reset();
|
||||
threads.erase(thread);
|
||||
});
|
||||
return {thread, true};
|
||||
// Connection is being destroyed before thread client is, so reset
|
||||
// thread client m_disconnect_cb member so thread client destructor does not
|
||||
// try to unregister this callback after connection is destroyed.
|
||||
thread->second->m_disconnect_cb.reset();
|
||||
|
||||
// Remove connection pointer about to be destroyed from the map
|
||||
const Lock lock(threads.mutex);
|
||||
threads.ref.erase(thread);
|
||||
});
|
||||
}
|
||||
return {thread, inserted};
|
||||
}
|
||||
|
||||
ProxyClient<Thread>::~ProxyClient()
|
||||
@@ -336,17 +350,18 @@ ProxyClient<Thread>::~ProxyClient()
|
||||
// cleanup callback that was registered to handle the connection being
|
||||
// destroyed before the thread being destroyed.
|
||||
if (m_disconnect_cb) {
|
||||
m_context.connection->removeSyncCleanup(*m_disconnect_cb);
|
||||
// Remove disconnect callback on the event loop thread with
|
||||
// loop->sync(), so if the connection is broken there is not a race
|
||||
// between this thread trying to remove the callback and the disconnect
|
||||
// handler attempting to call it.
|
||||
m_context.loop->sync([&]() {
|
||||
if (m_disconnect_cb) {
|
||||
m_context.connection->removeSyncCleanup(*m_disconnect_cb);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void ProxyClient<Thread>::setDisconnectCallback(const std::function<void()>& fn)
|
||||
{
|
||||
assert(fn);
|
||||
assert(!m_disconnect_cb);
|
||||
m_disconnect_cb = m_context.connection->addSyncCleanup(fn);
|
||||
}
|
||||
|
||||
ProxyServer<Thread>::ProxyServer(ThreadContext& thread_context, std::thread&& thread)
|
||||
: m_thread_context(thread_context), m_thread(std::move(thread))
|
||||
{
|
||||
@@ -364,7 +379,7 @@ ProxyServer<Thread>::~ProxyServer()
|
||||
assert(m_thread_context.waiter.get());
|
||||
std::unique_ptr<Waiter> waiter;
|
||||
{
|
||||
const std::unique_lock<std::mutex> lock(m_thread_context.waiter->m_mutex);
|
||||
const Lock lock(m_thread_context.waiter->m_mutex);
|
||||
//! Reset thread context waiter pointer, as shutdown signal for done
|
||||
//! lambda passed as waiter->wait() argument in makeThread code below.
|
||||
waiter = std::move(m_thread_context.waiter);
|
||||
@@ -398,7 +413,7 @@ kj::Promise<void> ProxyServer<ThreadMap>::makeThread(MakeThreadContext context)
|
||||
g_thread_context.thread_name = ThreadName(m_connection.m_loop->m_exe_name) + " (from " + from + ")";
|
||||
g_thread_context.waiter = std::make_unique<Waiter>();
|
||||
thread_context.set_value(&g_thread_context);
|
||||
std::unique_lock<std::mutex> lock(g_thread_context.waiter->m_mutex);
|
||||
Lock lock(g_thread_context.waiter->m_mutex);
|
||||
// Wait for shutdown signal from ProxyServer<Thread> destructor (signal
|
||||
// is just waiter getting set to null.)
|
||||
g_thread_context.waiter->wait(lock, [] { return !g_thread_context.waiter; });
|
||||
@@ -416,4 +431,16 @@ std::string LongThreadName(const char* exe_name)
|
||||
return g_thread_context.thread_name.empty() ? ThreadName(exe_name) : g_thread_context.thread_name;
|
||||
}
|
||||
|
||||
kj::StringPtr KJ_STRINGIFY(Log v)
|
||||
{
|
||||
switch (v) {
|
||||
case Log::Trace: return "Trace";
|
||||
case Log::Debug: return "Debug";
|
||||
case Log::Info: return "Info";
|
||||
case Log::Warning: return "Warning";
|
||||
case Log::Error: return "Error";
|
||||
case Log::Raise: return "Raise";
|
||||
}
|
||||
return "<Log?>";
|
||||
}
|
||||
} // namespace mp
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <kj/common.h>
|
||||
#include <kj/string-tree.h>
|
||||
#include <pthread.h>
|
||||
@@ -29,6 +31,8 @@
|
||||
#include <pthread_np.h>
|
||||
#endif // HAVE_PTHREAD_GETTHREADID_NP
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace mp {
|
||||
namespace {
|
||||
|
||||
@@ -138,6 +142,9 @@ void ExecProcess(const std::vector<std::string>& args)
|
||||
argv.push_back(nullptr);
|
||||
if (execvp(argv[0], argv.data()) != 0) {
|
||||
perror("execvp failed");
|
||||
if (errno == ENOENT && !args.empty()) {
|
||||
std::cerr << "Missing executable: " << fs::weakly_canonical(args.front()) << '\n';
|
||||
}
|
||||
_exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ $Proxy.includeTypes("mp/test/foo-types.h");
|
||||
|
||||
interface FooInterface $Proxy.wrap("mp::test::FooImplementation") {
|
||||
add @0 (a :Int32, b :Int32) -> (result :Int32);
|
||||
addOut @19 (a :Int32, b :Int32) -> (ret :Int32);
|
||||
addInOut @20 (x :Int32, sum :Int32) -> (sum :Int32);
|
||||
mapSize @1 (map :List(Pair(Text, Text))) -> (result :Int32);
|
||||
pass @2 (arg :FooStruct) -> (result :FooStruct);
|
||||
raise @3 (arg :FooStruct) -> (error :FooStruct $Proxy.exception("mp::test::FooStruct"));
|
||||
|
||||
@@ -62,6 +62,8 @@ class FooImplementation
|
||||
{
|
||||
public:
|
||||
int add(int a, int b) { return a + b; }
|
||||
void addOut(int a, int b, int& out) { out = a + b; }
|
||||
void addInOut(int x, int& sum) { sum += x; }
|
||||
int mapSize(const std::map<std::string, std::string>& map) { return map.size(); }
|
||||
FooStruct pass(FooStruct foo) { return foo; }
|
||||
void raise(FooStruct foo) { throw foo; }
|
||||
|
||||
@@ -5,26 +5,32 @@
|
||||
#include <mp/test/foo.capnp.h>
|
||||
#include <mp/test/foo.capnp.proxy.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <capnp/capability.h>
|
||||
#include <capnp/rpc.h>
|
||||
#include <condition_variable>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <iostream>
|
||||
#include <kj/async.h>
|
||||
#include <kj/async-io.h>
|
||||
#include <kj/common.h>
|
||||
#include <kj/debug.h>
|
||||
#include <kj/exception.h>
|
||||
#include <kj/memory.h>
|
||||
#include <kj/string.h>
|
||||
#include <kj/test.h>
|
||||
#include <memory>
|
||||
#include <mp/proxy.h>
|
||||
#include <mp/proxy.capnp.h>
|
||||
#include <mp/proxy-io.h>
|
||||
#include <mp/util.h>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -60,9 +66,10 @@ public:
|
||||
|
||||
TestSetup(bool client_owns_connection = true)
|
||||
: thread{[&] {
|
||||
EventLoop loop("mptest", [](bool raise, const std::string& log) {
|
||||
std::cout << "LOG" << raise << ": " << log << "\n";
|
||||
if (raise) throw std::runtime_error(log);
|
||||
EventLoop loop("mptest", [](mp::LogMessage log) {
|
||||
// Info logs are not printed by default, but will be shown with `mptest --verbose`
|
||||
KJ_LOG(INFO, log.level, log.message);
|
||||
if (log.level == mp::Log::Raise) throw std::runtime_error(log.message);
|
||||
});
|
||||
auto pipe = loop.m_io_context.provider->newTwoWayPipe();
|
||||
|
||||
@@ -113,6 +120,11 @@ KJ_TEST("Call FooInterface methods")
|
||||
ProxyClient<messages::FooInterface>* foo = setup.client.get();
|
||||
|
||||
KJ_EXPECT(foo->add(1, 2) == 3);
|
||||
int ret;
|
||||
foo->addOut(3, 4, ret);
|
||||
KJ_EXPECT(ret == 7);
|
||||
foo->addInOut(3, ret);
|
||||
KJ_EXPECT(ret == 10);
|
||||
|
||||
FooStruct in;
|
||||
in.name = "name";
|
||||
@@ -297,5 +309,71 @@ KJ_TEST("Calling IPC method, disconnecting and blocking during the call")
|
||||
signal.set_value();
|
||||
}
|
||||
|
||||
KJ_TEST("Make simultaneous IPC calls to trigger 'thread busy' error")
|
||||
{
|
||||
TestSetup setup;
|
||||
ProxyClient<messages::FooInterface>* foo = setup.client.get();
|
||||
std::promise<void> signal;
|
||||
|
||||
foo->initThreadMap();
|
||||
// Use callFnAsync() to get the client to set up the request_thread
|
||||
// that will be used for the test.
|
||||
setup.server->m_impl->m_fn = [&] {};
|
||||
foo->callFnAsync();
|
||||
ThreadContext& tc{g_thread_context};
|
||||
Thread::Client *callback_thread, *request_thread;
|
||||
foo->m_context.loop->sync([&] {
|
||||
Lock lock(tc.waiter->m_mutex);
|
||||
callback_thread = &tc.callback_threads.at(foo->m_context.connection)->m_client;
|
||||
request_thread = &tc.request_threads.at(foo->m_context.connection)->m_client;
|
||||
});
|
||||
|
||||
setup.server->m_impl->m_fn = [&] {
|
||||
try
|
||||
{
|
||||
signal.get_future().get();
|
||||
}
|
||||
catch (const std::future_error& e)
|
||||
{
|
||||
KJ_EXPECT(e.code() == std::make_error_code(std::future_errc::future_already_retrieved));
|
||||
}
|
||||
};
|
||||
|
||||
auto client{foo->m_client};
|
||||
bool caught_thread_busy = false;
|
||||
// NOTE: '3' was chosen because it was the lowest number
|
||||
// of simultaneous calls required to reliably catch a "thread busy" error
|
||||
std::atomic<size_t> running{3};
|
||||
foo->m_context.loop->sync([&]
|
||||
{
|
||||
for (size_t i = 0; i < running; i++)
|
||||
{
|
||||
auto request{client.callFnAsyncRequest()};
|
||||
auto context{request.initContext()};
|
||||
context.setCallbackThread(*callback_thread);
|
||||
context.setThread(*request_thread);
|
||||
foo->m_context.loop->m_task_set->add(request.send().then(
|
||||
[&](auto&& results) {
|
||||
running -= 1;
|
||||
tc.waiter->m_cv.notify_all();
|
||||
},
|
||||
[&](kj::Exception&& e) {
|
||||
KJ_EXPECT(std::string_view{e.getDescription().cStr()} ==
|
||||
"remote exception: std::exception: thread busy");
|
||||
caught_thread_busy = true;
|
||||
running -= 1;
|
||||
signal.set_value();
|
||||
tc.waiter->m_cv.notify_all();
|
||||
}
|
||||
));
|
||||
}
|
||||
});
|
||||
{
|
||||
Lock lock(tc.waiter->m_mutex);
|
||||
tc.waiter->wait(lock, [&running] { return running == 0; });
|
||||
}
|
||||
KJ_EXPECT(caught_thread_busy);
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace mp
|
||||
|
||||
@@ -147,7 +147,6 @@ public:
|
||||
// release ASAP to avoid it where possible.
|
||||
vSeeds.emplace_back("seed.bitcoin.sipa.be."); // Pieter Wuille, only supports x1, x5, x9, and xd
|
||||
vSeeds.emplace_back("dnsseed.bluematt.me."); // Matt Corallo, only supports x9
|
||||
vSeeds.emplace_back("dnsseed.bitcoin.dashjr-list-of-p2p-nodes.us."); // Luke Dashjr
|
||||
vSeeds.emplace_back("seed.bitcoin.jonasschnelli.ch."); // Jonas Schnelli, only supports x1, x5, x9, and xd
|
||||
vSeeds.emplace_back("seed.btc.petertodd.net."); // Peter Todd, only supports x1, x5, x9, and xd
|
||||
vSeeds.emplace_back("seed.bitcoin.sprovoost.nl."); // Sjors Provoost
|
||||
|
||||
@@ -132,7 +132,6 @@ public:
|
||||
}
|
||||
void appShutdown() override
|
||||
{
|
||||
Interrupt(*m_context);
|
||||
Shutdown(*m_context);
|
||||
}
|
||||
void startShutdown() override
|
||||
@@ -141,12 +140,7 @@ public:
|
||||
if (!(Assert(ctx.shutdown_request))()) {
|
||||
LogError("Failed to send shutdown signal\n");
|
||||
}
|
||||
|
||||
// Stop RPC for clean shutdown if any of waitfor* commands is executed.
|
||||
if (args().GetBoolArg("-server", false)) {
|
||||
InterruptRPC();
|
||||
StopRPC();
|
||||
}
|
||||
Interrupt(*m_context);
|
||||
}
|
||||
bool shutdownRequested() override { return ShutdownRequested(*Assert(m_context)); };
|
||||
bool isSettingIgnored(const std::string& name) override
|
||||
@@ -918,15 +912,21 @@ public:
|
||||
|
||||
std::unique_ptr<BlockTemplate> waitNext(BlockWaitOptions options) override
|
||||
{
|
||||
auto new_template = WaitAndCreateNewBlock(chainman(), notifications(), m_node.mempool.get(), m_block_template, options, m_assemble_options);
|
||||
auto new_template = WaitAndCreateNewBlock(chainman(), notifications(), m_node.mempool.get(), m_block_template, options, m_assemble_options, m_interrupt_wait);
|
||||
if (new_template) return std::make_unique<BlockTemplateImpl>(m_assemble_options, std::move(new_template), m_node);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void interruptWait() override
|
||||
{
|
||||
InterruptWait(notifications(), m_interrupt_wait);
|
||||
}
|
||||
|
||||
const BlockAssembler::Options m_assemble_options;
|
||||
|
||||
const std::unique_ptr<CBlockTemplate> m_block_template;
|
||||
|
||||
bool m_interrupt_wait{false};
|
||||
ChainstateManager& chainman() { return *Assert(m_node.chainman); }
|
||||
KernelNotifications& notifications() { return *Assert(m_node.notifications); }
|
||||
NodeContext& m_node;
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
#include <numeric>
|
||||
|
||||
namespace node {
|
||||
|
||||
@@ -453,12 +454,20 @@ void AddMerkleRootAndCoinbase(CBlock& block, CTransactionRef coinbase, uint32_t
|
||||
block.hashMerkleRoot = BlockMerkleRoot(block);
|
||||
}
|
||||
|
||||
void InterruptWait(KernelNotifications& kernel_notifications, bool& interrupt_wait)
|
||||
{
|
||||
LOCK(kernel_notifications.m_tip_block_mutex);
|
||||
interrupt_wait = true;
|
||||
kernel_notifications.m_tip_block_cv.notify_all();
|
||||
}
|
||||
|
||||
std::unique_ptr<CBlockTemplate> WaitAndCreateNewBlock(ChainstateManager& chainman,
|
||||
KernelNotifications& kernel_notifications,
|
||||
CTxMemPool* mempool,
|
||||
const std::unique_ptr<CBlockTemplate>& block_template,
|
||||
const BlockWaitOptions& options,
|
||||
const BlockAssembler::Options& assemble_options)
|
||||
const BlockAssembler::Options& assemble_options,
|
||||
bool& interrupt_wait)
|
||||
{
|
||||
// Delay calculating the current template fees, just in case a new block
|
||||
// comes in before the next tick.
|
||||
@@ -483,8 +492,12 @@ std::unique_ptr<CBlockTemplate> WaitAndCreateNewBlock(ChainstateManager& chainma
|
||||
// method on BlockTemplate and no template could have been
|
||||
// generated before a tip exists.
|
||||
tip_changed = Assume(tip_block) && tip_block != block_template->block.hashPrevBlock;
|
||||
return tip_changed || chainman.m_interrupt;
|
||||
return tip_changed || chainman.m_interrupt || interrupt_wait;
|
||||
});
|
||||
if (interrupt_wait) {
|
||||
interrupt_wait = false;
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (chainman.m_interrupt) return nullptr;
|
||||
@@ -522,18 +535,13 @@ std::unique_ptr<CBlockTemplate> WaitAndCreateNewBlock(ChainstateManager& chainma
|
||||
|
||||
// Calculate the original template total fees if we haven't already
|
||||
if (current_fees == -1) {
|
||||
current_fees = 0;
|
||||
for (CAmount fee : block_template->vTxFees) {
|
||||
current_fees += fee;
|
||||
}
|
||||
current_fees = std::accumulate(block_template->vTxFees.begin(), block_template->vTxFees.end(), CAmount{0});
|
||||
}
|
||||
|
||||
CAmount new_fees = 0;
|
||||
for (CAmount fee : new_tmpl->vTxFees) {
|
||||
new_fees += fee;
|
||||
Assume(options.fee_threshold != MAX_MONEY);
|
||||
if (new_fees >= current_fees + options.fee_threshold) return new_tmpl;
|
||||
}
|
||||
// Check if fees increased enough to return the new template
|
||||
const CAmount new_fees = std::accumulate(new_tmpl->vTxFees.begin(), new_tmpl->vTxFees.end(), CAmount{0});
|
||||
Assume(options.fee_threshold != MAX_MONEY);
|
||||
if (new_fees >= current_fees + options.fee_threshold) return new_tmpl;
|
||||
}
|
||||
|
||||
now = NodeClock::now();
|
||||
|
||||
@@ -238,6 +238,9 @@ void ApplyArgsManOptions(const ArgsManager& gArgs, BlockAssembler::Options& opti
|
||||
/* Compute the block's merkle root, insert or replace the coinbase transaction and the merkle root into the block */
|
||||
void AddMerkleRootAndCoinbase(CBlock& block, CTransactionRef coinbase, uint32_t version, uint32_t timestamp, uint32_t nonce);
|
||||
|
||||
|
||||
/* Interrupt the current wait for the next block template. */
|
||||
void InterruptWait(KernelNotifications& kernel_notifications, bool& interrupt_wait);
|
||||
/**
|
||||
* Return a new block template when fees rise to a certain threshold or after a
|
||||
* new tip; return nullopt if timeout is reached.
|
||||
@@ -247,7 +250,8 @@ std::unique_ptr<CBlockTemplate> WaitAndCreateNewBlock(ChainstateManager& chainma
|
||||
CTxMemPool* mempool,
|
||||
const std::unique_ptr<CBlockTemplate>& block_template,
|
||||
const BlockWaitOptions& options,
|
||||
const BlockAssembler::Options& assemble_options);
|
||||
const BlockAssembler::Options& assemble_options,
|
||||
bool& interrupt_wait);
|
||||
|
||||
/* Locks cs_main and returns the block hash and block height of the active chain if it exists; otherwise, returns nullopt.*/
|
||||
std::optional<BlockRef> GetTip(ChainstateManager& chainman);
|
||||
|
||||
@@ -72,6 +72,9 @@ namespace {
|
||||
|
||||
// don't add private key handling cmd's to the history
|
||||
const QStringList historyFilter = QStringList()
|
||||
<< "createwallet"
|
||||
<< "createwalletdescriptor"
|
||||
<< "migratewallet"
|
||||
<< "signmessagewithprivkey"
|
||||
<< "signrawtransactionwithkey"
|
||||
<< "walletpassphrase"
|
||||
|
||||
@@ -52,32 +52,78 @@ bool TransactionFilterProxy::filterAcceptsRow(int sourceRow, const QModelIndex &
|
||||
|
||||
void TransactionFilterProxy::setDateRange(const std::optional<QDateTime>& from, const std::optional<QDateTime>& to)
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
|
||||
beginFilterChange();
|
||||
#endif
|
||||
|
||||
dateFrom = from;
|
||||
dateTo = to;
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
|
||||
endFilterChange(QSortFilterProxyModel::Direction::Rows);
|
||||
#else
|
||||
invalidateFilter();
|
||||
#endif
|
||||
}
|
||||
|
||||
void TransactionFilterProxy::setSearchString(const QString &search_string)
|
||||
{
|
||||
if (m_search_string == search_string) return;
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
|
||||
beginFilterChange();
|
||||
#endif
|
||||
|
||||
m_search_string = search_string;
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
|
||||
endFilterChange(QSortFilterProxyModel::Direction::Rows);
|
||||
#else
|
||||
invalidateFilter();
|
||||
#endif
|
||||
}
|
||||
|
||||
void TransactionFilterProxy::setTypeFilter(quint32 modes)
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
|
||||
beginFilterChange();
|
||||
#endif
|
||||
|
||||
this->typeFilter = modes;
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
|
||||
endFilterChange(QSortFilterProxyModel::Direction::Rows);
|
||||
#else
|
||||
invalidateFilter();
|
||||
#endif
|
||||
}
|
||||
|
||||
void TransactionFilterProxy::setMinAmount(const CAmount& minimum)
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
|
||||
beginFilterChange();
|
||||
#endif
|
||||
|
||||
this->minAmount = minimum;
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
|
||||
endFilterChange(QSortFilterProxyModel::Direction::Rows);
|
||||
#else
|
||||
invalidateFilter();
|
||||
#endif
|
||||
}
|
||||
|
||||
void TransactionFilterProxy::setShowInactive(bool _showInactive)
|
||||
{
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
|
||||
beginFilterChange();
|
||||
#endif
|
||||
|
||||
this->showInactive = _showInactive;
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
|
||||
endFilterChange(QSortFilterProxyModel::Direction::Rows);
|
||||
#else
|
||||
invalidateFilter();
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -936,8 +936,9 @@ static RPCHelpMan submitpackage()
|
||||
,
|
||||
{
|
||||
{"package", RPCArg::Type::ARR, RPCArg::Optional::NO, "An array of raw transactions.\n"
|
||||
"The package must solely consist of a child transaction and all of its unconfirmed parents, if any. None of the parents may depend on each other.\n"
|
||||
"The package must be topologically sorted, with the child being the last element in the array.",
|
||||
"The package must consist of a transaction with (some, all, or none of) its unconfirmed parents. A single transaction is permitted.\n"
|
||||
"None of the parents may depend on each other. Parents that are already in mempool do not need to be present in the package.\n"
|
||||
"The package must be topologically sorted, with the child being the last element in the array if there are multiple elements.",
|
||||
{
|
||||
{"rawtx", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, ""},
|
||||
},
|
||||
|
||||
@@ -51,7 +51,7 @@ static std::vector<const char*> g_args;
|
||||
static void SetArgs(int argc, char** argv) {
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
// Only take into account arguments that start with `--`. The others are for the fuzz engine:
|
||||
// `fuzz -runs=1 fuzz_corpora/address_deserialize_v2 --checkaddrman=5`
|
||||
// `fuzz -runs=1 fuzz_corpora/address_deserialize --checkaddrman=5`
|
||||
if (strlen(argv[i]) > 2 && argv[i][0] == '-' && argv[i][1] == '-') {
|
||||
g_args.push_back(argv[i]);
|
||||
}
|
||||
|
||||
@@ -113,6 +113,22 @@ void MinerTestingSetup::TestPackageSelection(const CScript& scriptPubKey, const
|
||||
options.coinbase_output_script = scriptPubKey;
|
||||
|
||||
LOCK(tx_mempool.cs);
|
||||
BOOST_CHECK(tx_mempool.size() == 0);
|
||||
|
||||
// Block template should only have a coinbase when there's nothing in the mempool
|
||||
std::unique_ptr<BlockTemplate> block_template = mining->createNewBlock(options);
|
||||
BOOST_REQUIRE(block_template);
|
||||
CBlock block{block_template->getBlock()};
|
||||
BOOST_REQUIRE_EQUAL(block.vtx.size(), 1U);
|
||||
|
||||
// waitNext() on an empty mempool should return nullptr because there is no better template
|
||||
auto should_be_nullptr = block_template->waitNext({.timeout = MillisecondsDouble{0}, .fee_threshold = 1});
|
||||
BOOST_REQUIRE(should_be_nullptr == nullptr);
|
||||
|
||||
// Unless fee_threshold is 0
|
||||
block_template = block_template->waitNext({.timeout = MillisecondsDouble{0}, .fee_threshold = 0});
|
||||
BOOST_REQUIRE(block_template);
|
||||
|
||||
// Test the ancestor feerate transaction selection.
|
||||
TestMemPoolEntryHelper entry;
|
||||
|
||||
@@ -144,9 +160,9 @@ void MinerTestingSetup::TestPackageSelection(const CScript& scriptPubKey, const
|
||||
const auto high_fee_tx{entry.Fee(50000).Time(Now<NodeSeconds>()).SpendsCoinbase(false).FromTx(tx)};
|
||||
AddToMempool(tx_mempool, high_fee_tx);
|
||||
|
||||
std::unique_ptr<BlockTemplate> block_template = mining->createNewBlock(options);
|
||||
block_template = mining->createNewBlock(options);
|
||||
BOOST_REQUIRE(block_template);
|
||||
CBlock block{block_template->getBlock()};
|
||||
block = block_template->getBlock();
|
||||
BOOST_REQUIRE_EQUAL(block.vtx.size(), 4U);
|
||||
BOOST_CHECK(block.vtx[1]->GetHash() == hashParentTx);
|
||||
BOOST_CHECK(block.vtx[2]->GetHash() == hashHighFeeTx);
|
||||
@@ -184,7 +200,7 @@ void MinerTestingSetup::TestPackageSelection(const CScript& scriptPubKey, const
|
||||
AddToMempool(tx_mempool, entry.Fee(feeToUse).FromTx(tx));
|
||||
|
||||
// waitNext() should return nullptr because there is no better template
|
||||
auto should_be_nullptr = block_template->waitNext({.timeout = MillisecondsDouble{0}, .fee_threshold = 1});
|
||||
should_be_nullptr = block_template->waitNext({.timeout = MillisecondsDouble{0}, .fee_threshold = 1});
|
||||
BOOST_REQUIRE(should_be_nullptr == nullptr);
|
||||
|
||||
block = block_template->getBlock();
|
||||
|
||||
@@ -276,11 +276,17 @@ bool CreateFromDump(const ArgsManager& args, const std::string& name, const fs::
|
||||
|
||||
dump_file.close();
|
||||
}
|
||||
// On failure, gather the paths to remove
|
||||
std::vector<fs::path> paths_to_remove = wallet->GetDatabase().Files();
|
||||
if (!name.empty()) paths_to_remove.push_back(wallet_path);
|
||||
|
||||
wallet.reset(); // The pointer deleter will close the wallet for us.
|
||||
|
||||
// Remove the wallet dir if we have a failure
|
||||
if (!ret) {
|
||||
fs::remove_all(wallet_path);
|
||||
for (const auto& p : paths_to_remove) {
|
||||
fs::remove(p);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
@@ -403,6 +403,11 @@ CoinsResult AvailableCoins(const CWallet& wallet,
|
||||
if (wtx.tx->version != TRUC_VERSION) continue;
|
||||
// this unconfirmed v3 transaction already has a child
|
||||
if (wtx.truc_child_in_mempool.has_value()) continue;
|
||||
|
||||
// this unconfirmed v3 transaction has a parent: spending would create a third generation
|
||||
size_t ancestors, descendants;
|
||||
wallet.chain().getTransactionAncestry(wtx.tx->GetHash(), ancestors, descendants);
|
||||
if (ancestors > 1) continue;
|
||||
} else {
|
||||
if (wtx.tx->version == TRUC_VERSION) continue;
|
||||
Assume(!wtx.truc_child_in_mempool.has_value());
|
||||
|
||||
@@ -469,6 +469,8 @@ std::shared_ptr<CWallet> RestoreWallet(WalletContext& context, const fs::path& b
|
||||
const fs::path wallet_path = fsbridge::AbsPathJoin(GetWalletDir(), fs::u8path(wallet_name));
|
||||
auto wallet_file = wallet_path / "wallet.dat";
|
||||
std::shared_ptr<CWallet> wallet;
|
||||
bool wallet_file_copied = false;
|
||||
bool created_parent_dir = false;
|
||||
|
||||
try {
|
||||
if (!fs::exists(backup_file)) {
|
||||
@@ -477,13 +479,34 @@ std::shared_ptr<CWallet> RestoreWallet(WalletContext& context, const fs::path& b
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (fs::exists(wallet_path) || !TryCreateDirectories(wallet_path)) {
|
||||
error = Untranslated(strprintf("Failed to create database path '%s'. Database already exists.", fs::PathToString(wallet_path)));
|
||||
status = DatabaseStatus::FAILED_ALREADY_EXISTS;
|
||||
return nullptr;
|
||||
// Wallet directories are allowed to exist, but must not contain a .dat file.
|
||||
// Any existing wallet database is treated as a hard failure to prevent overwriting.
|
||||
if (fs::exists(wallet_path)) {
|
||||
// If this is a file, it is the db and we don't want to overwrite it.
|
||||
if (!fs::is_directory(wallet_path)) {
|
||||
error = Untranslated(strprintf("Failed to restore wallet. Database file exists '%s'.", fs::PathToString(wallet_path)));
|
||||
status = DatabaseStatus::FAILED_ALREADY_EXISTS;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Check we are not going to overwrite an existing db file
|
||||
if (fs::exists(wallet_file)) {
|
||||
error = Untranslated(strprintf("Failed to restore wallet. Database file exists in '%s'.", fs::PathToString(wallet_file)));
|
||||
status = DatabaseStatus::FAILED_ALREADY_EXISTS;
|
||||
return nullptr;
|
||||
}
|
||||
} else {
|
||||
// The directory doesn't exist, create it
|
||||
if (!TryCreateDirectories(wallet_path)) {
|
||||
error = Untranslated(strprintf("Failed to restore database path '%s'.", fs::PathToString(wallet_path)));
|
||||
status = DatabaseStatus::FAILED_ALREADY_EXISTS;
|
||||
return nullptr;
|
||||
}
|
||||
created_parent_dir = true;
|
||||
}
|
||||
|
||||
fs::copy_file(backup_file, wallet_file, fs::copy_options::none);
|
||||
wallet_file_copied = true;
|
||||
|
||||
if (load_after_restore) {
|
||||
wallet = LoadWallet(context, wallet_name, load_on_start, options, status, error, warnings);
|
||||
@@ -496,7 +519,13 @@ std::shared_ptr<CWallet> RestoreWallet(WalletContext& context, const fs::path& b
|
||||
|
||||
// Remove created wallet path only when loading fails
|
||||
if (load_after_restore && !wallet) {
|
||||
fs::remove_all(wallet_path);
|
||||
if (wallet_file_copied) fs::remove(wallet_file);
|
||||
// Clean up the parent directory if we created it during restoration.
|
||||
// As we have created it, it must be empty after deleting the wallet file.
|
||||
if (created_parent_dir) {
|
||||
Assume(fs::is_empty(wallet_path));
|
||||
fs::remove(wallet_path);
|
||||
}
|
||||
}
|
||||
|
||||
return wallet;
|
||||
@@ -4057,6 +4086,15 @@ bool CWallet::CanGrindR() const
|
||||
return !IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS);
|
||||
}
|
||||
|
||||
// Returns wallet prefix for migration.
|
||||
// Used to name the backup file and newly created wallets.
|
||||
// E.g. a watch-only wallet is named "<prefix>_watchonly".
|
||||
static std::string MigrationPrefixName(CWallet& wallet)
|
||||
{
|
||||
const std::string& name{wallet.GetName()};
|
||||
return name.empty() ? "default_wallet" : name;
|
||||
}
|
||||
|
||||
bool DoMigration(CWallet& wallet, WalletContext& context, bilingual_str& error, MigrationResult& res) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
|
||||
{
|
||||
AssertLockHeld(wallet.cs_wallet);
|
||||
@@ -4088,7 +4126,7 @@ bool DoMigration(CWallet& wallet, WalletContext& context, bilingual_str& error,
|
||||
|
||||
DatabaseStatus status;
|
||||
std::vector<bilingual_str> warnings;
|
||||
std::string wallet_name = wallet.GetName() + "_watchonly";
|
||||
std::string wallet_name = MigrationPrefixName(wallet) + "_watchonly";
|
||||
std::unique_ptr<WalletDatabase> database = MakeWalletDatabase(wallet_name, options, status, error);
|
||||
if (!database) {
|
||||
error = strprintf(_("Wallet file creation failed: %s"), error);
|
||||
@@ -4127,7 +4165,7 @@ bool DoMigration(CWallet& wallet, WalletContext& context, bilingual_str& error,
|
||||
|
||||
DatabaseStatus status;
|
||||
std::vector<bilingual_str> warnings;
|
||||
std::string wallet_name = wallet.GetName() + "_solvables";
|
||||
std::string wallet_name = MigrationPrefixName(wallet) + "_solvables";
|
||||
std::unique_ptr<WalletDatabase> database = MakeWalletDatabase(wallet_name, options, status, error);
|
||||
if (!database) {
|
||||
error = strprintf(_("Wallet file creation failed: %s"), error);
|
||||
@@ -4242,7 +4280,7 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>
|
||||
// cases, but in the case where the wallet name is a path to a data file,
|
||||
// the name of the data file is used, and in the case where the wallet name
|
||||
// is blank, "default_wallet" is used.
|
||||
const std::string backup_prefix = wallet_name.empty() ? "default_wallet" : [&] {
|
||||
const std::string backup_prefix = wallet_name.empty() ? MigrationPrefixName(*local_wallet) : [&] {
|
||||
// fs::weakly_canonical resolves relative specifiers and remove trailing slashes.
|
||||
const auto legacy_wallet_path = fs::weakly_canonical(GetWalletDir() / fs::PathFromString(wallet_name));
|
||||
return fs::PathToString(legacy_wallet_path.filename());
|
||||
@@ -4295,11 +4333,28 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>
|
||||
}
|
||||
}
|
||||
|
||||
// In case of loading failure, we need to remember the wallet dirs to remove.
|
||||
// In case of loading failure, we need to remember the wallet files we have created to remove.
|
||||
// A `set` is used as it may be populated with the same wallet directory paths multiple times,
|
||||
// both before and after loading. This ensures the set is complete even if one of the wallets
|
||||
// fails to load.
|
||||
std::set<fs::path> wallet_dirs;
|
||||
std::set<fs::path> wallet_files_to_remove;
|
||||
std::set<fs::path> wallet_empty_dirs_to_remove;
|
||||
|
||||
// Helper to track wallet files and directories for cleanup on failure.
|
||||
// Only directories of wallets created during migration (not the main wallet) are tracked.
|
||||
auto track_for_cleanup = [&](const CWallet& wallet) {
|
||||
const auto files = wallet.GetDatabase().Files();
|
||||
wallet_files_to_remove.insert(files.begin(), files.end());
|
||||
if (wallet.GetName() != wallet_name) {
|
||||
// If this isn’t the main wallet, mark its directory for removal.
|
||||
// This applies to the watch-only and solvable wallets.
|
||||
// Wallets stored directly as files in the top-level directory
|
||||
// (e.g. default unnamed wallets) don’t have a removable parent directory.
|
||||
wallet_empty_dirs_to_remove.insert(fs::PathFromString(wallet.GetDatabase().Filename()).parent_path());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (success) {
|
||||
Assume(!res.wallet); // We will set it here.
|
||||
// Check if the local wallet is empty after migration
|
||||
@@ -4307,23 +4362,30 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>
|
||||
// This wallet has no records. We can safely remove it.
|
||||
std::vector<fs::path> paths_to_remove = local_wallet->GetDatabase().Files();
|
||||
local_wallet.reset();
|
||||
for (const auto& path_to_remove : paths_to_remove) fs::remove_all(path_to_remove);
|
||||
for (const auto& path_to_remove : paths_to_remove) fs::remove(path_to_remove);
|
||||
}
|
||||
|
||||
LogInfo("Loading new wallets after migration...\n");
|
||||
// Migration successful, load all the migrated wallets.
|
||||
for (std::shared_ptr<CWallet>* wallet_ptr : {&local_wallet, &res.watchonly_wallet, &res.solvables_wallet}) {
|
||||
if (success && *wallet_ptr) {
|
||||
std::shared_ptr<CWallet>& wallet = *wallet_ptr;
|
||||
// Save db path and load wallet
|
||||
wallet_dirs.insert(fs::PathFromString(wallet->GetDatabase().Filename()).parent_path());
|
||||
// Track db path and load wallet
|
||||
track_for_cleanup(*wallet);
|
||||
assert(wallet.use_count() == 1);
|
||||
std::string wallet_name = wallet->GetName();
|
||||
wallet.reset();
|
||||
wallet = LoadWallet(context, wallet_name, /*load_on_start=*/std::nullopt, options, status, error, warnings);
|
||||
success = (wallet != nullptr);
|
||||
if (!wallet) {
|
||||
LogError("Failed to load wallet '%s' after migration. Rolling back migration to preserve consistency. "
|
||||
"Error cause: %s\n", wallet_name, error.original);
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// When no wallet is set, set the main wallet.
|
||||
if (success && !res.wallet) {
|
||||
// Set the first successfully loaded wallet as the main one.
|
||||
// The loop order is intentional and must always start with the local wallet.
|
||||
if (!res.wallet) {
|
||||
res.wallet_name = wallet->GetName();
|
||||
res.wallet = std::move(wallet);
|
||||
}
|
||||
@@ -4338,8 +4400,8 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>
|
||||
if (res.solvables_wallet) created_wallets.push_back(std::move(res.solvables_wallet));
|
||||
|
||||
// Get the directories to remove after unloading
|
||||
for (std::shared_ptr<CWallet>& w : created_wallets) {
|
||||
wallet_dirs.emplace(fs::PathFromString(w->GetDatabase().Filename()).parent_path());
|
||||
for (std::shared_ptr<CWallet>& wallet : created_wallets) {
|
||||
track_for_cleanup(*wallet);
|
||||
}
|
||||
|
||||
// Unload the wallets
|
||||
@@ -4358,9 +4420,15 @@ util::Result<MigrationResult> MigrateLegacyToDescriptor(std::shared_ptr<CWallet>
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the wallet directories
|
||||
for (const fs::path& dir : wallet_dirs) {
|
||||
fs::remove_all(dir);
|
||||
// First, delete the db files we have created throughout this process and nothing else
|
||||
for (const fs::path& file : wallet_files_to_remove) {
|
||||
fs::remove(file);
|
||||
}
|
||||
|
||||
// Second, delete the created wallet directories and nothing else. They must be empty at this point.
|
||||
for (const fs::path& dir : wallet_empty_dirs_to_remove) {
|
||||
Assume(fs::is_empty(dir));
|
||||
fs::remove(dir);
|
||||
}
|
||||
|
||||
// Restore the backup
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
"""Tests related to node initialization."""
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
import os
|
||||
import platform
|
||||
@@ -240,9 +241,59 @@ class InitTest(BitcoinTestFramework):
|
||||
self.stop_node(0)
|
||||
assert not custom_pidfile_absolute.exists()
|
||||
|
||||
def break_wait_test(self):
|
||||
"""Test what happens when a break signal is sent during a
|
||||
waitforblockheight RPC call with a long timeout. Ctrl-Break is sent on
|
||||
Windows and SIGTERM is sent on other platforms, to trigger the same node
|
||||
shutdown sequence that would happen if Ctrl-C were pressed in a
|
||||
terminal. (This can be different than the node shutdown sequence that
|
||||
happens when the stop RPC is sent.)
|
||||
|
||||
The waitforblockheight call should be interrupted and return right away,
|
||||
and not time out."""
|
||||
|
||||
self.log.info("Testing waitforblockheight RPC call followed by break signal")
|
||||
node = self.nodes[0]
|
||||
|
||||
if platform.system() == 'Windows':
|
||||
# CREATE_NEW_PROCESS_GROUP prevents python test from exiting
|
||||
# with STATUS_CONTROL_C_EXIT (-1073741510) when break is sent.
|
||||
self.start_node(node.index, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)
|
||||
else:
|
||||
self.start_node(node.index)
|
||||
|
||||
current_height = node.getblock(node.getbestblockhash())['height']
|
||||
|
||||
with ThreadPoolExecutor(max_workers=1) as ex:
|
||||
# Call waitforblockheight with wait timeout longer than RPC timeout,
|
||||
# so it is possible to distinguish whether it times out or returns
|
||||
# early. If it times out it will throw an exception, and if it
|
||||
# returns early it will return the current block height.
|
||||
self.log.debug(f"Calling waitforblockheight with {self.rpc_timeout} sec RPC timeout")
|
||||
fut = ex.submit(node.waitforblockheight, height=current_height+1, timeout=self.rpc_timeout*1000*2)
|
||||
|
||||
self.wait_until(lambda: any(c["method"] == "waitforblockheight" for c in node.cli.getrpcinfo()["active_commands"]))
|
||||
|
||||
self.log.debug(f"Sending break signal to pid {node.process.pid}")
|
||||
if platform.system() == 'Windows':
|
||||
# Note: CTRL_C_EVENT should not be sent here because unlike
|
||||
# CTRL_BREAK_EVENT it can not be targeted at a specific process
|
||||
# group and may behave unpredictably.
|
||||
node.process.send_signal(signal.CTRL_BREAK_EVENT)
|
||||
else:
|
||||
# Note: signal.SIGINT would work here as well
|
||||
node.process.send_signal(signal.SIGTERM)
|
||||
node.process.wait()
|
||||
|
||||
result = fut.result()
|
||||
self.log.debug(f"waitforblockheight returned {result!r}")
|
||||
assert_equal(result["height"], current_height)
|
||||
node.wait_until_stopped()
|
||||
|
||||
def run_test(self):
|
||||
self.init_pid_test()
|
||||
self.init_stress_test()
|
||||
self.break_wait_test()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -153,6 +153,7 @@ class IPCInterfaceTest(BitcoinTestFramework):
|
||||
self.log.debug("Wait for a new template")
|
||||
waitoptions = self.capnp_modules['mining'].BlockWaitOptions()
|
||||
waitoptions.timeout = timeout
|
||||
waitoptions.feeThreshold = 1
|
||||
waitnext = template.result.waitNext(ctx, waitoptions)
|
||||
self.generate(self.nodes[0], 1)
|
||||
template2 = await waitnext
|
||||
@@ -168,6 +169,7 @@ class IPCInterfaceTest(BitcoinTestFramework):
|
||||
block3 = await self.parse_and_deserialize_block(template4, ctx)
|
||||
assert_equal(len(block3.vtx), 2)
|
||||
self.log.debug("Wait again, this should return the same template, since the fee threshold is zero")
|
||||
waitoptions.feeThreshold = 0
|
||||
template5 = await template4.result.waitNext(ctx, waitoptions)
|
||||
block4 = await self.parse_and_deserialize_block(template5, ctx)
|
||||
assert_equal(len(block4.vtx), 2)
|
||||
@@ -182,6 +184,28 @@ class IPCInterfaceTest(BitcoinTestFramework):
|
||||
template7 = await template6.result.waitNext(ctx, waitoptions)
|
||||
assert_equal(template7.to_dict(), {})
|
||||
|
||||
self.log.debug("interruptWait should abort the current wait")
|
||||
wait_started = asyncio.Event()
|
||||
async def wait_for_block():
|
||||
new_waitoptions = self.capnp_modules['mining'].BlockWaitOptions()
|
||||
new_waitoptions.timeout = waitoptions.timeout * 60 # 1 minute wait
|
||||
new_waitoptions.feeThreshold = 1
|
||||
wait_started.set()
|
||||
return await template6.result.waitNext(ctx, new_waitoptions)
|
||||
|
||||
async def interrupt_wait():
|
||||
await wait_started.wait() # Wait for confirmation wait started
|
||||
await asyncio.sleep(0.1) # Minimal buffer
|
||||
template6.result.interruptWait()
|
||||
miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0])
|
||||
|
||||
wait_task = asyncio.create_task(wait_for_block())
|
||||
interrupt_task = asyncio.create_task(interrupt_wait())
|
||||
|
||||
result = await wait_task
|
||||
await interrupt_task
|
||||
assert_equal(result.to_dict(), {})
|
||||
|
||||
current_block_height = self.nodes[0].getchaintips()[0]["height"]
|
||||
check_opts = self.capnp_modules['mining'].BlockCheckOptions()
|
||||
template = await mining.result.createNewBlock(opts)
|
||||
|
||||
@@ -161,7 +161,7 @@ class TestNode():
|
||||
self.args.append("-logsourcelocations")
|
||||
if self.version_is_at_least(239000):
|
||||
self.args.append("-loglevel=trace")
|
||||
if self.version_is_at_least(299900):
|
||||
if self.version_is_at_least(290100):
|
||||
self.args.append("-nologratelimit")
|
||||
|
||||
# Default behavior from global -v2transport flag is added to args to persist it over restarts.
|
||||
|
||||
@@ -336,6 +336,7 @@ BASE_SCRIPTS = [
|
||||
'p2p_tx_privacy.py',
|
||||
'rpc_getdescriptoractivity.py',
|
||||
'rpc_scanblocks.py',
|
||||
'tool_bitcoin.py',
|
||||
'p2p_sendtxrcncl.py',
|
||||
'rpc_scantxoutset.py',
|
||||
'feature_unsupported_utxo_db.py',
|
||||
|
||||
111
test/functional/tool_bitcoin.py
Executable file
111
test/functional/tool_bitcoin.py
Executable file
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) The Bitcoin Core developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
"""Test the bitcoin wrapper tool."""
|
||||
from test_framework.test_framework import (
|
||||
BitcoinTestFramework,
|
||||
SkipTest,
|
||||
)
|
||||
from test_framework.util import (
|
||||
append_config,
|
||||
assert_equal,
|
||||
)
|
||||
|
||||
import platform
|
||||
import re
|
||||
|
||||
|
||||
class ToolBitcoinTest(BitcoinTestFramework):
|
||||
def set_test_params(self):
|
||||
self.setup_clean_chain = True
|
||||
self.num_nodes = 1
|
||||
|
||||
def skip_test_if_missing_module(self):
|
||||
# Skip test on windows because currently when `bitcoin node -version` is
|
||||
# run on windows, python doesn't capture output from the child
|
||||
# `bitcoind` and `bitcoin-node` process started with _wexecvp, and
|
||||
# stdout/stderr are always empty. See
|
||||
# https://github.com/bitcoin/bitcoin/pull/33229#issuecomment-3265524908
|
||||
if platform.system() == "Windows":
|
||||
raise SkipTest("Test does not currently work on windows")
|
||||
|
||||
def setup_network(self):
|
||||
"""Set up nodes normally, but save a copy of their arguments before starting them."""
|
||||
self.add_nodes(self.num_nodes, self.extra_args)
|
||||
node_argv = self.get_binaries().node_argv()
|
||||
self.node_options = [node.args[len(node_argv):] for node in self.nodes]
|
||||
assert all(node.args[:len(node_argv)] == node_argv for node in self.nodes)
|
||||
|
||||
def set_cmd_args(self, node, args):
|
||||
"""Set up node so it will be started through bitcoin wrapper command with specified arguments."""
|
||||
node.args = [self.binary_paths.bitcoin_bin] + args + ["node"] + self.node_options[node.index]
|
||||
|
||||
def test_args(self, cmd_args, node_args, expect_exe=None, expect_error=None):
|
||||
node = self.nodes[0]
|
||||
self.set_cmd_args(node, cmd_args)
|
||||
extra_args = node_args + ["-version"]
|
||||
if expect_error is not None:
|
||||
node.assert_start_raises_init_error(expected_msg=expect_error, extra_args=extra_args)
|
||||
else:
|
||||
assert expect_exe
|
||||
node.start(extra_args=extra_args)
|
||||
ret, out, err = get_node_output(node)
|
||||
try:
|
||||
assert_equal(get_exe_name(out), expect_exe.encode())
|
||||
assert_equal(err, b"")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Unexpected output from {node.args + extra_args}: {out=!r} {err=!r} {ret=!r}") from e
|
||||
|
||||
def run_test(self):
|
||||
node = self.nodes[0]
|
||||
|
||||
self.log.info("Ensure bitcoin node command invokes bitcoind by default")
|
||||
self.test_args([], [], expect_exe="bitcoind")
|
||||
|
||||
self.log.info("Ensure bitcoin -M invokes bitcoind")
|
||||
self.test_args(["-M"], [], expect_exe="bitcoind")
|
||||
|
||||
self.log.info("Ensure bitcoin -M does not accept -ipcbind")
|
||||
self.test_args(["-M"], ["-ipcbind=unix"], expect_error='Error: Error parsing command line arguments: Invalid parameter -ipcbind=unix')
|
||||
|
||||
if self.is_ipc_compiled():
|
||||
self.log.info("Ensure bitcoin -m invokes bitcoin-node")
|
||||
self.test_args(["-m"], [], expect_exe="bitcoin-node")
|
||||
|
||||
self.log.info("Ensure bitcoin -m does accept -ipcbind")
|
||||
self.test_args(["-m"], ["-ipcbind=unix"], expect_exe="bitcoin-node")
|
||||
|
||||
self.log.info("Ensure bitcoin accepts -ipcbind by default")
|
||||
self.test_args([], ["-ipcbind=unix"], expect_exe="bitcoin-node")
|
||||
|
||||
self.log.info("Ensure bitcoin recognizes -ipcbind in config file")
|
||||
append_config(node.datadir_path, ["ipcbind=unix"])
|
||||
self.test_args([], [], expect_exe="bitcoin-node")
|
||||
|
||||
|
||||
def get_node_output(node):
|
||||
ret = node.process.wait(timeout=60)
|
||||
node.stdout.seek(0)
|
||||
node.stderr.seek(0)
|
||||
out = node.stdout.read()
|
||||
err = node.stderr.read()
|
||||
node.stdout.close()
|
||||
node.stderr.close()
|
||||
|
||||
# Clean up TestNode state
|
||||
node.running = False
|
||||
node.process = None
|
||||
node.rpc_connected = False
|
||||
node.rpc = None
|
||||
|
||||
return ret, out, err
|
||||
|
||||
|
||||
def get_exe_name(version_str):
|
||||
"""Get exe name from last word of first line of version string."""
|
||||
return re.match(rb".*?(\S+)\s*?(?:\n|$)", version_str.strip()).group(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
ToolBitcoinTest(__file__).main()
|
||||
@@ -319,6 +319,12 @@ class ToolWalletTest(BitcoinTestFramework):
|
||||
self.write_dump(dump_data, bad_sum_wallet_dump)
|
||||
self.assert_raises_tool_error('Error: Checksum is not the correct size', '-wallet=badload', '-dumpfile={}'.format(bad_sum_wallet_dump), 'createfromdump')
|
||||
assert not (self.nodes[0].wallets_path / "badload").is_dir()
|
||||
self.assert_raises_tool_error('Error: Checksum is not the correct size', '-wallet=', '-dumpfile={}'.format(bad_sum_wallet_dump), 'createfromdump')
|
||||
assert self.nodes[0].wallets_path.exists()
|
||||
assert not (self.nodes[0].wallets_path / "wallet.dat").exists()
|
||||
|
||||
self.log.info('Checking createfromdump with an unnamed wallet')
|
||||
self.do_tool_createfromdump("", "wallet.dump")
|
||||
|
||||
def test_chainless_conflicts(self):
|
||||
self.log.info("Test wallet tool when wallet contains conflicting transactions")
|
||||
|
||||
@@ -39,6 +39,7 @@ from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.util import (
|
||||
assert_equal,
|
||||
assert_raises_rpc_error,
|
||||
sha256sum_file,
|
||||
)
|
||||
|
||||
|
||||
@@ -132,10 +133,65 @@ class WalletBackupTest(BitcoinTestFramework):
|
||||
backup_file = self.nodes[0].datadir_path / 'wallet.bak'
|
||||
wallet_name = "res0"
|
||||
wallet_file = node.wallets_path / wallet_name
|
||||
error_message = "Failed to create database path '{}'. Database already exists.".format(wallet_file)
|
||||
error_message = "Failed to restore wallet. Database file exists in '{}'.".format(wallet_file / "wallet.dat")
|
||||
assert_raises_rpc_error(-36, error_message, node.restorewallet, wallet_name, backup_file)
|
||||
assert wallet_file.exists()
|
||||
|
||||
def test_restore_existent_dir(self):
|
||||
self.log.info("Test restore on an existent empty directory")
|
||||
node = self.nodes[3]
|
||||
backup_file = self.nodes[0].datadir_path / 'wallet.bak'
|
||||
wallet_name = "restored_wallet"
|
||||
wallet_dir = node.wallets_path / wallet_name
|
||||
os.mkdir(wallet_dir)
|
||||
res = node.restorewallet(wallet_name, backup_file)
|
||||
assert_equal(res['name'], wallet_name)
|
||||
node.unloadwallet(wallet_name)
|
||||
|
||||
self.log.info("Test restore succeeds when the target directory contains non-wallet files")
|
||||
wallet_file = node.wallets_path / wallet_name / "wallet.dat"
|
||||
os.remove(wallet_file)
|
||||
extra_file = node.wallets_path / wallet_name / "not_a_wallet.txt"
|
||||
extra_file.touch()
|
||||
res = node.restorewallet(wallet_name, backup_file)
|
||||
assert_equal(res['name'], wallet_name)
|
||||
assert extra_file.exists() # extra file was not removed by mistake
|
||||
node.unloadwallet(wallet_name)
|
||||
|
||||
self.log.info("Test restore failure due to existing db file in the destination directory")
|
||||
original_shasum = sha256sum_file(wallet_file)
|
||||
error_message = "Failed to restore wallet. Database file exists in '{}'.".format(wallet_dir / "wallet.dat")
|
||||
assert_raises_rpc_error(-36, error_message, node.restorewallet, wallet_name, backup_file)
|
||||
# Ensure the wallet file remains untouched
|
||||
assert wallet_dir.exists()
|
||||
assert_equal(original_shasum, sha256sum_file(wallet_file))
|
||||
|
||||
self.log.info("Test restore succeeds when the .dat file in the destination has a different name")
|
||||
second_wallet = wallet_dir / "hidden_storage.dat"
|
||||
os.rename(wallet_dir / "wallet.dat", second_wallet)
|
||||
original_shasum = sha256sum_file(second_wallet)
|
||||
res = node.restorewallet(wallet_name, backup_file)
|
||||
assert_equal(res['name'], wallet_name)
|
||||
assert (wallet_dir / "hidden_storage.dat").exists()
|
||||
assert_equal(original_shasum, sha256sum_file(second_wallet))
|
||||
node.unloadwallet(wallet_name)
|
||||
|
||||
# Clean for follow-up tests
|
||||
os.remove(wallet_file)
|
||||
|
||||
def test_restore_into_unnamed_wallet(self):
|
||||
self.log.info("Test restore into a default unnamed wallet")
|
||||
# This is also useful to test the migration recovery after failure logic
|
||||
node = self.nodes[3]
|
||||
backup_file = self.nodes[0].datadir_path / 'wallet.bak'
|
||||
wallet_name = ""
|
||||
res = node.restorewallet(wallet_name, backup_file)
|
||||
assert_equal(res['name'], "")
|
||||
assert (node.wallets_path / "wallet.dat").exists()
|
||||
# Clean for follow-up tests
|
||||
node.unloadwallet("")
|
||||
os.remove(node.wallets_path / "wallet.dat")
|
||||
|
||||
def test_pruned_wallet_backup(self):
|
||||
self.log.info("Test loading backup on a pruned node when the backup was created close to the prune height of the restoring node")
|
||||
node = self.nodes[3]
|
||||
@@ -155,6 +211,13 @@ class WalletBackupTest(BitcoinTestFramework):
|
||||
# the backup to load successfully this close to the prune height
|
||||
node.restorewallet('pruned', node.datadir_path / 'wallet_pruned.bak')
|
||||
|
||||
self.log.info("Test restore on a pruned node when the backup was beyond the pruning point")
|
||||
backup_file = self.nodes[0].datadir_path / 'wallet.bak'
|
||||
wallet_name = ""
|
||||
error_message = "Wallet loading failed. Prune: last wallet synchronisation goes beyond pruned data. You need to -reindex (download the whole blockchain again in case of a pruned node)"
|
||||
assert_raises_rpc_error(-4, error_message, node.restorewallet, wallet_name, backup_file)
|
||||
assert node.wallets_path.exists() # ensure the wallets dir exists
|
||||
|
||||
def run_test(self):
|
||||
self.log.info("Generating initial blockchain")
|
||||
self.generate(self.nodes[0], 1)
|
||||
@@ -219,6 +282,8 @@ class WalletBackupTest(BitcoinTestFramework):
|
||||
assert_equal(res2_rpc.getbalance(), balance2)
|
||||
|
||||
self.restore_wallet_existent_name()
|
||||
self.test_restore_existent_dir()
|
||||
self.test_restore_into_unnamed_wallet()
|
||||
|
||||
# Backup to source wallet file must fail
|
||||
sourcePaths = [
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
"""Test Migrating a wallet from legacy to descriptor."""
|
||||
from pathlib import Path
|
||||
import os.path
|
||||
import random
|
||||
import shutil
|
||||
@@ -638,6 +639,14 @@ class WalletMigrationTest(BitcoinTestFramework):
|
||||
|
||||
assert_equal(bals, wallet.getbalances())
|
||||
|
||||
def clear_default_wallet(self, backup_file):
|
||||
# Test cleanup: Clear unnamed default wallet for subsequent tests
|
||||
(self.old_node.wallets_path / "wallet.dat").unlink()
|
||||
(self.master_node.wallets_path / "wallet.dat").unlink(missing_ok=True)
|
||||
shutil.rmtree(self.master_node.wallets_path / "default_wallet_watchonly", ignore_errors=True)
|
||||
shutil.rmtree(self.master_node.wallets_path / "default_wallet_solvables", ignore_errors=True)
|
||||
backup_file.unlink()
|
||||
|
||||
def test_default_wallet(self):
|
||||
self.log.info("Test migration of the wallet named as the empty string")
|
||||
wallet = self.create_legacy_wallet("")
|
||||
@@ -654,6 +663,58 @@ class WalletMigrationTest(BitcoinTestFramework):
|
||||
# migrate_and_get_rpc already checks for backup file existence
|
||||
assert os.path.basename(res["backup_path"]).startswith("default_wallet")
|
||||
|
||||
wallet.unloadwallet()
|
||||
self.clear_default_wallet(backup_file=Path(res["backup_path"]))
|
||||
|
||||
def test_default_wallet_watch_only(self):
|
||||
self.log.info("Test unnamed (default) watch-only wallet migration")
|
||||
master_wallet = self.master_node.get_wallet_rpc(self.default_wallet_name)
|
||||
wallet = self.create_legacy_wallet("", blank=True)
|
||||
wallet.importaddress(master_wallet.getnewaddress(address_type="legacy"))
|
||||
|
||||
res, wallet = self.migrate_and_get_rpc("")
|
||||
|
||||
info = wallet.getwalletinfo()
|
||||
assert_equal(info["descriptors"], True)
|
||||
assert_equal(info["format"], "sqlite")
|
||||
assert_equal(info["private_keys_enabled"], False)
|
||||
assert_equal(info["walletname"], "default_wallet_watchonly")
|
||||
# Check the default wallet is not available anymore
|
||||
assert not (self.master_node.wallets_path / "wallet.dat").exists()
|
||||
|
||||
wallet.unloadwallet()
|
||||
self.clear_default_wallet(backup_file=Path(res["backup_path"]))
|
||||
|
||||
def test_default_wallet_failure(self):
|
||||
self.log.info("Test failure during unnamed (default) wallet migration")
|
||||
master_wallet = self.master_node.get_wallet_rpc(self.default_wallet_name)
|
||||
wallet = self.create_legacy_wallet("", blank=True)
|
||||
wallet.importaddress(master_wallet.getnewaddress(address_type="legacy"))
|
||||
|
||||
# Create wallet directory with the watch-only name and a wallet file.
|
||||
# Because the wallet dir exists, this will cause migration to fail.
|
||||
watch_only_dir = self.master_node.wallets_path / "default_wallet_watchonly"
|
||||
os.mkdir(watch_only_dir)
|
||||
shutil.copyfile(self.old_node.wallets_path / "wallet.dat", watch_only_dir / "wallet.dat")
|
||||
|
||||
mocked_time = int(time.time())
|
||||
self.master_node.setmocktime(mocked_time)
|
||||
assert_raises_rpc_error(-4, "Failed to create database", self.migrate_and_get_rpc, "")
|
||||
self.master_node.setmocktime(0)
|
||||
|
||||
# Verify the /wallets/ path exists
|
||||
assert self.master_node.wallets_path.exists()
|
||||
# Check backup file exists. Because the wallet has no name, the backup is prefixed with 'default_wallet'
|
||||
backup_path = self.master_node.wallets_path / f"default_wallet_{mocked_time}.legacy.bak"
|
||||
assert backup_path.exists()
|
||||
# Verify the original unnamed wallet was restored
|
||||
assert (self.master_node.wallets_path / "wallet.dat").exists()
|
||||
# And verify it is still a BDB wallet
|
||||
self.assert_is_bdb("")
|
||||
|
||||
# Test cleanup: clear default wallet for next test
|
||||
self.clear_default_wallet(backup_path)
|
||||
|
||||
def test_direct_file(self):
|
||||
self.log.info("Test migration of a wallet that is not in a wallet directory")
|
||||
wallet = self.create_legacy_wallet("plainfile")
|
||||
@@ -1521,6 +1582,37 @@ class WalletMigrationTest(BitcoinTestFramework):
|
||||
self.start_node(self.old_node.index)
|
||||
self.connect_nodes(1, 0)
|
||||
|
||||
def unsynced_wallet_on_pruned_node_fails(self):
|
||||
self.log.info("Test migration of an unsynced wallet on a pruned node fails gracefully")
|
||||
wallet = self.create_legacy_wallet("", load_on_startup=False)
|
||||
last_wallet_synced_block = wallet.getwalletinfo()['lastprocessedblock']['height']
|
||||
wallet.unloadwallet()
|
||||
|
||||
shutil.copyfile(self.old_node.wallets_path / "wallet.dat", self.master_node.wallets_path / "wallet.dat")
|
||||
|
||||
# Generate blocks just so the wallet best block is pruned
|
||||
self.restart_node(0, ["-fastprune", "-prune=1", "-nowallet"])
|
||||
self.connect_nodes(0, 1)
|
||||
self.generate(self.master_node, 450, sync_fun=self.no_op)
|
||||
self.master_node.pruneblockchain(250)
|
||||
# Ensure next block to sync is unavailable
|
||||
assert_raises_rpc_error(-1, "Block not available (pruned data)", self.master_node.getblock, self.master_node.getblockhash(last_wallet_synced_block + 1))
|
||||
|
||||
# Check migration failure
|
||||
mocked_time = int(time.time())
|
||||
self.master_node.setmocktime(mocked_time)
|
||||
assert_raises_rpc_error(-4, "last wallet synchronisation goes beyond pruned data. You need to -reindex (download the whole blockchain again in case of a pruned node)", self.master_node.migratewallet, wallet_name="")
|
||||
self.master_node.setmocktime(0)
|
||||
|
||||
# Verify the /wallets/ path exists, the wallet is still BDB and the backup file is there.
|
||||
assert self.master_node.wallets_path.exists()
|
||||
self.assert_is_bdb("")
|
||||
backup_path = self.master_node.wallets_path / f"default_wallet_{mocked_time}.legacy.bak"
|
||||
assert backup_path.exists()
|
||||
|
||||
self.clear_default_wallet(backup_path)
|
||||
|
||||
|
||||
def run_test(self):
|
||||
self.master_node = self.nodes[0]
|
||||
self.old_node = self.nodes[1]
|
||||
@@ -1539,7 +1631,9 @@ class WalletMigrationTest(BitcoinTestFramework):
|
||||
self.test_wallet_with_relative_path()
|
||||
self.test_wallet_with_path("path/to/mywallet/")
|
||||
self.test_wallet_with_path("path/that/ends/in/..")
|
||||
self.test_default_wallet_failure()
|
||||
self.test_default_wallet()
|
||||
self.test_default_wallet_watch_only()
|
||||
self.test_direct_file()
|
||||
self.test_addressbook()
|
||||
self.test_migrate_raw_p2sh()
|
||||
@@ -1559,5 +1653,8 @@ class WalletMigrationTest(BitcoinTestFramework):
|
||||
self.test_solvable_no_privs()
|
||||
self.test_loading_failure_after_migration()
|
||||
|
||||
# Note: After this test the first 250 blocks of 'master_node' are pruned
|
||||
self.unsynced_wallet_on_pruned_node_fails()
|
||||
|
||||
if __name__ == '__main__':
|
||||
WalletMigrationTest(__file__).main()
|
||||
|
||||
@@ -119,6 +119,7 @@ class WalletV3Test(BitcoinTestFramework):
|
||||
self.sendall_truc_child_weight_limit()
|
||||
self.mix_non_truc_versions()
|
||||
self.cant_spend_multiple_unconfirmed_truc_outputs()
|
||||
self.test_spend_third_generation()
|
||||
|
||||
@cleanup
|
||||
def tx_spends_unconfirmed_tx_with_wrong_version(self, version_a, version_b):
|
||||
@@ -585,5 +586,49 @@ class WalletV3Test(BitcoinTestFramework):
|
||||
{'include_unsafe' : True}
|
||||
)
|
||||
|
||||
@cleanup
|
||||
def test_spend_third_generation(self):
|
||||
self.log.info("Test that we can't spend an unconfirmed TRUC output that already has an unconfirmed parent")
|
||||
|
||||
# Generation 1: Consolidate all UTXOs into one output using sendall
|
||||
self.charlie.sendall([self.charlie.getnewaddress()], version=3)
|
||||
outputs1 = self.charlie.listunspent(minconf=0)
|
||||
assert_equal(len(outputs1), 1)
|
||||
|
||||
# Generation 2: to ensure no change address is created, do another sendall
|
||||
self.charlie.sendall([self.charlie.getnewaddress()], version=3)
|
||||
outputs2 = self.charlie.listunspent(minconf=0)
|
||||
assert_equal(len(outputs2), 1)
|
||||
total_amount = sum([utxo['amount'] for utxo in outputs2])
|
||||
|
||||
# Generation 3: try to send half of total amount to Alice
|
||||
outputs = {self.alice.getnewaddress(): total_amount / 2}
|
||||
assert_raises_rpc_error(
|
||||
-4,
|
||||
"Insufficient funds",
|
||||
self.charlie.send,
|
||||
outputs,
|
||||
version=3
|
||||
)
|
||||
|
||||
# Also doesn't work with fundrawtransaction
|
||||
raw_tx = self.charlie.createrawtransaction(inputs=[], outputs=outputs, version=3)
|
||||
assert_raises_rpc_error(
|
||||
-4,
|
||||
"Insufficient funds",
|
||||
self.charlie.fundrawtransaction,
|
||||
raw_tx,
|
||||
{'include_unsafe' : True}
|
||||
)
|
||||
|
||||
# Also doesn't work with sendall
|
||||
assert_raises_rpc_error(
|
||||
-6,
|
||||
"Total value of UTXO pool too low to pay for transaction",
|
||||
self.charlie.sendall,
|
||||
[self.alice.getnewaddress()],
|
||||
version=3
|
||||
)
|
||||
|
||||
if __name__ == '__main__':
|
||||
WalletV3Test(__file__).main()
|
||||
|
||||
@@ -23,7 +23,7 @@ CMD_GREP_WALLET_ARGS = r"git grep --function-context 'void WalletInit::AddWallet
|
||||
CMD_GREP_WALLET_HIDDEN_ARGS = r"git grep --function-context 'void DummyWalletInit::AddWalletOptions' -- {}".format(CMD_ROOT_DIR)
|
||||
CMD_GREP_DOCS = r"git grep --perl-regexp '{}' {}".format(REGEX_DOC, CMD_ROOT_DIR)
|
||||
# list unsupported, deprecated and duplicate args as they need no documentation
|
||||
SET_DOC_OPTIONAL = set(['-h', '-?', '-dbcrashratio', '-forcecompactdb'])
|
||||
SET_DOC_OPTIONAL = set(['-h', '-?', '-dbcrashratio', '-forcecompactdb', '-ipcconnect', '-ipcfd'])
|
||||
|
||||
|
||||
def lint_missing_argument_documentation():
|
||||
|
||||
Reference in New Issue
Block a user