diff --git a/fvm/evm/emulator/emulator.go b/fvm/evm/emulator/emulator.go index 132bf32600a..16f45e500ad 100644 --- a/fvm/evm/emulator/emulator.go +++ b/fvm/evm/emulator/emulator.go @@ -6,6 +6,7 @@ import ( "github.com/holiman/uint256" "github.com/onflow/atree" + "github.com/onflow/crypto/hash" gethCommon "github.com/onflow/go-ethereum/common" gethCore "github.com/onflow/go-ethereum/core" gethTracing "github.com/onflow/go-ethereum/core/tracing" @@ -178,7 +179,8 @@ func (bl *BlockView) RunTransaction( } // all commit errors (StateDB errors) has to be returned - if err := proc.commit(true); err != nil { + res.StateChangeCommitment, err = proc.commit(true) + if err != nil { return nil, err } @@ -224,7 +226,8 @@ func (bl *BlockView) BatchRunTransactions(txs []*gethTypes.Transaction) ([]*type } // all commit errors (StateDB errors) has to be returned - if err := proc.commit(false); err != nil { + res.StateChangeCommitment, err = proc.commit(false) + if err != nil { return nil, err } @@ -332,18 +335,18 @@ type procedure struct { } // commit commits the changes to the state (with optional finalization) -func (proc *procedure) commit(finalize bool) error { - err := proc.state.Commit(finalize) +func (proc *procedure) commit(finalize bool) (hash.Hash, error) { + stateUpdateCommitment, err := proc.state.Commit(finalize) if err != nil { // if known types (state errors) don't do anything and return if types.IsAFatalError(err) || types.IsAStateError(err) || types.IsABackendError(err) { - return err + return stateUpdateCommitment, err } // else is a new fatal error - return types.NewFatalError(err) + return stateUpdateCommitment, types.NewFatalError(err) } - return nil + return stateUpdateCommitment, nil } func (proc *procedure) mintTo( @@ -386,7 +389,8 @@ func (proc *procedure) mintTo( } // commit and finalize the state and return any stateDB error - return res, proc.commit(true) + res.StateChangeCommitment, err = proc.commit(true) + return res, err } func (proc *procedure) withdrawFrom( @@ -432,7 +436,8 @@ func (proc *procedure) withdrawFrom( proc.state.SubBalance(bridge, value, gethTracing.BalanceIncreaseWithdrawal) // commit and finalize the state and return any stateDB error - return res, proc.commit(true) + res.StateChangeCommitment, err = proc.commit(true) + return res, err } // deployAt deploys a contract at the given target address @@ -574,7 +579,8 @@ func (proc *procedure) deployAt( res.CumulativeGasUsed = proc.config.BlockTotalGasUsedSoFar + res.GasConsumed proc.state.SetCode(addr, ret) - return res, proc.commit(true) + res.StateChangeCommitment, err = proc.commit(true) + return res, err } func (proc *procedure) runDirect( @@ -590,7 +596,8 @@ func (proc *procedure) runDirect( return nil, err } // commit and finalize the state and return any stateDB error - return res, proc.commit(true) + res.StateChangeCommitment, err = proc.commit(true) + return res, err } // run runs a geth core.message and returns the diff --git a/fvm/evm/emulator/state/stateDB.go b/fvm/evm/emulator/state/stateDB.go index 7dad40cc0d6..6076d193773 100644 --- a/fvm/evm/emulator/state/stateDB.go +++ b/fvm/evm/emulator/state/stateDB.go @@ -8,6 +8,7 @@ import ( "github.com/holiman/uint256" "github.com/onflow/atree" + "github.com/onflow/crypto/hash" gethCommon "github.com/onflow/go-ethereum/common" gethStateless "github.com/onflow/go-ethereum/core/stateless" gethTracing "github.com/onflow/go-ethereum/core/tracing" @@ -347,10 +348,10 @@ func (db *StateDB) Preimages() map[gethCommon.Hash][]byte { } // Commit commits state changes back to the underlying -func (db *StateDB) Commit(finalize bool) error { - // return error if any has been acumulated +func (db *StateDB) Commit(finalize bool) (hash.Hash, error) { + // return error if any has been accumulated if db.cachedError != nil { - return wrapError(db.cachedError) + return nil, wrapError(db.cachedError) } var err error @@ -378,6 +379,7 @@ func (db *StateDB) Commit(finalize bool) error { return bytes.Compare(sortedAddresses[i][:], sortedAddresses[j][:]) < 0 }) + updateCommitter := NewUpdateCommitter() // update accounts for _, addr := range sortedAddresses { deleted := false @@ -385,36 +387,53 @@ func (db *StateDB) Commit(finalize bool) error { if db.HasSelfDestructed(addr) { err = db.baseView.DeleteAccount(addr) if err != nil { - return wrapError(err) + return nil, wrapError(err) + } + err = updateCommitter.DeleteAccount(addr) + if err != nil { + return nil, wrapError(err) } deleted = true } if deleted { continue } + + bal := db.GetBalance(addr) + nonce := db.GetNonce(addr) + code := db.GetCode(addr) + codeHash := db.GetCodeHash(addr) // create new accounts if db.IsCreated(addr) { err = db.baseView.CreateAccount( addr, - db.GetBalance(addr), - db.GetNonce(addr), - db.GetCode(addr), - db.GetCodeHash(addr), + bal, + nonce, + code, + codeHash, ) if err != nil { - return wrapError(err) + return nil, wrapError(err) + } + err = updateCommitter.CreateAccount(addr, bal, nonce, codeHash) + if err != nil { + return nil, wrapError(err) } continue } err = db.baseView.UpdateAccount( addr, - db.GetBalance(addr), - db.GetNonce(addr), - db.GetCode(addr), - db.GetCodeHash(addr), + bal, + nonce, + code, + codeHash, ) if err != nil { - return wrapError(err) + return nil, wrapError(err) + } + err = updateCommitter.UpdateAccount(addr, bal, nonce, codeHash) + if err != nil { + return nil, wrapError(err) } } @@ -437,20 +456,29 @@ func (db *StateDB) Commit(finalize bool) error { if db.HasSelfDestructed(sk.Address) { continue } + val := db.GetState(sk.Address, sk.Key) err = db.baseView.UpdateSlot( sk, - db.GetState(sk.Address, sk.Key), + val, ) if err != nil { - return wrapError(err) + return nil, wrapError(err) + } + err = updateCommitter.UpdateSlot(sk.Address, sk.Key, val) + if err != nil { + return nil, wrapError(err) } } // don't purge views yet, people might call the logs etc + updateCommit := updateCommitter.Commitment() if finalize { - return db.Finalize() + err := db.Finalize() + if err != nil { + return nil, err + } } - return nil + return updateCommit, nil } // Finalize flushes all the changes diff --git a/fvm/evm/emulator/state/stateDB_test.go b/fvm/evm/emulator/state/stateDB_test.go index ef76234ee8a..7a6594e8691 100644 --- a/fvm/evm/emulator/state/stateDB_test.go +++ b/fvm/evm/emulator/state/stateDB_test.go @@ -88,8 +88,9 @@ func TestStateDB(t *testing.T) { ret = db.GetCommittedState(addr1, key1) require.Equal(t, gethCommon.Hash{}, ret) - err = db.Commit(true) + commit, err := db.Commit(true) require.NoError(t, err) + require.NotEmpty(t, commit) ret = db.GetCommittedState(addr1, key1) require.Equal(t, value1, ret) @@ -272,10 +273,10 @@ func TestStateDB(t *testing.T) { require.NoError(t, err) db.CreateAccount(testutils.RandomCommonAddress(t)) - - err = db.Commit(true) + commit, err := db.Commit(true) // ret := db.Error() require.Error(t, err) + require.Empty(t, commit) // check wrapping require.True(t, types.IsAStateError(err)) }) @@ -297,9 +298,10 @@ func TestStateDB(t *testing.T) { db.CreateAccount(testutils.RandomCommonAddress(t)) - err = db.Commit(true) + commit, err := db.Commit(true) // ret := db.Error() require.Error(t, err) + require.Empty(t, commit) // check wrapping require.True(t, types.IsAFatalError(err)) }) @@ -321,8 +323,9 @@ func TestStateDB(t *testing.T) { // accounts without slots db.CreateAccount(addr1) require.NoError(t, db.Error()) - err = db.Commit(true) + commit, err := db.Commit(true) require.NoError(t, err) + require.NotEmpty(t, commit) root = db.GetStorageRoot(addr1) require.NoError(t, db.Error()) @@ -330,8 +333,9 @@ func TestStateDB(t *testing.T) { db.AddBalance(addr1, uint256.NewInt(100), tracing.BalanceChangeTouchAccount) require.NoError(t, db.Error()) - err = db.Commit(true) + commit, err = db.Commit(true) require.NoError(t, err) + require.NotEmpty(t, commit) root = db.GetStorageRoot(addr1) require.NoError(t, db.Error()) @@ -344,8 +348,9 @@ func TestStateDB(t *testing.T) { require.NoError(t, db.Error()) db.SetState(addr1, key, value) require.NoError(t, db.Error()) - err = db.Commit(true) + commit, err = db.Commit(true) require.NoError(t, err) + require.NotEmpty(t, commit) root = db.GetStorageRoot(addr1) require.NoError(t, db.Error()) @@ -367,8 +372,9 @@ func TestStateDB(t *testing.T) { db.SetCode(addr1, code1) db.AddBalance(addr1, balance1, tracing.BalanceChangeTransfer) require.NoError(t, db.Error()) - err = db.Commit(true) + commit, err := db.Commit(true) require.NoError(t, err) + require.NotEmpty(t, commit) // renew db db, err = state.NewStateDB(ledger, rootAddr) require.NoError(t, err) @@ -388,8 +394,9 @@ func TestStateDB(t *testing.T) { db.AddBalance(addr2, balance2, tracing.BalanceChangeTransfer) require.NoError(t, db.Error()) // commit and renew db - err = db.Commit(true) + commit, err = db.Commit(true) require.NoError(t, err) + require.NotEmpty(t, commit) db, err = state.NewStateDB(ledger, rootAddr) require.NoError(t, err) // call self destruct should not work @@ -400,8 +407,9 @@ func TestStateDB(t *testing.T) { require.Empty(t, db.GetCode(addr2)) require.NoError(t, db.Error()) // commit and renew db - err = db.Commit(true) + commit, err = db.Commit(true) require.NoError(t, err) + require.NotEmpty(t, commit) db, err = state.NewStateDB(ledger, rootAddr) require.NoError(t, err) // set code and call contract creation @@ -411,8 +419,9 @@ func TestStateDB(t *testing.T) { // now calling selfdestruct should do the job db.Selfdestruct6780(addr2) require.NoError(t, db.Error()) - err = db.Commit(true) + commit, err = db.Commit(true) require.NoError(t, err) + require.NotEmpty(t, commit) db, err = state.NewStateDB(ledger, rootAddr) require.NoError(t, err) // now query @@ -438,8 +447,9 @@ func TestStateDB(t *testing.T) { db.Selfdestruct6780(addr3) require.NoError(t, db.Error()) // commit changes - err = db.Commit(true) + commit, err = db.Commit(true) require.NoError(t, err) + require.NotEmpty(t, commit) // renew db db, err = state.NewStateDB(ledger, rootAddr) require.NoError(t, err) diff --git a/fvm/evm/emulator/state/state_growth_test.go b/fvm/evm/emulator/state/state_growth_test.go index b2622b42a03..2ba0b12c2c3 100644 --- a/fvm/evm/emulator/state/state_growth_test.go +++ b/fvm/evm/emulator/state/state_growth_test.go @@ -64,7 +64,7 @@ func (s *storageTest) run(runner func(state types.StateDB)) error { runner(state) - err = state.Commit(true) + _, err = state.Commit(true) if err != nil { return err } diff --git a/fvm/evm/emulator/state/updateCommitter.go b/fvm/evm/emulator/state/updateCommitter.go new file mode 100644 index 00000000000..e2c3f331c6e --- /dev/null +++ b/fvm/evm/emulator/state/updateCommitter.go @@ -0,0 +1,133 @@ +package state + +import ( + "encoding/binary" + + "github.com/holiman/uint256" + "github.com/onflow/crypto/hash" + gethCommon "github.com/onflow/go-ethereum/common" +) + +type OpCode byte + +const ( + UnknownOpCode OpCode = 0 + + AccountCreationOpCode OpCode = 1 + AccountUpdateOpCode OpCode = 2 + AccountDeletionOpCode OpCode = 3 + SlotUpdateOpCode OpCode = 4 +) + +const ( + opcodeByteSize = 1 + addressByteSize = gethCommon.AddressLength + nonceByteSize = 8 + balanceByteSize = 32 + hashByteSize = gethCommon.HashLength + accountDeletionBufferSize = opcodeByteSize + addressByteSize + accountCreationBufferSize = opcodeByteSize + + addressByteSize + + nonceByteSize + + balanceByteSize + + hashByteSize + accountUpdateBufferSize = accountCreationBufferSize + slotUpdateBufferSize = opcodeByteSize + + addressByteSize + + hashByteSize + + hashByteSize +) + +// UpdateCommitter captures operations (delta) through +// a set of calls (order matters) and constructs a commitment over the state changes. +type UpdateCommitter struct { + hasher hash.Hasher +} + +// NewUpdateCommitter constructs a new UpdateCommitter +func NewUpdateCommitter() *UpdateCommitter { + return &UpdateCommitter{ + hasher: hash.NewSHA3_256(), + } +} + +// CreateAccount captures a create account operation +func (dc *UpdateCommitter) CreateAccount( + addr gethCommon.Address, + balance *uint256.Int, + nonce uint64, + codeHash gethCommon.Hash, +) error { + buffer := make([]byte, accountCreationBufferSize) + var index int + buffer[0] = byte(AccountCreationOpCode) + index += opcodeByteSize + copy(buffer[index:index+addressByteSize], addr.Bytes()) + index += addressByteSize + encodedBalance := balance.Bytes32() + copy(buffer[index:index+balanceByteSize], encodedBalance[:]) + index += balanceByteSize + binary.BigEndian.PutUint64(buffer[index:index+nonceByteSize], nonce) + index += nonceByteSize + copy(buffer[index:index+hashByteSize], codeHash.Bytes()) + _, err := dc.hasher.Write(buffer) + return err +} + +// UpdateAccount captures an update account operation +func (dc *UpdateCommitter) UpdateAccount( + addr gethCommon.Address, + balance *uint256.Int, + nonce uint64, + codeHash gethCommon.Hash, +) error { + buffer := make([]byte, accountUpdateBufferSize) + var index int + buffer[0] = byte(AccountUpdateOpCode) + index += opcodeByteSize + copy(buffer[index:index+addressByteSize], addr.Bytes()) + index += addressByteSize + encodedBalance := balance.Bytes32() + copy(buffer[index:index+balanceByteSize], encodedBalance[:]) + index += balanceByteSize + binary.BigEndian.PutUint64(buffer[index:index+nonceByteSize], nonce) + index += nonceByteSize + copy(buffer[index:index+hashByteSize], codeHash.Bytes()) + _, err := dc.hasher.Write(buffer) + return err +} + +// DeleteAccount captures a delete account operation +func (dc *UpdateCommitter) DeleteAccount(addr gethCommon.Address) error { + buffer := make([]byte, accountDeletionBufferSize) + var index int + buffer[0] = byte(AccountDeletionOpCode) + index += opcodeByteSize + copy(buffer[index:index+addressByteSize], addr.Bytes()) + _, err := dc.hasher.Write(buffer) + return err +} + +// UpdateSlot captures a update slot operation +func (dc *UpdateCommitter) UpdateSlot( + addr gethCommon.Address, + key gethCommon.Hash, + value gethCommon.Hash, +) error { + buffer := make([]byte, slotUpdateBufferSize) + var index int + buffer[0] = byte(SlotUpdateOpCode) + index += opcodeByteSize + copy(buffer[index:index+addressByteSize], addr.Bytes()) + index += addressByteSize + copy(buffer[index:index+hashByteSize], key.Bytes()) + index += hashByteSize + copy(buffer[index:index+hashByteSize], value.Bytes()) + _, err := dc.hasher.Write(buffer) + return err +} + +// Commitment calculates and returns the commitment +func (dc *UpdateCommitter) Commitment() hash.Hash { + return dc.hasher.SumHash() +} diff --git a/fvm/evm/emulator/state/updateCommitter_test.go b/fvm/evm/emulator/state/updateCommitter_test.go new file mode 100644 index 00000000000..ab0be67a08f --- /dev/null +++ b/fvm/evm/emulator/state/updateCommitter_test.go @@ -0,0 +1,129 @@ +package state_test + +import ( + "testing" + + "github.com/holiman/uint256" + "github.com/onflow/crypto/hash" + "github.com/stretchr/testify/require" + + "github.com/onflow/flow-go/fvm/evm/emulator/state" + "github.com/onflow/flow-go/fvm/evm/testutils" +) + +func TestChangeCommitter(t *testing.T) { + + addr := testutils.RandomAddress(t).ToCommon() + balance := uint256.NewInt(200) + nonce := uint64(1) + nonceBytes := []byte{0, 0, 0, 0, 0, 0, 0, 1} + randomHash := testutils.RandomCommonHash(t) + key := testutils.RandomCommonHash(t) + value := testutils.RandomCommonHash(t) + + t.Run("test create account", func(t *testing.T) { + dc := state.NewUpdateCommitter() + err := dc.CreateAccount(addr, balance, nonce, randomHash) + require.NoError(t, err) + + hasher := hash.NewSHA3_256() + + input := []byte{byte(state.AccountCreationOpCode)} + input = append(input, addr.Bytes()...) + encodedBalance := balance.Bytes32() + input = append(input, encodedBalance[:]...) + input = append(input, nonceBytes...) + input = append(input, randomHash.Bytes()...) + + n, err := hasher.Write(input) + require.NoError(t, err) + require.Equal(t, 93, n) + + expectedCommit := hasher.SumHash() + commit := dc.Commitment() + require.Equal(t, expectedCommit, commit) + }) + + t.Run("test update account", func(t *testing.T) { + dc := state.NewUpdateCommitter() + err := dc.UpdateAccount(addr, balance, nonce, randomHash) + require.NoError(t, err) + + hasher := hash.NewSHA3_256() + input := []byte{byte(state.AccountUpdateOpCode)} + input = append(input, addr.Bytes()...) + encodedBalance := balance.Bytes32() + input = append(input, encodedBalance[:]...) + input = append(input, nonceBytes...) + input = append(input, randomHash.Bytes()...) + + n, err := hasher.Write(input) + require.NoError(t, err) + require.Equal(t, 93, n) + + expectedCommit := hasher.SumHash() + commit := dc.Commitment() + require.Equal(t, expectedCommit, commit) + }) + + t.Run("test delete account", func(t *testing.T) { + dc := state.NewUpdateCommitter() + err := dc.DeleteAccount(addr) + require.NoError(t, err) + + hasher := hash.NewSHA3_256() + input := []byte{byte(state.AccountDeletionOpCode)} + input = append(input, addr.Bytes()...) + + n, err := hasher.Write(input) + require.NoError(t, err) + require.Equal(t, 21, n) + + expectedCommit := hasher.SumHash() + commit := dc.Commitment() + require.Equal(t, expectedCommit, commit) + }) + + t.Run("test update slot", func(t *testing.T) { + dc := state.NewUpdateCommitter() + err := dc.UpdateSlot(addr, key, value) + require.NoError(t, err) + + hasher := hash.NewSHA3_256() + + input := []byte{byte(state.SlotUpdateOpCode)} + input = append(input, addr.Bytes()...) + input = append(input, key[:]...) + input = append(input, value[:]...) + + n, err := hasher.Write(input) + require.NoError(t, err) + require.Equal(t, 85, n) + + expectedCommit := hasher.SumHash() + commit := dc.Commitment() + require.Equal(t, expectedCommit, commit) + }) +} + +func BenchmarkDeltaCommitter(b *testing.B) { + addr := testutils.RandomAddress(b) + balance := uint256.NewInt(200) + nonce := uint64(100) + randomHash := testutils.RandomCommonHash(b) + dc := state.NewUpdateCommitter() + + numberOfAccountUpdates := 10 + for i := 0; i < numberOfAccountUpdates; i++ { + err := dc.UpdateAccount(addr.ToCommon(), balance, nonce, randomHash) + require.NoError(b, err) + } + + numberOfSlotUpdates := 10 + for i := 0; i < numberOfSlotUpdates; i++ { + err := dc.UpdateSlot(addr.ToCommon(), randomHash, randomHash) + require.NoError(b, err) + } + com := dc.Commitment() + require.NotEmpty(b, com) +} diff --git a/fvm/evm/types/result.go b/fvm/evm/types/result.go index 8a4ba270dc6..31176529f7e 100644 --- a/fvm/evm/types/result.go +++ b/fvm/evm/types/result.go @@ -90,6 +90,8 @@ type Result struct { // PrecompiledCalls captures an encoded list of calls to the precompile // during the execution of transaction PrecompiledCalls []byte + // StateChangeCommitment captures a commitment over the state change (delta) + StateChangeCommitment []byte } // Invalid returns true if transaction has been rejected diff --git a/fvm/evm/types/state.go b/fvm/evm/types/state.go index edff68251ef..10539cef0fb 100644 --- a/fvm/evm/types/state.go +++ b/fvm/evm/types/state.go @@ -2,6 +2,7 @@ package types import ( "github.com/holiman/uint256" + "github.com/onflow/crypto/hash" gethCommon "github.com/onflow/go-ethereum/common" gethTypes "github.com/onflow/go-ethereum/core/types" gethVM "github.com/onflow/go-ethereum/core/vm" @@ -11,12 +12,13 @@ import ( type StateDB interface { gethVM.StateDB - // Commit commits the changes + // Commit commits the changes and + // returns a commitment over changes // setting `finalize` flag // calls a subsequent call to Finalize - // defering finalization and calling it once at the end + // deferring finalization and calling it once at the end // improves efficiency of batch operations. - Commit(finalize bool) error + Commit(finalize bool) (hash.Hash, error) // Finalize flushes all the changes // to the permanent storage