Encode Execution Engine Client Version In Graffiti (#5290)

* Add `engine_clientVersionV1` structs

* Implement `engine_clientVersionV1`

* Update to latest spec changes

* Implement GraffitiCalculator Service

* Added Unit Tests for GraffitiCalculator

* Address Mac's Comments

* Remove need to use clap in beacon chain

* Merge remote-tracking branch 'upstream/unstable' into el_client_version_graffiti

* Merge branch 'unstable' into el_client_version_graffiti

# Conflicts:
#	beacon_node/beacon_chain/Cargo.toml
This commit is contained in:
ethDreamer
2024-04-24 01:02:48 -05:00
committed by GitHub
parent c38b05d640
commit 4a48d7b546
20 changed files with 847 additions and 81 deletions

View File

@@ -52,3 +52,4 @@ arc-swap = "1.6.0"
eth2_network_config = { workspace = true }
alloy-rlp = "0.3"
alloy-consensus = { git = "https://github.com/alloy-rs/alloy.git", rev = "974d488bab5e21e9f17452a39a4bfa56677367b2" }
lighthouse_version = { workspace = true }

View File

@@ -1,9 +1,9 @@
use crate::engines::ForkchoiceState;
use crate::http::{
ENGINE_FORKCHOICE_UPDATED_V1, ENGINE_FORKCHOICE_UPDATED_V2, ENGINE_FORKCHOICE_UPDATED_V3,
ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1,
ENGINE_GET_PAYLOAD_V1, ENGINE_GET_PAYLOAD_V2, ENGINE_GET_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V1,
ENGINE_NEW_PAYLOAD_V2, ENGINE_NEW_PAYLOAD_V3,
ENGINE_GET_CLIENT_VERSION_V1, ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1,
ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, ENGINE_GET_PAYLOAD_V1, ENGINE_GET_PAYLOAD_V2,
ENGINE_GET_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V1, ENGINE_NEW_PAYLOAD_V2, ENGINE_NEW_PAYLOAD_V3,
};
use eth2::types::{
BlobsBundle, SsePayloadAttributes, SsePayloadAttributesV1, SsePayloadAttributesV2,
@@ -24,11 +24,11 @@ pub use types::{
ExecutionPayloadRef, FixedVector, ForkName, Hash256, Transactions, Uint256, VariableList,
Withdrawal, Withdrawals,
};
use types::{
ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadElectra, ExecutionPayloadMerge,
KzgProofs,
};
use types::{Graffiti, GRAFFITI_BYTES_LEN};
pub mod auth;
pub mod http;
@@ -61,13 +61,13 @@ pub enum Error {
ParentHashEqualsBlockHash(ExecutionBlockHash),
PayloadIdUnavailable,
TransitionConfigurationMismatch,
PayloadConversionLogicFlaw,
SszError(ssz_types::Error),
DeserializeWithdrawals(ssz_types::Error),
BuilderApi(builder_client::Error),
IncorrectStateVariant,
RequiredMethodUnsupported(&'static str),
UnsupportedForkVariant(String),
InvalidClientVersion(String),
RlpDecoderError(rlp::DecoderError),
}
@@ -652,6 +652,7 @@ pub struct EngineCapabilities {
pub get_payload_v1: bool,
pub get_payload_v2: bool,
pub get_payload_v3: bool,
pub get_client_version_v1: bool,
}
impl EngineCapabilities {
@@ -690,7 +691,141 @@ impl EngineCapabilities {
if self.get_payload_v3 {
response.push(ENGINE_GET_PAYLOAD_V3);
}
if self.get_client_version_v1 {
response.push(ENGINE_GET_CLIENT_VERSION_V1);
}
response
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum ClientCode {
Besu,
EtherumJS,
Erigon,
GoEthereum,
Grandine,
Lighthouse,
Lodestar,
Nethermind,
Nimbus,
Teku,
Prysm,
Reth,
Unknown(String),
}
impl std::fmt::Display for ClientCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
ClientCode::Besu => "BU",
ClientCode::EtherumJS => "EJ",
ClientCode::Erigon => "EG",
ClientCode::GoEthereum => "GE",
ClientCode::Grandine => "GR",
ClientCode::Lighthouse => "LH",
ClientCode::Lodestar => "LS",
ClientCode::Nethermind => "NM",
ClientCode::Nimbus => "NB",
ClientCode::Teku => "TK",
ClientCode::Prysm => "PM",
ClientCode::Reth => "RH",
ClientCode::Unknown(code) => code,
};
write!(f, "{}", s)
}
}
impl TryFrom<String> for ClientCode {
type Error = String;
fn try_from(code: String) -> Result<Self, Self::Error> {
match code.as_str() {
"BU" => Ok(Self::Besu),
"EJ" => Ok(Self::EtherumJS),
"EG" => Ok(Self::Erigon),
"GE" => Ok(Self::GoEthereum),
"GR" => Ok(Self::Grandine),
"LH" => Ok(Self::Lighthouse),
"LS" => Ok(Self::Lodestar),
"NM" => Ok(Self::Nethermind),
"NB" => Ok(Self::Nimbus),
"TK" => Ok(Self::Teku),
"PM" => Ok(Self::Prysm),
"RH" => Ok(Self::Reth),
string => {
if string.len() == 2 {
Ok(Self::Unknown(code))
} else {
Err(format!("Invalid client code: {}", code))
}
}
}
}
}
#[derive(Clone, Debug)]
pub struct CommitPrefix(pub String);
impl TryFrom<String> for CommitPrefix {
type Error = String;
fn try_from(value: String) -> Result<Self, Self::Error> {
// Check if the input starts with '0x' and strip it if it does
let commit_prefix = value.strip_prefix("0x").unwrap_or(&value);
// Ensure length is exactly 8 characters after '0x' removal
if commit_prefix.len() != 8 {
return Err(
"Input must be exactly 8 characters long (excluding any '0x' prefix)".to_string(),
);
}
// Ensure all characters are valid hex digits
if commit_prefix.chars().all(|c| c.is_ascii_hexdigit()) {
Ok(CommitPrefix(commit_prefix.to_lowercase()))
} else {
Err("Input must contain only hexadecimal characters".to_string())
}
}
}
impl std::fmt::Display for CommitPrefix {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Debug)]
pub struct ClientVersionV1 {
pub code: ClientCode,
pub name: String,
pub version: String,
pub commit: CommitPrefix,
}
impl ClientVersionV1 {
pub fn calculate_graffiti(&self, lighthouse_commit_prefix: CommitPrefix) -> Graffiti {
let graffiti_string = format!(
"{}{}LH{}",
self.code,
self.commit
.0
.get(..4)
.map_or_else(|| self.commit.0.as_str(), |s| s)
.to_lowercase(),
lighthouse_commit_prefix
.0
.get(..4)
.unwrap_or("0000")
.to_lowercase(),
);
let mut graffiti_bytes = [0u8; GRAFFITI_BYTES_LEN];
let bytes_to_copy = std::cmp::min(graffiti_string.len(), GRAFFITI_BYTES_LEN);
graffiti_bytes[..bytes_to_copy]
.copy_from_slice(&graffiti_string.as_bytes()[..bytes_to_copy]);
Graffiti::from(graffiti_bytes)
}
}

View File

@@ -3,6 +3,8 @@
use super::*;
use crate::auth::Auth;
use crate::json_structures::*;
use lazy_static::lazy_static;
use lighthouse_version::{COMMIT_PREFIX, VERSION};
use reqwest::header::CONTENT_TYPE;
use sensitive_url::SensitiveUrl;
use serde::de::DeserializeOwned;
@@ -51,6 +53,9 @@ pub const ENGINE_GET_PAYLOAD_BODIES_TIMEOUT: Duration = Duration::from_secs(10);
pub const ENGINE_EXCHANGE_CAPABILITIES: &str = "engine_exchangeCapabilities";
pub const ENGINE_EXCHANGE_CAPABILITIES_TIMEOUT: Duration = Duration::from_secs(1);
pub const ENGINE_GET_CLIENT_VERSION_V1: &str = "engine_getClientVersionV1";
pub const ENGINE_GET_CLIENT_VERSION_TIMEOUT: Duration = Duration::from_secs(1);
/// This error is returned during a `chainId` call by Geth.
pub const EIP155_ERROR_STR: &str = "chain not synced beyond EIP-155 replay-protection fork block";
/// This code is returned by all clients when a method is not supported
@@ -69,8 +74,22 @@ pub static LIGHTHOUSE_CAPABILITIES: &[&str] = &[
ENGINE_FORKCHOICE_UPDATED_V3,
ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1,
ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1,
ENGINE_GET_CLIENT_VERSION_V1,
];
lazy_static! {
/// We opt to initialize the JsonClientVersionV1 rather than the ClientVersionV1
/// for two reasons:
/// 1. This saves the overhead of converting into Json for every engine call
/// 2. The Json version lacks error checking so we can avoid calling `unwrap()`
pub static ref LIGHTHOUSE_JSON_CLIENT_VERSION: JsonClientVersionV1 = JsonClientVersionV1 {
code: ClientCode::Lighthouse.to_string(),
name: "Lighthouse".to_string(),
version: VERSION.replace("Lighthouse/", ""),
commit: COMMIT_PREFIX.to_string(),
};
}
/// Contains methods to convert arbitrary bytes to an ETH2 deposit contract object.
pub mod deposit_log {
use ssz::Decode;
@@ -546,22 +565,21 @@ pub mod deposit_methods {
}
}
#[derive(Clone, Debug)]
pub struct CapabilitiesCacheEntry {
engine_capabilities: EngineCapabilities,
fetch_time: Instant,
pub struct CachedResponse<T: Clone> {
pub data: T,
pub fetch_time: Instant,
}
impl CapabilitiesCacheEntry {
pub fn new(engine_capabilities: EngineCapabilities) -> Self {
impl<T: Clone> CachedResponse<T> {
pub fn new(data: T) -> Self {
Self {
engine_capabilities,
data,
fetch_time: Instant::now(),
}
}
pub fn engine_capabilities(&self) -> EngineCapabilities {
self.engine_capabilities
pub fn data(&self) -> T {
self.data.clone()
}
pub fn age(&self) -> Duration {
@@ -578,7 +596,8 @@ pub struct HttpJsonRpc {
pub client: Client,
pub url: SensitiveUrl,
pub execution_timeout_multiplier: u32,
pub engine_capabilities_cache: Mutex<Option<CapabilitiesCacheEntry>>,
pub engine_capabilities_cache: Mutex<Option<CachedResponse<EngineCapabilities>>>,
pub engine_version_cache: Mutex<Option<CachedResponse<Vec<ClientVersionV1>>>>,
auth: Option<Auth>,
}
@@ -592,6 +611,7 @@ impl HttpJsonRpc {
url,
execution_timeout_multiplier: execution_timeout_multiplier.unwrap_or(1),
engine_capabilities_cache: Mutex::new(None),
engine_version_cache: Mutex::new(None),
auth: None,
})
}
@@ -606,6 +626,7 @@ impl HttpJsonRpc {
url,
execution_timeout_multiplier: execution_timeout_multiplier.unwrap_or(1),
engine_capabilities_cache: Mutex::new(None),
engine_version_cache: Mutex::new(None),
auth: Some(auth),
})
}
@@ -1056,6 +1077,7 @@ impl HttpJsonRpc {
get_payload_v1: capabilities.contains(ENGINE_GET_PAYLOAD_V1),
get_payload_v2: capabilities.contains(ENGINE_GET_PAYLOAD_V2),
get_payload_v3: capabilities.contains(ENGINE_GET_PAYLOAD_V3),
get_client_version_v1: capabilities.contains(ENGINE_GET_CLIENT_VERSION_V1),
})
}
@@ -1078,15 +1100,78 @@ impl HttpJsonRpc {
) -> Result<EngineCapabilities, Error> {
let mut lock = self.engine_capabilities_cache.lock().await;
if let Some(lock) = lock.as_ref().filter(|entry| !entry.older_than(age_limit)) {
Ok(lock.engine_capabilities())
if let Some(lock) = lock
.as_ref()
.filter(|cached_response| !cached_response.older_than(age_limit))
{
Ok(lock.data())
} else {
let engine_capabilities = self.exchange_capabilities().await?;
*lock = Some(CapabilitiesCacheEntry::new(engine_capabilities));
*lock = Some(CachedResponse::new(engine_capabilities));
Ok(engine_capabilities)
}
}
/// This method fetches the response from the engine without checking
/// any caches or storing the result in the cache. It is better to use
/// `get_engine_version(Some(Duration::ZERO))` if you want to force
/// fetching from the EE as this will cache the result.
pub async fn get_client_version_v1(&self) -> Result<Vec<ClientVersionV1>, Error> {
let params = json!([*LIGHTHOUSE_JSON_CLIENT_VERSION]);
let response: Vec<JsonClientVersionV1> = self
.rpc_request(
ENGINE_GET_CLIENT_VERSION_V1,
params,
ENGINE_GET_CLIENT_VERSION_TIMEOUT * self.execution_timeout_multiplier,
)
.await?;
response
.into_iter()
.map(TryInto::try_into)
.collect::<Result<Vec<_>, _>>()
.map_err(Error::InvalidClientVersion)
}
pub async fn clear_engine_version_cache(&self) {
*self.engine_version_cache.lock().await = None;
}
/// Returns the execution engine version resulting from a call to
/// engine_getClientVersionV1. If the version cache is not populated, or if it
/// is populated with a cached result of age >= `age_limit`, this method will
/// fetch the result from the execution engine and populate the cache before
/// returning it. Otherwise it will return the cached result from an earlier
/// call.
///
/// Set `age_limit` to `None` to always return the cached result
/// Set `age_limit` to `Some(Duration::ZERO)` to force fetching from EE
pub async fn get_engine_version(
&self,
age_limit: Option<Duration>,
) -> Result<Vec<ClientVersionV1>, Error> {
// check engine capabilities first (avoids holding two locks at once)
let engine_capabilities = self.get_engine_capabilities(None).await?;
if !engine_capabilities.get_client_version_v1 {
// We choose an empty vec to denote that this method is not
// supported instead of an error since this method is optional
// & we don't want to log a warning and concern the user
return Ok(vec![]);
}
let mut lock = self.engine_version_cache.lock().await;
if let Some(lock) = lock
.as_ref()
.filter(|cached_response| !cached_response.older_than(age_limit))
{
Ok(lock.data())
} else {
let engine_version = self.get_client_version_v1().await?;
*lock = Some(CachedResponse::new(engine_version.clone()));
Ok(engine_version)
}
}
// automatically selects the latest version of
// new_payload that the execution engine supports
pub async fn new_payload<E: EthSpec>(

View File

@@ -747,3 +747,36 @@ pub mod serde_logs_bloom {
.map_err(|e| serde::de::Error::custom(format!("invalid logs bloom: {:?}", e)))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JsonClientVersionV1 {
pub code: String,
pub name: String,
pub version: String,
pub commit: String,
}
impl From<ClientVersionV1> for JsonClientVersionV1 {
fn from(client_version: ClientVersionV1) -> Self {
Self {
code: client_version.code.to_string(),
name: client_version.name,
version: client_version.version,
commit: client_version.commit.to_string(),
}
}
}
impl TryFrom<JsonClientVersionV1> for ClientVersionV1 {
type Error = String;
fn try_from(json: JsonClientVersionV1) -> Result<Self, Self::Error> {
Ok(Self {
code: json.code.try_into()?,
name: json.name,
version: json.version,
commit: json.commit.try_into()?,
})
}
}

View File

@@ -4,7 +4,7 @@ use crate::engine_api::{
EngineCapabilities, Error as EngineApiError, ForkchoiceUpdatedResponse, PayloadAttributes,
PayloadId,
};
use crate::HttpJsonRpc;
use crate::{ClientVersionV1, HttpJsonRpc};
use lru::LruCache;
use slog::{debug, error, info, warn, Logger};
use std::future::Future;
@@ -21,7 +21,7 @@ use types::ExecutionBlockHash;
///
/// Since the size of each value is small (~800 bytes) a large number is used for safety.
const PAYLOAD_ID_LRU_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(512);
const CACHED_ENGINE_CAPABILITIES_AGE_LIMIT: Duration = Duration::from_secs(900); // 15 minutes
const CACHED_RESPONSE_AGE_LIMIT: Duration = Duration::from_secs(900); // 15 minutes
/// Stores the remembered state of a engine.
#[derive(Copy, Clone, PartialEq, Debug, Eq, Default)]
@@ -34,11 +34,11 @@ enum EngineStateInternal {
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
enum CapabilitiesCacheAction {
enum ResponseCacheAction {
#[default]
None,
Update,
Clear,
Update, // Update cached responses
Clear, // Clear cached responses
}
/// A subset of the engine state to inform other services if the engine is online or offline.
@@ -266,12 +266,12 @@ impl Engine {
);
}
state.update(EngineStateInternal::Synced);
(**state, CapabilitiesCacheAction::Update)
(**state, ResponseCacheAction::Update)
}
Err(EngineApiError::IsSyncing) => {
let mut state = self.state.write().await;
state.update(EngineStateInternal::Syncing);
(**state, CapabilitiesCacheAction::Update)
(**state, ResponseCacheAction::Update)
}
Err(EngineApiError::Auth(err)) => {
error!(
@@ -282,7 +282,7 @@ impl Engine {
let mut state = self.state.write().await;
state.update(EngineStateInternal::AuthFailed);
(**state, CapabilitiesCacheAction::Clear)
(**state, ResponseCacheAction::Clear)
}
Err(e) => {
error!(
@@ -293,28 +293,37 @@ impl Engine {
let mut state = self.state.write().await;
state.update(EngineStateInternal::Offline);
// need to clear the engine capabilities cache if we detect the
// execution engine is offline as it is likely the engine is being
// updated to a newer version with new capabilities
(**state, CapabilitiesCacheAction::Clear)
// need to clear cached responses if we detect the execution engine
// is offline as it is likely the engine is being updated to a newer
// version which might also have new capabilities
(**state, ResponseCacheAction::Clear)
}
};
// do this after dropping state lock guard to avoid holding two locks at once
match cache_action {
CapabilitiesCacheAction::None => {}
CapabilitiesCacheAction::Update => {
ResponseCacheAction::None => {}
ResponseCacheAction::Update => {
if let Err(e) = self
.get_engine_capabilities(Some(CACHED_ENGINE_CAPABILITIES_AGE_LIMIT))
.get_engine_capabilities(Some(CACHED_RESPONSE_AGE_LIMIT))
.await
{
warn!(self.log,
"Error during exchange capabilities";
"error" => ?e,
)
} else {
// no point in running this if there was an error fetching the capabilities
// as it will just result in an error again
let _ = self
.get_engine_version(Some(CACHED_RESPONSE_AGE_LIMIT))
.await;
}
}
CapabilitiesCacheAction::Clear => self.api.clear_exchange_capabilties_cache().await,
ResponseCacheAction::Clear => {
self.api.clear_exchange_capabilties_cache().await;
self.api.clear_engine_version_cache().await;
}
}
debug!(
@@ -340,6 +349,22 @@ impl Engine {
self.api.get_engine_capabilities(age_limit).await
}
/// Returns the execution engine version resulting from a call to
/// engine_clientVersionV1. If the version cache is not populated, or if it
/// is populated with a cached result of age >= `age_limit`, this method will
/// fetch the result from the execution engine and populate the cache before
/// returning it. Otherwise it will return the cached result from an earlier
/// call.
///
/// Set `age_limit` to `None` to always return the cached result
/// Set `age_limit` to `Some(Duration::ZERO)` to force fetching from EE
pub async fn get_engine_version(
&self,
age_limit: Option<Duration>,
) -> Result<Vec<ClientVersionV1>, EngineApiError> {
self.api.get_engine_version(age_limit).await
}
/// Run `func` on the node regardless of the node's current state.
///
/// ## Note

View File

@@ -165,6 +165,17 @@ impl From<ApiError> for Error {
}
}
impl From<EngineError> for Error {
fn from(e: EngineError) -> Self {
match e {
// This removes an unnecessary layer of indirection.
// TODO (mark): consider refactoring these error enums
EngineError::Api { error } => Error::ApiError(error),
_ => Error::EngineError(Box::new(e)),
}
}
}
pub enum BlockProposalContentsType<E: EthSpec> {
Full(BlockProposalContents<E, FullPayload<E>>),
Blinded(BlockProposalContents<E, BlindedPayload<E>>),
@@ -1526,8 +1537,26 @@ impl<E: EthSpec> ExecutionLayer<E> {
self.engine()
.request(|engine| engine.get_engine_capabilities(age_limit))
.await
.map_err(Box::new)
.map_err(Error::EngineError)
.map_err(Into::into)
}
/// Returns the execution engine version resulting from a call to
/// engine_clientVersionV1. If the version cache is not populated, or if it
/// is populated with a cached result of age >= `age_limit`, this method will
/// fetch the result from the execution engine and populate the cache before
/// returning it. Otherwise it will return the cached result from an earlier
/// call.
///
/// Set `age_limit` to `None` to always return the cached result
/// Set `age_limit` to `Some(Duration::ZERO)` to force fetching from EE
pub async fn get_engine_version(
&self,
age_limit: Option<Duration>,
) -> Result<Vec<ClientVersionV1>, Error> {
self.engine()
.request(|engine| engine.get_engine_version(age_limit))
.await
.map_err(Into::into)
}
/// Used during block production to determine if the merge has been triggered.

View File

@@ -1,7 +1,7 @@
use super::Context;
use crate::engine_api::{http::*, *};
use crate::json_structures::*;
use crate::test_utils::DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI;
use crate::test_utils::{DEFAULT_CLIENT_VERSION, DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI};
use serde::{de::DeserializeOwned, Deserialize};
use serde_json::Value as JsonValue;
use std::sync::Arc;
@@ -528,6 +528,9 @@ pub async fn handle_rpc<E: EthSpec>(
let engine_capabilities = ctx.engine_capabilities.read();
Ok(serde_json::to_value(engine_capabilities.to_response()).unwrap())
}
ENGINE_GET_CLIENT_VERSION_V1 => {
Ok(serde_json::to_value([DEFAULT_CLIENT_VERSION.clone()]).unwrap())
}
ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1 => {
#[derive(Deserialize)]
#[serde(transparent)]

View File

@@ -4,11 +4,13 @@ use crate::engine_api::auth::JwtKey;
use crate::engine_api::{
auth::Auth, http::JSONRPC_VERSION, ExecutionBlock, PayloadStatusV1, PayloadStatusV1Status,
};
use crate::json_structures::JsonClientVersionV1;
use bytes::Bytes;
use environment::null_logger;
use execution_block_generator::PoWBlock;
use handle_rpc::handle_rpc;
use kzg::Kzg;
use lazy_static::lazy_static;
use parking_lot::{Mutex, RwLock, RwLockWriteGuard};
use serde::{Deserialize, Serialize};
use serde_json::json;
@@ -49,8 +51,18 @@ pub const DEFAULT_ENGINE_CAPABILITIES: EngineCapabilities = EngineCapabilities {
get_payload_v1: true,
get_payload_v2: true,
get_payload_v3: true,
get_client_version_v1: true,
};
lazy_static! {
pub static ref DEFAULT_CLIENT_VERSION: JsonClientVersionV1 = JsonClientVersionV1 {
code: "MC".to_string(), // "mock client"
name: "Mock Execution Client".to_string(),
version: "0.1.0".to_string(),
commit: "0xabcdef01".to_string(),
};
}
mod execution_block_generator;
mod handle_rpc;
mod hook;