iuna

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

commit 7874213250a9dcde8a94dd972f5b3e971f727919
parent 5f48038ec30d6c6d446cd54d324870b1acd3516b
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Wed, 22 Jul 2026 21:39:57 +0200

Add rolling burn ticket window

Diffstat:
MREADME.md | 12++++++------
Massets/luun-ui.js | 4++--
Mdevlogs/001-node-first.md | 2+-
Mdevlogs/002-vdf-clock.md | 2+-
Mdevlogs/003-friend-join.md | 2+-
Mdevlogs/005-burned-blocks-and-vdf.md | 2+-
Msrc/app.rs | 6+++---
Msrc/domain.rs | 121++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mtests/luun.rs | 123++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
9 files changed, 193 insertions(+), 81 deletions(-)

diff --git a/README.md b/README.md @@ -22,9 +22,9 @@ cargo run -- --genesis --http 127.0.0.1:18661 --p2p 127.0.0.1:9444 `--genesis` only works when the wallet file already exists and `.luun/chain.sqlite3` does not already contain a blockchain. It creates the starter chain, adaptively measures VDF throughput locally, extrapolates that measurement to a 60-second initial round count, and persists the validated chain. The same data directory resumes automatically on later runs without `--genesis`. -Mining is automatic. There is no "mine block" button and no exact sleep. Each node can burn its configured amount once per chain height. Those burns become one-shot leader tickets for a future height after the launch profile's maturity delay. Only the selected ticket owner builds the next block, signs a leader proof, performs the VDF work, and gossips the finished block. The VDF is the clock. +Mining is automatic. There is no "mine block" button and no exact sleep. Each node can burn its configured amount once per chain height. Those burns become one-shot leader tickets after the launch profile's maturity delay. The current devnet uses a 3-block maturity delay and a 3-block eligibility window: a burn included at height `h` can win heights `h + 3` through `h + 5`, then expires if it was not selected. Only the selected ticket owner builds the next block, signs a leader proof, performs the VDF work, and gossips the finished block. The VDF is the clock. -Genesis leaves the starter wallet with 100 spendable LUUN after the 1-LUUN bootstrap burn creates the first leader ticket and the genesis block pays its 100-LUUN reward. `--genesis` starts the automatic burn rate at 100 LUUN per block, so the starter can immediately create the block 1 burn ticket. +Genesis leaves the starter wallet with 100 spendable LUUN after the 1-LUUN bootstrap burn creates launch tickets for blocks 1, 2, and 3, and the genesis block pays its 100-LUUN reward. `--genesis` starts the automatic burn rate at 100 LUUN per block, so the starter can create the block 1 burn that becomes eligible at block 4. The management UI is a small AlpineJS app served from local vendored assets. It polls JSON endpoints every few seconds and includes: @@ -73,13 +73,13 @@ cargo run -- --data-dir .luun-friend --p2p 0.0.0.0:9445 --http 127.0.0.1:18661 - Friends who join after you start will adopt your genesis and current chain. The starter wallet begins with 100 spendable LUUN after the bootstrap burn and genesis reward, and `--genesis` starts it with a 100-LUUN automatic burn rate. After the starter mines additional block rewards, send friends LUUN from the UI; then they can choose a burn amount and compete for future blocks. Every joining node starts with a 0-LUUN automatic burn unless it is configured otherwise. -The genesis block bootstraps the chain with a 1-LUUN burn from the starter wallet. Burns included in a block create one-shot tickets for a future height through a deterministic ticket lottery. The selected leader creates the next block content, signs a proof for the selected ticket, and runs a hash-chain VDF before gossiping the block. +The genesis block bootstraps the chain with a 1-LUUN burn from the starter wallet. Genesis turns that burn into launch tickets for blocks 1 through 3 so the chain can move until normal burn tickets mature. Burns included after genesis create one-shot tickets through a deterministic ticket lottery. The selected leader creates the next block content, signs a proof for the selected ticket, and runs a hash-chain VDF before gossiping the block. -Every non-genesis block must consume the selected mature ticket, include at least one burn transaction, and fit under the 100kB serialized block limit. The VDF seed is bound to the parent hash and child height; the block hash separately commits to the miner, timestamp, miner payout, rounds, previous hash, leader proof, VDF output, and transactions. +Every non-genesis block must consume the selected eligible ticket, include at least one burn transaction, and fit under the 100kB serialized block limit. The VDF seed is bound to the parent hash and child height; the block hash separately commits to the miner, timestamp, miner payout, rounds, previous hash, leader proof, VDF output, and transactions. The protocol targets 60-second blocks by retargeting the expected VDF rounds after each block. It uses a rolling average of recent block intervals and only moves the next round count by about 10% per block, so short bursts do not make the delay swing wildly. Every node derives the same next-round count from the validated chain. -The base block reward is fixed at 100 LUUN, and miners collect transaction fees on top. The miner includes the best valid burn for liveness, then fills the remaining block space by fee-rate while respecting nonce and balance validity. The default burn is 0 LUUN per block, so new nodes can join before they own LUUN. Genesis starters begin at 100 LUUN per block; after another wallet has LUUN, raise its burn from the Mining screen. +The base block reward is fixed at 100 LUUN, and miners collect transfer fees on top. Burns do not need an extra fee because the burned amount is already the cost of entering the leader lottery. The miner includes the best valid burn for liveness, then fills the remaining block space by fee-rate while respecting nonce and balance validity. The default burn is 0 LUUN per block, so new nodes can join before they own LUUN. Genesis starters begin at 100 LUUN per block; after another wallet has LUUN, raise its burn from the Mining screen. The measured VDF round count is only the initial delay. After the first blocks, the protocol steers rounds toward the 60-second target. @@ -99,7 +99,7 @@ Luun stores the latest validated `ChainSnapshot` in SQLite at `<data-dir>/chain. ## Architecture -- `src/domain.rs`: wallet, fee-paying transactions, balances, genesis burn bootstrap, 100-LUUN base rewards, 100kB blocks, mature leader tickets, leader proofs, fork choice, launch profile, and VDF checks. +- `src/domain.rs`: wallet, fee-paying transfers, fee-free burns, balances, genesis burn bootstrap, 100-LUUN base rewards, 100kB blocks, rolling-window leader tickets, leader proofs, fork choice, launch profile, and VDF checks. - `src/app.rs`: node use cases, automatic VDF-paced mining, peer bookkeeping, and an in-memory network harness. - `src/adapters/http.rs`: HTTP management UI and status endpoint. - `src/adapters/p2p.rs`: line-delimited JSON gossip, block-range catch-up, and chain snapshots over one TCP port. diff --git a/assets/luun-ui.js b/assets/luun-ui.js @@ -419,8 +419,8 @@ window.luunApp = function luunApp() { automaticBurnFeeDraft() { const amount = Math.max(0, Math.trunc(Number(this.burnAmountDraft) || 0)); - const savedFee = this.status.mining?.automatic_burn_fee ?? 1; - return Math.min(savedFee || 1, Math.max(amount - 1, 0)); + const savedFee = this.status.mining?.automatic_burn_fee ?? 0; + return Math.min(savedFee, Math.max(amount - 1, 0)); }, async sendTransfer() { diff --git a/devlogs/001-node-first.md b/devlogs/001-node-first.md @@ -9,7 +9,7 @@ The important design choice is the hexagonal split. The Luun rules live in the d The consensus sketch is intentionally small: - burn LUUN into a block, -- turn those burns into mature one-shot tickets for future blocks, +- turn those burns into mature one-shot tickets for a short future block window, - use parent-bound VDF work as the pacing signal, - give the selected ticket owner the signed right to mine the next block, - forget the stake because the LUUN were already burned. diff --git a/devlogs/002-vdf-clock.md b/devlogs/002-vdf-clock.md @@ -2,7 +2,7 @@ The first UI had a "mine next block" button. That was useful for proving the ledger worked, but it was the wrong feeling for Luun. -Now the node runs by itself. Each wallet has a fixed burn amount. If that amount is above zero, once per chain height the node creates a burn transaction for that amount. Those burns become lottery tickets in the block, and the latest block's burns choose who gets to make the next block. +Now the node runs by itself. Each wallet has a fixed burn amount. If that amount is above zero, once per chain height the node creates a burn transaction for that amount. Those burns become lottery tickets after a short maturity delay, then stay eligible for a small future block window. The important correction is that there is no exact timer like "sleep 10 minutes, then make a block." The selected leader makes the block content and then does the VDF work. When the VDF is finished, the block is gossiped. That means the VDF is the clock. diff --git a/devlogs/003-friend-join.md b/devlogs/003-friend-join.md @@ -8,6 +8,6 @@ So the P2P port now does one extra friendly thing. When a node connects, the pee The default burn is now zero. That matters because a friend who just joined probably has no LUUN yet. They can still follow the chain, receive LUUN, and only then decide how much to burn per block. -Genesis changed too. The starter does not begin rich anymore. The starter gets 1 synthetic Luun in genesis and burns it immediately, so their visible balance is 0, but the chain has a first lottery ticket. That ticket lets the starter produce the first real reward block. +Genesis changed too. The starter does not begin rich anymore. The starter gets 1 synthetic Luun in genesis and burns it immediately, so their visible balance is 0, but the chain has bootstrap lottery tickets. Those tickets let the starter produce the first real reward blocks while normal burn tickets mature. This is still not real adversarial sync. It trusts the friend you join. But for the current Luun phase, that is exactly the point: make a small network feel real first, then harden it later. diff --git a/devlogs/005-burned-blocks-and-vdf.md b/devlogs/005-burned-blocks-and-vdf.md @@ -2,7 +2,7 @@ I tightened the rule that felt wrong during local testing: a normal block cannot be empty of burns anymore. -That means a block has to carry at least one positive burn transaction. Otherwise it would create a tip with no lottery tickets for the next leader, which is basically a protocol pothole. +That means a block has to carry at least one positive burn transaction. Otherwise it would create a future stretch with no rolling-window lottery tickets, which is basically a protocol pothole. The VDF also now runs over the candidate block content hash instead of just the previous hash. So if the leader changes the timestamp, miner, reward, rounds, previous hash, or transactions after doing the VDF, peers reject it. diff --git a/src/app.rs b/src/app.rs @@ -332,7 +332,7 @@ impl NodeCore { } pub fn burn(&mut self, amount: Amount) -> Result<Transaction> { - self.burn_with_fee(amount, DEFAULT_TRANSACTION_FEE) + self.burn_with_fee(amount, 0) } pub fn burn_with_fee(&mut self, amount: Amount, fee: Amount) -> Result<Transaction> { @@ -582,8 +582,8 @@ impl NodeCore { } } -fn automatic_burn_fee(burn_per_block: Amount) -> Amount { - DEFAULT_TRANSACTION_FEE.min(burn_per_block.saturating_sub(1)) +fn automatic_burn_fee(_burn_per_block: Amount) -> Amount { + 0 } #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] diff --git a/src/domain.rs b/src/domain.rs @@ -12,7 +12,8 @@ pub const MAX_BLOCK_BYTES: usize = 100_000; pub const VDF_TARGET_BLOCK_MS: u64 = 60_000; const MAX_PENDING_TRANSACTIONS: usize = 10_000; const MAX_BLOCK_TRANSACTIONS: usize = 1_000; -const DEFAULT_TICKET_MATURITY_DELAY: u64 = 1; +const DEFAULT_TICKET_MATURITY_DELAY: u64 = 3; +const DEFAULT_TICKET_EXPIRY_WINDOW: u64 = 3; const MIN_VDF_ROUNDS: u32 = 1; const VDF_RETARGET_WINDOW_BLOCKS: usize = 10; const MAX_VDF_RETARGET_STEP_PERCENT: u128 = 10; @@ -440,8 +441,8 @@ struct BurnTicket { id: String, owner: String, amount: Amount, - target_height: u64, - maturity_height: u64, + eligible_from_height: u64, + eligible_until_height: u64, } #[derive(Clone, Debug)] @@ -529,6 +530,8 @@ pub struct ChainSnapshot { pub struct LaunchProfile { pub profile_id: String, pub ticket_maturity_delay_heights: u64, + #[serde(default = "default_ticket_expiry_window_heights")] + pub ticket_expiry_window_heights: u64, pub max_pending_transactions: usize, pub max_block_transactions: usize, #[serde(default = "default_max_block_bytes")] @@ -538,8 +541,9 @@ pub struct LaunchProfile { impl Default for LaunchProfile { fn default() -> Self { Self { - profile_id: "luun-devnet-v2".to_string(), + profile_id: "luun-devnet-v3".to_string(), ticket_maturity_delay_heights: DEFAULT_TICKET_MATURITY_DELAY, + ticket_expiry_window_heights: DEFAULT_TICKET_EXPIRY_WINDOW, max_pending_transactions: MAX_PENDING_TRANSACTIONS, max_block_transactions: MAX_BLOCK_TRANSACTIONS, max_block_bytes: MAX_BLOCK_BYTES, @@ -551,12 +555,17 @@ fn default_max_block_bytes() -> usize { MAX_BLOCK_BYTES } +fn default_ticket_expiry_window_heights() -> u64 { + DEFAULT_TICKET_EXPIRY_WINDOW +} + impl LaunchProfile { pub fn hash(&self) -> String { hex_hash(format!( - "luun-launch-profile:{}:{}:{}:{}:{}", + "luun-launch-profile:{}:{}:{}:{}:{}:{}", self.profile_id, self.ticket_maturity_delay_heights, + self.ticket_expiry_window_heights, self.max_pending_transactions, self.max_block_transactions, self.max_block_bytes @@ -1338,7 +1347,7 @@ impl Ledger { fn selected_ticket_for_height(&self, height: u64) -> Option<BurnTicket> { self.tickets .iter() - .filter(|ticket| ticket.target_height == height && ticket.maturity_height <= height) + .filter(|ticket| ticket_is_eligible_for_height(ticket, height)) .min_by(|left, right| { ticket_rank(self.tip(), height, left) .cmp(&ticket_rank(self.tip(), height, right)) @@ -1362,6 +1371,9 @@ fn ticket_rank(parent: &Block, target_height: u64, ticket: &BurnTicket) -> Strin } fn tickets_created_by_block(block: &Block, profile: &LaunchProfile) -> Result<Vec<BurnTicket>> { + if profile.ticket_expiry_window_heights == 0 { + bail!("ticket expiry window must be at least one height"); + } let mut tickets = Vec::new(); for tx in &block.transactions { let Transaction::Burn { @@ -1380,13 +1392,15 @@ fn tickets_created_by_block(block: &Block, profile: &LaunchProfile) -> Result<Ve .height .checked_add(profile.ticket_maturity_delay_heights) .with_context(|| format!("ticket target height overflow at block {}", block.height))?; - let maturity_height = target_height; + let eligible_until_height = target_height + .checked_add(profile.ticket_expiry_window_heights - 1) + .with_context(|| format!("ticket expiry height overflow at block {}", block.height))?; tickets.push(BurnTicket { id: signature.clone(), owner: from.clone(), amount: *amount, - target_height, - maturity_height, + eligible_from_height: target_height, + eligible_until_height, }); } Ok(tickets) @@ -1397,9 +1411,29 @@ fn genesis_tickets( genesis: &Block, profile: &LaunchProfile, ) -> Result<Vec<BurnTicket>> { - let tickets = tickets_created_by_block(genesis, profile)?; - if !tickets.is_empty() { - return Ok(tickets); + if profile.ticket_maturity_delay_heights == 0 { + return tickets_created_by_block(genesis, profile); + } + + let burn_tickets = genesis + .transactions + .iter() + .filter_map(|tx| { + let Transaction::Burn { + from, + amount, + signature, + .. + } = tx + else { + return None; + }; + (*amount > 0).then(|| (from.clone(), *amount, signature.clone())) + }) + .collect::<Vec<_>>(); + + if !burn_tickets.is_empty() { + return genesis_bootstrap_tickets(burn_tickets, profile, genesis); } let Some((owner, amount)) = genesis_allocations @@ -1409,32 +1443,61 @@ fn genesis_tickets( else { return Ok(Vec::new()); }; - Ok(vec![BurnTicket { - id: hex_hash(format!( - "luun-genesis-ticket:{owner}:{amount}:{}", - genesis.hash - )), - owner: owner.clone(), - amount: 1, - target_height: profile.ticket_maturity_delay_heights, - maturity_height: profile.ticket_maturity_delay_heights, - }]) + genesis_bootstrap_tickets( + vec![( + owner.clone(), + 1, + hex_hash(format!( + "luun-genesis-ticket:{owner}:{amount}:{}", + genesis.hash + )), + )], + profile, + genesis, + ) +} + +fn genesis_bootstrap_tickets( + source_tickets: Vec<(String, Amount, String)>, + profile: &LaunchProfile, + genesis: &Block, +) -> Result<Vec<BurnTicket>> { + let mut tickets = Vec::new(); + for height in 1..=profile.ticket_maturity_delay_heights { + for (owner, amount, source_id) in &source_tickets { + tickets.push(BurnTicket { + id: hex_hash(format!( + "luun-genesis-bootstrap-ticket:{}:{source_id}:{height}", + genesis.hash + )), + owner: owner.clone(), + amount: *amount, + eligible_from_height: height, + eligible_until_height: height, + }); + } + } + Ok(tickets) } fn consume_leader_ticket(block: &Block, tickets: &mut Vec<BurnTicket>) -> Result<()> { let Some(proof) = &block.leader_proof else { bail!("block is missing leader proof"); }; - let Some(index) = tickets - .iter() - .position(|ticket| ticket.id == proof.ticket_id && ticket.target_height == block.height) - else { + let Some(index) = tickets.iter().position(|ticket| { + ticket.id == proof.ticket_id && ticket_is_eligible_for_height(ticket, block.height) + }) else { bail!("leader ticket is not pending for block {}", block.height); }; tickets.remove(index); + tickets.retain(|ticket| ticket.eligible_until_height > block.height); Ok(()) } +fn ticket_is_eligible_for_height(ticket: &BurnTicket, height: u64) -> bool { + ticket.eligible_from_height <= height && height <= ticket.eligible_until_height +} + fn ensure_block_has_burn(transactions: &[Transaction]) -> Result<()> { if !transactions.iter().any(Transaction::is_burn) { bail!("block must include at least one burn transaction"); @@ -1514,12 +1577,14 @@ fn verify_leader_proof(block: &Block, tickets: &[BurnTicket]) -> Result<()> { } let ticket = tickets .iter() - .find(|ticket| ticket.id == proof.ticket_id && ticket.target_height == block.height) + .find(|ticket| { + ticket.id == proof.ticket_id && ticket_is_eligible_for_height(ticket, block.height) + }) .context("leader ticket is not pending for this height")?; if ticket.owner != block.miner { bail!("leader ticket owner does not match block miner"); } - if ticket.maturity_height > block.height { + if ticket.eligible_from_height > block.height { bail!("leader ticket is not mature"); } diff --git a/tests/luun.rs b/tests/luun.rs @@ -119,7 +119,7 @@ fn starter_node_waits_for_a_burn_before_vdf_work() { } #[test] -fn burn_in_latest_block_creates_next_height_ticket() { +fn burn_in_block_creates_ticket_after_maturity_delay() { let alice = Wallet::from_seed("alice"); let bob = Wallet::from_seed("bob"); let mut allocations = BTreeMap::new(); @@ -128,34 +128,46 @@ fn burn_in_latest_block_creates_next_height_ticket() { let mut ledger = Ledger::new(allocations, 10); ledger - .submit_transaction(alice.burn(20, ledger.next_nonce(alice.address()))) - .unwrap(); - ledger .submit_transaction(bob.burn(80, ledger.next_nonce(bob.address()))) .unwrap(); - let first = ledger.mine_next_block(&alice, 1).unwrap(); + let launch_leader = + if ledger.expected_leader_for_next_block().as_deref() == Some(alice.address()) { + &alice + } else { + &bob + }; + let first = ledger.mine_next_block(launch_leader, 1).unwrap(); ledger.apply_block(first).unwrap(); - let expected = ledger.expected_leader_for_next_block().unwrap(); - assert!(expected == alice.address() || expected == bob.address()); - - let non_leader = if expected == alice.address() { - &bob - } else { - &alice - }; - assert!(ledger.mine_next_block(non_leader, 2).is_err()); - - let leader_wallet = if expected == alice.address() { - &alice - } else { - &bob - }; + for height in 2..=3 { + let leader_wallet = + if ledger.expected_leader_for_next_block().as_deref() == Some(alice.address()) { + &alice + } else { + &bob + }; + ledger + .submit_transaction(leader_wallet.burn(1, ledger.next_nonce(leader_wallet.address()))) + .unwrap(); + let block = ledger.mine_next_block(leader_wallet, height).unwrap(); + ledger.apply_block(block).unwrap(); + } + + assert_eq!( + ledger.expected_leader_for_next_block().as_deref(), + Some(bob.address()) + ); + + assert!( + ledger.mine_next_block(&alice, 4).is_err(), + "the block 1 burn should not be eligible before its maturity delay, and only Bob should hold it at block 4" + ); + ledger - .submit_transaction(leader_wallet.burn(1, ledger.next_nonce(leader_wallet.address()))) + .submit_transaction(bob.burn(1, ledger.next_nonce(bob.address()))) .unwrap(); - assert!(ledger.mine_next_block(leader_wallet, 2).is_ok()); + assert!(ledger.mine_next_block(&bob, 4).is_ok()); } #[test] @@ -539,7 +551,7 @@ fn automatic_burn_status_shows_configured_fee() { node.set_burn_per_block(50).unwrap(); assert_eq!(node.status().mining.burn_per_block, 50); - assert_eq!(node.status().mining.automatic_burn_fee, 1); + assert_eq!(node.status().mining.automatic_burn_fee, 0); } #[test] @@ -905,6 +917,25 @@ fn joined_nodes_import_transfer_block_and_every_wallet_mines() { mined_by.push(block6.miner.clone()); network.deliver_until_idle().unwrap(); + network.node_mut("a").unwrap().burn(1).unwrap(); + network.deliver_until_idle().unwrap(); + let block7 = network.node_mut("a").unwrap().mine_one_at(7).unwrap(); + mined_by.push(block7.miner.clone()); + network.deliver_until_idle().unwrap(); + + network.node_mut("a").unwrap().burn(1).unwrap(); + network.deliver_until_idle().unwrap(); + let block8 = network.node_mut("a").unwrap().mine_one_at(8).unwrap(); + mined_by.push(block8.miner.clone()); + network.deliver_until_idle().unwrap(); + + network + .node_mut("b") + .unwrap() + .transfer(carol.address(), 10) + .unwrap(); + network.node_mut("b").unwrap().burn(1).unwrap(); + network.deliver_until_idle().unwrap(); assert_eq!( network .node("a") @@ -913,21 +944,26 @@ fn joined_nodes_import_transfer_block_and_every_wallet_mines() { .expected_leader_for_next_block(), Some(bob.address().to_string()) ); - network - .node_mut("b") - .unwrap() - .transfer(carol.address(), 10) - .unwrap(); - network.node_mut("b").unwrap().burn(1).unwrap(); - network.deliver_until_idle().unwrap(); - let block7 = network.node_mut("b").unwrap().mine_one_at(7).unwrap(); - mined_by.push(block7.miner.clone()); + let block9 = network.node_mut("b").unwrap().mine_one_at(9).unwrap(); + mined_by.push(block9.miner.clone()); network.deliver_until_idle().unwrap(); network.node_mut("c").unwrap().burn(5).unwrap(); network.deliver_until_idle().unwrap(); - let block8 = network.node_mut("b").unwrap().mine_one_at(8).unwrap(); - mined_by.push(block8.miner.clone()); + let block10 = network.node_mut("a").unwrap().mine_one_at(10).unwrap(); + mined_by.push(block10.miner.clone()); + network.deliver_until_idle().unwrap(); + + network.node_mut("a").unwrap().burn(1).unwrap(); + network.deliver_until_idle().unwrap(); + let block11 = network.node_mut("a").unwrap().mine_one_at(11).unwrap(); + mined_by.push(block11.miner.clone()); + network.deliver_until_idle().unwrap(); + + network.node_mut("b").unwrap().burn(1).unwrap(); + network.deliver_until_idle().unwrap(); + let block12 = network.node_mut("b").unwrap().mine_one_at(12).unwrap(); + mined_by.push(block12.miner.clone()); network.deliver_until_idle().unwrap(); assert_eq!( @@ -940,13 +976,13 @@ fn joined_nodes_import_transfer_block_and_every_wallet_mines() { ); network.node_mut("c").unwrap().burn(1).unwrap(); network.deliver_until_idle().unwrap(); - let block9 = network.node_mut("c").unwrap().mine_one_at(9).unwrap(); - mined_by.push(block9.miner.clone()); + let block13 = network.node_mut("c").unwrap().mine_one_at(13).unwrap(); + mined_by.push(block13.miner.clone()); network.deliver_until_idle().unwrap(); let final_tip = network.node("a").unwrap().ledger().status().tip_hash; for id in ["a", "b", "c"] { - assert_eq!(network.node(id).unwrap().ledger().status().height, 9); + assert_eq!(network.node(id).unwrap().ledger().status().height, 13); assert_eq!( network.node(id).unwrap().ledger().status().tip_hash, final_tip @@ -1043,6 +1079,17 @@ fn persisted_joined_nodes_restart_and_keep_syncing_without_tcp() { network.deliver_until_idle().unwrap(); network.node_mut("a").unwrap().mine_one_at(3).unwrap(); network.deliver_until_idle().unwrap(); + + network.node_mut("a").unwrap().burn(1).unwrap(); + network.deliver_until_idle().unwrap(); + network.node_mut("a").unwrap().mine_one_at(4).unwrap(); + network.deliver_until_idle().unwrap(); + + network.node_mut("a").unwrap().burn(1).unwrap(); + network.deliver_until_idle().unwrap(); + network.node_mut("a").unwrap().mine_one_at(5).unwrap(); + network.deliver_until_idle().unwrap(); + assert_eq!( network .node("a") @@ -1055,7 +1102,7 @@ fn persisted_joined_nodes_restart_and_keep_syncing_without_tcp() { network.node_mut("b").unwrap().burn(1).unwrap(); network.deliver_until_idle().unwrap(); - let bob_block = network.node_mut("b").unwrap().mine_one_at(4).unwrap(); + let bob_block = network.node_mut("b").unwrap().mine_one_at(6).unwrap(); assert_eq!(bob_block.miner, bob.address()); network.deliver_until_idle().unwrap();