Skip to content

Commit

Permalink
Merge pull request onflow#6545 from onflow/ramtin/evm-offchain-part2
Browse files Browse the repository at this point in the history
[EVM] Offchain package - part 2
  • Loading branch information
ramtinms authored Oct 21, 2024
2 parents 1b731dd + 76758aa commit a4773d7
Show file tree
Hide file tree
Showing 14 changed files with 981 additions and 9 deletions.
13 changes: 6 additions & 7 deletions fvm/evm/evm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import (
"github.com/onflow/flow-go/fvm/evm/events"
"github.com/onflow/flow-go/fvm/evm/impl"
"github.com/onflow/flow-go/fvm/evm/stdlib"
"github.com/onflow/flow-go/fvm/evm/testutils"
. "github.com/onflow/flow-go/fvm/evm/testutils"
"github.com/onflow/flow-go/fvm/evm/types"
"github.com/onflow/flow-go/fvm/storage/snapshot"
Expand Down Expand Up @@ -112,12 +111,12 @@ func TestEVMRun(t *testing.T) {
// assert event fields are correct
require.Len(t, output.Events, 2)
txEvent := output.Events[0]
txEventPayload := testutils.TxEventToPayload(t, txEvent, sc.EVMContract.Address)
txEventPayload := TxEventToPayload(t, txEvent, sc.EVMContract.Address)
require.NoError(t, err)

// fee transfer event
feeTransferEvent := output.Events[1]
feeTranferEventPayload := testutils.TxEventToPayload(t, feeTransferEvent, sc.EVMContract.Address)
feeTranferEventPayload := TxEventToPayload(t, feeTransferEvent, sc.EVMContract.Address)
require.NoError(t, err)
require.Equal(t, uint16(types.ErrCodeNoError), feeTranferEventPayload.ErrorCode)
require.Equal(t, uint16(1), feeTranferEventPayload.Index)
Expand Down Expand Up @@ -379,7 +378,7 @@ func TestEVMRun(t *testing.T) {
require.NotEmpty(t, state.WriteSet)

txEvent := output.Events[0]
txEventPayload := testutils.TxEventToPayload(t, txEvent, sc.EVMContract.Address)
txEventPayload := TxEventToPayload(t, txEvent, sc.EVMContract.Address)

require.NotEmpty(t, txEventPayload.Hash)

Expand Down Expand Up @@ -512,7 +511,7 @@ func TestEVMBatchRun(t *testing.T) {

// last event is fee transfer event
feeTransferEvent := output.Events[batchCount]
feeTranferEventPayload := testutils.TxEventToPayload(t, feeTransferEvent, sc.EVMContract.Address)
feeTranferEventPayload := TxEventToPayload(t, feeTransferEvent, sc.EVMContract.Address)
require.NoError(t, err)
require.Equal(t, uint16(types.ErrCodeNoError), feeTranferEventPayload.ErrorCode)
require.Equal(t, uint16(batchCount), feeTranferEventPayload.Index)
Expand Down Expand Up @@ -996,7 +995,7 @@ func TestEVMAddressDeposit(t *testing.T) {

// tx executed event
txEvent := output.Events[2]
txEventPayload := testutils.TxEventToPayload(t, txEvent, sc.EVMContract.Address)
txEventPayload := TxEventToPayload(t, txEvent, sc.EVMContract.Address)

// deposit event
depositEvent := output.Events[3]
Expand Down Expand Up @@ -1250,7 +1249,7 @@ func TestCadenceOwnedAccountFunctionalities(t *testing.T) {
))

addr := cadence.NewArray(
ConvertToCadence(testutils.RandomAddress(t).Bytes()),
ConvertToCadence(RandomAddress(t).Bytes()),
).WithType(stdlib.EVMAddressBytesCadenceType)

script := fvm.Script(code).WithArguments(
Expand Down
27 changes: 27 additions & 0 deletions fvm/evm/offchain/blocks/blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ type Blocks struct {
bhl *handler.BlockHashList
}

var _ types.BlockSnapshot = (*Blocks)(nil)

// NewBlocks constructs a new blocks type
func NewBlocks(
chainID flow.ChainID,
Expand Down Expand Up @@ -99,6 +101,31 @@ func (b *Blocks) BlockHash(height uint64) (gethCommon.Hash, error) {
return hash, err
}

// BlockContext constructs a block context for the latest block
func (b *Blocks) BlockContext() (types.BlockContext, error) {
bm, err := b.LatestBlock()
if err != nil {
return types.BlockContext{}, err
}

return types.BlockContext{
ChainID: types.EVMChainIDFromFlowChainID(b.chainID),
BlockNumber: bm.Height,
BlockTimestamp: bm.Timestamp,
DirectCallBaseGasUsage: types.DefaultDirectCallBaseGasUsage,
DirectCallGasPrice: types.DefaultDirectCallGasPrice,
GasFeeCollector: types.CoinbaseAddress,
GetHashFunc: func(n uint64) gethCommon.Hash {
hash, err := b.BlockHash(n)
if err != nil {
panic(err)
}
return hash
},
Random: bm.Random,
}, nil
}

// storeBlockMetaData stores the block meta data into storage
func (b *Blocks) storeBlockMetaData(bm *Meta) error {
// store the encoded data into backend
Expand Down
74 changes: 74 additions & 0 deletions fvm/evm/offchain/blocks/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package blocks

import (
"fmt"

"github.com/onflow/flow-go/fvm/evm/events"
"github.com/onflow/flow-go/fvm/evm/types"
"github.com/onflow/flow-go/model/flow"
)

// BasicProvider implements a ledger-backed basic block snapshot provider
// it assumes sequential progress on blocks and expects a
// a OnBlockReceived call before block execution and
// a follow up OnBlockExecuted call after block execution.
type BasicProvider struct {
blks *Blocks
latestBlockPayload *events.BlockEventPayload
}

var _ types.BlockSnapshotProvider = (*BasicProvider)(nil)

func NewBasicProvider(
chainID flow.ChainID,
storage types.BackendStorage,
rootAddr flow.Address,
) (*BasicProvider, error) {
blks, err := NewBlocks(chainID, rootAddr, storage)
if err != nil {
return nil, err
}
return &BasicProvider{blks: blks}, nil
}

// GetSnapshotAt returns a block snapshot at the given height
// Snapshot at a height is not available until `OnBlockReceived` is called for that height.
func (p *BasicProvider) GetSnapshotAt(height uint64) (
types.BlockSnapshot,
error,
) {
if p.latestBlockPayload.Height != height {
return nil, fmt.Errorf("active block height doesn't match expected: %d, got: %d", p.latestBlockPayload.Height, height)
}
return p.blks, nil
}

// OnBlockReceived should be called before executing blocks.
func (p *BasicProvider) OnBlockReceived(blockEvent *events.BlockEventPayload) error {
p.latestBlockPayload = blockEvent
// push the new block meta
// it should be done before execution so block context creation
// can be done properly
return p.blks.PushBlockMeta(
NewMeta(
blockEvent.Height,
blockEvent.Timestamp,
blockEvent.PrevRandao,
),
)
}

// OnBlockExecuted should be called after executing blocks.
func (p *BasicProvider) OnBlockExecuted(
height uint64,
resCol types.ReplayResultCollector) error {
// we push the block hash after execution, so the behaviour of the blockhash is
// identical to the evm.handler.
if p.latestBlockPayload.Height != height {
return fmt.Errorf("active block height doesn't match expected: %d, got: %d", p.latestBlockPayload.Height, height)
}
return p.blks.PushBlockHash(
p.latestBlockPayload.Height,
p.latestBlockPayload.Hash,
)
}
94 changes: 94 additions & 0 deletions fvm/evm/offchain/storage/ephemeral.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package storage

import (
"fmt"

"github.com/onflow/atree"

"github.com/onflow/flow-go/fvm/environment"
"github.com/onflow/flow-go/fvm/evm/types"
"github.com/onflow/flow-go/model/flow"
)

// EphemeralStorage holds on to register changes instead of applying them directly to
// the provided backend storage. It can be used for dry running transaction/calls
// or batching updates for atomic operations.
type EphemeralStorage struct {
parent types.BackendStorage
deltas map[flow.RegisterID]flow.RegisterValue
}

// NewEphemeralStorage constructs a new EphemeralStorage
func NewEphemeralStorage(parent types.BackendStorage) *EphemeralStorage {
return &EphemeralStorage{
parent: parent,
deltas: make(map[flow.RegisterID]flow.RegisterValue),
}
}

var _ types.BackendStorage = (*EphemeralStorage)(nil)

var _ types.ReplayResultCollector = (*EphemeralStorage)(nil)

// GetValue reads a register value
func (s *EphemeralStorage) GetValue(owner []byte, key []byte) ([]byte, error) {
// check delta first
ret, found := s.deltas[RegisterID(owner, key)]
if found {
return ret, nil
}
return s.parent.GetValue(owner, key)
}

// SetValue sets a register value
func (s *EphemeralStorage) SetValue(owner, key, value []byte) error {
s.deltas[RegisterID(owner, key)] = value
return nil
}

// ValueExists checks if a register exists
func (s *EphemeralStorage) ValueExists(owner []byte, key []byte) (bool, error) {
ret, err := s.GetValue(owner, key)
return len(ret) > 0, err
}

// AllocateSlabIndex allocates an slab index based on the given owner
func (s *EphemeralStorage) AllocateSlabIndex(owner []byte) (atree.SlabIndex, error) {
statusBytes, err := s.GetValue(owner, []byte(flow.AccountStatusKey))
if err != nil {
return atree.SlabIndex{}, err
}
if len(statusBytes) == 0 {
return atree.SlabIndex{}, fmt.Errorf("state for account not found")
}

status, err := environment.AccountStatusFromBytes(statusBytes)
if err != nil {
return atree.SlabIndex{}, err
}

// get and increment the index
index := status.SlabIndex()
newIndexBytes := index.Next()

// update the storageIndex bytes
status.SetStorageIndex(newIndexBytes)
err = s.SetValue(owner, []byte(flow.AccountStatusKey), status.ToBytes())
if err != nil {
return atree.SlabIndex{}, err
}
return index, nil
}

// StorageRegisterUpdates returns a map of register updates
func (s *EphemeralStorage) StorageRegisterUpdates() map[flow.RegisterID]flow.RegisterValue {
return s.deltas
}

// RegisterID creates a RegisterID from owner and key
func RegisterID(owner []byte, key []byte) flow.RegisterID {
return flow.RegisterID{
Owner: string(owner),
Key: string(key),
}
}
71 changes: 71 additions & 0 deletions fvm/evm/offchain/storage/ephemeral_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package storage_test

import (
"testing"

"github.com/onflow/atree"
"github.com/stretchr/testify/require"

"github.com/onflow/flow-go/fvm/environment"
"github.com/onflow/flow-go/fvm/evm/offchain/storage"
"github.com/onflow/flow-go/fvm/evm/testutils"
"github.com/onflow/flow-go/model/flow"
)

func TestEphemeralStorage(t *testing.T) {

parent := testutils.GetSimpleValueStore()
// preset value
owner := []byte("owner")
key1 := []byte("key1")
value1 := []byte{1}
value2 := []byte{2}
err := parent.SetValue(owner, key1, value1)
require.NoError(t, err)

s := storage.NewEphemeralStorage(parent)
ret, err := s.GetValue(owner, key1)
require.NoError(t, err)
require.Equal(t, value1, ret)
found, err := s.ValueExists(owner, key1)
require.NoError(t, err)
require.True(t, found)

// test set value
err = s.SetValue(owner, key1, value2)
require.NoError(t, err)
ret, err = s.GetValue(owner, key1)
require.NoError(t, err)
require.Equal(t, value2, ret)
// the parent should still return the value1
ret, err = parent.GetValue(owner, key1)
require.NoError(t, err)
require.Equal(t, value1, ret)

// test allocate slab id
_, err = s.AllocateSlabIndex(owner)
require.Error(t, err)

// setup account
err = s.SetValue(owner, []byte(flow.AccountStatusKey), environment.NewAccountStatus().ToBytes())
require.NoError(t, err)

sid, err := s.AllocateSlabIndex(owner)
require.NoError(t, err)
expected := atree.SlabIndex([8]byte{0, 0, 0, 0, 0, 0, 0, 1})
require.Equal(t, expected, sid)

sid, err = s.AllocateSlabIndex(owner)
require.NoError(t, err)
expected = atree.SlabIndex([8]byte{0, 0, 0, 0, 0, 0, 0, 2})
require.Equal(t, expected, sid)

// fetch delta
delta := s.StorageRegisterUpdates()
require.Len(t, delta, 2)
ret = delta[flow.RegisterID{
Owner: string(owner),
Key: string(key1),
}]
require.Equal(t, value2, ret)
}
44 changes: 44 additions & 0 deletions fvm/evm/offchain/storage/readonly.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package storage

import (
"errors"

"github.com/onflow/atree"

"github.com/onflow/flow-go/fvm/evm/types"
)

// ReadOnlyStorage wraps an snapshot and only provides read functionality.
type ReadOnlyStorage struct {
snapshot types.BackendStorageSnapshot
}

var _ types.BackendStorage = &ReadOnlyStorage{}

// NewReadOnlyStorage constructs a new ReadOnlyStorage using the given snapshot
func NewReadOnlyStorage(snapshot types.BackendStorageSnapshot) *ReadOnlyStorage {
return &ReadOnlyStorage{
snapshot,
}
}

// GetValue reads a register value
func (s *ReadOnlyStorage) GetValue(owner []byte, key []byte) ([]byte, error) {
return s.snapshot.GetValue(owner, key)
}

// SetValue returns an error if called
func (s *ReadOnlyStorage) SetValue(owner, key, value []byte) error {
return errors.New("unexpected call received")
}

// ValueExists checks if a register exists
func (s *ReadOnlyStorage) ValueExists(owner []byte, key []byte) (bool, error) {
val, err := s.snapshot.GetValue(owner, key)
return len(val) > 0, err
}

// AllocateSlabIndex returns an error if called
func (s *ReadOnlyStorage) AllocateSlabIndex(owner []byte) (atree.SlabIndex, error) {
return atree.SlabIndex{}, errors.New("unexpected call received")
}
Loading

0 comments on commit a4773d7

Please sign in to comment.