Skip to content

Commit c4c808a

Browse files
raulkStebalien
andauthored
Add gas parameters for event validation + calibration harnesses. (#1635)
Co-authored-by: Steven Allen <steven@stebalien.com>
1 parent 7a11399 commit c4c808a

File tree

17 files changed

+335
-28
lines changed

17 files changed

+335
-28
lines changed

fvm/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,4 @@ testing = []
6969
arb = ["arbitrary", "quickcheck"]
7070
m2-native = []
7171
hyperspace = []
72+
gas_calibration = []

fvm/src/gas/price_list.rs

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -269,13 +269,25 @@ lazy_static! {
269269
memory_fill_per_byte_cost: Gas::from_milligas(400),
270270
},
271271

272-
// TODO::PARAM
272+
// These parameters are specifically sized for EVM events. They will need
273+
// to be revisited before Wasm actors are able to emit arbitrary events.
274+
//
275+
// Validation costs are dependent on encoded length, but also
276+
// co-dependent on the number of entries. The latter is a chicken-and-egg
277+
// situation because we can only learn how many entries were emitted once we
278+
// decode the CBOR event.
279+
//
280+
// We will likely need to revisit the ABI of emit_event to remove CBOR
281+
// as the exchange format.
273282
event_validation_cost: ScalingCost {
274-
flat: Zero::zero(),
275-
scale: Zero::zero(),
283+
flat: Gas::new(1750),
284+
scale: Gas::new(25),
276285
},
277286

278-
// TODO::PARAM
287+
// The protocol does not currently mandate indexing work, so these are
288+
// left at zero. Once we start populating and committing indexing data
289+
// structures, these costs will need to reflect the computation and
290+
// storage costs of such actions.
279291
event_accept_per_index_element: ScalingCost {
280292
flat: Zero::zero(),
281293
scale: Zero::zero(),
@@ -890,19 +902,33 @@ impl PriceList {
890902
}
891903
}
892904

893-
// The estimated size of the serialized StampedEvent event, which includes the ActorEvent
894-
// + 8 bytes for the actor ID + some bytes for CBOR framing.
905+
// The estimated size of the serialized StampedEvent event, which
906+
// includes the ActorEvent + 8 bytes for the actor ID + some bytes
907+
// for CBOR framing.
895908
const STAMP_EXTRA_SIZE: usize = 12;
896909
let stamped_event_size = serialized_len + STAMP_EXTRA_SIZE;
897910

911+
// Charge for 3 memory copy operations.
912+
// This includes the cost of forming a StampedEvent, copying into the
913+
// AMT's buffer on finish, and returning to the client.
898914
let memcpy = self.block_memcpy.apply(stamped_event_size);
915+
916+
// Charge for 2 memory allocations.
917+
// This includes the cost of retaining the StampedEvent in the call manager,
918+
// and allocaing into the AMT's buffer on finish.
899919
let alloc = self.block_allocate.apply(stamped_event_size);
900920

921+
// Charge for the hashing on AMT insertion.
922+
let hash = self.hashing_cost[&SupportedHashes::Blake2b256].apply(stamped_event_size);
923+
901924
GasCharge::new(
902925
"OnActorEventAccept",
903-
(memcpy * 3u32) + alloc,
926+
memcpy + alloc,
904927
self.event_accept_per_index_element.flat * indexed_elements
905-
+ self.event_accept_per_index_element.scale * indexed_bytes,
928+
+ self.event_accept_per_index_element.scale * indexed_bytes
929+
+ memcpy * 2u32 // deferred cost, placing here to hide from benchmark
930+
+ alloc // deferred cost, placing here to hide from benchmark
931+
+ hash, // deferred cost, placing here to hide from benchmark
906932
)
907933
}
908934
}

fvm/src/kernel/default.rs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -985,18 +985,20 @@ where
985985
.call_manager
986986
.charge_gas(self.call_manager.price_list().on_actor_event_validate(len))?;
987987

