chia-blockchain/chia/data_layer/data_layer.py

353 lines
16 KiB
Python
Raw Normal View History

import logging
2021-09-17 17:30:14 +03:00
from pathlib import Path
2022-02-17 19:31:01 +03:00
from typing import Any, Callable, Dict, List, Optional, Tuple, Awaitable, Set
import aiosqlite
2022-05-02 12:52:03 +03:00
import time
2022-02-02 23:06:37 +03:00
import traceback
import asyncio
2022-02-17 17:39:44 +03:00
import aiohttp
2022-05-04 20:23:29 +03:00
from chia.data_layer.data_layer_types import InternalNode, TerminalNode, Subscription, DiffData
from chia.data_layer.data_store import DataStore
2022-01-20 15:44:20 +03:00
from chia.rpc.wallet_rpc_client import WalletRpcClient
2021-09-29 02:37:50 +03:00
from chia.server.server import ChiaServer
2021-10-13 21:18:00 +03:00
from chia.types.blockchain_format.sized_bytes import bytes32
2021-11-10 17:12:11 +03:00
from chia.util.config import load_config
2021-09-17 17:30:14 +03:00
from chia.util.db_wrapper import DBWrapper
2022-04-04 13:45:38 +03:00
from chia.util.ints import uint16, uint32, uint64
from chia.util.path import mkdir, path_from_root
from chia.wallet.transaction_record import TransactionRecord
from chia.data_layer.data_layer_wallet import SingletonRecord
2022-05-11 21:59:11 +03:00
from chia.data_layer.download_data import insert_from_delta_file, write_files_for_root
2022-02-08 18:29:29 +03:00
from chia.data_layer.data_layer_server import DataLayerServer
2021-09-17 15:03:48 +03:00
class DataLayer:
data_store: DataStore
2022-02-08 18:29:29 +03:00
data_layer_server: DataLayerServer
2021-09-17 17:30:14 +03:00
db_wrapper: DBWrapper
db_path: Path
2021-12-09 06:25:30 +03:00
connection: Optional[aiosqlite.Connection]
2021-10-04 01:20:18 +03:00
config: Dict[str, Any]
log: logging.Logger
2022-01-20 19:11:17 +03:00
wallet_rpc_init: Awaitable[WalletRpcClient]
2021-10-04 01:20:18 +03:00
state_changed_callback: Optional[Callable[..., object]]
2022-01-20 16:07:50 +03:00
wallet_id: uint64
initialized: bool
2022-02-22 19:11:44 +03:00
none_bytes: bytes32
2021-09-17 15:03:48 +03:00
def __init__(
self,
2021-09-17 17:30:14 +03:00
root_path: Path,
2022-01-20 19:11:17 +03:00
wallet_rpc_init: Awaitable[WalletRpcClient],
name: Optional[str] = None,
):
if name == "":
# TODO: If no code depends on "" counting as 'unspecified' then we do not
# need this.
name = None
2021-11-10 17:12:11 +03:00
config = load_config(root_path, "config.yaml", "data_layer")
self.initialized = False
2021-10-04 00:35:44 +03:00
self.config = config
2021-12-09 06:25:30 +03:00
self.connection = None
2022-01-20 19:11:17 +03:00
self.wallet_rpc_init = wallet_rpc_init
self.log = logging.getLogger(name if name is None else __name__)
2022-01-27 21:11:30 +03:00
self._shut_down: bool = False
db_path_replaced: str = config["database_path"].replace("CHALLENGE", config["selected_network"])
self.db_path = path_from_root(root_path, db_path_replaced)
mkdir(self.db_path.parent)
2022-04-07 17:55:02 +03:00
server_files_replaced: str = config.get(
"server_files_location", "data_layer/db/server_files_location_CHALLENGE"
).replace("CHALLENGE", config["selected_network"])
2022-04-06 20:44:51 +03:00
self.server_files_location = path_from_root(root_path, server_files_replaced)
mkdir(self.server_files_location)
self.data_layer_server = DataLayerServer(root_path, self.config, self.log)
2022-02-22 19:11:44 +03:00
self.none_bytes = bytes32([0] * 32)
2021-09-17 15:03:48 +03:00
2021-09-29 02:37:50 +03:00
def _set_state_changed_callback(self, callback: Callable[..., object]) -> None:
self.state_changed_callback = callback
def set_server(self, server: ChiaServer) -> None:
self.server = server
2021-09-17 15:03:48 +03:00
async def _start(self) -> bool:
self.connection = await aiosqlite.connect(self.db_path)
2021-09-17 17:30:14 +03:00
self.db_wrapper = DBWrapper(self.connection)
self.data_store = await DataStore.create(self.db_wrapper)
2022-01-20 19:11:17 +03:00
self.wallet_rpc = await self.wallet_rpc_init
self.subscription_lock: asyncio.Lock = asyncio.Lock()
2022-02-08 18:29:29 +03:00
if self.config.get("run_server", False):
await self.data_layer_server.start()
2022-05-05 19:39:18 +03:00
self.periodically_manage_data_task: asyncio.Task[Any] = asyncio.create_task(self.periodically_manage_data())
return True
2021-09-29 02:37:50 +03:00
def _close(self) -> None:
# TODO: review for anything else we need to do here
self._shut_down = True
2021-09-29 02:37:50 +03:00
async def _await_closed(self) -> None:
2021-12-09 06:25:30 +03:00
if self.connection is not None:
await self.connection.close()
2022-02-08 18:29:29 +03:00
if self.config.get("run_server", False):
await self.data_layer_server.stop()
2022-05-05 19:39:18 +03:00
self.periodically_manage_data_task.cancel()
2021-09-29 02:37:50 +03:00
async def create_store(
self, fee: uint64, root: bytes32 = bytes32([0] * 32)
) -> Tuple[List[TransactionRecord], bytes32]:
2022-01-20 16:07:50 +03:00
txs, tree_id = await self.wallet_rpc.create_new_dl(root, fee)
2022-01-20 22:30:27 +03:00
res = await self.data_store.create_tree(tree_id=tree_id)
if res is None:
self.log.fatal("failed creating store")
2022-01-20 15:44:20 +03:00
self.initialized = True
2022-01-20 16:07:50 +03:00
return txs, tree_id
2021-10-13 21:18:00 +03:00
async def batch_update(
2021-10-13 21:18:00 +03:00
self,
tree_id: bytes32,
changelist: List[Dict[str, Any]],
fee: uint64,
2022-01-22 05:17:15 +03:00
) -> TransactionRecord:
2022-05-04 20:23:29 +03:00
old_root = await self.data_store.get_tree_root(tree_id=tree_id)
rollback_generation = 0 if old_root is None else old_root.generation
try:
2022-06-03 19:02:21 +03:00
t1 = time.time()
2022-05-04 20:23:29 +03:00
await self.data_store.insert_batch(tree_id, changelist)
2022-06-03 19:02:21 +03:00
t2 = time.time()
self.log.info(f"Data store batch update process time: {t2 - t1}.")
2022-05-04 20:23:29 +03:00
root = await self.data_store.get_tree_root(tree_id=tree_id)
# todo return empty node hash from get_tree_root
if root.node_hash is not None:
node_hash = root.node_hash
else:
node_hash = self.none_bytes # todo change
transaction_record = await self.wallet_rpc.dl_update_root(tree_id, node_hash, fee)
assert transaction_record
# todo register callback to change status in data store
# await self.data_store.change_root_status(root, Status.COMMITTED)
return transaction_record
except Exception:
await self.data_store.rollback_to_generation(tree_id, rollback_generation)
raise
2021-10-13 21:18:00 +03:00
async def get_value(self, store_id: bytes32, key: bytes) -> Optional[bytes]:
2021-10-18 12:42:55 +03:00
res = await self.data_store.get_node_by_key(tree_id=store_id, key=key)
if res is None:
self.log.error("Failed to fetch key")
return None
2021-10-18 12:42:55 +03:00
return res.value
2022-01-24 15:53:56 +03:00
async def get_keys_values(self, store_id: bytes32, root_hash: Optional[bytes32]) -> List[TerminalNode]:
res = await self.data_store.get_keys_values(store_id, root_hash)
2021-10-18 12:42:55 +03:00
if res is None:
self.log.error("Failed to fetch keys values")
2021-10-18 12:42:55 +03:00
return res
2021-10-13 21:18:00 +03:00
2021-12-09 06:25:30 +03:00
async def get_ancestors(self, node_hash: bytes32, store_id: bytes32) -> List[InternalNode]:
2022-01-25 20:41:43 +03:00
res = await self.data_store.get_ancestors(node_hash=node_hash, tree_id=store_id)
2021-10-27 14:58:36 +03:00
if res is None:
self.log.error("Failed to get ancestors")
2021-10-27 14:58:36 +03:00
return res
2022-02-15 22:22:54 +03:00
async def get_root(self, store_id: bytes32) -> Optional[SingletonRecord]:
latest = await self.wallet_rpc.dl_latest_singleton(store_id, True)
2022-02-08 14:11:12 +03:00
if latest is None:
self.log.error(f"Failed to get root for {store_id.hex()}")
2022-02-15 22:22:54 +03:00
return latest
2022-02-18 16:43:57 +03:00
async def get_local_root(self, store_id: bytes32) -> Optional[bytes32]:
res = await self.data_store.get_tree_root(tree_id=store_id)
if res is None:
self.log.error(f"Failed to get root for {store_id.hex()}")
return None
return res.node_hash
2022-02-09 16:13:35 +03:00
async def get_root_history(self, store_id: bytes32) -> List[SingletonRecord]:
records = await self.wallet_rpc.dl_history(store_id)
if records is None:
self.log.error(f"Failed to get root history for {store_id.hex()}")
root_history = []
prev: Optional[SingletonRecord] = None
for record in records:
2022-02-10 17:18:38 +03:00
if prev is None or record.root != prev.root:
2022-02-09 16:13:35 +03:00
root_history.append(record)
2022-02-21 23:16:17 +03:00
prev = record
2022-02-09 16:13:35 +03:00
return root_history
async def fetch_and_validate(self, subscription: Subscription) -> None:
tree_id = subscription.tree_id
singleton_record: Optional[SingletonRecord] = await self.wallet_rpc.dl_latest_singleton(tree_id, True)
if singleton_record is None:
2022-02-02 23:06:37 +03:00
self.log.info(f"Fetch data: No singleton record for {tree_id}.")
return
2022-01-31 16:58:27 +03:00
if singleton_record.generation == uint32(0):
2022-02-02 23:06:37 +03:00
self.log.info(f"Fetch data: No data on chain for {tree_id}.")
return
2022-05-18 19:38:34 +03:00
if not await self.data_store.tree_id_exists(tree_id=tree_id):
await self.data_store.create_tree(tree_id=tree_id)
2022-04-04 13:45:38 +03:00
for ip, port in zip(subscription.ip, subscription.port):
root = await self.data_store.get_tree_root(tree_id=tree_id)
2022-05-18 19:38:34 +03:00
if root.generation > int(singleton_record.generation):
self.log.info(
f"Fetch data: local DL store is ahead of chain generation for {tree_id}. "
"Most likely waiting for our batch update to be confirmed on chain."
)
break
2022-05-18 19:38:34 +03:00
if root.generation == int(singleton_record.generation):
self.log.info(f"Fetch data: wallet generation matching on-chain generation: {tree_id}.")
break
self.log.info(
f"Downloading files {subscription.tree_id}. "
2022-05-18 19:38:34 +03:00
f"Current wallet generation: {root.generation}. "
f"Target wallet generation: {singleton_record.generation}. "
f"Server used: {ip}:{port}."
)
2022-05-04 20:23:29 +03:00
to_download = await self.wallet_rpc.dl_history(
2022-05-04 20:23:29 +03:00
launcher_id=tree_id,
2022-05-18 19:38:34 +03:00
min_generation=uint32(root.generation + 1),
2022-05-04 20:23:29 +03:00
max_generation=singleton_record.generation,
)
2022-02-22 17:22:38 +03:00
try:
2022-05-03 21:00:19 +03:00
success = await insert_from_delta_file(
self.data_store,
2022-03-29 17:49:02 +03:00
subscription.tree_id,
2022-05-18 19:38:34 +03:00
root.generation,
[record.root for record in reversed(to_download)],
2022-03-30 21:18:48 +03:00
ip,
port,
2022-05-04 20:23:29 +03:00
self.server_files_location,
self.log,
2022-03-29 17:49:02 +03:00
)
2022-05-03 21:00:19 +03:00
if success:
self.log.info(
f"Finished downloading and validating {subscription.tree_id}. "
f"Wallet generation saved: {singleton_record.generation}. "
f"Root hash saved: {singleton_record.root}."
)
break
2022-02-23 00:25:55 +03:00
except asyncio.CancelledError:
raise
2022-03-29 17:49:02 +03:00
except aiohttp.client_exceptions.ClientConnectorError:
2022-05-03 21:00:19 +03:00
self.log.warning(f"Server {ip}:{port} unavailable for {tree_id}.")
2022-02-22 17:22:38 +03:00
except Exception as e:
2022-05-04 20:23:29 +03:00
self.log.warning(f"Exception while downloading files for {tree_id}: {e} {traceback.format_exc()}.")
2022-05-05 19:39:18 +03:00
async def upload_files(self, tree_id: bytes32) -> None:
singleton_record: Optional[SingletonRecord] = await self.wallet_rpc.dl_latest_singleton(tree_id, True)
if singleton_record is None:
return
root = await self.data_store.get_tree_root(tree_id=tree_id)
publish_generation = min(int(singleton_record.generation), 0 if root is None else root.generation)
# If we make some batch updates, which get confirmed to the chain, we need to create the files.
# We iterate back and write the missing files, until we find the files already written.
2022-05-19 17:24:06 +03:00
root = await self.data_store.get_tree_root(tree_id=tree_id, generation=publish_generation)
2022-05-12 16:58:21 +03:00
while publish_generation > 0 and await write_files_for_root(
2022-05-05 19:39:18 +03:00
self.data_store,
tree_id,
root,
self.server_files_location,
):
publish_generation -= 1
root = await self.data_store.get_tree_root(tree_id=tree_id, generation=publish_generation)
2022-05-24 15:24:56 +03:00
async def add_missing_files(self, store_id: bytes32, override: bool, foldername: Optional[Path]) -> None:
2022-05-11 21:59:11 +03:00
root = await self.data_store.get_tree_root(tree_id=store_id)
singleton_record: Optional[SingletonRecord] = await self.wallet_rpc.dl_latest_singleton(store_id, True)
if singleton_record is None:
self.log.error(f"No singletor record found for {store_id}.")
return
max_generation = min(int(singleton_record.generation), 0 if root is None else root.generation)
2022-05-24 15:24:56 +03:00
server_files_location = foldername if foldername is not None else self.server_files_location
2022-05-11 21:59:11 +03:00
for generation in range(1, max_generation + 1):
root = await self.data_store.get_tree_root(tree_id=store_id, generation=generation)
2022-05-24 15:24:56 +03:00
await write_files_for_root(self.data_store, store_id, root, server_files_location, override)
2022-05-11 21:59:11 +03:00
2022-04-04 13:45:38 +03:00
async def subscribe(self, store_id: bytes32, ip: List[str], port: List[uint16]) -> None:
subscription = Subscription(store_id, ip, port)
subscriptions = await self.get_subscriptions()
2022-02-17 17:39:44 +03:00
if subscription.tree_id in (subscription.tree_id for subscription in subscriptions):
2022-02-16 23:46:01 +03:00
await self.data_store.update_existing_subscription(subscription)
self.log.info(f"Successfully updated subscription {subscription.tree_id}")
return
await self.wallet_rpc.dl_track_new(subscription.tree_id)
async with self.subscription_lock:
await self.data_store.subscribe(subscription)
2022-01-28 18:57:25 +03:00
self.log.info(f"Subscribed to {subscription.tree_id}")
async def unsubscribe(self, tree_id: bytes32) -> None:
subscriptions = await self.get_subscriptions()
2022-02-17 17:39:44 +03:00
if tree_id not in (subscription.tree_id for subscription in subscriptions):
2022-02-16 23:46:01 +03:00
raise RuntimeError("No subscription found for the given tree_id.")
async with self.subscription_lock:
await self.data_store.unsubscribe(tree_id)
2022-01-28 15:36:45 +03:00
await self.wallet_rpc.dl_stop_tracking(tree_id)
2022-01-28 18:57:25 +03:00
self.log.info(f"Unsubscribed to {tree_id}")
async def get_subscriptions(self) -> List[Subscription]:
async with self.subscription_lock:
return await self.data_store.get_subscriptions()
2022-02-21 23:16:17 +03:00
async def get_kv_diff(self, tree_id: bytes32, hash_1: bytes32, hash_2: bytes32) -> Set[DiffData]:
2022-02-17 19:31:01 +03:00
return await self.data_store.get_kv_diff(tree_id, hash_1, hash_2)
2022-05-05 19:39:18 +03:00
async def periodically_manage_data(self) -> None:
manage_data_interval = self.config.get("manage_data_interval", 60)
while not self._shut_down:
async with self.subscription_lock:
try:
subscriptions = await self.data_store.get_subscriptions()
for subscription in subscriptions:
await self.wallet_rpc.dl_track_new(subscription.tree_id)
break
2022-04-27 15:30:48 +03:00
except aiohttp.client_exceptions.ClientConnectorError:
pass
2022-05-06 15:36:42 +03:00
except asyncio.CancelledError:
return
self.log.warning("Cannot connect to the wallet. Retrying in 3s.")
delay_until = time.monotonic() + 3
while time.monotonic() < delay_until:
if self._shut_down:
break
2022-05-06 15:36:42 +03:00
try:
await asyncio.sleep(0.1)
except asyncio.CancelledError:
return
while not self._shut_down:
async with self.subscription_lock:
2022-01-27 21:11:30 +03:00
subscriptions = await self.data_store.get_subscriptions()
# Subscribe to all local tree_ids that we can find on chain.
local_tree_ids = await self.data_store.get_tree_ids()
subscription_tree_ids = set(subscription.tree_id for subscription in subscriptions)
for local_id in local_tree_ids:
if local_id not in subscription_tree_ids:
try:
await self.subscribe(local_id, [], [])
except asyncio.CancelledError:
return
except Exception as e:
self.log.info(
f"Can't subscribe to locally stored {local_id}: {type(e)} {e} {traceback.format_exc()}"
)
async with self.subscription_lock:
for subscription in subscriptions:
2022-02-02 23:06:37 +03:00
try:
await self.fetch_and_validate(subscription)
2022-05-05 19:39:18 +03:00
await self.upload_files(subscription.tree_id)
2022-05-06 15:38:57 +03:00
except asyncio.CancelledError:
return
2022-02-02 23:06:37 +03:00
except Exception as e:
self.log.error(f"Exception while fetching data: {type(e)} {e} {traceback.format_exc()}.")
try:
2022-05-05 19:39:18 +03:00
await asyncio.sleep(manage_data_interval)
except asyncio.CancelledError:
return