Skip to content

Reduce peek memory use. #125

Open
Open

Description

@roconnor-blockstream

Recent analysis shows that during Simplicity execution, peek memory use could be as high as 720 bytes per DAG node. We should rearrange computations and allocations to try to reduce that as much as possible. Ideally I'd like to see less than 128 bytes per DAG node if possible.

  • Postpone the CMR computation until after type inference, and move the CMR out of the dag structure. Only the Root CMR and one CMR per disconnect node is needed, so perhaps there is a way to keep only these placing the CMR in a union in the dag structure only accessible to disconnect.
  • Move the TMR out of the type structure. Compute it as part of the IMR computation and free it afterwards. (The IMR calculation seems to require up to 4*32 bytes per DAG node to just by itself, so keeping peek memory use under 128 bytes per DAG node seems impossible).
  • Replace all size_t values with uint32_t values. Ensure that the maximum index of arrays is less than 2^32.
  • Replace all unification_var* with a uint32_t value and rearrange the unification_var allocations so that they all are in one single array.
  • make an isChild field in unification_var instead of using a NULL pointer. Make the parent pointer part of a union.

Activity

  1. apoelstra commented on Sep 23, 2026

    @apoelstra
    Collaborator

    Thinking about this a bit more, since we've gotten multiple security reports over the last few months that ultimately boil down to this issue.

    In addition to reducing the peak memory usage for the type checker, we could also move several checks in front of typechecking. In particular, we should be able to:

    1. "Pre-decode" the whole program just to count how many DAG nodes there are; early-reject if this doesn't match the length prefix or if it exceeds DAG_LEN_MAX. (Also DAG_LEN_MAX should be configurable so we can reduce it in policy, if need be). Only if this passes will we allocate any memory at all. This eliminates some particularly egregious memory attacks where you can goad a node into allocating a bunch of memory just in response to a big length prefix.
    2. Then do a second pass in which we compute the CMR but nothing else, which will cost us ~32 bytes per dag node. Afterward we can throw the CMR away except for disconnect nodes; we won't need the root CMR or any intermediate CMRs again. Doing this early means that an attacker will need an actual UTXO that commits to a well-formed program before they can trigger type-inference memory allocations.
    3. We may want to stick an extra "maximum number of disconnects" knob in there as well which can be tuned by policy which will limit the amount of space used for these CMRs.
    4. Then we do the real analysis where we do type inference, compute IMRs, weight budget, etc etc.

    Certainly step (1) is worth doing, because it's super easy and prevents somebody from allocating a bunch of memory without even creating a super big transaction.

    The rest though are essentially just minor roadblocks. Ultimately it's always possible for somebody to trigger the maximum amount of memory allowed for type inference (and have their transaction fail afterward), so in the end we just need to reduce that maximum.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions