import asyncio import time from typing import Any, Dict from pathlib import Path import pytest from blspy import PrivateKey from src.full_node.blockchain import Blockchain, ReceiveBlockResult from src.full_node.store import FullNodeStore from src.types.body import Body from src.types.full_block import FullBlock from src.types.hashable.coin import Coin from src.types.header import Header, HeaderData from src.types.proof_of_space import ProofOfSpace from src.full_node.coin_store import CoinStore from src.util.ints import uint8, uint64 from tests.block_tools import BlockTools bt = BlockTools() test_constants: Dict[str, Any] = { "DIFFICULTY_STARTING": 5, "DISCRIMINANT_SIZE_BITS": 16, "BLOCK_TIME_TARGET": 10, "MIN_BLOCK_TIME": 2, "DIFFICULTY_FACTOR": 3, "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 "VDF_IPS_STARTING": 50, } test_constants["GENESIS_BLOCK"] = bytes( bt.create_genesis_block(test_constants, bytes([0] * 32), b"0") ) @pytest.fixture(scope="module") def event_loop(): loop = asyncio.get_event_loop() yield loop class TestGenesisBlock: @pytest.mark.asyncio async def test_basic_blockchain(self): unspent_store = await CoinStore.create(Path("blockchain_test.db")) store = await FullNodeStore.create(Path("blockchain_test.db")) await store._clear_database() bc1 = await Blockchain.create(unspent_store, store, test_constants) assert len(bc1.get_current_tips()) == 1 genesis_block = bc1.get_current_tips()[0] assert genesis_block.height == 0 assert ( bc1.get_next_difficulty(genesis_block.header_hash) ) == genesis_block.weight assert bc1.get_next_ips(bc1.genesis) > 0 await unspent_store.close() await store.close() class TestBlockValidation: @pytest.fixture(scope="module") async def initial_blockchain(self): """ Provides a list of 10 valid blocks, as well as a blockchain with 9 blocks added to it. """ blocks = bt.get_consecutive_blocks(test_constants, 10, [], 10) store = await FullNodeStore.create(Path("blockchain_test.db")) await store._clear_database() unspent_store = await CoinStore.create(Path("blockchain_test.db")) b: Blockchain = await Blockchain.create(unspent_store, store, test_constants) for i in range(1, 9): result, removed = await b.receive_block(blocks[i]) assert result == ReceiveBlockResult.ADDED_TO_HEAD yield (blocks, b) await unspent_store.close() await store.close() @pytest.mark.asyncio async def test_prev_pointer(self, initial_blockchain): blocks, b = initial_blockchain block_bad = FullBlock( blocks[9].proof_of_space, blocks[9].proof_of_time, Header( HeaderData( blocks[9].header.data.height, bytes([1] * 32), blocks[9].header.data.timestamp, blocks[9].header.data.filter, blocks[9].header.data.proof_of_space_hash, blocks[9].header.data.body_hash, blocks[9].header.data.weight, blocks[9].header.data.total_iters, blocks[9].header.data.additions_root, blocks[9].header.data.removals_root, ), blocks[9].header.harvester_signature, ), blocks[9].body, ) result, removed = await b.receive_block(block_bad) assert (result) == ReceiveBlockResult.DISCONNECTED_BLOCK @pytest.mark.asyncio async def test_timestamp(self, initial_blockchain): blocks, b = initial_blockchain # Time too far in the past block_bad = FullBlock( blocks[9].proof_of_space, blocks[9].proof_of_time, Header( HeaderData( blocks[9].header.data.height, blocks[9].header.data.prev_header_hash, blocks[9].header.data.timestamp - 1000, blocks[9].header.data.filter, blocks[9].header.data.proof_of_space_hash, blocks[9].header.data.body_hash, blocks[9].header.data.weight, blocks[9].header.data.total_iters, blocks[9].header.data.additions_root, blocks[9].header.data.removals_root, ), blocks[9].header.harvester_signature, ), blocks[9].body, ) result, removed = await b.receive_block(block_bad) assert (result) == ReceiveBlockResult.INVALID_BLOCK # Time too far in the future block_bad = FullBlock( blocks[9].proof_of_space, blocks[9].proof_of_time, Header( HeaderData( blocks[9].header.data.height, blocks[9].header.data.prev_header_hash, uint64(int(time.time() + 3600 * 3)), blocks[9].header.data.filter, blocks[9].header.data.proof_of_space_hash, blocks[9].header.data.body_hash, blocks[9].header.data.weight, blocks[9].header.data.total_iters, blocks[9].header.data.additions_root, blocks[9].header.data.removals_root, ), blocks[9].header.harvester_signature, ), blocks[9].body, ) result, removed = await b.receive_block(block_bad) assert (result) == ReceiveBlockResult.INVALID_BLOCK @pytest.mark.asyncio async def test_body_hash(self, initial_blockchain): blocks, b = initial_blockchain block_bad = FullBlock( blocks[9].proof_of_space, blocks[9].proof_of_time, Header( HeaderData( blocks[9].header.data.height, blocks[9].header.data.prev_header_hash, blocks[9].header.data.timestamp, blocks[9].header.data.filter, blocks[9].header.data.proof_of_space_hash, bytes([1] * 32), blocks[9].header.data.weight, blocks[9].header.data.total_iters, blocks[9].header.data.additions_root, blocks[9].header.data.removals_root, ), blocks[9].header.harvester_signature, ), blocks[9].body, ) result, removed = await b.receive_block(block_bad) assert result == ReceiveBlockResult.INVALID_BLOCK @pytest.mark.asyncio async def test_harvester_signature(self, initial_blockchain): blocks, b = initial_blockchain # Time too far in the past block_bad = FullBlock( blocks[9].proof_of_space, blocks[9].proof_of_time, Header( blocks[9].header.data, PrivateKey.from_seed(b"0").sign_prepend(b"random junk"), ), blocks[9].body, ) result, removed = await b.receive_block(block_bad) assert result == ReceiveBlockResult.INVALID_BLOCK @pytest.mark.asyncio async def test_invalid_pos(self, initial_blockchain): blocks, b = initial_blockchain bad_pos = bytearray([i for i in blocks[9].proof_of_space.proof]) bad_pos[0] = uint8((bad_pos[0] + 1) % 256) # Proof of space invalid block_bad = FullBlock( ProofOfSpace( blocks[9].proof_of_space.challenge_hash, blocks[9].proof_of_space.pool_pubkey, blocks[9].proof_of_space.plot_pubkey, blocks[9].proof_of_space.size, bytes(bad_pos), ), blocks[9].proof_of_time, blocks[9].header, blocks[9].body, ) result, removed = await b.receive_block(block_bad) assert result == ReceiveBlockResult.INVALID_BLOCK @pytest.mark.asyncio async def test_invalid_coinbase_height(self, initial_blockchain): blocks, b = initial_blockchain # Coinbase height invalid block_bad = FullBlock( blocks[9].proof_of_space, blocks[9].proof_of_time, blocks[9].header, Body( Coin( blocks[7].body.coinbase.parent_coin_info, blocks[9].body.coinbase.puzzle_hash, uint64(9999999999), ), blocks[9].body.coinbase_signature, blocks[9].body.fees_coin, None, blocks[9].body.aggregated_signature, blocks[9].body.cost, blocks[9].body.extension_data, ), ) result, removed = await b.receive_block(block_bad) assert result == ReceiveBlockResult.INVALID_BLOCK @pytest.mark.asyncio async def test_difficulty_change(self): num_blocks = 30 # Make it 5x faster than target time blocks = bt.get_consecutive_blocks(test_constants, num_blocks, [], 2) unspent_store = await CoinStore.create(Path("blockchain_test.db")) store = await FullNodeStore.create(Path("blockchain_test.db")) await store._clear_database() b: Blockchain = await Blockchain.create(unspent_store, store, test_constants) for i in range(1, num_blocks): result, removed = await b.receive_block(blocks[i]) assert result == ReceiveBlockResult.ADDED_TO_HEAD diff_25 = b.get_next_difficulty(blocks[24].header_hash) diff_26 = b.get_next_difficulty(blocks[25].header_hash) diff_27 = b.get_next_difficulty(blocks[26].header_hash) assert diff_26 == diff_25 assert diff_27 > diff_26 assert (diff_27 / diff_26) <= test_constants["DIFFICULTY_FACTOR"] assert (b.get_next_ips(blocks[1])) == test_constants["VDF_IPS_STARTING"] assert (b.get_next_ips(blocks[24])) == (b.get_next_ips(blocks[23])) assert (b.get_next_ips(blocks[25])) == (b.get_next_ips(blocks[24])) assert (b.get_next_ips(blocks[26])) > (b.get_next_ips(blocks[25])) assert (b.get_next_ips(blocks[27])) == (b.get_next_ips(blocks[26])) await unspent_store.close() await store.close() class TestReorgs: @pytest.mark.asyncio async def test_basic_reorg(self): blocks = bt.get_consecutive_blocks(test_constants, 100, [], 9) unspent_store = await CoinStore.create(Path("blockchain_test.db")) store = await FullNodeStore.create(Path("blockchain_test.db")) await store._clear_database() b: Blockchain = await Blockchain.create(unspent_store, store, test_constants) for i in range(1, len(blocks)): await b.receive_block(blocks[i]) assert b.get_current_tips()[0].height == 100 blocks_reorg_chain = bt.get_consecutive_blocks( test_constants, 30, blocks[:90], 9, b"2" ) for i in range(1, len(blocks_reorg_chain)): reorg_block = blocks_reorg_chain[i] result, removed = await b.receive_block(reorg_block) if reorg_block.height < 90: assert result == ReceiveBlockResult.ALREADY_HAVE_BLOCK elif reorg_block.height < 99: assert result == ReceiveBlockResult.ADDED_AS_ORPHAN elif reorg_block.height >= 100: assert result == ReceiveBlockResult.ADDED_TO_HEAD assert b.get_current_tips()[0].height == 119 await unspent_store.close() await store.close() @pytest.mark.asyncio async def test_reorg_from_genesis(self): blocks = bt.get_consecutive_blocks(test_constants, 20, [], 9, b"0") unspent_store = await CoinStore.create(Path("blockchain_test.db")) store = await FullNodeStore.create(Path("blockchain_test.db")) await store._clear_database() b: Blockchain = await Blockchain.create(unspent_store, store, test_constants) for i in range(1, len(blocks)): await b.receive_block(blocks[i]) assert b.get_current_tips()[0].height == 20 # Reorg from genesis blocks_reorg_chain = bt.get_consecutive_blocks( test_constants, 21, [blocks[0]], 9, b"3" ) for i in range(1, len(blocks_reorg_chain)): print("I", i) reorg_block = blocks_reorg_chain[i] result, removed = await b.receive_block(reorg_block) if reorg_block.height == 0: assert result == ReceiveBlockResult.ALREADY_HAVE_BLOCK elif reorg_block.height < 19: assert result == ReceiveBlockResult.ADDED_AS_ORPHAN else: assert result == ReceiveBlockResult.ADDED_TO_HEAD assert b.get_current_tips()[0].height == 21 # Reorg back to original branch blocks_reorg_chain_2 = bt.get_consecutive_blocks( test_constants, 3, blocks[:-1], 9, b"4" ) result, _ = await b.receive_block(blocks_reorg_chain_2[20]) assert result == ReceiveBlockResult.ADDED_AS_ORPHAN result, _ = await b.receive_block(blocks_reorg_chain_2[21]) assert result == ReceiveBlockResult.ADDED_TO_HEAD result, _ = await b.receive_block(blocks_reorg_chain_2[22]) assert result == ReceiveBlockResult.ADDED_TO_HEAD await unspent_store.close() await store.close() @pytest.mark.asyncio async def test_lca(self): blocks = bt.get_consecutive_blocks(test_constants, 5, [], 9, b"0") unspent_store = await CoinStore.create(Path("blockchain_test.db")) store = await FullNodeStore.create(Path("blockchain_test.db")) await store._clear_database() b: Blockchain = await Blockchain.create(unspent_store, store, test_constants) for i in range(1, len(blocks)): await b.receive_block(blocks[i]) assert b.lca_block.header_hash == blocks[3].header_hash block_5_2 = bt.get_consecutive_blocks(test_constants, 1, blocks[:5], 9, b"1") block_5_3 = bt.get_consecutive_blocks(test_constants, 1, blocks[:5], 9, b"2") await b.receive_block(block_5_2[5]) assert b.lca_block.header_hash == blocks[4].header_hash await b.receive_block(block_5_3[5]) assert b.lca_block.header_hash == blocks[4].header_hash reorg = bt.get_consecutive_blocks(test_constants, 6, [], 9, b"3") for i in range(1, len(reorg)): await b.receive_block(reorg[i]) assert b.lca_block.header_hash == blocks[0].header_hash await unspent_store.close() await store.close() @pytest.mark.asyncio async def test_get_header_hashes(self): blocks = bt.get_consecutive_blocks(test_constants, 5, [], 9, b"0") unspent_store = await CoinStore.create(Path("blockchain_test.db")) store = await FullNodeStore.create(Path("blockchain_test.db")) await store._clear_database() b: Blockchain = await Blockchain.create(unspent_store, store, test_constants) for i in range(1, len(blocks)): await b.receive_block(blocks[i]) header_hashes = b.get_header_hashes(blocks[-1].header_hash) assert len(header_hashes) == 6 assert header_hashes == [block.header_hash for block in blocks] await unspent_store.close() await store.close()