Skip to content

Commit 65f6e70

Browse files
committed
Merge bitcoin/bitcoin#30510: multiprocess: Add IPC wrapper for Mining interface
1a33281 doc: multiprocess documentation improvements (Ryan Ofsky) d043950 multiprocess: Add serialization code for BlockValidationState (Ryan Ofsky) 33c2eee multiprocess: Add IPC wrapper for Mining interface (Ryan Ofsky) 06882f8 multiprocess: Add serialization code for vector<char> (Russell Yanofsky) 095286f multiprocess: Add serialization code for CTransaction (Russell Yanofsky) 69dfeb1 multiprocess: update common-types.h to use C++20 concepts (Ryan Ofsky) 206c6e7 build: Make bitcoin_ipc_test depend on bitcoin_ipc (Ryan Ofsky) 070e6a3 depends: Update libmultiprocess library for cmake headers target (Ryan Ofsky) Pull request description: Add Cap'n Proto wrapper for the Mining interface introduced in #30200, and its associated types. This PR combined with #30509 will allow a separate mining process, like the one being implemented in Sjors/bitcoin#48, to connect to the node over IPC, and create, manage, and submit block templates. (#30437 shows another simpler demo of a process using the Mining interface.) --- This PR is part of the [process separation project](bitcoin/bitcoin#28722). ACKs for top commit: achow101: ACK 1a33281 TheCharlatan: ACK 1a33281 itornaza: ACK 1a33281 Tree-SHA512: 0791078dd6885dbd81e3d14c75fffff3da8d1277873af379ea6f9633e910c11485bb324e4cde3d936d50d343b16a10b0e8fc1e0fc6d7bdca7f522211da50c01e
2 parents da612ce + 1a33281 commit 65f6e70

17 files changed

+327
-58
lines changed

depends/packages/native_libmultiprocess.mk

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package=native_libmultiprocess
2-
$(package)_version=c1b4ab4eb897d3af09bc9b3cc30e2e6fff87f3e2
2+
$(package)_version=015e95f7ebaa47619a213a19801e7fffafc56864
33
$(package)_download_path=https://github.com/chaincodelabs/libmultiprocess/archive
44
$(package)_file_name=$($(package)_version).tar.gz
5-
$(package)_sha256_hash=6edf5ad239ca9963c78f7878486fb41411efc9927c6073928a7d6edf947cac4a
5+
$(package)_sha256_hash=4b1266b121337f3f6f37e1863fba91c1a5ee9ad126bcffc6fe6b9ca47ad050a1
66
$(package)_dependencies=native_capnp
77

88
define $(package)_config_cmds

doc/design/multiprocess.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ This section describes the major components of the Inter-Process Communication (
8181
- In the generated code, we have C++ client subclasses that inherit from the abstract classes in [`src/interfaces/`](../../src/interfaces/). These subclasses are the workhorses of the IPC mechanism.
8282
- They implement all the methods of the interface, marshalling arguments into a structured format, sending them as requests to the IPC server via a UNIX socket, and handling the responses.
8383
- These subclasses effectively mask the complexity of IPC, presenting a familiar C++ interface to developers.
84-
- Internally, the client subclasses generated by the `mpgen` tool wrap [client classes generated by Cap'n Proto](https://capnproto.org/cxxrpc.html#clients), and use them to send IPC requests.
84+
- Internally, the client subclasses generated by the `mpgen` tool wrap [client classes generated by Cap'n Proto](https://capnproto.org/cxxrpc.html#clients), and use them to send IPC requests. The Cap'n Proto client classes are low-level, with non-blocking methods that use asynchronous I/O and pass request and response objects, while mpgen client subclasses provide normal C++ methods that block while executing and convert between request/response objects and arguments/return values.
8585

8686
### C++ Server Classes in Generated Code
8787
- On the server side, corresponding generated C++ classes receive IPC requests. These server classes are responsible for unmarshalling method arguments, invoking the corresponding methods in the local [`src/interfaces/`](../../src/interfaces/) objects, and creating the IPC response.
@@ -94,7 +94,7 @@ This section describes the major components of the Inter-Process Communication (
9494
- **Asynchronous I/O and Thread Management**: The library is also responsible for managing I/O and threading. Particularly, it ensures that IPC requests never block each other and that new threads on either side of a connection can always make client calls. It also manages worker threads on the server side of calls, ensuring that calls from the same client thread always execute on the same server thread (to avoid locking issues and support nested callbacks).
9595

9696
### Type Hooks in [`src/ipc/capnp/*-types.h`](../../src/ipc/capnp/)
97-
- **Custom Type Conversions**: In [`src/ipc/capnp/*-types.h`](../../src/ipc/capnp/), function overloads of two `libmultiprocess` C++ functions, `mp::CustomReadField` and `mp::CustomBuildFields`, are defined. These overloads are used for customizing the conversion of specific C++ types to and from Cap’n Proto types.
97+
- **Custom Type Conversions**: In [`src/ipc/capnp/*-types.h`](../../src/ipc/capnp/), function overloads of `libmultiprocess` C++ functions, `mp::CustomReadField`, `mp::CustomBuildField`, `mp::CustomReadMessage` and `mp::CustomBuildMessage`, are defined. These overloads are used for customizing the conversion of specific C++ types to and from Cap’n Proto types.
9898
- **Handling Special Cases**: The `mpgen` tool and `libmultiprocess` library can convert most C++ types to and from Cap’n Proto types automatically, including interface types, primitive C++ types, standard C++ types like `std::vector`, `std::set`, `std::map`, `std::tuple`, and `std::function`, as well as simple C++ structs that consist of aforementioned types and whose fields correspond 1:1 with Cap’n Proto struct fields. For other types, `*-types.h` files provide custom code to convert between C++ and Cap’n Proto data representations.
9999

100100
### Protocol-Agnostic IPC Code in [`src/ipc/`](../../src/ipc/)
@@ -197,7 +197,7 @@ sequenceDiagram
197197
- Upon receiving the request, the Cap'n Proto dispatching code in the `bitcoin-node` process calls the `getBlockHash` method of the `Chain` [server class](#c-server-classes-in-generated-code).
198198
- The server class is automatically generated by the `mpgen` tool from the [`chain.capnp`](https://github.com/ryanofsky/bitcoin/blob/pr/ipc/src/ipc/capnp/chain.capnp) file in [`src/ipc/capnp/`](../../src/ipc/capnp/).
199199
- The `getBlockHash` method of the generated `Chain` server subclass in `bitcoin-wallet` receives a Cap’n Proto request object with the `height` parameter, and calls the `getBlockHash` method on its local `Chain` object with the provided `height`.
200-
- When the call returns, it encapsulates the return value in a Cap’n Proto response, which it sends back to the `bitcoin-wallet` process,
200+
- When the call returns, it encapsulates the return value in a Cap’n Proto response, which it sends back to the `bitcoin-wallet` process.
201201

202202
5. **Response and Return**
203203
- The `getBlockHash` method of the generated `Chain` client subclass in `bitcoin-wallet` which sent the request now receives the response.
@@ -232,7 +232,7 @@ This modularization represents an advancement in Bitcoin Core's architecture, of
232232

233233
- **Cap’n Proto struct**: A structured data format used in Cap’n Proto, similar to structs in C++, for organizing and transporting data across different processes.
234234

235-
- **client class (in generated code)**: A C++ class generated from a Cap’n Proto interface which inherits from a Bitcoin core abstract class, and implements each virtual method to send IPC requests to another process. (see also [components section](#c-client-subclasses-in-generated-code))
235+
- **client class (in generated code)**: A C++ class generated from a Cap’n Proto interface which inherits from a Bitcoin Core abstract class, and implements each virtual method to send IPC requests to another process. (see also [components section](#c-client-subclasses-in-generated-code))
236236

237237
- **IPC (inter-process communication)**: Mechanisms that enable processes to exchange requests and data.
238238

doc/multiprocess.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ The multiprocess feature requires [Cap'n Proto](https://capnproto.org/) and [lib
1717
```
1818
cd <BITCOIN_SOURCE_DIRECTORY>
1919
make -C depends NO_QT=1 MULTIPROCESS=1
20-
cmake -B build --toolchain=depends/x86_64-pc-linux-gnu/toolchain.cmake
20+
# Set host platform to output of gcc -dumpmachine or clang -dumpmachine or check the depends/ directory for the generated subdirectory name
21+
HOST_PLATFORM="x86_64-pc-linux-gnu"
22+
cmake -B build --toolchain=depends/$HOST_PLATFORM/toolchain.cmake
2123
cmake --build build
2224
build/src/bitcoin-node -regtest -printtoconsole -debug=ipc
2325
BITCOIND=$(pwd)/build/src/bitcoin-node build/test/functional/test_runner.py

src/CMakeLists.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,22 @@ if(WITH_MULTIPROCESS)
325325
$<TARGET_NAME_IF_EXISTS:bitcoin_wallet>
326326
)
327327
list(APPEND installable_targets bitcoin-node)
328+
329+
if(BUILD_TESTS)
330+
# bitcoin_ipc_test library target is defined here in src/CMakeLists.txt
331+
# instead of src/test/CMakeLists.txt so capnp files in src/test/ are able to
332+
# reference capnp files in src/ipc/capnp/ by relative path. The Cap'n Proto
333+
# compiler only allows importing by relative path when the importing and
334+
# imported files are underneath the same compilation source prefix, so the
335+
# source prefix must be src/, not src/test/
336+
add_library(bitcoin_ipc_test STATIC EXCLUDE_FROM_ALL
337+
test/ipc_test.cpp
338+
)
339+
target_capnp_sources(bitcoin_ipc_test ${PROJECT_SOURCE_DIR}
340+
test/ipc_test.capnp
341+
)
342+
add_dependencies(bitcoin_ipc_test bitcoin_ipc_headers)
343+
endif()
328344
endif()
329345

330346

src/ipc/CMakeLists.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,21 @@
33
# file COPYING or https://opensource.org/license/mit/.
44

55
add_library(bitcoin_ipc STATIC EXCLUDE_FROM_ALL
6+
capnp/mining.cpp
67
capnp/protocol.cpp
78
interfaces.cpp
89
process.cpp
910
)
1011

1112
target_capnp_sources(bitcoin_ipc ${PROJECT_SOURCE_DIR}
12-
capnp/echo.capnp capnp/init.capnp
13+
capnp/common.capnp
14+
capnp/echo.capnp
15+
capnp/init.capnp
16+
capnp/mining.capnp
1317
)
1418

1519
target_link_libraries(bitcoin_ipc
1620
PRIVATE
1721
core_interface
22+
univalue
1823
)

src/ipc/capnp/common-types.h

Lines changed: 98 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
#define BITCOIN_IPC_CAPNP_COMMON_TYPES_H
77

88
#include <clientversion.h>
9+
#include <interfaces/types.h>
10+
#include <primitives/transaction.h>
11+
#include <serialize.h>
912
#include <streams.h>
1013
#include <univalue.h>
1114

@@ -16,76 +19,103 @@
1619

1720
namespace ipc {
1821
namespace capnp {
19-
//! Use SFINAE to define Serializeable<T> trait which is true if type T has a
20-
//! Serialize(stream) method, false otherwise.
21-
template <typename T>
22-
struct Serializable {
23-
private:
24-
template <typename C>
25-
static std::true_type test(decltype(std::declval<C>().Serialize(std::declval<std::nullptr_t&>()))*);
26-
template <typename>
27-
static std::false_type test(...);
28-
29-
public:
30-
static constexpr bool value = decltype(test<T>(nullptr))::value;
31-
};
22+
//! Construct a ParamStream wrapping a data stream with serialization parameters
23+
//! needed to pass transaction objects between bitcoin processes.
24+
//! In the future, more params may be added here to serialize other objects that
25+
//! require serialization parameters. Params should just be chosen to serialize
26+
//! objects completely and ensure that serializing and deserializing objects
27+
//! with the specified parameters produces equivalent objects. It's also
28+
//! harmless to specify serialization parameters here that are not used.
29+
template <typename S>
30+
auto Wrap(S& s)
31+
{
32+
return ParamsStream{s, TX_WITH_WITNESS};
33+
}
3234

33-
//! Use SFINAE to define Unserializeable<T> trait which is true if type T has
34-
//! an Unserialize(stream) method, false otherwise.
35+
//! Detect if type has a deserialize_type constructor, which is
36+
//! used to deserialize types like CTransaction that can't be unserialized into
37+
//! existing objects because they are immutable.
3538
template <typename T>
36-
struct Unserializable {
37-
private:
38-
template <typename C>
39-
static std::true_type test(decltype(std::declval<C>().Unserialize(std::declval<std::nullptr_t&>()))*);
40-
template <typename>
41-
static std::false_type test(...);
42-
43-
public:
44-
static constexpr bool value = decltype(test<T>(nullptr))::value;
45-
};
39+
concept Deserializable = std::is_constructible_v<T, ::deserialize_type, ::DataStream&>;
4640
} // namespace capnp
4741
} // namespace ipc
4842

4943
//! Functions to serialize / deserialize common bitcoin types.
5044
namespace mp {
5145
//! Overload multiprocess library's CustomBuildField hook to allow any
5246
//! serializable object to be stored in a capnproto Data field or passed to a
53-
//! canproto interface. Use Priority<1> so this hook has medium priority, and
47+
//! capnproto interface. Use Priority<1> so this hook has medium priority, and
5448
//! higher priority hooks could take precedence over this one.
5549
template <typename LocalType, typename Value, typename Output>
56-
void CustomBuildField(
57-
TypeList<LocalType>, Priority<1>, InvokeContext& invoke_context, Value&& value, Output&& output,
58-
// Enable if serializeable and if LocalType is not cv or reference
59-
// qualified. If LocalType is cv or reference qualified, it is important to
60-
// fall back to lower-priority Priority<0> implementation of this function
61-
// that strips cv references, to prevent this CustomBuildField overload from
62-
// taking precedence over more narrow overloads for specific LocalTypes.
63-
std::enable_if_t<ipc::capnp::Serializable<LocalType>::value &&
64-
std::is_same_v<LocalType, std::remove_cv_t<std::remove_reference_t<LocalType>>>>* enable = nullptr)
50+
void CustomBuildField(TypeList<LocalType>, Priority<1>, InvokeContext& invoke_context, Value&& value, Output&& output)
51+
// Enable if serializeable and if LocalType is not cv or reference qualified. If
52+
// LocalType is cv or reference qualified, it is important to fall back to
53+
// lower-priority Priority<0> implementation of this function that strips cv
54+
// references, to prevent this CustomBuildField overload from taking precedence
55+
// over more narrow overloads for specific LocalTypes.
56+
requires Serializable<LocalType, DataStream> && std::is_same_v<LocalType, std::remove_cv_t<std::remove_reference_t<LocalType>>>
6557
{
6658
DataStream stream;
67-
value.Serialize(stream);
59+
auto wrapper{ipc::capnp::Wrap(stream)};
60+
value.Serialize(wrapper);
6861
auto result = output.init(stream.size());
6962
memcpy(result.begin(), stream.data(), stream.size());
7063
}
7164

7265
//! Overload multiprocess library's CustomReadField hook to allow any object
7366
//! with an Unserialize method to be read from a capnproto Data field or
74-
//! returned from canproto interface. Use Priority<1> so this hook has medium
67+
//! returned from capnproto interface. Use Priority<1> so this hook has medium
7568
//! priority, and higher priority hooks could take precedence over this one.
7669
template <typename LocalType, typename Input, typename ReadDest>
77-
decltype(auto)
78-
CustomReadField(TypeList<LocalType>, Priority<1>, InvokeContext& invoke_context, Input&& input, ReadDest&& read_dest,
79-
std::enable_if_t<ipc::capnp::Unserializable<LocalType>::value>* enable = nullptr)
70+
decltype(auto) CustomReadField(TypeList<LocalType>, Priority<1>, InvokeContext& invoke_context, Input&& input, ReadDest&& read_dest)
71+
requires Unserializable<LocalType, DataStream> && (!ipc::capnp::Deserializable<LocalType>)
8072
{
8173
return read_dest.update([&](auto& value) {
8274
if (!input.has()) return;
8375
auto data = input.get();
8476
SpanReader stream({data.begin(), data.end()});
85-
value.Unserialize(stream);
77+
auto wrapper{ipc::capnp::Wrap(stream)};
78+
value.Unserialize(wrapper);
8679
});
8780
}
8881

82+
//! Overload multiprocess library's CustomReadField hook to allow any object
83+
//! with a deserialize constructor to be read from a capnproto Data field or
84+
//! returned from capnproto interface. Use Priority<1> so this hook has medium
85+
//! priority, and higher priority hooks could take precedence over this one.
86+
template <typename LocalType, typename Input, typename ReadDest>
87+
decltype(auto) CustomReadField(TypeList<LocalType>, Priority<1>, InvokeContext& invoke_context, Input&& input, ReadDest&& read_dest)
88+
requires ipc::capnp::Deserializable<LocalType>
89+
{
90+
assert(input.has());
91+
auto data = input.get();
92+
SpanReader stream({data.begin(), data.end()});
93+
auto wrapper{ipc::capnp::Wrap(stream)};
94+
return read_dest.construct(::deserialize, wrapper);
95+
}
96+
97+
//! Overload CustomBuildField and CustomReadField to serialize std::chrono
98+
//! parameters and return values as numbers.
99+
template <class Rep, class Period, typename Value, typename Output>
100+
void CustomBuildField(TypeList<std::chrono::duration<Rep, Period>>, Priority<1>, InvokeContext& invoke_context, Value&& value,
101+
Output&& output)
102+
{
103+
static_assert(std::numeric_limits<decltype(output.get())>::lowest() <= std::numeric_limits<Rep>::lowest(),
104+
"capnp type does not have enough range to hold lowest std::chrono::duration value");
105+
static_assert(std::numeric_limits<decltype(output.get())>::max() >= std::numeric_limits<Rep>::max(),
106+
"capnp type does not have enough range to hold highest std::chrono::duration value");
107+
output.set(value.count());
108+
}
109+
110+
template <class Rep, class Period, typename Input, typename ReadDest>
111+
decltype(auto) CustomReadField(TypeList<std::chrono::duration<Rep, Period>>, Priority<1>, InvokeContext& invoke_context,
112+
Input&& input, ReadDest&& read_dest)
113+
{
114+
return read_dest.construct(input.get());
115+
}
116+
117+
//! Overload CustomBuildField and CustomReadField to serialize UniValue
118+
//! parameters and return values as JSON strings.
89119
template <typename Value, typename Output>
90120
void CustomBuildField(TypeList<UniValue>, Priority<1>, InvokeContext& invoke_context, Value&& value, Output&& output)
91121
{
@@ -103,6 +133,33 @@ decltype(auto) CustomReadField(TypeList<UniValue>, Priority<1>, InvokeContext& i
103133
value.read(std::string_view{data.begin(), data.size()});
104134
});
105135
}
136+
137+
//! Generic ::capnp::Data field builder for any C++ type that can be converted
138+
//! to a span of bytes, like std::vector<char> or std::array<uint8_t>, or custom
139+
//! blob types like uint256 or PKHash with data() and size() methods pointing to
140+
//! bytes.
141+
//!
142+
//! Note: it might make sense to move this function into libmultiprocess, since
143+
//! it is fairly generic. However this would require decreasing its priority so
144+
//! it can be overridden, which would require more changes inside
145+
//! libmultiprocess to avoid conflicting with the Priority<1> CustomBuildField
146+
//! function it already provides for std::vector. Also, it might make sense to
147+
//! provide a CustomReadField counterpart to this function, which could be
148+
//! called to read C++ types that can be constructed from spans of bytes from
149+
//! ::capnp::Data fields. But so far there hasn't been a need for this.
150+
template <typename LocalType, typename Value, typename Output>
151+
void CustomBuildField(TypeList<LocalType>, Priority<2>, InvokeContext& invoke_context, Value&& value, Output&& output)
152+
requires
153+
(std::is_same_v<decltype(output.get()), ::capnp::Data::Builder>) &&
154+
(std::convertible_to<Value, std::span<const std::byte>> ||
155+
std::convertible_to<Value, std::span<const char>> ||
156+
std::convertible_to<Value, std::span<const unsigned char>> ||
157+
std::convertible_to<Value, std::span<const signed char>>)
158+
{
159+
auto data = std::span{value};
160+
auto result = output.init(data.size());
161+
memcpy(result.begin(), data.data(), data.size());
162+
}
106163
} // namespace mp
107164

108165
#endif // BITCOIN_IPC_CAPNP_COMMON_TYPES_H

src/ipc/capnp/common.capnp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright (c) 2024 The Bitcoin Core developers
2+
# Distributed under the MIT software license, see the accompanying
3+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
@0xcd2c6232cb484a28;
6+
7+
using Cxx = import "/capnp/c++.capnp";
8+
$Cxx.namespace("ipc::capnp::messages");
9+
10+
using Proxy = import "/mp/proxy.capnp";
11+
$Proxy.includeTypes("ipc/capnp/common-types.h");
12+
13+
struct BlockRef $Proxy.wrap("interfaces::BlockRef") {
14+
hash @0 :Data;
15+
height @1 :Int32;
16+
}

src/ipc/capnp/init-types.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@
66
#define BITCOIN_IPC_CAPNP_INIT_TYPES_H
77

88
#include <ipc/capnp/echo.capnp.proxy-types.h>
9+
#include <ipc/capnp/mining.capnp.proxy-types.h>
910

1011
#endif // BITCOIN_IPC_CAPNP_INIT_TYPES_H

src/ipc/capnp/init.capnp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ $Cxx.namespace("ipc::capnp::messages");
1010
using Proxy = import "/mp/proxy.capnp";
1111
$Proxy.include("interfaces/echo.h");
1212
$Proxy.include("interfaces/init.h");
13+
$Proxy.include("interfaces/mining.h");
1314
$Proxy.includeTypes("ipc/capnp/init-types.h");
1415

1516
using Echo = import "echo.capnp";
17+
using Mining = import "mining.capnp";
1618

1719
interface Init $Proxy.wrap("interfaces::Init") {
1820
construct @0 (threadMap: Proxy.ThreadMap) -> (threadMap :Proxy.ThreadMap);
1921
makeEcho @1 (context :Proxy.Context) -> (result :Echo.Echo);
22+
makeMining @2 (context :Proxy.Context) -> (result :Mining.Mining);
2023
}

src/ipc/capnp/mining-types.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) 2024 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#ifndef BITCOIN_IPC_CAPNP_MINING_TYPES_H
6+
#define BITCOIN_IPC_CAPNP_MINING_TYPES_H
7+
8+
#include <interfaces/mining.h>
9+
#include <ipc/capnp/common.capnp.proxy-types.h>
10+
#include <ipc/capnp/common-types.h>
11+
#include <ipc/capnp/mining.capnp.proxy.h>
12+
#include <node/miner.h>
13+
#include <node/types.h>
14+
#include <validation.h>
15+
16+
namespace mp {
17+
// Custom serialization for BlockValidationState.
18+
void CustomBuildMessage(InvokeContext& invoke_context,
19+
const BlockValidationState& src,
20+
ipc::capnp::messages::BlockValidationState::Builder&& builder);
21+
void CustomReadMessage(InvokeContext& invoke_context,
22+
const ipc::capnp::messages::BlockValidationState::Reader& reader,
23+
BlockValidationState& dest);
24+
} // namespace mp
25+
26+
#endif // BITCOIN_IPC_CAPNP_MINING_TYPES_H

0 commit comments

Comments
 (0)