988-
let actor_evt = match panic::catch_unwind(|| {
989-
fvm_ipld_encoding::from_slice(raw_evt).or_error(ErrorNumber::IllegalArgument)
990-
}) {
991-
Ok(v) => v,
992-
Err(e) => {
993-
log::error!("panic when decoding cbor from actor: {:?}", e);
994-
Err(syscall_error!(IllegalArgument; "panic when decoding cbor from actor").into())
995-
}
988+
let actor_evt = {
989+
let res = match panic::catch_unwind(|| {
990+
fvm_ipld_encoding::from_slice(raw_evt).or_error(ErrorNumber::IllegalArgument)
991+
}) {
992+
Ok(v) => v,
993+
Err(e) => {
994+
log::error!("panic when decoding event cbor from actor: {:?}", e);
995+
Err(syscall_error!(IllegalArgument; "panic when decoding event cbor from actor").into())
996+
}
997+
};
998+
t.stop();
999+
res
9961000
}?;
9971001

998-
t.stop();
999-
10001002
let t = self.call_manager.charge_gas(
10011003
self.call_manager
10021004
.price_list()

fvm/src/syscalls/event.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ pub fn emit_event(
1818
event_off: u32, // ActorEvent
1919
event_len: u32,
2020
) -> Result<()> {
21+
// Disable the limits for gas calibration.
22+
#[cfg(not(feature = "gas_calibration"))]
23+
{
24+
const MAX_CBOR_EVENT_LEN: u32 = 1024;
25+
if event_len > MAX_CBOR_EVENT_LEN {
26+
return Err(crate::syscall_error!(IllegalArgument; "exceeded maximum event length ({} bytes); got {}", MAX_CBOR_EVENT_LEN, event_len).into());
27+
}
28+
}
29+
2130
let raw = context.memory.try_slice(event_off, event_len)?;
2231
context.kernel.emit_event(raw)
2332
}

shared/src/event/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ use crate::ActorID;
1212
#[derive(Serialize_tuple, Deserialize_tuple, PartialEq, Eq, Clone, Debug)]
1313
pub struct StampedEvent {
1414
/// Carries the ID of the actor that emitted this event.
15-
emitter: ActorID,
15+
pub emitter: ActorID,
1616
/// The event as emitted by the actor.
17-
event: ActorEvent,
17+
pub event: ActorEvent,
1818
}
1919

2020
impl StampedEvent {

testing/calibration/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ authors = ["Protocol Labs", "Filecoin Core Devs"]
88
repository = "https://github.com/filecoin-project/ref-fvm"
99

1010
[dependencies]
11-
fvm = { version = "3.0.0-alpha.22", path = "../../fvm", default-features = false, features = ["testing"] }
11+
fvm = { version = "3.0.0-alpha.22", path = "../../fvm", default-features = false, features = ["testing", "gas_calibration"] }
1212
fvm_shared = { version = "3.0.0-alpha.18", path = "../../shared", features = ["testing"] }
1313
fvm_ipld_car = { version = "0.6.0", path = "../../ipld/car" }
1414
fvm_ipld_blockstore = { version = "0.1.1", path = "../../ipld/blockstore" }

testing/calibration/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ The calibration uses the machinery from the integration tests, but it's kept sep
1919

2020
Note that the `--release` flag has a huge impact on runtimes and therefore the model paramters, in the order of 100x.
2121

22-
Alternatively all the scenarios and exports can be executed the followign way:
22+
Alternatively all the scenarios and exports can be executed the following way:
2323

2424
```shell
2525
make run

testing/calibration/contract/fil-gas-calibration-actor/src/lib.rs

Lines changed: 105 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
// SPDX-License-Identifier: Apache-2.0, MIT
33
use anyhow::{anyhow, Result};
44
use cid::multihash::Code;
5-
use fvm_ipld_encoding::DAG_CBOR;
5+
use fvm_ipld_encoding::{RawBytes, DAG_CBOR};
66
use fvm_sdk::message::params_raw;
77
use fvm_sdk::vm::abort;
88
use fvm_shared::address::{Address, Protocol};
99
use fvm_shared::crypto::hash::SupportedHashes;
1010
use fvm_shared::crypto::signature::{Signature, SignatureType, SECP_SIG_LEN};
1111
use fvm_shared::econ::TokenAmount;
1212
use fvm_shared::error::ExitCode;
13+
use fvm_shared::event::{ActorEvent, Entry, Flags};
1314
use fvm_shared::sys::SendFlags;
1415
use num_derive::FromPrimitive;
1516
use num_traits::FromPrimitive;
@@ -35,6 +36,8 @@ pub enum Method {
3536
OnRecoverSecpPublicKey,
3637
/// Measure sends
3738
OnSend,
39+
/// Emit events, driven by the selected mode. See EventCalibrationMode for more info.
40+
OnEvent,
3841
}
3942

4043
#[derive(Serialize, Deserialize)]
@@ -76,6 +79,25 @@ pub struct OnRecoverSecpPublicKeyParams {
7679
pub seed: u64,
7780
}
7881

82+
#[derive(Serialize, Deserialize)]
83+
pub enum EventCalibrationMode {
84+
/// Produce events with the specified shape.
85+
Shape((usize, usize, usize)),
86+
/// Attempt to reach a target size for the CBOR event.
87+
TargetSize(usize),
88+
}
89+
90+
#[derive(Serialize, Deserialize)]
91+
pub struct OnEventParams {
92+
pub iterations: usize,
93+
pub mode: EventCalibrationMode,
94+
/// Number of entries in the event.
95+
pub entries: usize,
96+
/// Flags to apply to all entries.
97+
pub flags: Flags,
98+
pub seed: u64,
99+
}
100+
79101
#[derive(Serialize, Deserialize)]
80102
pub struct OnSendParams {
81103
pub iterations: usize,
@@ -125,6 +147,7 @@ fn dispatch(method: Method, params_ptr: u32) -> Result<()> {
125147
Method::OnVerifySignature => dispatch_to(on_verify_signature, params_ptr),
126148
Method::OnRecoverSecpPublicKey => dispatch_to(on_recover_secp_public_key, params_ptr),
127149
Method::OnSend => dispatch_to(on_send, params_ptr),
150+
Method::OnEvent => dispatch_to(on_event, params_ptr),
128151
}
129152
}
130153

@@ -231,6 +254,75 @@ fn on_send(p: OnSendParams) -> Result<()> {
231254
Ok(())
232255
}
233256

257+
fn on_event(p: OnEventParams) -> Result<()> {
258+
match p.mode {
259+
EventCalibrationMode::Shape(_) => on_event_shape(p),
260+
EventCalibrationMode::TargetSize(_) => on_event_target_size(p),
261+
}
262+
}
263+
264+
fn on_event_shape(p: OnEventParams) -> Result<()> {
265+
let EventCalibrationMode::Shape((key_size, value_size, last_value_size)) = p.mode else { panic!() };
266+
let mut value = vec![0; value_size];
267+
let mut last_value = vec![0; last_value_size];
268+
269+
for i in 0..p.iterations {
270+
random_mutations(&mut value, p.seed + i as u64, MUTATION_COUNT);
271+
let key = random_ascii_string(key_size, p.seed + p.iterations as u64 + i as u64); // non-overlapping seed
272+
let mut entries: Vec<Entry> = std::iter::repeat_with(|| Entry {
273+
flags: p.flags,
274+
key: key.clone(),
275+
value: value.clone().into(),
276+
})
277+
.take(p.entries - 1)
278+
.collect();
279+
280+
random_mutations(&mut last_value, p.seed + i as u64, MUTATION_COUNT);
281+
entries.push(Entry {
282+
flags: p.flags,
283+
key,
284+
value: last_value.clone().into(),
285+
});
286+
287+
fvm_sdk::event::emit_event(&ActorEvent::from(entries))?;
288+
}
289+
290+
Ok(())
291+
}
292+
293+
fn on_event_target_size(p: OnEventParams) -> Result<()> {
294+
let EventCalibrationMode::TargetSize(target_size) = p.mode else { panic!() };
295+
296+
// Deduct the approximate overhead of each entry (3 bytes) + flag (1 byte). This
297+
// is fuzzy because the size of the encoded CBOR depends on the length of fields, but it's good enough.
298+
let size_per_entry = ((target_size.checked_sub(p.entries * 4).unwrap_or(1)) / p.entries).max(1);
299+
let mut rand = lcg64(p.seed);
300+
for _ in 0..p.iterations {
301+
let mut entries = Vec::with_capacity(p.entries);
302+
for _ in 0..p.entries {
303+
let (r1, r2, r3) = (
304+
rand.next().unwrap(),
305+
rand.next().unwrap(),
306+
rand.next().unwrap(),
307+
);
308+
// Generate a random key of an arbitrary length that fits within the size per entry.
309+
// This will never be equal to size_per_entry, and it might be zero, which is fine
310+
// for gas calculation purposes.
311+
let key = random_ascii_string((r1 % size_per_entry as u64) as usize, r2);
312+
// Generate a value to fill up the remaining bytes.
313+
let value = random_bytes(size_per_entry - key.len(), r3);
314+
entries.push(Entry {
315+
flags: p.flags,
316+
key,
317+
value: RawBytes::from(value),
318+
})
319+
}
320+
fvm_sdk::event::emit_event(&ActorEvent::from(entries))?;
321+
}
322+
323+
Ok(())
324+
}
325+
234326
fn random_bytes(size: usize, seed: u64) -> Vec<u8> {
235327
lcg8(seed).take(size).collect()
236328
}
@@ -244,13 +336,21 @@ fn random_mutations(data: &mut Vec<u8>, seed: u64, n: usize) {
244336
}
245337
}
246338

339+
/// Generates a random string in the 0x20 - 0x7e ASCII character range
340+
/// (alphanumeric + symbols, excluding the delete symbol).
341+
fn random_ascii_string(n: usize, seed: u64) -> String {
342+
let bytes = lcg64(seed).map(|x| ((x % 95) + 32) as u8).take(n).collect();
343+
String::from_utf8(bytes).unwrap()
344+
}
345+
247346
/// Knuth's quick and dirty random number generator.
248347
/// https://en.wikipedia.org/wiki/Linear_congruential_generator
249-
fn lcg64(mut seed: u64) -> impl Iterator<Item = u64> {
250-
let a = 6364136223846793005;
251-
let c = 1442695040888963407;
348+
fn lcg64(initial_seed: u64) -> impl Iterator<Item = u64> {
349+
let a = 6364136223846793005_u64;
350+
let c = 1442695040888963407_u64;
351+
let mut seed = initial_seed;
252352
std::iter::repeat_with(move || {
253-
seed = a * seed + c;
353+
seed = a.wrapping_mul(seed).wrapping_add(c);
254354
seed
255355
})
256356
}
Loading
Loading
Loading
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{"label":"1entries","intercept":22.313744711945862,"slope":0.0003946919167965678,"r_squared":0.01663829416736118}
2+
{"label":"2entries","intercept":23.856545416096417,"slope":-0.0001249874126324024,"r_squared":0.0027511896009608794}
3+
{"label":"3entries","intercept":24.14099858063833,"slope":0.00025360607685473695,"r_squared":0.012103391403471941}
4+
{"label":"4entries","intercept":24.50200090218209,"slope":0.00026462487974013456,"r_squared":0.013041913153861495}
5+
{"label":"5entries","intercept":23.807326324824327,"slope":0.0003651319991397078,"r_squared":0.0241200673621067}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{"label":"1entries","intercept":219.3512815076574,"slope":0.025264402585468613,"r_squared":0.6969932172786232}
2+
{"label":"2entries","intercept":326.8452955079036,"slope":0.03197752325924088,"r_squared":0.6690516332666083}
3+
{"label":"3entries","intercept":442.6102960119082,"slope":0.02837213671866441,"r_squared":0.539347916250855}
4+
{"label":"4entries","intercept":548.9627365822935,"slope":0.029499917871311416,"r_squared":0.4811866768000761}
5+
{"label":"5entries","intercept":658.950925843136,"slope":0.030171458910286723,"r_squared":0.4922891005837865}

testing/calibration/src/bin/on_block.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ fn main() {
4747
if charge.name.starts_with("OnBlock") {
4848
if let Some(t) = charge.elapsed.get() {
4949
let ob = Obs {
50+
charge: charge.name.to_string(),
5051
label: "n/a".into(),
5152
elapsed_nanos: t.as_nanos(),
5253
variables: vec![*size],

0 commit comments

Comments
 (0)