committee.rs
1 // Copyright (c) 2019-2025 Alpha-Delta Network Inc. 2 // This file is part of the deltavm library. 3 4 // Licensed under the Apache License, Version 2.0 (the "License"); 5 // you may not use this file except in compliance with the License. 6 // You may obtain a copy of the License at: 7 8 // http://www.apache.org/licenses/LICENSE-2.0 9 10 // Unless required by applicable law or agreed to in writing, software 11 // distributed under the License is distributed on an "AS IS" BASIS, 12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 16 #![allow(clippy::redundant_closure)] 17 18 use deltavm_ledger_committee::Committee; 19 use console::{ 20 account::Address, 21 network::Network, 22 prelude::{cfg_into_iter, cfg_iter, cfg_reduce}, 23 program::{Identifier, Literal, Plaintext, Value}, 24 types::{Boolean, U8, U64}, 25 }; 26 27 use anyhow::{Result, bail, ensure}; 28 use indexmap::{IndexMap, indexmap}; 29 use std::str::FromStr; 30 31 #[cfg(not(feature = "serial"))] 32 use rayon::prelude::*; 33 34 /// Returns the committee given the committee map from finalize storage. 35 pub fn committee_and_delegated_maps_into_committee<N: Network>( 36 starting_round: u64, 37 committee_map: Vec<(Plaintext<N>, Value<N>)>, 38 delegated_map: Vec<(Plaintext<N>, Value<N>)>, 39 ) -> Result<Committee<N>> { 40 // Prepare the identifiers. 41 let is_open_identifier: Identifier<N> = Identifier::from_str("is_open")?; 42 let commission_identifier: Identifier<N> = Identifier::from_str("commission")?; 43 44 // Extract the committee members. 45 let committee_members: IndexMap<Address<N>, (u64, bool, u8)> = committee_map 46 .iter() 47 .map(|(key, value)| { 48 // Extract the address from the key. 49 let address = match key { 50 Plaintext::Literal(Literal::Address(address), _) => address, 51 _ => bail!("Invalid committee key (missing address) - {key}"), 52 }; 53 54 // Extract the committee state from the value. 55 let (is_open, commission) = match value { 56 Value::Plaintext(Plaintext::Struct(state, _)) => { 57 // Extract the is_open flag from the value. 58 let is_open = match state.get(&is_open_identifier) { 59 Some(Plaintext::Literal(Literal::Boolean(is_open), _)) => **is_open, 60 _ => bail!("Invalid committee state (missing boolean) - {value}"), 61 }; 62 // Extract the commission from the value. 63 let commission = match state.get(&commission_identifier) { 64 Some(Plaintext::Literal(Literal::U8(commission), _)) => **commission, 65 _ => bail!("Invalid committee state (missing commission) - {value}"), 66 }; 67 // Return the committee state. 68 (is_open, commission) 69 } 70 _ => bail!("Invalid committee value (missing struct) - {value}"), 71 }; 72 73 // Extract the microcredits for the address from the delegated map. 74 let Some(microcredits) = delegated_map.iter().find_map(|(delegated_key, delegated_value)| { 75 // Retrieve the delegated address. 76 let delegated_address = match delegated_key { 77 Plaintext::Literal(Literal::Address(address), _) => Some(address), 78 _ => None, 79 }; 80 // Check if the address matches. 81 match delegated_address == Some(address) { 82 // Extract the microcredits from the value. 83 true => match delegated_value { 84 Value::Plaintext(Plaintext::Literal(Literal::U64(microcredits), _)) => Some(**microcredits), 85 _ => None, 86 }, 87 false => None, 88 } 89 }) else { 90 bail!("Missing microcredits for committee member - {address}"); 91 }; 92 93 // Return the committee member. 94 Ok((*address, (microcredits, is_open, commission))) 95 }) 96 .collect::<Result<IndexMap<_, _>>>()?; 97 98 // Return the committee. 99 Committee::new(starting_round, committee_members) 100 } 101 102 /// Returns the stakers given the bonded map from finalize storage. 103 pub fn bonded_map_into_stakers<N: Network>( 104 bonded_map: Vec<(Plaintext<N>, Value<N>)>, 105 ) -> Result<IndexMap<Address<N>, (Address<N>, u64)>> { 106 // Prepare the identifiers. 107 let validator_identifier = Identifier::from_str("validator")?; 108 let microcredits_identifier = Identifier::from_str("microcredits")?; 109 110 // Convert the given key and value into a staker entry. 111 let convert = |key, value| { 112 // Extract the staker from the key. 113 let address = match key { 114 Plaintext::Literal(Literal::Address(address), _) => address, 115 _ => bail!("Invalid bonded key (missing staker) - {key}"), 116 }; 117 // Extract the bonded state from the value. 118 match &value { 119 Value::Plaintext(Plaintext::Struct(state, _)) => { 120 // Extract the validator from the value. 121 let validator = match state.get(&validator_identifier) { 122 Some(Plaintext::Literal(Literal::Address(validator), _)) => *validator, 123 _ => bail!("Invalid bonded state (missing validator) - {value}"), 124 }; 125 // Extract the microcredits from the value. 126 let microcredits = match state.get(µcredits_identifier) { 127 Some(Plaintext::Literal(Literal::U64(microcredits), _)) => **microcredits, 128 _ => bail!("Invalid bonded state (missing microcredits) - {value}"), 129 }; 130 // Return the bonded state. 131 Ok((address, (validator, microcredits))) 132 } 133 _ => bail!("Invalid bonded value (missing struct) - {value}"), 134 } 135 }; 136 137 // Convert the bonded map into stakers. 138 bonded_map.into_iter().map(|(key, value)| convert(key, value)).collect::<Result<IndexMap<_, _>>>() 139 } 140 141 /// Checks that the given committee from committee storage matches the given stakers. 142 pub fn ensure_stakers_matches<N: Network>( 143 committee: &Committee<N>, 144 stakers: &IndexMap<Address<N>, (Address<N>, u64)>, 145 ) -> Result<()> { 146 // Construct the validator map. 147 let validator_map: IndexMap<_, _> = cfg_reduce!( 148 cfg_into_iter!(stakers) 149 .map(|(_, (validator, microcredits))| { 150 if committee.members().contains_key(validator) { 151 Some(indexmap! {*validator => *microcredits}) 152 } else { 153 None 154 } 155 }) 156 .flatten(), 157 || IndexMap::new(), 158 |mut acc, e| { 159 for (validator, microcredits) in e { 160 let entry: &mut u64 = acc.entry(validator).or_default(); 161 *entry = entry.saturating_add(microcredits); 162 } 163 acc 164 } 165 ); 166 167 // Compute the total microcredits. 168 let total_microcredits = 169 cfg_reduce!(cfg_iter!(validator_map).map(|(_, microcredits)| *microcredits), || 0u64, |a, b| { 170 // Add the staker's microcredits to the total microcredits. 171 a.saturating_add(b) 172 }); 173 174 // Ensure the committee and committee map match. 175 ensure!(committee.members().len() == validator_map.len(), "Committee and validator map length do not match"); 176 // Ensure the total microcredits match. 177 ensure!(committee.total_stake() == total_microcredits, "Committee and validator map total stake do not match"); 178 179 // Iterate over the committee and ensure the committee and validators match. 180 for (validator, (microcredits, _, _)) in committee.members() { 181 let candidate_microcredits = validator_map.get(validator); 182 ensure!(candidate_microcredits.is_some(), "A validator is missing in finalize storage"); 183 ensure!( 184 *microcredits == *candidate_microcredits.unwrap(), 185 "Committee contains an incorrect 'microcredits' amount from stakers" 186 ); 187 } 188 189 Ok(()) 190 } 191 192 /// Returns the next committee, given the current committee and stakers. 193 pub fn to_next_committee<N: Network>( 194 current_committee: &Committee<N>, 195 next_round: u64, 196 next_delegated: &IndexMap<Address<N>, u64>, 197 ) -> Result<Committee<N>> { 198 // Return the next committee. 199 Committee::new( 200 next_round, 201 cfg_iter!(next_delegated) 202 .flat_map(|(delegatee, microcredits)| { 203 let Some((_, is_open, commission)) = current_committee.members().get(delegatee) else { 204 // Do nothing, as the delegatee is not part of the committee. 205 return None; 206 }; 207 Some((*delegatee, (*microcredits, *is_open, *commission))) 208 }) 209 .collect(), 210 ) 211 } 212 213 pub fn to_next_delegated<N: Network>( 214 next_stakers: &IndexMap<Address<N>, (Address<N>, u64)>, 215 ) -> IndexMap<Address<N>, u64> { 216 // Construct the delegated map. 217 let delegated_map: IndexMap<Address<N>, u64> = cfg_reduce!( 218 cfg_into_iter!(next_stakers).map(|(_, (delegatee, microcredits))| indexmap! {*delegatee => *microcredits}), 219 || IndexMap::new(), 220 |mut acc, e| { 221 for (delegatee, microcredits) in e { 222 let entry: &mut u64 = acc.entry(delegatee).or_default(); 223 *entry = entry.saturating_add(microcredits); 224 } 225 acc 226 } 227 ); 228 229 delegated_map 230 } 231 232 /// Returns the committee map, bonded map, and delegated map, given the committee and stakers. 233 pub fn to_next_committee_bonded_delegated_map<N: Network>( 234 next_committee: &Committee<N>, 235 next_stakers: &IndexMap<Address<N>, (Address<N>, u64)>, 236 next_delegated: &IndexMap<Address<N>, u64>, 237 ) -> (Vec<(Plaintext<N>, Value<N>)>, Vec<(Plaintext<N>, Value<N>)>, Vec<(Plaintext<N>, Value<N>)>) { 238 // Prepare the identifiers. 239 let validator_identifier = Identifier::from_str("validator").expect("Failed to parse 'validator'"); 240 let microcredits_identifier = Identifier::from_str("microcredits").expect("Failed to parse 'microcredits'"); 241 let is_open_identifier = Identifier::from_str("is_open").expect("Failed to parse 'is_open'"); 242 let commission_identifier = Identifier::from_str("commission").expect("Failed to parse 'commission'"); 243 244 // Construct the committee map. 245 let committee_map = cfg_iter!(next_committee.members()) 246 .map(|(validator, (_, is_open, commission))| { 247 // Construct the committee state. 248 let committee_state = indexmap! { 249 is_open_identifier => Plaintext::from(Literal::Boolean(Boolean::new(*is_open))), 250 commission_identifier => Plaintext::from(Literal::U8(U8::new(*commission))), 251 }; 252 // Return the committee state. 253 ( 254 Plaintext::from(Literal::Address(*validator)), 255 Value::Plaintext(Plaintext::Struct(committee_state, Default::default())), 256 ) 257 }) 258 .collect::<Vec<_>>(); 259 260 // Construct the bonded map. 261 let bonded_map = cfg_iter!(next_stakers) 262 .map(|(staker, (validator, microcredits))| { 263 // Construct the bonded state. 264 let bonded_state = indexmap! { 265 validator_identifier => Plaintext::from(Literal::Address(*validator)), 266 microcredits_identifier => Plaintext::from(Literal::U64(U64::new(*microcredits))), 267 }; 268 // Return the bonded state. 269 ( 270 Plaintext::from(Literal::Address(*staker)), 271 Value::Plaintext(Plaintext::Struct(bonded_state, Default::default())), 272 ) 273 }) 274 .collect::<Vec<_>>(); 275 276 // Construct the delegated map. 277 let delegated_map = cfg_iter!(next_delegated) 278 .map(|(delegatee, microcredits)| { 279 ( 280 Plaintext::from(Literal::Address(*delegatee)), 281 Value::Plaintext(Plaintext::Literal(Literal::U64(U64::new(*microcredits)), Default::default())), 282 ) 283 }) 284 .collect::<Vec<_>>(); 285 286 (committee_map, bonded_map, delegated_map) 287 } 288 289 /// Returns the withdraw map, given the withdrawal addresses. 290 pub fn to_next_withdraw_map<N: Network>( 291 withdrawal_addresses: &IndexMap<Address<N>, Address<N>>, 292 ) -> Vec<(Plaintext<N>, Value<N>)> { 293 cfg_iter!(withdrawal_addresses) 294 .map(|(staker, withdraw_address)| { 295 ( 296 Plaintext::from(Literal::Address(*staker)), 297 Value::Plaintext(Plaintext::Literal(Literal::Address(*withdraw_address), Default::default())), 298 ) 299 }) 300 .collect::<Vec<_>>() 301 } 302 303 #[cfg(test)] 304 pub(crate) mod test_helpers { 305 use super::*; 306 use crate::vm::TestRng; 307 use deltavm_ledger_committee::{MIN_DELEGATOR_STAKE, MIN_VALIDATOR_STAKE}; 308 309 use rand::{CryptoRng, Rng}; 310 311 /// Returns the stakers, given the map of `(validator, (microcredits, is_open, commission))` entries. 312 /// This method simulates the existence of delegators for the members. 313 pub(crate) fn to_stakers<N: Network, R: Rng + CryptoRng>( 314 members: &IndexMap<Address<N>, (u64, bool, u8)>, 315 rng: &mut R, 316 ) -> IndexMap<Address<N>, (Address<N>, u64)> { 317 members 318 .into_iter() 319 .flat_map(|(validator, (microcredits, _, _))| { 320 // Keep a tally of the remaining microcredits. 321 let remaining_microcredits = microcredits.saturating_sub(MIN_VALIDATOR_STAKE); 322 // Set the staker amount to `MIN_DELEGATOR_STAKE` microcredits. 323 let staker_amount = MIN_DELEGATOR_STAKE; 324 // Determine the number of iterations. 325 let num_iterations = (remaining_microcredits / staker_amount).saturating_sub(1); 326 327 // Construct the map of stakers. 328 let rngs = (0..num_iterations).map(|_| TestRng::from_seed(rng.r#gen())).collect::<Vec<_>>(); 329 let mut stakers: IndexMap<_, _> = cfg_into_iter!(rngs) 330 .map(|mut rng| { 331 // Sample a random staker. 332 let staker = Address::<N>::new(rng.r#gen()); 333 // Output the staker. 334 (staker, (*validator, staker_amount)) 335 }) 336 .collect(); 337 338 // Insert the validator. 339 stakers.insert(*validator, (*validator, MIN_VALIDATOR_STAKE)); 340 341 // Insert the last staker. 342 let final_amount = remaining_microcredits.saturating_sub(num_iterations * staker_amount); 343 if final_amount > 0 { 344 let staker = Address::<N>::new(rng.r#gen()); 345 stakers.insert(staker, (*validator, final_amount)); 346 } 347 // Return the stakers. 348 stakers 349 }) 350 .collect() 351 } 352 353 /// Returns the validator delegation totals, given the map of `(validator, (microcredits, is_open, commission))` entries. 354 /// This method simulates the existence of delegators for the members. 355 pub(crate) fn to_delegations<N: Network>( 356 members: &IndexMap<Address<N>, (u64, bool, u8)>, 357 ) -> IndexMap<Address<N>, u64> { 358 members.into_iter().map(|(validator, (microcredits, _, _))| (*validator, *microcredits)).collect() 359 } 360 361 /// Returns the withdrawal addresses, given the stakers. 362 /// This method simulates the existence of unique withdrawal addresses for the stakers. 363 pub(crate) fn to_withdraw_addresses<N: Network, R: Rng + CryptoRng>( 364 stakers: &IndexMap<Address<N>, (Address<N>, u64)>, 365 rng: &mut R, 366 ) -> IndexMap<Address<N>, Address<N>> { 367 stakers 368 .into_iter() 369 .map(|(staker, _)| { 370 // Sample a random withdraw address. 371 let withdraw_address = Address::<N>::new(rng.r#gen()); 372 // Return the withdraw address. 373 (*staker, withdraw_address) 374 }) 375 .collect() 376 } 377 } 378 379 #[cfg(test)] 380 mod tests { 381 use super::*; 382 use console::prelude::TestRng; 383 384 #[allow(unused_imports)] 385 use rayon::prelude::*; 386 use std::str::FromStr; 387 388 /// Returns the committee map, given the map of `(validator, (microcredits, is_open, commission))` entries. 389 fn to_committee_map<N: Network>(members: &IndexMap<Address<N>, (u64, bool, u8)>) -> Vec<(Plaintext<N>, Value<N>)> { 390 members 391 .par_iter() 392 .map(|(validator, (_, is_open, commission))| { 393 let is_open = Boolean::<N>::new(*is_open); 394 let commission = U8::<N>::new(*commission); 395 ( 396 Plaintext::from(Literal::Address(*validator)), 397 Value::from_str(&format!("{{ is_open: {is_open}, commission: {commission} }}")).unwrap(), 398 ) 399 }) 400 .collect() 401 } 402 403 /// Returns the delegated map, given the map of `(validator, (microcredits, is_open, commission))` entries. 404 fn to_delegated_map<N: Network>(members: &IndexMap<Address<N>, (u64, bool, u8)>) -> Vec<(Plaintext<N>, Value<N>)> { 405 members 406 .par_iter() 407 .map(|(validator, (microcredits, _, _))| { 408 ( 409 Plaintext::from(Literal::Address(*validator)), 410 Value::Plaintext(Plaintext::Literal(Literal::U64(U64::new(*microcredits)), Default::default())), 411 ) 412 }) 413 .collect() 414 } 415 416 /// Returns the bonded map, given the staker, validator and microcredits. 417 fn to_bonded_map<N: Network>(stakers: &IndexMap<Address<N>, (Address<N>, u64)>) -> Vec<(Plaintext<N>, Value<N>)> { 418 // Prepare the identifiers. 419 let validator_identifier = Identifier::from_str("validator").expect("Failed to parse 'validator'"); 420 let microcredits_identifier = Identifier::from_str("microcredits").expect("Failed to parse 'microcredits'"); 421 422 stakers 423 .par_iter() 424 .map(|(staker, (validator, microcredits))| { 425 // Construct the bonded state. 426 let bonded_state = indexmap! { 427 validator_identifier => Plaintext::from(Literal::Address(*validator)), 428 microcredits_identifier => Plaintext::from(Literal::U64(U64::new(*microcredits))), 429 }; 430 // Return the bonded state. 431 ( 432 Plaintext::from(Literal::Address(*staker)), 433 Value::Plaintext(Plaintext::Struct(bonded_state, Default::default())), 434 ) 435 }) 436 .collect() 437 } 438 439 /// Returns the withdrawal addresses given the withdraw map from finalize storage. 440 pub fn withdraw_map_to_withdrawal_addresses<N: Network>( 441 withdraw_map: Vec<(Plaintext<N>, Value<N>)>, 442 ) -> Result<IndexMap<Address<N>, Address<N>>> { 443 // Convert the given key and value into a staker entry. 444 let convert = |key, value| { 445 // Extract the staker from the key. 446 let staker = match key { 447 Plaintext::Literal(Literal::Address(address), _) => address, 448 _ => bail!("Invalid withdraw key (missing staker) - {key}"), 449 }; 450 451 // Extract the withdrawal address from the value. 452 let withdrawal_address = match value { 453 Value::Plaintext(Plaintext::Literal(Literal::Address(address), _)) => address, 454 _ => bail!("Invalid withdraw value (missing address) - {key}"), 455 }; 456 457 Ok((staker, withdrawal_address)) 458 }; 459 460 // Convert the withdraw map into withdrawal addresses. 461 withdraw_map.into_iter().map(|(key, value)| convert(key, value)).collect::<Result<IndexMap<_, _>>>() 462 } 463 464 #[test] 465 fn test_committee_and_delegated_maps_into_committee() { 466 let rng = &mut TestRng::default(); 467 468 // Sample a committee. 469 let committee = deltavm_ledger_committee::test_helpers::sample_committee_for_round_and_size(1, 25, rng); 470 471 // Initialize the committee map. 472 let committee_map = to_committee_map(committee.members()); 473 474 // Initialize the delegated map. 475 let delegated_map = to_delegated_map(committee.members()); 476 477 // Start a timer. 478 let timer = std::time::Instant::now(); 479 // Convert the committee map into a committee. 480 let candidate_committee = 481 committee_and_delegated_maps_into_committee(committee.starting_round(), committee_map, delegated_map) 482 .unwrap(); 483 println!("committee_and_delegated_maps_into_committee: {}ms", timer.elapsed().as_millis()); 484 assert_eq!(candidate_committee, committee); 485 } 486 487 #[test] 488 fn test_bonded_map_into_stakers() { 489 let rng = &mut TestRng::default(); 490 491 // Sample a committee. 492 let committee = deltavm_ledger_committee::test_helpers::sample_committee_for_round_and_size(1, 25, rng); 493 // Convert the committee into stakers. 494 let expected_stakers = crate::committee::test_helpers::to_stakers(committee.members(), rng); 495 // Initialize the bonded map. 496 let bonded_map = to_bonded_map(&expected_stakers); 497 498 // Start a timer. 499 let timer = std::time::Instant::now(); 500 // Convert the bonded map into stakers. 501 let candidate_stakers = bonded_map_into_stakers(bonded_map).unwrap(); 502 println!("bonded_map_into_stakers: {}ms", timer.elapsed().as_millis()); 503 assert_eq!(candidate_stakers.len(), expected_stakers.len()); 504 assert_eq!(candidate_stakers, expected_stakers); 505 } 506 507 #[test] 508 fn test_ensure_stakers_matches() { 509 let rng = &mut TestRng::default(); 510 511 // Sample a committee. 512 let committee = deltavm_ledger_committee::test_helpers::sample_committee_for_round_and_size(1, 25, rng); 513 // Convert the committee into stakers. 514 let stakers = crate::committee::test_helpers::to_stakers(committee.members(), rng); 515 516 // Start a timer. 517 let timer = std::time::Instant::now(); 518 // Ensure the stakers matches. 519 let result = ensure_stakers_matches(&committee, &stakers); 520 println!("ensure_stakers_matches: {}ms", timer.elapsed().as_millis()); 521 assert!(result.is_ok()); 522 } 523 524 #[test] 525 fn test_to_next_committee() { 526 let rng = &mut TestRng::default(); 527 528 // Sample a committee. 529 let committee = deltavm_ledger_committee::test_helpers::sample_committee_for_round_and_size(1, 25, rng); 530 // Convert the committee into stakers. 531 let _stakers = crate::committee::test_helpers::to_stakers(committee.members(), rng); 532 // Convert the committee into delegations. 533 let delegations = crate::committee::test_helpers::to_delegations(committee.members()); 534 535 // Start a timer. 536 let timer = std::time::Instant::now(); 537 // Ensure the next committee matches the current committee. 538 // Note: We can perform this check, in this specific case only, because we did not apply staking rewards. 539 let next_committee = to_next_committee(&committee, committee.starting_round() + 1, &delegations).unwrap(); 540 println!("to_next_committee: {}ms", timer.elapsed().as_millis()); 541 assert_eq!(committee.starting_round() + 1, next_committee.starting_round()); 542 assert_eq!(committee.members(), next_committee.members()); 543 } 544 545 #[test] 546 fn test_to_next_committee_bonded_delegated_map() { 547 let rng = &mut TestRng::default(); 548 549 // Sample a committee. 550 let committee = deltavm_ledger_committee::test_helpers::sample_committee(rng); 551 // Convert the committee into stakers. 552 let stakers: IndexMap<Address<console::network::MainnetV0>, (Address<console::network::MainnetV0>, u64)> = 553 crate::committee::test_helpers::to_stakers(committee.members(), rng); 554 // Convert the committee into delegations. 555 let delegations = crate::committee::test_helpers::to_delegations(committee.members()); 556 557 // Start a timer. 558 let timer = std::time::Instant::now(); 559 // Ensure the next committee matches the current committee. 560 // Note: We can perform this check, in this specific case only, because we did not apply staking rewards. 561 let (committee_map, bonded_map, _) = to_next_committee_bonded_delegated_map(&committee, &stakers, &delegations); 562 println!("to_next_committee_bonded_delegated_map: {}ms", timer.elapsed().as_millis()); 563 assert_eq!(committee_map, to_committee_map(committee.members())); 564 assert_eq!(bonded_map, to_bonded_map(&stakers)); 565 } 566 567 #[test] 568 fn test_to_withdraw_map() { 569 let rng = &mut TestRng::default(); 570 571 // Sample a committee. 572 let committee = deltavm_ledger_committee::test_helpers::sample_committee(rng); 573 // Convert the committee into stakers. 574 let stakers = crate::committee::test_helpers::to_stakers(committee.members(), rng); 575 576 // Construct the withdraw addresses. 577 let withdrawal_addresses = crate::committee::test_helpers::to_withdraw_addresses(&stakers, rng); 578 579 // Start a timer. 580 let timer = std::time::Instant::now(); 581 // Ensure the withdrawal map is correct. 582 let withdrawal_map = to_next_withdraw_map(&withdrawal_addresses); 583 println!("to_next_withdraw_map: {}ms", timer.elapsed().as_millis()); 584 assert_eq!(withdrawal_addresses, withdraw_map_to_withdrawal_addresses(withdrawal_map).unwrap()); 585 } 586 }