@agoric/transform-metering

transform-metering

Usage no npm install needed!

<script type="module">
  import agoricTransformMetering from 'https://cdn.skypack.dev/@agoric/transform-metering';
</script>

README

Transform Metering

The purpose of this package is to provide a loose, but deterministic way to interrupt Javascript code that exceeds a "meter".

This technique is not airtight, but it is at least is a best approximation in the absence of an instrumented host platform.

Quickstart

SES 1.0

The following example is for legacy SES-1.0:

import { SES1TameMeteringShim, SES1ReplaceGlobalMeter } from '@agoric/tame-metering';
import SES1 from 'ses';
import * as babelCore from '@babel/core';
import { makeMeteredEvaluator } from '@agoric/transform-metering';

// Create a new SES instance (root realm).
const sesRealm = SES1.makeSESRootRealm({
  shims: [SES1TameMeteringShim],
  configurableGlobals: true,
});

const replaceGlobalMeter = SES1ReplaceGlobalMeter(sesRealm);

const meteredEval = makeMeteredEvaluator({
  // Needed for enabling metering of the global builtins.
  replaceGlobalMeter,
  // Needed for source transforms that prevent runaways.
  babelCore,
  // Create an object with an `evaluate(src, endowments, options)` method
  makeEvaluator: opts => {
    const c = sesRealm.global.Realm.makeCompartment(opts);
    // FIXME: Realms bug doesn't propagate global properties.
    Object.defineProperties(
      c.global,
      Object.getOwnPropertyDescriptors(sesRealm.global),
    );
    return c;
  },
  // Call a callback when the code inside the meteredEval is done evaluating.
  quiesceCallback: cb => setTimeout(cb),
});

SES 2.0

This example is for the SES 2.0 (beta). Note that it currently has issues that prevents it from working correctly, but YMMV.

import { tameMetering } from '@agoric/tame-metering';
import { lockdown } from 'ses';
import * as babelCore from '@babel/core';
import { makeMeteredEvaluator } from '@agoric/transform-metering';

// Override all the global objects with metered versions.
const replaceGlobalMeter = tameMetering();

// Enter SES.
lockdown();

const meteredEval = makeMeteredEvaluator({
  // Needed for enabling metering of the global builtins.
  replaceGlobalMeter,
  // Needed for source transforms that prevent runaways.
  babelCore,
  // Create an object with an `evaluate(src, endowments)` method
  makeEvaluator: opts => {
    const c = new Compartment(undefined, undefined, opts);
    return {
      evaluate(src, endowments = {}) {
        return c.evaluate(src, { endowments });
      }
    }
  },
  // Call a callback when the code inside the meteredEval is done evaluating.
  quiesceCallback: cb => setTimeout(cb),
});

Using the new meteredEval

import { makeMeter } from '@agoric/transform-metering';

const { meter } = makeMeter();

// Then to evaluate some source with endowments:
meteredEval(meter, 'abc + def', {abc: 123, def: 456}).then(
  ([normalReturn, value, seenMeters]) => {
    for (const m of seenMeters) {
      const ex = m.isExhausted();
      if (ex) {
        console.log('meter', m, 'was exhausted with', ex);;
      }
    }
    if (normalReturn) {
      console.log('normal return', value);
    } else {
      console.log('exception', value);
    }
  }
);

Implementation Details

Meter types

There are three types of meters:

  • Allocation meters. These decrement a budget by a cost roughly proportional to the storage size of the provided argument.

  • Compute meters. These only decrement a budget by a cost roughly proportional to the execution time of the code that follows them.

  • Stack meters. These track the depth of stack used between nested calls to decrement and increment the meter.

Another type of meter is the "combined" meter, which can measure the consumption of more than one of the above against the same budget.

The transformation

To instrument source code robustly, the metering transform should be installed as part of an 3-argument evaluator call (such as the one provided by SES, Secure EcmaScript).

The deterministic results of exceeding any meter is a thrown exception. That same exception is rethrown by any meter adjustment, until the exceeded meter is reset. No user code can execute after a meter exception because any attempt to catch or finally handle that exception is instrumented with another meter check.

Thus, an exceeded meter for any reason throws a RangeError all the way to the top of the evaluation, in the calling host code.

Function bodies

This transformation replaces every function body (including single-expression arrow functions) with:

  try {
    [decrement stack meter, throw if exceeded]
    ... // existing function body
  } finally {
    [increment stack meter]
  }

Loop statements

Every loop body (including single-statement bodies), and catch and finally bodies are preceded with:

   {
     [decrement compute meter, throw if exceeded]
     ... // existing loop body
   }

RegExp literals

All regular expression literals (such as /some-regexp/g) are rewritten as follows:

const $h_re_1 = RegExp('some-regexp', 'g');
... // existing use of /some-regexp/g replaced by $h_re_1

The $h_re_-prefixed identifiers are blacklisted for pre-transformed evaluated sources.

This makes it possible for an endowedd RegExp constructor to prevent "catastrophic backtracking". One such suitable constructor is RE2.

Host endowments

Without precisely instrumenting the host platform code, this package provides an option to wrap a global object with code that does a rough instrumentation of function calls.

The reason for this wrapping is to provide some basic accounting for the resources consumed by the host platform methods. The wrapping makes some assumptions about the host, such as:

  1. only flat objects are allocated by builtins, and they are returned by the builtin
  2. builtins that are costly in time- or space-complexity have large return values, or many calls to a supplied function argument
  3. builtins have been "tamed" by the SES platform to prevent nondeterminism and pathological behaviour

This at least prevents user code from running after a builtin has exceeded a meter.