Branch data Line data Source code
1 : : // Copyright (c) 2019-2022 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 <chainparams.h>
6 : : #include <hash.h>
7 : : #include <net.h>
8 : : #include <netmessagemaker.h>
9 : : #include <protocol.h>
10 : : #include <test/fuzz/FuzzedDataProvider.h>
11 : : #include <test/fuzz/fuzz.h>
12 : : #include <test/fuzz/util.h>
13 : : #include <test/util/xoroshiro128plusplus.h>
14 : : #include <util/chaintype.h>
15 : :
16 : : #include <cassert>
17 [ + - ]: 173 : #include <cstdint>
18 [ + - ]: 173 : #include <limits>
19 : : #include <optional>
20 : : #include <vector>
21 : :
22 : : namespace {
23 : :
24 : 173 : std::vector<std::string> g_all_messages;
25 : :
26 : 4 : void initialize_p2p_transport_serialization()
27 : : {
28 : 4 : ECC_Start();
29 : 4 : SelectParams(ChainType::REGTEST);
30 : 4 : g_all_messages = getAllNetMessageTypes();
31 : 4 : std::sort(g_all_messages.begin(), g_all_messages.end());
32 : 4 : }
33 : :
34 : : } // namespace
35 : :
36 [ + - - + ]: 483 : FUZZ_TARGET(p2p_transport_serialization, .init = initialize_p2p_transport_serialization)
37 : : {
38 : : // Construct transports for both sides, with dummy NodeIds.
39 : 137 : V1Transport recv_transport{NodeId{0}, SER_NETWORK, INIT_PROTO_VERSION};
40 : 137 : V1Transport send_transport{NodeId{1}, SER_NETWORK, INIT_PROTO_VERSION};
41 : :
42 [ + - ]: 137 : FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
43 : :
44 [ + - ]: 137 : auto checksum_assist = fuzzed_data_provider.ConsumeBool();
45 [ + - ]: 137 : auto magic_bytes_assist = fuzzed_data_provider.ConsumeBool();
46 : 137 : std::vector<uint8_t> mutable_msg_bytes;
47 : :
48 : 137 : auto header_bytes_remaining = CMessageHeader::HEADER_SIZE;
49 [ + + ]: 137 : if (magic_bytes_assist) {
50 [ + - + - ]: 124 : auto msg_start = Params().MessageStart();
51 [ + + ]: 620 : for (size_t i = 0; i < CMessageHeader::MESSAGE_SIZE_SIZE; ++i) {
52 [ + - ]: 496 : mutable_msg_bytes.push_back(msg_start[i]);
53 : 496 : }
54 : 124 : header_bytes_remaining -= CMessageHeader::MESSAGE_SIZE_SIZE;
55 : 124 : }
56 : :
57 [ + + ]: 137 : if (checksum_assist) {
58 : 96 : header_bytes_remaining -= CMessageHeader::CHECKSUM_SIZE;
59 : 96 : }
60 : :
61 [ + - ]: 137 : auto header_random_bytes = fuzzed_data_provider.ConsumeBytes<uint8_t>(header_bytes_remaining);
62 [ + - ]: 137 : mutable_msg_bytes.insert(mutable_msg_bytes.end(), header_random_bytes.begin(), header_random_bytes.end());
63 [ + - ]: 137 : auto payload_bytes = fuzzed_data_provider.ConsumeRemainingBytes<uint8_t>();
64 : :
65 [ + + + + ]: 137 : if (checksum_assist && mutable_msg_bytes.size() == CMessageHeader::CHECKSUM_OFFSET) {
66 [ + - ]: 95 : CHash256 hasher;
67 : : unsigned char hsh[32];
68 [ + - + - ]: 95 : hasher.Write(payload_bytes);
69 [ + - ]: 95 : hasher.Finalize(hsh);
70 [ + + ]: 475 : for (size_t i = 0; i < CMessageHeader::CHECKSUM_SIZE; ++i) {
71 [ + - ]: 380 : mutable_msg_bytes.push_back(hsh[i]);
72 : 380 : }
73 : 95 : }
74 : 173 :
75 [ + - ]: 137 : mutable_msg_bytes.insert(mutable_msg_bytes.end(), payload_bytes.begin(), payload_bytes.end());
76 [ + - ]: 137 : Span<const uint8_t> msg_bytes{mutable_msg_bytes};
77 [ + + ]: 44000 : while (msg_bytes.size() > 0) {
78 [ + - + + ]: 43896 : if (!recv_transport.ReceivedBytes(msg_bytes)) {
79 : 33 : break;
80 : : }
81 [ + - + + ]: 43863 : if (recv_transport.ReceivedMessageComplete()) {
82 : 25923 : const std::chrono::microseconds m_time{std::numeric_limits<int64_t>::max()};
83 [ + - ]: 26096 : bool reject_message{false};
84 [ + - ]: 25923 : CNetMessage msg = recv_transport.GetReceivedMessage(m_time, reject_message);
85 [ + - ]: 25923 : assert(msg.m_type.size() <= CMessageHeader::COMMAND_SIZE);
86 [ + - ]: 25923 : assert(msg.m_raw_message_size <= mutable_msg_bytes.size());
87 [ + - ]: 25923 : assert(msg.m_raw_message_size == CMessageHeader::HEADER_SIZE + msg.m_message_size);
88 [ + - + - ]: 25923 : assert(msg.m_time == m_time);
89 : :
90 : 25923 : std::vector<unsigned char> header;
91 [ + - + - : 25923 : auto msg2 = CNetMsgMaker{msg.m_recv.GetVersion()}.Make(msg.m_type, Span{msg.m_recv});
+ - + - +
- ]
92 : 25923 : bool queued = send_transport.SetMessageToSend(msg2);
93 [ + - ]: 25923 : assert(queued);
94 : 25923 : std::optional<bool> known_more;
95 : 69704 : while (true) {
96 : 113485 : const auto& [to_send, more, _msg_type] = send_transport.GetBytesToSend(false);
97 [ + + + - : 69704 : if (known_more) assert(!to_send.empty() == *known_more);
+ - ]
98 [ + + ]: 69704 : if (to_send.empty()) break;
99 : 43781 : send_transport.MarkBytesSent(to_send.size());
100 [ + - ]: 43781 : known_more = more;
101 : : }
102 : 25923 : }
103 : : }
104 : 137 : }
105 : :
106 : : namespace {
107 : :
108 : : template<typename R>
109 : 697 : void SimulationTest(Transport& initiator, Transport& responder, R& rng, FuzzedDataProvider& provider)
110 : : {
111 : : // Simulation test with two Transport objects, which send messages to each other, with
112 : : // sending and receiving fragmented into multiple pieces that may be interleaved. It primarily
113 : : // verifies that the sending and receiving side are compatible with each other, plus a few
114 : : // sanity checks. It does not attempt to introduce errors in the communicated data.
115 : :
116 : : // Put the transports in an array for by-index access.
117 : 697 : const std::array<Transport*, 2> transports = {&initiator, &responder};
118 : :
119 : : // Two vectors representing in-flight bytes. inflight[i] is from transport[i] to transport[!i].
120 : 697 : std::array<std::vector<uint8_t>, 2> in_flight;
121 : :
122 : : // Two queues with expected messages. expected[i] is expected to arrive in transport[!i].
123 [ + - ]: 697 : std::array<std::deque<CSerializedNetMsg>, 2> expected;
124 : :
125 : : // Vectors with bytes last returned by GetBytesToSend() on transport[i].
126 : 697 : std::array<std::vector<uint8_t>, 2> to_send;
127 : :
128 : : // Last returned 'more' values (if still relevant) by transport[i]->GetBytesToSend(), for
129 : : // both have_next_message false and true.
130 : 697 : std::array<std::optional<bool>, 2> last_more, last_more_next;
131 : :
132 : : // Whether more bytes to be sent are expected on transport[i], before and after
133 : : // SetMessageToSend().
134 : 697 : std::array<std::optional<bool>, 2> expect_more, expect_more_next;
135 : :
136 : : // Function to consume a message type.
137 : 7771 : auto msg_type_fn = [&]() {
138 : 7074 : uint8_t v = provider.ConsumeIntegral<uint8_t>();
139 [ + + ]: 7074 : if (v == 0xFF) {
140 : : // If v is 0xFF, construct a valid (but possibly unknown) message type from the fuzz
141 : : // data.
142 : 984 : std::string ret;
143 [ + + ]: 4245 : while (ret.size() < CMessageHeader::COMMAND_SIZE) {
144 [ + - ]: 4155 : char c = provider.ConsumeIntegral<char>();
145 : : // Match the allowed characters in CMessageHeader::IsCommandValid(). Any other
146 : : // character is interpreted as end.
147 [ + + + + ]: 4155 : if (c < ' ' || c > 0x7E) break;
148 [ + - ]: 3261 : ret += c;
149 : : }
150 : 984 : return ret;
151 : 984 : } else {
152 : : // Otherwise, use it as index into the list of known messages.
153 : 6090 : return g_all_messages[v % g_all_messages.size()];
154 : : }
155 : 7074 : };
156 : :
157 : : // Function to construct a CSerializedNetMsg to send.
158 : 9165 : auto make_msg_fn = [&](bool first) {
159 : 8468 : CSerializedNetMsg msg;
160 [ + + ]: 8468 : if (first) {
161 : : // Always send a "version" message as first one.
162 [ + - ]: 1394 : msg.m_type = "version";
163 : 1394 : } else {
164 [ + - ]: 7074 : msg.m_type = msg_type_fn();
165 : : }
166 : : // Determine size of message to send (limited to 75 kB for performance reasons).
167 [ + - ]: 8468 : size_t size = provider.ConsumeIntegralInRange<uint32_t>(0, 75000);
168 : : // Get payload of message from RNG.
169 [ + - ]: 8468 : msg.data.resize(size);
170 [ + + ]: 92772241 : for (auto& v : msg.data) v = uint8_t(rng());
171 : : // Return.
172 : 8468 : return msg;
173 : 8468 : };
174 : :
175 : : // The next message to be sent (initially version messages, but will be replaced once sent).
176 : 697 : std::array<CSerializedNetMsg, 2> next_msg = {
177 [ + - ]: 697 : make_msg_fn(/*first=*/true),
178 [ + - ]: 697 : make_msg_fn(/*first=*/true)
179 : : };
180 : :
181 : : // Wrapper around transport[i]->GetBytesToSend() that performs sanity checks.
182 : 138478 : auto bytes_to_send_fn = [&](int side) -> Transport::BytesToSend {
183 : : // Invoke GetBytesToSend twice (for have_next_message = {false, true}). This function does
184 : : // not modify state (it's const), and only the "more" return value should differ between
185 : : // the calls.
186 : 1320314 : const auto& [bytes, more_nonext, msg_type] = transports[side]->GetBytesToSend(false);
187 : 493628 : const auto& [bytes_next, more_next, msg_type_next] = transports[side]->GetBytesToSend(true);
188 : : // Compare with expected more.
189 [ + + + - ]: 137781 : if (expect_more[side].has_value()) assert(!bytes.empty() == *expect_more[side]);
190 : : // Verify consistency between the two results.
191 [ + - ]: 137781 : assert(bytes == bytes_next);
192 [ + - ]: 137781 : assert(msg_type == msg_type_next);
193 [ + + - + ]: 137781 : if (more_nonext) assert(more_next);
194 : : // Compare with previously reported output.
195 [ + - + - ]: 275562 : assert(to_send[side].size() <= bytes.size());
196 [ + - + - ]: 275562 : assert(to_send[side] == Span{bytes}.first(to_send[side].size()));
197 : 275562 : to_send[side].resize(bytes.size());
198 : 275562 : std::copy(bytes.begin(), bytes.end(), to_send[side].begin());
199 : : // Remember 'more' results.
200 : 137781 : last_more[side] = {more_nonext};
201 : 137781 : last_more_next[side] = {more_next};
202 : : // Return.
203 : 137781 : return {bytes, more_nonext, msg_type};
204 : : };
205 : :
206 : : // Function to make side send a new message.
207 : 40184 : auto new_msg_fn = [&](int side) {
208 : : // Don't do anything if there are too many unreceived messages already.
209 [ + + ]: 39487 : if (expected[side].size() >= 16) return;
210 : : // Try to send (a copy of) the message in next_msg[side].
211 : 39280 : CSerializedNetMsg msg = next_msg[side].Copy();
212 : 39280 : bool queued = transports[side]->SetMessageToSend(msg);
213 : : // Update expected more data.
214 : 39280 : expect_more[side] = expect_more_next[side];
215 : 39280 : expect_more_next[side] = std::nullopt;
216 : : // Verify consistency of GetBytesToSend after SetMessageToSend
217 [ + - ]: 39280 : bytes_to_send_fn(/*side=*/side);
218 [ + + ]: 39280 : if (queued) {
219 : : // Remember that this message is now expected by the receiver.
220 [ + - ]: 7074 : expected[side].emplace_back(std::move(next_msg[side]));
221 : : // Construct a new next message to send.
222 [ + - ]: 7074 : next_msg[side] = make_msg_fn(/*first=*/false);
223 : 7074 : }
224 : 39487 : };
225 : :
226 : : // Function to make side send out bytes (if any).
227 : 54331 : auto send_fn = [&](int side, bool everything = false) {
228 : 167070 : const auto& [bytes, more, msg_type] = bytes_to_send_fn(/*side=*/side);
229 : : // Don't do anything if no bytes to send.
230 [ + + ]: 53634 : if (bytes.empty()) return false;
231 [ + + ]: 32436 : size_t send_now = everything ? bytes.size() : provider.ConsumeIntegralInRange<size_t>(0, bytes.size());
232 [ + + ]: 32436 : if (send_now == 0) return false;
233 : : // Add bytes to the in-flight queue, and mark those bytes as consumed.
234 : 81000 : in_flight[side].insert(in_flight[side].end(), bytes.begin(), bytes.begin() + send_now);
235 : 27000 : transports[side]->MarkBytesSent(send_now);
236 : : // If all to-be-sent bytes were sent, move last_more data to expect_more data.
237 [ + + + + ]: 54000 : if (send_now == bytes.size()) {
238 : 9892 : expect_more[side] = last_more[side];
239 : 9892 : expect_more_next[side] = last_more_next[side];
240 : 9892 : }
241 : : // Remove the bytes from the last reported to-be-sent vector.
242 [ + - ]: 27000 : assert(to_send[side].size() >= send_now);
243 : 27000 : to_send[side].erase(to_send[side].begin(), to_send[side].begin() + send_now);
244 : : // Verify that GetBytesToSend gives a result consistent with earlier.
245 : 27000 : bytes_to_send_fn(/*side=*/side);
246 : : // Return whether anything was sent.
247 : 27000 : return send_now > 0;
248 : 53634 : };
249 : :
250 : : // Function to make !side receive bytes (if any).
251 : 20649 : auto recv_fn = [&](int side, bool everything = false) {
252 : : // Don't do anything if no bytes in flight.
253 [ + + ]: 19952 : if (in_flight[side].empty()) return false;
254 : : // Decide span to receive
255 : 12561 : size_t to_recv_len = in_flight[side].size();
256 [ + + ]: 12561 : if (!everything) to_recv_len = provider.ConsumeIntegralInRange<size_t>(0, to_recv_len);
257 : 12561 : Span<const uint8_t> to_recv = Span{in_flight[side]}.first(to_recv_len);
258 : : // Process those bytes
259 [ + + ]: 30428 : while (!to_recv.empty()) {
260 : 17867 : size_t old_len = to_recv.size();
261 : 17867 : bool ret = transports[!side]->ReceivedBytes(to_recv);
262 : : // Bytes must always be accepted, as this test does not introduce any errors in
263 : : // communication.
264 [ + - ]: 17867 : assert(ret);
265 : : // Clear cached expected 'more' information: if certainly no more data was to be sent
266 : : // before, receiving bytes makes this uncertain.
267 [ + + ]: 17867 : if (expect_more[!side] == false) expect_more[!side] = std::nullopt;
268 [ + + ]: 17867 : if (expect_more_next[!side] == false) expect_more_next[!side] = std::nullopt;
269 : : // Verify consistency of GetBytesToSend after ReceivedBytes
270 : 17867 : bytes_to_send_fn(/*side=*/!side);
271 : 17867 : bool progress = to_recv.size() < old_len;
272 [ + + ]: 17867 : if (transports[!side]->ReceivedMessageComplete()) {
273 : 7074 : bool reject{false};
274 : 7074 : auto received = transports[!side]->GetReceivedMessage({}, reject);
275 : : // Receiving must succeed.
276 [ + - ]: 7074 : assert(!reject);
277 : : // There must be a corresponding expected message.
278 [ - + ]: 7074 : assert(!expected[side].empty());
279 : : // The m_message_size field must be correct.
280 [ - + ]: 7074 : assert(received.m_message_size == received.m_recv.size());
281 : : // The m_type must match what is expected.
282 [ - + ]: 7074 : assert(received.m_type == expected[side].front().m_type);
283 : : // The data must match what is expected.
284 [ - + ]: 7074 : assert(MakeByteSpan(received.m_recv) == MakeByteSpan(expected[side].front().data));
285 : 7074 : expected[side].pop_front();
286 : 7074 : progress = true;
287 : 7074 : }
288 : : // Progress must be made (by processing incoming bytes and/or returning complete
289 : : // messages) until all received bytes are processed.
290 [ - + ]: 17867 : assert(progress);
291 : : }
292 : : // Remove the processed bytes from the in_flight buffer.
293 : 12561 : in_flight[side].erase(in_flight[side].begin(), in_flight[side].begin() + to_recv_len);
294 : : // Return whether anything was received.
295 : 12561 : return to_recv_len > 0;
296 : 19952 : };
297 : :
298 : : // Main loop, interleaving new messages, sends, and receives.
299 [ + - + + : 106258 : LIMITED_WHILE(provider.remaining_bytes(), 1000) {
+ + ]
300 [ + - + - ]: 211122 : CallOneOf(provider,
301 : : // (Try to) give the next message to the transport.
302 : 135987 : [&] { new_msg_fn(/*side=*/0); },
303 : 114622 : [&] { new_msg_fn(/*side=*/1); },
304 : : // (Try to) send some bytes from the transport to the network.
305 : 124415 : [&] { send_fn(/*side=*/0); },
306 : 136585 : [&] { send_fn(/*side=*/1); },
307 : : // (Try to) receive bytes from the network, converting to messages.
308 : 116639 : [&] { recv_fn(/*side=*/0); },
309 : 110679 : [&] { recv_fn(/*side=*/1); }
310 : : );
311 : 105561 : }
312 : :
313 : : // When we're done, perform sends and receives of existing messages to flush anything already
314 : : // in flight.
315 : 1878 : while (true) {
316 : 1878 : bool any = false;
317 [ + - + + ]: 1878 : if (send_fn(/*side=*/0, /*everything=*/true)) any = true;
318 [ + - + + ]: 1878 : if (send_fn(/*side=*/1, /*everything=*/true)) any = true;
319 [ + - + + ]: 1878 : if (recv_fn(/*side=*/0, /*everything=*/true)) any = true;
320 [ + - + + ]: 1878 : if (recv_fn(/*side=*/1, /*everything=*/true)) any = true;
321 [ + + ]: 1878 : if (!any) break;
322 : : }
323 : :
324 : : // Make sure nothing is left in flight.
325 [ + - ]: 697 : assert(in_flight[0].empty());
326 [ + - ]: 697 : assert(in_flight[1].empty());
327 : :
328 : : // Make sure all expected messages were received.
329 [ + - ]: 697 : assert(expected[0].empty());
330 [ + - ]: 697 : assert(expected[1].empty());
331 : :
332 : : // Compare session IDs.
333 [ + - + - ]: 697 : assert(transports[0]->GetInfo().session_id == transports[1]->GetInfo().session_id);
334 : 697 : }
335 : :
336 : 637 : std::unique_ptr<Transport> MakeV1Transport(NodeId nodeid) noexcept
337 : : {
338 [ + - ]: 637 : return std::make_unique<V1Transport>(nodeid, SER_NETWORK, INIT_PROTO_VERSION);
339 : : }
340 : :
341 : : template<typename RNG>
342 : 809 : std::unique_ptr<Transport> MakeV2Transport(NodeId nodeid, bool initiator, RNG& rng, FuzzedDataProvider& provider)
343 : : {
344 : : // Retrieve key
345 : 809 : auto key = ConsumePrivateKey(provider);
346 [ + - + + ]: 809 : if (!key.IsValid()) return {};
347 : : // Construct garbage
348 : 776 : size_t garb_len = provider.ConsumeIntegralInRange<size_t>(0, V2Transport::MAX_GARBAGE_LEN);
349 : 776 : std::vector<uint8_t> garb;
350 [ + + ]: 776 : if (garb_len <= 64) {
351 : : // When the garbage length is up to 64 bytes, read it directly from the fuzzer input.
352 [ + - ]: 249 : garb = provider.ConsumeBytes<uint8_t>(garb_len);
353 [ + - ]: 249 : garb.resize(garb_len);
354 : 249 : } else {
355 : : // If it's longer, generate it from the RNG. This avoids having large amounts of
356 : : // (hopefully) irrelevant data needing to be stored in the fuzzer data.
357 [ - + ]: 527 : for (auto& v : garb) v = uint8_t(rng());
358 : : }
359 : : // Retrieve entropy
360 [ + - ]: 776 : auto ent = provider.ConsumeBytes<std::byte>(32);
361 [ + - ]: 776 : ent.resize(32);
362 : : // Use as entropy SHA256(ent || garbage). This prevents a situation where the fuzzer manages to
363 : : // include the garbage terminator (which is a function of both ellswift keys) in the garbage.
364 : : // This is extremely unlikely (~2^-116) with random keys/garbage, but the fuzzer can choose
365 : : // both non-randomly and dependently. Since the entropy is hashed anyway inside the ellswift
366 : : // computation, no coverage should be lost by using a hash as entropy, and it removes the
367 : : // possibility of garbage that happens to contain what is effectively a hash of the keys.
368 [ + - + - ]: 776 : CSHA256().Write(UCharCast(ent.data()), ent.size())
369 [ + - ]: 776 : .Write(garb.data(), garb.size())
370 [ + - ]: 776 : .Finalize(UCharCast(ent.data()));
371 : :
372 [ + - ]: 776 : return std::make_unique<V2Transport>(nodeid, initiator, SER_NETWORK, INIT_PROTO_VERSION, key, ent, std::move(garb));
373 : 809 : }
374 : :
375 : : } // namespace
376 : :
377 [ + - - + ]: 549 : FUZZ_TARGET(p2p_transport_bidirectional, .init = initialize_p2p_transport_serialization)
378 : : {
379 : : // Test with two V1 transports talking to each other.
380 : 203 : FuzzedDataProvider provider{buffer.data(), buffer.size()};
381 : 203 : XoRoShiRo128PlusPlus rng(provider.ConsumeIntegral<uint64_t>());
382 : 203 : auto t1 = MakeV1Transport(NodeId{0});
383 : 203 : auto t2 = MakeV1Transport(NodeId{1});
384 [ + - - + ]: 203 : if (!t1 || !t2) return;
385 [ + - + - : 203 : SimulationTest(*t1, *t2, rng, provider);
+ - ]
386 [ - + ]: 203 : }
387 : :
388 [ + - - + ]: 635 : FUZZ_TARGET(p2p_transport_bidirectional_v2, .init = initialize_p2p_transport_serialization)
389 : : {
390 : : // Test with two V2 transports talking to each other.
391 : 289 : FuzzedDataProvider provider{buffer.data(), buffer.size()};
392 : 289 : XoRoShiRo128PlusPlus rng(provider.ConsumeIntegral<uint64_t>());
393 : 289 : auto t1 = MakeV2Transport(NodeId{0}, true, rng, provider);
394 [ + - ]: 289 : auto t2 = MakeV2Transport(NodeId{1}, false, rng, provider);
395 [ + + + + ]: 289 : if (!t1 || !t2) return;
396 [ + - + - : 270 : SimulationTest(*t1, *t2, rng, provider);
+ - ]
397 [ - + ]: 289 : }
398 : :
399 [ + - - + ]: 577 : FUZZ_TARGET(p2p_transport_bidirectional_v1v2, .init = initialize_p2p_transport_serialization)
400 : : {
401 : : // Test with a V1 initiator talking to a V2 responder.
402 : 231 : FuzzedDataProvider provider{buffer.data(), buffer.size()};
403 : 231 : XoRoShiRo128PlusPlus rng(provider.ConsumeIntegral<uint64_t>());
404 : 231 : auto t1 = MakeV1Transport(NodeId{0});
405 [ + - ]: 231 : auto t2 = MakeV2Transport(NodeId{1}, false, rng, provider);
406 [ + - + + ]: 231 : if (!t1 || !t2) return;
407 [ + - + - : 224 : SimulationTest(*t1, *t2, rng, provider);
+ - ]
408 [ - + ]: 231 : }
|