44//! **Extended Channel** within a mining client.
55
66extern crate alloc;
7- use super :: HashMap ;
7+ use super :: { HashMap , MAX_FUTURE_JOBS } ;
88use crate :: {
99 bip141:: try_strip_bip141,
1010 chain_tip:: ChainTip ,
@@ -17,7 +17,7 @@ use crate::{
1717 target:: { bytes_to_hex, u256_to_block_hash} ,
1818 MAX_EXTRANONCE_LEN , VERSION_ROLLING_MASK ,
1919} ;
20- use alloc:: { format, string:: String , vec, vec:: Vec } ;
20+ use alloc:: { collections :: VecDeque , format, string:: String , vec, vec:: Vec } ;
2121use binary_sv2:: Sv2OptionOwned ;
2222use bitcoin:: {
2323 absolute:: LockTime ,
@@ -57,8 +57,8 @@ pub type ExtendedJob = (NewExtendedMiningJobOwned, Vec<u8>, Target);
5757/// - The channel's current target.
5858/// - The channel's nominal hashrate.
5959/// - Whether version rolling is supported (see [BIP 323](https://github.com/bitcoin/bips/blob/master/bip-0323.mediawiki)).
60- /// - Future jobs (indexed by `job_id`) to be activated by a [`SetNewPrevHash`](SetNewPrevHashMp)
61- /// message.
60+ /// - Future jobs (indexed by `job_id`, capped at [`MAX_FUTURE_JOBS`] ) to be activated by a
61+ /// [`SetNewPrevHash`](SetNewPrevHashMp) message.
6262/// - The currently active job.
6363/// - Past jobs (previously active under the current chain tip, indexed by `job_id`).
6464/// - Stale jobs (previously active and past jobs under the previous chain tip, indexed by
@@ -76,6 +76,9 @@ pub struct ExtendedChannel {
7676 version_rolling : bool ,
7777 // future jobs are indexed with job_id (u32)
7878 future_jobs : HashMap < u32 , ExtendedJob > ,
79+ // Future job IDs ordered by receipt, oldest at the front and newest at the back.
80+ // Replaced IDs move to the back; overflow evicts from the front.
81+ future_job_order : VecDeque < u32 > ,
7982 active_job : Option < ExtendedJob > ,
8083 // past jobs are indexed with job_id (u32)
8184 past_jobs : HashMap < u32 , ExtendedJob > ,
@@ -105,6 +108,7 @@ impl ExtendedChannel {
105108 nominal_hashrate,
106109 version_rolling,
107110 future_jobs : HashMap :: new ( ) ,
111+ future_job_order : VecDeque :: new ( ) ,
108112 active_job : None ,
109113 past_jobs : HashMap :: new ( ) ,
110114 stale_jobs : HashMap :: new ( ) ,
@@ -221,6 +225,8 @@ impl ExtendedChannel {
221225 }
222226
223227 /// Returns an iterator over all future jobs for this channel.
228+ ///
229+ /// At most [`MAX_FUTURE_JOBS`] jobs are kept (oldest evicted first).
224230 pub fn get_future_jobs ( & self ) -> impl Iterator < Item = ( & u32 , & ExtendedJob ) > + ' _ {
225231 self . future_jobs . iter ( )
226232 }
@@ -293,6 +299,8 @@ impl ExtendedChannel {
293299 ///
294300 /// - If [`NewExtendedMiningJob::min_ntime`](mining_sv2::NewExtendedMiningJob::min_ntime) is empty, the job is considered a future job and
295301 /// added to the future jobs list (see [`get_future_jobs`](ExtendedChannel::get_future_jobs)).
302+ /// At most [`MAX_FUTURE_JOBS`] future jobs are kept: storing a new one beyond that limit
303+ /// evicts the oldest.
296304 /// - Otherwise, the job is activated and previous active job moves to the past jobs list.
297305 pub fn on_new_extended_mining_job (
298306 & mut self ,
@@ -331,14 +339,25 @@ impl ExtendedChannel {
331339 ) ) ;
332340 }
333341 None => {
342+ let job_id = new_extended_mining_job. job_id ;
334343 self . future_jobs . insert (
335- new_extended_mining_job . job_id ,
344+ job_id,
336345 (
337346 new_extended_mining_job,
338347 self . extranonce_prefix . as_bytes ( ) . to_vec ( ) ,
339348 self . target ,
340349 ) ,
341350 ) ;
351+
352+ // a replaced job_id moves to the back of the eviction order
353+ self . future_job_order . retain ( |id| * id != job_id) ;
354+ self . future_job_order . push_back ( job_id) ;
355+
356+ if self . future_jobs . len ( ) > MAX_FUTURE_JOBS {
357+ if let Some ( evicted_job_id) = self . future_job_order . pop_front ( ) {
358+ self . future_jobs . remove ( & evicted_job_id) ;
359+ }
360+ }
342361 }
343362 }
344363
@@ -467,6 +486,7 @@ impl ExtendedChannel {
467486
468487 // all other future jobs are now useless
469488 self . future_jobs . clear ( ) ;
489+ self . future_job_order . clear ( ) ;
470490
471491 // mark all past jobs as stale, so that shares are not propagated
472492 self . stale_jobs = self . past_jobs . clone ( ) ;
@@ -505,6 +525,7 @@ impl ExtendedChannel {
505525
506526 // all other future jobs are now useless
507527 self . future_jobs . clear ( ) ;
528+ self . future_job_order . clear ( ) ;
508529
509530 // mark all past jobs as stale, so that shares are not propagated
510531 self . stale_jobs = self . past_jobs . clone ( ) ;
@@ -709,6 +730,7 @@ mod tests {
709730 client:: {
710731 extended:: ExtendedChannel ,
711732 share_accounting:: { ShareValidationError , ShareValidationResult } ,
733+ MAX_FUTURE_JOBS ,
712734 } ,
713735 extranonce_manager:: ExtranoncePrefix ,
714736 } ;
@@ -807,6 +829,145 @@ mod tests {
807829 ) ;
808830 }
809831
832+ #[ test]
833+ fn test_future_jobs_are_bounded ( ) {
834+ let channel_id = 1 ;
835+ let extranonce_prefix = [
836+ 83 , 116 , 114 , 97 , 116 , 117 , 109 , 32 , 86 , 50 , 32 , 83 , 82 , 73 , 32 , 80 , 111 , 111 , 108 , 0 ,
837+ 0 , 0 , 0 , 0 , 0 , 0 , 1 ,
838+ ]
839+ . to_vec ( ) ;
840+
841+ let mut channel = ExtendedChannel :: new (
842+ channel_id,
843+ "user_identity" . to_string ( ) ,
844+ ExtranoncePrefix :: from_wire ( extranonce_prefix) . unwrap ( ) ,
845+ Target :: from_le_bytes ( [ 0xff ; 32 ] ) ,
846+ 1.0 ,
847+ true ,
848+ 4u16 ,
849+ ) ;
850+
851+ let future_job = NewExtendedMiningJob {
852+ channel_id,
853+ job_id : 0 ,
854+ min_ntime : Sv2Option :: new ( None ) ,
855+ version : 536870912 ,
856+ version_rolling_allowed : true ,
857+ coinbase_tx_prefix : vec ! [
858+ 2 , 0 , 0 , 0 , 1 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
859+ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 255 , 255 , 255 , 255 , 34 , 82 , 0 ,
860+ ]
861+ . try_into ( )
862+ . unwrap ( ) ,
863+ coinbase_tx_suffix : vec ! [
864+ 255 , 255 , 255 , 255 , 2 , 0 , 242 , 5 , 42 , 1 , 0 , 0 , 0 , 22 , 0 , 20 , 235 , 225 , 183 , 220 ,
865+ 194 , 147 , 204 , 170 , 14 , 231 , 67 , 168 , 111 , 137 , 223 , 130 , 88 , 194 , 8 , 252 , 0 , 0 , 0 ,
866+ 0 , 0 , 0 , 0 , 0 , 38 , 106 , 36 , 170 , 33 , 169 , 237 , 226 , 246 , 28 , 63 , 113 , 209 , 222 ,
867+ 253 , 63 , 169 , 153 , 223 , 163 , 105 , 83 , 117 , 92 , 105 , 6 , 137 , 121 , 153 , 98 , 180 , 139 ,
868+ 235 , 216 , 54 , 151 , 78 , 140 , 249 , 0 , 0 , 0 , 0 ,
869+ ]
870+ . try_into ( )
871+ . unwrap ( ) ,
872+ merkle_path : vec ! [ ] . try_into ( ) . unwrap ( ) ,
873+ } ;
874+
875+ let flood_size = 10_000u32 ;
876+ for job_id in 0 ..flood_size {
877+ let mut job = future_job. clone ( ) ;
878+ job. job_id = job_id;
879+ channel. on_new_extended_mining_job ( job) . unwrap ( ) ;
880+ }
881+
882+ assert_eq ! ( channel. get_future_jobs_count( ) , MAX_FUTURE_JOBS ) ;
883+
884+ for job_id in 0 ..flood_size - MAX_FUTURE_JOBS as u32 {
885+ assert ! ( channel. get_future_job( job_id) . is_none( ) ) ;
886+ }
887+ for job_id in flood_size - MAX_FUTURE_JOBS as u32 ..flood_size {
888+ assert ! ( channel. get_future_job( job_id) . is_some( ) ) ;
889+ }
890+ }
891+
892+ #[ test]
893+ fn test_replaced_future_job_moves_to_back_of_eviction_order ( ) {
894+ let channel_id = 1 ;
895+ let extranonce_prefix = [
896+ 83 , 116 , 114 , 97 , 116 , 117 , 109 , 32 , 86 , 50 , 32 , 83 , 82 , 73 , 32 , 80 , 111 , 111 , 108 , 0 ,
897+ 0 , 0 , 0 , 0 , 0 , 0 , 1 ,
898+ ]
899+ . to_vec ( ) ;
900+
901+ let mut channel = ExtendedChannel :: new (
902+ channel_id,
903+ "user_identity" . to_string ( ) ,
904+ ExtranoncePrefix :: from_wire ( extranonce_prefix) . unwrap ( ) ,
905+ Target :: from_le_bytes ( [ 0xff ; 32 ] ) ,
906+ 1.0 ,
907+ true ,
908+ 4u16 ,
909+ ) ;
910+
911+ let future_job = NewExtendedMiningJob {
912+ channel_id,
913+ job_id : 0 ,
914+ min_ntime : Sv2Option :: new ( None ) ,
915+ version : 536870912 ,
916+ version_rolling_allowed : true ,
917+ coinbase_tx_prefix : vec ! [
918+ 2 , 0 , 0 , 0 , 1 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
919+ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 255 , 255 , 255 , 255 , 34 , 82 , 0 ,
920+ ]
921+ . try_into ( )
922+ . unwrap ( ) ,
923+ coinbase_tx_suffix : vec ! [
924+ 255 , 255 , 255 , 255 , 2 , 0 , 242 , 5 , 42 , 1 , 0 , 0 , 0 , 22 , 0 , 20 , 235 , 225 , 183 , 220 ,
925+ 194 , 147 , 204 , 170 , 14 , 231 , 67 , 168 , 111 , 137 , 223 , 130 , 88 , 194 , 8 , 252 , 0 , 0 , 0 ,
926+ 0 , 0 , 0 , 0 , 0 , 38 , 106 , 36 , 170 , 33 , 169 , 237 , 226 , 246 , 28 , 63 , 113 , 209 , 222 ,
927+ 253 , 63 , 169 , 153 , 223 , 163 , 105 , 83 , 117 , 92 , 105 , 6 , 137 , 121 , 153 , 98 , 180 , 139 ,
928+ 235 , 216 , 54 , 151 , 78 , 140 , 249 , 0 , 0 , 0 , 0 ,
929+ ]
930+ . try_into ( )
931+ . unwrap ( ) ,
932+ merkle_path : vec ! [ ] . try_into ( ) . unwrap ( ) ,
933+ } ;
934+
935+ // fill the store with MAX_FUTURE_JOBS distinct job_ids
936+ for job_id in 0 ..MAX_FUTURE_JOBS as u32 {
937+ let mut job = future_job. clone ( ) ;
938+ job. job_id = job_id;
939+ channel. on_new_extended_mining_job ( job) . unwrap ( ) ;
940+ }
941+
942+ // re-send job_id 0: it should move to the back of the eviction order
943+ channel
944+ . on_new_extended_mining_job ( future_job. clone ( ) )
945+ . unwrap ( ) ;
946+
947+ // one more distinct job_id: job_id 1 is now the oldest and gets evicted
948+ let mut job = future_job. clone ( ) ;
949+ job. job_id = MAX_FUTURE_JOBS as u32 ;
950+ channel. on_new_extended_mining_job ( job) . unwrap ( ) ;
951+
952+ assert_eq ! ( channel. get_future_jobs_count( ) , MAX_FUTURE_JOBS ) ;
953+ assert ! ( channel. get_future_job( 1 ) . is_none( ) ) ;
954+ assert ! ( channel. get_future_job( 0 ) . is_some( ) ) ;
955+
956+ // the replaced job_id can still be activated
957+ let set_new_prev_hash = SetNewPrevHashMp {
958+ channel_id,
959+ job_id : 0 ,
960+ prev_hash : [
961+ 200 , 53 , 253 , 129 , 214 , 31 , 43 , 84 , 179 , 58 , 58 , 76 , 128 , 213 , 24 , 53 , 38 , 144 ,
962+ 205 , 88 , 172 , 20 , 251 , 22 , 217 , 141 , 21 , 221 , 21 , 0 , 0 , 0 ,
963+ ]
964+ . into ( ) ,
965+ nbits : 503543726 ,
966+ min_ntime : 1746839905 ,
967+ } ;
968+ channel. on_set_new_prev_hash ( set_new_prev_hash) . unwrap ( ) ;
969+ }
970+
810971 #[ test]
811972 fn test_past_jobs_flow ( ) {
812973 let channel_id = 1 ;
0 commit comments