362 lines
13 KiB
Python
362 lines
13 KiB
Python
import asyncio
|
|
import logging
|
|
import aiosqlite
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Tuple
|
|
|
|
from src.types.body import Body
|
|
from src.types.full_block import FullBlock
|
|
from src.types.header import HeaderData, Header
|
|
from src.types.header_block import HeaderBlock
|
|
from src.types.proof_of_space import ProofOfSpace
|
|
from src.types.sized_bytes import bytes32
|
|
from src.util.ints import uint32, uint64
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class FullNodeStore:
|
|
db: aiosqlite.Connection
|
|
# Whether or not we are syncing
|
|
sync_mode: bool
|
|
# Whether we are waiting for tips (at the start of sync) or already syncing
|
|
waiting_for_tips: bool
|
|
# Potential new tips that we have received from others.
|
|
potential_tips: Dict[bytes32, FullBlock]
|
|
# List of all header hashes up to the tip, download up front
|
|
potential_hashes: List[bytes32]
|
|
# Header blocks received from other peers during sync
|
|
potential_headers: Dict[uint32, HeaderBlock]
|
|
# Event to signal when header hashes are received
|
|
potential_hashes_received: Optional[asyncio.Event]
|
|
# Event to signal when headers are received at each height
|
|
potential_headers_received: Dict[uint32, asyncio.Event]
|
|
# Event to signal when blocks are received at each height
|
|
potential_blocks_received: Dict[uint32, asyncio.Event]
|
|
# Blocks that we have finalized during sync, queue them up for adding after sync is done
|
|
potential_future_blocks: List[FullBlock]
|
|
# Current estimate of the speed of the network timelords
|
|
proof_of_time_estimate_ips: uint64
|
|
# Proof of time heights
|
|
proof_of_time_heights: Dict[Tuple[bytes32, uint64], uint32]
|
|
# Our best unfinished block
|
|
unfinished_blocks_leader: Tuple[uint32, uint64]
|
|
# Blocks which we have created, but don't have proof of space yet, old ones are cleared
|
|
candidate_blocks: Dict[bytes32, Tuple[Body, HeaderData, ProofOfSpace, uint32]]
|
|
# Blocks which are not finalized yet (no proof of time), old ones are cleared
|
|
unfinished_blocks: Dict[Tuple[bytes32, uint64], FullBlock]
|
|
# Header hashes of unfinished blocks that we have seen recently
|
|
seen_unfinished_blocks: set
|
|
# Blocks which we have received but our blockchain does not reach, old ones are cleared
|
|
disconnected_blocks: Dict[bytes32, FullBlock]
|
|
|
|
# Lock
|
|
lock: asyncio.Lock
|
|
|
|
@classmethod
|
|
async def create(cls, db_path: Path):
|
|
self = cls()
|
|
|
|
# All full blocks which have been added to the blockchain. Header_hash -> block
|
|
self.db = await aiosqlite.connect(db_path)
|
|
await self.db.execute(
|
|
"CREATE TABLE IF NOT EXISTS blocks(height bigint, header_hash text PRIMARY KEY, block blob)"
|
|
)
|
|
|
|
# Blocks received from other peers during sync, cleared after sync
|
|
await self.db.execute(
|
|
"CREATE TABLE IF NOT EXISTS potential_blocks(height bigint PRIMARY KEY, block blob)"
|
|
)
|
|
|
|
# Headers
|
|
await self.db.execute(
|
|
"CREATE TABLE IF NOT EXISTS headers(height bigint, header_hash "
|
|
"text PRIMARY KEY, header blob)"
|
|
)
|
|
|
|
# Height index so we can look up in order of height for sync purposes
|
|
await self.db.execute(
|
|
"CREATE INDEX IF NOT EXISTS block_height on blocks(height)"
|
|
)
|
|
await self.db.execute(
|
|
"CREATE INDEX IF NOT EXISTS header_height on headers(height)"
|
|
)
|
|
|
|
await self.db.commit()
|
|
|
|
self.sync_mode = False
|
|
self.waiting_for_tips = True
|
|
self.potential_tips = {}
|
|
self.potential_hashes = []
|
|
self.potential_headers = {}
|
|
self.potential_hashes_received = None
|
|
self.potential_headers_received = {}
|
|
self.potential_blocks_received = {}
|
|
self.potential_future_blocks = []
|
|
self.proof_of_time_estimate_ips = uint64(10000)
|
|
self.proof_of_time_heights = {}
|
|
self.unfinished_blocks_leader = (
|
|
uint32(0),
|
|
uint64((1 << 64) - 1),
|
|
)
|
|
self.candidate_blocks = {}
|
|
self.unfinished_blocks = {}
|
|
self.seen_unfinished_blocks = set()
|
|
self.disconnected_blocks = {}
|
|
self.lock = asyncio.Lock() # external
|
|
return self
|
|
|
|
async def close(self):
|
|
await self.db.close()
|
|
|
|
async def _clear_database(self):
|
|
await self.db.execute("DELETE FROM blocks")
|
|
await self.db.execute("DELETE FROM potential_blocks")
|
|
await self.db.execute("DELETE FROM headers")
|
|
await self.db.commit()
|
|
|
|
async def add_block(self, block: FullBlock) -> None:
|
|
cursor_1 = await self.db.execute(
|
|
"INSERT OR REPLACE INTO blocks VALUES(?, ?, ?)",
|
|
(block.height, block.header_hash.hex(), bytes(block)),
|
|
)
|
|
await cursor_1.close()
|
|
# assert block.challenge is not None
|
|
cursor_2 = await self.db.execute(
|
|
("INSERT OR REPLACE INTO headers VALUES(?, ?, ?)"),
|
|
(block.height, block.header_hash.hex(), bytes(block.header),),
|
|
)
|
|
await cursor_2.close()
|
|
await self.db.commit()
|
|
|
|
async def get_block(self, header_hash: bytes32) -> Optional[FullBlock]:
|
|
cursor = await self.db.execute(
|
|
"SELECT * from blocks WHERE header_hash=?", (header_hash.hex(),)
|
|
)
|
|
row = await cursor.fetchone()
|
|
await cursor.close()
|
|
if row is not None:
|
|
return FullBlock.from_bytes(row[2])
|
|
return None
|
|
|
|
async def get_blocks_at(self, heights: List[uint32]) -> List[FullBlock]:
|
|
if len(heights) == 0:
|
|
return []
|
|
|
|
heights_db = tuple(heights)
|
|
formatted_str = (
|
|
f'SELECT * from blocks WHERE height in ({"?," * (len(heights_db) - 1)}?)'
|
|
)
|
|
cursor = await self.db.execute(formatted_str, heights_db)
|
|
rows = await cursor.fetchall()
|
|
await cursor.close()
|
|
blocks: List[FullBlock] = []
|
|
for row in rows:
|
|
blocks.append(FullBlock.from_bytes(row[2]))
|
|
return blocks
|
|
|
|
async def get_headers(self) -> List[Header]:
|
|
cursor = await self.db.execute("SELECT * from headers")
|
|
rows = await cursor.fetchall()
|
|
await cursor.close()
|
|
return [Header.from_bytes(row[2]) for row in rows]
|
|
|
|
async def add_potential_block(self, block: FullBlock) -> None:
|
|
cursor = await self.db.execute(
|
|
"INSERT OR REPLACE INTO potential_blocks VALUES(?, ?)",
|
|
(block.height, bytes(block)),
|
|
)
|
|
await cursor.close()
|
|
await self.db.commit()
|
|
|
|
async def get_potential_block(self, height: uint32) -> Optional[FullBlock]:
|
|
cursor = await self.db.execute(
|
|
"SELECT * from potential_blocks WHERE height=?", (height,)
|
|
)
|
|
row = await cursor.fetchone()
|
|
await cursor.close()
|
|
if row is not None:
|
|
return FullBlock.from_bytes(row[1])
|
|
return None
|
|
|
|
def add_disconnected_block(self, block: FullBlock) -> None:
|
|
self.disconnected_blocks[block.header_hash] = block
|
|
|
|
def get_disconnected_block_by_prev(
|
|
self, prev_header_hash: bytes32
|
|
) -> Optional[FullBlock]:
|
|
for _, block in self.disconnected_blocks.items():
|
|
if block.prev_header_hash == prev_header_hash:
|
|
return block
|
|
return None
|
|
|
|
def get_disconnected_block(self, header_hash: bytes32) -> Optional[FullBlock]:
|
|
return self.disconnected_blocks.get(header_hash, None)
|
|
|
|
def clear_disconnected_blocks_below(self, height: uint32) -> None:
|
|
for key in list(self.disconnected_blocks.keys()):
|
|
if self.disconnected_blocks[key].height < height:
|
|
del self.disconnected_blocks[key]
|
|
|
|
def set_sync_mode(self, sync_mode: bool) -> None:
|
|
self.sync_mode = sync_mode
|
|
|
|
def get_sync_mode(self) -> bool:
|
|
return self.sync_mode
|
|
|
|
def set_waiting_for_tips(self, waiting_for_tips: bool) -> None:
|
|
self.waiting_for_tips = waiting_for_tips
|
|
|
|
def get_waiting_for_tips(self) -> bool:
|
|
return self.waiting_for_tips
|
|
|
|
async def clear_sync_info(self):
|
|
self.potential_tips.clear()
|
|
self.potential_headers.clear()
|
|
cursor = await self.db.execute("DELETE FROM potential_blocks")
|
|
await cursor.close()
|
|
self.potential_blocks_received.clear()
|
|
self.potential_future_blocks.clear()
|
|
self.waiting_for_tips = True
|
|
|
|
def get_potential_tips_tuples(self) -> List[Tuple[bytes32, FullBlock]]:
|
|
return list(self.potential_tips.items())
|
|
|
|
def add_potential_tip(self, block: FullBlock) -> None:
|
|
self.potential_tips[block.header_hash] = block
|
|
|
|
def get_potential_tip(self, header_hash: bytes32) -> Optional[FullBlock]:
|
|
return self.potential_tips.get(header_hash, None)
|
|
|
|
def add_potential_header(self, block: HeaderBlock) -> None:
|
|
self.potential_headers[block.height] = block
|
|
|
|
def get_potential_header(self, height: uint32) -> Optional[HeaderBlock]:
|
|
return self.potential_headers.get(height, None)
|
|
|
|
def clear_potential_headers(self) -> None:
|
|
self.potential_headers.clear()
|
|
|
|
def set_potential_hashes(self, potential_hashes: List[bytes32]) -> None:
|
|
self.potential_hashes = potential_hashes
|
|
|
|
def get_potential_hashes(self) -> List[bytes32]:
|
|
return self.potential_hashes
|
|
|
|
def set_potential_hashes_received(self, event: asyncio.Event):
|
|
self.potential_hashes_received = event
|
|
|
|
def get_potential_hashes_received(self) -> Optional[asyncio.Event]:
|
|
return self.potential_hashes_received
|
|
|
|
def set_potential_headers_received(self, height: uint32, event: asyncio.Event):
|
|
self.potential_headers_received[height] = event
|
|
|
|
def get_potential_headers_received(self, height: uint32) -> asyncio.Event:
|
|
return self.potential_headers_received[height]
|
|
|
|
def set_potential_blocks_received(self, height: uint32, event: asyncio.Event):
|
|
self.potential_blocks_received[height] = event
|
|
|
|
def get_potential_blocks_received(self, height: uint32) -> asyncio.Event:
|
|
return self.potential_blocks_received[height]
|
|
|
|
def add_potential_future_block(self, block: FullBlock):
|
|
self.potential_future_blocks.append(block)
|
|
|
|
def get_potential_future_blocks(self):
|
|
return self.potential_future_blocks
|
|
|
|
def add_candidate_block(
|
|
self,
|
|
pos_hash: bytes32,
|
|
body: Body,
|
|
header: HeaderData,
|
|
pos: ProofOfSpace,
|
|
height: uint32 = uint32(0),
|
|
):
|
|
self.candidate_blocks[pos_hash] = (body, header, pos, height)
|
|
|
|
def get_candidate_block(
|
|
self, pos_hash: bytes32
|
|
) -> Optional[Tuple[Body, HeaderData, ProofOfSpace]]:
|
|
res = self.candidate_blocks.get(pos_hash, None)
|
|
if res is None:
|
|
return None
|
|
return (res[0], res[1], res[2])
|
|
|
|
def clear_candidate_blocks_below(self, height: uint32) -> None:
|
|
del_keys = []
|
|
for key, value in self.candidate_blocks.items():
|
|
if value[3] < height:
|
|
del_keys.append(key)
|
|
for key in del_keys:
|
|
try:
|
|
del self.candidate_blocks[key]
|
|
except KeyError:
|
|
pass
|
|
|
|
def add_unfinished_block(
|
|
self, key: Tuple[bytes32, uint64], block: FullBlock
|
|
) -> None:
|
|
self.unfinished_blocks[key] = block
|
|
|
|
def get_unfinished_block(self, key: Tuple[bytes32, uint64]) -> Optional[FullBlock]:
|
|
return self.unfinished_blocks.get(key, None)
|
|
|
|
def seen_unfinished_block(self, header_hash: bytes32) -> bool:
|
|
if header_hash in self.seen_unfinished_blocks:
|
|
return True
|
|
self.seen_unfinished_blocks.add(header_hash)
|
|
return False
|
|
|
|
def clear_seen_unfinished_blocks(self) -> None:
|
|
self.seen_unfinished_blocks.clear()
|
|
|
|
def get_unfinished_blocks(self) -> Dict[Tuple[bytes32, uint64], FullBlock]:
|
|
return self.unfinished_blocks.copy()
|
|
|
|
def clear_unfinished_blocks_below(self, height: uint32) -> None:
|
|
del_keys = []
|
|
for key, unf in self.unfinished_blocks.items():
|
|
if unf.height < height:
|
|
del_keys.append(key)
|
|
for key in del_keys:
|
|
try:
|
|
del self.unfinished_blocks[key]
|
|
except KeyError:
|
|
pass
|
|
|
|
def set_unfinished_block_leader(self, key: Tuple[bytes32, uint64]) -> None:
|
|
self.unfinished_blocks_leader = key
|
|
|
|
def get_unfinished_block_leader(self) -> Tuple[bytes32, uint64]:
|
|
return self.unfinished_blocks_leader
|
|
|
|
def set_proof_of_time_estimate_ips(self, estimate: uint64):
|
|
self.proof_of_time_estimate_ips = estimate
|
|
|
|
def get_proof_of_time_estimate_ips(self) -> uint64:
|
|
return self.proof_of_time_estimate_ips
|
|
|
|
def add_proof_of_time_heights(
|
|
self, challenge_iters: Tuple[bytes32, uint64], height: uint32
|
|
) -> None:
|
|
self.proof_of_time_heights[challenge_iters] = height
|
|
|
|
def get_proof_of_time_heights(
|
|
self, challenge_iters: Tuple[bytes32, uint64]
|
|
) -> Optional[uint32]:
|
|
return self.proof_of_time_heights.get(challenge_iters, None)
|
|
|
|
def clear_proof_of_time_heights_below(self, height: uint32) -> None:
|
|
del_keys: List = []
|
|
for key, value in self.proof_of_time_heights.items():
|
|
if value < height:
|
|
del_keys.append(key)
|
|
for key in del_keys:
|
|
try:
|
|
del self.proof_of_time_heights[key]
|
|
except KeyError:
|
|
pass
|