Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions executor/fuzz/shared/leader-result.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use genvm::{public_abi::VmError, rt::vm::RunOk, wasi::genlayer_sdk::parse_leader_result};
use genvm::{
public_abi::VmError, rt::vm::ContractOutcome, wasi::genlayer_sdk::parse_leader_result,
};

fn is_derived_namespace(code: &str) -> bool {
is_code_or_space_extension(code, "leader_fault nondet_output")
Expand Down Expand Up @@ -36,13 +38,13 @@ pub fn assert_parse_properties(data: &[u8]) {
match parse_leader_result(data) {
Ok(res) => {
assert_eq!(
res.as_bytes(),
res.encode().into_bytes().as_ref(),
data,
"accepted leader result must serialize byte-identically"
);

match res {
RunOk::VMError(e, _) => {
ContractOutcome::VMError(e, _) => {
assert!(
VmError::is_valid_(&e.0),
"accepted vm_error must be a valid public ABI code: {:?}",
Expand All @@ -59,10 +61,7 @@ pub fn assert_parse_properties(data: &[u8]) {
e.0
);
}
RunOk::FatalVMError(..) => {
panic!("leader result parsing must reject fatal VM errors")
}
RunOk::Return(_) | RunOk::UserError(_) => {
ContractOutcome::Return(_) | ContractOutcome::UserError(_) => {
assert!(
data.len() > 1 && genvm::calldata::decode(&data[1..]).is_ok(),
"validate-only and materializing calldata decode must agree"
Expand Down
28 changes: 26 additions & 2 deletions executor/src/host/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1079,8 +1079,7 @@ mod tests {
assert_ne!(one, execution_hash_with(Vec::new()));
}

// Fatality reaches the host as its own code; only the payload is shared
// with an ordinary VM error.
// FullResult preserves nested fatality until the publication boundary
#[test]
fn fatal_run_ok_keeps_its_code_and_payload() {
let code = crate::public_abi::VmError::timeout();
Expand All @@ -1090,4 +1089,29 @@ mod tests {
assert_eq!(result.kind, host_fns::ResultCode::FatalVmError);
assert_eq!(result.data, calldata::Value::Str(code.into()).into());
}

#[test]
fn top_level_fatal_is_framed_and_reported_as_vm_error() {
let mut rt_result = rt::vm::FullResult::empty_from(rt::vm::RunOk::FatalVMError(
crate::public_abi::VmError::timeout(),
None,
));
rt_result.coalesce_fatal_for_top_level();
let result = FullResult::new(
rt_result,
Vec::new(),
None,
Vec::new(),
rt::fees::BucketsConsumed::default(),
primitive_types::U256::zero(),
Vec::new(),
);

let encoded = encode_result(&Ok(result)).unwrap();
let reported: genvm_modules_interfaces::ReportedResult =
calldata::decode_obj(&encoded[1..]).unwrap();

assert_eq!(encoded[0], host_fns::ResultCode::VmError as u8);
assert_eq!(reported.kind, genvm_modules_interfaces::ResultCode::VmError);
}
}
47 changes: 46 additions & 1 deletion executor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,10 @@ fn extra_leader_nondet_output_error(
return None;
}

Some(rt::errors::vm_error_for_leader_extra(&run_ok.as_bytes()))
let encoded = rt::vm::ContractOutcome::try_from(run_ok.duplicate())
.expect("fatal results return before extra leader output is checked")
.encode();
Some(rt::errors::vm_error_for_leader_extra(encoded.as_slice()))
}

fn has_extra_leader_output(run_mode: rt::RunMode, executed: u32, published: u32) -> bool {
Expand Down Expand Up @@ -521,6 +524,28 @@ pub async fn run_with_impl(
})
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ReportingBoundary {
TopLevel,
Nested,
}

impl ReportingBoundary {
fn for_execution_data(entry_data: &genvm_modules_interfaces::ExecutionData) -> Self {
if entry_data.nested.is_some() {
Self::Nested
} else {
Self::TopLevel
}
}

fn normalize(self, result: &mut rt::vm::FullResult) {
if self == Self::TopLevel {
result.coalesce_fatal_for_top_level();
}
}
}

pub async fn run_with(
entry_data: genvm_modules_interfaces::ExecutionData,
context: ExecutionContext,
Expand All @@ -532,6 +557,7 @@ pub async fn run_with(
// during deploy attaches to this very entry at spawn. Dropping the pin
// earlier would make that load's outcome depend on which queue processed it.
let mut deploy_pin: Option<runners::cache::ArchivePin> = None;
let reporting_boundary = ReportingBoundary::for_execution_data(&entry_data);

let supervisor = context.supervisor.clone();

Expand Down Expand Up @@ -588,6 +614,7 @@ pub async fn run_with(
let res = match res {
Ok((mut a, b)) => {
a.discard_effects_unless_returned();
reporting_boundary.normalize(&mut a);

Ok(host::FullResult::new(
a,
Expand Down Expand Up @@ -630,4 +657,22 @@ mod tests {
assert!(!has_extra_leader_output(rt::RunMode::Leader, 0, 0));
assert!(!has_extra_leader_output(rt::RunMode::Sync, 0, 0));
}

#[test]
fn reporting_boundary_coalesces_only_top_level_fatality() {
let fatal = || {
rt::vm::FullResult::empty_from(rt::vm::RunOk::FatalVMError(
public_abi::VmError::timeout(),
None,
))
};
let mut top_level = fatal();
let mut nested = fatal();

ReportingBoundary::TopLevel.normalize(&mut top_level);
ReportingBoundary::Nested.normalize(&mut nested);

assert_eq!(top_level.kind, host::host_fns::ResultCode::VmError);
assert_eq!(nested.kind, host::host_fns::ResultCode::FatalVmError);
}
}
4 changes: 2 additions & 2 deletions executor/src/rt/supervisor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,13 +264,13 @@ pub async fn submit_nondet_vm_task(zelf: &Arc<Supervisor>, task: NonDetVMTask) {
}

impl Supervisor {
pub async fn push_nondet_result(&self, call_no: u32, result: bytes::Bytes) {
pub async fn push_nondet_result(&self, call_no: u32, result: rt::vm::ContractResultBytes) {
let mut vec = self.nondet_results.lock().await;
let idx = u32_into_usize(call_no);
while vec.len() <= idx {
vec.push(bytes::Bytes::new());
}
vec[idx] = result;
vec[idx] = result.into_bytes();
}

pub async fn take_nondet_results(&self) -> Vec<bytes::Bytes> {
Expand Down
164 changes: 103 additions & 61 deletions executor/src/rt/vm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,93 @@ pub enum RunOk {
FatalVMError(abi::consts::VmError, Option<anyhow::Error>),
}

#[derive(Debug)]
pub enum ContractOutcome {
Return(calldata::unparsed::Maybe<calldata::Value>),
UserError(calldata::unparsed::Maybe<calldata::Value>),
VMError(abi::consts::VmError, Option<anyhow::Error>),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContractResultBytes(bytes::Bytes);

impl ContractResultBytes {
pub fn as_slice(&self) -> &[u8] {
&self.0
}

pub fn into_bytes(self) -> bytes::Bytes {
self.0
}
}

impl ContractOutcome {
pub fn duplicate(&self) -> Self {
match self {
Self::Return(buf) => Self::Return(buf.clone()),
Self::UserError(buf) => Self::UserError(buf.clone()),
Self::VMError(e, _) => Self::VMError(e.clone(), None),
}
}

pub fn encode(&self) -> ContractResultBytes {
use crate::public_abi::ResultCode;

let bytes = match self {
Self::Return(buf) => {
let encoded = calldata::encode_obj(buf);
let mut res = Vec::with_capacity(1 + encoded.len());
res.push(ResultCode::Return as u8);
res.extend_from_slice(&encoded);
res
}
Self::UserError(val) => {
let mut res = vec![ResultCode::UserError as u8];
match val {
calldata::unparsed::Maybe::Materialized(value) => {
res.extend_from_slice(&calldata::encode(value));
}
calldata::unparsed::Maybe::Checked(raw) => {
res.extend_from_slice(&raw.0);
}
}
res
}
Self::VMError(buf, _) => {
let mut res = Vec::with_capacity(1 + buf.0.len());
res.push(ResultCode::VmError as u8);
res.extend_from_slice(buf.0.as_bytes());
res
}
};

ContractResultBytes(bytes.into())
}
}

impl From<ContractOutcome> for RunOk {
fn from(value: ContractOutcome) -> Self {
match value {
ContractOutcome::Return(buf) => Self::Return(buf),
ContractOutcome::UserError(buf) => Self::UserError(buf),
ContractOutcome::VMError(e, cause) => Self::VMError(e, cause),
}
}
}

impl TryFrom<RunOk> for ContractOutcome {
type Error = rt::errors::Error;

fn try_from(value: RunOk) -> Result<Self, Self::Error> {
match value {
RunOk::Return(buf) => Ok(Self::Return(buf)),
RunOk::UserError(buf) => Ok(Self::UserError(buf)),
RunOk::VMError(e, cause) => Ok(Self::VMError(e, cause)),
RunOk::FatalVMError(e, cause) => Err(rt::errors::Error::fatal_vm_cause(e, cause)),
}
}
}

impl RunOk {
/// Like clone, but drops the `cause` of a VM error if present
pub fn duplicate(&self) -> Self {
Expand All @@ -25,15 +112,8 @@ impl RunOk {
}
}

pub fn into_nonfatal(self) -> rt::errors::Result<Self> {
match self {
RunOk::FatalVMError(e, cause) => Err(rt::errors::Error::fatal_vm_cause(e, cause)),
run_ok => Ok(run_ok),
}
}

pub fn into_contract_observable_bytes(self) -> rt::errors::Result<Vec<u8>> {
self.into_nonfatal().map(|run_ok| run_ok.as_bytes())
ContractOutcome::try_from(self).map(|outcome| outcome.encode().into_bytes().to_vec())
}
}

Expand Down Expand Up @@ -96,52 +176,18 @@ impl FullResult {
self.storage_changes.clear();
self.emissions.clear();
}

pub fn coalesce_fatal_for_top_level(&mut self) {
if self.kind == host_fns::ResultCode::FatalVmError {
self.kind = host_fns::ResultCode::VmError;
}
}
}

impl RunOk {
pub fn empty_return() -> Self {
Self::Return(calldata::Value::Null.into())
}

pub fn as_bytes(&self) -> Vec<u8> {
use crate::public_abi::ResultCode;
match self {
RunOk::Return(buf) => {
let encoded = calldata::encode_obj(buf);
let mut res = Vec::with_capacity(1 + encoded.len());
res.push(ResultCode::Return as u8);
res.extend_from_slice(&encoded);
res
}
RunOk::UserError(val) => {
let mut res = vec![ResultCode::UserError as u8];
match val {
calldata::unparsed::Maybe::Materialized(value) => {
res.extend_from_slice(&calldata::encode(value));
}
calldata::unparsed::Maybe::Checked(raw) => {
res.extend_from_slice(&raw.0);
}
}
res
}
RunOk::VMError(buf, _) => {
let mut res = Vec::with_capacity(1 + buf.0.len());
res.push(ResultCode::VmError as u8);
res.extend_from_slice(buf.0.as_bytes());
res
}
// Fatality has to survive a sub-VM buffer that is relayed across a
// major boundary, so it keeps a code of its own -- one the host
// wire admits and the contract-facing enumeration does not.
RunOk::FatalVMError(buf, _) => {
let mut res = Vec::with_capacity(1 + buf.0.len());
res.push(crate::host::host_fns::ResultCode::FatalVmError as u8);
res.extend_from_slice(buf.0.as_bytes());
res
}
}
}
}

impl std::fmt::Display for RunOk {
Expand Down Expand Up @@ -386,25 +432,21 @@ mod tests {
}

#[test]
fn fatal_result_serialization_is_total() {
let code = public_abi::VmError::timeout();
let bytes = RunOk::FatalVMError(code.clone(), None).as_bytes();
fn fatal_result_is_reported_as_fatal() {
let full =
FullResult::empty_from(RunOk::FatalVMError(public_abi::VmError::timeout(), None));

assert_eq!(
bytes[0],
crate::host::host_fns::ResultCode::FatalVmError as u8
);
assert_eq!(&bytes[1..], code.0.as_bytes());
assert_eq!(full.kind, host_fns::ResultCode::FatalVmError);
}

// Fatality is reported truthfully to the host; degrading it to an ordinary
// VM error is the manager's job, at the outermost boundary.
#[test]
fn fatal_result_is_reported_as_fatal() {
let full =
fn top_level_fatal_result_is_coalesced() {
let mut full =
FullResult::empty_from(RunOk::FatalVMError(public_abi::VmError::timeout(), None));

assert_eq!(full.kind, host_fns::ResultCode::FatalVmError);
full.coalesce_fatal_for_top_level();

assert_eq!(full.kind, host_fns::ResultCode::VmError);
}

fn with_effects(run_ok: RunOk) -> FullResult {
Expand Down
Loading