From 13957a5e7fd8b430bf2f9fbe32ca923f92096f9a Mon Sep 17 00:00:00 2001 From: Boog900 <54e72d8a-345f-4599-bd90-c6b9bc7d0ec5@aleeas.com> Date: Sun, 7 Jan 2024 01:15:33 +0000 Subject: [PATCH] link rules to monero-book. need to do transactions. --- consensus/rules/src/blocks.rs | 77 ++++++++++++++++++------ consensus/rules/src/decomposed_amount.rs | 4 +- consensus/rules/src/genesis.rs | 2 +- consensus/rules/src/hard_forks.rs | 26 +++++--- consensus/rules/src/lib.rs | 5 ++ consensus/rules/src/miner_tx.rs | 24 +++++--- consensus/src/bin/scan_chain.rs | 24 ++++---- consensus/src/block.rs | 18 +++--- consensus/src/randomx.rs | 2 - 9 files changed, 124 insertions(+), 58 deletions(-) diff --git a/consensus/rules/src/blocks.rs b/consensus/rules/src/blocks.rs index bc5d584e..5e92346f 100644 --- a/consensus/rules/src/blocks.rs +++ b/consensus/rules/src/blocks.rs @@ -30,22 +30,29 @@ pub enum BlockError { PreviousIDIncorrect, #[error("The blocks timestamp is invalid.")] TimeStampInvalid, + #[error("The block contains a duplicate transaction.")] + DuplicateTransaction, #[error("Hard-fork error: {0}")] HardForkError(#[from] HardForkError), #[error("Miner transaction error: {0}")] MinerTxError(#[from] MinerTxError), } +/// A trait to represent the RandomX VM. pub trait RandomX { type Error; fn calculate_hash(&self, buf: &[u8]) -> Result<[u8; 32], Self::Error>; } +/// Returns if this height is a RandomX seed height. pub fn is_randomx_seed_height(height: u64) -> bool { height % RX_SEEDHASH_EPOCH_BLOCKS == 0 } +/// Returns the RandomX seed height for this block. +/// +/// ref: https://monero-book.cuprate.org/consensus_rules/blocks.html#randomx-seed pub fn randomx_seed_height(height: u64) -> u64 { if height <= RX_SEEDHASH_EPOCH_BLOCKS + RX_SEEDHASH_EPOCH_LAG { 0 @@ -55,6 +62,8 @@ pub fn randomx_seed_height(height: u64) -> u64 { } /// Calculates the POW hash of this block. +/// +/// ref: https://monero-book.cuprate.org/consensus_rules/blocks.html#pow-function pub fn calculate_pow_hash( randomx_vm: &R, buf: &[u8], @@ -82,7 +91,7 @@ pub fn calculate_pow_hash( /// Returns if the blocks POW hash is valid for the current difficulty. /// -/// See: https://cuprate.github.io/monero-book/consensus_rules/blocks/difficulty.html#checking-a-blocks-proof-of-work +/// ref: https://monero-book.cuprate.org/consensus_rules/blocks.html#checking-pow-hash pub fn check_block_pow(hash: &[u8; 32], difficulty: u128) -> Result<(), BlockError> { let int_hash = U256::from_little_endian(hash); @@ -102,7 +111,7 @@ pub fn check_block_pow(hash: &[u8; 32], difficulty: u128) -> Result<(), BlockErr /// Sanity check on the block blob size. /// -/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#block-weight-and-size +/// ref: https://monero-book.cuprate.org/consensus_rules/blocks.html#block-weight-and-size fn block_size_sanity_check( block_blob_len: usize, effective_median: usize, @@ -114,20 +123,9 @@ fn block_size_sanity_check( } } -/// Sanity check on number of txs in the block. -/// -/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#amount-of-transactions -fn check_amount_txs(number_none_miner_txs: usize) -> Result<(), BlockError> { - if number_none_miner_txs + 1 > 0x10000000 { - Err(BlockError::TooManyTxs) - } else { - Ok(()) - } -} - /// Sanity check on the block weight. /// -/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#block-weight-and-siz +/// ref: https://monero-book.cuprate.org/consensus_rules/blocks.html#block-weight-and-size fn check_block_weight( block_weight: usize, median_for_block_reward: usize, @@ -139,9 +137,20 @@ fn check_block_weight( } } +/// Sanity check on number of txs in the block. +/// +/// ref: https://monero-book.cuprate.org/consensus_rules/blocks.html#amount-of-transactions +fn check_amount_txs(number_none_miner_txs: usize) -> Result<(), BlockError> { + if number_none_miner_txs + 1 > 0x10000000 { + Err(BlockError::TooManyTxs) + } else { + Ok(()) + } +} + /// Verifies the previous id is the last blocks hash /// -/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#previous-id +/// ref: https://monero-book.cuprate.org/consensus_rules/blocks.html#previous-id fn check_prev_id(block: &Block, top_hash: &[u8; 32]) -> Result<(), BlockError> { if &block.header.previous != top_hash { Err(BlockError::PreviousIDIncorrect) @@ -152,7 +161,7 @@ fn check_prev_id(block: &Block, top_hash: &[u8; 32]) -> Result<(), BlockError> { /// Checks the blocks timestamp is in the valid range. /// -/// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#timestamp +/// ref: https://monero-book.cuprate.org/consensus_rules/blocks.html#timestamp fn check_timestamp(block: &Block, median_timestamp: u64) -> Result<(), BlockError> { if block.header.timestamp < median_timestamp || block.header.timestamp > current_unix_timestamp() + BLOCK_FUTURE_TIME_LIMIT @@ -163,21 +172,52 @@ fn check_timestamp(block: &Block, median_timestamp: u64) -> Result<(), BlockErro } } +/// Checks that all txs in the block have a unique hash. +/// +/// ref: https://monero-book.cuprate.org/consensus_rules/blocks.html#no-duplicate-transactions +fn check_txs_unique(txs: &[[u8; 32]]) -> Result<(), BlockError> { + txs.windows(2).try_for_each(|window| { + if window[0] != window[1] { + Err(BlockError::DuplicateTransaction)?; + } + Ok(()) + }) +} + +/// This struct contains the data needed to verify a block, implementers MUST make sure +/// the data in this struct is calculated correctly. #[derive(Debug, Clone)] pub struct ContextToVerifyBlock { + /// ref: https://monero-book.cuprate.org/consensus_rules/blocks/weights.html#median-weight-for-coinbase-checks pub median_weight_for_block_reward: usize, + /// ref: https://monero-book.cuprate.org/consensus_rules/blocks/weights.html#effective-median-weight pub effective_median_weight: usize, + /// The top hash of the blockchain, aka the block hash of the previous block to the one we are verifying. pub top_hash: [u8; 32], + /// Contains the median timestamp over the last 60 blocks, if there is less than 60 blocks this should be [`None`] pub median_block_timestamp: Option, + /// The current chain height. pub chain_height: u64, + /// The current hard-fork. pub current_hf: HardFork, + /// ref: https://monero-book.cuprate.org/consensus_rules/blocks/difficulty.html#calculating-difficulty pub next_difficulty: u128, + /// The amount of coins already minted. pub already_generated_coins: u64, } -/// Checks the block is valid returning the blocks hard-fork vote and the amount of coins generated. +/// Checks the block is valid returning the blocks hard-fork `VOTE` and the amount of coins generated in this block. +/// +/// This does not check the POW nor does it calculate the POW hash, this is because checking POW is very expensive and +/// to allow the computation of the POW hashes to be done separately. This also does not check the transactions in the +/// block are valid. +/// +/// Missed block checks in this function: +/// +/// https://monero-book.cuprate.org/consensus_rules/blocks.html#key-images +/// https://monero-book.cuprate.org/consensus_rules/blocks.html#checking-pow-hash +/// /// -/// Does not check the proof of work as that check is expensive and should be done last. pub fn check_block( block: &Block, total_fees: u64, @@ -201,6 +241,7 @@ pub fn check_block( block_size_sanity_check(block_blob_len, block_chain_ctx.effective_median_weight)?; check_amount_txs(block.txs.len())?; + check_txs_unique(&block.txs)?; let generated_coins = check_miner_tx( &block.miner_tx, diff --git a/consensus/rules/src/decomposed_amount.rs b/consensus/rules/src/decomposed_amount.rs index 6ad6eda8..cc7b47c4 100644 --- a/consensus/rules/src/decomposed_amount.rs +++ b/consensus/rules/src/decomposed_amount.rs @@ -36,8 +36,8 @@ pub fn decomposed_amounts() -> &'static [u64; 172] { /// /// This is also used during miner tx verification. /// -/// https://cuprate.github.io/monero-book/consensus_rules/transactions/pre_rct.html#output-amount -/// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#output-amounts +/// ref: https://monero-book.cuprate.org/consensus_rules/transactions/ring_signatures.html#output-amount +/// ref: https://monero-book.cuprate.org/consensus_rules/blocks/miner_tx.html#output-amounts #[inline] pub fn is_decomposed_amount(amount: &u64) -> bool { decomposed_amounts().binary_search(amount).is_ok() diff --git a/consensus/rules/src/genesis.rs b/consensus/rules/src/genesis.rs index f77d4621..73ba648a 100644 --- a/consensus/rules/src/genesis.rs +++ b/consensus/rules/src/genesis.rs @@ -25,7 +25,7 @@ fn genesis_miner_tx(network: &Network) -> Transaction { /// Generates the Monero genesis block. /// -/// ref: https://cuprate.github.io/monero-docs/consensus_rules/genesis_block.html +/// ref: https://monero-book.cuprate.org/consensus_rules/genesis_block.html pub fn generate_genesis_block(network: &Network) -> Block { Block { header: BlockHeader { diff --git a/consensus/rules/src/hard_forks.rs b/consensus/rules/src/hard_forks.rs index d9ac4fae..dfefc3c1 100644 --- a/consensus/rules/src/hard_forks.rs +++ b/consensus/rules/src/hard_forks.rs @@ -17,8 +17,12 @@ use std::{ mod tests; /// Target block time for hf 1. +/// +/// ref: https://monero-book.cuprate.org/consensus_rules/blocks/difficulty.html#target-seconds const BLOCK_TIME_V1: Duration = Duration::from_secs(60); /// Target block time from v2. +/// +/// ref: https://monero-book.cuprate.org/consensus_rules/blocks/difficulty.html#target-seconds const BLOCK_TIME_V2: Duration = Duration::from_secs(120); pub const NUMB_OF_HARD_FORKS: usize = 16; @@ -60,7 +64,7 @@ impl HFsInfo { /// Returns the main-net hard-fork information. /// - /// https://cuprate.github.io/monero-book/consensus_rules/hardforks.html#Mainnet-Hard-Forks + /// ref: https://monero-book.cuprate.org/consensus_rules/hardforks.html#Mainnet-Hard-Forks pub const fn main_net() -> HFsInfo { Self([ HFInfo::new(0, 0), @@ -82,6 +86,9 @@ impl HFsInfo { ]) } + /// Returns the test-net hard-fork information. + /// + /// ref: https://monero-book.cuprate.org/consensus_rules/hardforks.html#Testnet-Hard-Forks pub const fn test_net() -> HFsInfo { Self([ HFInfo::new(0, 0), @@ -103,6 +110,9 @@ impl HFsInfo { ]) } + /// Returns the test-net hard-fork information. + /// + /// ref: https://monero-book.cuprate.org/consensus_rules/hardforks.html#Stagenet-Hard-Forks pub const fn stage_net() -> HFsInfo { Self([ HFInfo::new(0, 0), @@ -152,7 +162,7 @@ pub enum HardFork { impl HardFork { /// Returns the hard-fork for a blocks `major_version` field. /// - /// https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#blocks-version-and-vote + /// https://monero-book.cuprate.org/consensus_rules/hardforks.html#blocks-version-and-vote pub fn from_version(version: u8) -> Result { Ok(match version { 1 => HardFork::V1, @@ -177,7 +187,7 @@ impl HardFork { /// Returns the hard-fork for a blocks `minor_version` (vote) field. /// - /// https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#blocks-version-and-vote + /// https://monero-book.cuprate.org/consensus_rules/hardforks.html#blocks-version-and-vote pub fn from_vote(vote: u8) -> HardFork { if vote == 0 { // A vote of 0 is interpreted as 1 as that's what Monero used to default to. @@ -200,6 +210,8 @@ impl HardFork { } /// Returns the target block time for this hardfork. + /// + /// ref: https://monero-book.cuprate.org/consensus_rules/blocks/difficulty.html#target-seconds pub fn block_time(&self) -> Duration { match self { HardFork::V1 => BLOCK_TIME_V1, @@ -209,7 +221,7 @@ impl HardFork { /// Checks a blocks version and vote, assuming that `self` is the current hard-fork. /// - /// https://cuprate.github.io/monero-book/consensus_rules/blocks.html#version-and-vote + /// ref: https://monero-book.cuprate.org/consensus_rules/hardforks.html#blocks-version-and-vote pub fn check_block_version_vote( &self, version: &HardFork, @@ -280,7 +292,7 @@ impl HFVotes { /// Returns the total votes for a hard-fork. /// - /// https://cuprate.github.io/monero-docs/consensus_rules/hardforks.html#accepting-a-fork + /// ref: https://monero-book.cuprate.org/consensus_rules/hardforks.html#accepting-a-fork pub fn votes_for_hf(&self, hf: &HardFork) -> u64 { self.votes[*hf as usize - 1..].iter().sum() } @@ -293,7 +305,7 @@ impl HFVotes { /// Checks if a future hard fork should be activated, returning the next hard-fork that should be /// activated. /// - /// https://cuprate.github.io/monero-book/consensus_rules/hardforks.html#accepting-a-fork + /// ref: https://monero-book.cuprate.org/consensus_rules/hardforks.html#accepting-a-fork pub fn current_fork( &self, current_hf: &HardFork, @@ -323,7 +335,7 @@ impl HFVotes { /// Returns the votes needed for a hard-fork. /// -/// https://cuprate.github.io/monero-book/consensus_rules/hardforks.html#accepting-a-fork +/// ref: https://monero-book.cuprate.org/consensus_rules/hardforks.html#accepting-a-fork pub fn votes_needed(threshold: u64, window: u64) -> u64 { (threshold * window).div_ceil(100) } diff --git a/consensus/rules/src/lib.rs b/consensus/rules/src/lib.rs index 25a9fe7f..1b37b468 100644 --- a/consensus/rules/src/lib.rs +++ b/consensus/rules/src/lib.rs @@ -32,6 +32,7 @@ fn check_point_canonically_encoded(point: &curve25519_dalek::edwards::Compressed .is_some() } +/// Returns the current UNIX timestamp. pub fn current_unix_timestamp() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) @@ -39,6 +40,8 @@ pub fn current_unix_timestamp() -> u64 { .as_secs() } +/// An internal function that returns an iterator or a parallel iterator if the +/// `rayon` feature is enabled. #[cfg(feature = "rayon")] fn try_par_iter(t: T) -> T::Iter where @@ -47,6 +50,8 @@ where t.into_par_iter() } +/// An internal function that returns an iterator or a parallel iterator if the +/// `rayon` feature is enabled. #[cfg(not(feature = "rayon"))] fn try_par_iter(t: T) -> impl std::iter::Iterator where diff --git a/consensus/rules/src/miner_tx.rs b/consensus/rules/src/miner_tx.rs index 82d20a6a..0b6c2d23 100644 --- a/consensus/rules/src/miner_tx.rs +++ b/consensus/rules/src/miner_tx.rs @@ -39,6 +39,8 @@ const MINER_TX_TIME_LOCKED_BLOCKS: u64 = 60; /// Calculates the base block reward without taking away the penalty for expanding /// the block. +/// +/// ref: https://monero-book.cuprate.org/consensus_rules/blocks/reward.html#calculating-base-block-reward fn calculate_base_reward(already_generated_coins: u64, hf: &HardFork) -> u64 { let target_mins = hf.block_time().as_secs() / 60; let emission_speed_factor = 20 - (target_mins - 1); @@ -47,6 +49,8 @@ fn calculate_base_reward(already_generated_coins: u64, hf: &HardFork) -> u64 { } /// Calculates the miner reward for this block. +/// +/// ref: https://monero-book.cuprate.org/consensus_rules/blocks/reward.html#calculating-block-reward pub fn calculate_block_reward( block_weight: usize, median_bw: usize, @@ -71,7 +75,7 @@ pub fn calculate_block_reward( /// Checks the miner transactions version. /// -/// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#version +/// ref: https://monero-book.cuprate.org/consensus_rules/blocks/miner_tx.html#version fn check_miner_tx_version(tx_version: &TxVersion, hf: &HardFork) -> Result<(), MinerTxError> { // The TxVersion enum checks if the version is not 1 or 2 if hf >= &HardFork::V12 && tx_version != &TxVersion::RingCT { @@ -83,8 +87,7 @@ fn check_miner_tx_version(tx_version: &TxVersion, hf: &HardFork) -> Result<(), M /// Checks the miner transactions inputs. /// -/// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#input -/// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#height +/// ref: https://monero-book.cuprate.org/consensus_rules/blocks/miner_tx.html#input fn check_inputs(inputs: &[Input], chain_height: u64) -> Result<(), MinerTxError> { if inputs.len() != 1 { return Err(MinerTxError::IncorrectNumbOfInputs); @@ -104,13 +107,13 @@ fn check_inputs(inputs: &[Input], chain_height: u64) -> Result<(), MinerTxError> /// Checks the miner transaction has a correct time lock. /// -/// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#unlock-time +/// ref: https://monero-book.cuprate.org/consensus_rules/blocks/miner_tx.html#unlock-time fn check_time_lock(time_lock: &Timelock, chain_height: u64) -> Result<(), MinerTxError> { match time_lock { Timelock::Block(till_height) => { // Lock times above this amount are timestamps not blocks. // This is just for safety though and shouldn't actually be hit. - if till_height >= &500_000_000 { + if till_height > &500_000_000 { Err(MinerTxError::InvalidLockTime)?; } if u64::try_from(*till_height).unwrap() != chain_height + MINER_TX_TIME_LOCKED_BLOCKS { @@ -125,7 +128,7 @@ fn check_time_lock(time_lock: &Timelock, chain_height: u64) -> Result<(), MinerT /// Sums the outputs checking for overflow. /// -/// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#output-amounts +/// ref: https://monero-book.cuprate.org/consensus_rules/blocks/miner_tx.html#output-amounts fn sum_outputs(outputs: &[Output], hf: &HardFork) -> Result { let mut sum: u64 = 0; for out in outputs { @@ -140,7 +143,7 @@ fn sum_outputs(outputs: &[Output], hf: &HardFork) -> Result { /// Checks the total outputs amount is correct returning the amount of coins collected by the miner. /// -/// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#total-outputs +/// ref: https://monero-book.cuprate.org/consensus_rules/blocks/miner_tx.html#total-outputs fn check_total_output_amt( total_output: u64, reward: u64, @@ -160,6 +163,12 @@ fn check_total_output_amt( } } +/// Checks all miner transactions rules. +/// +/// Excluding: +/// https://monero-book.cuprate.org/consensus_rules/blocks/miner_tx.html#v2-output-pool +/// +/// as this needs to be done in a database. pub fn check_miner_tx( tx: &Transaction, total_fees: u64, @@ -172,6 +181,7 @@ pub fn check_miner_tx( let tx_version = TxVersion::from_raw(tx.prefix.version).ok_or(MinerTxError::VersionInvalid)?; check_miner_tx_version(&tx_version, hf)?; + // ref: https://monero-book.cuprate.org/consensus_rules/blocks/miner_tx.html#ringct-type if hf >= &HardFork::V12 && tx.rct_signatures.rct_type() != RctType::Null { return Err(MinerTxError::RCTTypeNotNULL); } diff --git a/consensus/src/bin/scan_chain.rs b/consensus/src/bin/scan_chain.rs index 7250ab5c..1a705a12 100644 --- a/consensus/src/bin/scan_chain.rs +++ b/consensus/src/bin/scan_chain.rs @@ -1,17 +1,18 @@ #![cfg(feature = "binaries")] -use std::collections::{HashMap, HashSet}; -use std::ops::Deref; -use std::time::Duration; -use std::{ops::Range, path::PathBuf, sync::Arc}; +use std::{ + collections::{HashMap, HashSet}, + ops::Range, + path::PathBuf, + sync::Arc, +}; use clap::Parser; use futures::{ channel::{mpsc, oneshot}, - SinkExt, StreamExt, TryFutureExt, + SinkExt, StreamExt, }; use monero_serai::{block::Block, transaction::Transaction}; -use rayon::prelude::*; use tokio::sync::RwLock; use tower::{Service, ServiceExt}; use tracing::level_filters::LevelFilter; @@ -221,7 +222,7 @@ where let mut randomx_vms: Option> = Some(HashMap::new()); tokio::spawn(async move { - while let Some(mut blocks) = incoming_blocks.next().await { + while let Some(blocks) = incoming_blocks.next().await { let unwrapped_rx_vms = randomx_vms.as_mut().unwrap(); let blocks = rayon_spawn_async(move || { @@ -243,12 +244,9 @@ where unwrapped_rx_vms.retain(|seed_height, _| seeds_needed.contains(seed_height)); for seed_height in seeds_needed { - if !unwrapped_rx_vms.contains_key(&seed_height) { - unwrapped_rx_vms.insert( - seed_height, - RandomXVM::new(rx_seed_cache.get_seeds_hash(seed_height)).unwrap(), - ); - } + unwrapped_rx_vms.entry(seed_height).or_insert_with(|| { + RandomXVM::new(rx_seed_cache.get_seeds_hash(seed_height)).unwrap() + }); } let arc_rx_vms = Arc::new(randomx_vms.take().unwrap()); diff --git a/consensus/src/block.rs b/consensus/src/block.rs index ae27f710..d24fbb3c 100644 --- a/consensus/src/block.rs +++ b/consensus/src/block.rs @@ -10,16 +10,14 @@ use monero_serai::block::Block; use monero_serai::transaction::Input; use tower::{Service, ServiceExt}; -use monero_consensus::blocks::{BlockError, RandomX}; -use monero_consensus::miner_tx::MinerTxError; use monero_consensus::{ - blocks::{calculate_pow_hash, check_block, check_block_pow}, + blocks::{calculate_pow_hash, check_block, check_block_pow, BlockError, RandomX}, + miner_tx::MinerTxError, ConsensusError, HardFork, }; use crate::{ context::{BlockChainContextRequest, BlockChainContextResponse}, - helper::rayon_spawn_async, transactions::{TransactionVerificationData, VerifyTxRequest, VerifyTxResponse}, ExtendedConsensusError, TxNotInPool, TxPoolRequest, TxPoolResponse, }; @@ -294,10 +292,10 @@ where } async fn verify_main_chain_block( - block: Block, - context_svc: C, - tx_verifier_svc: TxV, - tx_pool: TxP, + _block: Block, + _context_svc: C, + _tx_verifier_svc: TxV, + _tx_pool: TxP, ) -> Result where C: Service< @@ -313,6 +311,9 @@ where + Send + 'static, { + todo!("Single main chain block."); + + /* tracing::debug!("getting blockchain context"); let BlockChainContextResponse::Context(checked_context) = context_svc .oneshot(BlockChainContextRequest::Get) @@ -379,4 +380,5 @@ where hf_vote, cumulative_difficulty: context.cumulative_difficulty + context.next_difficulty, })) + */ } diff --git a/consensus/src/randomx.rs b/consensus/src/randomx.rs index db1c06be..7a9d37fd 100644 --- a/consensus/src/randomx.rs +++ b/consensus/src/randomx.rs @@ -7,7 +7,6 @@ pub struct RandomXVM { vms: ThreadLocal, cache: RandomXCache, flags: RandomXFlag, - seed: [u8; 32], } impl RandomXVM { @@ -20,7 +19,6 @@ impl RandomXVM { vms: ThreadLocal::new(), cache, flags, - seed, }) } }