Skip to content

PLT-465: wire key/size/op distributions into StorageRW - #54

Merged
bdchatham merged 6 commits into
mainfrom
brandon2/plt-465-m33-wire-key-distslot-and-size-distcalldata-into-storagerw
Aug 20, 2026
Merged

PLT-465: wire key/size/op distributions into StorageRW#54
bdchatham merged 6 commits into
mainfrom
brandon2/plt-465-m33-wire-key-distslot-and-size-distcalldata-into-storagerw

Conversation

@bdchatham

@bdchatham bdchatham commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Implements PLT-465 (M3.3) — turns StorageRW into the two customer-named axes (key-contention + tx-size).

What (generator/scenarios/StorageRW.go)

  • slot index = keyDist.SampleIndex(recordcount); op (read/write/rmw) by configured proportions; calldata pad length = sizeDist bucket draw.
  • Independent seeded sub-streams per axis: dist:%d:key, dist:%d:size (existing), dist:%d:op (new, append-only to the FROZEN set) — changing one axis never perturbs another's sequence (guarded by a determinism test).
  • Nil-guarded like the gas-picker: no distribution config → fixed slot 0 / rmw / empty pad, byte-identical to the PLT-461 scaffold (consumes zero randomness; account cadence untouched).
  • Pad gas added as intrinsic EIP-2028 cost (4/byte) on top of the 50k base — empty pad = exactly today's 50k; large pad can't underprovision.

One-way door (flagged)

The new dist:%d:op sub-stream id is an append-only addition to the FROZEN seed→stream contract (doesn't reseed existing streams; expands the saved-replay surface).

Verify

make lint 0 issues · go build · go test -race ./... green. Tests: contention sweep, size-bucket histogram, key/size + key/op independence, op-mix, byte-identical default, additive config.

🤖 Generated with Claude Code


📐 Design: decision brief — PLT-465 · parent design

Turn StorageRW into the contention + size axes: slot = keyDist.SampleIndex(
recordcount), op (read/write/rmw) by configured proportions, calldata pad =
sizeDist bucket draw — each on an INDEPENDENT seeded sub-stream so changing
one axis never perturbs another. Nil-guarded like the gas-picker: no
distribution config => fixed slot 0 / rmw / empty pad, byte-identical to the
PLT-461 scaffold (consumes zero randomness, account cadence untouched).

Adds one append-only sub-stream id (dist:%d:op) to the FROZEN set per the
append-only derivation rule; pad gas added as intrinsic EIP-2028 cost so it
can't underprovision.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cursor

cursor Bot commented Jun 15, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes load-test transaction shape, gas limits, and reproducibility semantics for StorageRW profiles; misconfiguration is mitigated by new validation, but benchmark comparisons require matching configs not just seeds.

Overview
StorageRW no longer emits a fixed slot-0 empty-pad rmw for every tx. Each transaction now optionally draws a storage slot from keyDistribution over recordCount, a calldata pad length from sizeDistribution over sizeBuckets, and read / write / rmw from operations, then calls the matching contract method. Draw order is fixed (slot → pad → operation) on the run’s single PRNG; docs and tests treat seed + full config as the replay contract (enabling an axis shifts other axes’ sequences).

Config adds frozen JSON fields recordCount, sizeBuckets, and operations (OperationMix), plus Scenario.Validate / LoadConfig.ValidateScenarios to reject half-paired axes, bad pad/keyspace sizes, and empty operation mixes. loadConfig and profile CI tests call validation at load time.

Gas stays at a 50k execution base and adds pad cost at the EIP-7623 floor (10 gas per ABI-padded byte), with validation capping pad buckets at 128 KiB to avoid admitted-but-failing txs on Sei.

Extensive unit and scenario tests cover contention, size buckets, op mix, default-path backward compatibility (no RNG use), draw-order golden sequence, and gas vs floor across pad sizes.

Reviewed by Cursor Bugbot for commit d0d0f4f. Bugbot is set up for automated code reviews on this repo. Configure here.

bdchatham and others added 2 commits June 15, 2026 15:50
…view)

- Scenario.Validate rejects SizeBuckets[i] < 0 (make([]byte,-1) panic on the
  pickPad hot path) and > maxCalldataPadBytes (1 MiB OOM guard); wired via
  LoadConfig.ValidateScenarios in loadConfig, mirroring ValidateFunding.
- Inline OperationMix.stream (drop the one-field opStreamHolder wrapper) to
  match GasPicker/Distribution; move the mis-subjected doc comment to the field.

