Files
chia-blockchain/src/full_node.py
T

1317 lines
53 KiB
Python

import asyncio
import concurrent
import logging
import time
from asyncio import Event
from hashlib import sha256
from secrets import token_bytes
from typing import AsyncGenerator, List, Optional, Tuple, Dict
from chiapos import Verifier
from src.blockchain import Blockchain, ReceiveBlockResult
from src.consensus.block_rewards import calculate_block_reward
from src.consensus.constants import constants
from src.consensus.pot_iterations import calculate_iterations
from src.consensus.weight_verifier import verify_weight
from src.store import FullNodeStore
from src.protocols import farmer_protocol, peer_protocol, timelord_protocol
from src.farming.farming_tools import best_solution_program
from src.mempool import Mempool
from src.server.outbound_message import Delivery, Message, NodeType, OutboundMessage
from src.server.server import ChiaServer
from src.types.body import Body
from src.types.challenge import Challenge
from src.types.full_block import FullBlock
from src.types.hashable.Coin import Coin
from src.types.hashable.BLSSignature import BLSSignature
from src.types.hashable.Hash import std_hash
from src.types.hashable.SpendBundle import SpendBundle
from src.types.hashable.Program import Program
from src.types.header import Header, HeaderData
from src.types.header_block import HeaderBlock, SmallHeaderBlock
from src.types.peer_info import PeerInfo
from src.types.proof_of_space import ProofOfSpace
from src.types.sized_bytes import bytes32
from src.unspent_store import UnspentStore
from src.util import errors
from src.util.api_decorators import api_request
from src.util.errors import BlockNotInBlockchain, InvalidUnfinishedBlock
from src.util.ints import uint32, uint64
OutboundMessageGenerator = AsyncGenerator[OutboundMessage, None]
class FullNode:
def __init__(
self,
store: FullNodeStore,
blockchain: Blockchain,
config: Dict,
mempool: Mempool,
unspent_store: UnspentStore,
name: str = None,
):
self.config: Dict = config
self.store: FullNodeStore = store
self.blockchain: Blockchain = blockchain
self.mempool: Mempool = mempool
self._shut_down = False # Set to true to close all infinite loops
self.server: Optional[ChiaServer] = None
self.unspent_store: UnspentStore = unspent_store
if name:
self.log = logging.getLogger(name)
else:
self.log = logging.getLogger(__name__)
def _set_server(self, server: ChiaServer):
self.server = server
async def _send_tips_to_farmers(
self, delivery: Delivery = Delivery.BROADCAST
) -> OutboundMessageGenerator:
"""
Sends all of the current heads to all farmer peers. Also sends the latest
estimated proof of time rate, so farmer can calulate which proofs are good.
"""
requests: List[farmer_protocol.ProofOfSpaceFinalized] = []
async with self.store.lock:
tips_raw = self.blockchain.get_current_tips()
tips = await self.store.get_header_blocks_by_hash(
[t.header_hash for t in tips_raw]
)
for tip in tips:
assert tip.proof_of_time and tip.challenge
challenge_hash = tip.challenge.get_hash()
quality = tip.proof_of_space.verify_and_get_quality()
if tip.height > 0:
difficulty: uint64 = self.blockchain.get_next_difficulty(
tip.prev_header_hash
)
else:
difficulty = tip.weight
requests.append(
farmer_protocol.ProofOfSpaceFinalized(
challenge_hash, tip.height, tip.weight, quality, difficulty
)
)
proof_of_time_rate: uint64 = self.blockchain.get_next_ips(tips[0])
rate_update = farmer_protocol.ProofOfTimeRate(proof_of_time_rate)
yield OutboundMessage(
NodeType.FARMER, Message("proof_of_time_rate", rate_update), delivery
)
for request in requests:
yield OutboundMessage(
NodeType.FARMER, Message("proof_of_space_finalized", request), delivery
)
async def _send_challenges_to_timelords(
self, delivery: Delivery = Delivery.BROADCAST
) -> OutboundMessageGenerator:
"""
Sends all of the current heads (as well as Pos infos) to all timelord peers.
"""
challenge_requests: List[timelord_protocol.ChallengeStart] = []
pos_info_requests: List[timelord_protocol.ProofOfSpaceInfo] = []
tips: List[SmallHeaderBlock] = self.blockchain.get_current_tips()
for tip in tips:
assert tip.challenge
challenge_hash = tip.challenge.get_hash()
challenge_requests.append(
timelord_protocol.ChallengeStart(
challenge_hash, tip.challenge.total_weight
)
)
tip_hashes = [tip.header_hash for tip in tips]
tip_infos = [
tup[0]
for tup in list((self.store.get_unfinished_blocks()).items())
if tup[1].prev_header_hash in tip_hashes
]
for chall, iters in tip_infos:
pos_info_requests.append(timelord_protocol.ProofOfSpaceInfo(chall, iters))
for challenge_msg in challenge_requests:
yield OutboundMessage(
NodeType.TIMELORD, Message("challenge_start", challenge_msg), delivery
)
for pos_info_msg in pos_info_requests:
yield OutboundMessage(
NodeType.TIMELORD,
Message("proof_of_space_info", pos_info_msg),
delivery,
)
async def _on_connect(self) -> OutboundMessageGenerator:
"""
Whenever we connect to another node, send them our current heads. Also send heads to farmers
and challenges to timelords.
"""
blocks: List[FullBlock] = []
tips: List[SmallHeaderBlock] = self.blockchain.get_current_tips()
for t in tips:
block = await self.store.get_block(t.header.get_hash())
assert block
blocks.append(block)
for block in blocks:
request = peer_protocol.Block(block)
yield OutboundMessage(
NodeType.FULL_NODE, Message("block", request), Delivery.RESPOND
)
# Update farmers and timelord with most recent information
async for msg in self._send_challenges_to_timelords(Delivery.RESPOND):
yield msg
async for msg in self._send_tips_to_farmers(Delivery.RESPOND):
yield msg
def _num_needed_peers(self) -> int:
assert self.server is not None
diff = self.config["target_peer_count"] - len(
self.server.global_connections.get_full_node_connections()
)
return diff if diff >= 0 else 0
def _start_bg_tasks(self):
"""
Start a background task connecting periodically to the introducer and
requesting the peer list.
"""
introducer = self.config["introducer_peer"]
introducer_peerinfo = PeerInfo(introducer["host"], introducer["port"])
async def introducer_client():
async def on_connect() -> OutboundMessageGenerator:
msg = Message("request_peers", peer_protocol.RequestPeers())
yield OutboundMessage(NodeType.INTRODUCER, msg, Delivery.RESPOND)
while not self._shut_down:
# The first time connecting to introducer, keep trying to connect
if self._num_needed_peers():
if not await self.server.start_client(
introducer_peerinfo, on_connect
):
await asyncio.sleep(5)
continue
await asyncio.sleep(self.config["introducer_connect_interval"])
asyncio.create_task(introducer_client())
def _shutdown(self):
self._shut_down = True
async def _sync(self) -> OutboundMessageGenerator:
"""
Performs a full sync of the blockchain.
- Check which are the heaviest tips
- Request headers for the heaviest
- Verify the weight of the tip, using the headers
- Find the fork point to see where to start downloading blocks
- Blacklist peers that provide invalid blocks
- Sync blockchain up to heads (request blocks in batches)
"""
self.log.info("Starting to perform sync with peers.")
self.log.info("Waiting to receive tips from peers.")
# TODO: better way to tell that we have finished receiving tips
await asyncio.sleep(5)
highest_weight: uint64 = uint64(0)
tip_block: FullBlock
tip_height = 0
sync_start_time = time.time()
# Based on responses from peers about the current heads, see which head is the heaviest
# (similar to longest chain rule).
potential_tips: List[
Tuple[bytes32, FullBlock]
] = self.store.get_potential_tips_tuples()
self.log.info(f"Have collected {len(potential_tips)} potential tips")
for header_hash, potential_tip_block in potential_tips:
if potential_tip_block.header_block.challenge is None:
raise ValueError(
f"Invalid tip block {potential_tip_block.header_hash} received"
)
if potential_tip_block.header_block.challenge.total_weight > highest_weight:
highest_weight = potential_tip_block.header_block.challenge.total_weight
tip_block = potential_tip_block
tip_height = potential_tip_block.height
if highest_weight <= max(
[t.weight for t in self.blockchain.get_current_tips()]
):
self.log.info("Not performing sync, already caught up.")
return
assert tip_block
self.log.info(
f"Tip block {tip_block.header_hash} tip height {tip_block.height}"
)
for height in range(0, tip_block.height + 1):
self.store.set_potential_headers_received(uint32(height), Event())
self.store.set_potential_blocks_received(uint32(height), Event())
self.store.set_potential_hashes_received(Event())
timeout = 200
sleep_interval = 10
total_time_slept = 0
while True:
if total_time_slept > timeout:
raise TimeoutError("Took too long to fetch header hashes.")
if self._shut_down:
return
# Download all the header hashes and find the fork point
request = peer_protocol.RequestAllHeaderHashes(tip_block.header_hash)
yield OutboundMessage(
NodeType.FULL_NODE,
Message("request_all_header_hashes", request),
Delivery.RANDOM,
)
try:
phr = self.store.get_potential_hashes_received()
assert phr is not None
await asyncio.wait_for(
phr.wait(), timeout=sleep_interval,
)
break
except concurrent.futures.TimeoutError:
total_time_slept += sleep_interval
self.log.warning("Did not receive desired header hashes")
# Finding the fork point allows us to only download headers and blocks from the fork point
header_hashes = self.store.get_potential_hashes()
fork_point_height: uint32 = self.blockchain.find_fork_point(header_hashes)
fork_point_hash: bytes32 = header_hashes[fork_point_height]
self.log.info(f"Fork point: {fork_point_hash} at height {fork_point_height}")
# Now, we download all of the headers in order to verify the weight, in batches
headers: List[HeaderBlock] = []
# Download headers in batches. We download a few batches ahead in case there are delays or peers
# that don't have the headers that we need.
last_request_time: float = 0
highest_height_requested: uint32 = uint32(0)
request_made: bool = False
for height_checkpoint in range(
fork_point_height + 1, tip_height + 1, self.config["max_headers_to_send"]
):
end_height = min(
height_checkpoint + self.config["max_headers_to_send"], tip_height + 1
)
total_time_slept = 0
while True:
if self._shut_down:
return
if total_time_slept > timeout:
raise TimeoutError("Took too long to fetch blocks")
# Request batches that we don't have yet
for batch in range(0, self.config["num_sync_batches"]):
batch_start = (
height_checkpoint + batch * self.config["max_headers_to_send"]
)
batch_end = min(
batch_start + self.config["max_headers_to_send"], tip_height + 1
)
if batch_start > tip_height:
# We have asked for all blocks
break
blocks_missing = any(
[
not (
self.store.get_potential_headers_received(uint32(h))
).is_set()
for h in range(batch_start, batch_end)
]
)
if (
time.time() - last_request_time > sleep_interval
and blocks_missing
) or (batch_end - 1) > highest_height_requested:
# If we are missing header blocks in this batch, and we haven't made a request in a while,
# Make a request for this batch. Also, if we have never requested this batch, make
# the request
if batch_end - 1 > highest_height_requested:
highest_height_requested = batch_end - 1
request_made = True
request_hb = peer_protocol.RequestHeaderBlocks(
tip_block.header_block.header.get_hash(),
[uint32(h) for h in range(batch_start, batch_end)],
)
self.log.info(
f"Requesting header blocks {batch_start, batch_end}."
)
yield OutboundMessage(
NodeType.FULL_NODE,
Message("request_header_blocks", request_hb),
Delivery.RANDOM,
)
if request_made:
# Reset the timer for requests, so we don't overload other peers with requests
last_request_time = time.time()
request_made = False
# Wait for the first batch (the next "max_blocks_to_send" blocks to arrive)
awaitables = [
(self.store.get_potential_headers_received(uint32(height))).wait()
for height in range(height_checkpoint, end_height)
]
future = asyncio.gather(*awaitables, return_exceptions=True)
try:
await asyncio.wait_for(future, timeout=sleep_interval)
break
except concurrent.futures.TimeoutError:
try:
await future
except asyncio.CancelledError:
pass
total_time_slept += sleep_interval
self.log.info(f"Did not receive desired header blocks")
for h in range(fork_point_height + 1, tip_height + 1):
header = self.store.get_potential_header(uint32(h))
assert header is not None
headers.append(header)
self.log.info(f"Downloaded headers up to tip height: {tip_height}")
if not verify_weight(
tip_block.header_block, headers, self.blockchain.headers[fork_point_hash],
):
raise errors.InvalidWeight(
f"Weight of {tip_block.header_block.header.get_hash()} not valid."
)
self.log.info(
f"Validated weight of headers. Downloaded {len(headers)} headers, tip height {tip_height}"
)
assert tip_height == fork_point_height + len(headers)
self.store.clear_potential_headers()
headers.clear()
# Download blocks in batches, and verify them as they come in. We download a few batches ahead,
# in case there are delays.
last_request_time = 0
highest_height_requested = uint32(0)
request_made = False
for height_checkpoint in range(
fork_point_height + 1, tip_height + 1, self.config["max_blocks_to_send"]
):
end_height = min(
height_checkpoint + self.config["max_blocks_to_send"], tip_height + 1
)
total_time_slept = 0
while True:
if self._shut_down:
return
if total_time_slept > timeout:
raise TimeoutError("Took too long to fetch blocks")
# Request batches that we don't have yet
for batch in range(0, self.config["num_sync_batches"]):
batch_start = (
height_checkpoint + batch * self.config["max_blocks_to_send"]
)
batch_end = min(
batch_start + self.config["max_blocks_to_send"], tip_height + 1
)
if batch_start > tip_height:
# We have asked for all blocks
break
blocks_missing = any(
[
not (
self.store.get_potential_blocks_received(uint32(h))
).is_set()
for h in range(batch_start, batch_end)
]
)
if (
time.time() - last_request_time > sleep_interval
and blocks_missing
) or (batch_end - 1) > highest_height_requested:
# If we are missing blocks in this batch, and we haven't made a request in a while,
# Make a request for this batch. Also, if we have never requested this batch, make
# the request
self.log.info(
f"Requesting sync blocks {[i for i in range(batch_start, batch_end)]}"
)
if batch_end - 1 > highest_height_requested:
highest_height_requested = batch_end - 1
request_made = True
request_sync = peer_protocol.RequestSyncBlocks(
tip_block.header_block.header.header_hash,
[
uint32(height)
for height in range(batch_start, batch_end)
],
)
yield OutboundMessage(
NodeType.FULL_NODE,
Message("request_sync_blocks", request_sync),
Delivery.RANDOM,
)
if request_made:
# Reset the timer for requests, so we don't overload other peers with requests
last_request_time = time.time()
request_made = False
# Wait for the first batch (the next "max_blocks_to_send" blocks to arrive)
awaitables = [
(self.store.get_potential_blocks_received(uint32(height))).wait()
for height in range(height_checkpoint, end_height)
]
future = asyncio.gather(*awaitables, return_exceptions=True)
try:
await asyncio.wait_for(future, timeout=sleep_interval)
break
except concurrent.futures.TimeoutError:
try:
await future
except asyncio.CancelledError:
pass
total_time_slept += sleep_interval
self.log.info("Did not receive desired blocks")
# Verifies this batch, which we are guaranteed to have (since we broke from the above loop)
blocks = []
for height in range(height_checkpoint, end_height):
b: Optional[FullBlock] = await self.store.get_potential_block(
uint32(height)
)
assert b is not None
blocks.append(b)
validation_start_time = time.time()
prevalidate_results = await self.blockchain.pre_validate_blocks(blocks)
index = 0
for height in range(height_checkpoint, end_height):
if self._shut_down:
return
block: Optional[FullBlock] = await self.store.get_potential_block(
uint32(height)
)
assert block is not None
prev_block: Optional[FullBlock] = await self.store.get_potential_block(
uint32(height - 1)
)
if prev_block is None:
prev_block = await self.store.get_block(block.prev_header_hash)
assert prev_block is not None
# The block gets permanantly added to the blockchain
validated, pos = prevalidate_results[index]
index += 1
async with self.store.lock:
result, header_block = await self.blockchain.receive_block(
block, prev_block.header_block, validated, pos
)
if (
result == ReceiveBlockResult.INVALID_BLOCK
or result == ReceiveBlockResult.DISCONNECTED_BLOCK
):
raise RuntimeError(f"Invalid block {block.header_hash}")
# Always immediately add the block to the database, after updating blockchain state
await self.store.add_block(block)
assert (
max([h.height for h in self.blockchain.get_current_tips()])
>= height
)
self.store.set_proof_of_time_estimate_ips(
self.blockchain.get_next_ips(block.header_block)
)
self.log.info(
f"Took {time.time() - validation_start_time} seconds to validate and add blocks "
f"{height_checkpoint} to {end_height}."
)
assert max([h.height for h in self.blockchain.get_current_tips()]) == tip_height
self.log.info(
f"Finished sync up to height {tip_height}. Total time: "
f"{round((time.time() - sync_start_time)/60, 2)} minutes."
)
async def _finish_sync(self) -> OutboundMessageGenerator:
"""
Finalize sync by setting sync mode to False, clearing all sync information, and adding any final
blocks that we have finalized recently.
"""
potential_fut_blocks = (self.store.get_potential_future_blocks()).copy()
self.store.set_sync_mode(False)
async with self.store.lock:
await self.store.clear_sync_info()
for block in potential_fut_blocks:
if self._shut_down:
return
async for msg in self.block(peer_protocol.Block(block)):
yield msg
# Update farmers and timelord with most recent information
async for msg in self._send_challenges_to_timelords():
yield msg
async for msg in self._send_tips_to_farmers():
yield msg
@api_request
async def request_all_header_hashes(
self, request: peer_protocol.RequestAllHeaderHashes
) -> OutboundMessageGenerator:
try:
header_hashes = self.blockchain.get_header_hashes(request.tip_header_hash)
message = Message(
"all_header_hashes", peer_protocol.AllHeaderHashes(header_hashes)
)
yield OutboundMessage(NodeType.FULL_NODE, message, Delivery.RESPOND)
except ValueError:
self.log.info("Do not have requested header hashes.")
@api_request
async def all_header_hashes(
self, all_header_hashes: peer_protocol.AllHeaderHashes
) -> OutboundMessageGenerator:
assert len(all_header_hashes.header_hashes) > 0
self.store.set_potential_hashes(all_header_hashes.header_hashes)
phr = self.store.get_potential_hashes_received()
assert phr is not None
phr.set()
for _ in []: # Yields nothing
yield _
@api_request
async def request_header_blocks(
self, request: peer_protocol.RequestHeaderBlocks
) -> OutboundMessageGenerator:
"""
A peer requests a list of header blocks, by height. Used for syncing or light clients.
"""
start = time.time()
if len(request.heights) > self.config["max_headers_to_send"]:
raise errors.TooManyheadersRequested(
f"The max number of headers is {self.config['max_headers_to_send']},\
but requested {len(request.heights)}"
)
try:
header_hashes: List[
HeaderBlock
] = self.blockchain.get_header_hashes_by_height(
request.heights, request.tip_header_hash
)
header_blocks: List[
HeaderBlock
] = await self.store.get_header_blocks_by_hash(header_hashes)
self.log.info(f"Got header blocks by height {time.time() - start}")
except KeyError:
return
except BlockNotInBlockchain as e:
self.log.info(f"{e}")
return
response = peer_protocol.HeaderBlocks(request.tip_header_hash, header_blocks)
yield OutboundMessage(
NodeType.FULL_NODE, Message("header_blocks", response), Delivery.RESPOND
)
@api_request
async def header_blocks(
self, request: peer_protocol.HeaderBlocks
) -> OutboundMessageGenerator:
"""
Receive header blocks from a peer.
"""
self.log.info(
f"Received header blocks {request.header_blocks[0].height, request.header_blocks[-1].height}."
)
for header_block in request.header_blocks:
self.store.add_potential_header(header_block)
(self.store.get_potential_headers_received(header_block.height)).set()
for _ in []: # Yields nothing
yield _
@api_request
async def request_sync_blocks(
self, request: peer_protocol.RequestSyncBlocks
) -> OutboundMessageGenerator:
"""
Responsd to a peers request for syncing blocks.
"""
blocks: List[FullBlock] = []
async with self.store.lock:
tip_block: Optional[FullBlock] = await self.store.get_block(
request.tip_header_hash
)
if tip_block is not None:
if len(request.heights) > self.config["max_blocks_to_send"]:
raise errors.TooManyheadersRequested(
f"The max number of blocks is "
f"{self.config['max_blocks_to_send']},"
f"but requested {len(request.heights)}"
)
try:
header_hashes: List[
HeaderBlock
] = self.blockchain.get_header_hashes_by_height(
request.heights, request.tip_header_hash
)
header_blocks: List[
HeaderBlock
] = await self.store.get_header_blocks_by_hash(header_hashes)
for header_block in header_blocks:
fetched = await self.store.get_block(header_block.header.get_hash())
assert fetched
blocks.append(fetched)
except KeyError:
self.log.info("Do not have required blocks")
return
except BlockNotInBlockchain as e:
self.log.info(f"{e}")
return
else:
# We don't have the blocks that the client is looking for
self.log.info(
f"Peer requested tip {request.tip_header_hash} that we don't have"
)
return
response = Message(
"sync_blocks", peer_protocol.SyncBlocks(request.tip_header_hash, blocks)
)
yield OutboundMessage(NodeType.FULL_NODE, response, Delivery.RESPOND)
@api_request
async def sync_blocks(
self, request: peer_protocol.SyncBlocks
) -> OutboundMessageGenerator:
"""
We have received the blocks that we needed for syncing. Add them to processing queue.
"""
self.log.info(f"Received sync blocks {[b.height for b in request.blocks]}")
if not self.store.get_sync_mode():
self.log.warning("Receiving sync blocks when we are not in sync mode.")
return
for block in request.blocks:
await self.store.add_potential_block(block)
if (
not self.store.get_sync_mode()
): # We might have left sync mode after the previous await
return
(self.store.get_potential_blocks_received(block.height)).set()
for _ in []: # Yields nothing
yield _
@api_request
async def request_header_hash(
self, request: farmer_protocol.RequestHeaderHash
) -> OutboundMessageGenerator:
"""
Creates a block body and header, with the proof of space, coinbase, and fee targets provided
by the farmer, and sends the hash of the header data back to the farmer.
"""
plot_seed: bytes32 = request.proof_of_space.get_plot_seed()
# Checks that the proof of space is valid
quality_string: bytes = Verifier().validate_proof(
plot_seed,
request.proof_of_space.size,
request.challenge_hash,
bytes(request.proof_of_space.proof),
)
assert quality_string
# Retrieves the correct tip for the challenge
tips: List[SmallHeaderBlock] = self.blockchain.get_current_tips()
target_tip: Optional[SmallHeaderBlock] = None
for tip in tips:
assert tip.challenge
if tip.challenge.get_hash() == request.challenge_hash:
target_tip = tip
if target_tip is None:
# TODO: should we still allow the farmer to farm?
self.log.warning(
f"Challenge hash: {request.challenge_hash} not in one of three tips"
)
return
assert target_tip is not None
# Grab best transactions from Mempool for given tip target
spend_bundle: Optional[SpendBundle] = await self.mempool.create_bundle_for_tip(
target_tip
)
spend_bundle_fees = 0
aggregate_sig: Optional[BLSSignature] = None
solution_program: Optional[Program] = None
if spend_bundle:
solution_program = best_solution_program(spend_bundle)
spend_bundle_fees = spend_bundle.fees()
aggregate_sig = spend_bundle.aggregated_signature
transactions_generator: bytes32 = sha256(b"").digest()
full_coinbase_reward = calculate_block_reward(target_tip.height)
base_fee_reward = full_coinbase_reward / 8
full_fee_reward = uint64(int(base_fee_reward + spend_bundle_fees))
# Create fees coin
fee_hash = std_hash(std_hash(target_tip.height))
fees_coin = Coin(fee_hash, request.fees_target_puzzle_hash, full_fee_reward)
# SpendBundle has all signatures already aggregated
# TODO: calculate cost of all transactions
cost = uint64(0)
# Creates a block with transactions, coinbase, and fees
body: Body = Body(
request.coinbase,
request.coinbase_signature,
fees_coin,
solution_program,
aggregate_sig,
transactions_generator,
cost,
)
# Creates the block header
prev_header_hash: bytes32 = target_tip.header.get_hash()
timestamp: uint64 = uint64(int(time.time()))
# TODO: use a real BIP158 filter based on transactions
filter_hash: bytes32 = token_bytes(32)
proof_of_space_hash: bytes32 = request.proof_of_space.get_hash()
body_hash: Body = body.get_hash()
extension_data: bytes32 = bytes32([0] * 32)
block_header_data: HeaderData = HeaderData(
uint32(target_tip.height + 1),
prev_header_hash,
timestamp,
filter_hash,
proof_of_space_hash,
body_hash,
extension_data,
)
block_header_data_hash: bytes32 = block_header_data.get_hash()
# self.stores this block so we can submit it to the blockchain after it's signed by harvester
self.store.add_candidate_block(
proof_of_space_hash,
body,
block_header_data,
request.proof_of_space,
target_tip.height + 1,
)
message = farmer_protocol.HeaderHash(
proof_of_space_hash, block_header_data_hash
)
yield OutboundMessage(
NodeType.FARMER, Message("header_hash", message), Delivery.RESPOND
)
@api_request
async def header_signature(
self, header_signature: farmer_protocol.HeaderSignature
) -> OutboundMessageGenerator:
"""
Signature of header hash, by the harvester. This is enough to create an unfinished
block, which only needs a Proof of Time to be finished. If the signature is valid,
we call the unfinished_block routine.
"""
candidate: Optional[
Tuple[Body, HeaderData, ProofOfSpace]
] = self.store.get_candidate_block(header_signature.pos_hash)
if candidate is None:
self.log.warning(
f"PoS hash {header_signature.pos_hash} not found in database"
)
return
# Verifies that we have the correct header and body self.stored
block_body, block_header_data, pos = candidate
assert block_header_data.get_hash() == header_signature.header_hash
block_header: Header = Header(
block_header_data, header_signature.header_signature
)
header: HeaderBlock = HeaderBlock(pos, None, None, block_header)
unfinished_block_obj: FullBlock = FullBlock(header, block_body)
# Propagate to ourselves (which validates and does further propagations)
request = peer_protocol.UnfinishedBlock(unfinished_block_obj)
async for m in self.unfinished_block(request):
# Yield all new messages (propagation to peers)
yield m
# TIMELORD PROTOCOL
@api_request
async def proof_of_time_finished(
self, request: timelord_protocol.ProofOfTimeFinished
) -> OutboundMessageGenerator:
"""
A proof of time, received by a peer timelord. We can use this to complete a block,
and call the block routine (which handles propagation and verification of blocks).
"""
dict_key = (
request.proof.challenge_hash,
request.proof.number_of_iterations,
)
unfinished_block_obj: Optional[FullBlock] = self.store.get_unfinished_block(
dict_key
)
if not unfinished_block_obj:
self.log.warning(
f"Received a proof of time that we cannot use to complete a block {dict_key}"
)
return
prev_full_block = await self.store.get_block(
unfinished_block_obj.prev_header_hash
)
assert prev_full_block
prev_block: HeaderBlock = prev_full_block.header_block
difficulty: uint64 = self.blockchain.get_next_difficulty(
unfinished_block_obj.prev_header_hash
)
assert prev_block.challenge
challenge: Challenge = Challenge(
request.proof.challenge_hash,
unfinished_block_obj.header_block.proof_of_space.get_hash(),
request.proof.output.get_hash(),
uint64(prev_block.challenge.total_weight + difficulty),
uint64(
prev_block.challenge.total_iters + request.proof.number_of_iterations
),
)
new_header_block = HeaderBlock(
unfinished_block_obj.header_block.proof_of_space,
request.proof,
challenge,
unfinished_block_obj.header_block.header,
)
new_full_block: FullBlock = FullBlock(
new_header_block, unfinished_block_obj.body
)
if self.store.get_sync_mode():
self.store.add_potential_future_block(new_full_block)
else:
async for msg in self.block(peer_protocol.Block(new_full_block)):
yield msg
# PEER PROTOCOL
@api_request
async def new_proof_of_time(
self, new_proof_of_time: peer_protocol.NewProofOfTime
) -> OutboundMessageGenerator:
"""
A proof of time, received by a peer full node. If we have the rest of the block,
we can complete it. Otherwise, we just verify and propagate the proof.
"""
finish_block: bool = False
propagate_proof: bool = False
if self.store.get_unfinished_block(
(
new_proof_of_time.proof.challenge_hash,
new_proof_of_time.proof.number_of_iterations,
)
):
finish_block = True
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)
async for msg in self.proof_of_time_finished(request):
yield msg
if propagate_proof:
yield OutboundMessage(
NodeType.FULL_NODE,
Message("new_proof_of_time", new_proof_of_time),
Delivery.BROADCAST_TO_OTHERS,
)
@api_request
async def unfinished_block(
self, unfinished_block: peer_protocol.UnfinishedBlock
) -> OutboundMessageGenerator:
"""
We have received an unfinished block, either created by us, or from another peer.
We can validate it and if it's a good block, propagate it to other peers and
timelords.
"""
# Adds the unfinished block to seen, and check if it's seen before
if self.store.seen_unfinished_block(unfinished_block.block.header_hash):
return
if not self.blockchain.is_child_of_head(unfinished_block.block):
return
if not await self.blockchain.validate_unfinished_block(unfinished_block.block):
raise InvalidUnfinishedBlock()
prev_full_block: Optional[FullBlock] = await self.store.get_block(
unfinished_block.block.prev_header_hash
)
assert prev_full_block
prev_block: HeaderBlock = prev_full_block.header_block
assert prev_block.challenge
challenge_hash: bytes32 = prev_block.challenge.get_hash()
difficulty: uint64 = self.blockchain.get_next_difficulty(
unfinished_block.block.header_block.prev_header_hash
)
vdf_ips: uint64 = self.blockchain.get_next_ips(prev_block)
iterations_needed: uint64 = calculate_iterations(
unfinished_block.block.header_block.proof_of_space,
difficulty,
vdf_ips,
constants["MIN_BLOCK_TIME"],
)
if (
self.store.get_unfinished_block((challenge_hash, iterations_needed))
is not None
):
return
expected_time: uint64 = uint64(
int(iterations_needed / (self.store.get_proof_of_time_estimate_ips()))
)
if expected_time > constants["PROPAGATION_DELAY_THRESHOLD"]:
self.log.info(f"Block is slow, expected {expected_time} seconds, waiting")
# If this block is slow, sleep to allow faster blocks to come out first
await asyncio.sleep(5)
leader: Tuple[uint32, uint64] = self.store.get_unfinished_block_leader()
if leader is None or unfinished_block.block.height > leader[0]:
self.log.info(
f"This is the first unfinished block at height {unfinished_block.block.height}, so propagate."
)
# If this is the first block we see at this height, propagate
self.store.set_unfinished_block_leader(
(unfinished_block.block.height, expected_time)
)
elif unfinished_block.block.height == leader[0]:
if expected_time > leader[1] + constants["PROPAGATION_THRESHOLD"]:
# If VDF is expected to finish X seconds later than the best, don't propagate
self.log.info(
f"VDF will finish too late {expected_time} seconds, so don't propagate"
)
return
elif expected_time < leader[1]:
self.log.info(
f"New best unfinished block at height {unfinished_block.block.height}"
)
# If this will be the first block to finalize, update our leader
self.store.set_unfinished_block_leader((leader[0], expected_time))
else:
# If we have seen an unfinished block at a greater or equal height, don't propagate
self.log.info(f"Unfinished block at old height, so don't propagate")
return
self.store.add_unfinished_block(
(challenge_hash, iterations_needed), unfinished_block.block
)
timelord_request = timelord_protocol.ProofOfSpaceInfo(
challenge_hash, iterations_needed
)
yield OutboundMessage(
NodeType.TIMELORD,
Message("proof_of_space_info", timelord_request),
Delivery.BROADCAST,
)
yield OutboundMessage(
NodeType.FULL_NODE,
Message("unfinished_block", unfinished_block),
Delivery.BROADCAST_TO_OTHERS,
)
# Receives a full transaction
# If we added it to mempool send tx id to others
# TODO if it's not added ?
@api_request
async def transaction(
self, tx: peer_protocol.Transaction
) -> OutboundMessageGenerator:
added, error = await self.mempool.add_spendbundle(tx.sb)
if added:
maybeTX = peer_protocol.TransactionId(tx.sb.name())
yield OutboundMessage(
NodeType.FULL_NODE,
Message("maybe_transaction", maybeTX),
Delivery.BROADCAST_TO_OTHERS,
)
else:
self.log.warning(
f"Wasn't able to add transaction with id {tx.sb.name()}, error: {error}"
)
return
# Receives a transaction_id,
# Ignore if we've seen it already
# Request full transaction if we haven't seen it previously
@api_request
async def maybe_transaction(
self, tx_id: peer_protocol.TransactionId
) -> OutboundMessageGenerator:
if self.mempool.seen(tx_id.transaction_id):
self.log.info(f"tx_id({tx_id.transaction_id}) already seen")
return
else:
requestTX = peer_protocol.RequestTransaction(tx_id.transaction_id)
yield OutboundMessage(
NodeType.FULL_NODE,
Message("request_transaction", requestTX),
Delivery.RESPOND,
)
# Peer has request a full transaction from us
@api_request
async def request_transaction(
self, tx_id: peer_protocol.RequestTransaction
) -> OutboundMessageGenerator:
spend_bundle = await self.mempool.get_spendbundle(tx_id.transaction_id)
if spend_bundle is None:
return
transaction = peer_protocol.Transaction(spend_bundle)
yield OutboundMessage(
NodeType.FULL_NODE, Message("transaction", transaction), Delivery.RESPOND,
)
self.log.info(f"sending transaction (tx_id: {spend_bundle.name()}) to peer")
@api_request
async def block(self, block: peer_protocol.Block) -> OutboundMessageGenerator:
"""
Receive a full block from a peer full node (or ourselves).
"""
header_hash = block.block.header_block.header.get_hash()
# Adds the block to seen, and check if it's seen before
if self.blockchain.cointains_block(header_hash):
return
if self.store.get_sync_mode():
# Add the block to our potential tips list
self.store.add_potential_tip(block.block)
return
prevalidate_block = await self.blockchain.pre_validate_blocks([block.block])
val, pos = prevalidate_block[0]
async with self.store.lock:
prev_block: Optional[FullBlock] = await self.store.get_block(
block.block.prev_header_hash
)
prevalidate_block = await self.blockchain.pre_validate_blocks([block.block])
val, pos = prevalidate_block[0]
added: ReceiveBlockResult
if prev_block is None:
added, replaced = ReceiveBlockResult.DISCONNECTED_BLOCK, None
else:
# Tries to add the block to the blockchain
added, replaced = await self.blockchain.receive_block(
block.block, prev_block.header_block, val, pos
)
if added == ReceiveBlockResult.ADDED_TO_HEAD:
await self.mempool.new_tips(await self.blockchain.get_full_tips())
if added == ReceiveBlockResult.ALREADY_HAVE_BLOCK:
return
elif added == ReceiveBlockResult.INVALID_BLOCK:
self.log.warning(
f"Block {header_hash} at height {block.block.height} is invalid."
)
return
elif added == ReceiveBlockResult.DISCONNECTED_BLOCK:
self.log.warning(f"Disconnected block {header_hash}")
tip_height = min(
[head.height for head in self.blockchain.get_current_tips()]
)
if (
block.block.height
> tip_height + self.config["sync_blocks_behind_threshold"]
):
async with self.store.lock:
if self.store.get_sync_mode():
return
await self.store.clear_sync_info()
self.store.add_potential_tip(block.block)
self.store.set_sync_mode(True)
self.log.info(
f"We are too far behind this block. Our height is {tip_height} and block is at "
f"{block.block.height}"
)
try:
# Performs sync, and catch exceptions so we don't close the connection
async for ret_msg in self._sync():
yield ret_msg
except asyncio.CancelledError:
self.log.error("Syncing failed, CancelledError")
except BaseException as e:
self.log.error(f"Error {type(e)}{e} with syncing")
finally:
async for ret_msg in self._finish_sync():
yield ret_msg
elif block.block.height >= tip_height - 3:
self.log.info(
f"We have received a disconnected block at height {block.block.height}, current tip is {tip_height}"
)
msg = Message(
"request_block",
peer_protocol.RequestBlock(block.block.prev_header_hash),
)
self.store.add_disconnected_block(block.block)
yield OutboundMessage(NodeType.FULL_NODE, msg, Delivery.RESPOND)
return
elif added == ReceiveBlockResult.ADDED_TO_HEAD:
# Only propagate blocks which extend the blockchain (becomes one of the heads)
self.log.info(
f"Updated heads, new heights: {[b.height for b in self.blockchain.get_current_tips()]}"
)
difficulty = self.blockchain.get_next_difficulty(
block.block.prev_header_hash
)
next_vdf_ips = self.blockchain.get_next_ips(block.block.header_block)
self.log.info(f"Difficulty {difficulty} IPS {next_vdf_ips}")
if next_vdf_ips != self.store.get_proof_of_time_estimate_ips():
self.store.set_proof_of_time_estimate_ips(next_vdf_ips)
rate_update = farmer_protocol.ProofOfTimeRate(next_vdf_ips)
self.log.info(f"Sending proof of time rate {next_vdf_ips}")
yield OutboundMessage(
NodeType.FARMER,
Message("proof_of_time_rate", rate_update),
Delivery.BROADCAST,
)
self.store.clear_seen_unfinished_blocks()
assert block.block.header_block.proof_of_time
assert block.block.header_block.challenge
pos_quality = (
block.block.header_block.proof_of_space.verify_and_get_quality()
)
farmer_request = farmer_protocol.ProofOfSpaceFinalized(
block.block.header_block.challenge.get_hash(),
block.block.height,
block.block.weight,
pos_quality,
difficulty,
)
timelord_request = timelord_protocol.ChallengeStart(
block.block.header_block.challenge.get_hash(),
block.block.header_block.challenge.total_weight,
)
# Tell timelord to stop previous challenge and start with new one
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 farmer about the new block
yield OutboundMessage(
NodeType.FARMER,
Message("proof_of_space_finalized", farmer_request),
Delivery.BROADCAST,
)
elif added == ReceiveBlockResult.ADDED_AS_ORPHAN:
assert block.block.header_block.proof_of_time
assert block.block.header_block.challenge
self.log.info(f"Received orphan block of height {block.block.height}")
else:
# Should never reach here, all the cases are covered
assert False
# Recursively process the next block if we have it
# This code path is reached if added == ADDED_AS_ORPHAN or ADDED_TO_HEAD
next_block: Optional[FullBlock] = self.store.get_disconnected_block_by_prev(
block.block.header_hash
)
if next_block is not None:
async for ret_msg in self.block(peer_protocol.Block(next_block)):
yield ret_msg
# Removes all temporary data for old blocks
lowest_tip = min(tip.height for tip in self.blockchain.get_current_tips())
clear_height = uint32(max(0, lowest_tip - 30))
self.store.clear_candidate_blocks_below(clear_height)
self.store.clear_unfinished_blocks_below(clear_height)
self.store.clear_disconnected_blocks_below(clear_height)
@api_request
async def request_block(
self, request_block: peer_protocol.RequestBlock
) -> OutboundMessageGenerator:
block: Optional[FullBlock] = await self.store.get_block(
request_block.header_hash
)
if block is not None:
yield OutboundMessage(
NodeType.FULL_NODE,
Message("block", peer_protocol.Block(block)),
Delivery.RESPOND,
)
@api_request
async def peers(self, request: peer_protocol.Peers) -> OutboundMessageGenerator:
if self.server is None:
return
conns = self.server.global_connections
for peer in request.peer_list:
conns.peers.add(peer)
# Pseudo-message to close the connection
yield OutboundMessage(NodeType.INTRODUCER, Message("", None), Delivery.CLOSE)
unconnected = conns.get_unconnected_peers(
recent_threshold=self.config["recent_peer_threshold"]
)
to_connect = unconnected[: self._num_needed_peers()]
if not len(to_connect):
return
self.log.info(f"Trying to connect to peers: {to_connect}")
tasks = []
for peer in to_connect:
tasks.append(asyncio.create_task(self.server.start_client(peer)))
await asyncio.gather(*tasks)