diff --git a/hyperware-wit/hypermap-cacher:sys-v0.wit b/hyperware-wit/hypermap-cacher:sys-v0.wit new file mode 100644 index 0000000..8219ca8 --- /dev/null +++ b/hyperware-wit/hypermap-cacher:sys-v0.wit @@ -0,0 +1,69 @@ +interface hypermap-cacher { + // Metadata associated with a batch of Ethereum logs. + record logs-metadata { + chain-id: string, + from-block: string, + to-block: string, + time-created: string, + created-by: string, + signature: string, + } + + // Represents an item in the manifest, detailing a single log cache file. + record manifest-item { + metadata: logs-metadata, + is-empty: bool, + file-hash: string, + file-name: string, + } + + // The main manifest structure, listing all available log cache files. + // WIT does not support direct map types, so a list of key-value tuples is used. + record manifest { + // The key is the filename of the log cache. + items: list>, + manifest-filename: string, + chain-id: string, + protocol-version: string, + } + + record get-logs-by-range-request { + from-block: u64, + to-block: option, // If None, signifies to the latest available/relevant cached block. + } + + // Defines the types of requests that can be sent to the Hypermap Cacher process. + variant cacher-request { + get-manifest, + get-log-cache-content(string), + get-status, + get-logs-by-range(get-logs-by-range-request), + // // Request to trigger a cache update manually (optional, primarily timer-driven). + // trigger-cache-update, + } + + // Represents the operational status of the cacher. + record cacher-status { + last-cached-block: u64, + chain-id: string, + protocol-version: string, + next-cache-attempt-in-seconds: option, + manifest-filename: string, + log-files-count: u32, + our-address: string, + } + + // Defines the types of responses the Hypermap Cacher process can send. + variant cacher-response { + get-manifest(option), + get-log-cache-content(result, string>), + get-status(cacher-status), + get-logs-by-range(result), + } +} + +world hypermap-cacher-sys-v0 { + import sign; + import hypermap-cacher; + include process-v1; +} diff --git a/hyperware-wit/process-lib.wit b/hyperware-wit/process-lib.wit new file mode 100644 index 0000000..834f96b --- /dev/null +++ b/hyperware-wit/process-lib.wit @@ -0,0 +1,5 @@ +world process-lib { + import sign; + import hypermap-cacher; + include lib; +} diff --git a/hyperware-wit/sign:sys-v0.wit b/hyperware-wit/sign:sys-v0.wit new file mode 100644 index 0000000..9d89bab --- /dev/null +++ b/hyperware-wit/sign:sys-v0.wit @@ -0,0 +1,61 @@ +interface sign { + use standard.{address}; + + variant request { + /// Request to sign the message given in blob with net key. + /// + /// lazy-load-blob: required; the message to sign. + net-key-sign, + /// Request to verify the message given in blob with net key. + /// + /// lazy-load-blob: required; the message to verify. + net-key-verify(net-key-verify-request), + /// Request to transform the message to the form that is signed with net key. + /// For use by outside verifiers (net-key-verify transforms naked message + /// properly under-the-hood). + /// + /// lazy-load-blob: required; the message to transform. + net-key-make-message, + } + + variant response { + /// Response containing the net key signature in blob. + /// The source (address) will always be prepended to the payload. + /// The source (address) of sign:sign:sys will also be prepended. + /// Thus the message signed looks like + /// [sign-address, address, blob.bytes].concat() + /// + /// Using request::net-key-verify handles the concatenation under-the-hood, + /// but verifying the signature will require the proper transformation of + /// the message. + /// + /// lazy-load-blob: required; signature. + net-key-sign, + /// Response: whether the net key signature is valid. + /// + /// lazy-load-blob: none. + net-key-verify(bool), + /// Response containing modified message in blob. + /// The source (address) will always be prepended to the payload. + /// The source (address) of sign:sign:sys will also be prepended. + /// Thus the message signed looks like + /// [sign-address, address, blob.bytes].concat() + /// + /// Using request::net-key-verify handles the concatenation under-the-hood, + /// but verifying the signature will require the proper transformation of + /// the message. + /// + /// lazy-load-blob: required; the transformed message. + net-key-make-message, + } + + record net-key-verify-request { + node: string, + signature: list, + } +} + +world sign-sys-v0 { + import sign; + include process-v1; +} diff --git a/src/eth.rs b/src/eth.rs index 488e19b..19395fb 100644 --- a/src/eth.rs +++ b/src/eth.rs @@ -318,6 +318,11 @@ impl Provider { request_timeout, } } + + pub fn get_chain_id(&self) -> u64 { + self.chain_id + } + /// Sends a request based on the specified [`EthAction`] and parses the response. /// /// This function constructs a request targeting the Ethereum distribution system, serializes the provided [`EthAction`], diff --git a/src/hypermap.rs b/src/hypermap.rs index a7c3c29..d533129 100644 --- a/src/hypermap.rs +++ b/src/hypermap.rs @@ -1,12 +1,25 @@ -use crate::eth::{EthError, Provider}; +use crate::eth::{ + BlockNumberOrTag, EthError, Filter as EthFilter, FilterBlockOption, Log as EthLog, Provider, +}; use crate::hypermap::contract::getCall; -use crate::net; +use crate::hyperware::process::hypermap_cacher::{ + CacherRequest, CacherResponse, CacherStatus, GetLogsByRangeRequest, LogsMetadata, Manifest, + ManifestItem, +}; +use crate::{net, sign}; +use crate::{print_to_terminal, Address as HyperAddress, Request}; +use alloy::hex; use alloy::rpc::types::request::{TransactionInput, TransactionRequest}; -use alloy::{hex, primitives::keccak256}; -use alloy_primitives::{Address, Bytes, FixedBytes, B256}; +use alloy_primitives::{keccak256, Address, Bytes, FixedBytes, B256}; use alloy_sol_types::{SolCall, SolEvent, SolValue}; use contract::tokenCall; -use serde::{Deserialize, Serialize}; +use serde::{ + self, + de::{self, MapAccess, Visitor}, + ser::{SerializeMap, SerializeStruct}, + Deserialize, Deserializer, Serialize, Serializer, +}; +use std::collections::HashSet; use std::error::Error; use std::fmt; use std::str::FromStr; @@ -27,6 +40,15 @@ pub const HYPERMAP_FIRST_BLOCK: u64 = 0; pub const HYPERMAP_ROOT_HASH: &'static str = "0x0000000000000000000000000000000000000000000000000000000000000000"; +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct LogCache { + pub metadata: LogsMetadata, + pub logs: Vec, +} + +const DEFAULT_CACHER_NODES: [&str; 0] = []; +const CACHER_REQUEST_TIMEOUT_S: u64 = 15; + /// Sol structures for Hypermap requests pub mod contract { use alloy_sol_macro::sol; @@ -290,10 +312,7 @@ impl fmt::Display for DecodeLogError { impl Error for DecodeLogError {} -/// Canonical function to determine if a hypermap entry is valid. This should -/// be used whenever reading a new hypermap entry from a mints query, because -/// while most frontends will enforce these rules, it is possible to post -/// invalid names to the hypermap contract. +/// Canonical function to determine if a hypermap entry is valid. /// /// This checks a **single name**, not the full path-name. A full path-name /// is comprised of valid names separated by `.` @@ -454,6 +473,80 @@ pub fn resolve_full_name(log: &crate::eth::Log, timeout: Option) -> Option< Some(format!("{name}.{parent_name}")) } +pub fn eth_apply_filter(logs: &[EthLog], filter: &EthFilter) -> anyhow::Result> { + let mut matched_logs = Vec::new(); + + let (filter_from_block, filter_to_block) = match filter.block_option { + FilterBlockOption::Range { + from_block, + to_block, + } => { + let parse_block_num = |bn: Option| -> Option { + match bn { + Some(BlockNumberOrTag::Number(n)) => Some(n), + _ => None, + } + }; + (parse_block_num(from_block), parse_block_num(to_block)) + } + _ => (None, None), + }; + + for log in logs.iter() { + let mut match_address = filter.address.is_empty(); + if !match_address { + if filter.address.matches(&log.address()) { + match_address = true; + } + } + if !match_address { + continue; + } + + if let Some(log_bn) = log.block_number { + if let Some(filter_from) = filter_from_block { + if log_bn < filter_from { + continue; + } + } + if let Some(filter_to) = filter_to_block { + if log_bn > filter_to { + continue; + } + } + } else { + if filter_from_block.is_some() || filter_to_block.is_some() { + continue; + } + } + + let mut match_topics = true; + for (i, filter_topic_alternatives) in filter.topics.iter().enumerate() { + if filter_topic_alternatives.is_empty() { + continue; + } + + let log_topic = log.topics().get(i); + let mut current_topic_matched = false; + for filter_topic in filter_topic_alternatives.iter() { + if log_topic == Some(filter_topic) { + current_topic_matched = true; + break; + } + } + if !current_topic_matched { + match_topics = false; + break; + } + } + + if match_topics { + matched_logs.push(log.clone()); + } + } + Ok(matched_logs) +} + /// Helper struct for reading from the hypermap. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Hypermap { @@ -619,4 +712,1012 @@ impl Hypermap { .collect::>(), ) } + + pub fn get_bootstrap_log_cache( + &self, + from_block: Option, + mut nodes: HashSet, + chain: Option, + ) -> anyhow::Result> { + print_to_terminal(2, + &format!("get_bootstrap_log_cache (using GetLogsByRange): from_block={:?}, nodes={:?}, chain={:?}", + from_block, nodes, chain) + ); + + for default_node in DEFAULT_CACHER_NODES.iter() { + nodes.insert(default_node.to_string()); + } + + if nodes.is_empty() { + print_to_terminal( + 1, + "get_bootstrap_log_cache: No cacher nodes specified or defaulted.", + ); + return Ok(Vec::new()); + } + + let target_chain_id = chain.unwrap_or_else(|| self.provider.get_chain_id().to_string()); + let mut all_retrieved_log_caches: Vec = Vec::new(); + let request_from_block_val = from_block.unwrap_or(0); + + for node_address_str in nodes { + let cacher_process_address = HyperAddress::new( + &node_address_str, + ("hypermap-cacher", "hypermap-cacher", "sys"), + ); + + print_to_terminal( + 2, + &format!( + "Querying cacher node with GetLogsByRange: {}", + cacher_process_address.to_string(), + ), + ); + + let get_logs_by_range_payload = GetLogsByRangeRequest { + from_block: request_from_block_val, + to_block: None, // Request all logs from from_block onwards. Cacher will return what it has. + }; + let cacher_request = CacherRequest::GetLogsByRange(get_logs_by_range_payload); + + let response_msg = Request::to(cacher_process_address.clone()) + .body(serde_json::to_vec(&cacher_request)?) + .send_and_await_response(CACHER_REQUEST_TIMEOUT_S)? + .map_err(|e| { + anyhow::anyhow!( + "Error response from cacher {} for GetLogsByRange: {:?}", + cacher_process_address.to_string(), + e + ) + })?; + + let logs_by_range_result = + match serde_json::from_slice::(response_msg.body())? { + CacherResponse::GetLogsByRange(res) => res, + _ => { + print_to_terminal( + 1, + &format!( + "Unexpected response type from cacher {} for GetLogsByRange", + cacher_process_address.to_string(), + ), + ); + continue; + } + }; + + match logs_by_range_result { + Ok(json_string_of_vec_log_cache) => { + if json_string_of_vec_log_cache.is_empty() + || json_string_of_vec_log_cache == "[]" + { + print_to_terminal( + 2, + &format!( + "Cacher {} returned no log caches for the range from block {}.", + cacher_process_address.to_string(), + request_from_block_val, + ), + ); + continue; + } + match serde_json::from_str::>(&json_string_of_vec_log_cache) { + Ok(retrieved_caches) => { + for log_cache in retrieved_caches { + if log_cache.metadata.chain_id == target_chain_id { + // Further filter: ensure the cache's own from_block isn't completely after what we need, + // and to_block isn't completely before. + // The GetLogsByRange on cacher side should handle this, but double check. + let cache_from = log_cache + .metadata + .from_block + .parse::() + .unwrap_or(u64::MAX); + let cache_to = + log_cache.metadata.to_block.parse::().unwrap_or(0); + + if cache_to >= request_from_block_val { + // Cache has some data at or after our request_from_block + all_retrieved_log_caches.push(log_cache); + } else { + print_to_terminal(3, &format!("Cache from {} ({} to {}) does not meet request_from_block {}", + cacher_process_address.to_string(), cache_from, cache_to, request_from_block_val)); + } + } else { + print_to_terminal(1,&format!("LogCache from {} has mismatched chain_id (expected {}, got {}). Skipping.", + cacher_process_address.to_string(), target_chain_id, log_cache.metadata.chain_id)); + } + } + } + Err(e) => { + print_to_terminal(1,&format!("Failed to deserialize Vec from cacher {}: {:?}. JSON: {:.100}", + cacher_process_address.to_string(), e, json_string_of_vec_log_cache)); + } + } + } + Err(e_str) => { + print_to_terminal( + 1, + &format!( + "Cacher {} reported error for GetLogsByRange: {}", + cacher_process_address.to_string(), + e_str, + ), + ); + } + } + } + print_to_terminal( + 2, + &format!( + "Retrieved {} log caches in total using GetLogsByRange.", + all_retrieved_log_caches.len(), + ), + ); + Ok(all_retrieved_log_caches) + } + + pub fn validate_log_cache(&self, log_cache: &LogCache) -> anyhow::Result { + let from_block = log_cache.metadata.from_block.parse::().map_err(|_| { + anyhow::anyhow!( + "Invalid from_block in metadata: {}", + log_cache.metadata.from_block + ) + })?; + let to_block = log_cache.metadata.to_block.parse::().map_err(|_| { + anyhow::anyhow!( + "Invalid to_block in metadata: {}", + log_cache.metadata.to_block + ) + })?; + + let mut bytes_to_verify = serde_json::to_vec(&log_cache.logs) + .map_err(|e| anyhow::anyhow!("Failed to serialize logs for validation: {:?}", e))?; + bytes_to_verify.extend_from_slice(&from_block.to_be_bytes()); + bytes_to_verify.extend_from_slice(&to_block.to_be_bytes()); + let hashed_data = keccak256(&bytes_to_verify); + + let signature_hex = log_cache.metadata.signature.trim_start_matches("0x"); + let signature_bytes = hex::decode(signature_hex) + .map_err(|e| anyhow::anyhow!("Failed to decode hex signature: {:?}", e))?; + + Ok(sign::net_key_verify( + hashed_data.to_vec(), + &log_cache.metadata.created_by.parse::()?, + signature_bytes, + )?) + } + + pub fn get_bootstrap( + &self, + from_block: Option, + nodes: HashSet, + chain: Option, + ) -> anyhow::Result> { + print_to_terminal( + 2, + &format!( + "get_bootstrap: from_block={:?}, nodes={:?}, chain={:?}", + from_block, nodes, chain, + ), + ); + let log_caches = self.get_bootstrap_log_cache(from_block, nodes, chain)?; + + let mut all_valid_logs: Vec = Vec::new(); + let request_from_block_val = from_block.unwrap_or(0); + + for log_cache in log_caches { + match self.validate_log_cache(&log_cache) { + Ok(true) => { + for log in log_cache.logs { + if let Some(log_block_number) = log.block_number { + if log_block_number >= request_from_block_val { + all_valid_logs.push(log); + } + } else { + if from_block.is_none() { + all_valid_logs.push(log); + } + } + } + } + Ok(false) => { + print_to_terminal( + 1, + &format!("LogCache validation failed for cache created by {}. Discarding {} logs.", + log_cache.metadata.created_by, + log_cache.logs.len()) + ); + } + Err(e) => { + print_to_terminal( + 1, + &format!( + "Error validating LogCache from {}: {:?}. Discarding.", + log_cache.metadata.created_by, e, + ), + ); + } + } + } + + all_valid_logs.sort_by(|a, b| { + let block_cmp = a.block_number.cmp(&b.block_number); + if block_cmp == std::cmp::Ordering::Equal { + std::cmp::Ordering::Equal + } else { + block_cmp + } + }); + + let mut unique_logs = Vec::new(); + for log in all_valid_logs { + if !unique_logs.contains(&log) { + unique_logs.push(log); + } + } + + print_to_terminal( + 2, + &format!( + "get_bootstrap: Consolidated {} unique logs.", + unique_logs.len(), + ), + ); + Ok(unique_logs) + } + + pub fn bootstrap( + &self, + from_block: Option, + filters: Vec, + nodes: HashSet, + chain: Option, + ) -> anyhow::Result>> { + print_to_terminal( + 2, + &format!( + "bootstrap: from_block={:?}, num_filters={}, nodes={:?}, chain={:?}", + from_block, + filters.len(), + nodes, + chain, + ), + ); + + let consolidated_logs = self.get_bootstrap(from_block, nodes, chain)?; + + if consolidated_logs.is_empty() { + print_to_terminal(2,"bootstrap: No logs retrieved after consolidation. Returning empty results for filters."); + return Ok(filters.iter().map(|_| Vec::new()).collect()); + } + + let mut results_per_filter: Vec> = Vec::new(); + for filter in filters { + let filtered_logs = eth_apply_filter(&consolidated_logs, &filter)?; + results_per_filter.push(filtered_logs); + } + + print_to_terminal( + 2, + &format!( + "bootstrap: Applied {} filters to bootstrapped logs.", + results_per_filter.len(), + ), + ); + Ok(results_per_filter) + } +} + +impl Serialize for ManifestItem { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("ManifestItem", 4)?; + state.serialize_field("metadata", &self.metadata)?; + state.serialize_field("is_empty", &self.is_empty)?; + state.serialize_field("file_hash", &self.file_hash)?; + state.serialize_field("file_name", &self.file_name)?; + state.end() + } +} + +impl<'de> Deserialize<'de> for ManifestItem { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "snake_case")] + enum Field { + Metadata, + IsEmpty, + FileHash, + FileName, + } + + struct ManifestItemVisitor; + + impl<'de> Visitor<'de> for ManifestItemVisitor { + type Value = ManifestItem; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct ManifestItem") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut metadata = None; + let mut is_empty = None; + let mut file_hash = None; + let mut file_name = None; + + while let Some(key) = map.next_key()? { + match key { + Field::Metadata => { + if metadata.is_some() { + return Err(de::Error::duplicate_field("metadata")); + } + metadata = Some(map.next_value()?); + } + Field::IsEmpty => { + if is_empty.is_some() { + return Err(de::Error::duplicate_field("is_empty")); + } + is_empty = Some(map.next_value()?); + } + Field::FileHash => { + if file_hash.is_some() { + return Err(de::Error::duplicate_field("file_hash")); + } + file_hash = Some(map.next_value()?); + } + Field::FileName => { + if file_name.is_some() { + return Err(de::Error::duplicate_field("file_name")); + } + file_name = Some(map.next_value()?); + } + } + } + + let metadata = metadata.ok_or_else(|| de::Error::missing_field("metadata"))?; + let is_empty = is_empty.ok_or_else(|| de::Error::missing_field("is_empty"))?; + let file_hash = file_hash.ok_or_else(|| de::Error::missing_field("file_hash"))?; + let file_name = file_name.ok_or_else(|| de::Error::missing_field("file_name"))?; + + Ok(ManifestItem { + metadata, + is_empty, + file_hash, + file_name, + }) + } + } + + deserializer.deserialize_struct( + "ManifestItem", + &["metadata", "is_empty", "file_hash", "file_name"], + ManifestItemVisitor, + ) + } +} + +impl Serialize for Manifest { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("Manifest", 4)?; + state.serialize_field("items", &self.items)?; + state.serialize_field("manifest_filename", &self.manifest_filename)?; + state.serialize_field("chain_id", &self.chain_id)?; + state.serialize_field("protocol_version", &self.protocol_version)?; + state.end() + } +} + +impl<'de> Deserialize<'de> for Manifest { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "snake_case")] + enum Field { + Items, + ManifestFilename, + ChainId, + ProtocolVersion, + } + + struct ManifestVisitor; + + impl<'de> Visitor<'de> for ManifestVisitor { + type Value = Manifest; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct Manifest") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut items = None; + let mut manifest_filename = None; + let mut chain_id = None; + let mut protocol_version = None; + + while let Some(key) = map.next_key()? { + match key { + Field::Items => { + if items.is_some() { + return Err(de::Error::duplicate_field("items")); + } + items = Some(map.next_value()?); + } + Field::ManifestFilename => { + if manifest_filename.is_some() { + return Err(de::Error::duplicate_field("manifest_filename")); + } + manifest_filename = Some(map.next_value()?); + } + Field::ChainId => { + if chain_id.is_some() { + return Err(de::Error::duplicate_field("chain_id")); + } + chain_id = Some(map.next_value()?); + } + Field::ProtocolVersion => { + if protocol_version.is_some() { + return Err(de::Error::duplicate_field("protocol_version")); + } + protocol_version = Some(map.next_value()?); + } + } + } + + let items = items.ok_or_else(|| de::Error::missing_field("items"))?; + let manifest_filename = manifest_filename + .ok_or_else(|| de::Error::missing_field("manifest_filename"))?; + let chain_id = chain_id.ok_or_else(|| de::Error::missing_field("chain_id"))?; + let protocol_version = + protocol_version.ok_or_else(|| de::Error::missing_field("protocol_version"))?; + + Ok(Manifest { + items, + manifest_filename, + chain_id, + protocol_version, + }) + } + } + + deserializer.deserialize_struct( + "Manifest", + &["items", "manifest_filename", "chain_id", "protocol_version"], + ManifestVisitor, + ) + } +} + +impl Serialize for GetLogsByRangeRequest { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("GetLogsByRangeRequest", 2)?; + state.serialize_field("from_block", &self.from_block)?; + state.serialize_field("to_block", &self.to_block)?; + state.end() + } +} + +impl<'de> Deserialize<'de> for GetLogsByRangeRequest { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "snake_case")] + enum Field { + FromBlock, + ToBlock, + } + + struct GetLogsByRangeRequestVisitor; + + impl<'de> Visitor<'de> for GetLogsByRangeRequestVisitor { + type Value = GetLogsByRangeRequest; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct GetLogsByRangeRequest") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut from_block = None; + let mut to_block = None; + + while let Some(key) = map.next_key()? { + match key { + Field::FromBlock => { + if from_block.is_some() { + return Err(de::Error::duplicate_field("from_block")); + } + from_block = Some(map.next_value()?); + } + Field::ToBlock => { + if to_block.is_some() { + return Err(de::Error::duplicate_field("to_block")); + } + to_block = Some(map.next_value()?); + } + } + } + + let from_block = + from_block.ok_or_else(|| de::Error::missing_field("from_block"))?; + + Ok(GetLogsByRangeRequest { + from_block, + to_block, + }) + } + } + + deserializer.deserialize_struct( + "GetLogsByRangeRequest", + &["from_block", "to_block"], + GetLogsByRangeRequestVisitor, + ) + } +} + +impl Serialize for CacherStatus { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("CacherStatus", 7)?; + state.serialize_field("last_cached_block", &self.last_cached_block)?; + state.serialize_field("chain_id", &self.chain_id)?; + state.serialize_field("protocol_version", &self.protocol_version)?; + state.serialize_field( + "next_cache_attempt_in_seconds", + &self.next_cache_attempt_in_seconds, + )?; + state.serialize_field("manifest_filename", &self.manifest_filename)?; + state.serialize_field("log_files_count", &self.log_files_count)?; + state.serialize_field("our_address", &self.our_address)?; + state.end() + } +} + +impl<'de> Deserialize<'de> for CacherStatus { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "snake_case")] + enum Field { + LastCachedBlock, + ChainId, + ProtocolVersion, + NextCacheAttemptInSeconds, + ManifestFilename, + LogFilesCount, + OurAddress, + } + + struct CacherStatusVisitor; + + impl<'de> Visitor<'de> for CacherStatusVisitor { + type Value = CacherStatus; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct CacherStatus") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut last_cached_block = None; + let mut chain_id = None; + let mut protocol_version = None; + let mut next_cache_attempt_in_seconds = None; + let mut manifest_filename = None; + let mut log_files_count = None; + let mut our_address = None; + + while let Some(key) = map.next_key()? { + match key { + Field::LastCachedBlock => { + if last_cached_block.is_some() { + return Err(de::Error::duplicate_field("last_cached_block")); + } + last_cached_block = Some(map.next_value()?); + } + Field::ChainId => { + if chain_id.is_some() { + return Err(de::Error::duplicate_field("chain_id")); + } + chain_id = Some(map.next_value()?); + } + Field::ProtocolVersion => { + if protocol_version.is_some() { + return Err(de::Error::duplicate_field("protocol_version")); + } + protocol_version = Some(map.next_value()?); + } + Field::NextCacheAttemptInSeconds => { + if next_cache_attempt_in_seconds.is_some() { + return Err(de::Error::duplicate_field( + "next_cache_attempt_in_seconds", + )); + } + next_cache_attempt_in_seconds = Some(map.next_value()?); + } + Field::ManifestFilename => { + if manifest_filename.is_some() { + return Err(de::Error::duplicate_field("manifest_filename")); + } + manifest_filename = Some(map.next_value()?); + } + Field::LogFilesCount => { + if log_files_count.is_some() { + return Err(de::Error::duplicate_field("log_files_count")); + } + log_files_count = Some(map.next_value()?); + } + Field::OurAddress => { + if our_address.is_some() { + return Err(de::Error::duplicate_field("our_address")); + } + our_address = Some(map.next_value()?); + } + } + } + + let last_cached_block = last_cached_block + .ok_or_else(|| de::Error::missing_field("last_cached_block"))?; + let chain_id = chain_id.ok_or_else(|| de::Error::missing_field("chain_id"))?; + let protocol_version = + protocol_version.ok_or_else(|| de::Error::missing_field("protocol_version"))?; + let manifest_filename = manifest_filename + .ok_or_else(|| de::Error::missing_field("manifest_filename"))?; + let log_files_count = + log_files_count.ok_or_else(|| de::Error::missing_field("log_files_count"))?; + let our_address = + our_address.ok_or_else(|| de::Error::missing_field("our_address"))?; + + Ok(CacherStatus { + last_cached_block, + chain_id, + protocol_version, + next_cache_attempt_in_seconds, + manifest_filename, + log_files_count, + our_address, + }) + } + } + + deserializer.deserialize_struct( + "CacherStatus", + &[ + "last_cached_block", + "chain_id", + "protocol_version", + "next_cache_attempt_in_seconds", + "manifest_filename", + "log_files_count", + "our_address", + ], + CacherStatusVisitor, + ) + } +} + +impl Serialize for CacherRequest { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + CacherRequest::GetManifest => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("GetManifest", &())?; + map.end() + } + CacherRequest::GetLogCacheContent(path) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("GetLogCacheContent", path)?; + map.end() + } + CacherRequest::GetStatus => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("GetStatus", &())?; + map.end() + } + CacherRequest::GetLogsByRange(request) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("GetLogsByRange", request)?; + map.end() + } + } + } +} + +impl<'de> Deserialize<'de> for CacherRequest { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct CacherRequestVisitor; + + impl<'de> Visitor<'de> for CacherRequestVisitor { + type Value = CacherRequest; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter + .write_str("a map with a single key representing the CacherRequest variant") + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let (variant, value) = map + .next_entry::()? + .ok_or_else(|| de::Error::invalid_length(0, &self))?; + + match variant.as_str() { + "GetManifest" => Ok(CacherRequest::GetManifest), + "GetLogCacheContent" => { + let path = serde_json::from_value(value).map_err(de::Error::custom)?; + Ok(CacherRequest::GetLogCacheContent(path)) + } + "GetStatus" => Ok(CacherRequest::GetStatus), + "GetLogsByRange" => { + let request = serde_json::from_value(value).map_err(de::Error::custom)?; + Ok(CacherRequest::GetLogsByRange(request)) + } + _ => Err(de::Error::unknown_variant( + &variant, + &[ + "GetManifest", + "GetLogCacheContent", + "GetStatus", + "GetLogsByRange", + ], + )), + } + } + } + + deserializer.deserialize_map(CacherRequestVisitor) + } +} + +impl Serialize for CacherResponse { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + CacherResponse::GetManifest(manifest) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("GetManifest", manifest)?; + map.end() + } + CacherResponse::GetLogCacheContent(result) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("GetLogCacheContent", result)?; + map.end() + } + CacherResponse::GetStatus(status) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("GetStatus", status)?; + map.end() + } + CacherResponse::GetLogsByRange(result) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("GetLogsByRange", result)?; + map.end() + } + } + } +} + +impl<'de> Deserialize<'de> for CacherResponse { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct CacherResponseVisitor; + + impl<'de> Visitor<'de> for CacherResponseVisitor { + type Value = CacherResponse; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter + .write_str("a map with a single key representing the CacherResponse variant") + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let (variant, value) = map + .next_entry::()? + .ok_or_else(|| de::Error::invalid_length(0, &self))?; + + match variant.as_str() { + "GetManifest" => { + let manifest = serde_json::from_value(value).map_err(de::Error::custom)?; + Ok(CacherResponse::GetManifest(manifest)) + } + "GetLogCacheContent" => { + let result = serde_json::from_value(value).map_err(de::Error::custom)?; + Ok(CacherResponse::GetLogCacheContent(result)) + } + "GetStatus" => { + let status = serde_json::from_value(value).map_err(de::Error::custom)?; + Ok(CacherResponse::GetStatus(status)) + } + "GetLogsByRange" => { + let result = serde_json::from_value(value).map_err(de::Error::custom)?; + Ok(CacherResponse::GetLogsByRange(result)) + } + _ => Err(de::Error::unknown_variant( + &variant, + &[ + "GetManifest", + "GetLogCacheContent", + "GetStatus", + "GetLogsByRange", + ], + )), + } + } + } + + deserializer.deserialize_map(CacherResponseVisitor) + } +} + +impl Serialize for LogsMetadata { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("LogsMetadata", 6)?; + state.serialize_field("chainId", &self.chain_id)?; + state.serialize_field("fromBlock", &self.from_block)?; + state.serialize_field("toBlock", &self.to_block)?; + state.serialize_field("timeCreated", &self.time_created)?; + state.serialize_field("createdBy", &self.created_by)?; + state.serialize_field("signature", &self.signature)?; + state.end() + } +} + +impl<'de> Deserialize<'de> for LogsMetadata { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(field_identifier, rename_all = "camelCase")] + enum Field { + ChainId, + FromBlock, + ToBlock, + TimeCreated, + CreatedBy, + Signature, + } + + struct LogsMetadataVisitor; + + impl<'de> Visitor<'de> for LogsMetadataVisitor { + type Value = LogsMetadata; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("struct LogsMetadata") + } + + fn visit_map(self, mut map: V) -> Result + where + V: MapAccess<'de>, + { + let mut chain_id = None; + let mut from_block = None; + let mut to_block = None; + let mut time_created = None; + let mut created_by = None; + let mut signature = None; + + while let Some(key) = map.next_key()? { + match key { + Field::ChainId => { + if chain_id.is_some() { + return Err(de::Error::duplicate_field("chainId")); + } + chain_id = Some(map.next_value()?); + } + Field::FromBlock => { + if from_block.is_some() { + return Err(de::Error::duplicate_field("fromBlock")); + } + from_block = Some(map.next_value()?); + } + Field::ToBlock => { + if to_block.is_some() { + return Err(de::Error::duplicate_field("toBlock")); + } + to_block = Some(map.next_value()?); + } + Field::TimeCreated => { + if time_created.is_some() { + return Err(de::Error::duplicate_field("timeCreated")); + } + time_created = Some(map.next_value()?); + } + Field::CreatedBy => { + if created_by.is_some() { + return Err(de::Error::duplicate_field("createdBy")); + } + created_by = Some(map.next_value()?); + } + Field::Signature => { + if signature.is_some() { + return Err(de::Error::duplicate_field("signature")); + } + signature = Some(map.next_value()?); + } + } + } + + let chain_id = chain_id.ok_or_else(|| de::Error::missing_field("chainId"))?; + let from_block = from_block.ok_or_else(|| de::Error::missing_field("fromBlock"))?; + let to_block = to_block.ok_or_else(|| de::Error::missing_field("toBlock"))?; + let time_created = + time_created.ok_or_else(|| de::Error::missing_field("timeCreated"))?; + let created_by = created_by.ok_or_else(|| de::Error::missing_field("createdBy"))?; + let signature = signature.ok_or_else(|| de::Error::missing_field("signature"))?; + + Ok(LogsMetadata { + chain_id, + from_block, + to_block, + time_created, + created_by, + signature, + }) + } + } + + deserializer.deserialize_struct( + "LogsMetadata", + &[ + "chainId", + "fromBlock", + "toBlock", + "timeCreated", + "createdBy", + "signature", + ], + LogsMetadataVisitor, + ) + } } diff --git a/src/lib.rs b/src/lib.rs index 211d688..c867924 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,8 +18,8 @@ use serde_json::Value; wit_bindgen::generate!({ path: "hyperware-wit", + world: "process-lib", generate_unused_types: true, - world: "lib", }); /// Interact with the eth provider module. @@ -54,6 +54,7 @@ pub mod logging; /// Your process must have the [`Capability`] to message and receive messages from /// `net:distro:sys` to use this module. pub mod net; +pub mod sign; /// Interact with the sqlite module /// /// Your process must have the [`Capability] to message and receive messages from @@ -312,20 +313,31 @@ where /// See if we have the [`Capability`] to message a certain process. /// Note if you have not saved the [`Capability`], you will not be able to message the other process. pub fn can_message(address: &Address) -> bool { + let address = eval_our(address); crate::our_capabilities() .iter() - .any(|cap| cap.params == "\"messaging\"" && cap.issuer == *address) + .any(|cap| cap.params == "\"messaging\"" && cap.issuer == address) } /// Get a [`Capability`] in our store pub fn get_capability(issuer: &Address, params: &str) -> Option { + let issuer = eval_our(issuer); let params = serde_json::from_str::(params).unwrap_or_default(); crate::our_capabilities().into_iter().find(|cap| { let cap_params = serde_json::from_str::(&cap.params).unwrap_or_default(); - cap.issuer == *issuer && params == cap_params + cap.issuer == issuer && params == cap_params }) } +pub fn eval_our(address: &Address) -> Address { + let mut address = address.clone(); + if address.node() == "our" { + let our = crate::our(); + address.node = our.node().to_string() + } + address +} + /// The `Spawn!()` macro is defined here as a no-op. /// However, in practice, `kit build` will rewrite it during pre-processing. /// diff --git a/src/sign.rs b/src/sign.rs new file mode 100644 index 0000000..3b559ec --- /dev/null +++ b/src/sign.rs @@ -0,0 +1,38 @@ +use crate::{last_blob, Address, Request}; +// TODO: use WIT types + +pub fn net_key_sign(message: Vec) -> anyhow::Result> { + Request::to(("our", "sign", "sign", "sys")) + .body("\"NetKeySign\"") + .blob_bytes(message) + .send_and_await_response(10)??; + Ok(last_blob().unwrap().bytes) +} + +pub fn net_key_verify( + message: Vec, + signer: &Address, + signature: Vec, +) -> anyhow::Result { + let response = Request::to(("our", "sign", "sign", "sys")) + .body( + serde_json::json!({ + "NetKeyVerify": { + "node": signer, + "signature": signature, + } + }) + .to_string(), + ) + .blob_bytes(message) + .send_and_await_response(10)??; + + let response: serde_json::Value = serde_json::from_slice(response.body())?; + let serde_json::Value::Bool(response) = response["NetKeyVerify"] else { + return Err(anyhow::anyhow!( + "unexpected response from sign:sign:sys: {response}" + )); + }; + + Ok(response) +}