Commit e3c96e7
fix: prevent seed state cache poisoning in loadState clone (#9246)
## Summary
Fixes the v1.42.0 "Withdrawal mismatch at index=0" regression by
changing the two `clone()` calls in `loadState.ts` to `clone(true)`
(i.e. `dontTransferCache=true`).
The default `clone()` in `@chainsafe/ssz` transfers the source
sub-view's cache to the new instance, which means the
`migratedState.validators` sub-view and the seed container's cached
child-view snapshot share the **same** internal `nodes[]` and `caches[]`
arrays. A subsequent `migratedState.commit()` writes modified validator
/ inactivity-score nodes into those shared arrays, silently corrupting
the seed state's cache snapshot at the modified indices.
The corruption stays latent until the seed state is later cloned with
transfer-cache enabled — the path `verifyBlock` takes via
`preState.clone({dontTransferCache: false})`. On that next-block clone,
reads at the modified index return the migrated state's validator
instead of the seed's, which surfaces as `Withdrawal mismatch at
index=0` divergences between Lodestar and EL.
### Timeline of the corruption
```
// Inside loadState() with seedState = head state:
migratedState.validators = seedState.validators.clone();
// ^ migrated.validators.nodes === seedState.caches[validatorsIndex].nodes (shared)
for (const i of modifiedValidators) {
migratedState.validators.set(i, loadValidator(...)); // staged in viewsChanged
}
migratedState.commit();
// ^ arrayComposite.js commit(): `this.nodes[index] = node`
// writes newValidator into the SHARED nodes[] array.
// seedState.caches[validatorsIndex].nodes[modifiedIndex] is now poisoned.
```
On the next block:
```
const preState = headState.clone(); // default dontTransferCache=false
// ^ transfers the poisoned caches[] snapshot to preState
preState.validators.getReadonly(i); // returns migrated validator, not seed's
// -> getExpectedWithdrawals reads wrong validator at index 0
// -> "Withdrawal mismatch at index=0"
```
### Root cause
Introduced by #8857 (`chore: consume BeaconStateView`) which added the
`loadOtherState` / shared-head seed path that exercises this clone in
production.
## Test plan
- [x] New regression test `loadState does not poison seed state's cache`
in `packages/state-transition/test/unit/util/loadState.test.ts`
- [x] Verified the test FAILS without the fix (reads `0xaa`-filled
validator on `postState.clone()`) and PASSES with the fix
- [x] All 176 existing `state-transition` util tests pass
- [x] `check-types` and `lint` clean
## Relation to #9245
PR #9245 (`fix: gate loadOtherState validators/balances preload behind
opt-in`) addresses a different regression from the same #8857-era
changes — the eager `getAllReadonlyValues()` preload causing memory
spikes on the API path. The two fixes are **independent and both
needed**:
- #9245 fixes the API-path memory spike.
- This PR fixes the correctness bug (withdrawal mismatch). Notably,
`persistentCheckpointsCache` also exercises `loadState()` with the
problematic `clone()`, so #9245's preload gating alone doesn't cover the
full surface.
🤖 Generated with AI assistance
---------
Co-authored-by: lodekeeper <lodekeeper@users.noreply.github.com>
Co-authored-by: Nico Flaig <nflaig@protonmail.com>1 parent b05ea98 commit e3c96e7
2 files changed
Lines changed: 74 additions & 6 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
110 | 110 | | |
111 | 111 | | |
112 | 112 | | |
113 | | - | |
114 | | - | |
| 113 | + | |
| 114 | + | |
115 | 115 | | |
116 | 116 | | |
117 | 117 | | |
| |||
187 | 187 | | |
188 | 188 | | |
189 | 189 | | |
190 | | - | |
191 | | - | |
| 190 | + | |
| 191 | + | |
192 | 192 | | |
193 | 193 | | |
194 | 194 | | |
| |||
Lines changed: 70 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
6 | | - | |
| 6 | + | |
| 7 | + | |
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
| |||
22 | 23 | | |
23 | 24 | | |
24 | 25 | | |
25 | | - | |
| 26 | + | |
26 | 27 | | |
27 | 28 | | |
28 | 29 | | |
| |||
38 | 39 | | |
39 | 40 | | |
40 | 41 | | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
0 commit comments