/tmp/bitcoin/src/ipc/test/ipc_tests.cpp
Line | Count | Source |
1 | | // Copyright (c) 2023-present 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 | | #include <interfaces/init.h> |
6 | | #include <ipc/capnp/mining.capnp.h> |
7 | | #include <ipc/capnp/protocol.h> |
8 | | #include <interfaces/ipc.h> |
9 | | #include <ipc/process.h> |
10 | | #include <ipc/protocol.h> |
11 | | #include <ipc/test/ipc_test.capnp.h> |
12 | | #include <ipc/test/ipc_test.capnp.proxy.h> |
13 | | #include <ipc/test/ipc_test.h> |
14 | | #include <mp/proxy-types.h> |
15 | | #include <ipc/types.h> |
16 | | |
17 | | #include <test/util/common.h> |
18 | | #include <test/util/setup_common.h> |
19 | | #include <tinyformat.h> |
20 | | #include <util/log.h> |
21 | | #include <validation.h> |
22 | | |
23 | | #include <future> |
24 | | #include <optional> |
25 | | #include <thread> |
26 | | #include <kj/common.h> |
27 | | #include <kj/memory.h> |
28 | | #include <kj/test.h> |
29 | | #include <stdexcept> |
30 | | |
31 | | #include <boost/test/unit_test.hpp> |
32 | | |
33 | | static_assert(ipc::capnp::messages::MAX_MONEY == MAX_MONEY); |
34 | | static_assert(ipc::capnp::messages::MAX_DOUBLE == std::numeric_limits<double>::max()); |
35 | | static_assert(ipc::capnp::messages::DEFAULT_BLOCK_RESERVED_WEIGHT == DEFAULT_BLOCK_RESERVED_WEIGHT); |
36 | | static_assert(ipc::capnp::messages::DEFAULT_COINBASE_OUTPUT_MAX_ADDITIONAL_SIGOPS == DEFAULT_COINBASE_OUTPUT_MAX_ADDITIONAL_SIGOPS); |
37 | | |
38 | | //! Remote init class. |
39 | | class TestInit : public interfaces::Init |
40 | | { |
41 | | public: |
42 | 8 | std::unique_ptr<interfaces::Echo> makeEcho() override { return interfaces::MakeEcho(); } |
43 | | }; |
44 | | |
45 | | //! Generate a temporary path with temp_directory_path and mkstemp |
46 | | static std::string TempPath(std::string_view pattern) |
47 | 3 | { |
48 | 3 | std::string temp{fs::PathToString(fs::path{fs::temp_directory_path()} / fs::PathFromString(std::string{pattern}))}; |
49 | 3 | temp.push_back('\0'); |
50 | 3 | int fd{mkstemp(temp.data())}; |
51 | 3 | BOOST_CHECK_GE(fd, 0); |
52 | 3 | BOOST_CHECK_EQUAL(close(fd), 0); |
53 | 3 | temp.resize(temp.size() - 1); |
54 | 3 | fs::remove(fs::PathFromString(temp)); |
55 | 3 | return temp; |
56 | 3 | } |
57 | | |
58 | | //! Unit test that tests execution of IPC calls without actually creating a |
59 | | //! separate process. This test is primarily intended to verify behavior of type |
60 | | //! conversion code that converts C++ objects to Cap'n Proto messages and vice |
61 | | //! versa. |
62 | | //! |
63 | | //! The test creates a thread which creates a FooImplementation object (defined |
64 | | //! in ipc_test.h) and a two-way pipe accepting IPC requests which call methods |
65 | | //! on the object through FooInterface (defined in ipc_test.capnp). |
66 | | void IpcPipeTest() |
67 | 1 | { |
68 | | // Setup: create FooImplementation object and listen for FooInterface requests |
69 | 1 | std::promise<std::unique_ptr<mp::ProxyClient<gen::FooInterface>>> foo_promise; |
70 | 1 | std::thread thread([&]() { |
71 | 67 | mp::EventLoop loop("IpcPipeTest", [](bool raise, const std::string& log) { LogInfo("LOG%i: %s", raise, log); }); |
72 | 1 | auto pipe = loop.m_io_context.provider->newTwoWayPipe(); |
73 | | |
74 | 1 | auto connection_client = std::make_unique<mp::Connection>(loop, kj::mv(pipe.ends[0])); |
75 | 1 | auto foo_client = std::make_unique<mp::ProxyClient<gen::FooInterface>>( |
76 | 1 | connection_client->m_rpc_system->bootstrap(mp::ServerVatId().vat_id).castAs<gen::FooInterface>(), |
77 | 1 | connection_client.get(), /* destroy_connection= */ true); |
78 | 1 | (void)connection_client.release(); |
79 | 1 | foo_promise.set_value(std::move(foo_client)); |
80 | | |
81 | 1 | auto connection_server = std::make_unique<mp::Connection>(loop, kj::mv(pipe.ends[1]), [&](mp::Connection& connection) { |
82 | 1 | auto foo_server = kj::heap<mp::ProxyServer<gen::FooInterface>>(std::make_shared<FooImplementation>(), connection); |
83 | 1 | return capnp::Capability::Client(kj::mv(foo_server)); |
84 | 1 | }); |
85 | 1 | connection_server->onDisconnect([&] { connection_server.reset(); }); |
86 | 1 | loop.loop(); |
87 | 1 | }); |
88 | 1 | std::unique_ptr<mp::ProxyClient<gen::FooInterface>> foo{foo_promise.get_future().get()}; |
89 | | |
90 | | // Test: make sure arguments were sent and return value is received |
91 | 1 | BOOST_CHECK_EQUAL(foo->add(1, 2), 3); |
92 | | |
93 | 1 | COutPoint txout1{Txid::FromUint256(uint256{100}), 200}; |
94 | 1 | COutPoint txout2{foo->passOutPoint(txout1)}; |
95 | 1 | BOOST_CHECK(txout1 == txout2); |
96 | | |
97 | 1 | UniValue uni1{UniValue::VOBJ}; |
98 | 1 | uni1.pushKV("i", 1); |
99 | 1 | uni1.pushKV("s", "two"); |
100 | 1 | UniValue uni2{foo->passUniValue(uni1)}; |
101 | 1 | BOOST_CHECK_EQUAL(uni1.write(), uni2.write()); |
102 | | |
103 | 1 | CMutableTransaction mtx; |
104 | 1 | mtx.version = 2; |
105 | 1 | mtx.nLockTime = 3; |
106 | 1 | mtx.vin.emplace_back(txout1); |
107 | 1 | mtx.vout.emplace_back(COIN, CScript()); |
108 | 1 | CTransactionRef tx1{MakeTransactionRef(mtx)}; |
109 | 1 | CTransactionRef tx2{foo->passTransaction(tx1)}; |
110 | 1 | BOOST_CHECK(Assert(tx1)->Equals(*Assert(tx2))); |
111 | | |
112 | 1 | std::vector<CTransactionRef> txs1; |
113 | 1 | txs1.push_back(tx1); |
114 | 1 | txs1.push_back(nullptr); |
115 | 1 | std::vector<CTransactionRef> txs2(foo->passTransactions(txs1)); |
116 | 1 | BOOST_CHECK_EQUAL(txs2.size(), 2); |
117 | 1 | BOOST_CHECK(Assert(txs1[0])->Equals(*Assert(txs2[0]))); |
118 | 1 | BOOST_CHECK(!txs2[1]); |
119 | | |
120 | 1 | std::vector<char> vec1{'H', 'e', 'l', 'l', 'o'}; |
121 | 1 | std::vector<char> vec2{foo->passVectorChar(vec1)}; |
122 | 1 | BOOST_CHECK_EQUAL(std::string_view(vec1.begin(), vec1.end()), std::string_view(vec2.begin(), vec2.end())); |
123 | | |
124 | 1 | auto script1{CScript() << OP_11}; |
125 | 1 | auto script2{foo->passScript(script1)}; |
126 | 1 | BOOST_CHECK_EQUAL(HexStr(script1), HexStr(script2)); |
127 | | |
128 | | // Test cleanup: disconnect and join thread |
129 | 1 | foo.reset(); |
130 | 1 | thread.join(); |
131 | 1 | } |
132 | | |
133 | | //! Test ipc::Protocol connect() and serve() methods connecting over a socketpair. |
134 | | void IpcSocketPairTest() |
135 | 1 | { |
136 | 1 | std::unique_ptr<interfaces::Init> init{std::make_unique<TestInit>()}; |
137 | 1 | std::unique_ptr<ipc::Protocol> protocol{ipc::capnp::MakeCapnpProtocol("IpcSocketPairTest")}; |
138 | 1 | mp::Stream client_stream; |
139 | 1 | std::promise<void> promise; |
140 | 1 | std::thread thread([&]() { |
141 | 1 | protocol->serve(*init, [&] { |
142 | 1 | auto pair{mp::SocketPair()}; |
143 | 1 | client_stream = protocol->makeStream(pair[0]); |
144 | 1 | promise.set_value(); |
145 | 1 | return protocol->makeStream(pair[1]); |
146 | 1 | }); |
147 | 1 | }); |
148 | 1 | promise.get_future().wait(); |
149 | 1 | std::unique_ptr<interfaces::Init> remote_init{protocol->connect(std::move(client_stream))}; |
150 | 1 | std::unique_ptr<interfaces::Echo> remote_echo{remote_init->makeEcho()}; |
151 | 1 | BOOST_CHECK_EQUAL(remote_echo->echo("echo test"), "echo test"); |
152 | 1 | remote_echo.reset(); |
153 | 1 | remote_init.reset(); |
154 | 1 | thread.join(); |
155 | 1 | } |
156 | | |
157 | | //! Test ipc::Process bind() and connect() methods connecting over a unix socket. |
158 | | void IpcSocketTest(const fs::path& datadir) |
159 | 1 | { |
160 | 1 | std::unique_ptr<interfaces::Init> init{std::make_unique<TestInit>()}; |
161 | 1 | std::unique_ptr<ipc::Protocol> protocol{ipc::capnp::MakeCapnpProtocol("IpcSocketTest")}; |
162 | 1 | std::unique_ptr<ipc::Process> process{ipc::MakeProcess()}; |
163 | | |
164 | 1 | std::string invalid_bind{"invalid:"}; |
165 | 1 | BOOST_CHECK_THROW(process->bind(datadir, "test_bitcoin", invalid_bind), std::invalid_argument); |
166 | 1 | BOOST_CHECK_THROW(process->connect(datadir, "test_bitcoin", invalid_bind), std::invalid_argument); |
167 | | |
168 | 2 | auto bind_and_listen{[&](const std::string& bind_address) { |
169 | 2 | std::string address{bind_address}; |
170 | 2 | mp::SocketId serve_fd = process->bind(datadir, "test_bitcoin", address); |
171 | 2 | BOOST_CHECK_NE(serve_fd, mp::SocketError); |
172 | 2 | BOOST_CHECK_EQUAL(address, bind_address); |
173 | 2 | protocol->listen(serve_fd, *init); |
174 | 2 | }}; |
175 | | |
176 | 5 | auto connect_and_test{[&](const std::string& connect_address) { |
177 | 5 | std::string address{connect_address}; |
178 | 5 | mp::SocketId connect_fd{process->connect(datadir, "test_bitcoin", address)}; |
179 | 5 | BOOST_CHECK_EQUAL(address, connect_address); |
180 | 5 | std::unique_ptr<interfaces::Init> remote_init{protocol->connect(protocol->makeStream(connect_fd))}; |
181 | 5 | std::unique_ptr<interfaces::Echo> remote_echo{remote_init->makeEcho()}; |
182 | 5 | BOOST_CHECK_EQUAL(remote_echo->echo("echo test"), "echo test"); |
183 | 5 | }}; |
184 | | |
185 | | // Need to specify explicit socket addresses outside the data directory, because the data |
186 | | // directory path is so long that the default socket address and any other |
187 | | // addresses in the data directory would fail with errors like: |
188 | | // Address 'unix' path '"/tmp/test_common_Bitcoin Core/ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff/test_bitcoin.sock"' exceeded maximum socket path length |
189 | 1 | std::vector<std::string> addresses{ |
190 | 1 | strprintf("unix:%s", TempPath("bitcoin_sock0_XXXXXX")), |
191 | 1 | strprintf("unix:%s", TempPath("bitcoin_sock1_XXXXXX")), |
192 | 1 | }; |
193 | | |
194 | | // Bind and listen on multiple addresses |
195 | 2 | for (const auto& address : addresses) { |
196 | 2 | bind_and_listen(address); |
197 | 2 | } |
198 | | |
199 | | // Connect and test each address multiple times. |
200 | 5 | for (int i : {0, 1, 0, 0, 1}) { |
201 | 5 | connect_and_test(addresses[i]); |
202 | 5 | } |
203 | 1 | } |
204 | | |
205 | | //! Test ipc::Protocol listen() local connection limiting over a unix socket. |
206 | | void IpcSocketMaxConnectionsTest(const fs::path& datadir) |
207 | 1 | { |
208 | 1 | std::unique_ptr<interfaces::Init> init{std::make_unique<TestInit>()}; |
209 | 1 | std::unique_ptr<ipc::Protocol> protocol{ipc::capnp::MakeCapnpProtocol("IpcSocketMaxConnectionsTest")}; |
210 | 1 | std::unique_ptr<ipc::Process> process{ipc::MakeProcess()}; |
211 | | |
212 | 1 | std::string address{strprintf("unix:%s", TempPath("bitcoin_sock_limit_XXXXXX"))}; |
213 | 1 | mp::SocketId serve_fd = process->bind(datadir, "test_bitcoin", address); |
214 | 1 | BOOST_CHECK_NE(serve_fd, mp::SocketError); |
215 | 1 | protocol->listen(serve_fd, *init, /*max_connections=*/1); |
216 | | |
217 | 1 | auto connect_fd{[&](const std::string& connect_address) { |
218 | 1 | std::string addr{connect_address}; |
219 | 1 | mp::SocketId fd{process->connect(datadir, "test_bitcoin", addr)}; |
220 | 1 | return std::make_pair(fd, addr); |
221 | 1 | }}; |
222 | | |
223 | 1 | auto [fd1, addr1] = connect_fd(address); |
224 | 1 | BOOST_CHECK_EQUAL(addr1, address); |
225 | 1 | std::unique_ptr<interfaces::Init> remote_init1{protocol->connect(protocol->makeStream(fd1))}; |
226 | 1 | std::unique_ptr<interfaces::Echo> remote_echo1{remote_init1->makeEcho()}; |
227 | 1 | BOOST_CHECK_EQUAL(remote_echo1->echo("echo test 1"), "echo test 1"); |
228 | | |
229 | 1 | auto second_call = std::async(std::launch::async, [&] { |
230 | 1 | std::string addr{address}; |
231 | 1 | mp::SocketId fd{process->connect(datadir, "test_bitcoin", addr)}; |
232 | 1 | std::unique_ptr<interfaces::Init> remote_init{protocol->connect(protocol->makeStream(fd))}; |
233 | 1 | std::unique_ptr<interfaces::Echo> remote_echo{remote_init->makeEcho()}; |
234 | 1 | return std::make_pair(remote_echo->echo("echo test 2"), addr); |
235 | 1 | }); |
236 | | |
237 | 1 | BOOST_CHECK(second_call.wait_for(std::chrono::milliseconds{100}) == std::future_status::timeout); |
238 | | |
239 | 1 | remote_echo1.reset(); |
240 | 1 | remote_init1.reset(); |
241 | | |
242 | 1 | BOOST_REQUIRE(second_call.wait_for(std::chrono::seconds{5}) == std::future_status::ready); |
243 | 1 | auto [echo2, addr2] = second_call.get(); |
244 | 1 | BOOST_CHECK_EQUAL(addr2, address); |
245 | 1 | BOOST_CHECK_EQUAL(echo2, "echo test 2"); |
246 | 1 | } |
247 | | |
248 | | BOOST_FIXTURE_TEST_SUITE(ipc_tests, BasicTestingSetup) |
249 | | BOOST_AUTO_TEST_CASE(ipc_tests) |
250 | 1 | { |
251 | 1 | IpcPipeTest(); |
252 | 1 | IpcSocketPairTest(); |
253 | 1 | IpcSocketTest(m_args.GetDataDirNet()); |
254 | 1 | IpcSocketMaxConnectionsTest(m_args.GetDataDirNet()); |
255 | 1 | } |
256 | | |
257 | | // Test address parsing. |
258 | | BOOST_AUTO_TEST_CASE(parse_address_test) |
259 | 1 | { |
260 | 1 | std::unique_ptr<ipc::Process> process{ipc::MakeProcess()}; |
261 | 1 | fs::path datadir{"/var/empty/notexist"}; |
262 | 3 | auto check_notexist{[](const std::system_error& e) { return e.code() == std::errc::no_such_file_or_directory; }}; |
263 | 5 | auto check_address{[&](std::string address, std::string expect_address, std::string expect_error) { |
264 | 5 | if (expect_error.empty()) { |
265 | 3 | BOOST_CHECK_EXCEPTION(process->connect(datadir, "test_bitcoin", address), std::system_error, check_notexist); |
266 | 3 | } else { |
267 | 2 | BOOST_CHECK_EXCEPTION(process->connect(datadir, "test_bitcoin", address), std::invalid_argument, HasReason(expect_error)); |
268 | 2 | } |
269 | 5 | BOOST_CHECK_EQUAL(address, expect_address); |
270 | 5 | }}; |
271 | 1 | std::string prefix{fs::PathToString(datadir / "")}; |
272 | 1 | check_address("unix", "unix:" + prefix + "test_bitcoin.sock", ""); |
273 | 1 | check_address("unix:", "unix:" + prefix + "test_bitcoin.sock", ""); |
274 | 1 | check_address("unix:path.sock", "unix:" + prefix + "path.sock", ""); |
275 | 1 | check_address("unix:0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.sock", |
276 | 1 | "unix:" + prefix + "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.sock", |
277 | 1 | "Unix address path \"" + prefix + "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.sock\" exceeded maximum socket path length"); |
278 | 1 | check_address("invalid", "invalid", "Unrecognized address 'invalid'"); |
279 | | |
280 | 21 | auto check_listen_address{[](const std::string& configured_address, const std::string& expected_address, size_t expected_max_connections, const std::string& expected_error = "") { |
281 | 21 | auto listen_address{interfaces::Ipc::parseListenAddress(configured_address)}; |
282 | 21 | if (!expected_error.empty()) { |
283 | 12 | BOOST_CHECK(!listen_address); |
284 | 12 | BOOST_CHECK_EQUAL(util::ErrorString(listen_address).original, expected_error); |
285 | 12 | return; |
286 | 12 | } |
287 | 21 | BOOST_REQUIRE(listen_address); |
288 | 9 | BOOST_CHECK_EQUAL(listen_address->address, expected_address); |
289 | 9 | BOOST_CHECK_EQUAL(listen_address->max_connections, expected_max_connections); |
290 | 9 | }}; |
291 | 1 | check_listen_address("unix", "unix", ipc::DEFAULT_MAX_CONNECTIONS); |
292 | 1 | check_listen_address("unix:", "unix:", ipc::DEFAULT_MAX_CONNECTIONS); |
293 | 1 | check_listen_address("unix:,max-connections=8", "unix:", 8); |
294 | 1 | check_listen_address("unix:path.sock,max-connections=8", "unix:path.sock", 8); |
295 | 1 | check_listen_address("unix:,max-connections=1", "unix:", 1); |
296 | 1 | check_listen_address("unix:,max-connections=256", "unix:", ipc::MAX_CONNECTIONS); |
297 | 1 | check_listen_address("unix:path.sock,max-connections=8,unknown=1", "", 0, "Unknown socket option 'unknown'"); |
298 | 1 | check_listen_address("unix:,max-connections=8,", "", 0, "Empty socket option"); |
299 | 1 | check_listen_address("unix:,,max-connections=8", "", 0, "Empty socket option"); |
300 | 1 | check_listen_address("unix:,max-connections=8,,max-connections=8", "", 0, "Empty socket option"); |
301 | 1 | check_listen_address("unix:,max-connections=0", "", 0, "max-connections must be at least 1"); |
302 | 1 | check_listen_address("unix:,max-connections=-1", "", 0, "max-connections must be at least 1"); |
303 | 1 | check_listen_address("unix:,max-connections=-9223372036854775808", "", 0, "max-connections must be at least 1"); |
304 | 1 | check_listen_address("unix:,max-connections=257", "", 0, "max-connections must be at most 256"); |
305 | 1 | check_listen_address("unix:,max-connections=9223372036854775808", "", 0, "Invalid max-connections value '9223372036854775808'"); |
306 | 1 | check_listen_address("unix:,max-connections=", "", 0, "Missing value for max-connections option"); |
307 | 1 | check_listen_address("unix:path,backup.sock=1", "", 0, "Unknown socket option 'backup.sock'"); |
308 | 1 | check_listen_address("unix:path%2Cbackup.sock=1", "unix:path,backup.sock=1", ipc::DEFAULT_MAX_CONNECTIONS); |
309 | 1 | check_listen_address("unix:path%2Cbackup.sock=1,max-connections=8", "unix:path,backup.sock=1", 8); |
310 | 1 | check_listen_address("unix:,max-connections", "", 0, "Missing value for max-connections option"); |
311 | 1 | check_listen_address("unix:,max-connections=2,max-connections=1", "unix:", 1); |
312 | 1 | } |
313 | | |
314 | | // Test command line parsing in ipc::Process::checkSpawned(). |
315 | | BOOST_AUTO_TEST_CASE(check_spawned_test) |
316 | 1 | { |
317 | 1 | std::unique_ptr<ipc::Process> process{ipc::MakeProcess()}; |
318 | 1 | char arg0[]{"bitcoin-node"}; |
319 | 1 | char arg_spawn[]{"-ipcchild"}; |
320 | 1 | char arg_invalid[]{"invalid"}; |
321 | 1 | char arg_other[]{"-ipcbind=unix"}; |
322 | | |
323 | | // no -ipcchild arg. |
324 | 1 | { |
325 | 1 | char* argv[]{arg0}; |
326 | 1 | BOOST_CHECK(!process->checkSpawned(1, argv)); |
327 | 1 | } |
328 | 1 | { |
329 | 1 | char* argv[]{arg0, arg_other}; |
330 | 1 | BOOST_CHECK(!process->checkSpawned(2, argv)); |
331 | 1 | } |
332 | 1 | { |
333 | 1 | char* argv[]{arg0, arg_other, arg_invalid}; |
334 | 1 | BOOST_CHECK(!process->checkSpawned(3, argv)); |
335 | 1 | } |
336 | | // -ipcchild without a value. |
337 | 1 | { |
338 | 1 | char* argv[]{arg0, arg_spawn}; |
339 | 1 | BOOST_CHECK(!process->checkSpawned(2, argv)); |
340 | 1 | } |
341 | | // -ipcchild combined with other arguments. |
342 | 1 | { |
343 | 1 | char* argv[]{arg0, arg_spawn, arg_invalid, arg_other}; |
344 | 1 | BOOST_CHECK(!process->checkSpawned(4, argv)); |
345 | 1 | } |
346 | | // -ipcchild with a value that is not a valid way of connecting to the |
347 | | // parent process. |
348 | 1 | { |
349 | 1 | char* argv[]{arg0, arg_spawn, arg_invalid}; |
350 | | BOOST_CHECK_THROW(process->checkSpawned(3, argv), std::runtime_error); |
351 | 1 | } |
352 | 1 | } |
353 | | |
354 | | BOOST_AUTO_TEST_SUITE_END() |