Skip to content
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

add support for actor events. #1049

Merged
merged 31 commits into from
Nov 15, 2022
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
277aa3c
events: add basic eventing data model.
raulk Nov 5, 2022
912e65b
events: event eventing data model.
raulk Nov 6, 2022
8a5d538
events: implement the fvm-side handling of actor events.
raulk Nov 6, 2022
2c2d958
events: add events AMT root CID in Receipt.
raulk Nov 6, 2022
b498f03
events: implement support in SDK.
raulk Nov 6, 2022
19f88c3
events: implement basic integration tests.
raulk Nov 6, 2022
56b2679
fix typo.
raulk Nov 6, 2022
7d72eee
events: Receipt: make events_root is an Option<Cid>.
raulk Nov 6, 2022
cc60eea
fix clippy.
raulk Nov 6, 2022
d0648f2
events: charge gas for actor events (params TODO) + use bitflags.
raulk Nov 6, 2022
f18b9d9
fix comment.
raulk Nov 6, 2022
ab4880b
fix compile error.
raulk Nov 9, 2022
da15510
Merge branch 'master' into raulk/events
raulk Nov 9, 2022
40bf949
fix clippy.
raulk Nov 9, 2022
9c29e6e
Merge branch 'master' into raulk/events
raulk Nov 13, 2022
bd35a04
events: make sdk::event::emit_event borrow.
raulk Nov 13, 2022
041ea1a
fix clippy.
raulk Nov 14, 2022
1949ba1
Merge branch 'master' into raulk/events
raulk Nov 14, 2022
4b93932
fix events test.
raulk Nov 14, 2022
3d2b971
Merge branch 'master' into raulk/events
raulk Nov 14, 2022
f1ac14b
fix: serde ActorEvent as a tuple.
raulk Nov 14, 2022
44cfc40
events: serialize ActorEvent as transparent.
raulk Nov 14, 2022
10e4f51
events: fix Entry#value type.
raulk Nov 14, 2022
7d1a62f
address review comment.
raulk Nov 14, 2022
f380575
improve TODO.
raulk Nov 14, 2022
547d8c2
events: initialize event accumulator with no capacity.
raulk Nov 15, 2022
3a8d2e5
address review comment.
raulk Nov 15, 2022
a19b83c
discard events emitted by aborting actors, and its subcallees.
raulk Nov 15, 2022
240b7e5
simplify events accumulator layer accounting.
raulk Nov 15, 2022
30c9d71
fix clippy.
raulk Nov 15, 2022
02e7944
address nits.
raulk Nov 15, 2022
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
10 changes: 10 additions & 0 deletions fvm/src/call_manager/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use fvm_ipld_encoding::{to_vec, RawBytes, DAG_CBOR};
use fvm_shared::address::{Address, Payload};
use fvm_shared::econ::TokenAmount;
use fvm_shared::error::{ErrorNumber, ExitCode};
use fvm_shared::event::StampedEvent;
use fvm_shared::sys::BlockId;
use fvm_shared::{ActorID, MethodNum, METHOD_SEND};
use num_traits::Zero;
Expand Down Expand Up @@ -48,6 +49,8 @@ pub struct InnerDefaultCallManager<M> {
exec_trace: ExecutionTrace,
/// Number of actors that have been invoked in this message execution.
invocation_count: u64,
/// Accumulator for events emitted in this call stack.
events: Vec<StampedEvent>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to handle reverts/aborts somehow.

Copy link
Member Author

@raulk raulk Nov 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is unfortunately a bit of a can of worms.

  1. Filecoin does not revert side effects on non-zero exit codes (i.e. you can change your state root and abort with a non-zero exit code, and the protocol doesn’t revert your state change).
  2. Events are trickier though, because they are fire-and-forget. Once an event is emitted, you can't take a compensating action to un-emit it.
    • By contrast, you can always revert a state root update. For example, you track your previous state root, update your state root, detect a failure scenario, and revert back to your original state root, before returning a non-zero exit code.
  3. Ethereum does revert side effects (including storage updates) on REVERT. We currently enforce the rollback of storage mutations by using a transaction block in the EVM runtime (i.e. entirely actor-side), but this begs to introduce protocol support for rollback on failure, which would create a new precedent.

I can think of two ways to handle this:

  1. Layer events in the call manager and the last layer on a non-zero exit. This would prevent us from intentionally emitting events on failure (e.g. I could reasonably conceive a "window post failed" event).
  2. Enable actors to throw away events emitted through their execution. The EVM runtime would use this feature, and we could make the transactional Runtime discard events by default on failure. Future native actors (or built-in actors) could choose retain events on failure.

I think (2) gives us the most flexibility.

Ultimately I propose we solve this in Sapphire (and I can own it myself). We can ship Topaz with this caveat.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, Filecoin totally reverts everything on non-zero exit codes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^What Steb said -- we do so in both the legacy VM and the FVM.

I think that makes most (all?) of the problems you're describing go away.

Copy link
Member Author

@raulk raulk Nov 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was in doubt and it sounded strange that it didn't, but I wasn't able to find where it was happening. It happens through the layering in the state tree:

fn with_transaction(

}

#[doc(hidden)]
Expand Down Expand Up @@ -93,6 +96,7 @@ where
backtrace: Backtrace::default(),
exec_trace: vec![],
invocation_count: 0,
events: Vec::with_capacity(8),
raulk marked this conversation as resolved.
Show resolved Hide resolved
})))
}

Expand Down Expand Up @@ -186,6 +190,7 @@ where
backtrace,
mut gas_tracker,
mut exec_trace,
events,
..
} = *self.0.take().expect("call manager is poisoned");

Expand All @@ -202,6 +207,7 @@ where
gas_used,
backtrace,
exec_trace,
events,
},
machine,
)
Expand Down Expand Up @@ -246,6 +252,10 @@ where
fn invocation_count(&self) -> u64 {
self.invocation_count
}

fn append_event(&mut self, evt: StampedEvent) {
self.events.push(evt)
}
}

impl<M> DefaultCallManager<M>
Expand Down
5 changes: 5 additions & 0 deletions fvm/src/call_manager/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub use backtrace::Backtrace;
mod default;

pub use default::DefaultCallManager;
use fvm_shared::event::StampedEvent;

use crate::trace::ExecutionTrace;

Expand Down Expand Up @@ -127,6 +128,9 @@ pub trait CallManager: 'static {
self.gas_tracker_mut().apply_charge(charge)?;
Ok(())
}

/// Appends an event to the event accumulator.
fn append_event(&mut self, evt: StampedEvent);
}

/// The result of a method invocation.
Expand Down Expand Up @@ -160,4 +164,5 @@ pub struct FinishRet {
pub gas_used: i64,
pub backtrace: Backtrace,
pub exec_trace: ExecutionTrace,
pub events: Vec<StampedEvent>,
}
114 changes: 69 additions & 45 deletions fvm/src/executor/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use fvm_shared::address::Address;
use fvm_shared::address::Payload;
use fvm_shared::econ::TokenAmount;
use fvm_shared::error::{ErrorNumber, ExitCode};
use fvm_shared::event::StampedEvent;
use fvm_shared::message::Message;
use fvm_shared::receipt::Receipt;
use fvm_shared::ActorID;
Expand All @@ -19,13 +20,14 @@ use crate::call_manager::{backtrace, CallManager, InvocationResult};
use crate::gas::{Gas, GasCharge, GasOutputs};
use crate::kernel::{Block, ClassifyResult, Context as _, ExecutionError, Kernel};
use crate::machine::{Machine, BURNT_FUNDS_ACTOR_ADDR, REWARD_ACTOR_ADDR};
use crate::trace::ExecutionTrace;

/// The default [`Executor`].
///
/// # Warning
///
/// Message execution might run out of stack and crash (the entire process) if it doesn't have at
/// least 64MiB of stacks space. If you can't guarantee 64MiB of stack space, wrap this executor in
/// least 64MiB of stack space. If you can't guarantee 64MiB of stack space, wrap this executor in
/// a [`ThreadedExecutor`][super::ThreadedExecutor].
// If the inner value is `None` it means the machine got poisoned and is unusable.
#[repr(transparent)]
Expand Down Expand Up @@ -66,46 +68,62 @@ where
};

// Apply the message.
let (res, gas_used, mut backtrace, exec_trace) = self.map_machine(|machine| {
// We're processing a chain message, so the sender is the origin of the call stack.
let mut cm = K::CallManager::new(
machine,
msg.gas_limit,
sender_id,
msg.sequence,
msg.gas_premium.clone(),
);
// This error is fatal because it should have already been accounted for inside
// preflight_message.
if let Err(e) = cm.charge_gas(inclusion_cost) {
return (Err(e), cm.finish().1);
}

let params = if msg.params.is_empty() {
None
} else {
Some(Block::new(DAG_CBOR, msg.params.bytes()))
};
let (res, gas_used, mut backtrace, exec_trace, events_root, events) =
self.map_machine(|machine| {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we've reached the point where we may want to just define a struct (we can define one inside this function). Even if we destructure immediately, it'll get us nice named fields and prevent potential mistakes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I can do that in this PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// We're processing a chain message, so the sender is the origin of the call stack.
let mut cm = K::CallManager::new(
machine,
msg.gas_limit,
sender_id,
msg.sequence,
msg.gas_premium.clone(),
);
// This error is fatal because it should have already been accounted for inside
// preflight_message.
if let Err(e) = cm.charge_gas(inclusion_cost) {
return (Err(e), cm.finish().1);
}

let result = cm.with_transaction(|cm| {
// Invoke the message.
let ret = cm.send::<K>(sender_id, msg.to, msg.method_num, params, &msg.value)?;
let params = if msg.params.is_empty() {
None
} else {
Some(Block::new(DAG_CBOR, msg.params.bytes()))
};

// Charge for including the result (before we end the transaction).
if let InvocationResult::Return(value) = &ret {
cm.charge_gas(cm.context().price_list.on_chain_return_value(
value.as_ref().map(|v| v.size() as usize).unwrap_or(0),
))?;
}
let result = cm.with_transaction(|cm| {
// Invoke the message.
let ret =
cm.send::<K>(sender_id, msg.to, msg.method_num, params, &msg.value)?;

// Charge for including the result (before we end the transaction).
if let InvocationResult::Return(value) = &ret {
cm.charge_gas(cm.context().price_list.on_chain_return_value(
value.as_ref().map(|v| v.size() as usize).unwrap_or(0),
))?;
}

Ok(ret)
});
let (res, machine) = cm.finish();

// Flush all events to the store.
let events_root = match machine.commit_events(res.events.as_slice()) {
raulk marked this conversation as resolved.
Show resolved Hide resolved
Ok(cid) => cid,
Err(e) => return (Err(e), machine),
};

Ok(ret)
});
let (res, machine) = cm.finish();
(
Ok((result, res.gas_used, res.backtrace, res.exec_trace)),
machine,
)
})?;
(
Ok((
result,
res.gas_used,
res.backtrace,
res.exec_trace,
events_root,
res.events,
)),
machine,
)
})?;

