Skip to content

Commit aff9869

Browse files
authored
Merge pull request #19953 from karalabe/state-accumulate-writes
core/state: accumulate writes and only update tries when must
2 parents 96fb839 + f49d6e5 commit aff9869

File tree

4 files changed

+242
-84
lines changed

4 files changed

+242
-84
lines changed

core/blockchain_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2287,3 +2287,78 @@ func TestSideImportPrunedBlocks(t *testing.T) {
22872287
t.Errorf("Got error, %v", err)
22882288
}
22892289
}
2290+
2291+
// TestDeleteCreateRevert tests a weird state transition corner case that we hit
2292+
// while changing the internals of statedb. The workflow is that a contract is
2293+
// self destructed, then in a followup transaction (but same block) it's created
2294+
// again and the transaction reverted.
2295+
//
2296+
// The original statedb implementation flushed dirty objects to the tries after
2297+
// each transaction, so this works ok. The rework accumulated writes in memory
2298+
// first, but the journal wiped the entire state object on create-revert.
2299+
func TestDeleteCreateRevert(t *testing.T) {
2300+
var (
2301+
aa = common.HexToAddress("0x000000000000000000000000000000000000aaaa")
2302+
bb = common.HexToAddress("0x000000000000000000000000000000000000bbbb")
2303+
// Generate a canonical chain to act as the main dataset
2304+
engine = ethash.NewFaker()
2305+
db = rawdb.NewMemoryDatabase()
2306+
2307+
// A sender who makes transactions, has some funds
2308+
key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
2309+
address = crypto.PubkeyToAddress(key.PublicKey)
2310+
funds = big.NewInt(1000000000)
2311+
gspec = &Genesis{
2312+
Config: params.TestChainConfig,
2313+
Alloc: GenesisAlloc{
2314+
address: {Balance: funds},
2315+
// The address 0xAAAAA selfdestructs if called
2316+
aa: {
2317+
// Code needs to just selfdestruct
2318+
Code: []byte{byte(vm.PC), 0xFF},
2319+
Nonce: 1,
2320+
Balance: big.NewInt(0),
2321+
},
2322+
// The address 0xBBBB send 1 wei to 0xAAAA, then reverts
2323+
bb: {
2324+
Code: []byte{
2325+
byte(vm.PC), // [0]
2326+
byte(vm.DUP1), // [0,0]
2327+
byte(vm.DUP1), // [0,0,0]
2328+
byte(vm.DUP1), // [0,0,0,0]
2329+
byte(vm.PUSH1), 0x01, // [0,0,0,0,1] (value)
2330+
byte(vm.PUSH2), 0xaa, 0xaa, // [0,0,0,0,1, 0xaaaa]
2331+
byte(vm.GAS),
2332+
byte(vm.CALL),
2333+
byte(vm.REVERT),
2334+
},
2335+
Balance: big.NewInt(1),
2336+
},
2337+
},
2338+
}
2339+
genesis = gspec.MustCommit(db)
2340+
)
2341+
2342+
blocks, _ := GenerateChain(params.TestChainConfig, genesis, engine, db, 1, func(i int, b *BlockGen) {
2343+
b.SetCoinbase(common.Address{1})
2344+
// One transaction to AAAA
2345+
tx, _ := types.SignTx(types.NewTransaction(0, aa,
2346+
big.NewInt(0), 50000, big.NewInt(1), nil), types.HomesteadSigner{}, key)
2347+
b.AddTx(tx)
2348+
// One transaction to BBBB
2349+
tx, _ = types.SignTx(types.NewTransaction(1, bb,
2350+
big.NewInt(0), 100000, big.NewInt(1), nil), types.HomesteadSigner{}, key)
2351+
b.AddTx(tx)
2352+
})
2353+
// Import the canonical chain
2354+
diskdb := rawdb.NewMemoryDatabase()
2355+
gspec.MustCommit(diskdb)
2356+
2357+
chain, err := NewBlockChain(diskdb, nil, params.TestChainConfig, engine, vm.Config{}, nil)
2358+
if err != nil {
2359+
t.Fatalf("failed to create tester chain: %v", err)
2360+
}
2361+
if n, err := chain.InsertChain(blocks); err != nil {
2362+
t.Fatalf("block %d: failed to insert into chain: %v", n, err)
2363+
}
2364+
}

