From 5588e8dfa880cb3c594d342403d4e3693cdea3dd Mon Sep 17 00:00:00 2001 From: ramtinms Date: Thu, 18 Jul 2024 15:50:53 -0700 Subject: [PATCH 1/5] move pc tracking logic to emulator --- fvm/evm/emulator/config.go | 10 +- fvm/evm/emulator/emulator.go | 42 +----- fvm/evm/emulator/emulator_test.go | 41 +----- fvm/evm/emulator/tracker.go | 124 ++++++++++++++++++ fvm/evm/precompiles/precompile.go | 45 +------ fvm/evm/precompiles/precompile_test.go | 24 ---- fvm/evm/types/emulator.go | 3 - .../types/{precompile.go => precompiled.go} | 28 ++-- ...precompile_test.go => precompiled_test.go} | 1 - 9 files changed, 158 insertions(+), 160 deletions(-) create mode 100644 fvm/evm/emulator/tracker.go rename fvm/evm/types/{precompile.go => precompiled.go} (78%) rename fvm/evm/types/{precompile_test.go => precompiled_test.go} (99%) diff --git a/fvm/evm/emulator/config.go b/fvm/evm/emulator/config.go index 43a5e4eeb34..944aa210103 100644 --- a/fvm/evm/emulator/config.go +++ b/fvm/evm/emulator/config.go @@ -32,8 +32,8 @@ type Config struct { TxContext *gethVM.TxContext // base unit of gas for direct calls DirectCallBaseGasUsage uint64 - // list of precompiles - ExtraPrecompiles []types.PrecompiledContract + // captures extra precompiled calls + PCTracker *CallTracker } func (c *Config) ChainRules() gethParams.Rules { @@ -100,6 +100,7 @@ func defaultConfig() *Config { }, GetPrecompile: gethCore.GetPrecompile, }, + PCTracker: NewCallTracker(), } } @@ -191,7 +192,9 @@ func WithExtraPrecompiledContracts(precompiledContracts []types.PrecompiledContr return func(c *Config) *Config { extraPreCompMap := make(map[gethCommon.Address]gethVM.PrecompiledContract) for _, pc := range precompiledContracts { - extraPreCompMap[pc.Address().ToCommon()] = pc + // wrap pcs for tracking + wpc := c.PCTracker.RegisterPrecompiledContract(pc) + extraPreCompMap[pc.Address().ToCommon()] = wpc } c.BlockContext.GetPrecompile = func(rules gethParams.Rules, addr gethCommon.Address) (gethVM.PrecompiledContract, bool) { prec, found := extraPreCompMap[addr] @@ -200,7 +203,6 @@ func WithExtraPrecompiledContracts(precompiledContracts []types.PrecompiledContr } return gethCore.GetPrecompile(rules, addr) } - c.ExtraPrecompiles = precompiledContracts return c } } diff --git a/fvm/evm/emulator/emulator.go b/fvm/evm/emulator/emulator.go index ed1d0d7089f..058171c71cc 100644 --- a/fvm/evm/emulator/emulator.go +++ b/fvm/evm/emulator/emulator.go @@ -533,14 +533,14 @@ func (proc *procedure) runDirect( txHash gethCommon.Hash, txIndex uint, ) (*types.Result, error) { - // set the nonce for the message (needed for some opeartions like deployment) + // set the nonce for the message (needed for some operations like deployment) msg.Nonce = proc.state.GetNonce(msg.From) proc.evm.TxContext.Origin = msg.From res, err := proc.run(msg, txHash, txIndex, types.DirectCallTxType) if err != nil { return nil, err } - // all commmit errors (StateDB errors) has to be returned + // all commit errors (StateDB errors) has to be returned return res, proc.commit(true) } @@ -561,8 +561,8 @@ func (proc *procedure) run( TxHash: txHash, } - // reset precompile tracking - proc.resetPrecompileTracking() + // reset precompile tracking in case + proc.config.PCTracker.Reset() gasPool := (*gethCore.GasPool)(&proc.config.BlockContext.GasLimit) execResult, err := gethCore.NewStateTransition( proc.evm, @@ -586,12 +586,9 @@ 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 - } + res.PrecompiledCalls, err = proc.config.PCTracker.CapturedCalls() + 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 @@ -617,31 +614,6 @@ 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 (proc *procedure) captureTraceBegin( depth int, typ gethVM.OpCode, diff --git a/fvm/evm/emulator/emulator_test.go b/fvm/evm/emulator/emulator_test.go index dd503228d45..c2e4b493405 100644 --- a/fvm/evm/emulator/emulator_test.go +++ b/fvm/evm/emulator/emulator_test.go @@ -756,7 +756,6 @@ func TestCallingExtraPrecompiles(t *testing.T) { input := []byte{1, 2} output := []byte{3, 4} addr := testutils.RandomAddress(t) - isCalled := false capturedCall := &types.PrecompiledCalls{ Address: addr, RequiredGasCalls: []types.RequiredGasCall{{ @@ -774,22 +773,12 @@ func TestCallingExtraPrecompiles(t *testing.T) { 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()) @@ -1057,12 +1046,9 @@ func TestTransactionTracing(t *testing.T) { } 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 + AddressFunc func() types.Address + RequiredGasFunc func(input []byte) uint64 + RunFunc func(input []byte) ([]byte, error) } var _ types.PrecompiledContract = &MockedPrecompiled{} @@ -1081,30 +1067,9 @@ func (mp *MockedPrecompiled) RequiredGas(input []byte) uint64 { return mp.RequiredGasFunc(input) } -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 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() -} diff --git a/fvm/evm/emulator/tracker.go b/fvm/evm/emulator/tracker.go new file mode 100644 index 00000000000..6cbb1c21d28 --- /dev/null +++ b/fvm/evm/emulator/tracker.go @@ -0,0 +1,124 @@ +package emulator + +import ( + "bytes" + "sort" + + "github.com/onflow/flow-go/fvm/evm/types" +) + +// CallTracker captures precompiled calls +type CallTracker struct { + callsByAddress map[types.Address]*types.PrecompiledCalls +} + +// NewCallTracker constructs a new CallTracker +func NewCallTracker() *CallTracker { + return &CallTracker{} +} + +// RegisterPrecompiledContract registers a precompiled contract for tracking +func (ct *CallTracker) RegisterPrecompiledContract(pc types.PrecompiledContract) types.PrecompiledContract { + return &WrappedPrecompiledContract{ + pc: pc, + ct: ct, + } +} + +// CaptureRequiredGas captures a required gas call +func (ct *CallTracker) CaptureRequiredGas(address types.Address, input []byte, output uint64) { + if ct.callsByAddress == nil { + ct.callsByAddress = make(map[types.Address]*types.PrecompiledCalls) + } + calls, found := ct.callsByAddress[address] + if !found { + calls = &types.PrecompiledCalls{ + Address: address, + } + ct.callsByAddress[address] = calls + } + + calls.RequiredGasCalls = append(calls.RequiredGasCalls, types.RequiredGasCall{ + Input: input, + Output: output, + }) +} + +// CaptureRun captures a run calls +func (ct *CallTracker) CaptureRun(address types.Address, input []byte, output []byte, err error) { + if ct.callsByAddress == nil { + ct.callsByAddress = make(map[types.Address]*types.PrecompiledCalls) + } + calls, found := ct.callsByAddress[address] + if !found { + calls = &types.PrecompiledCalls{ + Address: address, + } + ct.callsByAddress[address] = calls + } + errMsg := "" + if err != nil { + errMsg = err.Error() + } + calls.RunCalls = append(calls.RunCalls, types.RunCall{ + Input: input, + Output: output, + ErrorMsg: errMsg, + }) +} + +// IsCalled returns true if any calls has been captured +func (ct *CallTracker) IsCalled() bool { + return len(ct.callsByAddress) != 0 +} + +// Encoded +func (ct *CallTracker) CapturedCalls() ([]byte, error) { + if !ct.IsCalled() { + return nil, nil + } + // else constructs an aggregated precompiled calls + apc := make(types.AggregatedPrecompiledCalls, 0) + + sortedAddresses := make([]types.Address, 0, len(ct.callsByAddress)) + // we need to sort by address to stay deterministic + for addr := range ct.callsByAddress { + sortedAddresses = append(sortedAddresses, addr) + } + + sort.Slice(sortedAddresses, + func(i, j int) bool { + return bytes.Compare(sortedAddresses[i][:], sortedAddresses[j][:]) < 0 + }) + + for _, addr := range sortedAddresses { + apc = append(apc, *ct.callsByAddress[addr]) + } + + return apc.Encode() +} + +// Resets the tracker +func (ct *CallTracker) Reset() { + ct.callsByAddress = nil +} + +type WrappedPrecompiledContract struct { + pc types.PrecompiledContract + ct *CallTracker +} + +func (wpc *WrappedPrecompiledContract) Address() types.Address { + return wpc.pc.Address() +} +func (wpc *WrappedPrecompiledContract) RequiredGas(input []byte) uint64 { + output := wpc.pc.RequiredGas(input) + wpc.ct.CaptureRequiredGas(wpc.pc.Address(), input, output) + return output +} + +func (wpc *WrappedPrecompiledContract) Run(input []byte) ([]byte, error) { + output, err := wpc.pc.Run(input) + wpc.ct.CaptureRun(wpc.pc.Address(), input, output, err) + return output, err +} diff --git a/fvm/evm/precompiles/precompile.go b/fvm/evm/precompiles/precompile.go index 87833f19c8e..e35f3bc373a 100644 --- a/fvm/evm/precompiles/precompile.go +++ b/fvm/evm/precompiles/precompile.go @@ -40,10 +40,8 @@ func MultiFunctionPrecompiledContract( } type precompile struct { - address types.Address - functions map[FunctionSelector]Function - requiredGasCalls []types.RequiredGasCall - runCalls []types.RunCall + address types.Address + functions map[FunctionSelector]Function } func (p *precompile) Address() types.Address { @@ -52,14 +50,6 @@ func (p *precompile) Address() types.Address { // RequiredGas calculates the contract gas use func (p *precompile) RequiredGas(input []byte) (output uint64) { - defer func() { - p.requiredGasCalls = append( - p.requiredGasCalls, - types.RequiredGasCall{ - Input: input, - Output: output, - }) - }() if len(input) < FunctionSelectorLength { return InvalidMethodCallGasUsage } @@ -73,20 +63,6 @@ func (p *precompile) RequiredGas(input []byte) (output uint64) { // Run runs the precompiled contract func (p *precompile) Run(input []byte) (output []byte, err error) { - defer func() { - errMsg := "" - if err != nil { - errMsg = err.Error() - } - p.runCalls = append( - p.runCalls, - types.RunCall{ - Input: input, - Output: output, - ErrorMsg: errMsg, - }) - }() - if len(input) < FunctionSelectorLength { return nil, ErrInvalidMethodCall } @@ -97,20 +73,3 @@ func (p *precompile) Run(input []byte) (output []byte, err error) { } return callable.Run(data) } - -func (p *precompile) IsCalled() bool { - return len(p.requiredGasCalls) > 0 || len(p.runCalls) > 0 -} - -func (p *precompile) CapturedCalls() *types.PrecompiledCalls { - return &types.PrecompiledCalls{ - Address: p.address, - RequiredGasCalls: p.requiredGasCalls, - RunCalls: p.runCalls, - } -} - -func (p *precompile) Reset() { - p.requiredGasCalls = nil - p.runCalls = nil -} diff --git a/fvm/evm/precompiles/precompile_test.go b/fvm/evm/precompiles/precompile_test.go index 68876f09f65..bdfabd9a1c6 100644 --- a/fvm/evm/precompiles/precompile_test.go +++ b/fvm/evm/precompiles/precompile_test.go @@ -34,10 +34,8 @@ func TestMutiFunctionContract(t *testing.T) { }, }}) - require.False(t, pc.IsCalled()) require.Equal(t, address, pc.Address()) require.Equal(t, gas, pc.RequiredGas(input)) - require.True(t, pc.IsCalled()) ret, err := pc.Run(input) require.NoError(t, err) require.Equal(t, output, ret) @@ -45,28 +43,6 @@ func TestMutiFunctionContract(t *testing.T) { input2 := []byte("non existing signature and data") _, err = pc.Run(input2) require.Equal(t, precompiles.ErrInvalidMethodCall, err) - - calls := pc.CapturedCalls() - require.Equal(t, address, calls.Address) - require.Len(t, calls.RequiredGasCalls, 1) - require.Equal(t, input, calls.RequiredGasCalls[0].Input) - require.Equal(t, gas, calls.RequiredGasCalls[0].Output) - require.Len(t, calls.RunCalls, 2) - require.Equal(t, input, calls.RunCalls[0].Input) - require.Equal(t, output, calls.RunCalls[0].Output) - require.Empty(t, calls.RunCalls[0].ErrorMsg) - require.Equal(t, input2, calls.RunCalls[1].Input) - require.Empty(t, calls.RunCalls[1].Output) - require.Equal(t, precompiles.ErrInvalidMethodCall.Error(), calls.RunCalls[1].ErrorMsg) - - // test reset functionality - pc.Reset() - require.False(t, pc.IsCalled()) - - calls = pc.CapturedCalls() - require.Equal(t, address, calls.Address) - require.Len(t, calls.RequiredGasCalls, 0) - require.Len(t, calls.RunCalls, 0) } type mockedFunction struct { diff --git a/fvm/evm/types/emulator.go b/fvm/evm/types/emulator.go index 991b281ff50..dcfd1206d2e 100644 --- a/fvm/evm/types/emulator.go +++ b/fvm/evm/types/emulator.go @@ -12,9 +12,6 @@ import ( var ( DefaultDirectCallBaseGasUsage = uint64(21_000) DefaultDirectCallGasPrice = uint64(0) - - // anything block number above 0 works here - BlockNumberForEVMRules = big.NewInt(1) ) // BlockContext holds the context needed for the emulator operations diff --git a/fvm/evm/types/precompile.go b/fvm/evm/types/precompiled.go similarity index 78% rename from fvm/evm/types/precompile.go rename to fvm/evm/types/precompiled.go index 30d7fed1d22..59499b194ef 100644 --- a/fvm/evm/types/precompile.go +++ b/fvm/evm/types/precompiled.go @@ -1,26 +1,20 @@ package types import ( + "bytes" + gethVM "github.com/onflow/go-ethereum/core/vm" "github.com/onflow/go-ethereum/rlp" ) // PrecompiledContract wraps gethVM precompiles with -// functionality to hold on to the deployed address -// and captures calls to its method. +// functionality to return where the contract is deployed type PrecompiledContract interface { // PrecompiledContract provides an interface for // calling requiredGas and run gethVM.PrecompiledContract // Address returns the address where the precompile is deployed Address() Address - // IsCalled returned true if any of the methods on this precompiled is called - IsCalled() bool - // CapturedCalls returns a list of calls to the Run and RequiredGas methods - // it includes the input and returned value for each call - CapturedCalls() *PrecompiledCalls - // Reset resets the list of captured calls - Reset() } // RunCall captures a call to the RequiredGas method of a precompiled contract @@ -48,7 +42,13 @@ func (pc *PrecompiledCalls) IsEmpty() bool { return len(pc.RequiredGasCalls) == 0 && len(pc.RunCalls) == 0 } +const ( + AggregatedPrecompiledCallsEncodingVersion uint8 = 1 + AggregatedPrecompiledCallsEncodingByteSize int = 1 +) + // AggregatedPrecompiledCalls aggregates a list of precompiled calls +// the list should be sorted by the address type AggregatedPrecompiledCalls []PrecompiledCalls // IsEmpty returns true if all of the underlying precompiled calls are empty @@ -66,11 +66,15 @@ func (apc AggregatedPrecompiledCalls) IsEmpty() bool { // if there is no underlying call, we encode to empty bytes to save // space on transaction results (common case) func (apc AggregatedPrecompiledCalls) Encode() ([]byte, error) { - // optimization for empty case which would be most of transactions if apc.IsEmpty() { return []byte{}, nil } - return rlp.EncodeToBytes(apc) + buffer := bytes.NewBuffer(make([]byte, 0)) + // write the encoding version + buffer.WriteByte(AggregatedPrecompiledCallsEncodingVersion) + // then RLP encode + err := rlp.Encode(buffer, apc) + return buffer.Bytes(), err } // AggregatedPrecompileCallsFromEncoded constructs an AggregatedPrecompileCalls from encoded data @@ -79,5 +83,5 @@ func AggregatedPrecompileCallsFromEncoded(encoded []byte) (AggregatedPrecompiled if len(encoded) == 0 { return apc, nil } - return apc, rlp.DecodeBytes(encoded, &apc) + return apc, rlp.DecodeBytes(encoded[AggregatedPrecompiledCallsEncodingByteSize:], &apc) } diff --git a/fvm/evm/types/precompile_test.go b/fvm/evm/types/precompiled_test.go similarity index 99% rename from fvm/evm/types/precompile_test.go rename to fvm/evm/types/precompiled_test.go index e666a3f9ea4..cf9246ff863 100644 --- a/fvm/evm/types/precompile_test.go +++ b/fvm/evm/types/precompiled_test.go @@ -10,7 +10,6 @@ import ( ) func TestPrecompiledCallsEncoding(t *testing.T) { - // empty precompiled calls empty := types.AggregatedPrecompiledCalls{ types.PrecompiledCalls{ From 37029ac91ec993eed64fa912206dda566ab75838 Mon Sep 17 00:00:00 2001 From: ramtinms Date: Thu, 18 Jul 2024 15:53:56 -0700 Subject: [PATCH 2/5] add replayer precompiled contract --- fvm/evm/precompiles/replayer.go | 86 ++++++++++++++++++++++++++++ fvm/evm/precompiles/replayer_test.go | 69 ++++++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 fvm/evm/precompiles/replayer.go create mode 100644 fvm/evm/precompiles/replayer_test.go diff --git a/fvm/evm/precompiles/replayer.go b/fvm/evm/precompiles/replayer.go new file mode 100644 index 00000000000..a830a755811 --- /dev/null +++ b/fvm/evm/precompiles/replayer.go @@ -0,0 +1,86 @@ +package precompiles + +import ( + "bytes" + "errors" + "fmt" + + "github.com/onflow/flow-go/fvm/evm/types" +) + +var ( + // errInvalidPrecompiledContractCalls is returned when an invalid list of + // precompiled contract calls is passed + errInvalidPrecompiledContractCalls = fmt.Errorf("invalid list of precompiled contract calls") + // errUnexpectedCall is returned when a call to the precompile is not expected + errUnexpectedCall = fmt.Errorf("unexpected call") + // this error is returned where more calls were expected before CapturedCalls call + errMoreCallsWereExpected = fmt.Errorf("expecting more call") +) + +// AggregatedPrecompiledCallsToPrecompiledContracts +// converts an aggregated set of precompile calls +// into a list of replayer precompiled contract +func AggregatedPrecompiledCallsToPrecompiledContracts(apc types.AggregatedPrecompiledCalls) []types.PrecompiledContract { + res := make([]types.PrecompiledContract, 0) + for _, ap := range apc { + res = append(res, NewReplayerPrecompiledContract(&ap)) + } + return res +} + +// ReplayerPrecompiledContract is a precompiled contract +// that replay the outputs based on the input +type ReplayerPrecompiledContract struct { + expectedCalls *types.PrecompiledCalls + requiredGasIndex, runIndex int +} + +// NewReplayerPrecompiledContract constructs a ReplayerPrecompiledContract +func NewReplayerPrecompiledContract( + expectedCalls *types.PrecompiledCalls, +) *ReplayerPrecompiledContract { + if expectedCalls == nil { + panic(errInvalidPrecompiledContractCalls) + } + return &ReplayerPrecompiledContract{ + expectedCalls: expectedCalls, + } +} + +func (p *ReplayerPrecompiledContract) Address() types.Address { + return p.expectedCalls.Address +} + +func (p *ReplayerPrecompiledContract) RequiredGas(input []byte) (output uint64) { + if p.requiredGasIndex > len(p.expectedCalls.RequiredGasCalls) { + panic(errUnexpectedCall) + } + if !bytes.Equal(p.expectedCalls.RequiredGasCalls[p.requiredGasIndex].Input, input) { + panic(errUnexpectedCall) + } + output = p.expectedCalls.RequiredGasCalls[p.requiredGasIndex].Output + p.requiredGasIndex++ + return +} + +func (p *ReplayerPrecompiledContract) Run(input []byte) (output []byte, err error) { + if p.runIndex > len(p.expectedCalls.RunCalls) { + panic(errUnexpectedCall) + } + if !bytes.Equal(p.expectedCalls.RunCalls[p.runIndex].Input, input) { + panic(errUnexpectedCall) + } + output = p.expectedCalls.RunCalls[p.runIndex].Output + errMsg := p.expectedCalls.RunCalls[p.runIndex].ErrorMsg + if len(errMsg) > 0 { + err = errors.New(errMsg) + } + p.runIndex++ + return +} + +func (p *ReplayerPrecompiledContract) HasReplayedAll() bool { + return len(p.expectedCalls.RequiredGasCalls) == p.requiredGasIndex && + len(p.expectedCalls.RunCalls) == p.runIndex +} diff --git a/fvm/evm/precompiles/replayer_test.go b/fvm/evm/precompiles/replayer_test.go new file mode 100644 index 00000000000..50f8d256899 --- /dev/null +++ b/fvm/evm/precompiles/replayer_test.go @@ -0,0 +1,69 @@ +package precompiles_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/onflow/flow-go/fvm/evm/precompiles" + "github.com/onflow/flow-go/fvm/evm/testutils" + "github.com/onflow/flow-go/fvm/evm/types" +) + +func TestReplayer(t *testing.T) { + + address := testutils.RandomAddress(t) + input1 := []byte{0, 1} + input2 := []byte{2, 3} + gas1 := uint64(1) + gas2 := uint64(2) + output1 := []byte{4, 5} + output2 := []byte{} + errMsg2 := "some error message" + + pc := &types.PrecompiledCalls{ + Address: address, + RequiredGasCalls: []types.RequiredGasCall{ + { + Input: input1, + Output: gas1, + }, + { + Input: input2, + Output: gas2, + }, + }, + RunCalls: []types.RunCall{ + { + Input: input1, + Output: output1, + }, + { + Input: input2, + Output: output2, + ErrorMsg: errMsg2, + }, + }, + } + + rep := precompiles.NewReplayerPrecompiledContract(pc) + require.Equal(t, address, rep.Address()) + require.False(t, rep.HasReplayedAll()) + + require.Equal(t, gas1, rep.RequiredGas(input1)) + ret, err := rep.Run(input1) + require.NoError(t, err) + require.Equal(t, output1, ret) + require.False(t, rep.HasReplayedAll()) + + require.Equal(t, gas2, rep.RequiredGas(input2)) + ret, err = rep.Run(input2) + require.Equal(t, errMsg2, err.Error()) + require.Equal(t, output2, ret) + + require.True(t, rep.HasReplayedAll()) + + assert.Panics(t, func() { _ = rep.RequiredGas(input2) }, "expected to panic") + assert.Panics(t, func() { _, _ = rep.Run(input2) }, "expected to panic") +} From 5194a4b9b95495bdef9679970983f2693d9cc8c8 Mon Sep 17 00:00:00 2001 From: ramtinms Date: Thu, 18 Jul 2024 15:56:26 -0700 Subject: [PATCH 3/5] add notes --- fvm/evm/types/precompiled.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fvm/evm/types/precompiled.go b/fvm/evm/types/precompiled.go index 59499b194ef..ccb58976b35 100644 --- a/fvm/evm/types/precompiled.go +++ b/fvm/evm/types/precompiled.go @@ -65,6 +65,8 @@ func (apc AggregatedPrecompiledCalls) IsEmpty() bool { // Encode encodes the aggregated precompile calls using rlp encoding // if there is no underlying call, we encode to empty bytes to save // space on transaction results (common case) +// TODO: In the future versions of the encoding we might skip encoding the inputs +// given it just takes space and not needed during execution time func (apc AggregatedPrecompiledCalls) Encode() ([]byte, error) { if apc.IsEmpty() { return []byte{}, nil From 94dd5ecce2a76ce203dfbba79341f537aa9a35ab Mon Sep 17 00:00:00 2001 From: ramtinms Date: Thu, 18 Jul 2024 16:24:03 -0700 Subject: [PATCH 4/5] add test for tracker --- fvm/evm/emulator/tracker_test.go | 64 ++++++++++++++++++++++++++++++++ fvm/evm/testutils/misc.go | 23 ++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 fvm/evm/emulator/tracker_test.go diff --git a/fvm/evm/emulator/tracker_test.go b/fvm/evm/emulator/tracker_test.go new file mode 100644 index 00000000000..aee629f5fb0 --- /dev/null +++ b/fvm/evm/emulator/tracker_test.go @@ -0,0 +1,64 @@ +package emulator_test + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/onflow/flow-go/fvm/evm/emulator" + "github.com/onflow/flow-go/fvm/evm/testutils" + "github.com/onflow/flow-go/fvm/evm/types" +) + +func TestTracker(t *testing.T) { + apc := testutils.AggregatedPrecompiledCallsFixture(t) + var runCallCounter int + var requiredGasCallCounter int + pc := &MockedPrecompiled{ + AddressFunc: func() types.Address { + return apc[0].Address + }, + RequiredGasFunc: func(input []byte) uint64 { + res := apc[0].RequiredGasCalls[requiredGasCallCounter] + require.Equal(t, res.Input, input) + requiredGasCallCounter += 1 + return res.Output + }, + RunFunc: func(input []byte) ([]byte, error) { + res := apc[0].RunCalls[runCallCounter] + require.Equal(t, res.Input, input) + runCallCounter += 1 + var err error + if len(res.ErrorMsg) > 0 { + err = errors.New(res.ErrorMsg) + } + return res.Output, err + }, + } + tracker := emulator.NewCallTracker() + wpc := tracker.RegisterPrecompiledContract(pc) + + require.Equal(t, apc[0].Address, wpc.Address()) + for _, pc := range apc { + for _, call := range pc.RequiredGasCalls { + require.Equal(t, call.Output, wpc.RequiredGas(call.Input)) + } + for _, call := range pc.RunCalls { + ret, err := wpc.Run(call.Input) + require.Equal(t, call.Output, ret) + errMsg := "" + if err != nil { + errMsg = err.Error() + } + require.Equal(t, call.ErrorMsg, errMsg) + } + + } + require.True(t, tracker.IsCalled()) + + expectedEncoded, err := apc.Encode() + require.NoError(t, err) + encoded, err := tracker.CapturedCalls() + require.Equal(t, expectedEncoded, encoded) +} diff --git a/fvm/evm/testutils/misc.go b/fvm/evm/testutils/misc.go index 99c66a55f38..8b7d6e86f98 100644 --- a/fvm/evm/testutils/misc.go +++ b/fvm/evm/testutils/misc.go @@ -100,3 +100,26 @@ func RandomResultFixture(t testing.TB) *types.Result { }, } } + +func AggregatedPrecompiledCallsFixture(t testing.TB) types.AggregatedPrecompiledCalls { + return types.AggregatedPrecompiledCalls{ + types.PrecompiledCalls{ + Address: RandomAddress(t), + RequiredGasCalls: []types.RequiredGasCall{{ + Input: RandomData(t), + Output: 2, + }}, + RunCalls: []types.RunCall{ + { + Input: RandomData(t), + Output: RandomData(t), + }, + { + Input: RandomData(t), + Output: []byte{}, + ErrorMsg: "Some error msg", + }, + }, + }, + } +} From 65e7e2b1f55d8be45e2ae3b1edc75653eef4fe16 Mon Sep 17 00:00:00 2001 From: ramtinms Date: Fri, 19 Jul 2024 18:39:09 -0700 Subject: [PATCH 5/5] lint fix --- fvm/evm/emulator/tracker_test.go | 1 + fvm/evm/precompiles/replayer.go | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/fvm/evm/emulator/tracker_test.go b/fvm/evm/emulator/tracker_test.go index aee629f5fb0..176b43d42e4 100644 --- a/fvm/evm/emulator/tracker_test.go +++ b/fvm/evm/emulator/tracker_test.go @@ -60,5 +60,6 @@ func TestTracker(t *testing.T) { expectedEncoded, err := apc.Encode() require.NoError(t, err) encoded, err := tracker.CapturedCalls() + require.NoError(t, err) require.Equal(t, expectedEncoded, encoded) } diff --git a/fvm/evm/precompiles/replayer.go b/fvm/evm/precompiles/replayer.go index a830a755811..deebce92153 100644 --- a/fvm/evm/precompiles/replayer.go +++ b/fvm/evm/precompiles/replayer.go @@ -14,8 +14,6 @@ var ( errInvalidPrecompiledContractCalls = fmt.Errorf("invalid list of precompiled contract calls") // errUnexpectedCall is returned when a call to the precompile is not expected errUnexpectedCall = fmt.Errorf("unexpected call") - // this error is returned where more calls were expected before CapturedCalls call - errMoreCallsWereExpected = fmt.Errorf("expecting more call") ) // AggregatedPrecompiledCallsToPrecompiledContracts