PLT-465: wire key/size/op distributions into StorageRW - #54
Conversation
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>
PR SummaryMedium Risk Overview Config adds frozen JSON fields 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. |
…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>
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>
Implements PLT-465 (M3.3) — turns StorageRW into the two customer-named axes (key-contention + tx-size).
What (
generator/scenarios/StorageRW.go)keyDist.SampleIndex(recordcount); op (read/write/rmw) by configured proportions; calldata pad length =sizeDistbucket draw.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).One-way door (flagged)
The new
dist:%d:opsub-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 lint0 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