Skip to content

Commit 00b1863

Browse files
committed
fix: ordering of disk then memory state changes on execution
1 parent 0bf8b73 commit 00b1863

File tree

4 files changed

+60
-58
lines changed

4 files changed

+60
-58
lines changed

blocks/db.go

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ import (
44
"encoding/binary"
55
"fmt"
66
"math/big"
7-
"time"
87

98
"github.com/ava-labs/libevm/common"
109
"github.com/ava-labs/libevm/core/types"
1110
"github.com/ava-labs/libevm/ethdb"
1211
"github.com/ava-labs/libevm/trie"
1312
)
1413

14+
/* ===== Common =====*/
15+
1516
func blockNumDBKey(prefix string, blockNum uint64) []byte {
1617
return binary.BigEndian.AppendUint64([]byte(prefix), blockNum)
1718
}
@@ -26,19 +27,16 @@ func execResultsDBKey(blockNum uint64) []byte {
2627
return blockNumDBKey("sae-post-exec-", blockNum)
2728
}
2829

29-
// WritePostExecutionState writes, to w, the values passed to
30-
// [Block.MarkExecuted].
31-
func (b *Block) WritePostExecutionState(w ethdb.KeyValueWriter) error {
32-
e := b.execution.Load()
33-
if e == nil {
34-
return fmt.Errorf("writing post-execution state of block %d before execution", b.Height())
35-
}
30+
func (b *Block) writePostExecutionState(w ethdb.KeyValueWriter, e *executionResults) error {
3631
return b.writeToKVStore(w, execResultsDBKey, e.MarshalCanoto())
3732
}
3833

39-
// RestorePostExecutionState is the inverse of [Block.WritePostExecutionState].
40-
// The receipts MUST match those originally passed to [Block.MarkExecuted]
41-
// before the post-execution state was written to the database.
34+
// RestorePostExecutionState restores b to the same post-execution state as when
35+
// [Block.MarkExecuted] was called on it. This is only expected to be used after
36+
// a restart.
37+
//
38+
// The receipts MUST match those originally passed to [Block.MarkExecuted] as
39+
// they will be checked against the persisted Merkle root.
4240
func (b *Block) RestorePostExecutionState(db ethdb.Database, receipts types.Receipts) error {
4341
e, err := readExecResults(db, b.NumberU64())
4442
if err != nil {
@@ -47,7 +45,7 @@ func (b *Block) RestorePostExecutionState(db ethdb.Database, receipts types.Rece
4745
if e.receiptRoot != types.DeriveSha(receipts, trie.NewStackTrie(nil)) {
4846
return fmt.Errorf("restoring execution state of block %d: receipt-root mismatch", b.Height())
4947
}
50-
return b.MarkExecuted(e.byGas.Clone(), time.Time{}, receipts, e.stateRootPost)
48+
return b.markExecuted(e)
5149
}
5250

5351
// StateRootPostExecution returns the state root passed to [Block.MarkExecuted]

blocks/execution.go

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77

88
"github.com/ava-labs/avalanchego/vms/components/gas"
99
"github.com/ava-labs/libevm/common"
10+
"github.com/ava-labs/libevm/core/rawdb"
1011
"github.com/ava-labs/libevm/core/types"
12+
"github.com/ava-labs/libevm/ethdb"
1113
"github.com/ava-labs/libevm/trie"
1214
"github.com/ava-labs/strevm/gastime"
1315
)
@@ -48,10 +50,10 @@ func (e *executionResults) Equal(f *executionResults) bool {
4850
// time(s) and with the specified results. This function MUST NOT be called more
4951
// than once. The wall-clock [time.Time] is for metrics only.
5052
//
51-
// Note: only in-memory state is updated. Receipts SHOULD be stored
52-
// independently of a call to MarkExecuted, along with a call to
53-
// [Block.WritePostExecutionState].
54-
func (b *Block) MarkExecuted(byGas *gastime.Time, byWall time.Time, receipts types.Receipts, stateRootPost common.Hash) error {
53+
// MarkExecuted guarantees that state is persisted to the database before
54+
// in-memory indicators of execution are updated. [Block.Executed] returning
55+
// true is therefore indicative of a successful database write by MarkExecuted.
56+
func (b *Block) MarkExecuted(db ethdb.Database, isLastSyncBlock bool, byGas *gastime.Time, byWall time.Time, receipts types.Receipts, stateRootPost common.Hash) error {
5557
var used gas.Gas
5658
for _, r := range receipts {
5759
used += gas.Gas(r.GasUsed)
@@ -65,22 +67,45 @@ func (b *Block) MarkExecuted(byGas *gastime.Time, byWall time.Time, receipts typ
6567
receiptRoot: types.DeriveSha(receipts, trie.NewStackTrie(nil)),
6668
stateRootPost: stateRootPost,
6769
}
70+
71+
batch := db.NewBatch()
72+
if !isLastSyncBlock {
73+
// Although the last synchronous block is required to be the head block
74+
// at the time of upgrade, not setting the head here allows this method
75+
// to be called idempotently for that block. This is useful when
76+
// converting it to an SAE block.
77+
rawdb.WriteHeadBlockHash(batch, b.Hash())
78+
rawdb.WriteHeadHeaderHash(batch, b.Hash())
79+
}
80+
rawdb.WriteReceipts(batch, b.Hash(), b.NumberU64(), receipts)
81+
if err := b.writePostExecutionState(batch, e); err != nil {
82+
return err
83+
}
84+
if err := batch.Write(); err != nil {
85+
return err
86+
}
87+
88+
return b.markExecuted(e)
89+
}
90+
91+
func (b *Block) markExecuted(e *executionResults) error {
6892
if !b.execution.CompareAndSwap(nil, e) {
6993
b.log.Error("Block re-marked as executed")
7094
return fmt.Errorf("block %d re-marked as executed", b.Height())
7195
}
7296
return nil
7397
}
7498

75-
// Executed reports whether [Block.MarkExecuted] has been called.
99+
// Executed reports whether [Block.MarkExecuted] has been called and returned
100+
// without error.
76101
func (b *Block) Executed() bool {
77102
return b.execution.Load() != nil
78103
}
79104

80105
func zero[T any]() (z T) { return }
81106

82107
// ExecutedByGasTime returns a clone of the gas time passed to
83-
// [Block.MarkExecuted] or nil if no such call has been made.
108+
// [Block.MarkExecuted] or nil if no such successful call has been made.
84109
func (b *Block) ExecutedByGasTime() *gastime.Time {
85110
if e := b.execution.Load(); e != nil {
86111
return e.byGas.Clone()
@@ -90,7 +115,7 @@ func (b *Block) ExecutedByGasTime() *gastime.Time {
90115
}
91116

92117
// ExecutedByWallTime returns the wall time passed to [Block.MarkExecuted] or
93-
// the zero time if no such call has been made.
118+
// the zero time if no such successful call has been made.
94119
func (b *Block) ExecutedByWallTime() time.Time {
95120
if e := b.execution.Load(); e != nil {
96121
return e.byWall
@@ -100,7 +125,7 @@ func (b *Block) ExecutedByWallTime() time.Time {
100125
}
101126

102127
// Receipts returns the receipts passed to [Block.MarkExecuted] or nil if no
103-
// such call has been made.
128+
// such successful call has been made.
104129
func (b *Block) Receipts() types.Receipts {
105130
if e := b.execution.Load(); e != nil {
106131
return slices.Clone(e.receipts)
@@ -110,7 +135,7 @@ func (b *Block) Receipts() types.Receipts {
110135
}
111136

112137
// PostExecutionStateRoot returns the state root passed to [Block.MarkExecuted]
113-
// or the zero hash if no such call has been made.
138+
// or the zero hash if no such successful call has been made.
114139
func (b *Block) PostExecutionStateRoot() common.Hash {
115140
if e := b.execution.Load(); e != nil {
116141
return e.stateRootPost

db.go

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ func (vm *VM) upgradeLastSynchronousBlock(hash common.Hash) error {
3939
}
4040

4141
if err := block.MarkExecuted(
42+
vm.db,
43+
true,
4244
gastime.New(
4345
block.Time(),
4446
// TODO(arr4n) get the gas target and post-execution excess of the
@@ -51,30 +53,19 @@ func (vm *VM) upgradeLastSynchronousBlock(hash common.Hash) error {
5153
); err != nil {
5254
return err
5355
}
54-
55-
batch := vm.db.NewBatch()
56-
if err := block.WritePostExecutionState(batch); err != nil {
57-
return err
58-
}
59-
if err := block.WriteLastSettledNumber(batch); err != nil {
60-
return err
61-
}
62-
if err := batch.Write(); err != nil {
56+
if err := block.WriteLastSettledNumber(vm.db); err != nil {
6357
return err
6458
}
59+
block.MarkSettled()
6560

61+
if err := block.CheckInvariants(true, true); err != nil {
62+
return fmt.Errorf("upgrading last synchronous block: %v", err)
63+
}
6664
vm.logger().Info(
6765
"Last synchronous block before SAE",
6866
zap.Uint64("timestamp", block.Time()),
6967
zap.Stringer("hash", block.Hash()),
7068
)
71-
72-
// Although the block isn't returned, do this defensively in case of a
73-
// future refactor.
74-
block.MarkSettled()
75-
if err := block.CheckInvariants(true, true); err != nil {
76-
return fmt.Errorf("upgrading last synchronous block: %v", err)
77-
}
7869
return nil
7970
}
8071

saexec/execution.go

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"github.com/ava-labs/avalanchego/vms/components/gas"
1212
"github.com/ava-labs/libevm/common"
1313
"github.com/ava-labs/libevm/core"
14-
"github.com/ava-labs/libevm/core/rawdb"
1514
"github.com/ava-labs/libevm/core/state"
1615
"github.com/ava-labs/libevm/core/state/snapshot"
1716
"github.com/ava-labs/libevm/core/types"
@@ -156,28 +155,17 @@ func (e *Executor) execute(ctx context.Context, b *blocks.Block) error {
156155
if err != nil {
157156
return err
158157
}
159-
if err := b.MarkExecuted(e.gasClock.Clone(), endTime, receipts, root); err != nil {
158+
// The strict ordering of the next 3 calls guarantees invariants that MUST
159+
// NOT be broken:
160+
//
161+
// 1. [blocks.Block.MarkExecuted] guarantees disk then in-memory changes.
162+
// 2. Internal indicator of last executed MUST follow in-memory change.
163+
// 3. External indicator of last executed MUST follow internal indicator.
164+
if err := b.MarkExecuted(e.db, false, e.gasClock.Clone(), endTime, receipts, root); err != nil {
160165
return err
161166
}
162-
163-
batch := e.db.NewBatch()
164-
rawdb.WriteHeadBlockHash(batch, b.Hash())
165-
rawdb.WriteHeadHeaderHash(batch, b.Hash())
166-
// TODO(arr4n) move writing of receipts into settlement, where the
167-
// associated state root is committed on the trie DB. For now it's done here
168-
// to support immediate eth_getTransactionReceipt as the API treats
169-
// last-executed as the "latest" block and the upstream API implementation
170-
// requires this write.
171-
rawdb.WriteReceipts(batch, b.Hash(), b.NumberU64(), receipts)
172-
if err := b.WritePostExecutionState(batch); err != nil {
173-
return err
174-
}
175-
if err := batch.Write(); err != nil {
176-
return err
177-
}
178-
179-
e.sendPostExecutionEvents(b.Block, receipts)
180-
e.lastExecuted.Store(b)
167+
e.lastExecuted.Store(b) // (2)
168+
e.sendPostExecutionEvents(b.Block, receipts) // (3)
181169

182170
e.log.Debug(
183171
"Block execution complete",

0 commit comments

Comments
 (0)