在bitshares学习过程中,有一些零散问题整理在一块,互相之间并不一定存在关联性,可能也会穿插一些非bitshares的内容,计划每几个问题做一集发表。
1. bitshares节点数据同步
看看bitshares节点怎么同步数据的?
在启动node节点时可以指定-seed-node和--seed-nodes参数,指明启动时要连接的p2p节点,也可以在config.ini文件中指定。
seed-node 指定节点会直接连接
seed-nodes 指定为"[]"时不会连接线上节点,如果不指定则会连接代码中指定的世界各地缺省节点。
代码处理部分在application.cpp中:
void reset_p2p_node(const fc::path& data_dir)
{ try {
_p2p_network = std::make_shared<net::node>("BitShares Reference Implementation");
...
}
node.cpp主要实现节点网络处理:
void node_impl::connect_to_p2p_network()
{
VERIFY_CORRECT_THREAD();
assert(_node_public_key != fc::ecc::public_key_data());
assert(!_accept_loop_complete.valid() &&
!_p2p_network_connect_loop_done.valid() &&
!_fetch_sync_items_loop_done.valid() &&
!_fetch_item_loop_done.valid() &&
!_advertise_inventory_loop_done.valid() &&
!_terminate_inactive_connections_loop_done.valid() &&
!_fetch_updated_peer_lists_loop_done.valid() &&
!_bandwidth_monitor_loop_done.valid() &&
!_dump_node_status_task_done.valid());
if (_node_configuration.accept_incoming_connections)
_accept_loop_complete = fc::async( [=](){ accept_loop(); }, "accept_loop");
_p2p_network_connect_loop_done = fc::async( [=]() { p2p_network_connect_loop(); }, "p2p_network_connect_loop" );
_fetch_sync_items_loop_done = fc::async( [=]() { fetch_sync_items_loop(); }, "fetch_sync_items_loop" );
_fetch_item_loop_done = fc::async( [=]() { fetch_items_loop(); }, "fetch_items_loop" );
_advertise_inventory_loop_done = fc::async( [=]() { advertise_inventory_loop(); }, "advertise_inventory_loop" );
_terminate_inactive_connections_loop_done = fc::async( [=]() { terminate_inactive_connections_loop(); }, "terminate_inactive_connections_loop" );
_fetch_updated_peer_lists_loop_done = fc::async([=](){ fetch_updated_peer_lists_loop(); }, "fetch_updated_peer_lists_loop");
_bandwidth_monitor_loop_done = fc::async([=](){ bandwidth_monitor_loop(); }, "bandwidth_monitor_loop");
_dump_node_status_task_done = fc::async([=](){ dump_node_status_task(); }, "dump_node_status_task");
}
其它节点连上来时会取得此节点所知道的节点信息吗?
void node_impl::fetch_updated_peer_lists_loop()
{
VERIFY_CORRECT_THREAD();
std::list<peer_connection_ptr> original_active_peers(_active_connections.begin(), _active_connections.end());
for( const peer_connection_ptr& active_peer : original_active_peers )
{
try
{
active_peer->send_message(address_request_message());
}
catch ( const fc::canceled_exception& )
{
throw;
}
catch (const fc::exception& e)
{
dlog("Caught exception while sending address request message to peer ${peer} : ${e}",
("peer", active_peer->get_remote_endpoint())("e", e));
}
}
...
}
2. 怎么修改operation费用
- 初始可以在genesis.json文件中设置每个operation的费用,如下:
"initial_parameters": {
"current_fees": {
"parameters": [[
0,{
"fee": 2000000,
"price_per_kbyte": 1000000
}
],[
1,{
"fee": 500000
}
]
...
- 可以通过cli_wallet的提议修改,如下:
propose_fee_change "nathan" "2018-06-15T00:00:00" {"transfer":{"fee":12,"price_per_kbyte":2}} true
{
"ref_block_num": 30364,
"ref_block_prefix": 1398860509,
"expiration": "2018-05-31T03:26:35",
"operations": [[
22,{
"fee": {
"amount": 2000007,
"asset_id": "1.3.0"
},
"fee_paying_account": "1.2.17",
"expiration_time": "2018-10-01T00:00:00",
"proposed_ops": [{
"op": [
31,{
"fee": {
"amount": 100000,
"asset_id": "1.3.0"
},
"new_parameters": {
"current_fees": {
"parameters": [[
0,{
"fee": 12,
"price_per_kbyte": 2
}
再同意此次的提议即可。
3. 怎么修改全局参数?
在cli_wallet执行提议修改:
propose_parameter_change "nathan" "2018-06-15T00:00:00" {"block_interval":3} true
在修改全局参数和修改费用时都碰到以下错误:
Assert Exception: !o.review_period_seconds || fc::seconds(*o.review_period_seconds) < (o.expiration_time - d.head_block_time()): Proposal review period must be less than its overall lifetime.
这个是因为审查周期大于等于提议过期时间,当时测试时review_period_seconds为
"committee_proposal_review_period": 1209600,
也就是14天,所以提议过期时间得设为超过14天才行。当然提案都得有人同意才会生效。
4. 比特币每笔交易的最小数量?
0.00000546BTC
这也是Omni转帐时的缺省最小比特币数量。
为什么有限制?测试下的确有限制,认为数量太小就是DUST!
浪费0.00000374BTC测试一下,的确是最小转帐数量0.00000546BTC,看起来矿工费可以小于这个值也是我比特币转帐金额和矿工费都最少的一次交易了!
5. cli_wallet非得手动解锁吗?
在业务中许多时候需要用到cli_wallet,例如水龙头等通过cli_wallet调用,如果每次启动cli_wallet时都需要手动解锁,那还是很麻烦的,而且发现的确很多人是这么做的。
首先cli_wallet可以支持daemon运行,只要在启动时指定参数:--daemon
然后cli_wallet提供了判断是否锁定、解锁、锁定接口:
bool is_locked()
void lock()
void unlock(string password)
这样我们可以在通过rcp调用cli_wallet的程序中,先判断是否锁定,锁定了则解锁再操作,当然需要有钱包的密码。
6. 为什么在genesis.json中可以创建帐号带下划线而以后不可以?
cli_wallet是调用以下方法创建帐号:
signed_transaction create_account_with_brain_key(string brain_key, string account_name, string registrar_account, string referrer_account, bool broadcast)
signed_transaction register_account(string name, public_key_type owner, public_key_type active, string registrar_account, string referrer_account, uint32_t referrer_percent, bool broadcast)
"水龙头"也是调用 register_account 创建帐号。
wallet.cpp
signed_transaction register_account(string name,
public_key_type owner,
public_key_type active,
string registrar_account,
string referrer_account,
uint32_t referrer_percent,
bool broadcast = false)
{ try {
FC_ASSERT( !self.is_locked() );
FC_ASSERT( is_valid_name(name) );
account_create_operation account_create_op;
...
}
而create_account_with_brain_key是通过tx.validate()进行检测的。
创世区块初始化是直接应用operation,而不检查帐号有效性:
db_init.cpp
account_create_operation cop;
cop.name = asset.symbol + "-collateral-holder-" + std::to_string(collateral_holder_number);
boost::algorithm::to_lower(cop.name);
cop.registrar = GRAPHENE_TEMP_ACCOUNT;
cop.owner = authority(1, collateral_rec.owner, 1);
cop.active = cop.owner;
account_id_type owner_account_id = apply_operation(genesis_eval_state, cop).get<object_id_type>();
db_block.cpp
operation_result database::apply_operation(transaction_evaluation_state& eval_state, const operation& op)
{ try {
int i_which = op.which();
uint64_t u_which = uint64_t( i_which );
FC_ASSERT( i_which >= 0, "Negative operation tag in operation ${op}", ("op",op) );
FC_ASSERT( u_which < _operation_evaluators.size(), "No registered evaluator for operation ${op}", ("op",op) );
unique_ptr<op_evaluator>& eval = _operation_evaluators[ u_which ];
FC_ASSERT( eval, "No registered evaluator for operation ${op}", ("op",op) );
auto op_id = push_applied_operation( op );
auto result = eval->evaluate( eval_state, op, true );
set_applied_operation_result( op_id, result );
return result;
} FC_CAPTURE_AND_RETHROW( (op) ) }
7. 很多操作中的参数"AAAAA"是什么意思
在Bitshares中有许多api参数为 lowerbound,一般用"AAAAA"就可以取到数据,如list_assets,很多新人并不知道这个lowerbound是什么?看一下list_assets的代码:
vector<asset_object> database_api_impl::list_assets(const string& lower_bound_symbol, uint32_t limit)const
{
FC_ASSERT( limit <= 100 );
const auto& assets_by_symbol = _db.get_index_type<asset_index>().indices().get<by_symbol>();
vector<asset_object> result;
result.reserve(limit);
auto itr = assets_by_symbol.lower_bound(lower_bound_symbol);
if( lower_bound_symbol == "" )
itr = assets_by_symbol.begin();
while(limit-- && itr != assets_by_symbol.end())
result.emplace_back(*itr++);
return result;
}
从list_assets中可以看到,碰到这个lowerbound可以用""空参数来代替,会从第一个数据开始查找。
根据要找的数据字符位置,选择字符开始字母和个数,注意这个是区分大小写的。
8. 帐户为什么有多个密钥对
Bitshares的帐户有OwnerKey、ActiveKey、MemoKey
- Owner权限 控制帐户
- Active权限 控制资金
- Memo权限 读取备注
Owner和Active可以有多个密钥对,而Memo只支持一个密钥对。
Onwer权限和Active权限通过多个密钥对、权重、阀值做多重签名,那Memo权限为什么不用多个密钥对而只有一个呢?因为Memo需要发送方和接收方一起加密数据。
看看Memo怎么做到双方可以读取数据的,诀窍全在memo.cpp中:
void memo_data::set_message(const fc::ecc::private_key& priv, const fc::ecc::public_key& pub,
const string& msg, uint64_t custom_nonce)
{
if( priv != fc::ecc::private_key() && public_key_type(pub) != public_key_type() )
{
from = priv.get_public_key();
to = pub;
if( custom_nonce == 0 )
{
uint64_t entropy = fc::sha224::hash(fc::ecc::private_key::generate())._hash[0];
entropy <<= 32;
entropy &= 0xff00000000000000;
nonce = (fc::time_point::now().time_since_epoch().count() & 0x00ffffffffffffff) | entropy;
} else
nonce = custom_nonce;
auto secret = priv.get_shared_secret(pub);
auto nonce_plus_secret = fc::sha512::hash(fc::to_string(nonce) + secret.str());
string text = memo_message(digest_type::hash(msg)._hash[0], msg).serialize();
message = fc::aes_encrypt( nonce_plus_secret, vector<char>(text.begin(), text.end()) );
}
else
{
auto text = memo_message(0, msg).serialize();
message = vector<char>(text.begin(), text.end());
}
}
string memo_data::get_message(const fc::ecc::private_key& priv,
const fc::ecc::public_key& pub)const
{
if( from != public_key_type() )
{
auto secret = priv.get_shared_secret(pub);
auto nonce_plus_secret = fc::sha512::hash(fc::to_string(nonce) + secret.str());
auto plain_text = fc::aes_decrypt( nonce_plus_secret, message );
auto result = memo_message::deserialize(string(plain_text.begin(), plain_text.end()));
FC_ASSERT( result.checksum == uint32_t(digest_type::hash(result.text)._hash[0]) );
return result.text;
}
else
{
return memo_message::deserialize(string(message.begin(), message.end())).text;
}
}
string memo_message::serialize() const
{
auto serial_checksum = string(sizeof(checksum), ' ');
(uint32_t&)(*serial_checksum.data()) = checksum;
return serial_checksum + text;
}
memo_message memo_message::deserialize(const string& serial)
{
memo_message result;
FC_ASSERT( serial.size() >= sizeof(result.checksum) );
result.checksum = ((uint32_t&)(*serial.data()));
result.text = serial.substr(sizeof(result.checksum));
return result;
}
实际上Memo的加密是用aes加密,但加密时的密钥是通过“发送方的私钥和接收方的公钥” 获得的,也就是上面代码的 priv.get_shared_secret(pub)。
参考
https://www.peerplays.com/wp-content/uploads/2017/11/AdvisorRole_PeerplaysBlockchain.pdf
感谢您阅读 @chaimyu 的帖子,期待您能留言交流!