Skip to content
This repository has been archived by the owner on Aug 2, 2022. It is now read-only.

Add deprecate_deferred_transactions protocol feature #8697

Draft
wants to merge 46 commits into
base: develop
Choose a base branch
from

Conversation

johndebord
Copy link
Contributor

@johndebord johndebord commented Feb 24, 2020

1st Protocol Feature `stop_deferred_transactions`
=================================================

[X]  = completed
[XX] = double-checked
[?]  = seems to be covered if I've implemented the above spec correctly

[XX] `cancel_deferred`
[XX]   - shall continue working
[XX] `send_deferred`
[XX]   - If `replace_existing` == true in `send_deferred`; it shall be a noop
[XX] `canceldelay` native action shall not change
[XX] `schedule_deferred_transaction`
[XX]   - If there is no transaction to replace:
           fail
[XX]   - If there is a transaction to replace:
           continue to cancel the old one but do not schedule the new one

[XX] `delaysec` > 0 is not allowed
[XX]   - Contract-generated deferred shall not assert in native code (`send_deferred`)
[XX]   - User-generated deferred shall assert in native code (`controller.cpp:push_transaction` (not `schedule_transaciton`))
[?]    - As soon as the protocol feature is activated there shall be no more ways to
           insert deferred transactions into the generated transaction table(s)
[?]    - Only if a deferred transaction is expired can it be pushed (`controller.cpp:push_schedule_transaction`)

Testing:
[XX] The fixture is going to have to be changed (don't activate all protocol features right away)
[XX] A few tests test to see if the queue gets larger (New test shall be added to verify that it doesn't do anything)
[?]  Check that the transaction expires appropriately
[?]  Will have to have duplicate tests, testing the different combinations of turning on/off the protocol features relating to deferred transactions
[XX] 1) User-side
[XX]    - Check that a user-defined transaction cannot have a `delay_sec` greater than 0
[XX] 2) Contract-side
[XX]    - Make sure that the corresponding logic for deferred transactions works correctly

@johndebord johndebord force-pushed the deprecate-deferred-protocol-feature branch 2 times, most recently from ccf3f18 to 6c9f0c1 Compare March 24, 2020 18:31
@nksanthosh nksanthosh requested a review from swatanabe-b1 March 25, 2020 18:01
Copy link
Contributor

@swatanabe-b1 swatanabe-b1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't reviewed the tests yet.

