From 6c25ea2f6f5ea7d9fb531b6e89d9f8a4b8c03a53 Mon Sep 17 00:00:00 2001 From: Nikolay Eskov Date: Mon, 19 Aug 2024 12:01:03 +0300 Subject: [PATCH] Payments fix (#1468) * Add 'disable-bloom' bool flag for importer utility. * WIP: workaround. Moved payments check before light node activation after the nested invoke actions application. * WIP: workaround Add new behavior for payments check in nested invokes. * Add 'disable-bloom' bool flag for statehash utility. * Use 'newBehavior' only at known problem height. * WIP: apply 'payments-fix-test.patch' from @alexeykiselev * Add 'payments_fix_after_height' setting parameter. * Pass 'payments_fix_after_height' functionality settings param to an Ride evaluation environment. * First clean version of fix. * Fix lint. * Set 'payments_fix_after_height' to zero for testnet. * Add default mock function for 'testEnv' test builder. * Implemented correct behaviour for payments fix. * Removed 'FIXME' in 'TestEvaluatorComplexityFailedPaymentsCheck'. * Fixed 'TestRegularAvailableBalanceSwitchOnV5ToV6'. * Fixed 'TestInvokePaymentsCheckBeforeAndAfterInvoke'. --------- Co-authored-by: Alexey Kiselev --- pkg/ride/complexity.go | 17 +++ pkg/ride/environment.go | 29 ++++- pkg/ride/functions_proto.go | 45 +++++-- pkg/ride/runtime.go | 5 +- pkg/ride/runtime_moq_test.go | 118 ++++++++++++++++++ pkg/ride/test_helpers_test.go | 13 ++ pkg/ride/tree_evaluation_test.go | 180 ++++++++++++++++++++++++++-- pkg/settings/blockchain_settings.go | 15 ++- pkg/settings/embedded/mainnet.json | 1 + pkg/settings/embedded/stagenet.json | 1 + pkg/settings/embedded/testnet.json | 1 + pkg/state/invoke_applier.go | 1 + pkg/state/script_caller.go | 5 + 13 files changed, 400 insertions(+), 31 deletions(-) diff --git a/pkg/ride/complexity.go b/pkg/ride/complexity.go index 20d68c92a..e9efd16a4 100644 --- a/pkg/ride/complexity.go +++ b/pkg/ride/complexity.go @@ -21,6 +21,7 @@ type complexityCalculator interface { testPropertyComplexity() error addPropertyComplexity() setLimit(limit uint32) + clone() complexityCalculator } type complexityCalculatorError interface { @@ -138,6 +139,14 @@ func (cc *complexityCalculatorV1) limit() int { return cc.l } +func (cc *complexityCalculatorV1) clone() complexityCalculator { + return &complexityCalculatorV1{ + err: cc.err, + c: cc.c, + l: cc.l, + } +} + func (cc *complexityCalculatorV1) testNativeFunctionComplexity(name string, fc int) error { nc, err := common.AddInt(cc.c, fc) if err != nil { @@ -236,6 +245,14 @@ func (cc *complexityCalculatorV2) limit() int { return cc.l } +func (cc *complexityCalculatorV2) clone() complexityCalculator { + return &complexityCalculatorV2{ + err: cc.err, + c: cc.c, + l: cc.l, + } +} + func (cc *complexityCalculatorV2) testNativeFunctionComplexity(name string, fc int) error { if fc == 0 { // sanity check: zero complexity for functions is not allowed since Ride V6 return newZeroComplexityError(name) diff --git a/pkg/ride/environment.go b/pkg/ride/environment.go index 9a2765215..4b993f336 100644 --- a/pkg/ride/environment.go +++ b/pkg/ride/environment.go @@ -403,6 +403,7 @@ func (ws *WrappedState) validateAsset(action proto.ScriptAction, asset proto.Opt env.scheme(), env.state(), env.internalPaymentsValidationHeight(), + env.paymentsFixAfterHeight(), env.blockV5Activated(), env.rideV6Activated(), env.consensusImprovementsActivated(), @@ -504,6 +505,8 @@ func (ws *WrappedState) validatePaymentAction(res *proto.AttachedPaymentScriptAc return nil } +var errNegativeBalanceAfterPaymentsApplication = errors.New("negative balance after payments application") + func (ws *WrappedState) validateBalancesAfterPaymentsApplication(env environment, addr proto.WavesAddress, payments proto.ScriptPayments) error { for _, payment := range payments { var balance int64 @@ -528,7 +531,8 @@ func (ws *WrappedState) validateBalancesAfterPaymentsApplication(env environment } } if (env.validateInternalPayments() || env.rideV6Activated()) && balance < 0 { - return errors.Errorf("not enough money in the DApp, balance of asset %s on address %s after payments application is %d", + return errors.Wrapf(errNegativeBalanceAfterPaymentsApplication, + "not enough money in the DApp, balance of asset %s on address %s after payments application is %d", payment.Asset.String(), addr.String(), balance) } } @@ -1031,7 +1035,8 @@ type EvaluationEnvironment struct { takeStr func(s string, n int) rideString inv rideType ver ast.LibraryVersion - validatePaymentsAfter uint64 + validatePaymentsAfter proto.Height + paymentsFixAfter proto.Height isBlockV5Activated bool isRideV6Activated bool isConsensusImprovementsActivated bool // isConsensusImprovementsActivated => nodeVersion >= 1.4.12 @@ -1050,7 +1055,10 @@ func bytesSizeCheckV3V6(l int) bool { return l <= proto.MaxDataWithProofsBytes } -func NewEnvironment(scheme proto.Scheme, state types.SmartState, internalPaymentsValidationHeight uint64, +func NewEnvironment( + scheme proto.Scheme, + state types.SmartState, + internalPaymentsValidationHeight, paymentsFixAfterHeight proto.Height, blockV5, rideV6, consensusImprovements, blockRewardDistribution, lightNode bool, ) (*EvaluationEnvironment, error) { height, err := state.AddingBlockHeight() @@ -1064,6 +1072,7 @@ func NewEnvironment(scheme proto.Scheme, state types.SmartState, internalPayment check: bytesSizeCheckV1V2, // By default almost unlimited takeStr: func(s string, n int) rideString { panic("function 'takeStr' was not initialized") }, validatePaymentsAfter: internalPaymentsValidationHeight, + paymentsFixAfter: paymentsFixAfterHeight, isBlockV5Activated: blockV5, isRideV6Activated: rideV6, isBlockRewardDistributionActivated: blockRewardDistribution, @@ -1341,10 +1350,18 @@ func (e *EvaluationEnvironment) validateInternalPayments() bool { return int(e.h) > int(e.validatePaymentsAfter) } -func (e *EvaluationEnvironment) internalPaymentsValidationHeight() uint64 { +func (e *EvaluationEnvironment) internalPaymentsValidationHeight() proto.Height { return e.validatePaymentsAfter } +func (e *EvaluationEnvironment) paymentsFixActivated() bool { + return int(e.h) > int(e.paymentsFixAfter) +} + +func (e *EvaluationEnvironment) paymentsFixAfterHeight() proto.Height { + return e.paymentsFixAfter +} + func (e *EvaluationEnvironment) maxDataEntriesSize() int { return e.mds } @@ -1356,3 +1373,7 @@ func (e *EvaluationEnvironment) isProtobufTx() bool { func (e *EvaluationEnvironment) complexityCalculator() complexityCalculator { return e.cc } + +func (e *EvaluationEnvironment) setComplexityCalculator(cc complexityCalculator) { + e.cc = cc +} diff --git a/pkg/ride/functions_proto.go b/pkg/ride/functions_proto.go index ca9f620fc..46a49642e 100644 --- a/pkg/ride/functions_proto.go +++ b/pkg/ride/functions_proto.go @@ -262,19 +262,37 @@ func performInvoke(invocation invocation, env environment, args ...rideType) (ri } return nil, err } - checkPaymentsAfterApplication := func(errT EvaluationError) error { - err = ws.validateBalancesAfterPaymentsApplication(env, proto.WavesAddress(callerAddress), attachedPayments) - if err != nil && GetEvaluationErrorType(err) == Undefined { - err = errT.Wrapf(err, "%s: failed to apply attached payments", invocation.name()) + + var ( + checkPaymentsApplication = func(errT EvaluationError) error { + err = ws.validateBalancesAfterPaymentsApplication(env, proto.WavesAddress(callerAddress), attachedPayments) + if err != nil && GetEvaluationErrorType(err) == Undefined { + err = errT.Wrapf(err, "%s: failed to apply attached payments", invocation.name()) + } + return err } - return err - } - lightNodeActivated := env.lightNodeActivated() - if lightNodeActivated { // Check payments result balances here AFTER Light Node activation - if pErr := checkPaymentsAfterApplication(NegativeBalanceAfterPayment); pErr != nil { + lightNodeActivated = env.lightNodeActivated() + paymentsFixActivated = env.paymentsFixActivated() // payments fix cant be activated without light node activation + ) + if lightNodeActivated && paymentsFixActivated { // Check payments result balances here AFTER Payments Fix activation + if pErr := checkPaymentsApplication(EvaluationFailure); pErr != nil { return nil, pErr } } + var ( + restoreComplexityCalculator = func() {} // no-op by default + ) + if lightNodeActivated && !paymentsFixActivated { + if pErr := checkPaymentsApplication(NegativeBalanceAfterPayment); pErr != nil { + if !errors.Is(pErr, errNegativeBalanceAfterPaymentsApplication) { // unexpected error + return nil, errors.Wrap(pErr, "failed to check payments after application") + } + complexityCalcClone := env.complexityCalculator().clone() // clone calculator before the invoke call + restoreComplexityCalculator = func() { + env.setComplexityCalculator(complexityCalcClone) // restore complexity calculator + } + } + } address, err := env.state().NewestRecipientToAddress(recipient) if err != nil { @@ -307,7 +325,7 @@ func performInvoke(invocation invocation, env environment, args ...rideType) (ri } if !lightNodeActivated { // Check payments result balances here BEFORE Light Node activation - if pErr := checkPaymentsAfterApplication(InternalInvocationError); pErr != nil { + if pErr := checkPaymentsApplication(InternalInvocationError); pErr != nil { return nil, pErr } } @@ -319,6 +337,13 @@ func performInvoke(invocation invocation, env environment, args ...rideType) (ri } return nil, err } + // Check payments result balances here AFTER Light Node activation and BEFORE Payments Fix activation + if lightNodeActivated && !paymentsFixActivated { + if pErr := checkPaymentsApplication(NegativeBalanceAfterPayment); pErr != nil { + restoreComplexityCalculator() // restore complexity calculator + return nil, pErr + } + } if env.validateInternalPayments() || env.rideV6Activated() { err = ws.validateBalances(env.rideV6Activated()) diff --git a/pkg/ride/runtime.go b/pkg/ride/runtime.go index 55b229ef6..05c0cd2f1 100644 --- a/pkg/ride/runtime.go +++ b/pkg/ride/runtime.go @@ -434,8 +434,11 @@ type environment interface { consensusImprovementsActivated() bool blockRewardDistributionActivated() bool lightNodeActivated() bool - internalPaymentsValidationHeight() uint64 + internalPaymentsValidationHeight() proto.Height + paymentsFixAfterHeight() proto.Height + paymentsFixActivated() bool maxDataEntriesSize() int isProtobufTx() bool complexityCalculator() complexityCalculator + setComplexityCalculator(cc complexityCalculator) } diff --git a/pkg/ride/runtime_moq_test.go b/pkg/ride/runtime_moq_test.go index 46d214047..2a66d13d9 100644 --- a/pkg/ride/runtime_moq_test.go +++ b/pkg/ride/runtime_moq_test.go @@ -59,12 +59,21 @@ var _ environment = &mockRideEnvironment{} // maxDataEntriesSizeFunc: func() int { // panic("mock out the maxDataEntriesSize method") // }, +// paymentsFixActivatedFunc: func() bool { +// panic("mock out the paymentsFixActivated method") +// }, +// paymentsFixAfterHeightFunc: func() uint64 { +// panic("mock out the paymentsFixAfterHeight method") +// }, // rideV6ActivatedFunc: func() bool { // panic("mock out the rideV6Activated method") // }, // schemeFunc: func() byte { // panic("mock out the scheme method") // }, +// setComplexityCalculatorFunc: func(cc complexityCalculator) { +// panic("mock out the setComplexityCalculator method") +// }, // setInvocationFunc: func(inv rideType) { // panic("mock out the setInvocation method") // }, @@ -141,12 +150,21 @@ type mockRideEnvironment struct { // maxDataEntriesSizeFunc mocks the maxDataEntriesSize method. maxDataEntriesSizeFunc func() int + // paymentsFixActivatedFunc mocks the paymentsFixActivated method. + paymentsFixActivatedFunc func() bool + + // paymentsFixAfterHeightFunc mocks the paymentsFixAfterHeight method. + paymentsFixAfterHeightFunc func() uint64 + // rideV6ActivatedFunc mocks the rideV6Activated method. rideV6ActivatedFunc func() bool // schemeFunc mocks the scheme method. schemeFunc func() byte + // setComplexityCalculatorFunc mocks the setComplexityCalculator method. + setComplexityCalculatorFunc func(cc complexityCalculator) + // setInvocationFunc mocks the setInvocation method. setInvocationFunc func(inv rideType) @@ -220,12 +238,23 @@ type mockRideEnvironment struct { // maxDataEntriesSize holds details about calls to the maxDataEntriesSize method. maxDataEntriesSize []struct { } + // paymentsFixActivated holds details about calls to the paymentsFixActivated method. + paymentsFixActivated []struct { + } + // paymentsFixAfterHeight holds details about calls to the paymentsFixAfterHeight method. + paymentsFixAfterHeight []struct { + } // rideV6Activated holds details about calls to the rideV6Activated method. rideV6Activated []struct { } // scheme holds details about calls to the scheme method. scheme []struct { } + // setComplexityCalculator holds details about calls to the setComplexityCalculator method. + setComplexityCalculator []struct { + // Cc is the cc argument value. + Cc complexityCalculator + } // setInvocation holds details about calls to the setInvocation method. setInvocation []struct { // Inv is the inv argument value. @@ -280,8 +309,11 @@ type mockRideEnvironment struct { locklibVersion sync.RWMutex locklightNodeActivated sync.RWMutex lockmaxDataEntriesSize sync.RWMutex + lockpaymentsFixActivated sync.RWMutex + lockpaymentsFixAfterHeight sync.RWMutex lockrideV6Activated sync.RWMutex lockscheme sync.RWMutex + locksetComplexityCalculator sync.RWMutex locksetInvocation sync.RWMutex locksetLibVersion sync.RWMutex locksetNewDAppAddress sync.RWMutex @@ -650,6 +682,60 @@ func (mock *mockRideEnvironment) maxDataEntriesSizeCalls() []struct { return calls } +// paymentsFixActivated calls paymentsFixActivatedFunc. +func (mock *mockRideEnvironment) paymentsFixActivated() bool { + if mock.paymentsFixActivatedFunc == nil { + panic("mockRideEnvironment.paymentsFixActivatedFunc: method is nil but environment.paymentsFixActivated was just called") + } + callInfo := struct { + }{} + mock.lockpaymentsFixActivated.Lock() + mock.calls.paymentsFixActivated = append(mock.calls.paymentsFixActivated, callInfo) + mock.lockpaymentsFixActivated.Unlock() + return mock.paymentsFixActivatedFunc() +} + +// paymentsFixActivatedCalls gets all the calls that were made to paymentsFixActivated. +// Check the length with: +// +// len(mockedenvironment.paymentsFixActivatedCalls()) +func (mock *mockRideEnvironment) paymentsFixActivatedCalls() []struct { +} { + var calls []struct { + } + mock.lockpaymentsFixActivated.RLock() + calls = mock.calls.paymentsFixActivated + mock.lockpaymentsFixActivated.RUnlock() + return calls +} + +// paymentsFixAfterHeight calls paymentsFixAfterHeightFunc. +func (mock *mockRideEnvironment) paymentsFixAfterHeight() uint64 { + if mock.paymentsFixAfterHeightFunc == nil { + panic("mockRideEnvironment.paymentsFixAfterHeightFunc: method is nil but environment.paymentsFixAfterHeight was just called") + } + callInfo := struct { + }{} + mock.lockpaymentsFixAfterHeight.Lock() + mock.calls.paymentsFixAfterHeight = append(mock.calls.paymentsFixAfterHeight, callInfo) + mock.lockpaymentsFixAfterHeight.Unlock() + return mock.paymentsFixAfterHeightFunc() +} + +// paymentsFixAfterHeightCalls gets all the calls that were made to paymentsFixAfterHeight. +// Check the length with: +// +// len(mockedenvironment.paymentsFixAfterHeightCalls()) +func (mock *mockRideEnvironment) paymentsFixAfterHeightCalls() []struct { +} { + var calls []struct { + } + mock.lockpaymentsFixAfterHeight.RLock() + calls = mock.calls.paymentsFixAfterHeight + mock.lockpaymentsFixAfterHeight.RUnlock() + return calls +} + // rideV6Activated calls rideV6ActivatedFunc. func (mock *mockRideEnvironment) rideV6Activated() bool { if mock.rideV6ActivatedFunc == nil { @@ -704,6 +790,38 @@ func (mock *mockRideEnvironment) schemeCalls() []struct { return calls } +// setComplexityCalculator calls setComplexityCalculatorFunc. +func (mock *mockRideEnvironment) setComplexityCalculator(cc complexityCalculator) { + if mock.setComplexityCalculatorFunc == nil { + panic("mockRideEnvironment.setComplexityCalculatorFunc: method is nil but environment.setComplexityCalculator was just called") + } + callInfo := struct { + Cc complexityCalculator + }{ + Cc: cc, + } + mock.locksetComplexityCalculator.Lock() + mock.calls.setComplexityCalculator = append(mock.calls.setComplexityCalculator, callInfo) + mock.locksetComplexityCalculator.Unlock() + mock.setComplexityCalculatorFunc(cc) +} + +// setComplexityCalculatorCalls gets all the calls that were made to setComplexityCalculator. +// Check the length with: +// +// len(mockedenvironment.setComplexityCalculatorCalls()) +func (mock *mockRideEnvironment) setComplexityCalculatorCalls() []struct { + Cc complexityCalculator +} { + var calls []struct { + Cc complexityCalculator + } + mock.locksetComplexityCalculator.RLock() + calls = mock.calls.setComplexityCalculator + mock.locksetComplexityCalculator.RUnlock() + return calls +} + // setInvocation calls setInvocationFunc. func (mock *mockRideEnvironment) setInvocation(inv rideType) { if mock.setInvocationFunc == nil { diff --git a/pkg/ride/test_helpers_test.go b/pkg/ride/test_helpers_test.go index f56df9128..32d003798 100644 --- a/pkg/ride/test_helpers_test.go +++ b/pkg/ride/test_helpers_test.go @@ -146,6 +146,9 @@ func newTestEnv(t *testing.T) *testEnv { lightNodeActivatedFunc: func() bool { return false }, + paymentsFixActivatedFunc: func() bool { + return false + }, } r := &testEnv{ t: t, @@ -377,6 +380,9 @@ func (e *testEnv) withComplexityLimit(limit int) *testEnv { cc.setLimit(uint32(limit)) return cc } + e.me.setComplexityCalculatorFunc = func(newCC complexityCalculator) { + cc = newCC + } return e } @@ -461,6 +467,13 @@ func (e *testEnv) withValidateInternalPayments() *testEnv { return e } +func (e *testEnv) withPaymentsFix() *testEnv { + e.me.paymentsFixActivatedFunc = func() bool { + return true + } + return e +} + func (e *testEnv) withThis(acc *testAccount) *testEnv { e.this = acc.address() e.me.thisFunc = func() rideType { diff --git a/pkg/ride/tree_evaluation_test.go b/pkg/ride/tree_evaluation_test.go index 8f7f2a25a..8c6fa1b0a 100644 --- a/pkg/ride/tree_evaluation_test.go +++ b/pkg/ride/tree_evaluation_test.go @@ -5326,10 +5326,10 @@ func TestRegularAvailableBalanceSwitchOnV5ToV6(t *testing.T) { env = env.withRideV6Activated().withWrappedState() res, err = CallFunction(env.toEnv(), tree1, proto.NewFunctionCall("call", proto.Arguments{})) assert.Nil(t, res) - require.EqualError(t, err, "invoke: failed to apply attached payments: not enough money in the DApp, balance of asset WAVES on address 3MzDtgL5yw73C2xVLnLJCrT5gCL4357a4sz after payments application is -1000000000") + require.EqualError(t, err, "invoke: failed to apply attached payments: not enough money in the DApp, balance of asset WAVES on address 3MzDtgL5yw73C2xVLnLJCrT5gCL4357a4sz after payments application is -1000000000: negative balance after payments application") //nolint:lll } -func TestInvokePaymentsCheckBeforeAndAfterInvokeScriptTxActivation(t *testing.T) { +func TestInvokePaymentsCheckBeforeAndAfterInvoke(t *testing.T) { dApp1 := newTestAccount(t, "DAPP1") // 3MzDtgL5yw73C2xVLnLJCrT5gCL4357a4sz dApp2 := newTestAccount(t, "DAPP2") // 3N7Te7NXtGVoQqFqktwrFhQWAkc6J8vfPQ1 sender := newTestAccount(t, "SENDER") // 3N8CkZAyS4XcDoJTJoKNuNk2xmNKmQj7myW @@ -5370,17 +5370,42 @@ func TestInvokePaymentsCheckBeforeAndAfterInvokeScriptTxActivation(t *testing.T) withWrappedState() } - t.Run("BeforeInvokeScriptActivation", func(t *testing.T) { + t.Run("BeforeLightNodeActivationAndPaymentsFix", func(t *testing.T) { env := prepareEnv().withWrappedState() - res, err := CallFunction(env.toEnv(), tree1, proto.NewFunctionCall("call", proto.Arguments{})) + rideEnv := env.toEnv() + res, err := CallFunction(rideEnv, tree1, proto.NewFunctionCall("call", proto.Arguments{})) assert.Nil(t, res) assert.EqualError(t, err, "gotcha") + assert.Equal(t, UserError, GetEvaluationErrorType(err)) + // the call happens only once in `WrappedState.validatePaymentAction` during payment application + // payments check after application are not performed because of throw + assert.Len(t, rideEnv.calls.validateInternalPayments, 1) }) - t.Run("AfterInvokeScriptActivation", func(t *testing.T) { + t.Run("AfterLightNodeActivationWithoutPaymentsFix", func(t *testing.T) { env := prepareEnv().withLightNodeActivated() - res, err := CallFunction(env.toEnv(), tree1, proto.NewFunctionCall("call", proto.Arguments{})) + rideEnv := env.toEnv() + res, err := CallFunction(rideEnv, tree1, proto.NewFunctionCall("call", proto.Arguments{})) assert.Nil(t, res) - assert.EqualError(t, err, "invoke: failed to apply attached payments: not enough money in the DApp, balance of asset WAVES on address 3MzDtgL5yw73C2xVLnLJCrT5gCL4357a4sz after payments application is -4900000000") + assert.EqualError(t, err, "gotcha") + assert.Equal(t, UserError, GetEvaluationErrorType(err)) + // the calls happen in `WrappedState.validatePaymentAction` during payment application and before invoke + // in `performInvoke` function + // in `checkPaymentsApplication` inside `WrappedState.validateBalancesAfterPaymentsApplication` + // payments check after application are not second time because of throw + assert.Len(t, rideEnv.calls.validateInternalPayments, 2) + }) + t.Run("AfterLightNodeActivationAndPaymentsFix", func(t *testing.T) { + env := prepareEnv().withLightNodeActivated().withPaymentsFix() + rideEnv := env.toEnv() + res, err := CallFunction(rideEnv, tree1, proto.NewFunctionCall("call", proto.Arguments{})) + assert.Nil(t, res) + assert.EqualError(t, err, "invoke: failed to apply attached payments: not enough money in the DApp, balance of asset WAVES on address 3MzDtgL5yw73C2xVLnLJCrT5gCL4357a4sz after payments application is -4900000000: negative balance after payments application") //nolint:lll + assert.Equal(t, EvaluationFailure, GetEvaluationErrorType(err)) + // the calls happen in `WrappedState.validatePaymentAction` during payment application and before invoke + // in `performInvoke` function + // in `checkPaymentsApplication` inside `WrappedState.validateBalancesAfterPaymentsApplication` + // successfully fails before invoke because of negative balance + assert.Len(t, rideEnv.calls.validateInternalPayments, 2) }) } @@ -5985,8 +6010,8 @@ func TestEvaluatorComplexityFailedPaymentsCheck(t *testing.T) { }) t.Run("after_light_node", func(t *testing.T) { env := createEnv(t).withLightNodeActivated() - doTest(t, env, 82, 164, NegativeBalanceAfterPayment) // fails by negative balance - // FIXME: in scala node behaviour is the same, as in the 'before_light_node' case + // in scala node behaviour is the same, as in the 'before_light_node' case + doTest(t, env, 84, 84, UserError) }) }) t.Run("double-invoke_before_light_node", func(t *testing.T) { @@ -6004,7 +6029,7 @@ func TestEvaluatorComplexityFailedPaymentsCheck(t *testing.T) { assert.Equal(t, expected, EvaluationErrorSpentComplexity(callErr)) assert.Equal(t, InternalInvocationError, GetEvaluationErrorType(callErr)) }) - t.Run("double-invoke_before_light_node", func(t *testing.T) { + t.Run("double-invoke_after_light_node", func(t *testing.T) { env := createEnv(t).withLightNodeActivated() rideEnv := env.toEnv() res, callErr := CallFunction(rideEnv, tree1, proto.NewFunctionCall("f1", proto.Arguments{ @@ -6170,3 +6195,138 @@ func TestZeroComplexitySanityCheckInComplexityCalculator(t *testing.T) { assert.Equal(t, expectedSpentComplexity, env.complexityCalculator().complexity()) }) } + +func TestAttachedPaymentsValidation(t *testing.T) { + dApp1 := newTestAccount(t, "DAPP1") // 3MzDtgL5yw73C2xVLnLJCrT5gCL4357a4sz + dApp2 := newTestAccount(t, "DAPP2") // 3N7Te7NXtGVoQqFqktwrFhQWAkc6J8vfPQ1 + sender := newTestAccount(t, "SENDER") // 3N8CkZAyS4XcDoJTJoKNuNk2xmNKmQj7myW + + const src1 = ` + {-# STDLIB_VERSION 7 #-} + {-# CONTENT_TYPE DAPP #-} + {-# SCRIPT_TYPE ACCOUNT #-} + + @Callable(i) + func invokeNext(amount: Int) = { + %s + let addr = addressFromStringValue("3N7Te7NXtGVoQqFqktwrFhQWAkc6J8vfPQ1") + strict r = addr.invoke("returnPayment", [], [AttachedPayment(unit, amount)]) + [] + } + ` + const src2 = ` + {-# STDLIB_VERSION 7 #-} + {-# CONTENT_TYPE DAPP #-} + {-# SCRIPT_TYPE ACCOUNT #-} + + @Callable(i) + func returnPayment() = { + %s + [ScriptTransfer(i.caller, 5_0000_0000, unit)] + } + ` + + additionalComplexity := "strict foo = " + strings.Repeat("sigVerify(base58'', base58'', base58'') || ", 8) + "true" + + treeProxyLight, errs := ridec.CompileToTree(fmt.Sprintf(src1, "")) + require.Empty(t, errs) + treeProxyHeavy, errs := ridec.CompileToTree(fmt.Sprintf(src1, additionalComplexity)) + require.Empty(t, errs) + treeTargetLight, errs := ridec.CompileToTree(fmt.Sprintf(src2, "")) + require.Empty(t, errs) + treeTargetHeavy, errs := ridec.CompileToTree(fmt.Sprintf(src2, additionalComplexity)) + require.Empty(t, errs) + + t.Run("payments-check-fix-disabled-no-errors", func(t *testing.T) { + createEnv := func(t *testing.T, tree1, tree2 *ast.Tree) *testEnv { + return newTestEnv(t).withLibVersion(ast.LibV7).withComplexityLimit(52000). + withBlockV5Activated().withProtobufTx().withRideV6Activated(). + withConsensusImprovementsActivatedFunc().withBlockRewardDistribution(). + withDataEntriesSizeV2().withMessageLengthV3().withValidateInternalPayments().withLightNodeActivated(). + withThis(dApp1).withDApp(dApp1).withSender(sender). + withAdditionalDApp(dApp2).withTree(dApp2, tree2). + withInvocation("invokeNext", withTransactionID(crypto.Digest{})).withTree(dApp1, tree1). + withWavesBalance(sender, 0).withWavesBalance(dApp1, 0).withWavesBalance(dApp2, 0). + withWrappedState() + } + t.Run("neither proxy nor target exceeds fail-free complexity", func(t *testing.T) { + env := createEnv(t, treeProxyLight, treeTargetLight).toEnv() + res, err := CallFunction(env, treeProxyLight, proto.NewFunctionCall("invokeNext", + proto.Arguments{&proto.IntegerArgument{Value: 5_0000_0000}})) + assert.NoError(t, err) + assert.NotNil(t, res) + assert.Equal(t, 81, env.complexityCalculator().complexity()) + }) + t.Run("only proxy exceeds fail-free complexity", func(t *testing.T) { + env := createEnv(t, treeProxyHeavy, treeTargetLight).toEnv() + res, err := CallFunction(env, treeProxyHeavy, proto.NewFunctionCall("invokeNext", + proto.Arguments{&proto.IntegerArgument{Value: 5_0000_0000}})) + assert.NoError(t, err) + assert.NotNil(t, res) + assert.Equal(t, 1522, env.complexityCalculator().complexity()) + }) + t.Run("only target exceeds fail-free complexity", func(t *testing.T) { + env := createEnv(t, treeProxyLight, treeTargetHeavy).toEnv() + res, err := CallFunction(env, treeProxyLight, proto.NewFunctionCall("invokeNext", + proto.Arguments{&proto.IntegerArgument{Value: 5_0000_0000}})) + assert.NoError(t, err) + assert.NotNil(t, res) + assert.Equal(t, 1522, env.complexityCalculator().complexity()) + }) + t.Run("both proxy and target exceeds fail-free complexity", func(t *testing.T) { + env := createEnv(t, treeProxyHeavy, treeTargetHeavy).toEnv() + res, err := CallFunction(env, treeProxyHeavy, proto.NewFunctionCall("invokeNext", + proto.Arguments{&proto.IntegerArgument{Value: 5_0000_0000}})) + assert.NoError(t, err) + assert.NotNil(t, res) + assert.Equal(t, 2963, env.complexityCalculator().complexity()) + }) + }) + + t.Run("payments-check-fix-enabled-only-rejections", func(t *testing.T) { + createEnv := func(t *testing.T, tree1, tree2 *ast.Tree) *testEnv { + return newTestEnv(t).withLibVersion(ast.LibV7).withComplexityLimit(52000). + withBlockV5Activated().withProtobufTx().withRideV6Activated(). + withConsensusImprovementsActivatedFunc().withBlockRewardDistribution(). + withDataEntriesSizeV2().withMessageLengthV3().withValidateInternalPayments().withLightNodeActivated(). + withPaymentsFix(). + withThis(dApp1).withDApp(dApp1).withSender(sender). + withAdditionalDApp(dApp2).withTree(dApp2, tree2). + withInvocation("invokeNext", withTransactionID(crypto.Digest{})).withTree(dApp1, tree1). + withWavesBalance(sender, 0).withWavesBalance(dApp1, 0).withWavesBalance(dApp2, 0). + withWrappedState() + } + t.Run("neither proxy nor target exceeds fail-free complexity", func(t *testing.T) { + env := createEnv(t, treeProxyLight, treeTargetLight).toEnv() + res, err := CallFunction(env, treeProxyLight, proto.NewFunctionCall("invokeNext", + proto.Arguments{&proto.IntegerArgument{Value: 5_0000_0000}})) + assert.Error(t, err) + assert.Nil(t, res) + assert.Equal(t, EvaluationFailure, GetEvaluationErrorType(err)) + }) + t.Run("only proxy exceeds fail-free complexity", func(t *testing.T) { + env := createEnv(t, treeProxyHeavy, treeTargetLight).toEnv() + res, err := CallFunction(env, treeProxyHeavy, proto.NewFunctionCall("invokeNext", + proto.Arguments{&proto.IntegerArgument{Value: 5_0000_0000}})) + assert.Error(t, err) + assert.Nil(t, res) + assert.Equal(t, EvaluationFailure, GetEvaluationErrorType(err)) + }) + t.Run("only target exceeds fail-free complexity", func(t *testing.T) { + env := createEnv(t, treeProxyLight, treeTargetHeavy).toEnv() + res, err := CallFunction(env, treeProxyLight, proto.NewFunctionCall("invokeNext", + proto.Arguments{&proto.IntegerArgument{Value: 5_0000_0000}})) + assert.Error(t, err) + assert.Nil(t, res) + assert.Equal(t, EvaluationFailure, GetEvaluationErrorType(err)) + }) + t.Run("both proxy and target exceeds fail-free complexity", func(t *testing.T) { + env := createEnv(t, treeProxyHeavy, treeTargetHeavy).toEnv() + res, err := CallFunction(env, treeProxyHeavy, proto.NewFunctionCall("invokeNext", + proto.Arguments{&proto.IntegerArgument{Value: 5_0000_0000}})) + assert.Error(t, err) + assert.Nil(t, res) + assert.Equal(t, EvaluationFailure, GetEvaluationErrorType(err)) + }) + }) +} diff --git a/pkg/settings/blockchain_settings.go b/pkg/settings/blockchain_settings.go index 7f582ef8c..81c37a199 100644 --- a/pkg/settings/blockchain_settings.go +++ b/pkg/settings/blockchain_settings.go @@ -61,12 +61,15 @@ type FunctionalitySettings struct { AllowMultipleLeaseCancelUntilTime uint64 `json:"allow_multiple_lease_cancel_until_time"` AllowLeasedBalanceTransferUntilTime uint64 `json:"allow_leased_balance_transfer_until_time"` // Timestamps when different kinds of checks become relevant. - CheckTempNegativeAfterTime uint64 `json:"check_temp_negative_after_time"` - TxChangesSortedCheckAfterTime uint64 `json:"tx_changes_sorted_check_after_time"` - TxFromFutureCheckAfterTime uint64 `json:"tx_from_future_check_after_time"` - UnissuedAssetUntilTime uint64 `json:"unissued_asset_until_time"` - InvalidReissueInSameBlockUntilTime uint64 `json:"invalid_reissue_in_same_block_until_time"` - MinimalGeneratingBalanceCheckAfterTime uint64 `json:"minimal_generating_balance_check_after_time"` + CheckTempNegativeAfterTime uint64 `json:"check_temp_negative_after_time"` + TxChangesSortedCheckAfterTime uint64 `json:"tx_changes_sorted_check_after_time"` + TxFromFutureCheckAfterTime uint64 `json:"tx_from_future_check_after_time"` + UnissuedAssetUntilTime uint64 `json:"unissued_asset_until_time"` + InvalidReissueInSameBlockUntilTime uint64 `json:"invalid_reissue_in_same_block_until_time"` + MinimalGeneratingBalanceCheckAfterTime uint64 `json:"minimal_generating_balance_check_after_time"` + // PaymentsFixAfterHeight == 'paymentsCheckHeight' in scala node - reject any invoke tx + // after this height if account balance become negative + PaymentsFixAfterHeight uint64 `json:"payments_fix_after_height"` InternalInvokePaymentsValidationAfterHeight uint64 `json:"internal_invoke_payments_validation_after_height"` InternalInvokeCorrectFailRejectBehaviourAfterHeight uint64 `json:"internal_invoke_correct_fail_reject_behaviour_after_height"` InvokeNoZeroPaymentsAfterHeight uint64 `json:"invoke_no_zero_payments_after_height"` diff --git a/pkg/settings/embedded/mainnet.json b/pkg/settings/embedded/mainnet.json index 0a98c547e..7a383bfd3 100644 --- a/pkg/settings/embedded/mainnet.json +++ b/pkg/settings/embedded/mainnet.json @@ -19,6 +19,7 @@ "unissued_asset_until_time": 1479416400000, "invalid_reissue_in_same_block_until_time": 1492768800000, "minimal_generating_balance_check_after_time": 1479168000000, + "payments_fix_after_height": 4303300, "internal_invoke_payments_validation_after_height": 2959400, "internal_invoke_correct_fail_reject_behaviour_after_height": 2792473, "invoke_no_zero_payments_after_height": 0, diff --git a/pkg/settings/embedded/stagenet.json b/pkg/settings/embedded/stagenet.json index 3a4653f84..026642f2e 100644 --- a/pkg/settings/embedded/stagenet.json +++ b/pkg/settings/embedded/stagenet.json @@ -33,6 +33,7 @@ "unissued_asset_until_time": 0, "invalid_reissue_in_same_block_until_time": 0, "minimal_generating_balance_check_after_time": 0, + "payments_fix_after_height": 2195800, "internal_invoke_payments_validation_after_height": 966180, "internal_invoke_correct_fail_reject_behaviour_after_height": 390000, "invoke_no_zero_payments_after_height": 1317000, diff --git a/pkg/settings/embedded/testnet.json b/pkg/settings/embedded/testnet.json index fef45a784..0de93d686 100644 --- a/pkg/settings/embedded/testnet.json +++ b/pkg/settings/embedded/testnet.json @@ -19,6 +19,7 @@ "unissued_asset_until_time": 1479416400000, "invalid_reissue_in_same_block_until_time": 1492560000000, "minimal_generating_balance_check_after_time": 0, + "payments_fix_after_height": 0, "internal_invoke_payments_validation_after_height": 1698800, "internal_invoke_correct_fail_reject_behaviour_after_height": 1727461, "invoke_no_zero_payments_after_height": 0, diff --git a/pkg/state/invoke_applier.go b/pkg/state/invoke_applier.go index 338f21bb3..c7acf0d2a 100644 --- a/pkg/state/invoke_applier.go +++ b/pkg/state/invoke_applier.go @@ -1167,6 +1167,7 @@ func (ia *invokeApplier) validateActionSmartAsset(asset crypto.Digest, action pr ia.settings.AddressSchemeCharacter, ia.state, ia.settings.InternalInvokePaymentsValidationAfterHeight, + ia.settings.PaymentsFixAfterHeight, params.blockV5Activated, params.rideV6Activated, params.consensusImprovementsActivated, diff --git a/pkg/state/script_caller.go b/pkg/state/script_caller.go index 5a73efde3..471668884 100644 --- a/pkg/state/script_caller.go +++ b/pkg/state/script_caller.go @@ -57,6 +57,7 @@ func (a *scriptCaller) callAccountScriptWithOrder(order proto.Order, lastBlockIn a.settings.AddressSchemeCharacter, a.state, a.settings.InternalInvokePaymentsValidationAfterHeight, + a.settings.PaymentsFixAfterHeight, info.blockV5Activated, info.rideV6Activated, info.consensusImprovementsActivated, @@ -120,6 +121,7 @@ func (a *scriptCaller) callAccountScriptWithTx(tx proto.Transaction, params *app a.settings.AddressSchemeCharacter, a.state, a.settings.InternalInvokePaymentsValidationAfterHeight, + a.settings.PaymentsFixAfterHeight, params.blockV5Activated, params.rideV6Activated, params.consensusImprovementsActivated, @@ -221,6 +223,7 @@ func (a *scriptCaller) callAssetScriptWithScriptTransfer(tr *proto.FullScriptTra a.settings.AddressSchemeCharacter, a.state, a.settings.InternalInvokePaymentsValidationAfterHeight, + a.settings.PaymentsFixAfterHeight, params.blockV5Activated, params.rideV6Activated, params.consensusImprovementsActivated, @@ -242,6 +245,7 @@ func (a *scriptCaller) callAssetScript(tx proto.Transaction, assetID crypto.Dige a.settings.AddressSchemeCharacter, a.state, a.settings.InternalInvokePaymentsValidationAfterHeight, + a.settings.PaymentsFixAfterHeight, params.blockV5Activated, params.rideV6Activated, params.consensusImprovementsActivated, @@ -269,6 +273,7 @@ func (a *scriptCaller) invokeFunction( a.settings.AddressSchemeCharacter, a.state, a.settings.InternalInvokePaymentsValidationAfterHeight, + a.settings.PaymentsFixAfterHeight, info.blockV5Activated, info.rideV6Activated, info.consensusImprovementsActivated,