From 6d6eb9853d6b31f5fedfc7f44931cba7a5226746 Mon Sep 17 00:00:00 2001 From: Mariano Sorgente Date: Thu, 26 Sep 2019 16:04:35 +0900 Subject: [PATCH 1/2] Fix small bug with height --- src/blockchain.py | 6 +++--- src/farmer.py | 2 +- src/full_node.py | 17 ++++++++++------- src/protocols/farmer_protocol.py | 5 +---- src/server/server.py | 2 +- src/server/start_farmer.py | 2 +- src/server/start_plotter.py | 2 +- src/server/start_timelord.py | 2 +- src/util/api_decorators.py | 4 +--- src/util/errors.py | 5 +++++ 10 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/blockchain.py b/src/blockchain.py index 2e45fefd..1d60d755 100644 --- a/src/blockchain.py +++ b/src/blockchain.py @@ -256,7 +256,7 @@ class Blockchain: curr = self.blocks[curr.prev_header_hash] except KeyError: break - if len(last_timestamps) != NUMBER_OF_TIMESTAMPS and curr.trunk_block.challenge.height != 0: + if len(last_timestamps) != NUMBER_OF_TIMESTAMPS and curr.body.coinbase.height != 0: return False prev_time: uint64 = uint64(sum(last_timestamps) / len(last_timestamps)) if block.trunk_block.header.data.timestamp < prev_time: @@ -300,7 +300,7 @@ class Blockchain: return False # 10. Check coinbase amount - if calculate_block_reward(block.trunk_block.challenge.height) != block.body.coinbase.amount: + if calculate_block_reward(block.body.coinbase.height) != block.body.coinbase.amount: return False # 11. Check coinbase signature with pool pk @@ -424,7 +424,7 @@ class Blockchain: while len(self.heads) >= 4: self.heads.sort(key=lambda b: b.weight, reverse=True) self.heads.pop() - log.info(f"Updated heads, new heights: {[b.height for b in self.heads]}") + log.info(f"\tUpdated heads, new heights: {[b.height for b in self.heads]}") self._reconsider_lca() return True return False diff --git a/src/farmer.py b/src/farmer.py index 06e47799..90fdebbb 100644 --- a/src/farmer.py +++ b/src/farmer.py @@ -206,7 +206,7 @@ async def proof_of_space_finalized(proof_of_space_finalized: farmer_protocol.Pro coinbase_signature: PrependSignature = pool_sks[0].sign_prepend(coinbase.serialize()) db.coinbase_rewards[uint32(db.current_height + 1)] = (coinbase, coinbase_signature) - log.info(f"Current height set to {db.current_height}") + log.info(f"\tCurrent height set to {db.current_height}") db.seen_challenges.add(proof_of_space_finalized.challenge_hash) if proof_of_space_finalized.height not in db.challenges: db.challenges[proof_of_space_finalized.height] = [proof_of_space_finalized] diff --git a/src/full_node.py b/src/full_node.py index 86eb5077..e32ba82e 100644 --- a/src/full_node.py +++ b/src/full_node.py @@ -1,5 +1,4 @@ import logging -from src.util.errors import BlockNotInBlockchain, PeersDontHaveBlock import time import asyncio import collections @@ -30,6 +29,7 @@ from src.consensus.pot_iterations import calculate_iterations from src.consensus.constants import DIFFICULTY_TARGET from src.blockchain import Blockchain, ReceiveBlockResult from src.server.outbound_message import OutboundMessage, Delivery, NodeType, Message +from src.util.errors import BlockNotInBlockchain, PeersDontHaveBlock, InvalidUnfinishedBlock class Database: @@ -108,7 +108,7 @@ async def proof_of_time_estimate_interval(): async with db.lock: if estimated_ips is not None: db.proof_of_time_estimate_ips = estimated_ips - log.info(f"Updated proof of time estimate to {estimated_ips} iterations per second.") + log.info(f"Updated proof of time estimate to {estimated_ips} iterations per second.") await sleep(config['update_pot_estimate_interval']) @@ -476,7 +476,8 @@ async def new_proof_of_time(new_proof_of_time: peer_protocol.NewProofOfTime) -> yield msg if propagate_proof: # TODO: perhaps don't propagate everything, this is a DoS vector - yield OutboundMessage(NodeType.FULL_NODE, Message("new_proof_of_time", new_proof_of_time), Delivery.BROADCAST) + yield OutboundMessage(NodeType.FULL_NODE, Message("new_proof_of_time", new_proof_of_time), + Delivery.BROADCAST_TO_OTHERS) @api_request @@ -490,7 +491,9 @@ async def unfinished_block(unfinished_block: peer_protocol.UnfinishedBlock) -> A if not db.blockchain.is_child_of_head(unfinished_block.block): return - # TODO(alex): verify block using blockchain class, including coinbase rewards + if not db.blockchain.validate_unfinished_block(unfinished_block.block): + raise InvalidUnfinishedBlock() + prev_block: TrunkBlock = db.blockchain.get_trunk_block( unfinished_block.block.trunk_block.prev_header_hash) @@ -502,13 +505,13 @@ async def unfinished_block(unfinished_block: peer_protocol.UnfinishedBlock) -> A challenge_hash, difficulty) if (challenge_hash, iterations_needed) in db.unfinished_blocks: - log.info(f"Have already seen unfinished block {(challenge_hash, iterations_needed)}") + log.info(f"\tHave already seen unfinished block {(challenge_hash, iterations_needed)}") return expected_time: float = iterations_needed / db.proof_of_time_estimate_ips # TODO(alex): tweak this - log.info(f"Expected finish time: {expected_time}") + log.info(f"\tExpected finish time: {expected_time}") if expected_time > 10 * DIFFICULTY_TARGET: return @@ -535,7 +538,7 @@ async def block(block: peer_protocol.Block) -> AsyncGenerator[OutboundMessage, N return if header_hash in db.full_blocks: - log.info(f"Already have block {header_hash} height {block.block.trunk_block.challenge.height}") + log.info(f"\tAlready have block {header_hash} height {block.block.trunk_block.challenge.height}") return # TODO(alex): Check if we care about this block, we don't want to add random # disconnected blocks. For example if it's on one of the heads, or if it's an older diff --git a/src/protocols/farmer_protocol.py b/src/protocols/farmer_protocol.py index 36afec9a..597446c7 100644 --- a/src/protocols/farmer_protocol.py +++ b/src/protocols/farmer_protocol.py @@ -9,10 +9,7 @@ from src.types.coinbase import CoinbaseInfo Protocol between farmer and full node. """ -""" -Farmer <- Full node -Update current height -""" + @cbor_message(tag=2000) class ProofOfSpaceFinalized: challenge_hash: bytes32 diff --git a/src/server/server.py b/src/server/server.py index ae0fd474..0970ef49 100644 --- a/src/server/server.py +++ b/src/server/server.py @@ -226,7 +226,7 @@ async def initialize_pipeline(aiter, # length encoding and CBOR serialization async def serve_forever(): async for connection, message in expanded_messages_aiter: - log.info(f"Sending {message.function} to peer {connection.get_peername()}") + log.info(f"-> {message.function} to peer {connection.get_peername()}") try: await connection.send(message) except asyncio.CancelledError: diff --git a/src/server/start_farmer.py b/src/server/start_farmer.py index af120382..d0e47131 100644 --- a/src/server/start_farmer.py +++ b/src/server/start_farmer.py @@ -9,7 +9,7 @@ from src.protocols.plotter_protocol import PlotterHandshake from src.server.outbound_message import OutboundMessage, Message, Delivery, NodeType from src.util.network import parse_host_port -logging.basicConfig(format='Farmer %(name)-23s: %(levelname)-8s %(message)s', level=logging.INFO) +logging.basicConfig(format='Farmer %(name)-25s: %(levelname)-8s %(message)s', level=logging.INFO) async def main(): diff --git a/src/server/start_plotter.py b/src/server/start_plotter.py index 910e42dd..ccb22d34 100644 --- a/src/server/start_plotter.py +++ b/src/server/start_plotter.py @@ -5,7 +5,7 @@ from src.server.outbound_message import NodeType from src.util.network import parse_host_port from src import plotter -logging.basicConfig(format='Plotter %(name)-23s: %(levelname)-8s %(message)s', level=logging.INFO) +logging.basicConfig(format='Plotter %(name)-24s: %(levelname)-8s %(message)s', level=logging.INFO) async def main(): diff --git a/src/server/start_timelord.py b/src/server/start_timelord.py index 04ae209b..7339845c 100644 --- a/src/server/start_timelord.py +++ b/src/server/start_timelord.py @@ -5,7 +5,7 @@ from src.server.outbound_message import NodeType from src.util.network import parse_host_port from src import timelord -logging.basicConfig(format='Timelord %(name)-23s: %(levelname)-8s %(message)s', level=logging.INFO) +logging.basicConfig(format='Timelord %(name)-25s: %(levelname)-20s %(message)s', level=logging.INFO) async def main(): diff --git a/src/util/api_decorators.py b/src/util/api_decorators.py index ab1a9f72..550e7b31 100644 --- a/src/util/api_decorators.py +++ b/src/util/api_decorators.py @@ -18,8 +18,6 @@ def api_request(f): binding = sig.bind(*args, **kwargs) binding.apply_defaults() inter = dict(binding.arguments) - print_args = {k: v for (k, v) in inter.items() if k != "source_connection" - and k != "all_connections"} - log.info(f"{f.__name__}({print_args})"[:200]) + log.info(f"<- {f.__name__}") return f(**inter) return f_substitute diff --git a/src/util/errors.py b/src/util/errors.py index bdee87f5..a07a5fd3 100644 --- a/src/util/errors.py +++ b/src/util/errors.py @@ -42,3 +42,8 @@ class PeersDontHaveBlock(Exception): class InvalidWeight(Exception): """The weight of this block can not be validated""" pass + + +class InvalidUnfinishedBlock(Exception): + """The unfinished block we received is invalid""" + pass From 4f89752d62582c1cc30cdc911cadfcad1bfb330b Mon Sep 17 00:00:00 2001 From: Mariano Sorgente Date: Mon, 30 Sep 2019 22:15:32 +0900 Subject: [PATCH 2/2] Implement difficulty adjustment --- README.md | 1 + src/__init__.py | 0 src/blockchain.py | 162 ++++++++++++++--------------- src/consensus/__init__.py | 0 src/consensus/block_rewards.py | 3 + src/consensus/constants.py | 43 +++++--- src/consensus/pot_iterations.py | 3 +- src/consensus/weight_verifier.py | 5 + src/full_node.py | 118 ++++++++++++--------- src/plotter.py | 2 +- src/protocols/__init__.py | 0 src/protocols/peer_protocol.py | 40 ++++--- src/server/__init__.py | 0 src/simulation/simulate_network.sh | 6 +- src/timelord.py | 4 +- src/types/proof_of_time.py | 7 +- src/util/__init__.py | 0 tests/block_tools.py | 122 ++++++++++++++++------ tests/test_blockchain.py | 158 ++++++++++++++++------------ 19 files changed, 411 insertions(+), 263 deletions(-) create mode 100644 src/__init__.py create mode 100644 src/consensus/__init__.py create mode 100644 src/protocols/__init__.py create mode 100644 src/server/__init__.py create mode 100644 src/util/__init__.py diff --git a/README.md b/README.md index 6222b57b..9ea8d05e 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ python -m src.server.start_timelord python -m src.server.start_farmer python -m src.server.start_full_node "127.0.0.1" 8002 "-f" "-t" python -m src.server.start_full_node "127.0.0.1" 8004 +python -m src.server.start_full_node "127.0.0.1" 8005 ``` You can also run the simulation, which runs all servers at once. diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/blockchain.py b/src/blockchain.py index 1d60d755..f8c63f8c 100644 --- a/src/blockchain.py +++ b/src/blockchain.py @@ -10,17 +10,8 @@ from src.util.ints import uint64, uint32 from src.types.trunk_block import TrunkBlock from src.types.full_block import FullBlock from src.consensus.pot_iterations import calculate_iterations, calculate_iterations_quality -from src.consensus.constants import ( - DIFFICULTY_STARTING, - DIFFICULTY_TARGET, - DIFFICULTY_EPOCH, - DIFFICULTY_DELAY, - DIFFICULTY_WARP_FACTOR, - DIFFICULTY_FACTOR, - NUMBER_OF_TIMESTAMPS, - MAX_FUTURE_TIME, - GENESIS_BLOCK -) +from src.consensus.constants import constants as consensus_constants + log = logging.getLogger(__name__) @@ -39,29 +30,24 @@ class ReceiveBlockResult(Enum): class Blockchain: - def __init__(self, genesis: Optional[FullBlock] = None): - if not genesis: - try: - genesis = self.get_genesis_block() - except ValueError: - raise ValueError("Failed to parse genesis block.") + def __init__(self, override_constants: Dict = {}): + # Allow passing in custom overrides for any consesus parameters + self.constants: Dict = consensus_constants + for key, value in override_constants.items(): + self.constants[key] = value self.heads: List[FullBlock] = [] self.lca_block: FullBlock = None self.blocks: Dict[bytes32, FullBlock] = {} self.height_to_hash: Dict[uint64, bytes32] = {} - result = self.receive_block(genesis) + self.genesis = FullBlock.from_bytes(self.constants["GENESIS_BLOCK"]) + result = self.receive_block(self.genesis) assert result == ReceiveBlockResult.ADDED_TO_HEAD - self.genesis = genesis - # For blocks with height % DIFFICULTY_DELAY == 1, a link to the hash of - # the (DIFFICULTY_DELAY)-th parent of this block + # For blocks with height % constants["DIFFICULTY_DELAY"] == 1, a link to the hash of + # the (constants["DIFFICULTY_DELAY"])-th parent of this block self.header_warp: Dict[bytes32, bytes32] = {} - @staticmethod - def get_genesis_block() -> FullBlock: - return FullBlock.from_bytes(GENESIS_BLOCK) - def get_current_heads(self) -> List[TrunkBlock]: """ Return the heads. @@ -135,7 +121,7 @@ class Blockchain: if trunk is None: raise Exception("No block found for given header_hash") elif trunk is self.genesis.trunk_block: - return uint64(DIFFICULTY_STARTING) + return uint64(self.constants["DIFFICULTY_STARTING"]) prev_block = self.blocks.get(trunk.prev_header_hash, None) if prev_block is None: @@ -144,51 +130,82 @@ class Blockchain: - prev_block.trunk_block.challenge.total_weight) def get_next_difficulty(self, header_hash: bytes32) -> uint64: - return self.get_difficulty(header_hash) - # Returns the difficulty of the next block that extends onto header_hash. # Used to calculate the number of iterations. - # TODO: Assumes header_hash is of a connected block - block = self.blocks.get(header_hash, None) + next_height: uint32 = block.height + 1 if block is None: raise Exception("Given header_hash must reference block already added") - if block.height % DIFFICULTY_EPOCH != DIFFICULTY_DELAY: + if next_height % self.constants["DIFFICULTY_EPOCH"] != self.constants["DIFFICULTY_DELAY"]: # Not at a point where difficulty would change return self.get_difficulty(header_hash) - elif block.height == DIFFICULTY_DELAY: - return uint64(DIFFICULTY_FACTOR * DIFFICULTY_STARTING) + elif next_height < self.constants["DIFFICULTY_EPOCH"]: + # We are in the first epoch + return uint64(self.constants["DIFFICULTY_STARTING"]) - # The current block has height i + DELAY. + # old diff curr diff new diff + # ----------|-----|----------------------|-----|-----... + # h1 h2 h3 i-1 + height1 = uint64(next_height - self.constants["DIFFICULTY_EPOCH"] - self.constants["DIFFICULTY_DELAY"] - 1) + height2 = uint64(next_height - self.constants["DIFFICULTY_EPOCH"] - 1) + height3 = uint64(next_height - self.constants["DIFFICULTY_DELAY"] - 1) + + block1, block2, block3 = None, None, None + if block.trunk_block not in self.get_current_heads() or height3 not in self.height_to_hash: + # This means we are either on a fork, or on one of the chains, but after the LCA, + # so we manually backtrack. + curr = block + while (curr.height not in self.height_to_hash or self.height_to_hash[curr.height] != curr.header_hash): + if curr.height == height1: + block1 = curr + elif curr.height == height2: + block2 = curr + elif curr.height == height3: + block3 = curr + curr = self.blocks[curr.prev_header_hash] + # Once we are before the fork point (and before the LCA), we can use the height_to_hash map + if not block1 and height1 >= 0: + # hiehgt1 could be -1, for the first difficulty calculation + block1 = self.blocks[self.height_to_hash[height1]] + if not block2: + block2 = self.blocks[self.height_to_hash[height2]] + if not block3: + block3 = self.blocks[self.height_to_hash[height3]] + + # Current difficulty parameter (diff of block h = i - 1) Tc = self.get_difficulty(header_hash) - warp = header_hash - for _ in range(DIFFICULTY_DELAY - 1): - warp = self.blocks[warp].hash - # warp: header_hash of height {i + 1} - warp2 = warp - for _ in range(DIFFICULTY_WARP_FACTOR - 1): - warp2 = self.header_warp.get(warp2, None) - # warp2: header_hash of height {i + 1 - EPOCH + DELAY} - Tp = self.get_difficulty(self.blocks[warp2].prev_header_hash) + # Previous difficulty parameter (diff of block h = i - 2048 - 1) + Tp = self.get_difficulty(block2.header_hash) + if block1: + timestamp1 = block1.trunk_block.header.data.timestamp # i - 512 - 1 + else: + # In the case of height == -1, there is no timestamp here, so assume the genesis block + # took constants["BLOCK_TIME_TARGET"] seconds to mine. + timestamp1 = (self.blocks[self.height_to_hash[uint64(0)]].trunk_block.header.data.timestamp + - self.constants["BLOCK_TIME_TARGET"]) + timestamp2 = block2.trunk_block.header.data.timestamp # i - 2048 + 512 - 1 + timestamp3 = block3.trunk_block.header.data.timestamp # i - 512 - 1 - # X_i : timestamp of i-th block, (EPOCH divides i) - # Current block @warp is i+1 - temp_block = self.blocks[warp] - timestamp1 = temp_block.trunk_block.header.data.timestamp # X_{i+1} - temp_block = self.blocks[warp2] - timestamp2 = temp_block.trunk_block.header.data.timestamp # X_{i+1-EPOCH+DELAY} - temp_block = self.blocks[self.header_warp[temp_block.hash]] - timestamp3 = temp_block.trunk_block.header.data.timestamp # X_{i+1-EPOCH} + # Numerator fits in 128 bits, so big int is not necessary + # We multiply by the denominators here, so we only have one fraction in the end (avoiding floating point) + term1 = (self.constants["DIFFICULTY_DELAY"] * Tp * (timestamp3 - timestamp2) * + self.constants["BLOCK_TIME_TARGET"]) + term2 = ((self.constants["DIFFICULTY_WARP_FACTOR"] - 1) * (self.constants["DIFFICULTY_EPOCH"] - + self.constants["DIFFICULTY_DELAY"]) * Tc + * (timestamp2 - timestamp1) * self.constants["BLOCK_TIME_TARGET"]) - diff_natural = ( - (DIFFICULTY_EPOCH - DIFFICULTY_DELAY) * Tc * (timestamp2 - timestamp3) - ) - diff_natural += DIFFICULTY_DELAY * Tp * (timestamp1 - timestamp2) - diff_natural *= DIFFICULTY_TARGET - diff_natural //= (timestamp1 - timestamp2) * (timestamp2 - timestamp3) - difficulty = max(min(diff_natural, Tc * 4), Tc // 4) # truncated comparison - return difficulty + # Round down after the division + new_difficulty: uint64 = uint64((term1 + term2) // + (self.constants["DIFFICULTY_WARP_FACTOR"] * + (timestamp3 - timestamp2) * + (timestamp2 - timestamp1))) + + # Only change by a max factor, to prevent attacks, as in greenpaper, and must be at least 1 + if new_difficulty >= Tc: + return min(new_difficulty, uint64(self.constants["DIFFICULTY_FACTOR"] * Tc)) + else: + return max([uint64(1), new_difficulty, uint64(Tc // self.constants["DIFFICULTY_FACTOR"])]) def get_vdf_rate_estimate(self) -> Optional[uint64]: """ @@ -250,18 +267,18 @@ class Blockchain: last_timestamps: List[uint64] = [] prev_block: Optional[FullBlock] = self.blocks[block.prev_header_hash] curr = prev_block - while len(last_timestamps) < NUMBER_OF_TIMESTAMPS: + while len(last_timestamps) < self.constants["NUMBER_OF_TIMESTAMPS"]: last_timestamps.append(curr.trunk_block.header.data.timestamp) try: curr = self.blocks[curr.prev_header_hash] except KeyError: break - if len(last_timestamps) != NUMBER_OF_TIMESTAMPS and curr.body.coinbase.height != 0: + if len(last_timestamps) != self.constants["NUMBER_OF_TIMESTAMPS"] and curr.body.coinbase.height != 0: return False prev_time: uint64 = uint64(sum(last_timestamps) / len(last_timestamps)) if block.trunk_block.header.data.timestamp < prev_time: return False - if block.trunk_block.header.data.timestamp > time.time() + MAX_FUTURE_TIME: + if block.trunk_block.header.data.timestamp > time.time() + self.constants["MAX_FUTURE_TIME"]: return False else: prev_block: Optional[FullBlock] = None @@ -328,7 +345,7 @@ class Blockchain: if not genesis: difficulty: uint64 = self.get_next_difficulty(block.prev_header_hash) else: - difficulty: uint64 = uint64(DIFFICULTY_STARTING) + difficulty: uint64 = uint64(self.constants["DIFFICULTY_STARTING"]) # 2. Check proof of space hash if block.trunk_block.proof_of_space.get_hash() != block.trunk_block.challenge.proof_of_space_hash: @@ -344,7 +361,7 @@ class Blockchain: return False # 4. Check PoT - if not block.trunk_block.proof_of_time.is_valid(): + if not block.trunk_block.proof_of_time.is_valid(self.constants["DISCRIMINANT_SIZE_BITS"]): return False if block.body.coinbase.height != block.trunk_block.challenge.height: @@ -428,20 +445,3 @@ class Blockchain: self._reconsider_lca() return True return False - - def _get_warpable_trunk(self, trunk: TrunkBlock) -> TrunkBlock: - height = trunk.challenge.height - while height % DIFFICULTY_DELAY != 1: - trunk = self.blocks[trunk.header.header_hash].trunk_block - height = trunk.challenge.height - return trunk - - def _consider_warping_link(self, trunk: TrunkBlock): - # Assumes trunk is already connected - if trunk.challenge.height % DIFFICULTY_DELAY != 1: - return - warped_trunk: TrunkBlock = self.blocks[trunk.prev_header_hash].trunk_block - while warped_trunk and warped_trunk.challenge.height % DIFFICULTY_DELAY != 1: - warped_trunk = self.blocks.get(warped_trunk.prev_header_hash, None).trunk_block - if warped_trunk is not None: - self.header_warp[trunk.header.header_hash] = warped_trunk.header.header_hash diff --git a/src/consensus/__init__.py b/src/consensus/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/consensus/block_rewards.py b/src/consensus/block_rewards.py index 982f3416..f8703dbc 100644 --- a/src/consensus/block_rewards.py +++ b/src/consensus/block_rewards.py @@ -2,5 +2,8 @@ from src.util.ints import uint64, uint32 def calculate_block_reward(height: uint32) -> uint64: + """ + Returns the coinbase reward at a certain block height. # TODO: implement real block schedule + """ return uint64(10) diff --git a/src/consensus/constants.py b/src/consensus/constants.py index efe36dc2..d188057f 100644 --- a/src/consensus/constants.py +++ b/src/consensus/constants.py @@ -1,18 +1,31 @@ -NUMBER_OF_HEADS = 3 # The number of tips each full node keeps track of and propagates -DIFFICULTY_STARTING = 60 # These are in units of 2^32 -DIFFICULTY_EPOCH = 10 # The number of blocks per epoch -DIFFICULTY_TARGET = 10 # The target number of seconds per block -DIFFICULTY_FACTOR = 4 # The next difficulty is truncated to range [prev / FACTOR, prev * FACTOR] -DIFFICULTY_WARP_FACTOR = 4 # DELAY divides EPOCH in order to warp efficiently. -DIFFICULTY_DELAY = DIFFICULTY_EPOCH // DIFFICULTY_WARP_FACTOR # The delay in blocks before the difficulty reset applies -DISCRIMINANT_SIZE_BITS = 1024 +constants = { + "NUMBER_OF_HEADS": 3, # The number of tips each full node keeps track of and propagates + "DIFFICULTY_STARTING": 20, # These are in units of 2^32 + "BLOCK_TIME_TARGET": 10, # The target number of seconds per block + "DIFFICULTY_FACTOR": 4, # The next difficulty is truncated to range [prev / FACTOR, prev * FACTOR] -# The percentage of the difficulty target that the VDF must be run for, at a minimum -MIN_BLOCK_TIME_PERCENT = 20 -MIN_VDF_ITERATIONS = 1 # These are in units of 2^32 + # These 3 constants must be changed at the same time + "DIFFICULTY_EPOCH": 12, # The number of blocks per epoch + "DIFFICULTY_WARP_FACTOR": 4, # DELAY divides EPOCH in order to warp efficiently. + "DIFFICULTY_DELAY": 3, # EPOCH / WARP_FACTOR -MAX_FUTURE_TIME = 7200 # The next block can have a timestamp of at most these many seconds more -NUMBER_OF_TIMESTAMPS = 11 # Than the average of the last NUMBEBR_OF_TIMESTAMPS blocks + "DISCRIMINANT_SIZE_BITS": 1024, -# Hardcoded genesis block, generated using block tools -GENESIS_BLOCK = b'\x15N3\xd3\xf9H\xc2K\x96\xfe\xf2f\xa2\xbf\x87\x0e\x0f,\xd0\xd4\x0f6s\xb1".\\\xf5\x8a\xb4\x03\x84\x8e\xf9\xbb\xa1\xca\xdef3:\xe4?\x0c\xe5\xc6\x12\x80\x15N3\xd3\xf9H\xc2K\x96\xfe\xf2f\xa2\xbf\x87\x0e\x0f,\xd0\xd4\x0f6s\xb1".\\\xf5\x8a\xb4\x03\x84\x8e\xf9\xbb\xa1\xca\xdef3:\xe4?\x0c\xe5\xc6\x12\x80\x13\x00\x00\x00\x98\xf9\xeb\x86\x90Kj\x01\x1cZk_\xe1\x9c\x03;Z\xb9V\xe2\xe8\xa5\xc8\n\x0c\xbbU\xa6\xc5\xc5\xbcH\xa3\xb3fd\xcd\xb8\x83\t\xa9\x97\x96\xb5\x91G \xb2\x9e\x05\\\x91\xe1<\xee\xb1\x06\xc3\x18~XuI\xc8\x8a\xb5b\xd7.7\x96Ej\xf3DThs\x18s\xa5\xd4C\x1ea\xfd\xd5\xcf\xb9o\x18\xea6n\xe22*\xb0]%\x15\xd0i\x83\xcb\x9a\xa2.+\x0f1\xcd\x03Z\xf3]\'\xbf|\x8b\xa6\xbcF\x10\xe8Q\x19\xaeZ~\xe5\x1f\xf1)\xa3\xfb\x82\x1a\xb8\x12\xce\x19\xc8\xde\xb9n\x08[\xef\xfd\xf9\x0c\xec\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00/u\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00F\x172\xd9\xb50\x13\xd8\x99\xa7\x88UA)\xec\x0e\xc3//\xb15\n)z\xb6\xf8\x96kTpU\t+Q\xf1\x95\xe8\xd8\x1e\xcd\xe4RrVs\xb8\xee<5^\xf4\xbc\x0bA\x99\xa6\xeb\x95\xf7u\x89G\xd2\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$\x01\xf3\x05\x1b&\x8f%m\x15_\x8c\xec\x1f\x038W\xc9\xec\xe5\xf0\xe4Hn\xa6\xe2\x81Yh $?\xb6D\xb1\xa1\xef\x9fP~\x9a\x88\x15s\xb6\xe8\xdd\n\xa5\xb7~\xd7E\xfe\x1c?\xd2@\x87\x97\xf0\'\xf3\x17\x03\x00\x00\x03\x8e\x00\x15\xf0>\x9d\x9ef\x86\x86\xe2\xb4\xe0zt\xee\x86mX+L\xc4\xd7U/\xcc\x12\x8c\x81\x1a(\x17\x05\xcdIc\x066\xe8\xe2\xe1#Z\xb6\xe1\xd4b\xd3\x9b\x17(\x08r\xb0P\x02\xae\xa7>eO\x97-.\xd3X\x00\x00\x9a\xae\x8d\xd5\xcc\xd6\xc5\x80\xcc\xc2}V\xfc\xac\xcdAl\x97\xd0\xc3\x93:\xb6\xeb7t\x17O\xfb$\x01\xea\xa2\x13\xab=bk\x84|\xc4W\xac"\x1f<\x8d\x02@\x94\xa4f~\x89}\xfbsP\xd4\xaaE[\xb1\x00nT\x91\xc2\xd7D\x99f\xb3A\xbc\xdap\xf7\x1b\xd6\x93\xb8\xe8\x81\x96\x0f\xc1;\x85~\xba\xd5w&7\x17J\xec\xf3\x02\xff\x83\x1aHS\xd9g\xd0\xc1\xcef\xa1\xb6bj\xb1\xc5oR\xf6`\xe7\xe5\x97\xc7\xee\x83\x03\x98\x14\x9f%\x9c\x93\xad\xd8^\x9bx\x00.9T\x1fVo\x98\xb6\xd9\x99x\xc7\x859K\x12\xec\xc3\xe5\xf9\xb5+\xe0\x01\xed\xab\xd6\xcd\x1d[7\xe2\xf5\x11^\xad\xefPl\x8cC7\xd4y\xba\xc1j\x00\x15\x05\xf6K\xac\xfa!\x89\xf0\x88z"t\xc3\x121\x000`s\xaeS\xd0O}\xd9k\xe9\x96jC\xb0,\xfa\x086*Q)\x8f\x1a.\r#h\xb3\xf5T\xc4\xa5l$u\xcd}\xae"\xde\xdbO\xb2{\xdc\x1eQ\x8b\xb5\x9bKp\xa3cO\xf2\xbde\xe91\x16\xdc\x9d\xff\xef\xa6{\x8c\x04\xd1\r\x1d\xc8\xd8\x97\xd0\xee2\xfe\xc0\xfa\x0c\xf5\xb4n\xe6|r\xd8\x88\xee\x8cQ\x9bX\x1f\n\xcf\xb2Yh\x01\'6j\xfd\xe7\x0c\xbc\xb1\xa2dy\t\xf1\xc1\x03 \xcf\xbcX\xfb3H\x9fM2\xa7\x00\x11\x871\xfb\xc1\xd7M]\xfb\xfeo\xb6\xceBt\x9c\xd5\xc7d\xee\xe7\xbf\x0c\x08\x96\xd7S\x056\xf90\xf8\xc4\xf6!\xc1N\xf5\x9e@;\x81\xc6\xca\xe1\xa4*=r\xd0\xd3/U\xc3\x14\x99g\xb5\x96\xa4(\x1ek\xf4\xff\xfb$;\xb7\x134\x19Nmb\xc4\x04\xec\x02\xd0\xc2%\xf5L\xb5N\xc0\n\x8e=R\xdem\xd9%\xa6\xec\x01A\x14-\xdb\xa4Iz.;J\xd4<\xf3\xab\xfd9\x92$\x05\xe9\x1b\xc0\x0b\x13\xcd\xfd[\x9c\xc1\x97\x8f\x00H\xce\x90@s\n\xeb\x90\xd2\xfdtvle\xc2\x19E&K\x8e\xbf\xea\x96\xea6i\xff\x96\x83\xe0\x93\xe3\xa0?\xb5h\xff/\x96\x9f\xdb\xce\xe76\x8df\xe0\x02\xd0\xc9\xfca\xd4\xc2\xd3\xc8\xaf\xe7\xeb\xe0\xc3\xbd;\x13\xc7Lf\x0c"\x1aC?qX\xc3\xd1jY\tZ\x01\x13\x81R\xcb\n\xd3\tv\x8a\x10\xba\xa6\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00<\x00\x00\x00\x00\x00\x00/u\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00]\x89\x94\xda\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00~[u\x1f\x81\x7f\x0c)\x05\xe6\xfd\xe5\xd14\\a\n\xc6I\xccJ\x0cXk\xcf,Z\x1c\xdb>\xe0\xc3z!\xc9N\xd5\x03\x8b^\xd9\xe6\xc7I\xba\xb1\x0fm\xd4\xa0=\xb6^s\x94_f\xb5\xc1\\n\xfe\xf9\xd2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00H1V\xdeN\xcc\x8a;\x1a\x8b\xe6v\x9d\x82U\xfc?\xba2K\xdfiE\xfd\x16\xe6t\x90\x86\x14;\x1aT>\xed>u\xe8P\x87\xdf7i|/\xbf\x9a3\x10\x8e\xe0\xa9p\xc3\xdcd\x86\'A\x17:6\xc2\xdc\xe1\xc7b\x9f\xe0\xbe\xd1\xfb\x8eG\xfe?\xde\xee]\xee\x1f711L\t\x0b\xbcG+\xa8b\x0e\\O\xe5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\n+\x93\xa0\x02\xe4\xc2\x1d\xaa5R\xe5,\xbd\xa5\x15|%}\xa4@\xe5\x11\x00\x80\x1fG\x8aH\x0b\xe7\xe9\x10\xd3tK\xda`\xb5u\xca\x8c\xa2\xf7n\x1d\xd5\x92l\xb13k\xdb\n+\xbe/\x1e\xc0\xfe\xbf\xd9\x83\x88V\x11]~.<\x14\x0f\xce`\x8b\xbf\xb9\xa7\xce"6\x19\xa5\x19|\x81!r\x15V\xa6\x82\x07\x96w\x98F\xce\xb2(G\xcfm\x17@t\xb2\x1b\xba\xcf4I}\x0b\xc4\n\xd4\x9b\xe2E\x9e\x84\x98mY||\xa8[+\x93\xa0\x02\xe4\xc2\x1d\xaa5R\xe5,\xbd\xa5\x15|%}\xa4@\xe5\x11\x00\x80\x1fG\x8aH\x0b\xe7\xe9\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' # noqa: E501 + # The percentage of the difficulty target that the VDF must be run for, at a minimum + "MIN_BLOCK_TIME_PERCENT": 20, + "MIN_VDF_ITERATIONS": 1, # These are in units of 2^32 + + "MAX_FUTURE_TIME": 7200, # The next block can have a timestamp of at most these many seconds more + "NUMBER_OF_TIMESTAMPS": 11, # Than the average of the last NUMBEBR_OF_TIMESTAMPS blocks + + # If an unfinished block is more than these many seconds slower than the best unfinished block, + # don't propagate it. + "PROPAGATION_THRESHOLD": 1800, + # If the expected time is more than these seconds, slightly delay the propagation of the unfinished + # block, to allow better leaders to be released first. This is a slow block. + "PROPAGATION_DELAY_THRESHOLD": 600, + + # Hardcoded genesis block, generated using block tools + # GENESIS_BLOCK = b'\x15N3\xd3\xf9H\xc2K\x96\xfe\xf2f\xa2\xbf\x87\x0e\x0f,\xd0\xd4\x0f6s\xb1".\\\xf5\x8a\xb4\x03\x84\x8e\xf9\xbb\xa1\xca\xdef3:\xe4?\x0c\xe5\xc6\x12\x80\x15N3\xd3\xf9H\xc2K\x96\xfe\xf2f\xa2\xbf\x87\x0e\x0f,\xd0\xd4\x0f6s\xb1".\\\xf5\x8a\xb4\x03\x84\x8e\xf9\xbb\xa1\xca\xdef3:\xe4?\x0c\xe5\xc6\x12\x80\x14\x00\x00\x00\xa0_\x11\x18\x8d3\xa1\x8b\x8b1Q1@Z 6Q\xb4\xba\xafn{\x1c\xb5\xd7\xa4\xd9{\x93\xa1KB \xd2\x9fxK\xd1n\xa0wN\xfd&\nw\xbb7tm$/7\xa0f%\xf6\xd4\xc5\x1c\x98\xef\xb0\xd0\x10D\x10\x1a\x9b\xc3\xf8xd\x9d\xab\xaa>\xff\x7f\x84E\t.\xe5gz\\\x9a|\xdeE\x93\xe1\xba\xb9\xd0E\x1f\x9f\xc6\xb7\x89/\x0e8)\x1f\xdd\xc0\xa7\xa5|\xf0\\\xdf\xf9\xd1\xdbZm\xe6\xcb\xa5|F\xc1\xa3\x89\x87L\x14\xb8\xd9\xe82gIB\xe4\x14\x01q\x15r\xc1"E\x99\xc4\x10+\x0b^\xed?F\x01\x00Cs\x1a\x01\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x00\x00\x00\x00\x00\x00\tH\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x035*\x14\xda\xd9\xbbqE\xc6\xa1\x00\\\x8a^&\xc0\xec2\xa1\x16s\x0f\x8b\xe9\xbd\x0c\xe5\xca\x8fO\x06\x10\xfa\x85V!\xe9\xf6S\x97lu\xe6J\xcd\xb6\xfe\xa1F6g\xf2\xa6\xa6-\xa9~\x7f\x80:bGs\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xf3\xe8G5o&\xb6\x16\xf5\xe7n\xb9-\xebeO-0+\xbe\xc5\x96\xe3\x0f\x1be_<:\xed\xaa\xe8\x80\xbb\xa0Z\x1b>\xa1\x87(\xe9\xba\x08\xdf\xfe\x83n\xe1r\x9aUQ\xe5z9\xd8+D\xd5H\x11\xdd\x03\x00\x00\x03\x8e\x00Cr\xfa\x1c\x06\xb5\xd1\xcd\x8e\xf8\xdc\xbd\x19\xb4 \xb3\x19$\x0fsF\xd1\xbew\xad\x14\xab@\xdf\xc2\x14[\xef\xf0\xd536)\xf5\xfcN\x10\xfbK\xc3\xaeu\x01\xc0\xc8\x1e\x8e\x95:hf\xea?\\MSH\xb8\x88\x00/\x1f[!D\xfc\xb7w\x04\xf8L\xdd\x8b\x06:\xd4\xe7\xf5\xfcR\x11\\Ra}\xa7\x9aH\x9b0\x05\x9f\x80\xf5_3\xfd\xb8\x89R\xa7\xe4\xc0R\x17\xe9B\x1c7S#\xd4\xb1\x8a4zJ\xee\xb1\x01\xce\xd4\x0e\xff\x00EY\x90\x0bV\x8d0\xba\x8d\xf33e8\xe7\x9a\xa5Y~\x08\x19r\xcfP\x88\x8d\xbc\xd3TE\xedWc\x14\xc5-\x1b\xbc\x9e\xbf\xde\xa8\x1b\x90U\xa7\xdc.\xa8\xd6\xe6\'\xf1\x03\x89\xf8\t7\xfez\x02\xda\xae\xc3\xa6\xff\xfc\x07\x9a\xfb\xa6\xcf6\xa7\x8fP\x12\x17\xaa\x1f\xda\xae\xeaS\xac\xd2D\xa5\xe0\xc5\xe3\xab\'\x00\x84_\xabnZ\xf5\xd4\x10\xbd&\x16\xae\x1b{\xa0,%\x1f\xac\x08\x0b\r\xcb\xf7\xb0Ed\xa5h?\xb29^\xe1>\xed\x00O7\xbb\r$\xb5\x03\xaf\r\x0cy\\!_\xa1\xa3\xadE\xd5\x88\xe7\xef\x1d\x8c\x8a\xb0\x8bU\xde\x88\x01\xc0\xe0\xb5h\xe3\x94\x98c,j4\x18j\xe2\xd7\xa2\x05V\xce\xb9= \x05L\xbb\xcb\x9c\x99\xb6d\xa7\x1f\xff\xbb\xf1\xa3\x99q\xfdg\xc9\x89\xbc\xb8\xbdj\xc5hu\x0bZ\xe3\xa1\x7f\xcc\x0f\xfd\x10\xa1z\xe1\xcd\xb0\xcby=\x93:\xb5\xa7 \x07\xb6.\x07\x9c\xbaR\x97\xb1@\xf4V\xc0Qs\x06\x115x\x82\xb24@\xc4\xa5\x97\x00-\xc220\x85\xfd\x01\xd6\xfb\xe7oM\xa2~\xb8^\xa7\x13\xf5V\xb2\x84ax\x8c\x93H(!\x9a\xbb\xc8\xdb\x01\x0e\xc3l\xc1\xe2E\x92d\xc8B\xfdnt\x11\x11\xd9a\x8bP\x11\x87\xad\xedQ\xe0_\x1f>aR\x0f\xc5\xa0v\x16\x16\xf6\x94[g\xf6\xb6W\xc3\xc1\xfd\xff\xfc\x05$\x13h\x08\xdd\x97\xf4\xffQdS\xe5\x00\xdd\x9f{`\x91`\x88\xe9\xf5\xed\x8e.\xa4\x81\r\xd4\x80\x81\xc5]\x1b\xb5\xa3\x15\xde\xb9\x86\x9d\xcen&\xd3\xea\xf0SP\x84\xac\x9cqo9)\xcc\x114\xab\x973\x01\xad\xb1SA\x1e\xe5\x1d\x94\xd7\xfb\xb2\xf5:\x82[\xfb7\xfe\x81\x83\x1e\x1bIMT\xb7\nRIk\xb6]O\x97.y\xf5\xf1]\xc1\x89\xbf\x87\xcc\xa9,\x87\xc1\x13\xbcN&\xcc4\xf5h \x8d\xb1\xcd\xcc\xd1;2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x00\tH\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00]\x8c\x86s\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xad\xb1SA\x1e\xe5\x1d\x94\xd7\xfb\xb2\xf5:\x82[\xfb7\xfe\x81\x83\x1e\x1bIMT\xb7\nRIk\xb6]z!\xc9N\xd5\x03\x8b^\xd9\xe6\xc7I\xba\xb1\x0fm\xd4\xa0=\xb6^s\x94_f\xb5\xc1\\n\xfe\xf9\xd2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd8.\x1a\xd1\xa9f/B\xa9\xaaO\xe7\xb8O\x87\x9d(\xb0]\xf2r0\xdb]\x03\x81B\xa4\x04\xfc\xc4\xfc~\xcd\xf4\xe5\xb0\xa8{<\xe3.\xc1g\x84Y{V\x06\x1c\x15\xaf\xa47rD\xab\xb1-\xc9\x86\xbf&q\xf4\xc2_\xb3\x05\xa8\xb7\xbf\xb4\x0e\x7f\x85\xfa\xa1\xd3\xc6pS\xc6:\x13\xea2La0\xcf\xe35\xa2\xf1R\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\n+\x93\xa0\x02\xe4\xc2\x1d\xaa5R\xe5,\xbd\xa5\x15|%}\xa4@\xe5\x11\x00\x80\x1fG\x8aH\x0b\xe7\xe9\x10\xd3tK\xda`\xb5u\xca\x8c\xa2\xf7n\x1d\xd5\x92l\xb13k\xdb\n+\xbe/\x1e\xc0\xfe\xbf\xd9\x83\x88V\x11]~.<\x14\x0f\xce`\x8b\xbf\xb9\xa7\xce"6\x19\xa5\x19|\x81!r\x15V\xa6\x82\x07\x96w\x98F\xce\xb2(G\xcfm\x17@t\xb2\x1b\xba\xcf4I}\x0b\xc4\n\xd4\x9b\xe2E\x9e\x84\x98mY||\xa8[+\x93\xa0\x02\xe4\xc2\x1d\xaa5R\xe5,\xbd\xa5\x15|%}\xa4@\xe5\x11\x00\x80\x1fG\x8aH\x0b\xe7\xe9\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' # noqa: E501 + "GENESIS_BLOCK": b'\x15N3\xd3\xf9H\xc2K\x96\xfe\xf2f\xa2\xbf\x87\x0e\x0f,\xd0\xd4\x0f6s\xb1".\\\xf5\x8a\xb4\x03\x84\x8e\xf9\xbb\xa1\xca\xdef3:\xe4?\x0c\xe5\xc6\x12\x80\x15N3\xd3\xf9H\xc2K\x96\xfe\xf2f\xa2\xbf\x87\x0e\x0f,\xd0\xd4\x0f6s\xb1".\\\xf5\x8a\xb4\x03\x84\x8e\xf9\xbb\xa1\xca\xdef3:\xe4?\x0c\xe5\xc6\x12\x80\x14\x00\x00\x00\xa0_\x11\x18\x8d3\xa1\x8b\x8b1Q1@Z 6Q\xb4\xba\xafn{\x1c\xb5\xd7\xa4\xd9{\x93\xa1KB \xd2\x9fxK\xd1n\xa0wN\xfd&\nw\xbb7tm$/7\xa0f%\xf6\xd4\xc5\x1c\x98\xef\xb0\xd0\x10D\x10\x1a\x9b\xc3\xf8xd\x9d\xab\xaa>\xff\x7f\x84E\t.\xe5gz\\\x9a|\xdeE\x93\xe1\xba\xb9\xd0E\x1f\x9f\xc6\xb7\x89/\x0e8)\x1f\xdd\xc0\xa7\xa5|\xf0\\\xdf\xf9\xd1\xdbZm\xe6\xcb\xa5|F\xc1\xa3\x89\x87L\x14\xb8\xd9\xe82gIB\xe4\x14\x01q\x15r\xc1"E\x99\xc4\x10+\x0b^\xed?F\x01\x00Cs\x1a\x01\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x00\x00\x00\x00\x00\x00\x060\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00[^:j\x1bH\xe9\xc5\xe4\xe0.\xaf\x00\xaan\xa3\x8a\x12\x85\x00\xdf\xd8\xbe.\xd6\xe3\xcc\xec\xab4\x8b3\xf7 #T;a\xf4\xd4\x05\xb7\xf8B\x08\x1dc\x184\x07\x86MQ?N\x82\xd5t"\xd6\x1dL\xfa\xcf\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x15\x1e\xac\x12\xc97`p\x037\xc6\x17_\x81\xc7\x93\x85\x84\x91\xce\xf2_\xe6&\ri\xbcx\xb8T\x06l\x1e\xed\xbeS30T"Dk\xd2\x8e\x1e\xac7\x19Y\x94\x9f\xe8Lb\xd5\x1e%%l\xb7[\xb4A\xc7\x03\x00\x00\x03\x8e\x00P\x1e\xce\x92\xbb\x8bcWOopup\xe7"\xfb\xc1\x0e\xfd\x00\xb3U\xef\x07\xa4\x14\xbd\xdaw\xa1h\xd20.\x06\xc8\'\xe3d\x89LU\x1e\xdf\xb7\xeao\x9a\x0eZ@s3[\xc6\x0b\x90\xf5\xb1GKK\\\xfd\x00=\xf4\x9ai\xf6\xf3\n\x8fx\x9d\xf8\x859\x85\x90I\x84\xa8qO\x1d\xdcy\x1f*\x83\xea"\xfc\t\x02\x1cx\xe8\xba\xcf\x15\xbe\x1dM\x8bEU\x03\'d\xf6\xb5b\xc7X\xf2\xfc>G\xfa\xf7I\xd4h<\xa9Y\xc7\x00\t\x1c~\t\xa8\x03R\xf5\t\x83\x99\\T\x02\xf8$\x06\x88\x16\xa2\'\xb1\x95K+"\x9c\x9eU\xe8\x00!\xcbr4\xd8\xc0\xae\xa9\x86\xe0m\xef\x16\xfa\x7f \xdd&(\xe5Rj\xa6x\xab`\xebvQ\x19/\x96\xc1\xff\xfc\xb3\x14\xe6#\xc6\x02\xa8,n\xfd\x92\xfb\x82D\xc7\xe9\x94\xc4\xac\xdc\xf0O\x9d\x15\t\x8f\xa9In%\xe4q\xd3\x1a\r\xe2\x1e7b\x86\xd2\xb0=C\xfe<\xe3&\xa8\x13\xb1vl\x9f\x1f\x9c\x06\xf3\x98zi\xc4\xc3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\t\x1c~\t\xa8\x03R\xf5\t\x83\x99\\T\x02\xf8$\x06\x88\x16\xa2\'\xb1\x95K+"\x9c\x9eU\xe8\x00!\xcbr4\xd8\xc0\xae\xa9\x86\xe0m\xef\x16\xfa\x7f \xdd&(\xe5Rj\xa6x\xab`\xebvQ\x19/\x96\xc1\xff\xfc\xb3\x14\xe6#\xc6\x02\xa8,n\xfd\x92\xfb\x82D\xc7\xe9\x94\xc4\xac\xdc\xf0O\x9d\x15\t\x8f\xa9In%\xe4q\xd3\x1a\r\xe2\x1e7b\x86\xd2\xb0=C\xfe<\xe3&\xa8\x13\xb1vl\x9f\x1f\x9c\x06\xf3\x98zi\xc4\xc3\x00\x1dP\x98\xaaB\x9fu\x83\xca3\xe1)\xff8\xbc\x92\x19N6\xab\x9dp\xb1E\x99N/}\x9a\xef\xdf\xe1\x8bX\r`>\xc7Y{]\xf09\xd6\x82\t\xb8U\xca\xa4\xc2\x15\xcb\r\xf7@\x1aMM\xe9/q2\xb9\xff\xef\x05\xc1\xfa\xd5\x1c\xf2\'\xc6\xf5\x11\xe7\x12\x11\\\xe6CO?Ii\xff+JB\xfdd]>DS\xf3\x88;\xa4\xa1\xd3ZA\xf5\xf9\x0c\xe3\x9f\x1b\xa8\x99\xebE\x98\xe9T),V\xd3P?N\x85\x04H\xa9\xd1\x000\xae|\xcc\x02\x9f\x86[\x0f\x19\x94vt\x92\xb7\x80\x90\xbd(\xe5\xf8\x95T\xa1)\xb7\x95`?\xb0&\xce\xb3\x10\xaa0\xfe|\\\x0f;z,\xed\x98i\x02\xb7\xde\'9B`RYS\x05q\xfa0g\x81\xca\xf9\x00\x00\xb3\xaf2\xb8\x12\x0f\x7f\x81\xcazE\xda\x82\xa2x\xbaM\xa4\xe9\xfd\xae\x96\x85\xe8\xcdv\xb82d\xc7\xbb\xae!yS\x10\xd1\xb9AU7\x9c\xb9\xf0\x0f\xb2\xb0\x02\xfc(\x1bd\xef\x04\xda\xed.\xfen\x9b!\xbb]\x00^\x99@\xef\xab\x96\xaaDy\xbaB\xde\x96\xae/\xbe\xa4q\xb0xr.\xd2\xcc\xcd4\xaf\x8df\xe5j\x0c\x98\xc8>dz\x04PC{\xa0\xac\xa6\x9f\xc31\xb3j\xfa\x89\xc3u$\x16\x87\xe7\xf3\x9c\xc8D5L$\xff\xfa\xf3u\xcd|\x1a9\x94\xa0\xca\x1acw\xd8\x7f\xb9\xca\x98\xc69Z\x89\xefZ|\xc5K\x96U\xa4\xcb\x8e\x11\xf7M\x14V\xb4\x9d:`g\xd9O\x9c\nt\xbf%\x1c\xad\xfd]\xf7t\xe8~|\x8a\x16\xb5\x89\x98a\x01\xad\xb1SA\x1e\xe5\x1d\x94\xd7\xfb\xb2\xf5:\x82[\xfb7\xfe\x81\x83\x1e\x1bIMT\xb7\nRIk\xb6]\xcf{+\xdd\x80;g\x12\x81%6\xde\tx\x90\xe72\x96\xe8m\xda\xba\xe3@\x01\xd0\xd0#\xbe-\t\xe8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x14\x00\x00\x00\x00\x00\x00\x060\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00]\x8dV4\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xad\xb1SA\x1e\xe5\x1d\x94\xd7\xfb\xb2\xf5:\x82[\xfb7\xfe\x81\x83\x1e\x1bIMT\xb7\nRIk\xb6]z!\xc9N\xd5\x03\x8b^\xd9\xe6\xc7I\xba\xb1\x0fm\xd4\xa0=\xb6^s\x94_f\xb5\xc1\\n\xfe\xf9\xd2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc2B\x15\xbd\xb4\x02n\x03~\x1fK$\xf7\xe0|\xb1\x9a-Mg\xac\xc4\x8c%R\x08j|\x1d1\x8cB,\xc9\xbd\xe2\xf1\r\x8c\x0c\x0bO{b\xf7\xee\xe6e\x04>\xab\xba8\xde\x1eu\xc6\xae\x0e bool: """ Verifies whether the weight of the tip is valid or not. Naiveley, looks at every block from genesis, verifying proof of space, proof of time, and difficulty resets. + # TODO: implement """ + for height, block in enumerate(proof_blocks): + if not block.height == height: + return False + return True diff --git a/src/full_node.py b/src/full_node.py index e32ba82e..12faac72 100644 --- a/src/full_node.py +++ b/src/full_node.py @@ -7,11 +7,11 @@ import concurrent from secrets import token_bytes from hashlib import sha256 from chiapos import Verifier -from blspy import Util, Signature, PrivateKey +from blspy import Signature, PrivateKey from asyncio import Lock, sleep, Event from typing import Dict, List, Tuple, Optional, AsyncGenerator, Counter from src.util.api_decorators import api_request -from src.util.ints import uint64 +from src.util.ints import uint64, uint32 from src.util import errors from src.protocols import farmer_protocol from src.protocols import timelord_protocol @@ -26,28 +26,31 @@ from src.types.full_block import FullBlock from src.types.fees_target import FeesTarget from src.consensus.weight_verifier import verify_weight from src.consensus.pot_iterations import calculate_iterations -from src.consensus.constants import DIFFICULTY_TARGET +from src.consensus.constants import constants from src.blockchain import Blockchain, ReceiveBlockResult from src.server.outbound_message import OutboundMessage, Delivery, NodeType, Message from src.util.errors import BlockNotInBlockchain, PeersDontHaveBlock, InvalidUnfinishedBlock class Database: + # This protects all other resources lock: Lock = Lock() - blockchain: Blockchain = Blockchain() # Should be stored in memory - full_blocks: Dict[str, FullBlock] = {Blockchain.get_genesis_block().trunk_block.header.header_hash: - Blockchain.get_genesis_block()} + blockchain: Blockchain = Blockchain() + full_blocks: Dict[str, FullBlock] = { + FullBlock.from_bytes(constants["GENESIS_BLOCK"]).trunk_block.header.header_hash: + FullBlock.from_bytes(constants["GENESIS_BLOCK"])} sync_mode: bool = True # Block headers and blocks which we think might be heads, but we haven't verified yet. + # All these are used during sync mode potential_heads: Counter[bytes32] = collections.Counter() potential_heads_full_blocks: Dict[bytes32, FullBlock] = collections.Counter() # Headers/trunks downloaded for the during sync, by height - potential_trunks: Dict[uint64, TrunkBlock] = {} + potential_trunks: Dict[uint32, TrunkBlock] = {} # Blocks downloaded during sync, by height - potential_blocks: Dict[uint64, FullBlock] = {} + potential_blocks: Dict[uint32, FullBlock] = {} # Event, which gets set whenever we receive the block at each height. Waited for by sync(). - potential_blocks_received: Dict[uint64, Event] = {} + potential_blocks_received: Dict[uint32, Event] = {} # These are the blocks that we created, but don't have the PoS from farmer yet, # keyed from the proof of space hash @@ -55,7 +58,10 @@ class Database: # These are the blocks that we created, have PoS, but not PoT yet, keyed from the # block header hash - unfinished_blocks: Dict[Tuple[bytes32, int], FullBlock] = {} + unfinished_blocks: Dict[Tuple[bytes32, uint64], FullBlock] = {} + # Latest height with unfinished blocks, and expected timestamp of the finishing + unfinished_blocks_leader: Tuple[uint32, uint64] = (uint32(0), uint64(9999999999)) + proof_of_time_estimate_ips: uint64 = uint64(1500) @@ -187,7 +193,7 @@ async def sync(): if height not in db.potential_trunks: received_all_trunks = False break - local_trunks.append(db.potential_trunks[uint64(height)]) + local_trunks.append(db.potential_trunks[uint32(height)]) if received_all_trunks: trunks = local_trunks break @@ -210,12 +216,12 @@ async def sync(): if not have_block: request = peer_protocol.RequestSyncBlocks(tip_block.trunk_block.header.header_hash, [height]) async with db.lock: - db.potential_blocks_received[uint64(height)] = Event() + db.potential_blocks_received[uint32(height)] = Event() found = False for _ in range(30): yield OutboundMessage(NodeType.FULL_NODE, Message("request_sync_blocks", request), Delivery.RANDOM) try: - await asyncio.wait_for(db.potential_blocks_received[uint64(height)].wait(), timeout=2) + await asyncio.wait_for(db.potential_blocks_received[uint32(height)].wait(), timeout=2) found = True break except concurrent.futures._base.TimeoutError: @@ -227,7 +233,7 @@ async def sync(): if have_block: block = db.potential_heads_full_blocks[trunks[height].header.get_hash()] else: - block = db.potential_blocks[uint64(height)] + block = db.potential_blocks[uint32(height)] start = time.time() db.blockchain.receive_block(block) @@ -353,6 +359,7 @@ async def request_header_hash(request: farmer_protocol.RequestHeaderHash) -> Asy if head.challenge.get_hash() == request.challenge_hash: target_head = head if target_head is None: + # TODO: should we still allow the farmer to farm? log.warning(f"Challenge hash: {request.challenge_hash} not in one of three heads") return @@ -405,11 +412,6 @@ async def header_signature(header_signature: farmer_protocol.HeaderSignature) -> assert block_header_data.get_hash() == header_signature.header_hash - # Verifies the plotter's signature - # TODO: remove redundant checks after they are added to Blockchain class - assert header_signature.header_signature.verify([Util.hash256(header_signature.header_hash)], - [pos.plot_pubkey]) - block_header: BlockHeader = BlockHeader(block_header_data, header_signature.header_signature) trunk: TrunkBlock = TrunkBlock(pos, None, None, block_header) unfinished_block_obj: FullBlock = FullBlock(trunk, block_body) @@ -468,7 +470,7 @@ async def new_proof_of_time(new_proof_of_time: peer_protocol.NewProofOfTime) -> if (new_proof_of_time.proof.output.challenge_hash, new_proof_of_time.proof.output.number_of_iterations) in db.unfinished_blocks: finish_block = True - elif new_proof_of_time.proof.is_valid(): + elif new_proof_of_time.proof.is_valid(constants["DISCRIMINANT_SIZE_BITS"]): propagate_proof = True if finish_block: request = timelord_protocol.ProofOfTimeFinished(new_proof_of_time.proof) @@ -508,11 +510,26 @@ async def unfinished_block(unfinished_block: peer_protocol.UnfinishedBlock) -> A log.info(f"\tHave already seen unfinished block {(challenge_hash, iterations_needed)}") return - expected_time: float = iterations_needed / db.proof_of_time_estimate_ips + expected_time: uint64 = uint64(iterations_needed / db.proof_of_time_estimate_ips) - # TODO(alex): tweak this - log.info(f"\tExpected finish time: {expected_time}") - if expected_time > 10 * DIFFICULTY_TARGET: + if expected_time > constants["PROPAGATION_DELAY_THRESHOLD"]: + # If this block is slow, sleep to allow faster blocks to come out first + await asyncio.sleep(2) + + async with db.lock: + if unfinished_block.block.height > db.unfinished_blocks_leader[0]: + # If this is the first block we see at this height, propagate + db.unfinished_blocks_leader = (unfinished_block.block.height, expected_time) + elif unfinished_block.block.height == db.unfinished_blocks_leader[0]: + if expected_time > db.unfinished_blocks_leader[1] + constants["PROPAGATION_THRESHOLD"]: + # If VDF is expected to finish X seconds later than the best, don't propagate + return + elif expected_time < db.unfinished_blocks_leader[1]: + # If this will be the first block to finalize, update our leader + db.unfinished_blocks_leader = (db.unfinished_blocks_leader[0], expected_time) + else: + # If we have seen an unfinished block at a greater or equal height, don't propagate + # TODO: should we? return db.unfinished_blocks[(challenge_hash, iterations_needed)] = unfinished_block.block @@ -537,15 +554,14 @@ async def block(block: peer_protocol.Block) -> AsyncGenerator[OutboundMessage, N db.potential_heads_full_blocks[header_hash] = block.block return - if header_hash in db.full_blocks: - log.info(f"\tAlready have block {header_hash} height {block.block.trunk_block.challenge.height}") - return - # TODO(alex): Check if we care about this block, we don't want to add random - # disconnected blocks. For example if it's on one of the heads, or if it's an older - # block that we need added: ReceiveBlockResult = db.blockchain.receive_block(block.block) - if not (added == ReceiveBlockResult.ADDED_TO_HEAD or added == ReceiveBlockResult.ADDED_AS_ORPHAN): + if added == ReceiveBlockResult.ALREADY_HAVE_BLOCK: + log.info(f"\tAlready have block {header_hash} height {block.block.trunk_block.challenge.height}") + return + elif added == ReceiveBlockResult.INVALID_BLOCK: + log.warning(f"\tBlock {header_hash} at height {block.block.trunk_block.challenge.height} is invalid.") + elif added == ReceiveBlockResult.DISCONNECTED_BLOCK: async with db.lock: tip_height = max([head.challenge.height for head in db.blockchain.get_current_heads()]) if block.block.trunk_block.challenge.height > tip_height + config["sync_blocks_behind_threshold"]: @@ -558,8 +574,8 @@ async def block(block: peer_protocol.Block) -> AsyncGenerator[OutboundMessage, N # Perform a sync if we have to db.sync_mode = True try: + # Performs sync, and catch exceptions so we don't close the connection async for msg in sync(): - log.error(f"Yielding {msg}") yield msg except asyncio.CancelledError: log.warning("Syncing failed") @@ -580,24 +596,26 @@ async def block(block: peer_protocol.Block) -> AsyncGenerator[OutboundMessage, N async with db.lock: db.full_blocks[header_hash] = block.block - difficulty = db.blockchain.get_difficulty(header_hash) + if added == ReceiveBlockResult.ADDED_TO_HEAD: + # Only propagate blocks which extend the blockchain (one of the heads) + difficulty = db.blockchain.get_difficulty(header_hash) - pos_quality = block.block.trunk_block.proof_of_space.verify_and_get_quality( - block.block.trunk_block.proof_of_time.output.challenge_hash - ) - farmer_request = farmer_protocol.ProofOfSpaceFinalized(block.block.trunk_block.challenge.get_hash(), - block.block.trunk_block.challenge.height, - pos_quality, - difficulty) - timelord_request = timelord_protocol.ChallengeStart(block.block.trunk_block.challenge.get_hash()) - timelord_request_end = timelord_protocol.ChallengeStart(block.block.trunk_block.proof_of_time. - output.challenge_hash) - # Tell timelord to stop previous challenge and start with new one - yield OutboundMessage(NodeType.TIMELORD, Message("challenge_end", timelord_request_end), Delivery.BROADCAST) - yield OutboundMessage(NodeType.TIMELORD, Message("challenge_start", timelord_request), Delivery.BROADCAST) + pos_quality = block.block.trunk_block.proof_of_space.verify_and_get_quality( + block.block.trunk_block.proof_of_time.output.challenge_hash + ) + farmer_request = farmer_protocol.ProofOfSpaceFinalized(block.block.trunk_block.challenge.get_hash(), + block.block.trunk_block.challenge.height, + pos_quality, + difficulty) + timelord_request = timelord_protocol.ChallengeStart(block.block.trunk_block.challenge.get_hash()) + timelord_request_end = timelord_protocol.ChallengeStart(block.block.trunk_block.proof_of_time. + output.challenge_hash) + # Tell timelord to stop previous challenge and start with new one + yield OutboundMessage(NodeType.TIMELORD, Message("challenge_end", timelord_request_end), Delivery.BROADCAST) + yield OutboundMessage(NodeType.TIMELORD, Message("challenge_start", timelord_request), Delivery.BROADCAST) - # Tell full nodes about the new block - yield OutboundMessage(NodeType.FULL_NODE, Message("block", block), Delivery.BROADCAST_TO_OTHERS) + # Tell full nodes about the new block + yield OutboundMessage(NodeType.FULL_NODE, Message("block", block), Delivery.BROADCAST_TO_OTHERS) - # Tell farmer about the new block - yield OutboundMessage(NodeType.FARMER, Message("proof_of_space_finalized", farmer_request), Delivery.BROADCAST) + # Tell farmer about the new block + yield OutboundMessage(NodeType.FARMER, Message("proof_of_space_finalized", farmer_request), Delivery.BROADCAST) diff --git a/src/plotter.py b/src/plotter.py index 4a076cc0..ca2076b9 100644 --- a/src/plotter.py +++ b/src/plotter.py @@ -76,7 +76,7 @@ async def new_challenge(new_challenge: plotter_protocol.NewChallenge): quality_strings = prover.get_qualities_for_challenge(new_challenge.challenge_hash) for index, quality_str in enumerate(quality_strings): quality = ProofOfSpace.quality_str_to_quality(new_challenge.challenge_hash, quality_str) - db.challenge_hashes[quality] = (new_challenge.challenge_hash, filename, index) + db.challenge_hashes[quality] = (new_challenge.challenge_hash, filename, uint8(index)) response: plotter_protocol.ChallengeResponse = plotter_protocol.ChallengeResponse( new_challenge.challenge_hash, quality, diff --git a/src/protocols/__init__.py b/src/protocols/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/protocols/peer_protocol.py b/src/protocols/peer_protocol.py index 5266b052..24056f1b 100644 --- a/src/protocols/peer_protocol.py +++ b/src/protocols/peer_protocol.py @@ -14,17 +14,33 @@ Protocol between full nodes. """ -Receive a transaction from a peer. +Receive a transaction id from a peer. """ @cbor_message(tag=4000) -class NewTransaction: +class TransactionId: + transaction_id: bytes32 + + +""" +Request a transaction from a peer. +""" +@cbor_message(tag=4001) +class RequestTransaction: + transaction_id: bytes32 + + +""" +Receive a transaction from a peer. +""" +@cbor_message(tag=4002) +class Transaction: transaction: Transaction """ Receive a new proof of time from a peer. """ -@cbor_message(tag=4001) +@cbor_message(tag=4003) class NewProofOfTime: proof: ProofOfTime @@ -32,7 +48,7 @@ class NewProofOfTime: """ Receive an unfinished block from a peer. """ -@cbor_message(tag=4002) +@cbor_message(tag=4004) class UnfinishedBlock: # Block that does not have ProofOfTime and Challenge block: FullBlock @@ -41,7 +57,7 @@ class UnfinishedBlock: """ Requests a block from a peer. """ -@cbor_message(tag=4003) +@cbor_message(tag=4005) class RequestBlock: header_hash: bytes32 @@ -49,7 +65,7 @@ class RequestBlock: """ Receive a block from a peer. """ -@cbor_message(tag=4004) +@cbor_message(tag=4006) class Block: block: FullBlock @@ -57,7 +73,7 @@ class Block: """ Return full list of peers """ -@cbor_message(tag=4005) +@cbor_message(tag=4007) class RequestPeers: pass @@ -65,7 +81,7 @@ class RequestPeers: """ Update list of peers """ -@cbor_message(tag=4006) +@cbor_message(tag=4008) class Peers: peer_list: List[PeerInfo] @@ -73,7 +89,7 @@ class Peers: """ Request trunks of blocks that are ancestors of the specified tip. """ -@cbor_message(tag=4007) +@cbor_message(tag=4009) class RequestTrunkBlocks: tip_header_hash: bytes32 heights: List[uint64] @@ -82,7 +98,7 @@ class RequestTrunkBlocks: """ Sends trunk blocks that are ancestors of the specified tip, at the specified heights. """ -@cbor_message(tag=4008) +@cbor_message(tag=4010) class TrunkBlocks: tip_header_hash: bytes32 trunk_blocks: List[TrunkBlock] @@ -91,7 +107,7 @@ class TrunkBlocks: """ Request download of blocks, in the blockchain that has 'tip_header_hash' as the tip """ -@cbor_message(tag=4009) +@cbor_message(tag=4011) class RequestSyncBlocks: tip_header_hash: bytes32 heights: List[uint64] @@ -100,7 +116,7 @@ class RequestSyncBlocks: """ Send blocks to peer. """ -@cbor_message(tag=4010) +@cbor_message(tag=4012) class SyncBlocks: tip_header_hash: bytes32 blocks: List[FullBlock] diff --git a/src/server/__init__.py b/src/server/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/simulation/simulate_network.sh b/src/simulation/simulate_network.sh index b471d7c0..fc6495d0 100755 --- a/src/simulation/simulate_network.sh +++ b/src/simulation/simulate_network.sh @@ -13,7 +13,7 @@ python -m src.server.start_full_node "127.0.0.1" 8005 & P6=$! _term() { - echo "Caught SIGTERM signal!" + echo "Caught SIGTERM signal, killing all servers." kill -TERM "$P1" 2>/dev/null kill -TERM "$P2" 2>/dev/null kill -TERM "$P3" 2>/dev/null @@ -23,4 +23,6 @@ _term() { } trap _term SIGTERM -wait $P1 $P2 $P3 $P4 $P5 $P6 +trap _term SIGINT +trap _term INT +wait $P1 $P2 $P3 $P4 $P5 $P6 \ No newline at end of file diff --git a/src/timelord.py b/src/timelord.py index a5de4b7b..f058d5a2 100644 --- a/src/timelord.py +++ b/src/timelord.py @@ -14,7 +14,7 @@ from src.protocols import timelord_protocol from src.types.proof_of_time import ProofOfTimeOutput, ProofOfTime from src.types.classgroup import ClassgroupElement from src.util.ints import uint8 -from src.consensus import constants +from src.consensus.constants import constants from src.server.outbound_message import OutboundMessage, Delivery, Message, NodeType @@ -39,7 +39,7 @@ async def challenge_start(challenge_start: timelord_protocol.ChallengeStart): """ # TODO: stop previous processes async with db.lock: - disc: int = create_discriminant(challenge_start.challenge_hash, constants.DISCRIMINANT_SIZE_BITS) + disc: int = create_discriminant(challenge_start.challenge_hash, constants["DISCRIMINANT_SIZE_BITS"]) db.challenges[challenge_start.challenge_hash] = (time.time(), disc, None) # TODO: Start a VDF process diff --git a/src/types/proof_of_time.py b/src/types/proof_of_time.py index 861d31ed..f3c73e75 100644 --- a/src/types/proof_of_time.py +++ b/src/types/proof_of_time.py @@ -3,7 +3,6 @@ from src.util.streamable import streamable from src.types.sized_bytes import bytes32 from src.types.classgroup import ClassgroupElement from src.util.ints import uint8, uint64 -from src.consensus import constants from lib.chiavdf.inkfish.proof_of_time import check_proof_of_time_nwesolowski from lib.chiavdf.inkfish.create_discriminant import create_discriminant from lib.chiavdf.inkfish.classgroup import ClassGroup @@ -22,13 +21,13 @@ class ProofOfTime: witness_type: uint8 witness: List[uint8] - def is_valid(self): + def is_valid(self, discriminant_size_bits): disc: int = create_discriminant(self.output.challenge_hash, - constants.DISCRIMINANT_SIZE_BITS) + discriminant_size_bits) x = ClassGroup.from_ab_discriminant(2, 1, disc) y = ClassGroup.from_ab_discriminant(self.output.output.a, self.output.output.b, disc) return check_proof_of_time_nwesolowski(disc, x, y.serialize() + bytes(self.witness), self.output.number_of_iterations, - constants.DISCRIMINANT_SIZE_BITS, + discriminant_size_bits, self.witness_type) diff --git a/src/util/__init__.py b/src/util/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/block_tools.py b/tests/block_tools.py index 4d7f6055..95e8d558 100644 --- a/tests/block_tools.py +++ b/tests/block_tools.py @@ -14,7 +14,7 @@ from src.types.block_header import BlockHeader, BlockHeaderData from src.types.proof_of_space import ProofOfSpace from src.types.proof_of_time import ProofOfTime, ProofOfTimeOutput from src.types.classgroup import ClassgroupElement -from src.consensus import constants, pot_iterations, block_rewards +from src.consensus import pot_iterations, block_rewards from src.util.ints import uint64, uint32, uint8 from src.util.errors import NoProofsOfSpaceFound from src.types.coinbase import CoinbaseInfo @@ -22,16 +22,22 @@ from src.types.fees_target import FeesTarget from lib.chiavdf.inkfish.create_discriminant import create_discriminant from lib.chiavdf.inkfish.classgroup import ClassGroup from lib.chiavdf.inkfish.proof_of_time import create_proof_of_time_nwesolowski +from src.consensus.constants import constants +# Can't go much lower than 19, since plots start having no solutions +k = 19 +# Uses many plots for testing, in order to guarantee blocks at every height +num_plots = 80 # Use the empty string as the seed for the private key -sk: PrivateKey = PrivateKey.from_seed(b'') -pool_pk: PublicKey = sk.get_public_key() -plot_pk: PublicKey = sk.get_public_key() -coinbase_target = sha256(sk.get_public_key().serialize()).digest() -fee_target = sha256(sk.get_public_key().serialize()).digest() -k = 20 -num_plots = 4 +pool_sk: PrivateKey = PrivateKey.from_seed(b'') +pool_pk: PublicKey = pool_sk.get_public_key() +plot_sks: List[PrivateKey] = [PrivateKey.from_seed(pn.to_bytes(4, "big")) for pn in range(num_plots)] +plot_pks: List[PublicKey] = [sk.get_public_key() for sk in plot_sks] + +farmer_sk: PrivateKey = PrivateKey.from_seed(b'coinbase') +coinbase_target = sha256(farmer_sk.get_public_key().serialize()).digest() +fee_target = sha256(farmer_sk.get_public_key().serialize()).digest() n_wesolowski = 3 @@ -41,58 +47,117 @@ class BlockTools: """ def __init__(self): - self.plot_seed: bytes32 = ProofOfSpace.calculate_plot_seed(pool_pk, plot_pk) - self.filenames: List[str] = [os.path.join("tests", "plots", "genesis-plot-" + str(k) + + plot_seeds: List[bytes32] = [ProofOfSpace.calculate_plot_seed(pool_pk, plot_pk) for plot_pk in plot_pks] + self.filenames: List[str] = [os.path.join("tests", "plots", "genesis-plots-" + str(k) + sha256(int.to_bytes(i, 4, "big")).digest().hex() + ".dat") for i in range(num_plots)] try: - for filename in self.filenames: + for pn, filename in enumerate(self.filenames): if not os.path.exists(filename): plotter = DiskPlotter() - plotter.create_plot_disk(filename, k, b"genesis", self.plot_seed) + plotter.create_plot_disk(filename, k, b"genesis", plot_seeds[pn]) except KeyboardInterrupt: for filename in self.filenames: if os.path.exists(filename): os.remove(filename) sys.exit(1) - def get_consecutive_blocks(self, num_blocks: int) -> List[FullBlock]: + def get_consecutive_blocks(self, + num_blocks: int, + difficulty=constants["DIFFICULTY_STARTING"], + discriminant_size=constants["DISCRIMINANT_SIZE_BITS"], + seconds_per_block=constants["BLOCK_TIME_TARGET"]) -> List[FullBlock]: for i in range(100): block_list = [] try: - block_list.append(self.create_genesis_block(bytes([i]*32))) - for _ in range(num_blocks - 1): - block_list.append(self.create_next_block(block_list[-1])) + block_list.append(self.create_genesis_block(bytes([i]*32), difficulty, discriminant_size)) + prev_difficulty = difficulty + curr_difficulty = difficulty + timestamp = block_list[0].trunk_block.header.data.timestamp + for next_height in range(1, num_blocks): + if (next_height > constants["DIFFICULTY_EPOCH"] and + next_height % constants["DIFFICULTY_EPOCH"] == constants["DIFFICULTY_DELAY"]): + # Calculates new difficulty + height1 = uint64(next_height - (constants["DIFFICULTY_EPOCH"] + + constants["DIFFICULTY_DELAY"]) - 1) + height2 = uint64(next_height - (constants["DIFFICULTY_EPOCH"]) - 1) + height3 = uint64(next_height - (constants["DIFFICULTY_DELAY"]) - 1) + if height1 >= 0: + timestamp1 = block_list[height1].trunk_block.header.data.timestamp + else: + timestamp1 = (block_list[0].trunk_block.header.data.timestamp - + constants["BLOCK_TIME_TARGET"]) + timestamp2 = block_list[height2].trunk_block.header.data.timestamp + timestamp3 = block_list[height3].trunk_block.header.data.timestamp + term1 = (constants["DIFFICULTY_DELAY"] * prev_difficulty * + (timestamp3 - timestamp2) * constants["BLOCK_TIME_TARGET"]) + + term2 = ((constants["DIFFICULTY_WARP_FACTOR"] - 1) * + (constants["DIFFICULTY_EPOCH"] - constants["DIFFICULTY_DELAY"]) * curr_difficulty + * (timestamp2 - timestamp1) * constants["BLOCK_TIME_TARGET"]) + + # Round down after the division + new_difficulty: uint64 = uint64((term1 + term2) // + (constants["DIFFICULTY_WARP_FACTOR"] * + (timestamp3 - timestamp2) * + (timestamp2 - timestamp1))) + + if new_difficulty >= curr_difficulty: + new_difficulty = min(new_difficulty, uint64(constants["DIFFICULTY_FACTOR"] * + curr_difficulty)) + else: + new_difficulty = max([uint64(1), new_difficulty, + uint64(curr_difficulty // constants["DIFFICULTY_FACTOR"])]) + + prev_difficulty = curr_difficulty + curr_difficulty = new_difficulty + time_taken = seconds_per_block + timestamp += time_taken + block_list.append(self.create_next_block(block_list[-1], timestamp, curr_difficulty, + discriminant_size)) return block_list except NoProofsOfSpaceFound: pass raise NoProofsOfSpaceFound - def create_genesis_block(self, challenge_hash=bytes([0]*32)) -> FullBlock: + def create_genesis_block(self, challenge_hash=bytes([0]*32), difficulty=constants["DIFFICULTY_STARTING"], + discriminant_size=constants["DISCRIMINANT_SIZE_BITS"]) -> FullBlock: return self._create_block( challenge_hash, uint32(0), bytes([0]*32), uint64(0), uint64(0), - uint64(constants.DIFFICULTY_STARTING) + uint64(time.time()), + uint64(difficulty), + discriminant_size ) - def create_next_block(self, prev_block: FullBlock) -> FullBlock: + def create_next_block(self, prev_block: FullBlock, timestamp: uint64, + difficulty=constants["DIFFICULTY_STARTING"], + discriminant_size=constants["DISCRIMINANT_SIZE_BITS"]) -> FullBlock: return self._create_block( prev_block.trunk_block.challenge.get_hash(), prev_block.height + 1, prev_block.header_hash, prev_block.trunk_block.challenge.total_iters, prev_block.weight, - uint64(constants.DIFFICULTY_STARTING)) + timestamp, + uint64(difficulty), + discriminant_size) def _create_block(self, challenge_hash: bytes32, height: uint32, prev_header_hash: bytes32, - prev_iters: uint64, prev_weight: uint64, difficulty: uint64) -> FullBlock: + prev_iters: uint64, prev_weight: uint64, timestamp: uint64, difficulty: uint64, + discriminant_size: uint64) -> FullBlock: prover = None + plot_pk = None + plot_sk = None qualities = [] - for filename in self.filenames: + for pn in range(num_plots): + filename = self.filenames[pn] + plot_pk = plot_pks[pn] + plot_sk = plot_sks[pn] prover = DiskProver(filename) qualities = prover.get_qualities_for_challenge(challenge_hash) if len(qualities) > 0: @@ -107,11 +172,10 @@ class BlockTools: number_iters: uint64 = pot_iterations.calculate_iterations(proof_of_space, challenge_hash, difficulty) - disc: int = create_discriminant(challenge_hash, constants.DISCRIMINANT_SIZE_BITS) + disc: int = create_discriminant(challenge_hash, discriminant_size) start_x: ClassGroup = ClassGroup.from_ab_discriminant(2, 1, disc) - y_cl, proof_bytes = create_proof_of_time_nwesolowski( - disc, start_x, number_iters, constants.DISCRIMINANT_SIZE_BITS, n_wesolowski) + disc, start_x, number_iters, disc, n_wesolowski) output = ProofOfTimeOutput(challenge_hash, number_iters, ClassgroupElement(y_cl[0], y_cl[1])) @@ -120,19 +184,17 @@ class BlockTools: coinbase: CoinbaseInfo = CoinbaseInfo(height, block_rewards.calculate_block_reward(uint32(height)), coinbase_target) - coinbase_sig: PrependSignature = sk.sign_prepend(coinbase.serialize()) + coinbase_sig: PrependSignature = pool_sk.sign_prepend(coinbase.serialize()) fees_target: FeesTarget = FeesTarget(fee_target, 0) body: BlockBody = BlockBody(coinbase, coinbase_sig, fees_target, None, bytes([0]*32)) - timestamp = uint64(time.time()) - header_data: BlockHeaderData = BlockHeaderData(prev_header_hash, timestamp, bytes([0]*32), proof_of_space.get_hash(), body.get_hash(), bytes([0]*32)) - header_hash_sig: PrependSignature = sk.sign_prepend(header_data.get_hash()) + header_hash_sig: PrependSignature = plot_sk.sign_prepend(header_data.get_hash()) header: BlockHeader = BlockHeader(header_data, header_hash_sig) @@ -146,3 +208,5 @@ class BlockTools: # print(create_genesis_block().serialize()) +# bt = BlockTools() +# print(bt.create_genesis_block(bytes([4]*32)).serialize()) diff --git a/tests/test_blockchain.py b/tests/test_blockchain.py index 6368ddb7..96fcce16 100644 --- a/tests/test_blockchain.py +++ b/tests/test_blockchain.py @@ -1,3 +1,4 @@ +from src.consensus.constants import constants import time import pytest from blspy import PrivateKey @@ -32,62 +33,67 @@ class TestBlockValidation(): @pytest.fixture(scope="module") def initial_blockchain(self): """ - Provides a list of 3 valid blocks, as well as a blockchain with 2 blocks added to it. + Provides a list of 10 valid blocks, as well as a blockchain with 9 blocks added to it. """ - blocks = bt.get_consecutive_blocks(3) - b: Blockchain = Blockchain(blocks[0]) - assert b.receive_block(blocks[1]) == ReceiveBlockResult.ADDED_TO_HEAD + blocks = bt.get_consecutive_blocks(10, 5, 16) + b: Blockchain = Blockchain({ + "GENESIS_BLOCK": blocks[0].serialize(), + "DIFFICULTY_STARTING": 5, + "DISCRIMINANT_SIZE_BITS": 16 + }) + for i in range(1, 9): + assert b.receive_block(blocks[i]) == ReceiveBlockResult.ADDED_TO_HEAD return (blocks, b) def test_prev_pointer(self, initial_blockchain): blocks, b = initial_blockchain block_bad = FullBlock(TrunkBlock( - blocks[2].trunk_block.proof_of_space, - blocks[2].trunk_block.proof_of_time, - blocks[2].trunk_block.challenge, + blocks[9].trunk_block.proof_of_space, + blocks[9].trunk_block.proof_of_time, + blocks[9].trunk_block.challenge, BlockHeader(BlockHeaderData( bytes([1]*32), - blocks[2].trunk_block.header.data.timestamp, - blocks[2].trunk_block.header.data.filter_hash, - blocks[2].trunk_block.header.data.proof_of_space_hash, - blocks[2].trunk_block.header.data.body_hash, - blocks[2].trunk_block.header.data.extension_data - ), blocks[2].trunk_block.header.plotter_signature) - ), blocks[2].body) + blocks[9].trunk_block.header.data.timestamp, + blocks[9].trunk_block.header.data.filter_hash, + blocks[9].trunk_block.header.data.proof_of_space_hash, + blocks[9].trunk_block.header.data.body_hash, + blocks[9].trunk_block.header.data.extension_data + ), blocks[9].trunk_block.header.plotter_signature) + ), blocks[9].body) assert b.receive_block(block_bad) == ReceiveBlockResult.INVALID_BLOCK def test_timestamp(self, initial_blockchain): blocks, b = initial_blockchain # Time too far in the past block_bad = FullBlock(TrunkBlock( - blocks[2].trunk_block.proof_of_space, - blocks[2].trunk_block.proof_of_time, - blocks[2].trunk_block.challenge, + blocks[9].trunk_block.proof_of_space, + blocks[9].trunk_block.proof_of_time, + blocks[9].trunk_block.challenge, BlockHeader(BlockHeaderData( - blocks[2].trunk_block.header.data.prev_header_hash, - blocks[2].trunk_block.header.data.timestamp - 1000, - blocks[2].trunk_block.header.data.filter_hash, - blocks[2].trunk_block.header.data.proof_of_space_hash, - blocks[2].trunk_block.header.data.body_hash, - blocks[2].trunk_block.header.data.extension_data - ), blocks[2].trunk_block.header.plotter_signature) - ), blocks[2].body) + blocks[9].trunk_block.header.data.prev_header_hash, + blocks[9].trunk_block.header.data.timestamp - 1000, + blocks[9].trunk_block.header.data.filter_hash, + blocks[9].trunk_block.header.data.proof_of_space_hash, + blocks[9].trunk_block.header.data.body_hash, + blocks[9].trunk_block.header.data.extension_data + ), blocks[9].trunk_block.header.plotter_signature) + ), blocks[9].body) assert b.receive_block(block_bad) == ReceiveBlockResult.INVALID_BLOCK # Time too far in the future block_bad = FullBlock(TrunkBlock( - blocks[2].trunk_block.proof_of_space, - blocks[2].trunk_block.proof_of_time, - blocks[2].trunk_block.challenge, + blocks[9].trunk_block.proof_of_space, + blocks[9].trunk_block.proof_of_time, + blocks[9].trunk_block.challenge, BlockHeader(BlockHeaderData( - blocks[2].trunk_block.header.data.prev_header_hash, + blocks[9].trunk_block.header.data.prev_header_hash, time.time() + 3600 * 3, - blocks[2].trunk_block.header.data.filter_hash, - blocks[2].trunk_block.header.data.proof_of_space_hash, - blocks[2].trunk_block.header.data.body_hash, - blocks[2].trunk_block.header.data.extension_data - ), blocks[2].trunk_block.header.plotter_signature) - ), blocks[2].body) + blocks[9].trunk_block.header.data.filter_hash, + blocks[9].trunk_block.header.data.proof_of_space_hash, + blocks[9].trunk_block.header.data.body_hash, + blocks[9].trunk_block.header.data.extension_data + ), blocks[9].trunk_block.header.plotter_signature) + ), blocks[9].body) assert b.receive_block(block_bad) == ReceiveBlockResult.INVALID_BLOCK @@ -95,65 +101,87 @@ class TestBlockValidation(): blocks, b = initial_blockchain # Time too far in the past block_bad = FullBlock(TrunkBlock( - blocks[2].trunk_block.proof_of_space, - blocks[2].trunk_block.proof_of_time, - blocks[2].trunk_block.challenge, + blocks[9].trunk_block.proof_of_space, + blocks[9].trunk_block.proof_of_time, + blocks[9].trunk_block.challenge, BlockHeader(BlockHeaderData( - blocks[2].trunk_block.header.data.prev_header_hash, - blocks[2].trunk_block.header.data.timestamp, - blocks[2].trunk_block.header.data.filter_hash, - blocks[2].trunk_block.header.data.proof_of_space_hash, + blocks[9].trunk_block.header.data.prev_header_hash, + blocks[9].trunk_block.header.data.timestamp, + blocks[9].trunk_block.header.data.filter_hash, + blocks[9].trunk_block.header.data.proof_of_space_hash, bytes([1]*32), - blocks[2].trunk_block.header.data.extension_data - ), blocks[2].trunk_block.header.plotter_signature) - ), blocks[2].body) + blocks[9].trunk_block.header.data.extension_data + ), blocks[9].trunk_block.header.plotter_signature) + ), blocks[9].body) assert b.receive_block(block_bad) == ReceiveBlockResult.INVALID_BLOCK def test_plotter_signature(self, initial_blockchain): blocks, b = initial_blockchain # Time too far in the past block_bad = FullBlock(TrunkBlock( - blocks[2].trunk_block.proof_of_space, - blocks[2].trunk_block.proof_of_time, - blocks[2].trunk_block.challenge, + blocks[9].trunk_block.proof_of_space, + blocks[9].trunk_block.proof_of_time, + blocks[9].trunk_block.challenge, BlockHeader( - blocks[2].trunk_block.header.data, + blocks[9].trunk_block.header.data, PrivateKey.from_seed(b'0').sign_prepend(b"random junk")) - ), blocks[2].body) + ), blocks[9].body) assert b.receive_block(block_bad) == ReceiveBlockResult.INVALID_BLOCK def test_invalid_pos(self, initial_blockchain): blocks, b = initial_blockchain - bad_pos = blocks[2].trunk_block.proof_of_space.proof + bad_pos = blocks[9].trunk_block.proof_of_space.proof bad_pos[0] = (bad_pos[0] + 1) % 256 # Proof of space invalid block_bad = FullBlock(TrunkBlock( ProofOfSpace( - blocks[2].trunk_block.proof_of_space.pool_pubkey, - blocks[2].trunk_block.proof_of_space.plot_pubkey, - blocks[2].trunk_block.proof_of_space.size, + blocks[9].trunk_block.proof_of_space.pool_pubkey, + blocks[9].trunk_block.proof_of_space.plot_pubkey, + blocks[9].trunk_block.proof_of_space.size, bad_pos ), - blocks[2].trunk_block.proof_of_time, - blocks[2].trunk_block.challenge, - blocks[2].trunk_block.header - ), blocks[2].body) + blocks[9].trunk_block.proof_of_time, + blocks[9].trunk_block.challenge, + blocks[9].trunk_block.header + ), blocks[9].body) assert b.receive_block(block_bad) == ReceiveBlockResult.INVALID_BLOCK def test_invalid_coinbase_height(self, initial_blockchain): blocks, b = initial_blockchain # Coinbase height invalid - block_bad = FullBlock(blocks[2].trunk_block, BlockBody( + block_bad = FullBlock(blocks[9].trunk_block, BlockBody( CoinbaseInfo( 3, - blocks[2].body.coinbase.amount, - blocks[2].body.coinbase.puzzle_hash + blocks[9].body.coinbase.amount, + blocks[9].body.coinbase.puzzle_hash ), - blocks[2].body.coinbase_signature, - blocks[2].body.fees_target_info, - blocks[2].body.aggregated_signature, - blocks[2].body.solutions_generator + blocks[9].body.coinbase_signature, + blocks[9].body.fees_target_info, + blocks[9].body.aggregated_signature, + blocks[9].body.solutions_generator )) assert b.receive_block(block_bad) == ReceiveBlockResult.INVALID_BLOCK + + def test_difficulty_change(self): + num_blocks = 20 + # Make it 5x faster than target time + blocks = bt.get_consecutive_blocks(num_blocks, 5, 16, 1) + b: Blockchain = Blockchain({ + "GENESIS_BLOCK": blocks[0].serialize(), + "DIFFICULTY_STARTING": 5, + "DISCRIMINANT_SIZE_BITS": 16, + "BLOCK_TIME_TARGET": 10, + "DIFFICULTY_EPOCH": 12, # The number of blocks per epoch + "DIFFICULTY_WARP_FACTOR": 4, # DELAY divides EPOCH in order to warp efficiently. + "DIFFICULTY_DELAY": 3 # EPOCH / WARP_FACTOR + }) + + for i in range(1, num_blocks): + assert b.receive_block(blocks[i]) == ReceiveBlockResult.ADDED_TO_HEAD + + assert b.get_difficulty(blocks[14].header_hash) == b.get_difficulty(blocks[13].header_hash) + assert b.get_difficulty(blocks[15].header_hash) > b.get_difficulty(blocks[14].header_hash) + assert ((b.get_difficulty(blocks[15].header_hash) / b.get_difficulty(blocks[14].header_hash) + <= constants["DIFFICULTY_FACTOR"]))