diff --git a/integrationTests/json/scenariosContracts_test.go b/integrationTests/json/scenariosContracts_test.go index d1b83a964..826e630f7 100644 --- a/integrationTests/json/scenariosContracts_test.go +++ b/integrationTests/json/scenariosContracts_test.go @@ -119,3 +119,10 @@ func TestCAttestation(t *testing.T) { Run(). CheckNoError() } + +func TestRustPaymaster(t *testing.T) { + ScenariosTest(t). + Folder("paymaster/scenarios"). + Run(). + CheckNoError() +} diff --git a/test/paymaster/output/paymaster.wasm b/test/paymaster/output/paymaster.wasm new file mode 100755 index 000000000..b339e8384 Binary files /dev/null and b/test/paymaster/output/paymaster.wasm differ diff --git a/test/paymaster/scenarios/test_forward_call_wegld.scen.json b/test/paymaster/scenarios/test_forward_call_wegld.scen.json new file mode 100644 index 000000000..0dc61de8b --- /dev/null +++ b/test/paymaster/scenarios/test_forward_call_wegld.scen.json @@ -0,0 +1,179 @@ +{ + "name": "paymaster", + "comment": "add then check", + "gasSchedule": "v3", + "steps": [ + { + "step": "setState", + "accounts": { + "address:user": { + "nonce": "1", + "balance": "0", + "esdt": { + "str:FEE-123456": "100,000,000", + "str:WEGLD-123456": "100,000,000" + } + }, + "address:owner": { + "nonce": "1" + }, + "address:relayer": { + "nonce": "1", + "balance": "0" + }, + "sc:wegld": { + "nonce": "0", + "balance": "100,000,000", + "esdt": { + "str:WEGLD-123456": { + "instances": [ + { + "nonce": "", + "balance": "0" + } + ], + "roles": [ + "ESDTRoleLocalMint", + "ESDTRoleLocalBurn" + ] + } + }, + "storage": { + "str:wrappedEgldTokenId": "str:WEGLD-123456" + }, + "code": "file:../../wegld-swap/output/multiversx-wegld-swap-sc.wasm" + } + }, + "newAddresses": [ + { + "creatorAddress": "address:owner", + "creatorNonce": "1", + "newAddress": "sc:paymaster" + } + ] + }, + { + "step": "scDeploy", + "id": "", + "tx": { + "from": "address:owner", + "contractCode": "file:../output/paymaster.wasm", + "arguments": [], + "gasLimit": "5,000,000", + "gasPrice": "" + }, + "expect": { + "out": [], + "status": "0" + } + }, + { + "step": "checkState", + "accounts": { + "address:user": { + "esdt": { + "str:FEE-123456": "100,000,000", + "str:WEGLD-123456": "100,000,000" + }, + "storage": "*", + "code": "*", + "owner": "*", + "nonce": "*", + "balance": "0" + }, + "+": "" + } + }, + { + "step": "scCall", + "id": "paymaster-forward-execution", + "tx": { + "from": "address:user", + "to": "sc:paymaster", + "esdtValue": [ + { + "tokenIdentifier": "str:FEE-123456", + "value": "20,000" + }, + { + "tokenIdentifier": "str:WEGLD-123456", + "value": "100,000,000" + } + ], + "function": "forwardExecution", + "arguments": [ + "0x72656c617965725f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f", + "0x000000000000000000007765676c645f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f5f", + "0x756e7772617045676c64" + ], + "gasLimit": "100,000,000", + "gasPrice": "" + }, + "expect": { + "out": [], + "status": "0", + "gas": "*", + "refund": "*" + } + }, + { + "step": "checkState", + "accounts": { + "address:relayer": { + "esdt": { + "str:FEE-123456": "20,000", + "str:WEGLD-123456": "0" + }, + "storage": "*", + "code": "*", + "owner": "*", + "nonce": "1" + }, + "address:user": { + "esdt": { + "str:FEE-123456": "99,980,000" + }, + "balance": "100,000,000", + "storage": "*", + "code": "*", + "owner": "*", + "nonce": "2" + }, + "sc:paymaster": { + "esdt": { + "str:FEE-123456": "0", + "str:WEGLD-123456": "0" + }, + "balance": "0", + "storage": "*", + "code": "*", + "owner": "address:owner", + "nonce": "0" + }, + "sc:wegld": { + "esdt": { + "str:FEE-123456": "0", + "str:WEGLD-123456": { + "instances": [ + { + "nonce": "", + "balance": "0" + } + ], + "roles": [ + "ESDTRoleLocalMint", + "ESDTRoleLocalBurn" + ] + } + }, + "balance": "0", + "storage": "*", + "owner": "*", + "nonce": "0", + "code": "file:../../wegld-swap/output/multiversx-wegld-swap-sc.wasm" + }, + "+": "" + } + } + ] +} \ No newline at end of file diff --git a/vmhost/contexts/instanceTracker.go b/vmhost/contexts/instanceTracker.go index 042a5c766..f0fef8238 100644 --- a/vmhost/contexts/instanceTracker.go +++ b/vmhost/contexts/instanceTracker.go @@ -2,6 +2,7 @@ package contexts import ( "bytes" + "errors" "fmt" "github.com/multiversx/mx-chain-core-go/core/check" @@ -15,6 +16,8 @@ var _ vmhost.InstanceTracker = (*instanceTracker)(nil) type instanceCacheLevel int +var errTooManyInstances = errors.New("too many instances") + const ( // Warm indicates that the instance to track is a warm instance Warm instanceCacheLevel = iota @@ -178,27 +181,27 @@ func (tracker *instanceTracker) TrackedInstances() map[string]executor.Instance // UseWarmInstance attempts to retrieve a warm instance for the given codeHash // and to set it as active; returns false if not possible -func (tracker *instanceTracker) UseWarmInstance(codeHash []byte, newCode bool) bool { +func (tracker *instanceTracker) UseWarmInstance(codeHash []byte, newCode bool) (bool, error) { instance, ok := tracker.GetWarmInstance(codeHash) if !ok { - return false + return false, nil } ok = instance.Reset() if !ok { tracker.warmInstanceCache.Remove(codeHash) - return false + return false, nil } if newCode { // A warm instance was found, but newCode == true, meaning this is an // upgrade; the old warm instance must be cleaned tracker.ForceCleanInstance(false) - return false + return false, nil } - tracker.SetNewInstance(instance, Warm) - return true + err := tracker.SetNewInstance(instance, Warm) + return true, err } // ForceCleanInstance cleans the active instance and evicts it from the @@ -237,7 +240,6 @@ func (tracker *instanceTracker) ForceCleanInstance(bypassWarmAndStackChecks bool // SaveAsWarmInstance saves the active instance into the internal warm instance cache func (tracker *instanceTracker) SaveAsWarmInstance() { lenCacheBeforeSaving := tracker.warmInstanceCache.Len() - codeHashInWarmCache := tracker.warmInstanceCache.Has(tracker.codeHash) if codeHashInWarmCache { @@ -289,13 +291,18 @@ func (tracker *instanceTracker) GetCodeSize() uint64 { } // SetNewInstance sets the given instance as active and tracks its creation -func (tracker *instanceTracker) SetNewInstance(instance executor.Instance, cacheLevel instanceCacheLevel) { +func (tracker *instanceTracker) SetNewInstance(instance executor.Instance, cacheLevel instanceCacheLevel) error { tracker.ReplaceInstance(instance) tracker.cacheLevel = cacheLevel if cacheLevel != Warm { tracker.updateNumRunningInstances(+1) } tracker.instances[instance.ID()] = instance + + if len(tracker.instances) >= warmCacheSize-1 { + return errTooManyInstances + } + return nil } // ReplaceInstance replaces the currently active instance with the given one diff --git a/vmhost/contexts/instanceTracker_test.go b/vmhost/contexts/instanceTracker_test.go index 373a7d439..0aaf4b152 100644 --- a/vmhost/contexts/instanceTracker_test.go +++ b/vmhost/contexts/instanceTracker_test.go @@ -17,7 +17,7 @@ func TestInstanceTracker_TrackInstance(t *testing.T) { AlreadyClean: false, } - iTracker.SetNewInstance(newInstance, Bytecode) + _ = iTracker.SetNewInstance(newInstance, Bytecode) iTracker.codeHash = []byte("testinst") require.False(t, iTracker.IsCodeHashOnTheStack(iTracker.codeHash)) @@ -38,7 +38,7 @@ func TestInstanceTracker_InitState(t *testing.T) { require.Equal(t, 0, iTracker.numRunningInstances) for i := 0; i < 5; i++ { - iTracker.SetNewInstance(mock.NewInstanceMock(nil), Bytecode) + _ = iTracker.SetNewInstance(mock.NewInstanceMock(nil), Bytecode) } require.Equal(t, 5, iTracker.numRunningInstances) @@ -63,7 +63,7 @@ func TestInstanceTracker_GetWarmInstance(t *testing.T) { testData := []string{"warm1", "bytecode1", "bytecode2", "warm2"} for _, codeHash := range testData { - iTracker.SetNewInstance(mock.NewInstanceMock([]byte(codeHash)), Bytecode) + _ = iTracker.SetNewInstance(mock.NewInstanceMock([]byte(codeHash)), Bytecode) iTracker.codeHash = []byte(codeHash) if strings.Contains(codeHash, "warm") { iTracker.SaveAsWarmInstance() @@ -95,7 +95,7 @@ func TestInstanceTracker_UseWarmInstance(t *testing.T) { testData := []string{"warm1", "bytecode1", "warm2", "bytecode2"} for _, codeHash := range testData { - iTracker.SetNewInstance(mock.NewInstanceMock([]byte(codeHash)), Bytecode) + _ = iTracker.SetNewInstance(mock.NewInstanceMock([]byte(codeHash)), Bytecode) iTracker.codeHash = []byte(codeHash) if strings.Contains(codeHash, "warm") { @@ -106,7 +106,7 @@ func TestInstanceTracker_UseWarmInstance(t *testing.T) { require.Equal(t, []byte("bytecode2"), iTracker.CodeHash()) for _, codeHash := range testData { - ok := iTracker.UseWarmInstance([]byte(codeHash), false) + ok, _ := iTracker.UseWarmInstance([]byte(codeHash), false) if strings.Contains(codeHash, "warm") { require.True(t, ok) @@ -124,7 +124,7 @@ func TestInstanceTracker_IsCodeHashOnStack_Ok(t *testing.T) { testData := []string{"alpha", "beta", "alpha", "active"} for i, codeHash := range testData { - iTracker.SetNewInstance(mock.NewInstanceMock([]byte(codeHash)), Bytecode) + _ = iTracker.SetNewInstance(mock.NewInstanceMock([]byte(codeHash)), Bytecode) iTracker.codeHash = []byte(codeHash) if i < 2 || codeHash == "active" { iTracker.SaveAsWarmInstance() @@ -157,7 +157,7 @@ func TestInstanceTracker_PopSetActiveSelfScenario(t *testing.T) { testData := []string{"alpha", "alpha", "alpha", "alpha", "active"} for i, codeHash := range testData { - iTracker.SetNewInstance(mock.NewInstanceMock([]byte(codeHash)), Bytecode) + _ = iTracker.SetNewInstance(mock.NewInstanceMock([]byte(codeHash)), Bytecode) iTracker.codeHash = []byte(codeHash) if i == 0 || codeHash == "active" { iTracker.SaveAsWarmInstance() @@ -187,7 +187,7 @@ func TestInstanceTracker_PopSetActiveSimpleScenario(t *testing.T) { testData := []string{"alpha", "beta", "alpha", "beta", "active"} for i, codeHash := range testData { - iTracker.SetNewInstance(mock.NewInstanceMock([]byte(codeHash)), Bytecode) + _ = iTracker.SetNewInstance(mock.NewInstanceMock([]byte(codeHash)), Bytecode) iTracker.codeHash = []byte(codeHash) if i < 2 || codeHash == "active" { iTracker.SaveAsWarmInstance() @@ -226,7 +226,7 @@ func TestInstanceTracker_PopSetActiveComplexScenario(t *testing.T) { testData := []string{"alpha", "beta", "gamma", "beta", "gamma", "delta", "alpha", "active"} for i, codeHash := range testData { - iTracker.SetNewInstance(mock.NewInstanceMock([]byte(codeHash)), Bytecode) + _ = iTracker.SetNewInstance(mock.NewInstanceMock([]byte(codeHash)), Bytecode) iTracker.codeHash = []byte(codeHash) if i < 3 || codeHash == "delta" || codeHash == "active" { iTracker.SaveAsWarmInstance() @@ -255,7 +255,7 @@ func TestInstanceTracker_PopSetActiveWarmOnlyScenario(t *testing.T) { testData := []string{"alpha", "beta", "gamma", "delta", "active"} for _, codeHash := range testData { - iTracker.SetNewInstance(mock.NewInstanceMock([]byte(codeHash)), Bytecode) + _ = iTracker.SetNewInstance(mock.NewInstanceMock([]byte(codeHash)), Bytecode) iTracker.codeHash = []byte(codeHash) iTracker.SaveAsWarmInstance() @@ -283,7 +283,7 @@ func TestInstanceTracker_ForceCleanInstanceWithBypass(t *testing.T) { testData := []string{"warm1", "bytecode1"} for _, codeHash := range testData { - iTracker.SetNewInstance(mock.NewInstanceMock([]byte(codeHash)), Bytecode) + _ = iTracker.SetNewInstance(mock.NewInstanceMock([]byte(codeHash)), Bytecode) iTracker.codeHash = []byte(codeHash) if strings.Contains(codeHash, "warm") { @@ -298,7 +298,7 @@ func TestInstanceTracker_ForceCleanInstanceWithBypass(t *testing.T) { iTracker.ForceCleanInstance(true) require.Nil(t, iTracker.instance) - iTracker.UseWarmInstance([]byte("warm1"), false) + _, _ = iTracker.UseWarmInstance([]byte("warm1"), false) require.NotNil(t, iTracker.instance) iTracker.ForceCleanInstance(true) @@ -312,7 +312,7 @@ func TestInstanceTracker_DoubleForceClean(t *testing.T) { iTracker, err := NewInstanceTracker() require.Nil(t, err) - iTracker.SetNewInstance(mock.NewInstanceMock(nil), Bytecode) + _ = iTracker.SetNewInstance(mock.NewInstanceMock(nil), Bytecode) require.NotNil(t, iTracker.instance) require.Equal(t, 1, iTracker.numRunningInstances) diff --git a/vmhost/contexts/managedType.go b/vmhost/contexts/managedType.go index 37baf4ad3..40e26aa0b 100644 --- a/vmhost/contexts/managedType.go +++ b/vmhost/contexts/managedType.go @@ -10,6 +10,7 @@ import ( "math/big" "github.com/multiversx/mx-chain-core-go/core/check" + "github.com/multiversx/mx-chain-core-go/data/vm" logger "github.com/multiversx/mx-chain-logger-go" vmcommon "github.com/multiversx/mx-chain-vm-common-go" "github.com/multiversx/mx-chain-vm-go/math" @@ -139,7 +140,7 @@ func (context *managedTypesContext) InitState() { // PushState appends the values map to the state stack func (context *managedTypesContext) PushState() { newBigIntState, newBigFloatState, newEcState, newmBufferState, newmMapState := context.clone() - newTransfers := context.cloneBackTransfers() + newTransfers := cloneBackTransfers(context.managedTypesValues.backTransfers) context.managedTypesStack = append(context.managedTypesStack, managedTypesState{ bigIntValues: newBigIntState, bigFloatValues: newBigFloatState, @@ -150,6 +151,21 @@ func (context *managedTypesContext) PushState() { }) } +// PopBackTransferIfAsyncCallBack copies the back transfer from the top of the stack in case of callbacks +func (context *managedTypesContext) PopBackTransferIfAsyncCallBack(vmInput *vmcommon.ContractCallInput) { + if vmInput.CallType != vm.AsynchronousCallBack { + return + } + + managedTypesStackLen := len(context.managedTypesStack) + if managedTypesStackLen == 0 { + return + } + + prevState := context.managedTypesStack[managedTypesStackLen-1] + context.managedTypesValues.backTransfers = cloneBackTransfers(prevState.backTransfers) +} + // PopSetActiveState removes the latest entry from the state stack and sets it as the current values map func (context *managedTypesContext) PopSetActiveState() { managedTypesStackLen := len(context.managedTypesStack) @@ -797,7 +813,7 @@ func (context *managedTypesContext) AddValueOnlyBackTransfer(value *big.Int) { // GetBackTransfers returns all ESDT transfers and accumulated value as well, will clean accumulated values func (context *managedTypesContext) GetBackTransfers() ([]*vmcommon.ESDTTransfer, *big.Int) { - clonedTransfers := context.cloneBackTransfers() + clonedTransfers := cloneBackTransfers(context.managedTypesValues.backTransfers) context.managedTypesValues.backTransfers = backTransfers{ ESDTTransfers: make([]*vmcommon.ESDTTransfer, 0), CallValue: big.NewInt(0), @@ -806,9 +822,7 @@ func (context *managedTypesContext) GetBackTransfers() ([]*vmcommon.ESDTTransfer return clonedTransfers.ESDTTransfers, clonedTransfers.CallValue } -func (context *managedTypesContext) cloneBackTransfers() backTransfers { - currentBackTransfers := context.managedTypesValues.backTransfers - +func cloneBackTransfers(currentBackTransfers backTransfers) backTransfers { newBackTransfers := backTransfers{ ESDTTransfers: make([]*vmcommon.ESDTTransfer, len(currentBackTransfers.ESDTTransfers)), CallValue: big.NewInt(0).Set(currentBackTransfers.CallValue), diff --git a/vmhost/contexts/runtime.go b/vmhost/contexts/runtime.go index e2c0a04cd..f2da057d8 100644 --- a/vmhost/contexts/runtime.go +++ b/vmhost/contexts/runtime.go @@ -152,12 +152,18 @@ func (context *runtimeContext) StartWasmerInstance(contract []byte, gasLimit uin logRuntime.Trace("code was new", "new", newCode) }() - warmInstanceUsed := context.useWarmInstanceIfExists(gasLimit, newCode) + warmInstanceUsed, err := context.useWarmInstanceIfExists(gasLimit, newCode) + if err != nil { + return err + } if warmInstanceUsed { return nil } - compiledCodeUsed := context.makeInstanceFromCompiledCode(gasLimit, newCode) + compiledCodeUsed, err := context.makeInstanceFromCompiledCode(gasLimit, newCode) + if err != nil { + return err + } if compiledCodeUsed { return nil } @@ -165,17 +171,17 @@ func (context *runtimeContext) StartWasmerInstance(contract []byte, gasLimit uin return context.makeInstanceFromContractByteCode(contract, gasLimit, newCode) } -func (context *runtimeContext) makeInstanceFromCompiledCode(gasLimit uint64, newCode bool) bool { +func (context *runtimeContext) makeInstanceFromCompiledCode(gasLimit uint64, newCode bool) (bool, error) { codeHash := context.iTracker.CodeHash() if newCode || len(codeHash) == 0 { - return false + return false, nil } blockchain := context.host.Blockchain() found, compiledCode := blockchain.GetCompiledCode(codeHash) if !found { logRuntime.Trace("instance creation", "code", "cached compilation", "error", "compiled code was not found") - return false + return false, nil } gasSchedule := context.host.Metering().GasSchedule() @@ -191,10 +197,13 @@ func (context *runtimeContext) makeInstanceFromCompiledCode(gasLimit uint64, new newInstance, err := context.vmExecutor.NewInstanceFromCompiledCodeWithOptions(compiledCode, options) if err != nil { logRuntime.Error("instance creation", "from", "cached compilation", "error", err) - return false + return false, nil } - context.iTracker.SetNewInstance(newInstance, Precompiled) + err = context.iTracker.SetNewInstance(newInstance, Precompiled) + if err != nil { + return false, err + } context.verifyCode = false context.saveWarmInstance() @@ -202,7 +211,7 @@ func (context *runtimeContext) makeInstanceFromCompiledCode(gasLimit uint64, new "id", context.iTracker.Instance().ID(), "codeHash", context.iTracker.codeHash, ) - return true + return true, nil } func (context *runtimeContext) makeInstanceFromContractByteCode(contract []byte, gasLimit uint64, newCode bool) error { @@ -223,7 +232,10 @@ func (context *runtimeContext) makeInstanceFromContractByteCode(contract []byte, return err } - context.iTracker.SetNewInstance(newInstance, Bytecode) + err = context.iTracker.SetNewInstance(newInstance, Bytecode) + if err != nil { + return err + } if newCode || len(context.iTracker.CodeHash()) == 0 { codeHash := context.hasher.Compute(string(contract)) @@ -249,23 +261,26 @@ func (context *runtimeContext) makeInstanceFromContractByteCode(contract []byte, return nil } -func (context *runtimeContext) useWarmInstanceIfExists(gasLimit uint64, newCode bool) bool { +func (context *runtimeContext) useWarmInstanceIfExists(gasLimit uint64, newCode bool) (bool, error) { if !WarmInstancesEnabled { - return false + return false, nil } codeHash := context.iTracker.CodeHash() if newCode || len(codeHash) == 0 { - return false + return false, nil } if context.isContractOrCodeHashOnTheStack() { - return false + return false, nil } - ok := context.iTracker.UseWarmInstance(codeHash, newCode) + ok, err := context.iTracker.UseWarmInstance(codeHash, newCode) + if err != nil { + return false, err + } if !ok { - return false + return false, nil } context.SetPointsUsed(0) @@ -273,7 +288,7 @@ func (context *runtimeContext) useWarmInstanceIfExists(gasLimit uint64, newCode context.SetRuntimeBreakpointValue(vmhost.BreakpointNone) context.verifyCode = false logRuntime.Trace("start instance", "from", "warm", "id", context.iTracker.Instance().ID()) - return true + return true, nil } // GetSCCode returns the SC code of the current SC. diff --git a/vmhost/hostCore/execution.go b/vmhost/hostCore/execution.go index 3e21ce743..1eb04379e 100644 --- a/vmhost/hostCore/execution.go +++ b/vmhost/hostCore/execution.go @@ -448,6 +448,7 @@ func (host *vmHost) executeOnDestContextNoBuiltinFunction(input *vmcommon.Contra managedTypes, _, metering, output, runtime, async, storage := host.GetContexts() managedTypes.PushState() managedTypes.InitState() + managedTypes.PopBackTransferIfAsyncCallBack(input) output.PushState() output.CensorVMOutput() diff --git a/vmhost/interface.go b/vmhost/interface.go index e7edee399..03655d904 100644 --- a/vmhost/interface.go +++ b/vmhost/interface.go @@ -218,6 +218,7 @@ type ManagedTypesContext interface { GetBackTransfers() ([]*vmcommon.ESDTTransfer, *big.Int) AddValueOnlyBackTransfer(value *big.Int) AddBackTransfers(transfers []*vmcommon.ESDTTransfer) + PopBackTransferIfAsyncCallBack(vmInput *vmcommon.ContractCallInput) } // OutputContext defines the functionality needed for interacting with the output context