Skip to content

Commit b199324

Browse files
Eric Priceclaude
andcommitted
channels_sv2: address review on the configurable past-jobs cap
- Test the client `StandardChannel` override. That channel keeps its own `past_jobs`/`past_job_order` and eviction path rather than delegating to `JobStore`, so it was the one place `max_past_jobs` could regress unnoticed: the override was covered for the client extended channel and for the server via `JobStore`, but not here. - Factor the resolver. `max_past_jobs.map(NonZeroUsize::get).unwrap_or(...)` was copy-pasted into four constructors; each side now routes through a `resolve_max_past_jobs` helper colocated with its own default, so changing a default touches one line rather than four. - `debug_assert!(max_past_jobs > 0)` in `JobStore::new`. The nonzero guarantee lived only in the callers; this documents and enforces it internally without changing the signature. - Rename the new tests off "honour" — the crate uses American spellings throughout. - Make the client `MAX_PAST_JOBS` doc links explicit (`super::MAX_PAST_JOBS`) so they no longer depend on an import that the resolver made otherwise unused. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 7979601 commit b199324

6 files changed

Lines changed: 104 additions & 34 deletions

File tree

sv2/channels-sv2/src/client/extended.rs

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
//! **Extended Channel** within a mining client.
55
66
extern crate alloc;
7-
use super::{HashMap, MAX_FUTURE_JOBS, MAX_PAST_JOBS};
7+
use super::{resolve_max_past_jobs, HashMap, MAX_FUTURE_JOBS};
88
use crate::{
99
bip141::try_strip_bip141,
1010
chain_tip::ChainTip,
@@ -62,7 +62,7 @@ pub type ExtendedJob = (NewExtendedMiningJobOwned, Vec<u8>, Target);
6262
/// [`SetNewPrevHash`](SetNewPrevHashMp) message.
6363
/// - The currently active job.
6464
/// - Past jobs (previously active under the current chain tip, indexed by `job_id`, capped at
65-
/// [`MAX_PAST_JOBS`]).
65+
/// [`MAX_PAST_JOBS`](super::MAX_PAST_JOBS)).
6666
/// - Stale jobs (previously active and past jobs under the previous chain tip, indexed by
6767
/// `job_id`).
6868
/// - Share accounting for the channel (as tracked by the client).
@@ -100,7 +100,7 @@ impl ExtendedChannel {
100100
/// Constructs a new [`ExtendedChannel`].
101101
///
102102
/// `max_past_jobs` caps the past jobs retained under the current chain tip; `None` uses
103-
/// [`MAX_PAST_JOBS`].
103+
/// [`MAX_PAST_JOBS`](super::MAX_PAST_JOBS).
104104
#[allow(clippy::too_many_arguments)]
105105
pub fn new(
106106
channel_id: u32,
@@ -126,9 +126,7 @@ impl ExtendedChannel {
126126
past_jobs: HashMap::new(),
127127
past_job_order: VecDeque::new(),
128128
stale_jobs: HashMap::new(),
129-
max_past_jobs: max_past_jobs
130-
.map(NonZeroUsize::get)
131-
.unwrap_or(MAX_PAST_JOBS),
129+
max_past_jobs: resolve_max_past_jobs(max_past_jobs),
132130
share_accounting: ShareAccounting::new(),
133131
chain_tip: None,
134132
}
@@ -260,7 +258,7 @@ impl ExtendedChannel {
260258

261259
/// Returns an iterator over all past jobs for this channel.
262260
///
263-
/// At most [`MAX_PAST_JOBS`] jobs are kept (oldest evicted first).
261+
/// At most [`MAX_PAST_JOBS`](super::MAX_PAST_JOBS) jobs are kept (oldest evicted first).
264262
pub fn get_past_jobs(&self) -> impl Iterator<Item = (&u32, &ExtendedJob)> + '_ {
265263
self.past_jobs.iter()
266264
}
@@ -272,7 +270,7 @@ impl ExtendedChannel {
272270

273271
/// Returns the number of past jobs tracked by this channel.
274272
///
275-
/// At most [`MAX_PAST_JOBS`] jobs are kept (oldest evicted first).
273+
/// At most [`MAX_PAST_JOBS`](super::MAX_PAST_JOBS) jobs are kept (oldest evicted first).
276274
pub fn get_past_jobs_count(&self) -> usize {
277275
self.past_jobs.len()
278276
}
@@ -323,7 +321,7 @@ impl ExtendedChannel {
323321
/// At most [`MAX_FUTURE_JOBS`] future jobs are kept: storing a new one beyond that limit
324322
/// evicts the oldest.
325323
/// - Otherwise, the job is activated and previous active job moves to the past jobs list.
326-
/// At most [`MAX_PAST_JOBS`] past jobs are kept: retiring one beyond that limit evicts the
324+
/// At most [`MAX_PAST_JOBS`](super::MAX_PAST_JOBS) past jobs are kept: retiring one beyond that limit evicts the
327325
/// oldest.
328326
pub fn on_new_extended_mining_job(
329327
&mut self,
@@ -390,7 +388,7 @@ impl ExtendedChannel {
390388
/// Handles a `SetCustomMiningJobSuccess` message from upstream.
391389
/// Requires the corresponding `SetCustomMiningJob`.
392390
///
393-
/// The previous active job (if any) moves to the past jobs list. At most [`MAX_PAST_JOBS`]
391+
/// The previous active job (if any) moves to the past jobs list. At most [`MAX_PAST_JOBS`](super::MAX_PAST_JOBS)
394392
/// past jobs are kept: retiring one beyond that limit evicts the oldest.
395393
///
396394
/// To be used by a Sv2 Job Declarator Client
@@ -1112,7 +1110,7 @@ mod tests {
11121110
}
11131111

11141112
#[test]
1115-
fn test_past_jobs_honour_constructor_override() {
1113+
fn test_past_jobs_respect_constructor_override() {
11161114
// Some(cap) must override MAX_PAST_JOBS all the way through to the eviction path.
11171115
let custom_cap = NonZeroUsize::new(3).unwrap();
11181116
assert!(custom_cap.get() < MAX_PAST_JOBS);

sv2/channels-sv2/src/client/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ pub const MAX_FUTURE_JOBS: usize = 16;
3939
/// `max_past_jobs: Option<NonZeroUsize>` and fall back to this value when passed `None`.
4040
pub const MAX_PAST_JOBS: usize = 50;
4141

42+
/// Resolves a caller-supplied past-jobs cap against [`MAX_PAST_JOBS`].
43+
///
44+
/// Keeps the default referenced in one place, so changing it does not mean touching every
45+
/// channel constructor.
46+
pub(crate) fn resolve_max_past_jobs(max_past_jobs: Option<core::num::NonZeroUsize>) -> usize {
47+
max_past_jobs
48+
.map(core::num::NonZeroUsize::get)
49+
.unwrap_or(MAX_PAST_JOBS)
50+
}
51+
4252
// Type aliases that switch between `std::collections` and `hashbrown`
4353
// depending on whether the `no_std` feature is enabled.
4454
#[cfg(not(feature = "no_std"))]

sv2/channels-sv2/src/client/standard.rs

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
//! and chain tip state, enabling share validation and mining job lifecycle management.
66
77
extern crate alloc;
8-
use super::{HashMap, MAX_FUTURE_JOBS, MAX_PAST_JOBS};
8+
use super::{resolve_max_past_jobs, HashMap, MAX_FUTURE_JOBS};
99
use crate::{
1010
chain_tip::ChainTip,
1111
client::{
@@ -48,7 +48,7 @@ pub type StandardJob = (NewMiningJobOwned, Target);
4848
/// - future mining jobs (indexed by job_id, activated upon [`NewMiningJob`](mining_sv2::NewMiningJob) receipt, capped at [`MAX_FUTURE_JOBS`])
4949
/// - active mining job
5050
/// - past jobs (active jobs under current chain tip, indexed by job_id, capped at
51-
/// [`MAX_PAST_JOBS`])
51+
/// [`MAX_PAST_JOBS`](super::MAX_PAST_JOBS))
5252
/// - stale jobs (jobs from previous chain tip, indexed by job_id)
5353
/// - share accounting state
5454
/// - chain tip state
@@ -80,7 +80,7 @@ impl StandardChannel {
8080
/// Creates a new [`StandardChannel`] instance with provided channel parameters.
8181
///
8282
/// `max_past_jobs` caps the past jobs retained under the current chain tip; `None` uses
83-
/// [`MAX_PAST_JOBS`].
83+
/// [`MAX_PAST_JOBS`](super::MAX_PAST_JOBS).
8484
pub fn new(
8585
channel_id: u32,
8686
user_identity: String,
@@ -101,9 +101,7 @@ impl StandardChannel {
101101
past_jobs: HashMap::new(),
102102
past_job_order: VecDeque::new(),
103103
stale_jobs: HashMap::new(),
104-
max_past_jobs: max_past_jobs
105-
.map(NonZeroUsize::get)
106-
.unwrap_or(MAX_PAST_JOBS),
104+
max_past_jobs: resolve_max_past_jobs(max_past_jobs),
107105
share_accounting: ShareAccounting::new(),
108106
chain_tip: None,
109107
}
@@ -213,7 +211,7 @@ impl StandardChannel {
213211

214212
/// Returns an iterator over all past jobs for the channel (active jobs under current chain tip).
215213
///
216-
/// At most [`MAX_PAST_JOBS`] jobs are kept (oldest evicted first).
214+
/// At most [`MAX_PAST_JOBS`](super::MAX_PAST_JOBS) jobs are kept (oldest evicted first).
217215
pub fn get_past_jobs(&self) -> impl Iterator<Item = (&u32, &StandardJob)> + '_ {
218216
self.past_jobs.iter()
219217
}
@@ -225,7 +223,7 @@ impl StandardChannel {
225223

226224
/// Returns the number of past jobs tracked by this channel.
227225
///
228-
/// At most [`MAX_PAST_JOBS`] jobs are kept (oldest evicted first).
226+
/// At most [`MAX_PAST_JOBS`](super::MAX_PAST_JOBS) jobs are kept (oldest evicted first).
229227
pub fn get_past_jobs_count(&self) -> usize {
230228
self.past_jobs.len()
231229
}
@@ -307,7 +305,7 @@ impl StandardChannel {
307305
/// - If `min_ntime` is empty, the job is added to future jobs. At most [`MAX_FUTURE_JOBS`]
308306
/// future jobs are kept: storing a new one beyond that limit evicts the oldest.
309307
/// - If an active job exists, it is moved to past jobs on activation. At most
310-
/// [`MAX_PAST_JOBS`] past jobs are kept: retiring one beyond that limit evicts the oldest.
308+
/// [`MAX_PAST_JOBS`](super::MAX_PAST_JOBS) past jobs are kept: retiring one beyond that limit evicts the oldest.
311309
pub fn on_new_mining_job(&mut self, new_mining_job: NewMiningJobOwned) {
312310
self.store_new_mining_job(new_mining_job);
313311
}
@@ -568,6 +566,7 @@ mod tests {
568566
};
569567
use binary_sv2::Sv2OptionOwned as Sv2Option;
570568
use bitcoin::Target;
569+
use core::num::NonZeroUsize;
571570
use mining_sv2::{
572571
NewExtendedMiningJobOwned as NewExtendedMiningJob, NewMiningJobOwned as NewMiningJob,
573572
SetNewPrevHashOwned as SetNewPrevHashMp, SubmitSharesStandardOwned,
@@ -749,6 +748,60 @@ mod tests {
749748
channel.on_set_new_prev_hash(set_new_prev_hash).unwrap();
750749
}
751750

751+
#[test]
752+
fn test_past_jobs_respect_constructor_override() {
753+
// Some(cap) must override MAX_PAST_JOBS on the client standard channel's own eviction
754+
// path, which keeps its past jobs directly rather than delegating to a JobStore.
755+
let custom_cap = NonZeroUsize::new(3).unwrap();
756+
assert!(custom_cap.get() < MAX_PAST_JOBS);
757+
758+
let channel_id = 1;
759+
let extranonce_prefix = [
760+
83, 116, 114, 97, 116, 117, 109, 32, 86, 50, 32, 83, 82, 73, 32, 80, 111, 111, 108, 0,
761+
0, 0, 0, 0, 0, 0, 1,
762+
]
763+
.to_vec();
764+
765+
let mut channel = StandardChannel::new(
766+
channel_id,
767+
"user_identity".to_string(),
768+
ExtranoncePrefix::from_wire(extranonce_prefix).unwrap(),
769+
Target::from_le_bytes([0xff; 32]),
770+
1.0,
771+
Some(custom_cap),
772+
);
773+
774+
let active_job = NewMiningJob {
775+
channel_id,
776+
job_id: 0,
777+
merkle_root: [
778+
189, 200, 25, 246, 119, 73, 34, 42, 209, 112, 237, 50, 169, 71, 163, 192, 24, 84,
779+
56, 86, 147, 71, 243, 44, 18, 107, 167, 169, 169, 66, 186, 98,
780+
]
781+
.into(),
782+
version: 536870912,
783+
min_ntime: Sv2Option::new(Some(1746839905)),
784+
};
785+
786+
let job_count = 20u32;
787+
for job_id in 0..job_count {
788+
let mut job = active_job.clone();
789+
job.job_id = job_id;
790+
channel.on_new_mining_job(job);
791+
}
792+
793+
// bounded by the override, not by MAX_PAST_JOBS
794+
assert_eq!(channel.get_past_jobs_count(), custom_cap.get());
795+
796+
// the last job is active; only the newest `custom_cap` retired jobs survive
797+
for job_id in 0..job_count - 1 - custom_cap.get() as u32 {
798+
assert!(channel.get_past_job(job_id).is_none());
799+
}
800+
for job_id in job_count - 1 - custom_cap.get() as u32..job_count - 1 {
801+
assert!(channel.get_past_job(job_id).is_some());
802+
}
803+
}
804+
752805
#[test]
753806
fn test_past_jobs_are_bounded() {
754807
let channel_id = 1;

sv2/channels-sv2/src/server/extended.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ use crate::{
4949
jobs::{
5050
extended::ExtendedJob,
5151
factory::JobFactory,
52-
job_store::{JobStore, MAX_PAST_JOBS},
52+
job_store::{resolve_max_past_jobs, JobStore},
5353
JobOrigin,
5454
},
5555
share_accounting::{ShareAccounting, ShareValidationError, ShareValidationResult},
@@ -263,11 +263,7 @@ impl ExtendedChannel {
263263
job_id_to_target: HashMap::new(),
264264
nominal_hashrate,
265265
stable_hashrate: false,
266-
job_store: JobStore::new(
267-
max_past_jobs
268-
.map(NonZeroUsize::get)
269-
.unwrap_or(MAX_PAST_JOBS),
270-
),
266+
job_store: JobStore::new(resolve_max_past_jobs(max_past_jobs)),
271267
job_factory,
272268
share_accounting: ShareAccounting::new(share_batch_size),
273269
expected_share_per_minute,

sv2/channels-sv2/src/server/jobs/job_store.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
//! - **Retired Extranonce Prefixes**: Holds on to extranonce prefixes that were rotated out of the
1111
//! channel while jobs created under them can still accept shares, so that their allocator slots
1212
//! are not handed to another channel too early.
13-
use std::collections::{HashMap, VecDeque};
13+
use std::{
14+
collections::{HashMap, VecDeque},
15+
num::NonZeroUsize,
16+
};
1417

1518
use super::Job;
1619
use crate::extranonce_manager::ExtranoncePrefix;
@@ -41,6 +44,16 @@ pub(crate) const MAX_FUTURE_JOBS: usize = 16;
4144
/// override and fall back to this value when none is given.
4245
pub(crate) const MAX_PAST_JOBS: usize = 50;
4346

47+
/// Resolves a caller-supplied past-jobs cap against `MAX_PAST_JOBS`.
48+
///
49+
/// Keeps the default referenced in one place, so changing it does not mean touching every
50+
/// channel constructor.
51+
pub(crate) fn resolve_max_past_jobs(max_past_jobs: Option<NonZeroUsize>) -> usize {
52+
max_past_jobs
53+
.map(NonZeroUsize::get)
54+
.unwrap_or(MAX_PAST_JOBS)
55+
}
56+
4457
/// Internal implementation for tracking mining job states in SV2 server channels.
4558
///
4659
/// Maintains collections for future, active, past, and stale jobs, and tracks template-to-job ID
@@ -74,6 +87,10 @@ impl<T: Job> JobStore<T> {
7487
/// Creates a new empty job store retaining at most `max_past_jobs` past jobs under the
7588
/// current chain tip.
7689
pub fn new(max_past_jobs: usize) -> Self {
90+
// callers resolve an `Option<NonZeroUsize>` via `resolve_max_past_jobs`, so a zero cap
91+
// cannot arrive here; a zero cap would evict the just-retired job immediately and reject
92+
// the most common late share as `InvalidJobId`
93+
debug_assert!(max_past_jobs > 0, "max_past_jobs must be nonzero");
7794
Self {
7895
future_template_to_job_id: HashMap::new(),
7996
future_template_order: VecDeque::new(),
@@ -411,7 +428,7 @@ mod tests {
411428
}
412429

413430
#[test]
414-
fn past_jobs_honour_a_custom_cap() {
431+
fn past_jobs_respect_a_custom_cap() {
415432
// a store built with a cap below the default must evict against that cap, not the default
416433
let custom_cap = 2;
417434
assert!(custom_cap < MAX_PAST_JOBS);

sv2/channels-sv2/src/server/standard.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ use crate::{
4141
jobs::{
4242
extended::ExtendedJob,
4343
factory::JobFactory,
44-
job_store::{JobStore, MAX_PAST_JOBS},
44+
job_store::{resolve_max_past_jobs, JobStore},
4545
standard::StandardJob,
4646
},
4747
share_accounting::{ShareAccounting, ShareValidationError, ShareValidationResult},
@@ -246,11 +246,7 @@ impl StandardChannel {
246246
stable_hashrate: false,
247247
share_accounting: ShareAccounting::new(share_batch_size),
248248
expected_share_per_minute,
249-
job_store: JobStore::new(
250-
max_past_jobs
251-
.map(NonZeroUsize::get)
252-
.unwrap_or(MAX_PAST_JOBS),
253-
),
249+
job_store: JobStore::new(resolve_max_past_jobs(max_past_jobs)),
254250
job_factory,
255251
chain_tip: None,
256252
})

0 commit comments

Comments
 (0)