2021-09-16 04:31:41 +03:00
|
|
|
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
|
2021-09-17 18:29:46 +03:00
|
|
|
import aiosqlite
|
2022-02-02 23:06:37 +03:00
|
|
|
import traceback
|
2022-01-25 19:44:30 +03:00
|
|
|
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
|
2021-09-16 04:31:41 +03:00
|
|
|
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
|
2021-09-17 18:29:46 +03:00
|
|
|
from chia.util.path import mkdir, path_from_root
|
2022-01-14 18:58:26 +03:00
|
|
|
from chia.wallet.transaction_record import TransactionRecord
|
2022-01-25 19:44:30 +03:00
|
|
|
from chia.data_layer.data_layer_wallet import SingletonRecord
|
2022-05-04 20:23:29 +03:00
|
|
|
from chia.data_layer.download_data import insert_from_delta_file
|
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
|
|
|
|
2021-09-16 04:31:41 +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
|
2021-09-17 18:29:46 +03:00
|
|
|
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]
|
2021-09-16 04:31:41 +03:00
|
|
|
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
|
2021-09-16 04:31:41 +03:00
|
|
|
initialized: bool
|
2022-02-22 19:11:44 +03:00
|
|
|
none_bytes: bytes32
|
2021-09-17 15:03:48 +03:00
|
|
|
|
2021-09-16 04:31:41 +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],
|
2021-09-21 19:05:45 +03:00
|
|
|
name: Optional[str] = None,
|
2021-09-16 04:31:41 +03:00
|
|
|
):
|
2021-09-21 19:05:45 +03:00
|
|
|
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")
|
2021-09-16 04:31:41 +03:00
|
|
|
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
|
2021-09-21 19:05:45 +03:00
|
|
|
self.log = logging.getLogger(name if name is None else __name__)
|
2022-01-27 21:11:30 +03:00
|
|
|
self._shut_down: bool = False
|
2021-09-17 18:29:46 +03:00
|
|
|
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
|
|
|
|
2021-11-08 18:57:31 +03:00
|
|
|
async def _start(self) -> bool:
|
2021-09-17 18:29:46 +03:00
|
|
|
self.connection = await aiosqlite.connect(self.db_path)
|
2021-09-17 17:30:14 +03:00
|
|
|
self.db_wrapper = DBWrapper(self.connection)
|
2021-09-16 04:31:41 +03:00
|
|
|
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
|
2022-01-25 19:44:30 +03:00
|
|
|
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-02-23 15:26:29 +03:00
|
|
|
subscriptions = await self.get_subscriptions()
|
|
|
|
for subscription in subscriptions:
|
|
|
|
await self.wallet_rpc.dl_track_new(subscription.tree_id)
|
|
|
|
self.periodically_fetch_data_task: asyncio.Task[Any] = asyncio.create_task(self.periodically_fetch_data())
|
2022-01-14 18:58:26 +03:00
|
|
|
return True
|
|
|
|
|
2021-09-29 02:37:50 +03:00
|
|
|
def _close(self) -> None:
|
|
|
|
# TODO: review for anything else we need to do here
|
2022-01-25 19:44:30 +03:00
|
|
|
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()
|
|
|
|
self.periodically_fetch_data_task.cancel()
|
2021-09-29 02:37:50 +03:00
|
|
|
|
2022-01-30 14:25:30 +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)
|
2021-11-08 18:57:31 +03:00
|
|
|
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
|
|
|
|
2022-01-14 18:58:26 +03:00
|
|
|
async def batch_update(
|
2021-10-13 21:18:00 +03:00
|
|
|
self,
|
|
|
|
tree_id: bytes32,
|
|
|
|
changelist: List[Dict[str, Any]],
|
2022-01-30 14:25:30 +03:00
|
|
|
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:
|
|
|
|
await self.data_store.insert_batch(tree_id, changelist)
|
|
|
|
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
|
|
|
|
2022-01-14 18:58:26 +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:
|
2022-01-14 18:58:26 +03:00
|
|
|
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:
|
2022-01-14 18:58:26 +03:00
|
|
|
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:
|
2022-01-14 18:58:26 +03:00
|
|
|
self.log.error("Failed to get ancestors")
|
2021-10-27 14:58:36 +03:00
|
|
|
return res
|
2022-01-14 18:58:26 +03:00
|
|
|
|
2022-02-15 22:22:54 +03:00
|
|
|
async def get_root(self, store_id: bytes32) -> Optional[SingletonRecord]:
|
2022-02-08 16:42:04 +03:00
|
|
|
latest = await self.wallet_rpc.dl_latest_singleton(store_id, True)
|
2022-02-08 14:11:12 +03:00
|
|
|
if latest is None:
|
2022-01-14 18:58:26 +03:00
|
|
|
self.log.error(f"Failed to get root for {store_id.hex()}")
|
2022-02-15 22:22:54 +03:00
|
|
|
return latest
|
2022-01-14 18:58:26 +03:00
|
|
|
|
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
|
2022-01-14 18:58:26 +03:00
|
|
|
|
2022-01-25 19:44:30 +03:00
|
|
|
async def fetch_and_validate(self, subscription: Subscription) -> None:
|
|
|
|
tree_id = subscription.tree_id
|
2022-02-08 16:42:04 +03:00
|
|
|
singleton_record: Optional[SingletonRecord] = await self.wallet_rpc.dl_latest_singleton(tree_id, True)
|
2022-01-25 19:44:30 +03:00
|
|
|
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}.")
|
2022-01-25 19:44:30 +03:00
|
|
|
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}.")
|
2022-01-25 19:44:30 +03:00
|
|
|
return
|
|
|
|
|
2022-04-04 13:45:38 +03:00
|
|
|
for ip, port in zip(subscription.ip, subscription.port):
|
2022-05-05 18:46:45 +03:00
|
|
|
root = await self.data_store.get_tree_root(tree_id=tree_id)
|
|
|
|
wallet_current_generation = 0 if root is None else root.generation
|
|
|
|
if wallet_current_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
|
|
|
|
if wallet_current_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}. "
|
|
|
|
f"Current wallet generation: {int(wallet_current_generation)}. "
|
|
|
|
f"Target wallet generation: {singleton_record.generation}. "
|
|
|
|
f"Server used: {ip}:{port}."
|
|
|
|
)
|
2022-05-04 20:23:29 +03:00
|
|
|
|
2022-05-05 18:46:45 +03:00
|
|
|
to_download = await self.wallet_rpc.dl_history(
|
2022-05-04 20:23:29 +03:00
|
|
|
launcher_id=tree_id,
|
|
|
|
min_generation=uint32(wallet_current_generation + 1),
|
|
|
|
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(
|
2022-05-02 18:16:55 +03:00
|
|
|
self.data_store,
|
2022-03-29 17:49:02 +03:00
|
|
|
subscription.tree_id,
|
2022-05-05 18:46:45 +03:00
|
|
|
wallet_current_generation,
|
|
|
|
[record.root for record in 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}."
|
|
|
|
)
|
2022-05-05 18:46:45 +03:00
|
|
|
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-01-25 19:44:30 +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)
|
2022-01-25 19:44:30 +03:00
|
|
|
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}")
|
2022-01-25 19:44:30 +03:00
|
|
|
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}")
|
2022-01-25 19:44:30 +03:00
|
|
|
|
|
|
|
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.")
|
2022-01-25 19:44:30 +03:00
|
|
|
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}")
|
2022-01-25 19:44:30 +03:00
|
|
|
|
|
|
|
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-01-25 19:44:30 +03:00
|
|
|
async def periodically_fetch_data(self) -> None:
|
|
|
|
fetch_data_interval = self.config.get("fetch_data_interval", 60)
|
|
|
|
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()
|
2022-01-25 19:44:30 +03:00
|
|
|
for subscription in subscriptions:
|
2022-02-02 23:06:37 +03:00
|
|
|
try:
|
|
|
|
await self.fetch_and_validate(subscription)
|
|
|
|
except Exception as e:
|
|
|
|
self.log.error(f"Exception while fetching data: {type(e)} {e} {traceback.format_exc()}.")
|
2022-01-25 19:44:30 +03:00
|
|
|
try:
|
|
|
|
await asyncio.sleep(fetch_data_interval)
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
pass
|