Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion .github/workflows/build_bdk.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ jobs:
- name: Check out repository
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
fetch-depth: 0

- name: Set up Python 3
Expand Down Expand Up @@ -133,6 +132,28 @@ jobs:
cmake --build build --parallel 4
ctest --output-on-failure --test-dir build

- name: Build GoBDK shared library for purego
if: runner.os != 'Windows'
run: |
cmake --build build --target GoBDK --parallel 4

- name: Test GoBDK with purego
if: runner.os != 'Windows'
env:
GOFLAGS: ""
run: |
if [ "$(uname)" = "Darwin" ]; then
LIB_EXT="dylib"
else
LIB_EXT="so"
fi
LIB_PATH=${{ github.workspace }}/module/gobdk/bdkpurego/lib/libGoBDK_${{ env.OS_ARCH }}.${LIB_EXT}
echo "Library path: ${LIB_PATH}"
ls -lh "${LIB_PATH}" || echo "WARNING: shared library not found"
cd test/golang
BDK_LIB_PATH=${LIB_PATH} \
CGO_ENABLED=0 go test -tags purego -mod=mod -v ./... || echo "::warning::purego tests failed (non-blocking)"

- name: Check GoBDK has changed
id: gobdk_change
if: runner.os != 'Windows' && github.event_name == 'workflow_dispatch'
Expand All @@ -156,6 +177,18 @@ jobs:
retention-days: 1
overwrite: true

- name: Upload GoBDK shared libraries to artifact UNIX
if: runner.os != 'Windows' && github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v4
with:
name: libGoBDK_shared_${{ env.OS_ARCH }}
path: |
./module/gobdk/bdkpurego/lib/libGoBDK_${{ env.OS_ARCH }}.dylib
./module/gobdk/bdkpurego/lib/libGoBDK_${{ env.OS_ARCH }}.so
retention-days: 1
overwrite: true
if-no-files-found: ignore

- name: Prepare Environment and Dependencies for Windows
if: runner.os == 'Windows'
run: |
Expand Down
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# IDE
.idea/

# CMake build output
build/

# Shared libraries for purego are committed (same as static .a files in bdkcgo/)
# Only ignore intermediate build artifacts, not the final libs
# module/gobdk/bdkpurego/lib/ -- tracked, not ignored

# Go vendor directory (test module)
test/golang/vendor/
12 changes: 10 additions & 2 deletions cmake/BDKBuildSetting.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,18 @@ macro(bdkSetCompilationOptions)## This has to be macro instead of a function bec
endif()

if(UNIX)
set(CMAKE_CXX_FLAGS "-fPIC -fvisibility=default" CACHE STRING "compilation flags for C++" FORCE)
# Use the correct native-tuning flag for each architecture
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64)$")
set(_BDK_NATIVE_FLAG "-march=native")
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
set(_BDK_NATIVE_FLAG "-mcpu=native")
else()
set(_BDK_NATIVE_FLAG "")
endif()
set(CMAKE_CXX_FLAGS "-fPIC -fvisibility=default ${_BDK_NATIVE_FLAG}" CACHE STRING "compilation flags for C++" FORCE)
set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g3 " CACHE STRING "flags for C++ debug version" FORCE)
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -g0 -DNDEBUG" CACHE STRING "flags for C++ release version" FORCE)
set(CMAKE_C_FLAGS "-fPIC" CACHE STRING "flags for C" FORCE)
set(CMAKE_C_FLAGS "-fPIC ${_BDK_NATIVE_FLAG}" CACHE STRING "flags for C" FORCE)
set(CMAKE_C_FLAGS_DEBUG "-O0 -g3" CACHE STRING "flags for C debug version" FORCE)
set(CMAKE_C_FLAGS_RELEASE "-O3 -g0 -DNDEBUG" CACHE STRING "flags for C release version" FORCE)
endif()
Expand Down
73 changes: 73 additions & 0 deletions core/scriptengine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,79 @@ std::vector<ScriptError> bsv::CScriptEngine::VerifyScriptBatch(const VerifyBatch
return results;
}