core/state/state_object.go

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,10 @@ type stateObject struct {
7979
trie Trie // storage trie, which becomes non-nil on first access
8080
code Code // contract bytecode, which gets set when code is loaded
8181

82-
originStorage Storage // Storage cache of original entries to dedup rewrites
83-
dirtyStorage Storage // Storage entries that need to be flushed to disk
84-
fakeStorage Storage // Fake storage which constructed by caller for debugging purpose.
82+
originStorage Storage // Storage cache of original entries to dedup rewrites, reset for every transaction
83+
pendingStorage Storage // Storage entries that need to be flushed to disk, at the end of an entire block
84+
dirtyStorage Storage // Storage entries that have been modified in the current transaction execution
85+
fakeStorage Storage // Fake storage which constructed by caller for debugging purpose.
8586

8687
// Cache flags.
8788
// When an object is marked suicided it will be delete from the trie
@@ -113,13 +114,17 @@ func newObject(db *StateDB, address common.Address, data Account) *stateObject {
113114
if data.CodeHash == nil {
114115
data.CodeHash = emptyCodeHash
115116
}
117+
if data.Root == (common.Hash{}) {
118+
data.Root = emptyRoot
119+
}
116120
return &stateObject{
117-
db: db,
118-
address: address,
119-
addrHash: crypto.Keccak256Hash(address[:]),
120-
data: data,
121-
originStorage: make(Storage),
122-
dirtyStorage: make(Storage),
121+
db: db,
122+
address: address,
123+
addrHash: crypto.Keccak256Hash(address[:]),
124+
data: data,
125+
originStorage: make(Storage),
126+
pendingStorage: make(Storage),
127+
dirtyStorage: make(Storage),
123128
}
124129
}
125130

@@ -183,9 +188,11 @@ func (s *stateObject) GetCommittedState(db Database, key common.Hash) common.Has
183188
if s.fakeStorage != nil {
184189
return s.fakeStorage[key]
185190
}
186-
// If we have the original value cached, return that
187-
value, cached := s.originStorage[key]
188-
if cached {
191+
// If we have a pending write or clean cached, return that
192+
if value, pending := s.pendingStorage[key]; pending {
193+
return value
194+
}
195+
if value, cached := s.originStorage[key]; cached {
189196
return value
190197
}
191198
// Track the amount of time wasted on reading the storage trie
@@ -198,6 +205,7 @@ func (s *stateObject) GetCommittedState(db Database, key common.Hash) common.Has
198205
s.setError(err)
199206
return common.Hash{}
200207
}
208+
var value common.Hash
201209
if len(enc) > 0 {
202210
_, content, _, err := rlp.Split(enc)
203211
if err != nil {
@@ -252,17 +260,29 @@ func (s *stateObject) setState(key, value common.Hash) {
252260
s.dirtyStorage[key] = value
253261
}
254262

263+
// finalise moves all dirty storage slots into the pending area to be hashed or
264+
// committed later. It is invoked at the end of every transaction.
265+
func (s *stateObject) finalise() {
266+
for key, value := range s.dirtyStorage {
267+
s.pendingStorage[key] = value
268+
}
269+
if len(s.dirtyStorage) > 0 {
270+
s.dirtyStorage = make(Storage)
271+
}
272+
}
273+
255274
// updateTrie writes cached storage modifications into the object's storage trie.
256275
func (s *stateObject) updateTrie(db Database) Trie {
276+
// Make sure all dirty slots are finalized into the pending storage area
277+
s.finalise()
278+
257279
// Track the amount of time wasted on updating the storge trie
258280
if metrics.EnabledExpensive {
259281
defer func(start time.Time) { s.db.StorageUpdates += time.Since(start) }(time.Now())
260282
}
261-
// Update all the dirty slots in the trie
283+
// Insert all the pending updates into the trie
262284
tr := s.getTrie(db)
263-
for key, value := range s.dirtyStorage {
264-
delete(s.dirtyStorage, key)
265-
285+
for key, value := range s.pendingStorage {
266286
// Skip noop changes, persist actual changes
267287
if value == s.originStorage[key] {
268288
continue
@@ -277,6 +297,9 @@ func (s *stateObject) updateTrie(db Database) Trie {
277297
v, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(value[:]))
278298
s.setError(tr.TryUpdate(key[:], v))
279299
}
300+
if len(s.pendingStorage) > 0 {
301+
s.pendingStorage = make(Storage)
302+
}
280303
return tr
281304
}
282305

0 commit comments

Comments
 (0)