Skip to content

Dynamic Plasma: blocks can be under-funded if the fusion price rises between quote and inclusion #37

Open

Description

@digitalSloth

Problem

getRequiredPoWForAccountBlock quotes the requirement against the frontier momentum at quote time. go-zenon judges the block against the frontier at inclusion time (verifier/momentum.go and pillar/content_selector.go both use dp.NewDynamicPlasma(previousMomentum, ...)). If nextFusionPrice rises in between, a block that met the quote is under-funded and is rejected or stalls.

Example (zero-difficulty path)

  1. The SDK reads the frontier: nextFusionPrice = 1000.
  2. A new momentum raises the price to 1200.
  3. The node quotes { basePlasma: 21000, availablePlasma: 25200, requiredDifficulty: 0 }. This is valid, because 25200 >= ceil(21000 * 1200 / 1000).
  4. prepareBlock computes ceil(21000 * 1000 / 1000) = 21000 from the price it read in step 1. The 25200 >= 21000 check passes, so the SDK signs 21,000 where the node now requires 25,200.

Even if the SDK used the price from step 3, a rise between signing and inclusion produces the same failure. Any RPC client that signs exactly the quoted minimum has this exposure.

The window is widest on the PoW path, where generation can outlast one or more momentums.

Mitigation options

SDK:

  1. Sign with headroom over the minimum (fused amount, difficulty, or both).
  2. Re-quote after PoW and redo the work if the requirement rose.
  3. Re-read the frontier around the quote and retry if it changed. This closes only the SDK read-to-quote gap, not the quote-to-inclusion gap, so it needs one of the other options to be a full fix.

Node (go-zenon):
4. Return the price context (frontier hash or nextFusionPrice) with the quote, so clients can detect a stale quote.
5. Return a margin or a required fused amount that already allows for the maximum price step per momentum (maxPriceChangePercent in the plasma variables).

Open questions

  • How much headroom does the price step bound (maxPriceChangePercent / priceChangeDenominator) imply per momentum?
  • Is a node-side change acceptable, or must clients handle this alone?

References

Activity

  1. mrstockholmz commented on Oct 9, 2026

    @mrstockholmz

    Traced the go-zenon side (checked against a local checkout). The asymmetry is real:

    Quote (rpc/api/embedded/plasma.go, dynamic-plasma branch): requiredFusedPlasma = ceil(basePlasma * frontierMomentum.NextFusionPrice / PriceScaleFactor). It uses the frontier momentum's current price.

    Inclusion (verifier/momentum.go, pillar/worker_momentum.go): both build dp.NewDynamicPlasma(previousMomentum, config) from the momentum preceding the one being verified or produced, and NewDynamicPlasma takes that momentum's NextFusionPrice. So a block is judged against the price at inclusion time, not the quote-time price.

    Bound: dp/dp.go clamps each momentum's price move to MaxPriceChangePercent (default 10, governance-configurable, range 1-100). The worst-case drift over N momentums is therefore (1 + p/100)^N, which is about 1.21x for 2 momentums at the default. Momentums are roughly 10s apart, so a PoW run of a few minutes spans tens of momentums. That is a theoretical ceiling, and real drift will normally be far smaller.

    SDK mitigations:

    1. Sign with headroom over the quoted minimum (fused amount, difficulty, or both).
    2. Re-quote after PoW and redo the work if the requirement rose.
    3. Fused-only path (no PoW): the gap is a momentum or two, so about 1.25x headroom covers 2 momentums at the default cap, and it is cheap.

    Node-side option: return the frontier momentum identifier (or nextFusionPrice) with the quote so clients can detect staleness. It adds one field to GetRequiredResult and is backward-compatible. It closes the read-to-quote gap but not the quote-to-inclusion gap, so client headroom is still needed for that.

    Happy to sketch a PR for the context hint if useful.

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions