Skip to content

Commit 5074ac9

Browse files
leoyvensThat3PercentLeo Yvens
authored
Gas metering (#2414)
* WIP: gas * runtime: Add gas metering * gas: Fix Gas::add_assign, adjust size_of for BigInt/Decimal * gas: Adjust costs, log gas used * runtime: Test gas usage * gas: Justify the value of HOST_EXPORT_GAS * gas: Adjust cost of Ethereum calls * gas: Remove dead trait ConstGasSizeOf * gas: Move gas module from runtime to graph crate This will probably be necessary for multiblockchain. * gas: Fix `GasSizeOf` impl for `Entity` * gas: Make MAX_GAS_PER_HANDLER configurable for debugging * runtime: Fix `ens_name_by_hash` error message * gas: Cost `log.log` per byte * gas: Reduce limit to 1000 seconds * gas: Fix rebase artifact * gas: Update tests for gas limit change * runtime: Fix rebase artifacts in tests * runtime: Fix tests gas costs based on api version * gas: gas costing for ethereum calls * gas: Remove ambiguous impls * instance manager: Print backtrace of start subgraph error * runtime: Support sign extension instructions Co-authored-by: Zac Burns <[email protected]> Co-authored-by: Leo Yvens <[email protected]>
1 parent 5a6e38a commit 5074ac9

File tree

22 files changed

+1347
-167
lines changed

22 files changed

+1347
-167
lines changed

Cargo.lock

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

chain/ethereum/src/runtime/runtime_adapter.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::{
77
};
88
use anyhow::{Context, Error};
99
use blockchain::HostFn;
10+
use graph::runtime::gas::Gas;
1011
use graph::runtime::{AscIndexId, IndexForAscTypeId};
1112
use graph::{
1213
blockchain::{self, BlockPtr, HostFnCtx},
@@ -23,6 +24,15 @@ use graph_runtime_wasm::asc_abi::class::{AscEnumArray, EthereumValueKind};
2324

2425
use super::abi::{AscUnresolvedContractCall, AscUnresolvedContractCall_0_0_4};
2526

27+
// Allow up to 1,000 ethereum calls. The justification is that we don't know how much Ethereum gas a
28+
// call takes, but we limit the maximum to 25 million. One unit of Ethereum gas is at least 100ns
29+
// according to these benchmarks [1], so 1000 of our gas. Assuming the worst case, an Ethereum call
30+
// should therefore consume 25 billion gas. This allows for 400 calls per handler with the current
31+
// limits.
32+
//
33+
// [1] - https://www.sciencedirect.com/science/article/abs/pii/S0166531620300900
34+
pub const ETHEREUM_CALL: Gas = Gas::new(25_000_000_000);
35+
2636
pub struct RuntimeAdapter {
2737
pub(crate) eth_adapters: Arc<EthereumNetworkAdapters>,
2838
pub(crate) call_cache: Arc<dyn EthereumCallCache>,
@@ -60,6 +70,8 @@ fn ethereum_call(
6070
wasm_ptr: u32,
6171
abis: &[Arc<MappingABI>],
6272
) -> Result<AscEnumArray<EthereumValueKind>, HostExportError> {
73+
ctx.gas.consume_host_fn(ETHEREUM_CALL)?;
74+
6375
// For apiVersion >= 0.0.4 the call passed from the mapping includes the
6476
// function signature; subgraphs using an apiVersion < 0.0.4 don't pass
6577
// the signature along with the call.

core/src/subgraph/instance_manager.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ where
223223
Err(err) => error!(
224224
err_logger,
225225
"Failed to start subgraph";
226-
"error" => format!("{}", err),
226+
"error" => format!("{:#}", err),
227227
"code" => LogCode::SubgraphStartFailure
228228
),
229229
}
@@ -879,7 +879,7 @@ async fn process_block<T: RuntimeHostBuilder<C>, C: Blockchain>(
879879
)
880880
.await
881881
{
882-
// Triggers processed with no errors or with only determinstic errors.
882+
// Triggers processed with no errors or with only deterministic errors.
883883
Ok(block_state) => block_state,
884884

885885
// Some form of unknown or non-deterministic error ocurred.

graph/src/blockchain/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use crate::{
1818
},
1919
data::subgraph::UnifiedMappingApiVersion,
2020
prelude::DataSourceContext,
21-
runtime::{AscHeap, AscPtr, DeterministicHostError, HostExportError},
21+
runtime::{gas::GasCounter, AscHeap, AscPtr, DeterministicHostError, HostExportError},
2222
};
2323
use crate::{
2424
components::{
@@ -300,6 +300,7 @@ pub struct HostFnCtx<'a> {
300300
pub logger: Logger,
301301
pub block_ptr: BlockPtr,
302302
pub heap: &'a mut dyn AscHeap,
303+
pub gas: GasCounter,
303304
}
304305

305306
/// Host fn that receives one u32 argument and returns an u32.

graph/src/data/store/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::{
22
components::store::{DeploymentLocator, EntityType},
33
prelude::{q, r, s, CacheWeight, EntityKey, QueryExecutionError},
4+
runtime::gas::{Gas, GasSizeOf},
45
};
56
use crate::{data::subgraph::DeploymentHash, prelude::EntityChange};
67
use anyhow::{anyhow, Error};
@@ -622,6 +623,12 @@ impl CacheWeight for Entity {
622623
}
623624
}
624625

626+
impl GasSizeOf for Entity {
627+
fn gas_size_of(&self) -> Gas {
628+
self.0.gas_size_of()
629+
}
630+
}
631+
625632
/// A value that can (maybe) be converted to an `Entity`.
626633
pub trait TryIntoEntity {
627634
fn try_into_entity(self) -> Result<Entity, Error>;

graph/src/data/store/scalar.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ impl BigDecimal {
6060
self.0.as_bigint_and_exponent()
6161
}
6262

63-
pub(crate) fn digits(&self) -> u64 {
63+
pub fn digits(&self) -> u64 {
6464
self.0.digits()
6565
}
6666

graph/src/runtime/gas/combinators.rs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
use super::{Gas, GasSizeOf};
2+
use std::cmp::{max, min};
3+
4+
pub mod complexity {
5+
use super::*;
6+
7+
// Args have additive linear complexity
8+
// Eg: O(N₁+N₂)
9+
pub struct Linear;
10+
// Args have multiplicative complexity
11+
// Eg: O(N₁*N₂)
12+
pub struct Mul;
13+
14+
// Exponential complexity.
15+
// Eg: O(N₁^N₂)
16+
pub struct Exponential;
17+
18+
// There is only one arg and it scales linearly with it's size
19+
pub struct Size;
20+
21+
// Complexity is captured by the lesser complexity of the two args
22+
// Eg: O(min(N₁, N₂))
23+
pub struct Min;
24+
25+
// Complexity is captured by the greater complexity of the two args
26+
// Eg: O(max(N₁, N₂))
27+
pub struct Max;
28+
29+
impl GasCombinator for Linear {
30+
#[inline(always)]
31+
fn combine(lhs: Gas, rhs: Gas) -> Gas {
32+
lhs + rhs
33+
}
34+
}
35+
36+
impl GasCombinator for Mul {
37+
#[inline(always)]
38+
fn combine(lhs: Gas, rhs: Gas) -> Gas {
39+
Gas(lhs.0.saturating_mul(rhs.0))
40+
}
41+
}
42+
43+
impl GasCombinator for Min {
44+
#[inline(always)]
45+
fn combine(lhs: Gas, rhs: Gas) -> Gas {
46+
min(lhs, rhs)
47+
}
48+
}
49+
50+
impl GasCombinator for Max {
51+
#[inline(always)]
52+
fn combine(lhs: Gas, rhs: Gas) -> Gas {
53+
max(lhs, rhs)
54+
}
55+
}
56+
57+
impl<T> GasSizeOf for Combine<T, Size>
58+
where
59+
T: GasSizeOf,
60+
{
61+
fn gas_size_of(&self) -> Gas {
62+
self.0.gas_size_of()
63+
}
64+
}
65+
}
66+
67+
pub struct Combine<Tuple, Combinator>(pub Tuple, pub Combinator);
68+
69+
pub trait GasCombinator {
70+
fn combine(lhs: Gas, rhs: Gas) -> Gas;
71+
}
72+
73+
impl<T0, T1, C> GasSizeOf for Combine<(T0, T1), C>
74+
where
75+
T0: GasSizeOf,
76+
T1: GasSizeOf,
77+
C: GasCombinator,
78+
{
79+
fn gas_size_of(&self) -> Gas {
80+
let (a, b) = &self.0;
81+
C::combine(a.gas_size_of(), b.gas_size_of())
82+
}
83+
84+
#[inline]
85+
fn const_gas_size_of() -> Option<Gas> {
86+
if let Some(t0) = T0::const_gas_size_of() {
87+
if let Some(t1) = T1::const_gas_size_of() {
88+
return Some(C::combine(t0, t1));
89+
}
90+
}
91+
None
92+
}
93+
}
94+
95+
impl<T0, T1, T2, C> GasSizeOf for Combine<(T0, T1, T2), C>
96+
where
97+
T0: GasSizeOf,
98+
T1: GasSizeOf,
99+
T2: GasSizeOf,
100+
C: GasCombinator,
101+
{
102+
fn gas_size_of(&self) -> Gas {
103+
let (a, b, c) = &self.0;
104+
C::combine(
105+
C::combine(a.gas_size_of(), b.gas_size_of()),
106+
c.gas_size_of(),
107+
)
108+
}
109+
110+
#[inline] // Const propagation to the rescue. I hope.
111+
fn const_gas_size_of() -> Option<Gas> {
112+
if let Some(t0) = T0::const_gas_size_of() {
113+
if let Some(t1) = T1::const_gas_size_of() {
114+
if let Some(t2) = T2::const_gas_size_of() {
115+
return Some(C::combine(C::combine(t0, t1), t2));
116+
}
117+
}
118+
}
119+
None
120+
}
121+
}
122+
123+
impl<T0> GasSizeOf for Combine<(T0, u8), complexity::Exponential>
124+
where
125+
T0: GasSizeOf,
126+
{
127+
fn gas_size_of(&self) -> Gas {
128+
let (a, b) = &self.0;
129+
Gas(a.gas_size_of().0.saturating_pow(*b as u32))
130+
}
131+
132+
#[inline]
133+
fn const_gas_size_of() -> Option<Gas> {
134+
None
135+
}
136+
}

graph/src/runtime/gas/costs.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//! Stores all the gas costs is one place so they can be compared easily.
2+
//! Determinism: Once deployed, none of these values can be changed without a version upgrade.
3+
4+
use super::*;
5+
use lazy_static::lazy_static;
6+
use std::str::FromStr;
7+
8+
/// Using 10 gas = ~1ns for WASM instructions.
9+
const GAS_PER_SECOND: u64 = 10_000_000_000;
10+
11+
/// Set max gas to 1000 seconds worth of gas per handler. The intent here is to have the determinism
12+
/// cutoff be very high, while still allowing more reasonable timer based cutoffs. Having a unit
13+
/// like 10 gas for ~1ns allows us to be granular in instructions which are aggregated into metered
14+
/// blocks via https://docs.rs/pwasm-utils/0.16.0/pwasm_utils/fn.inject_gas_counter.html But we can
15+
/// still charge very high numbers for other things.
16+
const CONST_MAX_GAS_PER_HANDLER: u64 = 1000 * GAS_PER_SECOND;
17+
18+
lazy_static! {
19+
/// This is configurable only for debugging purposes. This value is set by the protocol,
20+
/// so indexers running in the network should never set this config.
21+
pub static ref MAX_GAS_PER_HANDLER: u64 = std::env::var("GRAPH_MAX_GAS_PER_HANDLER")
22+
.ok()
23+
.map(|s| {
24+
u64::from_str(&s.replace("_", "")).unwrap_or_else(|_| {
25+
panic!("GRAPH_LOAD_WINDOW_SIZE must be a number, but is `{}`", s)
26+
})
27+
})
28+
.unwrap_or(CONST_MAX_GAS_PER_HANDLER);
29+
}
30+
31+
/// Gas for instructions are aggregated into blocks, so hopefully gas calls each have relatively
32+
/// large gas. But in the case they don't, we don't want the overhead of calling out into a host
33+
/// export to be the dominant cost that causes unexpectedly high execution times.
34+
///
35+
/// This value is based on the benchmark of an empty infinite loop, which does basically nothing
36+
/// other than call the gas function. The benchmark result was closer to 5000 gas but use 10_000 to
37+
/// be conservative.
38+
pub const HOST_EXPORT_GAS: Gas = Gas(10_000);
39+
40+
/// As a heuristic for the cost of host fns it makes sense to reason in terms of bandwidth and
41+
/// calculate the cost from there. Because we don't have benchmarks for each host fn, we go with
42+
/// pessimistic assumption of performance of 10 MB/s, which nonetheless allows for 10 GB to be
43+
/// processed through host exports by a single handler at a 1000 seconds budget.
44+
const DEFAULT_BYTE_PER_SECOND: u64 = 10_000_000;
45+
46+
/// With the current parameters DEFAULT_GAS_PER_BYTE = 1_000.
47+
const DEFAULT_GAS_PER_BYTE: u64 = GAS_PER_SECOND / DEFAULT_BYTE_PER_SECOND;
48+
49+
/// Base gas cost for calling any host export.
50+
/// Security: This must be non-zero.
51+
pub const DEFAULT_BASE_COST: u64 = 100_000;
52+
53+
pub const DEFAULT_GAS_OP: GasOp = GasOp {
54+
base_cost: DEFAULT_BASE_COST,
55+
size_mult: DEFAULT_GAS_PER_BYTE,
56+
};
57+
58+
/// Because big math has a multiplicative complexity, that can result in high sizes, so assume a
59+
/// bandwidth of 100 MB/s, faster than the default.
60+
const BIG_MATH_BYTE_PER_SECOND: u64 = 100_000_000;
61+
const BIG_MATH_GAS_PER_BYTE: u64 = GAS_PER_SECOND / BIG_MATH_BYTE_PER_SECOND;
62+
63+
pub const BIG_MATH_GAS_OP: GasOp = GasOp {
64+
base_cost: DEFAULT_BASE_COST,
65+
size_mult: BIG_MATH_GAS_PER_BYTE,
66+
};
67+
68+
// Allow up to 100,000 data sources to be created
69+
pub const CREATE_DATA_SOURCE: Gas = Gas(CONST_MAX_GAS_PER_HANDLER / 100_000);
70+
71+
pub const LOG_OP: GasOp = GasOp {
72+
// Allow up to 100,000 logs
73+
base_cost: CONST_MAX_GAS_PER_HANDLER / 100_000,
74+
size_mult: DEFAULT_GAS_PER_BYTE,
75+
};
76+
77+
// Saving to the store is one of the most expensive operations.
78+
pub const STORE_SET: GasOp = GasOp {
79+
// Allow up to 250k entities saved.
80+
base_cost: CONST_MAX_GAS_PER_HANDLER / 250_000,
81+
// If the size roughly corresponds to bytes, allow 1GB to be saved.
82+
size_mult: CONST_MAX_GAS_PER_HANDLER / 1_000_000_000,
83+
};
84+
85+
// Reading from the store is much cheaper than writing.
86+
pub const STORE_GET: GasOp = GasOp {
87+
base_cost: CONST_MAX_GAS_PER_HANDLER / 10_000_000,
88+
size_mult: CONST_MAX_GAS_PER_HANDLER / 10_000_000_000,
89+
};
90+
91+
pub const STORE_REMOVE: GasOp = STORE_SET;

0 commit comments

Comments
 (0)