void bsv::CScriptEngine::ensureThreadPool() const
{
std::call_once(threadPoolInit, [this]() {
size_t n = std::thread::hardware_concurrency();
if (n == 0) n = 1;
threadPool = std::make_unique<ThreadPool>(n);
});
}

std::vector<ScriptError> bsv::CScriptEngine::VerifyScriptBatchParallel(const VerifyBatch& batch, size_t numThreads) const
{
const size_t batchSize = batch.size();
if (batchSize == 0) return {};

ensureThreadPool();

if (numThreads == 0) {
numThreads = threadPool->size();
}

numThreads = std::min(numThreads, batchSize);

std::vector<ScriptError> results(batchSize, SCRIPT_ERR_OK);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default result should be SCRIPT_ERR_UNKNOWN_ERROR, i.e in case something unknown happens, the error is relevant.


if (numThreads == 1) {
size_t i = 0;
for (const auto& elem : batch) {
try {
results[i] = VerifyScript(elem.extendedTX, elem.utxoHeights, elem.blockHeight, elem.consensus, elem.customFlags);
} catch (...) {
results[i] = SCRIPT_ERR_UNKNOWN_ERROR;
}
++i;
}
return results;
}

auto worker = [&](size_t start, size_t end) {
auto it = batch.begin();
std::advance(it, start);
for (size_t i = start; i < end; ++i, ++it) {
try {
results[i] = VerifyScript(it->extendedTX, it->utxoHeights, it->blockHeight, it->consensus, it->customFlags);
} catch (...) {
results[i] = SCRIPT_ERR_UNKNOWN_ERROR;
}
}
};

const size_t chunkSize = batchSize / numThreads;
const size_t remainder = batchSize % numThreads;

std::vector<std::future<void>> futures;
futures.reserve(numThreads - 1);

size_t start = 0;
for (size_t t = 0; t < numThreads; ++t) {
size_t end = start + chunkSize + (t < remainder ? 1 : 0);
if (t == numThreads - 1) {
worker(start, end); // Main thread handles last chunk
} else {
futures.push_back(threadPool->submit([&worker, start, end]() { worker(start, end); }));
}
start = end;
}

for (auto& f : futures) {
f.get();
}

return results;
}

ScriptError bsv::CScriptEngine::verifyImpl(
const CScript& unlocking_script,
const CScript& locking_script,
Expand Down
14 changes: 14 additions & 0 deletions core/scriptengine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@
#include <cstdint>
#include <span>
#include <memory>
#include <mutex>
#include <optional>
#include <utility>
#include <vector>
#include <thread>

#include <configscriptpolicy.h>
#include <chainparams.h>
#include <taskcancellation.h>
#include <script/script.h>
#include <script/interpreter.h>
#include <verifyarg.hpp>
#include <thread_pool.hpp>

namespace bsv
{
Expand Down Expand Up @@ -96,11 +99,22 @@ class CScriptEngine {
// Returns a vector of ScriptError results, one for each VerifyArg in the input
std::vector<ScriptError> VerifyScriptBatch(const VerifyBatch& batch) const;

// VerifyScriptBatchParallel processes multiple script verifications in parallel
// numThreads: number of threads to use (0 = std::thread::hardware_concurrency())
// Divides the batch into chunks and processes each chunk on a separate thread
// Per-item exceptions are caught and reported as SCRIPT_ERR_UNKNOWN_ERROR
std::vector<ScriptError> VerifyScriptBatchParallel(const VerifyBatch& batch, size_t numThreads = 0) const;

private :
ConfigScriptPolicy policySettings;
std::unique_ptr<CChainParams> chainParams;
std::shared_ptr<task::CCancellationSource> source;

// Lazy-initialized thread pool for VerifyScriptBatchParallel
mutable std::unique_ptr<ThreadPool> threadPool;
mutable std::once_flag threadPoolInit;
void ensureThreadPool() const;

ScriptError verifyImpl(
const CScript& unlocking_script,
const CScript& locking_script,
Expand Down
14 changes: 14 additions & 0 deletions core/setting-secp256k1.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@ endif()
set(SECP256K1_BUILD_TESTS OFF)
set(SECP256K1_BUILD_EXHAUSTIVE_TESTS OFF)

# Enable architecture-specific optimizations for secp256k1:
# - x86_64: hand-optimized assembly for field/scalar ops + -march=native (AVX2, etc.)
# - ARM64: generic C with __int128 + -mcpu=native (Apple M-series/Neoverse tuning)
# Also override secp256k1's default -O2 with -O3 for maximum throughput.
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64)$")
set(SECP256K1_ASM "x86_64" CACHE STRING "" FORCE)
set(SECP256K1_APPEND_CFLAGS "-march=native -O3" CACHE STRING "" FORCE)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)$")
set(SECP256K1_ASM "OFF" CACHE STRING "" FORCE)
set(SECP256K1_APPEND_CFLAGS "-mcpu=native -O3" CACHE STRING "" FORCE)
else()
set(SECP256K1_APPEND_CFLAGS "-O3" CACHE STRING "" FORCE)
endif()

add_subdirectory("${BDK_BSV_ROOT_DIR}/src/secp256k1" ${CMAKE_CURRENT_BINARY_DIR}/secp256k1)

## Set the IDE Folder to the created targets for secp256k1 to the right place #########
Expand Down
79 changes: 79 additions & 0 deletions core/thread_pool.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#ifndef __THREAD_POOL_HPP__
#define __THREAD_POOL_HPP__

#include <condition_variable>
#include <functional>
#include <future>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>

namespace bsv
{

class ThreadPool {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying hard to make the bdk core not depending to thread. The reason was to plan for wasm build, that don't like threading.

Threading implementation would be in client code, i.e module level (go module, py module ...), or at higher level (client code)

public:
explicit ThreadPool(size_t numThreads)
{
workers.reserve(numThreads);
for (size_t i = 0; i < numThreads; ++i) {
workers.emplace_back([this] {
for (;;) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this] { return stop || !tasks.empty(); });
if (stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
});
}
}

~ThreadPool()
{
{
std::lock_guard<std::mutex> lock(mtx);
stop = true;
}
cv.notify_all();
for (auto& w : workers) {
w.join();
}
}

// Non-copyable, non-movable
ThreadPool(const ThreadPool&) = delete;
ThreadPool& operator=(const ThreadPool&) = delete;

template<typename F>
auto submit(F&& f) -> std::future<decltype(f())>
{
using ReturnType = decltype(f());
auto task = std::make_shared<std::packaged_task<ReturnType()>>(std::forward<F>(f));
auto future = task->get_future();
{
std::lock_guard<std::mutex> lock(mtx);
tasks.emplace([task]() { (*task)(); });
}
cv.notify_one();
return future;
}

size_t size() const { return workers.size(); }

private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex mtx;
std::condition_variable cv;
bool stop = false;
};

} // namespace bsv

#endif /* __THREAD_POOL_HPP__ */
63 changes: 59 additions & 4 deletions module/gobdk/bdkcgo/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,47 @@ set(_GO_MODULE_SOURCE_FILES
"${BSV_APPLICATION_SRC_FILES}"
)

# The shared libraries is not to be used in a normal situation. But we keep building this target
# just in case we might use it sometime later.
# The shared library is built for use with purego (Go FFI without CGO).
# It bundles all dependencies so the .so/.dylib is self-contained.
add_library(${_GO_MODULE_NAME} SHARED ${_GO_MODULE_HEADER_FILES} ${_GO_MODULE_SOURCE_FILES} ${BDK_VERSION_GOLANG_CPP})
target_link_libraries(${_GO_MODULE_NAME} PUBLIC bdk_core secp256k1 univalue OpenSSL::Crypto OpenSSL::SSL Boost::thread Boost::filesystem Boost::program_options Boost::chrono ${CMAKE_DL_LIBS} ${CMAKE_THREAD_LIBS_INIT})
target_include_directories(${_GO_MODULE_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/.. ${CMAKE_SOURCE_DIR})
target_include_directories(${_GO_MODULE_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/.. ${CMAKE_SOURCE_DIR} ${BSV_INCLUDE_DIRS})
# Get boost/openssl include directories without linking (linking is done via whole-archive below)
foreach(depTarget IN ITEMS Boost::thread Boost::filesystem Boost::program_options Boost::chrono OpenSSL::Crypto OpenSSL::SSL)
get_target_property(_inc ${depTarget} INTERFACE_INCLUDE_DIRECTORIES)
if(_inc)
target_include_directories(${_GO_MODULE_NAME} PRIVATE ${_inc})
endif()
endforeach()

# Collect static library paths for bundling into the shared library
set(_SHARED_STATIC_DEPS)
foreach(impTarget IN ITEMS OpenSSL::Crypto OpenSSL::SSL Boost::thread Boost::filesystem Boost::program_options Boost::chrono)
get_property(vImported TARGET ${impTarget} PROPERTY IMPORTED)
if (${vImported})
get_property(vLocation TARGET ${impTarget} PROPERTY LOCATION)
list(APPEND _SHARED_STATIC_DEPS ${vLocation})
endif()
endforeach()

if(APPLE)
# macOS: use -force_load to include all symbols from static archives
set(_FORCE_LOAD_FLAGS "")
foreach(dep IN LISTS _SHARED_STATIC_DEPS)
list(APPEND _FORCE_LOAD_FLAGS "-Wl,-force_load,${dep}")
endforeach()
target_link_libraries(${_GO_MODULE_NAME} PUBLIC
bdk_core secp256k1 univalue
${_FORCE_LOAD_FLAGS}
${CMAKE_DL_LIBS} ${CMAKE_THREAD_LIBS_INIT})
elseif(UNIX)
# Linux: use --whole-archive to include all symbols from static archives
target_link_libraries(${_GO_MODULE_NAME} PUBLIC
bdk_core secp256k1 univalue
-Wl,--whole-archive ${_SHARED_STATIC_DEPS} -Wl,--no-whole-archive
${CMAKE_DL_LIBS} ${CMAKE_THREAD_LIBS_INIT})
else()
target_link_libraries(${_GO_MODULE_NAME} PUBLIC bdk_core secp256k1 univalue OpenSSL::Crypto OpenSSL::SSL Boost::thread Boost::filesystem Boost::program_options Boost::chrono ${CMAKE_DL_LIBS} ${CMAKE_THREAD_LIBS_INIT})
endif()

# The static libraries is built and to be merge will all of its dependant archives
# to make a all-in-one standalone static library, ready to be linked in cgo
Expand Down Expand Up @@ -148,4 +184,23 @@ if(UNIX)
)
endif()

## Install shared library with os_arch naming for purego usage
## IMPORTANT: shared lib goes into bdkpurego/lib/, NOT bdkcgo/ — placing it next
## to the static .a would cause the CGO linker to prefer the .so and break static linking.
if(APPLE)
set(sharedLibExt "dylib")
else()
set(sharedLibExt "so")
endif()
set(sharedLibName "libGoBDK_${os_arch}.${sharedLibExt}")
set_target_properties(${_GO_MODULE_NAME} PROPERTIES OUTPUT_NAME "GoBDK_${os_arch}")
install(FILES "$<TARGET_FILE:${_GO_MODULE_NAME}>" DESTINATION "lib/bdkpurego" COMPONENT GoModules)
if (BUILD_MODULE_GOLANG_INSTALL_INSOURCE)
add_custom_command(TARGET ${_GO_MODULE_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_SOURCE_DIR}/module/gobdk/bdkpurego/lib"
COMMAND ${CMAKE_COMMAND} -E copy "$<TARGET_FILE:${_GO_MODULE_NAME}>" "${CMAKE_SOURCE_DIR}/module/gobdk/bdkpurego/lib/${sharedLibName}"
COMMENT "Install insource ${sharedLibName} to bdkpurego/lib/"
)
endif()

endif()
Binary file modified module/gobdk/bdkcgo/libGoBDK_darwin_arm64.a
Binary file not shown.
Binary file modified module/gobdk/bdkcgo/libGoBDK_darwin_x86_64.a
Binary file not shown.
Binary file modified module/gobdk/bdkcgo/libGoBDK_linux_aarch64.a
Binary file not shown.
Binary file modified module/gobdk/bdkcgo/libGoBDK_linux_x86_64.a
Binary file not shown.
Loading
Loading