Skip to content

Commit 5a09f6c

Browse files
committed
chore: self review
1 parent ba5e996 commit 5a09f6c

4 files changed

Lines changed: 166 additions & 33 deletions

File tree

migration/statehistory/class_hash_ingestor.go

Lines changed: 75 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,31 @@ func newClassHashIngestor(
2626
return &classHashIngestor{baseIngestor: newBaseIngestor(ctx, sem, database)}
2727
}
2828

29+
// Run migrates the class-hash history of a single contract.
30+
//
31+
// Legend: Bₙ = block at which the n-th class-hash *replacement* happened.
32+
// Vₙ = the class hash active *after* Bₙ; V₀ is the deploy-time hash. The
33+
// deprecated layout writes nothing at deploy: each entry is written only
34+
// on a *Replace*, and the value stored is the hash that was active before
35+
// that replace. So deprecated[B₁] = V₀ even though no replace happened at
36+
// deploy_h itself. The new layout adds an explicit deploy entry and shifts
37+
// everything else by one slot:
38+
//
39+
// block │ deprecated │ new
40+
// ─────────┼────────────────┼──────
41+
// deploy_h │ — │ V₀ ← inserted from first deprecated entry
42+
// B₁ │ V₀ │ V₁
43+
// B₂ │ V₁ │ V₂
44+
// B₃ │ V₂ │ V₃
45+
// ─────────┼────────────────┼──────
46+
// > B₃ │ contract │ V₃ (last entry — self-contained)
47+
// .ClassHash ← deprecated must reach into the Contract
48+
// record for any block past the last replace
49+
//
50+
// If the deprecated history is empty (no replaces ever), the single deploy
51+
// entry is written with contract.ClassHash directly. Deprecated rows are
52+
// deleted at the end of the run. Resume-safe: empty-deprecated + existing
53+
// deploy entry → no-op.
2954
func (i *classHashIngestor) Run(index int, addr *felt.Felt, outputs chan<- task) error {
3055
t := &i.tasks[index]
3156

@@ -35,64 +60,81 @@ func (i *classHashIngestor) Run(index int, addr *felt.Felt, outputs chan<- task)
3560
return fmt.Errorf("class-hash: GetContract(%s): %w", addr, err)
3661
}
3762

38-
deployKey := db.ContractClassHashHistoryAtBlockKey(addr, contract.DeployedHeight)
39-
deployEntryExists, err := i.database.Has(deployKey)
40-
if err != nil {
41-
return fmt.Errorf("class-hash: Has(deploy entry): %w", err)
42-
}
43-
4463
depIt, err := i.database.NewIterator(deprecatedPrefix, true)
4564
if err != nil {
4665
return fmt.Errorf("class-hash: open deprecated iter(%s): %w", addr, err)
4766
}
4867
defer depIt.Close()
4968

5069
if !depIt.First() {
51-
if deployEntryExists {
52-
return nil
53-
}
54-
err = state.WriteClassHashHistory(
55-
t.batch,
56-
addr,
57-
contract.DeployedHeight,
58-
&contract.ClassHash,
59-
)
60-
if err != nil {
61-
return err
62-
}
63-
t.completedAddrs++
64-
t.entryCount++
65-
return i.flush(t, outputs)
70+
return i.writeDeployOnly(t, outputs, addr, contract.DeployedHeight, &contract.ClassHash)
71+
}
72+
return i.writeShiftedHistory(
73+
t, outputs, depIt, deprecatedPrefix, addr,
74+
contract.DeployedHeight, &contract.ClassHash,
75+
)
76+
}
77+
78+
// writeDeployOnly handles the "no deprecated history" branch: write the
79+
// deploy-time entry from contract.ClassHash, unless a previous run already
80+
// wrote it.
81+
func (i *classHashIngestor) writeDeployOnly(
82+
t *task,
83+
outputs chan<- task,
84+
addr *felt.Felt,
85+
deployHeight uint64,
86+
classHash *felt.Felt,
87+
) error {
88+
deployKey := db.ContractClassHashHistoryAtBlockKey(addr, deployHeight)
89+
deployEntryExists, err := i.database.Has(deployKey)
90+
if err != nil {
91+
return fmt.Errorf("class-hash: Has(deploy entry): %w", err)
92+
}
93+
if deployEntryExists {
94+
return nil
6695
}
96+
if err := state.WriteClassHashHistory(t.batch, addr, deployHeight, classHash); err != nil {
97+
return err
98+
}
99+
t.completedAddrs++
100+
t.entryCount++
101+
return i.flush(t, outputs)
102+
}
67103

