/*

Copyright (c) 2010-2022, Arvid Norberg
Copyright (c) 2015, Mike Tzou
Copyright (c) 2016, 2018, 2020-2021, Alden Torres
Copyright (c) 2016, Andrei Kurushin
Copyright (c) 2016, Steven Siloti
All rights reserved.

You may use, distribute and modify this code under the terms of the BSD license,
see LICENSE file.
*/

#include "libtorrent/peer_id.hpp"
#include "libtorrent/io_context.hpp"
#include "libtorrent/socket.hpp"
#include "libtorrent/address.hpp"
#include "libtorrent/error_code.hpp"
#include "libtorrent/aux_/io_bytes.hpp"
#include "libtorrent/torrent_info.hpp"
#include "libtorrent/create_torrent.hpp"
#include "libtorrent/hasher.hpp"
#include "libtorrent/aux_/socket_io.hpp"
#include "libtorrent/string_view.hpp"
#include "libtorrent/session.hpp" // for default_disk_io_constructor
#include "libtorrent/disk_interface.hpp"
#include "libtorrent/load_torrent.hpp"
#include "libtorrent/write_resume_data.hpp"
#include "libtorrent/performance_counters.hpp"
#include "libtorrent/aux_/session_settings.hpp"
#include "libtorrent/info_hash.hpp"
#include "libtorrent/file_storage.hpp"
#include "libtorrent/aux_/merkle.hpp"
#include "libtorrent/aux_/merkle_tree.hpp"
#include "libtorrent/error.hpp"
#include <random>
#include <algorithm>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <thread>
#include <functional>
#include <iostream>
#include <atomic>
#include <array>
#include <chrono>
#include <map>
#include <memory>

#ifdef BOOST_ASIO_DYN_LINK
#include <boost/asio/impl/src.hpp>
#endif

namespace {

using namespace lt;
using namespace lt::aux; // for write_* and read_*
using lt::make_address_v4;

using namespace std::placeholders;

void generate_block(span<std::uint32_t> buffer, piece_index_t const piece
	, int const offset)
{
	std::uint32_t const fill = static_cast<std::uint32_t>(
		(static_cast<int>(piece) << 8) | ((offset / 0x4000) & 0xff));
	for (auto& w : buffer) w = fill;
}

// in order to circumvent the restriction of only
// one connection per IP that most clients implement
// all sockets created by this tester are bound to
// unique local IPs in the range (127.0.0.1 - 127.255.255.255)
// it's only enabled if the target is also on the loopback
int local_if_counter = 0;
bool local_bind = false;

// when set to true, blocks downloaded are verified to match
// the test torrents
bool verify_downloads = false;

// if this is true, one block in 1000 will be sent corrupt.
// this only applies to dual and upload tests
bool test_corruption = false;

// number of seeds we've spawned. The test is terminated
// when this reaches zero, for dual tests
std::atomic<int> num_seeds(0);

// the kind of test to run. Upload sends data to a
// bittorrent client, download requests data from
// a client and dual uploads and downloads from a client
// at the same time (this is presumably the most realistic
// test). hash_stress_test floods the target with v2
// HASH_REQUEST messages to stress its hash-response path.
enum test_mode_t
{
	none,
	upload_test,
	download_test,
	dual_test,
	hash_stress_test
};
test_mode_t test_mode = none;

// which torrent metadata version to produce in gen-torrent
enum class gen_version_t
{
	v1,
	v2,
	hybrid
};

// when true and the torrent is hybrid, force the v1 sha1 info hash and
// clear the v2 reserved bit in the handshake.
bool force_v1_handshake = false;

// list of file roots used by hash-stress to pick random files to query.
// Parallel-index into file_trees (defined below after the file_merkle_tree
// struct). Built once at startup; read-only afterwards.
std::vector<sha256_hash> file_root_list;

// absolute byte ranges [begin, end) covered by pad files. A real seed reads
// these from disk, where pad files read as zero, so the seed-side send path
// must send zero for them. generate_block() fills the whole block with the
// test pattern, so write_piece() zeroes any overlapping pad bytes afterwards.
std::vector<std::pair<std::int64_t, std::int64_t>> pad_ranges;

// when greater than zero, each connection emits a periodic STATUS log
// line every interval. zero disables status printing entirely.
std::chrono::seconds status_interval{0};

// the number of suggest messages received (total across all peers)
std::atomic<int> num_suggest(0);

// the number of requests made from suggested pieces
std::atomic<int> num_suggested_requests(0);

std::string leaf_path(std::string f)
{
	if (f.empty()) return "";
	char const* first = f.c_str();
	char const* sep = strrchr(first, '/');
#if defined(TORRENT_WINDOWS) || defined(TORRENT_OS2)
	char const* altsep = strrchr(first, '\\');
	if (sep == 0 || altsep > sep) sep = altsep;
#endif
	if (sep == nullptr) return f;

	if (sep - first == int(f.size()) - 1)
	{
		// if the last character is a / (or \)
		// ignore it
		std::size_t len = 0;
		while (sep > first)
		{
			--sep;
			if (*sep == '/'
#if defined(TORRENT_WINDOWS) || defined(TORRENT_OS2)
				|| *sep == '\\'
#endif
				)
				return std::string(sep + 1, len);
			++len;
		}
		return std::string(first, len);
	}
	return std::string(sep + 1);
}

namespace {
std::random_device dev;
std::mt19937 rng(dev());
}

// flat merkle tree for a single file, plus dimensions. used by both
// gen-torrent (when computing v2 piece roots) and the seed-side
// HASH_REQUEST responder.
struct file_merkle_tree
{
	aux::vector<sha256_hash> tree;
	int num_blocks = 0;
	int num_leafs = 0;
	int blocks_per_piece = 0;
};

// per-file merkle trees, keyed by the file's merkle root (pieces_root).
// Built once at startup; read-only afterwards.
std::map<sha256_hash, file_merkle_tree> file_trees;

// build a full per-file merkle tree from the deterministic content that
// generate_block() produces. The file's content for absolute piece P at
// 16 KiB offset O is generate_block(buf, P, O).
file_merkle_tree build_file_merkle_tree(piece_index_t const file_first_piece,
	int const file_num_pieces,
	std::int64_t const file_size,
	int const piece_length)
{
	file_merkle_tree out;
	out.num_blocks = int((file_size + default_block_size - 1) / default_block_size);
	out.num_leafs = merkle_num_leafs(out.num_blocks);
	out.blocks_per_piece = piece_length / default_block_size;

	int const num_nodes = merkle_num_nodes(out.num_leafs);
	int const first_leaf = merkle_first_leaf(out.num_leafs);
	out.tree.resize(num_nodes);

	std::uint32_t block_buf[default_block_size / 4];
	int block_idx = 0;
	for (int p = 0; p < file_num_pieces; ++p)
	{
		piece_index_t const abs_piece(static_cast<int>(file_first_piece) + p);
		std::int64_t const piece_offset = std::int64_t(p) * piece_length;
		int const bytes_in_piece =
			int(std::min(std::int64_t(piece_length), file_size - piece_offset));
		int const blocks_in_piece = (bytes_in_piece + default_block_size - 1) / default_block_size;

		for (int b = 0; b < blocks_in_piece; ++b)
		{
			generate_block(block_buf, abs_piece, b * default_block_size);
			int const block_bytes = (b == blocks_in_piece - 1)
				? (bytes_in_piece - b * default_block_size)
				: default_block_size;
			out.tree[first_leaf + block_idx] =
				hasher256(reinterpret_cast<char const*>(block_buf), block_bytes).final();
			++block_idx;
		}
	}
	// remaining leaves are zero (default-constructed sha256_hash), which is
	// the correct merkle padding for tail leaves below the piece layer.
	merkle_fill_tree(out.tree, out.num_leafs);
	return out;
}

// extract the hashes for a HASH_REQUEST response: `count` hashes at the given
// (base, index) followed by the uncle proof chain (`max(0, proof_layers -
// (merkle_num_layers(merkle_num_leafs(count)) - 1))` hashes). Mirrors the
// wire format produced by bt_peer_connection::write_hashes().
std::vector<sha256_hash> get_hashes_for_request(file_merkle_tree const& ft,
	int const base,
	int const index,
	int const count,
	int const proof_layers)
{
	if (count <= 0 || count > 8192) return {};
	int const tree_layers = merkle_num_layers(ft.num_leafs);
	if (base < 0 || base >= tree_layers) return {};
	int const base_layer_idx = tree_layers - base;
	int const base_start_idx = merkle_to_flat_index(base_layer_idx, index);
	if (base_start_idx < 0 || base_start_idx + count > int(ft.tree.size())) return {};

	std::vector<sha256_hash> ret;
	int const base_tree_layers = merkle_num_layers(merkle_num_leafs(count)) - 1;
	int const proof_hashes = std::max(0, proof_layers - base_tree_layers);
	ret.reserve(std::size_t(count + proof_hashes));

	for (int i = 0; i < count; ++i)
		ret.push_back(ft.tree[base_start_idx + i]);

	int proof_idx = base_start_idx;
	for (int i = 0; i < proof_layers; ++i)
	{
		proof_idx = merkle_get_parent(proof_idx);
		if (proof_idx <= 0) break;
		if (i >= base_tree_layers)
		{
			int const sibling = merkle_get_sibling(proof_idx);
			if (sibling < 0 || sibling >= int(ft.tree.size())) return {};
			ret.push_back(ft.tree[sibling]);
		}
	}
	return ret;
}

struct peer_conn
{
	peer_conn(io_context& ios,
		int piece_count,
		int blocks_pp,
		int last_piece_size_,
		tcp::endpoint const& ep,
		sha1_hash const& ih,
		bool v2_,
		bool seed_,
		int churn_,
		bool corrupt_,
		bool flood_hashes_ = false)
		: s(ios)
		, status_timer(ios)
		, read_pos(0)
		, state(handshaking)
		, choked(true)
		, current_piece(-1)
		, current_piece_is_allowed(false)
		, block(0)
		, blocks_per_piece(blocks_pp)
		, last_piece_blocks((last_piece_size_ + 0x3fff) / 0x4000)
		, last_block_size(last_piece_size_ - ((last_piece_size_ + 0x3fff) / 0x4000 - 1) * 0x4000)
		, info_hash(ih)
		, outstanding_requests(0)
		, v2(v2_)
		, seed(seed_)
		, fast_extension(false)
		, flood_hashes(flood_hashes_)
		, blocks_received(0)
		, blocks_sent(0)
		, hashes_received(0)
		, hashes_rejected(0)
		, hash_requests_sent(0)
		, hashes_sent(0)
		, hash_rejects_sent(0)
		, last_status_blocks_received(0)
		, last_status_blocks_sent(0)
		, num_pieces(piece_count)
		, start_time(clock_type::now())
		, churn(churn_)
		, corrupt(corrupt_)
		, endpoint(ep)
		, restarting(false)
	{
		corruption_counter = rand() % 1000;
		if (seed) ++num_seeds;
		pieces.reserve(std::size_t(piece_count));
		start_conn();
	}