No change to draw behavior — byte-identical-default + independence tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
De-changelog the doc.go StorageRW narrative (drop 'PLT-465 turns it into',
'PLT-461 scaffold byte-for-byte') to a timeless present-tense description, and
replace the now-dangling era term 'scaffold' with 'default' in inline
default-path notes. Keep load-bearing comments (FROZEN append-only stream-id
ledger, EIP-2028 gas-per-byte why, nil-guard/default invariants, the
no-underprovision note). Comment-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread config/config.go Outdated
Comment thread config/operation.go
bdchatham and others added 3 commits August 19, 2026 19:40
The generator rewrite (890ce1e, #56) deleted utils/rng and replaced named
per-axis PRNG sub-streams with a single *mrand.Rand threaded per call. This
branch adapts to that model rather than restoring the deleted package.

OperationMix.Select, pickSlot, pickPad, and pickOp now take the RNG that
CreateContractTransaction already receives, matching Distribution and GasPicker.
The draw order — slot, pad, operation — is fixed and documented, since the three
axes now share one stream.

Per-axis independence is therefore no longer provided, so the two tests that
guarded it are dropped rather than left asserting something untrue. Reproducible
seeding is tracked separately.

The additive guarantee is unchanged and now directly tested: an unconfigured
scenario produces the same fixed rmw and draws no randomness, asserted by
comparing the handed RNG against an untouched one at the same seed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Seed doc promised deterministic PRNG sub-streams and a per-stream draw
multiset "for fair A/B comparison". The generator rewrite removed sub-streams,
so that text sold a property the code no longer has — and it sold it around the
exact task an operator would use it for. An operator changing one axis and
attributing the delta to that axis would have been wrong.

The contract now stated: one stream serves the whole run, the config is half of
the contract because it fixes the call order, and two runs compare only when
their configs match. Swept the same stale vocabulary out of config/doc.go,
sender/doc.go, and a distribution test comment.

Also corrected in the StorageRW docs:
- the fixed draw order carried no obligation and overclaimed, since a default
  config runs zero draws, not three
- the op mix was called a "distribution", which it is not — Distribution is a
  tagged type keyed by "Name", and a reader following that wording would have
  written a config that silently selects all-rmw
- the gas rationale justified the 50k base for rmw alone, though read and write
  now share it
- the SizeBuckets cap was documented only on an unexported const, so an operator
  met it as a load error rather than in the field doc

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…8 rate

The pad was charged at 4 gas per byte, the pre-Prague zero-byte rate. EIP-7623
is live on Sei (PragueTime is 0) and charges a transaction no less than
21000 + 10 per calldata token, so above roughly 4.6 KiB of pad the floor is the
real cost and the declared limit fell under it. Measured against the pinned
go-ethereum: short by 174 gas at a 4609-byte pad, and by 6,263,636 at 1 MiB.

Sei's ante checks only the intrinsic cost, not the floor, so such a transaction
is admitted, reserves its full declared limit, then fails in execution with
GasUsed equal to the limit. It lands in a block as an included failure and
inflates the gas-used metric the run reports — a plausible-looking result
measuring nothing.

The pad is now charged at 10 gas per on-wire byte, rounded up to the ABI word
the pad actually occupies. An empty pad still leaves the limit at exactly 50k,
so density below the crossover is unchanged.

TestStorageRWGasClearsFloorAcrossPadSizes asserts the limit clears both
core.IntrinsicGas and core.FloorDataGas across a pad sweep. Verified it fails on
the old rate at four pad sizes, so it guards the invariant rather than
restating the formula — which is what the previous assertion did.

Validation now rejects the configurations that silently ran the baseline instead
of the requested experiment: a half-configured axis in either direction, an
explicit all-zero operations mix, weights that sum past uint64, and a keyspace
beyond the point where the zipfian zeta precompute stalls the generator. The pad
cap drops to 128 KiB, under both the CometBFT per-transaction limit and a
sensible share of the block gas budget. Committed profiles now run scenario
validation in CI.

Documented what the measurements showed: read is the worst case at 46,269, not
rmw; every read writes one contract-wide accumulator slot, so a read-weighted
mix does not sweep contention; and the new wire keys are frozen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@bdchatham
bdchatham merged commit 359db44 into main Aug 20, 2026
4 checks passed
@bdchatham
bdchatham deleted the brandon2/plt-465-m33-wire-key-distslot-and-size-distcalldata-into-storagerw branch August 20, 2026 03:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants