前言
从前面的分析中,我们知道,从API投递给EOSIO的交易,在执行时是要进行权限鉴定的,如果权限鉴定通过了才能继续执行交易,否则抛出异常。
在EOSIO中,权限的鉴定一般使用在三个地方
- 新投递交易的权限鉴定
- 内链交易的权限鉴定
- get_required_keys函数调用
权限鉴定接口调用
新投递交易的权限鉴定
调用入口如下
// 如果非隐式交易,并且要求鉴定,才进入权限鉴定流程
const bool check_auth = !self.skip_auth_check() && !trx->implicit;
if( check_auth ) {
authorization.check_authorization(
trn.actions,
trx->recovered_keys(),
{
},
trx_context.delay,
[&trx_context](){
trx_context.checktime(); },
false
);
}
// 继续进入函数
/***** 对应参数 ******************************************************
参数1:需要鉴定的action
参数2:需要提供的公钥权限
参数3:需要提供的账户权限
参数4:延迟权限
参数5:超时判断
参数6:表示是否允许未使用的key,这个估计和以前的get_requred_key有关
参数7:继承的权限,这个在内链操作时需要传递
*******************************************************************/
void
authorization_manager::check_authorization( const vector<action>& actions,
const flat_set<public_key_type>& provided_keys,
const flat_set<permission_level>& provided_permissions,
fc::microseconds provided_delay,
const std::function<void()>& _checktime,
bool allow_unused_keys,
const flat_set<permission_level>& satisfied_authorizations
)const
{
// 构建权限鉴定时需要的参数
// 1, 超时检查,EOSIO是按照资源收费的,所以做任何一项操作都要检查资源,如这里检查CPU
// 2, 权限递归深度,前面我们分析过权限的数据结构,EOSIO的权限是一个可以递归嵌套的数据结构
// 3, 读取权限的回调函数,get_permission
const auto& checktime = ( static_cast<bool>(_checktime) ? _checktime : _noop_checktime );
auto delay_max_limit = fc::seconds( _control.get_global_properties().configuration.max_transaction_delay );
auto effective_provided_delay = (provided_delay >= delay_max_limit) ? fc::microseconds::maximum() : provided_delay;
auto checker = make_auth_checker( [&](const permission_level& p){
return get_permission(p).auth; },
_control.get_global_properties().configuration.max_authority_depth,
provided_keys,
provided_permissions,
effective_provided_delay,
checktime
);
map<permission_level, fc::microseconds> permissions_to_satisfy;
// 注意: 对于交易的权限,EOSIO是逐个检查每个action的权限
// 并且对于如果是eosio账户操作的权限,是要单独鉴定的,按照不同的action,对应不同的权限判断
for( const auto& act : actions ) {
bool special_case = false;
fc::microseconds delay = effective_provided_delay;
if( act.account == config::system_account_name ) {
special_case = true;
if( act.name == updateauth::get_name() ) {
check_updateauth_authorization( act.data_as<updateauth>(), act.authorization );
} else if( act.name == deleteauth::get_name() ) {
check_deleteauth_authorization( act.data_as<deleteauth>(), act.authorization );
} else if( act.name == linkauth::get_name() ) {
check_linkauth_authorization( act.data_as<linkauth>(), act.authorization );
} else if( act.name == unlinkauth::get_name() ) {
check_unlinkauth_authorization( act.data_as<unlinkauth>(), act.authorization );
} else if( act.name == canceldelay::get_name() ) {
delay = std::max( delay, check_canceldelay_authorization(act.data_as<canceldelay>(), act.authorization) );
} else {
special_case = false;
}
}
// 对最小权限的判断
// 对标的结构是我们前面介绍过的权限可以与合约的action进行绑定,如果没有绑定的最小权限,默认使用账户的active权限
for( const auto& declared_auth : act.authorization ) {
checktime();
if( !special_case ) {
auto min_permission_name = lookup_minimum_permission(declared_auth.actor, act.account, act.name);
if( min_permission_name ) {
// since special cases were already handled, it should only be false if the permission is eosio.any
const auto& min_permission = get_permission({
declared_auth.actor, *min_permission_name});
EOS_ASSERT( get_permission(declared_auth).satisfies( min_permission,
_db.get_index<permission_index>().indices() ),
irrelevant_auth_exception,
"action declares irrelevant authority '${auth}'; minimum authority is ${min}",
("auth", declared_auth)("min", permission_level{
min_permission.owner, min_permission.name}) );
}
}
if( satisfied_authorizations.find( declared_auth ) == satisfied_authorizations.end() ) {
auto res = permissions_to_satisfy.emplace( declared_auth, delay );
if( !res.second && res.first->second > delay) {
// if the declared_auth was already in the map and with a higher delay
res.first->second = delay;
}
}
}
}
// 最后对于挑选出来的权限,调用checker.satisfied函数进行权限鉴定
for( const auto& p : permissions_to_satisfy ) {
checktime(); // TODO: this should eventually move into authority_checker instead
EOS_ASSERT( checker.satisfied( p.first, p.second ), unsatisfied_authorization,
"transaction declares authority '${auth}', "
"but does not have signatures for it under a provided delay of ${provided_delay} ms, "
"provided permissions ${provided_permissions}, provided keys ${provided_keys}, "
"and a delay max limit of ${delay_max_limit_ms} ms",
("auth", p.first)
("provided_delay", provided_delay.count()/1000)
("provided_permissions", provided_permissions)
("provided_keys", provided_keys)
("delay_max_limit_ms", delay_max_limit.count()/1000)
);
}
if( !allow_unused_keys ) {
EOS_ASSERT( checker.all_keys_used(), tx_irrelevant_sig,
"transaction bears irrelevant signatures from these keys: ${keys}",
("keys", checker.unused_keys()) );
}
}
内链交易的权限鉴定
// 内链交易的权限鉴定和投递交易的权限鉴定调用函数是一样的,只是在要求提供的权限上要求有config::eosio_code_name权限
if( !control.skip_auth_check() && !privileged ) {
try {
control.get_authorization_manager()
.check_authorization( {
a},
{
},
{
{
receiver, config::eosio_code_name}},
control.pending_block_time() - trx_context.published,
std::bind(&transaction_context::checktime, &this->trx_context),
false,
inherited_authorizations
);
} catch( const fc::exception& e ) {
// 异常处理
}
}
get_required_keys函数调用
flat_set<public_key_type> authorization_manager::get_required_keys( const transaction& trx,
const flat_set<public_key_type>& candidate_keys,
fc::microseconds provided_delay
)const
{
auto checker = make_auth_checker( [&](const permission_level& p){
return get_permission(p).auth; },
_control.get_global_properties().configuration.max_authority_depth,
candidate_keys,
{
},
provided_delay,
_noop_checktime
);
for (const auto& act : trx.actions ) {
for (const auto& declared_auth : act.authorization) {
EOS_ASSERT( checker.satisfied(declared_auth), unsatisfied_authorization,
"transaction declares authority '${auth}', but does not have signatures for it.",
("auth", declared_auth) );
}
}
return checker.used_keys();
}
从权限鉴定的三处用处我们可以得出以下结论
- 如果不做特殊过滤,对于投递过来的交易都是要鉴定的
- 对于投递过来的建议,如果是合约是eosio系统合约,权限是要特别鉴定的
- 对于内链交易,如果当前合约账户是超级账户,则可以不用鉴定,如eosio账户系统上默认是超级的,如果在eosio合约上调用其他合约是不需要进行鉴定的
- 内链交易,在权限的要求上,增加了config::eosio_code_name权限,就是说你必须要给账户申请config::eosio_code_name权限,才能进行内链交易
- 三处权限鉴定最终都是调用checker.satisfied函数来完成的
authority_checker::satisfied函数分析
EOSIO系统权限鉴定最终都是调用authority_checker::satisfied函数来完成的,整个类的实现都在下列文件中
#include <eosio/chain/authority_checker.hpp>
核心代码如下
template<typename AuthorityType>
bool satisfied( const AuthorityType& authority, permission_cache_type& cached_permissions, uint16_t depth ) {
// 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 = _used_keys] () mutable {
_used_keys = keys;
});
// Sort key permissions and account permissions together into a single set of meta_permissions
detail::meta_permission_map permissions;
// 构建权限判断的访问器,这里只是定义了权限判断的回调函数
weight_tally_visitor visitor(*this, cached_permissions, depth);
auto emplace_permission = [&permissions, &visitor](int priority, const auto& mp) {
permissions.emplace(
std::make_tuple(mp.weight, priority),
[&mp, &visitor]() {
return visitor(mp); // 调用weight_tally_visitor::operator()函数
}
);
};
// 将权限对应的三个子域变量分别于emplace_permission绑定,建立访问判断的调用关系
// 请回忆权限的定义结构 { 阈值,keys, accounts, waits }
permissions.reserve(authority.waits.size() + authority.keys.size() + authority.accounts.size());
std::for_each(authority.accounts.begin(), authority.accounts.end(), std::bind(emplace_permission, 1, std::placeholders::_1));
std::for_each(authority.keys.begin(), authority.keys.end(), std::bind(emplace_permission, 2, std::placeholders::_1));
std::for_each(authority.waits.begin(), authority.waits.end(), std::bind(emplace_permission, 3, std::placeholders::_1));
// Check all permissions, from highest weight to lowest, seeing if provided authorization factors satisfies them or not
// 检查所有的权限,调用构建的访问函数,计算对应的权重,计算完成后和权限的阈值做比较,如果大于或者等于阈值则权限判断成功
for( const auto& p: permissions )
// If we've got enough weight, to satisfy the authority, return!
if( p.second() >= authority.threshold ) {
KeyReverter.cancel();
return true;
}
return false;
}
// 最核心的获取权限与计算权重之和的函数
uint32_t operator()(const permission_level_weight& permission) {
auto status = authority_checker::permission_status_in_cache( cached_permissions, permission.permission );
// 如果权限在cache中直接计算判断,如果不在需要去数据库读取
if( !status ) {
// 判断递归深度,如果深度大于checker.recursion_depth_limit就不判断了,在config.hpp中定义
if( recursion_depth < checker.recursion_depth_limit ) {
bool r = false;
typename permission_cache_type::iterator itr = cached_permissions.end();
bool propagate_error = false;
try {
// 利用回调函数获取要求的权限
auto&& auth = checker.permission_to_authority( permission.permission );
propagate_error = true;
auto res = cached_permissions.emplace( permission.permission, being_evaluated );
itr = res.first;
// 注意这里递归调用,它判断了keys,accounts权限子域中出现递归权限
r = checker.satisfied( std::forward<decltype(auth)>(auth), cached_permissions, recursion_depth + 1 );
} catch( const permission_query_exception& ) {
if( propagate_error )
throw;
else
return total_weight; // if the permission doesn't exist, continue without it
}
// 计算权重值和
if( r ) {
total_weight += permission.weight;
itr->second = permission_satisfied;
} else {
itr->second = permission_unsatisfied;
}
}
} else if( *status == permission_satisfied ) {
//计算权重值和
total_weight += permission.weight;
}
return total_weight;
}
};
总体来看,权限的判断比较简单,就是判断了账户对应权限中权重之和是否大于权限对应的阈值,如果大于或者等于则权限验证通过。
注意这里需要着重理解权重,阈值相关概念,同时也要记住权限相关的数据结构,这样我们在后续的权限操作和阅读代码会更加容易
eosio合约的权限判断
在前面的介绍中,我们看到对于eosio的合约,权限是要单独判断的,如下代码
if( act.name == updateauth::get_name() ) {
check_updateauth_authorization( act.data_as<updateauth>(), act.authorization );
} else if( act.name == deleteauth::get_name() ) {
check_deleteauth_authorization( act.data_as<deleteauth>(), act.authorization );
} else if( act.name == linkauth::get_name() ) {
check_linkauth_authorization( act.data_as<linkauth>(), act.authorization );
} else if( act.name == unlinkauth::get_name() ) {
check_unlinkauth_authorization( act.data_as<unlinkauth>(), act.authorization );
} else if( act.name == canceldelay::get_name() ) {
delay = std::max( delay, check_canceldelay_authorization(act.data_as<canceldelay>(), act.authorization) );
} else {
special_case = false;
}
上面的代码显示出,如果是权限相关的操作,需要特别鉴定,我们以updateauth操作为例, 分析代码如下
void authorization_manager::check_updateauth_authorization( const updateauth& update,
const vector<permission_level>& auths
)const
{
// 账户只能携带一个权限
EOS_ASSERT( auths.size() == 1, irrelevant_auth_exception,
"updateauth action should only have one declared authorization" );
const auto& auth = auths[0];
// 只能有eosio账户来操作
EOS_ASSERT( auth.actor == update.account, irrelevant_auth_exception,
"the owner of the affected permission needs to be the actor of the declared authorization" );
// 判断权限是否存在
const auto* min_permission = find_permission({
update.account, update.permission});
if( !min_permission ) {
// creating a new permission
min_permission = &get_permission({
update.account, update.parent});
}
// 判断权限父子关系,只有父权限才能操作
EOS_ASSERT( get_permission(auth).satisfies( *min_permission,
_db.get_index<permission_index>().indices() ),
irrelevant_auth_exception,
"updateauth action declares irrelevant authority '${auth}'; minimum authority is ${min}",
("auth", auth)("min", permission_level{
update.account, min_permission->name}) );
}
其他的action权限,我们对应分析就可以逐个理解关于eosio合约的权限鉴定,总结出以下共同规则
- 账户只能携带一个权限
- 只能有eosio账户来操作
- 只有父权限才有操作的权利
至于每个动作私有的判断,分别判断就可以
总结
以上就是对EOSIO权限鉴定的分析过程,总结起来牢记以下几点就能深刻理解EOSIO的权限管理
- 熟悉并牢记权限相关的数据结构,特别要理解阈值,权重的概念
- 明确EOSIO的权限是可以嵌套的,一个账户可以借助另外一个账户的权限
- 公钥与账户是多对多的关系,所以EOSIO是支持多账户签名的
- eosio合约中关于权限操作时交易是要做特别鉴权的
- 深刻理解内链交易,特别要理解内链时,对相关账户分配code权限