	void schedule_status_timer()
	{
		if (status_interval.count() <= 0) return;
		status_timer.expires_after(status_interval);
		status_timer.async_wait([this](error_code const& ec) {
			if (ec) return; // canceled or aborted
			on_status_timer();
		});
	}

	// periodic per-connection status. A stalled connection shows
	// delta=0 here, plus the choked/outstanding fields tell you
	// whether we're waiting for the peer or out of work to send.
	void on_status_timer()
	{
		int const drecv = blocks_received - last_status_blocks_received;
		int const dsent = blocks_sent - last_status_blocks_sent;
		last_status_blocks_received = blocks_received;
		last_status_blocks_sent = blocks_sent;
		std::fprintf(stderr,
			"STATUS %s recv=%d(+%d) sent=%d(+%d) outstanding=%d "
			"choked=%s pieces-queued=%d current-piece=%d block=%d "
			"hash-req-sent=%d hashes-recv=%d hashes-rejected=%d\n",
			seed ? "seed" : "leech",
			blocks_received,
			drecv,
			blocks_sent,
			dsent,
			outstanding_requests,
			choked ? "yes" : "no",
			int(pieces.size()),
			static_cast<int>(current_piece),
			block,
			hash_requests_sent,
			hashes_received,
			hashes_rejected);
		schedule_status_timer();
	}

	void start_conn()
	{
		if (local_bind)
		{
			error_code ec;
			s.open(endpoint.protocol(), ec);
			if (ec)
			{
				close("ERROR OPEN", ec);
				return;
			}
			tcp::endpoint bind_if(address_v4(
				(127 << 24) + unsigned (local_if_counter + 1)), 0);
			++local_if_counter;
			s.bind(bind_if, ec);
			if (ec)
			{
				close("ERROR BIND", ec);
				return;
			}
		}
		restarting = false;
		s.async_connect(endpoint, std::bind(&peer_conn::on_connect, this, _1));
	}

	tcp::socket s;
	// fires every status_interval while the connection is open (when the
	// interval is positive); emits a one-line state dump useful for
	// diagnosing stalls.
	boost::asio::steady_timer status_timer;
	// per-connection scratch buffers reused across messages. We rely on each
	// async_write being followed by the next one only from inside its
	// completion handler, so writes on a single socket never overlap and the
	// buffer below is never clobbered while in flight. Adding an overlapping
	// async_write on the same socket would silently corrupt the in-flight
	// payload.
	char write_buf_proto[100];
	std::uint32_t write_buffer[17*1024/4];
	std::uint32_t buffer[17*1024/4];
	int read_pos;
	int corruption_counter;

	enum state_t
	{
		handshaking,
		sending_request,
		receiving_message
	};
	int state;
	std::vector<piece_index_t> pieces;
	std::vector<piece_index_t> suggested_pieces;
	std::vector<piece_index_t> allowed_fast;
	bool choked;
	piece_index_t current_piece; // the piece we're currently requesting blocks from
	bool current_piece_is_allowed;
	int block;
	int blocks_per_piece;
	// number of blocks in the final (possibly partial) piece, and the size of
	// the last block within that piece. for regular pieces, every block is
	// `blocks_per_piece` blocks of 16 KiB.
	int last_piece_blocks;
	int last_block_size;
	sha1_hash info_hash;
	int outstanding_requests;
	// when set, this connection sets the v2 protocol bit in the handshake
	// and responds to / drains HASH_REQUEST / HASHES / HASH_REJECT messages.
	bool v2;
	// if this is true, this connection is a seed
	bool seed;
	bool fast_extension;
	// when set, this downloader sends a flood of HASH_REQUEST messages instead
	// of (or in addition to) regular piece requests. Used by hash-stress mode.
	bool flood_hashes;
	int blocks_received;
	int blocks_sent;
	int hashes_received;
	int hashes_rejected;
	int hash_requests_sent;
	int hashes_sent;
	int hash_rejects_sent;
	// snapshot of blocks_{received,sent} at the previous status timer
	// fire, used to compute per-interval deltas.
	int last_status_blocks_received;
	int last_status_blocks_sent;
	int num_pieces;
	time_point start_time;
	time_point end_time;
	int churn;
	bool corrupt;
	tcp::endpoint endpoint;
	bool restarting;

	void on_connect(error_code const& ec)
	{
		if (ec)
		{
			close("ERROR CONNECT", ec);
			return;
		}

		static char const handshake[] = "\x13"
										"BitTorrent protocol\0\0\0\0\0\0\0\x04"
										"                    " // space for info-hash
										"aaaaaaaaaaaaaaaaaaaa" // peer-id
										"\0\0\0\x01\x02"; // interested
		constexpr int handshake_size = int(sizeof(handshake) - 1);
		static_assert(handshake_size <= int(sizeof(write_buf_proto)), "write_buf_proto too small");
		std::memcpy(write_buf_proto, handshake, handshake_size);
		// bit 4 of reserved byte 7 (0x10) advertises the v2 hash-exchange
		// protocol (BEP 52). reserved bytes are at offset 20..27.
		if (v2) write_buf_proto[27] |= 0x10;
		std::memcpy(write_buf_proto + 28, info_hash.data(), 20);
		std::generate(write_buf_proto + 48, write_buf_proto + 68, [] { return char(rand()); });
		// for seeds, don't send the interested message
		boost::asio::async_write(s,
			boost::asio::buffer(write_buf_proto, std::size_t(handshake_size - (seed ? 5 : 0))),
			std::bind(&peer_conn::on_handshake, this, _1, _2));
	}

	void on_handshake(error_code const& ec, size_t)
	{
		if (ec)
		{
			close("ERROR SEND HANDSHAKE", ec);
			return;
		}

		// read handshake
		boost::asio::async_read(s, boost::asio::buffer(buffer, 68)
			, std::bind(&peer_conn::on_handshake2, this, _1, _2));
	}

	void on_handshake2(error_code const& ec, size_t)
	{
		if (ec)
		{
			close("ERROR READ HANDSHAKE", ec);
			return;
		}

		// buffer is the full 68 byte handshake
		// look at the extension bits

		fast_extension = (reinterpret_cast<char const*>(buffer)[27] & 4) != 0;

		// start the periodic status timer once the connection is alive.
		schedule_status_timer();

		if (seed)
		{
			write_have_all();
		}
		else
		{
			work_download();
		}
	}

	void write_have_all()
	{
		if (fast_extension)
		{
			char* ptr = write_buf_proto;
			// have_all
			write_uint32(1, ptr);
			write_uint8(0xe, ptr);
			// unchoke
			write_uint32(1, ptr);
			write_uint8(1, ptr);
			boost::asio::async_write(s, boost::asio::buffer(write_buf_proto, std::size_t(ptr - write_buf_proto))
				, std::bind(&peer_conn::on_sent, this, _1, _2, "ERROR SENT HAVE ALL"));
		}
		else
		{
			// bitfield
			int len = (num_pieces + 7) / 8;
			char* ptr = reinterpret_cast<char*>(buffer);
			write_uint32(len + 1, ptr);
			write_uint8(5, ptr);
			memset(ptr, 255, std::size_t(len));
			ptr += len;
			// unchoke
			write_uint32(1, ptr);
			write_uint8(1, ptr);
			boost::asio::async_write(s, boost::asio::buffer(buffer, std::size_t(len + 10))
				, std::bind(&peer_conn::on_sent, this, _1, _2, "ERROR SENT HAVE ALL"));
		}
	}

	void on_sent(error_code const& ec, size_t, char const* msg)
	{
		if (ec)
		{
			close(msg, ec);
			return;
		}

		if (seed)
		{
			// read next message from the peer
			boost::asio::async_read(s,
				boost::asio::buffer(buffer, 4),
				std::bind(&peer_conn::on_msg_length, this, _1, _2));
		}
		else
		{
			// the only write from a downloader that lands here is write_have(),
			// called after the last block of a piece. Run the completion
			// check / pipeline refill instead of just blocking on a read --
			// otherwise if the last block to arrive is the last block of any
			// piece, the connection stalls near 100%.
			work_download();
		}
	}

