Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.0] Fix unlinked blocks caused by deferred trx removal #1734

Merged
merged 12 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions libraries/chain/controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1595,12 +1595,14 @@ struct controller_impl {
trx->packed_trx()->get_prunable_size() );
}

trx_context.delay = fc::seconds(trn.delay_sec);

if( check_auth ) {
authorization.check_authorization(
trn.actions,
trx->recovered_keys(),
{},
fc::seconds(trn.delay_sec),
trx_context.delay,
[&trx_context](){ trx_context.checktime(); },
false,
trx->is_dry_run()
Expand All @@ -1613,7 +1615,9 @@ struct controller_impl {

trx->billed_cpu_time_us = trx_context.billed_cpu_time_us;
if (!trx->implicit() && !trx->is_read_only()) {
transaction_receipt::status_enum s = transaction_receipt::executed;
transaction_receipt::status_enum s = (trx_context.delay == fc::seconds(0))
? transaction_receipt::executed
: transaction_receipt::delayed;
trace->receipt = push_receipt(*trx->packed_trx(), s, trx_context.billed_cpu_time_us, trace->net_usage);
std::get<building_block>(pending->_block_stage)._pending_trx_metas.emplace_back(trx);
} else {
Expand Down
1 change: 0 additions & 1 deletion libraries/chain/include/eosio/chain/controller.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,6 @@ namespace eosio { namespace chain {
private:
friend class apply_context;
friend class transaction_context;
friend void modify_gto_for_canceldelay_test(controller& control, const transaction_id_type& trx_id); // canceldelay_test in delay_tests.cpp need access to mutable_db

chainbase::database& mutable_db()const;

Expand Down
2 changes: 2 additions & 0 deletions libraries/chain/include/eosio/chain/transaction_context.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ namespace eosio { namespace chain {

void execute_action( uint32_t action_ordinal, uint32_t recurse_depth );

void schedule_transaction();
void record_transaction( const transaction_id_type& id, fc::time_point_sec expire );

void validate_cpu_usage_to_bill( int64_t billed_us, int64_t account_cpu_limit, bool check_minimum, int64_t subjective_billed_us )const;
Expand Down Expand Up @@ -142,6 +143,7 @@ namespace eosio { namespace chain {
/// the maximum number of virtual CPU instructions of the transaction that can be safely billed to the billable accounts
uint64_t initial_max_billable_cpu = 0;

fc::microseconds delay;
bool is_input = false;
bool apply_context_free = true;
bool enforce_whiteblacklist = true;
Expand Down
60 changes: 57 additions & 3 deletions libraries/chain/transaction_context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,12 @@ namespace eosio { namespace chain {
uint64_t packed_trx_prunable_size )
{
const transaction& trx = packed_trx.get_transaction();
EOS_ASSERT( trx.delay_sec.value == 0, transaction_exception, "transaction cannot be delayed" );
// delayed transactions are not allowed after protocol feature
// DISABLE_DEFERRED_TRXS_STAGE_1 is activated;
// read-only and dry-run transactions are not allowed to be delayed at any time
if( control.is_builtin_activated(builtin_protocol_feature_t::disable_deferred_trxs_stage_1) || is_transient() ) {
EOS_ASSERT( trx.delay_sec.value == 0, transaction_exception, "transaction cannot be delayed" );
}
heifner marked this conversation as resolved.
Show resolved Hide resolved
if( trx.transaction_extensions.size() > 0 ) {
disallow_transaction_extensions( "no transaction extensions supported yet for input transactions" );
}
Expand All @@ -266,6 +271,13 @@ namespace eosio { namespace chain {
uint64_t initial_net_usage = static_cast<uint64_t>(cfg.base_per_transaction_net_usage)
+ packed_trx_unprunable_size + discounted_size_for_pruned_data;

if( trx.delay_sec.value > 0 ) {
// If delayed, also charge ahead of time for the additional net usage needed to retire the delayed transaction
// whether that be by successfully executing, soft failure, hard failure, or expiration.
initial_net_usage += static_cast<uint64_t>(cfg.base_per_transaction_net_usage)
+ static_cast<uint64_t>(config::transaction_id_net_usage);
}

published = control.pending_block_time();
is_input = true;
if (!control.skip_trx_checks()) {
Expand Down Expand Up @@ -309,15 +321,21 @@ namespace eosio { namespace chain {
}
}

for( const auto& act : trx.actions ) {
schedule_action( act, act.account, false, 0, 0 );
if( delay == fc::microseconds() ) {
for( const auto& act : trx.actions ) {
schedule_action( act, act.account, false, 0, 0 );
}
}

auto& action_traces = trace->action_traces;
uint32_t num_original_actions_to_execute = action_traces.size();
for( uint32_t i = 1; i <= num_original_actions_to_execute; ++i ) {
execute_action( i, 0 );
}

if( delay != fc::microseconds() ) {
schedule_transaction();
}
}

void transaction_context::finalize() {
Expand Down Expand Up @@ -715,6 +733,42 @@ namespace eosio { namespace chain {
acontext.exec();
}

void transaction_context::schedule_transaction() {
// Charge ahead of time for the additional net usage needed to retire the delayed transaction
// whether that be by successfully executing, soft failure, hard failure, or expiration.
const transaction& trx = packed_trx.get_transaction();
if( trx.delay_sec.value == 0 ) { // Do not double bill. Only charge if we have not already charged for the delay.
const auto& cfg = control.get_global_properties().configuration;
add_net_usage( static_cast<uint64_t>(cfg.base_per_transaction_net_usage)
+ static_cast<uint64_t>(config::transaction_id_net_usage) ); // Will exit early if net usage cannot be payed.
}

auto first_auth = trx.first_authorizer();

uint32_t trx_size = 0;
const auto& cgto = control.mutable_db().create<generated_transaction_object>( [&]( auto& gto ) {
gto.trx_id = id;
gto.payer = first_auth;
gto.sender = account_name(); /// delayed transactions have no sender
gto.sender_id = transaction_id_to_sender_id( gto.trx_id );
gto.published = control.pending_block_time();
gto.delay_until = gto.published + delay;
gto.expiration = gto.delay_until + fc::seconds(control.get_global_properties().configuration.deferred_trx_expiration_window);
trx_size = gto.set( trx );

if (auto dm_logger = control.get_deep_mind_logger(is_transient())) {
std::string event_id = RAM_EVENT_ID("${id}", ("id", gto.id));

dm_logger->on_create_deferred(deep_mind_handler::operation_qualifier::push, gto, packed_trx);
dm_logger->on_ram_trace(std::move(event_id), "deferred_trx", "push", "deferred_trx_pushed");
}
});

int64_t ram_delta = (config::billable_size_v<generated_transaction_object> + trx_size);
add_ram_usage( cgto.payer, ram_delta );
trace->account_ram_delta = account_delta( cgto.payer, ram_delta );
}

void transaction_context::record_transaction( const transaction_id_type& id, fc::time_point_sec expire ) {
try {
control.mutable_db().create<transaction_object>([&](transaction_object& transaction) {
Expand Down
4 changes: 4 additions & 0 deletions plugins/producer_plugin/producer_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,10 @@ class producer_plugin_impl : public std::enable_shared_from_this<producer_plugin
transaction_metadata::trx_type trx_type,
bool return_failure_traces,
next_function<transaction_trace_ptr> next) {

const transaction& t = trx->get_transaction();
EOS_ASSERT( t.delay_sec.value == 0, transaction_exception, "transaction cannot be delayed" );

if (trx_type == transaction_metadata::trx_type::read_only) {
assert(_ro_thread_pool_size > 0); // enforced by chain_plugin
assert(app().executor().get_main_thread_id() != std::this_thread::get_id()); // should only be called from read only threads
Expand Down
7 changes: 0 additions & 7 deletions tests/nodeos_chainbase_allocation_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
# The following is the list of chainbase objects that need to be verified:
# - account_object (bootstrap)
# - code_object (bootstrap)
# - generated_transaction_object
# - global_property_object
# - key_value_object (bootstrap)
# - protocol_state_object (bootstrap)
Expand All @@ -55,12 +54,6 @@
irrNode = cluster.getNode(irrNodeId)
nonProdNode = cluster.getNode(nonProdNodeId)

# Create delayed transaction to create "generated_transaction_object"
cmd = "create account -j eosio sample EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV\
EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV --delay-sec 600 -p eosio"
trans = producerNode.processCleosCmd(cmd, cmd, silentErrors=False)
assert trans

# Schedule a new producer to trigger new producer schedule for "global_property_object"
newProducerAcc = Account("newprod")
newProducerAcc.ownerPublicKey = "EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV"
Expand Down
57 changes: 50 additions & 7 deletions unittests/api_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -767,11 +767,11 @@ BOOST_FIXTURE_TEST_CASE(cfa_stateful_api, validating_tester) try {
BOOST_REQUIRE_EQUAL( validate(), true );
} FC_LOG_AND_RETHROW()

BOOST_FIXTURE_TEST_CASE(deferred_cfa_not_allowed, validating_tester) try {
BOOST_FIXTURE_TEST_CASE(deferred_cfa_failed, validating_tester_no_disable_deferred_trx) try {

create_account( "testapi"_n );
produce_blocks(1);
set_code( "testapi"_n, test_contracts::test_api_wasm() );
produce_blocks(1);
set_code( "testapi"_n, test_contracts::test_api_wasm() );

account_name a = "testapi2"_n;
account_name creator = config::system_account_name;
Expand All @@ -785,15 +785,58 @@ BOOST_FIXTURE_TEST_CASE(deferred_cfa_not_allowed, validating_tester) try {
.owner = authority( get_public_key( a, "owner" ) ),
.active = authority( get_public_key( a, "active" ) )
});
action act({}, test_api_action<TEST_METHOD("test_transaction", "context_free_api")>{});
action act({}, test_api_action<TEST_METHOD("test_transaction", "stateful_api")>{});
trx.context_free_actions.push_back(act);
set_transaction_headers(trx, 10, 2); // set delay_sec to 2
set_transaction_headers(trx, 10, 2);
trx.sign( get_private_key( creator, "active" ), control->get_chain_id() );

BOOST_CHECK_EXCEPTION(push_transaction( trx ), fc::exception,
[&](const fc::exception &e) {
// any incoming trx is blocked
return expect_assert_message(e, "transaction cannot be delayed");
return expect_assert_message(e, "only context free api's can be used in this context");
});

produce_blocks(10);

// CFA failed, testapi2 not created
create_account( "testapi2"_n );

BOOST_REQUIRE_EQUAL( validate(), true );
} FC_LOG_AND_RETHROW()

BOOST_FIXTURE_TEST_CASE(deferred_cfa_success, validating_tester_no_disable_deferred_trx) try {

create_account( "testapi"_n );
produce_blocks(1);
set_code( "testapi"_n, test_contracts::test_api_wasm() );

account_name a = "testapi2"_n;
account_name creator = config::system_account_name;
signed_transaction trx;
trx.actions.emplace_back( vector<permission_level>{{creator,config::active_name}},
newaccount{
.creator = creator,
.name = a,
.owner = authority( get_public_key( a, "owner" ) ),
.active = authority( get_public_key( a, "active" ) )
});
action act({}, test_api_action<TEST_METHOD("test_transaction", "context_free_api")>{});
trx.context_free_actions.push_back(act);
set_transaction_headers(trx, 10, 2);
trx.sign( get_private_key( creator, "active" ), control->get_chain_id() );
auto trace = push_transaction( trx );
BOOST_REQUIRE(trace != nullptr);
if (trace) {
BOOST_REQUIRE_EQUAL(transaction_receipt_header::status_enum::delayed, trace->receipt->status);
BOOST_REQUIRE_EQUAL(1, trace->action_traces.size());
}
produce_blocks(10);

// CFA success, testapi2 created
BOOST_CHECK_EXCEPTION(create_account( "testapi2"_n ), fc::exception,
[&](const fc::exception &e) {
return expect_assert_message(e, "Cannot create account named testapi2, as that name is already taken");
});
BOOST_REQUIRE_EQUAL( validate(), true );
} FC_LOG_AND_RETHROW()

BOOST_AUTO_TEST_CASE(light_validation_skip_cfa) try {
Expand Down
Loading