PLT-456: Seed → deterministic PRNG sub-stream derivation#44
Conversation
Pin a frozen derivation substream(seed, streamID) = NewPCG(seed, splitmix64(fnv1a64(streamID))) so same seed + config replays byte-identically. Sub-streams are keyed by a logical consumer id, never a live-goroutine counter, so the draw sequence is independent of --workers. Add utils/rng (Source + per-consumer Stream handle, mutex-guarded for concurrent draws). Thread the resolved seed through NewConfigBasedGenerator; unseeded runs get a random seed that is recorded on cfg.Seed and logged for after-the-fact replay (PLT-467 reads it there). Convert the three workload RNG call-sites to derived streams, falling back to the unseeded global RNG when no stream is bound: - config/gas.go RandomGasGenerator.GenerateGas - types/account_pool.go accountPool.NextAccount - generator/weighted.go NewWeightedGenerator startup shuffle Seed TestAccountPoolMixedRate so it asserts an exact reproducible split instead of a probabilistic tolerance. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review: the seed guarantees a reproducible per-stream draw multiset (statistically reproducible workload for A/B), not byte-identical ordered replay above one worker. Restate the contract honestly in the rng package doc and the config Seed comment; drop byte-identical claims. Freeze stream-ids and per-stream draw order alongside the formula and centralize the stream-id strings in utils/rng/streams.go so the frozen surface is reviewable in one place. Document the SetStream fixed/empty no-op. Tests: rename to TestWorkerCountMultisetInvariant (asserts the multiset), add TestSingleWorkerOrderedReplay (pins ordered replay at one worker), cover tip/feecap streams in the seeded config, and add a config-level guard that a bound random picker draws seed-determined values so a refactor breaking the pointer aliasing fails loudly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The worker-count invariant test held the mutex across gen.Generate(), so workers never drew concurrently, and it asserted per-tx gasDraw tuples — which are not worker-invariant: concurrent txs interleave their base/tip/feecap draws across three independently-locked streams, so tuples reassemble differently while each stream's multiset is unchanged. Run Generate() outside the work-claim lock so workers genuinely interleave, and assert each gas-stream column multiset independently rather than the tuple, matching the per-stream guarantee the contract states. Passes -race -count=10. Also freeze the per-tx account draw cadence (sender then receiver NextAccount) as FROZEN input #4, and correct the bindGasStreams aliasing comment: a shallow copy is safe; only one that also clones the gas delegate breaks it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PR SummaryMedium Risk Overview New Gas pickers, account pools (new-account rate), and weighted scenario shuffle stop using the global Tests cover stream derivation, gas Reviewed by Cursor Bugbot for commit d0605a0. Bugbot is set up for automated code reviews on this repo. Configure here. |
Implements PLT-456 — pins how a single run
seedderives the independent per-consumer PRNG sub-streams, making load runs reproducible. The derivation is a one-way door (frozen).What
utils/rngpackage:substream(seed, streamID) = math/rand/v2.NewPCG(seed, splitmix64(fnv1a64(streamID))), keyed by a logical stream id (string), so the draw a consumer sees is independent of worker count.Source→Stream(mutex-guarded). Unseeded runs get a crypto-random seed, recorded for after-the-fact replay.randcall-sites (config/gas.go,types/account_pool.go,generator/weighted.go) with derived sub-streams; falls back to the global RNG when no seed is set (behaviour-preserving).Seed *uint64onLoadConfig(nil = unseeded). Stream ids centralized inutils/rng/streams.go.Reproducibility contract (stated honestly)
Same seed + config ⇒ identical per-stream draw multiset — the workload (distribution of keys/sizes/gas/accounts) is statistically reproducible, which is what fair A/B comparison needs. Per-tx ordering is reproducible only at one worker (concurrent workers interleave draws); on-chain arrival order is concurrent regardless; individual txs are not byte-identical. The FROZEN block freezes four inputs: the formula, the stream-id set, the per-stream draw order, and the per-tx account draw cadence — changing any requires a
config_sha256version bump.Tests
go test -race -count=10 ./...green.TestWorkerCountMultisetInvariantruns generation concurrently and asserts per-stream column multisets;TestSingleWorkerOrderedReplaypins ordered replay at 1 worker; aliasing + tip/feecap + SetStream-no-op covered.TestAccountPoolMixedRateis now deterministic (seeded).Review
Cross-reviewed: idiom + two independent systems rounds — which caught and corrected an overclaiming reproducibility contract (byte-identical → honest per-stream-multiset) before merge.
Decision brief:
designs/sei-load-workload-modeler/PLT-456-seed-prng-substreams.md(sei-protocol/bdchatham-designs).🤖 Generated with Claude Code