system合约源码分析

写在前面

system合约是EOS区块链最核心的智能合约,分析其源码可以一窥EOS的精妙之处。

文件结构

native 基础的数据结构和功能
delegate_bandwidth 带宽抵押与内存买卖
exchange_state 内存市场数据结构与bancor算法
eosio.system 关键数据结构
producer_pay和voting DPOS相关

接口

// *** native.hpp ***
newaccount
updateauth
deleteauth
linkauth
unlinkauth
canceldelay
onerror

// 更新区块链参数到链上
// require_auth(eosio)
void setparams(const blockchain_parameters& params);

// 设置用户特权级
// ispriv为bool值
void setpriv(account_name account, uint8_t ispriv);

数据结构

/* 区块链全局状态 */
struct eosio_global_state : public blockchain_parameters
{
	uint64_t max_ram_size = 64 * 1024 * 1024 * 1024; // 64GB
	uint64_t total_ram_bytes_reserved;
	int64_t total_ram_stake;
	block_timestamp last_porducer_schedule_update;
	uint64_t last_pervote_bucket_fill;
	int64_t pervote_bucket;
	int64_t perblock_bucket;
	uint32_t total_unpaid_blocks;
	int64_t total_activated_stake_time;
	uint64_t thresh_activated_stake_time;
	uint16_t last_producer_schedule_size;
	double total_producer_vote_weight;
	block_timestamp last_name_close;
}

// 区块链参数
struct blockchain_parameters
{
	uint64_t max_block_net_usage;
	uint32_t target_block_net_usage_pct;
	uint32_t max_transaction_net_usage;
	uint32_t base_per_transaction_net_usage;
	uint32_t net_usage_leeway;
	uint32_t context_free_discount_net_usage_num;
	uint32_t context_free_discount_net_usage_den;
	uint32_t max_block_cpu_usage;
	uint32_t target_block_cpu_usage_pct;
	uint32_t max_transaction_cpu_usage;
	uint32_t min_transaction_cpu_usage;
	uint64_t context_free_discount_cpu_usage_num;
	uint64_t context_free_discount_cpu_usage_den;
	uint32_t max_transaction_lifetime;
	uint32_t deferred_trx_expiration_window;
	uint32_t max_transaction_delay;
	uint32_t max_inline_action_size;
	uint16_t max_inline_action_depth;
	uint16_t max_authority_depth;
};

核心算法

SYSTEM合约的构造与析构

有一定EOS智能合约开发经验的朋友可以知道,在每次调用合约对外接口时,智能合约对应的类的构造函数和析构函数都是会被调用一遍的,如此使得合约类的构造析构函数会发挥特殊的作用。

构造
1.获取当前区块链参数
2.开启内存市场

析构
更新区块链参数

单例singleton
system合约类定义了两个特殊的成员变量,_gstate和_global。
_global为eosio::singleton类型,_gstate为eosio_global_state类型。
这里涉及到eos内置的singleton,通过观察其定义,可以知道是通过eosio::multi_index存储实例的模板类单例。
eosio::singleton的用法通常会与EOS合约的构造析构函数结合起来,一个是singleton,一个是临时变量。在构造函数中,把singleton的值传到临时变量,其他接口都是通过操作临时变量来完成实现,最后在析构函数中把最新的值更新到singleton中。

带宽抵押

// N(userres) user_resources_table
user_resources {
	account_name owner // primary key
	asset        net_weight
	asset        cpu_weight
	int64_t      ram_bytes
}

// N(delband) del_bandwidth_table
delegated_bandwidth {
    account_name from
    account_name to // primary key
    asset        net_weight
	asset        cpu_weight
}

// N(refunds) refunds_request
refund_request {
	account_name owner // primary key
	time         request_time
	asset        net_amount
	asset        cpu_amount
}

+ void delegatebw(account_name from, 
				  account_name receiver,
				  asset stake_net_quantity,
				  asset stake_cpu_quantity,
				  bool transfer);

+ void undelegatebw(account_name from,
				    account_name receiver,
				    asset unstake_net_quantity,
				    asset unstake_cpu_quantity);
				    
+ void refund(account_name);

- void changebw(account_name from,
				account_name receiver,
				const asset stake_net_delta,
				const asset stake_cpu_delta,
				bool transfer)

抵押带宽delegatebw和反抵押带宽undelegatebw内部都是通过调用更改带宽changebw来实现抵押与反抵押。
**changebw的逻辑:

  1. 要求from的授权
  2. 更新抵押记录表(更新del_bandwidth_table)
  3. 更新抵押总量表(更新user_resources_table)
  4. 更新退款表(更新refunds_table)
  5. 若有需要,发送延迟退款交易
  6. 更新选票权重

EOS中可以用EOS抵押的带宽有CPU,网络带宽,抵押比例为1:1。
在抵押带宽的过程中,如果含有transfer标志,则资产会转移到接受者(receiver)账下(后面反抵押由receiver发起);否则,资产仍然属于抵押者(from)账下(后面反抵押由from发起)。
在反抵押的过程中,先前抵押以获得的带宽会立刻减少对应的数量,而抵押的EOS会在抵押周期过后返还给抵押者(from)。
在抵押和反抵押的过程中,用户选票的权重会立即根据当前的抵押量而变化。

内存买卖

// 更新最大内存总量
+ void setram(uint64_t max_ram_size);

+ void buyrambytes(account_name from,
				   account_name receiver,
				   uint32_t bytes);

+ void buyram(account_name buyer, 
			  account_name receiver, 
			  asset tokens);
			
+ void sellram(account_name receiver, int64_t bytes);

购买内存逻辑

  1. 计算手续费,公式为:fee = (token + 199) / 200
  2. 购买内存的token(扣去手续费)会转移到eosio.ram帐号,手续费会转移到eosio.ramfee帐号
  3. 通过bancor算法计算出对应的内存数量
  4. 更新eosio_global_state字段
  5. 更新user_resource_table(scope=receiver)
  6. 更新用户的资源限制配置

用户以通货购买内存,根据当前内存价格(bancor算法),以提高内存的使用额度。


出售内存逻辑

  1. 根据bancor算法计算出内存的代币售价
  2. 更新eosio_global_state字段
  3. 更新user_resource_table(scope=receiver)
  4. 更新用户的资源限制配置
  5. 代币售价从eosio.ram转移到原来购买内存的用户
  6. 收取手续费,从用户账户转移到eosio.ram

在卖出内存时,返还的通货数量是以原来购买内存时购买额度的平均价钱决定的。

帐号竞拍

/* 账号竞拍信息 */
name_bid
{
	account_name newname; // primary key
	account_name high_bidder;
	int64_t high_bid; // secondary key,负数表示等待揭晓的已关闭的拍卖
	uint64_t last_bid_time;
}

+ void bidname(account_name bidder, account_name newname, asset bid)

EOS可以对指定名字的账户进行竞拍,但有特殊要求:长度为0 - 11个字符 或 12字符且包含点(dot .)。
新的竞价也要比当前的最高竞价高出至少10%:bid.amount - current->high_bid > (current->high_bid / 10)。

DPOS相关

/* 生产者/超级节点信息 */
producer_info
{
	account_name owner;  // primary key
	double total_votes; // secondary key, 
	public_key producer_key;
	bool is_active;
	string url;
	uint32_t unpaid_blocks;
	uint64_t last_claim_time;
	uint16_t location;
}

/* 选民信息 
包含选民ID,代理,投票权重等信息
*/
voter_info
{
	account_name owner;  // primary key
	account_name proxy;
	vector<account_name> producers;
	int64_t staked;
	double last_vote_weight;
	double proxied_vote_weight;
	bool is_proxy;
	uint32_t reserved1;
	time reserved2;
	asset reserved3;
}

+ void regproducer(const account_name producer,
				   const public_key& producer_key,
				   const string& url,
				   uint16_t location);

void rmvproducer(account_name producer)

+ void voteproducer(const account_name voter,
					const account_name proxy,
					const vector<account_name>& producers);

