0005-gas-costing
Abstract
This GIP proposes that the computational cost of a handler be measured in units of gas and limited to a maximum cost. If a handler attempts to consume more gas than the maximum, the subgraph will fail with a deterministic error. This is necessary so that it is possible to prove that a subgraph cannot be indexed past the block which triggers such a handler.
Motivation
Subgraph handlers that run for too long are an issue for operators, since they excessively consume resources, and also a issue for protocol security since they make the subgraph impossible to sync in a reasonable time, if it can be synced at all. Graph Node currently implements a timeout for handlers, which is useful to prevent excessive resource consumption, however timeouts are not deterministic. For protocol security, it is necessary to deterministically measure the cost of a handler. This GIP proposes gas costing for handlers, similar to the gas costing for transactions that exists in blockchain protocols.
Detailed Specification
Gas is defined as the unit of execution cost. As a reference, one unit of gas should correspond to 0.1ns of execution time, making for 10 billion gas in a second. Close correlation with execution time is not a primary goal of gas costing, at least not in this first iteration. Still gas needs to have some correlation to execution time in order to be useful.
The maximum gas cost for a handler should be sufficient for any reasonable subgraph, and be limited by the cost a Fisherman or Arbitrator would be willing to incur in order to check an attestation. The proposed limit is 1000 seconds of execution time, which corresponds to 10 trillion gas.
The costs incurred by executing a handler can be categorized as either WASM execution costs or host function execution costs. WASM execution costs must be measured by injecting a callback at the block, see the documentation for this technique in the pwasm-utils crate. For the gas costing of individual WASM instructions, refer to the implementation in this commit.
The cost of most host functions is calculated as a base cost added to a cost per byte of input. To account for host functions that have non-linear complexity, the input bytes are first combined through a complexity function and then multiplied by the cost per byte. To calculate the size of the input bytes, a gas_size_of
function exists for each data type that is an input to a host function, see implementations of GasSizeOf
in the reference implementation for details. Note that gas_size_of
is an approximation of the true in-memory size of the data, the actual size may vary between platforms and implementations but still this approximation should be sufficient for gas costing purposes.
Constants
This section specifies the constants used for applying a cost to host functions.
Table 1:
GAS_PER_SECOND
10_000_000_000
Relates time and gas units.
MAX_GAS_PER_HANDLER
3600 * GAS_PER_SECOND
The limit is one hour worth of gas.
HOST_EXPORT_GAS
10_000
Cost of the callback that measures gas.
DEFAULT_BASE_COST
100_000
Base cost of most host functions.
DEFAULT_GAS_PER_BYTE
1_000
Equivalent to 10MB/s, so 10GB in the 1000 seconds limit.
BIG_MATH_GAS_PER_BYTE
100
Equivalent to 100MB/s.
ETHEREUM_CALL
25_000_000_000
Gas cost of a contract call. Allows for 400 contract calls.
CREATE_DATA_SOURCE
MAX_GAS_PER_HANDLER / 100_000
Cost of dataSource.create
.
LOG_GAS
MAX_GAS_PER_HANDLER / 100_000
Base cost of log.log
.
STORE_SET_BASE_COST
MAX_GAS_PER_HANDLER / 250_000
Base cost of store.set
.
STORE_SET_GAS_PER_BYTE
MAX_GAS_PER_HANDLER / 1_000_000_000
Allows 1GB of entity data for store.set
.
STORE_GET_BASE_COST
MAX_GAS_PER_HANDLER / 10_000_000
Base cost of store.get
.
STORE_GET_GAS_PER_BYTE
MAX_GAS_PER_HANDLER / 1_000_000_000
Allows 10GB of entity data for store.get
.
Costs for host functions
This section specifies the costs attributed to each host function.
Most host functions have linear complexity on a single parameter N
and are costed as:
Host functions that differ from this are listed in the table below:
abort
DEFAULT_BASE_COST
store.set(key, data)
STORE_SET_BASE_COST + STORE_SET_GAS_PER_BYTE * (gas_size_of(key) + gas_size_of(data))
store.remove(key)
STORE_SET_BASE_COST + STORE_SET_GAS_PER_BYTE * gas_size_of(key)
store.get(key) -> data
STORE_GET_BASE_COST + STORE_GET_GAS_PER_BYTE * (gas_size_of(key) + gas_size_of(data))
ethereum.call
ETHEREUM_CALL
dataSource.create
CREATE_DATA_SOURCE
log.log(m)
LOG_GAS + DEFAULT_GAS_PER_BYTE * gas_size_of(m)
bigInt.plus(x, y)
DEFAULT_BASE_COST + BIG_MATH_GAS_PER_BYTE * max(gas_size_of(x), gas_size_of(y))
bigInt.minus(x, y)
DEFAULT_BASE_COST + BIG_MATH_GAS_PER_BYTE * max(gas_size_of(x), gas_size_of(y))
bigInt.times(x, y)
DEFAULT_BASE_COST + BIG_MATH_GAS_PER_BYTE * gas_size_of(x) * gas_size_of(y)
bigInt.divided_by(x, y)
DEFAULT_BASE_COST + BIG_MATH_GAS_PER_BYTE * gas_size_of(x) * gas_size_of(y)
bigInt.mod(x, y)
DEFAULT_BASE_COST + BIG_MATH_GAS_PER_BYTE * gas_size_of(x) * gas_size_of(y)
bigInt.pow(x, exp)
DEFAULT_BASE_COST + BIG_MATH_GAS_PER_BYTE * gas_size_of(x) ^ exp
bigInt.bit_or(x, y)
DEFAULT_BASE_COST + BIG_MATH_GAS_PER_BYTE * max(gas_size_of(x), gas_size_of(y))
bigInt.bit_and(x, y)
DEFAULT_BASE_COST + BIG_MATH_GAS_PER_BYTE * min(gas_size_of(x), gas_size_of(y))
bigDecimal.plus(x, y)
DEFAULT_BASE_COST + BIG_MATH_GAS_PER_BYTE * (gas_size_of(x) + gas_size_of(y))
bigDecimal.minus(x, y)
DEFAULT_BASE_COST + BIG_MATH_GAS_PER_BYTE * (gas_size_of(x) + gas_size_of(y))
bigDecimal.times(x, y)
DEFAULT_BASE_COST + BIG_MATH_GAS_PER_BYTE * gas_size_of(x) * gas_size_of(y)
bigDecimal.divided_by(x, y)
DEFAULT_BASE_COST + BIG_MATH_GAS_PER_BYTE * gas_size_of(x) * gas_size_of(y)
bigDecimal.equals(x, y)
DEFAULT_BASE_COST + BIG_MATH_GAS_PER_BYTE * min(gas_size_of(x), gas_size_of(y))
Last updated