diff --git a/ledger-core/virtual/execute/execute.go b/ledger-core/virtual/execute/execute.go index 20b72d412e..1b62bf88d9 100644 --- a/ledger-core/virtual/execute/execute.go +++ b/ledger-core/virtual/execute/execute.go @@ -802,8 +802,9 @@ func (s *SMExecute) stepSaveNewObject(ctx smachine.ExecutionContext) smachine.St class reference.Global ) - if s.intolerableCall() { - s.executionNewState.Result = requestresult.New(executionNewState.Result(), executionNewState.ObjectReference()) + if s.isIntolerableCallChangeState() { + s.prepareExecutionError(throw.E("intolerable call trying to change object state")) + return ctx.Jump(s.stepSendCallResult) } if s.deactivate { @@ -852,6 +853,10 @@ func (s *SMExecute) stepSaveNewObject(ctx smachine.ExecutionContext) smachine.St return ctx.Jump(s.stepSendCallResult) } +func (s *SMExecute) isIntolerableCallChangeState() bool { + return s.intolerableCall() && (s.deactivate || s.executionNewState.Result.Type() != requestresult.SideEffectNone) +} + func (s *SMExecute) stepAwaitSMCallSummary(ctx smachine.ExecutionContext) smachine.StateUpdate { syncAccessor, ok := callsummary.GetSummarySMSyncAccessor(ctx, s.execution.Object) diff --git a/ledger-core/virtual/execute/execute.plantuml b/ledger-core/virtual/execute/execute.plantuml index 8af2ddbb82..ef493b21f7 100644 --- a/ledger-core/virtual/execute/execute.plantuml +++ b/ledger-core/virtual/execute/execute.plantuml @@ -134,6 +134,7 @@ T01_S030 --> T01_S031 state "stepSaveNewObject" as T01_S028 T01_S028 : SMExecute T01_S028 --[dotted]> T01_S016 +T01_S028 --> T01_S032 : [(...).isIntolerableCallChangeState()] T01_S028 --> T01_S032 : [(...).migrationHappened||s.newObjectDescriptor==nil] T01_S028 --> T01_S028 : [!stepUpdate.IsEmpty()] T01_S028 --> T01_S032 diff --git a/ledger-core/virtual/integration/deduplication_test.go b/ledger-core/virtual/integration/deduplication_test.go index 83e8567516..45fba4e63e 100644 --- a/ledger-core/virtual/integration/deduplication_test.go +++ b/ledger-core/virtual/integration/deduplication_test.go @@ -407,10 +407,10 @@ func TestDeduplication_MethodUsingPrevVE(t *testing.T) { executeDone := suite.server.Journal.WaitStopOf(&execute.SMExecute{}, 1) suite.switchPulse(ctx) - suite.generateClass(ctx) - suite.generateCaller(ctx) - suite.generateObjectRef(ctx) - suite.generateOutgoing(ctx) + suite.generateClass() + suite.generateCaller() + suite.generateObjectRef() + suite.generateOutgoing() suite.setMessageCheckers(ctx, t, test) suite.setRunnerMock() @@ -502,14 +502,14 @@ func (s *deduplicateMethodUsingPrevVETest) getP1() pulse.Number { return s.p1 } -func (s *deduplicateMethodUsingPrevVETest) generateCaller(ctx context.Context) { +func (s *deduplicateMethodUsingPrevVETest) generateCaller() { s.mu.Lock() defer s.mu.Unlock() s.caller = reference.NewSelf(gen.UniqueLocalRefWithPulse(s.p1)) } -func (s *deduplicateMethodUsingPrevVETest) generateObjectRef(ctx context.Context) { +func (s *deduplicateMethodUsingPrevVETest) generateObjectRef() { p := s.getP1() s.mu.Lock() @@ -518,7 +518,7 @@ func (s *deduplicateMethodUsingPrevVETest) generateObjectRef(ctx context.Context s.object = reference.NewSelf(gen.UniqueLocalRefWithPulse(p)) } -func (s *deduplicateMethodUsingPrevVETest) generateOutgoing(ctx context.Context) { +func (s *deduplicateMethodUsingPrevVETest) generateOutgoing() { p := s.getP1() s.mu.Lock() @@ -527,7 +527,7 @@ func (s *deduplicateMethodUsingPrevVETest) generateOutgoing(ctx context.Context) s.outgoing = reference.NewRecordOf(s.caller, gen.UniqueLocalRefWithPulse(p)) } -func (s *deduplicateMethodUsingPrevVETest) generateClass(ctx context.Context) { +func (s *deduplicateMethodUsingPrevVETest) generateClass() { s.mu.Lock() defer s.mu.Unlock() @@ -703,12 +703,7 @@ func (s *deduplicateMethodUsingPrevVETest) setRunnerMock() { isolation := contract.MethodIsolation{Interference: contract.CallIntolerable, State: contract.CallDirty} s.runnerMock.AddExecutionClassify("SomeMethod", isolation, nil) - newObjDescriptor := descriptor.NewObject( - reference.Global{}, reference.Local{}, s.getClass(), []byte(""), - ) - requestResult := requestresult.New([]byte("execution"), gen.UniqueGlobalRef()) - requestResult.SetAmend(newObjDescriptor, []byte("new memory")) executionMock := s.runnerMock.AddExecutionMock("SomeMethod") executionMock.AddStart(func(ctx execution.Context) { diff --git a/ledger-core/virtual/integration/method_pulse_change_test.go b/ledger-core/virtual/integration/method_pulse_change_test.go index e461ebeaea..125630a751 100644 --- a/ledger-core/virtual/integration/method_pulse_change_test.go +++ b/ledger-core/virtual/integration/method_pulse_change_test.go @@ -21,8 +21,9 @@ import ( "github.com/insolar/assured-ledger/ledger-core/pulse" "github.com/insolar/assured-ledger/ledger-core/reference" "github.com/insolar/assured-ledger/ledger-core/runner/execution" + "github.com/insolar/assured-ledger/ledger-core/runner/executor/common/foundation" "github.com/insolar/assured-ledger/ledger-core/runner/requestresult" - commontestutils "github.com/insolar/assured-ledger/ledger-core/testutils" + commonTestUtils "github.com/insolar/assured-ledger/ledger-core/testutils" "github.com/insolar/assured-ledger/ledger-core/testutils/gen" "github.com/insolar/assured-ledger/ledger-core/testutils/predicate" "github.com/insolar/assured-ledger/ledger-core/testutils/runner/logicless" @@ -69,7 +70,7 @@ func TestVirtual_Method_PulseChanged(t *testing.T) { for _, test := range table { t.Run(test.name, func(t *testing.T) { - defer commontestutils.LeakTester(t) + defer commonTestUtils.LeakTester(t) mc := minimock.NewController(t) @@ -200,7 +201,13 @@ func TestVirtual_Method_PulseChanged(t *testing.T) { }) typedChecker.VCallResult.Set(func(res *payload.VCallResult) bool { assert.Equal(t, object, res.Callee) - assert.Equal(t, []byte("call result"), res.ReturnArguments) + if test.isolation == intolerableFlags() && test.withSideEffect { + contractErr, sysErr := foundation.UnmarshalMethodResult(res.ReturnArguments) + require.NoError(t, sysErr) + require.Equal(t, "intolerable call trying to change object state", contractErr.Error()) + } else { + assert.Equal(t, []byte("call result"), res.ReturnArguments) + } assert.Equal(t, p1, res.CallOutgoing.GetLocal().Pulse()) assert.Equal(t, expectedToken, res.DelegationSpec) return false @@ -243,7 +250,7 @@ func TestVirtual_Method_PulseChanged(t *testing.T) { // 2 ordered and 2 unordered calls func TestVirtual_Method_CheckPendingsCount(t *testing.T) { - defer commontestutils.LeakTester(t) + defer commonTestUtils.LeakTester(t) t.Log("C5104") @@ -296,7 +303,7 @@ func TestVirtual_Method_CheckPendingsCount(t *testing.T) { LatestValidatedState: &objectState, } - payload := &payload.VStateReport{ + vsrPayload := &payload.VStateReport{ Status: payload.Ready, Object: object, UnorderedPendingEarliestPulse: pulse.OfNow(), @@ -304,7 +311,7 @@ func TestVirtual_Method_CheckPendingsCount(t *testing.T) { } server.WaitIdleConveyor() - server.SendPayload(ctx, payload) + server.SendPayload(ctx, vsrPayload) server.WaitActiveThenIdleConveyor() } @@ -453,7 +460,7 @@ func TestVirtual_MethodCall_IfConstructorIsPending(t *testing.T) { for _, test := range table { t.Run(test.name, func(t *testing.T) { - defer commontestutils.LeakTester(t) + defer commonTestUtils.LeakTester(t) mc := minimock.NewController(t) @@ -489,7 +496,7 @@ func TestVirtual_MethodCall_IfConstructorIsPending(t *testing.T) { // create object state { - payload := &payload.VStateReport{ + vsrPayload := &payload.VStateReport{ Status: payload.Empty, Object: object, AsOf: p1, @@ -497,7 +504,7 @@ func TestVirtual_MethodCall_IfConstructorIsPending(t *testing.T) { OrderedPendingEarliestPulse: p1, } - server.SendPayload(ctx, payload) + server.SendPayload(ctx, vsrPayload) server.WaitActiveThenIdleConveyor() } diff --git a/ledger-core/virtual/integration/method_test.go b/ledger-core/virtual/integration/method_test.go index 40c46a7241..1531c0cf63 100644 --- a/ledger-core/virtual/integration/method_test.go +++ b/ledger-core/virtual/integration/method_test.go @@ -21,6 +21,7 @@ import ( "github.com/insolar/assured-ledger/ledger-core/virtual/authentication" "github.com/insolar/assured-ledger/ledger-core/virtual/handlers" "github.com/insolar/assured-ledger/ledger-core/virtual/object" + "github.com/insolar/assured-ledger/ledger-core/virtual/object/finalizedstate" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -67,14 +68,14 @@ func Method_PrepareObject(ctx context.Context, server *utils.Server, state paylo panic("unexpected state") } - payload := &payload.VStateReport{ + vsrPayload := &payload.VStateReport{ Status: state, Object: object, ProvidedContent: content, } wait := server.Journal.WaitStopOf(&handlers.SMVStateReport{}, 1) - server.SendPayload(ctx, payload) + server.SendPayload(ctx, vsrPayload) select { case <-wait: @@ -1345,112 +1346,167 @@ func Test_MethodCall_HappyPath(t *testing.T) { defer commontestutils.LeakTester(t) t.Log("C5089") - mc := minimock.NewController(t) - server, ctx := utils.NewUninitializedServer(nil, t) - defer server.Stop() + const ( + origObjectMem = "original object memory" + changedObjectMem = "new object memory" + callResult = "call result" + ) + cases := []struct { + name string + isolation contract.MethodIsolation + canChangeState bool + }{ + { + name: "Tolerable call can change object state", + canChangeState: true, + isolation: contract.MethodIsolation{ + Interference: contract.CallTolerable, + State: contract.CallDirty, + }, + }, + { + name: "Intolerable call cannot change object state", + canChangeState: false, + isolation: contract.MethodIsolation{ + Interference: contract.CallIntolerable, + State: contract.CallValidated, + }, + }, + } - logger := inslogger.FromContext(ctx) - executeDone := server.Journal.WaitStopOf(&execute.SMExecute{}, 1) + for _, testCase := range cases { - runnerMock := logicless.NewServiceMock(ctx, mc, func(execution execution.Context) string { - return execution.Request.CallSiteMethod - }) - { - server.ReplaceRunner(runnerMock) - server.Init(ctx) - server.IncrementPulseAndWaitIdle(ctx) - } + mc := minimock.NewController(t) - typedChecker := server.PublisherMock.SetTypedChecker(ctx, mc, server) + server, ctx := utils.NewUninitializedServer(nil, t) - var ( - class = gen.UniqueGlobalRef() - object = reference.NewSelf(server.RandomLocalWithPulse()) - p1 = server.GetPulse().PulseNumber - ) + executeDone := server.Journal.WaitStopOf(&execute.SMExecute{}, 1) + stateReportSend := server.Journal.WaitStopOf(&finalizedstate.SMStateFinalizer{}, 1) - server.IncrementPulseAndWaitIdle(ctx) - outgoingP2 := server.BuildRandomOutgoingWithPulse() + runnerMock := logicless.NewServiceMock(ctx, mc, func(execution execution.Context) string { + return execution.Request.CallSiteMethod + }) + { + server.ReplaceRunner(runnerMock) + server.Init(ctx) + } - // add ExecutionMock to runnerMock - { - runnerMock.AddExecutionClassify("SomeMethod", intolerableFlags(), nil) - requestResult := requestresult.New([]byte("call result"), gen.UniqueGlobalRef()) + typedChecker := server.PublisherMock.SetTypedChecker(ctx, mc, server) - objectExecutionMock := runnerMock.AddExecutionMock("SomeMethod") - objectExecutionMock.AddStart( - func(ctx execution.Context) { - logger.Debug("ExecutionStart [SomeMethod]") - require.Equal(t, object, ctx.Request.Callee) - require.Equal(t, []byte("new object memory"), ctx.ObjectDescriptor.Memory()) - }, - &execution.Update{ - Type: execution.Done, - Result: requestResult, - }, + var ( + class = gen.UniqueGlobalRef() + objectRef = reference.NewSelf(server.RandomLocalWithPulse()) + p1 = server.GetPulse().PulseNumber ) - } - typedChecker.VStateRequest.Set(func(req *payload.VStateRequest) bool { - require.Equal(t, p1, req.AsOf) - require.Equal(t, object, req.Object) - - flags := payload.StateRequestContentFlags(0) - flags.Set( - payload.RequestLatestDirtyState, - payload.RequestLatestValidatedState, - payload.RequestOrderedQueue, - payload.RequestUnorderedQueue, - ) - require.Equal(t, flags, req.RequestedContent) + server.IncrementPulseAndWaitIdle(ctx) + outgoing := server.BuildRandomOutgoingWithPulse() - content := &payload.VStateReport_ProvidedContentBody{ - LatestDirtyState: &payload.ObjectState{ - Reference: reference.Local{}, - Class: testwalletProxy.GetClass(), - State: []byte("new object memory"), - }, - } + // add ExecutionMock to runnerMock + { + runnerMock.AddExecutionClassify("SomeMethod", testCase.isolation, nil) + requestResult := requestresult.New([]byte(callResult), gen.UniqueGlobalRef()) + if testCase.canChangeState { + newObjDescriptor := descriptor.NewObject( + reference.Global{}, reference.Local{}, class, []byte(""), + ) + requestResult.SetAmend(newObjDescriptor, []byte(changedObjectMem)) + } - report := payload.VStateReport{ - Status: payload.Ready, - AsOf: req.AsOf, - Object: object, - ProvidedContent: content, + objectExecutionMock := runnerMock.AddExecutionMock("SomeMethod") + objectExecutionMock.AddStart( + func(ctx execution.Context) { + require.Equal(t, objectRef, ctx.Request.Callee) + require.Equal(t, []byte(origObjectMem), ctx.ObjectDescriptor.Memory()) + }, + &execution.Update{ + Type: execution.Done, + Result: requestResult, + }, + ) } - server.SendPayload(ctx, &report) - return false // no resend msg - }) - typedChecker.VCallResult.Set(func(res *payload.VCallResult) bool { - require.Equal(t, []byte("call result"), res.ReturnArguments) - require.Equal(t, object, res.Callee) - require.Equal(t, outgoingP2, res.CallOutgoing) - return false // no resend msg - }) + typedChecker.VStateRequest.Set(func(req *payload.VStateRequest) bool { + require.Equal(t, p1, req.AsOf) + require.Equal(t, objectRef, req.Object) - // VCallRequest - { - pl := payload.VCallRequest{ - CallType: payload.CTMethod, - CallFlags: payload.BuildCallFlags(contract.CallIntolerable, contract.CallValidated), - Caller: server.GlobalCaller(), - Callee: object, - CallSiteDeclaration: class, - CallSiteMethod: "SomeMethod", - CallOutgoing: outgoingP2, + flags := payload.RequestLatestDirtyState | payload.RequestLatestValidatedState | + payload.RequestOrderedQueue | payload.RequestUnorderedQueue + require.Equal(t, flags, req.RequestedContent) + + content := &payload.VStateReport_ProvidedContentBody{ + LatestDirtyState: &payload.ObjectState{ + Reference: reference.Local{}, + Class: testwalletProxy.GetClass(), + State: []byte(origObjectMem), + }, + } + + report := payload.VStateReport{ + Status: payload.Ready, + AsOf: req.AsOf, + Object: objectRef, + ProvidedContent: content, + } + server.SendPayload(ctx, &report) + return false // no resend msg + }) + + typedChecker.VCallResult.Set(func(res *payload.VCallResult) bool { + require.Equal(t, objectRef, res.Callee) + require.Equal(t, outgoing, res.CallOutgoing) + require.Equal(t, []byte(callResult), res.ReturnArguments) + return false // no resend msg + }) + typedChecker.VStateReport.Set(func(report *payload.VStateReport) bool { + require.Equal(t, objectRef, report.Object) + require.Equal(t, payload.Ready, report.Status) + require.True(t, report.DelegationSpec.IsZero()) + require.Equal(t, int32(0), report.UnorderedPendingCount) + require.Equal(t, int32(0), report.OrderedPendingCount) + require.NotNil(t, report.ProvidedContent) + switch testCase.isolation.Interference { + case contract.CallIntolerable: + require.Equal(t, []byte(origObjectMem), report.ProvidedContent.LatestDirtyState.State) + case contract.CallTolerable: + require.Equal(t, []byte(changedObjectMem), report.ProvidedContent.LatestValidatedState.State) + } + return false + }) + + // VCallRequest + { + pl := payload.VCallRequest{ + CallType: payload.CTMethod, + CallFlags: payload.BuildCallFlags(testCase.isolation.Interference, testCase.isolation.State), + Caller: server.GlobalCaller(), + Callee: objectRef, + CallSiteDeclaration: class, + CallSiteMethod: "SomeMethod", + CallOutgoing: outgoing, + } + server.SendPayload(ctx, &pl) } - server.SendPayload(ctx, &pl) - } - testutils.WaitSignalsTimed(t, 20*time.Second, executeDone) - testutils.WaitSignalsTimed(t, 10*time.Second, server.Journal.WaitAllAsyncCallsDone()) + testutils.WaitSignalsTimed(t, 10*time.Second, executeDone) + testutils.WaitSignalsTimed(t, 10*time.Second, server.Journal.WaitAllAsyncCallsDone()) - require.Equal(t, 1, typedChecker.VStateRequest.Count()) - require.Equal(t, 1, typedChecker.VCallResult.Count()) + // increment pulse twice for stop SMStateFinalizer + server.IncrementPulseAndWaitIdle(ctx) + server.IncrementPulseAndWaitIdle(ctx) - mc.Finish() + testutils.WaitSignalsTimed(t, 10*time.Second, stateReportSend) + testutils.WaitSignalsTimed(t, 10*time.Second, server.Journal.WaitAllAsyncCallsDone()) + + require.Equal(t, 1, typedChecker.VStateReport.Count()) + require.Equal(t, 1, typedChecker.VStateRequest.Count()) + require.Equal(t, 1, typedChecker.VCallResult.Count()) + typedChecker.MinimockFinish() + + server.Stop() + mc.Finish() + } } func TestVirtual_Method_ForObjectWithMissingState(t *testing.T) { @@ -1689,3 +1745,141 @@ func TestVirtual_Method_ForbidenIsolation(t *testing.T) { }) } } + +func TestVirtual_Method_IntolerableCallChangeState(t *testing.T) { + const ( + origObjectMem = "original object memory" + changedObjectMem = "new object memory" + ) + + t.Log("C5463") + + defer commontestutils.LeakTester(t) + + mc := minimock.NewController(t) + + server, ctx := utils.NewUninitializedServer(nil, t) + + executeDone := server.Journal.WaitStopOf(&execute.SMExecute{}, 1) + stateReportSend := server.Journal.WaitStopOf(&finalizedstate.SMStateFinalizer{}, 1) + + runnerMock := logicless.NewServiceMock(ctx, mc, func(execution execution.Context) string { + return execution.Request.CallSiteMethod + }) + { + server.ReplaceRunner(runnerMock) + server.Init(ctx) + } + + typedChecker := server.PublisherMock.SetTypedChecker(ctx, mc, server) + + var ( + class = gen.UniqueGlobalRef() + objectRef = reference.NewSelf(server.RandomLocalWithPulse()) + p1 = server.GetPulse().PulseNumber + isolation = contract.MethodIsolation{ + Interference: contract.CallIntolerable, + State: contract.CallValidated, + } + ) + + server.IncrementPulseAndWaitIdle(ctx) + outgoing := server.BuildRandomOutgoingWithPulse() + + { + runnerMock.AddExecutionClassify("SomeMethod", isolation, nil) + requestResult := requestresult.New([]byte("call result"), gen.UniqueGlobalRef()) + newObjDescriptor := descriptor.NewObject( + reference.Global{}, reference.Local{}, class, []byte(""), + ) + requestResult.SetAmend(newObjDescriptor, []byte(changedObjectMem)) + + objectExecutionMock := runnerMock.AddExecutionMock("SomeMethod") + objectExecutionMock.AddStart( + func(ctx execution.Context) { + require.Equal(t, objectRef, ctx.Request.Callee) + require.Equal(t, []byte(origObjectMem), ctx.ObjectDescriptor.Memory()) + }, + &execution.Update{ + Type: execution.Done, + Result: requestResult, + }, + ) + } + + typedChecker.VStateRequest.Set(func(req *payload.VStateRequest) bool { + require.Equal(t, p1, req.AsOf) + require.Equal(t, objectRef, req.Object) + + flags := payload.RequestLatestDirtyState | payload.RequestLatestValidatedState | + payload.RequestOrderedQueue | payload.RequestUnorderedQueue + require.Equal(t, flags, req.RequestedContent) + + content := &payload.VStateReport_ProvidedContentBody{ + LatestDirtyState: &payload.ObjectState{ + Reference: reference.Local{}, + Class: testwalletProxy.GetClass(), + State: []byte(origObjectMem), + }, + } + + report := payload.VStateReport{ + Status: payload.Ready, + AsOf: req.AsOf, + Object: objectRef, + ProvidedContent: content, + } + server.SendPayload(ctx, &report) + return false // no resend msg + }) + + typedChecker.VCallResult.Set(func(res *payload.VCallResult) bool { + require.Equal(t, objectRef, res.Callee) + require.Equal(t, outgoing, res.CallOutgoing) + contractErr, sysErr := foundation.UnmarshalMethodResult(res.ReturnArguments) + require.NoError(t, sysErr) + require.Equal(t, "intolerable call trying to change object state", contractErr.Error()) + return false // no resend msg + }) + typedChecker.VStateReport.Set(func(report *payload.VStateReport) bool { + require.Equal(t, objectRef, report.Object) + require.Equal(t, payload.Ready, report.Status) + require.True(t, report.DelegationSpec.IsZero()) + require.Equal(t, int32(0), report.UnorderedPendingCount) + require.Equal(t, int32(0), report.OrderedPendingCount) + require.NotNil(t, report.ProvidedContent) + require.Equal(t, []byte(origObjectMem), report.ProvidedContent.LatestDirtyState.State) + return false + }) + + { + pl := payload.VCallRequest{ + CallType: payload.CTMethod, + CallFlags: payload.BuildCallFlags(isolation.Interference, isolation.State), + Caller: server.GlobalCaller(), + Callee: objectRef, + CallSiteDeclaration: class, + CallSiteMethod: "SomeMethod", + CallOutgoing: outgoing, + } + server.SendPayload(ctx, &pl) + } + + testutils.WaitSignalsTimed(t, 10*time.Second, executeDone) + testutils.WaitSignalsTimed(t, 10*time.Second, server.Journal.WaitAllAsyncCallsDone()) + + // increment pulse twice for stop SMStateFinalizer + server.IncrementPulseAndWaitIdle(ctx) + server.IncrementPulseAndWaitIdle(ctx) + + testutils.WaitSignalsTimed(t, 10*time.Second, stateReportSend) + testutils.WaitSignalsTimed(t, 10*time.Second, server.Journal.WaitAllAsyncCallsDone()) + + require.Equal(t, 1, typedChecker.VStateReport.Count()) + require.Equal(t, 1, typedChecker.VStateRequest.Count()) + require.Equal(t, 1, typedChecker.VCallResult.Count()) + typedChecker.MinimockFinish() + + server.Stop() + mc.Finish() +}