Skip to content

Commit

Permalink
Merge pull request #6157 from onflow/ramtin/6152-capture-arch-calls
Browse files Browse the repository at this point in the history
[Flow EVM] capturing extra precompiled calls outcome as part of results and tx executed events
  • Loading branch information
ramtinms authored Jul 1, 2024
2 parents 581ebaf + 63b76a1 commit b7a7d15
Show file tree
Hide file tree
Showing 17 changed files with 448 additions and 78 deletions.
2 changes: 1 addition & 1 deletion engine/execution/state/bootstrap/bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func TestBootstrapLedger(t *testing.T) {
}

func TestBootstrapLedger_ZeroTokenSupply(t *testing.T) {
expectedStateCommitmentBytes, _ := hex.DecodeString("405d5de5149727bf841aa545a858612783811bcea74f365e7be536a388bb3f91")
expectedStateCommitmentBytes, _ := hex.DecodeString("3c67d8e4d76dd71357deff581c44b849904f25317a7f3c4e87717abf1fc0827d")
expectedStateCommitment, err := flow.ToStateCommitment(expectedStateCommitmentBytes)
require.NoError(t, err)

Expand Down
9 changes: 6 additions & 3 deletions fvm/evm/emulator/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ type Config struct {
TxContext *gethVM.TxContext
// base unit of gas for direct calls
DirectCallBaseGasUsage uint64
// list of precompiles
ExtraPrecompiles []types.PrecompiledContract
}

func (c *Config) ChainRules() gethParams.Rules {
Expand Down Expand Up @@ -183,11 +185,11 @@ func WithDirectCallBaseGasUsage(gas uint64) Option {
}
}

// WithExtraPrecompiles appends precompile list with extra precompiles
func WithExtraPrecompiles(precompiles []types.Precompile) Option {
// WithExtraPrecompiledContracts appends the precompiled contract list with extra precompiled contracts
func WithExtraPrecompiledContracts(precompiledContracts []types.PrecompiledContract) Option {
return func(c *Config) *Config {
extraPreCompMap := make(map[gethCommon.Address]gethVM.PrecompiledContract)
for _, pc := range precompiles {
for _, pc := range precompiledContracts {
extraPreCompMap[pc.Address().ToCommon()] = pc
}
c.BlockContext.GetPrecompile = func(rules gethParams.Rules, addr gethCommon.Address) (gethVM.PrecompiledContract, bool) {
Expand All @@ -197,6 +199,7 @@ func WithExtraPrecompiles(precompiles []types.Precompile) Option {
}
return gethCore.GetPrecompile(rules, addr)
}
c.ExtraPrecompiles = precompiledContracts
return c
}
}
Expand Down
40 changes: 36 additions & 4 deletions fvm/evm/emulator/emulator.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func newConfig(ctx types.BlockContext) *Config {
WithBlockTime(ctx.BlockTimestamp),
WithCoinbase(ctx.GasFeeCollector.ToCommon()),
WithDirectCallBaseGasUsage(ctx.DirectCallBaseGasUsage),
WithExtraPrecompiles(ctx.ExtraPrecompiles),
WithExtraPrecompiledContracts(ctx.ExtraPrecompiledContracts),
WithGetBlockHashFunction(ctx.GetHashFunc),
WithRandom(&ctx.Random),
WithTransactionTracer(ctx.Tracer),
Expand Down Expand Up @@ -95,9 +95,6 @@ func (bv *ReadOnlyBlockView) CodeHashOf(address types.Address) ([]byte, error) {
}

// BlockView allows mutation of the evm state as part of a block
//
// TODO: allow multiple calls per block view
// TODO: add block level commit (separation of trie commit to storage)
type BlockView struct {
config *Config
rootAddr flow.Address
Expand Down Expand Up @@ -545,11 +542,14 @@ func (proc *procedure) run(
txIndex uint,
txType uint8,
) (*types.Result, error) {
var err error
res := types.Result{
TxType: txType,
TxHash: txHash,
}

// reset precompile tracking
proc.resetPrecompileTracking()
gasPool := (*gethCore.GasPool)(&proc.config.BlockContext.GasLimit)
execResult, err := gethCore.NewStateTransition(
proc.evm,
Expand All @@ -573,6 +573,13 @@ func (proc *procedure) run(
res.GasConsumed = execResult.UsedGas
res.GasRefund = proc.state.GetRefund()
res.Index = uint16(txIndex)

if proc.extraPrecompiledIsCalled() {
res.PrecompiledCalls, err = proc.capturePrecompiledCalls()
if err != nil {
return nil, err
}
}
// we need to capture the returned value no matter the status
// if the tx is reverted the error message is returned as returned value
res.ReturnedData = execResult.ReturnData
Expand All @@ -597,6 +604,31 @@ func (proc *procedure) run(
return &res, nil
}

func (proc *procedure) resetPrecompileTracking() {
for _, pc := range proc.config.ExtraPrecompiles {
pc.Reset()
}
}

func (proc *procedure) extraPrecompiledIsCalled() bool {
for _, pc := range proc.config.ExtraPrecompiles {
if pc.IsCalled() {
return true
}
}
return false
}

func (proc *procedure) capturePrecompiledCalls() ([]byte, error) {
apc := make(types.AggregatedPrecompiledCalls, 0)
for _, pc := range proc.config.ExtraPrecompiles {
if pc.IsCalled() {
apc = append(apc, *pc.CapturedCalls())
}
}
return apc.Encode()
}

func AddOne64th(n uint64) uint64 {
// NOTE: Go's integer division floors, but that is desirable here
return n + (n / 64)
Expand Down
80 changes: 68 additions & 12 deletions fvm/evm/emulator/emulator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ func TestContractInteraction(t *testing.T) {
require.NoError(t, err)
require.GreaterOrEqual(t, res.GasConsumed, uint64(40_000))
nonce += 1
require.Empty(t, res.PrecompiledCalls)
})
})

Expand Down Expand Up @@ -755,21 +756,44 @@ func TestCallingExtraPrecompiles(t *testing.T) {
input := []byte{1, 2}
output := []byte{3, 4}
addr := testutils.RandomAddress(t)
pc := &MockedPrecompile{
isCalled := false
capturedCall := &types.PrecompiledCalls{
Address: addr,
RequiredGasCalls: []types.RequiredGasCall{{
Input: input,
Output: uint64(10),
}},
RunCalls: []types.RunCall{{
Input: input,
Output: output,
ErrorMsg: "",
}},
}
pc := &MockedPrecompiled{
AddressFunc: func() types.Address {
return addr
},
RequiredGasFunc: func(input []byte) uint64 {
isCalled = true
return uint64(10)
},
RunFunc: func(inp []byte) ([]byte, error) {
isCalled = true
require.Equal(t, input, inp)
return output, nil
},
IsCalledFunc: func() bool {
return isCalled
},
CapturedCallsFunc: func() *types.PrecompiledCalls {
return capturedCall
},
ResetFunc: func() {
},
}

ctx := types.NewDefaultBlockContext(blockNumber.Uint64())
ctx.ExtraPrecompiles = []types.Precompile{pc}
ctx.ExtraPrecompiledContracts = []types.PrecompiledContract{pc}

blk, err := em.NewBlockView(ctx)
require.NoError(t, err)
Expand All @@ -786,6 +810,12 @@ func TestCallingExtraPrecompiles(t *testing.T) {
)
require.NoError(t, err)
require.Equal(t, output, res.ReturnedData)
require.NotEmpty(t, res.PrecompiledCalls)

apc, err := types.AggregatedPrecompileCallsFromEncoded(res.PrecompiledCalls)
require.NoError(t, err)
require.Len(t, apc, 1)
require.Equal(t, *capturedCall, apc[0])
})
})
})
Expand Down Expand Up @@ -1019,29 +1049,55 @@ func TestTransactionTracing(t *testing.T) {

}

type MockedPrecompile struct {
AddressFunc func() types.Address
RequiredGasFunc func(input []byte) uint64
RunFunc func(input []byte) ([]byte, error)
type MockedPrecompiled struct {
AddressFunc func() types.Address
RequiredGasFunc func(input []byte) uint64
RunFunc func(input []byte) ([]byte, error)
CapturedCallsFunc func() *types.PrecompiledCalls
ResetFunc func()
IsCalledFunc func() bool
}

func (mp *MockedPrecompile) Address() types.Address {
var _ types.PrecompiledContract = &MockedPrecompiled{}

func (mp *MockedPrecompiled) Address() types.Address {
if mp.AddressFunc == nil {
panic("Address not set for the mocked precompile")
panic("Address not set for the mocked precompiled contract")
}
return mp.AddressFunc()
}

func (mp *MockedPrecompile) RequiredGas(input []byte) uint64 {
func (mp *MockedPrecompiled) RequiredGas(input []byte) uint64 {
if mp.RequiredGasFunc == nil {
panic("RequiredGas not set for the mocked precompile")
panic("RequiredGas not set for the mocked precompiled contract")
}
return mp.RequiredGasFunc(input)
}

func (mp *MockedPrecompile) Run(input []byte) ([]byte, error) {
func (mp *MockedPrecompiled) IsCalled() bool {
if mp.IsCalledFunc == nil {
panic("IsCalled not set for the mocked precompiled contract")
}
return mp.IsCalledFunc()
}

func (mp *MockedPrecompiled) Run(input []byte) ([]byte, error) {
if mp.RunFunc == nil {
panic("Run not set for the mocked precompile")
panic("Run not set for the mocked precompiled contract")
}
return mp.RunFunc(input)
}

func (mp *MockedPrecompiled) CapturedCalls() *types.PrecompiledCalls {
if mp.CapturedCallsFunc == nil {
panic("CapturedCalls not set for the mocked precompiled contract")
}
return mp.CapturedCallsFunc()
}

func (mp *MockedPrecompiled) Reset() {
if mp.ResetFunc == nil {
panic("Reset not set for the mocked precompiled contract")
}
mp.ResetFunc()
}
42 changes: 21 additions & 21 deletions fvm/evm/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ import (
// ContractHandler is responsible for triggering calls to emulator, metering,
// event emission and updating the block
type ContractHandler struct {
flowChainID flow.ChainID
evmContractAddress flow.Address
flowTokenAddress common.Address
blockStore types.BlockStore
addressAllocator types.AddressAllocator
backend types.Backend
emulator types.Emulator
precompiles []types.Precompile
tracer debug.EVMTracer
flowChainID flow.ChainID
evmContractAddress flow.Address
flowTokenAddress common.Address
blockStore types.BlockStore
addressAllocator types.AddressAllocator
backend types.Backend
emulator types.Emulator
precompiledContracts []types.PrecompiledContract
tracer debug.EVMTracer
}

func (h *ContractHandler) FlowTokenAddress() common.Address {
Expand All @@ -53,15 +53,15 @@ func NewContractHandler(
tracer debug.EVMTracer,
) *ContractHandler {
return &ContractHandler{
flowChainID: flowChainID,
evmContractAddress: evmContractAddress,
flowTokenAddress: flowTokenAddress,
blockStore: blockStore,
addressAllocator: addressAllocator,
backend: backend,
emulator: emulator,
tracer: tracer,
precompiles: preparePrecompiles(evmContractAddress, randomBeaconAddress, addressAllocator, backend),
flowChainID: flowChainID,
evmContractAddress: evmContractAddress,
flowTokenAddress: flowTokenAddress,
blockStore: blockStore,
addressAllocator: addressAllocator,
backend: backend,
emulator: emulator,
tracer: tracer,
precompiledContracts: preparePrecompiledContracts(evmContractAddress, randomBeaconAddress, addressAllocator, backend),
}
}

Expand Down Expand Up @@ -456,9 +456,9 @@ func (h *ContractHandler) getBlockContext() (types.BlockContext, error) {
panicOnError(err) // we have to handle it here given we can't continue with it even in try case
return hash
},
ExtraPrecompiles: h.precompiles,
Random: rand,
Tracer: h.tracer.TxTracer(),
ExtraPrecompiledContracts: h.precompiledContracts,
Random: rand,
Tracer: h.tracer.TxTracer(),
}, nil
}

Expand Down
41 changes: 41 additions & 0 deletions fvm/evm/handler/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,47 @@ func TestHandler_COA(t *testing.T) {

ret := foa.Call(arch, precompiles.FlowBlockHeightFuncSig[:], math.MaxUint64, types.NewBalanceFromUFix64(0))
require.Equal(t, big.NewInt(int64(blockHeight)), new(big.Int).SetBytes(ret.ReturnedData))

events := backend.Events()
require.Len(t, events, 6)
// last transaction executed event
event := events[4]
assert.Equal(t, event.Type, types.EventTypeTransactionExecuted)
ev, err := jsoncdc.Decode(nil, event.Payload)
require.NoError(t, err)
cadenceEvent, ok := ev.(cadence.Event)
require.True(t, ok)
txEventPayload, err := types.DecodeTransactionEventPayload(cadenceEvent)
require.NoError(t, err)

values := txEventPayload.PrecompiledCalls.Values
aggregated := make([]byte, len(values))
for i, v := range values {
aggregated[i] = uint8(v.(cadence.UInt8))
}
apc, err := types.AggregatedPrecompileCallsFromEncoded(aggregated)
require.NoError(t, err)

require.False(t, apc.IsEmpty())
pc := apc[0]
require.Equal(t, arch, pc.Address)
require.Len(t, pc.RequiredGasCalls, 1)
require.Equal(t,
pc.RequiredGasCalls[0],
types.RequiredGasCall{
Input: precompiles.FlowBlockHeightFuncSig[:],
Output: precompiles.FlowBlockHeightFixedGas,
},
)
require.Len(t, pc.RunCalls, 1)
require.Equal(t,
pc.RunCalls[0],
types.RunCall{
Input: precompiles.FlowBlockHeightFuncSig[:],
Output: ret.ReturnedData,
ErrorMsg: "",
},
)
})
})
})
Expand Down
6 changes: 3 additions & 3 deletions fvm/evm/handler/precompiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ import (
"github.com/onflow/flow-go/model/flow"
)

func preparePrecompiles(
func preparePrecompiledContracts(
evmContractAddress flow.Address,
randomBeaconAddress flow.Address,
addressAllocator types.AddressAllocator,
backend types.Backend,
) []types.Precompile {
) []types.PrecompiledContract {
archAddress := addressAllocator.AllocatePrecompileAddress(1)
archContract := precompiles.ArchContract(
archAddress,
Expand All @@ -27,7 +27,7 @@ func preparePrecompiles(
randomSourceProvider(randomBeaconAddress, backend),
revertibleRandomGenerator(backend),
)
return []types.Precompile{archContract}
return []types.PrecompiledContract{archContract}
}

func blockHeightProvider(backend types.Backend) func() (uint64, error) {
Expand Down
Loading

0 comments on commit b7a7d15

Please sign in to comment.