+ void unregprod(const account_name producer);

+ void regproxy(const account_name proxy, bool isproxy);

// 更新区块生产信息
void onblock(block_timestamp timestamp, account_name producer);

// 派发区块生产者奖励
void claimrewards(const account_name& owner)

投票权重算法
weight = int64_t((now() - (block_timestamp::block_timestamp_epoch / 1000)) / (seconds_per_day * 7)) / double(52)
vote = double(staked) * 2 ** weight

生产者投票与投票代理

生产者producer:即生产区块的超级节点。
投票人voter:生产者的投票人
代理人proxy:代表其他人来投票,代理人需要有staked的资产。

regproducer,unregprod,regproxy逻辑较为简单,在此略过。


更新投票(update_votes)
前提要求:

  • 投票人voter != 代理人proxy
  • vector投票的生产者列表必须已排序
  • 如果voter是一个proxy,则不允许把自身投票代理给其他人。

逻辑过程:

  1. 如果投票人是第一次投票,则更新global_state.total_activated_stake += voter.staked
    1.1 如果投票量刚刚越过投票有效阀值,则更新global_state.thresh_activated_stake_time = current_time
  2. 计算投票人新的投票权重,stake2vote(vote.staked)
    2.1. 如果投票人同时也是代理人,新投票权重 += voter.proxied_vote_weight
  3. 如果投票人已经投过票(即不是第一次投票)而且投票人已经代理投票给其他人,则回收旧的投票量
    3.1 如果投票人已经投过票但没有代理投票给其他人,则
for (const auto & p : voter->producers)
{
	auto& d = producer_deltas[p];
	d.first -= voter->last_vote_weight;
	d.second = false;
}
  1. 如果proxy参数不为空,即给当前投票人的投票代理给新的代理人,则
    4.1. new_proxy.proxied_vote_weight += new_vote_weight
    4.2. propage_weight_change(*new_proxy)
  2. 添加投票增量到每一个被投票的生产者
for every producer of producer_deltas
	producer.total_votes += producer_deltas[produceritr].vote_weight
	_gstate.total_producer_vote_weight += producer_deltas[produceritr].vote_weight
  1. 更新投票人数据记录
voter.last_vote_weight = new_vote_weight
voter.producers = producers
voter.proxy = proxy

产块奖励与代币增发

在每个新的区块中,区块生产者会生成第一个交易,该交易就是onblock,用于标记未付款的区块数(unpaid block)和更新账户竞标的信息。
而发放奖励的接口claimrewards由外部调用触发。

onblock

  1. 当前总激活抵押量需要超过最低激活抵押量才可继续,否则结束。PS:激活抵押量即已投票的抵押量。
  2. 如果last_pervote_bucket_fill为0,更新last_pervote_bucket_fill时间点。
  3. global_state的total_unpaid_blocks和producer_table对应区块生产者的unpaid_blocks计数+1。
  4. 如果当前时间比最后一次生产者产块调度更新时间大60s,则
    4.1. 更新生产者调度update_elected_producers,即重新更新当前票数最高的前21个生产者到global_state。
    4.2. 更新帐号竞拍信息。

宣布奖励claimrewards
发放给指定的生产者以对应的奖励。该接口在区块链内部不会自动调用,由外部调用触发。

前提要求:

  • 需要eosio和待奖励的生产者的权限
  • 一个区块只能包含一个claimrewards的交易
  • owner必须是一个激活的生产者
  • 当前总激活抵押量超过最低激活抵押量
  • 对于单独一个生产者,一天内只能发放一次奖励(即可以多次调用)

EOSIO并非每年只增发一次,一次性增发完全年所有的EOS,而是采用连续增发方式,即任何节点领取(claim)时都会触发系统的增发动作,这也就意味着,增发是根据流动时间长度不定期发生的。

new_tokens为增发的代币量,公式为:

增发代币数量 = 年通货膨胀系数(0.04879 = 4.879%) × 当前代币发行总量 × 上一次增发代币到现在的时长 / 一年的微秒数

