@@ -391,6 +391,90 @@ BOOST_AUTO_TEST_CASE( recurrent_transfer_modify_last_execution )
391391 FC_LOG_AND_RETHROW ()
392392}
393393
394+ BOOST_AUTO_TEST_CASE ( recurrent_transfer_delete_with_single_execution )
395+ {
396+ try
397+ {
398+ bool is_hf29 = false ;
399+
400+ // Stringified evaluator assertion expression for the delete (amount == 0) path. It is intentionally
401+ // textually different from the create ('op.executions >= 2') and modify ('op.executions > 1') paths
402+ // so it gets its own assertion id.
403+ const std::string delete_executions_assert_expr = " 2 <= op.executions" ;
404+
405+ auto _content = [ &is_hf29, &delete_executions_assert_expr ]( ptr_hardfork_database_fixture& executor )
406+ {
407+ BOOST_TEST_MESSAGE ( " Testing: deleting a recurrent transfer with executions == 1 must match pre-HF29 nodes (issue #786)" );
408+ BOOST_REQUIRE_EQUAL ( (bool )executor, true );
409+
410+ ACTORS_EXT ( (*executor), (alice)(bob) )
411+ executor->fund ( " alice" , HIVE_asset ( 10'000 ) );
412+ executor->generate_block ();
413+
414+ BOOST_TEST_MESSAGE ( " --- Create a recurrent transfer with 2 executions" );
415+ recurrent_transfer_operation op;
416+ op.from = " alice" ;
417+ op.to = " bob" ;
418+ op.memo = " original" ;
419+ op.amount = ASSET ( " 1.000 TESTS" );
420+ op.recurrence = 24 ;
421+ op.executions = 2 ;
422+ executor->push_transaction ( op, alice_private_key );
423+
424+ // the first execution fires on the next produced block, leaving a single execution remaining
425+ executor->generate_block ();
426+ {
427+ const auto * rt = executor->db ->find < recurrent_transfer_object, by_from_to_id >( boost::make_tuple ( alice_id, bob_id ) );
428+ BOOST_REQUIRE ( rt != nullptr );
429+ BOOST_REQUIRE_EQUAL ( rt->remaining_executions , 1 );
430+ }
431+
432+ BOOST_TEST_MESSAGE ( " --- Deleting the transfer (amount == 0) with executions == 1" );
433+ recurrent_transfer_operation del_op;
434+ del_op.from = " alice" ;
435+ del_op.to = " bob" ;
436+ del_op.memo = " delete" ;
437+ del_op.amount = ASSET ( " 0.000 TESTS" );
438+ del_op.recurrence = 24 ;
439+ del_op.executions = 1 ;
440+
441+ if ( is_hf29 )
442+ {
443+ // since HF29 executions == 1 is accepted, so the deletion goes through
444+ executor->push_transaction ( del_op, alice_private_key );
445+ const auto * rt = executor->db ->find < recurrent_transfer_object, by_from_to_id >( boost::make_tuple ( alice_id, bob_id ) );
446+ BOOST_REQUIRE ( rt == nullptr );
447+ }
448+ else
449+ {
450+ // Old behavior must be preserved before HF29: pre-HF29 nodes reject executions == 1 in validate(),
451+ // so the evaluator must reject it on the delete path too - otherwise a new node would accept a
452+ // transaction an old node rejects, splitting the network before the hardfork activates.
453+ HIVE_REQUIRE_ASSERT ( executor->push_transaction ( del_op, alice_private_key ), delete_executions_assert_expr );
454+ const auto * rt = executor->db ->find < recurrent_transfer_object, by_from_to_id >( boost::make_tuple ( alice_id, bob_id ) );
455+ BOOST_REQUIRE ( rt != nullptr ); // not deleted
456+
457+ BOOST_TEST_MESSAGE ( " --- Deleting with executions >= 2 still works before HF29" );
458+ del_op.executions = 2 ;
459+ executor->push_transaction ( del_op, alice_private_key );
460+ const auto * rt_after = executor->db ->find < recurrent_transfer_object, by_from_to_id >( boost::make_tuple ( alice_id, bob_id ) );
461+ BOOST_REQUIRE ( rt_after == nullptr );
462+ }
463+
464+ executor->validate_database ();
465+ };
466+
467+ BOOST_TEST_MESSAGE ( " *****HF-28*****" );
468+ execute_hardfork<28 >( _content );
469+
470+ is_hf29 = true ;
471+
472+ BOOST_TEST_MESSAGE ( " *****HF-29*****" );
473+ execute_hardfork<29 >( _content );
474+ }
475+ FC_LOG_AND_RETHROW ()
476+ }
477+
394478BOOST_AUTO_TEST_SUITE_END ()
395479
396480#endif
0 commit comments