iuna

iuna - experimental devnet protocol
git clone https://iuna.jhx.app/git/iuna.git
Log | Files | Refs | README | LICENSE

commit 8b68013dae2d26e940d326ca3af5bb46f1f9cdd7
parent 354573da59bec6602c4d6989106ab8a7fc5bc077
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Tue, 11 Aug 2026 15:39:33 +0200

Fix VDF modulus and reset consensus gates

Diffstat:
A.github/workflows/ci.yml | 20++++++++++++++++++++
MCargo.lock | 36++++++++++++++++++++++++++++++++++++
MCargo.toml | 2++
Mdocs/protocol.md | 6++++--
Msrc/adapters/ui_data_store.rs | 32+++-----------------------------
Msrc/app/receive.rs | 15++++++++++++++-
Msrc/domain.rs | 12+++++-------
Msrc/domain/block.rs | 69++++++++++++---------------------------------------------------------
Msrc/domain/ledger_apply.rs | 42+++++++++++++-----------------------------
Msrc/domain/ledger_ops.rs | 13++++---------
Msrc/domain/ledger_pending.rs | 44++++++++++++++++++++------------------------
Msrc/domain/mine_policy.rs | 14++------------
Msrc/domain/protocol.rs | 4----
Msrc/domain/tests.rs | 225+++++++++++++------------------------------------------------------------------
Msrc/domain/vdf.rs | 176++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/main.rs | 4++--
Msrc/main_tests.rs | 29++++++++++++++++-------------
17 files changed, 318 insertions(+), 425 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml @@ -0,0 +1,20 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - run: cargo fmt --check + - run: cargo test --locked diff --git a/Cargo.lock b/Cargo.lock @@ -43,6 +43,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] name = "axum" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -545,6 +551,8 @@ dependencies = [ "chacha20poly1305", "ed25519-dalek", "getrandom 0.2.17", + "num-bigint", + "num-traits", "pbkdf2", "rusqlite", "serde", @@ -623,6 +631,34 @@ dependencies = [ ] [[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -11,6 +11,8 @@ bip39 = "2.2.2" chacha20poly1305 = "0.10.1" ed25519-dalek = "2.2.0" getrandom = "0.2.17" +num-bigint = "0.4.6" +num-traits = "0.2.19" pbkdf2 = "0.12.2" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.150" diff --git a/docs/protocol.md b/docs/protocol.md @@ -54,7 +54,9 @@ Wallet-created transfers and burns are not gossiped as plaintext. Their blinded ## VDF Timing -The VDF is there to make block production sequential and time-based. It cannot be parallelized in the same way as normal hashing work. +The VDF is there to make block production sequential and time-based. It uses repeated squaring in an unknown-order RSA group: validators know the public modulus, but not its factorization. That unknown order is essential. If the factorization were known, a finalizer could skip the delay with normal modular exponentiation. + +The devnet uses the public RSA-2048 challenge modulus. A production mainnet should use a purpose-specific trusted setup ceremony with destroyed factors, or a class-group VDF that avoids trusted setup. The target block time is `5 minutes`. The protocol retargets VDF rounds from recent observed block times: @@ -115,7 +117,7 @@ Difficulty targets about one mine action per block: This keeps issuance separate from finalization. PoW miners compete to create mine actions; burn-ticket finalizers decide blocks. -Starting at height `200`, a block may contain at most `2` mine actions for the same anchor. Upgraded nodes already produce and select at most `2` pending mine actions per anchor before that activation height, so duplicate-anchor floods drain without invalidating older blocks during rollout. This leaves room for the difficulty retarget to move upward when PoW regularly fills both slots, while still bounding issuance from any single anchor. +A block may contain at most `2` mine actions for the same anchor. This leaves room for the difficulty retarget to move upward when PoW regularly fills both slots, while still bounding issuance from any single anchor. ## Fair Burn Inclusion diff --git a/src/adapters/ui_data_store.rs b/src/adapters/ui_data_store.rs @@ -13,9 +13,9 @@ use serde::Serialize; use crate::{ adapters::ui_index::{UiChainIndex, build_ui_chain_index}, domain::{ - AGGREGATE_FINALIZER_FEE_ACTIVATION_HEIGHT, Amount, BLINDED_COMMITTER_FEE_BPS, - BLINDED_FEE_BPS_DENOMINATOR, BLINDED_REVEAL_BUNDLE_SIGNER_FEE_BPS, BlindedTransaction, - Block, BurnLeaderRank, ChainSnapshot, Ledger, MINE_REWARD, OutPoint, REVEAL_COMMITTEE_SIZE, + Amount, BLINDED_COMMITTER_FEE_BPS, BLINDED_FEE_BPS_DENOMINATOR, + BLINDED_REVEAL_BUNDLE_SIGNER_FEE_BPS, BlindedTransaction, Block, BurnLeaderRank, + ChainSnapshot, Ledger, MINE_REWARD, OutPoint, REVEAL_COMMITTEE_SIZE, RevealedBlindedTransaction, Transaction, TxInput, TxOutput, blinded_reveal_finalizer_fee, hex_hash, reveal_committee_slot_count, revealed_blinded_transactions, }, @@ -1174,10 +1174,6 @@ fn metrics_from_snapshot(snapshot: &ChainSnapshot) -> Result<Vec<BlockMetricRow> &revealed.included_by, block, transaction.fee(), - reveal_bundle_slots_by_height - .get(&block.height) - .copied() - .unwrap_or(REVEAL_COMMITTEE_SIZE), ); fees_amount = fees_amount .checked_add(transaction.fee()) @@ -1418,7 +1414,6 @@ fn metric_index_blinded_fee_outputs( included_by: &str, block: &Block, fee: Amount, - available_reveal_bundle_slots: usize, ) { if fee == 0 { return; @@ -1433,20 +1428,6 @@ fn metric_index_blinded_fee_outputs( }, ); } - let reveal_finalizer_fee = blinded_reveal_finalizer_fee( - fee, - block.included_reveal_bundle_count(), - available_reveal_bundle_slots, - ); - if reveal_finalizer_fee > 0 && block.height < AGGREGATE_FINALIZER_FEE_ACTIVATION_HEIGHT { - utxos.insert( - metric_blinded_executor_fee_outpoint(commitment), - TxOutput { - address: block.miner.clone(), - amount: reveal_finalizer_fee, - }, - ); - } let reveal_bundle_signer_fee = blinded_fee_share(fee, BLINDED_REVEAL_BUNDLE_SIGNER_FEE_BPS); if reveal_bundle_signer_fee > 0 { for signature in &block.reveal_bundle_section.signatures { @@ -1517,13 +1498,6 @@ fn metric_blinded_committer_fee_outpoint(commitment: &str) -> OutPoint { } } -fn metric_blinded_executor_fee_outpoint(commitment: &str) -> OutPoint { - OutPoint { - txid: commitment.to_string(), - index: u32::MAX - 2, - } -} - fn metric_blinded_reveal_bundle_signer_fee_outpoint(commitment: &str, slot: u8) -> OutPoint { OutPoint { txid: commitment.to_string(), diff --git a/src/app/receive.rs b/src/app/receive.rs @@ -423,6 +423,15 @@ mod tests { let plan = node.prepare_automatic_finalization(1); assert!(plan.burned.is_some()); + assert_eq!(node.ledger().pending_blinded_transactions().len(), 1); + let automatic_burn_commitment = node.ledger().pending_blinded_transactions()[0] + .commitment + .clone(); + assert_eq!( + node.ledger().pending_blinded_transactions()[0].commitment, + automatic_burn_commitment + ); + node.drain_outbox(); let (_, anchor_burn) = node .local_block_anchor_burn .clone() @@ -442,7 +451,11 @@ mod tests { node.receive_blinded_transaction(conflicting.transaction) .unwrap(); - assert!(node.ledger().pending_blinded_transactions().is_empty()); + assert_eq!(node.ledger().pending_blinded_transactions().len(), 1); + assert_eq!( + node.ledger().pending_blinded_transactions()[0].commitment, + automatic_burn_commitment + ); assert!(node.drain_outbox().is_empty()); assert!(node.prepare_automatic_finalization(1).work.is_some()); } diff --git a/src/domain.rs b/src/domain.rs @@ -70,13 +70,11 @@ use mine_policy::{ use mining::{mine_payload, mine_signature}; pub use profile::{GenesisBurn, LaunchProfile}; pub use protocol::{ - AGGREGATE_FINALIZER_FEE_ACTIVATION_HEIGHT, Amount, BLINDED_COMMITTER_FEE_BPS, - BLINDED_FEE_BPS_DENOMINATOR, BLINDED_REVEAL_BUNDLE_SIGNER_FEE_BPS, - BLINDED_REVEAL_FINALIZER_FEE_BPS, BLOCK_REWARD, DEFAULT_FEE_PER_BYTE, DEFAULT_MINE_FEE, - DEFAULT_TRANSACTION_FEE, FALLBACK_VDF_RETARGET_ACTIVATION_HEIGHT, - FALLBACK_VDF_RETARGET_DEACTIVATION_HEIGHT, MAX_BLINDED_TRANSACTION_EXPIRY_HEIGHTS, - MAX_BLOCK_BYTES, MAX_PENDING_TRANSACTIONS, MAX_REVEAL_BUNDLE_BYTES, MAX_VDF_ROUNDS, MICRO_IUNA, - MINE_ACTIONS_PER_ANCHOR_LIMIT, MINE_ACTIONS_PER_ANCHOR_LIMIT_ACTIVATION_HEIGHT, + Amount, BLINDED_COMMITTER_FEE_BPS, BLINDED_FEE_BPS_DENOMINATOR, + BLINDED_REVEAL_BUNDLE_SIGNER_FEE_BPS, BLINDED_REVEAL_FINALIZER_FEE_BPS, BLOCK_REWARD, + DEFAULT_FEE_PER_BYTE, DEFAULT_MINE_FEE, DEFAULT_TRANSACTION_FEE, + MAX_BLINDED_TRANSACTION_EXPIRY_HEIGHTS, MAX_BLOCK_BYTES, MAX_PENDING_TRANSACTIONS, + MAX_REVEAL_BUNDLE_BYTES, MAX_VDF_ROUNDS, MICRO_IUNA, MINE_ACTIONS_PER_ANCHOR_LIMIT, MINE_DIFFICULTY_BITS, MINE_FINALIZER_FEE, MINE_REWARD, RECOVERY_BLOCK_DELAY_MS, REVEAL_COMMITTEE_SIZE, TransactionSubmitOutcome, VDF_TARGET_BLOCK_MS, }; diff --git a/src/domain/block.rs b/src/domain/block.rs @@ -88,66 +88,21 @@ impl Block { ) }) .unwrap_or_default(); - if !self.blinded_transactions.is_empty() || !self.reveal_bundle_section.is_empty() { - return hex_hash(format!( - "block-content-v3:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}", - self.height, - self.prev_hash, - self.timestamp_ms, - self.miner, - self.finalizer_rank, - self.reward, - self.vdf_rounds, - leader_proof, - txs, - canonical_blinded_block_items(&blinded, &reveal_section) - )); - } hex_hash(format!( - "{}:{}", - self.legacy_content_hash_prefix(&leader_proof), - txs + "block-content-v3:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}", + self.height, + self.prev_hash, + self.timestamp_ms, + self.miner, + self.finalizer_rank, + self.reward, + self.vdf_rounds, + leader_proof, + txs, + canonical_blinded_block_items(&blinded, &reveal_section) )) } - fn legacy_content_hash_prefix(&self, leader_proof: &str) -> String { - if self.finalizer_mode == FinalizerMode::Recovery { - format!( - "block-content-recovery-v1:{}:{}:{}:{}:{}:{}:{}", - self.height, - self.prev_hash, - self.timestamp_ms, - self.miner, - self.reward, - self.vdf_rounds, - leader_proof - ) - } else if self.finalizer_rank == 0 { - format!( - "block-content:{}:{}:{}:{}:{}:{}:{}", - self.height, - self.prev_hash, - self.timestamp_ms, - self.miner, - self.reward, - self.vdf_rounds, - leader_proof - ) - } else { - format!( - "block-content-v2:{}:{}:{}:{}:{}:{}:{}:{}", - self.height, - self.prev_hash, - self.timestamp_ms, - self.miner, - self.finalizer_rank, - self.reward, - self.vdf_rounds, - leader_proof - ) - } - } - pub(super) fn leader_score(&self) -> LeaderScore { LeaderScore { finalizer_mode_rank: self.finalizer_mode.fork_choice_rank(), @@ -370,7 +325,7 @@ mod tests { use crate::domain::{RevealBundleSection, Transaction}; #[test] - fn leader_proof_payload_keeps_legacy_primary_canonical_form() { + fn primary_leader_proof_payload_omits_rank_from_canonical_form() { let primary = LeaderProofPayload { height: 1, prev_hash: "prev".to_string(), diff --git a/src/domain/ledger_apply.rs b/src/domain/ledger_apply.rs @@ -6,9 +6,9 @@ use super::blinded::{ ActiveBlindedTransaction, credit_blinded_fee_outputs, credit_expired_blinded_outputs, }; use super::ledger_ops::{ - aggregate_finalizer_fees_active, apply_transaction, block_reward, credit_reward_output, - ensure_block_has_burn, ensure_valid_recovery_block, fee_reward, spend_blinded_inputs, - validate_block_blinded_items, verify_leader_proof, + apply_transaction, block_reward, credit_reward_output, ensure_block_has_burn, + ensure_valid_recovery_block, spend_blinded_inputs, validate_block_blinded_items, + verify_leader_proof, }; use super::mine_policy::ensure_mine_anchor_limit; use super::ticket::{ @@ -104,17 +104,15 @@ impl Ledger { &tx, &block.reveal_bundle_section.signatures, reveal_bundle_slot_count, - aggregate_finalizer_fees_active(block.height), + true, )?; - if aggregate_finalizer_fees_active(block.height) { - aggregated_reveal_finalizer_fees = aggregated_reveal_finalizer_fees - .checked_add(blinded_reveal_finalizer_fee( - tx.fee(), - block.included_reveal_bundle_count(), - reveal_bundle_slot_count, - )) - .context("aggregated reveal finalizer fees overflow")?; - } + aggregated_reveal_finalizer_fees = aggregated_reveal_finalizer_fees + .checked_add(blinded_reveal_finalizer_fee( + tx.fee(), + block.included_reveal_bundle_count(), + reveal_bundle_slot_count, + )) + .context("aggregated reveal finalizer fees overflow")?; revealed_transactions.push(tx); } for (commitment, active) in &self.active_blinded { @@ -340,15 +338,9 @@ impl Ledger { reveal_bundle_section: &RevealBundleSection, ) -> Result<Amount> { let height = self.tip().height + 1; - if !aggregate_finalizer_fees_active(height) { - return fee_reward(transactions); - } let reveal_bundle_slot_count = self.reveal_committee_for_height(height).len(); - let aggregate = self.aggregate_reveal_finalizer_fees( - height, - reveal_bundle_section, - reveal_bundle_slot_count, - )?; + let aggregate = + self.aggregate_reveal_finalizer_fees(reveal_bundle_section, reveal_bundle_slot_count)?; block_reward(transactions, aggregate) } @@ -357,11 +349,7 @@ impl Ledger { block: &Block, reveal_bundle_slot_count: usize, ) -> Result<Amount> { - if !aggregate_finalizer_fees_active(block.height) { - return fee_reward(&block.transactions); - } let aggregate = self.aggregate_reveal_finalizer_fees( - block.height, &block.reveal_bundle_section, reveal_bundle_slot_count, )?; @@ -370,13 +358,9 @@ impl Ledger { fn aggregate_reveal_finalizer_fees( &self, - height: u64, reveal_bundle_section: &RevealBundleSection, reveal_bundle_slot_count: usize, ) -> Result<Amount> { - if !aggregate_finalizer_fees_active(height) { - return Ok(0); - } reveal_bundle_section .all_reveals() .into_iter() diff --git a/src/domain/ledger_ops.rs b/src/domain/ledger_ops.rs @@ -10,11 +10,10 @@ use super::selection::{TransactionKind, blinded_fee_rate_key, fee_rate_key}; use super::ticket::ticket_is_eligible_for_height; use super::transaction::{BlindedTransaction, Transaction}; use super::{ - AGGREGATE_FINALIZER_FEE_ACTIVATION_HEIGHT, Amount, Block, BlockSelection, BurnTicket, - FinalizerMode, LeaderProof, LeaderProofPayload, Ledger, MINE_REWARD, OutPoint, - PUBLIC_KEY_BYTES, RECOVERY_BLOCK_DELAY_MS, REVEAL_COMMITTEE_SIZE, SIGNATURE_BYTES, TxInput, - TxOutput, decode_hex_array, validate_address, validate_hash, validate_protocol_id, - validate_signature, + Amount, Block, BlockSelection, BurnTicket, FinalizerMode, LeaderProof, LeaderProofPayload, + Ledger, MINE_REWARD, OutPoint, PUBLIC_KEY_BYTES, RECOVERY_BLOCK_DELAY_MS, + REVEAL_COMMITTEE_SIZE, SIGNATURE_BYTES, TxInput, TxOutput, decode_hex_array, validate_address, + validate_hash, validate_protocol_id, validate_signature, }; pub(super) fn validate_genesis_allocations( @@ -296,10 +295,6 @@ pub(super) fn block_reward( .context("block reward overflow") } -pub(super) fn aggregate_finalizer_fees_active(height: u64) -> bool { - height >= AGGREGATE_FINALIZER_FEE_ACTIVATION_HEIGHT -} - pub(super) fn spend_inputs( transaction: &Transaction, utxos: &mut BTreeMap<OutPoint, TxOutput>, diff --git a/src/domain/ledger_pending.rs b/src/domain/ledger_pending.rs @@ -15,8 +15,7 @@ use super::ledger_ops::{ transaction_has_missing_inputs, validate_transaction_inputs, validate_transaction_outputs, }; use super::mine_policy::{ - MINE_MAX_ANCHOR_AGE_BLOCKS, mine_actions_per_anchor_limit_active, mine_anchor, - mine_anchor_count_before_height, + MINE_MAX_ANCHOR_AGE_BLOCKS, mine_anchor, mine_anchor_count_before_height, }; use super::selection::{ BlockSelection, SelectableItem, TransactionKind, best_selectable_item, blinded_fee_rate_key, @@ -298,28 +297,25 @@ impl Ledger { } pub(super) fn validate_mine_anchor_available(&self, transaction: &Transaction) -> Result<()> { - if mine_actions_per_anchor_limit_active(self.height().saturating_add(1)) { - if let Some(anchor) = mine_anchor(transaction) { - let known_count = - mine_anchor_count_before_height(&self.chain, anchor, self.height()) - .saturating_add( - self.pending - .iter() - .filter(|tx| mine_anchor(tx) == Some(anchor)) - .count(), - ) - .saturating_add( - self.orphans - .iter() - .filter(|tx| { - mine_anchor(tx) == Some(anchor) - && tx.signature() != transaction.signature() - }) - .count(), - ); - if known_count >= MINE_ACTIONS_PER_ANCHOR_LIMIT { - bail!("mine transaction anchor limit reached"); - } + if let Some(anchor) = mine_anchor(transaction) { + let known_count = mine_anchor_count_before_height(&self.chain, anchor, self.height()) + .saturating_add( + self.pending + .iter() + .filter(|tx| mine_anchor(tx) == Some(anchor)) + .count(), + ) + .saturating_add( + self.orphans + .iter() + .filter(|tx| { + mine_anchor(tx) == Some(anchor) + && tx.signature() != transaction.signature() + }) + .count(), + ); + if known_count >= MINE_ACTIONS_PER_ANCHOR_LIMIT { + bail!("mine transaction anchor limit reached"); } } Ok(()) diff --git a/src/domain/mine_policy.rs b/src/domain/mine_policy.rs @@ -2,10 +2,7 @@ use std::collections::BTreeMap; use anyhow::{Result, bail}; -use super::{ - Block, MINE_ACTIONS_PER_ANCHOR_LIMIT, MINE_ACTIONS_PER_ANCHOR_LIMIT_ACTIVATION_HEIGHT, - Transaction, -}; +use super::{Block, MINE_ACTIONS_PER_ANCHOR_LIMIT, Transaction}; pub(super) const MINE_RETARGET_WINDOW_BLOCKS: u64 = 10; pub(super) const MINE_MAX_RETARGET_STEP_BITS: u32 = 2; @@ -56,10 +53,7 @@ fn floor_log2_ratio(numerator: u64, denominator: u64) -> u32 { step } -pub(super) fn ensure_mine_anchor_limit(height: u64, transactions: &[Transaction]) -> Result<()> { - if !mine_actions_per_anchor_limit_active(height) { - return Ok(()); - } +pub(super) fn ensure_mine_anchor_limit(_height: u64, transactions: &[Transaction]) -> Result<()> { let mut anchor_counts = BTreeMap::new(); for transaction in transactions { let Some(anchor) = mine_anchor(transaction) else { @@ -74,10 +68,6 @@ pub(super) fn ensure_mine_anchor_limit(height: u64, transactions: &[Transaction] Ok(()) } -pub(super) fn mine_actions_per_anchor_limit_active(height: u64) -> bool { - height >= MINE_ACTIONS_PER_ANCHOR_LIMIT_ACTIVATION_HEIGHT -} - pub(super) fn mine_anchor(transaction: &Transaction) -> Option<&str> { match transaction { Transaction::Mine { anchor, .. } => Some(anchor.as_str()), diff --git a/src/domain/protocol.rs b/src/domain/protocol.rs @@ -13,10 +13,6 @@ pub const RECOVERY_BLOCK_DELAY_MS: u64 = VDF_TARGET_BLOCK_MS * 6; pub const MAX_VDF_ROUNDS: u64 = i64::MAX as u64; pub const MINE_DIFFICULTY_BITS: u32 = 12; pub const MINE_ACTIONS_PER_ANCHOR_LIMIT: usize = 2; -pub const MINE_ACTIONS_PER_ANCHOR_LIMIT_ACTIVATION_HEIGHT: u64 = 200; -pub const FALLBACK_VDF_RETARGET_ACTIVATION_HEIGHT: u64 = 380; -pub const FALLBACK_VDF_RETARGET_DEACTIVATION_HEIGHT: u64 = 1_000; -pub const AGGREGATE_FINALIZER_FEE_ACTIVATION_HEIGHT: u64 = 795; pub const MAX_BLINDED_TRANSACTION_EXPIRY_HEIGHTS: u64 = 20; pub const REVEAL_COMMITTEE_SIZE: usize = 3; pub const MAX_REVEAL_BUNDLE_BYTES: usize = 10_000; diff --git a/src/domain/tests.rs b/src/domain/tests.rs @@ -78,20 +78,6 @@ fn test_mine_with_salt(ledger: &Ledger, recipient: &str, salt: u64) -> Transacti panic!("test should find a valid mine action"); } -fn advance_to_mine_anchor_limit_activation_parent(ledger: &mut Ledger, wallet: &Wallet) { - while ledger.height().saturating_add(1) < MINE_ACTIONS_PER_ANCHOR_LIMIT_ACTIVATION_HEIGHT { - let timestamp_ms = ledger - .tip() - .timestamp_ms - .saturating_add(VDF_TARGET_BLOCK_MS); - apply_preverified_burn_block_at(ledger, wallet, timestamp_ms); - } - assert_eq!( - ledger.height().saturating_add(1), - MINE_ACTIONS_PER_ANCHOR_LIMIT_ACTIVATION_HEIGHT - ); -} - fn apply_preverified_burn_block_at( ledger: &mut Ledger, wallet: &Wallet, @@ -262,17 +248,6 @@ fn prepare_preverified_as_next_leader_with_reveal_bundles( prepared.finish(wallet, "preverified-vdf".to_string()) } -fn advance_preverified_to_height(ledger: &mut Ledger, wallets: &[Wallet], target_height: u64) { - while ledger.height() < target_height { - queue_next_leader_burn(ledger, wallets); - let timestamp_ms = ledger - .tip() - .timestamp_ms - .saturating_add(VDF_TARGET_BLOCK_MS); - mine_preverified_as_next_leader(ledger, wallets, timestamp_ms); - } -} - fn queue_next_leader_burn(ledger: &mut Ledger, wallets: &[Wallet]) { let leader = ledger.expected_leader_for_next_block().unwrap(); let wallet = wallet_for_address(wallets, &leader); @@ -741,36 +716,10 @@ fn vdf_retarget_observed_block_time_is_clamped() { } #[test] -fn vdf_retarget_observed_block_time_includes_historical_ticket_fallback_ranks() { +fn vdf_retarget_observed_block_time_ignores_ticket_fallback_ranks() { let parent = vdf_retarget_sample_block(0, FinalizerMode::Ticket, 0); - let primary_child = vdf_retarget_sample_block(VDF_TARGET_BLOCK_MS, FinalizerMode::Ticket, 0); - let mut rank_one_child = + let fallback_child = vdf_retarget_sample_block(VDF_TARGET_BLOCK_MS * 2, FinalizerMode::Ticket, 1); - rank_one_child.height = FALLBACK_VDF_RETARGET_ACTIVATION_HEIGHT; - let mut rank_two_child = - vdf_retarget_sample_block(VDF_TARGET_BLOCK_MS * 4, FinalizerMode::Ticket, 2); - rank_two_child.height = FALLBACK_VDF_RETARGET_DEACTIVATION_HEIGHT - 1; - - assert_eq!( - vdf_retarget_observed_block_ms(&parent, &primary_child), - Some(VDF_TARGET_BLOCK_MS) - ); - assert_eq!( - vdf_retarget_observed_block_ms(&parent, &rank_one_child), - Some(VDF_TARGET_BLOCK_MS * 2) - ); - assert_eq!( - vdf_retarget_observed_block_ms(&parent, &rank_two_child), - Some(VDF_TARGET_BLOCK_MS * 4) - ); -} - -#[test] -fn vdf_retarget_observed_block_time_ignores_new_ticket_fallback_ranks() { - let parent = vdf_retarget_sample_block(0, FinalizerMode::Ticket, 0); - let mut fallback_child = - vdf_retarget_sample_block(VDF_TARGET_BLOCK_MS * 2, FinalizerMode::Ticket, 1); - fallback_child.height = FALLBACK_VDF_RETARGET_DEACTIVATION_HEIGHT; assert_eq!( vdf_retarget_observed_block_ms(&parent, &fallback_child), @@ -848,7 +797,7 @@ fn vdf_rounds_retarget_below_legacy_u32_limit_after_slow_blocks() { } #[test] -fn fallback_block_before_activation_is_excluded_from_vdf_retarget_observations() { +fn fallback_block_is_excluded_from_vdf_retarget_observations() { let alice = Wallet::from_seed("fallback-retarget-alice"); let bob = Wallet::from_seed("fallback-retarget-bob"); let wallets = [&alice, &bob]; @@ -1542,10 +1491,16 @@ fn blinded_burn_commits_ciphertext_and_reveal_executes_later() { amount: committer_fee, } ); + assert!( + !ledger + .utxos + .contains_key(&blinded_executor_fee_outpoint(commitment)) + ); + assert_eq!(reveal_block.reward, reveal_finalizer_fee); assert_eq!( ledger .utxos - .get(&blinded_executor_fee_outpoint(commitment)) + .get(&reward_outpoint(&reveal_block.hash)) .unwrap(), &TxOutput { address: reveal_executor.clone(), @@ -1586,21 +1541,16 @@ fn blinded_burn_commits_ciphertext_and_reveal_executes_later() { } #[test] -fn activated_blinded_reveal_finalizer_fees_are_aggregated_into_block_reward() { - let alice = Wallet::from_seed("activated-finalizer-fee-alice"); - let bob = Wallet::from_seed("activated-finalizer-fee-bob"); - let carol = Wallet::from_seed("activated-finalizer-fee-carol"); - let dave = Wallet::from_seed("activated-finalizer-fee-dave"); +fn blinded_reveal_finalizer_fees_are_aggregated_into_block_reward() { + let alice = Wallet::from_seed("aggregated-finalizer-fee-alice"); + let bob = Wallet::from_seed("aggregated-finalizer-fee-bob"); + let carol = Wallet::from_seed("aggregated-finalizer-fee-carol"); + let dave = Wallet::from_seed("aggregated-finalizer-fee-dave"); let finalizers = [alice.clone(), bob.clone()]; let mut ledger = ledger_with_finalizers( &finalizers, &[(&carol, 10 * MICRO_IUNA), (&dave, 10 * MICRO_IUNA)], ); - advance_preverified_to_height( - &mut ledger, - &finalizers, - AGGREGATE_FINALIZER_FEE_ACTIVATION_HEIGHT - 2, - ); let first_fee = 100; let second_fee = 200; let first_blinded = ledger @@ -1624,10 +1574,7 @@ fn activated_blinded_reveal_finalizer_fees_are_aggregated_into_block_reward() { .saturating_add(VDF_TARGET_BLOCK_MS); let commit_block = mine_preverified_as_next_leader(&mut ledger, &finalizers, commit_timestamp_ms); - assert_eq!( - commit_block.height, - AGGREGATE_FINALIZER_FEE_ACTIVATION_HEIGHT - 1 - ); + assert_eq!(commit_block.height, 1); ledger.submit_blinded_reveal(first_blinded.reveal).unwrap(); ledger.submit_blinded_reveal(second_blinded.reveal).unwrap(); @@ -1661,17 +1608,14 @@ fn activated_blinded_reveal_finalizer_fees_are_aggregated_into_block_reward() { .checked_add(second_reveal_finalizer_fee) .unwrap(); - assert_eq!( - reveal_block.height, - AGGREGATE_FINALIZER_FEE_ACTIVATION_HEIGHT - ); + assert_eq!(reveal_block.height, 2); assert_eq!(reveal_block.reward, aggregate_reveal_finalizer_fee); - let mut legacy_reward_block = reveal_block.clone(); - legacy_reward_block.reward = fee_reward(&legacy_reward_block.transactions).unwrap(); - legacy_reward_block.hash = legacy_reward_block.compute_hash(); + let mut plain_fee_reward_block = reveal_block.clone(); + plain_fee_reward_block.reward = fee_reward(&plain_fee_reward_block.transactions).unwrap(); + plain_fee_reward_block.hash = plain_fee_reward_block.compute_hash(); let error = ledger .clone() - .apply_preverified_block_at(legacy_reward_block, u64::MAX) + .apply_preverified_block_at(plain_fee_reward_block, u64::MAX) .unwrap_err(); assert!(format!("{error:#}").contains("block reward is invalid")); @@ -1698,79 +1642,6 @@ fn activated_blinded_reveal_finalizer_fees_are_aggregated_into_block_reward() { } #[test] -fn pre_activation_blinded_reveal_finalizer_fee_stays_as_executor_utxo_at_boundary() { - let alice = Wallet::from_seed("pre-activated-finalizer-fee-alice"); - let bob = Wallet::from_seed("pre-activated-finalizer-fee-bob"); - let carol = Wallet::from_seed("pre-activated-finalizer-fee-carol"); - let finalizers = [alice.clone(), bob.clone()]; - let mut ledger = ledger_with_finalizers(&finalizers, &[(&carol, 10 * MICRO_IUNA)]); - advance_preverified_to_height( - &mut ledger, - &finalizers, - AGGREGATE_FINALIZER_FEE_ACTIVATION_HEIGHT - 3, - ); - let fee = 100; - let blinded = ledger - .build_blinded_burn(&carol, 3, fee, ledger.height() + 4) - .unwrap(); - let commitment = blinded.transaction.commitment.clone(); - ledger - .submit_blinded_transaction(blinded.transaction) - .unwrap(); - queue_next_leader_burn(&mut ledger, &finalizers); - let commit_timestamp_ms = ledger - .tip() - .timestamp_ms - .saturating_add(VDF_TARGET_BLOCK_MS); - let commit_block = - mine_preverified_as_next_leader(&mut ledger, &finalizers, commit_timestamp_ms); - assert_eq!( - commit_block.height, - AGGREGATE_FINALIZER_FEE_ACTIVATION_HEIGHT - 2 - ); - - ledger.submit_blinded_reveal(blinded.reveal).unwrap(); - queue_next_leader_burn(&mut ledger, &finalizers); - let reveal_timestamp_ms = ledger - .tip() - .timestamp_ms - .saturating_add(VDF_TARGET_BLOCK_MS); - let reveal_block = mine_preverified_as_next_leader_with_reveal_bundles( - &mut ledger, - &finalizers, - reveal_timestamp_ms, - ); - let reveal_finalizer_fee = blinded_reveal_finalizer_fee( - fee, - reveal_block.included_reveal_bundle_count(), - ledger - .burn_leader_ranks_for_block(reveal_block.height) - .unwrap() - .len(), - ); - - assert_eq!( - reveal_block.height, - AGGREGATE_FINALIZER_FEE_ACTIVATION_HEIGHT - 1 - ); - assert_eq!(reveal_block.reward, 0); - assert_eq!( - ledger - .utxos - .get(&blinded_executor_fee_outpoint(&commitment)), - Some(&TxOutput { - address: reveal_block.miner, - amount: reveal_finalizer_fee, - }) - ); - assert!( - !ledger - .utxos - .contains_key(&reward_outpoint(&reveal_block.hash)) - ); -} - -#[test] fn blinded_utxo_commit_exposes_and_locks_inputs_until_reveal_or_expiry() { let alice = Wallet::from_seed("blinded-lock-alice"); let bob = Wallet::from_seed("blinded-lock-bob"); @@ -2130,13 +2001,21 @@ fn blinded_reveal_with_wrong_key_is_rejected_in_block() { reveals: vec![wrong_reveal], }); prepared.reveal_bundle_section = ledger.reveal_bundle_section_from_bundles(vec![wrong_bundle]); + prepared.reward = blinded_reveal_finalizer_fee( + blinded.transaction.fee, + prepared.reveal_bundle_section.signatures.len(), + ledger.reveal_committee_for_next_block().len(), + ); let block = prepared.finish(wallet, "preverified-vdf".to_string()); let error = ledger .apply_preverified_block_at(block, u64::MAX) .unwrap_err(); - assert!(format!("{error:#}").contains("failed to decrypt blinded transaction payload")); + assert!( + format!("{error:#}").contains("decrypt blinded transaction"), + "{error:#}" + ); } #[test] @@ -2622,10 +2501,7 @@ fn block_selection_limits_mine_actions_per_anchor() { ledger.submit_transaction(first_mine.clone()).unwrap(); let second_mine = ledger.build_mine(alice.address()).unwrap(); ledger.submit_transaction(second_mine.clone()).unwrap(); - let third_mine = ledger.build_mine(alice.address()).unwrap(); - ledger.submit_transaction(third_mine.clone()).unwrap(); - - assert_eq!(ledger.pending().len(), 4); + assert_eq!(ledger.pending().len(), 3); let block = ledger.mine_next_block(&alice, 1).unwrap(); assert_eq!(block.transactions.len(), 3); @@ -2648,44 +2524,14 @@ fn block_selection_limits_mine_actions_per_anchor() { .iter() .any(|tx| tx.signature() == second_mine.signature()) ); - assert!( - !block - .transactions - .iter() - .any(|tx| tx.signature() == third_mine.signature()) - ); assert_ne!(first_mine.signature(), second_mine.signature()); - assert_ne!(second_mine.signature(), third_mine.signature()); assert_eq!(block.reward, first_mine.fee() + second_mine.fee()); } #[test] -fn pre_activation_block_may_keep_multiple_mine_actions_for_one_anchor() { - let alice = Wallet::from_seed("mine-anchor-limit-pre-activation-alice"); - let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); - assert!(ledger.height().saturating_add(1) < MINE_ACTIONS_PER_ANCHOR_LIMIT_ACTIVATION_HEIGHT); - - let first_mine = test_mine_with_salt(&ledger, alice.address(), 1); - let second_mine = test_mine_with_salt(&ledger, alice.address(), 2); - let burn = ledger.build_burn(&alice, MICRO_IUNA, 0).unwrap(); - ledger.submit_transaction(burn).unwrap(); - let mut block = ledger - .prepare_next_block(alice.address(), 1) - .unwrap() - .finish(&alice, "preverified-vdf".to_string()); - block.transactions.push(first_mine); - block.transactions.push(second_mine); - block.reward = fee_reward(&block.transactions).unwrap(); - block.hash = block.compute_hash(); - - ledger.apply_preverified_block_at(block, u64::MAX).unwrap(); -} - -#[test] -fn activated_blocks_reject_too_many_mine_actions_for_one_anchor() { - let alice = Wallet::from_seed("mine-anchor-limit-active-block-alice"); +fn blocks_reject_too_many_mine_actions_for_one_anchor() { + let alice = Wallet::from_seed("mine-anchor-limit-block-alice"); let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); - advance_to_mine_anchor_limit_activation_parent(&mut ledger, &alice); let first_mine = test_mine_with_salt(&ledger, alice.address(), 1); let second_mine = test_mine_with_salt(&ledger, alice.address(), 2); @@ -2716,10 +2562,9 @@ fn activated_blocks_reject_too_many_mine_actions_for_one_anchor() { } #[test] -fn activated_mempool_rejects_mine_actions_above_anchor_limit() { - let alice = Wallet::from_seed("mine-anchor-limit-active-mempool-alice"); +fn mempool_rejects_mine_actions_above_anchor_limit() { + let alice = Wallet::from_seed("mine-anchor-limit-mempool-alice"); let mut ledger = ledger_with_allocation(&alice, 10 * MICRO_IUNA); - advance_to_mine_anchor_limit_activation_parent(&mut ledger, &alice); let first_mine = test_mine_with_salt(&ledger, alice.address(), 1); let second_mine = test_mine_with_salt(&ledger, alice.address(), 2); diff --git a/src/domain/vdf.rs b/src/domain/vdf.rs @@ -1,11 +1,23 @@ +use std::sync::OnceLock; + +use num_bigint::BigUint; +use num_traits::{One, Zero}; use sha2::{Digest, Sha256}; -use super::{ - Block, FALLBACK_VDF_RETARGET_ACTIVATION_HEIGHT, FALLBACK_VDF_RETARGET_DEACTIVATION_HEIGHT, - FinalizerMode, MAX_VDF_ROUNDS, VDF_TARGET_BLOCK_MS, -}; +use super::{Block, FinalizerMode, MAX_VDF_ROUNDS, VDF_TARGET_BLOCK_MS}; -const VDF_MODULUS: u128 = 4_611_685_975_477_714_963; +const VDF_RSA_2048_MODULUS_DECIMAL: &str = concat!( + "2519590847565789349402718324004839857142928212620403202777713783604366202070", + "7595556264018525880784406918290641249515082189298559149176184502808489120072", + "8449926873928072877767359714183472702618963750149718246911650776133798590957", + "0009733045974880842840179742910064245869181719511874612151517265463228221686", + "9987549182422433637259085141865462043576798423387184774447920739934236584823", + "8242811981638150106748104516603773060562016196762561338441436038339044149526", + "3443219011465754445417842402092461651572335077870774981712577246796292638635", + "6373289912154831438167899885040445364023527381951378636564391212010397122822", + "120720357", +); +const VDF_ELEMENT_HEX_LEN: usize = 512; const VDF_CHALLENGE_MIN: u64 = 1_073_741_827; const MIN_VDF_ROUNDS: u64 = 1; pub(super) const VDF_RETARGET_WINDOW_BLOCKS: usize = 20; @@ -16,13 +28,13 @@ pub(super) const MAX_VDF_RETARGET_OBSERVED_BLOCK_MS: u64 = VDF_TARGET_BLOCK_MS * pub fn run_vdf(seed: &str, rounds: u64) -> String { let x = vdf_seed_element(seed); - let mut y = x; + let mut y = x.clone(); for _ in 0..rounds { - y = mul_mod(y, y); + y = square_mod(&y); } - let challenge = vdf_challenge_prime(seed, rounds, y); - let proof = vdf_proof(x, rounds, challenge); + let challenge = vdf_challenge_prime(seed, rounds, &y); + let proof = vdf_proof(&x, rounds, challenge); encode_vdf_solution(y, proof) } @@ -30,14 +42,17 @@ pub fn verify_vdf(seed: &str, rounds: u64, solution: &str) -> bool { let Some((y, proof)) = decode_vdf_solution(solution) else { return false; }; - if y == 0 || y >= VDF_MODULUS || proof >= VDF_MODULUS { + if y.is_zero() || y >= *vdf_modulus() || proof >= *vdf_modulus() { return false; } let x = vdf_seed_element(seed); - let challenge = vdf_challenge_prime(seed, rounds, y); - let remainder = pow_mod_small(2, rounds, challenge) as u128; - let verified = mul_mod(mod_pow(proof, challenge as u128), mod_pow(x, remainder)); + let challenge = vdf_challenge_prime(seed, rounds, &y); + let remainder = BigUint::from(pow_mod_small(2, rounds, challenge)); + let verified = mul_mod( + &proof.modpow(&BigUint::from(challenge), vdf_modulus()), + &x.modpow(&remainder, vdf_modulus()), + ); verified == y } @@ -72,10 +87,7 @@ pub(super) fn vdf_retarget_observed_block_ms(parent: &Block, child: &Block) -> O if child.finalizer_mode != FinalizerMode::Ticket { return None; } - if child.finalizer_rank != 0 - && (child.height < FALLBACK_VDF_RETARGET_ACTIVATION_HEIGHT - || child.height >= FALLBACK_VDF_RETARGET_DEACTIVATION_HEIGHT) - { + if child.finalizer_rank != 0 { return None; } @@ -84,14 +96,44 @@ pub(super) fn vdf_retarget_observed_block_ms(parent: &Block, child: &Block) -> O )) } -fn vdf_seed_element(seed: &str) -> u128 { - let digest = Sha256::digest(format!("iuna-vdf-seed:{seed}").as_bytes()); - let mut bytes = [0_u8; 16]; - bytes.copy_from_slice(&digest[..16]); - 2 + (u128::from_be_bytes(bytes) % (VDF_MODULUS - 3)) +fn vdf_modulus() -> &'static BigUint { + static MODULUS: OnceLock<BigUint> = OnceLock::new(); + MODULUS.get_or_init(|| { + BigUint::parse_bytes(VDF_RSA_2048_MODULUS_DECIMAL.as_bytes(), 10) + .expect("VDF RSA-2048 modulus must parse") + }) +} + +fn vdf_seed_element(seed: &str) -> BigUint { + let one = BigUint::one(); + let two = BigUint::from(2_u32); + for attempt in 0_u32.. { + let candidate = hash_to_modulus("iuna-vdf-seed-v2", seed, attempt); + if candidate <= one { + continue; + } + let element = candidate.modpow(&two, vdf_modulus()); + if element > one { + return element; + } + } + unreachable!("VDF seed hashing must eventually produce a usable element") +} + +fn hash_to_modulus(domain: &str, seed: &str, attempt: u32) -> BigUint { + let byte_len = vdf_modulus().bits().div_ceil(8) as usize; + let mut bytes = Vec::with_capacity(byte_len); + let mut counter = 0_u32; + while bytes.len() < byte_len { + let digest = Sha256::digest(format!("{domain}:{seed}:{attempt}:{counter}").as_bytes()); + bytes.extend_from_slice(&digest); + counter = counter.saturating_add(1); + } + bytes.truncate(byte_len); + BigUint::from_bytes_be(&bytes) % vdf_modulus() } -fn vdf_challenge_prime(seed: &str, rounds: u64, output: u128) -> u64 { +fn vdf_challenge_prime(seed: &str, rounds: u64, output: &BigUint) -> u64 { let digest = Sha256::digest(format!("iuna-vdf-challenge:{seed}:{rounds}:{output:x}")); let mut bytes = [0_u8; 8]; bytes.copy_from_slice(&digest[..8]); @@ -99,50 +141,45 @@ fn vdf_challenge_prime(seed: &str, rounds: u64, output: u128) -> u64 { next_odd_prime(candidate | 1) } -fn vdf_proof(x: u128, rounds: u64, challenge: u64) -> u128 { - let mut proof = 1_u128; +fn vdf_proof(x: &BigUint, rounds: u64, challenge: u64) -> BigUint { + let mut proof = BigUint::one(); let mut remainder = 1_u64 % challenge; for _ in 0..rounds { let doubled = remainder * 2; let carry = doubled >= challenge; - proof = mul_mod(proof, proof); + proof = square_mod(&proof); if carry { - proof = mul_mod(proof, x); + proof = mul_mod(&proof, x); } remainder = doubled % challenge; } proof } -fn encode_vdf_solution(output: u128, proof: u128) -> String { - format!("{output:032x}:{proof:032x}") +fn encode_vdf_solution(output: BigUint, proof: BigUint) -> String { + format!( + "{output:0>width$x}:{proof:0>width$x}", + width = VDF_ELEMENT_HEX_LEN + ) } -fn decode_vdf_solution(solution: &str) -> Option<(u128, u128)> { +fn decode_vdf_solution(solution: &str) -> Option<(BigUint, BigUint)> { let (output, proof) = solution.split_once(':')?; - if output.len() != 32 || proof.len() != 32 { + if output.len() != VDF_ELEMENT_HEX_LEN || proof.len() != VDF_ELEMENT_HEX_LEN { return None; } Some(( - u128::from_str_radix(output, 16).ok()?, - u128::from_str_radix(proof, 16).ok()?, + BigUint::parse_bytes(output.as_bytes(), 16)?, + BigUint::parse_bytes(proof.as_bytes(), 16)?, )) } -fn mul_mod(left: u128, right: u128) -> u128 { - (left * right) % VDF_MODULUS +fn square_mod(value: &BigUint) -> BigUint { + mul_mod(value, value) } -fn mod_pow(mut base: u128, mut exponent: u128) -> u128 { - let mut result = 1_u128; - while exponent > 0 { - if exponent & 1 == 1 { - result = mul_mod(result, base); - } - base = mul_mod(base, base); - exponent >>= 1; - } - result +fn mul_mod(left: &BigUint, right: &BigUint) -> BigUint { + (left * right) % vdf_modulus() } fn pow_mod_small(base: u64, exponent: u64, modulus: u64) -> u64 { @@ -183,7 +220,7 @@ fn is_odd_prime(candidate: u64) -> bool { #[cfg(test)] mod tests { - use super::{run_vdf, verify_vdf}; + use super::{VDF_ELEMENT_HEX_LEN, pow_mod_small, run_vdf, vdf_modulus, verify_vdf}; #[test] fn vdf_solution_verifies_and_is_bound_to_seed_and_rounds() { @@ -194,4 +231,51 @@ mod tests { assert!(!verify_vdf("test-seed", 129, &solution)); assert!(!verify_vdf("test-seed", 128, "not-a-vdf-solution")); } + + #[test] + fn vdf_solution_uses_2048_bit_elements() { + let solution = run_vdf("test-seed", 16); + let (output, proof) = solution.split_once(':').unwrap(); + + assert_eq!(output.len(), VDF_ELEMENT_HEX_LEN); + assert_eq!(proof.len(), VDF_ELEMENT_HEX_LEN); + assert!(vdf_modulus().bits() >= 2048); + } + + #[test] + fn legacy_factorable_modulus_attack_is_not_the_active_modulus() { + const LEGACY_MODULUS: u128 = 4_611_685_975_477_714_963; + const LEGACY_P: u128 = 2_147_483_629; + const LEGACY_Q: u128 = 2_147_483_647; + assert_eq!(LEGACY_P * LEGACY_Q, LEGACY_MODULUS); + assert_ne!(vdf_modulus().to_str_radix(10), LEGACY_MODULUS.to_string()); + + let phi = (LEGACY_P - 1) * (LEGACY_Q - 1); + let seed = 42_u128; + let rounds = 10_000_u64; + let sequential = legacy_repeated_squaring(seed, rounds, LEGACY_MODULUS); + let shortcut_exponent = pow_mod_small(2, rounds, phi as u64) as u128; + let shortcut = legacy_mod_pow(seed, shortcut_exponent, LEGACY_MODULUS); + + assert_eq!(shortcut, sequential); + } + + fn legacy_repeated_squaring(mut value: u128, rounds: u64, modulus: u128) -> u128 { + for _ in 0..rounds { + value = (value * value) % modulus; + } + value + } + + fn legacy_mod_pow(mut base: u128, mut exponent: u128, modulus: u128) -> u128 { + let mut result = 1_u128; + while exponent > 0 { + if exponent & 1 == 1 { + result = (result * base) % modulus; + } + base = (base * base) % modulus; + exponent >>= 1; + } + result + } } diff --git a/src/main.rs b/src/main.rs @@ -34,8 +34,8 @@ use cli::{default_data_dir, help_text}; const GENESIS_BOOTSTRAP_BURN_AMOUNT: Amount = MICRO_IUNA; const GENESIS_INITIAL_BURN_PER_BLOCK: Amount = config_store::DEFAULT_BURN_AMOUNT; const GENESIS_INITIAL_BURN_FEE: Amount = config_store::DEFAULT_BURN_FEE; -const VDF_MEASUREMENT_INITIAL_ROUNDS: u64 = 1_000_000; -const VDF_MEASUREMENT_MAX_ROUNDS: u64 = 100_000_000; +const VDF_MEASUREMENT_INITIAL_ROUNDS: u64 = 1_000; +const VDF_MEASUREMENT_MAX_ROUNDS: u64 = 10_000_000; const VDF_MEASUREMENT_MIN_ELAPSED: Duration = Duration::from_millis(150); #[tokio::main] diff --git a/src/main_tests.rs b/src/main_tests.rs @@ -636,28 +636,31 @@ async fn persistence_loop_saves_new_tip_after_node_changes() { let expected_tip = node.lock().await.ledger().status().tip_hash; let mut restored_tip = None; - for _ in 0..50 { + let mut projected_tip = None; + for _ in 0..100 { if let Some(snapshot) = store.load().unwrap() { restored_tip = snapshot.blocks.last().map(|block| block.hash.clone()); - if restored_tip.as_deref() == Some(expected_tip.as_str()) { - break; - } + } + projected_tip = Connection::open(ui_data_store.path()) + .unwrap() + .query_row( + "SELECT tip_hash FROM ui_cache_meta WHERE id = 1", + [], + |row| row.get::<_, String>(0), + ) + .ok(); + if restored_tip.as_deref() == Some(expected_tip.as_str()) + && projected_tip.as_deref() == Some(expected_tip.as_str()) + { + break; } tokio::time::sleep(Duration::from_millis(10)).await; } persistence_task.abort(); assert_eq!(restored_tip.as_deref(), Some(expected_tip.as_str())); + assert_eq!(projected_tip.as_deref(), Some(expected_tip.as_str())); assert!(ui_data_store.load_metrics().unwrap().is_empty()); - let ui_data_connection = Connection::open(ui_data_store.path()).unwrap(); - let projected_tip: String = ui_data_connection - .query_row( - "SELECT tip_hash FROM ui_cache_meta WHERE id = 1", - [], - |row| row.get(0), - ) - .unwrap(); - assert_eq!(projected_tip, expected_tip); } #[tokio::test]