diff --git a/fvm/flex/environment/environment.go b/fvm/flex/environment/environment.go index a846213cdcd..e704102d389 100644 --- a/fvm/flex/environment/environment.go +++ b/fvm/flex/environment/environment.go @@ -2,8 +2,11 @@ package env import ( "bytes" + "encoding/binary" + "fmt" "math/big" + "github.com/onflow/flow-go/fvm/flex/models" "github.com/onflow/flow-go/fvm/flex/storage" "github.com/ethereum/go-ethereum/common" @@ -19,12 +22,13 @@ import ( // Environment is a one-time use flex environment and // should not be used more than once type Environment struct { - Config *Config - EVM *vm.EVM - Database *storage.Database - State *state.StateDB - Result *Result - Used bool + Config *Config + EVM *vm.EVM + Database *storage.Database + State *state.StateDB + LastExecutedBlock *models.FlexBlock + Result *Result + Used bool } // NewEnvironment constructs a new Flex Enviornment @@ -33,12 +37,12 @@ func NewEnvironment( db *storage.Database, ) (*Environment, error) { - rootHash, err := db.GetRootHash() + lastExcutedBlock, err := db.GetLatestBlock() if err != nil { return nil, err } - execState, err := state.New(rootHash, + execState, err := state.New(lastExcutedBlock.StateRoot, state.NewDatabase( rawdb.NewDatabase(db), ), @@ -56,10 +60,14 @@ func NewEnvironment( cfg.ChainConfig, cfg.EVMConfig, ), - Database: db, - State: execState, - Result: &Result{}, - Used: false, + Database: db, + State: execState, + LastExecutedBlock: lastExcutedBlock, + Result: &Result{ + UUIDIndex: lastExcutedBlock.UUIDIndex, + TotalSupplyOfNativeToken: lastExcutedBlock.TotalSupply, + }, + Used: false, }, nil } @@ -98,7 +106,14 @@ func (fe *Environment) commit() error { return err } - err = fe.Database.SetRootHash(newRoot) + newBlock := models.NewFlexBlock(fe.LastExecutedBlock.Height, + fe.Result.UUIDIndex, + fe.Result.TotalSupplyOfNativeToken, + newRoot, + types.EmptyRootHash, + ) + + err = fe.Database.SetLatestBlock(newBlock) if err != nil { return err } @@ -108,6 +123,35 @@ func (fe *Environment) commit() error { return nil } +// TODO: properly use an address generator (zeros + random section) and verify collision +// TODO: does this leads to trie depth issue? +func (fe *Environment) AllocateAddressAndMintTo(balance *big.Int) (*models.FlexAddress, error) { + if err := fe.checkExecuteOnce(); err != nil { + return nil, err + } + + target := fe.allocateAddress() + fe.mintTo(balance, target.ToCommon()) + + // TODO: emit an event + + return target, fe.commit() +} + +func (fe *Environment) allocateAddress() *models.FlexAddress { + target := models.FlexAddress{} + // first 12 bytes would be zero + // the next 8 bytes would be incremented of uuid + binary.BigEndian.PutUint64(target[12:], fe.LastExecutedBlock.UUIDIndex) + fe.Result.UUIDIndex++ + + // TODO: if account exist try some new number + // if fe.State.Exist(target.ToCommon()) { + // } + + return &target +} + // MintTo mints tokens into the target address, if the address dees not // exist it would create it first. // @@ -119,6 +163,12 @@ func (fe *Environment) MintTo(balance *big.Int, target common.Address) error { return err } + fe.mintTo(balance, target) + + return fe.commit() +} + +func (fe *Environment) mintTo(balance *big.Int, target common.Address) { // update the gas consumed // TODO: revisit // do it as the very first thing to prevent attacks fe.Result.GasConsumed = TransferGasUsage @@ -130,12 +180,11 @@ func (fe *Environment) MintTo(balance *big.Int, target common.Address) error { // add balance fe.State.AddBalance(target, balance) + fe.Result.TotalSupplyOfNativeToken += balance.Uint64() // we don't need to increment any nonce, given the origin doesn't exist // TODO: emit an event - - return fe.commit() } // WithdrawFrom deduct the balance from the given source account. @@ -148,6 +197,10 @@ func (fe *Environment) WithdrawFrom(amount *big.Int, source common.Address) erro return err } + if amount.Uint64() > fe.Result.TotalSupplyOfNativeToken { + return fmt.Errorf("total supply does not match %d > %d", amount.Uint64(), fe.Result.TotalSupplyOfNativeToken) + } + // update the gas consumed // TODO: revisit // do it as the very first thing to prevent attacks fe.Result.GasConsumed = TransferGasUsage @@ -167,6 +220,7 @@ func (fe *Environment) WithdrawFrom(amount *big.Int, source common.Address) erro // add balance fe.State.SubBalance(source, amount) + fe.Result.TotalSupplyOfNativeToken -= amount.Uint64() // we increment the nonce for source account cause // withdraw counts as a transaction (similar to the way calls increment the nonce) diff --git a/fvm/flex/environment/result.go b/fvm/flex/environment/result.go index 7f383b46453..56128f467f2 100644 --- a/fvm/flex/environment/result.go +++ b/fvm/flex/environment/result.go @@ -9,12 +9,14 @@ import ( // TODO we might not need this and reuse the ExecutionResult provided by VM // extra methods like convert can be provided by the interface layer type Result struct { - Failed bool // this tracks user level failures, other errors indicates fatal issues - RootHash common.Hash - DeployedContractAddress common.Address - RetValue []byte - GasConsumed uint64 - Logs []*types.Log + Failed bool // this tracks user level failures, other errors indicates fatal issues + RootHash common.Hash + DeployedContractAddress common.Address + RetValue []byte + GasConsumed uint64 + Logs []*types.Log + UUIDIndex uint64 + TotalSupplyOfNativeToken uint64 } func (r *Result) Events() { diff --git a/fvm/flex/handler.go b/fvm/flex/handler.go index 5dfcc8dbdde..e49b41808a7 100644 --- a/fvm/flex/handler.go +++ b/fvm/flex/handler.go @@ -93,11 +93,11 @@ func (h FlexContractHandler) NewFlowOwnedAccount() models.FlowOwnedAccount { // allocate a new address // TODO check for collission // from 20 bytes, the first 12 could be zero, the next 8 could be the output of a uuid generator (resourceID?) - // does this leads to trie depth issue? + panic("not implemented yet") } -func (h FlexContractHandler) LastExecutedBlock() models.FlexBlock { +func (h FlexContractHandler) LastExecutedBlock() *models.FlexBlock { panic("not implemented yet") } diff --git a/fvm/flex/models/block.go b/fvm/flex/models/block.go new file mode 100644 index 00000000000..660b12ef163 --- /dev/null +++ b/fvm/flex/models/block.go @@ -0,0 +1,100 @@ +package models + +import ( + "encoding/binary" + + gethCommon "github.com/ethereum/go-ethereum/common" + gethTypes "github.com/ethereum/go-ethereum/core/types" +) + +// FlexBlock represents a Flex block. +// It captures block info such as height and state +type FlexBlock struct { + // Height returns the height of this block + Height uint64 + + // UUID Index + UUIDIndex uint64 + + // holds the total amount of the native token deposited in Flex + TotalSupply uint64 + + // StateRoot returns the EVM root hash of the state after executing this block + StateRoot gethCommon.Hash + + // EventRoot returns the EVM root hash of the events emitted during execution of this block + EventRoot gethCommon.Hash +} + +const ( + // encodedUInt64Size = 8 + // encodedHashSize = 32 + encodedHeightSize = 8 + encodedUUIDIndexSize = 8 + encodedTotalSupplySize = 8 + encodedStateRootSize = 32 + encodedEventRootSize = 32 + // ( height + uuid + state root + event root) + encodedBlockSize = encodedHeightSize + + encodedUUIDIndexSize + + encodedTotalSupplySize + + encodedStateRootSize + + encodedEventRootSize +) + +func (b *FlexBlock) ToBytes() []byte { + encoded := make([]byte, encodedBlockSize) + var index int + // encode height first + binary.BigEndian.PutUint64(encoded[index:index+encodedHeightSize], b.Height) + index += encodedHeightSize + // encode the uuid index + binary.BigEndian.PutUint64(encoded[index:index+encodedUUIDIndexSize], b.UUIDIndex) + index += encodedUUIDIndexSize + // encode the total supply + binary.BigEndian.PutUint64(encoded[index:index+encodedTotalSupplySize], b.TotalSupply) + index += encodedTotalSupplySize + // encode state root + copy(encoded[index:index+encodedStateRootSize], b.StateRoot[:]) + index += encodedStateRootSize + // encode event root + copy(encoded[index:index+encodedEventRootSize], b.StateRoot[:]) + return encoded[:] +} + +func NewFlexBlock(height, uuidIndex, totalSupply uint64, stateRoot, eventRoot gethCommon.Hash) *FlexBlock { + return &FlexBlock{ + Height: height, + UUIDIndex: uuidIndex, + TotalSupply: totalSupply, + StateRoot: stateRoot, + EventRoot: eventRoot, + } +} + +func NewFlexBlockFromEncoded(encoded []byte) *FlexBlock { + var index int + height := binary.BigEndian.Uint64(encoded[index : index+encodedHeightSize]) + index += encodedHeightSize + uuidIndex := binary.BigEndian.Uint64(encoded[index : index+encodedUUIDIndexSize]) + index += encodedUUIDIndexSize + totalSupply := binary.BigEndian.Uint64(encoded[index : index+encodedTotalSupplySize]) + index += encodedTotalSupplySize + stateRoot := gethCommon.BytesToHash(encoded[index : index+encodedStateRootSize]) + index += encodedStateRootSize + eventRoot := gethCommon.BytesToHash(encoded[index : index+encodedEventRootSize]) + return &FlexBlock{ + Height: height, + UUIDIndex: uuidIndex, + TotalSupply: totalSupply, + StateRoot: stateRoot, + EventRoot: eventRoot, + } +} + +var GenesisFlexBlock = &FlexBlock{ + Height: uint64(0), + UUIDIndex: uint64(1), + StateRoot: gethTypes.EmptyRootHash, + EventRoot: gethTypes.EmptyRootHash, +} diff --git a/fvm/flex/models/models.go b/fvm/flex/models/models.go index e69b7edc3bf..6c98e4850cc 100644 --- a/fvm/flex/models/models.go +++ b/fvm/flex/models/models.go @@ -34,17 +34,6 @@ func NewFlexAddressFromString(str string) FlexAddress { return FlexAddress(gethCommon.BytesToAddress([]byte(str))) } -// FlexBlock represents an EVM block. -// It captures block info such as height and state -type FlexBlock interface { - // Height returns the height of this EVM block (auto-incremented number) - Height() uint64 - // StateRoot returns the EVM root hash of the state after executing this EVM block - StateRoot() gethCommon.Hash - // EventRoot returns the EVM root hash of the events emitted during execution of this EVM block - EventRoot() gethCommon.Hash -} - // Balance represents the balance of a Flex account // a separate type has been considered here to prevent // accidental dev mistakes when dealing with the conversion @@ -114,7 +103,7 @@ type FlexContractHandler interface { NewFlowOwnedAccount() FlowOwnedAccount // LastExecutedBlock returns information about the last executed block - LastExecutedBlock() FlexBlock + LastExecutedBlock() *FlexBlock // Run runs a transaction in the Flex environment, // collects the gas fees, and transfers the gas fees to the given coinbase account. diff --git a/fvm/flex/storage/database.go b/fvm/flex/storage/database.go index 08b7672a6fa..2563734c07f 100644 --- a/fvm/flex/storage/database.go +++ b/fvm/flex/storage/database.go @@ -5,24 +5,27 @@ import ( "sync" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb" "github.com/onflow/atree" + "github.com/onflow/flow-go/fvm/flex/models" "github.com/onflow/flow-go/model/flow" ) // TODO: add a vault to hold on to the passed Cadence token vaults under Flex account // this could make sure even if there is a bug in EVM there won't be more withdraws +// TODO: I put a lot of functionality into this database, but it could later on be by different +// components holding access to the ledger or runtime.Environment + var ( // err not implemented errNotImplemented = errors.New("not implemented yet") ) var FlexAddress = flow.BytesToAddress([]byte("Flex")) -var RootHashKey = "RootHash" +var FlexLatextBlockKey = "LatestBlock" // Database is an ephemeral key-value store. Apart from basic data storage // functionality it also supports batch writes and iterating over the keyspace in @@ -88,20 +91,20 @@ func (db *Database) Delete(key []byte) error { return err } -// SetRootHash sets the root hash +// SetLatestBlock sets the latest executed block // we have this functionality given we only allow on state to exist -func (db *Database) SetRootHash(root common.Hash) error { - return db.led.SetValue(FlexAddress[:], []byte(RootHashKey), root[:]) +func (db *Database) SetLatestBlock(block *models.FlexBlock) error { + return db.led.SetValue(FlexAddress[:], []byte(FlexLatextBlockKey), block.ToBytes()) } -// GetRootHash returns the latest root hash -// we have this functionality given we only allow on state to exist -func (db *Database) GetRootHash() (common.Hash, error) { - data, err := db.led.GetValue(FlexAddress[:], []byte(RootHashKey)) +// GetLatestBlock returns the latest executed block +// we have this functionality given we only allow on state to exist (no forks, etc.) +func (db *Database) GetLatestBlock() (*models.FlexBlock, error) { + data, err := db.led.GetValue(FlexAddress[:], []byte(FlexLatextBlockKey)) if len(data) == 0 { - return types.EmptyRootHash, err + return models.GenesisFlexBlock, err } - return common.Hash(data), err + return models.NewFlexBlockFromEncoded(data), err } // Close deallocates the internal map and ensures any consecutive data access op