104+
// writeShiftedHistory handles the "non-empty deprecated history" branch:
105+
// writes the deploy entry from the first deprecated value, shifts each
106+
// deprecated entry into the new layout using the next entry's pre-value
107+
// (or contract.ClassHash for the last), and deletes the deprecated rows.
108+
// depIt must be positioned at the first deprecated entry.
109+
func (i *classHashIngestor) writeShiftedHistory(
110+
t *task,
111+
outputs chan<- task,
112+
depIt db.Iterator,
113+
prefix []byte,
114+
addr *felt.Felt,
115+
deployHeight uint64,
116+
headClassHash *felt.Felt,
117+
) error {
68118
rawValue, err := depIt.Value()
69119
if err != nil {
70120
return fmt.Errorf("class-hash: read first value(%s): %w", addr, err)
71121
}
72122
deployClassHash := felt.FromBytes[felt.Felt](rawValue)
73-
if err := state.WriteClassHashHistory(
74-
t.batch,
75-
addr,
76-
contract.DeployedHeight,
77-
&deployClassHash,
78-
); err != nil {
123+
if err := state.WriteClassHashHistory(t.batch, addr, deployHeight, &deployClassHash); err != nil {
79124
return err
80125
}
81126
t.entryCount++
82127
if err := i.flush(t, outputs); err != nil {
83128
return err
84129
}
85130

86-
// Shift-up loop: each block in the deprecated history gets the *next*
87-
// entry's value (since in the old layout the value at block B was the
88-
// value before B's write). The final block gets the head class hash.
89131
for {
90-
block, err := parseBlockKey(depIt.Key(), deprecatedPrefix)
132+
block, err := parseBlockKey(depIt.Key(), prefix)
91133
if err != nil {
92134
return fmt.Errorf("class-hash(%s): %w", addr, err)
93135
}
94136
hasNext := depIt.Next()
95-
historyValue := contract.ClassHash
137+
historyValue := *headClassHash
96138
if hasNext {
97139
rawValue, err := depIt.Value()
98140
if err != nil {
@@ -112,7 +154,7 @@ func (i *classHashIngestor) Run(index int, addr *felt.Felt, outputs chan<- task)
112154
}
113155
}
114156

115-
if err := t.batch.DeleteRange(deprecatedPrefix, dbutils.UpperBound(deprecatedPrefix)); err != nil {
157+
if err := t.batch.DeleteRange(prefix, dbutils.UpperBound(prefix)); err != nil {
116158
return fmt.Errorf("class-hash: DeleteRange deprecated(%s): %w", addr, err)
117159
}
118160
t.completedAddrs++

migration/statehistory/migrator.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,33 @@ var (
3636

3737
var _ migration.Migration = (*Migrator)(nil)
3838

39+
// Migrator rewrites the contract history layout so each entry stores the
40+
// post-update value at its block, instead of the pre-update value.
41+
//
42+
// Example — a contract whose class hash was 0xAA at deploy (block 100),
43+
// changed to 0xBB at block 200, then to 0xCC at block 500:
44+
//
45+
// block │ old layout (pre-value) │ new layout (post-value)
46+
// ──────┼────────────────────────┼─────────────────────────
47+
// 100 │ (no entry) │ 0xAA ← explicit deploy
48+
// 200 │ 0xAA │ 0xBB
49+
// 500 │ 0xBB │ 0xCC
50+
// head │ 0xCC (contract record) │ (read from history)
51+
//
52+
// The same shape change applies to nonces and per-slot storage. The
53+
// migrator runs three phases (class-hash, nonce, storage); each phase
54+
// iterates the Contract bucket and rewrites one contract's deprecated
55+
// entries at a time, deleting them in the same batch.
56+
//
57+
// Crash / cancellation safety: pebble batches commit atomically, so the
58+
// writes inside any single committed batch are durable as a unit. A
59+
// contract whose history is large may span more than one batch — but each
60+
// new entry's value is a pure function of the deprecated source data, so
61+
// re-running over an already-partially-rewritten contract overwrites with
62+
// identical values and then deletes the (still-present) deprecated rows.
63+
// Contracts whose deprecated entries are already gone short-circuit on an
64+
// empty iterator. The three phases run sequentially: a later phase only
65+
// starts after the earlier phase completes.
3966
type Migrator struct{}
4067

4168
func (Migrator) Before([]byte) error { return nil }

migration/statehistory/nonce_ingestor.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,26 @@ func newNonceIngestor(
2626
return &nonceIngestor{baseIngestor: newBaseIngestor(ctx, sem, database)}
2727
}
2828

29+
// Run migrates the nonce history of a single contract.
30+
//
31+
// Legend: Bₙ = block at which the n-th nonce change happened. Nₙ = the
32+
// nonce active *after* Bₙ; the deploy nonce is always 0 and is *not*
33+
// written to the deprecated history — its presence is implicit in the
34+
// pre-value of the first change entry. The new layout stores the same
35+
// number of entries, just shifted to post-values:
36+
//
37+
// block │ deprecated │ new
38+
// ───────┼────────────────┼──────
39+
// B₁ │ 0 │ N₁
40+
// B₂ │ N₁ │ N₂
41+
// B₃ │ N₂ │ N₃
42+
// ───────┼────────────────┼──────
43+
// > B₃ │ contract │ N₃ (last entry — self-contained)
44+
// .Nonce ← deprecated must reach into the Contract
45+
// record for any block past the last change
46+
//
47+
// Contracts with no deprecated nonce history are skipped. Deprecated rows
48+
// are deleted at the end of the run.
2949
func (i *nonceIngestor) Run(index int, addr *felt.Felt, outputs chan<- task) error {
3050
t := &i.tasks[index]
3151
deprecatedPrefix := db.DeprecatedContractNonceHistoryKey(addr)

migration/statehistory/storage_ingestor.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,50 @@ func newStorageIngestor(
2929
return &storageIngestor{baseIngestor: newBaseIngestor(ctx, sem, database)}
3030
}
3131

32+
// Run migrates the per-slot storage history of a single contract.
33+
//
34+
// Legend: Bₙ = block at which the n-th change to a slot happened. preXₙ
35+
// is the value of slot X before Bₙ (= what the deprecated layout stores
36+
// at [X, Bₙ]); headX is the slot's current value, read from the head
37+
// storage trie. The deprecated layout writes nothing at deploy — the
38+
// pre-deploy value (0) is implicit in the first change entry. The new
39+
// layout stores the same number of entries per slot, just shifted to
40+
// post-values. For one slot:
41+
//
42+
// block │ deprecated[slotA] │ new[slotA]
43+
// ───────┼───────────────────┼───────────
44+
// B₁ │ 0 │ preA₁
45+
// B₂ │ preA₁ │ preA₂
46+
// B₃ │ preA₂ │ headA
47+
// ───────┼───────────────────┼───────────
48+
// > B₃ │ head trie leaf │ headA (last entry — self-contained)
49+
// for slotA ← deprecated must reach into the head
50+
// storage trie for any block past the
51+
// last change
52+
//
53+
// For each deprecated entry the post-value comes from one of:
54+
//
55+
// - the *next* deprecated entry, when it's on the same slot — its stored
56+
// pre-value is exactly this block's post-value;
57+
// - the head storage trie leaf for that slot, when there is no next
58+
// deprecated entry on the same slot;
59+
// - felt.Zero, when there is no head leaf for the slot (the slot was
60+
// eventually zeroed out and dropped from the trie).
61+
//
62+
// Both the deprecated history and the head trie are sorted by raw slot
63+
// bytes, so the ingestor walks them in lockstep — the head-trie iterator
64+
// advances only when its current leaf matches the slot just resolved:
65+
//
66+
// deprecated history head trie new history
67+
// ───────────────────── ───────────── ─────────────────────────
68+
// [slotA, B₁..B₃] ──→ [slotA] = headA [slotA, B₁..B₃] last uses headA
69+
// [slotB, B₁..B₂] ──→ (no leaf) [slotB, B₁..B₂] last uses 0
70+
// ← slotB was set (slotB was zeroed
71+
// and later zeroed at B₂)
72+
// [slotC, B₁] ──→ [slotC] = headC [slotC, B₁] = headC
73+
//
74+
// Contracts with no deprecated storage history are skipped; deprecated
75+
// rows are deleted at the end of the run via DeleteRange.
3276
func (i *storageIngestor) Run(index int, addr *felt.Felt, outputs chan<- task) error {
3377
t := &i.tasks[index]
3478

0 commit comments

Comments
 (0)