-
Notifications
You must be signed in to change notification settings - Fork 180
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6545 from onflow/ramtin/evm-offchain-part2
[EVM] Offchain package - part 2
- Loading branch information
Showing
14 changed files
with
981 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
Oops, something went wrong.