diff --git a/libraries/chain/chain_controller.cpp b/libraries/chain/chain_controller.cpp index 13713d9e038..6f269f2694b 100644 --- a/libraries/chain/chain_controller.cpp +++ b/libraries/chain/chain_controller.cpp @@ -859,6 +859,7 @@ chain_controller::chain_controller(database& database, fork_database& fork_db, b }(); initialize_indexes(); + starter.register_types(*this, _db); initialize_chain(starter); spinup_db(); spinup_fork_db(); diff --git a/libraries/chain/include/eos/chain/chain_administration_interface.hpp b/libraries/chain/include/eos/chain/chain_administration_interface.hpp index 41300c35dd2..35143c77140 100644 --- a/libraries/chain/include/eos/chain/chain_administration_interface.hpp +++ b/libraries/chain/include/eos/chain/chain_administration_interface.hpp @@ -22,7 +22,12 @@ class chain_administration_interface { public: virtual ~chain_administration_interface(); - virtual ProducerRound get_next_round(const chainbase::database& db) = 0; + /** + * @brief Calculate the next round of block producers and return it + * @param db The current blockchain database state. Private state or block producer scheduling may be modiied. + * @return The next round of block producers, sorted by owner name + */ + virtual ProducerRound get_next_round(chainbase::database& db) = 0; virtual BlockchainConfiguration get_blockchain_configuration(const chainbase::database& db, const ProducerRound& round) = 0; }; diff --git a/libraries/chain/include/eos/chain/chain_controller.hpp b/libraries/chain/include/eos/chain/chain_controller.hpp index 5f377c83b0e..5a02ae1f9ff 100644 --- a/libraries/chain/include/eos/chain/chain_controller.hpp +++ b/libraries/chain/include/eos/chain/chain_controller.hpp @@ -252,10 +252,6 @@ namespace eos { namespace chain { void apply_debug_updates(); void debug_update(const fc::variant_object& update); - // these were formerly private, but they have a fairly well-defined API, so let's make them public - void apply_block(const signed_block& next_block, uint32_t skip = skip_nothing); - void apply_transaction(const SignedTransaction& trx, uint32_t skip = skip_nothing); - protected: const chainbase::database& get_database() const { return _db; } @@ -266,6 +262,8 @@ namespace eos { namespace chain { void replay(); + void apply_block(const signed_block& next_block, uint32_t skip = skip_nothing); + void apply_transaction(const SignedTransaction& trx, uint32_t skip = skip_nothing); void _apply_block(const signed_block& next_block); void _apply_transaction(const SignedTransaction& trx); diff --git a/libraries/chain/include/eos/chain/chain_initializer_interface.hpp b/libraries/chain/include/eos/chain/chain_initializer_interface.hpp index 9030d047dc7..56a7e0f2840 100644 --- a/libraries/chain/include/eos/chain/chain_initializer_interface.hpp +++ b/libraries/chain/include/eos/chain/chain_initializer_interface.hpp @@ -24,27 +24,36 @@ class chain_initializer_interface { /// Retrieve the first round of block producers virtual std::array get_chain_start_producers() = 0; + /** + * @brief Install necessary indices and message handlers that chain_controller doesn't know about + * + * This method is called every time the chain_controller is initialized, before any chain state is read or written, + * regardless of whether the chain is new or not. + * + * This method may perform any necessary initializations on the chain and/or database, such as installing indices + * and message handlers that should be defined before the first block is processed. This may be necessary in order + * for the list of messages returned by @ref initialize_database to be processed successfully. + */ + virtual void register_types(chain_controller& chain, chainbase::database& db) = 0; /** * @brief Prepare the database, creating objects and defining state which should exist before the first block * @param chain A reference to the @ref chain_controller * @param db A reference to the @ref chainbase::database * @return A list of @ref Message "Messages" to be applied before the first block * + * This method is only called when starting a new blockchain. It is called at the end of chain initialization, after + * setting the state used in core chain operations. + * * This method creates the @ref account_object "account_objects" and @ref producer_object "producer_objects" for * at least the initial block producers returned by @ref get_chain_start_producers * * This method also provides an opportunity to create objects and setup the database to the state it should be in - * prior to the first block. This method should only initialize state that the @ref chain_controller itself does - * not understand. The other methods on @ref chain_initializer are called to retrieve the data necessary to - * initialize chain state the controller does understand, including the initial round of block producers and the - * initial @ref BlockchainConfiguration. - * - * Finally, this method may perform any necessary initializations on the chain and/or database, such as - * installing indexes and message handlers that should be defined before the first block is processed. This may - * be necessary in order for the returned list of messages to be processed successfully. + * prior to the first block, including registering any message types unknown to @ref chain_controller. This method + * should only initialize state that the @ref chain_controller itself does not understand. * - * This method is called at the end of chain initialization, after setting the state used in core chain - * operations. + * The other methods on @ref chain_initializer_interface are called to retrieve the data necessary to initialize + * chain state the controller does understand, including the initial round of block producers, the initial chain + * time, and the initial @ref BlockchainConfiguration. */ virtual vector prepare_database(chain_controller& chain, chainbase::database& db) = 0; }; diff --git a/libraries/chain/include/eos/chain/types.hpp b/libraries/chain/include/eos/chain/types.hpp index de183cd55b0..0c47d79cf0a 100644 --- a/libraries/chain/include/eos/chain/types.hpp +++ b/libraries/chain/include/eos/chain/types.hpp @@ -161,6 +161,7 @@ namespace eos { namespace chain { balance_object_type, ///< Defined by native_system_contract_plugin staked_balance_object_type, ///< Defined by native_system_contract_plugin producer_votes_object_type, ///< Defined by native_system_contract_plugin + producer_schedule_object_type, ///< Defined by native_system_contract_plugin OBJECT_TYPE_COUNT ///< Sentry value which contains the number of different object types }; @@ -201,6 +202,7 @@ FC_REFLECT_ENUM(eos::chain::object_type, (balance_object_type) (staked_balance_object_type) (producer_votes_object_type) + (producer_schedule_object_type) (OBJECT_TYPE_COUNT) ) FC_REFLECT( eos::chain::void_t, ) diff --git a/libraries/native_contract/include/eos/native_contract/native_contract_chain_administrator.hpp b/libraries/native_contract/include/eos/native_contract/native_contract_chain_administrator.hpp index 68eb3d11860..147acb2262d 100644 --- a/libraries/native_contract/include/eos/native_contract/native_contract_chain_administrator.hpp +++ b/libraries/native_contract/include/eos/native_contract/native_contract_chain_administrator.hpp @@ -6,7 +6,7 @@ namespace eos { namespace native_contract { using chain::ProducerRound; class native_contract_chain_administrator : public chain::chain_administration_interface { - ProducerRound get_next_round(const chainbase::database& db); + ProducerRound get_next_round(chainbase::database& db); chain::BlockchainConfiguration get_blockchain_configuration(const chainbase::database& db, const ProducerRound& round); }; diff --git a/libraries/native_contract/include/eos/native_contract/native_contract_chain_initializer.hpp b/libraries/native_contract/include/eos/native_contract/native_contract_chain_initializer.hpp index 4b6dc58e90a..a9b71e02ddb 100644 --- a/libraries/native_contract/include/eos/native_contract/native_contract_chain_initializer.hpp +++ b/libraries/native_contract/include/eos/native_contract/native_contract_chain_initializer.hpp @@ -12,10 +12,13 @@ class native_contract_chain_initializer : public chain::chain_initializer_interf native_contract_chain_initializer(const genesis_state_type& genesis) : genesis(genesis) {} virtual ~native_contract_chain_initializer() {} - virtual std::vector prepare_database(chain::chain_controller& chain, chainbase::database& db); - virtual types::Time get_chain_start_time(); - virtual chain::BlockchainConfiguration get_chain_start_configuration(); - virtual std::array get_chain_start_producers(); + virtual types::Time get_chain_start_time() override; + virtual chain::BlockchainConfiguration get_chain_start_configuration() override; + virtual std::array get_chain_start_producers() override; + + virtual void register_types(chain::chain_controller& chain, chainbase::database& db) override; + virtual std::vector prepare_database(chain::chain_controller& chain, + chainbase::database& db) override; }; } } // namespace eos::native_contract diff --git a/libraries/native_contract/include/eos/native_contract/producer_objects.hpp b/libraries/native_contract/include/eos/native_contract/producer_objects.hpp index 50dd39d36b9..1fa5974ef0f 100644 --- a/libraries/native_contract/include/eos/native_contract/producer_objects.hpp +++ b/libraries/native_contract/include/eos/native_contract/producer_objects.hpp @@ -5,12 +5,16 @@ #include +#include + #include #include namespace eos { +FC_DECLARE_EXCEPTION(ProducerRaceOverflowException, 10000000, "Producer Virtual Race time has overflowed"); + /** * @brief The ProducerVotesObject class tracks all votes for and by the block producers * @@ -65,18 +69,67 @@ class ProducerVotesObject : public chainbase::object::max(); + + /// Set all fields on race, given the current speed, position, and time + void update(types::ShareType currentSpeed, types::UInt128 currentPosition, types::UInt128 currentRaceTime) { + speed = currentSpeed; + position = currentPosition; + positionUpdateTime = currentRaceTime; + auto distanceRemaining = config::ProducerRaceLapLength - position; + auto projectedTimeToFinish = speed > 0? distanceRemaining / speed + : std::numeric_limits::max(); + EOS_ASSERT(currentRaceTime <= std::numeric_limits::max() - projectedTimeToFinish, + ProducerRaceOverflowException, "Producer race time has overflowed", + ("currentTime", currentRaceTime)("timeToFinish", projectedTimeToFinish)("limit", std::numeric_limits::max())); + projectedFinishTime = currentRaceTime + projectedTimeToFinish; + } } race; + void startNewRaceLap(types::UInt128 currentRaceTime) { race.update(race.speed, 0, currentRaceTime); } types::UInt128 projectedRaceFinishTime() const { return race.projectedFinishTime; } }; +/** + * @brief The ProducerScheduleObject class schedules producers into rounds + * + * This class stores the state of the virtual race to select runner-up producers, and provides the logic for selecting + * a round of producers. + * + * This is a singleton object within the database; there will only be one stored. + */ +class ProducerScheduleObject : public chainbase::object { + OBJECT_CTOR(ProducerScheduleObject) + + id_type id; + types::UInt128 currentRaceTime = 0; + + /// Retrieve a reference to the ProducerScheduleObject stored in the provided database + static const ProducerScheduleObject& get(const chainbase::database& db) { return db.get(id_type()); } + + /** + * @brief Calculate the next round of block producers + * @param db The blockchain database + * @return The next round of block producers, sorted by owner name + * + * This method calculates the next round of block producers according to votes and the virtual race for runner-up + * producers. Although it is a const method, it will use its non-const db parameter to update its own records, as + * well as the race records stored in the @ref ProducerVotesObjects + */ + chain::ProducerRound calculateNextRound(chainbase::database& db) const; + + /** + * @brief Reset all producers in the virtual race to the starting line, and reset virtual time to zero + */ + void resetProducerRace(chainbase::database& db) const; +}; + using boost::multi_index::const_mem_fun; /// Index producers by their owner's name struct byOwnerName; @@ -104,6 +157,16 @@ using ProducerVotesMultiIndex = chainbase::shared_multi_index_container< > >; +using ProducerScheduleMultiIndex = chainbase::shared_multi_index_container< + ProducerScheduleObject, + indexed_by< + ordered_unique, + member + > + > +>; + } // namespace eos CHAINBASE_SET_INDEX_TYPE(eos::ProducerVotesObject, eos::ProducerVotesMultiIndex) +CHAINBASE_SET_INDEX_TYPE(eos::ProducerScheduleObject, eos::ProducerScheduleMultiIndex) diff --git a/libraries/native_contract/native_contract_chain_administrator.cpp b/libraries/native_contract/native_contract_chain_administrator.cpp index a397a0746a1..fffd16d3dcc 100644 --- a/libraries/native_contract/native_contract_chain_administrator.cpp +++ b/libraries/native_contract/native_contract_chain_administrator.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include #include @@ -11,9 +11,8 @@ namespace eos { namespace native_contract { using administrator = native_contract_chain_administrator; -ProducerRound administrator::get_next_round(const chainbase::database& db) { -#warning TODO: Implement me - return db.get(chain::global_property_object::id_type()).active_producers; +ProducerRound administrator::get_next_round(chainbase::database& db) { + return ProducerScheduleObject::get(db).calculateNextRound(db); } chain::BlockchainConfiguration administrator::get_blockchain_configuration(const chainbase::database& db, @@ -22,11 +21,11 @@ chain::BlockchainConfiguration administrator::get_blockchain_configuration(const using types::AccountName; using chain::producer_object; - auto get_producer_votes = transformed([&db](const AccountName& owner) { + auto ProducerNameToConfiguration = transformed([&db](const AccountName& owner) { return db.get(owner).configuration; }); - auto votes_range = round | get_producer_votes; + auto votes_range = round | ProducerNameToConfiguration; return chain::BlockchainConfiguration::get_median_values({votes_range.begin(), votes_range.end()}); } diff --git a/libraries/native_contract/native_contract_chain_initializer.cpp b/libraries/native_contract/native_contract_chain_initializer.cpp index 6a271b49f63..0a8b5ea280f 100644 --- a/libraries/native_contract/native_contract_chain_initializer.cpp +++ b/libraries/native_contract/native_contract_chain_initializer.cpp @@ -12,30 +12,27 @@ namespace eos { namespace native_contract { using namespace chain; -std::vector native_contract_chain_initializer::prepare_database(chain_controller& chain, - chainbase::database& db) { - std::vector messages_to_process; +types::Time native_contract_chain_initializer::get_chain_start_time() { + return genesis.initial_timestamp; +} + +chain::BlockchainConfiguration native_contract_chain_initializer::get_chain_start_configuration() { + return genesis.initial_configuration; +} +std::array native_contract_chain_initializer::get_chain_start_producers() { + std::array result; + std::transform(genesis.initial_producers.begin(), genesis.initial_producers.end(), result.begin(), + [](const auto& p) { return p.owner_name; }); + return result; +} + +void native_contract_chain_initializer::register_types(chain_controller& chain, chainbase::database& db) { // Install the native contract's indexes; we can't do anything until our objects are recognized db.add_index(); db.add_index(); db.add_index(); - - /// Create the native contract accounts manually; sadly, we can't run their contracts to make them create themselves - auto CreateNativeAccount = [this, &db](auto name, auto liquidBalance) { - db.create([this, &name](account_object& a) { - a.name = name; - a.creation_date = genesis.initial_timestamp; - }); - db.create([&name, liquidBalance](BalanceObject& b) { - b.ownerName = name; - b.balance = liquidBalance; - }); - db.create([&name](StakedBalanceObject& sb) { sb.ownerName = name; }); - }; - CreateNativeAccount(config::SystemContractName, config::InitialTokenSupply); - CreateNativeAccount(config::EosContractName, 0); - CreateNativeAccount(config::StakedBalanceContractName, 0); + db.add_index(); // Install the native contract's message handlers // First, set message handlers @@ -63,6 +60,11 @@ std::vector native_contract_chain_initializer::prepare_database( &CreateAccount_Notify_Eos::validate_preconditions, &CreateAccount_Notify_Eos::apply); SetNotifyHandlers(config::SystemContractName, config::StakedBalanceContractName, "CreateAccount", &CreateAccount_Notify_Staked::validate_preconditions, &CreateAccount_Notify_Staked::apply); +} + +std::vector native_contract_chain_initializer::prepare_database(chain_controller& chain, + chainbase::database& db) { + std::vector messages_to_process; // Register native contract message types #define MACRO(r, data, elem) chain.register_type(data); @@ -71,6 +73,25 @@ std::vector native_contract_chain_initializer::prepare_database( BOOST_PP_SEQ_FOR_EACH(MACRO, config::StakedBalanceContractName, EOS_STAKED_BALANCE_CONTRACT_FUNCTIONS) #undef MACRO + // Create the singleton object, ProducerScheduleObject + db.create([](const auto&){}); + + /// Create the native contract accounts manually; sadly, we can't run their contracts to make them create themselves + auto CreateNativeAccount = [this, &db](auto name, auto liquidBalance) { + db.create([this, &name](account_object& a) { + a.name = name; + a.creation_date = genesis.initial_timestamp; + }); + db.create([&name, liquidBalance](BalanceObject& b) { + b.ownerName = name; + b.balance = liquidBalance; + }); + db.create([&name](StakedBalanceObject& sb) { sb.ownerName = name; }); + }; + CreateNativeAccount(config::SystemContractName, config::InitialTokenSupply); + CreateNativeAccount(config::EosContractName, 0); + CreateNativeAccount(config::StakedBalanceContractName, 0); + // Queue up messages which will run contracts to create the initial accounts auto KeyAuthority = [](PublicKey k) { return types::Authority(1, {{k, 1}}, {}); @@ -102,19 +123,4 @@ std::vector native_contract_chain_initializer::prepare_database( return messages_to_process; } -types::Time native_contract_chain_initializer::get_chain_start_time() { - return genesis.initial_timestamp; -} - -chain::BlockchainConfiguration native_contract_chain_initializer::get_chain_start_configuration() { - return genesis.initial_configuration; -} - -std::array native_contract_chain_initializer::get_chain_start_producers() { - std::array result; - std::transform(genesis.initial_producers.begin(), genesis.initial_producers.end(), result.begin(), - [](const auto& p) { return p.owner_name; }); - return result; -} - } } // namespace eos::native_contract diff --git a/libraries/native_contract/producer_objects.cpp b/libraries/native_contract/producer_objects.cpp index de7816e55da..6677b180007 100644 --- a/libraries/native_contract/producer_objects.cpp +++ b/libraries/native_contract/producer_objects.cpp @@ -1,19 +1,109 @@ #include +#include + +#include +#include +#include + namespace eos { using namespace chain; using namespace types; void ProducerVotesObject::updateVotes(ShareType deltaVotes, UInt128 currentRaceTime) { auto timeSinceLastUpdate = currentRaceTime - race.positionUpdateTime; - race.position += race.speed * timeSinceLastUpdate; - race.positionUpdateTime = currentRaceTime; + auto newPosition = race.position + race.speed * timeSinceLastUpdate; + auto newSpeed = race.speed + deltaVotes; + + race.update(newSpeed, newPosition, currentRaceTime); +} + +ProducerRound ProducerScheduleObject::calculateNextRound(chainbase::database& db) const { + // Create storage and machinery with nice names, for choosing the top-voted producers + ProducerRound round; + auto FilterRetiredProducers = boost::adaptors::filtered([&db](const ProducerVotesObject& pvo) { + return db.get(pvo.ownerName).signing_key != PublicKey(); + }); + auto ProducerObjectToName = boost::adaptors::transformed([](const ProducerVotesObject& p) { return p.ownerName; }); + const auto& AllProducersByVotes = db.get_index(); + auto ActiveProducersByVotes = AllProducersByVotes | FilterRetiredProducers; + + FC_ASSERT(boost::distance(ActiveProducersByVotes) >= config::BlocksPerRound, + "Not enough active producers registered to schedule a round!", + ("ActiveProducers", boost::distance(ActiveProducersByVotes))("AllProducers", AllProducersByVotes.size())); + + // Copy the top voted active producer's names into the round + auto runnerUpStorage = + boost::copy_n(ActiveProducersByVotes | ProducerObjectToName, config::VotedProducersPerRound, round.begin()); + + // More machinery with nice names, this time for choosing runner-up producers + auto VotedProducerRange = boost::make_iterator_range(round.begin(), runnerUpStorage); + // Sort the voted producer names; we'll need to do it anyways, and it makes searching faster if we do it now + boost::sort(VotedProducerRange); + auto FilterVotedProducers = boost::adaptors::filtered([&VotedProducerRange](const ProducerVotesObject& pvo) { + return !boost::binary_search(VotedProducerRange, pvo.ownerName); + }); + const auto& AllProducersByFinishTime = db.get_index(); + auto EligibleProducersByFinishTime = AllProducersByFinishTime | FilterRetiredProducers | FilterVotedProducers; + + auto runnerUpProducerCount = config::BlocksPerRound - config::VotedProducersPerRound; + + // Copy the front producers in the race into the round + auto roundEnd = + boost::copy_n(EligibleProducersByFinishTime | ProducerObjectToName, runnerUpProducerCount, runnerUpStorage); + + FC_ASSERT(roundEnd == round.end(), + "Round scheduling yielded an unexpected number of producers: got ${actual}, but expected ${expected}", + ("actual", std::distance(round.begin(), roundEnd))("expected", round.size())); + auto lastRunnerUpName = *(roundEnd - 1); + // Sort the runner-up producers into the voted ones + boost::inplace_merge(round, runnerUpStorage); + + // Machinery to update the virtual race tracking for the producers that completed their lap + auto lastRunnerUp = AllProducersByFinishTime.iterator_to(db.get(lastRunnerUpName)); + auto newRaceTime = lastRunnerUp->projectedRaceFinishTime(); + auto StartNewLap = [&db, newRaceTime](const ProducerVotesObject& pvo) { + db.modify(pvo, [newRaceTime](ProducerVotesObject& pvo) { + pvo.startNewRaceLap(newRaceTime); + }); + }; + auto LapCompleters = boost::make_iterator_range(AllProducersByFinishTime.begin(), ++lastRunnerUp); + + // Start each producer that finished his lap on the next one, and update the global race time. + try { + if (boost::distance(LapCompleters) < AllProducersByFinishTime.size() + && newRaceTime < std::numeric_limits::max()) { + ilog("Processed producer race. ${count} producers completed a lap at virtual time ${time}", + ("count", boost::distance(LapCompleters))("time", newRaceTime)); + boost::for_each(LapCompleters, StartNewLap); + db.modify(*this, [newRaceTime](ProducerScheduleObject& pso) { + pso.currentRaceTime = newRaceTime; + }); + } else { + wlog("All producers finished race, or race time at maximum; resetting race."); + resetProducerRace(db); + } + } catch (ProducerRaceOverflowException&) { + // Virtual race time has overflown. Reset race for everyone. + wlog("Producer race virtual time overflow detected! Resetting race."); + resetProducerRace(db); + } + + return round; +} - race.speed += deltaVotes; - auto distanceRemaining = config::ProducerRaceLapLength - race.position; - auto projectedTimeToFinish = distanceRemaining / race.speed; +void ProducerScheduleObject::resetProducerRace(chainbase::database& db) const { + auto ResetRace = [&db](const ProducerVotesObject& pvo) { + db.modify(pvo, [](ProducerVotesObject& pvo) { + pvo.startNewRaceLap(0); + }); + }; + const auto& AllProducers = db.get_index(); - race.projectedFinishTime = currentRaceTime + projectedTimeToFinish; + boost::for_each(AllProducers, ResetRace); + db.modify(*this, [](ProducerScheduleObject& pso) { + pso.currentRaceTime = 0; + }); } } // namespace eos diff --git a/libraries/native_contract/staked_balance_contract.cpp b/libraries/native_contract/staked_balance_contract.cpp index a0c4ba2dff4..854f045009a 100644 --- a/libraries/native_contract/staked_balance_contract.cpp +++ b/libraries/native_contract/staked_balance_contract.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -109,6 +110,11 @@ void CreateProducer::apply(apply_context& context) { p.signing_key = create.key; p.configuration = create.configuration; }); + auto raceTime = ProducerScheduleObject::get(db).currentRaceTime; + db.create([&create, &raceTime](ProducerVotesObject& pvo) { + pvo.ownerName = create.name; + pvo.startNewRaceLap(raceTime); + }); } void UpdateProducer::validate(message_validate_context& context) {