diff --git a/cmd/geth/retesteth.go b/cmd/geth/retesteth.go index 29590b63b..ef4744be5 100644 --- a/cmd/geth/retesteth.go +++ b/cmd/geth/retesteth.go @@ -686,7 +686,7 @@ func (api *RetestethAPI) AccountRange(ctx context.Context, root = statedb.IntermediateRoot(vmenv.ChainConfig().IsEIP158(block.Number())) if idx == int(txIndex) { // This is to make sure root can be opened by OpenTrie - root, err = statedb.Commit(api.chainConfig.IsEIP158(block.Number())) + root, _, err = statedb.Commit(api.chainConfig.IsEIP158(block.Number())) if err != nil { return AccountRangeResult{}, err } @@ -796,7 +796,7 @@ func (api *RetestethAPI) StorageRangeAt(ctx context.Context, _ = statedb.IntermediateRoot(vmenv.ChainConfig().IsEIP158(block.Number())) if idx == int(txIndex) { // This is to make sure root can be opened by OpenTrie - _, err = statedb.Commit(vmenv.ChainConfig().IsEIP158(block.Number())) + _, _, err = statedb.Commit(vmenv.ChainConfig().IsEIP158(block.Number())) if err != nil { return StorageRangeResult{}, err } diff --git a/core/blockchain.go b/core/blockchain.go index 0987d65be..b412d43f2 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -1434,10 +1434,13 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types. log.Crit("Failed to write block into disk", "err", err) } // Commit all cached state changes into underlying memory database. - root, err := state.Commit(bc.chainConfig.IsEIP158(block.Number())) - if err != nil { - return NonStatTy, err + root, stateChanges, commitErr := state.Commit(bc.chainConfig.IsEIP158(block.Number())) + log.Debug("StateChanges", "count", len(stateChanges)) + + if commitErr != nil { + return NonStatTy, commitErr } + triedb := bc.stateCache.TrieDB() // If we're running an archive node, always flush diff --git a/core/chain_makers.go b/core/chain_makers.go index 6524087d4..ae21191c6 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -216,7 +216,7 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse block, _ := b.engine.FinalizeAndAssemble(chainreader, b.header, statedb, b.txs, b.uncles, b.receipts) // Write state changes to db - root, err := statedb.Commit(config.IsEIP158(b.header.Number)) + root, _, err := statedb.Commit(config.IsEIP158(b.header.Number)) if err != nil { panic(fmt.Sprintf("state write error: %v", err)) } diff --git a/core/state/state_test.go b/core/state/state_test.go index 41d9b4655..0bf5d12d2 100644 --- a/core/state/state_test.go +++ b/core/state/state_test.go @@ -167,7 +167,7 @@ func TestSnapshot2(t *testing.T) { so0.deleted = false state.setStateObject(so0) - root, _ := state.Commit(false) + root, _, _ := state.Commit(false) state.Reset(root) // and one with deleted == true diff --git a/core/state/statedb.go b/core/state/statedb.go index 0cfb902b6..33a8ee37e 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -802,28 +802,51 @@ func (s *StateDB) clearJournalAndRefund() { s.validRevisions = s.validRevisions[:0] // Snapshots can be created without journal entires } +// ModifiedAccount is an Account and it's Storage trie that has changed in the current block. +type ModifiedAccount struct { + Account + Storage +} + +// StateChanges are a map between an Account's address to it's ModifiedAccount. +type StateChanges map[common.Address]ModifiedAccount + // Commit writes the state to the underlying in-memory trie database. -func (s *StateDB) Commit(deleteEmptyObjects bool) (common.Hash, error) { +func (s *StateDB) Commit(deleteEmptyObjects bool) (common.Hash, StateChanges, error) { if s.dbErr != nil { - return common.Hash{}, fmt.Errorf("commit aborted due to earlier error: %v", s.dbErr) + return common.Hash{}, nil, fmt.Errorf("commit aborted due to earlier error: %v", s.dbErr) } + // Finalize any pending changes and merge everything into the tries s.IntermediateRoot(deleteEmptyObjects) + stateChanges := make(StateChanges) + // Commit objects to the trie, measuring the elapsed time for addr := range s.stateObjectsDirty { + modifiedAccount := ModifiedAccount{} + if obj := s.stateObjects[addr]; !obj.deleted { // Write any contract code associated with the state object if obj.code != nil && obj.dirtyCode { s.db.TrieDB().InsertBlob(common.BytesToHash(obj.CodeHash()), obj.code) obj.dirtyCode = false } + + // Add the account to the modifiedAccounts map + modifiedAccount = ModifiedAccount{Account: obj.data} + // Add the origin storage to the modifiedAccounts map before it's committed (and flushed from in-memory) + modifiedAccount.Storage = obj.originStorage + // Write any storage changes in the state object to its storage trie if err := obj.CommitTrie(s.db); err != nil { - return common.Hash{}, err + return common.Hash{}, StateChanges{}, err } } + + stateChanges[addr] = modifiedAccount } + if len(s.stateObjectsDirty) > 0 { s.stateObjectsDirty = make(map[common.Address]struct{}) } @@ -867,5 +890,6 @@ func (s *StateDB) Commit(deleteEmptyObjects bool) (common.Hash, error) { } s.snap, s.snapDestructs, s.snapAccounts, s.snapStorage = nil, nil, nil, nil } - return root, err + + return root, stateChanges, err } diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go index be2463535..d38544d76 100644 --- a/core/state/statedb_test.go +++ b/core/state/statedb_test.go @@ -67,6 +67,114 @@ func TestUpdateLeaks(t *testing.T) { it.Release() } +func TestStateChangesEmittedFromCommit(t *testing.T) { + // Create an empty state database + db := rawdb.NewMemoryDatabase() + state, _ := New(common.Hash{}, NewDatabase(db), nil) + + // Commit 1: Create accounts with storage + expectedStateChangesOne := make(StateChanges) + + for i := byte(0); i < 2; i++ { + addr := common.BytesToAddress([]byte{i}) + modifiedAccount := ModifiedAccount{ + Storage: make(map[common.Hash]common.Hash), + Account: Account{ + Nonce: 0, + Balance: big.NewInt(0), + Root: emptyRoot, + CodeHash: emptyCodeHash, + }, + } + newBalance := big.NewInt(int64(i)) + state.SetBalance(addr, newBalance) + modifiedAccount.Account.Balance = newBalance + + if i%2 == 0 { + storageKey := common.BytesToHash([]byte{i, i, i}) + storageValue := common.BytesToHash([]byte{i, i, i, i}) + modifiedAccount.Storage[storageKey] = storageValue + + state.SetState(addr, storageKey, storageValue) + } + + // Collect modified accounts to assert against + expectedStateChangesOne[addr] = modifiedAccount + } + + _, actualStateChangesOne, commitErr := state.Commit(false) + if commitErr != nil { + t.Errorf("error committing to statedb") + } + + assertStateChanges(actualStateChangesOne, expectedStateChangesOne, t) + + // Commit 2: Update existing account storage + expectedStateChangesTwo := make(StateChanges) + + var counter int + for addr, account := range expectedStateChangesOne { + // Only update some of the account storage + if counter%2 == 0 { + newBalance := big.NewInt(100) + account.Balance = newBalance + state.AddBalance(addr, newBalance) + + for storageKey := range account.Storage { + updatedStorageValue := common.BytesToHash([]byte{0}) + + // Collect modified accounts to assert against + account.Storage[storageKey] = updatedStorageValue + expectedStateChangesTwo[addr] = account + + state.SetState(addr, storageKey, updatedStorageValue) + } + } + counter++ + } + + _, stateChangesTwo, commitTwoErr := state.Commit(false) + if commitTwoErr != nil { + t.Errorf("error committing to statedb") + } + + assertStateChanges(stateChangesTwo, expectedStateChangesTwo, t) +} + +func assertStateChanges(actualChanges, expectedChanges StateChanges, t *testing.T) { + if len(actualChanges) != len(expectedChanges) { + t.Error("Test failure:", t.Name()) + t.Logf("Mismatch in number of StateChanges.ModifiedAccounts. actual: %v, expected: %v", len(actualChanges), len(expectedChanges)) + } + + for addr, account := range actualChanges { + expected := expectedChanges[addr] + + if !reflect.DeepEqual(account.Nonce, expected.Nonce) { + t.Error("Test failure:", t.Name()) + t.Errorf("Account Nonce does not match expected. actual: %v, expected: %v", account.Nonce, expected.Nonce) + } + + if !reflect.DeepEqual(account.Balance, expected.Balance) { + t.Error("Test failure:", t.Name()) + t.Errorf("Account Balance does not match expected. actual: %v, expected: %v", account.Balance, expected.Balance) + } + + if !reflect.DeepEqual(account.CodeHash, expected.CodeHash) { + t.Error("Test failure:", t.Name()) + t.Errorf("Account CodeHash does not match expected. actual: %v, expected: %v", account.CodeHash, expected.CodeHash) + } + + // Not asserting on the Account's Root because it's not easy to get the root between `SetState` calls + + if !reflect.DeepEqual(account.Storage, expected.Storage) { + t.Error("Test failure:", t.Name()) + t.Errorf("Account Storage does not match expected. actual: %v, expected: %v", account.Storage, expected.Storage) + } + } + +} + // Tests that no intermediate state of an object is stored into the database, // only the one right before the commit. func TestIntermediateLeaks(t *testing.T) { @@ -102,7 +210,7 @@ func TestIntermediateLeaks(t *testing.T) { } // Commit and cross check the databases. - transRoot, err := transState.Commit(false) + transRoot, _, err := transState.Commit(false) if err != nil { t.Fatalf("failed to commit transition state: %v", err) } @@ -110,7 +218,7 @@ func TestIntermediateLeaks(t *testing.T) { t.Errorf("can not commit trie %v to persistent database", transRoot.Hex()) } - finalRoot, err := finalState.Commit(false) + finalRoot, _, err := finalState.Commit(false) if err != nil { t.Fatalf("failed to commit final state: %v", err) } @@ -459,7 +567,7 @@ func (test *snapshotTest) checkEqual(state, checkstate *StateDB) error { func TestTouchDelete(t *testing.T) { s := newStateTest() s.state.GetOrNewStateObject(common.Address{}) - root, _ := s.state.Commit(false) + root, _, _ := s.state.Commit(false) s.state.Reset(root) snapshot := s.state.Snapshot() @@ -661,7 +769,7 @@ func TestDeleteCreateRevert(t *testing.T) { addr := toAddr([]byte("so")) state.SetBalance(addr, big.NewInt(1)) - root, _ := state.Commit(false) + root, _, _ := state.Commit(false) state.Reset(root) // Simulate self-destructing in one transaction, then create-reverting in another @@ -673,7 +781,7 @@ func TestDeleteCreateRevert(t *testing.T) { state.RevertToSnapshot(id) // Commit the entire state and make sure we don't crash and have the correct state - root, _ = state.Commit(true) + root, _, _ = state.Commit(true) state.Reset(root) if state.getStateObject(addr) != nil { @@ -698,7 +806,7 @@ func TestMissingTrieNodes(t *testing.T) { a2 := toAddr([]byte("another")) state.SetBalance(a2, big.NewInt(100)) state.SetCode(a2, []byte{1, 2, 4}) - root, _ = state.Commit(false) + root, _, _ = state.Commit(false) t.Logf("root: %x", root) // force-flush state.Database().TrieDB().Cap(0) @@ -722,7 +830,7 @@ func TestMissingTrieNodes(t *testing.T) { } // Modify the state state.SetBalance(addr, big.NewInt(2)) - root, err := state.Commit(false) + root, err, _ := state.Commit(false) if err == nil { t.Fatalf("expected error, got root :%x", root) } diff --git a/core/state/sync_test.go b/core/state/sync_test.go index 924c8c2f9..dcc325878 100644 --- a/core/state/sync_test.go +++ b/core/state/sync_test.go @@ -62,7 +62,7 @@ func makeTestState() (Database, common.Hash, []*testAccount) { state.updateStateObject(obj) accounts = append(accounts, acc) } - root, _ := state.Commit(false) + root, _, _ := state.Commit(false) // Return the generated state return db, root, accounts diff --git a/eth/api_tracer.go b/eth/api_tracer.go index 68a61bea0..dacd92be4 100644 --- a/eth/api_tracer.go +++ b/eth/api_tracer.go @@ -295,7 +295,7 @@ func (api *PrivateDebugAPI) traceChain(ctx context.Context, start, end *types.Bl break } // Finalize the state so any modifications are written to the trie - root, err := statedb.Commit(api.eth.blockchain.Config().IsEIP158(block.Number())) + root, _, err := statedb.Commit(api.eth.blockchain.Config().IsEIP158(block.Number())) if err != nil { failed = err break @@ -681,7 +681,7 @@ func (api *PrivateDebugAPI) computeStateDB(block *types.Block, reexec uint64) (* return nil, fmt.Errorf("processing block %d failed: %v", block.NumberU64(), err) } // Finalize the state so any modifications are written to the trie - root, err := statedb.Commit(api.eth.blockchain.Config().IsEIP158(block.Number())) + root, _, err := statedb.Commit(api.eth.blockchain.Config().IsEIP158(block.Number())) if err != nil { return nil, err } diff --git a/tests/state_test_util.go b/tests/state_test_util.go index a8d6fac51..3a03934a1 100644 --- a/tests/state_test_util.go +++ b/tests/state_test_util.go @@ -218,7 +218,7 @@ func MakePreState(db ethdb.Database, accounts core.GenesisAlloc, snapshotter boo } } // Commit and re-open to start with a clean state. - root, _ := statedb.Commit(false) + root, _, _ := statedb.Commit(false) var snaps *snapshot.Tree if snapshotter {