	bool write_request()
	{
		// if we're choked (and there are no allowed-fast pieces left)
		if (choked && allowed_fast.empty() && !current_piece_is_allowed) return false;

		// if there are no pieces left to request
		if (pieces.empty() && suggested_pieces.empty() && allowed_fast.empty()
			&& current_piece == piece_index_t(-1))
		{
			return false;
		}

		if (current_piece == piece_index_t(-1))
		{
			// pick a new piece. allowed-fast pieces are usable both while
			// choked and while unchoked -- so once the normal `pieces` queue
			// has been drained, fall back to allowed_fast even when unchoked
			// (otherwise the seed's ALLOWED_FAST set is silently leaked and
			// the leech caps at less than 100%).
			if (choked && allowed_fast.size() > 0)
			{
				current_piece = allowed_fast.front();
				allowed_fast.erase(allowed_fast.begin());
				current_piece_is_allowed = true;
			}
			else if (suggested_pieces.size() > 0)
			{
				current_piece = suggested_pieces.front();
				suggested_pieces.erase(suggested_pieces.begin());
				++num_suggested_requests;
				current_piece_is_allowed = false;
			}
			else if (pieces.size() > 0)
			{
				current_piece = pieces.front();
				pieces.erase(pieces.begin());
				current_piece_is_allowed = false;
			}
			else if (allowed_fast.size() > 0)
			{
				current_piece = allowed_fast.front();
				allowed_fast.erase(allowed_fast.begin());
				current_piece_is_allowed = true;
			}
			else
			{
				TORRENT_ASSERT_FAIL();
			}
		}
		bool const is_last_piece = (static_cast<int>(current_piece) == num_pieces - 1);
		int const this_blocks_per_piece = is_last_piece ? last_piece_blocks : blocks_per_piece;
		int const this_block_size =
			(is_last_piece && block == last_piece_blocks - 1) ? last_block_size : 16 * 1024;

		char* ptr = write_buf_proto;
		write_uint32(13, ptr); // payload size
		write_uint8(6, ptr); // request
		write_uint32(static_cast<int>(current_piece), ptr);
		write_uint32(block * 16 * 1024, ptr);
		write_uint32(this_block_size, ptr);
		boost::asio::async_write(s,
			boost::asio::buffer(write_buf_proto, 17),
			std::bind(&peer_conn::on_req_sent, this, _1, _2));

		++outstanding_requests;
		++block;
		if (block == this_blocks_per_piece)
		{
			block = 0;
			current_piece = piece_index_t(-1);
			current_piece_is_allowed = false;
		}
		return true;
	}

	void on_req_sent(error_code const& ec, size_t)
	{
		if (ec)
		{
			close("ERROR SEND REQUEST", ec);
			return;
		}

		work_download();
	}

	void close(char const* msg, error_code const& ec)
	{
		end_time = clock_type::now();
		status_timer.cancel();
		char tmp[1024];
		std::snprintf(tmp, sizeof(tmp), "%s: %s", msg, ec ? ec.message().c_str() : "");
		int time = int(total_milliseconds(end_time - start_time));
		if (time == 0) time = 1;
		double const up = double(std::int64_t(blocks_sent) * 0x4000 / time) / 1000.0;
		double const down = double(std::int64_t(blocks_received) * 0x4000 / time) / 1000.0;
		error_code e;

		char ep_str[200];
		address const& addr = s.local_endpoint(e).address();
		if (addr.is_v6())
			std::snprintf(ep_str, sizeof(ep_str), "[%s]:%d", addr.to_string().c_str()
				, s.local_endpoint(e).port());
		else
			std::snprintf(ep_str, sizeof(ep_str), "%s:%d", addr.to_string().c_str()
				, s.local_endpoint(e).port());
		std::printf("%s ep: %s sent: %d received: %d duration: %d ms "
					"up: %.1fMB/s down: %.1fMB/s outstanding: %d choked: %s "
					"pieces-queued: %d\n",
			tmp,
			ep_str,
			blocks_sent,
			blocks_received,
			time,
			up,
			down,
			outstanding_requests,
			choked ? "yes" : "no",
			int(pieces.size()));
		if (seed) --num_seeds;
	}

	void work_download()
	{
		if (flood_hashes)
		{
			if (file_root_list.empty())
			{
				close("HASH_STRESS: no v2 files in torrent", error_code());
				return;
			}
			if (outstanding_requests < 40)
			{
				if (write_flood_hash_request()) return;
			}
			boost::asio::async_read(s,
				boost::asio::buffer(buffer, 4),
				std::bind(&peer_conn::on_msg_length, this, _1, _2));
			return;
		}

		int const total_blocks = (num_pieces - 1) * blocks_per_piece + last_piece_blocks;
		if (pieces.empty() && suggested_pieces.empty() && allowed_fast.empty()
			&& current_piece == piece_index_t(-1) && outstanding_requests == 0
			&& blocks_received >= total_blocks)
		{
			close("COMPLETED DOWNLOAD", error_code());
			return;
		}

		// send requests
		if (outstanding_requests < 40)
		{
			if (write_request()) return;
		}

		// read message
		boost::asio::async_read(s, boost::asio::buffer(buffer, 4)
			, std::bind(&peer_conn::on_msg_length, this, _1, _2));
	}

	// pick a random file and piece-range, send a HASH_REQUEST at the
	// block layer. Returns false only if the file map is empty.
	bool write_flood_hash_request()
	{
		auto const& root = file_root_list[std::size_t(std::rand()) % file_root_list.size()];
		auto it = file_trees.find(root);
		if (it == file_trees.end()) return false;
		file_merkle_tree const& ft = it->second;
		int const tree_layers = merkle_num_layers(ft.num_leafs);
		if (tree_layers < 1 || ft.blocks_per_piece <= 0 || ft.num_blocks <= 0) return false;

		int const num_pieces_in_file =
			std::max(1, (ft.num_blocks + ft.blocks_per_piece - 1) / ft.blocks_per_piece);
		int const piece_idx = std::rand() % num_pieces_in_file;
		int const block_start = piece_idx * ft.blocks_per_piece;
		int const blocks_in_this_piece = std::min(ft.blocks_per_piece, ft.num_blocks - block_start);

		// validate_hash_request() requires proof_layers < num_layers - base.
		// With base == 0, the maximum valid proof_layers is tree_layers - 1
		// (uncle chain from the block layer up to just below the root).
		write_hash_request(root, 0 /*base*/, block_start, blocks_in_this_piece, tree_layers - 1);
		++outstanding_requests;
		return true;
	}

	void on_msg_length(error_code const& ec, size_t)
	{
		if ((ec == boost::asio::error::operation_aborted || ec == boost::asio::error::bad_descriptor)
			&& restarting)
		{
			start_conn();
			return;
		}

		if (ec)
		{
			close("ERROR RECEIVE MESSAGE PREFIX", ec);
			return;
		}
		char* ptr = reinterpret_cast<char*>(buffer);
		unsigned int length = read_uint32(ptr);
		if (length > sizeof(buffer))
		{
			std::fprintf(stderr, "len: %u\n", length);
			close("ERROR RECEIVE MESSAGE PREFIX: packet too big", error_code());
			return;
		}
		boost::asio::async_read(s, boost::asio::buffer(buffer, length)
			, std::bind(&peer_conn::on_message, this, _1, _2));
	}

	void on_message(error_code const& ec, size_t bytes_transferred)
	{
		if ((ec == boost::asio::error::operation_aborted || ec == boost::asio::error::bad_descriptor)
			&& restarting)
		{
			start_conn();
			return;
		}

		if (ec)
		{
			close("ERROR RECEIVE MESSAGE", ec);
			return;
		}
		char* ptr = reinterpret_cast<char*>(buffer);
		int msg = read_uint8(ptr);

		if (test_mode == dual_test && num_seeds == 0)
		{
			TORRENT_ASSERT(!seed);
			close("NO MORE SEEDS, test done", error_code());
			return;
		}

		//std::printf("msg: %d len: %d\n", msg, int(bytes_transferred));

		if (seed)
		{
			if (msg == 6)
			{
				if (bytes_transferred != 13)
				{
					close("REQUEST packet has invalid size", error_code());
					return;
				}
				piece_index_t const piece = piece_index_t(aux::read_int32(ptr));
				int const start = aux::read_int32(ptr);
				int const length = aux::read_int32(ptr);
				write_piece(piece, start, length);
			}
			else if (msg == 21) // hash_request
			{
				if (bytes_transferred != 49)
				{
					close("HASH_REQUEST packet has invalid size", error_code());
					return;
				}
				handle_hash_request(ptr);
			}
			else if (msg == 3) // not-interested
			{
				close("DONE", error_code());
				return;
			}
			else
			{
				// read another message
				boost::asio::async_read(s, boost::asio::buffer(buffer, 4)
					, std::bind(&peer_conn::on_msg_length, this, _1, _2));
			}
		}
		else
		{
			if (msg == 0xe) // have_all
			{
				// build a list of all pieces and request them all!
				pieces.resize(std::size_t(num_pieces));
				for (std::size_t i = 0; i < pieces.size(); ++i)
					pieces[i] = piece_index_t(int(i));
				std::shuffle(pieces.begin(), pieces.end(), rng);
			}
			else if (msg == 4) // have
			{
				piece_index_t const piece(aux::read_int32(ptr));
				if (pieces.empty()) pieces.push_back(piece);
				else pieces.insert(pieces.begin() + (unsigned(rand()) % pieces.size()), piece);
			}
			else if (msg == 5) // bitfield
			{
				pieces.reserve(std::size_t(num_pieces));
				piece_index_t piece(0);
				for (int i = 0; i < int(bytes_transferred); ++i)
				{
					int mask = 0x80;
					for (int k = 0; k < 8; ++k)
					{
						if (piece > piece_index_t(num_pieces)) break;
						if (*ptr & mask) pieces.push_back(piece);
						mask >>= 1;
						++piece;
					}
					++ptr;
				}
				std::shuffle(pieces.begin(), pieces.end(), rng);
			}
			else if (msg == 7) // piece
			{
				if (verify_downloads)
				{
					piece_index_t const piece(read_int32(ptr));
					int start = read_int32(ptr);
					int size = int(bytes_transferred) - 9;
					verify_piece(piece, start, ptr, size);
				}
				++blocks_received;
				--outstanding_requests;
				piece_index_t const piece = piece_index_t(aux::read_int32(ptr));
				int start = aux::read_int32(ptr);

				if (churn && (blocks_received % churn) == 0) {
					outstanding_requests = 0;
					restarting = true;
					s.close();
					return;
				}
				if ((start + int(bytes_transferred)) / 0x4000 == blocks_per_piece)
				{
					write_have(piece);
					return;
				}
			}
			else if (msg == 13) // suggest
			{
				piece_index_t const piece(aux::read_int32(ptr));
				auto i = std::find(pieces.begin(), pieces.end(), piece);
				if (i != pieces.end())
				{
					pieces.erase(i);
					suggested_pieces.push_back(piece);
					++num_suggest;
				}
			}
			else if (msg == 16) // reject request
			{
				piece_index_t const piece(aux::read_int32(ptr));
				int start = aux::read_int32(ptr);
				int length = aux::read_int32(ptr);

				// put it back!
				if (current_piece != piece)
				{
					if (pieces.empty() || pieces.back() != piece)
						pieces.push_back(piece);
				}
				else
				{
					block = std::min(start / 0x4000, block);
					if (block == 0)
					{
						pieces.push_back(current_piece);
						current_piece = piece_index_t(-1);
						current_piece_is_allowed = false;
					}
				}
				--outstanding_requests;
				std::fprintf(stderr, "REJECT: [ piece: %d start: %d length: %d ]\n"
					, static_cast<int>(piece), start, length);
			}
			else if (msg == 0) // choke
			{
				choked = true;
			}
			else if (msg == 1) // unchoke
			{
				choked = false;
			}
			else if (msg == 17) // allowed_fast
			{
				piece_index_t const piece = piece_index_t(aux::read_int32(ptr));
				auto i = std::find(pieces.begin(), pieces.end(), piece);
				if (i != pieces.end())
				{
					pieces.erase(i);
					allowed_fast.push_back(piece);
				}
			}
			else if (msg == 22) // hashes
			{
				++hashes_received;
				if (flood_hashes && outstanding_requests > 0) --outstanding_requests;
			}
			else if (msg == 23) // hash_reject
			{
				++hashes_rejected;
				std::fprintf(stderr,
					"HASH_REJECT received (total=%d) -- "
					"indicates an issue in the hash-request logic\n",
					hashes_rejected);
				if (flood_hashes && outstanding_requests > 0) --outstanding_requests;
			}
			work_download();
		}
	}

