-
Notifications
You must be signed in to change notification settings - Fork 804
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Check executor params coherence #1774
Changes from all commits
e237da0
6c8cdfd
072af16
b7b972b
7412c3a
5e2b0d3
2287dfd
c1c0f4c
0ada761
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,35 +26,101 @@ use parity_scale_codec::{Decode, Encode}; | |
use polkadot_core_primitives::Hash; | ||
use scale_info::TypeInfo; | ||
use serde::{Deserialize, Serialize}; | ||
use sp_std::{ops::Deref, time::Duration, vec, vec::Vec}; | ||
use sp_std::{collections::btree_map::BTreeMap, ops::Deref, time::Duration, vec, vec::Vec}; | ||
|
||
/// Default maximum number of wasm values allowed for the stack during execution of a PVF. | ||
pub const DEFAULT_LOGICAL_STACK_MAX: u32 = 65536; | ||
/// Default maximum number of bytes devoted for the stack during execution of a PVF. | ||
pub const DEFAULT_NATIVE_STACK_MAX: u32 = 256 * 1024 * 1024; | ||
|
||
/// The limit of [`ExecutorParam::MaxMemoryPages`]. | ||
pub const MEMORY_PAGES_MAX: u32 = 65536; | ||
/// The lower bound of [`ExecutorParam::StackLogicalMax`]. | ||
pub const LOGICAL_MAX_LO: u32 = 1024; | ||
/// The upper bound of [`ExecutorParam::StackLogicalMax`]. | ||
pub const LOGICAL_MAX_HI: u32 = 2 * 65536; | ||
/// The lower bound of [`ExecutorParam::PrecheckingMaxMemory`]. | ||
pub const PRECHECK_MEM_MAX_LO: u64 = 256 * 1024 * 1024; | ||
/// The upper bound of [`ExecutorParam::PrecheckingMaxMemory`]. | ||
pub const PRECHECK_MEM_MAX_HI: u64 = 16 * 1024 * 1024 * 1024; | ||
|
||
// Default PVF timeouts. Must never be changed! Use executor environment parameters to adjust them. | ||
// See also `PvfPrepTimeoutKind` and `PvfExecTimeoutKind` docs. | ||
|
||
/// Default PVF preparation timeout for prechecking requests. | ||
pub const DEFAULT_PRECHECK_PREPARATION_TIMEOUT: Duration = Duration::from_secs(60); | ||
/// Default PVF preparation timeout for execution requests. | ||
pub const DEFAULT_LENIENT_PREPARATION_TIMEOUT: Duration = Duration::from_secs(360); | ||
/// Default PVF execution timeout for backing. | ||
pub const DEFAULT_BACKING_EXECUTION_TIMEOUT: Duration = Duration::from_secs(2); | ||
/// Default PVF execution timeout for approval or disputes. | ||
pub const DEFAULT_APPROVAL_EXECUTION_TIMEOUT: Duration = Duration::from_secs(12); | ||
|
||
const DEFAULT_PRECHECK_PREPARATION_TIMEOUT_MS: u64 = | ||
DEFAULT_PRECHECK_PREPARATION_TIMEOUT.as_millis() as u64; | ||
const DEFAULT_LENIENT_PREPARATION_TIMEOUT_MS: u64 = | ||
DEFAULT_LENIENT_PREPARATION_TIMEOUT.as_millis() as u64; | ||
const DEFAULT_BACKING_EXECUTION_TIMEOUT_MS: u64 = | ||
DEFAULT_BACKING_EXECUTION_TIMEOUT.as_millis() as u64; | ||
const DEFAULT_APPROVAL_EXECUTION_TIMEOUT_MS: u64 = | ||
DEFAULT_APPROVAL_EXECUTION_TIMEOUT.as_millis() as u64; | ||
|
||
/// The different executor parameters for changing the execution environment semantics. | ||
#[derive(Clone, Debug, Encode, Decode, PartialEq, Eq, TypeInfo, Serialize, Deserialize)] | ||
pub enum ExecutorParam { | ||
/// Maximum number of memory pages (64KiB bytes per page) the executor can allocate. | ||
/// A valid value lies within (0, 65536]. | ||
#[codec(index = 1)] | ||
MaxMemoryPages(u32), | ||
/// Wasm logical stack size limit (max. number of Wasm values on stack) | ||
/// Wasm logical stack size limit (max. number of Wasm values on stack). | ||
/// A valid value lies within [[`LOGICAL_MAX_LO`], [`LOGICAL_MAX_HI`]]. | ||
/// | ||
/// For WebAssembly, the stack limit is subject to implementations, meaning that it may vary on | ||
/// different platforms. However, we want execution to be deterministic across machines of | ||
/// different architectures, including failures like stack overflow. For deterministic | ||
/// overflow, we rely on a **logical** limit, the maximum number of values allowed to be pushed | ||
/// on the stack. | ||
#[codec(index = 2)] | ||
StackLogicalMax(u32), | ||
/// Executor machine stack size limit, in bytes | ||
/// Executor machine stack size limit, in bytes. | ||
/// If `StackLogicalMax` is also present, a valid value should not fall below | ||
/// 128 * `StackLogicalMax`. | ||
/// | ||
/// For deterministic overflow, `StackLogicalMax` should be reached before the native stack is | ||
/// exhausted. | ||
#[codec(index = 3)] | ||
StackNativeMax(u32), | ||
/// Max. amount of memory the preparation worker is allowed to use during | ||
/// pre-checking, in bytes | ||
/// pre-checking, in bytes. | ||
/// Valid max. memory ranges from [`PRECHECK_MEM_MAX_LO`] to [`PRECHECK_MEM_MAX_HI`]. | ||
#[codec(index = 4)] | ||
PrecheckingMaxMemory(u64), | ||
/// PVF preparation timeouts, millisec | ||
/// PVF preparation timeouts, in millisecond. | ||
/// Always ensure that `precheck_timeout` < `lenient_timeout`. | ||
/// When absent, the default values will be used. | ||
#[codec(index = 5)] | ||
PvfPrepTimeout(PvfPrepTimeoutKind, u64), | ||
/// PVF execution timeouts, millisec | ||
/// PVF execution timeouts, in millisecond. | ||
/// Always ensure that `backing_timeout` < `approval_timeout`. | ||
/// When absent, the default values will be used. | ||
#[codec(index = 6)] | ||
PvfExecTimeout(PvfExecTimeoutKind, u64), | ||
/// Enables WASM bulk memory proposal | ||
#[codec(index = 7)] | ||
WasmExtBulkMemory, | ||
} | ||
|
||
/// Possible inconsistencies of executor params. | ||
#[derive(Debug)] | ||
pub enum ExecutorParamError { | ||
/// A param is duplicated. | ||
DuplicatedParam(&'static str), | ||
/// A param value exceeds its limitation. | ||
OutsideLimit(&'static str), | ||
/// Two param values are incompatible or senseless when put together. | ||
IncompatibleValues(&'static str, &'static str), | ||
} | ||
|
||
/// Unit type wrapper around [`type@Hash`] that represents an execution parameter set hash. | ||
/// | ||
/// This type is produced by [`ExecutorParams::hash`]. | ||
|
@@ -130,6 +196,126 @@ impl ExecutorParams { | |
} | ||
None | ||
} | ||
|
||
/// Check params coherence. | ||
pub fn check_consistency(&self) -> Result<(), ExecutorParamError> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we have this logic right in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, it would be nice to if we could put the checks in one place. But that may require opening up the internal of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure what you mean exactly, but I think it's fine. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I was thinking that we might need to make the internal state public in order to do the checks in another crate, like I just realized that it comes with an iterator :) But still, for the sake of separation of concerns, I think it wouldn't be a bad idea that the type does its own checks. Like, what if the checks are needed else where (other than There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair enough. :) |
||
use ExecutorParam::*; | ||
use ExecutorParamError::*; | ||
|
||
let mut seen = BTreeMap::<&str, u64>::new(); | ||
|
||
macro_rules! check { | ||
mrcnski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
($param:ident, $val:expr $(,)?) => { | ||
if seen.contains_key($param) { | ||
return Err(DuplicatedParam($param)) | ||
} | ||
seen.insert($param, $val as u64); | ||
}; | ||
|
||
// should check existence before range | ||
($param:ident, $val:expr, $out_of_limit:expr $(,)?) => { | ||
if seen.contains_key($param) { | ||
return Err(DuplicatedParam($param)) | ||
} | ||
if $out_of_limit { | ||
return Err(OutsideLimit($param)) | ||
} | ||
seen.insert($param, $val as u64); | ||
}; | ||
} | ||
|
||
for param in &self.0 { | ||
eagr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// should ensure to be unique | ||
let param_ident = match *param { | ||
MaxMemoryPages(_) => "MaxMemoryPages", | ||
StackLogicalMax(_) => "StackLogicalMax", | ||
StackNativeMax(_) => "StackNativeMax", | ||
PrecheckingMaxMemory(_) => "PrecheckingMaxMemory", | ||
PvfPrepTimeout(kind, _) => match kind { | ||
PvfPrepTimeoutKind::Precheck => "PvfPrepTimeoutKind::Precheck", | ||
PvfPrepTimeoutKind::Lenient => "PvfPrepTimeoutKind::Lenient", | ||
}, | ||
PvfExecTimeout(kind, _) => match kind { | ||
PvfExecTimeoutKind::Backing => "PvfExecTimeoutKind::Backing", | ||
PvfExecTimeoutKind::Approval => "PvfExecTimeoutKind::Approval", | ||
}, | ||
WasmExtBulkMemory => "WasmExtBulkMemory", | ||
}; | ||
|
||
match *param { | ||
MaxMemoryPages(val) => { | ||
check!(param_ident, val, val == 0 || val > MEMORY_PAGES_MAX,); | ||
}, | ||
|
||
StackLogicalMax(val) => { | ||
check!(param_ident, val, val < LOGICAL_MAX_LO || val > LOGICAL_MAX_HI,); | ||
}, | ||
|
||
StackNativeMax(val) => { | ||
check!(param_ident, val); | ||
}, | ||
|
||
PrecheckingMaxMemory(val) => { | ||
check!( | ||
param_ident, | ||
val, | ||
val < PRECHECK_MEM_MAX_LO || val > PRECHECK_MEM_MAX_HI, | ||
); | ||
}, | ||
|
||
PvfPrepTimeout(_, val) => { | ||
check!(param_ident, val); | ||
}, | ||
|
||
PvfExecTimeout(_, val) => { | ||
check!(param_ident, val); | ||
}, | ||
|
||
WasmExtBulkMemory => { | ||
check!(param_ident, 1); | ||
}, | ||
} | ||
} | ||
|
||
if let (Some(lm), Some(nm)) = ( | ||
seen.get("StackLogicalMax").or(Some(&(DEFAULT_LOGICAL_STACK_MAX as u64))), | ||
seen.get("StackNativeMax").or(Some(&(DEFAULT_NATIVE_STACK_MAX as u64))), | ||
) { | ||
if *nm < 128 * *lm { | ||
return Err(IncompatibleValues("StackLogicalMax", "StackNativeMax")) | ||
} | ||
} | ||
|
||
if let (Some(precheck), Some(lenient)) = ( | ||
seen.get("PvfPrepTimeoutKind::Precheck") | ||
.or(Some(&DEFAULT_PRECHECK_PREPARATION_TIMEOUT_MS)), | ||
seen.get("PvfPrepTimeoutKind::Lenient") | ||
.or(Some(&DEFAULT_LENIENT_PREPARATION_TIMEOUT_MS)), | ||
) { | ||
if *precheck >= *lenient { | ||
return Err(IncompatibleValues( | ||
"PvfPrepTimeoutKind::Precheck", | ||
"PvfPrepTimeoutKind::Lenient", | ||
)) | ||
} | ||
} | ||
|
||
if let (Some(backing), Some(approval)) = ( | ||
seen.get("PvfExecTimeoutKind::Backing") | ||
.or(Some(&DEFAULT_BACKING_EXECUTION_TIMEOUT_MS)), | ||
seen.get("PvfExecTimeoutKind::Approval") | ||
.or(Some(&DEFAULT_APPROVAL_EXECUTION_TIMEOUT_MS)), | ||
) { | ||
if *backing >= *approval { | ||
return Err(IncompatibleValues( | ||
"PvfExecTimeoutKind::Backing", | ||
"PvfExecTimeoutKind::Approval", | ||
)) | ||
} | ||
} | ||
|
||
Ok(()) | ||
} | ||
} | ||
|
||
impl Deref for ExecutorParams { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! But can we just use the value from
sem
instead?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we discussed it here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I misread this code, nevermind. Should it be this instead though?
And are these values correct? I would have thought the extra pages were 32. @s0me0ne-unkn0wn
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I believe everything is okay with the current version.
EXTRA_HEAP_PAGES
is the number of pages of linear memory the instance is allowed to allocate in addition to its initial memory size.DEFAULT_HEAP_PAGES_ESTIMATE
is the number of pages the instance will use for its own purposes, they are not part of linear memory.So, the corresponding executor parameter defines the number of linear memory pages the instance is allowed to allocate, and here, we implicitly add those auxiliary pages to that limit (for example, we can set the limit to 0, and Wasm code not using any linear memory should still be functional, but it's not possible without having some space for shadow stack and other bits).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it clarify stuff if we name it
SHADOW_STACK_HEAP_PAGES
? Then current name may be a bit vague.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not just shadow stack, according to the comment 🤔 But I doubt if the reasoning behind the calculation provided is correct enough. I didn't dig deeper into this aspect of Wasmtime, but it seems obvious that data segment pages shouldn't be allocated implicitly. Wasm source must specify the initial memory size, and it should always be enough to accommodate the data segment at least. Considering that I'm not the author of the original constant naming, I'm not 100% sure what is better, but probably renaming
DEFAULT_HEAP_PAGES_ESTIMATE
toINITIAL_HEAP_PAGES_ESTIMATE
would bring more clarity.