From 5afe86da8890704894d2582d1c63b4ca8e4a36ab Mon Sep 17 00:00:00 2001 From: Zac Burns Date: Thu, 1 Apr 2021 09:36:11 -0500 Subject: [PATCH 01/22] WIP: gas --- Cargo.lock | 19 ++ runtime/wasm/Cargo.toml | 2 + runtime/wasm/src/gas/combinators.rs | 114 ++++++++++++ runtime/wasm/src/gas/costs.rs | 261 ++++++++++++++++++++++++++++ runtime/wasm/src/gas/mod.rs | 90 ++++++++++ runtime/wasm/src/gas/ops.rs | 54 ++++++ runtime/wasm/src/gas/saturating.rs | 37 ++++ runtime/wasm/src/gas/size_of.rs | 195 +++++++++++++++++++++ runtime/wasm/src/host_exports.rs | 57 +++++- runtime/wasm/src/lib.rs | 2 + runtime/wasm/src/mapping.rs | 12 +- 11 files changed, 841 insertions(+), 2 deletions(-) create mode 100644 runtime/wasm/src/gas/combinators.rs create mode 100644 runtime/wasm/src/gas/costs.rs create mode 100644 runtime/wasm/src/gas/mod.rs create mode 100644 runtime/wasm/src/gas/ops.rs create mode 100644 runtime/wasm/src/gas/saturating.rs create mode 100644 runtime/wasm/src/gas/size_of.rs diff --git a/Cargo.lock b/Cargo.lock index b814819155b..1551bc2182d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1716,6 +1716,8 @@ dependencies = [ "hex", "lazy_static", "never", + "parity-wasm", + "pwasm-utils", "semver 1.0.4", "strum", "strum_macros", @@ -2879,6 +2881,12 @@ dependencies = [ "syn 1.0.74", ] +[[package]] +name = "parity-wasm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d17797de36b94bc5f73edad736fd0a77ce5ab64dd622f809c1eead8c91fa6564" + [[package]] name = "parking_lot" version = "0.9.0" @@ -3337,6 +3345,17 @@ dependencies = [ "cc", ] +[[package]] +name = "pwasm-utils" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51992bc74c0f34f759ff97fb303602e60343afc83693769c91aa17724442809e" +dependencies = [ + "byteorder", + "log", + "parity-wasm", +] + [[package]] name = "quick-error" version = "1.2.3" diff --git a/runtime/wasm/Cargo.toml b/runtime/wasm/Cargo.toml index c39106d50e7..349c525ba94 100644 --- a/runtime/wasm/Cargo.toml +++ b/runtime/wasm/Cargo.toml @@ -23,3 +23,5 @@ anyhow = "1.0" wasmtime = "0.27.0" defer = "0.1" never = "0.1" +pwasm-utils = "0.17" +parity-wasm = "0.42" diff --git a/runtime/wasm/src/gas/combinators.rs b/runtime/wasm/src/gas/combinators.rs new file mode 100644 index 00000000000..800f1c388ad --- /dev/null +++ b/runtime/wasm/src/gas/combinators.rs @@ -0,0 +1,114 @@ +use super::{Gas, GasSizeOf}; +use std::cmp::{max, min}; + +pub mod complexity { + use super::*; + + // Args have additive linear complexity + // Eg: O(N₁+N₂) + pub struct Linear; + // Args have additive exponential complexity + // Eg: O(N₁*N₂) + pub struct Exponential; + // There is only one arg and it scales linearly with it's size + pub struct Size; + // Complexity is captured by the lesser complexity of the two args + // Eg: O(min(N₁, N₂)) + pub struct Min; + // Complexity is captured by the greater complexity of the two args + // Eg: O(max(N₁, N₂)) + pub struct Max; + + impl GasCombinator for Linear { + #[inline(always)] + fn combine(lhs: Gas, rhs: Gas) -> Gas { + lhs + rhs + } + } + + impl GasCombinator for Exponential { + #[inline(always)] + fn combine(lhs: Gas, rhs: Gas) -> Gas { + Gas(lhs.0.saturating_mul(rhs.0)) + } + } + + impl GasCombinator for Min { + #[inline(always)] + fn combine(lhs: Gas, rhs: Gas) -> Gas { + min(lhs, rhs) + } + } + + impl GasCombinator for Max { + #[inline(always)] + fn combine(lhs: Gas, rhs: Gas) -> Gas { + max(lhs, rhs) + } + } + + impl GasSizeOf for Combine + where + T: GasSizeOf, + { + fn gas_size_of(&self) -> Gas { + self.0.gas_size_of() + } + } +} + +pub struct Combine(pub Tuple, pub Combinator); + +pub trait GasCombinator { + fn combine(lhs: Gas, rhs: Gas) -> Gas; +} + +impl GasSizeOf for Combine<(T0, T1), C> +where + T0: GasSizeOf, + T1: GasSizeOf, + C: GasCombinator, +{ + fn gas_size_of(&self) -> Gas { + let (a, b) = &self.0; + C::combine(a.gas_size_of(), b.gas_size_of()) + } + + #[inline] + fn const_gas_size_of() -> Option { + if let Some(t0) = T0::const_gas_size_of() { + if let Some(t1) = T1::const_gas_size_of() { + return Some(C::combine(t0, t1)); + } + } + None + } +} + +impl GasSizeOf for Combine<(T0, T1, T2), C> +where + T0: GasSizeOf, + T1: GasSizeOf, + T2: GasSizeOf, + C: GasCombinator, +{ + fn gas_size_of(&self) -> Gas { + let (a, b, c) = &self.0; + C::combine( + C::combine(a.gas_size_of(), b.gas_size_of()), + c.gas_size_of(), + ) + } + + #[inline] // Const propagation to the rescue. I hope. + fn const_gas_size_of() -> Option { + if let Some(t0) = T0::const_gas_size_of() { + if let Some(t1) = T1::const_gas_size_of() { + if let Some(t2) = T2::const_gas_size_of() { + return Some(C::combine(C::combine(t0, t1), t2)); + } + } + } + None + } +} diff --git a/runtime/wasm/src/gas/costs.rs b/runtime/wasm/src/gas/costs.rs new file mode 100644 index 00000000000..02cbb73dd13 --- /dev/null +++ b/runtime/wasm/src/gas/costs.rs @@ -0,0 +1,261 @@ +//! Stores all the gas costs is one place so they can be compared easily. +//! Determinism: Once deployed, none of these values can be changed without a version upgrade. + +use super::*; + +/// Using 10 gas = ~1ns for WASM instructions +/// So the maximum time spent doing pure math +/// would be in the 1 hour range. The intent here +/// is to have the determinism cutoff be very high, +/// while still allowing more reasonable timer based cutoffs. +/// Having a unit like 10 gas for ~1ns allows us to be granular +/// in instructions which are aggregated into metered blocks +/// via https://docs.rs/pwasm-utils/0.16.0/pwasm_utils/fn.inject_gas_counter.html +/// But we can still charge very high numbers for other things. +pub const MAX_GAS: u64 = 36_000_000_000_000; + +/// Base gas cost for calling any host export +// Security: This must be non-zero. +/// Gas for instructions are aggregated into blocks, so hopefully gas calls each have relatively large gas. +/// But in the case they don't, we don't want the overhead of calling out into a host export to be +/// the dominant cost that causes unexpectedly high execution times. +pub const HOST_EXPORT: Gas = Gas(100_000); + +// Allow up to 25,000 ethereum calls +pub const ETHEREUM_CALL: Gas = Gas(MAX_GAS / 25000); + +// The cost of allocating an AscHeap object +pub const ALLOC_NEW: GasOp = GasOp { + base_cost: 200_000, + size_mult: 400, +}; + +// Cost of reading from the AscHeap +pub const HEAP_READ: GasOp = GasOp { + base_cost: 10_000, + size_mult: 100, +}; + +pub const BIG_INT_TO_HEX: GasOp = GasOp { + base_cost: 150_000, + size_mult: 500, +}; + +// TODO: Heap write? + +// Saving to the store is one of the most expensive operations. +pub const STORE_SET: GasOp = GasOp { + // Allow up to 250k entities saved. + base_cost: MAX_GAS / 250_000, + // If the size roughly corresponds to bytes, allow 1GB to be saved. + size_mult: MAX_GAS / 1_000_000_000, +}; + +// Reading from the store is much cheaper than writing. +pub const STORE_GET: GasOp = GasOp { + base_cost: MAX_GAS / 10_000_000, + size_mult: MAX_GAS / 10_000_000_000, +}; + +pub const STORE_REMOVE: GasOp = STORE_SET; + +pub const JSON_TO_I64: GasOp = GasOp { + base_cost: 150_000, + size_mult: 1_400, +}; +pub const JSON_TO_U64: GasOp = JSON_TO_I64; + +pub const JSON_TO_F64: GasOp = GasOp { + base_cost: 150_000, + size_mult: 1_800, +}; + +pub const JSON_TO_BIGINT: GasOp = GasOp { + base_cost: 250_000, + size_mult: 10_000, +}; + +pub const KECCAK256: GasOp = GasOp { + base_cost: 250_000, + size_mult: 4_000, +}; + +pub const BIG_INT_PLUS: GasOp = GasOp { + base_cost: 100_000, + size_mult: 800, +}; + +pub const BIG_INT_MINUS: GasOp = GasOp { + base_cost: 100_000, + size_mult: 850, +}; + +pub const BIG_INT_MUL: GasOp = GasOp { + base_cost: 100_000, + size_mult: 3_200, +}; + +pub const BIG_INT_DIV: GasOp = GasOp { + base_cost: 100_000, + size_mult: 6_400, +}; + +pub const BIG_INT_MOD: GasOp = BIG_INT_DIV; + +pub struct GasRules; + +impl Rules for GasRules { + fn instruction_cost(&self, instruction: &Instruction) -> Option { + use Instruction::*; + let weight = match instruction { + // These are taken from this post: https://github.com/paritytech/substrate/pull/7361#issue-506217103 + // from the table under the "Schedule" dropdown. Each decimal is multiplied by 10. + I64Const(_) => 16, + I64Load(_, _) => 1573, + I64Store(_, _) => 2263, + Select => 61, + Instruction::If(_) => 79, + Br(_) => 30, + BrIf(_) => 63, + BrTable(data) => 146 + (1030000 * (1 + data.table.len() as u32)), + Call(_) => 951, + // TODO: To figure out the param cost we need to look up the function + CallIndirect(_, _) => 1995, + GetLocal(_) => 18, + SetLocal(_) => 21, + TeeLocal(_) => 21, + GetGlobal(_) => 66, + SetGlobal(_) => 107, + CurrentMemory(_) => 23, + GrowMemory(_) => 435000, + I64Clz => 23, + I64Ctz => 23, + I64Popcnt => 29, + I64Eqz => 24, + I64ExtendSI32 => 22, + I64ExtendUI32 => 22, + I32WrapI64 => 23, + I64Eq => 26, + I64Ne => 25, + I64LtS => 25, + I64LtU => 26, + I64GtS => 25, + I64GtU => 25, + I64LeS => 25, + I64LeU => 26, + I64GeS => 26, + I64GeU => 25, + I64Add => 25, + I64Sub => 26, + I64Mul => 25, + I64DivS => 82, + I64DivU => 72, + I64RemS => 81, + I64RemU => 73, + I64And => 25, + I64Or => 25, + I64Xor => 26, + I64Shl => 25, + I64ShrS => 26, + I64ShrU => 26, + I64Rotl => 25, + I64Rotr => 26, + + // These are similar enough to something above so just referencing a similar + // instruction + I32Load(_, _) + | F32Load(_, _) + | F64Load(_, _) + | I32Load8S(_, _) + | I32Load8U(_, _) + | I32Load16S(_, _) + | I32Load16U(_, _) + | I64Load8S(_, _) + | I64Load8U(_, _) + | I64Load16S(_, _) + | I64Load16U(_, _) + | I64Load32S(_, _) + | I64Load32U(_, _) => 1573, + + I32Store(_, _) + | F32Store(_, _) + | F64Store(_, _) + | I32Store8(_, _) + | I32Store16(_, _) + | I64Store8(_, _) + | I64Store16(_, _) + | I64Store32(_, _) => 2263, + + I32Const(_) | F32Const(_) | F64Const(_) => 16, + I32Eqz => 26, + I32Eq => 26, + I32Ne => 25, + I32LtS => 25, + I32LtU => 26, + I32GtS => 25, + I32GtU => 25, + I32LeS => 25, + I32LeU => 26, + I32GeS => 26, + I32GeU => 25, + I32Add => 25, + I32Sub => 26, + I32Mul => 25, + I32DivS => 82, + I32DivU => 72, + I32RemS => 81, + I32RemU => 73, + I32And => 25, + I32Or => 25, + I32Xor => 26, + I32Shl => 25, + I32ShrS => 26, + I32ShrU => 26, + I32Rotl => 25, + I32Rotr => 26, + I32Clz => 23, + I32Popcnt => 29, + I32Ctz => 23, + + // Float weights not calculated by reference source material. Making up + // some conservative values. The point here is not to be perfect but just + // to have some reasonable upper bound. + F64ReinterpretI64 | F32ReinterpretI32 | F64PromoteF32 | F64ConvertUI64 + | F64ConvertSI64 | F64ConvertUI32 | F64ConvertSI32 | F32DemoteF64 | F32ConvertUI64 + | F32ConvertSI64 | F32ConvertUI32 | F32ConvertSI32 | I64TruncUF64 | I64TruncSF64 + | I64TruncUF32 | I64TruncSF32 | I32TruncUF64 | I32TruncSF64 | I32TruncUF32 + | I32TruncSF32 | F64Copysign | F64Max | F64Min | F64Mul | F64Sub | F64Add + | F64Trunc | F64Floor | F64Ceil | F64Neg | F64Abs | F64Nearest | F32Copysign + | F32Max | F32Min | F32Mul | F32Sub | F32Add | F32Nearest | F32Trunc | F32Floor + | F32Ceil | F32Neg | F32Abs | F32Eq | F32Ne | F32Lt | F32Gt | F32Le | F32Ge | F64Eq + | F64Ne | F64Lt | F64Gt | F64Le | F64Ge | I32ReinterpretF32 | I64ReinterpretF64 => 100, + F64Div | F64Sqrt | F32Div | F32Sqrt => 1000, + + // More invented weights + Block(_) => 1000, + Loop(_) => 1000, + Else => 1000, + End => 1000, + Return => 1000, + Drop => 1000, + Nop => 1, + Unreachable => 1, + }; + Some(weight) + } + fn memory_grow_cost(&self) -> Option { + // Each page is 64KiB which is 65536 bytes. + const PAGE: u64 = 64 * 1024; + // 1 GB + const GIB: u64 = 1073741824; + // 12GiB to pages for the max memory allocation + // In practice this will never be hit unless we also + // free pages because this is 32bit WASM. + const MAX_PAGES: u64 = 12 * GIB / PAGE; + // This ends up at 439,453,125 per page. + const GAS_PER_PAGE: u64 = MAX_GAS / MAX_PAGES; + let gas_per_page = NonZeroU32::new(GAS_PER_PAGE.try_into().unwrap()).unwrap(); + + Some(MemoryGrowCost::Linear(gas_per_page)) + } +} diff --git a/runtime/wasm/src/gas/mod.rs b/runtime/wasm/src/gas/mod.rs new file mode 100644 index 00000000000..c1a66df6028 --- /dev/null +++ b/runtime/wasm/src/gas/mod.rs @@ -0,0 +1,90 @@ +mod combinators; +mod costs; +mod ops; +mod saturating; +mod size_of; +pub use combinators::*; +pub use costs::*; +pub use saturating::*; + +use parity_wasm::elements::Instruction; +use pwasm_utils::rules::{MemoryGrowCost, Rules}; +use std::convert::TryInto; +use std::num::NonZeroU32; +use std::sync::atomic::{AtomicU64, Ordering::SeqCst}; + +use crate::error::DeterministicHostError; + +pub struct GasOp { + base_cost: u64, + size_mult: u64, +} + +impl GasOp { + pub fn with_args(&self, c: C, args: T) -> Gas + where + Combine: GasSizeOf, + { + Gas(self.base_cost) + Combine(args, c).gas_size_of() * self.size_mult + } +} + +/// Sort of a base unit for gas operations. For example, if one is operating +/// on a BigDecimal one might like to know how large that BigDecimal is compared +/// to other BigDecimals so that one could to (MultCost * gas_size_of(big_decimal)) +/// and re-use that logic for (WriteToDBCost or ReadFromDBCost) rather than having +/// one-offs for each use-case. +/// This is conceptually much like CacheWeight, but has some key differences. +/// First, this needs to be stable - like StableHash (same independent of +/// platform/compiler/run). Also this can be somewhat context dependent. An example +/// of context dependent costs might be if a value is being hex encoded or binary encoded +/// when serializing. +/// +/// Either implement gas_size_of or const_gas_size_of but never none or both. +pub trait GasSizeOf { + #[inline(always)] + fn gas_size_of(&self) -> Gas { + Self::const_gas_size_of().expect("GasSizeOf unimplemented") + } + /// Some when every member of the type has the same gas size. + #[inline(always)] + fn const_gas_size_of() -> Option { + None + } +} + +pub trait ConstGasSizeOf { + fn gas_size_of() -> Gas; +} + +/// This struct mostly exists to avoid typing out saturating_mul +#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)] +pub struct Gas(u64); + +impl Gas { + pub const ZERO: Gas = Gas(0); +} + +pub struct GasCounter(AtomicU64); + +impl GasCounter { + pub fn new() -> Self { + Self(AtomicU64::new(0)) + } + + pub fn consume(&self, amount: Gas) -> Result<(), DeterministicHostError> { + let new = self + .0 + .fetch_update(SeqCst, SeqCst, |v| Some(v.saturating_add(amount.0))) + .unwrap(); + + if new >= MAX_GAS { + Err(DeterministicHostError(anyhow::anyhow!( + "Gas limit exceeded. Used: {}", + new + ))) + } else { + Ok(()) + } + } +} diff --git a/runtime/wasm/src/gas/ops.rs b/runtime/wasm/src/gas/ops.rs new file mode 100644 index 00000000000..25eb6aa7783 --- /dev/null +++ b/runtime/wasm/src/gas/ops.rs @@ -0,0 +1,54 @@ +//! All the operators go here +//! Gas operations are all saturating and additive (never trending toward zero) + +use super::{Gas, SaturatingInto as _}; +use std::iter::Sum; +use std::ops::{Add, AddAssign, Mul, MulAssign}; + +impl Add for Gas { + type Output = Gas; + #[inline] + fn add(self, rhs: Gas) -> Self::Output { + Gas(self.0.saturating_add(rhs.0)) + } +} + +impl Mul for Gas { + type Output = Gas; + #[inline] + fn mul(self, rhs: u64) -> Self::Output { + Gas(self.0.saturating_mul(rhs)) + } +} + +impl Mul for Gas { + type Output = Gas; + #[inline] + fn mul(self, rhs: usize) -> Self::Output { + Gas(self.0.saturating_mul(rhs.saturating_into())) + } +} + +impl MulAssign for Gas { + #[inline] + fn mul_assign(&mut self, rhs: u64) { + self.0 = self.0.saturating_add(rhs); + } +} + +impl AddAssign for Gas { + #[inline] + fn add_assign(&mut self, rhs: Gas) { + self.0 = self.0.saturating_mul(rhs.0); + } +} + +impl Sum for Gas { + fn sum>(iter: I) -> Self { + let mut sum = Gas::ZERO; + for elem in iter { + sum += elem; + } + sum + } +} diff --git a/runtime/wasm/src/gas/saturating.rs b/runtime/wasm/src/gas/saturating.rs new file mode 100644 index 00000000000..52c39911fa8 --- /dev/null +++ b/runtime/wasm/src/gas/saturating.rs @@ -0,0 +1,37 @@ +use super::Gas; +use std::convert::TryInto as _; + +pub trait SaturatingFrom { + fn saturating_from(value: T) -> Self; +} + +// It would be good to put this trait into a new or existing crate. +// Tried conv but the owner seems to be away +// https://github.com/DanielKeep/rust-conv/issues/15 +pub trait SaturatingInto { + fn saturating_into(self) -> T; +} + +impl SaturatingInto for I +where + F: SaturatingFrom, +{ + #[inline(always)] + fn saturating_into(self) -> F { + F::saturating_from(self) + } +} + +impl SaturatingFrom for Gas { + #[inline] + fn saturating_from(value: usize) -> Gas { + Gas(value.try_into().unwrap_or(u64::MAX)) + } +} + +impl SaturatingFrom for u64 { + #[inline] + fn saturating_from(value: usize) -> Self { + value.try_into().unwrap_or(u64::MAX) + } +} diff --git a/runtime/wasm/src/gas/size_of.rs b/runtime/wasm/src/gas/size_of.rs new file mode 100644 index 00000000000..b2554c3986c --- /dev/null +++ b/runtime/wasm/src/gas/size_of.rs @@ -0,0 +1,195 @@ +//! Various implementations of GasSizeOf; + +use graph::{ + components::store::EntityType, + data::store::{scalar::Bytes, Value}, + prelude::{BigDecimal, BigInt, Entity, EntityKey}, +}; +use std::ops::Deref as _; + +use super::{Gas, GasSizeOf, SaturatingInto as _}; + +impl GasSizeOf for Value { + fn gas_size_of(&self) -> Gas { + let inner = match self { + Value::BigDecimal(big_decimal) => big_decimal.gas_size_of(), + Value::String(string) => string.gas_size_of(), + Value::Null => Gas(1), + Value::List(list) => list.gas_size_of(), + Value::Int(int) => int.gas_size_of(), + Value::Bytes(bytes) => bytes.gas_size_of(), + Value::Bool(bool) => bool.gas_size_of(), + Value::BigInt(big_int) => big_int.gas_size_of(), + }; + Gas(4) + inner + } +} + +impl GasSizeOf for Bytes { + fn gas_size_of(&self) -> Gas { + (&self[..]).gas_size_of() + } +} + +impl GasSizeOf for bool { + #[inline(always)] + fn const_gas_size_of() -> Option { + Some(Gas(1)) + } +} + +impl GasSizeOf for Option +where + T: GasSizeOf, +{ + fn gas_size_of(&self) -> Gas { + if let Some(v) = self { + Gas(1) + v.gas_size_of() + } else { + Gas(1) + } + } +} + +impl GasSizeOf for BigInt { + fn gas_size_of(&self) -> Gas { + // This does not do % 8 to count bytes but that's ok since this type is inherantly expensive. + let gas: Gas = self.bits().saturating_into(); + gas + Gas(100) + } +} + +impl GasSizeOf for Entity { + fn gas_size_of(&self) -> Gas { + self.deref().gas_size_of() + } +} + +impl GasSizeOf for BigDecimal { + fn gas_size_of(&self) -> Gas { + // This can be overly pessimistic + let (int, _) = self.as_bigint_and_exponent(); + // This does not do % 8 to count bytes but that's ok since this type is inherantly expensive. + let gas: Gas = int.bits().saturating_into(); + gas + Gas(1000) + } +} + +impl GasSizeOf for str { + fn gas_size_of(&self) -> Gas { + self.len().saturating_into() + } +} + +impl GasSizeOf for String { + fn gas_size_of(&self) -> Gas { + self.as_str().gas_size_of() + } +} + +impl GasSizeOf for std::collections::HashMap +where + K: GasSizeOf, + V: GasSizeOf, +{ + fn gas_size_of(&self) -> Gas { + let members = match (K::const_gas_size_of(), V::const_gas_size_of()) { + (Some(k_gas), None) => { + self.values().map(|v| v.gas_size_of()).sum::() + k_gas * self.len() + } + (None, Some(v_gas)) => { + self.keys().map(|k| k.gas_size_of()).sum::() + v_gas * self.len() + } + (Some(k_gas), Some(v_gas)) => (k_gas + v_gas) * self.len(), + (None, None) => self.iter().map(|e| e.gas_size_of()).sum(), + }; + members + Gas(32) + (Gas(8) * self.len()) + } +} + +impl GasSizeOf for &[T] +where + T: GasSizeOf, +{ + fn gas_size_of(&self) -> Gas { + if let Some(gas) = T::const_gas_size_of() { + gas * self.len() + } else { + self.iter().map(|e| e.gas_size_of()).sum() + } + } +} + +impl GasSizeOf for Vec +where + T: GasSizeOf, +{ + fn gas_size_of(&self) -> Gas { + let members = (&self[..]).gas_size_of(); + // Overhead for Vec so that Vec> is more expensive than Vec + members + Gas(16) + self.len().saturating_into() + } +} + +impl GasSizeOf for &T +where + T: GasSizeOf, +{ + #[inline(always)] + fn gas_size_of(&self) -> Gas { + ::gas_size_of(*self) + } + + #[inline(always)] + fn const_gas_size_of() -> Option { + T::const_gas_size_of() + } +} + +macro_rules! int_gas { + ($($name: ident),*) => { + $( + impl GasSizeOf for $name { + #[inline(always)] + fn const_gas_size_of() -> Option { + Some(std::mem::size_of::<$name>().saturating_into()) + } + } + )* + } +} + +int_gas!(u8, i8, u16, i16, u32, i32, u64, i64, u128, i128); + +impl GasSizeOf for usize { + fn const_gas_size_of() -> Option { + // Must be the same regardless of platform. + u64::const_gas_size_of() + } +} + +impl GasSizeOf for EntityKey { + fn gas_size_of(&self) -> Gas { + self.subgraph_id.gas_size_of() + + self.entity_type.gas_size_of() + + self.entity_id.gas_size_of() + } +} + +impl GasSizeOf for EntityType {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn const_tuples_propagate() { + assert_eq!(Some(Gas(16)), <(u64, u64)>::const_gas_size_of()); + assert_eq!(None, <(&str, u64)>::const_gas_size_of()); + } + + #[test] + fn dyn_tuples_propagate() { + assert_eq!(Gas(99), ("abc", 0u64).gas_size_of()) + } +} diff --git a/runtime/wasm/src/host_exports.rs b/runtime/wasm/src/host_exports.rs index 50610400ea0..011e82421bb 100644 --- a/runtime/wasm/src/host_exports.rs +++ b/runtime/wasm/src/host_exports.rs @@ -68,6 +68,7 @@ pub struct HostExports { templates: Arc>, pub(crate) link_resolver: Arc, store: Arc, + gas_used: GasCounter, } impl HostExports { @@ -90,9 +91,17 @@ impl HostExports { templates, link_resolver, store, + gas_used: GasCounter::new(), } } + // TODO: What's the scope of this when is it set to 0? Ideally per block? + /// This should be called once per host export + #[inline] + pub(crate) fn consume_gas(&self, amount: Gas) -> Result<(), DeterministicHostError> { + self.gas_used.consume(amount + gas::HOST_EXPORT) + } + pub(crate) fn abort( &self, message: Option, @@ -167,6 +176,9 @@ impl HostExports { entity_type: EntityType::new(entity_type), entity_id, }; + + self.consume_gas(gas::STORE_SET.with_args(complexity::Linear, (&key, &data)))?; + let entity = Entity::from(data); let schema = self.store.input_schema(&self.subgraph_id)?; let is_valid = validate_entity(&schema.document, &key, &entity).is_ok(); @@ -209,6 +221,9 @@ impl HostExports { entity_type: EntityType::new(entity_type), entity_id, }; + + self.consume_gas(gas::STORE_REMOVE.with_args(complexity::Size, &key))?; + state.entity_cache.remove(key); Ok(()) @@ -226,6 +241,9 @@ impl HostExports { entity_id: entity_id.clone(), }; + let result = state.entity_cache.get(&store_key)?; + self.consume_gas(gas::STORE_GET.with_args(complexity::Linear, (&store_key, &result)))?; + Ok(state.entity_cache.get(&store_key)?) } @@ -235,6 +253,8 @@ impl HostExports { /// /// https://godoc.org/github.com/ethereum/go-ethereum/common/hexutil#hdr-Encoding_Rules pub(crate) fn big_int_to_hex(&self, n: BigInt) -> Result { + self.consume_gas(gas::BIG_INT_TO_HEX.with_args(Size, n))?; + if n == 0.into() { return Ok("0x0".to_string()); } @@ -247,6 +267,9 @@ impl HostExports { } pub(crate) fn ipfs_cat(&self, logger: &Logger, link: String) -> Result, anyhow::Error> { + // Does not consume gas because this is not a part of the deterministic feature set. + // Ideally this would first consume gas for fetching the file stats, and then again + // for the bytes of the file. block_on03(self.link_resolver.cat(logger, &Link { link })) } @@ -265,6 +288,10 @@ impl HostExports { user_data: store::Value, flags: Vec, ) -> Result>, anyhow::Error> { + // Does not consume gas because this is not a part of deterministic APIs. + // Ideally we would consume gas the same as ipfs_cat and then share + // gas across the spawned modules for callbacks. + const JSON_FLAG: &str = "json"; ensure!( flags.contains(&JSON_FLAG.to_string()), @@ -318,6 +345,7 @@ impl HostExports { /// Expects a decimal string. pub(crate) fn json_to_i64(&self, json: String) -> Result { + self.consume_gas(gas::JSON_TO_I64.with_args(complexity::Size, &json))?; i64::from_str(&json) .with_context(|| format!("JSON `{}` cannot be parsed as i64", json)) .map_err(DeterministicHostError) @@ -325,6 +353,8 @@ impl HostExports { /// Expects a decimal string. pub(crate) fn json_to_u64(&self, json: String) -> Result { + self.consume_gas(gas::JSON_TO_U64.with_args(complexity::Size, &json))?; + u64::from_str(&json) .with_context(|| format!("JSON `{}` cannot be parsed as u64", json)) .map_err(DeterministicHostError) @@ -332,6 +362,8 @@ impl HostExports { /// Expects a decimal string. pub(crate) fn json_to_f64(&self, json: String) -> Result { + self.consume_gas(gas::JSON_TO_F64.with_args(complexity::Size, &json))?; + f64::from_str(&json) .with_context(|| format!("JSON `{}` cannot be parsed as f64", json)) .map_err(DeterministicHostError) @@ -339,6 +371,8 @@ impl HostExports { /// Expects a decimal string. pub(crate) fn json_to_big_int(&self, json: String) -> Result, DeterministicHostError> { + self.consume_gas(gas::JSON_TO_BIGINT.with_args(complexity::Size, &json))?; + let big_int = BigInt::from_str(&json) .with_context(|| format!("JSON `{}` is not a decimal string", json)) .map_err(DeterministicHostError)?; @@ -349,7 +383,9 @@ impl HostExports { &self, input: Vec, ) -> Result<[u8; 32], DeterministicHostError> { - Ok(tiny_keccak::keccak256(&input)) + let data = &input[..]; + self.consume_gas(gas::KECCAK256.with_args(complexity::Size, data))?; + Ok(tiny_keccak::keccak256(data)) } pub(crate) fn big_int_plus( @@ -357,6 +393,7 @@ impl HostExports { x: BigInt, y: BigInt, ) -> Result { + self.consume_gas(gas::BIG_INT_PLUS.with_args(complexity::Max, (&x, &y)))?; Ok(x + y) } @@ -365,6 +402,7 @@ impl HostExports { x: BigInt, y: BigInt, ) -> Result { + self.consume_gas(gas::BIG_INT_MINUS.with_args(complexity::Max, (&x, &y)))?; Ok(x - y) } @@ -373,6 +411,7 @@ impl HostExports { x: BigInt, y: BigInt, ) -> Result { + self.consume_gas(gas::BIG_INT_MUL.with_args(complexity::Exponential, (&x, &y)))?; Ok(x * y) } @@ -381,6 +420,7 @@ impl HostExports { x: BigInt, y: BigInt, ) -> Result { + self.consume_gas(gas::BIG_INT_DIV.with_args(complexity::Exponential, (&x, &y)))?; if y == 0.into() { return Err(DeterministicHostError(anyhow!( "attempted to divide BigInt `{}` by zero", @@ -395,6 +435,7 @@ impl HostExports { x: BigInt, y: BigInt, ) -> Result { + self.consume_gas(gas::BIG_INT_MOD.with_args(complexity::Exponential, (&x, &y)))?; if y == 0.into() { return Err(DeterministicHostError(anyhow!( "attempted to calculate the remainder of `{}` with a divisor of zero", @@ -410,10 +451,12 @@ impl HostExports { x: BigInt, exponent: u8, ) -> Result { + self.consume_gas(gas::BIG_INT_POW.with_args(complexity::TODO, (&x, &y)))?; Ok(x.pow(exponent)) } pub(crate) fn big_int_from_string(&self, s: String) -> Result { + self.consume_gas(gas::BIG_INT_FROM_STRING.with_args(complexity::Size, &s))?; BigInt::from_str(&s) .with_context(|| format!("string is not a BigInt: `{}`", s)) .map_err(DeterministicHostError) @@ -424,6 +467,7 @@ impl HostExports { x: BigInt, y: BigInt, ) -> Result { + self.consume_gas(gas::BIG_INT_BIT_OR.with_args(complexity::MAX, (&x, &y)))?; Ok(x | y) } @@ -432,6 +476,7 @@ impl HostExports { x: BigInt, y: BigInt, ) -> Result { + self.consume_gas(gas::BIG_INT_BIT_AND.with_args(complexity::MIN, (&x, &y)))?; Ok(x & y) } @@ -440,6 +485,7 @@ impl HostExports { x: BigInt, bits: u8, ) -> Result { + self.consume_gas(gas::BIG_INT_SHL.with_args(complexity::TODO, (&x, &y)))?; Ok(x << bits) } @@ -448,11 +494,13 @@ impl HostExports { x: BigInt, bits: u8, ) -> Result { + self.consume_gas(gas::BIG_INT_SHR.with_args(complexity::TODO, (&x, &y)))?; Ok(x >> bits) } /// Useful for IPFS hashes stored as bytes pub(crate) fn bytes_to_base58(&self, bytes: Vec) -> Result { + self.consume_gas(gas::BYTES_TO_BASE58.with_args(complexity::Size, &bytes))?; Ok(::bs58::encode(&bytes).into_string()) } @@ -461,6 +509,7 @@ impl HostExports { x: BigDecimal, y: BigDecimal, ) -> Result { + self.consume_gas(gas::BIG_DECIMAL_PLUS.with_args(complexity::Linear, (&x, &y)))?; Ok(x + y) } @@ -469,6 +518,7 @@ impl HostExports { x: BigDecimal, y: BigDecimal, ) -> Result { + self.consume_gas(gas::BIG_DECIMAL_MINUS.with_args(complexity::Linear, (&x, &y)))?; Ok(x - y) } @@ -477,6 +527,7 @@ impl HostExports { x: BigDecimal, y: BigDecimal, ) -> Result { + self.consume_gas(gas::BIG_DECIMAL_MUL.with_args(complexity::Exponential, (&x, &y)))?; Ok(x * y) } @@ -486,6 +537,7 @@ impl HostExports { x: BigDecimal, y: BigDecimal, ) -> Result { + self.consume_gas(gas::BIG_DECIMAL_DIV.with_args(complexity::Exponential, (&x, &y)))?; if y == 0.into() { return Err(DeterministicHostError(anyhow!( "attempted to divide BigDecimal `{}` by zero", @@ -500,6 +552,7 @@ impl HostExports { x: BigDecimal, y: BigDecimal, ) -> Result { + self.consume_gas(gas::BIG_DECIMAL_EQ.with_args(complexity::MIN, (&x, &y)))?; Ok(x == y) } @@ -507,6 +560,7 @@ impl HostExports { &self, x: BigDecimal, ) -> Result { + self.consume_gas(gas::BIG_DECIMAL_TO_STR.with_args(complexity::Linear, (&x, &y)))?; Ok(x.to_string()) } @@ -514,6 +568,7 @@ impl HostExports { &self, s: String, ) -> Result { + self.consume_gas(gas::BIG_DECIMAL_PARSE.with_args(complexity::Linear, (&x, &y)))?; BigDecimal::from_str(&s) .with_context(|| format!("string is not a BigDecimal: '{}'", s)) .map_err(DeterministicHostError) diff --git a/runtime/wasm/src/lib.rs b/runtime/wasm/src/lib.rs index 7c3febe1697..2a339eb82eb 100644 --- a/runtime/wasm/src/lib.rs +++ b/runtime/wasm/src/lib.rs @@ -16,6 +16,8 @@ pub mod host_exports; pub mod error; +mod gas; + pub use host::RuntimeHostBuilder; pub use host_exports::HostExports; pub use mapping::{MappingContext, ValidModule}; diff --git a/runtime/wasm/src/mapping.rs b/runtime/wasm/src/mapping.rs index 7df8f9f7f8f..2f16a4d15ef 100644 --- a/runtime/wasm/src/mapping.rs +++ b/runtime/wasm/src/mapping.rs @@ -1,3 +1,4 @@ +use crate::gas::GasRules; use crate::module::{ExperimentalFeatures, WasmInstance}; use futures::sync::mpsc; use futures03::channel::oneshot::Sender; @@ -141,6 +142,15 @@ pub struct ValidModule { impl ValidModule { /// Pre-process and validate the module. pub fn new(raw_module: &[u8]) -> Result { + // Add the gas calls here. + // Module name "gas" must match. See also e3f03e62-40e4-4f8c-b4a1-d0375cca0b76 + // We do this by round-tripping the module through parity - injecting gas then + // serializing again. + let parity_module = parity_wasm::elements::Module::from_bytes(raw_module)?; + let parity_module = pwasm_utils::inject_gas_counter(parity_module, &GasRules, "gas") + .map_err(|_| anyhow!("Failed to inject gas counter"))?; + let raw_module = parity_module.to_bytes()?; + // We currently use Cranelift as a compilation engine. Cranelift is an optimizing compiler, // but that should not cause determinism issues since it adheres to the Wasm spec. Still we // turn off optional optimizations to be conservative. @@ -152,7 +162,7 @@ impl ValidModule { config.max_wasm_stack(*MAX_STACK_SIZE).unwrap(); // Safe because this only panics if size passed is 0. let engine = &wasmtime::Engine::new(&config)?; - let module = wasmtime::Module::from_binary(&engine, raw_module)?; + let module = wasmtime::Module::from_binary(&engine, &raw_module)?; let mut import_name_to_modules: BTreeMap> = BTreeMap::new(); From 22a3f82ff46907132e3523b3b5ed1f0a53f6b103 Mon Sep 17 00:00:00 2001 From: Leonardo Yvens Date: Fri, 23 Apr 2021 18:17:50 -0300 Subject: [PATCH 02/22] runtime: Add gas metering --- core/src/subgraph/instance_manager.rs | 2 +- graph/src/data/store/scalar.rs | 2 +- runtime/wasm/src/gas/combinators.rs | 24 +- runtime/wasm/src/gas/costs.rs | 116 ++++------ runtime/wasm/src/gas/mod.rs | 25 ++- runtime/wasm/src/gas/saturating.rs | 14 ++ runtime/wasm/src/gas/size_of.rs | 32 +-- runtime/wasm/src/host_exports.rs | 254 ++++++++++++++------- runtime/wasm/src/mapping.rs | 7 +- runtime/wasm/src/module/mod.rs | 311 +++++++++++++++++++------- 10 files changed, 515 insertions(+), 272 deletions(-) diff --git a/core/src/subgraph/instance_manager.rs b/core/src/subgraph/instance_manager.rs index 277f306ff0c..1430b8738ce 100644 --- a/core/src/subgraph/instance_manager.rs +++ b/core/src/subgraph/instance_manager.rs @@ -869,7 +869,7 @@ async fn process_block, C: Blockchain>( ) .await { - // Triggers processed with no errors or with only determinstic errors. + // Triggers processed with no errors or with only deterministic errors. Ok(block_state) => block_state, // Some form of unknown or non-deterministic error ocurred. diff --git a/graph/src/data/store/scalar.rs b/graph/src/data/store/scalar.rs index d45814c0d54..998a5b14257 100644 --- a/graph/src/data/store/scalar.rs +++ b/graph/src/data/store/scalar.rs @@ -60,7 +60,7 @@ impl BigDecimal { self.0.as_bigint_and_exponent() } - pub(crate) fn digits(&self) -> u64 { + pub fn digits(&self) -> u64 { self.0.digits() } diff --git a/runtime/wasm/src/gas/combinators.rs b/runtime/wasm/src/gas/combinators.rs index 800f1c388ad..814efa25417 100644 --- a/runtime/wasm/src/gas/combinators.rs +++ b/runtime/wasm/src/gas/combinators.rs @@ -7,9 +7,14 @@ pub mod complexity { // Args have additive linear complexity // Eg: O(N₁+N₂) pub struct Linear; - // Args have additive exponential complexity + // Args have multiplicative complexity // Eg: O(N₁*N₂) + pub struct Mul; + + // Exponential complexity. + // Eg: O(N₁^N₂) pub struct Exponential; + // There is only one arg and it scales linearly with it's size pub struct Size; // Complexity is captured by the lesser complexity of the two args @@ -26,7 +31,7 @@ pub mod complexity { } } - impl GasCombinator for Exponential { + impl GasCombinator for Mul { #[inline(always)] fn combine(lhs: Gas, rhs: Gas) -> Gas { Gas(lhs.0.saturating_mul(rhs.0)) @@ -112,3 +117,18 @@ where None } } + +impl GasSizeOf for Combine<(T0, u8), complexity::Exponential> +where + T0: GasSizeOf, +{ + fn gas_size_of(&self) -> Gas { + let (a, b) = &self.0; + Gas(a.gas_size_of().0.saturating_pow(*b as u32)) + } + + #[inline] + fn const_gas_size_of() -> Option { + None + } +} diff --git a/runtime/wasm/src/gas/costs.rs b/runtime/wasm/src/gas/costs.rs index 02cbb73dd13..17114337aa0 100644 --- a/runtime/wasm/src/gas/costs.rs +++ b/runtime/wasm/src/gas/costs.rs @@ -3,105 +3,66 @@ use super::*; -/// Using 10 gas = ~1ns for WASM instructions -/// So the maximum time spent doing pure math -/// would be in the 1 hour range. The intent here -/// is to have the determinism cutoff be very high, -/// while still allowing more reasonable timer based cutoffs. -/// Having a unit like 10 gas for ~1ns allows us to be granular -/// in instructions which are aggregated into metered blocks -/// via https://docs.rs/pwasm-utils/0.16.0/pwasm_utils/fn.inject_gas_counter.html -/// But we can still charge very high numbers for other things. -pub const MAX_GAS: u64 = 36_000_000_000_000; +/// Using 10 gas = ~1ns for WASM instructions. +const GAS_PER_SECOND: u64 = 10_000_000_000; + +/// Set max gas to 1 hour worth of gas per handler. The intent here is to have the determinism +/// cutoff be very high, while still allowing more reasonable timer based cutoffs. Having a unit +/// like 10 gas for ~1ns allows us to be granular in instructions which are aggregated into metered +/// blocks via https://docs.rs/pwasm-utils/0.16.0/pwasm_utils/fn.inject_gas_counter.html But we can +/// still charge very high numbers for other things. +pub const MAX_GAS_PER_HANDLER: u64 = 3600 * GAS_PER_SECOND; + +/// Base gas cost for calling any host export. +/// Security: This must be non-zero. +const HOST_EXPORT: u64 = 100_000; -/// Base gas cost for calling any host export -// Security: This must be non-zero. /// Gas for instructions are aggregated into blocks, so hopefully gas calls each have relatively large gas. /// But in the case they don't, we don't want the overhead of calling out into a host export to be /// the dominant cost that causes unexpectedly high execution times. -pub const HOST_EXPORT: Gas = Gas(100_000); +pub const HOST_EXPORT_GAS: Gas = Gas(HOST_EXPORT); -// Allow up to 25,000 ethereum calls -pub const ETHEREUM_CALL: Gas = Gas(MAX_GAS / 25000); +/// As a heuristic for the cost of host fns it makes sense to reason in terms of bandwidth and +/// calculate the cost from there. Because we don't have benchmarks for each host fn, we go with +/// pessimistic assumption of performance of 10 MB/s, which nonetheless allows for 36 GB to be +/// processed through host exports by a single handler at a 1 hour budget. +const DEFAULT_BYTE_PER_SECOND: u64 = 10_000_000; -// The cost of allocating an AscHeap object -pub const ALLOC_NEW: GasOp = GasOp { - base_cost: 200_000, - size_mult: 400, -}; +/// With the current parameters DEFAULT_GAS_PER_BYTE = 1_000. +const DEFAULT_GAS_PER_BYTE: u64 = GAS_PER_SECOND / DEFAULT_BYTE_PER_SECOND; -// Cost of reading from the AscHeap -pub const HEAP_READ: GasOp = GasOp { - base_cost: 10_000, - size_mult: 100, -}; +pub(crate) const DEFAULT_BASE_COST: u64 = 100_000; -pub const BIG_INT_TO_HEX: GasOp = GasOp { - base_cost: 150_000, - size_mult: 500, +pub(crate) const DEFAULT_GAS_OP: GasOp = GasOp { + base_cost: DEFAULT_BASE_COST, + size_mult: DEFAULT_GAS_PER_BYTE, }; -// TODO: Heap write? +// Allow up to 25,000 ethereum calls +pub const ETHEREUM_CALL: Gas = Gas(MAX_GAS_PER_HANDLER / 25_000); + +// Allow up to 100,000 data sources to be created +pub const CREATE_DATA_SOURCE: Gas = Gas(MAX_GAS_PER_HANDLER / 100_000); + +// Allow up to 100,000 logs +pub const LOG: Gas = Gas(MAX_GAS_PER_HANDLER / 100_000); // Saving to the store is one of the most expensive operations. pub const STORE_SET: GasOp = GasOp { // Allow up to 250k entities saved. - base_cost: MAX_GAS / 250_000, + base_cost: MAX_GAS_PER_HANDLER / 250_000, // If the size roughly corresponds to bytes, allow 1GB to be saved. - size_mult: MAX_GAS / 1_000_000_000, + size_mult: MAX_GAS_PER_HANDLER / 1_000_000_000, }; // Reading from the store is much cheaper than writing. pub const STORE_GET: GasOp = GasOp { - base_cost: MAX_GAS / 10_000_000, - size_mult: MAX_GAS / 10_000_000_000, + base_cost: MAX_GAS_PER_HANDLER / 10_000_000, + size_mult: MAX_GAS_PER_HANDLER / 10_000_000_000, }; pub const STORE_REMOVE: GasOp = STORE_SET; -pub const JSON_TO_I64: GasOp = GasOp { - base_cost: 150_000, - size_mult: 1_400, -}; -pub const JSON_TO_U64: GasOp = JSON_TO_I64; - -pub const JSON_TO_F64: GasOp = GasOp { - base_cost: 150_000, - size_mult: 1_800, -}; - -pub const JSON_TO_BIGINT: GasOp = GasOp { - base_cost: 250_000, - size_mult: 10_000, -}; - -pub const KECCAK256: GasOp = GasOp { - base_cost: 250_000, - size_mult: 4_000, -}; - -pub const BIG_INT_PLUS: GasOp = GasOp { - base_cost: 100_000, - size_mult: 800, -}; - -pub const BIG_INT_MINUS: GasOp = GasOp { - base_cost: 100_000, - size_mult: 850, -}; - -pub const BIG_INT_MUL: GasOp = GasOp { - base_cost: 100_000, - size_mult: 3_200, -}; - -pub const BIG_INT_DIV: GasOp = GasOp { - base_cost: 100_000, - size_mult: 6_400, -}; - -pub const BIG_INT_MOD: GasOp = BIG_INT_DIV; - pub struct GasRules; impl Rules for GasRules { @@ -243,6 +204,7 @@ impl Rules for GasRules { }; Some(weight) } + fn memory_grow_cost(&self) -> Option { // Each page is 64KiB which is 65536 bytes. const PAGE: u64 = 64 * 1024; @@ -253,7 +215,7 @@ impl Rules for GasRules { // free pages because this is 32bit WASM. const MAX_PAGES: u64 = 12 * GIB / PAGE; // This ends up at 439,453,125 per page. - const GAS_PER_PAGE: u64 = MAX_GAS / MAX_PAGES; + const GAS_PER_PAGE: u64 = MAX_GAS_PER_HANDLER / MAX_PAGES; let gas_per_page = NonZeroU32::new(GAS_PER_PAGE.try_into().unwrap()).unwrap(); Some(MemoryGrowCost::Linear(gas_per_page)) diff --git a/runtime/wasm/src/gas/mod.rs b/runtime/wasm/src/gas/mod.rs index c1a66df6028..ed7ea7ebcb3 100644 --- a/runtime/wasm/src/gas/mod.rs +++ b/runtime/wasm/src/gas/mod.rs @@ -5,15 +5,15 @@ mod saturating; mod size_of; pub use combinators::*; pub use costs::*; +use graph::prelude::CheapClone; +use graph::runtime::DeterministicHostError; pub use saturating::*; use parity_wasm::elements::Instruction; use pwasm_utils::rules::{MemoryGrowCost, Rules}; -use std::convert::TryInto; use std::num::NonZeroU32; use std::sync::atomic::{AtomicU64, Ordering::SeqCst}; - -use crate::error::DeterministicHostError; +use std::{convert::TryInto, rc::Rc}; pub struct GasOp { base_cost: u64, @@ -65,20 +65,31 @@ impl Gas { pub const ZERO: Gas = Gas(0); } -pub struct GasCounter(AtomicU64); +impl From for Gas { + fn from(x: u64) -> Self { + Gas(x) + } +} + +#[derive(Clone)] +pub struct GasCounter(Rc); + +impl CheapClone for GasCounter {} impl GasCounter { pub fn new() -> Self { - Self(AtomicU64::new(0)) + Self(Rc::new(AtomicU64::new(0))) } - pub fn consume(&self, amount: Gas) -> Result<(), DeterministicHostError> { + /// This should be called once per host export + pub fn consume_host_fn(&self, mut amount: Gas) -> Result<(), DeterministicHostError> { + amount += costs::HOST_EXPORT_GAS; let new = self .0 .fetch_update(SeqCst, SeqCst, |v| Some(v.saturating_add(amount.0))) .unwrap(); - if new >= MAX_GAS { + if new >= MAX_GAS_PER_HANDLER { Err(DeterministicHostError(anyhow::anyhow!( "Gas limit exceeded. Used: {}", new diff --git a/runtime/wasm/src/gas/saturating.rs b/runtime/wasm/src/gas/saturating.rs index 52c39911fa8..de2a477d49a 100644 --- a/runtime/wasm/src/gas/saturating.rs +++ b/runtime/wasm/src/gas/saturating.rs @@ -35,3 +35,17 @@ impl SaturatingFrom for u64 { value.try_into().unwrap_or(u64::MAX) } } + +impl SaturatingFrom for Gas { + #[inline] + fn saturating_from(value: f64) -> Self { + Gas(value as u64) + } +} + +impl SaturatingFrom for Gas { + #[inline] + fn saturating_from(value: u32) -> Self { + Gas(value as u64) + } +} diff --git a/runtime/wasm/src/gas/size_of.rs b/runtime/wasm/src/gas/size_of.rs index b2554c3986c..18b4983a32f 100644 --- a/runtime/wasm/src/gas/size_of.rs +++ b/runtime/wasm/src/gas/size_of.rs @@ -53,8 +53,7 @@ where impl GasSizeOf for BigInt { fn gas_size_of(&self) -> Gas { - // This does not do % 8 to count bytes but that's ok since this type is inherantly expensive. - let gas: Gas = self.bits().saturating_into(); + let gas: Gas = (self.bits() / 8).saturating_into(); gas + Gas(100) } } @@ -68,9 +67,9 @@ impl GasSizeOf for Entity { impl GasSizeOf for BigDecimal { fn gas_size_of(&self) -> Gas { // This can be overly pessimistic - let (int, _) = self.as_bigint_and_exponent(); - // This does not do % 8 to count bytes but that's ok since this type is inherantly expensive. - let gas: Gas = int.bits().saturating_into(); + let gas: Gas = ((self.digits() as f64 * std::f64::consts::LOG2_10) / 8.0) + .ceil() + .saturating_into(); gas + Gas(1000) } } @@ -101,7 +100,10 @@ where self.keys().map(|k| k.gas_size_of()).sum::() + v_gas * self.len() } (Some(k_gas), Some(v_gas)) => (k_gas + v_gas) * self.len(), - (None, None) => self.iter().map(|e| e.gas_size_of()).sum(), + (None, None) => self + .iter() + .map(|(k, v)| k.gas_size_of() + v.gas_size_of()) + .sum(), }; members + Gas(32) + (Gas(8) * self.len()) } @@ -176,20 +178,8 @@ impl GasSizeOf for EntityKey { } } -impl GasSizeOf for EntityType {} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn const_tuples_propagate() { - assert_eq!(Some(Gas(16)), <(u64, u64)>::const_gas_size_of()); - assert_eq!(None, <(&str, u64)>::const_gas_size_of()); - } - - #[test] - fn dyn_tuples_propagate() { - assert_eq!(Gas(99), ("abc", 0u64).gas_size_of()) +impl GasSizeOf for EntityType { + fn gas_size_of(&self) -> Gas { + self.as_str().gas_size_of() } } diff --git a/runtime/wasm/src/host_exports.rs b/runtime/wasm/src/host_exports.rs index 011e82421bb..0a92f26f499 100644 --- a/runtime/wasm/src/host_exports.rs +++ b/runtime/wasm/src/host_exports.rs @@ -1,3 +1,4 @@ +use crate::gas::{self, complexity, Gas, GasCounter}; use crate::{error::DeterminismLevel, module::IntoTrap}; use ethabi::param_type::Reader; use ethabi::{decode, encode, Token}; @@ -95,20 +96,16 @@ impl HostExports { } } - // TODO: What's the scope of this when is it set to 0? Ideally per block? - /// This should be called once per host export - #[inline] - pub(crate) fn consume_gas(&self, amount: Gas) -> Result<(), DeterministicHostError> { - self.gas_used.consume(amount + gas::HOST_EXPORT) - } - pub(crate) fn abort( &self, message: Option, file_name: Option, line_number: Option, column_number: Option, + gas: &GasCounter, ) -> Result { + gas.consume_host_fn(gas::DEFAULT_BASE_COST.into())?; + let message = message .map(|message| format!("message: {}", message)) .unwrap_or_else(|| "no message".into()); @@ -140,6 +137,7 @@ impl HostExports { entity_id: String, mut data: HashMap, stopwatch: &StopwatchMetrics, + gas: &GasCounter, ) -> Result<(), anyhow::Error> { let poi_section = stopwatch.start_section("host_export_store_set__proof_of_indexing"); write_poi_event( @@ -177,7 +175,7 @@ impl HostExports { entity_id, }; - self.consume_gas(gas::STORE_SET.with_args(complexity::Linear, (&key, &data)))?; + gas.consume_host_fn(gas::STORE_SET.with_args(complexity::Linear, (&key, &data)))?; let entity = Entity::from(data); let schema = self.store.input_schema(&self.subgraph_id)?; @@ -206,6 +204,7 @@ impl HostExports { proof_of_indexing: &SharedProofOfIndexing, entity_type: String, entity_id: String, + gas: &GasCounter, ) -> Result<(), HostExportError> { write_poi_event( proof_of_indexing, @@ -222,7 +221,7 @@ impl HostExports { entity_id, }; - self.consume_gas(gas::STORE_REMOVE.with_args(complexity::Size, &key))?; + gas.consume_host_fn(gas::STORE_REMOVE.with_args(complexity::Size, &key))?; state.entity_cache.remove(key); @@ -234,6 +233,7 @@ impl HostExports { state: &mut BlockState, entity_type: String, entity_id: String, + gas: &GasCounter, ) -> Result, anyhow::Error> { let store_key = EntityKey { subgraph_id: self.subgraph_id.clone(), @@ -242,7 +242,7 @@ impl HostExports { }; let result = state.entity_cache.get(&store_key)?; - self.consume_gas(gas::STORE_GET.with_args(complexity::Linear, (&store_key, &result)))?; + gas.consume_host_fn(gas::STORE_GET.with_args(complexity::Linear, (&store_key, &result)))?; Ok(state.entity_cache.get(&store_key)?) } @@ -252,8 +252,12 @@ impl HostExports { /// Their encoding may be of uneven length. The number zero encodes as "0x0". /// /// https://godoc.org/github.com/ethereum/go-ethereum/common/hexutil#hdr-Encoding_Rules - pub(crate) fn big_int_to_hex(&self, n: BigInt) -> Result { - self.consume_gas(gas::BIG_INT_TO_HEX.with_args(Size, n))?; + pub(crate) fn big_int_to_hex( + &self, + n: BigInt, + gas: &GasCounter, + ) -> Result { + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Size, &n))?; if n == 0.into() { return Ok("0x0".to_string()); @@ -344,16 +348,24 @@ impl HostExports { } /// Expects a decimal string. - pub(crate) fn json_to_i64(&self, json: String) -> Result { - self.consume_gas(gas::JSON_TO_I64.with_args(complexity::Size, &json))?; + pub(crate) fn json_to_i64( + &self, + json: String, + gas: &GasCounter, + ) -> Result { + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Size, &json))?; i64::from_str(&json) .with_context(|| format!("JSON `{}` cannot be parsed as i64", json)) .map_err(DeterministicHostError) } /// Expects a decimal string. - pub(crate) fn json_to_u64(&self, json: String) -> Result { - self.consume_gas(gas::JSON_TO_U64.with_args(complexity::Size, &json))?; + pub(crate) fn json_to_u64( + &self, + json: String, + gas: &GasCounter, + ) -> Result { + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Size, &json))?; u64::from_str(&json) .with_context(|| format!("JSON `{}` cannot be parsed as u64", json)) @@ -361,8 +373,12 @@ impl HostExports { } /// Expects a decimal string. - pub(crate) fn json_to_f64(&self, json: String) -> Result { - self.consume_gas(gas::JSON_TO_F64.with_args(complexity::Size, &json))?; + pub(crate) fn json_to_f64( + &self, + json: String, + gas: &GasCounter, + ) -> Result { + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Size, &json))?; f64::from_str(&json) .with_context(|| format!("JSON `{}` cannot be parsed as f64", json)) @@ -370,8 +386,12 @@ impl HostExports { } /// Expects a decimal string. - pub(crate) fn json_to_big_int(&self, json: String) -> Result, DeterministicHostError> { - self.consume_gas(gas::JSON_TO_BIGINT.with_args(complexity::Size, &json))?; + pub(crate) fn json_to_big_int( + &self, + json: String, + gas: &GasCounter, + ) -> Result, DeterministicHostError> { + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Size, &json))?; let big_int = BigInt::from_str(&json) .with_context(|| format!("JSON `{}` is not a decimal string", json)) @@ -382,9 +402,10 @@ impl HostExports { pub(crate) fn crypto_keccak_256( &self, input: Vec, + gas: &GasCounter, ) -> Result<[u8; 32], DeterministicHostError> { let data = &input[..]; - self.consume_gas(gas::KECCAK256.with_args(complexity::Size, data))?; + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Size, data))?; Ok(tiny_keccak::keccak256(data)) } @@ -392,8 +413,9 @@ impl HostExports { &self, x: BigInt, y: BigInt, + gas: &GasCounter, ) -> Result { - self.consume_gas(gas::BIG_INT_PLUS.with_args(complexity::Max, (&x, &y)))?; + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Max, (&x, &y)))?; Ok(x + y) } @@ -401,8 +423,9 @@ impl HostExports { &self, x: BigInt, y: BigInt, + gas: &GasCounter, ) -> Result { - self.consume_gas(gas::BIG_INT_MINUS.with_args(complexity::Max, (&x, &y)))?; + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Max, (&x, &y)))?; Ok(x - y) } @@ -410,8 +433,9 @@ impl HostExports { &self, x: BigInt, y: BigInt, + gas: &GasCounter, ) -> Result { - self.consume_gas(gas::BIG_INT_MUL.with_args(complexity::Exponential, (&x, &y)))?; + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Mul, (&x, &y)))?; Ok(x * y) } @@ -419,8 +443,9 @@ impl HostExports { &self, x: BigInt, y: BigInt, + gas: &GasCounter, ) -> Result { - self.consume_gas(gas::BIG_INT_DIV.with_args(complexity::Exponential, (&x, &y)))?; + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Mul, (&x, &y)))?; if y == 0.into() { return Err(DeterministicHostError(anyhow!( "attempted to divide BigInt `{}` by zero", @@ -434,8 +459,9 @@ impl HostExports { &self, x: BigInt, y: BigInt, + gas: &GasCounter, ) -> Result { - self.consume_gas(gas::BIG_INT_MOD.with_args(complexity::Exponential, (&x, &y)))?; + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Mul, (&x, &y)))?; if y == 0.into() { return Err(DeterministicHostError(anyhow!( "attempted to calculate the remainder of `{}` with a divisor of zero", @@ -449,14 +475,19 @@ impl HostExports { pub(crate) fn big_int_pow( &self, x: BigInt, - exponent: u8, + exp: u8, + gas: &GasCounter, ) -> Result { - self.consume_gas(gas::BIG_INT_POW.with_args(complexity::TODO, (&x, &y)))?; - Ok(x.pow(exponent)) + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Exponential, (&x, exp)))?; + Ok(x.pow(exp)) } - pub(crate) fn big_int_from_string(&self, s: String) -> Result { - self.consume_gas(gas::BIG_INT_FROM_STRING.with_args(complexity::Size, &s))?; + pub(crate) fn big_int_from_string( + &self, + s: String, + gas: &GasCounter, + ) -> Result { + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Size, &s))?; BigInt::from_str(&s) .with_context(|| format!("string is not a BigInt: `{}`", s)) .map_err(DeterministicHostError) @@ -466,8 +497,9 @@ impl HostExports { &self, x: BigInt, y: BigInt, + gas: &GasCounter, ) -> Result { - self.consume_gas(gas::BIG_INT_BIT_OR.with_args(complexity::MAX, (&x, &y)))?; + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Max, (&x, &y)))?; Ok(x | y) } @@ -475,8 +507,9 @@ impl HostExports { &self, x: BigInt, y: BigInt, + gas: &GasCounter, ) -> Result { - self.consume_gas(gas::BIG_INT_BIT_AND.with_args(complexity::MIN, (&x, &y)))?; + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Min, (&x, &y)))?; Ok(x & y) } @@ -484,8 +517,9 @@ impl HostExports { &self, x: BigInt, bits: u8, + gas: &GasCounter, ) -> Result { - self.consume_gas(gas::BIG_INT_SHL.with_args(complexity::TODO, (&x, &y)))?; + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Linear, (&x, &bits)))?; Ok(x << bits) } @@ -493,14 +527,19 @@ impl HostExports { &self, x: BigInt, bits: u8, + gas: &GasCounter, ) -> Result { - self.consume_gas(gas::BIG_INT_SHR.with_args(complexity::TODO, (&x, &y)))?; + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Linear, (&x, &bits)))?; Ok(x >> bits) } /// Useful for IPFS hashes stored as bytes - pub(crate) fn bytes_to_base58(&self, bytes: Vec) -> Result { - self.consume_gas(gas::BYTES_TO_BASE58.with_args(complexity::Size, &bytes))?; + pub(crate) fn bytes_to_base58( + &self, + bytes: Vec, + gas: &GasCounter, + ) -> Result { + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Size, &bytes))?; Ok(::bs58::encode(&bytes).into_string()) } @@ -508,8 +547,9 @@ impl HostExports { &self, x: BigDecimal, y: BigDecimal, + gas: &GasCounter, ) -> Result { - self.consume_gas(gas::BIG_DECIMAL_PLUS.with_args(complexity::Linear, (&x, &y)))?; + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Linear, (&x, &y)))?; Ok(x + y) } @@ -517,8 +557,9 @@ impl HostExports { &self, x: BigDecimal, y: BigDecimal, + gas: &GasCounter, ) -> Result { - self.consume_gas(gas::BIG_DECIMAL_MINUS.with_args(complexity::Linear, (&x, &y)))?; + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Linear, (&x, &y)))?; Ok(x - y) } @@ -526,8 +567,9 @@ impl HostExports { &self, x: BigDecimal, y: BigDecimal, + gas: &GasCounter, ) -> Result { - self.consume_gas(gas::BIG_DECIMAL_MUL.with_args(complexity::Exponential, (&x, &y)))?; + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Mul, (&x, &y)))?; Ok(x * y) } @@ -536,8 +578,9 @@ impl HostExports { &self, x: BigDecimal, y: BigDecimal, + gas: &GasCounter, ) -> Result { - self.consume_gas(gas::BIG_DECIMAL_DIV.with_args(complexity::Exponential, (&x, &y)))?; + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Mul, (&x, &y)))?; if y == 0.into() { return Err(DeterministicHostError(anyhow!( "attempted to divide BigDecimal `{}` by zero", @@ -551,24 +594,27 @@ impl HostExports { &self, x: BigDecimal, y: BigDecimal, + gas: &GasCounter, ) -> Result { - self.consume_gas(gas::BIG_DECIMAL_EQ.with_args(complexity::MIN, (&x, &y)))?; + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Min, (&x, &y)))?; Ok(x == y) } pub(crate) fn big_decimal_to_string( &self, x: BigDecimal, + gas: &GasCounter, ) -> Result { - self.consume_gas(gas::BIG_DECIMAL_TO_STR.with_args(complexity::Linear, (&x, &y)))?; + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Size, &x))?; Ok(x.to_string()) } pub(crate) fn big_decimal_from_string( &self, s: String, + gas: &GasCounter, ) -> Result { - self.consume_gas(gas::BIG_DECIMAL_PARSE.with_args(complexity::Linear, (&x, &y)))?; + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Size, &s))?; BigDecimal::from_str(&s) .with_context(|| format!("string is not a BigDecimal: '{}'", s)) .map_err(DeterministicHostError) @@ -582,7 +628,9 @@ impl HostExports { params: Vec, context: Option, creation_block: BlockNumber, + gas: &GasCounter, ) -> Result<(), HostExportError> { + gas.consume_host_fn(gas::CREATE_DATA_SOURCE)?; info!( logger, "Create data source"; @@ -632,7 +680,10 @@ impl HostExports { logger: &Logger, level: slog::Level, msg: String, + gas: &GasCounter, ) -> Result<(), DeterministicHostError> { + gas.consume_host_fn(gas::LOG)?; + let rs = record_static!(level, self.data_source_name.as_str()); logger.log(&slog::Record::new( @@ -649,29 +700,98 @@ impl HostExports { Ok(()) } - pub(crate) fn data_source_address(&self) -> Vec { - self.data_source_address.clone() + pub(crate) fn data_source_address( + &self, + gas: &GasCounter, + ) -> Result, DeterministicHostError> { + gas.consume_host_fn(Gas::from(gas::DEFAULT_BASE_COST))?; + Ok(self.data_source_address.clone()) } - pub(crate) fn data_source_network(&self) -> String { - self.data_source_network.clone() + pub(crate) fn data_source_network( + &self, + gas: &GasCounter, + ) -> Result { + gas.consume_host_fn(Gas::from(gas::DEFAULT_BASE_COST))?; + Ok(self.data_source_network.clone()) } - pub(crate) fn data_source_context(&self) -> Entity { - self.data_source_context + pub(crate) fn data_source_context( + &self, + gas: &GasCounter, + ) -> Result { + gas.consume_host_fn(Gas::from(gas::DEFAULT_BASE_COST))?; + Ok(self + .data_source_context .as_ref() .clone() - .unwrap_or_default() + .unwrap_or_default()) + } + + pub(crate) fn json_from_bytes( + &self, + bytes: &Vec, + ) -> Result { + serde_json::from_reader(bytes.as_slice()).map_err(|e| DeterministicHostError(e.into())) + } + + pub(crate) fn string_to_h160( + &self, + string: &str, + gas: &GasCounter, + ) -> Result { + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Size, &string))?; + string_to_h160(string) + } + + pub(crate) fn bytes_to_string( + &self, + logger: &Logger, + bytes: Vec, + gas: &GasCounter, + ) -> Result { + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Size, &bytes))?; + + Ok(bytes_to_string(logger, bytes)) + } + + pub(crate) fn ethereum_encode( + &self, + token: Token, + gas: &GasCounter, + ) -> Result, DeterministicHostError> { + let encoded = encode(&[token]); + + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Size, &encoded))?; + + Ok(encoded) + } + + pub(crate) fn ethereum_decode( + &self, + types: String, + data: Vec, + gas: &GasCounter, + ) -> Result { + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Size, &data))?; + + let param_types = Reader::read(&types) + .or_else(|e| Err(anyhow::anyhow!("Failed to read types: {}", e)))?; + + decode(&[param_types], &data) + // The `.pop().unwrap()` here is ok because we're always only passing one + // `param_types` to `decode`, so the returned `Vec` has always size of one. + // We can't do `tokens[0]` because the value can't be moved out of the `Vec`. + .map(|mut tokens| tokens.pop().unwrap()) + .context("Failed to decode") } } -pub(crate) fn json_from_bytes( - bytes: &Vec, -) -> Result { - serde_json::from_reader(bytes.as_slice()).map_err(|e| DeterministicHostError(e.into())) +fn block_on03(future: impl futures03::Future + Send) -> T { + graph::block_on(future) } -pub(crate) fn string_to_h160(string: &str) -> Result { +fn string_to_h160(string: &str) -> Result { // `H160::from_str` takes a hex string with no leading `0x`. let s = string.trim_start_matches("0x"); H160::from_str(s) @@ -679,7 +799,7 @@ pub(crate) fn string_to_h160(string: &str) -> Result) -> String { +fn bytes_to_string(logger: &Logger, bytes: Vec) -> String { let s = String::from_utf8_lossy(&bytes); // If the string was re-allocated, that means it was not UTF8. @@ -698,22 +818,6 @@ pub(crate) fn bytes_to_string(logger: &Logger, bytes: Vec) -> String { s.trim_end_matches('\u{0000}').to_string() } -pub(crate) fn ethereum_encode(token: Token) -> Result, anyhow::Error> { - Ok(encode(&[token])) -} - -pub(crate) fn ethereum_decode(types: String, data: Vec) -> Result { - let param_types = - Reader::read(&types).or_else(|e| Err(anyhow::anyhow!("Failed to read types: {}", e)))?; - - decode(&[param_types], &data) - // The `.pop().unwrap()` here is ok because we're always only passing one - // `param_types` to `decode`, so the returned `Vec` has always size of one. - // We can't do `tokens[0]` because the value can't be moved out of the `Vec`. - .map(|mut tokens| tokens.pop().unwrap()) - .context("Failed to decode") -} - #[test] fn test_string_to_h160_with_0x() { assert_eq!( @@ -722,10 +826,6 @@ fn test_string_to_h160_with_0x() { ) } -fn block_on03(future: impl futures03::Future + Send) -> T { - graph::block_on(future) -} - #[test] fn bytes_to_string_is_lossy() { assert_eq!( diff --git a/runtime/wasm/src/mapping.rs b/runtime/wasm/src/mapping.rs index 2f16a4d15ef..cad14d2e39c 100644 --- a/runtime/wasm/src/mapping.rs +++ b/runtime/wasm/src/mapping.rs @@ -142,10 +142,9 @@ pub struct ValidModule { impl ValidModule { /// Pre-process and validate the module. pub fn new(raw_module: &[u8]) -> Result { - // Add the gas calls here. - // Module name "gas" must match. See also e3f03e62-40e4-4f8c-b4a1-d0375cca0b76 - // We do this by round-tripping the module through parity - injecting gas then - // serializing again. + // Add the gas calls here. Module name "gas" must match. See also + // e3f03e62-40e4-4f8c-b4a1-d0375cca0b76. We do this by round-tripping the module through + // parity - injecting gas then serializing again. let parity_module = parity_wasm::elements::Module::from_bytes(raw_module)?; let parity_module = pwasm_utils::inject_gas_counter(parity_module, &GasRules, "gas") .map_err(|_| anyhow!("Failed to inject gas counter"))?; diff --git a/runtime/wasm/src/module/mod.rs b/runtime/wasm/src/module/mod.rs index e02dde43eeb..c51eb16508e 100644 --- a/runtime/wasm/src/module/mod.rs +++ b/runtime/wasm/src/module/mod.rs @@ -2,6 +2,7 @@ use std::cell::{RefCell, RefMut}; use std::collections::HashMap; use std::convert::TryFrom; use std::rc::Rc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Instant; use graph::blockchain::{Blockchain, HostFnCtx, TriggerWithHandler}; @@ -297,6 +298,11 @@ impl WasmInstance { }); } + // Because `gas` and `deterministic_host_trap` need to be accessed from the gas + // host fn, they need to be separate from the rest of the context. + let gas = GasCounter::new(); + let deterministic_host_trap = Rc::new(AtomicBool::new(false)); + macro_rules! link { ($wasm_name:expr, $rust_name:ident, $($param:ident),*) => { link!($wasm_name, $rust_name, "host_export_other", $($param),*) @@ -316,6 +322,7 @@ impl WasmInstance { let host_metrics = host_metrics.cheap_clone(); let timeout_stopwatch = timeout_stopwatch.cheap_clone(); let ctx = ctx.cheap_clone(); + let gas = gas.cheap_clone(); linker.func( module, $wasm_name, @@ -340,6 +347,7 @@ impl WasmInstance { let _section = instance.host_metrics.stopwatch.start_section($section); let result = instance.$rust_name( + &gas, $($param.into()),* ); match result { @@ -515,6 +523,25 @@ impl WasmInstance { link!("box.profile", box_profile, ptr); } + // link the `gas` function + // See also e3f03e62-40e4-4f8c-b4a1-d0375cca0b76 + { + let host_metrics = host_metrics.cheap_clone(); + let gas = gas.cheap_clone(); + linker.func("gas", "gas", move |gas_used: u32| -> Result<(), Trap> { + // Starting the section is probably more expensive than the gas operation itself, + // but still we need insight into whether this is relevant to indexing performance. + let _section = host_metrics.stopwatch.start_section("host_export_gas"); + + if let Err(e) = gas.consume_host_fn(gas_used.saturating_into()) { + deterministic_host_trap.store(true, Ordering::SeqCst); + return Err(e.into_trap()); + } + + Ok(()) + })?; + } + let instance = linker.instantiate(&valid_module.module)?; // Usually `shared_ctx` is still `None` because no host fns were called during start. @@ -745,6 +772,7 @@ impl WasmInstanceContext { /// Always returns a trap. pub fn abort( &mut self, + gas: &GasCounter, message_ptr: AscPtr, file_name_ptr: AscPtr, line_number: u32, @@ -769,12 +797,13 @@ impl WasmInstanceContext { self.ctx .host_exports - .abort(message, file_name, line_number, column_number) + .abort(message, file_name, line_number, column_number, gas) } /// function store.set(entity: string, id: string, data: Entity): void pub fn store_set( &mut self, + gas: &GasCounter, entity_ptr: AscPtr, id_ptr: AscPtr, data_ptr: AscPtr, @@ -794,6 +823,7 @@ impl WasmInstanceContext { id, data, stopwatch, + gas, )?; Ok(()) } @@ -801,6 +831,7 @@ impl WasmInstanceContext { /// function store.remove(entity: string, id: string): void pub fn store_remove( &mut self, + gas: &GasCounter, entity_ptr: AscPtr, id_ptr: AscPtr, ) -> Result<(), HostExportError> { @@ -812,12 +843,14 @@ impl WasmInstanceContext { &self.ctx.proof_of_indexing, entity, id, + gas, ) } /// function store.get(entity: string, id: string): Entity | null pub fn store_get( &mut self, + gas: &GasCounter, entity_ptr: AscPtr, id_ptr: AscPtr, ) -> Result, HostExportError> { @@ -830,7 +863,7 @@ impl WasmInstanceContext { let entity_option = self.ctx .host_exports - .store_get(&mut self.ctx.state, entity_ptr, id_ptr)?; + .store_get(&mut self.ctx.state, entity_ptr, id_ptr, gas)?; let ret = match entity_option { Some(entity) => { @@ -849,9 +882,14 @@ impl WasmInstanceContext { /// function typeConversion.bytesToString(bytes: Bytes): string pub fn bytes_to_string( &mut self, + gas: &GasCounter, bytes_ptr: AscPtr, ) -> Result, DeterministicHostError> { - let string = host_exports::bytes_to_string(&self.ctx.logger, asc_get(self, bytes_ptr)?); + let string = self.ctx.host_exports.bytes_to_string( + &self.ctx.logger, + asc_get(self, bytes_ptr)?, + gas, + )?; asc_new(self, &string) } @@ -862,9 +900,12 @@ impl WasmInstanceContext { /// https://github.com/ethereum/web3.js/blob/f98fe1462625a6c865125fecc9cb6b414f0a5e83/packages/web3-utils/src/utils.js#L283 pub fn bytes_to_hex( &mut self, + gas: &GasCounter, bytes_ptr: AscPtr, ) -> Result, DeterministicHostError> { let bytes: Vec = asc_get(self, bytes_ptr)?; + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(gas::complexity::Size, &bytes))?; + // Even an empty string must be prefixed with `0x`. // Encodes each byte as a two hex digits. let hex = format!("0x{}", hex::encode(bytes)); @@ -874,52 +915,61 @@ impl WasmInstanceContext { /// function typeConversion.bigIntToString(n: Uint8Array): string pub fn big_int_to_string( &mut self, + gas: &GasCounter, big_int_ptr: AscPtr, ) -> Result, DeterministicHostError> { let n: BigInt = asc_get(self, big_int_ptr)?; + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(gas::complexity::Size, &n))?; asc_new(self, &n.to_string()) } /// function bigInt.fromString(x: string): BigInt pub fn big_int_from_string( &mut self, + gas: &GasCounter, string_ptr: AscPtr, ) -> Result, DeterministicHostError> { let result = self .ctx .host_exports - .big_int_from_string(asc_get(self, string_ptr)?)?; + .big_int_from_string(asc_get(self, string_ptr)?, gas)?; asc_new(self, &result) } /// function typeConversion.bigIntToHex(n: Uint8Array): string pub fn big_int_to_hex( &mut self, + gas: &GasCounter, big_int_ptr: AscPtr, ) -> Result, DeterministicHostError> { let n: BigInt = asc_get(self, big_int_ptr)?; - let hex = self.ctx.host_exports.big_int_to_hex(n)?; + let hex = self.ctx.host_exports.big_int_to_hex(n, gas)?; asc_new(self, &hex) } /// function typeConversion.stringToH160(s: String): H160 pub fn string_to_h160( &mut self, + gas: &GasCounter, str_ptr: AscPtr, ) -> Result, DeterministicHostError> { let s: String = asc_get(self, str_ptr)?; - let h160 = host_exports::string_to_h160(&s)?; + let h160 = self.ctx.host_exports.string_to_h160(&s, gas)?; asc_new(self, &h160) } /// function json.fromBytes(bytes: Bytes): JSONValue pub fn json_from_bytes( &mut self, + gas: &GasCounter, bytes_ptr: AscPtr, ) -> Result>, DeterministicHostError> { let bytes: Vec = asc_get(self, bytes_ptr)?; - - let result = host_exports::json_from_bytes(&bytes) + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(gas::complexity::Size, &bytes))?; + let result = self + .ctx + .host_exports + .json_from_bytes(&bytes) .with_context(|| { format!( "Failed to parse JSON from byte array. Bytes (truncated to 1024 chars): `{:?}`", @@ -933,11 +983,13 @@ impl WasmInstanceContext { /// function json.try_fromBytes(bytes: Bytes): Result pub fn json_try_from_bytes( &mut self, + gas: &GasCounter, bytes_ptr: AscPtr, ) -> Result>, bool>>, DeterministicHostError> { let bytes: Vec = asc_get(self, bytes_ptr)?; - let result = host_exports::json_from_bytes(&bytes).map_err(|e| { + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(gas::complexity::Size, &bytes))?; + let result = self.ctx.host_exports.json_from_bytes(&bytes).map_err(|e| { warn!( &self.ctx.logger, "Failed to parse JSON from byte array"; @@ -955,8 +1007,12 @@ impl WasmInstanceContext { /// function ipfs.cat(link: String): Bytes pub fn ipfs_cat( &mut self, + gas: &GasCounter, link_ptr: AscPtr, ) -> Result, HostExportError> { + // Not enabled on the network, no gas consumed. + drop(gas); + if !self.experimental_features.allow_non_deterministic_ipfs { return Err(HostExportError::Deterministic(anyhow!( "`ipfs.cat` is deprecated. Improved support for IPFS will be added in the future" @@ -981,11 +1037,17 @@ impl WasmInstanceContext { /// function ipfs.map(link: String, callback: String, flags: String[]): void pub fn ipfs_map( &mut self, + gas: &GasCounter, link_ptr: AscPtr, callback: AscPtr, user_data: AscPtr>, flags: AscPtr>>, ) -> Result<(), HostExportError> { + // Does not consume gas because this is not a part of deterministic APIs. + // Ideally we would consume gas the same as ipfs_cat and then share + // gas across the spawned modules for callbacks. + drop(gas); + if !self.experimental_features.allow_non_deterministic_ipfs { return Err(HostExportError::Deterministic(anyhow!( "`ipfs.map` is deprecated. Improved support for IPFS will be added in the future" @@ -1032,136 +1094,158 @@ impl WasmInstanceContext { /// function json.toI64(json: String): i64 pub fn json_to_i64( &mut self, + gas: &GasCounter, json_ptr: AscPtr, ) -> Result { - self.ctx.host_exports.json_to_i64(asc_get(self, json_ptr)?) + self.ctx + .host_exports + .json_to_i64(asc_get(self, json_ptr)?, gas) } /// Expects a decimal string. /// function json.toU64(json: String): u64 pub fn json_to_u64( &mut self, + gas: &GasCounter, json_ptr: AscPtr, ) -> Result { - self.ctx.host_exports.json_to_u64(asc_get(self, json_ptr)?) + self.ctx + .host_exports + .json_to_u64(asc_get(self, json_ptr)?, gas) } /// Expects a decimal string. /// function json.toF64(json: String): f64 pub fn json_to_f64( &mut self, + gas: &GasCounter, json_ptr: AscPtr, ) -> Result { - self.ctx.host_exports.json_to_f64(asc_get(self, json_ptr)?) + self.ctx + .host_exports + .json_to_f64(asc_get(self, json_ptr)?, gas) } /// Expects a decimal string. /// function json.toBigInt(json: String): BigInt pub fn json_to_big_int( &mut self, + gas: &GasCounter, json_ptr: AscPtr, ) -> Result, DeterministicHostError> { let big_int = self .ctx .host_exports - .json_to_big_int(asc_get(self, json_ptr)?)?; + .json_to_big_int(asc_get(self, json_ptr)?, gas)?; asc_new(self, &*big_int) } /// function crypto.keccak256(input: Bytes): Bytes pub fn crypto_keccak_256( &mut self, + gas: &GasCounter, input_ptr: AscPtr, ) -> Result, DeterministicHostError> { let input = self .ctx .host_exports - .crypto_keccak_256(asc_get(self, input_ptr)?)?; + .crypto_keccak_256(asc_get(self, input_ptr)?, gas)?; asc_new(self, input.as_ref()) } /// function bigInt.plus(x: BigInt, y: BigInt): BigInt pub fn big_int_plus( &mut self, + gas: &GasCounter, x_ptr: AscPtr, y_ptr: AscPtr, ) -> Result, DeterministicHostError> { - let result = self - .ctx - .host_exports - .big_int_plus(asc_get(self, x_ptr)?, asc_get(self, y_ptr)?)?; + let result = self.ctx.host_exports.big_int_plus( + asc_get(self, x_ptr)?, + asc_get(self, y_ptr)?, + gas, + )?; asc_new(self, &result) } /// function bigInt.minus(x: BigInt, y: BigInt): BigInt pub fn big_int_minus( &mut self, + gas: &GasCounter, x_ptr: AscPtr, y_ptr: AscPtr, ) -> Result, DeterministicHostError> { - let result = self - .ctx - .host_exports - .big_int_minus(asc_get(self, x_ptr)?, asc_get(self, y_ptr)?)?; + let result = self.ctx.host_exports.big_int_minus( + asc_get(self, x_ptr)?, + asc_get(self, y_ptr)?, + gas, + )?; asc_new(self, &result) } /// function bigInt.times(x: BigInt, y: BigInt): BigInt pub fn big_int_times( &mut self, + gas: &GasCounter, x_ptr: AscPtr, y_ptr: AscPtr, ) -> Result, DeterministicHostError> { - let result = self - .ctx - .host_exports - .big_int_times(asc_get(self, x_ptr)?, asc_get(self, y_ptr)?)?; + let result = self.ctx.host_exports.big_int_times( + asc_get(self, x_ptr)?, + asc_get(self, y_ptr)?, + gas, + )?; asc_new(self, &result) } /// function bigInt.dividedBy(x: BigInt, y: BigInt): BigInt pub fn big_int_divided_by( &mut self, + gas: &GasCounter, x_ptr: AscPtr, y_ptr: AscPtr, ) -> Result, DeterministicHostError> { - let result = self - .ctx - .host_exports - .big_int_divided_by(asc_get(self, x_ptr)?, asc_get(self, y_ptr)?)?; + let result = self.ctx.host_exports.big_int_divided_by( + asc_get(self, x_ptr)?, + asc_get(self, y_ptr)?, + gas, + )?; asc_new(self, &result) } /// function bigInt.dividedByDecimal(x: BigInt, y: BigDecimal): BigDecimal pub fn big_int_divided_by_decimal( &mut self, + gas: &GasCounter, x_ptr: AscPtr, y_ptr: AscPtr, ) -> Result, DeterministicHostError> { - let x = BigDecimal::new(asc_get::(self, x_ptr)?, 0); - let result = self - .ctx - .host_exports - .big_decimal_divided_by(x, try_asc_get(self, y_ptr)?)?; + let x = BigDecimal::new(asc_get(self, x_ptr)?, 0); + let result = + self.ctx + .host_exports + .big_decimal_divided_by(x, try_asc_get(self, y_ptr)?, gas)?; asc_new(self, &result) } /// function bigInt.mod(x: BigInt, y: BigInt): BigInt pub fn big_int_mod( &mut self, + gas: &GasCounter, x_ptr: AscPtr, y_ptr: AscPtr, ) -> Result, DeterministicHostError> { - let result = self - .ctx - .host_exports - .big_int_mod(asc_get(self, x_ptr)?, asc_get(self, y_ptr)?)?; + let result = + self.ctx + .host_exports + .big_int_mod(asc_get(self, x_ptr)?, asc_get(self, y_ptr)?, gas)?; asc_new(self, &result) } /// function bigInt.pow(x: BigInt, exp: u8): BigInt pub fn big_int_pow( &mut self, + gas: &GasCounter, x_ptr: AscPtr, exp: u32, ) -> Result, DeterministicHostError> { @@ -1169,39 +1253,44 @@ impl WasmInstanceContext { let result = self .ctx .host_exports - .big_int_pow(asc_get(self, x_ptr)?, exp)?; + .big_int_pow(asc_get(self, x_ptr)?, exp, gas)?; asc_new(self, &result) } /// function bigInt.bitOr(x: BigInt, y: BigInt): BigInt pub fn big_int_bit_or( &mut self, + gas: &GasCounter, x_ptr: AscPtr, y_ptr: AscPtr, ) -> Result, DeterministicHostError> { - let result = self - .ctx - .host_exports - .big_int_bit_or(asc_get(self, x_ptr)?, asc_get(self, y_ptr)?)?; + let result = self.ctx.host_exports.big_int_bit_or( + asc_get(self, x_ptr)?, + asc_get(self, y_ptr)?, + gas, + )?; asc_new(self, &result) } /// function bigInt.bitAnd(x: BigInt, y: BigInt): BigInt pub fn big_int_bit_and( &mut self, + gas: &GasCounter, x_ptr: AscPtr, y_ptr: AscPtr, ) -> Result, DeterministicHostError> { - let result = self - .ctx - .host_exports - .big_int_bit_and(asc_get(self, x_ptr)?, asc_get(self, y_ptr)?)?; + let result = self.ctx.host_exports.big_int_bit_and( + asc_get(self, x_ptr)?, + asc_get(self, y_ptr)?, + gas, + )?; asc_new(self, &result) } /// function bigInt.leftShift(x: BigInt, bits: u8): BigInt pub fn big_int_left_shift( &mut self, + gas: &GasCounter, x_ptr: AscPtr, bits: u32, ) -> Result, DeterministicHostError> { @@ -1209,13 +1298,14 @@ impl WasmInstanceContext { let result = self .ctx .host_exports - .big_int_left_shift(asc_get(self, x_ptr)?, bits)?; + .big_int_left_shift(asc_get(self, x_ptr)?, bits, gas)?; asc_new(self, &result) } /// function bigInt.rightShift(x: BigInt, bits: u8): BigInt pub fn big_int_right_shift( &mut self, + gas: &GasCounter, x_ptr: AscPtr, bits: u32, ) -> Result, DeterministicHostError> { @@ -1223,112 +1313,127 @@ impl WasmInstanceContext { let result = self .ctx .host_exports - .big_int_right_shift(asc_get(self, x_ptr)?, bits)?; + .big_int_right_shift(asc_get(self, x_ptr)?, bits, gas)?; asc_new(self, &result) } /// function typeConversion.bytesToBase58(bytes: Bytes): string pub fn bytes_to_base58( &mut self, + gas: &GasCounter, bytes_ptr: AscPtr, ) -> Result, DeterministicHostError> { let result = self .ctx .host_exports - .bytes_to_base58(asc_get(self, bytes_ptr)?)?; + .bytes_to_base58(asc_get(self, bytes_ptr)?, gas)?; asc_new(self, &result) } /// function bigDecimal.toString(x: BigDecimal): string pub fn big_decimal_to_string( &mut self, + gas: &GasCounter, big_decimal_ptr: AscPtr, ) -> Result, DeterministicHostError> { let result = self .ctx .host_exports - .big_decimal_to_string(try_asc_get(self, big_decimal_ptr)?)?; + .big_decimal_to_string(try_asc_get(self, big_decimal_ptr)?, gas)?; asc_new(self, &result) } /// function bigDecimal.fromString(x: string): BigDecimal pub fn big_decimal_from_string( &mut self, + gas: &GasCounter, string_ptr: AscPtr, ) -> Result, DeterministicHostError> { let result = self .ctx .host_exports - .big_decimal_from_string(asc_get(self, string_ptr)?)?; + .big_decimal_from_string(asc_get(self, string_ptr)?, gas)?; asc_new(self, &result) } /// function bigDecimal.plus(x: BigDecimal, y: BigDecimal): BigDecimal pub fn big_decimal_plus( &mut self, + gas: &GasCounter, x_ptr: AscPtr, y_ptr: AscPtr, ) -> Result, DeterministicHostError> { - let result = self - .ctx - .host_exports - .big_decimal_plus(try_asc_get(self, x_ptr)?, try_asc_get(self, y_ptr)?)?; + let result = self.ctx.host_exports.big_decimal_plus( + try_asc_get(self, x_ptr)?, + try_asc_get(self, y_ptr)?, + gas, + )?; asc_new(self, &result) } /// function bigDecimal.minus(x: BigDecimal, y: BigDecimal): BigDecimal pub fn big_decimal_minus( &mut self, + gas: &GasCounter, x_ptr: AscPtr, y_ptr: AscPtr, ) -> Result, DeterministicHostError> { - let result = self - .ctx - .host_exports - .big_decimal_minus(try_asc_get(self, x_ptr)?, try_asc_get(self, y_ptr)?)?; + let result = self.ctx.host_exports.big_decimal_minus( + try_asc_get(self, x_ptr)?, + try_asc_get(self, y_ptr)?, + gas, + )?; asc_new(self, &result) } /// function bigDecimal.times(x: BigDecimal, y: BigDecimal): BigDecimal pub fn big_decimal_times( &mut self, + gas: &GasCounter, x_ptr: AscPtr, y_ptr: AscPtr, ) -> Result, DeterministicHostError> { - let result = self - .ctx - .host_exports - .big_decimal_times(try_asc_get(self, x_ptr)?, try_asc_get(self, y_ptr)?)?; + let result = self.ctx.host_exports.big_decimal_times( + try_asc_get(self, x_ptr)?, + try_asc_get(self, y_ptr)?, + gas, + )?; asc_new(self, &result) } /// function bigDecimal.dividedBy(x: BigDecimal, y: BigDecimal): BigDecimal pub fn big_decimal_divided_by( &mut self, + gas: &GasCounter, x_ptr: AscPtr, y_ptr: AscPtr, ) -> Result, DeterministicHostError> { - let result = self - .ctx - .host_exports - .big_decimal_divided_by(try_asc_get(self, x_ptr)?, try_asc_get(self, y_ptr)?)?; + let result = self.ctx.host_exports.big_decimal_divided_by( + try_asc_get(self, x_ptr)?, + try_asc_get(self, y_ptr)?, + gas, + )?; asc_new(self, &result) } /// function bigDecimal.equals(x: BigDecimal, y: BigDecimal): bool pub fn big_decimal_equals( &mut self, + gas: &GasCounter, x_ptr: AscPtr, y_ptr: AscPtr, ) -> Result { - self.ctx - .host_exports - .big_decimal_equals(try_asc_get(self, x_ptr)?, try_asc_get(self, y_ptr)?) + self.ctx.host_exports.big_decimal_equals( + try_asc_get(self, x_ptr)?, + try_asc_get(self, y_ptr)?, + gas, + ) } /// function dataSource.create(name: string, params: Array): void pub fn data_source_create( &mut self, + gas: &GasCounter, name_ptr: AscPtr, params_ptr: AscPtr>>, ) -> Result<(), HostExportError> { @@ -1341,12 +1446,14 @@ impl WasmInstanceContext { params, None, self.ctx.block_ptr.number, + gas, ) } /// function createWithContext(name: string, params: Array, context: DataSourceContext): void pub fn data_source_create_with_context( &mut self, + gas: &GasCounter, name_ptr: AscPtr, params_ptr: AscPtr>>, context_ptr: AscPtr, @@ -1361,30 +1468,55 @@ impl WasmInstanceContext { params, Some(context.into()), self.ctx.block_ptr.number, + gas, ) } /// function dataSource.address(): Bytes - pub fn data_source_address(&mut self) -> Result, DeterministicHostError> { - asc_new(self, self.ctx.host_exports.data_source_address().as_slice()) + pub fn data_source_address( + &mut self, + gas: &GasCounter, + ) -> Result, DeterministicHostError> { + asc_new(self, &self.ctx.host_exports.data_source_address(gas)?) } /// function dataSource.network(): String - pub fn data_source_network(&mut self) -> Result, DeterministicHostError> { - asc_new(self, &self.ctx.host_exports.data_source_network()) + pub fn data_source_network( + &mut self, + gas: &GasCounter, + ) -> Result, DeterministicHostError> { + asc_new(self, &self.ctx.host_exports.data_source_network(gas)?) } /// function dataSource.context(): DataSourceContext - pub fn data_source_context(&mut self) -> Result, DeterministicHostError> { - asc_new(self, &self.ctx.host_exports.data_source_context().sorted()) + pub fn data_source_context( + &mut self, + gas: &GasCounter, + ) -> Result, DeterministicHostError> { + asc_new( + self, + &self.ctx.host_exports.data_source_context(gas)?.sorted(), + ) } pub fn ens_name_by_hash( &mut self, + gas: &GasCounter, hash_ptr: AscPtr, ) -> Result, HostExportError> { + // Not enabled on the network, no gas consumed. + drop(gas); + + // This is unrelated to IPFS, but piggyback on the config to disallow it on the network. + if !self.experimental_features.allow_non_deterministic_ipfs { + return Err(HostExportError::Deterministic(anyhow!( + "`ipfs.map` is deprecated. Improved support for IPFS will be added in the future" + ))); + } + let hash: String = asc_get(self, hash_ptr)?; let name = self.ctx.host_exports.ens_name_by_hash(&*hash)?; + // map `None` to `null`, and `Some(s)` to a runtime string name.map(|name| asc_new(self, &*name).map_err(Into::into)) .unwrap_or(Ok(AscPtr::null())) @@ -1392,20 +1524,28 @@ impl WasmInstanceContext { pub fn log_log( &mut self, + gas: &GasCounter, level: u32, msg: AscPtr, ) -> Result<(), DeterministicHostError> { let level = LogLevel::from(level).into(); let msg: String = asc_get(self, msg)?; - self.ctx.host_exports.log_log(&self.ctx.logger, level, msg) + self.ctx + .host_exports + .log_log(&self.ctx.logger, level, msg, gas) } /// function encode(token: ethereum.Value): Bytes | null pub fn ethereum_encode( &mut self, + gas: &GasCounter, token_ptr: AscPtr>, ) -> Result, DeterministicHostError> { - let data = host_exports::ethereum_encode(asc_get(self, token_ptr)?); + let data = self + .ctx + .host_exports + .ethereum_encode(asc_get(self, token_ptr)?, gas); + // return `null` if it fails data.map(|bytes| asc_new(self, &*bytes)) .unwrap_or(Ok(AscPtr::null())) @@ -1414,11 +1554,16 @@ impl WasmInstanceContext { /// function decode(types: String, data: Bytes): ethereum.Value | null pub fn ethereum_decode( &mut self, + gas: &GasCounter, types_ptr: AscPtr, data_ptr: AscPtr, ) -> Result>, DeterministicHostError> { - let result = - host_exports::ethereum_decode(asc_get(self, types_ptr)?, asc_get(self, data_ptr)?); + let result = self.ctx.host_exports.ethereum_decode( + asc_get(self, types_ptr)?, + asc_get(self, data_ptr)?, + gas, + ); + // return `null` if it fails result .map(|param| asc_new(self, ¶m)) @@ -1428,6 +1573,7 @@ impl WasmInstanceContext { /// function arweave.transactionData(txId: string): Bytes | null pub fn arweave_transaction_data( &mut self, + gas: &GasCounter, _tx_id: AscPtr, ) -> Result, HostExportError> { Err(HostExportError::Deterministic(anyhow!( @@ -1438,6 +1584,7 @@ impl WasmInstanceContext { /// function box.profile(address: string): JSONValue | null pub fn box_profile( &mut self, + gas: &GasCounter, _address: AscPtr, ) -> Result, HostExportError> { Err(HostExportError::Deterministic(anyhow!( From 115a085026e94a710cacd399adf70216f9d783db Mon Sep 17 00:00:00 2001 From: Leonardo Yvens Date: Mon, 26 Apr 2021 15:14:38 -0300 Subject: [PATCH 03/22] gas: Fix Gas::add_assign, adjust size_of for BigInt/Decimal --- runtime/wasm/src/gas/combinators.rs | 2 ++ runtime/wasm/src/gas/ops.rs | 2 +- runtime/wasm/src/gas/size_of.rs | 13 ++++++------- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/runtime/wasm/src/gas/combinators.rs b/runtime/wasm/src/gas/combinators.rs index 814efa25417..a6bc37954db 100644 --- a/runtime/wasm/src/gas/combinators.rs +++ b/runtime/wasm/src/gas/combinators.rs @@ -17,9 +17,11 @@ pub mod complexity { // There is only one arg and it scales linearly with it's size pub struct Size; + // Complexity is captured by the lesser complexity of the two args // Eg: O(min(N₁, N₂)) pub struct Min; + // Complexity is captured by the greater complexity of the two args // Eg: O(max(N₁, N₂)) pub struct Max; diff --git a/runtime/wasm/src/gas/ops.rs b/runtime/wasm/src/gas/ops.rs index 25eb6aa7783..a7e59877b61 100644 --- a/runtime/wasm/src/gas/ops.rs +++ b/runtime/wasm/src/gas/ops.rs @@ -39,7 +39,7 @@ impl MulAssign for Gas { impl AddAssign for Gas { #[inline] fn add_assign(&mut self, rhs: Gas) { - self.0 = self.0.saturating_mul(rhs.0); + self.0 = self.0.saturating_add(rhs.0); } } diff --git a/runtime/wasm/src/gas/size_of.rs b/runtime/wasm/src/gas/size_of.rs index 18b4983a32f..da73040dc17 100644 --- a/runtime/wasm/src/gas/size_of.rs +++ b/runtime/wasm/src/gas/size_of.rs @@ -53,8 +53,10 @@ where impl GasSizeOf for BigInt { fn gas_size_of(&self) -> Gas { - let gas: Gas = (self.bits() / 8).saturating_into(); - gas + Gas(100) + // Add one to always have an upper bound on the number of bytes required to represent the + // number, and so that `0` has a size of 1. + let n_bytes = self.bits() / 8 + 1; + n_bytes.saturating_into() } } @@ -66,11 +68,8 @@ impl GasSizeOf for Entity { impl GasSizeOf for BigDecimal { fn gas_size_of(&self) -> Gas { - // This can be overly pessimistic - let gas: Gas = ((self.digits() as f64 * std::f64::consts::LOG2_10) / 8.0) - .ceil() - .saturating_into(); - gas + Gas(1000) + let (int, _) = self.as_bigint_and_exponent(); + BigInt::from(int).gas_size_of() } } From 06a847fb17bd3b43877b9e7b137b599175c30b70 Mon Sep 17 00:00:00 2001 From: Leonardo Yvens Date: Tue, 27 Apr 2021 13:50:41 -0300 Subject: [PATCH 04/22] gas: Adjust costs, log gas used --- runtime/wasm/src/gas/costs.rs | 35 ++++++++++++++++++++------------ runtime/wasm/src/gas/mod.rs | 18 ++++++++++++---- runtime/wasm/src/host.rs | 7 ++++++- runtime/wasm/src/host_exports.rs | 30 +++++++++++++-------------- runtime/wasm/src/mapping.rs | 4 ++-- runtime/wasm/src/module/mod.rs | 21 +++++++++++-------- 6 files changed, 72 insertions(+), 43 deletions(-) diff --git a/runtime/wasm/src/gas/costs.rs b/runtime/wasm/src/gas/costs.rs index 17114337aa0..f7392e9242c 100644 --- a/runtime/wasm/src/gas/costs.rs +++ b/runtime/wasm/src/gas/costs.rs @@ -13,14 +13,10 @@ const GAS_PER_SECOND: u64 = 10_000_000_000; /// still charge very high numbers for other things. pub const MAX_GAS_PER_HANDLER: u64 = 3600 * GAS_PER_SECOND; -/// Base gas cost for calling any host export. -/// Security: This must be non-zero. -const HOST_EXPORT: u64 = 100_000; - /// Gas for instructions are aggregated into blocks, so hopefully gas calls each have relatively large gas. /// But in the case they don't, we don't want the overhead of calling out into a host export to be /// the dominant cost that causes unexpectedly high execution times. -pub const HOST_EXPORT_GAS: Gas = Gas(HOST_EXPORT); +pub const HOST_EXPORT_GAS: Gas = Gas(10_000); /// As a heuristic for the cost of host fns it makes sense to reason in terms of bandwidth and /// calculate the cost from there. Because we don't have benchmarks for each host fn, we go with @@ -31,6 +27,8 @@ const DEFAULT_BYTE_PER_SECOND: u64 = 10_000_000; /// With the current parameters DEFAULT_GAS_PER_BYTE = 1_000. const DEFAULT_GAS_PER_BYTE: u64 = GAS_PER_SECOND / DEFAULT_BYTE_PER_SECOND; +/// Base gas cost for calling any host export. +/// Security: This must be non-zero. pub(crate) const DEFAULT_BASE_COST: u64 = 100_000; pub(crate) const DEFAULT_GAS_OP: GasOp = GasOp { @@ -38,6 +36,16 @@ pub(crate) const DEFAULT_GAS_OP: GasOp = GasOp { size_mult: DEFAULT_GAS_PER_BYTE, }; +/// Because big math has a multiplicative complexity, that can result in high sizes, so assume a +/// bandwidth of 100 MB/s, faster than the default. +const BIG_MATH_BYTE_PER_SECOND: u64 = 100_000_000; +const BIG_MATH_GAS_PER_BYTE: u64 = GAS_PER_SECOND / BIG_MATH_BYTE_PER_SECOND; + +pub(crate) const BIG_MATH_GAS_OP: GasOp = GasOp { + base_cost: DEFAULT_BASE_COST, + size_mult: BIG_MATH_GAS_PER_BYTE, +}; + // Allow up to 25,000 ethereum calls pub const ETHEREUM_CALL: Gas = Gas(MAX_GAS_PER_HANDLER / 25_000); @@ -71,6 +79,7 @@ impl Rules for GasRules { let weight = match instruction { // These are taken from this post: https://github.com/paritytech/substrate/pull/7361#issue-506217103 // from the table under the "Schedule" dropdown. Each decimal is multiplied by 10. + // Note that those were calculated for wasi, not wasmtime, so they are likely very conservative. I64Const(_) => 16, I64Load(_, _) => 1573, I64Store(_, _) => 2263, @@ -78,7 +87,7 @@ impl Rules for GasRules { Instruction::If(_) => 79, Br(_) => 30, BrIf(_) => 63, - BrTable(data) => 146 + (1030000 * (1 + data.table.len() as u32)), + BrTable(data) => 146 + data.table.len() as u32, Call(_) => 951, // TODO: To figure out the param cost we need to look up the function CallIndirect(_, _) => 1995, @@ -190,15 +199,15 @@ impl Rules for GasRules { | F32Max | F32Min | F32Mul | F32Sub | F32Add | F32Nearest | F32Trunc | F32Floor | F32Ceil | F32Neg | F32Abs | F32Eq | F32Ne | F32Lt | F32Gt | F32Le | F32Ge | F64Eq | F64Ne | F64Lt | F64Gt | F64Le | F64Ge | I32ReinterpretF32 | I64ReinterpretF64 => 100, - F64Div | F64Sqrt | F32Div | F32Sqrt => 1000, + F64Div | F64Sqrt | F32Div | F32Sqrt => 100, // More invented weights - Block(_) => 1000, - Loop(_) => 1000, - Else => 1000, - End => 1000, - Return => 1000, - Drop => 1000, + Block(_) => 100, + Loop(_) => 100, + Else => 100, + End => 100, + Return => 100, + Drop => 100, Nop => 1, Unreachable => 1, }; diff --git a/runtime/wasm/src/gas/mod.rs b/runtime/wasm/src/gas/mod.rs index ed7ea7ebcb3..b57f938d41a 100644 --- a/runtime/wasm/src/gas/mod.rs +++ b/runtime/wasm/src/gas/mod.rs @@ -11,9 +11,9 @@ pub use saturating::*; use parity_wasm::elements::Instruction; use pwasm_utils::rules::{MemoryGrowCost, Rules}; -use std::num::NonZeroU32; use std::sync::atomic::{AtomicU64, Ordering::SeqCst}; use std::{convert::TryInto, rc::Rc}; +use std::{fmt, fmt::Display, num::NonZeroU32}; pub struct GasOp { base_cost: u64, @@ -57,7 +57,7 @@ pub trait ConstGasSizeOf { fn gas_size_of() -> Gas; } -/// This struct mostly exists to avoid typing out saturating_mul +/// This wrapper ensures saturating arithmetic is used #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)] pub struct Gas(u64); @@ -71,6 +71,12 @@ impl From for Gas { } } +impl Display for Gas { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + self.0.fmt(f) + } +} + #[derive(Clone)] pub struct GasCounter(Rc); @@ -84,11 +90,11 @@ impl GasCounter { /// This should be called once per host export pub fn consume_host_fn(&self, mut amount: Gas) -> Result<(), DeterministicHostError> { amount += costs::HOST_EXPORT_GAS; - let new = self + let old = self .0 .fetch_update(SeqCst, SeqCst, |v| Some(v.saturating_add(amount.0))) .unwrap(); - + let new = old.saturating_add(amount.0); if new >= MAX_GAS_PER_HANDLER { Err(DeterministicHostError(anyhow::anyhow!( "Gas limit exceeded. Used: {}", @@ -98,4 +104,8 @@ impl GasCounter { Ok(()) } } + + pub fn get(&self) -> Gas { + Gas(self.0.load(SeqCst)) + } } diff --git a/runtime/wasm/src/host.rs b/runtime/wasm/src/host.rs index 13aad2d04d1..449ecb05ad1 100644 --- a/runtime/wasm/src/host.rs +++ b/runtime/wasm/src/host.rs @@ -14,6 +14,7 @@ use graph::prelude::{ RuntimeHost as RuntimeHostTrait, RuntimeHostBuilder as RuntimeHostBuilderTrait, *, }; +use crate::gas::Gas; use crate::mapping::{MappingContext, MappingRequest}; use crate::{host_exports::HostExports, module::ExperimentalFeatures}; @@ -197,15 +198,19 @@ where let elapsed = start_time.elapsed(); metrics.observe_handler_execution_time(elapsed.as_secs_f64(), &handler); + // If there is an error, "gas_used" is incorrectly reported as 0. + let gas_used = result.as_ref().map(|(_, gas)| gas).unwrap_or(&Gas::ZERO); info!( logger, "Done processing trigger"; &extras, "total_ms" => elapsed.as_millis(), "handler" => handler, "data_source" => &self.data_source.name(), + "gas_used" => gas_used.to_string(), ); - result + // Discard the gas value + result.map(|(block_state, _)| block_state) } } diff --git a/runtime/wasm/src/host_exports.rs b/runtime/wasm/src/host_exports.rs index 0a92f26f499..703a2dcd3f9 100644 --- a/runtime/wasm/src/host_exports.rs +++ b/runtime/wasm/src/host_exports.rs @@ -415,7 +415,7 @@ impl HostExports { y: BigInt, gas: &GasCounter, ) -> Result { - gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Max, (&x, &y)))?; + gas.consume_host_fn(gas::BIG_MATH_GAS_OP.with_args(complexity::Max, (&x, &y)))?; Ok(x + y) } @@ -425,7 +425,7 @@ impl HostExports { y: BigInt, gas: &GasCounter, ) -> Result { - gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Max, (&x, &y)))?; + gas.consume_host_fn(gas::BIG_MATH_GAS_OP.with_args(complexity::Max, (&x, &y)))?; Ok(x - y) } @@ -435,7 +435,7 @@ impl HostExports { y: BigInt, gas: &GasCounter, ) -> Result { - gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Mul, (&x, &y)))?; + gas.consume_host_fn(gas::BIG_MATH_GAS_OP.with_args(complexity::Mul, (&x, &y)))?; Ok(x * y) } @@ -445,7 +445,7 @@ impl HostExports { y: BigInt, gas: &GasCounter, ) -> Result { - gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Mul, (&x, &y)))?; + gas.consume_host_fn(gas::BIG_MATH_GAS_OP.with_args(complexity::Mul, (&x, &y)))?; if y == 0.into() { return Err(DeterministicHostError(anyhow!( "attempted to divide BigInt `{}` by zero", @@ -461,7 +461,7 @@ impl HostExports { y: BigInt, gas: &GasCounter, ) -> Result { - gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Mul, (&x, &y)))?; + gas.consume_host_fn(gas::BIG_MATH_GAS_OP.with_args(complexity::Mul, (&x, &y)))?; if y == 0.into() { return Err(DeterministicHostError(anyhow!( "attempted to calculate the remainder of `{}` with a divisor of zero", @@ -478,7 +478,7 @@ impl HostExports { exp: u8, gas: &GasCounter, ) -> Result { - gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Exponential, (&x, exp)))?; + gas.consume_host_fn(gas::BIG_MATH_GAS_OP.with_args(complexity::Exponential, (&x, exp)))?; Ok(x.pow(exp)) } @@ -499,7 +499,7 @@ impl HostExports { y: BigInt, gas: &GasCounter, ) -> Result { - gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Max, (&x, &y)))?; + gas.consume_host_fn(gas::BIG_MATH_GAS_OP.with_args(complexity::Max, (&x, &y)))?; Ok(x | y) } @@ -509,7 +509,7 @@ impl HostExports { y: BigInt, gas: &GasCounter, ) -> Result { - gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Min, (&x, &y)))?; + gas.consume_host_fn(gas::BIG_MATH_GAS_OP.with_args(complexity::Min, (&x, &y)))?; Ok(x & y) } @@ -519,7 +519,7 @@ impl HostExports { bits: u8, gas: &GasCounter, ) -> Result { - gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Linear, (&x, &bits)))?; + gas.consume_host_fn(gas::BIG_MATH_GAS_OP.with_args(complexity::Linear, (&x, &bits)))?; Ok(x << bits) } @@ -529,7 +529,7 @@ impl HostExports { bits: u8, gas: &GasCounter, ) -> Result { - gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Linear, (&x, &bits)))?; + gas.consume_host_fn(gas::BIG_MATH_GAS_OP.with_args(complexity::Linear, (&x, &bits)))?; Ok(x >> bits) } @@ -549,7 +549,7 @@ impl HostExports { y: BigDecimal, gas: &GasCounter, ) -> Result { - gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Linear, (&x, &y)))?; + gas.consume_host_fn(gas::BIG_MATH_GAS_OP.with_args(complexity::Linear, (&x, &y)))?; Ok(x + y) } @@ -559,7 +559,7 @@ impl HostExports { y: BigDecimal, gas: &GasCounter, ) -> Result { - gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Linear, (&x, &y)))?; + gas.consume_host_fn(gas::BIG_MATH_GAS_OP.with_args(complexity::Linear, (&x, &y)))?; Ok(x - y) } @@ -569,7 +569,7 @@ impl HostExports { y: BigDecimal, gas: &GasCounter, ) -> Result { - gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Mul, (&x, &y)))?; + gas.consume_host_fn(gas::BIG_MATH_GAS_OP.with_args(complexity::Mul, (&x, &y)))?; Ok(x * y) } @@ -580,7 +580,7 @@ impl HostExports { y: BigDecimal, gas: &GasCounter, ) -> Result { - gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Mul, (&x, &y)))?; + gas.consume_host_fn(gas::BIG_MATH_GAS_OP.with_args(complexity::Mul, (&x, &y)))?; if y == 0.into() { return Err(DeterministicHostError(anyhow!( "attempted to divide BigDecimal `{}` by zero", @@ -596,7 +596,7 @@ impl HostExports { y: BigDecimal, gas: &GasCounter, ) -> Result { - gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(complexity::Min, (&x, &y)))?; + gas.consume_host_fn(gas::BIG_MATH_GAS_OP.with_args(complexity::Min, (&x, &y)))?; Ok(x == y) } diff --git a/runtime/wasm/src/mapping.rs b/runtime/wasm/src/mapping.rs index cad14d2e39c..e1b26cfda06 100644 --- a/runtime/wasm/src/mapping.rs +++ b/runtime/wasm/src/mapping.rs @@ -1,4 +1,4 @@ -use crate::gas::GasRules; +use crate::gas::{Gas, GasRules}; use crate::module::{ExperimentalFeatures, WasmInstance}; use futures::sync::mpsc; use futures03::channel::oneshot::Sender; @@ -99,7 +99,7 @@ pub fn spawn_module( pub struct MappingRequest { pub(crate) ctx: MappingContext, pub(crate) trigger: TriggerWithHandler, - pub(crate) result_sender: Sender, MappingError>>, + pub(crate) result_sender: Sender, Gas), MappingError>>, } pub struct MappingContext { diff --git a/runtime/wasm/src/module/mod.rs b/runtime/wasm/src/module/mod.rs index c51eb16508e..0f1b115256e 100644 --- a/runtime/wasm/src/module/mod.rs +++ b/runtime/wasm/src/module/mod.rs @@ -13,6 +13,7 @@ use wasmtime::{Memory, Trap}; use crate::error::DeterminismLevel; pub use crate::host_exports; +use crate::gas::{self, Gas, GasCounter, SaturatingInto}; use crate::mapping::MappingContext; use anyhow::Error; use graph::data::store; @@ -51,6 +52,9 @@ pub struct WasmInstance { // Also this is the only strong reference, so the instance will be dropped once this is dropped. // The weak references are circulary held by instance itself through host exports. pub instance_ctx: Rc>>>, + + // A reference to the gas counter used for reporting the gas used. + gas: GasCounter, } impl Drop for WasmInstance { @@ -111,7 +115,7 @@ impl WasmInstance { pub(crate) fn handle_trigger( mut self, trigger: TriggerWithHandler, - ) -> Result, MappingError> { + ) -> Result<(BlockState, Gas), MappingError> { let handler_name = trigger.handler_name().to_owned(); let asc_trigger = trigger.to_asc_ptr(&mut self)?; self.invoke_handler(&handler_name, asc_trigger) @@ -138,7 +142,7 @@ impl WasmInstance { &mut self, handler: &str, arg: AscPtr, - ) -> Result, MappingError> { + ) -> Result<(BlockState, Gas), MappingError> { let func = self .instance .get_func(handler) @@ -209,7 +213,8 @@ impl WasmInstance { self.instance_ctx_mut().ctx.state.exit_handler(); } - Ok(self.take_ctx().ctx.state) + let gas = self.gas.get(); + Ok((self.take_ctx().ctx.state, gas)) } } @@ -526,13 +531,12 @@ impl WasmInstance { // link the `gas` function // See also e3f03e62-40e4-4f8c-b4a1-d0375cca0b76 { - let host_metrics = host_metrics.cheap_clone(); let gas = gas.cheap_clone(); linker.func("gas", "gas", move |gas_used: u32| -> Result<(), Trap> { - // Starting the section is probably more expensive than the gas operation itself, - // but still we need insight into whether this is relevant to indexing performance. - let _section = host_metrics.stopwatch.start_section("host_export_gas"); - + // Gas metering has a relevant execution cost cost, being called tens of thousands + // of times per handler, but it's not worth having a stopwatch section here because + // the cost of measuring would be greater than the cost of `consume_host_fn`. Last + // time this was benchmarked it took < 100ns to run. if let Err(e) = gas.consume_host_fn(gas_used.saturating_into()) { deterministic_host_trap.store(true, Ordering::SeqCst); return Err(e.into_trap()); @@ -571,6 +575,7 @@ impl WasmInstance { Ok(WasmInstance { instance, instance_ctx: shared_ctx, + gas, }) } } From acb70528a13c3d770ba74bd805ac9f40c7f5e6c8 Mon Sep 17 00:00:00 2001 From: Leonardo Yvens Date: Tue, 27 Apr 2021 16:51:51 -0300 Subject: [PATCH 05/22] runtime: Test gas usage --- runtime/test/src/test.rs | 17 +++++++++++++++++ runtime/test/src/test/abi.rs | 1 + runtime/wasm/src/gas/mod.rs | 6 ++++++ runtime/wasm/src/module/mod.rs | 5 +++++ 4 files changed, 29 insertions(+) diff --git a/runtime/test/src/test.rs b/runtime/test/src/test.rs index 2e5bc2667de..b36154affa0 100644 --- a/runtime/test/src/test.rs +++ b/runtime/test/src/test.rs @@ -211,11 +211,15 @@ fn test_json_conversions(api_version: Version) { let number = "-922337203685077092345034"; let number_ptr = asc_new(&mut module, number).unwrap(); let big_int_obj: AscPtr = module.invoke_export1("testToBigInt", number_ptr); + let bytes: Vec = asc_get(&module, big_int_obj).unwrap(); + assert_eq!( scalar::BigInt::from_str(number).unwrap(), scalar::BigInt::from_signed_bytes_le(&bytes) ); + + assert_eq!(module.gas_used(), 184180372); } #[tokio::test] @@ -251,8 +255,11 @@ fn test_json_parsing(api_version: Version) { let bytes: &[u8] = s.as_ref(); let bytes_ptr = asc_new(&mut module, bytes).unwrap(); let return_value: AscPtr = module.invoke_export1("handleJsonError", bytes_ptr); + let output: String = asc_get(&module, return_value).unwrap(); assert_eq!(output, "OK: foo, ERROR: false"); + + assert_eq!(module.gas_used(), 1236877); } #[tokio::test] @@ -575,6 +582,8 @@ fn test_big_int_to_hex(api_version: Version) { u256_max_hex_str, "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" ); + + assert_eq!(module.gas_used(), 184013403); } #[tokio::test] @@ -650,6 +659,8 @@ fn test_big_int_arithmetic(api_version: Version) { let result_ptr: AscPtr = module.invoke_export2("mod", five, two); let result: BigInt = asc_get(&module, result_ptr).unwrap(); assert_eq!(result, BigInt::from(1)); + + assert_eq!(module.gas_used(), 184342018); } #[tokio::test] @@ -705,7 +716,9 @@ fn test_bytes_to_base58(api_version: Version) { let bytes_ptr = asc_new(&mut module, bytes.as_slice()).unwrap(); let result_ptr: AscPtr = module.invoke_export1("bytes_to_base58", bytes_ptr); let base58: String = asc_get(&module, result_ptr).unwrap(); + assert_eq!(base58, "QmWmyoMoctfbAaiEs2G46gpeUmhqFRDW6KWo64y5r581Vz"); + assert_eq!(module.gas_used(), 183820465); } #[tokio::test] @@ -737,6 +750,9 @@ fn test_data_source_create(api_version: Version) { module.instance_ctx_mut().ctx.state.enter_handler(); module.invoke_export2_void("dataSourceCreate", name, params)?; module.instance_ctx_mut().ctx.state.exit_handler(); + + assert_eq!(module.gas_used(), 543636483); + Ok(module.take_ctx().ctx.state.drain_created_data_sources()) }; @@ -929,6 +945,7 @@ fn test_detect_contract_calls(api_version: Version) { data_source_with_calls.mapping.requires_archive().unwrap(), true ); + assert_eq!(module.gas_used(), 312058391); } #[tokio::test] diff --git a/runtime/test/src/test/abi.rs b/runtime/test/src/test/abi.rs index 0333603c89b..768fa6e36d2 100644 --- a/runtime/test/src/test/abi.rs +++ b/runtime/test/src/test/abi.rs @@ -81,6 +81,7 @@ fn test_abi_array(api_version: Version) { module.invoke_export1("test_array", vec_obj); let new_vec: Vec = asc_get(&module, new_vec_obj).unwrap(); + assert_eq!(module.gas_used(), 200657); assert_eq!( new_vec, vec![ diff --git a/runtime/wasm/src/gas/mod.rs b/runtime/wasm/src/gas/mod.rs index b57f938d41a..10d7ca1524a 100644 --- a/runtime/wasm/src/gas/mod.rs +++ b/runtime/wasm/src/gas/mod.rs @@ -71,6 +71,12 @@ impl From for Gas { } } +impl From for u64 { + fn from(x: Gas) -> Self { + x.0 + } +} + impl Display for Gas { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { self.0.fmt(f) diff --git a/runtime/wasm/src/module/mod.rs b/runtime/wasm/src/module/mod.rs index 0f1b115256e..9002b79149f 100644 --- a/runtime/wasm/src/module/mod.rs +++ b/runtime/wasm/src/module/mod.rs @@ -138,6 +138,11 @@ impl WasmInstance { self.instance.get_func(func_name).unwrap() } + #[cfg(test)] + pub(crate) fn gas_used(&self) -> u64 { + self.gas.get().into() + } + fn invoke_handler( &mut self, handler: &str, From 8cb41d96b9c2ff4dc8fac8062e0863d61e45e163 Mon Sep 17 00:00:00 2001 From: Leonardo Yvens Date: Tue, 27 Apr 2021 17:57:03 -0300 Subject: [PATCH 06/22] gas: Justify the value of HOST_EXPORT_GAS --- runtime/wasm/src/gas/costs.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/runtime/wasm/src/gas/costs.rs b/runtime/wasm/src/gas/costs.rs index f7392e9242c..74cfd0acafc 100644 --- a/runtime/wasm/src/gas/costs.rs +++ b/runtime/wasm/src/gas/costs.rs @@ -13,9 +13,13 @@ const GAS_PER_SECOND: u64 = 10_000_000_000; /// still charge very high numbers for other things. pub const MAX_GAS_PER_HANDLER: u64 = 3600 * GAS_PER_SECOND; -/// Gas for instructions are aggregated into blocks, so hopefully gas calls each have relatively large gas. -/// But in the case they don't, we don't want the overhead of calling out into a host export to be -/// the dominant cost that causes unexpectedly high execution times. +/// Gas for instructions are aggregated into blocks, so hopefully gas calls each have relatively +/// large gas. But in the case they don't, we don't want the overhead of calling out into a host +/// export to be the dominant cost that causes unexpectedly high execution times. +/// +/// This value is based on the benchmark of an empty infinite loop, which does basically nothing +/// other than call the gas function. The benchmark result was closer to 5000 gas but use 10_000 to +/// be conservative. pub const HOST_EXPORT_GAS: Gas = Gas(10_000); /// As a heuristic for the cost of host fns it makes sense to reason in terms of bandwidth and From f31241520ee75581bfb1d3a58e840a84cde0f9a4 Mon Sep 17 00:00:00 2001 From: Leonardo Yvens Date: Wed, 28 Apr 2021 18:07:07 -0300 Subject: [PATCH 07/22] gas: Adjust cost of Ethereum calls --- runtime/wasm/src/gas/costs.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/runtime/wasm/src/gas/costs.rs b/runtime/wasm/src/gas/costs.rs index 74cfd0acafc..35c820a0342 100644 --- a/runtime/wasm/src/gas/costs.rs +++ b/runtime/wasm/src/gas/costs.rs @@ -50,8 +50,14 @@ pub(crate) const BIG_MATH_GAS_OP: GasOp = GasOp { size_mult: BIG_MATH_GAS_PER_BYTE, }; -// Allow up to 25,000 ethereum calls -pub const ETHEREUM_CALL: Gas = Gas(MAX_GAS_PER_HANDLER / 25_000); +// Allow up to 1,000 ethereum calls. The justification is that we don't know how much Ethereum gas a +// call takes, but we limit the maximum to 25 million. One unit of Ethereum gas is at least 100ns +// according to these benchmarks [1], so 1000 of our gas. Assuming the worst case, an Ethereum call +// should therefore consume 25 billion gas. This allows for 1440 calls per handler with the current +// limits. +// +// [1] - https://www.sciencedirect.com/science/article/abs/pii/S0166531620300900 +pub const ETHEREUM_CALL: Gas = Gas(25_000_000_000); // Allow up to 100,000 data sources to be created pub const CREATE_DATA_SOURCE: Gas = Gas(MAX_GAS_PER_HANDLER / 100_000); From c2e2bb497999245903bfe8d5a01efde94c2f2d62 Mon Sep 17 00:00:00 2001 From: Leonardo Yvens Date: Wed, 28 Apr 2021 18:07:32 -0300 Subject: [PATCH 08/22] gas: Remove dead trait ConstGasSizeOf --- runtime/wasm/src/gas/mod.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/runtime/wasm/src/gas/mod.rs b/runtime/wasm/src/gas/mod.rs index 10d7ca1524a..1ce2801ca78 100644 --- a/runtime/wasm/src/gas/mod.rs +++ b/runtime/wasm/src/gas/mod.rs @@ -53,10 +53,6 @@ pub trait GasSizeOf { } } -pub trait ConstGasSizeOf { - fn gas_size_of() -> Gas; -} - /// This wrapper ensures saturating arithmetic is used #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)] pub struct Gas(u64); From d9c5060fcbe0e73a1f3b84a0b6122eca7e02997b Mon Sep 17 00:00:00 2001 From: Leonardo Yvens Date: Wed, 28 Apr 2021 18:24:50 -0300 Subject: [PATCH 09/22] gas: Move gas module from runtime to graph crate This will probably be necessary for multiblockchain. --- .../src/runtime}/gas/combinators.rs | 0 graph/src/runtime/gas/costs.rs | 82 ++++++++++++++++++ .../wasm/src => graph/src/runtime}/gas/mod.rs | 11 ++- .../wasm/src => graph/src/runtime}/gas/ops.rs | 0 .../src/runtime}/gas/saturating.rs | 0 .../src => graph/src/runtime}/gas/size_of.rs | 2 +- graph/src/runtime/mod.rs | 2 + .../wasm/src/{gas/costs.rs => gas_rules.rs} | 85 +------------------ runtime/wasm/src/host.rs | 2 +- runtime/wasm/src/host_exports.rs | 4 +- runtime/wasm/src/lib.rs | 2 +- runtime/wasm/src/mapping.rs | 3 +- runtime/wasm/src/module/mod.rs | 1 + 13 files changed, 101 insertions(+), 93 deletions(-) rename {runtime/wasm/src => graph/src/runtime}/gas/combinators.rs (100%) create mode 100644 graph/src/runtime/gas/costs.rs rename {runtime/wasm/src => graph/src/runtime}/gas/mod.rs (92%) rename {runtime/wasm/src => graph/src/runtime}/gas/ops.rs (100%) rename {runtime/wasm/src => graph/src/runtime}/gas/saturating.rs (100%) rename {runtime/wasm/src => graph/src/runtime}/gas/size_of.rs (99%) rename runtime/wasm/src/{gas/costs.rs => gas_rules.rs} (59%) diff --git a/runtime/wasm/src/gas/combinators.rs b/graph/src/runtime/gas/combinators.rs similarity index 100% rename from runtime/wasm/src/gas/combinators.rs rename to graph/src/runtime/gas/combinators.rs diff --git a/graph/src/runtime/gas/costs.rs b/graph/src/runtime/gas/costs.rs new file mode 100644 index 00000000000..bc62d1d7d33 --- /dev/null +++ b/graph/src/runtime/gas/costs.rs @@ -0,0 +1,82 @@ +//! Stores all the gas costs is one place so they can be compared easily. +//! Determinism: Once deployed, none of these values can be changed without a version upgrade. + +use super::*; + +/// Using 10 gas = ~1ns for WASM instructions. +const GAS_PER_SECOND: u64 = 10_000_000_000; + +/// Set max gas to 1 hour worth of gas per handler. The intent here is to have the determinism +/// cutoff be very high, while still allowing more reasonable timer based cutoffs. Having a unit +/// like 10 gas for ~1ns allows us to be granular in instructions which are aggregated into metered +/// blocks via https://docs.rs/pwasm-utils/0.16.0/pwasm_utils/fn.inject_gas_counter.html But we can +/// still charge very high numbers for other things. +pub const MAX_GAS_PER_HANDLER: u64 = 3600 * GAS_PER_SECOND; + +/// Gas for instructions are aggregated into blocks, so hopefully gas calls each have relatively +/// large gas. But in the case they don't, we don't want the overhead of calling out into a host +/// export to be the dominant cost that causes unexpectedly high execution times. +/// +/// This value is based on the benchmark of an empty infinite loop, which does basically nothing +/// other than call the gas function. The benchmark result was closer to 5000 gas but use 10_000 to +/// be conservative. +pub const HOST_EXPORT_GAS: Gas = Gas(10_000); + +/// As a heuristic for the cost of host fns it makes sense to reason in terms of bandwidth and +/// calculate the cost from there. Because we don't have benchmarks for each host fn, we go with +/// pessimistic assumption of performance of 10 MB/s, which nonetheless allows for 36 GB to be +/// processed through host exports by a single handler at a 1 hour budget. +const DEFAULT_BYTE_PER_SECOND: u64 = 10_000_000; + +/// With the current parameters DEFAULT_GAS_PER_BYTE = 1_000. +const DEFAULT_GAS_PER_BYTE: u64 = GAS_PER_SECOND / DEFAULT_BYTE_PER_SECOND; + +/// Base gas cost for calling any host export. +/// Security: This must be non-zero. +pub const DEFAULT_BASE_COST: u64 = 100_000; + +pub const DEFAULT_GAS_OP: GasOp = GasOp { + base_cost: DEFAULT_BASE_COST, + size_mult: DEFAULT_GAS_PER_BYTE, +}; + +/// Because big math has a multiplicative complexity, that can result in high sizes, so assume a +/// bandwidth of 100 MB/s, faster than the default. +const BIG_MATH_BYTE_PER_SECOND: u64 = 100_000_000; +const BIG_MATH_GAS_PER_BYTE: u64 = GAS_PER_SECOND / BIG_MATH_BYTE_PER_SECOND; + +pub const BIG_MATH_GAS_OP: GasOp = GasOp { + base_cost: DEFAULT_BASE_COST, + size_mult: BIG_MATH_GAS_PER_BYTE, +}; + +// Allow up to 1,000 ethereum calls. The justification is that we don't know how much Ethereum gas a +// call takes, but we limit the maximum to 25 million. One unit of Ethereum gas is at least 100ns +// according to these benchmarks [1], so 1000 of our gas. Assuming the worst case, an Ethereum call +// should therefore consume 25 billion gas. This allows for 1440 calls per handler with the current +// limits. +// +// [1] - https://www.sciencedirect.com/science/article/abs/pii/S0166531620300900 +pub const ETHEREUM_CALL: Gas = Gas(25_000_000_000); + +// Allow up to 100,000 data sources to be created +pub const CREATE_DATA_SOURCE: Gas = Gas(MAX_GAS_PER_HANDLER / 100_000); + +// Allow up to 100,000 logs +pub const LOG: Gas = Gas(MAX_GAS_PER_HANDLER / 100_000); + +// Saving to the store is one of the most expensive operations. +pub const STORE_SET: GasOp = GasOp { + // Allow up to 250k entities saved. + base_cost: MAX_GAS_PER_HANDLER / 250_000, + // If the size roughly corresponds to bytes, allow 1GB to be saved. + size_mult: MAX_GAS_PER_HANDLER / 1_000_000_000, +}; + +// Reading from the store is much cheaper than writing. +pub const STORE_GET: GasOp = GasOp { + base_cost: MAX_GAS_PER_HANDLER / 10_000_000, + size_mult: MAX_GAS_PER_HANDLER / 10_000_000_000, +}; + +pub const STORE_REMOVE: GasOp = STORE_SET; diff --git a/runtime/wasm/src/gas/mod.rs b/graph/src/runtime/gas/mod.rs similarity index 92% rename from runtime/wasm/src/gas/mod.rs rename to graph/src/runtime/gas/mod.rs index 1ce2801ca78..0787ba84bd6 100644 --- a/runtime/wasm/src/gas/mod.rs +++ b/graph/src/runtime/gas/mod.rs @@ -3,17 +3,16 @@ mod costs; mod ops; mod saturating; mod size_of; +use crate::prelude::CheapClone; +use crate::runtime::DeterministicHostError; pub use combinators::*; +pub use costs::DEFAULT_BASE_COST; pub use costs::*; -use graph::prelude::CheapClone; -use graph::runtime::DeterministicHostError; pub use saturating::*; -use parity_wasm::elements::Instruction; -use pwasm_utils::rules::{MemoryGrowCost, Rules}; +use std::rc::Rc; use std::sync::atomic::{AtomicU64, Ordering::SeqCst}; -use std::{convert::TryInto, rc::Rc}; -use std::{fmt, fmt::Display, num::NonZeroU32}; +use std::{fmt, fmt::Display}; pub struct GasOp { base_cost: u64, diff --git a/runtime/wasm/src/gas/ops.rs b/graph/src/runtime/gas/ops.rs similarity index 100% rename from runtime/wasm/src/gas/ops.rs rename to graph/src/runtime/gas/ops.rs diff --git a/runtime/wasm/src/gas/saturating.rs b/graph/src/runtime/gas/saturating.rs similarity index 100% rename from runtime/wasm/src/gas/saturating.rs rename to graph/src/runtime/gas/saturating.rs diff --git a/runtime/wasm/src/gas/size_of.rs b/graph/src/runtime/gas/size_of.rs similarity index 99% rename from runtime/wasm/src/gas/size_of.rs rename to graph/src/runtime/gas/size_of.rs index da73040dc17..7a1566b8cc3 100644 --- a/runtime/wasm/src/gas/size_of.rs +++ b/graph/src/runtime/gas/size_of.rs @@ -1,6 +1,6 @@ //! Various implementations of GasSizeOf; -use graph::{ +use crate::{ components::store::EntityType, data::store::{scalar::Bytes, Value}, prelude::{BigDecimal, BigInt, Entity, EntityKey}, diff --git a/graph/src/runtime/mod.rs b/graph/src/runtime/mod.rs index 71100c81c0d..c6231edae0a 100644 --- a/graph/src/runtime/mod.rs +++ b/graph/src/runtime/mod.rs @@ -3,6 +3,8 @@ //! implementation. These methods take types that implement `To`/`FromAscObj` and are therefore //! convertible to/from an `AscType`. +pub mod gas; + mod asc_heap; mod asc_ptr; diff --git a/runtime/wasm/src/gas/costs.rs b/runtime/wasm/src/gas_rules.rs similarity index 59% rename from runtime/wasm/src/gas/costs.rs rename to runtime/wasm/src/gas_rules.rs index 35c820a0342..e18b4317105 100644 --- a/runtime/wasm/src/gas/costs.rs +++ b/runtime/wasm/src/gas_rules.rs @@ -1,85 +1,8 @@ -//! Stores all the gas costs is one place so they can be compared easily. -//! Determinism: Once deployed, none of these values can be changed without a version upgrade. +use std::{convert::TryInto, num::NonZeroU32}; -use super::*; - -/// Using 10 gas = ~1ns for WASM instructions. -const GAS_PER_SECOND: u64 = 10_000_000_000; - -/// Set max gas to 1 hour worth of gas per handler. The intent here is to have the determinism -/// cutoff be very high, while still allowing more reasonable timer based cutoffs. Having a unit -/// like 10 gas for ~1ns allows us to be granular in instructions which are aggregated into metered -/// blocks via https://docs.rs/pwasm-utils/0.16.0/pwasm_utils/fn.inject_gas_counter.html But we can -/// still charge very high numbers for other things. -pub const MAX_GAS_PER_HANDLER: u64 = 3600 * GAS_PER_SECOND; - -/// Gas for instructions are aggregated into blocks, so hopefully gas calls each have relatively -/// large gas. But in the case they don't, we don't want the overhead of calling out into a host -/// export to be the dominant cost that causes unexpectedly high execution times. -/// -/// This value is based on the benchmark of an empty infinite loop, which does basically nothing -/// other than call the gas function. The benchmark result was closer to 5000 gas but use 10_000 to -/// be conservative. -pub const HOST_EXPORT_GAS: Gas = Gas(10_000); - -/// As a heuristic for the cost of host fns it makes sense to reason in terms of bandwidth and -/// calculate the cost from there. Because we don't have benchmarks for each host fn, we go with -/// pessimistic assumption of performance of 10 MB/s, which nonetheless allows for 36 GB to be -/// processed through host exports by a single handler at a 1 hour budget. -const DEFAULT_BYTE_PER_SECOND: u64 = 10_000_000; - -/// With the current parameters DEFAULT_GAS_PER_BYTE = 1_000. -const DEFAULT_GAS_PER_BYTE: u64 = GAS_PER_SECOND / DEFAULT_BYTE_PER_SECOND; - -/// Base gas cost for calling any host export. -/// Security: This must be non-zero. -pub(crate) const DEFAULT_BASE_COST: u64 = 100_000; - -pub(crate) const DEFAULT_GAS_OP: GasOp = GasOp { - base_cost: DEFAULT_BASE_COST, - size_mult: DEFAULT_GAS_PER_BYTE, -}; - -/// Because big math has a multiplicative complexity, that can result in high sizes, so assume a -/// bandwidth of 100 MB/s, faster than the default. -const BIG_MATH_BYTE_PER_SECOND: u64 = 100_000_000; -const BIG_MATH_GAS_PER_BYTE: u64 = GAS_PER_SECOND / BIG_MATH_BYTE_PER_SECOND; - -pub(crate) const BIG_MATH_GAS_OP: GasOp = GasOp { - base_cost: DEFAULT_BASE_COST, - size_mult: BIG_MATH_GAS_PER_BYTE, -}; - -// Allow up to 1,000 ethereum calls. The justification is that we don't know how much Ethereum gas a -// call takes, but we limit the maximum to 25 million. One unit of Ethereum gas is at least 100ns -// according to these benchmarks [1], so 1000 of our gas. Assuming the worst case, an Ethereum call -// should therefore consume 25 billion gas. This allows for 1440 calls per handler with the current -// limits. -// -// [1] - https://www.sciencedirect.com/science/article/abs/pii/S0166531620300900 -pub const ETHEREUM_CALL: Gas = Gas(25_000_000_000); - -// Allow up to 100,000 data sources to be created -pub const CREATE_DATA_SOURCE: Gas = Gas(MAX_GAS_PER_HANDLER / 100_000); - -// Allow up to 100,000 logs -pub const LOG: Gas = Gas(MAX_GAS_PER_HANDLER / 100_000); - -// Saving to the store is one of the most expensive operations. -pub const STORE_SET: GasOp = GasOp { - // Allow up to 250k entities saved. - base_cost: MAX_GAS_PER_HANDLER / 250_000, - // If the size roughly corresponds to bytes, allow 1GB to be saved. - size_mult: MAX_GAS_PER_HANDLER / 1_000_000_000, -}; - -// Reading from the store is much cheaper than writing. -pub const STORE_GET: GasOp = GasOp { - base_cost: MAX_GAS_PER_HANDLER / 10_000_000, - size_mult: MAX_GAS_PER_HANDLER / 10_000_000_000, -}; - -pub const STORE_REMOVE: GasOp = STORE_SET; +use graph::runtime::gas::MAX_GAS_PER_HANDLER; +use parity_wasm::elements::Instruction; +use pwasm_utils::rules::{MemoryGrowCost, Rules}; pub struct GasRules; diff --git a/runtime/wasm/src/host.rs b/runtime/wasm/src/host.rs index 449ecb05ad1..0405af8156b 100644 --- a/runtime/wasm/src/host.rs +++ b/runtime/wasm/src/host.rs @@ -14,9 +14,9 @@ use graph::prelude::{ RuntimeHost as RuntimeHostTrait, RuntimeHostBuilder as RuntimeHostBuilderTrait, *, }; -use crate::gas::Gas; use crate::mapping::{MappingContext, MappingRequest}; use crate::{host_exports::HostExports, module::ExperimentalFeatures}; +use graph::runtime::gas::Gas; lazy_static! { static ref TIMEOUT: Option = std::env::var("GRAPH_MAPPING_HANDLER_TIMEOUT") diff --git a/runtime/wasm/src/host_exports.rs b/runtime/wasm/src/host_exports.rs index 703a2dcd3f9..019504cc260 100644 --- a/runtime/wasm/src/host_exports.rs +++ b/runtime/wasm/src/host_exports.rs @@ -1,4 +1,3 @@ -use crate::gas::{self, complexity, Gas, GasCounter}; use crate::{error::DeterminismLevel, module::IntoTrap}; use ethabi::param_type::Reader; use ethabi::{decode, encode, Token}; @@ -11,7 +10,8 @@ use graph::data::store; use graph::prelude::serde_json; use graph::prelude::{slog::b, slog::record_static, *}; pub use graph::runtime::{DeterministicHostError, HostExportError}; -use never::Never; +use graph::runtime::gas::{self, complexity, Gas, GasCounter}; +use graph::{blockchain::DataSource, bytes::Bytes}; use semver::Version; use std::collections::HashMap; use std::ops::Deref; diff --git a/runtime/wasm/src/lib.rs b/runtime/wasm/src/lib.rs index 2a339eb82eb..0466a9add4a 100644 --- a/runtime/wasm/src/lib.rs +++ b/runtime/wasm/src/lib.rs @@ -16,7 +16,7 @@ pub mod host_exports; pub mod error; -mod gas; +mod gas_rules; pub use host::RuntimeHostBuilder; pub use host_exports::HostExports; diff --git a/runtime/wasm/src/mapping.rs b/runtime/wasm/src/mapping.rs index e1b26cfda06..b279a39d12e 100644 --- a/runtime/wasm/src/mapping.rs +++ b/runtime/wasm/src/mapping.rs @@ -1,10 +1,11 @@ -use crate::gas::{Gas, GasRules}; +use crate::gas_rules::GasRules; use crate::module::{ExperimentalFeatures, WasmInstance}; use futures::sync::mpsc; use futures03::channel::oneshot::Sender; use graph::blockchain::{Blockchain, HostFn, TriggerWithHandler}; use graph::components::subgraph::{MappingError, SharedProofOfIndexing}; use graph::prelude::*; +use graph::runtime::gas::Gas; use std::collections::BTreeMap; use std::sync::Arc; use std::thread; diff --git a/runtime/wasm/src/module/mod.rs b/runtime/wasm/src/module/mod.rs index 9002b79149f..f29d5da148f 100644 --- a/runtime/wasm/src/module/mod.rs +++ b/runtime/wasm/src/module/mod.rs @@ -19,6 +19,7 @@ use anyhow::Error; use graph::data::store; use graph::prelude::*; use graph::runtime::{AscHeap, IndexForAscTypeId}; +use graph::runtime::gas::{self, Gas, GasCounter, SaturatingInto}; use graph::{components::subgraph::MappingError, runtime::AscPtr}; use graph::{ data::subgraph::schema::SubgraphError, From 9bebc15537dee2cd52343ca03962bf3164a529e2 Mon Sep 17 00:00:00 2001 From: Leonardo Yvens Date: Wed, 28 Apr 2021 18:43:32 -0300 Subject: [PATCH 10/22] gas: Fix `GasSizeOf` impl for `Entity` --- graph/src/data/store/mod.rs | 9 ++++++++- graph/src/runtime/gas/size_of.rs | 9 +-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/graph/src/data/store/mod.rs b/graph/src/data/store/mod.rs index 07bc89ac2a9..a4fdd341c4e 100644 --- a/graph/src/data/store/mod.rs +++ b/graph/src/data/store/mod.rs @@ -1,6 +1,7 @@ use crate::{ components::store::{DeploymentLocator, EntityType}, - prelude::{q, r, s, CacheWeight, EntityKey, QueryExecutionError}, + prelude::{q, s, CacheWeight, EntityKey, QueryExecutionError}, + runtime::gas::{Gas, GasSizeOf}, }; use crate::{data::subgraph::DeploymentHash, prelude::EntityChange}; use anyhow::{anyhow, Error}; @@ -622,6 +623,12 @@ impl CacheWeight for Entity { } } +impl GasSizeOf for Entity { + fn gas_size_of(&self) -> Gas { + self.0.gas_size_of() + } +} + /// A value that can (maybe) be converted to an `Entity`. pub trait TryIntoEntity { fn try_into_entity(self) -> Result; diff --git a/graph/src/runtime/gas/size_of.rs b/graph/src/runtime/gas/size_of.rs index 7a1566b8cc3..794f98e7c9f 100644 --- a/graph/src/runtime/gas/size_of.rs +++ b/graph/src/runtime/gas/size_of.rs @@ -3,9 +3,8 @@ use crate::{ components::store::EntityType, data::store::{scalar::Bytes, Value}, - prelude::{BigDecimal, BigInt, Entity, EntityKey}, + prelude::{BigDecimal, BigInt, EntityKey}, }; -use std::ops::Deref as _; use super::{Gas, GasSizeOf, SaturatingInto as _}; @@ -60,12 +59,6 @@ impl GasSizeOf for BigInt { } } -impl GasSizeOf for Entity { - fn gas_size_of(&self) -> Gas { - self.deref().gas_size_of() - } -} - impl GasSizeOf for BigDecimal { fn gas_size_of(&self) -> Gas { let (int, _) = self.as_bigint_and_exponent(); From 146ef3e0c08cd53de70bee577dddb1e28bf2dad8 Mon Sep 17 00:00:00 2001 From: Leonardo Yvens Date: Thu, 29 Apr 2021 14:16:46 -0300 Subject: [PATCH 11/22] gas: Make MAX_GAS_PER_HANDLER configurable for debugging --- graph/src/runtime/gas/costs.rs | 29 ++++++++++++++++++++++------- graph/src/runtime/gas/mod.rs | 2 +- runtime/wasm/src/gas_rules.rs | 4 ++-- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/graph/src/runtime/gas/costs.rs b/graph/src/runtime/gas/costs.rs index bc62d1d7d33..e47c0669618 100644 --- a/graph/src/runtime/gas/costs.rs +++ b/graph/src/runtime/gas/costs.rs @@ -2,6 +2,8 @@ //! Determinism: Once deployed, none of these values can be changed without a version upgrade. use super::*; +use lazy_static::lazy_static; +use std::str::FromStr; /// Using 10 gas = ~1ns for WASM instructions. const GAS_PER_SECOND: u64 = 10_000_000_000; @@ -11,7 +13,20 @@ const GAS_PER_SECOND: u64 = 10_000_000_000; /// like 10 gas for ~1ns allows us to be granular in instructions which are aggregated into metered /// blocks via https://docs.rs/pwasm-utils/0.16.0/pwasm_utils/fn.inject_gas_counter.html But we can /// still charge very high numbers for other things. -pub const MAX_GAS_PER_HANDLER: u64 = 3600 * GAS_PER_SECOND; +const CONST_MAX_GAS_PER_HANDLER: u64 = 3600 * GAS_PER_SECOND; + +lazy_static! { + /// This is configurable only for debugging purposes. This value is set by the protocol, + /// so indexers running in the network should never set this config. + pub static ref MAX_GAS_PER_HANDLER: u64 = std::env::var("GRAPH_MAX_GAS_PER_HANDLER") + .ok() + .map(|s| { + u64::from_str(&s).unwrap_or_else(|_| { + panic!("GRAPH_LOAD_WINDOW_SIZE must be a number, but is `{}`", s) + }) + }) + .unwrap_or(CONST_MAX_GAS_PER_HANDLER); +} /// Gas for instructions are aggregated into blocks, so hopefully gas calls each have relatively /// large gas. But in the case they don't, we don't want the overhead of calling out into a host @@ -60,23 +75,23 @@ pub const BIG_MATH_GAS_OP: GasOp = GasOp { pub const ETHEREUM_CALL: Gas = Gas(25_000_000_000); // Allow up to 100,000 data sources to be created -pub const CREATE_DATA_SOURCE: Gas = Gas(MAX_GAS_PER_HANDLER / 100_000); +pub const CREATE_DATA_SOURCE: Gas = Gas(CONST_MAX_GAS_PER_HANDLER / 100_000); // Allow up to 100,000 logs -pub const LOG: Gas = Gas(MAX_GAS_PER_HANDLER / 100_000); +pub const LOG: Gas = Gas(CONST_MAX_GAS_PER_HANDLER / 100_000); // Saving to the store is one of the most expensive operations. pub const STORE_SET: GasOp = GasOp { // Allow up to 250k entities saved. - base_cost: MAX_GAS_PER_HANDLER / 250_000, + base_cost: CONST_MAX_GAS_PER_HANDLER / 250_000, // If the size roughly corresponds to bytes, allow 1GB to be saved. - size_mult: MAX_GAS_PER_HANDLER / 1_000_000_000, + size_mult: CONST_MAX_GAS_PER_HANDLER / 1_000_000_000, }; // Reading from the store is much cheaper than writing. pub const STORE_GET: GasOp = GasOp { - base_cost: MAX_GAS_PER_HANDLER / 10_000_000, - size_mult: MAX_GAS_PER_HANDLER / 10_000_000_000, + base_cost: CONST_MAX_GAS_PER_HANDLER / 10_000_000, + size_mult: CONST_MAX_GAS_PER_HANDLER / 10_000_000_000, }; pub const STORE_REMOVE: GasOp = STORE_SET; diff --git a/graph/src/runtime/gas/mod.rs b/graph/src/runtime/gas/mod.rs index 0787ba84bd6..935774eacc6 100644 --- a/graph/src/runtime/gas/mod.rs +++ b/graph/src/runtime/gas/mod.rs @@ -96,7 +96,7 @@ impl GasCounter { .fetch_update(SeqCst, SeqCst, |v| Some(v.saturating_add(amount.0))) .unwrap(); let new = old.saturating_add(amount.0); - if new >= MAX_GAS_PER_HANDLER { + if new >= *MAX_GAS_PER_HANDLER { Err(DeterministicHostError(anyhow::anyhow!( "Gas limit exceeded. Used: {}", new diff --git a/runtime/wasm/src/gas_rules.rs b/runtime/wasm/src/gas_rules.rs index e18b4317105..6fc774a1156 100644 --- a/runtime/wasm/src/gas_rules.rs +++ b/runtime/wasm/src/gas_rules.rs @@ -157,8 +157,8 @@ impl Rules for GasRules { // free pages because this is 32bit WASM. const MAX_PAGES: u64 = 12 * GIB / PAGE; // This ends up at 439,453,125 per page. - const GAS_PER_PAGE: u64 = MAX_GAS_PER_HANDLER / MAX_PAGES; - let gas_per_page = NonZeroU32::new(GAS_PER_PAGE.try_into().unwrap()).unwrap(); + let gas_per_page = + NonZeroU32::new((*MAX_GAS_PER_HANDLER / MAX_PAGES).try_into().unwrap()).unwrap(); Some(MemoryGrowCost::Linear(gas_per_page)) } From c161f63a6630c6dcf65e5caed475c87bdf3e918c Mon Sep 17 00:00:00 2001 From: Leonardo Yvens Date: Fri, 30 Apr 2021 14:59:18 -0300 Subject: [PATCH 12/22] runtime: Fix `ens_name_by_hash` error message --- runtime/wasm/src/module/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/wasm/src/module/mod.rs b/runtime/wasm/src/module/mod.rs index f29d5da148f..a1330c2f480 100644 --- a/runtime/wasm/src/module/mod.rs +++ b/runtime/wasm/src/module/mod.rs @@ -1521,7 +1521,7 @@ impl WasmInstanceContext { // This is unrelated to IPFS, but piggyback on the config to disallow it on the network. if !self.experimental_features.allow_non_deterministic_ipfs { return Err(HostExportError::Deterministic(anyhow!( - "`ipfs.map` is deprecated. Improved support for IPFS will be added in the future" + "`ens_name_by_hash` is deprecated" ))); } From 6f31e41825416213a01e448b13fe3ff0f5fa11f1 Mon Sep 17 00:00:00 2001 From: Leonardo Yvens Date: Fri, 7 May 2021 10:28:31 -0300 Subject: [PATCH 13/22] gas: Cost `log.log` per byte --- graph/src/runtime/gas/costs.rs | 7 +++++-- runtime/wasm/src/host_exports.rs | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/graph/src/runtime/gas/costs.rs b/graph/src/runtime/gas/costs.rs index e47c0669618..6296d511630 100644 --- a/graph/src/runtime/gas/costs.rs +++ b/graph/src/runtime/gas/costs.rs @@ -77,8 +77,11 @@ pub const ETHEREUM_CALL: Gas = Gas(25_000_000_000); // Allow up to 100,000 data sources to be created pub const CREATE_DATA_SOURCE: Gas = Gas(CONST_MAX_GAS_PER_HANDLER / 100_000); -// Allow up to 100,000 logs -pub const LOG: Gas = Gas(CONST_MAX_GAS_PER_HANDLER / 100_000); +pub const LOG_OP: GasOp = GasOp { + // Allow up to 100,000 logs + base_cost: CONST_MAX_GAS_PER_HANDLER / 100_000, + size_mult: DEFAULT_GAS_PER_BYTE, +}; // Saving to the store is one of the most expensive operations. pub const STORE_SET: GasOp = GasOp { diff --git a/runtime/wasm/src/host_exports.rs b/runtime/wasm/src/host_exports.rs index 019504cc260..89a89eeef5b 100644 --- a/runtime/wasm/src/host_exports.rs +++ b/runtime/wasm/src/host_exports.rs @@ -682,7 +682,7 @@ impl HostExports { msg: String, gas: &GasCounter, ) -> Result<(), DeterministicHostError> { - gas.consume_host_fn(gas::LOG)?; + gas.consume_host_fn(gas::LOG_OP.with_args(complexity::Size, &msg))?; let rs = record_static!(level, self.data_source_name.as_str()); From 73394a95209ccc16d3f0fdbb0f92261af2b469e2 Mon Sep 17 00:00:00 2001 From: Leonardo Yvens Date: Mon, 10 May 2021 10:56:31 -0300 Subject: [PATCH 14/22] gas: Reduce limit to 1000 seconds --- graph/src/runtime/gas/costs.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graph/src/runtime/gas/costs.rs b/graph/src/runtime/gas/costs.rs index 6296d511630..7eaa52600b5 100644 --- a/graph/src/runtime/gas/costs.rs +++ b/graph/src/runtime/gas/costs.rs @@ -8,12 +8,12 @@ use std::str::FromStr; /// Using 10 gas = ~1ns for WASM instructions. const GAS_PER_SECOND: u64 = 10_000_000_000; -/// Set max gas to 1 hour worth of gas per handler. The intent here is to have the determinism +/// Set max gas to 1000 seconds worth of gas per handler. The intent here is to have the determinism /// cutoff be very high, while still allowing more reasonable timer based cutoffs. Having a unit /// like 10 gas for ~1ns allows us to be granular in instructions which are aggregated into metered /// blocks via https://docs.rs/pwasm-utils/0.16.0/pwasm_utils/fn.inject_gas_counter.html But we can /// still charge very high numbers for other things. -const CONST_MAX_GAS_PER_HANDLER: u64 = 3600 * GAS_PER_SECOND; +const CONST_MAX_GAS_PER_HANDLER: u64 = 1000 * GAS_PER_SECOND; lazy_static! { /// This is configurable only for debugging purposes. This value is set by the protocol, @@ -68,7 +68,7 @@ pub const BIG_MATH_GAS_OP: GasOp = GasOp { // Allow up to 1,000 ethereum calls. The justification is that we don't know how much Ethereum gas a // call takes, but we limit the maximum to 25 million. One unit of Ethereum gas is at least 100ns // according to these benchmarks [1], so 1000 of our gas. Assuming the worst case, an Ethereum call -// should therefore consume 25 billion gas. This allows for 1440 calls per handler with the current +// should therefore consume 25 billion gas. This allows for 400 calls per handler with the current // limits. // // [1] - https://www.sciencedirect.com/science/article/abs/pii/S0166531620300900 From f997d3248d9d323c3f36a84db63c74ba5f3f1fd1 Mon Sep 17 00:00:00 2001 From: Leonardo Yvens Date: Mon, 10 May 2021 11:13:30 -0300 Subject: [PATCH 15/22] gas: Fix rebase artifact --- runtime/wasm/src/host_exports.rs | 2 ++ runtime/wasm/src/module/mod.rs | 32 +++++++++++++++++--------------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/runtime/wasm/src/host_exports.rs b/runtime/wasm/src/host_exports.rs index 89a89eeef5b..a81673b64bf 100644 --- a/runtime/wasm/src/host_exports.rs +++ b/runtime/wasm/src/host_exports.rs @@ -731,7 +731,9 @@ impl HostExports { pub(crate) fn json_from_bytes( &self, bytes: &Vec, + gas: &GasCounter, ) -> Result { + gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(gas::complexity::Size, &bytes))?; serde_json::from_reader(bytes.as_slice()).map_err(|e| DeterministicHostError(e.into())) } diff --git a/runtime/wasm/src/module/mod.rs b/runtime/wasm/src/module/mod.rs index a1330c2f480..e27c84ec263 100644 --- a/runtime/wasm/src/module/mod.rs +++ b/runtime/wasm/src/module/mod.rs @@ -976,11 +976,10 @@ impl WasmInstanceContext { bytes_ptr: AscPtr, ) -> Result>, DeterministicHostError> { let bytes: Vec = asc_get(self, bytes_ptr)?; - gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(gas::complexity::Size, &bytes))?; let result = self .ctx .host_exports - .json_from_bytes(&bytes) + .json_from_bytes(&bytes, gas) .with_context(|| { format!( "Failed to parse JSON from byte array. Bytes (truncated to 1024 chars): `{:?}`", @@ -999,19 +998,22 @@ impl WasmInstanceContext { ) -> Result>, bool>>, DeterministicHostError> { let bytes: Vec = asc_get(self, bytes_ptr)?; - gas.consume_host_fn(gas::DEFAULT_GAS_OP.with_args(gas::complexity::Size, &bytes))?; - let result = self.ctx.host_exports.json_from_bytes(&bytes).map_err(|e| { - warn!( - &self.ctx.logger, - "Failed to parse JSON from byte array"; - "bytes" => format!("{:?}", bytes), - "error" => format!("{}", e) - ); - - // Map JSON errors to boolean to match the `Result` - // result type expected by mappings - true - }); + let result = self + .ctx + .host_exports + .json_from_bytes(&bytes, gas) + .map_err(|e| { + warn!( + &self.ctx.logger, + "Failed to parse JSON from byte array"; + "bytes" => format!("{:?}", bytes), + "error" => format!("{}", e) + ); + + // Map JSON errors to boolean to match the `Result` + // result type expected by mappings + true + }); asc_new(self, &result) } From 92c2ca30d6cbf6a4276a991c280b989ae68916e4 Mon Sep 17 00:00:00 2001 From: Leonardo Yvens Date: Mon, 10 May 2021 18:37:50 -0300 Subject: [PATCH 16/22] gas: Update tests for gas limit change --- graph/src/runtime/gas/costs.rs | 6 +++--- runtime/test/src/test.rs | 12 ++++++------ runtime/wasm/src/gas_rules.rs | 1 - 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/graph/src/runtime/gas/costs.rs b/graph/src/runtime/gas/costs.rs index 7eaa52600b5..01b9dee9b1a 100644 --- a/graph/src/runtime/gas/costs.rs +++ b/graph/src/runtime/gas/costs.rs @@ -21,7 +21,7 @@ lazy_static! { pub static ref MAX_GAS_PER_HANDLER: u64 = std::env::var("GRAPH_MAX_GAS_PER_HANDLER") .ok() .map(|s| { - u64::from_str(&s).unwrap_or_else(|_| { + u64::from_str(&s.replace("_", "")).unwrap_or_else(|_| { panic!("GRAPH_LOAD_WINDOW_SIZE must be a number, but is `{}`", s) }) }) @@ -39,8 +39,8 @@ pub const HOST_EXPORT_GAS: Gas = Gas(10_000); /// As a heuristic for the cost of host fns it makes sense to reason in terms of bandwidth and /// calculate the cost from there. Because we don't have benchmarks for each host fn, we go with -/// pessimistic assumption of performance of 10 MB/s, which nonetheless allows for 36 GB to be -/// processed through host exports by a single handler at a 1 hour budget. +/// pessimistic assumption of performance of 10 MB/s, which nonetheless allows for 10 GB to be +/// processed through host exports by a single handler at a 1000 seconds budget. const DEFAULT_BYTE_PER_SECOND: u64 = 10_000_000; /// With the current parameters DEFAULT_GAS_PER_BYTE = 1_000. diff --git a/runtime/test/src/test.rs b/runtime/test/src/test.rs index b36154affa0..509c6397df8 100644 --- a/runtime/test/src/test.rs +++ b/runtime/test/src/test.rs @@ -219,7 +219,7 @@ fn test_json_conversions(api_version: Version) { scalar::BigInt::from_signed_bytes_le(&bytes) ); - assert_eq!(module.gas_used(), 184180372); + assert_eq!(module.gas_used(), 51937534); } #[tokio::test] @@ -583,7 +583,7 @@ fn test_big_int_to_hex(api_version: Version) { "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" ); - assert_eq!(module.gas_used(), 184013403); + assert_eq!(module.gas_used(), 51770565); } #[tokio::test] @@ -660,7 +660,7 @@ fn test_big_int_arithmetic(api_version: Version) { let result: BigInt = asc_get(&module, result_ptr).unwrap(); assert_eq!(result, BigInt::from(1)); - assert_eq!(module.gas_used(), 184342018); + assert_eq!(module.gas_used(), 52099180); } #[tokio::test] @@ -718,7 +718,7 @@ fn test_bytes_to_base58(api_version: Version) { let base58: String = asc_get(&module, result_ptr).unwrap(); assert_eq!(base58, "QmWmyoMoctfbAaiEs2G46gpeUmhqFRDW6KWo64y5r581Vz"); - assert_eq!(module.gas_used(), 183820465); + assert_eq!(module.gas_used(), 51577627); } #[tokio::test] @@ -751,7 +751,7 @@ fn test_data_source_create(api_version: Version) { module.invoke_export2_void("dataSourceCreate", name, params)?; module.instance_ctx_mut().ctx.state.exit_handler(); - assert_eq!(module.gas_used(), 543636483); + assert_eq!(module.gas_used(), 151393645); Ok(module.take_ctx().ctx.state.drain_created_data_sources()) }; @@ -945,7 +945,7 @@ fn test_detect_contract_calls(api_version: Version) { data_source_with_calls.mapping.requires_archive().unwrap(), true ); - assert_eq!(module.gas_used(), 312058391); + assert_eq!(module.gas_used(), 87948791); } #[tokio::test] diff --git a/runtime/wasm/src/gas_rules.rs b/runtime/wasm/src/gas_rules.rs index 6fc774a1156..f746b7f1bf3 100644 --- a/runtime/wasm/src/gas_rules.rs +++ b/runtime/wasm/src/gas_rules.rs @@ -156,7 +156,6 @@ impl Rules for GasRules { // In practice this will never be hit unless we also // free pages because this is 32bit WASM. const MAX_PAGES: u64 = 12 * GIB / PAGE; - // This ends up at 439,453,125 per page. let gas_per_page = NonZeroU32::new((*MAX_GAS_PER_HANDLER / MAX_PAGES).try_into().unwrap()).unwrap(); From 6e3697aa25685d1c244296248bb80ed4fbb632e9 Mon Sep 17 00:00:00 2001 From: Leonardo Yvens Date: Thu, 17 Jun 2021 16:59:36 -0300 Subject: [PATCH 17/22] runtime: Fix rebase artifacts in tests --- graph/src/data/store/mod.rs | 2 +- graph/src/runtime/gas/mod.rs | 6 +++--- runtime/test/src/test.rs | 15 ++++++--------- runtime/test/src/test/abi.rs | 2 +- runtime/wasm/src/host_exports.rs | 6 ++---- runtime/wasm/src/module/mod.rs | 16 +++++++++------- 6 files changed, 22 insertions(+), 25 deletions(-) diff --git a/graph/src/data/store/mod.rs b/graph/src/data/store/mod.rs index a4fdd341c4e..98a31eb3734 100644 --- a/graph/src/data/store/mod.rs +++ b/graph/src/data/store/mod.rs @@ -1,6 +1,6 @@ use crate::{ components::store::{DeploymentLocator, EntityType}, - prelude::{q, s, CacheWeight, EntityKey, QueryExecutionError}, + prelude::{q, r, s, CacheWeight, EntityKey, QueryExecutionError}, runtime::gas::{Gas, GasSizeOf}, }; use crate::{data::subgraph::DeploymentHash, prelude::EntityChange}; diff --git a/graph/src/runtime/gas/mod.rs b/graph/src/runtime/gas/mod.rs index 935774eacc6..ac1b0dc569c 100644 --- a/graph/src/runtime/gas/mod.rs +++ b/graph/src/runtime/gas/mod.rs @@ -10,8 +10,8 @@ pub use costs::DEFAULT_BASE_COST; pub use costs::*; pub use saturating::*; -use std::rc::Rc; use std::sync::atomic::{AtomicU64, Ordering::SeqCst}; +use std::sync::Arc; use std::{fmt, fmt::Display}; pub struct GasOp { @@ -79,13 +79,13 @@ impl Display for Gas { } #[derive(Clone)] -pub struct GasCounter(Rc); +pub struct GasCounter(Arc); impl CheapClone for GasCounter {} impl GasCounter { pub fn new() -> Self { - Self(Rc::new(AtomicU64::new(0))) + Self(Arc::new(AtomicU64::new(0))) } /// This should be called once per host export diff --git a/runtime/test/src/test.rs b/runtime/test/src/test.rs index 509c6397df8..924e43cc762 100644 --- a/runtime/test/src/test.rs +++ b/runtime/test/src/test.rs @@ -211,7 +211,6 @@ fn test_json_conversions(api_version: Version) { let number = "-922337203685077092345034"; let number_ptr = asc_new(&mut module, number).unwrap(); let big_int_obj: AscPtr = module.invoke_export1("testToBigInt", number_ptr); - let bytes: Vec = asc_get(&module, big_int_obj).unwrap(); assert_eq!( @@ -219,7 +218,7 @@ fn test_json_conversions(api_version: Version) { scalar::BigInt::from_signed_bytes_le(&bytes) ); - assert_eq!(module.gas_used(), 51937534); + assert_eq!(module.gas_used(), 912148); } #[tokio::test] @@ -258,8 +257,7 @@ fn test_json_parsing(api_version: Version) { let output: String = asc_get(&module, return_value).unwrap(); assert_eq!(output, "OK: foo, ERROR: false"); - - assert_eq!(module.gas_used(), 1236877); + assert_eq!(module.gas_used(), 2693805); } #[tokio::test] @@ -583,7 +581,7 @@ fn test_big_int_to_hex(api_version: Version) { "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" ); - assert_eq!(module.gas_used(), 51770565); + assert_eq!(module.gas_used(), 962685); } #[tokio::test] @@ -660,7 +658,7 @@ fn test_big_int_arithmetic(api_version: Version) { let result: BigInt = asc_get(&module, result_ptr).unwrap(); assert_eq!(result, BigInt::from(1)); - assert_eq!(module.gas_used(), 52099180); + assert_eq!(module.gas_used(), 3035221); } #[tokio::test] @@ -718,7 +716,7 @@ fn test_bytes_to_base58(api_version: Version) { let base58: String = asc_get(&module, result_ptr).unwrap(); assert_eq!(base58, "QmWmyoMoctfbAaiEs2G46gpeUmhqFRDW6KWo64y5r581Vz"); - assert_eq!(module.gas_used(), 51577627); + assert_eq!(module.gas_used(), 477157); } #[tokio::test] @@ -751,7 +749,7 @@ fn test_data_source_create(api_version: Version) { module.invoke_export2_void("dataSourceCreate", name, params)?; module.instance_ctx_mut().ctx.state.exit_handler(); - assert_eq!(module.gas_used(), 151393645); + assert_eq!(module.gas_used(), 100440279); Ok(module.take_ctx().ctx.state.drain_created_data_sources()) }; @@ -945,7 +943,6 @@ fn test_detect_contract_calls(api_version: Version) { data_source_with_calls.mapping.requires_archive().unwrap(), true ); - assert_eq!(module.gas_used(), 87948791); } #[tokio::test] diff --git a/runtime/test/src/test/abi.rs b/runtime/test/src/test/abi.rs index 768fa6e36d2..bfc34296a3c 100644 --- a/runtime/test/src/test/abi.rs +++ b/runtime/test/src/test/abi.rs @@ -81,7 +81,7 @@ fn test_abi_array(api_version: Version) { module.invoke_export1("test_array", vec_obj); let new_vec: Vec = asc_get(&module, new_vec_obj).unwrap(); - assert_eq!(module.gas_used(), 200657); + assert_eq!(module.gas_used(), 722564); assert_eq!( new_vec, vec![ diff --git a/runtime/wasm/src/host_exports.rs b/runtime/wasm/src/host_exports.rs index a81673b64bf..da91197ceb2 100644 --- a/runtime/wasm/src/host_exports.rs +++ b/runtime/wasm/src/host_exports.rs @@ -9,9 +9,9 @@ use graph::components::subgraph::{CausalityRegion, ProofOfIndexingEvent, SharedP use graph::data::store; use graph::prelude::serde_json; use graph::prelude::{slog::b, slog::record_static, *}; -pub use graph::runtime::{DeterministicHostError, HostExportError}; use graph::runtime::gas::{self, complexity, Gas, GasCounter}; -use graph::{blockchain::DataSource, bytes::Bytes}; +pub use graph::runtime::{DeterministicHostError, HostExportError}; +use never::Never; use semver::Version; use std::collections::HashMap; use std::ops::Deref; @@ -69,7 +69,6 @@ pub struct HostExports { templates: Arc>, pub(crate) link_resolver: Arc, store: Arc, - gas_used: GasCounter, } impl HostExports { @@ -92,7 +91,6 @@ impl HostExports { templates, link_resolver, store, - gas_used: GasCounter::new(), } } diff --git a/runtime/wasm/src/module/mod.rs b/runtime/wasm/src/module/mod.rs index e27c84ec263..91d24fd105f 100644 --- a/runtime/wasm/src/module/mod.rs +++ b/runtime/wasm/src/module/mod.rs @@ -13,13 +13,12 @@ use wasmtime::{Memory, Trap}; use crate::error::DeterminismLevel; pub use crate::host_exports; -use crate::gas::{self, Gas, GasCounter, SaturatingInto}; use crate::mapping::MappingContext; use anyhow::Error; use graph::data::store; use graph::prelude::*; -use graph::runtime::{AscHeap, IndexForAscTypeId}; use graph::runtime::gas::{self, Gas, GasCounter, SaturatingInto}; +use graph::runtime::{AscHeap, IndexForAscTypeId}; use graph::{components::subgraph::MappingError, runtime::AscPtr}; use graph::{ data::subgraph::schema::SubgraphError, @@ -139,8 +138,8 @@ impl WasmInstance { self.instance.get_func(func_name).unwrap() } - #[cfg(test)] - pub(crate) fn gas_used(&self) -> u64 { + #[cfg(debug_assertions)] + pub fn gas_used(&self) -> u64 { self.gas.get().into() } @@ -1490,7 +1489,10 @@ impl WasmInstanceContext { &mut self, gas: &GasCounter, ) -> Result, DeterministicHostError> { - asc_new(self, &self.ctx.host_exports.data_source_address(gas)?) + asc_new( + self, + self.ctx.host_exports.data_source_address(gas)?.as_slice(), + ) } /// function dataSource.network(): String @@ -1586,7 +1588,7 @@ impl WasmInstanceContext { /// function arweave.transactionData(txId: string): Bytes | null pub fn arweave_transaction_data( &mut self, - gas: &GasCounter, + _gas: &GasCounter, _tx_id: AscPtr, ) -> Result, HostExportError> { Err(HostExportError::Deterministic(anyhow!( @@ -1597,7 +1599,7 @@ impl WasmInstanceContext { /// function box.profile(address: string): JSONValue | null pub fn box_profile( &mut self, - gas: &GasCounter, + _gas: &GasCounter, _address: AscPtr, ) -> Result, HostExportError> { Err(HostExportError::Deterministic(anyhow!( From 59a4c61799251b1848c895457cb2fb8cdb8b592e Mon Sep 17 00:00:00 2001 From: Leo Yvens Date: Thu, 9 Dec 2021 16:36:43 +0000 Subject: [PATCH 18/22] runtime: Fix tests gas costs based on api version --- runtime/test/src/test.rs | 48 ++++++++++++++++++------------------ runtime/test/src/test/abi.rs | 8 +++--- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/runtime/test/src/test.rs b/runtime/test/src/test.rs index 924e43cc762..23969228eae 100644 --- a/runtime/test/src/test.rs +++ b/runtime/test/src/test.rs @@ -179,7 +179,7 @@ impl WasmInstanceExt for WasmInstance { } } -fn test_json_conversions(api_version: Version) { +fn test_json_conversions(api_version: Version, gas_used: u64) { let mut module = test_module( "jsonConversions", mock_data_source( @@ -218,20 +218,20 @@ fn test_json_conversions(api_version: Version) { scalar::BigInt::from_signed_bytes_le(&bytes) ); - assert_eq!(module.gas_used(), 912148); + assert_eq!(module.gas_used(), gas_used); } #[tokio::test] async fn json_conversions_v0_0_4() { - test_json_conversions(API_VERSION_0_0_4); + test_json_conversions(API_VERSION_0_0_4, 51937534); } #[tokio::test] async fn json_conversions_v0_0_5() { - test_json_conversions(API_VERSION_0_0_5); + test_json_conversions(API_VERSION_0_0_5, 912148); } -fn test_json_parsing(api_version: Version) { +fn test_json_parsing(api_version: Version, gas_used: u64) { let mut module = test_module( "jsonParsing", mock_data_source( @@ -257,17 +257,17 @@ fn test_json_parsing(api_version: Version) { let output: String = asc_get(&module, return_value).unwrap(); assert_eq!(output, "OK: foo, ERROR: false"); - assert_eq!(module.gas_used(), 2693805); + assert_eq!(module.gas_used(), gas_used); } #[tokio::test] async fn json_parsing_v0_0_4() { - test_json_parsing(API_VERSION_0_0_4); + test_json_parsing(API_VERSION_0_0_4, 2062683); } #[tokio::test] async fn json_parsing_v0_0_5() { - test_json_parsing(API_VERSION_0_0_5); + test_json_parsing(API_VERSION_0_0_5, 2693805); } async fn test_ipfs_cat(api_version: Version) { @@ -547,7 +547,7 @@ async fn crypto_keccak256_v0_0_5() { test_crypto_keccak256(API_VERSION_0_0_5); } -fn test_big_int_to_hex(api_version: Version) { +fn test_big_int_to_hex(api_version: Version, gas_used: u64) { let mut module = test_module( "BigIntToHex", mock_data_source( @@ -581,20 +581,20 @@ fn test_big_int_to_hex(api_version: Version) { "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" ); - assert_eq!(module.gas_used(), 962685); + assert_eq!(module.gas_used(), gas_used); } #[tokio::test] async fn big_int_to_hex_v0_0_4() { - test_big_int_to_hex(API_VERSION_0_0_4); + test_big_int_to_hex(API_VERSION_0_0_4, 51770565); } #[tokio::test] async fn big_int_to_hex_v0_0_5() { - test_big_int_to_hex(API_VERSION_0_0_5); + test_big_int_to_hex(API_VERSION_0_0_5, 962685); } -fn test_big_int_arithmetic(api_version: Version) { +fn test_big_int_arithmetic(api_version: Version, gas_used: u64) { let mut module = test_module( "BigIntArithmetic", mock_data_source( @@ -658,17 +658,17 @@ fn test_big_int_arithmetic(api_version: Version) { let result: BigInt = asc_get(&module, result_ptr).unwrap(); assert_eq!(result, BigInt::from(1)); - assert_eq!(module.gas_used(), 3035221); + assert_eq!(module.gas_used(), gas_used); } #[tokio::test] async fn big_int_arithmetic_v0_0_4() { - test_big_int_arithmetic(API_VERSION_0_0_4); + test_big_int_arithmetic(API_VERSION_0_0_4, 52099180); } #[tokio::test] async fn big_int_arithmetic_v0_0_5() { - test_big_int_arithmetic(API_VERSION_0_0_5); + test_big_int_arithmetic(API_VERSION_0_0_5, 3035221); } fn test_abort(api_version: Version, error_msg: &str) { @@ -700,7 +700,7 @@ async fn abort_v0_0_5() { ); } -fn test_bytes_to_base58(api_version: Version) { +fn test_bytes_to_base58(api_version: Version, gas_used: u64) { let mut module = test_module( "bytesToBase58", mock_data_source( @@ -716,20 +716,20 @@ fn test_bytes_to_base58(api_version: Version) { let base58: String = asc_get(&module, result_ptr).unwrap(); assert_eq!(base58, "QmWmyoMoctfbAaiEs2G46gpeUmhqFRDW6KWo64y5r581Vz"); - assert_eq!(module.gas_used(), 477157); + assert_eq!(module.gas_used(), gas_used); } #[tokio::test] async fn bytes_to_base58_v0_0_4() { - test_bytes_to_base58(API_VERSION_0_0_4); + test_bytes_to_base58(API_VERSION_0_0_4, 51577627); } #[tokio::test] async fn bytes_to_base58_v0_0_5() { - test_bytes_to_base58(API_VERSION_0_0_5); + test_bytes_to_base58(API_VERSION_0_0_5, 477157); } -fn test_data_source_create(api_version: Version) { +fn test_data_source_create(api_version: Version, gas_used: u64) { let run_data_source_create = move |name: String, params: Vec| @@ -749,7 +749,7 @@ fn test_data_source_create(api_version: Version) { module.invoke_export2_void("dataSourceCreate", name, params)?; module.instance_ctx_mut().ctx.state.exit_handler(); - assert_eq!(module.gas_used(), 100440279); + assert_eq!(module.gas_used(), gas_used); Ok(module.take_ctx().ctx.state.drain_created_data_sources()) }; @@ -777,12 +777,12 @@ fn test_data_source_create(api_version: Version) { #[tokio::test] async fn data_source_create_v0_0_4() { - test_data_source_create(API_VERSION_0_0_4); + test_data_source_create(API_VERSION_0_0_4, 151393645); } #[tokio::test] async fn data_source_create_v0_0_5() { - test_data_source_create(API_VERSION_0_0_5); + test_data_source_create(API_VERSION_0_0_5, 100440279); } fn test_ens_name_by_hash(api_version: Version) { diff --git a/runtime/test/src/test/abi.rs b/runtime/test/src/test/abi.rs index bfc34296a3c..7172c5c6319 100644 --- a/runtime/test/src/test/abi.rs +++ b/runtime/test/src/test/abi.rs @@ -59,7 +59,7 @@ async fn unbounded_recursion_v0_0_5() { test_unbounded_recursion(API_VERSION_0_0_5); } -fn test_abi_array(api_version: Version) { +fn test_abi_array(api_version: Version, gas_used: u64) { let mut module = test_module( "abiArray", mock_data_source( @@ -81,7 +81,7 @@ fn test_abi_array(api_version: Version) { module.invoke_export1("test_array", vec_obj); let new_vec: Vec = asc_get(&module, new_vec_obj).unwrap(); - assert_eq!(module.gas_used(), 722564); + assert_eq!(module.gas_used(), gas_used); assert_eq!( new_vec, vec![ @@ -96,12 +96,12 @@ fn test_abi_array(api_version: Version) { #[tokio::test] async fn abi_array_v0_0_4() { - test_abi_array(API_VERSION_0_0_4); + test_abi_array(API_VERSION_0_0_4, 200657); } #[tokio::test] async fn abi_array_v0_0_5() { - test_abi_array(API_VERSION_0_0_5); + test_abi_array(API_VERSION_0_0_5, 722564); } fn test_abi_subarray(api_version: Version) { From 0ed0c3149bf25bb1fca69ec352f9de163be9a71e Mon Sep 17 00:00:00 2001 From: Leonardo Yvens Date: Thu, 16 Dec 2021 16:05:21 +0000 Subject: [PATCH 19/22] gas: gas costing for ethereum calls --- chain/ethereum/src/runtime/runtime_adapter.rs | 12 ++++++++++++ graph/src/blockchain/mod.rs | 3 ++- graph/src/runtime/gas/costs.rs | 9 --------- graph/src/runtime/gas/mod.rs | 4 ++++ runtime/wasm/src/module/mod.rs | 2 ++ 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/chain/ethereum/src/runtime/runtime_adapter.rs b/chain/ethereum/src/runtime/runtime_adapter.rs index 9d96c78cf70..4be8fdbc3dd 100644 --- a/chain/ethereum/src/runtime/runtime_adapter.rs +++ b/chain/ethereum/src/runtime/runtime_adapter.rs @@ -7,6 +7,7 @@ use crate::{ }; use anyhow::{Context, Error}; use blockchain::HostFn; +use graph::runtime::gas::Gas; use graph::runtime::{AscIndexId, IndexForAscTypeId}; use graph::{ blockchain::{self, BlockPtr, HostFnCtx}, @@ -23,6 +24,15 @@ use graph_runtime_wasm::asc_abi::class::{AscEnumArray, EthereumValueKind}; use super::abi::{AscUnresolvedContractCall, AscUnresolvedContractCall_0_0_4}; +// Allow up to 1,000 ethereum calls. The justification is that we don't know how much Ethereum gas a +// call takes, but we limit the maximum to 25 million. One unit of Ethereum gas is at least 100ns +// according to these benchmarks [1], so 1000 of our gas. Assuming the worst case, an Ethereum call +// should therefore consume 25 billion gas. This allows for 400 calls per handler with the current +// limits. +// +// [1] - https://www.sciencedirect.com/science/article/abs/pii/S0166531620300900 +pub const ETHEREUM_CALL: Gas = Gas::new(25_000_000_000); + pub struct RuntimeAdapter { pub(crate) eth_adapters: Arc, pub(crate) call_cache: Arc, @@ -60,6 +70,8 @@ fn ethereum_call( wasm_ptr: u32, abis: &[Arc], ) -> Result, HostExportError> { + ctx.gas.consume_host_fn(ETHEREUM_CALL)?; + // For apiVersion >= 0.0.4 the call passed from the mapping includes the // function signature; subgraphs using an apiVersion < 0.0.4 don't pass // the signature along with the call. diff --git a/graph/src/blockchain/mod.rs b/graph/src/blockchain/mod.rs index 7503f3744e3..a180399924f 100644 --- a/graph/src/blockchain/mod.rs +++ b/graph/src/blockchain/mod.rs @@ -17,7 +17,7 @@ use crate::{ }, data::subgraph::UnifiedMappingApiVersion, prelude::DataSourceContext, - runtime::{AscHeap, AscPtr, DeterministicHostError, HostExportError}, + runtime::{gas::GasCounter, AscHeap, AscPtr, DeterministicHostError, HostExportError}, }; use crate::{ components::{ @@ -286,6 +286,7 @@ pub struct HostFnCtx<'a> { pub logger: Logger, pub block_ptr: BlockPtr, pub heap: &'a mut dyn AscHeap, + pub gas: GasCounter, } /// Host fn that receives one u32 argument and returns an u32. diff --git a/graph/src/runtime/gas/costs.rs b/graph/src/runtime/gas/costs.rs index 01b9dee9b1a..8033d5716f6 100644 --- a/graph/src/runtime/gas/costs.rs +++ b/graph/src/runtime/gas/costs.rs @@ -65,15 +65,6 @@ pub const BIG_MATH_GAS_OP: GasOp = GasOp { size_mult: BIG_MATH_GAS_PER_BYTE, }; -// Allow up to 1,000 ethereum calls. The justification is that we don't know how much Ethereum gas a -// call takes, but we limit the maximum to 25 million. One unit of Ethereum gas is at least 100ns -// according to these benchmarks [1], so 1000 of our gas. Assuming the worst case, an Ethereum call -// should therefore consume 25 billion gas. This allows for 400 calls per handler with the current -// limits. -// -// [1] - https://www.sciencedirect.com/science/article/abs/pii/S0166531620300900 -pub const ETHEREUM_CALL: Gas = Gas(25_000_000_000); - // Allow up to 100,000 data sources to be created pub const CREATE_DATA_SOURCE: Gas = Gas(CONST_MAX_GAS_PER_HANDLER / 100_000); diff --git a/graph/src/runtime/gas/mod.rs b/graph/src/runtime/gas/mod.rs index ac1b0dc569c..7f6dbc22dbb 100644 --- a/graph/src/runtime/gas/mod.rs +++ b/graph/src/runtime/gas/mod.rs @@ -58,6 +58,10 @@ pub struct Gas(u64); impl Gas { pub const ZERO: Gas = Gas(0); + + pub const fn new(gas: u64) -> Self { + Gas(gas) + } } impl From for Gas { diff --git a/runtime/wasm/src/module/mod.rs b/runtime/wasm/src/module/mod.rs index 91d24fd105f..0e876a02834 100644 --- a/runtime/wasm/src/module/mod.rs +++ b/runtime/wasm/src/module/mod.rs @@ -393,6 +393,7 @@ impl WasmInstance { for module in modules { let func_shared_ctx = Rc::downgrade(&shared_ctx); let host_fn = host_fn.cheap_clone(); + let gas = gas.cheap_clone(); linker.func(module, host_fn.name, move |call_ptr: u32| { let start = Instant::now(); let instance = func_shared_ctx.upgrade().unwrap(); @@ -420,6 +421,7 @@ impl WasmInstance { logger: instance.ctx.logger.cheap_clone(), block_ptr: instance.ctx.block_ptr.cheap_clone(), heap: instance, + gas: gas.cheap_clone(), }; let ret = (host_fn.func)(ctx, call_ptr).map_err(|e| match e { HostExportError::Deterministic(e) => { From 6acffb0d04b885ddc77f980323f7d1df7562c58a Mon Sep 17 00:00:00 2001 From: Leonardo Yvens Date: Thu, 16 Dec 2021 16:10:31 +0000 Subject: [PATCH 20/22] gas: Remove ambiguous impls --- graph/src/runtime/gas/mod.rs | 13 +++---------- runtime/wasm/src/host_exports.rs | 8 ++++---- runtime/wasm/src/module/mod.rs | 2 +- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/graph/src/runtime/gas/mod.rs b/graph/src/runtime/gas/mod.rs index 7f6dbc22dbb..213dd97dace 100644 --- a/graph/src/runtime/gas/mod.rs +++ b/graph/src/runtime/gas/mod.rs @@ -62,17 +62,10 @@ impl Gas { pub const fn new(gas: u64) -> Self { Gas(gas) } -} - -impl From for Gas { - fn from(x: u64) -> Self { - Gas(x) - } -} -impl From for u64 { - fn from(x: Gas) -> Self { - x.0 + #[cfg(debug_assertions)] + pub const fn value(&self) -> u64 { + self.0 } } diff --git a/runtime/wasm/src/host_exports.rs b/runtime/wasm/src/host_exports.rs index da91197ceb2..50fbe236263 100644 --- a/runtime/wasm/src/host_exports.rs +++ b/runtime/wasm/src/host_exports.rs @@ -102,7 +102,7 @@ impl HostExports { column_number: Option, gas: &GasCounter, ) -> Result { - gas.consume_host_fn(gas::DEFAULT_BASE_COST.into())?; + gas.consume_host_fn(Gas::new(gas::DEFAULT_BASE_COST))?; let message = message .map(|message| format!("message: {}", message)) @@ -702,7 +702,7 @@ impl HostExports { &self, gas: &GasCounter, ) -> Result, DeterministicHostError> { - gas.consume_host_fn(Gas::from(gas::DEFAULT_BASE_COST))?; + gas.consume_host_fn(Gas::new(gas::DEFAULT_BASE_COST))?; Ok(self.data_source_address.clone()) } @@ -710,7 +710,7 @@ impl HostExports { &self, gas: &GasCounter, ) -> Result { - gas.consume_host_fn(Gas::from(gas::DEFAULT_BASE_COST))?; + gas.consume_host_fn(Gas::new(gas::DEFAULT_BASE_COST))?; Ok(self.data_source_network.clone()) } @@ -718,7 +718,7 @@ impl HostExports { &self, gas: &GasCounter, ) -> Result { - gas.consume_host_fn(Gas::from(gas::DEFAULT_BASE_COST))?; + gas.consume_host_fn(Gas::new(gas::DEFAULT_BASE_COST))?; Ok(self .data_source_context .as_ref() diff --git a/runtime/wasm/src/module/mod.rs b/runtime/wasm/src/module/mod.rs index 0e876a02834..220c680f4b3 100644 --- a/runtime/wasm/src/module/mod.rs +++ b/runtime/wasm/src/module/mod.rs @@ -140,7 +140,7 @@ impl WasmInstance { #[cfg(debug_assertions)] pub fn gas_used(&self) -> u64 { - self.gas.get().into() + self.gas.get().value() } fn invoke_handler( From 48bb3e0c851dfcb917d235f4012d69dd34a54602 Mon Sep 17 00:00:00 2001 From: Leonardo Yvens Date: Thu, 16 Dec 2021 17:04:36 +0000 Subject: [PATCH 21/22] instance manager: Print backtrace of start subgraph error --- core/src/subgraph/instance_manager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/subgraph/instance_manager.rs b/core/src/subgraph/instance_manager.rs index 1430b8738ce..37efe88fda3 100644 --- a/core/src/subgraph/instance_manager.rs +++ b/core/src/subgraph/instance_manager.rs @@ -223,7 +223,7 @@ where Err(err) => error!( err_logger, "Failed to start subgraph"; - "error" => format!("{}", err), + "error" => format!("{:#}", err), "code" => LogCode::SubgraphStartFailure ), } From d592c0062ed401363ef178e5995d10438b764af9 Mon Sep 17 00:00:00 2001 From: Leonardo Yvens Date: Fri, 17 Dec 2021 18:59:09 +0000 Subject: [PATCH 22/22] runtime: Support sign extension instructions --- Cargo.lock | 9 ++++----- runtime/wasm/Cargo.toml | 8 ++++++-- runtime/wasm/src/gas_rules.rs | 1 + 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1551bc2182d..dc9e5ebf821 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2883,9 +2883,9 @@ dependencies = [ [[package]] name = "parity-wasm" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d17797de36b94bc5f73edad736fd0a77ce5ab64dd622f809c1eead8c91fa6564" +checksum = "be5e13c266502aadf83426d87d81a0f5d1ef45b8027f5a471c360abfe4bfae92" [[package]] name = "parking_lot" @@ -3347,9 +3347,8 @@ dependencies = [ [[package]] name = "pwasm-utils" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51992bc74c0f34f759ff97fb303602e60343afc83693769c91aa17724442809e" +version = "0.19.0" +source = "git+https://github.com/edgeandnode/wasm-utils?branch=sign-ext-feature-flag#1b939aba3ee861338d6102ce01d6511128b7b11f" dependencies = [ "byteorder", "log", diff --git a/runtime/wasm/Cargo.toml b/runtime/wasm/Cargo.toml index 349c525ba94..eb15ad28e03 100644 --- a/runtime/wasm/Cargo.toml +++ b/runtime/wasm/Cargo.toml @@ -23,5 +23,9 @@ anyhow = "1.0" wasmtime = "0.27.0" defer = "0.1" never = "0.1" -pwasm-utils = "0.17" -parity-wasm = "0.42" + +# Patch being upstreamed in https://github.com/paritytech/wasm-utils/pull/174 +pwasm-utils = { git = "https://github.com/edgeandnode/wasm-utils", branch = "sign-ext-feature-flag", features = ["sign_ext"] } + +# AssemblyScript uses sign extensions +parity-wasm = { version = "0.42", features = ["std", "sign_ext"] } diff --git a/runtime/wasm/src/gas_rules.rs b/runtime/wasm/src/gas_rules.rs index f746b7f1bf3..ee4d1fd3221 100644 --- a/runtime/wasm/src/gas_rules.rs +++ b/runtime/wasm/src/gas_rules.rs @@ -141,6 +141,7 @@ impl Rules for GasRules { End => 100, Return => 100, Drop => 100, + SignExt(_) => 100, Nop => 1, Unreachable => 1, };