	bool verify_piece(piece_index_t const piece, int start, char const* ptr, int size)
	{
		std::uint32_t const* buf = reinterpret_cast<std::uint32_t const*>(ptr);
		std::uint32_t const fill = static_cast<std::uint32_t>(
			(static_cast<int>(piece) << 8) | ((start / 0x4000) & 0xff));
		for (int i = 0; i < size / 4; ++i)
		{
			if (buf[i] != fill)
			{
				std::fprintf(stderr, "received invalid block. piece %d block %d\n"
					, static_cast<int>(piece), start / 0x4000);
				exit(1);
			}
		}
		return true;
	}

	void write_piece(piece_index_t const piece, int start, int length)
	{
		// BT REQUEST is at most 16 KiB but the last block of a (partial)
		// last piece can be smaller. Fill the buffer rounded up to a 4-byte
		// word so all `length` bytes are deterministic.
		int const fill_words = (length + 3) / 4;
		generate_block({write_buffer, fill_words}, piece, start);

		// zero any bytes of this block that fall within a pad file. A real
		// seed serves zeros there (pad files read as zero from disk); without
		// this a data/pad-straddling block fails the v1 piece hash while
		// passing the v2 block hash on hybrid torrents. See pad_ranges.
		if (!pad_ranges.empty())
		{
			std::int64_t const piece_len = std::int64_t(blocks_per_piece) * 0x4000;
			std::int64_t const block_begin =
				std::int64_t(static_cast<int>(piece)) * piece_len + start;
			std::int64_t const block_end = block_begin + length;
			char* const buf = reinterpret_cast<char*>(write_buffer);
			// pad_ranges is sorted by offset and the ranges don't overlap, so
			// binary-search for the first range that can reach into this block
			auto it = std::lower_bound(pad_ranges.begin(),
				pad_ranges.end(),
				block_begin,
				[](auto const& pr, std::int64_t const off) { return pr.second <= off; });
			for (; it != pad_ranges.end() && it->first < block_end; ++it)
			{
				std::int64_t const lo = std::max(block_begin, it->first);
				std::int64_t const hi = std::min(block_end, it->second);
				if (lo < hi) std::memset(buf + (lo - block_begin), 0, std::size_t(hi - lo));
			}
		}

		if (corrupt)
		{
			--corruption_counter;
			if (corruption_counter == 0)
			{
				corruption_counter = 1000;
				std::memset(write_buffer, 0, 10);
			}
		}
		char* ptr = write_buf_proto;
		write_uint32(9 + length, ptr);
		write_uint8(7, ptr);
		write_uint32(static_cast<int>(piece), ptr);
		write_uint32(start, ptr);
		std::array<boost::asio::const_buffer, 2> vec;
		vec[0] = boost::asio::buffer(write_buf_proto, std::size_t(ptr - write_buf_proto));
		vec[1] = boost::asio::buffer(write_buffer, std::size_t(length));
		boost::asio::async_write(s, vec, std::bind(&peer_conn::on_sent, this, _1, _2, "ERROR SENT PIECE"));
		++blocks_sent;
		if (churn && (blocks_sent % churn) == 0 && seed) {
			outstanding_requests = 0;
			restarting = true;
			s.close();
		}
	}

	void write_have(piece_index_t const piece)
	{
		char* ptr = write_buf_proto;
		write_uint32(5, ptr);
		write_uint8(4, ptr);
		write_uint32(static_cast<int>(piece), ptr);
		boost::asio::async_write(s, boost::asio::buffer(write_buf_proto, 9), std::bind(&peer_conn::on_sent, this, _1, _2, "ERROR SENT HAVE"));
	}

	// reads a 48-byte HASH_REQUEST/HASH_REJECT payload (file-root + 4 int32s)
	// from `ptr` and produces a HASHES (msg 22) or HASH_REJECT (msg 23) reply
	// from the precomputed merkle tree for `file_root`.
	void handle_hash_request(char const* ptr)
	{
		sha256_hash file_root;
		std::memcpy(file_root.data(), ptr, 32);
		ptr += 32;
		int const base = aux::read_int32(ptr);
		int const index = aux::read_int32(ptr);
		int const count = aux::read_int32(ptr);
		int const proof_layers = aux::read_int32(ptr);

		auto it = file_trees.find(file_root);
		std::vector<sha256_hash> hashes;
		if (it != file_trees.end())
			hashes = get_hashes_for_request(it->second, base, index, count, proof_layers);

		if (hashes.empty())
		{
			++hash_rejects_sent;
			write_hash_reject(file_root, base, index, count, proof_layers);
		}
		else
		{
			++hashes_sent;
			write_hashes(file_root, base, index, count, proof_layers, hashes);
		}
	}

	void write_hashes(sha256_hash const& file_root,
		int const base,
		int const index,
		int const count,
		int const proof_layers,
		std::vector<sha256_hash> const& hashes)
	{
		int const payload_size = 1 + 32 + 4 + 4 + 4 + 4 + int(hashes.size()) * 32;
		int const total = 4 + payload_size;
		// HASHES is the only message big enough that it doesn't fit in
		// write_buf_proto. write_buffer is sized for a 16 KiB block plus
		// header so it easily accommodates any HASHES reply this tester
		// produces (get_hashes_for_request caps count at blocks_per_piece).
		TORRENT_ASSERT(total <= int(sizeof(write_buffer)));
		char* ptr = reinterpret_cast<char*>(write_buffer);
		write_uint32(payload_size, ptr);
		write_uint8(22, ptr); // msg_hashes
		std::memcpy(ptr, file_root.data(), 32);
		ptr += 32;
		write_uint32(base, ptr);
		write_uint32(index, ptr);
		write_uint32(count, ptr);
		write_uint32(proof_layers, ptr);
		for (auto const& h : hashes)
		{
			std::memcpy(ptr, h.data(), 32);
			ptr += 32;
		}
		boost::asio::async_write(s,
			boost::asio::buffer(write_buffer, std::size_t(total)),
			std::bind(&peer_conn::on_hash_sent, this, _1, _2, "ERROR SENT HASHES"));
	}

	void write_hash_reject(sha256_hash const& file_root,
		int const base,
		int const index,
		int const count,
		int const proof_layers)
	{
		constexpr int payload_size = 1 + 32 + 4 + 4 + 4 + 4;
		constexpr int total = 4 + payload_size;
		static_assert(total <= int(sizeof(write_buf_proto)), "write_buf_proto too small");
		char* ptr = write_buf_proto;
		write_uint32(payload_size, ptr);
		write_uint8(23, ptr); // msg_hash_reject
		std::memcpy(ptr, file_root.data(), 32);
		ptr += 32;
		write_uint32(base, ptr);
		write_uint32(index, ptr);
		write_uint32(count, ptr);
		write_uint32(proof_layers, ptr);
		boost::asio::async_write(s,
			boost::asio::buffer(write_buf_proto, std::size_t(total)),
			std::bind(&peer_conn::on_hash_sent, this, _1, _2, "ERROR SENT HASH_REJECT"));
	}

	void write_hash_request(sha256_hash const& file_root,
		int const base,
		int const index,
		int const count,
		int const proof_layers)
	{
		constexpr int payload_size = 1 + 32 + 4 + 4 + 4 + 4;
		constexpr int total = 4 + payload_size;
		static_assert(total <= int(sizeof(write_buf_proto)), "write_buf_proto too small");
		char* ptr = write_buf_proto;
		write_uint32(payload_size, ptr);
		write_uint8(21, ptr); // msg_hash_request
		std::memcpy(ptr, file_root.data(), 32);
		ptr += 32;
		write_uint32(base, ptr);
		write_uint32(index, ptr);
		write_uint32(count, ptr);
		write_uint32(proof_layers, ptr);
		boost::asio::async_write(s,
			boost::asio::buffer(write_buf_proto, std::size_t(total)),
			std::bind(&peer_conn::on_hash_sent, this, _1, _2, "ERROR SENT HASH_REQUEST"));
		++hash_requests_sent;
	}

