Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion console/tracker-client/src/console/clients/udp/checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::num::NonZeroU16;
use std::time::Duration;

use bittorrent_primitives::info_hash::InfoHash as TorrustInfoHash;
use bittorrent_tracker_client::peer_id::default_production_peer_id;
use bittorrent_tracker_client::udp::client::UdpTrackerClient;
use bittorrent_udp_tracker_protocol::common::InfoHash;
use bittorrent_udp_tracker_protocol::{
Expand Down Expand Up @@ -129,7 +130,7 @@ impl Client {
action_placeholder: AnnounceActionPlaceholder::default(),
transaction_id,
info_hash: InfoHash(info_hash.bytes()),
peer_id: params.peer_id.map_or(PeerId(*b"-qB00000000000000001"), PeerId),
peer_id: params.peer_id.map_or(default_production_peer_id(), PeerId),
bytes_downloaded: NumberOfBytes::new(params.downloaded.unwrap_or(0)),
bytes_uploaded: NumberOfBytes::new(params.uploaded.unwrap_or(0)),
bytes_left: NumberOfBytes::new(params.left.unwrap_or(0)),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Define Tracker-Client Peer ID Convention

## Description

Tracker-client defaults currently use a qBittorrent peer ID prefix (`-qB`), which
misrepresents Torrust tracker-client traffic.

Issue [#1564](https://github.com/torrust/torrust-tracker/issues/1564) requires
adopting a Torrust-specific convention while keeping protocol fixtures explicit
and package boundaries decoupled.

## Agreement

We adopt the following tracker-client peer ID convention:

- Prefix: `RC` (Rust Client)
- Version field: `3000` for the current `v3.0.0` line
- Full layout: `-<CC><VVVV>-<12-digit-suffix>` (Azureus-style)

Defaults are split by context:

- Production defaults use `-RC3000-` plus a randomized 12-digit suffix.
- The production default is generated once per process and reused.
- Tests and fixtures use deterministic values such as
`-RC3000-000000000001`.
Comment thread
josecelano marked this conversation as resolved.

Version source policy:

- Version bytes are hard-coded per release for now.
- The value is updated explicitly when the client versioning policy changes.

Package coupling policy:

- Protocol and server package fixtures do not import tracker-client constants.
- They may define local deterministic constants that follow the same convention.

## Date

2026-05-12

## References

- <https://github.com/torrust/torrust-tracker/issues/1564>
- <https://www.bittorrent.org/beps/bep_0020.html>
- <https://wiki.theory.org/BitTorrentSpecification#peer_id>
- [Issue Spec](../issues/open/1564-tracker-client-change-default-peer-id.md)
1 change: 1 addition & 0 deletions docs/adrs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
| [20240227164834](20240227164834_use_plural_for_modules_containing_collections.md) | 2024-02-27 | Use plural for modules containing collections | Module names should use plural when they contain multiple types with the same responsibility (e.g. `requests/`, `responses/`). |
| [20260420200013](20260420200013_adopt_custom_github_copilot_aligned_agent_framework.md) | 2026-04-20 | Adopt a custom, GitHub-Copilot-aligned agent framework | Use AGENTS.md, Agent Skills, and Custom Agent profiles instead of third-party agent frameworks. |
| [20260429000000](20260429000000_keep_database_as_aggregate_supertrait.md) | 2026-04-29 | Keep `Database` as an aggregate supertrait | Split the 18-method monolithic `Database` trait into four narrow context traits (`SchemaMigrator`, `TorrentMetricsStore`, `WhitelistStore`, `AuthKeyStore`) while keeping `Database` as an empty aggregate supertrait with a blanket impl. |
| [20260512102000](20260512102000_define_tracker_client_peer_id_convention.md) | 2026-05-12 | Define tracker-client peer ID convention | Adopt `-RC3000-` Azureus-style defaults for tracker-client, use a once-per-process randomized production suffix, and keep deterministic `RC` test fixtures without cross-package constant coupling. |
50 changes: 25 additions & 25 deletions docs/issues/open/1564-tracker-client-change-default-peer-id.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,15 @@ The literal `b"-qB00000000000000001"` appears in several places:

## Goals

- [ ] Replace all hard-coded `b"-qB00000000000000001"` peer IDs with a Torrust-specific prefix
- [ ] Define tracker-client constants for deterministic test PeerId and production default generation
- [ ] Update all affected test fixtures so protocol-level tests still pass
- [ ] Add ADR documenting the PeerId convention for production and tests
- [ ] Version bytes are hard-coded per release in tracker-client defaults
- [ ] Production default PeerId suffix is generated once per process run
- [ ] `linter all` exits with code `0`
- [ ] `cargo machete` reports no unused dependencies
- [ ] Existing tests pass
- [x] Replace all hard-coded `b"-qB00000000000000001"` peer IDs with a Torrust-specific prefix
- [x] Define tracker-client constants for deterministic test PeerId and production default generation
- [x] Update all affected test fixtures so protocol-level tests still pass
- [x] Add ADR documenting the PeerId convention for production and tests
- [x] Version bytes are hard-coded per release in tracker-client defaults
- [x] Production default PeerId suffix is generated once per process run
- [x] `linter all` exits with code `0`
- [x] `cargo machete` reports no unused dependencies
- [x] Existing tests pass

## Implementation Plan

Expand Down Expand Up @@ -170,18 +170,18 @@ Create an ADR under `docs/adrs/` documenting:

### Acceptance Verification

| AC ID | Status (`TODO`/`DONE`) | Evidence |
| ----- | ---------------------- | -------- |
| AC1 | TODO | |
| AC2 | TODO | |
| AC3 | TODO | |
| AC4 | TODO | |
| AC5 | TODO | |
| AC6 | TODO | |
| AC7 | TODO | |
| AC8 | TODO | |
| AC9 | TODO | |
| AC10 | TODO | |
| AC ID | Status (`TODO`/`DONE`) | Evidence |
| ----- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| AC1 | DONE | `rg -- '-qB00000000000000001' packages/tracker-client/src console/tracker-client/src` returns no matches |
| AC2 | DONE | `packages/tracker-client/src/peer_id.rs` defines deterministic test constants and production helper |
| AC3 | DONE | HTTP `QueryBuilder::with_default_values` and UDP checker default now call `default_production_peer_id()` |
| AC4 | DONE | Protocol fixtures/docs in `packages/http-protocol/src/v1/{requests/announce.rs,responses/announce.rs,query.rs}` use `-RC3000-...` |
| AC5 | DONE | Added `docs/adrs/20260512102000_define_tracker_client_peer_id_convention.md` and indexed in `docs/adrs/index.md` |
| AC6 | DONE | Hard-coded `-RC3000-` prefix/version in `packages/tracker-client/src/peer_id.rs` |
| AC7 | DONE | `OnceLock` caches process-wide default peer ID in `default_production_peer_id()` |
| AC8 | DONE | `cargo test -p bittorrent-tracker-client`, `cargo test -p torrust-tracker-client`, and `cargo test -p bittorrent-http-tracker-protocol` pass |
| AC9 | DONE | `linter all` passes |
| AC10 | DONE | `cargo machete` reports no unused dependencies |

## Risks and Trade-offs

Expand All @@ -197,21 +197,21 @@ Create an ADR under `docs/adrs/` documenting:
| Field | Value |
| ------------------ | ---------------------------------------------------------------- |
| Type | Enhancement |
| Status | Planned |
| Status | Implemented (pending review) |
| Priority | P3 |
| GitHub Issue | [#1564](https://github.com/torrust/torrust-tracker/issues/1564) |
| Spec Path | `docs/issues/open/1564-tracker-client-change-default-peer-id.md` |
| Branch | `1564-tracker-client-change-default-peer-id` |
| Branch | `1564-change-default-peer-id` |
| Related PR | To be assigned |
| Last Updated (UTC) | 2026-05-12 08:00 |
| Last Updated (UTC) | 2026-05-12 10:25 |

## Progress Tracking

### Workflow Checkpoints

- [ ] Spec drafted in `docs/issues/open/`
- [ ] Spec reviewed and approved by user/maintainer
- [ ] Implementation completed
- [x] Implementation completed
- [ ] Reviewer validated acceptance criteria and updated checkboxes
- [ ] Committer verified spec progress is up to date before commit
- [ ] Issue closed and spec moved from `docs/issues/open/` to `docs/issues/closed/`
Expand Down
4 changes: 2 additions & 2 deletions packages/http-protocol/src/v1/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,15 +229,15 @@ mod tests {
#[test]
fn should_parse_the_query_params_from_an_url_query_string() {
let raw_query =
"info_hash=%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0&peer_id=-qB00000000000000001&port=17548";
"info_hash=%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0&peer_id=-RC3000-000000000001&port=17548";

let query = raw_query.parse::<Query>().unwrap();

assert_eq!(
query.get_param("info_hash").unwrap(),
"%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"
);
assert_eq!(query.get_param("peer_id").unwrap(), "-qB00000000000000001");
assert_eq!(query.get_param("peer_id").unwrap(), "-RC3000-000000000001");
assert_eq!(query.get_param("port").unwrap(), "17548");
}

Expand Down
30 changes: 15 additions & 15 deletions packages/http-protocol/src/v1/requests/announce.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const NUMWANT: &str = "numwant";
/// let request = Announce {
/// // Mandatory params
/// info_hash: "3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0".parse::<InfoHash>().unwrap(),
/// peer_id: PeerId(*b"-qB00000000000000001"),
/// peer_id: PeerId(*b"-RC3000-000000000001"),
/// port: 17548,
/// // Optional params
/// downloaded: Some(NumberOfBytes::new(1)),
Expand Down Expand Up @@ -452,7 +452,7 @@ mod tests {
fn should_be_instantiated_from_the_url_query_with_only_the_mandatory_params() {
let raw_query = Query::from(vec![
(INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
(PEER_ID, "-qB00000000000000001"),
(PEER_ID, "-RC3000-000000000001"),
(PORT, "17548"),
])
.to_string();
Expand All @@ -465,7 +465,7 @@ mod tests {
announce_request,
Announce {
info_hash: "3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0".parse::<InfoHash>().unwrap(), // DevSkim: ignore DS173237
peer_id: PeerId(*b"-qB00000000000000001"),
peer_id: PeerId(*b"-RC3000-000000000001"),
port: 17548,
downloaded: None,
uploaded: None,
Expand All @@ -481,7 +481,7 @@ mod tests {
fn should_be_instantiated_from_the_url_query_params() {
let raw_query = Query::from(vec![
(INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
(PEER_ID, "-qB00000000000000001"),
(PEER_ID, "-RC3000-000000000001"),
(PORT, "17548"),
(DOWNLOADED, "1"),
(UPLOADED, "2"),
Expand All @@ -500,7 +500,7 @@ mod tests {
announce_request,
Announce {
info_hash: "3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0".parse::<InfoHash>().unwrap(), // DevSkim: ignore DS173237
peer_id: PeerId(*b"-qB00000000000000001"),
peer_id: PeerId(*b"-RC3000-000000000001"),
port: 17548,
downloaded: Some(NumberOfBytes::new(1)),
uploaded: Some(NumberOfBytes::new(2)),
Expand All @@ -521,7 +521,7 @@ mod tests {

#[test]
fn it_should_fail_if_the_query_does_not_include_all_the_mandatory_params() {
let raw_query_without_info_hash = "peer_id=-qB00000000000000001&port=17548";
let raw_query_without_info_hash = "peer_id=-RC3000-000000000001&port=17548";

assert!(Announce::try_from(raw_query_without_info_hash.parse::<Query>().unwrap()).is_err());

Expand All @@ -530,7 +530,7 @@ mod tests {
assert!(Announce::try_from(raw_query_without_peer_id.parse::<Query>().unwrap()).is_err());

let raw_query_without_port =
"info_hash=%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0&peer_id=-qB00000000000000001";
"info_hash=%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0&peer_id=-RC3000-000000000001";

assert!(Announce::try_from(raw_query_without_port.parse::<Query>().unwrap()).is_err());
}
Expand All @@ -539,7 +539,7 @@ mod tests {
fn it_should_fail_if_the_info_hash_param_is_invalid() {
let raw_query = Query::from(vec![
(INFO_HASH, "INVALID_INFO_HASH_VALUE"),
(PEER_ID, "-qB00000000000000001"),
(PEER_ID, "-RC3000-000000000001"),
(PORT, "17548"),
])
.to_string();
Expand All @@ -563,7 +563,7 @@ mod tests {
fn it_should_fail_if_the_port_param_is_invalid() {
let raw_query = Query::from(vec![
(INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
(PEER_ID, "-qB00000000000000001"),
(PEER_ID, "-RC3000-000000000001"),
(PORT, "INVALID_PORT_VALUE"),
])
.to_string();
Expand All @@ -575,7 +575,7 @@ mod tests {
fn it_should_fail_if_the_downloaded_param_is_invalid() {
let raw_query = Query::from(vec![
(INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
(PEER_ID, "-qB00000000000000001"),
(PEER_ID, "-RC3000-000000000001"),
(PORT, "17548"),
(DOWNLOADED, "INVALID_DOWNLOADED_VALUE"),
])
Expand All @@ -588,7 +588,7 @@ mod tests {
fn it_should_fail_if_the_uploaded_param_is_invalid() {
let raw_query = Query::from(vec![
(INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
(PEER_ID, "-qB00000000000000001"),
(PEER_ID, "-RC3000-000000000001"),
(PORT, "17548"),
(UPLOADED, "INVALID_UPLOADED_VALUE"),
])
Expand All @@ -601,7 +601,7 @@ mod tests {
fn it_should_fail_if_the_left_param_is_invalid() {
let raw_query = Query::from(vec![
(INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
(PEER_ID, "-qB00000000000000001"),
(PEER_ID, "-RC3000-000000000001"),
(PORT, "17548"),
(LEFT, "INVALID_LEFT_VALUE"),
])
Expand All @@ -614,7 +614,7 @@ mod tests {
fn it_should_fail_if_the_event_param_is_invalid() {
let raw_query = Query::from(vec![
(INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
(PEER_ID, "-qB00000000000000001"),
(PEER_ID, "-RC3000-000000000001"),
(PORT, "17548"),
(EVENT, "INVALID_EVENT_VALUE"),
])
Expand All @@ -627,7 +627,7 @@ mod tests {
fn it_should_fail_if_the_compact_param_is_invalid() {
let raw_query = Query::from(vec![
(INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
(PEER_ID, "-qB00000000000000001"),
(PEER_ID, "-RC3000-000000000001"),
(PORT, "17548"),
(COMPACT, "INVALID_COMPACT_VALUE"),
])
Expand All @@ -640,7 +640,7 @@ mod tests {
fn it_should_fail_if_the_numwant_param_is_invalid() {
let raw_query = Query::from(vec![
(INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
(PEER_ID, "-qB00000000000000001"),
(PEER_ID, "-RC3000-000000000001"),
(PORT, "17548"),
(NUMWANT, "-1"),
])
Expand Down
8 changes: 4 additions & 4 deletions packages/http-protocol/src/v1/responses/announce.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ impl Into<Vec<u8>> for Compact {
/// use bittorrent_http_tracker_protocol::v1::responses::announce::{Normal, NormalPeer};
///
/// let peer = NormalPeer {
/// peer_id: *b"-qB00000000000000001",
/// peer_id: *b"-RC3000-000000000001",
/// ip: IpAddr::V4(Ipv4Addr::new(0x69, 0x69, 0x69, 0x69)), // 105.105.105.105
/// port: 0x7070, // 28784
/// };
Expand Down Expand Up @@ -300,12 +300,12 @@ mod tests {
let policy = AnnouncePolicy::new(111, 222);

let peer_ipv4 = PeerBuilder::default()
.with_peer_id(&PeerId(*b"-qB00000000000000001"))
.with_peer_id(&PeerId(*b"-RC3000-000000000001"))
.with_peer_addr(&SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0x69, 0x69, 0x69, 0x69)), 0x7070))
.build();

let peer_ipv6 = PeerBuilder::default()
.with_peer_id(&PeerId(*b"-qB00000000000000002"))
.with_peer_id(&PeerId(*b"-RC3000-000000000002"))
.with_peer_addr(&SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969)),
0x7070,
Expand All @@ -324,7 +324,7 @@ mod tests {
let bytes = response.data.into();

// cspell:disable-next-line
let expected_bytes = b"d8:completei333e10:incompletei444e8:intervali111e12:min intervali222e5:peersld2:ip15:105.105.105.1057:peer id20:-qB000000000000000014:porti28784eed2:ip39:6969:6969:6969:6969:6969:6969:6969:69697:peer id20:-qB000000000000000024:porti28784eeee";
let expected_bytes = b"d8:completei333e10:incompletei444e8:intervali111e12:min intervali222e5:peersld2:ip15:105.105.105.1057:peer id20:-RC3000-0000000000014:porti28784eed2:ip39:6969:6969:6969:6969:6969:6969:6969:69697:peer id20:-RC3000-0000000000024:porti28784eeee";

assert_eq!(
String::from_utf8(bytes).unwrap(),
Expand Down
3 changes: 2 additions & 1 deletion packages/tracker-client/src/http/client/requests/announce.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use bittorrent_udp_tracker_protocol::PeerId;
use serde_repr::Serialize_repr;

use crate::http::{percent_encode_byte_array, ByteArray20};
use crate::peer_id::default_production_peer_id;

pub struct Query {
pub info_hash: ByteArray20,
Expand Down Expand Up @@ -99,7 +100,7 @@ impl QueryBuilder {
peer_addr: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 88)),
downloaded: 0,
uploaded: 0,
peer_id: PeerId(*b"-qB00000000000000001").0,
peer_id: default_production_peer_id().0,
port: 17548,
left: 0,
event: Some(Event::Started),
Expand Down
1 change: 1 addition & 0 deletions packages/tracker-client/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod http;
pub mod peer_id;
pub mod udp;
Loading
Loading