提交 133442b6 编写于 作者: B Bart Wyatt

Add naive scheduler to block production

add basic tests for scheduler
上级 867a5380
......@@ -4,6 +4,7 @@ file(GLOB HEADERS "include/eos/chain/*.hpp")
add_library( eos_chain
chain_controller.cpp
wasm_interface.cpp
block_schedule.cpp
fork_database.cpp
......
/*
* Copyright (c) 2017, Respective Authors.
*
* The MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include <eos/chain/block_schedule.hpp>
namespace eos { namespace chain {
static uint next_power_of_two(uint input) {
if (input == 0) {
return 0;
}
uint result = input;
result--;
result |= result >> 1;
result |= result >> 2;
result |= result >> 4;
result |= result >> 8;
result |= result >> 16;
result++;
return result;
};
typedef std::hash<decltype(AccountName::value)> account_hash;
static account_hash account_hasher;
struct schedule_entry {
schedule_entry(uint _cycle, uint _thread, SignedTransaction const * _transaction)
: cycle(_cycle)
, thread(_thread)
, transaction(_transaction)
{}
uint cycle;
uint thread;
SignedTransaction const *transaction;
friend bool operator<( schedule_entry const &l, schedule_entry const &r ) {
if (l.cycle < r.cycle) {
return true;
} else if (l.cycle == r.cycle) {
return l.thread < r.thread;
}
return false;
}
};
static block_schedule from_entries(vector<schedule_entry> &entries) {
// sort in reverse to save allocations, this should put the highest thread index
// for the highest cycle index first meaning the naive resize in the loop below
// is usually the largest and only resize
auto reverse = [](schedule_entry const &l, schedule_entry const &r) {
return !(l < r);
};
std::sort(entries.begin(), entries.end(), reverse);
block_schedule result;
for(auto const & entry : entries) {
if (result.cycles.size() <= entry.cycle) {
result.cycles.resize(entry.cycle + 1);
}
auto &cycle = result.cycles.at(entry.cycle);
if (cycle.size() <= entry.thread) {
cycle.resize(entry.thread + 1);
}
// because we are traversing the schedule in reverse to save
// allocations, we cannot emplace_back as that would reverse
// the transactions in a thread
auto &thread = cycle.at(entry.thread);
thread.user_input.emplace(thread.user_input.begin(), entry.transaction);
}
return result;
}
template<typename CONTAINER>
auto initialize_pointer_vector(CONTAINER const &c) {
vector<SignedTransaction const *> result;
result.reserve(c.size());
for (auto const &t : c) {
result.emplace_back(&t);
}
return result;
}
struct block_size_skipper {
size_t current_size;
size_t const max_size;
bool should_skip(SignedTransaction const *t) const {
size_t transaction_size = fc::raw::pack_size(*t);
// postpone transaction if it would make block too big
if( transaction_size + current_size > max_size ) {
return true;
} else {
return false;
}
}
void apply(SignedTransaction const *t) {
size_t transaction_size = fc::raw::pack_size(*t);
current_size += transaction_size;
}
};
auto make_skipper(const global_property_object &properties) {
static const size_t max_block_header_size = fc::raw::pack_size( signed_block_header() ) + 4;
auto maximum_block_size = properties.configuration.maxBlockSize;
return block_size_skipper { max_block_header_size, (size_t)maximum_block_size };
}
block_schedule block_schedule::by_threading_conflicts(
deque<SignedTransaction> const &transactions,
const global_property_object &properties
)
{
static uint const MAX_TXS_PER_THREAD = 4;
auto skipper = make_skipper(properties);
uint HASH_SIZE = std::max<uint>(4096, next_power_of_two(transactions.size() / 8));
vector<optional<uint>> assigned_threads(HASH_SIZE);
vector<schedule_entry> schedule;
schedule.reserve(transactions.size());
auto current = initialize_pointer_vector(transactions);
vector<SignedTransaction const *> postponed;
postponed.reserve(transactions.size());
vector<uint> txs_per_thread;
txs_per_thread.reserve(HASH_SIZE);
int cycle = 0;
bool scheduled = true;
while (scheduled) {
scheduled = false;
uint next_thread = 0;
for (auto t : current) {
// skip ?
if (skipper.should_skip(t)) {
continue;
}
auto assigned_to = optional<uint>();
bool postpone = false;
for (const auto &a : t->scope) {
uint hash_index = account_hasher(a) % HASH_SIZE;
if (assigned_to && assigned_threads[hash_index] && assigned_to != assigned_threads[hash_index]) {
postpone = true;
postponed.push_back(t);
break;
}
if (assigned_threads[hash_index])
{
assigned_to = assigned_threads[hash_index];
}
}
if (!postpone) {
if (!assigned_to) {
assigned_to = next_thread++;
txs_per_thread.resize(next_thread);
}
if (txs_per_thread[*assigned_to] < MAX_TXS_PER_THREAD) {
for (const auto &a : t->scope)
{
uint hash_index = account_hasher(a) % HASH_SIZE;
assigned_threads[hash_index] = assigned_to;
}
txs_per_thread[*assigned_to]++;
schedule.emplace_back(cycle, *assigned_to, t);
scheduled = true;
skipper.apply(t);
} else {
postponed.push_back(t);
}
}
}
current.resize(0);
txs_per_thread.resize(0);
assigned_threads.resize(0);
assigned_threads.resize(HASH_SIZE);
std::swap(current, postponed);
++cycle;
}
return from_entries(schedule);
}
block_schedule block_schedule::by_cycling_conflicts(
deque<SignedTransaction> const &transactions,
const global_property_object &properties
)
{
auto skipper = make_skipper(properties);
uint HASH_SIZE = std::max<uint>(4096, next_power_of_two(transactions.size() / 8));
vector<schedule_entry> schedule;
schedule.reserve(transactions.size());
auto current = initialize_pointer_vector(transactions);
vector<SignedTransaction const *> postponed;
postponed.reserve(transactions.size());
int cycle = 0;
vector<bool> used(HASH_SIZE);
bool scheduled = true;
while (scheduled) {
scheduled = false;
int thread = 0;
for (auto t : current) {
// skip ?
if (skipper.should_skip(t)) {
continue;
}
bool u = false;
for (const auto &a : t->scope) {
uint hash_index = account_hasher(a) % HASH_SIZE;
if (used[hash_index]) {
u = true;
postponed.push_back(t);
break;
}
}
if (!u) {
for (const auto &a : t->scope) {
uint hash_index = account_hasher(a) % HASH_SIZE;
used[hash_index] = true;
}
schedule.emplace_back(cycle, thread++, t);
scheduled = true;
skipper.apply(t);
}
}
current.resize(0);
used.resize(0);
used.resize(HASH_SIZE);
std::swap(current, postponed);
++cycle;
}
return from_entries(schedule);
}
} /* namespace chain */ } /* namespace eos */
......@@ -26,6 +26,7 @@
#include <eos/chain/exceptions.hpp>
#include <eos/chain/block_summary_object.hpp>
#include <eos/chain/block_schedule.hpp>
#include <eos/chain/global_property_object.hpp>
#include <eos/chain/key_value_object.hpp>
#include <eos/chain/action_objects.hpp>
......@@ -292,11 +293,7 @@ signed_block chain_controller::_generate_block(
if( !(skip & skip_producer_signature) )
FC_ASSERT( producer_obj.signing_key == block_signing_private_key.get_public_key() );
static const size_t max_block_header_size = fc::raw::pack_size( signed_block_header() ) + 4;
auto maximum_block_size = get_global_properties().configuration.maxBlockSize;
size_t total_block_size = max_block_header_size;
signed_block pending_block;
auto schedule = block_schedule::by_threading_conflicts(_pending_transactions, get_global_properties());
//
// The following code throws away existing pending_tx_session and
......@@ -312,45 +309,74 @@ signed_block chain_controller::_generate_block(
_pending_tx_session.reset();
_pending_tx_session = _db.start_undo_session(true);
uint64_t postponed_tx_count = 0;
// pop pending state (reset to head block state)
for( const SignedTransaction& tx : _pending_transactions )
{
size_t new_total_size = total_block_size + fc::raw::pack_size( tx );
// postpone transaction if it would make block too big
if( new_total_size >= maximum_block_size )
{
postponed_tx_count++;
continue;
}
auto process_one = [this](SignedTransaction const *tx, database &db) -> optional<ProcessedTransaction> {
try
{
auto temp_session = _db.start_undo_session(true);
_apply_transaction(tx);
auto temp_session = db.start_undo_session(true);
auto ptx = _apply_transaction(*tx);
temp_session.squash();
total_block_size += fc::raw::pack_size(tx);
if (pending_block.cycles.empty()) {
pending_block.cycles.resize(1);
pending_block.cycles.back().resize(1);
}
pending_block.cycles.back().back().user_input.emplace_back(tx);
#warning TODO: Populate generated blocks with generated transactions
return optional<ProcessedTransaction>(ptx);
}
catch ( const fc::exception& e )
{
// Do nothing, transaction will not be re-applied
wlog( "Transaction was not processed while generating block due to ${e}", ("e", e) );
wlog( "The transaction was ${t}", ("t", tx) );
wlog( "The transaction was ${t}", ("t", *tx) );
}
return optional<ProcessedTransaction>();
};
signed_block pending_block;
pending_block.cycles.reserve(schedule.cycles.size());
size_t invalid_transaction_count = 0;
size_t valid_transaction_count = 0;
for (const auto &c : schedule.cycles) {
cycle block_cycle;
block_cycle.reserve(c.size());
for (const auto &t : c) {
thread block_thread;
block_thread.generated_input.reserve(t.generated_input.size());
for (const auto &trx : t.generated_input) {
#warning TODO: Process generated transaction
}
block_thread.user_input.reserve(t.user_input.size());
for (const auto &trx : t.user_input) {
auto processed = process_one(trx, _db);
if (processed) {
block_thread.user_input.emplace_back(*processed);
valid_transaction_count++;
} else {
invalid_transaction_count++;
}
}
if (!(block_thread.generated_input.empty() && block_thread.user_input.empty())) {
block_cycle.emplace_back(std::move(block_thread));
}
}
if (!block_cycle.empty()) {
pending_block.cycles.emplace_back(std::move(block_cycle));
}
}
size_t postponed_tx_count = _pending_transactions.size() - valid_transaction_count - invalid_transaction_count;
if( postponed_tx_count > 0 )
{
wlog( "Postponed ${n} transactions due to block size limit", ("n", postponed_tx_count) );
}
if( invalid_transaction_count > 0 )
{
wlog( "Postponed ${n} transactions errors when processing", ("n", invalid_transaction_count) );
}
_pending_tx_session.reset();
// We have temporarily broken the invariant that
......
/*
* Copyright (c) 2017, Respective Authors.
*
* The MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#pragma once
#include <eos/chain/chain_controller.hpp>
#include <eos/chain/transaction.hpp>
namespace eos { namespace chain {
struct thread_schedule {
vector<generated_transaction_id_type> generated_input;
vector<SignedTransaction const *> user_input;
};
using cycle_schedule = vector<thread_schedule>;
/**
* @class block_schedule
* @brief represents a proposed order of execution for a generated block
*/
struct block_schedule
{
vector<cycle_schedule> cycles;
// Algorithms
/**
* A greedy scheduler that attempts to make short threads to resolve scope contention before
* falling back on cycles
* @return the block scheduler
*/
static block_schedule by_threading_conflicts(deque<SignedTransaction> const &transactions, const global_property_object& properties);
/**
* A greedy scheduler that attempts uses future cycles to resolve scope contention
* @return the block scheduler
*/
static block_schedule by_cycling_conflicts(deque<SignedTransaction> const &transactions, const global_property_object& properties);
};
} } // eos::chain
FC_REFLECT(eos::chain::thread_schedule, (generated_input)(user_input) )
FC_REFLECT(eos::chain::block_schedule, (cycles))
/*
* Copyright (c) 2017, Respective Authors.
*
* The MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#pragma once
namespace eos { namespace test {
template<typename PRED, typename EVAL, typename T>
struct expector {
expector(EVAL _eval, T const &_expected, char const * const _msg)
: expected(_expected)
, eval(_eval)
, msg(_msg)
{}
template<typename INPUT>
void operator() (INPUT const &input) {
BOOST_TEST_INFO(msg);
auto actual = eval(input);
BOOST_CHECK_EQUAL(PRED()(actual, expected), true);
}
T expected;
EVAL eval;
char const * const msg;
};
template<typename PRED, typename EVAL, typename T>
auto expect(EVAL &&eval, T expected, char const * const msg) {
return expector<PRED, EVAL, T>(eval, expected, msg);
}
template<typename EVAL, typename T>
auto expect(EVAL &&eval, T expected, char const * const msg) {
return expector<std::equal_to<T>, EVAL, T>(eval, expected, msg);
}
template<typename EVAL>
auto expect(EVAL &&eval, char const * const msg) {
return expector<std::equal_to<bool>, EVAL, bool>(eval, true, msg);
}
#define _NUM_ARGS(A,B,C,N, ...) N
#define NUM_ARGS(...) _NUM_ARGS(__VA_ARGS__, 3, 2, 1)
#define EXPECT(...) EXPECT_(NUM_ARGS(__VA_ARGS__),__VA_ARGS__)
#define EXPECT_(N, ...) EXPECT__(N, __VA_ARGS__)
#define EXPECT__(N, ...) EXPECT_##N(__VA_ARGS__)
#define EXPECT_3(P, E, T) eos::test::expect<P>(E,T,"EXPECT<" #P ">(" #E "," #T ")")
#define EXPECT_2(E, T) eos::test::expect(E,T,"EXPECT(" #E "," #T ")")
#define EXPECT_1(E) eos::test::expect(E,"EXPECT(" #E ")")
}}
\ No newline at end of file
/*
* Copyright (c) 2017, Respective Authors.
*
* The MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include <boost/test/unit_test.hpp>
#include <eos/chain/block_schedule.hpp>
#include "../common/expect.hpp"
using namespace eos;
using namespace chain;
/*
* Policy based Fixtures for chain properties
*/
class common_fixture {
public:
struct test_transaction {
test_transaction(std::initializer_list<AccountName> const &_scopes)
: scopes(_scopes)
{
}
std::initializer_list<AccountName> const &scopes;
};
protected:
auto create_pending( std::initializer_list<test_transaction> const &transactions ) {
std::deque<SignedTransaction> result;
for (auto const &t: transactions) {
SignedTransaction st;
st.scope.reserve(t.scopes.size());
st.scope.insert(st.scope.end(), t.scopes.begin(), t.scopes.end());
result.emplace_back(st);
}
return result;
}
};
template<typename PROPERTIES_POLICY>
class compose_fixture: public common_fixture {
public:
template<typename SCHED_FN, typename ...VALIDATORS>
void schedule_and_validate(SCHED_FN sched_fn, std::initializer_list<test_transaction> const &transactions, VALIDATORS ...validators) {
try {
auto pending = create_pending(transactions);
auto schedule = sched_fn(pending, properties_policy.properties);
validate(schedule, validators...);
} FC_LOG_AND_RETHROW()
}
private:
template<typename VALIDATOR>
void validate(block_schedule const &schedule, VALIDATOR validator) {
validator(schedule);
}
template<typename VALIDATOR, typename ...VALIDATORS>
void validate(block_schedule const &schedule, VALIDATOR validator, VALIDATORS ... others) {
validate(schedule, validator);
validate(schedule, others...);
}
PROPERTIES_POLICY properties_policy;
};
static void null_global_property_object_constructor(global_property_object const &)
{}
static chainbase::allocator<global_property_object> null_global_property_object_allocator(nullptr);
struct base_properties {
base_properties()
: properties(null_global_property_object_constructor, null_global_property_object_allocator)
{
}
global_property_object properties;
};
struct default_properties : public base_properties {
default_properties() {
properties.configuration.maxBlockSize = 256 * 1024;
}
};
struct small_block_properties : public base_properties {
small_block_properties() {
properties.configuration.maxBlockSize = 512;
}
};
typedef compose_fixture<default_properties> default_fixture;
/*
* Evaluators for expect
*/
static uint transaction_count(block_schedule const &schedule) {
uint result = 0;
for (auto const &c : schedule.cycles) {
for (auto const &t: c) {
result += t.generated_input.size();
result += t.user_input.size();
}
}
return result;
}
static uint cycle_count(block_schedule const &schedule) {
return schedule.cycles.size();
}
static bool schedule_is_valid(block_schedule const &schedule) {
for (auto const &c : schedule.cycles) {
std::vector<bool> scope_in_use;
for (auto const &t: c) {
std::set<size_t> thread_bits;
size_t max_bit = 0;
for(auto const &gi: t.generated_input) {
#warning TODO: Process generated transaction
}
for(auto const &ui: t.user_input) {
for (auto const &s : ui->scope) {
size_t bit = boost::numeric_cast<size_t>((uint64_t)s);
thread_bits.emplace(bit);
max_bit = std::max(max_bit, bit);
}
}
if (scope_in_use.size() <= max_bit) {
scope_in_use.resize(max_bit + 1);
}
for ( auto b: thread_bits ) {
if(scope_in_use.at(b)) {
return false;
};
scope_in_use.at(b) = true;
}
}
}
return true;
}
/*
* Test Cases
*/
BOOST_AUTO_TEST_SUITE(block_schedule_tests)
BOOST_FIXTURE_TEST_CASE(no_conflicts, default_fixture) {
// ensure the scheduler can handle basic non-conflicted transactions
// in a single cycle
schedule_and_validate(
block_schedule::by_threading_conflicts,
{
{0x1ULL, 0x2ULL},
{0x3ULL, 0x4ULL},
{0x5ULL, 0x6ULL},
{0x7ULL, 0x8ULL},
{0x9ULL, 0xAULL}
},
EXPECT(schedule_is_valid),
EXPECT(transaction_count, 5),
EXPECT(cycle_count, 1)
);
}
BOOST_FIXTURE_TEST_CASE(some_conflicts, default_fixture) {
// ensure the scheduler can handle conflicted transactions
// using multiple cycles
schedule_and_validate(
block_schedule::by_threading_conflicts,
{
{0x1ULL, 0x2ULL},
{0x3ULL, 0x2ULL},
{0x5ULL, 0x1ULL},
{0x7ULL, 0x1ULL},
{0x1ULL, 0x7ULL}
},
EXPECT(schedule_is_valid),
EXPECT(transaction_count, 5),
EXPECT(std::greater<uint>, cycle_count, 1)
);
}
BOOST_FIXTURE_TEST_CASE(basic_cycle, default_fixture) {
// ensure the scheduler can handle a basic scope cycle
schedule_and_validate(
block_schedule::by_threading_conflicts,
{
{0x1ULL, 0x2ULL},
{0x2ULL, 0x3ULL},
{0x3ULL, 0x1ULL},
},
EXPECT(schedule_is_valid),
EXPECT(transaction_count, 3)
);
}
BOOST_FIXTURE_TEST_CASE(small_block, compose_fixture<small_block_properties>) {
// ensure the scheduler can handle basic block size restrictions
schedule_and_validate(
block_schedule::by_threading_conflicts,
{
{0x1ULL, 0x2ULL},
{0x3ULL, 0x4ULL},
{0x5ULL, 0x6ULL},
{0x7ULL, 0x8ULL},
{0x9ULL, 0xAULL},
{0xBULL, 0xCULL},
{0xDULL, 0xEULL},
{0x11ULL, 0x12ULL},
{0x13ULL, 0x14ULL},
{0x15ULL, 0x16ULL},
{0x17ULL, 0x18ULL},
{0x19ULL, 0x1AULL},
{0x1BULL, 0x1CULL},
{0x1DULL, 0x1EULL},
{0x21ULL, 0x22ULL},
{0x23ULL, 0x24ULL},
{0x25ULL, 0x26ULL},
{0x27ULL, 0x28ULL},
{0x29ULL, 0x2AULL},
{0x2BULL, 0x2CULL},
{0x2DULL, 0x2EULL},
},
EXPECT(schedule_is_valid),
EXPECT(std::less<uint>, transaction_count, 21)
);
}
BOOST_AUTO_TEST_SUITE_END()
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册