Merge bitcoin/bitcoin#33201: Add functional test for IPC interface
a341e11ac9ci: test IPC on additional hosts (Sjors Provoost)6aee573bfcci: enable IPC tests in CI (Pieter Wuille)8d2ee88fa2tests: add functional tests for IPC interface (Pieter Wuille)3cc9a06c8dtest: Add TestNode ipcbind option (Ryan Ofsky)3cceb60a71test: Provide path to `bitcoin` binary (Ryan Ofsky)8c7f005629test: add is_ipc_compiled() and skip_if_no_ipc() functions (Ryan Ofsky) Pull request description: This adds support to the functional test framework to run the multiprocess `bitcoin-node` binary, and then tests it in a new `interface_ipc.py` functional test through the `pycapnp` module. ACKs for top commit: Sjors: ACKa341e11ac9ryanofsky: Code review ACKa341e11ac9. Changes since last review: rebasing, switching to miniwallet and expanding wallet test, improving pycapnp install steps in instructions and CI. TheCharlatan: ACKa341e11ac9Tree-SHA512: 98330283cf0d66d5537eec1219345b8aec5740dbc2171bd8b70680d7a282e0962fcdf0588a75518110761e8bc95af57d6097c93937cac710805b0df10837093c
This commit is contained in:
14 files changed
+290
-20
No files matched your search
@@ -95,6 +95,7 @@ jobs:
|
|||||||
- run: |
|
- run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install clang mold ccache build-essential cmake ninja-build pkgconf python3-zmq libevent-dev libboost-dev libsqlite3-dev systemtap-sdt-dev libzmq3-dev qt6-base-dev qt6-tools-dev qt6-l10n-tools libqrencode-dev capnproto libcapnp-dev -y
|
sudo apt-get install clang mold ccache build-essential cmake ninja-build pkgconf python3-zmq libevent-dev libboost-dev libsqlite3-dev systemtap-sdt-dev libzmq3-dev qt6-base-dev qt6-tools-dev qt6-l10n-tools libqrencode-dev capnproto libcapnp-dev -y
|
||||||
|
sudo pip3 install --break-system-packages pycapnp
|
||||||
- name: Compile and run tests
|
- name: Compile and run tests
|
||||||
run: |
|
run: |
|
||||||
# Run tests on commits after the last merge commit and before the PR head commit
|
# Run tests on commits after the last merge commit and before the PR head commit
|
||||||
@@ -152,6 +153,11 @@ jobs:
|
|||||||
brew install --quiet python@3 || brew link --overwrite python@3
|
brew install --quiet python@3 || brew link --overwrite python@3
|
||||||
brew install --quiet coreutils ninja pkgconf gnu-getopt ccache boost libevent zeromq qt@6 qrencode capnp
|
brew install --quiet coreutils ninja pkgconf gnu-getopt ccache boost libevent zeromq qt@6 qrencode capnp
|
||||||
|
|
||||||
|
- name: Install Python packages
|
||||||
|
run: |
|
||||||
|
git clone -b v2.1.0 https://github.com/capnproto/pycapnp
|
||||||
|
pip3 install ./pycapnp -C force-bundled-libcapnp=True --break-system-packages
|
||||||
|
|
||||||
- name: Set Ccache directory
|
- name: Set Ccache directory
|
||||||
run: echo "CCACHE_DIR=${RUNNER_TEMP}/ccache_dir" >> "$GITHUB_ENV"
|
run: echo "CCACHE_DIR=${RUNNER_TEMP}/ccache_dir" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
@@ -275,6 +281,7 @@ jobs:
|
|||||||
if: matrix.job-type == 'standard'
|
if: matrix.job-type == 'standard'
|
||||||
working-directory: build
|
working-directory: build
|
||||||
env:
|
env:
|
||||||
|
BITCOIN_BIN: '${{ github.workspace }}\build\bin\Release\bitcoin.exe'
|
||||||
BITCOIND: '${{ github.workspace }}\build\bin\Release\bitcoind.exe'
|
BITCOIND: '${{ github.workspace }}\build\bin\Release\bitcoind.exe'
|
||||||
BITCOINCLI: '${{ github.workspace }}\build\bin\Release\bitcoin-cli.exe'
|
BITCOINCLI: '${{ github.workspace }}\build\bin\Release\bitcoin-cli.exe'
|
||||||
BITCOINTX: '${{ github.workspace }}\build\bin\Release\bitcoin-tx.exe'
|
BITCOINTX: '${{ github.workspace }}\build\bin\Release\bitcoin-tx.exe'
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ fi
|
|||||||
|
|
||||||
export CONTAINER_NAME=ci_native_asan
|
export CONTAINER_NAME=ci_native_asan
|
||||||
export APT_LLVM_V="21"
|
export APT_LLVM_V="21"
|
||||||
export PACKAGES="systemtap-sdt-dev clang-${APT_LLVM_V} llvm-${APT_LLVM_V} libclang-rt-${APT_LLVM_V}-dev python3-zmq qt6-base-dev qt6-tools-dev qt6-l10n-tools libevent-dev libboost-dev libzmq3-dev libqrencode-dev libsqlite3-dev ${BPFCC_PACKAGE} libcapnp-dev capnproto"
|
export PACKAGES="systemtap-sdt-dev clang-${APT_LLVM_V} llvm-${APT_LLVM_V} libclang-rt-${APT_LLVM_V}-dev python3-zmq qt6-base-dev qt6-tools-dev qt6-l10n-tools libevent-dev libboost-dev libzmq3-dev libqrencode-dev libsqlite3-dev ${BPFCC_PACKAGE} libcapnp-dev capnproto python3-pip"
|
||||||
|
export PIP_PACKAGES="--break-system-packages pycapnp"
|
||||||
export NO_DEPENDS=1
|
export NO_DEPENDS=1
|
||||||
export GOAL="install"
|
export GOAL="install"
|
||||||
export CI_LIMIT_STACK_SIZE=1
|
export CI_LIMIT_STACK_SIZE=1
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export LC_ALL=C.UTF-8
|
|||||||
export CONTAINER_NAME=ci_native_centos
|
export CONTAINER_NAME=ci_native_centos
|
||||||
export CI_IMAGE_NAME_TAG="quay.io/centos/centos:stream10"
|
export CI_IMAGE_NAME_TAG="quay.io/centos/centos:stream10"
|
||||||
export CI_BASE_PACKAGES="gcc-c++ glibc-devel libstdc++-devel ccache make ninja-build git python3 python3-pip which patch xz procps-ng rsync coreutils bison e2fsprogs cmake dash"
|
export CI_BASE_PACKAGES="gcc-c++ glibc-devel libstdc++-devel ccache make ninja-build git python3 python3-pip which patch xz procps-ng rsync coreutils bison e2fsprogs cmake dash"
|
||||||
export PIP_PACKAGES="pyzmq"
|
export PIP_PACKAGES="pyzmq pycapnp"
|
||||||
export DEP_OPTS="DEBUG=1"
|
export DEP_OPTS="DEBUG=1"
|
||||||
export GOAL="install"
|
export GOAL="install"
|
||||||
export BITCOIN_CONFIG="\
|
export BITCOIN_CONFIG="\
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ LIBCXX_FLAGS="-nostdinc++ -nostdlib++ -isystem ${LIBCXX_DIR}include/c++/v1 -L${L
|
|||||||
export MSAN_AND_LIBCXX_FLAGS="${MSAN_FLAGS} ${LIBCXX_FLAGS}"
|
export MSAN_AND_LIBCXX_FLAGS="${MSAN_FLAGS} ${LIBCXX_FLAGS}"
|
||||||
|
|
||||||
export CONTAINER_NAME="ci_native_msan"
|
export CONTAINER_NAME="ci_native_msan"
|
||||||
export PACKAGES="clang-${APT_LLVM_V} llvm-${APT_LLVM_V} llvm-${APT_LLVM_V}-dev libclang-${APT_LLVM_V}-dev libclang-rt-${APT_LLVM_V}-dev"
|
export PACKAGES="clang-${APT_LLVM_V} llvm-${APT_LLVM_V} llvm-${APT_LLVM_V}-dev libclang-${APT_LLVM_V}-dev libclang-rt-${APT_LLVM_V}-dev python3-pip"
|
||||||
|
export PIP_PACKAGES="--break-system-packages pycapnp"
|
||||||
export DEP_OPTS="DEBUG=1 NO_QT=1 CC=clang CXX=clang++ CFLAGS='${MSAN_FLAGS}' CXXFLAGS='${MSAN_AND_LIBCXX_FLAGS}'"
|
export DEP_OPTS="DEBUG=1 NO_QT=1 CC=clang CXX=clang++ CFLAGS='${MSAN_FLAGS}' CXXFLAGS='${MSAN_AND_LIBCXX_FLAGS}'"
|
||||||
export GOAL="install"
|
export GOAL="install"
|
||||||
export CI_LIMIT_STACK_SIZE=1
|
export CI_LIMIT_STACK_SIZE=1
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ export LC_ALL=C.UTF-8
|
|||||||
export CONTAINER_NAME=ci_native_nowallet_libbitcoinkernel
|
export CONTAINER_NAME=ci_native_nowallet_libbitcoinkernel
|
||||||
export CI_IMAGE_NAME_TAG="mirror.gcr.io/debian:bookworm"
|
export CI_IMAGE_NAME_TAG="mirror.gcr.io/debian:bookworm"
|
||||||
# Use minimum supported python3.10 (or best-effort 3.11) and clang-16, see doc/dependencies.md
|
# Use minimum supported python3.10 (or best-effort 3.11) and clang-16, see doc/dependencies.md
|
||||||
export PACKAGES="python3-zmq clang-16 llvm-16 libc++abi-16-dev libc++-16-dev"
|
export PACKAGES="python3-zmq python3-pip clang-16 llvm-16 libc++abi-16-dev libc++-16-dev"
|
||||||
|
export PIP_PACKAGES="--break-system-packages pycapnp"
|
||||||
export DEP_OPTS="NO_WALLET=1 CC=clang-16 CXX='clang++-16 -stdlib=libc++'"
|
export DEP_OPTS="NO_WALLET=1 CC=clang-16 CXX='clang++-16 -stdlib=libc++'"
|
||||||
export GOAL="install"
|
export GOAL="install"
|
||||||
export BITCOIN_CONFIG="-DREDUCE_EXPORTS=ON -DBUILD_UTIL_CHAINSTATE=ON -DBUILD_KERNEL_LIB=ON -DBUILD_SHARED_LIBS=ON"
|
export BITCOIN_CONFIG="-DREDUCE_EXPORTS=ON -DBUILD_UTIL_CHAINSTATE=ON -DBUILD_KERNEL_LIB=ON -DBUILD_SHARED_LIBS=ON"
|
||||||
@@ -11,7 +11,8 @@ export CI_IMAGE_NAME_TAG="mirror.gcr.io/ubuntu:24.04"
|
|||||||
export APT_LLVM_V="21"
|
export APT_LLVM_V="21"
|
||||||
LIBCXX_DIR="/cxx_build/"
|
LIBCXX_DIR="/cxx_build/"
|
||||||
LIBCXX_FLAGS="-fsanitize=thread -nostdinc++ -nostdlib++ -isystem ${LIBCXX_DIR}include/c++/v1 -L${LIBCXX_DIR}lib -Wl,-rpath,${LIBCXX_DIR}lib -lc++ -lc++abi -lpthread -Wno-unused-command-line-argument"
|
LIBCXX_FLAGS="-fsanitize=thread -nostdinc++ -nostdlib++ -isystem ${LIBCXX_DIR}include/c++/v1 -L${LIBCXX_DIR}lib -Wl,-rpath,${LIBCXX_DIR}lib -lc++ -lc++abi -lpthread -Wno-unused-command-line-argument"
|
||||||
export PACKAGES="clang-${APT_LLVM_V} llvm-${APT_LLVM_V} llvm-${APT_LLVM_V}-dev libclang-${APT_LLVM_V}-dev libclang-rt-${APT_LLVM_V}-dev python3-zmq"
|
export PACKAGES="clang-${APT_LLVM_V} llvm-${APT_LLVM_V} llvm-${APT_LLVM_V}-dev libclang-${APT_LLVM_V}-dev libclang-rt-${APT_LLVM_V}-dev python3-zmq python3-pip"
|
||||||
|
export PIP_PACKAGES="--break-system-packages pycapnp"
|
||||||
export DEP_OPTS="CC=clang CXX=clang++ CXXFLAGS='${LIBCXX_FLAGS}' NO_QT=1"
|
export DEP_OPTS="CC=clang CXX=clang++ CXXFLAGS='${LIBCXX_FLAGS}' NO_QT=1"
|
||||||
export GOAL="install"
|
export GOAL="install"
|
||||||
export CI_LIMIT_STACK_SIZE=1
|
export CI_LIMIT_STACK_SIZE=1
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ export LC_ALL=C.UTF-8
|
|||||||
|
|
||||||
export CI_IMAGE_NAME_TAG="mirror.gcr.io/ubuntu:24.04"
|
export CI_IMAGE_NAME_TAG="mirror.gcr.io/ubuntu:24.04"
|
||||||
export CONTAINER_NAME=ci_native_valgrind
|
export CONTAINER_NAME=ci_native_valgrind
|
||||||
export PACKAGES="valgrind python3-zmq libevent-dev libboost-dev libzmq3-dev libsqlite3-dev libcapnp-dev capnproto"
|
export PACKAGES="valgrind python3-zmq libevent-dev libboost-dev libzmq3-dev libsqlite3-dev libcapnp-dev capnproto python3-pip"
|
||||||
|
export PIP_PACKAGES="--break-system-packages pycapnp"
|
||||||
export USE_VALGRIND=1
|
export USE_VALGRIND=1
|
||||||
export NO_DEPENDS=1
|
export NO_DEPENDS=1
|
||||||
# bind tests excluded for now, see https://github.com/bitcoin/bitcoin/issues/17765#issuecomment-602068547
|
# bind tests excluded for now, see https://github.com/bitcoin/bitcoin/issues/17765#issuecomment-602068547
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ function(create_test_config)
|
|||||||
set_configure_variable(WITH_ZMQ ENABLE_ZMQ)
|
set_configure_variable(WITH_ZMQ ENABLE_ZMQ)
|
||||||
set_configure_variable(ENABLE_EXTERNAL_SIGNER ENABLE_EXTERNAL_SIGNER)
|
set_configure_variable(ENABLE_EXTERNAL_SIGNER ENABLE_EXTERNAL_SIGNER)
|
||||||
set_configure_variable(WITH_USDT ENABLE_USDT_TRACEPOINTS)
|
set_configure_variable(WITH_USDT ENABLE_USDT_TRACEPOINTS)
|
||||||
|
set_configure_variable(ENABLE_IPC ENABLE_IPC)
|
||||||
|
|
||||||
configure_file(config.ini.in config.ini USE_SOURCE_PERMISSIONS @ONLY)
|
configure_file(config.ini.in config.ini USE_SOURCE_PERMISSIONS @ONLY)
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|||||||
@@ -34,6 +34,22 @@ The ZMQ functional test requires a python ZMQ library. To install it:
|
|||||||
- on Unix, run `sudo apt-get install python3-zmq`
|
- on Unix, run `sudo apt-get install python3-zmq`
|
||||||
- on mac OS, run `pip3 install pyzmq`
|
- on mac OS, run `pip3 install pyzmq`
|
||||||
|
|
||||||
|
The IPC functional test requires a python IPC library. `pip3 install pycapnp` may work, but if not, install it from source:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone -b v2.1.0 https://github.com/capnproto/pycapnp
|
||||||
|
pip3 install ./pycapnp
|
||||||
|
```
|
||||||
|
|
||||||
|
If that does not work, try adding `-C force-bundled-libcapnp=True` to the `pip` command.
|
||||||
|
Depending on the system, it may be necessary to install and run in a venv:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python -m venv venv
|
||||||
|
git clone -b v2.1.0 https://github.com/capnproto/pycapnp
|
||||||
|
venv/bin/pip3 install ./pycapnp -C force-bundled-libcapnp=True
|
||||||
|
venv/bin/python3 build/test/functional/interface_ipc.py
|
||||||
|
```
|
||||||
|
|
||||||
On Windows the `PYTHONUTF8` environment variable must be set to 1:
|
On Windows the `PYTHONUTF8` environment variable must be set to 1:
|
||||||
|
|
||||||
|
|||||||
@@ -26,3 +26,4 @@ RPCAUTH=@abs_top_srcdir@/share/rpcauth/rpcauth.py
|
|||||||
@ENABLE_ZMQ_TRUE@ENABLE_ZMQ=true
|
@ENABLE_ZMQ_TRUE@ENABLE_ZMQ=true
|
||||||
@ENABLE_EXTERNAL_SIGNER_TRUE@ENABLE_EXTERNAL_SIGNER=true
|
@ENABLE_EXTERNAL_SIGNER_TRUE@ENABLE_EXTERNAL_SIGNER=true
|
||||||
@ENABLE_USDT_TRACEPOINTS_TRUE@ENABLE_USDT_TRACEPOINTS=true
|
@ENABLE_USDT_TRACEPOINTS_TRUE@ENABLE_USDT_TRACEPOINTS=true
|
||||||
|
@ENABLE_IPC_TRUE@ENABLE_IPC=true
|
||||||
Executable
+188
@@ -0,0 +1,188 @@
|
|||||||
|
#!/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 IPC (multiprocess) interface."""
|
||||||
|
import asyncio
|
||||||
|
from io import BytesIO
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
from test_framework.messages import (CBlock, CTransaction, ser_uint256)
|
||||||
|
from test_framework.test_framework import (BitcoinTestFramework, assert_equal)
|
||||||
|
from test_framework.wallet import MiniWallet
|
||||||
|
|
||||||
|
# Test may be skipped and not have capnp installed
|
||||||
|
try:
|
||||||
|
import capnp # type: ignore[import] # noqa: F401
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class IPCInterfaceTest(BitcoinTestFramework):
|
||||||
|
|
||||||
|
def skip_test_if_missing_module(self):
|
||||||
|
self.skip_if_no_ipc()
|
||||||
|
self.skip_if_no_py_capnp()
|
||||||
|
|
||||||
|
def load_capnp_modules(self):
|
||||||
|
if capnp_bin := shutil.which("capnp"):
|
||||||
|
# Add the system cap'nproto path so include/capnp/c++.capnp can be found.
|
||||||
|
capnp_dir = Path(capnp_bin).parent.parent / "include"
|
||||||
|
else:
|
||||||
|
# If there is no system cap'nproto, the pycapnp module should have its own "bundled"
|
||||||
|
# includes at this location. If pycapnp was installed with bundled capnp,
|
||||||
|
# capnp/c++.capnp can be found here.
|
||||||
|
capnp_dir = Path(capnp.__path__[0]).parent
|
||||||
|
src_dir = Path(self.config['environment']['SRCDIR']) / "src"
|
||||||
|
mp_dir = src_dir / "ipc" / "libmultiprocess" / "include"
|
||||||
|
imports = [str(capnp_dir), str(src_dir), str(mp_dir)]
|
||||||
|
return {
|
||||||
|
"proxy": capnp.load(str(mp_dir / "mp" / "proxy.capnp"), imports=imports),
|
||||||
|
"init": capnp.load(str(src_dir / "ipc" / "capnp" / "init.capnp"), imports=imports),
|
||||||
|
"echo": capnp.load(str(src_dir / "ipc" / "capnp" / "echo.capnp"), imports=imports),
|
||||||
|
"mining": capnp.load(str(src_dir / "ipc" / "capnp" / "mining.capnp"), imports=imports),
|
||||||
|
}
|
||||||
|
|
||||||
|
def set_test_params(self):
|
||||||
|
self.num_nodes = 1
|
||||||
|
|
||||||
|
def setup_nodes(self):
|
||||||
|
self.extra_init = [{"ipcbind": True}]
|
||||||
|
super().setup_nodes()
|
||||||
|
# Use this function to also load the capnp modules (we cannot use set_test_params for this,
|
||||||
|
# as it is being called before knowing whether capnp is available).
|
||||||
|
self.capnp_modules = self.load_capnp_modules()
|
||||||
|
|
||||||
|
async def make_capnp_init_ctx(self):
|
||||||
|
node = self.nodes[0]
|
||||||
|
# Establish a connection, and create Init proxy object.
|
||||||
|
connection = await capnp.AsyncIoStream.create_unix_connection(node.ipc_socket_path)
|
||||||
|
client = capnp.TwoPartyClient(connection)
|
||||||
|
init = client.bootstrap().cast_as(self.capnp_modules['init'].Init)
|
||||||
|
# Create a remote thread on the server for the IPC calls to be executed in.
|
||||||
|
threadmap = init.construct().threadMap
|
||||||
|
thread = threadmap.makeThread("pythread").result
|
||||||
|
ctx = self.capnp_modules['proxy'].Context()
|
||||||
|
ctx.thread = thread
|
||||||
|
# Return both.
|
||||||
|
return ctx, init
|
||||||
|
|
||||||
|
async def parse_and_deserialize_block(self, block_template, ctx):
|
||||||
|
block_data = BytesIO((await block_template.result.getBlock(ctx)).result)
|
||||||
|
block = CBlock()
|
||||||
|
block.deserialize(block_data)
|
||||||
|
return block
|
||||||
|
|
||||||
|
def run_echo_test(self):
|
||||||
|
self.log.info("Running echo test")
|
||||||
|
async def async_routine():
|
||||||
|
ctx, init = await self.make_capnp_init_ctx()
|
||||||
|
self.log.debug("Create Echo proxy object")
|
||||||
|
echo = init.makeEcho(ctx).result
|
||||||
|
self.log.debug("Test a few invocations of echo")
|
||||||
|
for s in ["hallo", "", "haha"]:
|
||||||
|
result_eval = (await echo.echo(ctx, s)).result
|
||||||
|
assert_equal(s, result_eval)
|
||||||
|
self.log.debug("Destroy the Echo object")
|
||||||
|
echo.destroy(ctx)
|
||||||
|
asyncio.run(capnp.run(async_routine()))
|
||||||
|
|
||||||
|
def run_mining_test(self):
|
||||||
|
self.log.info("Running mining test")
|
||||||
|
block_hash_size = 32
|
||||||
|
block_header_size = 80
|
||||||
|
timeout = 1000.0 # 1000 milliseconds
|
||||||
|
miniwallet = MiniWallet(self.nodes[0])
|
||||||
|
|
||||||
|
async def async_routine():
|
||||||
|
ctx, init = await self.make_capnp_init_ctx()
|
||||||
|
self.log.debug("Create Mining proxy object")
|
||||||
|
mining = init.makeMining(ctx)
|
||||||
|
self.log.debug("Test simple inspectors")
|
||||||
|
assert (await mining.result.isTestChain(ctx))
|
||||||
|
assert (await mining.result.isInitialBlockDownload(ctx))
|
||||||
|
blockref = await mining.result.getTip(ctx)
|
||||||
|
assert blockref.hasResult
|
||||||
|
assert_equal(len(blockref.result.hash), block_hash_size)
|
||||||
|
current_block_height = self.nodes[0].getchaintips()[0]["height"]
|
||||||
|
assert blockref.result.height == current_block_height
|
||||||
|
self.log.debug("Mine a block")
|
||||||
|
wait = mining.result.waitTipChanged(ctx, blockref.result.hash, )
|
||||||
|
self.generate(self.nodes[0], 1)
|
||||||
|
newblockref = await wait
|
||||||
|
assert_equal(len(newblockref.result.hash), block_hash_size)
|
||||||
|
assert_equal(newblockref.result.height, current_block_height + 1)
|
||||||
|
self.log.debug("Wait for timeout")
|
||||||
|
wait = mining.result.waitTipChanged(ctx, newblockref.result.hash, timeout)
|
||||||
|
oldblockref = await wait
|
||||||
|
assert_equal(len(newblockref.result.hash), block_hash_size)
|
||||||
|
assert_equal(oldblockref.result.hash, newblockref.result.hash)
|
||||||
|
assert_equal(oldblockref.result.height, newblockref.result.height)
|
||||||
|
|
||||||
|
self.log.debug("Create a template")
|
||||||
|
opts = self.capnp_modules['mining'].BlockCreateOptions()
|
||||||
|
opts.useMempool = True
|
||||||
|
opts.blockReservedWeight = 4000
|
||||||
|
opts.coinbaseOutputMaxAdditionalSigops = 0
|
||||||
|
template = mining.result.createNewBlock(opts)
|
||||||
|
self.log.debug("Test some inspectors of Template")
|
||||||
|
header = await template.result.getBlockHeader(ctx)
|
||||||
|
assert_equal(len(header.result), block_header_size)
|
||||||
|
block = await self.parse_and_deserialize_block(template, ctx)
|
||||||
|
assert_equal(ser_uint256(block.hashPrevBlock), newblockref.result.hash)
|
||||||
|
assert len(block.vtx) >= 1
|
||||||
|
txfees = await template.result.getTxFees(ctx)
|
||||||
|
assert_equal(len(txfees.result), 0)
|
||||||
|
txsigops = await template.result.getTxSigops(ctx)
|
||||||
|
assert_equal(len(txsigops.result), 0)
|
||||||
|
coinbase_data = BytesIO((await template.result.getCoinbaseTx(ctx)).result)
|
||||||
|
coinbase = CTransaction()
|
||||||
|
coinbase.deserialize(coinbase_data)
|
||||||
|
assert_equal(coinbase.vin[0].prevout.hash, 0)
|
||||||
|
self.log.debug("Wait for a new template")
|
||||||
|
waitoptions = self.capnp_modules['mining'].BlockWaitOptions()
|
||||||
|
waitoptions.timeout = timeout
|
||||||
|
waitnext = template.result.waitNext(ctx, waitoptions)
|
||||||
|
self.generate(self.nodes[0], 1)
|
||||||
|
template2 = await waitnext
|
||||||
|
block2 = await self.parse_and_deserialize_block(template2, ctx)
|
||||||
|
assert_equal(len(block2.vtx), 1)
|
||||||
|
self.log.debug("Wait for another, but time out")
|
||||||
|
template3 = await template2.result.waitNext(ctx, waitoptions)
|
||||||
|
assert_equal(template3.to_dict(), {})
|
||||||
|
self.log.debug("Wait for another, get one after increase in fees in the mempool")
|
||||||
|
waitnext = template2.result.waitNext(ctx, waitoptions)
|
||||||
|
miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0])
|
||||||
|
template4 = await waitnext
|
||||||
|
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")
|
||||||
|
template5 = await template4.result.waitNext(ctx, waitoptions)
|
||||||
|
block4 = await self.parse_and_deserialize_block(template5, ctx)
|
||||||
|
assert_equal(len(block4.vtx), 2)
|
||||||
|
waitoptions.feeThreshold = 1
|
||||||
|
self.log.debug("Wait for another, get one after increase in fees in the mempool")
|
||||||
|
waitnext = template5.result.waitNext(ctx, waitoptions)
|
||||||
|
miniwallet.send_self_transfer(fee_rate=10, from_node=self.nodes[0])
|
||||||
|
template6 = await waitnext
|
||||||
|
block4 = await self.parse_and_deserialize_block(template6, ctx)
|
||||||
|
assert_equal(len(block4.vtx), 3)
|
||||||
|
self.log.debug("Wait for another, but time out, since the fee threshold is set now")
|
||||||
|
template7 = await template6.result.waitNext(ctx, waitoptions)
|
||||||
|
assert_equal(template7.to_dict(), {})
|
||||||
|
self.log.debug("Destroy template objects")
|
||||||
|
template.result.destroy(ctx)
|
||||||
|
template2.result.destroy(ctx)
|
||||||
|
template3.result.destroy(ctx)
|
||||||
|
template4.result.destroy(ctx)
|
||||||
|
template5.result.destroy(ctx)
|
||||||
|
template6.result.destroy(ctx)
|
||||||
|
template7.result.destroy(ctx)
|
||||||
|
asyncio.run(capnp.run(async_routine()))
|
||||||
|
|
||||||
|
def run_test(self):
|
||||||
|
self.run_echo_test()
|
||||||
|
self.run_mining_test()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
IPCInterfaceTest(__file__).main()
|
||||||
@@ -76,9 +76,9 @@ class Binaries:
|
|||||||
self.paths = paths
|
self.paths = paths
|
||||||
self.bin_dir = bin_dir
|
self.bin_dir = bin_dir
|
||||||
|
|
||||||
def node_argv(self):
|
def node_argv(self, **kwargs):
|
||||||
"Return argv array that should be used to invoke bitcoind"
|
"Return argv array that should be used to invoke bitcoind"
|
||||||
return self._argv("node", self.paths.bitcoind)
|
return self._argv("node", self.paths.bitcoind, **kwargs)
|
||||||
|
|
||||||
def rpc_argv(self):
|
def rpc_argv(self):
|
||||||
"Return argv array that should be used to invoke bitcoin-cli"
|
"Return argv array that should be used to invoke bitcoin-cli"
|
||||||
@@ -101,16 +101,19 @@ class Binaries:
|
|||||||
"Return argv array that should be used to invoke bitcoin-chainstate"
|
"Return argv array that should be used to invoke bitcoin-chainstate"
|
||||||
return self._argv("chainstate", self.paths.bitcoinchainstate)
|
return self._argv("chainstate", self.paths.bitcoinchainstate)
|
||||||
|
|
||||||
def _argv(self, command, bin_path):
|
def _argv(self, command, bin_path, need_ipc=False):
|
||||||
"""Return argv array that should be used to invoke the command. It
|
"""Return argv array that should be used to invoke the command. It
|
||||||
either uses the bitcoin wrapper executable (if BITCOIN_CMD is set), or
|
either uses the bitcoin wrapper executable (if BITCOIN_CMD is set or
|
||||||
the direct binary path (bitcoind, etc). When bin_dir is set (by tests
|
need_ipc is True), or the direct binary path (bitcoind, etc). When
|
||||||
calling binaries from previous releases) it always uses the direct
|
bin_dir is set (by tests calling binaries from previous releases) it
|
||||||
path."""
|
always uses the direct path."""
|
||||||
if self.bin_dir is not None:
|
if self.bin_dir is not None:
|
||||||
return [os.path.join(self.bin_dir, os.path.basename(bin_path))]
|
return [os.path.join(self.bin_dir, os.path.basename(bin_path))]
|
||||||
elif self.paths.bitcoin_cmd is not None:
|
elif self.paths.bitcoin_cmd is not None or need_ipc:
|
||||||
return self.paths.bitcoin_cmd + [command]
|
# If the current test needs IPC functionality, use the bitcoin
|
||||||
|
# wrapper binary and append -m so it calls multiprocess binaries.
|
||||||
|
bitcoin_cmd = self.paths.bitcoin_cmd or [self.paths.bitcoin_bin]
|
||||||
|
return bitcoin_cmd + (["-m"] if need_ipc else []) + [command]
|
||||||
else:
|
else:
|
||||||
return [bin_path]
|
return [bin_path]
|
||||||
|
|
||||||
@@ -158,6 +161,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
|
|||||||
self.noban_tx_relay: bool = False
|
self.noban_tx_relay: bool = False
|
||||||
self.nodes: list[TestNode] = []
|
self.nodes: list[TestNode] = []
|
||||||
self.extra_args = None
|
self.extra_args = None
|
||||||
|
self.extra_init = None
|
||||||
self.network_thread = None
|
self.network_thread = None
|
||||||
self.rpc_timeout = 60 # Wait for up to 60 seconds for the RPC server to respond
|
self.rpc_timeout = 60 # Wait for up to 60 seconds for the RPC server to respond
|
||||||
self.supports_cli = True
|
self.supports_cli = True
|
||||||
@@ -278,6 +282,7 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
|
|||||||
|
|
||||||
paths = types.SimpleNamespace()
|
paths = types.SimpleNamespace()
|
||||||
binaries = {
|
binaries = {
|
||||||
|
"bitcoin": "BITCOIN_BIN",
|
||||||
"bitcoind": "BITCOIND",
|
"bitcoind": "BITCOIND",
|
||||||
"bitcoin-cli": "BITCOINCLI",
|
"bitcoin-cli": "BITCOINCLI",
|
||||||
"bitcoin-util": "BITCOINUTIL",
|
"bitcoin-util": "BITCOINUTIL",
|
||||||
@@ -285,6 +290,8 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
|
|||||||
"bitcoin-chainstate": "BITCOINCHAINSTATE",
|
"bitcoin-chainstate": "BITCOINCHAINSTATE",
|
||||||
"bitcoin-wallet": "BITCOINWALLET",
|
"bitcoin-wallet": "BITCOINWALLET",
|
||||||
}
|
}
|
||||||
|
# Set paths to bitcoin core binaries allowing overrides with environment
|
||||||
|
# variables.
|
||||||
for binary, env_variable_name in binaries.items():
|
for binary, env_variable_name in binaries.items():
|
||||||
default_filename = os.path.join(
|
default_filename = os.path.join(
|
||||||
self.config["environment"]["BUILDDIR"],
|
self.config["environment"]["BUILDDIR"],
|
||||||
@@ -550,15 +557,15 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
|
|||||||
|
|
||||||
bin_dirs.append(bin_dir)
|
bin_dirs.append(bin_dir)
|
||||||
|
|
||||||
|
extra_init = [{}] * num_nodes if self.extra_init is None else self.extra_init # type: ignore[var-annotated]
|
||||||
|
assert_equal(len(extra_init), num_nodes)
|
||||||
assert_equal(len(extra_confs), num_nodes)
|
assert_equal(len(extra_confs), num_nodes)
|
||||||
assert_equal(len(extra_args), num_nodes)
|
assert_equal(len(extra_args), num_nodes)
|
||||||
assert_equal(len(versions), num_nodes)
|
assert_equal(len(versions), num_nodes)
|
||||||
assert_equal(len(bin_dirs), num_nodes)
|
assert_equal(len(bin_dirs), num_nodes)
|
||||||
for i in range(num_nodes):
|
for i in range(num_nodes):
|
||||||
args = list(extra_args[i])
|
args = list(extra_args[i])
|
||||||
test_node_i = TestNode(
|
init = dict(
|
||||||
i,
|
|
||||||
get_datadir_path(self.options.tmpdir, i),
|
|
||||||
chain=self.chain,
|
chain=self.chain,
|
||||||
rpchost=rpchost,
|
rpchost=rpchost,
|
||||||
timewait=self.rpc_timeout,
|
timewait=self.rpc_timeout,
|
||||||
@@ -575,6 +582,11 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
|
|||||||
v2transport=self.options.v2transport,
|
v2transport=self.options.v2transport,
|
||||||
uses_wallet=self.uses_wallet,
|
uses_wallet=self.uses_wallet,
|
||||||
)
|
)
|
||||||
|
init.update(extra_init[i])
|
||||||
|
test_node_i = TestNode(
|
||||||
|
i,
|
||||||
|
get_datadir_path(self.options.tmpdir, i),
|
||||||
|
**init)
|
||||||
self.nodes.append(test_node_i)
|
self.nodes.append(test_node_i)
|
||||||
if not test_node_i.version_is_at_least(170000):
|
if not test_node_i.version_is_at_least(170000):
|
||||||
# adjust conf for pre 17
|
# adjust conf for pre 17
|
||||||
@@ -952,6 +964,13 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
raise SkipTest("sqlite3 module not available.")
|
raise SkipTest("sqlite3 module not available.")
|
||||||
|
|
||||||
|
def skip_if_no_py_capnp(self):
|
||||||
|
"""Attempt to import the capnp package and skip the test if the import fails."""
|
||||||
|
try:
|
||||||
|
import capnp # type: ignore[import] # noqa: F401
|
||||||
|
except ImportError:
|
||||||
|
raise SkipTest("capnp module not available.")
|
||||||
|
|
||||||
def skip_if_no_python_bcc(self):
|
def skip_if_no_python_bcc(self):
|
||||||
"""Attempt to import the bcc package and skip the tests if the import fails."""
|
"""Attempt to import the bcc package and skip the tests if the import fails."""
|
||||||
try:
|
try:
|
||||||
@@ -1016,6 +1035,11 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
|
|||||||
if not self.is_cli_compiled():
|
if not self.is_cli_compiled():
|
||||||
raise SkipTest("bitcoin-cli has not been compiled.")
|
raise SkipTest("bitcoin-cli has not been compiled.")
|
||||||
|
|
||||||
|
def skip_if_no_ipc(self):
|
||||||
|
"""Skip the running test if ipc is not compiled."""
|
||||||
|
if not self.is_ipc_compiled():
|
||||||
|
raise SkipTest("ipc has not been compiled.")
|
||||||
|
|
||||||
def skip_if_no_previous_releases(self):
|
def skip_if_no_previous_releases(self):
|
||||||
"""Skip the running test if previous releases are not available."""
|
"""Skip the running test if previous releases are not available."""
|
||||||
if not self.has_previous_releases():
|
if not self.has_previous_releases():
|
||||||
@@ -1075,6 +1099,10 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
|
|||||||
"""Checks whether the USDT tracepoints were compiled."""
|
"""Checks whether the USDT tracepoints were compiled."""
|
||||||
return self.config["components"].getboolean("ENABLE_USDT_TRACEPOINTS")
|
return self.config["components"].getboolean("ENABLE_USDT_TRACEPOINTS")
|
||||||
|
|
||||||
|
def is_ipc_compiled(self):
|
||||||
|
"""Checks whether ipc was compiled."""
|
||||||
|
return self.config["components"].getboolean("ENABLE_IPC")
|
||||||
|
|
||||||
def has_blockfile(self, node, filenum: str):
|
def has_blockfile(self, node, filenum: str):
|
||||||
return (node.blocks_path/ f"blk{filenum}.dat").is_file()
|
return (node.blocks_path/ f"blk{filenum}.dat").is_file()
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from enum import Enum
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import pathlib
|
||||||
import platform
|
import platform
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -19,6 +20,7 @@ import time
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
import collections
|
import collections
|
||||||
import shlex
|
import shlex
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -52,6 +54,13 @@ CLI_MAX_ARG_SIZE = 131071 # many systems have a 128kb limit per arg (MAX_ARG_STR
|
|||||||
NULL_BLK_XOR_KEY = bytes([0] * NUM_XOR_BYTES)
|
NULL_BLK_XOR_KEY = bytes([0] * NUM_XOR_BYTES)
|
||||||
BITCOIN_PID_FILENAME_DEFAULT = "bitcoind.pid"
|
BITCOIN_PID_FILENAME_DEFAULT = "bitcoind.pid"
|
||||||
|
|
||||||
|
if sys.platform.startswith("linux"):
|
||||||
|
UNIX_PATH_MAX = 108 # includes the trailing NUL
|
||||||
|
elif sys.platform.startswith(("darwin", "freebsd", "netbsd", "openbsd")):
|
||||||
|
UNIX_PATH_MAX = 104
|
||||||
|
else: # safest portable value
|
||||||
|
UNIX_PATH_MAX = 92
|
||||||
|
|
||||||
|
|
||||||
class FailedToStartError(Exception):
|
class FailedToStartError(Exception):
|
||||||
"""Raised when a node fails to start correctly."""
|
"""Raised when a node fails to start correctly."""
|
||||||
@@ -77,7 +86,7 @@ class TestNode():
|
|||||||
To make things easier for the test writer, any unrecognised messages will
|
To make things easier for the test writer, any unrecognised messages will
|
||||||
be dispatched to the RPC connection."""
|
be dispatched to the RPC connection."""
|
||||||
|
|
||||||
def __init__(self, i, datadir_path, *, chain, rpchost, timewait, timeout_factor, binaries, coverage_dir, cwd, extra_conf=None, extra_args=None, use_cli=False, start_perf=False, use_valgrind=False, version=None, v2transport=False, uses_wallet=False):
|
def __init__(self, i, datadir_path, *, chain, rpchost, timewait, timeout_factor, binaries, coverage_dir, cwd, extra_conf=None, extra_args=None, use_cli=False, start_perf=False, use_valgrind=False, version=None, v2transport=False, uses_wallet=False, ipcbind=False):
|
||||||
"""
|
"""
|
||||||
Kwargs:
|
Kwargs:
|
||||||
start_perf (bool): If True, begin profiling the node with `perf` as soon as
|
start_perf (bool): If True, begin profiling the node with `perf` as soon as
|
||||||
@@ -109,7 +118,7 @@ class TestNode():
|
|||||||
# Configuration for logging is set as command-line args rather than in the bitcoin.conf file.
|
# Configuration for logging is set as command-line args rather than in the bitcoin.conf file.
|
||||||
# This means that starting a bitcoind using the temp dir to debug a failed test won't
|
# This means that starting a bitcoind using the temp dir to debug a failed test won't
|
||||||
# spam debug.log.
|
# spam debug.log.
|
||||||
self.args = self.binaries.node_argv() + [
|
self.args = self.binaries.node_argv(need_ipc=ipcbind) + [
|
||||||
f"-datadir={self.datadir_path}",
|
f"-datadir={self.datadir_path}",
|
||||||
"-logtimemicros",
|
"-logtimemicros",
|
||||||
"-debug",
|
"-debug",
|
||||||
@@ -121,6 +130,17 @@ class TestNode():
|
|||||||
if uses_wallet is not None and not uses_wallet:
|
if uses_wallet is not None and not uses_wallet:
|
||||||
self.args.append("-disablewallet")
|
self.args.append("-disablewallet")
|
||||||
|
|
||||||
|
self.ipc_tmp_dir = None
|
||||||
|
if ipcbind:
|
||||||
|
self.ipc_socket_path = self.chain_path / "node.sock"
|
||||||
|
if len(os.fsencode(self.ipc_socket_path)) < UNIX_PATH_MAX:
|
||||||
|
self.args.append("-ipcbind=unix")
|
||||||
|
else:
|
||||||
|
# Work around default CI path exceeding maximum socket path length.
|
||||||
|
self.ipc_tmp_dir = pathlib.Path(tempfile.mkdtemp(prefix="test-ipc-"))
|
||||||
|
self.ipc_socket_path = self.ipc_tmp_dir / "node.sock"
|
||||||
|
self.args.append(f"-ipcbind=unix:{self.ipc_socket_path}")
|
||||||
|
|
||||||
# Use valgrind, expect for previous release binaries
|
# Use valgrind, expect for previous release binaries
|
||||||
if use_valgrind and version is None:
|
if use_valgrind and version is None:
|
||||||
default_suppressions_file = Path(__file__).parents[3] / "contrib" / "valgrind.supp"
|
default_suppressions_file = Path(__file__).parents[3] / "contrib" / "valgrind.supp"
|
||||||
@@ -208,6 +228,9 @@ class TestNode():
|
|||||||
# this destructor is called.
|
# this destructor is called.
|
||||||
print(self._node_msg("Cleaning up leftover process"), file=sys.stderr)
|
print(self._node_msg("Cleaning up leftover process"), file=sys.stderr)
|
||||||
self.process.kill()
|
self.process.kill()
|
||||||
|
if self.ipc_tmp_dir:
|
||||||
|
print(self._node_msg(f"Cleaning up ipc directory {str(self.ipc_tmp_dir)!r}"))
|
||||||
|
shutil.rmtree(self.ipc_tmp_dir)
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
"""Dispatches any unrecognised messages to the RPC connection or a CLI instance."""
|
"""Dispatches any unrecognised messages to the RPC connection or a CLI instance."""
|
||||||
|
|||||||
@@ -339,6 +339,7 @@ BASE_SCRIPTS = [
|
|||||||
'rpc_scantxoutset.py',
|
'rpc_scantxoutset.py',
|
||||||
'feature_unsupported_utxo_db.py',
|
'feature_unsupported_utxo_db.py',
|
||||||
'feature_logging.py',
|
'feature_logging.py',
|
||||||
|
'interface_ipc.py',
|
||||||
'feature_anchors.py',
|
'feature_anchors.py',
|
||||||
'mempool_datacarrier.py',
|
'mempool_datacarrier.py',
|
||||||
'feature_coinstatsindex.py',
|
'feature_coinstatsindex.py',
|
||||||
|
|||||||
Reference in new issue
Block a user