Skip to content

Commit 788b5f2

Browse files
fridrik01Stebalien
authored andcommitted
feat: Improved event syscall API (#1807)
This PR changes the internal emit_event syscall and removes the CBOR formatting made between SDK and FVM. It does so by splitting the ActorEvent entries manually into three buffers that we know the exact size of and allows us to perform validation of certain cases (e.g check for max values) before doing any parsing.
1 parent b344ed4 commit 788b5f2

File tree

15 files changed

+269
-148
lines changed

15 files changed

+269
-148
lines changed

Cargo.lock

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fvm/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ rand = "0.8.5"
3838
quickcheck = { version = "1", optional = true }
3939
once_cell = "1.18"
4040
minstant = "0.1.2"
41+
static_assertions = "1.1.0"
4142

4243
[dev-dependencies]
4344
pretty_assertions = "1.3.0"

fvm/src/gas/price_list.rs

+44-67
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ use std::ops::Mul;
77

88
use anyhow::Context;
99
use fvm_shared::crypto::signature::SignatureType;
10-
use fvm_shared::event::{ActorEvent, Flags};
1110
use fvm_shared::piece::PieceInfo;
1211
use fvm_shared::sector::{
1312
AggregateSealVerifyProofAndInfos, RegisteredPoStProof, RegisteredSealProof, ReplicaUpdateInfo,
@@ -27,6 +26,21 @@ use crate::kernel::SupportedHashes;
2726
// https://docs.rs/wasmtime/2.0.2/wasmtime/struct.InstanceLimits.html#structfield.table_elements
2827
const TABLE_ELEMENT_SIZE: u32 = 8;
2928

29+
// The maximum overhead (in bytes) of a single event when encoded into CBOR.
30+
//
31+
// 1: CBOR tuple with 2 fields (StampedEvent)
32+
// 9: Emitter ID
33+
// 2: Entry array overhead (max size 255)
34+
const EVENT_OVERHEAD: u64 = 12;
35+
// The maximum overhead (in bytes) of a single event entry when encoded into CBOR.
36+
//
37+
// 1: CBOR tuple with 4 fields
38+
// 1: Flags (will adjust as more flags are added)
39+
// 2: Key major type + length (up to 255 bytes)
40+
// 2: Codec major type + value (codec should be <= 255)
41+
// 3: Value major type + length (up to 8192 bytes)
42+
const EVENT_ENTRY_OVERHEAD: u64 = 9;
43+
3044
/// Create a mapping from enum items to values in a way that guarantees at compile
3145
/// time that we did not miss any member, in any of the prices, even if the enum
3246
/// gets a new member later.
@@ -293,26 +307,15 @@ lazy_static! {
293307
memory_fill_per_byte_cost: Gas::from_milligas(400),
294308
},
295309

296-
// These parameters are specifically sized for EVM events. They will need
297-
// to be revisited before Wasm actors are able to emit arbitrary events.
298-
//
299-
// Validation costs are dependent on encoded length, but also
300-
// co-dependent on the number of entries. The latter is a chicken-and-egg
301-
// situation because we can only learn how many entries were emitted once we
302-
// decode the CBOR event.
303-
//
304-
// We will likely need to revisit the ABI of emit_event to remove CBOR
305-
// as the exchange format.
306-
event_validation_cost: ScalingCost {
310+
// TODO(#1817): Per-entry event validation cost. These parameters were benchmarked for the
311+
// EVM but haven't been revisited since revising the API.
312+
event_per_entry: ScalingCost {
307313
flat: Gas::new(1750),
308314
scale: Gas::new(25),
309315
},
310316

311-
// The protocol does not currently mandate indexing work, so these are
312-
// left at zero. Once we start populating and committing indexing data
313-
// structures, these costs will need to reflect the computation and
314-
// storage costs of such actions.
315-
event_accept_per_index_element: ScalingCost {
317+
// TODO(#1817): Cost of validating utf8 (used in event parsing).
318+
utf8_validation: ScalingCost {
316319
flat: Zero::zero(),
317320
scale: Zero::zero(),
318321
},
@@ -468,14 +471,14 @@ pub struct PriceList {
468471

469472
/// Gas cost to validate an ActorEvent as soon as it's received from the actor, and prior
470473
/// to it being parsed.
471-
pub(crate) event_validation_cost: ScalingCost,
472-
473-
/// Gas cost of every indexed element, scaling per number of bytes indexed.
474-
pub(crate) event_accept_per_index_element: ScalingCost,
474+
pub(crate) event_per_entry: ScalingCost,
475475

476476
/// Gas cost of doing lookups in the builtin actor mappings.
477477
pub(crate) builtin_actor_manifest_lookup: Gas,
478478

479+
/// Gas cost of utf8 parsing.
480+
pub(crate) utf8_validation: ScalingCost,
481+
479482
/// Gas cost of accessing the network context.
480483
pub(crate) network_context: Gas,
481484
/// Gas cost of accessing the message context.
@@ -921,59 +924,33 @@ impl PriceList {
921924
}
922925

923926
#[inline]
924-
pub fn on_actor_event_validate(&self, data_size: usize) -> GasCharge {
925-
let memcpy = self.block_memcpy.apply(data_size);
926-
let alloc = self.block_allocate.apply(data_size);
927-
let validate = self.event_validation_cost.apply(data_size);
928-
929-
GasCharge::new(
930-
"OnActorEventValidate",
931-
memcpy + alloc + validate,
932-
Zero::zero(),
933-
)
934-
}
935-
936-
#[inline]
937-
pub fn on_actor_event_accept(&self, evt: &ActorEvent, serialized_len: usize) -> GasCharge {
938-
let (mut indexed_bytes, mut indexed_elements) = (0usize, 0u32);
939-
for evt in evt.entries.iter() {
940-
if evt.flags.contains(Flags::FLAG_INDEXED_KEY) {
941-
indexed_bytes += evt.key.len();
942-
indexed_elements += 1;
943-
}
944-
if evt.flags.contains(Flags::FLAG_INDEXED_VALUE) {
945-
indexed_bytes += evt.value.len();
946-
indexed_elements += 1;
947-
}
948-
}
927+
pub fn on_actor_event(&self, entries: usize, keysize: usize, valuesize: usize) -> GasCharge {
928+
// Here we estimate per-event overhead given the constraints on event values.
949929

950-
// The estimated size of the serialized StampedEvent event, which
951-
// includes the ActorEvent + 8 bytes for the actor ID + some bytes
952-
// for CBOR framing.
953-
const STAMP_EXTRA_SIZE: usize = 12;
954-
let stamped_event_size = serialized_len + STAMP_EXTRA_SIZE;
930+
let validate_entries = self.event_per_entry.apply(entries);
931+
let validate_utf8 = self.utf8_validation.apply(keysize);
955932

956-
// Charge for 3 memory copy operations.
957-
// This includes the cost of forming a StampedEvent, copying into the
958-
// AMT's buffer on finish, and returning to the client.
959-
let memcpy = self.block_memcpy.apply(stamped_event_size);
933+
// Estimate the size, saturating at max-u64. Given how we calculate gas, this will saturate
934+
// the gas maximum at max-u64 milligas.
935+
let estimated_size = EVENT_OVERHEAD
936+
.saturating_add(EVENT_ENTRY_OVERHEAD.saturating_mul(entries as u64))
937+
.saturating_add(keysize as u64)
938+
.saturating_add(valuesize as u64);
960939

961-
// Charge for 2 memory allocations.
962-
// This includes the cost of retaining the StampedEvent in the call manager,
963-
// and allocaing into the AMT's buffer on finish.
964-
let alloc = self.block_allocate.apply(stamped_event_size);
940+
// Calculate the cost per copy (one memcpy + one allocation).
941+
let mem =
942+
self.block_memcpy.apply(estimated_size) + self.block_allocate.apply(estimated_size);
965943

966944
// Charge for the hashing on AMT insertion.
967-
let hash = self.hashing_cost[&SupportedHashes::Blake2b256].apply(stamped_event_size);
945+
let hash = self.hashing_cost[&SupportedHashes::Blake2b256].apply(estimated_size);
968946

969947
GasCharge::new(
970-
"OnActorEventAccept",
971-
memcpy + alloc,
972-
self.event_accept_per_index_element.flat * indexed_elements
973-
+ self.event_accept_per_index_element.scale * indexed_bytes
974-
+ memcpy * 2u32 // deferred cost, placing here to hide from benchmark
975-
+ alloc // deferred cost, placing here to hide from benchmark
976-
+ hash, // deferred cost, placing here to hide from benchmark
948+
"OnActorEvent",
949+
// Charge for validation/storing events.
950+
mem + validate_entries + validate_utf8,
951+
// Charge for forming the AMT and returning the events to the client.
952+
// one copy into the AMT, one copy to the client.
953+
hash + (mem * 2u32),
977954
)
978955
}
979956
}

fvm/src/kernel/default.rs

+84-56
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use fvm_shared::consensus::ConsensusFault;
1717
use fvm_shared::crypto::signature;
1818
use fvm_shared::econ::TokenAmount;
1919
use fvm_shared::error::ErrorNumber;
20-
use fvm_shared::event::ActorEvent;
20+
use fvm_shared::event::{ActorEvent, Entry, Flags};
2121
use fvm_shared::piece::{zero_piece_commitment, PaddedPieceSize};
2222
use fvm_shared::sector::RegisteredPoStProof::{StackedDRGWindow32GiBV1, StackedDRGWindow32GiBV1P1};
2323
use fvm_shared::sector::{RegisteredPoStProof, SectorInfo};
@@ -1004,42 +1004,99 @@ impl<C> EventOps for DefaultKernel<C>
10041004
where
10051005
C: CallManager,
10061006
{
1007-
fn emit_event(&mut self, raw_evt: &[u8]) -> Result<()> {
1007+
fn emit_event(
1008+
&mut self,
1009+
event_headers: &[fvm_shared::sys::EventEntry],
1010+
event_keys: &[u8],
1011+
event_values: &[u8],
1012+
) -> Result<()> {
1013+
const MAX_NR_ENTRIES: usize = 255;
1014+
const MAX_KEY_LEN: usize = 31;
1015+
const MAX_TOTAL_VALUES_LEN: usize = 8 << 10;
1016+
10081017
if self.read_only {
10091018
return Err(syscall_error!(ReadOnly; "cannot emit events while read-only").into());
10101019
}
1011-
let len = raw_evt.len();
1020+
10121021
let t = self
10131022
.call_manager
1014-
.charge_gas(self.call_manager.price_list().on_actor_event_validate(len))?;
1023+
.charge_gas(self.call_manager.price_list().on_actor_event(
1024+
event_headers.len(),
1025+
event_keys.len(),
1026+
event_values.len(),
1027+
))?;
1028+
1029+
if event_headers.len() > MAX_NR_ENTRIES {
1030+
return Err(syscall_error!(IllegalArgument; "event exceeded max entries: {} > {MAX_NR_ENTRIES}", event_headers.len()).into());
1031+
}
10151032

1016-
// This is an over-estimation of the maximum event size, for safety. No valid event can even
1017-
// get close to this. We check this first so we don't try to decode a large event.
1018-
const MAX_ENCODED_SIZE: usize = 1 << 20;
1019-
if raw_evt.len() > MAX_ENCODED_SIZE {
1020-
return Err(syscall_error!(IllegalArgument; "event WAY too large").into());
1033+
// We check this here purely to detect/prevent integer overflows.
1034+
if event_values.len() > MAX_TOTAL_VALUES_LEN {
1035+
return Err(syscall_error!(IllegalArgument; "total event value lengths exceeded the max size: {} > {MAX_TOTAL_VALUES_LEN}", event_values.len()).into());
10211036
}
10221037

1023-
let actor_evt = {
1024-
let res = match panic::catch_unwind(|| {
1025-
fvm_ipld_encoding::from_slice(raw_evt).or_error(ErrorNumber::IllegalArgument)
1026-
}) {
1027-
Ok(v) => v,
1028-
Err(e) => {
1029-
log::error!("panic when decoding event cbor from actor: {:?}", e);
1030-
Err(syscall_error!(IllegalArgument; "panic when decoding event cbor from actor").into())
1031-
}
1038+
let mut key_offset: usize = 0;
1039+
let mut val_offset: usize = 0;
1040+
1041+
let mut entries: Vec<Entry> = Vec::with_capacity(event_headers.len());
1042+
for header in event_headers {
1043+
// make sure that the fixed parsed values are within bounds before we do any allocation
1044+
let flags = header.flags;
1045+
if Flags::from_bits(flags.bits()).is_none() {
1046+
return Err(
1047+
syscall_error!(IllegalArgument; "event flags are invalid: {}", flags.bits())
1048+
.into(),
1049+
);
1050+
}
1051+
if header.key_len > MAX_KEY_LEN as u32 {
1052+
let tmp = header.key_len;
1053+
return Err(syscall_error!(IllegalArgument; "event key exceeded max size: {} > {MAX_KEY_LEN}", tmp).into());
1054+
}
1055+
// We check this here purely to detect/prevent integer overflows.
1056+
if header.val_len > MAX_TOTAL_VALUES_LEN as u32 {
1057+
return Err(
1058+
syscall_error!(IllegalArgument; "event entry value out of range").into(),
1059+
);
1060+
}
1061+
if header.codec != IPLD_RAW {
1062+
let tmp = header.codec;
1063+
return Err(
1064+
syscall_error!(IllegalCodec; "event codec must be IPLD_RAW, was: {}", tmp)
1065+
.into(),
1066+
);
1067+
}
1068+
1069+
// parse the variable sized fields from the raw_key/raw_val buffers
1070+
let key = &event_keys
1071+
.get(key_offset..key_offset + header.key_len as usize)
1072+
.context("event entry key out of range")
1073+
.or_illegal_argument()?;
1074+
1075+
let key = std::str::from_utf8(key)
1076+
.context("invalid event key")
1077+
.or_illegal_argument()?;
1078+
1079+
let value = &event_values
1080+
.get(val_offset..val_offset + header.val_len as usize)
1081+
.context("event entry value out of range")
1082+
.or_illegal_argument()?;
1083+
1084+
// we have all we need to construct a new Entry
1085+
let entry = Entry {
1086+
flags: header.flags,
1087+
key: key.to_string(),
1088+
codec: header.codec,
1089+
value: value.to_vec(),
10321090
};
1033-
t.stop();
1034-
res
1035-
}?;
1036-
validate_actor_event(&actor_evt)?;
10371091

1038-
let t = self.call_manager.charge_gas(
1039-
self.call_manager
1040-
.price_list()
1041-
.on_actor_event_accept(&actor_evt, len),
1042-
)?;
1092+
// shift the key/value offsets
1093+
key_offset += header.key_len as usize;
1094+
val_offset += header.val_len as usize;
1095+
1096+
entries.push(entry);
1097+
}
1098+
1099+
let actor_evt = ActorEvent::from(entries);
10431100

10441101
let stamped_evt = StampedEvent::new(self.actor_id, actor_evt);
10451102
self.call_manager.append_event(stamped_evt);
@@ -1060,35 +1117,6 @@ fn catch_and_log_panic<F: FnOnce() -> Result<R> + UnwindSafe, R>(context: &str,
10601117
}
10611118
}
10621119

1063-
fn validate_actor_event(evt: &ActorEvent) -> Result<()> {
1064-
const MAX_ENTRIES: usize = 256;
1065-
const MAX_DATA: usize = 8 << 10;
1066-
const MAX_KEY_LEN: usize = 32;
1067-
1068-
if evt.entries.len() > MAX_ENTRIES {
1069-
return Err(syscall_error!(IllegalArgument; "event exceeded max entries: {} > {MAX_ENTRIES}", evt.entries.len()).into());
1070-
}
1071-
let mut total_value_size: usize = 0;
1072-
for entry in &evt.entries {
1073-
if entry.key.len() > MAX_KEY_LEN {
1074-
return Err(syscall_error!(IllegalArgument; "event key exceeded max size: {} > {MAX_KEY_LEN}", entry.key.len()).into());
1075-
}
1076-
if entry.codec != IPLD_RAW {
1077-
return Err(
1078-
syscall_error!(IllegalCodec; "event codec must be IPLD_RAW, was: {}", entry.codec)
1079-
.into(),
1080-
);
1081-
}
1082-
total_value_size += entry.value.len();
1083-
}
1084-
if total_value_size > MAX_DATA {
1085-
return Err(
1086-
syscall_error!(IllegalArgument; "event total values exceeded max size: {total_value_size} > {MAX_DATA}").into(),
1087-
);
1088-
}
1089-
Ok(())
1090-
}
1091-
10921120
fn prover_id_from_u64(id: u64) -> ProverId {
10931121
let mut prover_id = ProverId::default();
10941122
let prover_bytes = Address::new_id(id).payload().to_raw_bytes();

fvm/src/kernel/mod.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -373,5 +373,10 @@ pub trait LimiterOps {
373373
/// Eventing APIs.
374374
pub trait EventOps {
375375
/// Records an event emitted throughout execution.
376-
fn emit_event(&mut self, raw_evt: &[u8]) -> Result<()>;
376+
fn emit_event(
377+
&mut self,
378+
event_headers: &[fvm_shared::sys::EventEntry],
379+
raw_key: &[u8],
380+
raw_val: &[u8],
381+
) -> Result<()>;
377382
}

0 commit comments

Comments
 (0)