// Extract the exit code and build the result of the message application.
let receipt = match res {
Expand All @@ -121,6 +139,7 @@ where
exit_code: ExitCode::OK,
return_data,
gas_used,
events_root,
}
}
Ok(InvocationResult::Failure(exit_code)) => {
Expand All @@ -131,12 +150,14 @@ where
exit_code,
return_data: Default::default(),
gas_used,
events_root,
}
}
Err(ExecutionError::OutOfGas) => Receipt {
exit_code: ExitCode::SYS_OUT_OF_GAS,
return_data: Default::default(),
gas_used,
events_root,
},
Err(ExecutionError::Syscall(err)) => {
// Errors indicate the message couldn't be dispatched at all
Expand All @@ -153,6 +174,7 @@ where
exit_code,
return_data: Default::default(),
gas_used,
events_root,
}
}
Err(ExecutionError::Fatal(err)) => {
Expand All @@ -177,6 +199,7 @@ where
exit_code: ExitCode::SYS_ASSERTION_FAILED,
return_data: Default::default(),
gas_used: msg.gas_limit,
events_root,
}
}
};
Expand All @@ -188,12 +211,9 @@ where
};

match apply_kind {
ApplyKind::Explicit => self
.finish_message(msg, receipt, failure_info, gas_cost)
.map(|mut apply_ret| {
apply_ret.exec_trace = exec_trace;
apply_ret
}),
ApplyKind::Explicit => {
self.finish_message(msg, receipt, failure_info, gas_cost, exec_trace, events)
}
ApplyKind::Implicit => Ok(ApplyRet {
msg_receipt: receipt,
penalty: TokenAmount::zero(),
Expand All @@ -205,6 +225,7 @@ where
gas_burned: 0,
failure_info,
exec_trace,
events,
}),
}
}
Expand Down Expand Up @@ -367,6 +388,8 @@ where
receipt: Receipt,
failure_info: Option<ApplyFailure>,
gas_cost: TokenAmount,
exec_trace: ExecutionTrace,
events: Vec<StampedEvent>,
) -> anyhow::Result<ApplyRet> {
// NOTE: we don't support old network versions in the FVM, so we always burn.
let GasOutputs {
Expand Down Expand Up @@ -425,7 +448,8 @@ where
gas_refund,
gas_burned,
failure_info,
exec_trace: vec![],
exec_trace,
events,
})
}

Expand Down
5 changes: 5 additions & 0 deletions fvm/src/executor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub use default::DefaultExecutor;
use fvm_ipld_encoding::RawBytes;
use fvm_shared::econ::TokenAmount;
use fvm_shared::error::ExitCode;
use fvm_shared::event::StampedEvent;
use fvm_shared::message::Message;
use fvm_shared::receipt::Receipt;
use num_traits::Zero;
Expand Down Expand Up @@ -88,6 +89,8 @@ pub struct ApplyRet {
pub failure_info: Option<ApplyFailure>,
/// Execution trace information, for debugging.
pub exec_trace: ExecutionTrace,
/// Events generated while applying the message.
pub events: Vec<StampedEvent>,
}

impl ApplyRet {
Expand All @@ -102,6 +105,7 @@ impl ApplyRet {
exit_code: code,
return_data: RawBytes::default(),
gas_used: 0,
events_root: None,
},
penalty: miner_penalty,
miner_tip: TokenAmount::zero(),
Expand All @@ -112,6 +116,7 @@ impl ApplyRet {
gas_burned: 0,
failure_info: Some(ApplyFailure::PreValidation(message.into())),
exec_trace: vec![],
events: vec![],
}
}
}
Expand Down
Loading