年通胀系数为什么不是5%?简单来说,5%是每年只增发一次的年通胀率,但EOS采用的是无限次数增发的方式,因此,在这种增发方式下求出来的年通胀系数为4.879%,可使得EOS的年通胀率近似为5%。
设所有BP全年领取的总次数(即增发次数)n为无穷大,单次通胀率为x。那么在EOS初始总量为10亿,一年增发后数量为10.5亿的情况下,可得等式10乘以(1+x)的n次方等于10.5,求极限可得年通胀系数x乘以n等于ln1.05,即 0.04879。

const auto usecs_since_last_fill = ct - _gstate.last_pervote_bucket_fill;
auto new_tokens = continuous_rate(=0.04879) * double(token_supply.amount)  * double(usecs_since_last_fill)) / double(useconds_per_year) );

代币增发的频率没有限制。
增发的代币中的

  • 20%会用于奖励生产者(会根据指定生产者所占总量的占比来分配奖励)
    • 其中有25%的量按生产者的产块量奖励
    • 75%的量按生产者所收获的票数奖励
  • 80%存储在eosio.saving(用作WPS工作提案奖励用)。
auto to_producers       = new_tokens / 5;
auto to_savings         = new_tokens - to_producers;
auto to_per_block_pay   = to_producers / 4;
auto to_per_vote_pay    = to_producers - to_per_block_pay;

INLINE_ACTION_SENDER(eosio::token, issue)( N(eosio.token), {{N(eosio),N(active)}},
                                              {N(eosio), asset(new_tokens), std::string("issue tokens for producer pay and savings")} );
INLINE_ACTION_SENDER(eosio::token, transfer)( N(eosio.token), {N(eosio),N(active)},
                                              { N(eosio), N(eosio.saving), asset(to_savings), "unallocated inflation" } );
INLINE_ACTION_SENDER(eosio::token, transfer)( N(eosio.token), {N(eosio),N(active)},
                                              { N(eosio), N(eosio.bpay), asset(to_per_block_pay), "fund per-block bucket" } );
INLINE_ACTION_SENDER(eosio::token, transfer)( N(eosio.token), {N(eosio),N(active)},
                                              { N(eosio), N(eosio.vpay), asset(to_per_vote_pay), "fund per-vote bucket" } );

_gstate.pervote_bucket  += to_per_vote_pay;
_gstate.perblock_bucket += to_per_block_pay;
_gstate.last_pervote_bucket_fill = ct;

producer_per_block_pay = (_gstate.perblock_bucket * prod.unpaid_blocks) / _gstate.total_unpaid_blocks;
producer_per_vote_pay  = int64_t((_gstate.pervote_bucket * prod.total_votes ) / _gstate.total_producer_vote_weight);

INLINE_ACTION_SENDER(eosio::token, issue)( N(eosio.token), {{N(eosio),N(active)}},
                                          {N(eosio), asset(new_tokens), std::string("issue tokens for producer pay and savings")} );
INLINE_ACTION_SENDER(eosio::token, transfer)( N(eosio.token), {N(eosio),N(active)},
                                          { N(eosio), N(eosio.saving), asset(to_savings), "unallocated inflation" } );
INLINE_ACTION_SENDER(eosio::token, transfer)( N(eosio.token), {N(eosio),N(active)},
                                          { N(eosio), N(eosio.bpay), asset(to_per_block_pay), "fund per-block bucket" } );
INLINE_ACTION_SENDER(eosio::token, transfer)( N(eosio.token), {N(eosio),N(active)},
                                          { N(eosio), N(eosio.vpay), asset(to_per_vote_pay), "fund per-vote bucket" } );

_gstate.pervote_bucket      -= producer_per_vote_pay;
_gstate.perblock_bucket     -= producer_per_block_pay;
_gstate.total_unpaid_blocks -= prod.unpaid_blocks;

其他

区块链內会维护一个数据库,名为context.db,存有信息:

  • 区块链参数
  • 账户信息
  • 等等

你可能感兴趣的:(blockchain)