@@ -512,29 +519,33 @@ void apply_context::schedule_deferred_transaction( const uint128_t& sender_id, a

// Use remove and create rather than modify because mutating the trx_id field in a modifier is unsafe.
db.remove( *ptr );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a no-op, which doesn't seem consistent with the fact that send_deferred skips schedule_deferred_transaction entirely. Actually, I don't understand the motivation for allowing replace_existing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My assumption, based off of the specification, is that there are two ways to enter deferred transaction logic:

  1. Through transaction_api::send_deferred.
  2. Through a user extending his/her code via a plugin and calling apply_context::schedule_deferred_transaction.

Therefore two individual (seemingly redundant) checks must be made. Am I wrong in this assumption?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think apply_context::schedule_deferred_transaction should ever be called outside of wasm. Also, the main point that I was trying to make was that the behavior is not the same in the two cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe they now have the same behavior.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The behavior is still different in the case of replace_existing = true. transaction_api::send_deferred short-circuits this case to a no-op, but apply_context::schedule_deferred_transaction handles it in a more complex way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is what I was told to implement.

[XX] send_deferred
[XX] - If replace_existing == true in send_deferred; it shall be a noop

[XX] schedule_deferred_transaction
[XX] - If there is no transaction to replace: fail
[XX] - If there is a transaction to replace: continue to cancel the old one but do not schedule the new one

Hence, the assertion in apply_context::schedule_deferred_transaction.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain the motivation for this difference?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The motivation is unbeknownst to me. I just took it at face-value.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm somewhat concerned that this may be a misunderstanding. The spec for schedule_deferred_transaction sounds to me like reasonable behavior for both of them. @arhag

libraries/chain/apply_context.cpp Outdated Show resolved Hide resolved
libraries/chain/wasm_interface.cpp Show resolved Hide resolved
libraries/chain/protocol_feature_manager.cpp Outdated Show resolved Hide resolved
libraries/testing/include/eosio/testing/tester.hpp Outdated Show resolved Hide resolved
libraries/testing/include/eosio/testing/tester.hpp Outdated Show resolved Hide resolved
libraries/testing/tester.cpp Outdated Show resolved Hide resolved
task_spec Outdated Show resolved Hide resolved

bool stop_deferred_transactions_activated = self.is_builtin_activated(builtin_protocol_feature_t::stop_deferred_transactions);

if( gtrx.expiration < self.pending_block_time() || stop_deferred_transactions_activated ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to match the spec. "Only if a deferred transaction is expired can it be pushed" You're instead treating all transactions as if they were expired.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The behavior now matches the spec; if the transaction has expired then the transaction shall expire. Then we check to see if builtin_protocol_feature_t::stop_deferred_transactions has been activated; if so, then the trace shall be returned, and no further operations are done on the transaction.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does producer_plugin know how to handle this outcome?

libraries/testing/include/eosio/testing/tester.hpp Outdated Show resolved Hide resolved
@@ -379,7 +378,7 @@ namespace eosio { namespace testing {

void schedule_protocol_features_wo_preactivation(const vector<digest_type> feature_digests);
void preactivate_protocol_features(const vector<digest_type> feature_digests);
void preactivate_all_builtin_protocol_features();
void preactivate_builtin_protocol_features(const std::vector<builtin_protocol_feature_t>& ignored_features);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name will be confusing at the call site. If I see:

t.preactivate_builtin_protocol_features({wtmsig_block_signatures, replace_deferred});

I would assume that it meant the features to activate, not the features to exclude. I wouldn't even bother looking for the declaration because the meaning is "obvious."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to preactivate_selected_protocol_features.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how the new name expresses the meaning of the function better than the old name. I think part of the problem is that the function's behavior is odd, which makes it hard to come up with a good name.

const auto dependency_is_ignored = std::find( ignored_digests.cbegin(), ignored_digests.cend(), feature );
const auto dependency_has_dependency = std::find( pf.dependencies.cbegin(), pf.dependencies.cend(), feature );
if ( dependency_is_ignored != ignored_digests.cend() && dependency_has_dependency != pf.dependencies.cend() ) {
check_dependencies(*dependency_has_dependency);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this causes infinite recursion? *dependency_has_dependency should be equal to feature, right? I can't quite figure out what you're trying to do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should now be checking protocol feature dependencies correctly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only handles the simple case of A depends on B, ignore B. Does not handle the possibility of chaining ignores: A -> B -> C -> D, ignore D, which needs to cause all of A, B, C, and D to be ignored.

Also I'm on the fence about whether it's better to ignore additional features or whether it's better to issue an error. On the one hand, it's bad if adding a new protocol feature breaks existing tests. On the other hand, silently disabling more protocol features than were given may be surprising. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitely considered that case. But for the time being we only have the case of A depends on B.
Would it be best practice to account for future cases, such as chaining ignores? Or is the best practice only to account for what is currently in use?

And I definitely agree with you there on the last point. If it were up to me, I think tester is due for a re-write; to consider, from the ground up, an architecture where an arbitrary number of protocol features that may or may not depend on each other.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Best practice is to handle the public API of protocol features. Since arbitrary protocol feature dependencies are allowed, you should support them. Tester should not need any specific knowledge of what protocol features are actually defined. The existing code in tester to determine the activation order of protocol features is designed to handle arbitrary dependencies correctly, so I think it's reasonable for you to do so as well.

Now, with all of that having been said, being fully general is not always worth the effort. If you do decide to take shortcuts, you should definitely call it out in a comment, to help anyone who adds a new protocol feature and gets confused by mysterious failures.

libraries/testing/tester.cpp Outdated Show resolved Hide resolved
libraries/testing/tester.cpp Outdated Show resolved Hide resolved
set_code( N(testapi), contracts::test_api_wasm() );
produce_blocks(1);
BOOST_AUTO_TEST_CASE(action_receipt) { try {
validating_tester chain( {}, {::eosio::chain::builtin_protocol_feature_t::stop_deferred_transactions} );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original code uses TESTER. Can you make tester define the same constructors as validating_tester?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made matching constructors for class tester.

unittests/api_tests.cpp Outdated Show resolved Hide resolved
unittests/currency_tests.cpp Outdated Show resolved Hide resolved
@@ -522,8 +528,22 @@ void apply_context::schedule_deferred_transaction( const uint128_t& sender_id, a
gtx.expiration = gtx.delay_until + fc::seconds(control.get_global_properties().configuration.deferred_trx_expiration_window);

trx_size = gtx.set( trx );

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this now allows deferred transactions to be replaced. I'm not so sure that that's a good idea, since it allows the expiration to be arbitrarily extended as long as someone keeps replacing it.

libraries/chain/apply_context.cpp Outdated Show resolved Hide resolved
libraries/chain/apply_context.cpp Outdated Show resolved Hide resolved
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants