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

Progress on #7, upgrades to testing framework #151

Merged
merged 7 commits into from
Aug 10, 2017
Merged
Show file tree
Hide file tree
Changes from all 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
11 changes: 9 additions & 2 deletions libraries/chain/chain_controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#include <eos/chain/transaction_object.hpp>
#include <eos/chain/producer_object.hpp>
#include <eos/chain/permission_link_object.hpp>
#include <eos/chain/authority_checker.hpp>

#include <eos/chain/wasm_interface.hpp>

Expand Down Expand Up @@ -245,6 +246,7 @@ ProcessedTransaction chain_controller::_push_transaction(const SignedTransaction
_pending_tx_session = _db.start_undo_session(true);

auto temp_session = _db.start_undo_session(true);
validate_referenced_accounts(trx);
check_transaction_authorization(trx);
auto pt = _apply_transaction(trx);
_pending_transactions.push_back(trx);
Expand Down Expand Up @@ -330,6 +332,7 @@ signed_block chain_controller::_generate_block(
try
{
auto temp_session = _db.start_undo_session(true);
validate_referenced_accounts(tx);
check_transaction_authorization(tx);
_apply_transaction(tx);
temp_session.squash();
Expand Down Expand Up @@ -440,8 +443,10 @@ void chain_controller::_apply_block(const signed_block& next_block)

for (const auto& cycle : next_block.cycles)
for (const auto& thread : cycle)
for (const auto& trx : thread.user_input)
for (const auto& trx : thread.user_input) {
validate_referenced_accounts(trx);
check_transaction_authorization(trx);
}

/* We do not need to push the undo state for each transaction
* because they either all apply and are valid or the
Expand Down Expand Up @@ -505,6 +510,9 @@ void chain_controller::check_transaction_authorization(const SignedTransaction&
("auth", declaredAuthority));
}
}

EOS_ASSERT(checker.all_keys_used(), tx_irrelevant_sig,
"Transaction bears irrelevant signatures from these keys: ${keys}", ("keys", checker.unused_keys()));
}

ProcessedTransaction chain_controller::apply_transaction(const SignedTransaction& trx, uint32_t skip)
Expand All @@ -520,7 +528,6 @@ try {
validate_expiration(trx);
validate_uniqueness(trx);
validate_tapos(trx);
validate_referenced_accounts(trx);

} FC_CAPTURE_AND_RETHROW( (trx) ) }

Expand Down
53 changes: 3 additions & 50 deletions libraries/chain/include/eos/chain/authority.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,62 +30,15 @@ struct shared_authority {
shared_vector<types::KeyPermissionWeight> keys;
};

/**
* @brief This class determines whether a set of signing keys are sufficient to satisfy an authority or not
*
* To determine whether an authority is satisfied or not, we first determine which keys have approved of a message, and
* then determine whether that list of keys is sufficient to satisfy the authority. This class takes a list of keys and
* provides the @ref satisfied method to determine whether that list of keys satisfies a provided authority.
*
* @tparam F A callable which takes a single argument of type @ref AccountPermission and returns the corresponding
* authority
*/
template<typename F>
class AuthorityChecker {
F PermissionToAuthority;
flat_set<public_key_type> signingKeys;

public:
AuthorityChecker(F PermissionToAuthority, const flat_set<public_key_type>& signingKeys)
: PermissionToAuthority(PermissionToAuthority), signingKeys(signingKeys) {}

bool satisfied(const types::AccountPermission& permission) const {
return satisfied(PermissionToAuthority(permission));
}
template<typename AuthorityType>
bool satisfied(const AuthorityType& authority) const {
UInt32 weight = 0;
for (const auto& kpw : authority.keys) {
if (signingKeys.count(kpw.key)) {
weight += kpw.weight;
if (weight >= authority.threshold)
return true;
}
}
for (const auto& apw : authority.accounts)
//#warning TODO: Recursion limit? Yes: implement as producer-configurable parameter
Copy link
Contributor

Choose a reason for hiding this comment

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

From now on all such TODO's need to reference a specific github issue number and should be tagged as "bug" and assigned to a final release milestone (June 2018) or earlier. We should also not flood our builds with warnings like this, especially in header files.

if (satisfied(apw.permission)) {
weight += apw.weight;
if (weight >= authority.threshold)
return true;
}
return false;
}
};

inline bool operator < ( const types::AccountPermission& a, const types::AccountPermission& b ) {
return std::tie( a.account, a.permission ) < std::tie( b.account, b.permission );
}
template<typename F>
AuthorityChecker<F> MakeAuthorityChecker(F&& pta, const flat_set<public_key_type>& signingKeys) {
return AuthorityChecker<F>(std::forward<F>(pta), signingKeys);
inline bool operator< (const types::AccountPermission& a, const types::AccountPermission& b) {
return std::tie(a.account, a.permission) < std::tie(b.account, b.permission);
}

/**
* Makes sure all keys are unique and sorted and all account permissions are unique and sorted and that authority can
* be satisfied
*/
inline bool validate( types::Authority& auth ) {
inline bool validate(const types::Authority& auth) {
const types::KeyPermissionWeight* prev = nullptr;
decltype(auth.threshold) totalWeight = 0;

Expand Down
127 changes: 127 additions & 0 deletions libraries/chain/include/eos/chain/authority_checker.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#pragma once

#include <eos/chain/types.hpp>
#include <eos/types/generated.hpp>

#include <eos/utilities/parallel_markers.hpp>

#include <fc/scoped_exit.hpp>

#include <boost/range/algorithm/find.hpp>
#include <boost/algorithm/cxx11/all_of.hpp>

namespace eos { namespace chain {

namespace detail {
using MetaPermission = static_variant<types::KeyPermissionWeight, types::AccountPermissionWeight>;

struct GetWeightVisitor {
using result_type = UInt32;

template<typename Permission>
UInt32 operator()(const Permission& permission) { return permission.weight; }
};

// Orders permissions descending by weight, and breaks ties with Key permissions being less than Account permissions
struct MetaPermissionComparator {
bool operator()(const MetaPermission& a, const MetaPermission& b) const {
GetWeightVisitor scale;
if (a.visit(scale) > b.visit(scale)) return true;
return a.contains<types::KeyPermissionWeight>() && !b.contains<types::KeyPermissionWeight>();
}
};

using MetaPermissionSet = boost::container::flat_multiset<MetaPermission, MetaPermissionComparator>;
}

/**
* @brief This class determines whether a set of signing keys are sufficient to satisfy an authority or not
*
* To determine whether an authority is satisfied or not, we first determine which keys have approved of a message, and
* then determine whether that list of keys is sufficient to satisfy the authority. This class takes a list of keys and
* provides the @ref satisfied method to determine whether that list of keys satisfies a provided authority.
*
* @tparam F A callable which takes a single argument of type @ref AccountPermission and returns the corresponding
* authority
*/
template<typename F>
class AuthorityChecker {
F PermissionToAuthority;
vector<public_key_type> signingKeys;
vector<bool> usedKeys;

struct WeightTallyVisitor {
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor style note: I lean towards stateless visitors when I can in order to improve the colocation of code and intent. Below in AuthorityChecker::satisfied we loop over all the permissions mutating the internal state of a visitor. The only hint that this is happening in the context of that method's scope is the use of Tally in the visitor's type.

I am not going to ding a review for it but, I would consider making the visitor simply produce the weight and then putting the aggregation of weights in the ::satisfied method where the result is important/used.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hrm, on second blush there are a few hidden side effects in this visitor, like usedKeys mutation. So, making it "pure" from an aggregation point of view is rather hollow.

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, the whole AuthorityChecker class got pretty complex, but what it's doing is also pretty bloody complex... I didn't come up with a simpler approach, but perhaps one exists. I'm open to suggestions. In the meantime, I designed it to hide all the details inside.

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 would consider making the visitor simply produce the weight and then putting the aggregation of weights in the ::satisfied method where the result is important/used.

Not sure I understand what you mean here. Do you mean not taking the early out you mentioned below?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ohh, you mean add up the weights in satisfied... Seems like 6 of one, half dozen of the other to me, but sure, I can change it.

using result_type = UInt32;

AuthorityChecker& checker;
UInt32 totalWeight = 0;

WeightTallyVisitor(AuthorityChecker& checker)
: checker(checker) {}

UInt32 operator()(const types::KeyPermissionWeight& permission) {
auto itr = boost::find(checker.signingKeys, permission.key);
if (itr != checker.signingKeys.end()) {
checker.usedKeys[itr - checker.signingKeys.begin()] = true;
totalWeight += permission.weight;
}
return totalWeight;
}
UInt32 operator()(const types::AccountPermissionWeight& permission) {
//TODO: Recursion limit? Yes: implement as producer-configurable parameter
if (checker.satisfied(permission.permission))
totalWeight += permission.weight;
return totalWeight;
}
};

public:
AuthorityChecker(F PermissionToAuthority, const flat_set<public_key_type>& signingKeys)
: PermissionToAuthority(PermissionToAuthority),
signingKeys(signingKeys.begin(), signingKeys.end()),
usedKeys(signingKeys.size(), false)
{}

bool satisfied(const types::AccountPermission& permission) {
return satisfied(PermissionToAuthority(permission));
}
template<typename AuthorityType>
bool satisfied(const AuthorityType& authority) {
// Save the current used keys; if we do not satisfy this authority, the newly used keys aren't actually used
auto KeyReverter = fc::make_scoped_exit([this, keys = usedKeys] () mutable {
usedKeys = keys;
});

// Sort key permissions and account permissions together into a single set of MetaPermissions
detail::MetaPermissionSet permissions;
permissions.insert(authority.keys.begin(), authority.keys.end());
permissions.insert(authority.accounts.begin(), authority.accounts.end());

// Check all permissions, from highest weight to lowest, seeing if signingKeys satisfies them or not
WeightTallyVisitor visitor(*this);
for (const auto& permission : permissions)
// If we've got enough weight, to satisfy the authority, return!
if (permission.visit(visitor) >= authority.threshold) {
KeyReverter.cancel();
return true;
Copy link
Contributor

Choose a reason for hiding this comment

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

are we concerned that this early out may lead to problems when permissions are over-authorized?
For instance, if a user were to have 2 messages that required (2 of 3: A, B, C) and (1 of 3: A, B) and had signatures from A, B and C. Depending on the order of the checks this will either throw for unecessary sigs (A,B pass, A pass) or not (B, C pass, A pass).

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 don't see what affect breaking early has on that... but we definitely want to break early, some of these authorities can be quite expensive to check (many recursions down an account multisig tree, for instance) and we don't want to do that if we already know the answer.

As to the case where two messages might let the user slip some extra sigs in... Can they, though? Authorities should be sorted, so we should always check the keys within them in the same order... That should prevent the situation you described, yes?

Copy link
Contributor

Choose a reason for hiding this comment

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

the sorting certainly means that for any transaction this always goes the same way. My concern is the burden on wallets to match this algorithm in their signature collection and pruning. There should be a usable set of rules for this pruning but it seems excessive.

A wallet must prune any signatures made irrelevant by this evaluation algorithm or it risks being rejected. Likely this means it will collect signatures and re-run this algorithm with the early out to determine which of the obtained signatures will be marked as "unused" in the case of over authorization.

I understand that over-authorization has costs but the rules for a properly formed transaction are made more complex by this early out. Without it, we may exceed the authority threshold but we will mark all signatures that could be part of a message authorization as used. Relieving the burden of wallets but putting potentially more stress on the chain/processing.

Copy link
Contributor Author

@nathanielhourt nathanielhourt Aug 10, 2017

Choose a reason for hiding this comment

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

I guess the ordering requirement only means that we have consistent behavior across messages, not that we would detect that oversigning issue you described. I suppose to address that, I can make WeightTallyVisitor check if keys that are already used would satisfy the authority first, and only then check unused keys/marking them used if they help.

I'll write up a test case to exercise this issue, then fix it.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can make the over-authorization a "soft" consensus rather than a "hard" consensus. In other words, block producers may choose to include over-authorized transactions, but in practice will reject them. This will allow us to update / refine this without hard forking the chain.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Then again, I can't figure out a way to make WeightTallyVisitor mark a key as used when it didn't need to be used. Since it always checks keys in the same order, it always prefers using already-used keys to unused ones without me explicitly checking for it.

}
return false;
}

bool all_keys_used() const { return boost::algorithm::all_of_equal(usedKeys, true); }
flat_set<public_key_type> used_keys() const {
auto range = utilities::FilterDataByMarker(signingKeys, usedKeys, true);
return {range.begin(), range.end()};
}
flat_set<public_key_type> unused_keys() const {
auto range = utilities::FilterDataByMarker(signingKeys, usedKeys, false);
return {range.begin(), range.end()};
}
};

template<typename F>
AuthorityChecker<F> MakeAuthorityChecker(F&& pta, const flat_set<public_key_type>& signingKeys) {
return AuthorityChecker<F>(std::forward<F>(pta), signingKeys);
}

}} // namespace eos::chain
15 changes: 3 additions & 12 deletions libraries/chain/message_handling_contexts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@
#include <eos/chain/key_value_object.hpp>
#include <eos/chain/chain_controller.hpp>

#include <eos/utilities/parallel_markers.hpp>

#include <boost/algorithm/cxx11/all_of.hpp>
#include <boost/range/algorithm/find_if.hpp>
#include <boost/range/combine.hpp>
#include <boost/range/adaptor/filtered.hpp>
#include <boost/range/adaptor/transformed.hpp>

namespace eos { namespace chain {

Expand Down Expand Up @@ -48,15 +47,7 @@ bool apply_context::all_authorizations_used() const {
}

vector<types::AccountPermission> apply_context::unused_authorizations() const {
auto RemoveUsed = boost::adaptors::filtered([](const auto& tuple) {
return !boost::get<0>(tuple);
});
auto ToPermission = boost::adaptors::transformed([](const auto& tuple) {
return boost::get<1>(tuple);
});

// zip the parallel arrays, filter out the used authorizations, and return just the permissions that are left
auto range = boost::combine(used_authorizations, msg.authorization) | RemoveUsed | ToPermission;
auto range = utilities::FilterDataByMarker(msg.authorization, used_authorizations, false);
return {range.begin(), range.end()};
}

Expand Down
8 changes: 6 additions & 2 deletions libraries/fc/include/fc/scoped_exit.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,23 @@ namespace fc {
scoped_exit( C&& c ):callback( std::forward<C>(c) ){}
scoped_exit( scoped_exit&& mv ):callback( std::move( mv.callback ) ){}

void cancel() { canceled = true; }

~scoped_exit() {
try { callback(); } catch( ... ) {}
if (!canceled)
try { callback(); } catch( ... ) {}
}

scoped_exit& operator = ( scoped_exit&& mv ) {
callback = std::move(mv);
callback = std::move(mv.callback);
return *this;
}
private:
scoped_exit( const scoped_exit& );
scoped_exit& operator=( const scoped_exit& );

Callback callback;
bool canceled = false;
};

template<typename Callback>
Expand Down
39 changes: 39 additions & 0 deletions libraries/utilities/include/eos/utilities/parallel_markers.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#pragma once

#include <boost/range/combine.hpp>
#include <boost/range/adaptor/filtered.hpp>
#include <boost/range/adaptor/transformed.hpp>

namespace eos { namespace utilities {

/**
* @brief Return values in DataRange corresponding to matching Markers
*
* Takes two parallel ranges, a Data range containing data values, and a Marker range containing markers on the
* corresponding data values. Returns a new Data range containing only the values corresponding to markers which match
* markerValue
*
* For example:
* @code{.cpp}
* vector<char> data = {'A', 'B', 'C'};
* vector<bool> markers = {true, false, true};
* auto markedData = FilterDataByMarker(data, markers, true);
* // markedData contains {'A', 'C'}
* @endcode
*/
template<typename DataRange, typename MarkerRange, typename Marker>
DataRange FilterDataByMarker(DataRange data, MarkerRange markers, const Marker& markerValue) {
auto RemoveMismatchedMarkers = boost::adaptors::filtered([&markerValue](const auto& tuple) {
return boost::get<0>(tuple) == markerValue;
});
auto ToData = boost::adaptors::transformed([](const auto& tuple) {
return boost::get<1>(tuple);
});

// Zip the ranges together, filter out data with markers that don't match, and return the data without the markers
auto range = boost::combine(markers, data) | RemoveMismatchedMarkers | ToData;
return {range.begin(), range.end()};
}

}} // namespace eos::utilities

Loading