commit 2369bf40de462316cffdfbd50c183766ec7f5ba6
parent 7b1cd479f0ac5e5fafc6b033abaa8c0e6c06159c
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Thu, 27 Aug 2026 00:29:11 +0200
fix(consensus): resist block grinding from height 1000
Diffstat:
10 files changed, 332 insertions(+), 52 deletions(-)
diff --git a/docs/operator-playbooks.md b/docs/operator-playbooks.md
@@ -147,6 +147,26 @@ Avoid:
- deleting or replacing wallets as part of the chain reset;
- starting before the published genesis hash and release checksum are available.
+## Height 1000 Grinding-Resistance Upgrade
+
+Height `1000` is a coordinated consensus activation. At that height, VDF seeds
+start committing to block content and ticket draws stop using the final block
+hash. This preserves blocks below `1000`, but nodes running the earlier rule will
+reject the upgraded chain or build an incompatible fork at activation.
+
+Before height `1000`:
+
+1. Publish a tagged release, commit, checksums, and the activation height.
+2. Upgrade every known public peer, finalizer, and bootstrap node.
+3. Verify the reported package version on each managed node and compare tips.
+4. Stop or isolate nodes that cannot be upgraded before activation.
+5. Keep chain database backups from immediately before the activation window.
+
+At and after height `1000`, compare height and tip hash across at least three
+independent nodes. If upgraded nodes disagree, preserve both histories and stop
+automated restarts; do not reset the apparent majority until both forks have
+been validated.
+
## No Burn Committee Signatures
Symptoms:
diff --git a/docs/protocol.md b/docs/protocol.md
@@ -43,7 +43,8 @@ The current mainnet-candidate parameter set is intentionally close to Bitcoin wh
- burn committee size: `5` slots;
- maximum signed burn bundle size: `10,000` bytes;
- burn committee lineage maturity: `20` blocks;
-- fallback ticket invalidation activation height: `300`.
+- fallback ticket invalidation activation height: `300`;
+- grinding-resistance activation height: `1000`.
Changing any value in this section requires a conscious mainnet-candidate reset or later hard-fork process.
@@ -78,6 +79,8 @@ In the mainnet-candidate profile, tickets mature after `3` blocks and remain eli
The lottery draw for the next height is deterministic. Nodes rank all eligible burn tickets using the parent block hash, the parent VDF output, the target height, and the ticket amounts. More burned IUNA means more weight, but the winner is still drawn by the protocol.
+From height `1000`, leader selection uses the parent's VDF seed and VDF output instead of the parent's final block hash. Blocks at height `1000` and later commit the finalizer identity, mode, rank, reward, VDF rounds, leader ticket, transactions, and burn-bundle section into their VDF seed. Together these rules prevent a finalizer from completing one VDF and then cheaply varying transaction selection or the publication timestamp to grind the next leader. Ticket-block timestamps remain adjustable to the actual completion time, but they no longer influence the next lottery draw. Earlier candidate history retains the original parent-hash lottery rule.
+
## Finalizing Blocks
For each block height, eligible tickets are ranked:
diff --git a/docs/security-review.md b/docs/security-review.md
@@ -229,6 +229,9 @@ see which revision was tested.
playbooks are the maintained in-tree references.
- Release evidence: keep successful release-gate logs from the exact tagged
candidate revision.
+- Grinding resistance: height `1000` activates a VDF content commitment and
+ removes the final block hash from future ticket draws. All candidate nodes
+ must upgrade before activation; mixed versions will split at height `1000`.
## Sign-Off Table
diff --git a/src/domain.rs b/src/domain.rs
@@ -45,7 +45,8 @@ use ledger_lineage::{
pub use ledger_ops::reward_outputs_for_block;
use ledger_ops::{
apply_transaction, credit_reward_output, ensure_block_has_burn, ensure_block_has_burn_from,
- recovery_vdf_seed_for_child, validate_genesis_burn_transaction, vdf_seed_for_child,
+ recovery_vdf_seed_for_child, validate_genesis_burn_transaction, vdf_content_commitment,
+ vdf_seed_for_child,
};
pub(crate) use ledger_pending::MINE_ANCHOR_LIMIT_REACHED;
pub use ledger_state::Ledger;
@@ -55,10 +56,10 @@ use mining::{mine_payload, mine_signature};
pub use profile::{GenesisBurn, LaunchProfile};
pub use protocol::{
Amount, BLOCK_REWARD, BURN_COMMITTEE_SIZE, BURN_LINEAGE_MATURITY_HEIGHTS, DEFAULT_FEE_PER_BYTE,
- DEFAULT_MINE_FEE, DEFAULT_TRANSACTION_FEE, MAX_BLOCK_BYTES, MAX_BURN_BUNDLE_BYTES,
- MAX_PENDING_TRANSACTIONS, MAX_VDF_ROUNDS, MICRO_IUNA, MINE_ACTIONS_PER_ANCHOR_LIMIT,
- MINE_DIFFICULTY_BITS, MINE_FINALIZER_FEE, MINE_REWARD, RECOVERY_BLOCK_DELAY_MS,
- TransactionSubmitOutcome, VDF_TARGET_BLOCK_MS,
+ DEFAULT_MINE_FEE, DEFAULT_TRANSACTION_FEE, GRINDING_RESISTANCE_ACTIVATION_HEIGHT,
+ MAX_BLOCK_BYTES, MAX_BURN_BUNDLE_BYTES, MAX_PENDING_TRANSACTIONS, MAX_VDF_ROUNDS, MICRO_IUNA,
+ MINE_ACTIONS_PER_ANCHOR_LIMIT, MINE_DIFFICULTY_BITS, MINE_FINALIZER_FEE, MINE_REWARD,
+ RECOVERY_BLOCK_DELAY_MS, TransactionSubmitOutcome, VDF_TARGET_BLOCK_MS,
};
use protocol::{
BLOCK_MEDIAN_TIME_PAST_WINDOW, DEFAULT_TICKET_EXPIRY_WINDOW, DEFAULT_TICKET_MATURITY_DELAY,
diff --git a/src/domain/adversarial_tests.rs b/src/domain/adversarial_tests.rs
@@ -12,9 +12,10 @@ use super::ticket::{
use super::{
Amount, BURN_COMMITTEE_SIZE, BURN_LINEAGE_MATURITY_HEIGHTS, Block, BurnBundle,
BurnBundleSignature, BurnCommitteeMember, BurnLeaderRank, ChainSnapshot, FinalizerMode,
- GenesisBurn, LeaderProofPayload, Ledger, MAX_BLOCK_BYTES, MAX_BURN_BUNDLE_BYTES, MICRO_IUNA,
- MaskedBurn, OutPoint, Transaction, TransactionSubmitOutcome, TxOutput, UtxoLineageRoot,
- VDF_TARGET_BLOCK_MS, Wallet, hex_hash, reward_outputs_for_block, run_vdf,
+ GRINDING_RESISTANCE_ACTIVATION_HEIGHT, GenesisBurn, LeaderProofPayload, Ledger,
+ MAX_BLOCK_BYTES, MAX_BURN_BUNDLE_BYTES, MICRO_IUNA, MaskedBurn, OutPoint, Transaction,
+ TransactionSubmitOutcome, TxOutput, UtxoLineageRoot, VDF_TARGET_BLOCK_MS, Wallet, hex_hash,
+ reward_outputs_for_block, run_vdf,
};
const NOW_MS: u64 = 10_000_000_000;
@@ -1388,15 +1389,17 @@ fn mini_weighted_ticket_draw(
rank: u32,
total_weight: u128,
) -> u128 {
+ let parent_randomness = if target_height >= GRINDING_RESISTANCE_ACTIVATION_HEIGHT {
+ format!("{}:{}", parent.vdf_seed(), parent.vdf_output)
+ } else {
+ format!("{}:{}", parent.hash, parent.vdf_output)
+ };
let seed = if rank == 0 {
- format!(
- "iuna-ticket-draw:{}:{}:{}",
- target_height, parent.hash, parent.vdf_output
- )
+ format!("iuna-ticket-draw:{}:{}", target_height, parent_randomness)
} else {
format!(
- "iuna-ticket-draw-rank:{}:{}:{}:{}",
- target_height, rank, parent.hash, parent.vdf_output
+ "iuna-ticket-draw-rank:{}:{}:{}",
+ target_height, rank, parent_randomness
)
};
let digest = Sha256::digest(seed.as_bytes());
diff --git a/src/domain/block.rs b/src/domain/block.rs
@@ -5,7 +5,8 @@ use serde::{Deserialize, Serialize};
use super::{
Amount, BURN_COMMITTEE_SIZE, BurnBundleSection, BurnTicket, LaunchProfile, LeaderScore,
- Transaction, Wallet, hex_hash, recovery_vdf_seed_for_child, vdf_seed_for_child,
+ Transaction, Wallet, hex_hash, recovery_vdf_seed_for_child, vdf_content_commitment,
+ vdf_seed_for_child,
};
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
@@ -48,19 +49,41 @@ impl Block {
pub fn vdf_seed(&self) -> String {
let bundle_hashes = self.burn_bundle_hashes();
+ let content_commitment = self.vdf_content_commitment();
match self.finalizer_mode {
- FinalizerMode::Ticket => {
- vdf_seed_for_child(&self.prev_hash, self.height, &bundle_hashes)
- }
+ FinalizerMode::Ticket => vdf_seed_for_child(
+ &self.prev_hash,
+ self.height,
+ &bundle_hashes,
+ &content_commitment,
+ ),
FinalizerMode::Recovery => recovery_vdf_seed_for_child(
&self.prev_hash,
self.height,
self.timestamp_ms,
&bundle_hashes,
+ &content_commitment,
),
}
}
+ fn vdf_content_commitment(&self) -> String {
+ vdf_content_commitment(
+ self.height,
+ &self.prev_hash,
+ &self.miner,
+ self.finalizer_mode,
+ self.finalizer_rank,
+ self.reward,
+ self.vdf_rounds,
+ self.leader_proof
+ .as_ref()
+ .map(|proof| proof.ticket_id.as_str()),
+ &self.burn_bundle_section,
+ &self.transactions,
+ )
+ }
+
fn content_hash(&self) -> String {
let txs = self
.transactions
@@ -317,7 +340,10 @@ mod tests {
use super::{
Block, BurnBundleSection, FinalizerMode, LeaderProofPayload, canonical_burn_block_items,
};
- use crate::domain::Transaction;
+ use crate::domain::{
+ BurnTicket, GRINDING_RESISTANCE_ACTIVATION_HEIGHT, PreparedBlock, Transaction, Wallet,
+ run_vdf, verify_vdf,
+ };
#[test]
fn primary_leader_proof_payload_omits_rank_from_canonical_form() {
@@ -377,6 +403,105 @@ mod tests {
}
#[test]
+ fn post_activation_vdf_binds_transactions_but_allows_ticket_timestamp_completion() {
+ let mut block = Block {
+ height: GRINDING_RESISTANCE_ACTIVATION_HEIGHT,
+ prev_hash: "0".repeat(64),
+ timestamp_ms: 1,
+ miner: "1".repeat(64),
+ finalizer_mode: FinalizerMode::Ticket,
+ finalizer_rank: 0,
+ reward: 1,
+ vdf_rounds: 16,
+ vdf_output: String::new(),
+ leader_proof: None,
+ burn_bundle_section: BurnBundleSection::default(),
+ transactions: vec![Transaction::genesis_burn("2".repeat(64), 1)],
+ hash: String::new(),
+ };
+ let original_seed = block.vdf_seed();
+ let output = run_vdf(&original_seed, block.vdf_rounds);
+ block.vdf_output = output.clone();
+ assert!(verify_vdf(&block.vdf_seed(), block.vdf_rounds, &output));
+
+ let mut changed_transactions = block.clone();
+ changed_transactions.transactions = vec![Transaction::genesis_burn("3".repeat(64), 1)];
+ assert_ne!(changed_transactions.vdf_seed(), original_seed);
+ assert!(!verify_vdf(
+ &changed_transactions.vdf_seed(),
+ changed_transactions.vdf_rounds,
+ &output
+ ));
+
+ let mut completed_later = block.clone();
+ completed_later.timestamp_ms += 1_000;
+ assert_eq!(completed_later.vdf_seed(), original_seed);
+ assert!(verify_vdf(
+ &completed_later.vdf_seed(),
+ completed_later.vdf_rounds,
+ &output
+ ));
+ }
+
+ #[test]
+ fn prepared_and_finished_blocks_share_post_activation_vdf_commitment() {
+ let wallet = Wallet::from_seed("vdf-commitment-finalizer");
+ let transactions = vec![Transaction::genesis_burn(wallet.address(), 1)];
+ let mut prepared = PreparedBlock {
+ height: GRINDING_RESISTANCE_ACTIVATION_HEIGHT,
+ prev_hash: "0".repeat(64),
+ timestamp_ms: 10,
+ miner: wallet.address().to_string(),
+ finalizer_mode: FinalizerMode::Ticket,
+ finalizer_rank: 0,
+ reward: 1,
+ vdf_rounds: 1,
+ vdf_seed: String::new(),
+ leader_ticket: Some(BurnTicket {
+ id: "4".repeat(64),
+ owner: wallet.address().to_string(),
+ amount: 1,
+ eligible_from_height: GRINDING_RESISTANCE_ACTIVATION_HEIGHT,
+ eligible_until_height: GRINDING_RESISTANCE_ACTIVATION_HEIGHT,
+ }),
+ burn_bundle_section: BurnBundleSection::default(),
+ transactions,
+ };
+ let bundle_hashes = prepared.burn_bundle_section.burn_bundle_hashes(
+ prepared.height,
+ &prepared.prev_hash,
+ &prepared.miner,
+ );
+ let commitment = super::vdf_content_commitment(
+ prepared.height,
+ &prepared.prev_hash,
+ &prepared.miner,
+ prepared.finalizer_mode,
+ prepared.finalizer_rank,
+ prepared.reward,
+ prepared.vdf_rounds,
+ prepared
+ .leader_ticket
+ .as_ref()
+ .map(|ticket| ticket.id.as_str()),
+ &prepared.burn_bundle_section,
+ &prepared.transactions,
+ );
+ prepared.vdf_seed = super::vdf_seed_for_child(
+ &prepared.prev_hash,
+ prepared.height,
+ &bundle_hashes,
+ &commitment,
+ );
+ let expected_seed = prepared.vdf_seed.clone();
+ let output = run_vdf(&expected_seed, prepared.vdf_rounds);
+
+ let block = prepared.finish_at(&wallet, output, 20);
+
+ assert_eq!(block.vdf_seed(), expected_seed);
+ }
+
+ #[test]
fn json_block_size_uses_canonical_node_representation() {
let compact_wire_json = format!(
r#"{{"height":1,"prev_hash":"{}","timestamp_ms":1,"miner":"{}","reward":0,"vdf_rounds":1,"vdf_output":"out","leader_proof":null,"transactions":[],"hash":"{}"}}"#,
diff --git a/src/domain/ledger_ops.rs b/src/domain/ledger_ops.rs
@@ -13,9 +13,10 @@ use super::transaction::Transaction;
use super::vdf::vdf_solution_placeholder;
use super::{
Amount, BURN_COMMITTEE_SIZE, Block, BlockSelection, BurnCommitteeMember, BurnTicket,
- FinalizerMode, LeaderProof, LeaderProofPayload, Ledger, MINE_REWARD, OutPoint,
- PUBLIC_KEY_BYTES, RECOVERY_BLOCK_DELAY_MS, SIGNATURE_BYTES, TxInput, TxOutput,
- decode_hex_array, validate_address, validate_hash, validate_protocol_id, validate_signature,
+ FinalizerMode, GRINDING_RESISTANCE_ACTIVATION_HEIGHT, LeaderProof, LeaderProofPayload, Ledger,
+ MINE_REWARD, OutPoint, PUBLIC_KEY_BYTES, RECOVERY_BLOCK_DELAY_MS, SIGNATURE_BYTES, TxInput,
+ TxOutput, decode_hex_array, validate_address, validate_hash, validate_protocol_id,
+ validate_signature,
};
pub(super) fn compact_block_context(ledger: &Ledger) -> &CompactBlockContext {
@@ -198,9 +199,16 @@ pub(super) fn vdf_seed_for_child(
prev_hash: &str,
height: u64,
bundle_hashes: &[String; super::BURN_COMMITTEE_SIZE],
+ content_commitment: &str,
) -> String {
+ if height < GRINDING_RESISTANCE_ACTIVATION_HEIGHT {
+ return hex_hash(format!(
+ "iuna-vdf-child:{prev_hash}:{height}:{}",
+ canonical_burn_bundle_hashes(bundle_hashes)
+ ));
+ }
hex_hash(format!(
- "iuna-vdf-child:{prev_hash}:{height}:{}",
+ "iuna-vdf-child-v2:{prev_hash}:{height}:{content_commitment}:{}",
canonical_burn_bundle_hashes(bundle_hashes)
))
}
@@ -210,13 +218,55 @@ pub(super) fn recovery_vdf_seed_for_child(
height: u64,
timestamp_ms: u64,
bundle_hashes: &[String; super::BURN_COMMITTEE_SIZE],
+ content_commitment: &str,
) -> String {
+ if height < GRINDING_RESISTANCE_ACTIVATION_HEIGHT {
+ return hex_hash(format!(
+ "iuna-recovery-vdf-child:{prev_hash}:{height}:{timestamp_ms}:{}",
+ canonical_burn_bundle_hashes(bundle_hashes)
+ ));
+ }
hex_hash(format!(
- "iuna-recovery-vdf-child:{prev_hash}:{height}:{timestamp_ms}:{}",
+ "iuna-recovery-vdf-child-v2:{prev_hash}:{height}:{timestamp_ms}:{content_commitment}:{}",
canonical_burn_bundle_hashes(bundle_hashes)
))
}
+#[allow(clippy::too_many_arguments)]
+pub(super) fn vdf_content_commitment(
+ height: u64,
+ prev_hash: &str,
+ miner: &str,
+ finalizer_mode: FinalizerMode,
+ finalizer_rank: u32,
+ reward: Amount,
+ vdf_rounds: u64,
+ leader_ticket_id: Option<&str>,
+ burn_bundle_section: &BurnBundleSection,
+ transactions: &[Transaction],
+) -> String {
+ let mode = match finalizer_mode {
+ FinalizerMode::Ticket => "ticket",
+ FinalizerMode::Recovery => "recovery",
+ };
+ let ticket_id = leader_ticket_id.unwrap_or("none");
+ let transaction_hash = hex_hash(format!(
+ "iuna-vdf-transactions-v1:{}",
+ transactions
+ .iter()
+ .map(Transaction::canonical)
+ .collect::<Vec<_>>()
+ .join("|")
+ ));
+ let burn_section_hash = hex_hash(format!(
+ "iuna-vdf-burn-section-v1:{}",
+ burn_bundle_section.canonical()
+ ));
+ hex_hash(format!(
+ "iuna-vdf-content-v1:{height}:{prev_hash}:{miner}:{mode}:{finalizer_rank}:{reward}:{vdf_rounds}:{ticket_id}:{transaction_hash}:{burn_section_hash}"
+ ))
+}
+
pub(super) fn apply_transaction(
transaction: &Transaction,
utxos: &mut BTreeMap<OutPoint, TxOutput>,
@@ -792,15 +842,27 @@ mod tests {
#[test]
fn recovery_vdf_seed_binds_timestamp_while_ticket_seed_does_not() {
let hashes = std::array::from_fn(super::super::default_burn_bundle_hash);
- let ticket_seed = vdf_seed_for_child(&"a".repeat(64), 42, &hashes);
- let recovery_at_one = recovery_vdf_seed_for_child(&"a".repeat(64), 42, 1, &hashes);
- let recovery_at_two = recovery_vdf_seed_for_child(&"a".repeat(64), 42, 2, &hashes);
+ let ticket_seed = vdf_seed_for_child(&"a".repeat(64), 42, &hashes, "content");
+ let recovery_at_one =
+ recovery_vdf_seed_for_child(&"a".repeat(64), 42, 1, &hashes, "content");
+ let recovery_at_two =
+ recovery_vdf_seed_for_child(&"a".repeat(64), 42, 2, &hashes, "content");
assert_ne!(ticket_seed, recovery_at_one);
assert_ne!(recovery_at_one, recovery_at_two);
assert_eq!(
ticket_seed,
- vdf_seed_for_child(&"a".repeat(64), 42, &hashes)
+ vdf_seed_for_child(&"a".repeat(64), 42, &hashes, "content")
+ );
+ assert_eq!(
+ ticket_seed,
+ vdf_seed_for_child(&"a".repeat(64), 42, &hashes, "different-content")
+ );
+
+ let activated = GRINDING_RESISTANCE_ACTIVATION_HEIGHT;
+ assert_ne!(
+ vdf_seed_for_child(&"a".repeat(64), activated, &hashes, "content"),
+ vdf_seed_for_child(&"a".repeat(64), activated, &hashes, "different-content")
);
}
}
diff --git a/src/domain/ledger_prepare.rs b/src/domain/ledger_prepare.rs
@@ -3,7 +3,7 @@ use anyhow::{Result, bail};
use super::{
BurnBundle, FinalizerMode, Ledger, PreparedBlock, RECOVERY_BLOCK_DELAY_MS, Wallet,
ensure_block_has_burn, ensure_block_has_burn_from, recovery_vdf_seed_for_child, run_vdf,
- ticket_block_min_timestamp, vdf_seed_for_child,
+ ticket_block_min_timestamp, vdf_content_commitment, vdf_seed_for_child,
};
impl Ledger {
@@ -67,16 +67,30 @@ impl Ledger {
let prev_hash = tip.hash.clone();
let timestamp_ms = timestamp_ms.max(ticket_block_min_timestamp(tip, finalizer_rank)?);
let bundle_hashes = burn_bundle_section.burn_bundle_hashes(height, &prev_hash, miner);
- let vdf_seed = vdf_seed_for_child(&prev_hash, height, &bundle_hashes);
+ let reward =
+ self.expected_reward_for_next_block(&selection.transactions, &burn_bundle_section)?;
+ let vdf_rounds = self.vdf_rounds_for_finalizer_rank(finalizer_rank)?;
+ let content_commitment = vdf_content_commitment(
+ height,
+ &prev_hash,
+ miner,
+ FinalizerMode::Ticket,
+ finalizer_rank,
+ reward,
+ vdf_rounds,
+ Some(&leader_ticket.id),
+ &burn_bundle_section,
+ &selection.transactions,
+ );
+ let vdf_seed = vdf_seed_for_child(&prev_hash, height, &bundle_hashes, &content_commitment);
Ok(PreparedBlock {
height,
prev_hash,
timestamp_ms,
miner: miner.to_string(),
finalizer_mode: FinalizerMode::Ticket,
- reward: self
- .expected_reward_for_next_block(&selection.transactions, &burn_bundle_section)?,
- vdf_rounds: self.vdf_rounds_for_finalizer_rank(finalizer_rank)?,
+ reward,
+ vdf_rounds,
vdf_seed,
finalizer_rank,
leader_ticket: Some(leader_ticket),
@@ -140,8 +154,28 @@ impl Ledger {
let prev_hash = tip.hash.clone();
let timestamp_ms = timestamp_ms.max(tip.timestamp_ms + 1);
let bundle_hashes = burn_bundle_section.burn_bundle_hashes(height, &prev_hash, miner);
- let vdf_seed =
- recovery_vdf_seed_for_child(&prev_hash, height, timestamp_ms, &bundle_hashes);
+ let reward =
+ self.expected_reward_for_next_block(&selection.transactions, &burn_bundle_section)?;
+ let vdf_rounds = self.recovery_vdf_rounds()?;
+ let content_commitment = vdf_content_commitment(
+ height,
+ &prev_hash,
+ miner,
+ FinalizerMode::Recovery,
+ 0,
+ reward,
+ vdf_rounds,
+ None,
+ &burn_bundle_section,
+ &selection.transactions,
+ );
+ let vdf_seed = recovery_vdf_seed_for_child(
+ &prev_hash,
+ height,
+ timestamp_ms,
+ &bundle_hashes,
+ &content_commitment,
+ );
Ok(PreparedBlock {
height,
prev_hash,
@@ -149,9 +183,8 @@ impl Ledger {
miner: miner.to_string(),
finalizer_mode: FinalizerMode::Recovery,
finalizer_rank: 0,
- reward: self
- .expected_reward_for_next_block(&selection.transactions, &burn_bundle_section)?,
- vdf_rounds: self.recovery_vdf_rounds()?,
+ reward,
+ vdf_rounds,
vdf_seed,
leader_ticket: None,
burn_bundle_section,
diff --git a/src/domain/protocol.rs b/src/domain/protocol.rs
@@ -16,6 +16,7 @@ pub const MINE_ACTIONS_PER_ANCHOR_LIMIT: usize = 2;
pub const BURN_COMMITTEE_SIZE: usize = 5;
pub const MAX_BURN_BUNDLE_BYTES: usize = 10_000;
pub const BURN_LINEAGE_MATURITY_HEIGHTS: u64 = 20;
+pub const GRINDING_RESISTANCE_ACTIVATION_HEIGHT: u64 = 1_000;
pub const MAX_PENDING_TRANSACTIONS: usize = 10_000;
pub(super) const MAX_PENDING_POOL_BYTES: usize = 8 * 1024 * 1024;
@@ -61,6 +62,7 @@ mod tests {
assert_eq!(BURN_COMMITTEE_SIZE, 5);
assert_eq!(MAX_BURN_BUNDLE_BYTES, 10_000);
assert_eq!(BURN_LINEAGE_MATURITY_HEIGHTS, 20);
+ assert_eq!(GRINDING_RESISTANCE_ACTIVATION_HEIGHT, 1_000);
assert_eq!(MAX_PENDING_TRANSACTIONS, 10_000);
assert_eq!(MAX_PENDING_POOL_BYTES, 8 * 1024 * 1024);
assert_eq!(MAX_ORPHAN_TRANSACTIONS, 1_024);
diff --git a/src/domain/ticket.rs b/src/domain/ticket.rs
@@ -4,8 +4,8 @@ use anyhow::{Context, Result, bail};
use sha2::{Digest, Sha256};
use super::{
- Amount, Block, FinalizerMode, LaunchProfile, MAX_VDF_ROUNDS, Transaction, VDF_TARGET_BLOCK_MS,
- hex_hash,
+ Amount, Block, FinalizerMode, GRINDING_RESISTANCE_ACTIVATION_HEIGHT, LaunchProfile,
+ MAX_VDF_ROUNDS, Transaction, VDF_TARGET_BLOCK_MS, hex_hash,
};
pub(super) const MISSED_FALLBACK_TICKET_INVALIDATION_HEIGHT: u64 = 300;
@@ -68,23 +68,29 @@ fn select_weighted_ticket_index(
}
fn weighted_ticket_draw(parent: &Block, target_height: u64, rank: u32, total_weight: u128) -> u128 {
- let seed = if rank == 0 {
- format!(
- "iuna-ticket-draw:{}:{}:{}",
- target_height, parent.hash, parent.vdf_output
- )
- } else {
- format!(
- "iuna-ticket-draw-rank:{}:{}:{}:{}",
- target_height, rank, parent.hash, parent.vdf_output
- )
- };
+ let seed = ticket_draw_seed(parent, target_height, rank);
let digest = Sha256::digest(seed.as_bytes());
let mut bytes = [0_u8; 16];
bytes.copy_from_slice(&digest[..16]);
u128::from_be_bytes(bytes) % total_weight
}
+fn ticket_draw_seed(parent: &Block, target_height: u64, rank: u32) -> String {
+ let parent_randomness = if target_height >= GRINDING_RESISTANCE_ACTIVATION_HEIGHT {
+ format!("{}:{}", parent.vdf_seed(), parent.vdf_output)
+ } else {
+ format!("{}:{}", parent.hash, parent.vdf_output)
+ };
+ if rank == 0 {
+ format!("iuna-ticket-draw:{}:{}", target_height, parent_randomness)
+ } else {
+ format!(
+ "iuna-ticket-draw-rank:{}:{}:{}",
+ target_height, rank, parent_randomness
+ )
+ }
+}
+
pub(super) fn vdf_rounds_for_finalizer_rank(base_rounds: u64, rank: u32) -> Result<u64> {
let rounds = base_rounds
.checked_mul(u64::from(
@@ -352,6 +358,28 @@ mod tests {
}
}
+ #[test]
+ fn ticket_draw_stops_using_grindable_parent_hash_at_height_1000() {
+ let mut legacy_left = parent(GRINDING_RESISTANCE_ACTIVATION_HEIGHT - 2);
+ legacy_left.hash = "1".repeat(64);
+ let mut legacy_right = legacy_left.clone();
+ legacy_right.hash = "2".repeat(64);
+
+ assert_ne!(
+ ticket_draw_seed(&legacy_left, GRINDING_RESISTANCE_ACTIVATION_HEIGHT - 1, 0),
+ ticket_draw_seed(&legacy_right, GRINDING_RESISTANCE_ACTIVATION_HEIGHT - 1, 0)
+ );
+
+ let mut activated_left = parent(GRINDING_RESISTANCE_ACTIVATION_HEIGHT - 1);
+ activated_left.hash = "1".repeat(64);
+ let mut activated_right = activated_left.clone();
+ activated_right.hash = "2".repeat(64);
+ assert_eq!(
+ ticket_draw_seed(&activated_left, GRINDING_RESISTANCE_ACTIVATION_HEIGHT, 0),
+ ticket_draw_seed(&activated_right, GRINDING_RESISTANCE_ACTIVATION_HEIGHT, 0)
+ );
+ }
+
fn ticket(id: char, owner: &str, from: u64, until: u64) -> BurnTicket {
BurnTicket {
id: id.to_string().repeat(64),