Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
42 changes: 42 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# VCS and hosted-CI metadata are not needed in the build context.
.git
.github
.circleci
.travis
.git-blame-ignore-revs

# Local build/install outputs.
release
debug
xcode
install
build
bin
lib
result
result-*

# Nix/local Python helper outputs.
**/.nix-install
**/.nix-pip
LOCAL_PIP

# Python/package caches and artifacts.
**/__pycache__
**/.pytest_cache
**/*.pyc
**/*.egg-info
**/dist
.benchmarks

# Compiled artifacts from local builds.
**/*.so
**/*.dylib

# Editor, OS, and runtime noise.
.idea
*.DS_Store
*.log
test-chain
docs/_build
docs/_output
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: CI

on:
pull_request:
push:
branches:
- main
- master
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
nix-btc:
name: Nix BTC checks
runs-on: ubuntu-24.04
timeout-minutes: 120

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install Nix
uses: cachix/install-nix-action@v31
with:
github_access_token: ${{ secrets.GITHUB_TOKEN }}
extra_nix_config: |
experimental-features = nix-command flakes
accept-flake-config = true

- name: Run BTC flake checks
run: nix flake check -L --cores 1 --max-jobs 1
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
release
install
xcode
debug
build
bin/
lib/
.mtime_cache
*.DS_Store
*.log
Notebooks/blocksci
*.so
*.dylib
*.pyc
__pycache__/
include/blocksci/external
include/blocksci/blocksci_export.h
python/blocksci/pybind11
*.egg-info
blockscipy/dist/*
.pytest_cache
.idea
.benchmarks
result
result-*
.nix-install
.nix-pip
LOCAL_PIP/
test-chain/
/.direnv
/.envrc
3 changes: 0 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
[submodule "Notebooks/blocksci/Blockchain-Known-Pools"]
path = blockscipy/blocksci/Blockchain-Known-Pools
url = https://github.com/blockchain/Blockchain-Known-Pools
[submodule "test/testchain-generator"]
path = test/testchain-generator
url = https://github.com/citp/testchain-generator
6 changes: 5 additions & 1 deletion blockscipy/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ file(GLOB PYTHON_INTERFACE_GENERIC_PROXY_SOURCES "src/generic_proxy/*.cpp")
file(GLOB PYTHON_INTERFACE_HEADERS "src/*.hpp")
file(GLOB PYTHON_INTERFACE_SOURCES "src/*.cpp")

pybind11_add_module(_blocksci ${PYTHON_INTERFACE_PROXY_HEADERS} ${PYTHON_INTERFACE_PROXY_SOURCES} ${PYTHON_CHAIN_SOURCES} ${PYTHON_CLUSTER_HEADERS} ${PYTHON_CLUSTER_SOURCES} ${PYTHON_CHAIN_HEADERS} ${PYTHON_SCRIPTS_HEADERS} ${PYTHON_SCRIPTS_SOURCES} ${PYTHON_INTERFACE_SOURCES} ${PYTHON_INTERFACE_HEADERS} ${PYTHON_INTERFACE_SIMPLE_HEADERS} ${PYTHON_INTERFACE_SIMPLE_SOURCES} ${PYTHON_INTERFACE_GENERIC_PROXY_HEADERS} ${PYTHON_INTERFACE_GENERIC_PROXY_SOURCES})
pybind11_add_module(_blocksci NO_EXTRAS ${PYTHON_INTERFACE_PROXY_HEADERS} ${PYTHON_INTERFACE_PROXY_SOURCES} ${PYTHON_CHAIN_SOURCES} ${PYTHON_CLUSTER_HEADERS} ${PYTHON_CLUSTER_SOURCES} ${PYTHON_CHAIN_HEADERS} ${PYTHON_SCRIPTS_HEADERS} ${PYTHON_SCRIPTS_SOURCES} ${PYTHON_INTERFACE_SOURCES} ${PYTHON_INTERFACE_HEADERS} ${PYTHON_INTERFACE_SIMPLE_HEADERS} ${PYTHON_INTERFACE_SIMPLE_SOURCES} ${PYTHON_INTERFACE_GENERIC_PROXY_HEADERS} ${PYTHON_INTERFACE_GENERIC_PROXY_SOURCES})

set_target_properties(_blocksci PROPERTIES CXX_VISIBILITY_PRESET hidden)
set_target_properties(_blocksci PROPERTIES VISIBILITY_INLINES_HIDDEN 1)
Expand All @@ -39,6 +39,10 @@ add_subdirectory(external)

target_compile_options(_blocksci PRIVATE -Wall -Wextra -Wpedantic)

if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
target_compile_options(_blocksci PRIVATE -O1)
endif()

target_link_libraries(
_blocksci

Expand Down
1 change: 0 additions & 1 deletion blockscipy/blocksci/Blockchain-Known-Pools
Submodule Blockchain-Known-Pools deleted from 29ab27
92 changes: 78 additions & 14 deletions blockscipy/blocksci/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@
from functools import reduce

import dateparser
import multiprocess as mp
import pandas as pd
import psutil
from dateutil.relativedelta import relativedelta
from multiprocess import Pool

from ._blocksci import *
from ._blocksci import _traverse
Expand All @@ -39,6 +39,48 @@
sys.modules["blocksci.heuristics"] = heuristics
sys.modules["blocksci.heuristics.change"] = heuristics.change

# pybind11 3 gives enums Python-native ``Enum`` formatting by default
# (``address_type.pubkeyhash`` / ``<address_type.pubkeyhash: 2>``).
# BlockSci's public API and regression fixtures historically exposed a
# human-readable ``str()`` and a compact qualified-name ``repr()``.
_ADDRESS_TYPE_LABELS = {
address_type.pubkey: "Pay to pubkey",
address_type.pubkeyhash: "Pay to pubkey hash",
address_type.scripthash: "Pay to script hash",
address_type.multisig: "Multisig",
address_type.multisig_pubkey: "Multisig Public Key",
address_type.nonstandard: "Nonstandard",
address_type.nulldata: "Null data",
address_type.witness_pubkeyhash: "Pay to witness pubkey hash",
address_type.witness_scripthash: "Pay to witness script hash",
address_type.witness_unknown: "Pay to witness unknown",
}

_ADDRESS_TYPE_REPR_NAMES = {
address_type.pubkey: "pubkey",
address_type.pubkeyhash: "pubkeyhash",
address_type.scripthash: "scripthash",
address_type.multisig: "multisig",
address_type.multisig_pubkey: "multisig_pubkey",
address_type.nonstandard: "nonstandard",
address_type.nulldata: "nulldata",
address_type.witness_pubkeyhash: "witness_pubkeyhash",
address_type.witness_scripthash: "witness_scripthash",
address_type.witness_unknown: "witness_unknown",
}


def _address_type_str(value):
return _ADDRESS_TYPE_LABELS.get(value, "Unknown Address Type")


def _address_type_repr(value):
return f"address_type.{_ADDRESS_TYPE_REPR_NAMES.get(value, 'unknown')}"


address_type.__str__ = _address_type_str
address_type.__repr__ = _address_type_repr


class _NoDefault:
def __repr__(self):
Expand All @@ -48,6 +90,15 @@ def __repr__(self):
MISSING_PARAM = _NoDefault()


def _multiprocess_context():
# Python 3.12+ warns when forking a multithreaded process. Pytest and
# native extension imports can start helper threads, so prefer spawn on
# Darwin where fork-without-exec is particularly fragile.
if sys.platform == "darwin" and sys.version_info >= (3, 12):
return mp.get_context("spawn")
return mp.get_context()


# Alert the user if the disk space is getting full
disk_info = os.statvfs("/")
free_space = (disk_info.f_frsize * disk_info.f_bavail) // (1024**3)
Expand Down Expand Up @@ -96,7 +147,7 @@ def real_map_func(input):
file.seek(0)
return file

with Pool(cpu_count - 1) as p:
with _multiprocess_context().Pool(cpu_count - 1) as p:
results_future = p.map_async(real_map_func, segments[1:])
first = map_func(chain[raw_segments[0][0] : raw_segments[0][1]])
results = results_future.get()
Expand Down Expand Up @@ -865,6 +916,21 @@ class DummyClass:
loaderDirectory = os.path.dirname(os.path.abspath(inspect.getsourcefile(DummyClass)))


def _load_pool_data():
import json

pools_path = os.environ.get(
"BLOCKSCI_POOLS_JSON",
os.path.join(loaderDirectory, "Blockchain-Known-Pools", "pools.json"),
)
try:
with open(pools_path) as f:
return json.load(f)
except FileNotFoundError:
logging.warning("BlockSci miner labels disabled: pools.json not found at %s", pools_path)
return {"payout_addresses": {}, "coinbase_tags": {}}


def get_miner(block) -> str:
"""
Get the miner of the block based on the text in the coinbase transaction
Expand All @@ -874,20 +940,18 @@ def get_miner(block) -> str:
global pool_data
global coinbase_tag_re
if first_miner_run:
import json

with open(loaderDirectory + "/Blockchain-Known-Pools/pools.json") as f:
pool_data = json.load(f)
addresses = [block._access.address_from_string(addr_string) for addr_string in pool_data["payout_addresses"]]
tagged_addresses = {
pointer: pool_data["payout_addresses"][address]
for address in addresses
if address in pool_data["payout_addresses"]
}
coinbase_tag_re = re.compile("|".join(map(re.escape, pool_data["coinbase_tags"])))
pool_data = _load_pool_data()
tagged_addresses = {}
for addr_string, pool_info in pool_data.get("payout_addresses", {}).items():
try:
tagged_addresses[block._access.address_from_string(addr_string)] = pool_info
except Exception:
logging.debug("Skipping invalid miner payout address %s", addr_string, exc_info=True)
coinbase_tags = pool_data.get("coinbase_tags", {})
coinbase_tag_re = re.compile("|".join(map(re.escape, coinbase_tags))) if coinbase_tags else None
first_miner_run = False
coinbase = block.coinbase_param.decode("utf_8", "replace")
tag_matches = re.findall(coinbase_tag_re, coinbase)
tag_matches = re.findall(coinbase_tag_re, coinbase) if coinbase_tag_re is not None else []
if tag_matches:
return pool_data["coinbase_tags"][tag_matches[0]]["name"]
for txout in block.coinbase_tx.outs:
Expand Down
2 changes: 1 addition & 1 deletion blockscipy/src/blocksci_range_type.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ struct BlocksciRangeTypeObjectCaster {

struct BlockSciRangeToIterator {
template <typename T> BlocksciIteratorType operator()(T &&t) {
return BlocksciIteratorType{RawIterator<ranges::range_value_type_t<T>>{std::forward<T>(t)}};
return BlocksciIteratorType{RawIterator<ranges::range_value_t<T>>{std::forward<T>(t)}};
}
};

Expand Down
10 changes: 5 additions & 5 deletions blockscipy/src/blocksci_type_converter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@

#include <range/v3/view/any_view.hpp>

blocksci::AnyScript BlockSciTypeConverter::operator()(const blocksci::Address &address) {
blocksci::AnyScript BlockSciTypeConverter::operator()(const blocksci::Address &address) const {
return address.getScript();
}

RawRange<blocksci::Input> BlockSciTypeConverter::operator()(const blocksci::InputRange &val) {
RawRange<blocksci::Input> BlockSciTypeConverter::operator()(const blocksci::InputRange &val) const {
return ranges::any_view<blocksci::Input, random_access_sized>{val};
}

RawRange<blocksci::Output> BlockSciTypeConverter::operator()(const blocksci::OutputRange &val) {
RawRange<blocksci::Output> BlockSciTypeConverter::operator()(const blocksci::OutputRange &val) const {
return ranges::any_view<blocksci::Output, random_access_sized>{val};
}

RawRange<blocksci::Block> BlockSciTypeConverter::operator()(const blocksci::BlockRange &val) {
RawRange<blocksci::Block> BlockSciTypeConverter::operator()(const blocksci::BlockRange &val) const {
return ranges::any_view<blocksci::Block, random_access_sized>{val};
}
}
26 changes: 13 additions & 13 deletions blockscipy/src/blocksci_type_converter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,51 +22,51 @@

struct BlockSciTypeConverter {

template <typename T> T operator()(const T &t) {
template <typename T> T operator()(const T &t) const {
return t;
}

blocksci::AnyScript operator()(const blocksci::Address &address);
blocksci::AnyScript operator()(const blocksci::Address &address) const;

int64_t operator()(uint16_t val) {
int64_t operator()(uint16_t val) const {
return static_cast<int64_t>(val);
}

int64_t operator()(int16_t val) {
int64_t operator()(int16_t val) const {
return static_cast<int64_t>(val);
}

int64_t operator()(uint32_t val) {
int64_t operator()(uint32_t val) const {
return static_cast<int64_t>(val);
}

int64_t operator()(int32_t val) {
int64_t operator()(int32_t val) const {
return static_cast<int64_t>(val);
}

// Potential overflow
int64_t operator()(uint64_t val) {
int64_t operator()(uint64_t val) const {
return static_cast<int64_t>(val);
}

RawRange<blocksci::Input> operator()(const blocksci::InputRange &val);
RawRange<blocksci::Output> operator()(const blocksci::OutputRange &val);
RawRange<blocksci::Block> operator()(const blocksci::BlockRange &val);
RawRange<blocksci::Input> operator()(const blocksci::InputRange &val) const;
RawRange<blocksci::Output> operator()(const blocksci::OutputRange &val) const;
RawRange<blocksci::Block> operator()(const blocksci::BlockRange &val) const;

template <typename T>
auto operator()(const ranges::optional<T> &val) -> ranges::optional<decltype(this->operator()(*val))> {
auto operator()(const ranges::optional<T> &val) const -> ranges::optional<decltype(this->operator()(*val))> {
if (val) {
return this->operator()(*val);
} else {
return ranges::nullopt;
}
}

template <typename T> auto operator()(Iterator<T> &val) -> Iterator<decltype(this->operator()(*val))> {
template <typename T> auto operator()(Iterator<T> &val) const -> Iterator<decltype(this->operator()(*val))> {
return val | ranges::views::transform(this);
}

template <typename T> auto operator()(Range<T> &val) -> Range<decltype(this->operator()(*val))> {
template <typename T> auto operator()(Range<T> &val) const -> Range<decltype(this->operator()(*val))> {
return val | ranges::views::transform(this);
}
};
Expand Down
4 changes: 2 additions & 2 deletions blockscipy/src/proxy/range.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ template <typename T, typename BaseSimple> void setupRangesProxy(AllProxyClasses
if (!slice.compute(chainSize, &start, &stop, &step, &slicelength))
throw pybind11::error_already_set();

auto subset = range[{static_cast<ranges::range_size_type_t<RawRange<T>>>(start),
static_cast<ranges::range_size_type_t<RawRange<T>>>(stop)}];
auto subset = range[{static_cast<ranges::range_size_t<RawRange<T>>>(start),
static_cast<ranges::range_size_t<RawRange<T>>>(stop)}];
return subset | ranges::views::stride(step);
});
},
Expand Down
Loading
Loading