commit 6b86727bfc91fd2af9fd3cd1c21f15d97515ef52
parent edbd2594b380fb49c76e8d64d63d9daf440e7a45
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Thu, 3 Sep 2026 21:19:19 +0200
Speed up and stabilize release soak
Diffstat:
4 files changed, 92 insertions(+), 11 deletions(-)
diff --git a/src/app/helpers.rs b/src/app/helpers.rs
@@ -40,6 +40,13 @@ pub(super) fn converge_fee_by_byte(
fee = required_fee;
}
+ // Transaction size can oscillate when a new fee selects a different UTXO.
+ // A previously built candidate whose fee covers its own size is already
+ // valid, even when the final iteration happens to underpay.
+ if let Some(best) = best {
+ return Ok(best);
+ }
+
let built = build(fee)?;
let bytes = built.economic_size_bytes();
let required_fee = fee_per_byte
@@ -130,3 +137,43 @@ pub(super) fn recovery_vdf_sample_percent(address: &str, tip_hash: &str) -> u8 {
let digest = Sha256::digest(format!("iuna-recovery-vdf-sample:{tip_hash}:{address}"));
digest[0] % 100
}
+
+#[cfg(test)]
+mod tests {
+ use crate::domain::{OutPoint, Transaction, TxInput};
+
+ use super::converge_fee_by_byte;
+
+ fn burn_with_inputs(fee: u64, input_count: usize) -> Transaction {
+ Transaction::Burn {
+ inputs: (0..input_count)
+ .map(|index| TxInput {
+ outpoint: OutPoint {
+ txid: format!("{index:064x}"),
+ index: index as u32,
+ },
+ owner: "a".repeat(64),
+ signature: "b".repeat(128),
+ })
+ .collect(),
+ change: Vec::new(),
+ amount: 1,
+ fee,
+ anchor: None,
+ signature: "c".repeat(128),
+ }
+ }
+
+ #[test]
+ fn fee_convergence_keeps_valid_candidate_when_transaction_size_oscillates() {
+ let large_size = burn_with_inputs(1, 2).economic_size_bytes() as u64;
+ let (transaction, estimate) = converge_fee_by_byte(1, |fee| {
+ let input_count = if fee < large_size { 2 } else { 1 };
+ Ok(burn_with_inputs(fee, input_count))
+ })
+ .unwrap();
+
+ assert_eq!(estimate.fee, transaction.fee());
+ assert!(estimate.fee >= transaction.economic_size_bytes() as u64);
+ }
+}
diff --git a/src/domain.rs b/src/domain.rs
@@ -90,6 +90,8 @@ pub use validation::validate_address;
use validation::{
canonical_transaction_size_bytes, validate_hash, validate_protocol_id, validate_signature,
};
+#[cfg(feature = "e2e")]
+pub use vdf::configure_e2e_vdf_round_divisor_for_tests;
pub use vdf::{
VdfProgress, VdfProgressPhase, run_vdf, run_vdf_cancellable_with_progress,
run_vdf_with_progress, verify_vdf,
diff --git a/src/domain/vdf/mod.rs b/src/domain/vdf/mod.rs
@@ -1,8 +1,12 @@
+use std::borrow::Cow;
use std::{
sync::atomic::AtomicBool,
time::{Duration, Instant},
};
+#[cfg(feature = "e2e")]
+use std::sync::atomic::{AtomicU64, Ordering};
+
use super::{Block, FinalizerMode, MAX_VDF_ROUNDS, VDF_TARGET_BLOCK_MS, decode_hex, hex_encode};
#[cfg(test)]
@@ -19,6 +23,8 @@ mod reference;
const VDF_SOLUTION_PREFIX: &str = "classgroup-wesolowski-bqfc-v1:";
const MIN_VDF_ROUNDS: u64 = 1;
+#[cfg(feature = "e2e")]
+static E2E_VDF_ROUND_DIVISOR: AtomicU64 = AtomicU64::new(1);
pub(super) const VDF_RETARGET_WINDOW_BLOCKS: usize = 20;
pub(super) const MAX_VDF_RETARGET_STEP_PERCENT: u128 = 2;
pub(super) const VDF_RETARGET_DEADBAND_PERCENT: u128 = 10;
@@ -40,6 +46,13 @@ pub enum VdfProgressPhase {
Proof,
}
+/// Shortens newly produced and verified VDFs inside an isolated e2e test
+/// process. Every participant in that process must use the same divisor.
+#[cfg(feature = "e2e")]
+pub fn configure_e2e_vdf_round_divisor_for_tests(divisor: u64) {
+ E2E_VDF_ROUND_DIVISOR.store(divisor.max(1), Ordering::Relaxed);
+}
+
pub fn run_vdf(seed: &str, rounds: u64) -> String {
run_vdf_with_progress(seed, rounds, Duration::MAX, |_| {})
}
@@ -62,15 +75,16 @@ pub fn run_vdf_cancellable_with_progress(
cancelled: &AtomicBool,
mut progress: impl FnMut(VdfProgress),
) -> Option<String> {
- let total_steps = rounds.saturating_mul(2);
+ let (proof_seed, proof_rounds) = vdf_proof_parameters(seed, rounds);
+ let total_steps = proof_rounds.saturating_mul(2);
let mut last_progress = Instant::now();
let solution = wesolowski::prove_cancellable(
- seed.as_bytes(),
- rounds,
+ proof_seed.as_bytes(),
+ proof_rounds,
|phase, completed_phase_rounds| {
let completed_steps = match phase {
VdfProgressPhase::Output => completed_phase_rounds,
- VdfProgressPhase::Proof => rounds.saturating_add(completed_phase_rounds),
+ VdfProgressPhase::Proof => proof_rounds.saturating_add(completed_phase_rounds),
};
maybe_report_vdf_progress(
&mut last_progress,
@@ -79,7 +93,7 @@ pub fn run_vdf_cancellable_with_progress(
completed_steps,
total_steps,
completed_phase_rounds,
- phase_rounds: rounds,
+ phase_rounds: proof_rounds,
phase,
},
&mut progress,
@@ -95,7 +109,22 @@ pub fn verify_vdf(seed: &str, rounds: u64, solution: &str) -> bool {
let Some(solution) = decode_vdf_solution(solution) else {
return false;
};
- wesolowski::verify(seed.as_bytes(), rounds, &solution)
+ let (proof_seed, proof_rounds) = vdf_proof_parameters(seed, rounds);
+ wesolowski::verify(proof_seed.as_bytes(), proof_rounds, &solution)
+}
+
+fn vdf_proof_parameters(seed: &str, rounds: u64) -> (Cow<'_, str>, u64) {
+ #[cfg(feature = "e2e")]
+ {
+ let divisor = E2E_VDF_ROUND_DIVISOR.load(Ordering::Relaxed);
+ if divisor > 1 {
+ return (
+ Cow::Owned(format!("iuna-e2e-vdf:{rounds}:{seed}")),
+ rounds.div_ceil(divisor).max(MIN_VDF_ROUNDS),
+ );
+ }
+ }
+ (Cow::Borrowed(seed), rounds)
}
pub(super) fn vdf_solution_placeholder() -> String {
diff --git a/tests/properties.rs b/tests/properties.rs
@@ -13,7 +13,7 @@ use iuna::{
app::{NodeCore, PeerBook, SharedNode, now_ms},
domain::{
Ledger, OBJECTIVE_FINALITY_ACTIVATION_HEIGHT, Transaction, VDF_TARGET_BLOCK_MS, Wallet,
- run_vdf,
+ configure_e2e_vdf_round_divisor_for_tests, run_vdf,
},
};
use serde_json::{Value, json};
@@ -26,6 +26,7 @@ use tokio::{
};
const SOAK_BLOCKS: u64 = 12;
+const SOAK_VDF_ROUND_DIVISOR: u64 = 100;
const BURN_COLLECTION_MS: u64 = VDF_TARGET_BLOCK_MS / 20 + 1;
const SOAK_START_HEIGHT: u64 = OBJECTIVE_FINALITY_ACTIVATION_HEIGHT + 1;
const FIXTURE_SERVICES: [&str; 6] = ["bootstrap", "node2", "node3", "node4", "node5", "node6"];
@@ -33,6 +34,7 @@ const FIXTURE_SERVICES: [&str; 6] = ["bootstrap", "node2", "node3", "node4", "no
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[ignore = "long-running post-activation soak; run with cargo test --release --features e2e --test properties -- --ignored"]
async fn release_soak_post_activation_auto_finalization_p2p_stratum_and_restarts() -> Result<()> {
+ configure_e2e_vdf_round_divisor_for_tests(SOAK_VDF_ROUND_DIVISOR);
let (wallets, genesis) = post_activation_fixture()?;
let p2p_addrs = reserve_loopback_addrs(wallets.len())?;
let stratum_addr = reserve_loopback_addrs(1)?.remove(0);
@@ -223,10 +225,11 @@ async fn finalize_one_block(nodes: &[SoakNode], target_height: u64) -> Result<()
let deadline = tokio::time::Instant::now() + Duration::from_secs(8);
loop {
for node in nodes {
- if let Some(block) = complete_if_ready(node, now_ms()).await? {
- node.network
- .broadcast(node.node.lock().await.drain_outbox())
- .await?;
+ let block = complete_if_ready(node, now_ms()).await?;
+ node.network
+ .broadcast(node.node.lock().await.drain_outbox())
+ .await?;
+ if let Some(block) = block {
assert_eq!(block.height, target_height);
return Ok(());
}