diff --git a/Cargo.toml b/Cargo.toml index 7685d9bd2..37c49532f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,7 +105,14 @@ build-override.opt-level = 3 build-override.opt-level = 3 [profile.dev] -build-override.opt-level = 3 +build-override.opt-level = 2 +opt-level = 0 +debug = false +incremental = true +debug-assertions = false +overflow-checks = false +lto = false +panic = "abort" # The profile that 'dist' will build with [profile.dist] diff --git a/neptune-core/src/application/loops/main_loop.rs b/neptune-core/src/application/loops/main_loop.rs index f7d5d7b32..17ad52614 100644 --- a/neptune-core/src/application/loops/main_loop.rs +++ b/neptune-core/src/application/loops/main_loop.rs @@ -3134,6 +3134,86 @@ mod tests { mod peer_messages { use super::*; + #[traced_test] + #[apply(shared_tokio_runtime)] + async fn main_loop_does_not_do_verification() { + // Ensure that the main loop does no validation of block validity + // or PoW, as these checks belong in the peer loop, or elsewhere. + let network = Network::Main; + let cli = cli_args::Args::default_with_network(network); + let TestSetup { + mut main_loop_handler, + .. + } = setup(1, 1, cli).await; + let mut main_loop_state = main_loop_handler.mutable(); + + let genesis = Block::genesis(network); + let block1 = invalid_empty_block(&genesis, network); + assert!( + !block1 + .is_valid(&genesis, block1.header().timestamp, network) + .await + ); + assert!(!block1.has_proof_of_work(network, genesis.header())); + + let block2 = invalid_empty_block(&block1, network); + assert!( + !block2 + .is_valid(&genesis, block2.header().timestamp, network) + .await + ); + assert!(!block2.has_proof_of_work(network, block1.header())); + + assert_eq!( + BlockHeight::genesis(), + main_loop_handler + .global_state_lock + .lock_guard() + .await + .chain + .light_state() + .header() + .height + ); + main_loop_handler + .handle_peer_task_message( + PeerTaskToMain::NewBlocks(vec![block1.clone()]), + &mut main_loop_state, + ) + .await + .unwrap(); + assert_eq!( + block1.header().height, + main_loop_handler + .global_state_lock + .lock_guard() + .await + .chain + .light_state() + .header() + .height + ); + + main_loop_handler + .handle_peer_task_message( + PeerTaskToMain::NewBlocks(vec![block1.clone(), block2.clone()]), + &mut main_loop_state, + ) + .await + .unwrap(); + assert_eq!( + block2.header().height, + main_loop_handler + .global_state_lock + .lock_guard() + .await + .chain + .light_state() + .header() + .height + ); + } + #[traced_test] #[apply(shared_tokio_runtime)] async fn new_block_from_peer_invokes_block_notify() { diff --git a/neptune-core/src/application/loops/mine_loop.rs b/neptune-core/src/application/loops/mine_loop.rs index 9859fea26..39bdefb80 100644 --- a/neptune-core/src/application/loops/mine_loop.rs +++ b/neptune-core/src/application/loops/mine_loop.rs @@ -46,6 +46,7 @@ use crate::protocol::consensus::block::block_transaction::BlockTransaction; use crate::protocol::consensus::block::difficulty_control::difficulty_control; use crate::protocol::consensus::block::pow::GuesserBuffer; use crate::protocol::consensus::block::pow::Pow; +use crate::protocol::consensus::block::pow::PowMastPaths; use crate::protocol::consensus::block::*; use crate::protocol::consensus::consensus_rule_set::ConsensusRuleSet; use crate::protocol::consensus::transaction::transaction_proof::TransactionProofType; @@ -65,6 +66,8 @@ use crate::COMPOSITION_FAILED_EXIT_CODE; pub(crate) struct GuessingConfiguration { pub(crate) num_guesser_threads: Option, pub(crate) address: ReceivingAddress, + pub(crate) override_rng: Option, + pub(crate) override_timestamp: Option, } /// Creates a block transaction and composes a block from it. Returns the block @@ -151,7 +154,6 @@ pub(crate) async fn guess_nonce( previous_block_header, sender, guessing_configuration, - Timestamp::now(), None, ) }) @@ -159,6 +161,12 @@ pub(crate) async fn guess_nonce( .unwrap() } +fn std_rng_from_thread_rng() -> StdRng { + let mut thread_rng = rand::rng(); + let seed: [u8; 32] = thread_rng.random(); + StdRng::from_seed(seed) +} + /// Guess the nonce in parallel until success. fn guess_worker( network: Network, @@ -166,32 +174,29 @@ fn guess_worker( previous_block_header: BlockHeader, sender: oneshot::Sender, guessing_configuration: GuessingConfiguration, - now: Timestamp, override_target_block_interval: Option, ) { - let target_block_interval = - override_target_block_interval.unwrap_or(network.target_block_interval()); - let GuessingConfiguration { num_guesser_threads, address: guesser_address, + override_rng: rng, + override_timestamp: now, } = guessing_configuration; - // Following code must match the rules in `[Block::has_proof_of_work]`. + let now = now.unwrap_or(Timestamp::now()); + info!( + "prev block height: {}, prev block time: {}, now: {}", + previous_block_header.height, previous_block_header.timestamp, now + ); + + // Following code must match the rules in `[Block::has_proof_of_work]`. // a difficulty reset (to min difficulty) occurs on testnet(s) // when the elapsed time between two blocks is greater than a // max interval, defined by the network. It never occurs for // mainnet. let should_reset_difficulty = Block::should_reset_difficulty(network, now, previous_block_header.timestamp); - - info!( - "prev block height: {}, prev block time: {}, now: {}", - previous_block_header.height, previous_block_header.timestamp, now - ); - - let prev_difficulty = previous_block_header.difficulty; let new_difficulty = if should_reset_difficulty { let new_difficulty = network.genesis_difficulty(); info!( @@ -201,6 +206,8 @@ fn guess_worker( ); new_difficulty } else { + let target_block_interval = + override_target_block_interval.unwrap_or(network.target_block_interval()); difficulty_control( now, previous_block_header.timestamp, @@ -210,13 +217,15 @@ fn guess_worker( ) }; + let prev_difficulty = previous_block_header.difficulty; let threshold = prev_difficulty.target(); let threads_to_use = num_guesser_threads.unwrap_or_else(rayon::current_num_threads); + let new_block_height = block.header().height; info!( "Guessing with {} threads on block {:x} of height {} with {} outputs and difficulty {}. Target: {threshold:x}", threads_to_use, block.hash(), - block.header().height, + new_block_height, block.body().transaction_kernel.outputs.len(), previous_block_header.difficulty, ); @@ -231,22 +240,37 @@ fn guess_worker( block.set_header_guesser_address(guesser_address); info!("Start: guess preprocessing."); - let guesser_buffer = block.guess_preprocess(Some(&sender), Some(threads_to_use)); + let consensus_rule_set = ConsensusRuleSet::infer_from(network, new_block_height); + let guesser_buffer = + block.guess_preprocess(Some(&sender), Some(threads_to_use), consensus_rule_set); if sender.is_canceled() { info!("Guess preprocessing canceled. Stopping guessing task."); return; } info!("Completed: guess preprocessing."); + let mast_auth_paths = block.pow_mast_paths(); let pool = ThreadPoolBuilder::new() .num_threads(threads_to_use) .build() .unwrap(); + + let index_picker_preimage = guesser_buffer.index_picker_preimage(&mast_auth_paths); let guess_result = pool.install(|| { rayon::iter::repeat(0) - .map_init(rand::rng, |rng, _i| { - guess_nonce_iteration(&guesser_buffer, threshold, rng, &sender) - }) + .map_init( + || rng.clone().unwrap_or(std_rng_from_thread_rng()), + |rng, _i| { + guess_nonce_iteration( + &guesser_buffer, + &mast_auth_paths, + index_picker_preimage, + threshold, + rng, + &sender, + ) + }, + ) .find_any(|r| !r.block_not_found()) .unwrap() }); @@ -308,8 +332,10 @@ impl GuessNonceResult { #[inline] fn guess_nonce_iteration( guesser_buffer: &GuesserBuffer<{ BlockPow::MERKLE_TREE_HEIGHT }>, + mast_auth_paths: &PowMastPaths, + index_picker_preimage: Digest, threshold: Digest, - rng: &mut rand::rngs::ThreadRng, + rng: &mut rand::rngs::StdRng, sender: &oneshot::Sender, ) -> GuessNonceResult { let nonce: Digest = rng.random(); @@ -320,7 +346,13 @@ fn guess_nonce_iteration( return GuessNonceResult::Cancelled; } - let result = Pow::guess(guesser_buffer, nonce, threshold); + let result = Pow::guess( + guesser_buffer, + mast_auth_paths, + index_picker_preimage, + nonce, + threshold, + ); match result { Some(pow) => GuessNonceResult::NonceFound { pow: Box::new(pow) }, @@ -656,12 +688,18 @@ pub(crate) async fn mine( .reset(tokio::time::Instant::now() + infinite); let (is_connected, is_syncing) = global_state_lock - .lock(|s| (!s.net.peer_map.is_empty(), s.net.sync_anchor.is_some())) + .lock(|s| { + ( + // Prevent isolated mining on main net + !s.net.peer_map.is_empty() || !s.cli().network.is_main(), + s.net.sync_anchor.is_some(), + ) + }) .await; if !is_connected { const WAIT_TIME_WHEN_DISCONNECTED_IN_SECONDS: u64 = 5; global_state_lock.set_mining_status_to_inactive().await; - warn!("Not mining because client has no connections"); + warn!("Not mining because main net client has no connections."); sleep(Duration::from_secs(WAIT_TIME_WHEN_DISCONNECTED_IN_SECONDS)).await; continue; } @@ -710,6 +748,8 @@ pub(crate) async fn mine( GuessingConfiguration { num_guesser_threads: cli_args.guesser_threads, address: guesser_key.to_address().into(), + override_rng: None, + override_timestamp: None, }, ); @@ -1067,16 +1107,26 @@ pub(crate) mod tests { Some(target_block_interval), network, ); + let mast_auth_paths = block.pow_mast_paths(); let threshold = previous_block.header().difficulty.target(); let num_iterations_launched = 1_000_000; let tick = std::time::SystemTime::now(); let (worker_task_tx, worker_task_rx) = oneshot::channel::(); - let guesser_buffer = block.guess_preprocess(Some(&worker_task_tx), None); + let guesser_buffer = + block.guess_preprocess(Some(&worker_task_tx), None, ConsensusRuleSet::default()); + let index_picker_preimage = guesser_buffer.index_picker_preimage(&mast_auth_paths); let num_iterations_run = rayon::iter::IntoParallelIterator::into_par_iter(0..num_iterations_launched) - .map_init(rand::rng, |prng, _i| { - guess_nonce_iteration(&guesser_buffer, threshold, prng, &worker_task_tx); + .map_init(std_rng_from_thread_rng, |prng, _i| { + guess_nonce_iteration( + &guesser_buffer, + &mast_auth_paths, + index_picker_preimage, + threshold, + prng, + &worker_task_tx, + ); }) .count(); drop(worker_task_rx); @@ -1565,8 +1615,9 @@ pub(crate) mod tests { GuessingConfiguration { num_guesser_threads, address: guesser_key.to_address().into(), + override_rng: None, + override_timestamp: None, }, - Timestamp::now(), None, ); @@ -1648,8 +1699,9 @@ pub(crate) mod tests { GuessingConfiguration { num_guesser_threads, address: guesser_key.to_address().into(), + override_rng: None, + override_timestamp: None, }, - Timestamp::now(), None, ); @@ -1799,8 +1851,9 @@ pub(crate) mod tests { GuessingConfiguration { num_guesser_threads, address: guesser_key.to_address().into(), + override_rng: None, + override_timestamp: None, }, - Timestamp::now(), Some(target_block_interval), ); @@ -2137,10 +2190,21 @@ pub(crate) mod tests { BlockProof::Invalid, ); - let guesser_buffer = successor_block.guess_preprocess(None, None); + let guesser_buffer = + successor_block.guess_preprocess(None, None, ConsensusRuleSet::default()); + let mast_auth_paths = successor_block.pow_mast_paths(); + let index_picker_preimage = guesser_buffer.index_picker_preimage(&mast_auth_paths); let target = predecessor_block.header().difficulty.target(); loop { - if BlockPow::guess(&guesser_buffer, rng.random(), target).is_some() { + if BlockPow::guess( + &guesser_buffer, + &mast_auth_paths, + index_picker_preimage, + rng.random(), + target, + ) + .is_some() + { println!("found solution after {counter} guesses."); break; } @@ -2426,8 +2490,9 @@ pub(crate) mod tests { GuessingConfiguration { num_guesser_threads, address: guesser_key.to_address().into(), + override_rng: None, + override_timestamp: Some(block_time), }, - block_time, None, ); diff --git a/neptune-core/src/application/loops/peer_loop.rs b/neptune-core/src/application/loops/peer_loop.rs index 72da854e8..183722ce2 100644 --- a/neptune-core/src/application/loops/peer_loop.rs +++ b/neptune-core/src/application/loops/peer_loop.rs @@ -2314,10 +2314,20 @@ mod tests { } mod blocks { + use futures::channel::oneshot; use itertools::Itertools; use super::*; + use crate::application::loops::channel::NewBlockFound; + use crate::application::loops::mine_loop::guess_nonce; + use crate::application::loops::mine_loop::GuessingConfiguration; + use crate::protocol::consensus::block::difficulty_control::Difficulty; + use crate::protocol::consensus::block::validity::block_primitive_witness::BlockPrimitiveWitness; + use crate::state::wallet::address::generation_address::GenerationReceivingAddress; use crate::tests::shared::blocks::fake_valid_block_proposal_successor_for_test; + use crate::tests::shared::blocks::next_block; + use crate::tests::shared::globalstate::test_setup_custom_genesis_block; + use crate::tests::tokio_runtime; #[traced_test] #[apply(shared_tokio_runtime)] @@ -2407,14 +2417,15 @@ mod tests { Ok(()) } - /// Return three blocks: + /// Return four blocks: /// - one with invalid PoW and invalid mock-PoW /// - one with invalid PoW and valid mock-Pow - /// - one with valid Pow + /// - one with valid reboot PoW + /// - one with valid hardfork PoW async fn pow_related_blocks( network: Network, predecessor: &Block, - ) -> (Block, Block, Block) { + ) -> (Block, Block, Block, Block) { let rng = StdRng::seed_from_u64(5550001).random(); let block = fake_valid_block_proposal_successor_for_test( predecessor, @@ -2436,12 +2447,23 @@ mod tests { assert!(block_with_valid_mock_pow.is_valid_mock_pow(difficulty.target())); assert!(!block_with_valid_mock_pow.has_proof_of_work(network, predecessor.header())); - let mut block_with_valid_pow = block; - block_with_valid_pow.satisfy_pow(difficulty, rand::random()); - assert!(block_with_valid_pow.is_valid_mock_pow(difficulty.target())); - assert!(block_with_valid_pow.has_proof_of_work(network, predecessor.header())); - - (invalid_pow, block_with_valid_mock_pow, block_with_valid_pow) + let mut block_with_valid_reboot_pow = block.clone(); + block_with_valid_reboot_pow.satisfy_pow(difficulty, ConsensusRuleSet::Reboot); + assert!(block_with_valid_reboot_pow.is_valid_mock_pow(difficulty.target())); + assert!(block_with_valid_reboot_pow.has_proof_of_work(network, predecessor.header())); + + let mut block_with_valid_alpha_pow = block.clone(); + block_with_valid_alpha_pow.satisfy_pow(difficulty, ConsensusRuleSet::HardforkAlpha); + assert!(block_with_valid_alpha_pow.is_valid_mock_pow(difficulty.target())); + assert!(block_with_valid_alpha_pow + .pow_verify(difficulty.target(), ConsensusRuleSet::HardforkAlpha)); + + ( + invalid_pow, + block_with_valid_mock_pow, + block_with_valid_reboot_pow, + block_with_valid_alpha_pow, + ) } #[traced_test] @@ -2478,8 +2500,12 @@ mod tests { 1, ); - let (block_without_any_pow, block_with_valid_mock_pow, block_with_valid_pow) = - pow_related_blocks(network, &genesis).await; + let ( + block_without_any_pow, + block_with_valid_mock_pow, + valid_pow_reboot, + valid_pow_alpha, + ) = pow_related_blocks(network, &genesis).await; assert!( peer_loop_handler .handle_blocks(vec![block_without_any_pow], genesis.clone()) @@ -2499,12 +2525,20 @@ mod tests { assert_eq!( BlockHeight::genesis().next(), peer_loop_handler - .handle_blocks(vec![block_with_valid_pow], genesis.clone()) + .handle_blocks(vec![valid_pow_reboot], genesis.clone()) .await .unwrap() .unwrap(), "Must return Some(1) on valid Pow" ); + assert!( + peer_loop_handler + .handle_blocks(vec![valid_pow_alpha], genesis.clone()) + .await + .unwrap() + .is_none(), + "Must return None on hardfork-alpha solution when rule set is reboot" + ); } #[traced_test] @@ -2524,9 +2558,13 @@ mod tests { ) = get_test_genesis_setup(network, 0, cli_args::Args::default()).await?; let peer_address = get_dummy_socket_address(0); - let (block_without_any_pow, block_with_valid_mock_pow, _) = + let (without_any_pow, with_valid_mock_pow, _, with_valid_hf_alpha_pow) = pow_related_blocks(network, &Block::genesis(network)).await; - for block_without_valid_pow in [block_without_any_pow, block_with_valid_mock_pow] { + for block_without_valid_pow in [ + without_any_pow, + with_valid_mock_pow, + with_valid_hf_alpha_pow, + ] { // Sending an invalid block will not necessarily result in a ban. This depends on the peer // tolerance that is set in the client. For this reason, we include a "Bye" here. let mock = Mock::new(vec![ @@ -3747,6 +3785,146 @@ mod tests { Ok(()) } + + #[traced_test] + #[test] + fn hardfork_alpha_happy_case() { + // Ensure that valid blocks are all accepted across hardfork alpha. + // Scenario: Current height is hardfork minus 2. Then receives peer + // notification of first block after hardfork. Must accept all + // blocks from peer, not punish peer, and send blocks to main + // loop. + let init_block_heigth = BlockHeight::from(14999u64); + let bpw = BlockPrimitiveWitness::deterministic_with_block_height_and_difficulty( + init_block_heigth, + Difficulty::MINIMUM, + ); + + tokio_runtime().block_on(runner(bpw)); + async fn runner(block_primitive_witness: BlockPrimitiveWitness) { + let network = Network::Main; + let (hard_fork_minus_2, hard_fork_minus_1_no_pow) = + Block::fake_block_pair_genesis_and_child_from_witness(block_primitive_witness) + .await; + + let ( + _peer_broadcast_tx, + from_main_rx_clone, + to_main_tx, + mut to_main_rx1, + state_lock, + hsd, + ) = test_setup_custom_genesis_block( + network, + 1, + cli_args::Args::default(), + hard_fork_minus_2.clone(), + ) + .await + .unwrap(); + + // Solve PoW for 1st block after genesis + let (guesser_tx_b, guesser_rx) = oneshot::channel::(); + let guesser_timestamp = hard_fork_minus_1_no_pow.header().timestamp; + let rng = StdRng::seed_from_u64(55512345); + guess_nonce( + network, + hard_fork_minus_1_no_pow, + *hard_fork_minus_2.header(), + guesser_tx_b, + GuessingConfiguration { + num_guesser_threads: state_lock.cli().guesser_threads, + address: GenerationReceivingAddress::derive_from_seed(Digest::default()) + .into(), + // For deterministic pow-guessing, both RNG and timestamp + // must be deterministic. + override_rng: Some(rng), + override_timestamp: Some(guesser_timestamp), + }, + ) + .await; + + let hard_fork_minus_1 = *guesser_rx.await.unwrap().block; + + let first_block_after_hardfork = + next_block(state_lock.clone(), hard_fork_minus_1.clone()).await; + + // Verify assumption about block height of hardfork + assert!(hard_fork_minus_1.pow_verify( + hard_fork_minus_2.header().difficulty.target(), + ConsensusRuleSet::Reboot + )); + assert!(!hard_fork_minus_1.pow_verify( + hard_fork_minus_2.header().difficulty.target(), + ConsensusRuleSet::HardforkAlpha + )); + assert!(first_block_after_hardfork.pow_verify( + hard_fork_minus_2.header().difficulty.target(), + ConsensusRuleSet::HardforkAlpha + )); + assert!(!first_block_after_hardfork.pow_verify( + hard_fork_minus_2.header().difficulty.target(), + ConsensusRuleSet::Reboot + )); + + // Declare expected order of messages + let mock = Mock::new(vec![ + Action::Read(PeerMessage::BlockNotification( + (&first_block_after_hardfork).into(), + )), + Action::Write(PeerMessage::BlockRequestByHeight(BlockHeight::from( + 15000u64, + ))), + Action::Read(PeerMessage::Block(Box::new( + first_block_after_hardfork.clone().try_into().unwrap(), + ))), + Action::Write(PeerMessage::BlockRequestByHash(hard_fork_minus_1.hash())), + Action::Read(PeerMessage::Block(Box::new( + hard_fork_minus_1.clone().try_into().unwrap(), + ))), + Action::Read(PeerMessage::Bye), + ]); + + let peer_address = get_dummy_socket_address(0); + let mut peer_loop_handler = PeerLoopHandler::with_mocked_time( + to_main_tx.clone(), + state_lock.clone(), + peer_address, + hsd, + false, + 1, + first_block_after_hardfork.header().timestamp, + ); + peer_loop_handler + .run_wrapper(mock, from_main_rx_clone) + .await + .unwrap(); + + match to_main_rx1.recv().await { + Some(PeerTaskToMain::NewBlocks(blocks)) => { + assert_eq!( + vec![hard_fork_minus_1, first_block_after_hardfork], + blocks, + "Two blocks must be sent to main loop in right order" + ); + } + _ => panic!("Did not find msg sent to main task"), + }; + + // Verify that peer is not sanctioned + assert!(!state_lock + .lock_guard() + .await + .net + .get_peer_standing_from_database(peer_address.ip()) + .await + .unwrap() + .standing + .is_negative()); + + drop(to_main_rx1); + } + } } mod transactions { diff --git a/neptune-core/src/application/rpc/server.rs b/neptune-core/src/application/rpc/server.rs index 265039071..62359bcc6 100644 --- a/neptune-core/src/application/rpc/server.rs +++ b/neptune-core/src/application/rpc/server.rs @@ -2221,7 +2221,7 @@ impl NeptuneRPCServer { let latest_block_header = *self.state.lock_guard().await.chain.light_state().header(); proposal.set_header_guesser_address(guesser_address); - let puzzle = ProofOfWorkPuzzle::new(proposal.clone(), latest_block_header); + let puzzle = ProofOfWorkPuzzle::new(proposal.clone(), latest_block_header.difficulty); // Record block proposal in case of guesser-success, for later // retrieval. But limit number of blocks stored this way. @@ -3811,7 +3811,7 @@ impl RPC for NeptuneRPCServer { }; proposal.set_header_guesser_address(guesser_fee_address); - let puzzle = ProofOfWorkPuzzle::new(proposal.clone(), latest_block_header); + let puzzle = ProofOfWorkPuzzle::new(proposal.clone(), latest_block_header.difficulty); Ok(Some((proposal, puzzle))) } @@ -5675,6 +5675,7 @@ mod tests { use crate::protocol::consensus::block::block_header::BlockPow; use crate::protocol::consensus::block::pow::Pow; use crate::protocol::consensus::block::BlockProof; + use crate::protocol::consensus::consensus_rule_set::ConsensusRuleSet; use crate::protocol::consensus::transaction::validity::neptune_proof::NeptuneProof; use crate::state::mining::block_proposal::BlockProposal; use crate::state::wallet::address::generation_address::GenerationReceivingAddress; @@ -5691,7 +5692,8 @@ mod tests { let guesser_address = GenerationReceivingAddress::derive_from_seed(rng.random()); block1.set_header_guesser_address(guesser_address.into()); - let guess_challenge = ProofOfWorkPuzzle::new(block1.clone(), *genesis.header()); + let guess_challenge = + ProofOfWorkPuzzle::new(block1.clone(), genesis.header().difficulty); assert_eq!(guess_challenge.prev_block, genesis.hash()); let pow: BlockPow = random(); @@ -5779,7 +5781,7 @@ mod tests { "Node must reject new tip with invalid PoW solution." ); - let solution = puzzle.solve(); + let solution = puzzle.solve(ConsensusRuleSet::default()); assert!( bob.clone() .provide_new_tip(context::current(), bob_token, solution, proposal.clone()) @@ -5949,7 +5951,7 @@ mod tests { ); let pow: BlockPow = random(); - let resulting_block_hash = pow_puzzle.auth_paths.fast_mast_hash(pow); + let resulting_block_hash = pow_puzzle.pow_mast_paths.fast_mast_hash(pow); block1.set_header_pow(pow); block1.set_header_guesser_address(guesser_address.into()); @@ -5960,10 +5962,19 @@ mod tests { ); // Check that succesful guess is accepted by endpoint. - let guesser_buffer = block1.guess_preprocess(None, None); + let consensus_rule_set = ConsensusRuleSet::Reboot; + let guesser_buffer = block1.guess_preprocess(None, None, consensus_rule_set); + let mast_auth_paths = block1.pow_mast_paths(); + let index_picker_preimage = guesser_buffer.index_picker_preimage(&mast_auth_paths); let target = genesis.header().difficulty.target(); let valid_pow = loop { - if let Some(valid_pow) = Pow::guess(&guesser_buffer, random(), target) { + if let Some(valid_pow) = Pow::guess( + &guesser_buffer, + &mast_auth_paths, + index_picker_preimage, + random(), + target, + ) { break valid_pow; } }; diff --git a/neptune-core/src/application/rpc/server/proof_of_work_puzzle.rs b/neptune-core/src/application/rpc/server/proof_of_work_puzzle.rs index b80d59a26..37a93774b 100644 --- a/neptune-core/src/application/rpc/server/proof_of_work_puzzle.rs +++ b/neptune-core/src/application/rpc/server/proof_of_work_puzzle.rs @@ -5,10 +5,11 @@ use tasm_lib::prelude::Tip5; use tasm_lib::twenty_first::bfe_array; use tracing::info; -use crate::application::rpc::server::BlockHeader; use crate::application::rpc::server::NativeCurrencyAmount; use crate::protocol::consensus::block::block_header::BlockPow; +use crate::protocol::consensus::block::difficulty_control::Difficulty; use crate::protocol::consensus::block::pow::PowMastPaths; +use crate::protocol::consensus::consensus_rule_set::ConsensusRuleSet; use crate::BFieldElement; use crate::Block; @@ -17,7 +18,7 @@ use crate::Block; #[derive(Clone, Debug, Copy, Serialize, Deserialize)] pub struct ProofOfWorkPuzzle { // All fields public since used downstream by mining pool software. - pub auth_paths: PowMastPaths, + pub pow_mast_paths: PowMastPaths, /// The threshold digest that defines when a PoW solution is valid. The /// block's hash must be less than or equal to this value. @@ -40,19 +41,19 @@ pub struct ProofOfWorkPuzzle { impl ProofOfWorkPuzzle { /// Return a PoW puzzle assuming that the caller has already set the correct /// guesser digest. - pub fn new(block_proposal: Block, latest_block_header: BlockHeader) -> Self { + pub fn new(block_proposal: Block, parent_difficulty: Difficulty) -> Self { let guesser_reward = block_proposal .body() .total_guesser_reward() .expect("Block proposal must have well-defined guesser reward"); let auth_paths = block_proposal.pow_mast_paths(); - let threshold = latest_block_header.difficulty.target(); + let threshold = parent_difficulty.target(); let prev_block = block_proposal.header().prev_block_digest; let id = Tip5::hash(&auth_paths); Self { - auth_paths, + pow_mast_paths: auth_paths, threshold, total_guesser_reward: guesser_reward, id, @@ -62,21 +63,32 @@ impl ProofOfWorkPuzzle { /// Solve a PoW from a puzzle. /// - /// Takes a very long time and cannot be cancelled while running. - pub fn solve(&self) -> BlockPow { - use rayon::prelude::*; + /// Takes a very long time and cannot be cancelled while running. Slow + /// implementation, as it only uses one thread. + pub fn solve(&self, consensus_rule_set: ConsensusRuleSet) -> BlockPow { info!("Starting PoW preprocessing"); - let guesser_buffer = BlockPow::preprocess(self.auth_paths, None); + let guesser_buffer = BlockPow::preprocess( + self.pow_mast_paths, + None, + consensus_rule_set, + self.prev_block, + ); info!("Done with PoW preprocessing"); info!("Now attempting to find valid nonce"); + let index_picker_preimage = guesser_buffer.index_picker_preimage(&self.pow_mast_paths); let solution = (0u64..u64::MAX) - .into_par_iter() .map(|i| { let nonce = Digest(bfe_array![0, 0, 0, 0, i]); - BlockPow::guess(&guesser_buffer, nonce, self.threshold) + BlockPow::guess( + &guesser_buffer, + &self.pow_mast_paths, + index_picker_preimage, + nonce, + self.threshold, + ) }) - .find_map_any(|x| x) + .find_map(|x| x) .expect("Should find solution within 2^{64} attempts"); info!("Found valid nonce! nonce: {}", solution.nonce); diff --git a/neptune-core/src/protocol/consensus/block/block_header.rs b/neptune-core/src/protocol/consensus/block/block_header.rs index 8edd93308..a2b4fd983 100644 --- a/neptune-core/src/protocol/consensus/block/block_header.rs +++ b/neptune-core/src/protocol/consensus/block/block_header.rs @@ -275,8 +275,9 @@ impl BlockHeaderWithBlockHashWitness { #[cfg(any(test, feature = "arbitrary-impls"))] impl BlockHeader { - pub(crate) fn arbitrary_with_height( - block_height: BlockHeight, + pub(crate) fn arbitrary_with_height_and_difficulty( + height: BlockHeight, + difficulty: Difficulty, ) -> proptest::prelude::BoxedStrategy { use proptest::prelude::Strategy; use proptest_arbitrary_interop::arb; @@ -286,7 +287,6 @@ impl BlockHeader { let timestamp = arb::(); let pow = arb::(); let cumulative_proof_of_work = arb::(); - let difficulty = arb::(); let guesser_receiver_data = arb::(); ( @@ -295,7 +295,6 @@ impl BlockHeader { timestamp, pow, cumulative_proof_of_work, - difficulty, guesser_receiver_data, ) .prop_map( @@ -305,12 +304,11 @@ impl BlockHeader { timestamp, pow, cumulative_proof_of_work, - difficulty, guesser_receiver_data, )| { BlockHeader { version, - height: block_height, + height, prev_block_digest, timestamp, pow, diff --git a/neptune-core/src/protocol/consensus/block/mod.rs b/neptune-core/src/protocol/consensus/block/mod.rs index 9f9c68354..7b6b81c1b 100644 --- a/neptune-core/src/protocol/consensus/block/mod.rs +++ b/neptune-core/src/protocol/consensus/block/mod.rs @@ -936,7 +936,9 @@ impl Block { return true; } - self.pow_verify(threshold) + let consensus_rule_set = + ConsensusRuleSet::infer_from(network, previous_block_header.height.next()); + self.pow_verify(threshold, consensus_rule_set) } /// Produce the MAST authentication paths for the `pow` field on @@ -964,6 +966,7 @@ impl Block { &self, maybe_cancel_channel: Option<&dyn Cancelable>, num_guesser_threads: Option, + consensus_rule_set: ConsensusRuleSet, ) -> GuesserBuffer<{ BlockPow::MERKLE_TREE_HEIGHT }> { // build a rayon thread pool that respects the limitation on the number // of threads @@ -974,8 +977,14 @@ impl Block { .unwrap(); let auth_paths = self.pow_mast_paths(); + let prev_block_digest = self.header().prev_block_digest; thread_pool.install(|| { - Pow::<{ BlockPow::MERKLE_TREE_HEIGHT }>::preprocess(auth_paths, maybe_cancel_channel) + Pow::<{ BlockPow::MERKLE_TREE_HEIGHT }>::preprocess( + auth_paths, + maybe_cancel_channel, + consensus_rule_set, + prev_block_digest, + ) }) } @@ -1001,9 +1010,17 @@ impl Block { } /// Verify that block digest is less than threshold and integral. - fn pow_verify(&self, target: Digest) -> bool { + pub(crate) fn pow_verify(&self, target: Digest, consensus_rule_set: ConsensusRuleSet) -> bool { let auth_paths = self.pow_mast_paths(); - self.header().pow.validate(auth_paths, target).is_ok() + self.header() + .pow + .validate( + auth_paths, + target, + consensus_rule_set, + self.header().prev_block_digest, + ) + .is_ok() } pub fn set_header_pow(&mut self, pow: BlockPow) { @@ -1117,6 +1134,7 @@ pub(crate) mod tests { use rand::rngs::StdRng; use rand::Rng; use rand::SeedableRng; + use strum::IntoEnumIterator; use tasm_lib::twenty_first::util_types::mmr::mmr_trait::LeafMutation; use tracing_test::traced_test; @@ -1133,6 +1151,7 @@ pub(crate) mod tests { use crate::application::loops::mine_loop::composer_parameters::ComposerParameters; use crate::application::loops::mine_loop::prepare_coinbase_transaction_stateless; use crate::application::loops::mine_loop::tests::make_coinbase_transaction_from_state; + use crate::application::rpc::server::proof_of_work_puzzle::ProofOfWorkPuzzle; use crate::application::triton_vm_job_queue::vm_job_queue; use crate::application::triton_vm_job_queue::TritonVmJobPriority; use crate::protocol::consensus::transaction::primitive_witness::PrimitiveWitness; @@ -1219,22 +1238,21 @@ pub(crate) mod tests { } /// Satisfy PoW for this block. Only to be used for tests since this - /// function cannot be cancelled. - pub(crate) fn satisfy_pow(&mut self, difficulty: Difficulty, seed: [u8; 32]) { - let guesser_buffer = self.guess_preprocess(None, None); - println!("Trying to guess for difficulty: {difficulty}"); + /// function cannot be cancelled. Deterministic, will always return the + /// same solution for the same input. + pub(crate) fn satisfy_pow( + &mut self, + parent_difficulty: Difficulty, + consensus_rule_set: ConsensusRuleSet, + ) { + println!("Trying to guess for difficulty: {parent_difficulty}"); assert!( - difficulty < Difficulty::from(DIFFICULTY_LIMIT_FOR_TESTS), + parent_difficulty < Difficulty::from(DIFFICULTY_LIMIT_FOR_TESTS), "Don't use high difficulty in test" ); - let target = difficulty.target(); - let mut rng = ::from_seed(seed); - let valid_pow = loop { - if let Some(valid_pow) = Pow::guess(&guesser_buffer, rng.random(), target) { - break valid_pow; - } - }; + let puzzle = ProofOfWorkPuzzle::new(self.clone(), parent_difficulty); + let valid_pow = puzzle.solve(consensus_rule_set); self.set_header_pow(valid_pow); } @@ -1286,23 +1304,35 @@ pub(crate) mod tests { #[test] fn guess_nonce_happy_path() { let network = Network::Main; - let mut invalid_block = invalid_empty_block(&Block::genesis(network), network); - let guesser_buffer = invalid_block.guess_preprocess(None, None); - let target = Difficulty::from(2u32).target(); - let mut rng = rng(); - - let valid_pow = loop { - if let Some(valid_pow) = Pow::guess(&guesser_buffer, rng.random(), target) { - break valid_pow; - } - }; + let genesis = Block::genesis(network); + let mut invalid_block = invalid_empty_block(&genesis, network); + let mast_auth_paths = invalid_block.pow_mast_paths(); - assert!( - !invalid_block.pow_verify(target), - "Pow verification must fail prior to setting PoW" - ); - invalid_block.set_header_pow(valid_pow); - assert!(invalid_block.pow_verify(target)); + for consensus_rule_set in ConsensusRuleSet::iter() { + let guesser_buffer = invalid_block.guess_preprocess(None, None, consensus_rule_set); + let target = Difficulty::from(2u32).target(); + let mut rng = rng(); + let index_picker_preimage = guesser_buffer.index_picker_preimage(&mast_auth_paths); + + let valid_pow = loop { + if let Some(valid_pow) = Pow::guess( + &guesser_buffer, + &mast_auth_paths, + index_picker_preimage, + rng.random(), + target, + ) { + break valid_pow; + } + }; + + assert!( + !invalid_block.pow_verify(target, consensus_rule_set), + "Pow verification must fail prior to setting PoW" + ); + invalid_block.set_header_pow(valid_pow); + assert!(invalid_block.pow_verify(target, consensus_rule_set)); + } } #[test] diff --git a/neptune-core/src/protocol/consensus/block/pow.rs b/neptune-core/src/protocol/consensus/block/pow.rs index 2eaab993e..a1e8f75eb 100644 --- a/neptune-core/src/protocol/consensus/block/pow.rs +++ b/neptune-core/src/protocol/consensus/block/pow.rs @@ -1,5 +1,6 @@ use std::fmt::Display; use std::mem::MaybeUninit; +use std::num::NonZeroUsize; use get_size2::GetSize; use itertools::Itertools; @@ -23,6 +24,7 @@ use crate::application::loops::channel::Cancelable; use crate::protocol::consensus::block::block_header::BlockHeader; use crate::protocol::consensus::block::block_kernel::BlockKernel; use crate::protocol::consensus::block::Block; +use crate::protocol::consensus::consensus_rule_set::ConsensusRuleSet; use crate::protocol::proof_abstractions::mast_hash::MastHash; use crate::BFieldElement; @@ -197,6 +199,7 @@ pub struct Pow { } #[derive(Clone, Debug, Copy, Serialize, Deserialize, BFieldCodec, Default, PartialEq, Eq)] +#[cfg_attr(any(test, feature = "arbitrary-impls"), derive(arbitrary::Arbitrary))] pub struct PowMastPaths { pub(super) pow: [Digest; BlockHeader::MAST_HEIGHT], pub(super) header: [Digest; BlockKernel::MAST_HEIGHT], @@ -204,7 +207,7 @@ pub struct PowMastPaths { } impl PowMastPaths { - pub(super) fn commit(&self) -> Digest { + fn commit(&self) -> Digest { Tip5::hash_varlen( &[ self.pow.to_vec(), @@ -274,18 +277,38 @@ impl Display for Pow { pub struct GuesserBuffer { merkle_tree: MTree, - hash: Digest, + /// The hash of the parent block of this guesser buffer. + prev_block_digest: Digest, +} + +impl GuesserBuffer { + /// A commitment that refers to both the Merkle tree of the guesser buffer + /// and the current proposal being guessed on, designed in such a way that + /// the Merkle tree root must be known before indices can be picked. + /// Combined with a nonce, the opened indices can be derived from this + /// value. + fn index_picker_preimage_from_root( + merkle_tree_root: Digest, + mast_auth_paths: &PowMastPaths, + ) -> Digest { + Tip5::hash_pair(merkle_tree_root, mast_auth_paths.commit()) + } - /// Authentication paths for all fields but the PoW field - mast_auth_paths: PowMastPaths, + /// A commitment to everything in the block except for the nonce and the + /// authentication paths of the pow structure. + /// + /// The return value is hashed together with the nonce to calculate the + /// indices which are opened into the guesser buffer's Merkle tree. + pub fn index_picker_preimage(&self, mast_auth_paths: &PowMastPaths) -> Digest { + Self::index_picker_preimage_from_root(self.merkle_tree.root(), mast_auth_paths) + } } impl Default for GuesserBuffer { fn default() -> Self { Self { merkle_tree: MTree::default(), - hash: Default::default(), - mast_auth_paths: Default::default(), + prev_block_digest: Default::default(), } } } @@ -318,12 +341,42 @@ impl Pow { (index_a, index_b) } + const fn bitreverse(mut k: u32, log2_n: u32) -> u32 { + k = ((k & 0x55555555) << 1) | ((k & 0xaaaaaaaa) >> 1); + k = ((k & 0x33333333) << 2) | ((k & 0xcccccccc) >> 2); + k = ((k & 0x0f0f0f0f) << 4) | ((k & 0xf0f0f0f0) >> 4); + k = ((k & 0x00ff00ff) << 8) | ((k & 0xff00ff00) >> 8); + k = k.rotate_right(16); + k >> ((32 - log2_n) & 0x1f) + } + + fn swap_indices(len: usize) -> Vec> { + let log_2_len = len.checked_ilog2().unwrap_or(0); + (0..len) + .map(|k| { + let rev_k = Self::bitreverse(k as u32, log_2_len); + + // 0 >= bitreverse(0, log_2_len) == 0 => unwrap is fine + ((k as u32) < rev_k).then(|| NonZeroUsize::new(rev_k as usize).unwrap()) + }) + .collect() + } + pub(crate) fn preprocess( mast_auth_paths: PowMastPaths, cancel_channel: Option<&dyn Cancelable>, + consensus_rule_set: ConsensusRuleSet, + prev_block_digest: Digest, ) -> GuesserBuffer { - // Commitment to all the fields in the block that are not pow - let commitment = mast_auth_paths.commit(); + let bud_prefix = if consensus_rule_set == ConsensusRuleSet::Reboot { + // Commitment to all the fields in the block that are not pow + mast_auth_paths.commit() + } else { + // Commitment only to previous block hash such that preprocessing + // can be reused across different block proposals with the same + // parent. + prev_block_digest + }; // number steps between checking channel cancellation let checkpoint_distance: usize = @@ -350,7 +403,7 @@ impl Pow { .into_par_iter() .zip(ins[range].par_iter_mut()) .for_each(|(i, bud)| { - bud.write(Self::bud(commitment, i as u64)); + bud.write(Self::bud(bud_prefix, i as u64)); }); if cancel_channel.is_some_and(|channel| channel.is_canceled()) { @@ -365,7 +418,7 @@ impl Pow { // iterate log-many times over the buffer to compute leafs from buds let mut outs = ins.clone(); let mut buds = &mut ins; - let mut leafs = &mut outs; + let mut resulting_leafs = &mut outs; let mut num_swaps = 0; for i in 0..NUM_BUD_LAYERS { for j in 0..num_checkpoints { @@ -373,7 +426,7 @@ impl Pow { range .clone() .into_par_iter() - .zip(leafs[range].par_iter_mut()) + .zip(resulting_leafs[range].par_iter_mut()) .for_each(|(k, leaf)| { *leaf = Tip5::hash_pair(buds[k], buds[(k + (1 << i)) % Self::NUM_LEAFS]); }); @@ -382,36 +435,49 @@ impl Pow { return Default::default(); } } - std::mem::swap(&mut leafs, &mut buds); + std::mem::swap(&mut resulting_leafs, &mut buds); num_swaps += 1; } - std::mem::swap(&mut leafs, &mut buds); + std::mem::swap(&mut resulting_leafs, &mut buds); num_swaps += 1; - let merkle_tree = if num_swaps & 1 == 1 { - MTree::build_inplace(ins, outs, cancel_channel) + let (mut leafs, internal_nodes) = if num_swaps & 1 == 1 { + (ins, outs) } else { - MTree::build_inplace(outs, ins, cancel_channel) + (outs, ins) + }; + + if consensus_rule_set != ConsensusRuleSet::Reboot { + // The index swapping could be done here, or in each guess. Since + // we're optimizing for fast guessing, the index swapping is done + // here. + let swap_indices = Self::swap_indices(leafs.len()); + for (k, maybe_rev_k) in swap_indices.iter().enumerate() { + if let Some(rev_k) = maybe_rev_k { + leafs.swap(k, rev_k.get()); + } + } }; - let hash = Tip5::hash_pair(merkle_tree.root(), commitment); + let merkle_tree = MTree::build_inplace(leafs, internal_nodes, cancel_channel); GuesserBuffer:: { merkle_tree, - hash, - mast_auth_paths, + prev_block_digest, } } pub fn guess( buffer: &GuesserBuffer, + mast_auth_paths: &PowMastPaths, + index_picker_preimage: Digest, nonce: Digest, target: Digest, ) -> Option { let root = buffer.merkle_tree.root(); - let (index_a, index_b) = Self::indices(buffer.hash, nonce); + let (index_a, index_b) = Self::indices(index_picker_preimage, nonce); let path_a = buffer .merkle_tree @@ -432,7 +498,7 @@ impl Pow { path_b, }; - let pow_digest = buffer.mast_auth_paths.fast_mast_hash(pow); + let pow_digest = mast_auth_paths.fast_mast_hash(pow); if pow_digest > target { None } else { @@ -444,16 +510,39 @@ impl Pow { self, auth_paths: PowMastPaths, target: Digest, + consensus_rule_set: ConsensusRuleSet, + parent_digest: Digest, ) -> Result<(), PowValidationError> { - let commitment = auth_paths.commit(); - let buffer_hash = Tip5::hash_pair(self.root, commitment); - let (index_a, index_b) = Self::indices(buffer_hash, self.nonce); - let leaf_a = Self::leaf(commitment, index_a); + let leaf_prefix = match consensus_rule_set { + ConsensusRuleSet::Reboot => auth_paths.commit(), + ConsensusRuleSet::HardforkAlpha => parent_digest, + }; + let index_picker_preimage = Tip5::hash_pair(self.root, auth_paths.commit()); + let (index_a, index_b) = Self::indices(index_picker_preimage, self.nonce); + + let (leaf_a, leaf_b) = if consensus_rule_set == ConsensusRuleSet::Reboot { + ( + Self::leaf(leaf_prefix, index_a), + Self::leaf(leaf_prefix, index_b), + ) + } else { + let index_a = u64::from(Self::bitreverse( + index_a.try_into().unwrap(), + Self::MERKLE_TREE_HEIGHT as u32, + )); + let index_b = u64::from(Self::bitreverse( + index_b.try_into().unwrap(), + Self::MERKLE_TREE_HEIGHT as u32, + )); + ( + Self::leaf(leaf_prefix, index_a), + Self::leaf(leaf_prefix, index_b), + ) + }; + if !MTree::verify(self.root, index_a as usize, &self.path_a, leaf_a) { return Err(PowValidationError::PathAInvalid); } - - let leaf_b = Self::leaf(commitment, index_b); if !MTree::verify(self.root, index_b as usize, &self.path_b, leaf_b) { return Err(PowValidationError::PathBInvalid); } @@ -502,12 +591,18 @@ impl Distribution> for pub(crate) mod tests { use std::time::Instant; + use proptest::prop_assert; + use proptest::prop_assert_eq; + use proptest_arbitrary_interop::arb; use rand::rng; + use strum::IntoEnumIterator; use tasm_lib::twenty_first::bfe; + use test_strategy::proptest; use super::*; use crate::api::export::Network; use crate::protocol::consensus::block::difficulty_control::Difficulty; + use crate::protocol::consensus::block::tests::DIFFICULTY_LIMIT_FOR_TESTS; use crate::tests::shared::blocks::invalid_empty_block; impl MTree { @@ -526,7 +621,13 @@ pub(crate) mod tests { const MERKLE_TREE_NUM_LEAFS: usize = 1usize << 10; let mut rng = rng(); let auth_paths = rng.random::(); - let buffer = Pow::::preprocess(auth_paths, None); + let prev_block_digest = rng.random(); + let buffer = Pow::::preprocess( + auth_paths, + None, + ConsensusRuleSet::default(), + prev_block_digest, + ); let index = rng.random_range(0..MERKLE_TREE_NUM_LEAFS); let expensive_leaf = Pow::::leaf(auth_paths.commit(), index as u64); @@ -537,9 +638,17 @@ pub(crate) mod tests { #[test] #[ignore = "benchmark of memory and time requirements of preprocess for guessing"] fn benchmark_memory_requirements() { - fn report(auth_paths: PowMastPaths) { + fn report( + auth_paths: PowMastPaths, + parent_block_hash: Digest, + ) { let start = Instant::now(); - let buffer = Pow::::preprocess(auth_paths, None); + let buffer = Pow::::preprocess( + auth_paths, + None, + ConsensusRuleSet::default(), + parent_block_hash, + ); let duration = start.elapsed(); let estimated_mt_size = buffer.merkle_tree.num_leafs() * 2 * Digest::BYTES; println!("Merkle tree height: {MERKLE_TREE_HEIGHT}"); @@ -549,11 +658,12 @@ pub(crate) mod tests { let mut rng = rng(); let auth_paths = rng.random::(); - report::<25>(auth_paths); - report::<26>(auth_paths); - report::<27>(auth_paths); - report::<28>(auth_paths); - report::<29>(auth_paths); + let parent_block_hash = rng.random::(); + report::<25>(auth_paths, parent_block_hash); + report::<26>(auth_paths, parent_block_hash); + report::<27>(auth_paths, parent_block_hash); + report::<28>(auth_paths, parent_block_hash); + report::<29>(auth_paths, parent_block_hash); } #[test] @@ -583,28 +693,170 @@ pub(crate) mod tests { assert_eq!(invalid_block.hash(), hash_from_fast_mast); } + #[test] + fn bitreverse_unit_test() { + assert_eq!(7, Pow::<10>::bitreverse(7, 3)); + assert_eq!(14, Pow::<10>::bitreverse(7, 4)); + assert_eq!(3, Pow::<10>::bitreverse(7, 2)); + assert_eq!(1, Pow::<10>::bitreverse(7, 1)); + assert_eq!(7, Pow::<10>::bitreverse(14, 4)); + assert_eq!(3, Pow::<10>::bitreverse(12, 4)); + assert_eq!(19, Pow::<10>::bitreverse(100, 7)); + } + + #[proptest] + fn bitreverse_is_symmetric(k: u32, #[strategy(1u32..=32)] log2_n: u32) { + let mask = u32::MAX >> (32 - log2_n); + + let r = Pow::<10>::bitreverse(k, log2_n); + + // result must be within [0, 2^log2_n) + prop_assert!(r <= mask); + + // applying bitreverse again recovers the masked original + let rr = Pow::<10>::bitreverse(r, log2_n); + prop_assert_eq!(rr, k & mask); + } + #[test] fn happy_path() { const MERKLE_TREE_HEIGHT: usize = 10; let mut rng = rng(); let auth_paths = rng.random::(); - let buffer = Pow::::preprocess(auth_paths, None); - - for difficulty in [2_u32, 4] { - let target = Difficulty::from(difficulty).target(); - let mut successful_guess = None; - 'inner_loop: for _ in 0..120 { - let nonce = rng.random(); - if let Some(solution) = Pow::guess(&buffer, nonce, target) { - successful_guess = Some(solution); - break 'inner_loop; - } + let prev_block_digest = rng.random(); + + for consensus_rule_set in ConsensusRuleSet::iter() { + let buffer = Pow::::preprocess( + auth_paths, + None, + consensus_rule_set, + prev_block_digest, + ); + + for difficulty in [2_u32, 4] { + let difficulty = Difficulty::from(difficulty); + let successful_guess = solve(&buffer, &auth_paths, difficulty); + assert!(successful_guess + .validate( + auth_paths, + difficulty.target(), + consensus_rule_set, + prev_block_digest + ) + .is_ok()); } + } + } - assert_eq!( - Ok(()), - successful_guess.unwrap().validate(auth_paths, target) - ); + /// Ensure that indices cannot be reused over two proposals that share the + /// same parent. + #[proptest(cases = 1)] + fn indices_depend_on_concrete_proposal_hardfork_alpha( + #[strategy(arb())] prev_block_digest: Digest, + #[strategy(arb())] auth_paths_1: PowMastPaths, + #[strategy(arb())] auth_paths_2: PowMastPaths, + #[strategy(arb())] nonce: Digest, + ) { + const MERKLE_TREE_HEIGHT: usize = 10; + let buffer = Pow::::preprocess( + auth_paths_1, + None, + ConsensusRuleSet::HardforkAlpha, + prev_block_digest, + ); + let indices_1 = + Pow::::indices(buffer.index_picker_preimage(&auth_paths_1), nonce); + let indices_2 = + Pow::::indices(buffer.index_picker_preimage(&auth_paths_2), nonce); + assert_ne!(indices_1, indices_2); + } + + #[proptest(cases = 6)] + fn guesser_buffer_can_be_reused_after_hardfork_alpha( + #[strategy(arb())] prev_block_digest: Digest, + #[strategy(arb())] auth_paths_1: PowMastPaths, + #[strategy(arb())] auth_paths_2: PowMastPaths, + ) { + const MERKLE_TREE_HEIGHT: usize = 10; + + let buffer = Pow::::preprocess( + auth_paths_1, + None, + ConsensusRuleSet::HardforkAlpha, + prev_block_digest, + ); + assert_eq!( + buffer, + Pow::::preprocess( + auth_paths_2, + None, + ConsensusRuleSet::HardforkAlpha, + prev_block_digest, + ), + "After hardfork, buffer must only depend on previous block hash" + ); + + // Verify that 1st block proposal can be solved + let difficulty = Difficulty::from(2u32); + let correct_guess_1 = solve(&buffer, &auth_paths_1, difficulty); + assert!(correct_guess_1 + .validate( + auth_paths_1, + difficulty.target(), + ConsensusRuleSet::HardforkAlpha, + prev_block_digest + ) + .is_ok()); + + // Verify that old solution does not work when auth paths change. + assert_eq!( + PowValidationError::PathAInvalid, + correct_guess_1 + .validate( + auth_paths_2, + difficulty.target(), + ConsensusRuleSet::HardforkAlpha, + prev_block_digest + ) + .unwrap_err(), + "2nd set of auth paths must make 1st PoW solution invalid, as pow's\ + Merkle authentication path becomes invalid" + ); + + // Verify that a 2nd proposal can use the same `buffer` value to create + // a successful guess, and that only the `auth_paths` value needs to + // change. + let correct_guess_2 = solve(&buffer, &auth_paths_2, difficulty); + assert!(correct_guess_2 + .validate( + auth_paths_2, + difficulty.target(), + ConsensusRuleSet::HardforkAlpha, + prev_block_digest + ) + .is_ok()); + } + + fn solve( + buffer: &GuesserBuffer, + auth_paths: &PowMastPaths, + difficulty: Difficulty, + ) -> Pow { + assert!( + difficulty < Difficulty::from(DIFFICULTY_LIMIT_FOR_TESTS), + "Let's not make tests run too long" + ); + + let target = difficulty.target(); + let mut rng = rand::rng(); + let index_picker_preimage = buffer.index_picker_preimage(auth_paths); + loop { + let nonce = rng.random(); + if let Some(solution) = + Pow::guess(buffer, auth_paths, index_picker_preimage, nonce, target) + { + break solution; + } } } @@ -625,8 +877,14 @@ pub(crate) mod tests { let mast_auth_paths = rng.random::(); // spawn preprocess task + let prev_block_digest = rng.random(); let join_handle = tokio::task::spawn_blocking(move || { - Pow::::preprocess(mast_auth_paths, Some(&tx)) + Pow::::preprocess( + mast_auth_paths, + Some(&tx), + ConsensusRuleSet::default(), + prev_block_digest, + ) }); // wait 0.5 millis diff --git a/neptune-core/src/protocol/consensus/block/validity/block_primitive_witness.rs b/neptune-core/src/protocol/consensus/block/validity/block_primitive_witness.rs index 541e3ea61..e6b5c8148 100644 --- a/neptune-core/src/protocol/consensus/block/validity/block_primitive_witness.rs +++ b/neptune-core/src/protocol/consensus/block/validity/block_primitive_witness.rs @@ -161,6 +161,7 @@ pub(crate) mod tests { use crate::protocol::consensus::block::block_header::BlockHeader; use crate::protocol::consensus::block::block_kernel::BlockKernel; use crate::protocol::consensus::block::block_transaction::BlockTransaction; + use crate::protocol::consensus::block::difficulty_control::Difficulty; use crate::protocol::consensus::block::Block; use crate::protocol::consensus::block::BlockProof; use crate::protocol::consensus::consensus_rule_set::ConsensusRuleSet; @@ -280,8 +281,21 @@ pub(crate) mod tests { .current() } - pub(crate) fn arbitrary_with_block_height( + pub(crate) fn deterministic_with_block_height_and_difficulty( + block_height: BlockHeight, + difficulty: Difficulty, + ) -> Self { + let mut test_runner = TestRunner::deterministic(); + + Self::arbitrary_with_height_and_difficulty(block_height, difficulty) + .new_tree(&mut test_runner) + .unwrap() + .current() + } + + pub(crate) fn arbitrary_with_height_and_difficulty( block_height: BlockHeight, + difficulty: Difficulty, ) -> BoxedStrategy { const NUM_INPUTS: usize = 2; let network = Network::Main; @@ -347,7 +361,10 @@ pub(crate) mod tests { let parent_height = block_height.previous().unwrap(); let parent_header = - BlockHeader::arbitrary_with_height(parent_height); + BlockHeader::arbitrary_with_height_and_difficulty( + parent_height, + difficulty, + ); let parent_appendix = arb::(); let parent_body = BlockBody::arbitrary_with_mutator_set_accumulator( intermediate_mutator_set_accumulator.clone(), @@ -430,5 +447,16 @@ pub(crate) mod tests { ) .boxed() } + + pub(crate) fn arbitrary_with_block_height( + block_height: BlockHeight, + ) -> BoxedStrategy { + let difficulty = arb::(); + difficulty + .prop_flat_map(move |difficulty| { + Self::arbitrary_with_height_and_difficulty(block_height, difficulty) + }) + .boxed() + } } } diff --git a/neptune-core/src/protocol/consensus/consensus_rule_set.rs b/neptune-core/src/protocol/consensus/consensus_rule_set.rs index 6888144e8..9bd8fa3ad 100644 --- a/neptune-core/src/protocol/consensus/consensus_rule_set.rs +++ b/neptune-core/src/protocol/consensus/consensus_rule_set.rs @@ -3,6 +3,15 @@ use strum_macros::EnumIter; use crate::api::export::BlockHeight; use crate::api::export::Network; use crate::protocol::consensus::block::MAX_NUM_INPUTS_OUTPUTS_ANNOUNCEMENTS; +use crate::BFieldElement; + +/// Height of 1st block that follows the alpha consensus ruleset, for main net. +pub const BLOCK_HEIGHT_HARDFORK_ALPHA_MAIN_NET: BlockHeight = + BlockHeight::new(BFieldElement::new(15_000u64)); + +/// Height of 1st block that follows the alpha consensus ruleset, for test net. +pub const BLOCK_HEIGHT_HARDFORK_ALPHA_TESTNET: BlockHeight = + BlockHeight::new(BFieldElement::new(120u64)); /// Enumerates all possible sets of consensus rules. /// @@ -18,13 +27,14 @@ use crate::protocol::consensus::block::MAX_NUM_INPUTS_OUTPUTS_ANNOUNCEMENTS; pub enum ConsensusRuleSet { #[default] Reboot, + HardforkAlpha, } impl ConsensusRuleSet { /// Maximum block size in number of BFieldElements pub(crate) const fn max_block_size(&self) -> usize { match self { - ConsensusRuleSet::Reboot => { + ConsensusRuleSet::Reboot | ConsensusRuleSet::HardforkAlpha => { // This size is 8MB which should keep it feasible to run archival nodes for // many years without requiring excessive disk space. 1_000_000 @@ -37,23 +47,46 @@ impl ConsensusRuleSet { /// planned hard or soft forks that activate at a given height. The first /// argument is necessary because the forks can activate at different /// heights based on the network. - pub(crate) fn infer_from(_network: Network, _block_height: BlockHeight) -> Self { - Self::Reboot + pub(crate) fn infer_from(network: Network, block_height: BlockHeight) -> Self { + match network { + Network::Main => { + if block_height < BLOCK_HEIGHT_HARDFORK_ALPHA_MAIN_NET { + ConsensusRuleSet::Reboot + } else { + ConsensusRuleSet::HardforkAlpha + } + } + Network::TestnetMock => ConsensusRuleSet::HardforkAlpha, + Network::RegTest => ConsensusRuleSet::HardforkAlpha, + Network::Testnet(_) => { + if block_height < BLOCK_HEIGHT_HARDFORK_ALPHA_TESTNET { + ConsensusRuleSet::Reboot + } else { + ConsensusRuleSet::HardforkAlpha + } + } + } } pub(crate) fn max_num_inputs(&self) -> usize { match self { - ConsensusRuleSet::Reboot => MAX_NUM_INPUTS_OUTPUTS_ANNOUNCEMENTS, + ConsensusRuleSet::Reboot | ConsensusRuleSet::HardforkAlpha => { + MAX_NUM_INPUTS_OUTPUTS_ANNOUNCEMENTS + } } } pub(crate) fn max_num_outputs(&self) -> usize { match self { - ConsensusRuleSet::Reboot => MAX_NUM_INPUTS_OUTPUTS_ANNOUNCEMENTS, + ConsensusRuleSet::Reboot | ConsensusRuleSet::HardforkAlpha => { + MAX_NUM_INPUTS_OUTPUTS_ANNOUNCEMENTS + } } } pub(crate) fn max_num_announcements(&self) -> usize { match self { - ConsensusRuleSet::Reboot => MAX_NUM_INPUTS_OUTPUTS_ANNOUNCEMENTS, + ConsensusRuleSet::Reboot | ConsensusRuleSet::HardforkAlpha => { + MAX_NUM_INPUTS_OUTPUTS_ANNOUNCEMENTS + } } } } @@ -61,6 +94,9 @@ impl ConsensusRuleSet { #[cfg(test)] pub(crate) mod tests { + use std::sync::Arc; + + use futures::channel::oneshot; use itertools::Itertools; use rand::rngs::StdRng; use rand::Rng; @@ -73,26 +109,33 @@ pub(crate) mod tests { use crate::api::export::KeyType; use crate::api::export::NativeCurrencyAmount; use crate::api::export::OutputFormat; + use crate::api::export::ReceivingAddress; use crate::api::export::StateLock; use crate::api::export::Timestamp; - use crate::api::export::Transaction; use crate::api::export::TransactionProofType; + use crate::api::export::TxCreationArtifacts; use crate::api::export::TxProvingCapability; use crate::api::tx_initiation::builder::transaction_builder::TransactionBuilder; use crate::api::tx_initiation::builder::transaction_details_builder::TransactionDetailsBuilder; use crate::api::tx_initiation::builder::transaction_proof_builder::TransactionProofBuilder; use crate::api::tx_initiation::builder::triton_vm_proof_job_options_builder::TritonVmProofJobOptionsBuilder; + use crate::api::tx_initiation::builder::tx_input_list_builder::SortOrder; use crate::api::tx_initiation::builder::tx_input_list_builder::TxInputListBuilder; use crate::application::config::cli_args; + use crate::application::loops::channel::NewBlockFound; use crate::application::loops::mine_loop::compose_block_helper; use crate::application::loops::mine_loop::create_block_transaction_from; + use crate::application::loops::mine_loop::guess_nonce; + use crate::application::loops::mine_loop::GuessingConfiguration; use crate::application::loops::mine_loop::TxMergeOrigin; use crate::application::triton_vm_job_queue::vm_job_queue; + use crate::protocol::consensus::block::difficulty_control::Difficulty; use crate::protocol::consensus::block::validity::block_primitive_witness::BlockPrimitiveWitness; use crate::protocol::consensus::block::Block; use crate::protocol::proof_abstractions::tasm::program::TritonVmProofJobOptions; use crate::state::wallet::expected_utxo::ExpectedUtxo; use crate::state::wallet::wallet_entropy::WalletEntropy; + use crate::tests::shared::blocks::next_block; use crate::tests::shared::globalstate::mock_genesis_global_state_with_block; use crate::tests::tokio_runtime; @@ -100,7 +143,7 @@ pub(crate) mod tests { mut state: GlobalStateLock, num_outputs: usize, timestamp: Timestamp, - ) -> Transaction { + ) -> TxCreationArtifacts { let mut addresses_and_amts = vec![]; let same_address = state .api() @@ -131,7 +174,7 @@ pub(crate) mod tests { .into_iter() .collect(), ) - .policy(InputSelectionPolicy::ByProvidedOrder) + .policy(InputSelectionPolicy::ByUtxoSize(SortOrder::Ascending)) .spend_amount(tx_outputs.total_native_coins() + fee) .build(); let tx_inputs = tx_inputs.into_iter().collect_vec(); @@ -164,11 +207,16 @@ pub(crate) mod tests { .await .unwrap(); - TransactionBuilder::new() + let transaction = TransactionBuilder::new() .transaction_details(&tx_details) .transaction_proof(proof) .build() - .unwrap() + .unwrap(); + + TxCreationArtifacts { + transaction: Arc::new(transaction), + details: Arc::new(tx_details), + } } async fn block_with_n_outputs( @@ -183,7 +231,7 @@ pub(crate) mod tests { me, timestamp, TritonVmProofJobOptions::default(), - TxMergeOrigin::ExplicitList(vec![tx_many_outputs]), + TxMergeOrigin::ExplicitList(vec![tx_many_outputs.transaction.into()]), ) .await .unwrap(); @@ -297,4 +345,250 @@ pub(crate) mod tests { predecessor = next_block; } } + + #[traced_test] + #[test] + fn hard_fork_alpha() { + // Start at hard fork block height minus 2 + // Then mine enough blocks to activate the hard fork. Verify that all + // blocks are valid under the expected consensus rule set. + let init_block_heigth = BlockHeight::from(14998u64); + let bpw = BlockPrimitiveWitness::deterministic_with_block_height_and_difficulty( + init_block_heigth, + Difficulty::MINIMUM, + ); + + tokio_runtime().block_on(new_blocks_hardfork_alpha(bpw)); + + async fn new_blocks_hardfork_alpha(block_primitive_witness: BlockPrimitiveWitness) { + // 1. generate state synced to height + let mut rng = StdRng::seed_from_u64(55512345); + let network = Network::Main; + let bob_wallet = WalletEntropy::new_pseudorandom(rng.random()); + let cli = cli_args::Args { + network, + compose: true, + guess: true, + tx_proving_capability: Some(TxProvingCapability::SingleProof), + ..Default::default() + }; + + let (block_a, block_b_no_pow) = + Block::fake_block_pair_genesis_and_child_from_witness(block_primitive_witness) + .await; + + assert!( + block_b_no_pow + .is_valid(&block_a, block_b_no_pow.header().timestamp, network) + .await + ); + let mut bob = + mock_genesis_global_state_with_block(2, bob_wallet, cli.clone(), block_a.clone()) + .await; + + // Solve PoW for block_b + let guesser_address: ReceivingAddress = bob + .lock_guard() + .await + .wallet_state + .wallet_entropy + .guesser_fee_key() + .to_address() + .into(); + let (guesser_tx_b, guesser_rx_b) = oneshot::channel::(); + let guesser_timestamp_b = block_b_no_pow.header().timestamp; + guess_nonce( + network, + block_b_no_pow, + *block_a.header(), + guesser_tx_b, + GuessingConfiguration { + num_guesser_threads: cli.guesser_threads, + address: guesser_address.clone(), + // For deterministic pow-guessing, both RNG and timestamp + // must be deterministic. + override_rng: Some(rng), + override_timestamp: Some(guesser_timestamp_b), + }, + ) + .await; + let block_b = *guesser_rx_b.await.unwrap().block; + assert!( + block_b + .is_valid(&block_a, block_b.header().timestamp, network) + .await + ); + assert!(block_b.has_proof_of_work(network, block_a.header())); + assert!(block_b.pow_verify( + block_a.header().difficulty.target(), + ConsensusRuleSet::Reboot + )); + assert!(!block_b.pow_verify( + block_a.header().difficulty.target(), + ConsensusRuleSet::HardforkAlpha + )); + + bob.set_new_tip(block_b.clone()).await.unwrap(); + assert_eq!( + 14998u64, + bob.lock_guard() + .await + .chain + .light_state() + .header() + .height + .value() + ); + + // hard fork minus 1 + let block_c = next_block(bob.clone(), block_b.clone()).await; + assert!( + block_c + .is_valid(&block_b, block_c.header().timestamp, network) + .await + ); + assert!(block_c.has_proof_of_work(network, block_b.header())); + assert!(block_c.pow_verify( + block_b.header().difficulty.target(), + ConsensusRuleSet::Reboot + )); + assert!(!block_c.pow_verify( + block_b.header().difficulty.target(), + ConsensusRuleSet::HardforkAlpha + )); + bob.set_new_tip(block_c.clone()).await.unwrap(); + assert_eq!( + 14999u64, + bob.lock_guard() + .await + .chain + .light_state() + .header() + .height + .value() + ); + + // 1st block after hard fork! + let block_d = next_block(bob.clone(), block_c.clone()).await; + assert!( + block_d + .is_valid(&block_c, block_d.header().timestamp, network) + .await + ); + assert!(block_d.has_proof_of_work(network, block_c.header())); + assert!(!block_d.pow_verify( + block_c.header().difficulty.target(), + ConsensusRuleSet::Reboot + )); + assert!(block_d.pow_verify( + block_c.header().difficulty.target(), + ConsensusRuleSet::HardforkAlpha + )); + bob.set_new_tip(block_d.clone()).await.unwrap(); + assert_eq!( + 15000u64, + bob.lock_guard() + .await + .chain + .light_state() + .header() + .height + .value() + ); + + // 2nd block after hard fork + let block_e = next_block(bob.clone(), block_d.clone()).await; + assert!( + block_e + .is_valid(&block_d, block_e.header().timestamp, network) + .await + ); + assert!(block_e.has_proof_of_work(network, block_d.header())); + assert!(!block_e.pow_verify( + block_d.header().difficulty.target(), + ConsensusRuleSet::Reboot + )); + assert!(block_e.pow_verify( + block_d.header().difficulty.target(), + ConsensusRuleSet::HardforkAlpha + )); + bob.set_new_tip(block_e.clone()).await.unwrap(); + assert_eq!( + 15001u64, + bob.lock_guard() + .await + .chain + .light_state() + .header() + .height + .value() + ); + + // 3rd block after hard fork + let block_f = next_block(bob.clone(), block_e.clone()).await; + assert!( + block_f + .is_valid(&block_e, block_f.header().timestamp, network) + .await + ); + assert!(block_f.has_proof_of_work(network, block_e.header())); + assert!(!block_f.pow_verify( + block_e.header().difficulty.target(), + ConsensusRuleSet::Reboot + )); + assert!(block_f.pow_verify( + block_e.header().difficulty.target(), + ConsensusRuleSet::HardforkAlpha + )); + bob.set_new_tip(block_f.clone()).await.unwrap(); + assert_eq!( + 15002u64, + bob.lock_guard() + .await + .chain + .light_state() + .header() + .height + .value() + ); + + // 4th block after hard fork, with a transaction. + // Create transaction + let tx_timestamp = block_f.header().timestamp + Timestamp::minutes(6); + let tx_artifacts = tx_with_n_outputs(bob.clone(), 2, tx_timestamp).await; + bob.api_mut() + .tx_initiator_mut() + .record_and_broadcast_transaction(&tx_artifacts) + .await + .unwrap(); + + // Create block, with above transaction + let block_g = next_block(bob.clone(), block_f.clone()).await; + assert!( + block_g + .is_valid(&block_f, block_g.header().timestamp, network) + .await + ); + assert!(block_g.has_proof_of_work(network, block_f.header())); + assert!(!block_g.pow_verify( + block_f.header().difficulty.target(), + ConsensusRuleSet::Reboot + )); + assert!(block_g.pow_verify( + block_f.header().difficulty.target(), + ConsensusRuleSet::HardforkAlpha + )); + bob.set_new_tip(block_g.clone()).await.unwrap(); + assert_eq!( + 15003u64, + bob.lock_guard() + .await + .chain + .light_state() + .header() + .height + .value() + ); + } + } } diff --git a/neptune-core/src/protocol/consensus/transaction/validity/single_proof.rs b/neptune-core/src/protocol/consensus/transaction/validity/single_proof.rs index 981116540..68a14cde3 100644 --- a/neptune-core/src/protocol/consensus/transaction/validity/single_proof.rs +++ b/neptune-core/src/protocol/consensus/transaction/validity/single_proof.rs @@ -281,7 +281,7 @@ pub(crate) async fn produce_single_proof( consensus_rule_set: ConsensusRuleSet, ) -> Result { match consensus_rule_set { - ConsensusRuleSet::Reboot => { + ConsensusRuleSet::Reboot | ConsensusRuleSet::HardforkAlpha => { SingleProof::produce(primitive_witness, triton_vm_job_queue, proof_job_options).await } } @@ -297,7 +297,9 @@ pub(crate) fn single_proof_claim( consensus_rule_set: ConsensusRuleSet, ) -> Claim { match consensus_rule_set { - ConsensusRuleSet::Reboot => SingleProof::claim(tx_kernel_mast_hash), + ConsensusRuleSet::Reboot | ConsensusRuleSet::HardforkAlpha => { + SingleProof::claim(tx_kernel_mast_hash) + } } } diff --git a/neptune-core/src/state/wallet/wallet_state.rs b/neptune-core/src/state/wallet/wallet_state.rs index 0a4fff8ff..314571169 100644 --- a/neptune-core/src/state/wallet/wallet_state.rs +++ b/neptune-core/src/state/wallet/wallet_state.rs @@ -3194,6 +3194,8 @@ pub(crate) mod tests { GuessingConfiguration { num_guesser_threads: Some(2), address: guesser_key.to_address().into(), + override_rng: None, + override_timestamp: None, }, ) .await; diff --git a/neptune-core/src/tests/shared/blocks.rs b/neptune-core/src/tests/shared/blocks.rs index 3d6630d44..f737429a8 100644 --- a/neptune-core/src/tests/shared/blocks.rs +++ b/neptune-core/src/tests/shared/blocks.rs @@ -1,3 +1,4 @@ +use futures::channel::oneshot; use num_traits::Zero; use rand::rngs::StdRng; use rand::Rng; @@ -15,9 +16,13 @@ use crate::api::export::Network; use crate::api::export::ReceivingAddress; use crate::api::export::Timestamp; use crate::application::config::fee_notification_policy::FeeNotificationPolicy; +use crate::application::loops::channel::NewBlockFound; use crate::application::loops::mine_loop::coinbase_distribution::CoinbaseDistribution; +use crate::application::loops::mine_loop::compose_block_helper; use crate::application::loops::mine_loop::composer_parameters::ComposerParameters; +use crate::application::loops::mine_loop::guess_nonce; use crate::application::loops::mine_loop::make_coinbase_transaction_stateless; +use crate::application::loops::mine_loop::GuessingConfiguration; use crate::application::triton_vm_job_queue::TritonVmJobQueue; use crate::protocol::consensus::block::block_appendix::BlockAppendix; use crate::protocol::consensus::block::block_body::BlockBody; @@ -34,11 +39,13 @@ use crate::protocol::consensus::block::validity::block_program::BlockProgram; use crate::protocol::consensus::block::validity::block_proof_witness::BlockProofWitness; use crate::protocol::consensus::block::Block; use crate::protocol::consensus::block::BlockProof; +use crate::protocol::consensus::consensus_rule_set::ConsensusRuleSet; use crate::protocol::consensus::transaction::transaction_kernel::TransactionKernel; use crate::protocol::consensus::transaction::transaction_kernel::TransactionKernelModifier; use crate::protocol::consensus::transaction::transaction_kernel::TransactionKernelProxy; use crate::protocol::consensus::transaction::validity::neptune_proof::Proof; use crate::protocol::consensus::transaction::Transaction; +use crate::protocol::proof_abstractions::tasm::program::TritonVmProofJobOptions; use crate::protocol::proof_abstractions::verifier::cache_true_claim; use crate::state::wallet::address::generation_address; use crate::state::wallet::address::generation_address::GenerationReceivingAddress; @@ -48,6 +55,57 @@ use crate::util_types::mutator_set::addition_record::AdditionRecord; use crate::util_types::mutator_set::mutator_set_accumulator::MutatorSetAccumulator; use crate::util_types::mutator_set::removal_record::RemovalRecord; +/// Create a valid block on top of provided block. Returned block is valid in +/// terms of both block validity and PoW, and is thus the new canonical block of +/// the chain, assuming that tip is already the most canonical block. +/// +/// Returned PoW solution is deterministic, as is the block proof, and +/// consequently the entire block and its hash. +/// +/// The most valuable synced SingleProof-backed transaction in the mempool will +/// be included in the block. If mempool is empty a dummy transaction will be +/// merged with the coinbase transaction to set the merge bit. +pub(crate) async fn next_block(global_state_lock: GlobalStateLock, parent: Block) -> Block { + let network = global_state_lock.cli().network; + let (child_no_pow, _) = compose_block_helper( + parent.clone(), + global_state_lock.clone(), + parent.header().timestamp, + TritonVmProofJobOptions::default(), + ) + .await + .unwrap(); + + let deterministic_guesser_rng = StdRng::seed_from_u64(55512345); + + let guesser_address = global_state_lock + .lock_guard() + .await + .wallet_state + .wallet_entropy + .guesser_fee_key() + .to_address() + .into(); + let new_timestamp = parent.header().timestamp + Timestamp::minutes(9); + let (guesser_tx, guesser_rx) = oneshot::channel::(); + guess_nonce( + network, + child_no_pow, + *parent.header(), + guesser_tx, + GuessingConfiguration { + num_guesser_threads: global_state_lock.cli().guesser_threads, + address: guesser_address, + override_rng: Some(deterministic_guesser_rng), + override_timestamp: Some(new_timestamp), + }, + ) + .await; + let child = *guesser_rx.await.unwrap().block; + + child +} + /// Create an invalid block with the provided transaction kernel, using the /// provided mutator set as the predessor block's mutator set. Invalid block in /// most ways you can think of but the mutator set evolution is consistent. @@ -368,13 +426,13 @@ pub(crate) fn invalid_empty_block(predecessor: &Block, network: Network) -> Bloc /// Return a list of `n` invalid, empty blocks. pub(crate) fn invalid_empty_blocks_with_proof_size( - ancestor: &Block, + parent: &Block, n: usize, network: Network, proof_size: usize, ) -> Vec { let mut blocks = vec![]; - let mut predecessor = ancestor; + let mut predecessor = parent; for _ in 0..n { blocks.push(invalid_empty_block_with_proof_size( predecessor, @@ -440,14 +498,15 @@ pub(crate) async fn fake_valid_block_proposal_from_tx( /// Create a block from a transaction without the hassle of proving but such /// that it appears valid. -pub(crate) async fn fake_valid_block_from_block_tx_for_tests( +async fn fake_valid_block_from_block_tx_for_tests( predecessor: &Block, tx: BlockTransaction, - seed: [u8; 32], network: Network, ) -> Block { let mut block = fake_valid_block_proposal_from_tx(predecessor, tx, network).await; - block.satisfy_pow(predecessor.header().difficulty, seed); + let block_height = predecessor.header().height; + let consensus_rule_set = ConsensusRuleSet::infer_from(network, block_height); + block.satisfy_pow(predecessor.header().difficulty, consensus_rule_set); block } @@ -518,13 +577,7 @@ pub async fn fake_block_successor_with_merged_tx( .unwrap(); if with_valid_pow { - fake_valid_block_from_block_tx_for_tests( - predecessor, - block_tx, - seed_bytes.pop().unwrap(), - network, - ) - .await + fake_valid_block_from_block_tx_for_tests(predecessor, block_tx, network).await } else { fake_valid_block_proposal_from_tx(predecessor, block_tx, network).await } diff --git a/neptune-core/src/tests/shared/globalstate.rs b/neptune-core/src/tests/shared/globalstate.rs index a7877aaa8..412c254dc 100644 --- a/neptune-core/src/tests/shared/globalstate.rs +++ b/neptune-core/src/tests/shared/globalstate.rs @@ -164,13 +164,30 @@ pub(crate) async fn get_test_genesis_setup( mpsc::Receiver, GlobalStateLock, HandshakeData, +)> { + let genesis = Block::genesis(network); + test_setup_custom_genesis_block(network, peer_count, cli, genesis).await +} + +pub(crate) async fn test_setup_custom_genesis_block( + network: Network, + peer_count: u8, + cli: cli_args::Args, + custom_genesis: Block, +) -> anyhow::Result<( + broadcast::Sender, + broadcast::Receiver, + mpsc::Sender, + mpsc::Receiver, + GlobalStateLock, + HandshakeData, )> { let (peer_broadcast_tx, from_main_rx) = broadcast::channel::(PEER_CHANNEL_CAPACITY); let (to_main_tx, to_main_rx) = mpsc::channel::(PEER_CHANNEL_CAPACITY); - let devnet_wallet = WalletEntropy::devnet_wallet(); - let state = mock_genesis_global_state(peer_count, devnet_wallet, cli).await; + let wallet = WalletEntropy::devnet_wallet(); + let state = mock_genesis_global_state_with_block(peer_count, wallet, cli, custom_genesis).await; Ok(( peer_broadcast_tx, from_main_rx, diff --git a/neptune-core/tests/pow_puzzle.rs b/neptune-core/tests/pow_puzzle.rs index 660b02cb8..10b39ef7e 100644 --- a/neptune-core/tests/pow_puzzle.rs +++ b/neptune-core/tests/pow_puzzle.rs @@ -9,6 +9,7 @@ use common::logging; use neptune_cash::api::export::Network; use neptune_cash::application::rpc::server::proof_of_work_puzzle::ProofOfWorkPuzzle; use neptune_cash::protocol::consensus::block::block_header::BlockPow; +use neptune_cash::protocol::consensus::consensus_rule_set::ConsensusRuleSet; use neptune_cash::protocol::proof_abstractions::timestamp::Timestamp; use tasm_lib::triton_vm::prelude::BFieldElement; @@ -82,13 +83,14 @@ pub async fn can_find_valid_pow_solution() { proposal.set_header_guesser_address(guesser_address.into()); let latest_block_header = *alice.gsl.lock_guard().await.chain.light_state().header(); - let puzzle = ProofOfWorkPuzzle::new(proposal.clone(), latest_block_header); + let puzzle = ProofOfWorkPuzzle::new(proposal.clone(), latest_block_header.difficulty); println!("puzzle:\n\n{}", serde_json::to_string(&puzzle).unwrap()); + let consensus_rule_set = ConsensusRuleSet::Reboot; let solution = match precalculated_solution() { Some(solution) => solution, None => { - let solution = puzzle.solve(); + let solution = puzzle.solve(consensus_rule_set); let file = File::create(file_path()).unwrap(); serde_json::to_writer_pretty(file, &solution).unwrap();