	void on_hash_sent(error_code const& ec, size_t, char const* msg)
	{
		if (ec)
		{
			close(msg, ec);
			return;
		}
		if (seed)
		{
			// after sending a HASHES / HASH_REJECT, resume reading.
			boost::asio::async_read(s,
				boost::asio::buffer(buffer, 4),
				std::bind(&peer_conn::on_msg_length, this, _1, _2));
		}
		else
		{
			// downloader in flood mode: pipeline another HASH_REQUEST or drain
			// pending responses.
			work_download();
		}
	}
};

[[noreturn]] void print_usage()
{
	std::fprintf(stderr,
		"usage: connection_tester command [options]\n\n"
		"command is one of:\n"
		"  gen-torrent        generate a test torrent\n"
		"    options for this command:\n"
		"    -s <size>          the size of the torrent in megabytes\n"
		"    -n <num-files>     the number of files in the test torrent\n"
		"    -t <file>          the file to save the .torrent file to\n"
		"    -V <version>       torrent format: 1 = v1-only, 2 = v2-only,\n"
		"                       h = hybrid (default)\n"
		"    -U <num>           Add <num> random test tracker URLs\n"
		"    for v2/hybrid torrents a <file>.merkle sidecar holding the precomputed\n"
		"    per-file merkle trees is written next to the .torrent, so up/dual/\n"
		"    hash-stress runs load it instead of rehashing at startup\n\n"
		"  gen-data             generate the data file(s) for the test torrent\n"
		"    options for this command:\n"
		"    -t <file>          the torrent file that was previously generated\n"
		"    -P <path>          the path to where the data should be stored\n"
		"    -R                 also write a resume file under <path>/.resume/\n"
		"                       that marks every piece as owned, so a client\n"
		"                       can start seeding without a startup check pass\n"
		"  gen-test-torrents    generate many test torrents (cannot be used for up/down tests)\n"
		"    options for this command:\n"
		"    -N <num-torrents>  number of torrents to generate\n"
		"    -n <num-files>     number of files in each torrent\n"
		"    -t <name>          base name of torrent files (index is appended)\n\n"
		"    -T <URL>           add the specified tracker URL to each torrent\n"
		"                       this option may appear multiple times\n\n"
		"  upload               start an uploader test\n"
		"  download             start a downloader test\n"
		"  dual                 start a download and upload test\n"
		"  hash-stress          flood the target with v2 HASH_REQUEST messages\n"
		"                       (requires a v2 or hybrid torrent on the target)\n"
		"    options for these commands:\n"
		"    -c <num-conns>     the number of connections to make to the target\n"
		"    -d <dst>           the IP address of the target\n"
		"    -p <dst-port>      the port the target listens on\n"
		"    -t <torrent-file>  the torrent file previously generated by gen-torrent\n"
		"    -C                 send corrupt pieces sometimes (applies to upload and dual)\n"
		"    -1                 for hybrid torrents, use the v1 info hash in the\n"
		"                       handshake and clear the v2 reserved bit (default is\n"
		"                       to use the v2 info hash and exercise the v2 path)\n"
		"    -r <reconnects>    churn - number of reconnects per second\n"
		"    -S <seconds>       print a per-connection STATUS line every\n"
		"                       <seconds> seconds (0 disables, default)\n\n"
		"examples:\n\n"
		"connection_tester gen-torrent -s 1024 -n 4 -t test.torrent\n"
		"connection_tester upload -c 200 -d 127.0.0.1 -p 6881 -t test.torrent\n"
		"connection_tester download -c 200 -d 127.0.0.1 -p 6881 -t test.torrent\n"
		"connection_tester dual -c 200 -d 127.0.0.1 -p 6881 -t test.torrent\n"
		"connection_tester hash-stress -c 100 -d 127.0.0.1 -p 6881 -t test.torrent\n");
	exit(1);
}

void hasher_thread(lt::aux::vector<sha1_hash, piece_index_t>* output
	, lt::create_torrent const& ct
	, file_slice current_file
	, piece_index_t const start_piece
	, piece_index_t const end_piece
	, bool print)
{
	if (print) std::fprintf(stderr, "\n");
	std::uint32_t piece[0x4000 / 4];
	int const piece_size = ct.piece_length();

	for (piece_index_t i = start_piece; i < end_piece; ++i)
	{
		hasher ph;
		for (int j = 0; j < piece_size; j += 0x4000)
		{
			generate_block(piece, i, j);

			// if any part of this block overlaps with a pad-file, we need to
			// clear those bytes to 0
			for (int k = 0; k < 0x4000; )
			{
				while (current_file.size == 0)
				{
					++current_file.file_index;
					if (current_file.file_index >= ct.end_file())
					{
						TORRENT_ASSERT(i == prev(end_piece));
						TORRENT_ASSERT(k > 0);
						TORRENT_ASSERT(k < 0x4000);
						// this is the last piece of the torrent, and the piece
						// extends a bit past the end of the last file. This part
						// should be truncated
						ph.update(reinterpret_cast<char*>(piece), k);
						goto out;
					}
					current_file.offset = 0;
					current_file.size = ct.file_at(current_file.file_index).size;
				}
				int const range = int(std::min(std::int64_t(0x4000 - k), current_file.size));
				if (ct.file_at(current_file.file_index).flags & file_storage::flag_pad_file)
					std::memset(reinterpret_cast<char*>(piece) + k, 0, std::size_t(range));

				current_file.offset += range;
				current_file.size -= range;
				k += range;
			}
			ph.update(reinterpret_cast<char*>(piece), 0x4000);
		}
out:
		(*output)[i] = ph.final();
		int const range = static_cast<int>(end_piece) - static_cast<int>(start_piece);
		if (print && (static_cast<int>(i) & 1))
		{
			int const delta_piece = static_cast<int>(i) - static_cast<int>(start_piece);
			std::fprintf(stderr, "\r%.1f %% ", double(delta_piece * 100) / double(range));
		}
	}
	if (print) std::fprintf(stderr, "\n");
}

// describes the (canonicalized) file an absolute piece belongs to.
// Only meaningful for v2 and hybrid torrents, where canonicalize() guarantees
// each piece lives within a single file.
struct piece_file_map_entry
{
	file_index_t file{0};
	piece_index_t::diff_type piece_in_file{0};
	bool is_pad = false;
};

// walk t.file_at(fi) in order, attributing each piece to the data file it
// belongs to. Pad files are skipped because they fill the tail of the
// previous data file's last (partial) piece; they own no pieces of their own.
std::vector<piece_file_map_entry> compute_piece_to_file_map(create_torrent const& t)
{
	int const piece_length = t.piece_length();
	int const num_pieces = t.num_pieces();
	std::vector<piece_file_map_entry> result;
	result.resize(std::size_t(num_pieces));

	int abs = 0;
	for (file_index_t fi{0}; fi < t.end_file(); ++fi)
	{
		auto const& fe = t.file_at(fi);
		if (fe.flags & file_storage::flag_pad_file) continue;
		if (fe.size == 0) continue;
		int const file_pieces = int((fe.size + piece_length - 1) / piece_length);
		for (int p = 0; p < file_pieces && abs < num_pieces; ++p, ++abs)
		{
			result[std::size_t(abs)].file = fi;
			result[std::size_t(abs)].piece_in_file = piece_index_t::diff_type(p);
			result[std::size_t(abs)].is_pad = false;
		}
	}
	return result;
}

void v2_hasher_thread(create_torrent const* ct,
	std::vector<piece_file_map_entry> const* piece_map,
	lt::aux::vector<sha256_hash, piece_index_t>* output,
	piece_index_t const start_piece,
	piece_index_t const end_piece)
{
	int const piece_length = ct->piece_length();
	int const blocks_per_piece = piece_length / default_block_size;
	// largest subtree we will ever need: a full piece. For files smaller
	// than a piece the subtree is smaller and we reuse the prefix.
	aux::vector<sha256_hash> subtree(merkle_num_nodes(blocks_per_piece));

	std::uint32_t block_buf[default_block_size / 4];
	for (piece_index_t i = start_piece; i < end_piece; ++i)
	{
		auto const& pm = (*piece_map)[std::size_t(static_cast<int>(i))];
		if (pm.is_pad) continue;
		auto const& fe = ct->file_at(pm.file);
		std::int64_t const piece_offset =
			std::int64_t(static_cast<int>(pm.piece_in_file)) * piece_length;
		int const bytes_in_piece =
			int(std::min(std::int64_t(piece_length), fe.size - piece_offset));
		if (bytes_in_piece <= 0) continue;
		int const blocks_in_piece = (bytes_in_piece + default_block_size - 1) / default_block_size;
		// BEP 52 padding rule: per-piece merkle roots use blocks_per_piece
		// leaves for files at least a piece long (zero-padding the last
		// piece's tail). For files shorter than a piece, pad to next pow-2
		// of the block count. Matches create_torrent.cpp on_hash().
		int const num_leafs =
			(fe.size < piece_length) ? merkle_num_leafs(blocks_in_piece) : blocks_per_piece;
		int const num_nodes = merkle_num_nodes(num_leafs);
		int const first_leaf = merkle_first_leaf(num_leafs);

		for (int n = 0; n < num_nodes; ++n)
			subtree[n] = sha256_hash{};
		for (int b = 0; b < blocks_in_piece; ++b)
		{
			generate_block(block_buf, i, b * default_block_size);
			int const block_bytes = (b == blocks_in_piece - 1)
				? (bytes_in_piece - b * default_block_size)
				: default_block_size;
			subtree[first_leaf + b] =
				hasher256(reinterpret_cast<char const*>(block_buf), block_bytes).final();
		}
		merkle_fill_tree(span<sha256_hash>(subtree).first(num_nodes), num_leafs);
		(*output)[i] = subtree[0];
	}
}

// size is in megabytes
std::vector<char> generate_torrent(int num_pieces,
	int num_files,
	char const* torrent_name,
	int num_trackers,
	gen_version_t const version)
{
	std::vector<lt::create_file_entry> files;
	// 1 MiB piece size
	const int piece_size = 1024 * 1024;
	const std::int64_t total_size = std::int64_t(piece_size) * num_pieces;

	std::int64_t s = total_size;
	int file_index = 0;
	std::int64_t file_size = total_size / num_files;
	while (s > 0)
	{
		char b[100];
		std::snprintf(b, sizeof(b), "%s/stress_test%d", torrent_name, file_index);
		++file_index;
		files.push_back({std::string(b), file_size, {}, 0, {}});
		s -= file_size;
		file_size += 200;
	}

	lt::create_flags_t flags{};
	if (version == gen_version_t::v1)
		flags = lt::create_torrent::v1_only;
	else if (version == gen_version_t::v2)
		flags = lt::create_torrent::v2_only;
	// hybrid: no version flag — canonicalize and emit both v1 and v2 metadata

	lt::create_torrent t(std::move(files), piece_size, flags);

	num_pieces = t.num_pieces();
	bool const do_v1 = (version != gen_version_t::v2);
	bool const do_v2 = (version != gen_version_t::v1);

	int const num_threads = std::thread::hardware_concurrency()
		? int(std::thread::hardware_concurrency()) : 4;
	std::printf("hashing in %d threads\n", num_threads);

	if (do_v1)
	{
		std::vector<std::thread> threads;
		threads.reserve(std::size_t(num_threads));
		lt::aux::vector<lt::sha1_hash, piece_index_t> hashes{static_cast<std::size_t>(num_pieces)};
		lt::file_slice current_file;
		current_file.file_index = file_index_t{0};
		current_file.offset = 0;
		current_file.size = t.file_at(current_file.file_index).size;
		std::int64_t offset = 0;
		for (int i = 0; i < num_threads; ++i)
		{
			auto const start_piece = piece_index_t(i * num_pieces / num_threads);
			std::int64_t const target_offset =
				static_cast<int>(start_piece) * std::int64_t(t.piece_length());
			while (offset < target_offset)
			{
				while (current_file.size == 0)
				{
					++current_file.file_index;
					current_file.offset = 0;
					current_file.size = t.file_at(current_file.file_index).size;
				}
				std::int64_t const increment = std::min(current_file.size, target_offset - offset);
				current_file.offset += increment;
				current_file.size -= increment;
				offset += increment;
			}
			threads.emplace_back(&hasher_thread,
				&hashes,
				std::cref(t),
				current_file,
				start_piece,
				piece_index_t((i + 1) * num_pieces / num_threads),
				i == 0);
		}

		for (auto& i : threads)
			i.join();

		for (auto i : t.piece_range())
			t.set_hash(i, hashes[i]);
	}

	if (do_v2)
	{
		auto const piece_map = compute_piece_to_file_map(t);
		lt::aux::vector<sha256_hash, piece_index_t> v2_hashes{static_cast<std::size_t>(num_pieces)};

		std::vector<std::thread> threads;
		threads.reserve(std::size_t(num_threads));
		for (int i = 0; i < num_threads; ++i)
		{
			threads.emplace_back(&v2_hasher_thread,
				&t,
				&piece_map,
				&v2_hashes,
				piece_index_t(i * num_pieces / num_threads),
				piece_index_t((i + 1) * num_pieces / num_threads));
		}
		for (auto& th : threads)
			th.join();

		for (piece_index_t p : t.piece_range())
		{
			auto const& pm = piece_map[std::size_t(static_cast<int>(p))];
			if (pm.is_pad) continue;
			if (v2_hashes[p].is_all_zeros()) continue;
			t.set_hash2(pm.file, pm.piece_in_file, v2_hashes[p]);
		}
	}

	for (int i = 0; i < num_trackers; ++i)
	{
		char b[100];
		std::snprintf(b, sizeof(b), "http://test.tracker%d.com/announce", i);
		t.add_tracker(b);
	}

	return t.generate_buf();
}

// forces a flush of this piece: pread_disk_io only flushes once a piece is
// hashed or the cache crosses max_queued_disk_bytes, and this tool never
// keeps enough blocks in flight to cross that on its own.
void hash_piece_if_last_block(disk_interface& disk,
	storage_holder& st,
	torrent_info const& ti,
	piece_index_t const piece,
	int const offset,
	int const length)
{
	if (offset + length < ti.piece_size_for_req(piece))
		return;

	// async_hash() asserts unless v1_hash is set or the v2 span is
	// non-empty, so this must supply whichever the torrent actually has.
	file_storage const& fs = ti.layout();
	auto v2_hashes =
		std::make_shared<std::vector<sha256_hash>>(ti.v2() ? fs.blocks_in_piece2(piece) : 0);
	disk_job_flags_t flags = disk_interface::flush_piece;
	if (ti.v1())
		flags |= disk_interface::v1_hash;

	// kept alive by the handler: async_hash() fills the span in only once
	// the (possibly deferred) hash job runs. Errors here are expected on
	// shutdown, canceled by write_handler's final abort(); real failures
	// already surface via the write path.
	disk.async_hash(st,
		piece,
		*v2_hashes,
		flags,
		[v2_hashes](piece_index_t, sha1_hash const&, lt::storage_error const&) {});
}

// throws so the exception unwinds out of generate_data(), whose
// disk_interface destructor aborts and joins the disk threads.
void write_handler(torrent_info const& ti,
	disk_interface& disk,
	storage_holder& st,
	piece_index_t& piece,
	int& offset,
	lt::storage_error const& error)
{
	if (error)
	{
		if (error.ec == error::operation_aborted)
			return;
		throw lt::system_error(error.ec);
	}

	file_storage const& fs = ti.layout();

	if (static_cast<int>(piece) & 1)
	{
		std::fprintf(stderr, "\r%.1f %% "
			, double(static_cast<int>(piece) * 100) / double(fs.num_pieces()));
	}

	if (piece >= fs.end_piece()) return;
	offset += 0x4000;
	if (offset >= ti.piece_size_for_req(piece))
	{
		offset = 0;
		++piece;
	}
	if (piece >= fs.end_piece())
	{
		disk.abort(false);
		return;
	}

	std::uint32_t buffer[0x4000 / 4];
	generate_block(buffer, piece, offset);

	int const left_in_piece = ti.piece_size_for_req(piece) - offset;
	if (left_in_piece <= 0) return;

	int const this_length = std::min(left_in_piece, 0x4000);
	disk.async_write(st,
		{piece, offset, this_length},
		reinterpret_cast<char const*>(buffer),
		std::shared_ptr<disk_observer>(),
		[&](lt::storage_error const& e) { write_handler(ti, disk, st, piece, offset, e); });

	hash_piece_if_last_block(disk, st, ti, piece, offset, this_length);

	disk.submit_jobs();
}

void generate_data(std::string const path, torrent_info const& ti)
{
	io_context ios;
	counters stats_counters;
	settings_pack sett = default_settings();
	// a piece is bigger than the fixed number of blocks kept in flight
	// below, so it can't complete on its own to trigger
	// hash_piece_if_last_block()'s flush. Lowering the watermark below
	// the in-flight budget forces continuous flushing instead.
	sett.set_int(settings_pack::max_queued_disk_bytes, 8 * 0x4000);
	std::unique_ptr<lt::disk_interface> disk = default_disk_io_constructor(ios, sett, stats_counters);

	file_storage const& fs = ti.layout();

	aux::vector<download_priority_t, file_index_t> priorities;
	sha1_hash info_hash;
	renamed_files rf;
	storage_params params{
		fs,
		rf,
		path,
		{},
		storage_mode_sparse,
		priorities,
		info_hash,
		ti.v1(),
		ti.v2(),
	};

	storage_holder st = disk->new_torrent(params, std::shared_ptr<void>());

	piece_index_t piece(0);
	int offset = 0;

	std::uint32_t buffer[0x4000 / 4];
	generate_block(buffer, piece, offset);

	int const initial_length = std::min(ti.piece_size_for_req(piece), 0x4000);
	disk->async_write(st,
		{piece, offset, initial_length},
		reinterpret_cast<char const*>(buffer),
		std::shared_ptr<disk_observer>(),
		[&](lt::storage_error const& error) {
			write_handler(ti, *disk, st, piece, offset, error);
		});

	hash_piece_if_last_block(*disk, st, ti, piece, offset, initial_length);

	// keep 10 writes in flight at all times
	for (int i = 0; i < 10; ++i)
	{
		write_handler(ti, *disk, st, piece, offset, lt::storage_error());
	}

	disk->submit_jobs();

	ios.run();
}

void io_thread(io_context* ios) try
{
	ios->run();
}
catch (std::exception const& e)
{
	std::fprintf(stderr, "ERROR: %s\n", e.what());
}

// build the per-file merkle trees needed to satisfy HASH_REQUEST messages
// and to drive the hash-stress flood. Populates the globals file_trees and
// file_root_list. No-op for v1-only torrents.
void build_global_file_trees(torrent_info const& ti)
{
	if (!ti.v2()) return;
	file_storage const& fs = ti.layout();
	int const piece_length = fs.piece_length();

	for (file_index_t fi : fs.file_range())
	{
		if (fs.pad_file_at(fi)) continue;
		std::int64_t const file_size = fs.file_size(fi);
		if (file_size == 0) continue;

		piece_index_t const file_first_piece = fs.piece_index_at_file(fi);
		int const file_num_pieces = fs.file_num_pieces(fi);

		file_merkle_tree ft =
			build_file_merkle_tree(file_first_piece, file_num_pieces, file_size, piece_length);
		if (ft.tree.empty()) continue;
		sha256_hash const root = ft.tree[0];
		file_root_list.push_back(root);
		file_trees.emplace(root, std::move(ft));
	}
	std::printf("built %d v2 file merkle trees\n", int(file_trees.size()));
}

// the precomputed per-file merkle trees written alongside a v2/hybrid .torrent
// (the ".merkle" sidecar file). The point is to front-load the expensive
// per-file tree hashing to gen-torrent time so the repeated upload/dual/
// hash-stress runs can load the trees instead of rehashing the whole payload at
// startup.
//
// The sidecar is tied to a specific .torrent, so it carries no header or
// per-file framing: it is just the merkle-tree node hashes for each file (in
// file order, skipping pad and empty files), concatenated. Both the number of
// files and the node count per file are re-derived from the torrent when
// reading, exactly as build_global_file_trees() does.

// serialize the global file_trees to `path` in file order. Called from
// gen-torrent for v2/hybrid torrents (see load_merkle_trees() for the reader).
// Best-effort: failures only print a warning, since the test can rebuild.
// The non-pad, non-empty files are visited in file order; the reader must walk
// the same order so the headerless sidecar lines up.
void save_merkle_trees(std::string const& path, file_storage const& fs)
{
	std::ofstream out(path, std::ios::binary);
	if (!out)
	{
		std::fprintf(stderr, "failed to open '%s' for writing merkle trees\n", path.c_str());
		return;
	}

	int num_files = 0;
	for (file_index_t fi : fs.file_range())
	{
		if (fs.pad_file_at(fi)) continue;
		if (fs.file_size(fi) == 0) continue;
		auto it = file_trees.find(fs.root(fi));
		// build_global_file_trees() inserts a tree for every non-pad, non-empty
		// file, keyed by exactly this root. A miss means the writer and the
		// (headerless, purely positional) reader would disagree on this file's
		// bytes, producing a silently misaligned sidecar. That should be
		// impossible, so abort rather than write a corrupt file.
		if (it == file_trees.end())
		{
			std::fprintf(stderr, "missing merkle tree for file %d\n", static_cast<int>(fi));
			std::abort();
		}
		file_merkle_tree const& ft = it->second;
		// sha256_hash is exactly 32 contiguous bytes, so the node array can be
		// written in one shot.
		out.write(
			reinterpret_cast<char const*>(ft.tree.data()), std::streamsize(ft.tree.size()) * 32);
		++num_files;
	}
	if (!out)
	{
		std::fprintf(stderr, "error writing merkle trees to '%s'\n", path.c_str());
		return;
	}
	std::printf("wrote %d merkle trees to %s\n", num_files, path.c_str());
}

// load merkle trees previously written by save_merkle_trees() into the global
// file_trees / file_root_list. The dimensions of each tree are derived from
// `fs`, so the sidecar is just raw node data. The loaded root of each file must
// equal the torrent's pieces_root, which rejects a stale sidecar from a
// different torrent. On any mismatch or read error the globals are left
// untouched and false is returned, so the caller falls back to rebuilding.
bool load_merkle_trees(std::string const& path, file_storage const& fs)
{
	std::ifstream in(path, std::ios::binary);
	if (!in) return false;

	int const blocks_per_piece = fs.piece_length() / default_block_size;

	time_point const load_start = clock_type::now();
	std::map<sha256_hash, file_merkle_tree> loaded;
	std::vector<sha256_hash> roots;
	for (file_index_t fi : fs.file_range())
	{
		if (fs.pad_file_at(fi)) continue;
		std::int64_t const file_size = fs.file_size(fi);
		if (file_size == 0) continue;

		file_merkle_tree ft;
		ft.num_blocks = int((file_size + default_block_size - 1) / default_block_size);
		ft.num_leafs = merkle_num_leafs(ft.num_blocks);
		ft.blocks_per_piece = blocks_per_piece;
		int const num_nodes = merkle_num_nodes(ft.num_leafs);
		ft.tree.resize(num_nodes);
		if (!in.read(reinterpret_cast<char*>(ft.tree.data()), std::streamsize(num_nodes) * 32))
			return false;

		sha256_hash const root = fs.root(fi);
		// the stored tree must be for this torrent: its root has to match the
		// file's pieces_root.
		if (ft.tree[0] != root) return false;
		roots.push_back(root);
		loaded.emplace(root, std::move(ft));
	}

	// the sidecar must contain exactly the trees for this torrent, no trailing
	// bytes.
	if (in.peek() != std::char_traits<char>::eof()) return false;

	double const load_s = std::chrono::duration<double>(clock_type::now() - load_start).count();

	file_trees = std::move(loaded);
	file_root_list = std::move(roots);
	std::printf(
		"loaded %d merkle trees from %s in %.3f s\n", int(file_trees.size()), path.c_str(), load_s);
	return true;
}

// record the absolute byte ranges of pad files into pad_ranges, so the
// seed-side send path can zero them (see pad_ranges). No-op when there are
// no pad files (v1-only torrents, or any torrent without padding).
void build_pad_ranges(file_storage const& fs)
{
	pad_ranges.clear();
	std::int64_t offset = 0;
	for (file_index_t fi : fs.file_range())
	{
		std::int64_t const sz = fs.file_size(fi);
		if (fs.pad_file_at(fi) && sz > 0) pad_ranges.emplace_back(offset, offset + sz);
		offset += sz;
	}
}

// write a resume file under <data_path>/.resume/ that asserts ownership
// of every piece, so a seed-only client (e.g. client_test for the upload
// benchmark) comes up in seeding state without a startup check pass.
// expects the canonical payload to already exist at <data_path> (run
// generate_data() first).
void write_seeding_resume_file(torrent_info const& ti, char const* data_path)
{
	add_torrent_params atp;
	atp.ti = std::make_shared<torrent_info>(ti);
	atp.have_pieces.resize(ti.num_pieces(), true);
	atp.save_path = data_path;

	// for v2 torrents, the per-file piece-layer hashes are already in the
	// .torrent file (its `piece layers` field) and load_torrent() puts each
	// merkle_tree into piece_layer mode for us. block hashes are only needed
	// to answer HASH_REQUEST messages from peers; connection_tester's leech
	// mode does not send those (only the explicit hash-stress mode does), so
	// we don't store block hashes in the resume file. that keeps the resume
	// small and avoids hashing the entire payload here.

	std::vector<char> buf = lt::write_resume_data_buf(atp);

	// mirror client_test's resume layout:
	// <save_path>/.resume/<info-hash-hex>.resume
	std::filesystem::path const resume_dir = std::filesystem::path(data_path) / ".resume";
	std::error_code ec;
	std::filesystem::create_directories(resume_dir, ec);

	std::stringstream hex;
	hex << ti.info_hashes().get_best();
	std::filesystem::path const resume_path = resume_dir / (hex.str() + ".resume");

	std::ofstream out(resume_path, std::ios::binary);
	if (!out.write(buf.data(), std::streamsize(buf.size())))
	{
		std::fprintf(stderr, "failed to write '%s'\n", resume_path.string().c_str());
		return;
	}
	std::printf("wrote resume file: %s\n", resume_path.string().c_str());
}

} // anonymous namespace

