Skip to content

Commit 2b42e9f

Browse files
drov0vogel76
authored andcommitted
Guard recurrent transfer delete path against executions == 1 before HF29 (follow-up to !2022)
1 parent 8d435f0 commit 2b42e9f

2 files changed

Lines changed: 91 additions & 0 deletions

File tree

libraries/chain/hive_evaluator_transfer.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -987,6 +987,13 @@ void recurrent_transfer_evaluator::do_apply( const recurrent_transfer_operation&
987987
}
988988
else if( op.amount.amount == 0 )
989989
{
990+
// Pre-HF29 nodes reject executions == 1 on every path (incl. delete) in validate()
991+
if( !_db.has_hardfork( HIVE_HARDFORK_1_29_ALLOW_MODIFY_LAST_RECURRENT_TRANSFER ) )
992+
{
993+
HIVE_CHAIN_LIMIT_ASSERT( 2 <= op.executions, op.executions,
994+
"Executions must be at least 2 when deleting a recurrent transfer before HF29, "
995+
"setting executions to 1 was rejected by pre-HF29 nodes" );
996+
}
990997
_db.remove( *itr );
991998
_db.modify( from_account, [&](account_object& a )
992999
{

tests/unit/tests/hf29_tests.cpp

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
394478
BOOST_AUTO_TEST_SUITE_END()
395479

396480
#endif

0 commit comments

Comments
 (0)