diff --git a/engine/execution/state/bootstrap/bootstrap_test.go b/engine/execution/state/bootstrap/bootstrap_test.go index 0500bbf2928..9cd60681595 100644 --- a/engine/execution/state/bootstrap/bootstrap_test.go +++ b/engine/execution/state/bootstrap/bootstrap_test.go @@ -53,7 +53,7 @@ func TestBootstrapLedger(t *testing.T) { } func TestBootstrapLedger_ZeroTokenSupply(t *testing.T) { - expectedStateCommitmentBytes, _ := hex.DecodeString("719729e453d826640fafd769746604e0ee0a67efa6df43f87714689c45f7e228") + expectedStateCommitmentBytes, _ := hex.DecodeString("48460c42a43f700562d6c17408af8b52823774cc24fe795148c8e37b62f77615") expectedStateCommitment, err := flow.ToStateCommitment(expectedStateCommitmentBytes) require.NoError(t, err) diff --git a/fvm/evm/emulator/config.go b/fvm/evm/emulator/config.go index cefb5ff309a..b1ac1c0b51b 100644 --- a/fvm/evm/emulator/config.go +++ b/fvm/evm/emulator/config.go @@ -71,7 +71,7 @@ var DefaultChainConfig = &gethParams.ChainConfig{ // Fork scheduling based on timestamps ShanghaiTime: &zero, // already on Shanghai CancunTime: &zero, // already on Cancun - PragueTime: &zero, // already on Prague + PragueTime: nil, // not on Prague } // Default config supports the dynamic fee structure (EIP-1559) diff --git a/fvm/evm/evm_test.go b/fvm/evm/evm_test.go index 3cd517cea8f..ae5ace70b6e 100644 --- a/fvm/evm/evm_test.go +++ b/fvm/evm/evm_test.go @@ -155,7 +155,6 @@ func TestEVMRun(t *testing.T) { require.Equal(t, innerTxBytes, txPayload) require.Equal(t, uint16(types.ErrCodeNoError), txEventPayload.ErrorCode) require.Equal(t, uint16(0), txEventPayload.Index) - require.Equal(t, blockEventPayload.Hash, txEventPayload.BlockHash) require.Equal(t, blockEventPayload.Height, txEventPayload.BlockHeight) require.Equal(t, blockEventPayload.TotalGasUsed, txEventPayload.GasConsumed) require.Equal(t, uint64(43785), blockEventPayload.TotalGasUsed) diff --git a/fvm/evm/handler/blockstore.go b/fvm/evm/handler/blockstore.go index 32c282a0d84..3939ee4d7ee 100644 --- a/fvm/evm/handler/blockstore.go +++ b/fvm/evm/handler/blockstore.go @@ -11,15 +11,15 @@ import ( ) const ( - BlockHashListCapacity = 16 - BlockStoreLatestBlockKey = "LatestBlock" - BlockStoreBlockHashesKey = "LatestBlockHashes" + BlockHashListCapacity = 16 + BlockStoreLatestBlockKey = "LatestBlock" + BlockStoreLatestBlockProposalKey = "LatestBlockProposal" + BlockStoreBlockHashesKey = "LatestBlockHashes" ) type BlockStore struct { - backend types.Backend - rootAddress flow.Address - blockProposal *types.Block + backend types.Backend + rootAddress flow.Address } var _ types.BlockStore = &BlockStore{} @@ -33,11 +33,17 @@ func NewBlockStore(backend types.Backend, rootAddress flow.Address) *BlockStore } // BlockProposal returns the block proposal to be updated by the handler -func (bs *BlockStore) BlockProposal() (*types.Block, error) { - if bs.blockProposal != nil { - return bs.blockProposal, nil +func (bs *BlockStore) BlockProposal() (*types.BlockProposal, error) { + // first fetch it from the storage + data, err := bs.backend.GetValue(bs.rootAddress[:], []byte(BlockStoreLatestBlockProposalKey)) + if err != nil { + return nil, types.NewFatalError(err) + } + if len(data) != 0 { + return types.NewBlockProposalFromBytes(data) } + // if available construct a new one cadenceHeight, err := bs.backend.GetCurrentBlockHeight() if err != nil { return nil, err @@ -65,25 +71,43 @@ func (bs *BlockStore) BlockProposal() (*types.Block, error) { // expect timestamps in unix seconds so we convert here timestamp := uint64(cadenceBlock.Timestamp / int64(time.Second)) - bs.blockProposal = types.NewBlock( + blockProposal := types.NewBlockProposal( parentHash, lastExecutedBlock.Height+1, timestamp, lastExecutedBlock.TotalSupply, - gethCommon.Hash{}, - make([]gethCommon.Hash, 0), ) - return bs.blockProposal, nil + return blockProposal, nil } -// CommitBlockProposal commits the block proposal to the chain -func (bs *BlockStore) CommitBlockProposal() error { - bp, err := bs.BlockProposal() +// UpdateBlockProposal updates the block proposal +func (bs *BlockStore) UpdateBlockProposal(bp *types.BlockProposal) error { + blockProposalBytes, err := bp.ToBytes() if err != nil { - return err + return types.NewFatalError(err) } - blockBytes, err := bp.ToBytes() + return bs.backend.SetValue( + bs.rootAddress[:], + []byte(BlockStoreLatestBlockProposalKey), + blockProposalBytes, + ) +} + +func (bs *BlockStore) ResetBlockProposal() error { + return bs.backend.SetValue( + bs.rootAddress[:], + []byte(BlockStoreLatestBlockProposalKey), + nil, + ) +} + +// CommitBlockProposal commits the block proposal to the chain +func (bs *BlockStore) CommitBlockProposal(bp *types.BlockProposal) error { + // populate receipt root hash + bp.PopulateReceiptRoot() + + blockBytes, err := bp.Block.ToBytes() if err != nil { return types.NewFatalError(err) } @@ -93,18 +117,21 @@ func (bs *BlockStore) CommitBlockProposal() error { return err } - err = bs.updateBlockHashList(bs.blockProposal) + hash, err := bp.Block.Hash() if err != nil { return err } - bs.blockProposal = nil - return nil -} + err = bs.updateBlockHashList(bp.Block.Height, hash) + if err != nil { + return err + } + + err = bs.ResetBlockProposal() + if err != nil { + return err + } -// ResetBlockProposal resets the block proposal -func (bs *BlockStore) ResetBlockProposal() error { - bs.blockProposal = nil return nil } @@ -143,16 +170,12 @@ func (bs *BlockStore) getBlockHashList() (*types.BlockHashList, error) { return types.NewBlockHashListFromEncoded(data) } -func (bs *BlockStore) updateBlockHashList(block *types.Block) error { +func (bs *BlockStore) updateBlockHashList(height uint64, hash gethCommon.Hash) error { bhl, err := bs.getBlockHashList() if err != nil { return err } - hash, err := block.Hash() - if err != nil { - return err - } - err = bhl.Push(block.Height, hash) + err = bhl.Push(height, hash) if err != nil { return err } diff --git a/fvm/evm/handler/blockstore_benchmark_test.go b/fvm/evm/handler/blockstore_benchmark_test.go new file mode 100644 index 00000000000..e55c2204205 --- /dev/null +++ b/fvm/evm/handler/blockstore_benchmark_test.go @@ -0,0 +1,59 @@ +package handler_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/onflow/flow-go/fvm/evm/handler" + "github.com/onflow/flow-go/fvm/evm/testutils" + "github.com/onflow/flow-go/model/flow" +) + +func BenchmarkProposalGrowth(b *testing.B) { benchmarkBlockProposalGrowth(b, 1000) } + +func benchmarkBlockProposalGrowth(b *testing.B, txCounts int) { + testutils.RunWithTestBackend(b, func(backend *testutils.TestBackend) { + testutils.RunWithTestFlowEVMRootAddress(b, backend, func(rootAddr flow.Address) { + + bs := handler.NewBlockStore(backend, rootAddr) + for i := 0; i < txCounts; i++ { + bp, err := bs.BlockProposal() + require.NoError(b, err) + res := testutils.RandomResultFixture(b) + bp.AppendTransaction(res) + err = bs.UpdateBlockProposal(bp) + require.NoError(b, err) + } + + // check the impact of updating block proposal after x number of transactions + backend.ResetStats() + startTime := time.Now() + bp, err := bs.BlockProposal() + require.NoError(b, err) + res := testutils.RandomResultFixture(b) + bp.AppendTransaction(res) + err = bs.UpdateBlockProposal(bp) + require.NoError(b, err) + + b.ReportMetric(float64(time.Since(startTime).Nanoseconds()), "proposal_update_time_ns") + b.ReportMetric(float64(backend.TotalBytesRead()), "proposal_update_bytes_read") + b.ReportMetric(float64(backend.TotalBytesWritten()), "proposal_update_bytes_written") + b.ReportMetric(float64(backend.TotalStorageSize()), "proposal_update_total_storage_size") + + // check the impact of block commit after x number of transactions + backend.ResetStats() + startTime = time.Now() + bp, err = bs.BlockProposal() + require.NoError(b, err) + err = bs.CommitBlockProposal(bp) + require.NoError(b, err) + + b.ReportMetric(float64(time.Since(startTime).Nanoseconds()), "block_commit_time_ns") + b.ReportMetric(float64(backend.TotalBytesRead()), "block_commit_bytes_read") + b.ReportMetric(float64(backend.TotalBytesWritten()), "block_commit_bytes_written") + b.ReportMetric(float64(backend.TotalStorageSize()), "block_commit_total_storage_size") + }) + }) +} diff --git a/fvm/evm/handler/blockstore_test.go b/fvm/evm/handler/blockstore_test.go index 5bc06a697ac..0b322ad8ea6 100644 --- a/fvm/evm/handler/blockstore_test.go +++ b/fvm/evm/handler/blockstore_test.go @@ -20,7 +20,7 @@ func TestBlockStore(t *testing.T) { testutils.RunWithTestFlowEVMRootAddress(t, backend, func(root flow.Address) { bs := handler.NewBlockStore(backend, root) - // check gensis block + // check the Genesis block b, err := bs.LatestBlock() require.NoError(t, err) require.Equal(t, types.GenesisBlock, b) @@ -28,7 +28,7 @@ func TestBlockStore(t *testing.T) { require.NoError(t, err) require.Equal(t, types.GenesisBlockHash, h) - // test block proposal from genesis + // test block proposal construction from the Genesis block bp, err := bs.BlockProposal() require.NoError(t, err) require.Equal(t, uint64(1), bp.Height) @@ -36,18 +36,43 @@ func TestBlockStore(t *testing.T) { require.NoError(t, err) require.Equal(t, expectedParentHash, bp.ParentBlockHash) - // commit block proposal + // if no commit and again block proposal call should return the same + retbp, err := bs.BlockProposal() + require.NoError(t, err) + require.Equal(t, bp, retbp) + + // update the block proposal + bp.TotalGasUsed += 100 + err = bs.UpdateBlockProposal(bp) + require.NoError(t, err) + + // reset the bs and check if it still return the block proposal + bs = handler.NewBlockStore(backend, root) + retbp, err = bs.BlockProposal() + require.NoError(t, err) + require.Equal(t, bp, retbp) + + // update the block proposal again supply := big.NewInt(100) bp.TotalSupply = supply - err = bs.CommitBlockProposal() + err = bs.UpdateBlockProposal(bp) require.NoError(t, err) - b, err = bs.LatestBlock() + // this should still return the gensis block + retb, err := bs.LatestBlock() require.NoError(t, err) - require.Equal(t, supply, b.TotalSupply) - require.Equal(t, uint64(1), b.Height) - bp, err = bs.BlockProposal() + require.Equal(t, types.GenesisBlock, retb) + + // commit the changes + err = bs.CommitBlockProposal(bp) require.NoError(t, err) - require.Equal(t, uint64(2), bp.Height) + retb, err = bs.LatestBlock() + require.NoError(t, err) + require.Equal(t, supply, retb.TotalSupply) + require.Equal(t, uint64(1), retb.Height) + + retbp, err = bs.BlockProposal() + require.NoError(t, err) + require.Equal(t, uint64(2), retbp.Height) // check block hashes // genesis @@ -58,7 +83,7 @@ func TestBlockStore(t *testing.T) { // block 1 h, err = bs.BlockHash(1) require.NoError(t, err) - expected, err := b.Hash() + expected, err := bp.Hash() require.NoError(t, err) require.Equal(t, expected, h) @@ -147,6 +172,9 @@ func TestBlockStore_AddedTimestamp(t *testing.T) { err = backend.SetValue(root[:], []byte(handler.BlockStoreLatestBlockKey), blockBytes2) require.NoError(t, err) + err = bs.ResetBlockProposal() + require.NoError(t, err) + block2, err := bs.LatestBlock() require.NoError(t, err) @@ -160,10 +188,10 @@ func TestBlockStore_AddedTimestamp(t *testing.T) { bp, err := bs.BlockProposal() require.NoError(t, err) - blockBytes, err = bp.ToBytes() + blockBytes3, err := gethRLP.EncodeToBytes(bp.Block) require.NoError(t, err) - err = backend.SetValue(root[:], []byte(handler.BlockStoreLatestBlockKey), blockBytes) + err = backend.SetValue(root[:], []byte(handler.BlockStoreLatestBlockKey), blockBytes3) require.NoError(t, err) bb, err := bs.LatestBlock() diff --git a/fvm/evm/handler/handler.go b/fvm/evm/handler/handler.go index 30d6b04da51..d41e5f591f7 100644 --- a/fvm/evm/handler/handler.go +++ b/fvm/evm/handler/handler.go @@ -198,12 +198,6 @@ func (h *ContractHandler) batchRun(rlpEncodedTxs [][]byte, coinbase types.Addres return nil, types.ErrUnexpectedEmptyResult } - // Populate receipt root - bp.PopulateReceiptRoot(res) - - // Populate total gas used - bp.CalculateGasUsage(res) - // meter all the transaction gas usage and append hashes to the block for _, r := range res { // meter gas anyway (even for invalid or failed states) @@ -214,15 +208,10 @@ func (h *ContractHandler) batchRun(rlpEncodedTxs [][]byte, coinbase types.Addres // include it in a block only if valid (not invalid) if !r.Invalid() { - bp.AppendTxHash(r.TxHash) + bp.AppendTransaction(r) } } - blockHash, err := bp.Hash() - if err != nil { - return nil, err - } - // if there were no valid transactions skip emitting events // and commiting a new block if len(bp.TransactionHashes) == 0 { @@ -237,7 +226,6 @@ func (h *ContractHandler) batchRun(rlpEncodedTxs [][]byte, coinbase types.Addres r, rlpEncodedTxs[i], bp.Height, - blockHash, )) if err != nil { return nil, err @@ -246,12 +234,14 @@ func (h *ContractHandler) batchRun(rlpEncodedTxs [][]byte, coinbase types.Addres h.tracer.Collect(r.TxHash) } - err = h.emitEvent(types.NewBlockEvent(bp)) + // update the block proposal + err = h.blockStore.UpdateBlockProposal(bp) if err != nil { return nil, err } - err = h.blockStore.CommitBlockProposal() + // TODO: don't commit right away. + err = h.commitBlockProposal() if err != nil { return nil, err } @@ -259,6 +249,33 @@ func (h *ContractHandler) batchRun(rlpEncodedTxs [][]byte, coinbase types.Addres return res, nil } +// CommitBlockProposal commits the block proposal and add a new block to the EVM blockchain +func (h *ContractHandler) CommitBlockProposal() { + panicOnError(h.commitBlockProposal()) +} + +func (h *ContractHandler) commitBlockProposal() error { + // load latest block proposal + bp, err := h.blockStore.BlockProposal() + if err != nil { + return err + } + + // commit the proposal + err = h.blockStore.CommitBlockProposal(bp) + if err != nil { + return err + } + + // emit block executed event + err = h.emitEvent(types.NewBlockEvent(&bp.Block)) + if err != nil { + return err + } + + return nil +} + func (h *ContractHandler) run( rlpEncodedTx []byte, coinbase types.Address, @@ -312,32 +329,23 @@ func (h *ContractHandler) run( return nil, err } - bp.AppendTxHash(res.TxHash) - - // Populate receipt root - bp.PopulateReceiptRoot([]*types.Result{res}) - - // Populate total gas used - bp.CalculateGasUsage([]*types.Result{res}) - - blockHash, err := bp.Hash() - if err != nil { - return nil, err - } + // append tx to the block proposal + bp.AppendTransaction(res) // step 4 - emit events - err = h.emitEvent(types.NewTransactionEvent(res, rlpEncodedTx, bp.Height, blockHash)) + err = h.emitEvent(types.NewTransactionEvent(res, rlpEncodedTx, bp.Height)) if err != nil { return nil, err } - err = h.emitEvent(types.NewBlockEvent(bp)) + // update the block proposal + err = h.blockStore.UpdateBlockProposal(bp) if err != nil { return nil, err } - // step 5 - commit block proposal - err = h.blockStore.CommitBlockProposal() + // TODO: don't commit right away. + err = h.commitBlockProposal() if err != nil { return nil, err } @@ -502,13 +510,8 @@ func (h *ContractHandler) executeAndHandleCall( return nil, err } - bp.AppendTxHash(res.TxHash) - - // Populate receipt root - bp.PopulateReceiptRoot([]*types.Result{res}) - - // Populate total gas used - bp.CalculateGasUsage([]*types.Result{res}) + // append transaction to the block proposal + bp.AppendTransaction(res) if totalSupplyDiff != nil { if deductSupplyDiff { @@ -521,11 +524,6 @@ func (h *ContractHandler) executeAndHandleCall( } } - blockHash, err := bp.Hash() - if err != nil { - return nil, err - } - // emit events encoded, err := call.Encode() if err != nil { @@ -533,19 +531,20 @@ func (h *ContractHandler) executeAndHandleCall( } err = h.emitEvent( - types.NewTransactionEvent(res, encoded, bp.Height, blockHash), + types.NewTransactionEvent(res, encoded, bp.Height), ) if err != nil { return nil, err } - err = h.emitEvent(types.NewBlockEvent(bp)) + // update the block proposal + err = h.blockStore.UpdateBlockProposal(bp) if err != nil { return nil, err } - // commit block proposal - err = h.blockStore.CommitBlockProposal() + // TODO: don't commit right away. + err = h.commitBlockProposal() if err != nil { return nil, err } diff --git a/fvm/evm/stdlib/contract.cdc b/fvm/evm/stdlib/contract.cdc index 38d79c3b799..4856ada9537 100644 --- a/fvm/evm/stdlib/contract.cdc +++ b/fvm/evm/stdlib/contract.cdc @@ -60,8 +60,6 @@ contract EVM { logs: [UInt8], // block height in which transaction was inclued blockHeight: UInt64, - // block hash in which transaction was included - blockHash: String, /// captures the hex encoded data that is returned from /// the evm. For contract deployments /// it returns the code deployed to @@ -815,4 +813,19 @@ contract EVM { ?.borrowBridgeAccessor() ?? panic("Could not borrow reference to the EVM bridge") } + + /// The Heartbeat resource controls the block production + access(all) resource Heartbeat { + + /// heartbeat calls commit block proposals and forms new blocks including all the + /// recently executed transactions. + /// The Flow protocol makes sure to call this function once per block as a system call. + access(all) fun heartbeat() { + InternalEVM.commitBlockProposal() + } + } + + init() { + self.account.storage.save(<-create Heartbeat(), to: /storage/EVMHeartbeat) + } } diff --git a/fvm/evm/stdlib/contract.go b/fvm/evm/stdlib/contract.go index 29940b9bb14..b8926099031 100644 --- a/fvm/evm/stdlib/contract.go +++ b/fvm/evm/stdlib/contract.go @@ -1528,7 +1528,7 @@ func newInternalEVMTypeDepositFunction( ) *interpreter.HostFunctionValue { return interpreter.NewStaticHostFunctionValue( gauge, - internalEVMTypeCallFunctionType, + internalEVMTypeDepositFunctionType, func(invocation interpreter.Invocation) interpreter.Value { inter := invocation.Interpreter locationRange := invocation.LocationRange @@ -1599,7 +1599,7 @@ func newInternalEVMTypeBalanceFunction( ) *interpreter.HostFunctionValue { return interpreter.NewStaticHostFunctionValue( gauge, - internalEVMTypeCallFunctionType, + internalEVMTypeBalanceFunctionType, func(invocation interpreter.Invocation) interpreter.Value { inter := invocation.Interpreter locationRange := invocation.LocationRange @@ -1641,7 +1641,7 @@ func newInternalEVMTypeNonceFunction( ) *interpreter.HostFunctionValue { return interpreter.NewStaticHostFunctionValue( gauge, - internalEVMTypeCallFunctionType, + internalEVMTypeNonceFunctionType, func(invocation interpreter.Invocation) interpreter.Value { inter := invocation.Interpreter locationRange := invocation.LocationRange @@ -1683,7 +1683,7 @@ func newInternalEVMTypeCodeFunction( ) *interpreter.HostFunctionValue { return interpreter.NewStaticHostFunctionValue( gauge, - internalEVMTypeCallFunctionType, + internalEVMTypeCodeFunctionType, func(invocation interpreter.Invocation) interpreter.Value { inter := invocation.Interpreter locationRange := invocation.LocationRange @@ -1725,7 +1725,7 @@ func newInternalEVMTypeCodeHashFunction( ) *interpreter.HostFunctionValue { return interpreter.NewStaticHostFunctionValue( gauge, - internalEVMTypeCallFunctionType, + internalEVMTypeCodeHashFunctionType, func(invocation interpreter.Invocation) interpreter.Value { inter := invocation.Interpreter locationRange := invocation.LocationRange @@ -1770,7 +1770,7 @@ func newInternalEVMTypeWithdrawFunction( ) *interpreter.HostFunctionValue { return interpreter.NewStaticHostFunctionValue( gauge, - internalEVMTypeCallFunctionType, + internalEVMTypeWithdrawFunctionType, func(invocation interpreter.Invocation) interpreter.Value { inter := invocation.Interpreter locationRange := invocation.LocationRange @@ -1868,7 +1868,7 @@ func newInternalEVMTypeDeployFunction( ) *interpreter.HostFunctionValue { return interpreter.NewStaticHostFunctionValue( gauge, - internalEVMTypeCallFunctionType, + internalEVMTypeDeployFunctionType, func(invocation interpreter.Invocation) interpreter.Value { inter := invocation.Interpreter locationRange := invocation.LocationRange @@ -1945,7 +1945,7 @@ func newInternalEVMTypeCastToAttoFLOWFunction( ) *interpreter.HostFunctionValue { return interpreter.NewStaticHostFunctionValue( gauge, - internalEVMTypeCallFunctionType, + internalEVMTypeCastToAttoFLOWFunctionType, func(invocation interpreter.Invocation) interpreter.Value { balanceValue, ok := invocation.Arguments[0].(interpreter.UFix64Value) if !ok { @@ -1975,7 +1975,7 @@ func newInternalEVMTypeCastToFLOWFunction( ) *interpreter.HostFunctionValue { return interpreter.NewStaticHostFunctionValue( gauge, - internalEVMTypeCallFunctionType, + internalEVMTypeCastToFLOWFunctionType, func(invocation interpreter.Invocation) interpreter.Value { balanceValue, ok := invocation.Arguments[0].(interpreter.UIntValue) if !ok { @@ -1992,6 +1992,27 @@ func newInternalEVMTypeCastToFLOWFunction( ) } +const internalEVMTypeCommitBlockProposalFunctionName = "commitBlockProposal" + +var internalEVMTypeCommitBlockProposalFunctionType = &sema.FunctionType{ + Parameters: []sema.Parameter{}, + ReturnTypeAnnotation: sema.NewTypeAnnotation(sema.VoidType), +} + +func newInternalEVMTypeCommitBlockProposalFunction( + gauge common.MemoryGauge, + handler types.ContractHandler, +) *interpreter.HostFunctionValue { + return interpreter.NewStaticHostFunctionValue( + gauge, + internalEVMTypeCommitBlockProposalFunctionType, + func(invocation interpreter.Invocation) interpreter.Value { + handler.CommitBlockProposal() + return interpreter.Void + }, + ) +} + const internalEVMTypeGetLatestBlockFunctionName = "getLatestBlock" var internalEVMTypeGetLatestBlockFunctionType = &sema.FunctionType{ @@ -2006,7 +2027,7 @@ func newInternalEVMTypeGetLatestBlockFunction( ) *interpreter.HostFunctionValue { return interpreter.NewStaticHostFunctionValue( gauge, - internalEVMTypeCallFunctionType, + internalEVMTypeGetLatestBlockFunctionType, func(invocation interpreter.Invocation) interpreter.Value { inter := invocation.Interpreter locationRange := invocation.LocationRange @@ -2098,6 +2119,7 @@ func NewInternalEVMContractValue( internalEVMTypeCastToFLOWFunctionName: newInternalEVMTypeCastToFLOWFunction(gauge), internalEVMTypeGetLatestBlockFunctionName: newInternalEVMTypeGetLatestBlockFunction(gauge, handler), internalEVMTypeDryRunFunctionName: newInternalEVMTypeDryRunFunction(gauge, handler), + internalEVMTypeCommitBlockProposalFunctionName: newInternalEVMTypeCommitBlockProposalFunction(gauge, handler), }, nil, nil, @@ -2216,6 +2238,12 @@ var InternalEVMContractType = func() *sema.CompositeType { internalEVMTypeGetLatestBlockFunctionType, "", ), + sema.NewUnmeteredPublicFunctionMember( + ty, + internalEVMTypeCommitBlockProposalFunctionName, + internalEVMTypeCommitBlockProposalFunctionType, + "", + ), }) return ty }() diff --git a/fvm/evm/stdlib/contract_test.go b/fvm/evm/stdlib/contract_test.go index 170ebb3a17c..0ce2a8ed6ae 100644 --- a/fvm/evm/stdlib/contract_test.go +++ b/fvm/evm/stdlib/contract_test.go @@ -38,6 +38,7 @@ type testContractHandler struct { batchRun func(txs [][]byte, coinbase types.Address) []*types.ResultSummary generateResourceUUID func() uint64 dryRun func(tx []byte, from types.Address) *types.ResultSummary + commitBlockProposal func() } var _ types.ContractHandler = &testContractHandler{} @@ -101,6 +102,13 @@ func (t *testContractHandler) GenerateResourceUUID() uint64 { return t.generateResourceUUID() } +func (t *testContractHandler) CommitBlockProposal() { + if t.commitBlockProposal == nil { + panic("unexpected CommitBlockProposal") + } + t.commitBlockProposal() +} + type testFlowAccount struct { address types.Address balance func() types.Balance @@ -3428,7 +3436,7 @@ func TestEVMCreateCadenceOwnedAccount(t *testing.T) { require.NoError(t, err) expected := cadence.NewArray([]cadence.Value{ - cadence.UInt8(4), cadence.UInt8(0), + cadence.UInt8(5), cadence.UInt8(0), cadence.UInt8(0), cadence.UInt8(0), cadence.UInt8(0), cadence.UInt8(0), cadence.UInt8(0), cadence.UInt8(0), @@ -3453,10 +3461,10 @@ func TestEVMCreateCadenceOwnedAccount(t *testing.T) { CheckCadenceEventTypes(t, events, expectedEventTypes) // check cadence owned account created events - expectedCoaAddress := types.Address{3} + expectedCoaAddress := types.Address{4} requireEqualEventAddress(t, events[0], expectedCoaAddress) - expectedCoaAddress = types.Address{4} + expectedCoaAddress = types.Address{5} requireEqualEventAddress(t, events[1], expectedCoaAddress) } @@ -3472,7 +3480,7 @@ func TestCadenceOwnedAccountCall(t *testing.T) { handler := &testContractHandler{ evmContractAddress: common.Address(contractsAddress), accountByAddress: func(fromAddress types.Address, isAuthorized bool) types.Account { - assert.Equal(t, types.Address{3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, fromAddress) + assert.Equal(t, types.Address{4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, fromAddress) assert.True(t, isAuthorized) return &testFlowAccount{ @@ -3483,7 +3491,7 @@ func TestCadenceOwnedAccountCall(t *testing.T) { limit types.GasLimit, balance types.Balance, ) *types.ResultSummary { - assert.Equal(t, types.Address{2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, toAddress) + assert.Equal(t, types.Address{4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, toAddress) assert.Equal(t, types.Data{4, 5, 6}, data) assert.Equal(t, types.GasLimit(9999), limit) assert.Equal(t, types.NewBalanceFromUFix64(expectedBalance), balance) @@ -3512,7 +3520,7 @@ func TestCadenceOwnedAccountCall(t *testing.T) { bal.setFLOW(flow: 1.23) let response = cadenceOwnedAccount.call( to: EVM.EVMAddress( - bytes: [2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + bytes: [4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ), data: [4, 5, 6], gasLimit: 9999, @@ -3599,7 +3607,7 @@ func TestEVMAddressDeposit(t *testing.T) { handler := &testContractHandler{ accountByAddress: func(fromAddress types.Address, isAuthorized bool) types.Account { - assert.Equal(t, types.Address{2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, fromAddress) + assert.Equal(t, types.Address{4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, fromAddress) assert.False(t, isAuthorized) return &testFlowAccount{ @@ -3636,7 +3644,7 @@ func TestEVMAddressDeposit(t *testing.T) { destroy minter let address = EVM.EVMAddress( - bytes: [2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + bytes: [4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ) address.deposit(from: <-vault) } @@ -3708,7 +3716,7 @@ func TestCOADeposit(t *testing.T) { var deposited bool - var expectedCoaAddress = types.Address{5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + var expectedCoaAddress = types.Address{6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} handler := &testContractHandler{ @@ -3837,7 +3845,7 @@ func TestCOADeposit(t *testing.T) { // check depositedUUID, based on the transaction content // its expected the uuid of 4 be allocated to the source vault. - expectedDepositedUUID := cadence.UInt64(4) + expectedDepositedUUID := cadence.UInt64(5) require.Equal(t, expectedDepositedUUID, tokenDepositEventFields["depositedUUID"], @@ -3861,7 +3869,7 @@ func TestCadenceOwnedAccountWithdraw(t *testing.T) { var nextUUID uint64 = 1 - var expectedCoaAddress = types.Address{5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + var expectedCoaAddress = types.Address{6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} handler := &testContractHandler{ flowTokenAddress: common.Address(contractsAddress), @@ -4035,7 +4043,7 @@ func TestCadenceOwnedAccountDeploy(t *testing.T) { handler := &testContractHandler{ evmContractAddress: common.Address(contractsAddress), accountByAddress: func(fromAddress types.Address, isAuthorized bool) types.Account { - assert.Equal(t, types.Address{3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, fromAddress) + assert.Equal(t, types.Address{4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, fromAddress) assert.True(t, isAuthorized) return &testFlowAccount{ @@ -4048,7 +4056,7 @@ func TestCadenceOwnedAccountDeploy(t *testing.T) { return &types.ResultSummary{ Status: types.StatusSuccessful, - DeployedContractAddress: &types.Address{4}, + DeployedContractAddress: &types.Address{5}, ReturnedData: types.Data{5}, } }, @@ -4074,7 +4082,7 @@ func TestCadenceOwnedAccountDeploy(t *testing.T) { ) destroy cadenceOwnedAccount - assert(res.deployedContract?.bytes == [4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + assert(res.deployedContract?.bytes == [5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) return res.data } `) @@ -4224,7 +4232,7 @@ func TestEVMAccountBalance(t *testing.T) { flowTokenAddress: common.Address(contractsAddress), evmContractAddress: common.Address(contractsAddress), accountByAddress: func(fromAddress types.Address, isAuthorized bool) types.Account { - assert.Equal(t, types.Address{3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, fromAddress) + assert.Equal(t, types.Address{4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, fromAddress) assert.False(t, isAuthorized) return &testFlowAccount{ @@ -4259,7 +4267,7 @@ func TestEVMAccountNonce(t *testing.T) { flowTokenAddress: common.Address(contractsAddress), evmContractAddress: common.Address(contractsAddress), accountByAddress: func(fromAddress types.Address, isAuthorized bool) types.Account { - assert.Equal(t, types.Address{3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, fromAddress) + assert.Equal(t, types.Address{4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, fromAddress) assert.False(t, isAuthorized) return &testFlowAccount{ @@ -4299,7 +4307,7 @@ func TestEVMAccountCode(t *testing.T) { flowTokenAddress: common.Address(contractsAddress), evmContractAddress: common.Address(contractsAddress), accountByAddress: func(fromAddress types.Address, isAuthorized bool) types.Account { - assert.Equal(t, types.Address{3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, fromAddress) + assert.Equal(t, types.Address{4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, fromAddress) assert.False(t, isAuthorized) return &testFlowAccount{ @@ -4339,7 +4347,7 @@ func TestEVMAccountCodeHash(t *testing.T) { flowTokenAddress: common.Address(contractsAddress), evmContractAddress: common.Address(contractsAddress), accountByAddress: func(fromAddress types.Address, isAuthorized bool) types.Account { - assert.Equal(t, types.Address{3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, fromAddress) + assert.Equal(t, types.Address{4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, fromAddress) assert.False(t, isAuthorized) return &testFlowAccount{ diff --git a/fvm/evm/testutils/misc.go b/fvm/evm/testutils/misc.go index b2f5493cdd7..2f4605cdf09 100644 --- a/fvm/evm/testutils/misc.go +++ b/fvm/evm/testutils/misc.go @@ -79,3 +79,19 @@ func COAOwnershipProofInContextFixture(t testing.TB) *types.COAOwnershipProofInC EVMAddress: RandomAddress(t), } } + +func RandomResultFixture(t testing.TB) *types.Result { + contractAddress := RandomAddress(t) + return &types.Result{ + Index: 1, + TxType: 1, + TxHash: RandomCommonHash(t), + ReturnedData: RandomData(t), + GasConsumed: RandomGas(1000), + DeployedContractAddress: &contractAddress, + Logs: []*gethTypes.Log{ + GetRandomLogFixture(t), + GetRandomLogFixture(t), + }, + } +} diff --git a/fvm/evm/types/block.go b/fvm/evm/types/block.go index 48e155324b4..cd0c232e54e 100644 --- a/fvm/evm/types/block.go +++ b/fvm/evm/types/block.go @@ -50,52 +50,19 @@ func (b *Block) Hash() (gethCommon.Hash, error) { return gethCrypto.Keccak256Hash(data), err } -// PopulateReceiptRoot populates receipt root with the given results -func (b *Block) PopulateReceiptRoot(results []*Result) { - if len(results) == 0 { - b.ReceiptRoot = gethTypes.EmptyReceiptsHash - return - } - - receipts := make(gethTypes.Receipts, 0) - for _, res := range results { - r := res.Receipt() - if r == nil { - continue - } - receipts = append(receipts, r) - } - b.ReceiptRoot = gethTypes.DeriveSha(receipts, gethTrie.NewStackTrie(nil)) -} - -// CalculateGasUsage sums up all the gas transactions in the block used -func (b *Block) CalculateGasUsage(results []*Result) { - for _, res := range results { - b.TotalGasUsed += res.GasConsumed - } -} - -// AppendTxHash appends a transaction hash to the list of transaction hashes of the block -func (b *Block) AppendTxHash(txHash gethCommon.Hash) { - b.TransactionHashes = append(b.TransactionHashes, txHash) -} - // NewBlock constructs a new block func NewBlock( parentBlockHash gethCommon.Hash, height uint64, timestamp uint64, totalSupply *big.Int, - receiptRoot gethCommon.Hash, - txHashes []gethCommon.Hash, ) *Block { return &Block{ - ParentBlockHash: parentBlockHash, - Height: height, - Timestamp: timestamp, - TotalSupply: totalSupply, - ReceiptRoot: receiptRoot, - TransactionHashes: txHashes, + ParentBlockHash: parentBlockHash, + Height: height, + Timestamp: timestamp, + TotalSupply: totalSupply, + ReceiptRoot: gethTypes.EmptyReceiptsHash, } } @@ -110,7 +77,6 @@ func NewBlockFromBytes(encoded []byte) (*Block, error) { return nil, err } } - return res, nil } @@ -124,6 +90,74 @@ var GenesisBlock = &Block{ var GenesisBlockHash, _ = GenesisBlock.Hash() +// BlockProposal is a EVM block proposal +// holding all the iterim data of block before commitment +type BlockProposal struct { + Block + + // Receipts keeps a order list of light receipts generated during block execution + Receipts []LightReceipt +} + +// AppendTransaction appends a transaction hash to the list of transaction hashes of the block +// and also update the receipts +func (b *BlockProposal) AppendTransaction(res *Result) { + if res == nil { + return + } + b.TransactionHashes = append(b.TransactionHashes, res.TxHash) + r := res.LightReceipt(b.TotalGasUsed) + if r == nil { + return + } + b.Receipts = append(b.Receipts, *r) + b.TotalGasUsed = r.CumulativeGasUsed +} + +// PopulateReceiptRoot populates receipt root hash value +func (b *BlockProposal) PopulateReceiptRoot() { + if len(b.Receipts) == 0 { + b.ReceiptRoot = gethTypes.EmptyReceiptsHash + return + } + receipts := make(gethTypes.Receipts, len(b.Receipts)) + for i, lr := range b.Receipts { + receipts[i] = lr.ToReceipt() + } + + b.ReceiptRoot = gethTypes.DeriveSha(receipts, gethTrie.NewStackTrie(nil)) +} + +// ToBytes encodes the block proposal into bytes +func (b *BlockProposal) ToBytes() ([]byte, error) { + return gethRLP.EncodeToBytes(b) +} + +// NewBlockProposalFromBytes constructs a new block proposal from encoded data +func NewBlockProposalFromBytes(encoded []byte) (*BlockProposal, error) { + res := &BlockProposal{} + return res, gethRLP.DecodeBytes(encoded, res) +} + +func NewBlockProposal( + parentBlockHash gethCommon.Hash, + height uint64, + timestamp uint64, + totalSupply *big.Int, +) *BlockProposal { + return &BlockProposal{ + Block: Block{ + ParentBlockHash: parentBlockHash, + Height: height, + Timestamp: timestamp, + TotalSupply: totalSupply, + ReceiptRoot: gethTypes.EmptyRootHash, + TransactionHashes: make([]gethCommon.Hash, 0), + }, + Receipts: make([]LightReceipt, 0), + } +} + // todo remove this if confirmed we no longer need it on testnet, mainnet and previewnet. // Below block type section, defines earlier block types, diff --git a/fvm/evm/types/block_test.go b/fvm/evm/types/block_test.go index 83d180f5c8b..3bbb237bd8c 100644 --- a/fvm/evm/types/block_test.go +++ b/fvm/evm/types/block_test.go @@ -33,15 +33,29 @@ func Test_BlockHash(t *testing.T) { // hashes should not equal if any data is changed assert.NotEqual(t, h1, h2) +} + +func Test_BlockProposal(t *testing.T) { + bp := NewBlockProposal(gethCommon.Hash{1}, 1, 0, nil) - b.PopulateReceiptRoot(nil) - require.Equal(t, gethTypes.EmptyReceiptsHash, b.ReceiptRoot) + bp.AppendTransaction(nil) + require.Empty(t, bp.TransactionHashes) + require.Equal(t, uint64(0), bp.TotalGasUsed) - res := Result{ + bp.PopulateReceiptRoot() + require.Equal(t, gethTypes.EmptyReceiptsHash, bp.ReceiptRoot) + + res := &Result{ + TxHash: gethCommon.Hash{2}, GasConsumed: 10, } - b.PopulateReceiptRoot([]*Result{&res}) - require.NotEqual(t, gethTypes.EmptyReceiptsHash, b.ReceiptRoot) + bp.AppendTransaction(res) + require.Equal(t, res.TxHash, bp.TransactionHashes[0]) + require.Equal(t, res.GasConsumed, bp.TotalGasUsed) + require.Equal(t, *res.LightReceipt(0), bp.Receipts[0]) + + bp.PopulateReceiptRoot() + require.NotEqual(t, gethTypes.EmptyReceiptsHash, bp.ReceiptRoot) } func Test_DecodeBlocks(t *testing.T) { diff --git a/fvm/evm/types/events.go b/fvm/evm/types/events.go index a766e05e997..bbf7b582a90 100644 --- a/fvm/evm/types/events.go +++ b/fvm/evm/types/events.go @@ -46,13 +46,11 @@ func NewTransactionEvent( result *Result, payload []byte, blockHeight uint64, - blockHash gethCommon.Hash, ) *Event { return &Event{ Etype: EventTypeTransactionExecuted, Payload: &transactionEvent{ BlockHeight: blockHeight, - BlockHash: blockHash, Payload: payload, Result: result, }, @@ -205,7 +203,6 @@ type TransactionEventPayload struct { ContractAddress string `cadence:"contractAddress"` Logs cadence.Array `cadence:"logs"` BlockHeight uint64 `cadence:"blockHeight"` - BlockHash string `cadence:"blockHash"` ErrorMessage string `cadence:"errorMessage"` ReturnedData cadence.Array `cadence:"returnedData"` PrecompiledCalls cadence.Array `cadence:"precompiledCalls"` diff --git a/fvm/evm/types/events_test.go b/fvm/evm/types/events_test.go index 87619ae0220..9774203f801 100644 --- a/fvm/evm/types/events_test.go +++ b/fvm/evm/types/events_test.go @@ -119,7 +119,7 @@ func TestEVMTransactionExecutedEventCCFEncodingDecoding(t *testing.T) { } t.Run("evm.TransactionExecuted with failed status", func(t *testing.T) { - event := types.NewTransactionEvent(txResult, txBytes, blockHeight, blockHash) + event := types.NewTransactionEvent(txResult, txBytes, blockHeight) ev, err := event.Payload.ToCadence(evmLocation) require.NoError(t, err) @@ -127,7 +127,6 @@ func TestEVMTransactionExecutedEventCCFEncodingDecoding(t *testing.T) { require.NoError(t, err) assert.Equal(t, tep.BlockHeight, blockHeight) - assert.Equal(t, tep.BlockHash, blockHash.Hex()) assert.Equal(t, tep.Hash, txHash.Hex()) assert.Equal(t, tep.Payload, types.BytesToCadenceUInt8ArrayValue(txBytes)) assert.Equal(t, types.ErrorCode(tep.ErrorCode), types.ExecutionErrCodeOutOfGas) @@ -161,7 +160,7 @@ func TestEVMTransactionExecutedEventCCFEncodingDecoding(t *testing.T) { t.Run("evm.TransactionExecuted with non-failed status", func(t *testing.T) { txResult.VMError = nil - event := types.NewTransactionEvent(txResult, txBytes, blockHeight, blockHash) + event := types.NewTransactionEvent(txResult, txBytes, blockHeight) ev, err := event.Payload.ToCadence(evmLocation) require.NoError(t, err) @@ -169,7 +168,6 @@ func TestEVMTransactionExecutedEventCCFEncodingDecoding(t *testing.T) { require.NoError(t, err) assert.Equal(t, tep.BlockHeight, blockHeight) - assert.Equal(t, tep.BlockHash, blockHash.Hex()) assert.Equal(t, tep.Hash, txHash.Hex()) assert.Equal(t, tep.Payload, types.BytesToCadenceUInt8ArrayValue(txBytes)) assert.Equal(t, types.ErrCodeNoError, types.ErrorCode(tep.ErrorCode)) diff --git a/fvm/evm/types/handler.go b/fvm/evm/types/handler.go index 051e3a660f6..0d64f690d97 100644 --- a/fvm/evm/types/handler.go +++ b/fvm/evm/types/handler.go @@ -55,6 +55,9 @@ type ContractHandler interface { // GenerateResourceUUID generates a new UUID for a resource GenerateResourceUUID() uint64 + + // Constructs and commits a new block from the block proposal + CommitBlockProposal() } // AddressAllocator allocates addresses, used by the handler @@ -81,12 +84,12 @@ type BlockStore interface { // BlockHash returns the hash of the block at the given height BlockHash(height uint64) (gethCommon.Hash, error) - // BlockProposal returns the block proposal - BlockProposal() (*Block, error) + // BlockProposal returns the active block proposal + BlockProposal() (*BlockProposal, error) - // CommitBlockProposal commits the block proposal and update the chain of blocks - CommitBlockProposal() error + // UpdateBlockProposal replaces the current block proposal with the ones passed + UpdateBlockProposal(*BlockProposal) error - // ResetBlockProposal resets the block proposal - ResetBlockProposal() error + // CommitBlockProposal commits the block proposal and update the chain of blocks + CommitBlockProposal(*BlockProposal) error } diff --git a/fvm/evm/types/result.go b/fvm/evm/types/result.go index 1e69b434bfb..b90eec15e2f 100644 --- a/fvm/evm/types/result.go +++ b/fvm/evm/types/result.go @@ -1,6 +1,7 @@ package types import ( + "github.com/onflow/go-ethereum/common" gethCommon "github.com/onflow/go-ethereum/common" gethTypes "github.com/onflow/go-ethereum/core/types" ) @@ -33,7 +34,7 @@ var ( StatusSuccessful Status = 3 ) -// ResultSummary summerizes the outcome of a EVM call or tx run +// ResultSummary summarizes the outcome of a EVM call or tx run type ResultSummary struct { Status Status ErrorCode ErrorCode @@ -60,7 +61,7 @@ func NewInvalidResult(tx *gethTypes.Transaction, err error) *Result { // evm transaction. // Its more comprehensive than typical evm receipt, usually // the receipt generation requires some extra calculation (e.g. Deployed contract address) -// but we take a different apporach here and include more data so that +// but we take a different approach here and include more data so that // it requires less work for anyone who tracks and consume results. type Result struct { // captures error returned during validation step (pre-checks) @@ -70,7 +71,7 @@ type Result struct { // type of transaction defined by the evm package // see DirectCallTxType as extra type we added type for direct calls. TxType uint8 - // total gas consumed during an opeartion + // total gas consumed during execution GasConsumed uint64 // total gas refunds after transaction execution GasRefund uint64 @@ -78,9 +79,9 @@ type Result struct { DeployedContractAddress *Address // returned data from a function call ReturnedData []byte - // EVM logs (events that are emited by evm) + // EVM logs (events that are emitted by evm) Logs []*gethTypes.Log - // TX hash holdes the cached value of tx hash + // TX hash holds the cached value of tx hash TxHash gethCommon.Hash // transaction block inclusion index Index uint16 @@ -123,19 +124,26 @@ func (res *Result) VMErrorString() string { // can be used by json-rpc and other integration to be returned. // // This is method is also used to construct block receipt root hash -// which requires the return receipt satisfy RLP encoding and cover these feilds +// which requires the return receipt satisfy RLP encoding and cover these fields // Type (txType), PostState or Status, CumulativeGasUsed, Logs and Logs Bloom // and for each log, Address, Topics, Data (consensus fields) // During execution we also do fill in BlockNumber, TxIndex, Index (event index) -func (res *Result) Receipt() *gethTypes.Receipt { +func (res *Result) Receipt(cumulativeGasUsed uint64) *gethTypes.Receipt { if res.Invalid() { return nil } + receipt := &gethTypes.Receipt{ - Type: res.TxType, - CumulativeGasUsed: res.GasConsumed, // TODO: update to capture cumulative + GasUsed: res.GasConsumed, + CumulativeGasUsed: cumulativeGasUsed + res.GasConsumed, Logs: res.Logs, } + + // only add tx type if not direct call + if res.TxType != DirectCallTxType { + receipt.Type = res.TxType + } + if res.DeployedContractAddress != nil { receipt.ContractAddress = res.DeployedContractAddress.ToCommon() } @@ -149,6 +157,41 @@ func (res *Result) Receipt() *gethTypes.Receipt { return receipt } +// LightReceipt constructs a light receipt from the result +// that is used for storing in block proposal. +func (res *Result) LightReceipt(gasUsedUntilNow uint64) *LightReceipt { + if res.Invalid() { + return nil + } + + receipt := &LightReceipt{ + CumulativeGasUsed: res.GasConsumed + gasUsedUntilNow, + } + + receipt.Logs = make([]LightLog, len(res.Logs)) + for i, l := range res.Logs { + receipt.Logs[i] = LightLog{ + Address: l.Address, + Topics: l.Topics, + Data: l.Data, + } + } + + // only add tx type if not direct call + if res.TxType != DirectCallTxType { + receipt.Type = res.TxType + } + + // add status + if res.Failed() { + receipt.Status = uint8(gethTypes.ReceiptStatusFailed) + } else { + receipt.Status = uint8(gethTypes.ReceiptStatusSuccessful) + } + + return receipt +} + // ResultSummary constructs a result summary func (res *Result) ResultSummary() *ResultSummary { rs := &ResultSummary{ @@ -175,3 +218,54 @@ func (res *Result) ResultSummary() *ResultSummary { return rs } + +// LightLog captures only consensus fields of an EVM log +// used by the LightReceipt +type LightLog struct { + // address of the contract that generated the event + Address common.Address + // list of topics provided by the contract. + Topics []common.Hash + // supplied by the contract, usually ABI-encoded + Data []byte +} + +// LightReceipt captures only the consensus fields of +// a receipt, making storage of receipts for the purpose +// of trie building more storage efficient. +// +// Note that we don't store Bloom as we can reconstruct it +// later. We don't have PostState and we use a uint8 for +// status as there is currently only acts as boolean. +// Data shows that using the light receipt results in 60% storage reduction +// for block proposals and the extra overheads are manageable. +type LightReceipt struct { + Type uint8 + Status uint8 + CumulativeGasUsed uint64 + Logs []LightLog +} + +// ToReceipt constructs a Receipt from the LightReceipt +// Warning, this only populates the consensus fields +// and if you want the full data, use the receipt +// from the result. +func (lr *LightReceipt) ToReceipt() *gethTypes.Receipt { + receipt := &gethTypes.Receipt{ + Type: lr.Type, + Status: uint64(lr.Status), + CumulativeGasUsed: lr.CumulativeGasUsed, + } + + receipt.Logs = make([]*gethTypes.Log, len(lr.Logs)) + for i, l := range lr.Logs { + receipt.Logs[i] = &gethTypes.Log{ + Address: l.Address, + Topics: l.Topics, + Data: l.Data, + } + } + + receipt.Bloom = gethTypes.CreateBloom(gethTypes.Receipts{receipt}) + return receipt +} diff --git a/fvm/evm/types/result_test.go b/fvm/evm/types/result_test.go new file mode 100644 index 00000000000..3d56714b74a --- /dev/null +++ b/fvm/evm/types/result_test.go @@ -0,0 +1,28 @@ +package types_test + +import ( + "testing" + + gethTypes "github.com/onflow/go-ethereum/core/types" + gethTrie "github.com/onflow/go-ethereum/trie" + "github.com/stretchr/testify/require" + + "github.com/onflow/flow-go/fvm/evm/testutils" +) + +func TestLightReceipts(t *testing.T) { + resCount := 10 + receipts := make(gethTypes.Receipts, resCount) + reconstructedReceipts := make(gethTypes.Receipts, resCount) + var totalGas uint64 + for i := 0; i < resCount; i++ { + res := testutils.RandomResultFixture(t) + receipts[i] = res.Receipt(totalGas) + reconstructedReceipts[i] = res.LightReceipt(totalGas).ToReceipt() + totalGas += res.GasConsumed + } + // the root hash for reconstructed receipts should match the receipts + root1 := gethTypes.DeriveSha(receipts, gethTrie.NewStackTrie(nil)) + root2 := gethTypes.DeriveSha(reconstructedReceipts, gethTrie.NewStackTrie(nil)) + require.Equal(t, root1, root2) +} diff --git a/utils/unittest/execution_state.go b/utils/unittest/execution_state.go index 86bc106a08e..cf331cdfd7d 100644 --- a/utils/unittest/execution_state.go +++ b/utils/unittest/execution_state.go @@ -23,7 +23,7 @@ const ServiceAccountPrivateKeySignAlgo = crypto.ECDSAP256 const ServiceAccountPrivateKeyHashAlgo = hash.SHA2_256 // Pre-calculated state commitment with root account with the above private key -const GenesisStateCommitmentHex = "1f38e32700ca6f4ad92834f1a086624f172868b0c58c249775305bb1b74735a4" +const GenesisStateCommitmentHex = "d31a2d57a74c97aca5df275819b4b42141672808e0bece2fda417e8dc563c818" var GenesisStateCommitment flow.StateCommitment @@ -87,10 +87,10 @@ func genesisCommitHexByChainID(chainID flow.ChainID) string { return GenesisStateCommitmentHex } if chainID == flow.Testnet { - return "6f6069d94c283ae744a5bfdd3193cb31aa96efdd34d56eb0372a7f06918527df" + return "e222dc960f317d13cb2b10b92760a8f3ee4c89052745d560f3a7cebb4517dc08" } if chainID == flow.Sandboxnet { return "e1c08b17f9e5896f03fe28dd37ca396c19b26628161506924fbf785834646ea1" } - return "31df4b53081ea3d8db029483b2e4eebc86ca5e9fb970e7b1fb6e299c43c90952" + return "5e178f8a23aa21c27b127104a3964b3e3ea273c4c531d48b572086fa68e07eba" }