int main(int argc, char* argv[])
{
	if (argc <= 1) print_usage();

	char const* command = argv[1];
	int size = 1000;
	int num_files = 10;
	int num_torrents = 1;
	int num_trackers = 0;
	char const* torrent_file = "benchmark.torrent";
	char const* data_path = ".";
	int num_connections = 50;
	char const* destination_ip = "127.0.0.1";
	int destination_port = 6881;
	int churn = 0;
	std::vector<std::string> trackers;
	gen_version_t gen_version = gen_version_t::hybrid;
	bool gen_resume_too = false;

	argv += 2;
	argc -= 2;

	while (argc > 0)
	{
		char const* optname = argv[0];
		++argv;
		--argc;

		if (optname[0] != '-' || strlen(optname) != 2)
		{
			std::fprintf(stderr, "unknown option: %s\n", optname);
			continue;
		}

		// options with no arguments
		switch (optname[1])
		{
			case 'C': test_corruption = true; continue;
			case '1':
				force_v1_handshake = true;
				continue;
			case 'R':
				gen_resume_too = true;
				continue;
		}

		if (argc == 0)
		{
			std::fprintf(stderr, "missing argument for option: %s\n", optname);
			break;
		}

		char const* opt = argv[0];
		++argv;
		--argc;

		switch (optname[1])
		{
			case 's': size = atoi(opt); break;
			case 'n': num_files = atoi(opt); break;
			case 'N': num_torrents = atoi(opt); break;
			case 't': torrent_file = opt; break;
			case 'T': trackers.push_back(opt); break;
			case 'U': num_trackers = atoi(opt); break;
			case 'P': data_path = opt; break;
			case 'c': num_connections = atoi(opt); break;
			case 'p': destination_port = atoi(opt); break;
			case 'd': destination_ip = opt; break;
			case 'r': churn = atoi(opt); break;
			case 'S':
				status_interval = std::chrono::seconds(atoi(opt));
				break;
			case 'V':
				if (opt[0] == '1' && opt[1] == 0)
					gen_version = gen_version_t::v1;
				else if (opt[0] == '2' && opt[1] == 0)
					gen_version = gen_version_t::v2;
				else if (opt[0] == 'h' && opt[1] == 0)
					gen_version = gen_version_t::hybrid;
				else
				{
					std::fprintf(stderr, "invalid -V value: %s (expected 1, 2 or h)\n", opt);
					return 1;
				}
				break;
			default: std::fprintf(stderr, "unknown option: %s\n", optname);
		}
	}

	if (command == "gen-torrent"_sv)
	{
		std::string name = leaf_path(torrent_file);
		name = name.substr(0, name.find_last_of('.'));
		std::printf("generating torrent: %s\n", name.c_str());
		std::vector<char> tmp = generate_torrent(
			size ? size : 1024, num_files ? num_files : 1, name.c_str(), num_trackers, gen_version);

		FILE* output = stdout;
		if ("-"_sv != torrent_file)
		{
			if( (output = std::fopen(torrent_file, "wb+")) == nullptr)
			{
				std::fprintf(stderr, "Could not open file '%s' for writing: %s\n"
					, torrent_file, std::strerror(errno));
				exit(2);
			}
		}
		std::fprintf(stderr, "writing file to: %s\n", torrent_file);
		fwrite(&tmp[0], 1, tmp.size(), output);
		if (output != stdout)
			std::fclose(output);

		// for v2/hybrid torrents, precompute the per-file merkle trees and save
		// them next to the .torrent as a ".merkle" sidecar, so the seed-side
		// HASH_REQUEST responder and hash-stress flood can load them at test
		// startup instead of rehashing the entire payload. Skipped when writing
		// the torrent to stdout (no on-disk path to anchor the sidecar to).
		if ("-"_sv != torrent_file)
		{
			try
			{
				add_torrent_params atp = load_torrent_file(torrent_file);
				if (atp.ti->v2())
				{
					build_global_file_trees(*atp.ti);
					save_merkle_trees(std::string(torrent_file) + ".merkle", atp.ti->layout());
				}
			}
			catch (lt::system_error const& err)
			{
				std::fprintf(stderr,
					"WARNING: could not build merkle trees: %s\n",
					err.code().message().c_str());
			}
		}

		return 0;
	}
	else if (command == "gen-data"_sv)
	{
		try
		{
			add_torrent_params atp = load_torrent_file(torrent_file);
			generate_data(data_path, *atp.ti);
			if (gen_resume_too) write_seeding_resume_file(*atp.ti, data_path);
		}
		catch (lt::system_error const& err)
		{
			std::fprintf(stderr, "ERROR: %s\n", err.code().message().c_str());
			return 1;
		}
		return 0;
	}
	else if (command == "gen-test-torrents"_sv)
	{
		for (int i = 0; i < num_torrents; ++i)
		{
			char torrent_name[100];
			std::snprintf(torrent_name, sizeof(torrent_name), "%s-%d.torrent", torrent_file, i);

			std::vector<create_file_entry> fs;
			for (int j = 0; j < num_files; ++j)
			{
				char file_name[100];
				std::snprintf(file_name, sizeof(file_name), "%s-%d/file-%d", torrent_file, i, j);
				fs.push_back({std::string(file_name), std::int64_t(j + i + 1) * 251, {}, 0, {}});
			}
			// 1 MiB piece size
			const int piece_size = 1024 * 1024;
			lt::create_torrent t(fs, piece_size, lt::create_torrent::v1_only);
			sha1_hash dummy("abcdefghijklmnopqrst");
			for (auto const k : t.piece_range())
				t.set_hash(k, dummy);

			int tier = 0;
			for (auto const& tr : trackers)
				t.add_tracker(tr, tier++);

			std::vector<char> buf = t.generate_buf();
			FILE* f = std::fopen(torrent_name, "w+");
			if (f == nullptr)
			{
				std::fprintf(stderr, "Could not open file '%s' for writing: %s\n"
					, torrent_name, std::strerror(errno));
				return 1;
			}
			size_t ret = fwrite(buf.data(), 1, buf.size(), f);
			if (ret != buf.size())
			{
				std::fprintf(stderr, "write returned: %d (expected %d)\n", int(ret), int(buf.size()));
				std::fclose(f);
				return 1;
			}
			std::printf("wrote %s\n", torrent_name);
			std::fclose(f);
		}
		return 0;
	}
	else if (command == "upload"_sv)
	{
		test_mode = upload_test;
	}
	else if (command == "download"_sv)
	{
		test_mode = download_test;
	}
	else if (command == "dual"_sv)
	{
		test_mode = dual_test;
	}
	else if (command == "hash-stress"_sv)
	{
		test_mode = hash_stress_test;
	}
	else
	{
		std::fprintf(stderr, "unknown command: %s\n\n", command);
		print_usage();
	}

	error_code ec;
	address_v4 addr = make_address_v4(destination_ip, ec);
	if (ec)
	{
		std::fprintf(stderr, "ERROR RESOLVING %s: %s\n", destination_ip, ec.message().c_str());
		return 1;
	}
	tcp::endpoint ep(addr, std::uint16_t(destination_port));

#if !defined __APPLE__
	// apparently darwin doesn't seems to let you bind to
	// loopback on any other IP than 127.0.0.1
	std::uint32_t const ip = addr.to_uint();
	if ((ip & 0xff000000) == 0x7f000000)
	{
		local_bind = true;
	}
#endif

	add_torrent_params atp = load_torrent_file(torrent_file);
	if (ec)
	{
		std::fprintf(stderr, "ERROR LOADING .TORRENT: %s\n", ec.message().c_str());
		return 1;
	}

	info_hash_t const& ihs = atp.ti->info_hashes();
	bool const torrent_has_v2 = ihs.has_v2();
	bool const torrent_has_v1 = ihs.has_v1();
	bool const handshake_uses_v2 = torrent_has_v2 && !(torrent_has_v1 && force_v1_handshake);
	sha1_hash const handshake_info_hash = handshake_uses_v2 ? sha1_hash(ihs.v2.data()) : ihs.v1;

	if (test_mode == hash_stress_test && !torrent_has_v2)
	{
		std::fprintf(stderr, "ERROR: hash-stress requires a v2 or hybrid torrent\n");
		return 1;
	}

	if (test_mode == hash_stress_test && force_v1_handshake)
	{
		std::fprintf(stderr, "ERROR: -1 (force v1 handshake) is incompatible with hash-stress\n");
		return 1;
	}

	// build the per-file merkle trees we need to respond to HASH_REQUEST (as
	// seed) and to drive the hash-stress flood. download-only mode does not
	// touch the trees, so the build is skipped to save startup cost. Prefer the
	// ".merkle" sidecar written by gen-torrent: loading the precomputed trees
	// avoids rehashing the whole payload here. Fall back to rebuilding if the
	// sidecar is missing or stale.
	if (torrent_has_v2
		&& (test_mode == upload_test || test_mode == dual_test || test_mode == hash_stress_test))
	{
		if (!load_merkle_trees(std::string(torrent_file) + ".merkle", atp.ti->layout()))
			build_global_file_trees(*atp.ti);
	}

	// seed modes generate piece data on the fly; record pad-file ranges so the
	// send path can zero them to match what a disk-backed seed would serve.
	if (test_mode == upload_test || test_mode == dual_test) build_pad_ranges(atp.ti->layout());

	bool const flood_hashes_all = (test_mode == hash_stress_test);

	std::vector<peer_conn*> conns;
	conns.reserve(std::size_t(num_connections));
	int const num_threads = 2;
	io_context ios[num_threads];
	int const last_piece_size = atp.ti->piece_size(piece_index_t(atp.ti->num_pieces() - 1));

	// start of the transfer window. This is after the merkle-tree build above,
	// which can take several seconds for a large v2 torrent and must not count
	// against the transfer rate.
	time_point const transfer_start = clock_type::now();
	for (int i = 0; i < num_connections; ++i)
	{
		bool corrupt = test_corruption && (i & 1) == 0;
		bool seed = false;
		if (test_mode == upload_test) seed = true;
		else if (test_mode == dual_test) seed = (i & 1);
		// hash-stress connections all act as downloaders that flood HASH_REQUEST.
		conns.push_back(new peer_conn(ios[i % num_threads],
			atp.ti->num_pieces(),
			atp.ti->piece_length() / 16 / 1024,
			last_piece_size,
			ep,
			handshake_info_hash,
			handshake_uses_v2,
			seed,
			churn,
			corrupt,
			flood_hashes_all));
		std::this_thread::sleep_for(std::chrono::milliseconds(1));
		ios[i % num_threads].poll_one();
	}

	std::thread t1(&io_thread, &ios[0]);
	std::thread t2(&io_thread, &ios[1]);

	t1.join();
	t2.join();
	time_point const transfer_end = clock_type::now();

	std::int64_t total_sent = 0;
	std::int64_t total_received = 0;
	std::int64_t total_hashes_received = 0;
	std::int64_t total_hashes_rejected = 0;
	std::int64_t total_hash_requests = 0;
	std::int64_t total_hashes_sent = 0;
	std::int64_t total_hash_rejects_sent = 0;

	for (peer_conn* p : conns)
	{
		total_sent += p->blocks_sent;
		total_received += p->blocks_received;
		total_hashes_received += p->hashes_received;
		total_hashes_rejected += p->hashes_rejected;
		total_hash_requests += p->hash_requests_sent;
		total_hashes_sent += p->hashes_sent;
		total_hash_rejects_sent += p->hash_rejects_sent;
		delete p;
	}

	double const sent_mb = double(total_sent * 0x4000) / 1000000.0;
	double const recv_mb = double(total_received * 0x4000) / 1000000.0;
	// duration of the transfer window only (excludes the merkle-tree build at
	// startup), so the rate reflects actual transfer throughput.
	double const duration_s = double(total_milliseconds(transfer_end - transfer_start)) / 1000.0;
	double const rate_div = duration_s > 0.0 ? duration_s : 0.001;

	std::printf("=========================\n"
				"suggests: %d suggested-requests: %d\n"
				"total sent: %.1f %% received: %.1f %%\n"
				"transfer sent: %.1f MB received: %.1f MB\n"
				"duration: %.2f s\n"
				"rate sent: %.2f MB/s received: %.2f MB/s\n"
				"hash-requests: sent=%lld   hashes: sent=%lld received=%lld\n"
				"hash-rejects:  sent=%lld received=%lld\n",
		int(num_suggest),
		int(num_suggested_requests),
		double(total_sent * 0x4000) * 100.0 / double(atp.ti->total_size()),
		double(total_received * 0x4000) * 100.0 / double(atp.ti->total_size()),
		sent_mb,
		recv_mb,
		duration_s,
		sent_mb / rate_div,
		recv_mb / rate_div,
		static_cast<long long>(total_hash_requests),
		static_cast<long long>(total_hashes_sent),
		static_cast<long long>(total_hashes_received),
		static_cast<long long>(total_hash_rejects_sent),
		static_cast<long long>(total_hashes_rejected));

	return 0;
}
