Files
chia-blockchain/chia/wallet/wallet_node.py
T
Kyle AltendorfandGitHub 75115f2475 dataclass WalletNode (#11925)
* dataclass WalletNode

* all private at the end
2022-06-16 17:50:29 +01:00

1523 lines
74 KiB
Python

import asyncio
import dataclasses
import json
import logging
import random
import time
import traceback
from pathlib import Path
from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Tuple
from blspy import AugSchemeMPL, PrivateKey, G2Element, G1Element
from packaging.version import Version
from chia.consensus.block_record import BlockRecord
from chia.consensus.blockchain import ReceiveBlockResult
from chia.consensus.constants import ConsensusConstants
from chia.daemon.keychain_proxy import (
KeychainProxy,
KeychainProxyConnectionFailure,
KeyringIsEmpty,
connect_to_keychain_and_validate,
wrap_local_keychain,
)
from chia.protocols import wallet_protocol
from chia.protocols.full_node_protocol import RequestProofOfWeight, RespondProofOfWeight
from chia.protocols.protocol_message_types import ProtocolMessageTypes
from chia.protocols.wallet_protocol import (
CoinState,
RequestSESInfo,
RespondBlockHeader,
RespondSESInfo,
RespondToCoinUpdates,
RespondToPhUpdates,
)
from chia.server.node_discovery import WalletPeers
from chia.server.outbound_message import Message, NodeType, make_msg
from chia.server.peer_store_resolver import PeerStoreResolver
from chia.server.server import ChiaServer
from chia.server.ws_connection import WSChiaConnection
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.blockchain_format.sub_epoch_summary import SubEpochSummary
from chia.types.coin_spend import CoinSpend
from chia.types.header_block import HeaderBlock
from chia.types.mempool_inclusion_status import MempoolInclusionStatus
from chia.types.peer_info import PeerInfo
from chia.types.weight_proof import SubEpochData, WeightProof
from chia.util.byte_types import hexstr_to_bytes
from chia.util.chunks import chunks
from chia.util.config import WALLET_PEERS_PATH_KEY_DEPRECATED
from chia.util.default_root import STANDALONE_ROOT_PATH
from chia.util.ints import uint32, uint64
from chia.util.keychain import Keychain, KeyringIsLocked
from chia.util.path import path_from_root
from chia.util.profiler import profile_task
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.util.new_peak_queue import NewPeakItem, NewPeakQueue, NewPeakQueueTypes
from chia.wallet.util.peer_request_cache import PeerRequestCache, can_use_peer_request_cache
from chia.wallet.util.wallet_sync_utils import (
fetch_header_blocks_in_range,
fetch_last_tx_from_peer,
last_change_height_cs,
request_and_validate_additions,
request_and_validate_removals,
request_header_blocks,
subscribe_to_coin_updates,
subscribe_to_phs,
)
from chia.wallet.wallet_action import WalletAction
from chia.wallet.wallet_coin_record import WalletCoinRecord
from chia.wallet.wallet_state_manager import WalletStateManager
@dataclasses.dataclass
class WalletNode:
config: Dict
root_path: Path
constants: ConsensusConstants
local_keychain: Optional[Keychain] = None
log: logging.Logger = logging.getLogger(__name__)
# Normal operation data
cached_blocks: Dict = dataclasses.field(default_factory=dict)
future_block_hashes: Dict = dataclasses.field(default_factory=dict)
# Sync data
proof_hashes: List = dataclasses.field(default_factory=list)
state_changed_callback: Optional[Callable] = None
wallet_state_manager: Optional[WalletStateManager] = None
server: Optional[ChiaServer] = None
wsm_close_task: Optional[asyncio.Task] = None
sync_task: Optional[asyncio.Task] = None
logged_in_fingerprint: Optional[int] = None
peer_task: Optional[asyncio.Task] = None
logged_in: bool = False
keychain_proxy: Optional[KeychainProxy] = None
height_to_time: Dict[uint32, uint64] = dataclasses.field(default_factory=dict)
# Peers that we have long synced to
synced_peers: Set[bytes32] = dataclasses.field(default_factory=set)
wallet_peers: Optional[WalletPeers] = None
wallet_peers_initialized: bool = False
valid_wp_cache: Dict[bytes32, Any] = dataclasses.field(default_factory=dict)
untrusted_caches: Dict[bytes32, PeerRequestCache] = dataclasses.field(default_factory=dict)
# in Untrusted mode wallet might get the state update before receiving the block
race_cache: Dict[bytes32, Set[CoinState]] = dataclasses.field(default_factory=dict)
race_cache_hashes: List[Tuple[uint32, bytes32]] = dataclasses.field(default_factory=list)
node_peaks: Dict[bytes32, Tuple[uint32, bytes32]] = dataclasses.field(default_factory=dict)
validation_semaphore: Optional[asyncio.Semaphore] = None
local_node_synced: bool = False
LONG_SYNC_THRESHOLD: int = 200
last_wallet_tx_resend_time: int = 0
# Duration in seconds
wallet_tx_resend_timeout_secs: int = 1800
new_peak_queue: NewPeakQueue = dataclasses.field(default_factory=lambda: NewPeakQueue(asyncio.PriorityQueue()))
full_node_peer: Optional[PeerInfo] = None
_shut_down: bool = False
_process_new_subscriptions_task: Optional[asyncio.Task] = None
_primary_peer_sync_task: Optional[asyncio.Task] = None
_secondary_peer_sync_task: Optional[asyncio.Task] = None
async def ensure_keychain_proxy(self) -> KeychainProxy:
if self.keychain_proxy is None:
if self.local_keychain:
self.keychain_proxy = wrap_local_keychain(self.local_keychain, log=self.log)
else:
self.keychain_proxy = await connect_to_keychain_and_validate(self.root_path, self.log)
if not self.keychain_proxy:
raise KeychainProxyConnectionFailure("Failed to connect to keychain service")
return self.keychain_proxy
def get_cache_for_peer(self, peer) -> PeerRequestCache:
if peer.peer_node_id not in self.untrusted_caches:
self.untrusted_caches[peer.peer_node_id] = PeerRequestCache()
return self.untrusted_caches[peer.peer_node_id]
def rollback_request_caches(self, reorg_height: int):
# Everything after reorg_height should be removed from the cache
for cache in self.untrusted_caches.values():
cache.clear_after_height(reorg_height)
async def get_key_for_fingerprint(self, fingerprint: Optional[int]) -> Optional[PrivateKey]:
try:
keychain_proxy = await self.ensure_keychain_proxy()
key = await keychain_proxy.get_key_for_fingerprint(fingerprint)
except KeyringIsEmpty:
self.log.warning("No keys present. Create keys with the UI, or with the 'chia keys' program.")
return None
except KeyringIsLocked:
self.log.warning("Keyring is locked")
return None
except KeychainProxyConnectionFailure as e:
tb = traceback.format_exc()
self.log.error(f"Missing keychain_proxy: {e} {tb}")
raise e # Re-raise so that the caller can decide whether to continue or abort
return key
async def _start(
self,
fingerprint: Optional[int] = None,
) -> bool:
self.synced_peers = set()
private_key = await self.get_key_for_fingerprint(fingerprint)
if private_key is None:
self.logged_in = False
return False
if self.config.get("enable_profiler", False):
asyncio.create_task(profile_task(self.root_path, "wallet", self.log))
db_path_key_suffix = str(private_key.get_g1().get_fingerprint())
db_path_replaced: str = (
self.config["database_path"]
.replace("CHALLENGE", self.config["selected_network"])
.replace("KEY", db_path_key_suffix)
)
path = path_from_root(self.root_path, db_path_replaced.replace("v1", "v2"))
path.parent.mkdir(parents=True, exist_ok=True)
standalone_path = path_from_root(STANDALONE_ROOT_PATH, f"{db_path_replaced.replace('v2', 'v1')}_new")
if not path.exists():
if standalone_path.exists():
self.log.info(f"Copying wallet db from {standalone_path} to {path}")
path.write_bytes(standalone_path.read_bytes())
assert self.server is not None
self.wallet_state_manager = await WalletStateManager.create(
private_key,
self.config,
path,
self.constants,
self.server,
self.root_path,
self,
)
assert self.wallet_state_manager is not None
self.config["starting_height"] = 0
if self.wallet_peers is None:
self.initialize_wallet_peers()
if self.state_changed_callback is not None:
self.wallet_state_manager.set_callback(self.state_changed_callback)
self.last_wallet_tx_resend_time = int(time.time())
self.wallet_tx_resend_timeout_secs = self.config.get("tx_resend_timeout_secs", 60 * 60)
self.wallet_state_manager.set_pending_callback(self._pending_tx_handler)
self._shut_down = False
self._process_new_subscriptions_task = asyncio.create_task(self._process_new_subscriptions())
self.sync_event = asyncio.Event()
if fingerprint is None:
self.logged_in_fingerprint = private_key.get_g1().get_fingerprint()
else:
self.logged_in_fingerprint = fingerprint
self.logged_in = True
self.wallet_state_manager.set_sync_mode(False)
async with self.wallet_state_manager.puzzle_store.lock:
index = await self.wallet_state_manager.puzzle_store.get_last_derivation_path()
if index is None or index < self.config["initial_num_public_keys"] - 1:
await self.wallet_state_manager.create_more_puzzle_hashes(from_zero=True)
self.wsm_close_task = None
return True
def _close(self):
self.log.info("self._close")
self.logged_in_fingerprint = None
self._shut_down = True
if self._process_new_subscriptions_task is not None:
self._process_new_subscriptions_task.cancel()
if self._primary_peer_sync_task is not None:
self._primary_peer_sync_task.cancel()
if self._secondary_peer_sync_task is not None:
self._secondary_peer_sync_task.cancel()
async def _await_closed(self, shutting_down: bool = True):
self.log.info("self._await_closed")
if self.server is not None:
await self.server.close_all_connections()
if self.wallet_peers is not None:
await self.wallet_peers.ensure_is_closed()
if self.wallet_state_manager is not None:
await self.wallet_state_manager._await_closed()
self.wallet_state_manager = None
if shutting_down and self.keychain_proxy is not None:
proxy = self.keychain_proxy
self.keychain_proxy = None
await proxy.close()
await asyncio.sleep(0.5) # https://docs.aiohttp.org/en/stable/client_advanced.html#graceful-shutdown
self.logged_in = False
self.wallet_peers = None
def _set_state_changed_callback(self, callback: Callable):
self.state_changed_callback = callback
if self.wallet_state_manager is not None:
self.wallet_state_manager.set_callback(self.state_changed_callback)
self.wallet_state_manager.set_pending_callback(self._pending_tx_handler)
def _pending_tx_handler(self):
if self.wallet_state_manager is None:
return None
asyncio.create_task(self._resend_queue())
async def _action_messages(self) -> List[Message]:
if self.wallet_state_manager is None:
return []
actions: List[WalletAction] = await self.wallet_state_manager.action_store.get_all_pending_actions()
result: List[Message] = []
for action in actions:
data = json.loads(action.data)
action_data = data["data"]["action_data"]
if action.name == "request_puzzle_solution":
coin_name = bytes32(hexstr_to_bytes(action_data["coin_name"]))
height = uint32(action_data["height"])
msg = make_msg(
ProtocolMessageTypes.request_puzzle_solution,
wallet_protocol.RequestPuzzleSolution(coin_name, height),
)
result.append(msg)
return result
async def _resend_queue(self):
if self._shut_down or self.server is None or self.wallet_state_manager is None:
return None
for msg, sent_peers in await self._messages_to_resend():
if self._shut_down or self.server is None or self.wallet_state_manager is None:
return None
full_nodes = self.server.get_full_node_connections()
for peer in full_nodes:
if peer.peer_node_id in sent_peers:
continue
self.log.debug(f"sending: {msg}")
await peer.send_message(msg)
for msg in await self._action_messages():
if self._shut_down or self.server is None or self.wallet_state_manager is None:
return None
await self.server.send_to_all([msg], NodeType.FULL_NODE)
async def _messages_to_resend(self) -> List[Tuple[Message, Set[bytes32]]]:
if self.wallet_state_manager is None or self._shut_down:
return []
messages: List[Tuple[Message, Set[bytes32]]] = []
current_time = int(time.time())
retry_accepted_txs = False
if self.last_wallet_tx_resend_time < current_time - self.wallet_tx_resend_timeout_secs:
self.last_wallet_tx_resend_time = current_time
retry_accepted_txs = True
records: List[TransactionRecord] = await self.wallet_state_manager.tx_store.get_not_sent(
include_accepted_txs=retry_accepted_txs
)
for record in records:
if record.spend_bundle is None:
continue
msg = make_msg(
ProtocolMessageTypes.send_transaction,
wallet_protocol.SendTransaction(record.spend_bundle),
)
already_sent = set()
for peer, status, _ in record.sent_to:
if status == MempoolInclusionStatus.SUCCESS.value:
already_sent.add(bytes32.from_hexstr(peer))
messages.append((msg, already_sent))
return messages
async def _process_new_subscriptions(self):
while not self._shut_down:
# Here we process four types of messages in the queue, where the first one has higher priority (lower
# number in the queue), and priority decreases for each type.
peer: Optional[WSChiaConnection] = None
item: Optional[NewPeakItem] = None
try:
peer, item = None, None
item = await self.new_peak_queue.get()
self.log.debug("Pulled from queue: %s", item)
assert item is not None
if item.item_type == NewPeakQueueTypes.COIN_ID_SUBSCRIPTION:
# Subscriptions are the highest priority, because we don't want to process any more peaks or
# state updates until we are sure that we subscribed to everything that we need to. Otherwise,
# we might not be able to process some state.
coin_ids: List[bytes32] = item.data
for peer in self.server.get_full_node_connections():
coin_states: List[CoinState] = await subscribe_to_coin_updates(coin_ids, peer, uint32(0))
if len(coin_states) > 0:
async with self.wallet_state_manager.lock:
await self.receive_state_from_peer(coin_states, peer)
elif item.item_type == NewPeakQueueTypes.PUZZLE_HASH_SUBSCRIPTION:
puzzle_hashes: List[bytes32] = item.data
for peer in self.server.get_full_node_connections():
# Puzzle hash subscription
coin_states: List[CoinState] = await subscribe_to_phs(puzzle_hashes, peer, uint32(0))
if len(coin_states) > 0:
async with self.wallet_state_manager.lock:
await self.receive_state_from_peer(coin_states, peer)
elif item.item_type == NewPeakQueueTypes.FULL_NODE_STATE_UPDATED:
# Note: this can take a while when we have a lot of transactions. We want to process these
# before new_peaks, since new_peak_wallet requires that we first obtain the state for that peak.
request: wallet_protocol.CoinStateUpdate = item.data[0]
peer = item.data[1]
assert peer is not None
await self.state_update_received(request, peer)
elif item.item_type == NewPeakQueueTypes.NEW_PEAK_WALLET:
# This can take a VERY long time, because it might trigger a long sync. It is OK if we miss some
# subscriptions or state updates, since all subscriptions and state updates will be handled by
# long_sync (up to the target height).
request: wallet_protocol.NewPeakWallet = item.data[0]
peer = item.data[1]
assert peer is not None
await self.new_peak_wallet(request, peer)
else:
assert False
except asyncio.CancelledError:
self.log.info("Queue task cancelled, exiting.")
raise
except Exception as e:
self.log.error(f"Exception handling {item}, {e} {traceback.format_exc()}")
if peer is not None:
await peer.close(9999)
def set_server(self, server: ChiaServer):
self.server = server
self.initialize_wallet_peers()
def initialize_wallet_peers(self):
self.server.on_connect = self.on_connect
network_name = self.config["selected_network"]
connect_to_unknown_peers = self.config.get("connect_to_unknown_peers", True)
testing = self.config.get("testing", False)
if self.wallet_peers is None and connect_to_unknown_peers and not testing:
self.wallet_peers = WalletPeers(
self.server,
self.config["target_peer_count"],
PeerStoreResolver(
self.root_path,
self.config,
selected_network=network_name,
peers_file_path_key="wallet_peers_file_path",
legacy_peer_db_path_key=WALLET_PEERS_PATH_KEY_DEPRECATED,
default_peers_file_path="wallet/db/wallet_peers.dat",
),
self.config["introducer_peer"],
self.config.get("dns_servers", ["dns-introducer.chia.net"]),
self.config["peer_connect_interval"],
network_name,
None,
self.log,
)
asyncio.create_task(self.wallet_peers.start())
def on_disconnect(self, peer: WSChiaConnection):
if self.is_trusted(peer):
self.local_node_synced = False
self.initialize_wallet_peers()
if peer.peer_node_id in self.untrusted_caches:
self.untrusted_caches.pop(peer.peer_node_id)
if peer.peer_node_id in self.synced_peers:
self.synced_peers.remove(peer.peer_node_id)
if peer.peer_node_id in self.node_peaks:
self.node_peaks.pop(peer.peer_node_id)
async def on_connect(self, peer: WSChiaConnection):
if self.wallet_state_manager is None:
return None
if Version(peer.protocol_version) < Version("0.0.33"):
self.log.info("Disconnecting, full node running old software")
await peer.close()
trusted = self.is_trusted(peer)
if not trusted and self.local_node_synced:
await peer.close()
if peer.peer_node_id in self.synced_peers:
self.synced_peers.remove(peer.peer_node_id)
self.log.info(f"Connected peer {peer.get_peer_info()} is trusted: {trusted}")
messages_peer_ids = await self._messages_to_resend()
self.wallet_state_manager.state_changed("add_connection")
for msg, peer_ids in messages_peer_ids:
if peer.peer_node_id in peer_ids:
continue
await peer.send_message(msg)
if self.wallet_peers is not None:
await self.wallet_peers.on_connect(peer)
async def perform_atomic_rollback(self, fork_height: int, cache: Optional[PeerRequestCache] = None):
assert self.wallet_state_manager is not None
self.log.info(f"perform_atomic_rollback to {fork_height}")
async with self.wallet_state_manager.db_wrapper.lock:
try:
await self.wallet_state_manager.db_wrapper.begin_transaction()
removed_wallet_ids = await self.wallet_state_manager.reorg_rollback(fork_height)
await self.wallet_state_manager.blockchain.set_finished_sync_up_to(fork_height, True, in_rollback=True)
if cache is None:
self.rollback_request_caches(fork_height)
else:
cache.clear_after_height(fork_height)
await self.wallet_state_manager.db_wrapper.commit_transaction()
except Exception as e:
tb = traceback.format_exc()
self.log.error(f"Exception while perform_atomic_rollback: {e} {tb}")
await self.wallet_state_manager.db_wrapper.rollback_transaction()
await self.wallet_state_manager.coin_store.rebuild_wallet_cache()
await self.wallet_state_manager.tx_store.rebuild_tx_cache()
await self.wallet_state_manager.pool_store.rebuild_cache()
raise
else:
await self.wallet_state_manager.blockchain.clean_block_records()
for wallet_id in removed_wallet_ids:
self.wallet_state_manager.wallets.pop(wallet_id)
async def long_sync(
self,
target_height: uint32,
full_node: WSChiaConnection,
fork_height: int,
*,
rollback: bool,
):
"""
Sync algorithm:
- Download and verify weight proof (if not trusted)
- Roll back anything after the fork point (if rollback=True)
- Subscribe to all puzzle_hashes over and over until there are no more updates
- Subscribe to all coin_ids over and over until there are no more updates
- rollback=False means that we are just double-checking with this peer to make sure we don't have any
missing transactions, so we don't need to rollback
"""
def is_new_state_update(cs: CoinState) -> bool:
if cs.spent_height is None and cs.created_height is None:
return True
if cs.spent_height is not None and cs.spent_height >= fork_height:
return True
if cs.created_height is not None and cs.created_height >= fork_height:
return True
return False
trusted: bool = self.is_trusted(full_node)
self.log.info(f"Starting sync trusted: {trusted} to peer {full_node.peer_host}")
assert self.wallet_state_manager is not None
start_time = time.time()
if rollback:
# we should clear all peers since this is a full rollback
await self.perform_atomic_rollback(fork_height)
await self.update_ui()
# We only process new state updates to avoid slow reprocessing. We set the sync height after adding
# Things, so we don't have to reprocess these later. There can be many things in ph_update_res.
already_checked_ph: Set[bytes32] = set()
continue_while: bool = True
all_puzzle_hashes: List[bytes32] = await self.get_puzzle_hashes_to_subscribe()
while continue_while:
# Get all phs from puzzle store
ph_chunks: Iterator[List[bytes32]] = chunks(all_puzzle_hashes, 1000)
for chunk in ph_chunks:
ph_update_res: List[CoinState] = await subscribe_to_phs(
[p for p in chunk if p not in already_checked_ph], full_node, 0
)
ph_update_res = list(filter(is_new_state_update, ph_update_res))
if not await self.receive_state_from_peer(ph_update_res, full_node, update_finished_height=True):
# If something goes wrong, abort sync
return
already_checked_ph.update(chunk)
# Check if new puzzle hashed have been created
await self.wallet_state_manager.create_more_puzzle_hashes()
all_puzzle_hashes = await self.get_puzzle_hashes_to_subscribe()
continue_while = False
for ph in all_puzzle_hashes:
if ph not in already_checked_ph:
continue_while = True
break
self.log.info(f"Successfully subscribed and updated {len(already_checked_ph)} puzzle hashes")
# The number of coin id updates are usually going to be significantly less than ph updates, so we can
# sync from 0 every time.
continue_while = True
all_coin_ids: List[bytes32] = await self.get_coin_ids_to_subscribe(0)
already_checked_coin_ids: Set[bytes32] = set()
while continue_while:
one_k_chunks = chunks(all_coin_ids, 1000)
for chunk in one_k_chunks:
c_update_res: List[CoinState] = await subscribe_to_coin_updates(chunk, full_node, 0)
if not await self.receive_state_from_peer(c_update_res, full_node):
# If something goes wrong, abort sync
return
already_checked_coin_ids.update(chunk)
all_coin_ids = await self.get_coin_ids_to_subscribe(0)
continue_while = False
for coin_id in all_coin_ids:
if coin_id not in already_checked_coin_ids:
continue_while = True
break
self.log.info(f"Successfully subscribed and updated {len(already_checked_coin_ids)} coin ids")
# Only update this fully when the entire sync has completed
await self.wallet_state_manager.blockchain.set_finished_sync_up_to(target_height)
if trusted:
self.local_node_synced = True
self.wallet_state_manager.state_changed("new_block")
self.synced_peers.add(full_node.peer_node_id)
await self.update_ui()
end_time = time.time()
duration = end_time - start_time
self.log.info(f"Sync (trusted: {trusted}) duration was: {duration}")
async def receive_state_from_peer(
self,
items_input: List[CoinState],
peer: WSChiaConnection,
fork_height: Optional[uint32] = None,
height: Optional[uint32] = None,
header_hash: Optional[bytes32] = None,
update_finished_height: bool = False,
) -> bool:
# Adds the state to the wallet state manager. If the peer is trusted, we do not validate. If the peer is
# untrusted we do, but we might not add the state, since we need to receive the new_peak message as well.
if self.wallet_state_manager is None:
return False
trusted = self.is_trusted(peer)
# Validate states in parallel, apply serial
# TODO: optimize fetching
if self.validation_semaphore is None:
self.validation_semaphore = asyncio.Semaphore(10)
# Rollback is handled in wallet_short_sync_backtrack for untrusted peers, so we don't need to do it here.
# Also it's not safe to rollback, an untrusted peer can give us old fork point and make our TX dissapear.
# wallet_short_sync_backtrack can safely rollback because we validated the weight for the new peak so we
# know the peer is telling the truth about the reorg.
# If there is a fork, we need to ensure that we roll back in trusted mode to properly handle reorgs
cache: PeerRequestCache = self.get_cache_for_peer(peer)
if trusted and fork_height is not None and height is not None and fork_height != height - 1:
# only one peer told us to rollback so only clear for that peer
await self.perform_atomic_rollback(fork_height, cache=cache)
else:
if fork_height is not None:
# only one peer told us to rollback so only clear for that peer
cache.clear_after_height(fork_height)
self.log.info(f"clear_after_height {fork_height} for peer {peer}")
all_tasks: List[asyncio.Task] = []
target_concurrent_tasks: int = 30
concurrent_tasks_cs_heights: List[uint32] = []
# Ensure the list is sorted
items = sorted(items_input, key=last_change_height_cs)
async def receive_and_validate(inner_states: List[CoinState], inner_idx_start: int, cs_heights: List[uint32]):
assert self.wallet_state_manager is not None
try:
assert self.validation_semaphore is not None
async with self.validation_semaphore:
if header_hash is not None:
assert height is not None
for inner_state in inner_states:
self.add_state_to_race_cache(header_hash, height, inner_state)
self.log.info(f"Added to race cache: {height}, {inner_state}")
valid_states = [
inner_state
for inner_state in inner_states
if await self.validate_received_state_from_peer(inner_state, peer, cache, fork_height)
]
if len(valid_states) > 0:
async with self.wallet_state_manager.db_wrapper.lock:
self.log.info(
f"new coin state received ({inner_idx_start}-"
f"{inner_idx_start + len(inner_states) - 1}/ {len(items)})"
)
if self.wallet_state_manager is None:
return
try:
await self.wallet_state_manager.db_wrapper.begin_transaction()
await self.wallet_state_manager.new_coin_state(valid_states, peer, fork_height)
if update_finished_height:
if len(cs_heights) == 1:
# We have processed all past tasks, so we can increase the height safely
synced_up_to = last_change_height_cs(valid_states[-1]) - 1
else:
# We know we have processed everything before this min height
synced_up_to = min(cs_heights) - 1
await self.wallet_state_manager.blockchain.set_finished_sync_up_to(
synced_up_to, in_transaction=True
)
await self.wallet_state_manager.db_wrapper.commit_transaction()
except Exception as e:
tb = traceback.format_exc()
self.log.error(f"Exception while adding state: {e} {tb}")
await self.wallet_state_manager.db_wrapper.rollback_transaction()
await self.wallet_state_manager.coin_store.rebuild_wallet_cache()
await self.wallet_state_manager.tx_store.rebuild_tx_cache()
await self.wallet_state_manager.pool_store.rebuild_cache()
else:
await self.wallet_state_manager.blockchain.clean_block_records()
except Exception as e:
tb = traceback.format_exc()
self.log.error(f"Exception while adding state: {e} {tb}")
finally:
cs_heights.remove(last_change_height_cs(inner_states[0]))
idx = 1
# Keep chunk size below 1000 just in case, windows has sqlite limits of 999 per query
# Untrusted has a smaller batch size since validation has to happen which takes a while
chunk_size: int = 900 if trusted else 20
for states in chunks(items, chunk_size):
if self.server is None:
self.log.error("No server")
await asyncio.gather(*all_tasks)
return False
if peer.peer_node_id not in self.server.all_connections:
self.log.error(f"Disconnected from peer {peer.peer_node_id} host {peer.peer_host}")
await asyncio.gather(*all_tasks)
return False
if trusted:
async with self.wallet_state_manager.db_wrapper.lock:
try:
self.log.info(f"new coin state received ({idx}-" f"{idx + len(states) - 1}/ {len(items)})")
await self.wallet_state_manager.db_wrapper.begin_transaction()
await self.wallet_state_manager.new_coin_state(states, peer, fork_height)
await self.wallet_state_manager.blockchain.set_finished_sync_up_to(
last_change_height_cs(states[-1]) - 1, in_transaction=True
)
await self.wallet_state_manager.db_wrapper.commit_transaction()
except Exception as e:
await self.wallet_state_manager.db_wrapper.rollback_transaction()
await self.wallet_state_manager.coin_store.rebuild_wallet_cache()
await self.wallet_state_manager.tx_store.rebuild_tx_cache()
await self.wallet_state_manager.pool_store.rebuild_cache()
tb = traceback.format_exc()
self.log.error(f"Error adding states.. {e} {tb}")
return False
else:
await self.wallet_state_manager.blockchain.clean_block_records()
else:
while len(concurrent_tasks_cs_heights) >= target_concurrent_tasks:
await asyncio.sleep(0.1)
if self._shut_down:
self.log.info("Terminating receipt and validation due to shut down request")
await asyncio.gather(*all_tasks)
return False
concurrent_tasks_cs_heights.append(last_change_height_cs(states[0]))
all_tasks.append(asyncio.create_task(receive_and_validate(states, idx, concurrent_tasks_cs_heights)))
idx += len(states)
still_connected = self.server is not None and peer.peer_node_id in self.server.all_connections
await asyncio.gather(*all_tasks)
await self.update_ui()
return still_connected and self.server is not None and peer.peer_node_id in self.server.all_connections
async def get_coins_with_puzzle_hash(self, puzzle_hash) -> List[CoinState]:
assert self.wallet_state_manager is not None
assert self.server is not None
all_nodes = self.server.connection_by_type[NodeType.FULL_NODE]
if len(all_nodes.keys()) == 0:
raise ValueError("Not connected to the full node")
first_node = list(all_nodes.values())[0]
msg = wallet_protocol.RegisterForPhUpdates(puzzle_hash, uint32(0))
coin_state: Optional[RespondToPhUpdates] = await first_node.register_interest_in_puzzle_hash(msg)
assert coin_state is not None
return coin_state.coin_states
async def is_peer_synced(
self, peer: WSChiaConnection, header_block: HeaderBlock, request_time: uint64
) -> Optional[uint64]:
# Get last timestamp
last_tx: Optional[HeaderBlock] = await fetch_last_tx_from_peer(header_block.height, peer)
latest_timestamp: Optional[uint64] = None
if last_tx is not None:
assert last_tx.foliage_transaction_block is not None
latest_timestamp = last_tx.foliage_transaction_block.timestamp
# Return None if not synced
if latest_timestamp is None or self.config["testing"] is False and latest_timestamp < request_time - 600:
return None
return latest_timestamp
def is_trusted(self, peer) -> bool:
assert self.server is not None
return self.server.is_trusted_peer(peer, self.config["trusted_peers"])
def add_state_to_race_cache(self, header_hash: bytes32, height: uint32, coin_state: CoinState) -> None:
# Clears old state that is no longer relevant
delete_threshold = 100
for rc_height, rc_hh in self.race_cache_hashes:
if height - delete_threshold >= rc_height:
self.race_cache.pop(rc_hh)
self.race_cache_hashes = [
(rc_height, rc_hh) for rc_height, rc_hh in self.race_cache_hashes if height - delete_threshold < rc_height
]
if header_hash not in self.race_cache:
self.race_cache[header_hash] = set()
self.race_cache[header_hash].add(coin_state)
async def state_update_received(self, request: wallet_protocol.CoinStateUpdate, peer: WSChiaConnection) -> None:
# This gets called every time there is a new coin or puzzle hash change in the DB
# that is of interest to this wallet. It is not guaranteed to come for every height. This message is guaranteed
# to come before the corresponding new_peak for each height. We handle this differently for trusted and
# untrusted peers. For trusted, we always process the state, and we process reorgs as well.
assert self.wallet_state_manager is not None
assert self.server is not None
for coin in request.items:
self.log.info(f"request coin: {coin.coin.name()}{coin}")
async with self.wallet_state_manager.lock:
await self.receive_state_from_peer(
request.items,
peer,
request.fork_height,
request.height,
request.peak_hash,
)
def get_full_node_peer(self) -> Optional[WSChiaConnection]:
if self.server is None:
return None
nodes = self.server.get_full_node_connections()
if len(nodes) > 0:
return random.choice(nodes)
else:
return None
async def disconnect_and_stop_wpeers(self) -> None:
if self.server is None:
return
# Close connection of non-trusted peers
if len(self.server.get_full_node_connections()) > 1:
for peer in self.server.get_full_node_connections():
if not self.is_trusted(peer):
await peer.close()
if self.wallet_peers is not None:
await self.wallet_peers.ensure_is_closed()
self.wallet_peers = None
async def check_for_synced_trusted_peer(self, header_block: HeaderBlock, request_time: uint64) -> bool:
if self.server is None:
return False
for peer in self.server.get_full_node_connections():
if self.is_trusted(peer) and await self.is_peer_synced(peer, header_block, request_time):
return True
return False
async def get_timestamp_for_height(self, height: uint32) -> uint64:
"""
Returns the timestamp for transaction block at h=height, if not transaction block, backtracks until it finds
a transaction block
"""
if height in self.height_to_time:
return self.height_to_time[height]
for cache in self.untrusted_caches.values():
cache_ts: Optional[uint64] = cache.get_height_timestamp(height)
if cache_ts is not None:
return cache_ts
peer: Optional[WSChiaConnection] = self.get_full_node_peer()
if peer is None:
raise ValueError("Cannot fetch timestamp, no peers")
self.log.debug(f"Fetching block at height: {height}")
last_tx_block: Optional[HeaderBlock] = await fetch_last_tx_from_peer(height, peer)
if last_tx_block is None:
raise ValueError(f"Error fetching blocks from peer {peer.get_peer_info()}")
assert last_tx_block.foliage_transaction_block is not None
self.get_cache_for_peer(peer).add_to_blocks(last_tx_block)
return last_tx_block.foliage_transaction_block.timestamp
async def new_peak_wallet(self, new_peak: wallet_protocol.NewPeakWallet, peer: WSChiaConnection):
if self.wallet_state_manager is None:
# When logging out of wallet
return
assert self.server is not None
request_time = uint64(int(time.time()))
trusted: bool = self.is_trusted(peer)
peak_hb: Optional[HeaderBlock] = await self.wallet_state_manager.blockchain.get_peak_block()
if peak_hb is not None and new_peak.weight < peak_hb.weight:
# Discards old blocks, but accepts blocks that are equal in weight to peak
return
request = wallet_protocol.RequestBlockHeader(new_peak.height)
response: Optional[RespondBlockHeader] = await peer.request_block_header(request)
if response is None:
self.log.warning(f"Peer {peer.get_peer_info()} did not respond in time.")
await peer.close(120)
return
header_block: HeaderBlock = response.header_block
latest_timestamp: Optional[uint64] = await self.is_peer_synced(peer, header_block, request_time)
if latest_timestamp is None:
if trusted:
self.log.debug(f"Trusted peer {peer.get_peer_info()} is not synced.")
return
else:
self.log.warning(f"Non-trusted peer {peer.get_peer_info()} is not synced, disconnecting")
await peer.close(120)
return
current_height: uint32 = await self.wallet_state_manager.blockchain.get_finished_sync_up_to()
if self.is_trusted(peer):
async with self.wallet_state_manager.lock:
await self.wallet_state_manager.blockchain.set_peak_block(header_block, latest_timestamp)
# Disconnect from all untrusted peers if our local node is trusted and synced
await self.disconnect_and_stop_wpeers()
# Sync to trusted node if we haven't done so yet. As long as we have synced once (and not
# disconnected), we assume that the full node will continue to give us state updates, so we do
# not need to resync.
if peer.peer_node_id not in self.synced_peers:
if new_peak.height - current_height > self.LONG_SYNC_THRESHOLD:
self.wallet_state_manager.set_sync_mode(True)
self._primary_peer_sync_task = asyncio.create_task(
self.long_sync(new_peak.height, peer, uint32(max(0, current_height - 256)), rollback=True)
)
await self._primary_peer_sync_task
self._primary_peer_sync_task = None
self.wallet_state_manager.set_sync_mode(False)
else:
far_behind: bool = (
new_peak.height - self.wallet_state_manager.blockchain.get_peak_height() > self.LONG_SYNC_THRESHOLD
)
# check if claimed peak is heavier or same as our current peak
# if we haven't synced fully to this peer sync again
if (
peer.peer_node_id not in self.synced_peers or far_behind
) and new_peak.height >= self.constants.WEIGHT_PROOF_RECENT_BLOCKS:
if await self.check_for_synced_trusted_peer(header_block, request_time):
self.wallet_state_manager.set_sync_mode(False)
self.log.info("Cancelling untrusted sync, we are connected to a trusted peer")
return
syncing = False
if far_behind or len(self.synced_peers) == 0:
syncing = True
self.wallet_state_manager.set_sync_mode(True)
try:
(
valid_weight_proof,
weight_proof,
summaries,
block_records,
) = await self.fetch_and_validate_the_weight_proof(peer, response.header_block)
if valid_weight_proof is False:
if syncing:
self.wallet_state_manager.set_sync_mode(False)
await peer.close()
return
if await self.check_for_synced_trusted_peer(header_block, request_time):
self.wallet_state_manager.set_sync_mode(False)
self.log.info("Cancelling untrusted sync, we are connected to a trusted peer")
return
assert weight_proof is not None
old_proof = self.wallet_state_manager.blockchain.synced_weight_proof
if syncing:
# This usually happens the first time we start up the wallet. We roll back slightly to be
# safe, but we don't want to rollback too much (hence 16)
fork_point: int = max(0, current_height - 16)
else:
# In this case we will not rollback so it's OK to check some older updates as well, to ensure
# that no recent transactions are being hidden.
fork_point = 0
if old_proof is not None:
# If the weight proof fork point is in the past, rollback more to ensure we don't have duplicate
# state.
wp_fork_point = self.wallet_state_manager.weight_proof_handler.get_fork_point(
old_proof, weight_proof
)
fork_point = min(fork_point, wp_fork_point)
await self.wallet_state_manager.blockchain.new_weight_proof(weight_proof, block_records)
if syncing:
async with self.wallet_state_manager.lock:
self.log.info("Primary peer syncing")
self._primary_peer_sync_task = asyncio.create_task(
self.long_sync(new_peak.height, peer, fork_point, rollback=True)
)
await self._primary_peer_sync_task
self._primary_peer_sync_task = None
else:
if self._secondary_peer_sync_task is None or self._secondary_peer_sync_task.done():
self.log.info("Secondary peer syncing")
self._secondary_peer_sync_task = asyncio.create_task(
self.long_sync(new_peak.height, peer, fork_point, rollback=False)
)
return
else:
self.log.info("Will not do secondary sync, there is already another sync task running.")
return
self.log.info(f"New peak wallet.. {new_peak.height} {peer.get_peer_info()} 12")
if (
self.wallet_state_manager.blockchain.synced_weight_proof is None
or weight_proof.recent_chain_data[-1].weight
> self.wallet_state_manager.blockchain.synced_weight_proof.recent_chain_data[-1].weight
):
await self.wallet_state_manager.blockchain.new_weight_proof(weight_proof, block_records)
except Exception as e:
tb = traceback.format_exc()
self.log.error(f"Error syncing to {peer.get_peer_info()} {e} {tb}")
if syncing:
self.wallet_state_manager.set_sync_mode(False)
tb = traceback.format_exc()
self.log.error(f"Error syncing to {peer.get_peer_info()} {tb}")
await peer.close()
return
if syncing:
self.wallet_state_manager.set_sync_mode(False)
else:
# This is the (untrusted) case where we already synced and are not too far behind. Here we just
# fetch one by one.
async with self.wallet_state_manager.lock:
peak_hb = await self.wallet_state_manager.blockchain.get_peak_block()
if peak_hb is None or new_peak.weight > peak_hb.weight:
backtrack_fork_height: int = await self.wallet_short_sync_backtrack(header_block, peer)
else:
backtrack_fork_height = new_peak.height - 1
if peer.peer_node_id not in self.synced_peers:
# Edge case, this happens when the peak < WEIGHT_PROOF_RECENT_BLOCKS
# we still want to subscribe for all phs and coins.
# (Hints are not in filter)
all_coin_ids: List[bytes32] = await self.get_coin_ids_to_subscribe(uint32(0))
phs: List[bytes32] = await self.get_puzzle_hashes_to_subscribe()
ph_updates: List[CoinState] = await subscribe_to_phs(phs, peer, uint32(0))
coin_updates: List[CoinState] = await subscribe_to_coin_updates(all_coin_ids, peer, uint32(0))
peer_new_peak_height, peer_new_peak_hash = self.node_peaks[peer.peer_node_id]
success = await self.receive_state_from_peer(
ph_updates + coin_updates,
peer,
height=peer_new_peak_height,
header_hash=peer_new_peak_hash,
)
if success:
self.synced_peers.add(peer.peer_node_id)
else:
if peak_hb is not None and new_peak.weight <= peak_hb.weight:
# Don't process blocks at the same weight
return
# For every block, we need to apply the cache from race_cache
for potential_height in range(backtrack_fork_height + 1, new_peak.height + 1):
header_hash = self.wallet_state_manager.blockchain.height_to_hash(uint32(potential_height))
if header_hash in self.race_cache:
self.log.info(f"Receiving race state: {self.race_cache[header_hash]}")
await self.receive_state_from_peer(list(self.race_cache[header_hash]), peer)
self.wallet_state_manager.state_changed("new_block")
self.wallet_state_manager.set_sync_mode(False)
self.log.info(f"Finished processing new peak of {new_peak.height}")
if peer.peer_node_id in self.synced_peers:
await self.wallet_state_manager.blockchain.set_finished_sync_up_to(new_peak.height)
async with self.wallet_state_manager.lock:
await self.wallet_state_manager.new_peak(new_peak)
async def wallet_short_sync_backtrack(self, header_block: HeaderBlock, peer: WSChiaConnection) -> int:
assert self.wallet_state_manager is not None
peak: Optional[HeaderBlock] = await self.wallet_state_manager.blockchain.get_peak_block()
top = header_block
blocks = [top]
# Fetch blocks backwards until we hit the one that we have,
# then complete them with additions / removals going forward
fork_height = 0
if self.wallet_state_manager.blockchain.contains_block(header_block.prev_header_hash):
fork_height = header_block.height - 1
while not self.wallet_state_manager.blockchain.contains_block(top.prev_header_hash) and top.height > 0:
request_prev = wallet_protocol.RequestBlockHeader(top.height - 1)
response_prev: Optional[RespondBlockHeader] = await peer.request_block_header(request_prev)
if response_prev is None or not isinstance(response_prev, RespondBlockHeader):
raise RuntimeError("bad block header response from peer while syncing")
prev_head = response_prev.header_block
blocks.append(prev_head)
top = prev_head
fork_height = top.height - 1
blocks.reverse()
# Roll back coins and transactions
peak_height = self.wallet_state_manager.blockchain.get_peak_height()
if fork_height < peak_height:
self.log.info(f"Rolling back to {fork_height}")
# we should clear all peers since this is a full rollback
await self.perform_atomic_rollback(fork_height)
await self.update_ui()
if peak is not None:
assert header_block.weight >= peak.weight
for block in blocks:
# Set blockchain to the latest peak
res, err = await self.wallet_state_manager.blockchain.receive_block(block)
if res == ReceiveBlockResult.INVALID_BLOCK:
raise ValueError(err)
return fork_height
async def update_ui(self):
for wallet_id, wallet in self.wallet_state_manager.wallets.items():
self.wallet_state_manager.state_changed("coin_removed", wallet_id)
self.wallet_state_manager.state_changed("coin_added", wallet_id)
async def fetch_and_validate_the_weight_proof(
self, peer: WSChiaConnection, peak: HeaderBlock
) -> Tuple[bool, Optional[WeightProof], List[SubEpochSummary], List[BlockRecord]]:
assert self.wallet_state_manager is not None
assert self.wallet_state_manager.weight_proof_handler is not None
weight_request = RequestProofOfWeight(peak.height, peak.header_hash)
wp_timeout = self.config.get("weight_proof_timeout", 360)
self.log.debug(f"weight proof timeout is {wp_timeout} sec")
weight_proof_response: RespondProofOfWeight = await peer.request_proof_of_weight(
weight_request, timeout=wp_timeout
)
if weight_proof_response is None:
return False, None, [], []
start_validation = time.time()
weight_proof = weight_proof_response.wp
if weight_proof.recent_chain_data[-1].reward_chain_block.height != peak.height:
return False, None, [], []
if weight_proof.recent_chain_data[-1].reward_chain_block.weight != peak.weight:
return False, None, [], []
if weight_proof.get_hash() in self.valid_wp_cache:
valid, fork_point, summaries, block_records = self.valid_wp_cache[weight_proof.get_hash()]
else:
start_validation = time.time()
(
valid,
fork_point,
summaries,
block_records,
) = await self.wallet_state_manager.weight_proof_handler.validate_weight_proof(weight_proof)
if valid:
self.valid_wp_cache[weight_proof.get_hash()] = valid, fork_point, summaries, block_records
end_validation = time.time()
self.log.info(f"It took {end_validation - start_validation} time to validate the weight proof")
return valid, weight_proof, summaries, block_records
async def get_puzzle_hashes_to_subscribe(self) -> List[bytes32]:
assert self.wallet_state_manager is not None
all_puzzle_hashes = list(await self.wallet_state_manager.puzzle_store.get_all_puzzle_hashes())
# Get all phs from interested store
interested_puzzle_hashes = [
t[0] for t in await self.wallet_state_manager.interested_store.get_interested_puzzle_hashes()
]
all_puzzle_hashes.extend(interested_puzzle_hashes)
return all_puzzle_hashes
async def get_coin_ids_to_subscribe(self, min_height: int) -> List[bytes32]:
assert self.wallet_state_manager is not None
all_coins: Set[WalletCoinRecord] = await self.wallet_state_manager.coin_store.get_coins_to_check(min_height)
all_coin_names: Set[bytes32] = {coin_record.name() for coin_record in all_coins}
removed_dict = await self.wallet_state_manager.trade_manager.get_coins_of_interest()
all_coin_names.update(removed_dict.keys())
all_coin_names.update(await self.wallet_state_manager.interested_store.get_interested_coin_ids())
return list(all_coin_names)
async def validate_received_state_from_peer(
self,
coin_state: CoinState,
peer: WSChiaConnection,
peer_request_cache: PeerRequestCache,
fork_height: Optional[uint32],
) -> bool:
"""
Returns all state that is valid and included in the blockchain proved by the weight proof. If return_old_states
is False, only new states that are not in the coin_store are returned.
"""
assert self.wallet_state_manager is not None
# Only use the cache if we are talking about states before the fork point. If we are evaluating something
# in a reorg, we cannot use the cache, since we don't know if it's actually in the new chain after the reorg.
if await can_use_peer_request_cache(coin_state, peer_request_cache, fork_height):
return True
spent_height = coin_state.spent_height
confirmed_height = coin_state.created_height
current = await self.wallet_state_manager.coin_store.get_coin_record(coin_state.coin.name())
# if remote state is same as current local state we skip validation
# CoinRecord unspent = height 0, coin state = None. We adjust for comparison below
current_spent_height = None
if current is not None and current.spent_block_height != 0:
current_spent_height = current.spent_block_height
# Same as current state, nothing to do
if (
current is not None
and current_spent_height == spent_height
and current.confirmed_block_height == confirmed_height
):
peer_request_cache.add_to_states_validated(coin_state)
return True
reorg_mode = False
# If coin was removed from the blockchain
if confirmed_height is None:
if current is None:
# Coin does not exist in local DB, so no need to do anything
return False
# This coin got reorged
reorg_mode = True
confirmed_height = current.confirmed_block_height
# request header block for created height
state_block: Optional[HeaderBlock] = peer_request_cache.get_block(confirmed_height)
if state_block is None or reorg_mode:
state_blocks = await request_header_blocks(peer, confirmed_height, confirmed_height)
if state_blocks is None:
return False
state_block = state_blocks[0]
assert state_block is not None
peer_request_cache.add_to_blocks(state_block)
# get proof of inclusion
assert state_block.foliage_transaction_block is not None
validate_additions_result = await request_and_validate_additions(
peer,
peer_request_cache,
state_block.height,
state_block.header_hash,
coin_state.coin.puzzle_hash,
state_block.foliage_transaction_block.additions_root,
)
if validate_additions_result is False:
self.log.warning("Validate false 1")
await peer.close(9999)
return False
# If spent_height is None, we need to validate that the creation block is actually in the longest blockchain.
# Otherwise, we don't have to, since we will validate the spent block later.
if coin_state.spent_height is None:
validated = await self.validate_block_inclusion(state_block, peer, peer_request_cache)
if not validated:
return False
# TODO: make sure all cases are covered
if current is not None:
if spent_height is None and current.spent_block_height != 0:
# Peer is telling us that coin that was previously known to be spent is not spent anymore
# Check old state
spent_state_blocks: Optional[List[HeaderBlock]] = await request_header_blocks(
peer, current.spent_block_height, current.spent_block_height
)
if spent_state_blocks is None:
return False
spent_state_block = spent_state_blocks[0]
assert spent_state_block.height == current.spent_block_height
assert spent_state_block.foliage_transaction_block is not None
peer_request_cache.add_to_blocks(spent_state_block)
validate_removals_result: bool = await request_and_validate_removals(
peer,
current.spent_block_height,
spent_state_block.header_hash,
coin_state.coin.name(),
spent_state_block.foliage_transaction_block.removals_root,
)
if validate_removals_result is False:
self.log.warning("Validate false 2")
await peer.close(9999)
return False
validated = await self.validate_block_inclusion(spent_state_block, peer, peer_request_cache)
if not validated:
return False
if spent_height is not None:
# request header block for created height
cached_spent_state_block = peer_request_cache.get_block(spent_height)
if cached_spent_state_block is None:
spent_state_blocks = await request_header_blocks(peer, spent_height, spent_height)
if spent_state_blocks is None:
return False
spent_state_block = spent_state_blocks[0]
assert spent_state_block.height == spent_height
assert spent_state_block.foliage_transaction_block is not None
peer_request_cache.add_to_blocks(spent_state_block)
else:
spent_state_block = cached_spent_state_block
assert spent_state_block is not None
assert spent_state_block.foliage_transaction_block is not None
validate_removals_result = await request_and_validate_removals(
peer,
spent_state_block.height,
spent_state_block.header_hash,
coin_state.coin.name(),
spent_state_block.foliage_transaction_block.removals_root,
)
if validate_removals_result is False:
self.log.warning("Validate false 3")
await peer.close(9999)
return False
validated = await self.validate_block_inclusion(spent_state_block, peer, peer_request_cache)
if not validated:
return False
peer_request_cache.add_to_states_validated(coin_state)
return True
async def validate_block_inclusion(
self, block: HeaderBlock, peer: WSChiaConnection, peer_request_cache: PeerRequestCache
) -> bool:
assert self.wallet_state_manager is not None
assert self.server is not None
if self.wallet_state_manager.blockchain.contains_height(block.height):
stored_hash = self.wallet_state_manager.blockchain.height_to_hash(block.height)
stored_record = self.wallet_state_manager.blockchain.try_block_record(stored_hash)
if stored_record is not None:
if stored_record.header_hash == block.header_hash:
return True
weight_proof: Optional[WeightProof] = self.wallet_state_manager.blockchain.synced_weight_proof
if weight_proof is None:
return False
if block.height >= weight_proof.recent_chain_data[0].height:
# this was already validated as part of the wp validation
index = block.height - weight_proof.recent_chain_data[0].height
if index >= len(weight_proof.recent_chain_data):
return False
if weight_proof.recent_chain_data[index].header_hash != block.header_hash:
self.log.error("Failed validation 1")
return False
return True
else:
start = block.height + 1
compare_to_recent = False
current_ses: Optional[SubEpochData] = None
inserted: Optional[SubEpochData] = None
first_height_recent = weight_proof.recent_chain_data[0].height
if start > first_height_recent - 1000:
compare_to_recent = True
end = first_height_recent
else:
if block.height < self.constants.SUB_EPOCH_BLOCKS:
inserted = weight_proof.sub_epochs[1]
end = self.constants.SUB_EPOCH_BLOCKS + inserted.num_blocks_overflow
else:
request = RequestSESInfo(block.height, block.height + 32)
res_ses: Optional[RespondSESInfo] = peer_request_cache.get_ses_request(block.height)
if res_ses is None:
res_ses = await peer.request_ses_hashes(request)
peer_request_cache.add_to_ses_requests(block.height, res_ses)
assert res_ses is not None
ses_0 = res_ses.reward_chain_hash[0]
last_height = res_ses.heights[0][-1] # Last height in sub epoch
end = last_height
num_sub_epochs = len(weight_proof.sub_epochs)
for idx, ses in enumerate(weight_proof.sub_epochs):
if idx > num_sub_epochs - 3:
break
if ses.reward_chain_hash == ses_0:
current_ses = ses
inserted = weight_proof.sub_epochs[idx + 2]
break
if current_ses is None:
self.log.error("Failed validation 2")
return False
all_peers = self.server.get_full_node_connections()
blocks: Optional[List[HeaderBlock]] = await fetch_header_blocks_in_range(
start, end, peer_request_cache, all_peers
)
if blocks is None:
self.log.error(f"Error fetching blocks {start} {end}")
return False
if compare_to_recent and weight_proof.recent_chain_data[0].header_hash != blocks[-1].header_hash:
self.log.error("Failed validation 3")
return False
if not compare_to_recent:
last = blocks[-1].finished_sub_slots[-1].reward_chain.get_hash()
if inserted is None or last != inserted.reward_chain_hash:
self.log.error("Failed validation 4")
return False
pk_m_sig: List[Tuple[G1Element, bytes32, G2Element]] = []
sigs_to_cache: List[HeaderBlock] = []
blocks_to_cache: List[Tuple[bytes32, uint32]] = []
signatures_to_validate: int = 30
for idx in range(len(blocks)):
en_block = blocks[idx]
if idx < signatures_to_validate and not peer_request_cache.in_block_signatures_validated(en_block):
# Validate that the block is buried in the foliage by checking the signatures
pk_m_sig.append(
(
en_block.reward_chain_block.proof_of_space.plot_public_key,
en_block.foliage.foliage_block_data.get_hash(),
en_block.foliage.foliage_block_data_signature,
)
)
sigs_to_cache.append(en_block)
# This is the reward chain challenge. If this is in the cache, it means the prev block
# has been validated. We must at least check the first block to ensure they are connected
reward_chain_hash: bytes32 = en_block.reward_chain_block.reward_chain_ip_vdf.challenge
if idx != 0 and peer_request_cache.in_blocks_validated(reward_chain_hash):
# As soon as we see a block we have already concluded is in the chain, we can quit.
if idx > signatures_to_validate:
break
else:
# Validate that the block is committed to by the weight proof
if idx == 0:
prev_block_rc_hash: bytes32 = block.reward_chain_block.get_hash()
prev_hash = block.header_hash
else:
prev_block_rc_hash = blocks[idx - 1].reward_chain_block.get_hash()
prev_hash = blocks[idx - 1].header_hash
if not en_block.prev_header_hash == prev_hash:
self.log.error("Failed validation 5")
return False
if len(en_block.finished_sub_slots) > 0:
reversed_slots = en_block.finished_sub_slots.copy()
reversed_slots.reverse()
for slot_idx, slot in enumerate(reversed_slots[:-1]):
hash_val = reversed_slots[slot_idx + 1].reward_chain.get_hash()
if not hash_val == slot.reward_chain.end_of_slot_vdf.challenge:
self.log.error("Failed validation 6")
return False
if not prev_block_rc_hash == reversed_slots[-1].reward_chain.end_of_slot_vdf.challenge:
self.log.error("Failed validation 7")
return False
else:
if not prev_block_rc_hash == reward_chain_hash:
self.log.error("Failed validation 8")
return False
blocks_to_cache.append((reward_chain_hash, en_block.height))
agg_sig: G2Element = AugSchemeMPL.aggregate([sig for (_, _, sig) in pk_m_sig])
if not AugSchemeMPL.aggregate_verify(
[pk for (pk, _, _) in pk_m_sig], [m for (_, m, _) in pk_m_sig], agg_sig
):
self.log.error("Failed signature validation")
return False
for header_block in sigs_to_cache:
peer_request_cache.add_to_block_signatures_validated(header_block)
for reward_chain_hash, height in blocks_to_cache:
peer_request_cache.add_to_blocks_validated(reward_chain_hash, height)
return True
async def fetch_puzzle_solution(self, peer: WSChiaConnection, height: uint32, coin: Coin) -> CoinSpend:
solution_response = await peer.request_puzzle_solution(
wallet_protocol.RequestPuzzleSolution(coin.name(), height)
)
if solution_response is None or not isinstance(solution_response, wallet_protocol.RespondPuzzleSolution):
raise ValueError(f"Was not able to obtain solution {solution_response}")
assert solution_response.response.puzzle.get_tree_hash() == coin.puzzle_hash
assert solution_response.response.coin_name == coin.name()
return CoinSpend(
coin,
solution_response.response.puzzle,
solution_response.response.solution,
)
async def get_coin_state(
self, coin_names: List[bytes32], fork_height: Optional[uint32] = None, peer: Optional[WSChiaConnection] = None
) -> List[CoinState]:
assert self.server is not None
all_nodes = self.server.connection_by_type[NodeType.FULL_NODE]
if len(all_nodes.keys()) == 0:
raise ValueError("Not connected to the full node")
# Use supplied if provided, prioritize trusted otherwise
if peer is None:
for node in list(all_nodes.values()):
if self.is_trusted(node):
peer = node
break
if peer is None:
peer = list(all_nodes.values())[0]
assert peer is not None
msg = wallet_protocol.RegisterForCoinUpdates(coin_names, uint32(0))
coin_state: Optional[RespondToCoinUpdates] = await peer.register_interest_in_coin(msg)
assert coin_state is not None
if not self.is_trusted(peer):
valid_list = []
for coin in coin_state.coin_states:
valid = await self.validate_received_state_from_peer(
coin, peer, self.get_cache_for_peer(peer), fork_height
)
if valid:
valid_list.append(coin)
return valid_list
return coin_state.coin_states
async def fetch_children(
self, peer: WSChiaConnection, coin_name: bytes32, fork_height: Optional[uint32] = None
) -> List[CoinState]:
response: Optional[wallet_protocol.RespondChildren] = await peer.request_children(
wallet_protocol.RequestChildren(coin_name)
)
if response is None or not isinstance(response, wallet_protocol.RespondChildren):
raise ValueError(f"Was not able to obtain children {response}")
if not self.is_trusted(peer):
request_cache = self.get_cache_for_peer(peer)
validated = []
for state in response.coin_states:
valid = await self.validate_received_state_from_peer(state, peer, request_cache, fork_height)
if valid:
validated.append(state)
return validated
return response.coin_states
# For RPC only. You should use wallet_state_manager.add_pending_transaction for normal wallet business.
async def push_tx(self, spend_bundle):
msg = make_msg(
ProtocolMessageTypes.send_transaction,
wallet_protocol.SendTransaction(spend_bundle),
)
full_nodes = self.server.get_full_node_connections()
for peer in full_nodes:
await peer.send_message(msg)