BitShares-Core  7.0.2
BitShares blockchain node software and command-line wallet software
account_history_plugin.cpp
Go to the documentation of this file.
1 /*
2  * Copyright (c) 2015 Cryptonomex, Inc., and contributors.
3  *
4  * The MIT License
5  *
6  * Permission is hereby granted, free of charge, to any person obtaining a copy
7  * of this software and associated documentation files (the "Software"), to deal
8  * in the Software without restriction, including without limitation the rights
9  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10  * copies of the Software, and to permit persons to whom the Software is
11  * furnished to do so, subject to the following conditions:
12  *
13  * The above copyright notice and this permission notice shall be included in
14  * all copies or substantial portions of the Software.
15  *
16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22  * THE SOFTWARE.
23  */
24 
26 
28 
36 #include <graphene/chain/hardfork.hpp>
37 
39 
40 #include <fc/thread/thread.hpp>
41 
42 namespace graphene { namespace account_history {
43 
44 namespace detail
45 {
46 
47 
49 {
50  public:
52  : _self( _plugin )
53  { }
54 
55  private:
59  void update_account_histories( const signed_block& b );
60 
62  {
63  return _self.database();
64  }
65 
67 
69  flat_set<account_id_type> _tracked_accounts;
70  flat_set<account_id_type> _extended_history_accounts;
71  flat_set<account_id_type> _extended_history_registrars;
72  bool _partial_operations = false;
74  uint64_t _max_ops_per_account = -1;
75  uint64_t _extended_max_ops_per_account = -1;
76  uint32_t _min_blocks_to_keep = 30000;
77  uint64_t _max_ops_per_acc_by_min_blocks = 1000;
78 
79  uint32_t _latest_block_number_to_remove = 0;
80 
81  uint64_t get_max_ops_to_keep( const account_id_type& account_id );
82 
84  void add_account_history( const account_id_type& account_id, const operation_history_object& op );
85 
86  void remove_old_histories_by_account( const account_statistics_object& stats_obj,
87  const exceeded_account_object* p_exa_obj = nullptr );
88 
89  void remove_old_histories();
90 
93  void check_and_remove_op_history_obj( const operation_history_object& op );
94 
95  void init_program_options(const boost::program_options::variables_map& options);
96 };
97 
98 template< typename T >
99 static T get_biggest_number_to_remove( T biggest_number, T amount_to_keep )
100 {
101  return ( biggest_number > amount_to_keep ) ? ( biggest_number - amount_to_keep ) : 0;
102 }
103 
104 void account_history_plugin_impl::update_account_histories( const signed_block& b )
105 {
106  _latest_block_number_to_remove = get_biggest_number_to_remove( b.block_num(), _min_blocks_to_keep );
107 
109  const vector<optional< operation_history_object > >& hist = db.get_applied_operations();
110  bool is_first = true;
111  auto skip_oho_id = [&is_first,&db,this]() {
112  if( is_first && db._undo_db.enabled() ) // this ensures that the current id is rolled back on undo
113  {
114  db.remove( db.create<operation_history_object>( []( operation_history_object& obj) {} ) );
115  is_first = false;
116  }
117  else
118  _oho_index->use_next_id();
119  };
120 
121  for( const optional< operation_history_object >& o_op : hist )
122  {
123  optional<operation_history_object> oho;
124 
125  auto create_oho = [&]() {
126  is_first = false;
127  return optional<operation_history_object>( db.create<operation_history_object>( [&]( operation_history_object& h )
128  {
129  if( o_op.valid() )
130  {
131  h.op = o_op->op;
132  h.result = o_op->result;
133  h.block_num = o_op->block_num;
134  h.trx_in_block = o_op->trx_in_block;
135  h.op_in_trx = o_op->op_in_trx;
136  h.virtual_op = o_op->virtual_op;
137  h.is_virtual = o_op->is_virtual;
138  h.block_time = o_op->block_time;
139  }
140  } ) );
141  };
142 
143  if( !o_op.valid() || ( _max_ops_per_account == 0 && _partial_operations ) )
144  {
145  // Note: the 2nd and 3rd checks above are for better performance, when the db is not clean,
146  // they will break consistency of account_stats.total_ops and removed_ops and most_recent_op
147  skip_oho_id();
148  continue;
149  }
150  else if( !_partial_operations )
151  // add to the operation history index
152  oho = create_oho();
153 
154  const operation_history_object& op = *o_op;
155 
156  // get the set of accounts this operation applies to
157  flat_set<account_id_type> impacted;
158  vector<authority> other;
159  // fee payer is added here
160  operation_get_required_authorities( op.op, impacted, impacted, other,
161  MUST_IGNORE_CUSTOM_OP_REQD_AUTHS( db.head_block_time() ) );
162 
163  if( op.op.is_type< account_create_operation >() )
164  impacted.insert( account_id_type( op.result.get<object_id_type>() ) );
165 
166  // https://github.com/bitshares/bitshares-core/issues/265
167  if( HARDFORK_CORE_265_PASSED(b.timestamp) || !op.op.is_type< account_create_operation >() )
168  {
169  operation_get_impacted_accounts( op.op, impacted,
170  MUST_IGNORE_CUSTOM_OP_REQD_AUTHS( db.head_block_time() ) );
171  }
172 
173  if( op.result.is_type<extendable_operation_result>() )
174  {
175  const auto& op_result = op.result.get<extendable_operation_result>();
176  if( op_result.value.impacted_accounts.valid() )
177  {
178  for( const auto& a : *op_result.value.impacted_accounts )
179  impacted.insert( a );
180  }
181  }
182 
183  for( auto& a : other )
184  for( auto& item : a.account_auths )
185  impacted.insert( item.first );
186 
187  // be here, either _max_ops_per_account > 0, or _partial_operations == false, or both
188  // if _partial_operations == false, oho should have been created above
189  // so the only case should be checked here is:
190  // whether need to create oho if _max_ops_per_account > 0 and _partial_operations == true
191 
192  // for each operation this account applies to that is in the config link it into the history
193  if( _tracked_accounts.size() == 0 ) // tracking all accounts
194  {
195  // if tracking all accounts, when impacted is not empty (although it will always be),
196  // still need to create oho if _max_ops_per_account > 0 and _partial_operations == true
197  // so always need to create oho if not done
198  if (!impacted.empty() && !oho.valid()) { oho = create_oho(); }
199 
200  if( _max_ops_per_account > 0 )
201  {
202  // Note: the check above is for better performance, when the db is not clean,
203  // it breaks consistency of account_stats.total_ops and removed_ops and most_recent_op,
204  // but it ensures it's safe to remove old entries in add_account_history(...)
205  for( auto& account_id : impacted )
206  {
207  // we don't do index_account_keys here anymore, because
208  // that indexing now happens in observers' post_evaluate()
209 
210  // add history
211  add_account_history( account_id, *oho );
212  }
213  }
214  }
215  else // tracking a subset of accounts
216  {
217  // whether need to create oho if _max_ops_per_account > 0 and _partial_operations == true ?
218  // the answer: only need to create oho if a tracked account is impacted and need to save history
219 
220  if( _max_ops_per_account > 0 )
221  {
222  // Note: the check above is for better performance, when the db is not clean,
223  // it breaks consistency of account_stats.total_ops and removed_ops and most_recent_op,
224  // but it ensures it's safe to remove old entries in add_account_history(...)
225  for( auto account_id : _tracked_accounts )
226  {
227  if( impacted.find( account_id ) != impacted.end() )
228  {
229  if (!oho.valid()) { oho = create_oho(); }
230  // add history
231  add_account_history( account_id, *oho );
232  }
233  }
234  }
235  }
236  if (_partial_operations && ! oho.valid())
237  skip_oho_id();
238  }
239 
240  remove_old_histories();
241 }
242 
243 void account_history_plugin_impl::add_account_history( const account_id_type& account_id,
244  const operation_history_object& op )
245 {
247  const auto& stats_obj = db.get_account_stats_by_owner( account_id );
248  // add new entry
249  const auto& aho = db.create<account_history_object>( [&account_id,&op,&stats_obj](account_history_object& obj){
250  obj.operation_id = op.id;
251  obj.account = account_id;
252  obj.sequence = stats_obj.total_ops + 1;
253  obj.next = stats_obj.most_recent_op;
254  });
255  db.modify( stats_obj, [&aho]( account_statistics_object& obj ){
256  obj.most_recent_op = aho.id;
257  obj.total_ops = aho.sequence;
258  });
259  // Remove the earliest account history entries if too many.
260  remove_old_histories_by_account( stats_obj );
261 }
262 
263 uint64_t account_history_plugin_impl::get_max_ops_to_keep( const account_id_type& account_id )
264 {
265  const graphene::chain::database& db = database();
266  // Amount of history to keep depends on if account is in the "extended history" list
267  bool extended_hist = ( _extended_history_accounts.find( account_id ) != _extended_history_accounts.end() );
268  if( !extended_hist && !_extended_history_registrars.empty() )
269  {
270  const account_id_type& registrar_id = account_id(db).registrar;
271  extended_hist = ( _extended_history_registrars.find( registrar_id ) != _extended_history_registrars.end() );
272  }
273  // _max_ops_per_account is guaranteed to be non-zero outside; max_ops_to_keep
274  // will likewise be non-zero, and also non-negative (it is unsigned).
275  auto max_ops_to_keep = _max_ops_per_account;
276  if( extended_hist && _extended_max_ops_per_account > max_ops_to_keep )
277  {
278  max_ops_to_keep = _extended_max_ops_per_account;
279  }
280  if( 0 == max_ops_to_keep )
281  return 1;
282  return max_ops_to_keep;
283 }
284 
285 void account_history_plugin_impl::remove_old_histories()
286 {
287  if( 0 == _latest_block_number_to_remove )
288  return;
289 
290  const graphene::chain::database& db = database();
291  const auto& exa_idx = db.get_index_type<exceeded_account_index>().indices().get<by_block_num>();
292  auto itr = exa_idx.begin();
293  while( itr != exa_idx.end() && itr->block_num <= _latest_block_number_to_remove )
294  {
295  const auto& stats_obj = db.get_account_stats_by_owner( itr->account_id );
296  remove_old_histories_by_account( stats_obj, &(*itr) );
297  itr = exa_idx.begin();
298  }
299 }
300 
301 void account_history_plugin_impl::check_and_remove_op_history_obj( const operation_history_object& op )
302 {
303  if( _partial_operations )
304  {
305  // check for references
307  const auto& his_idx = db.get_index_type<account_history_index>();
308  const auto& by_opid_idx = his_idx.indices().get<by_opid>();
309  if( by_opid_idx.find( op.get_id() ) == by_opid_idx.end() )
310  {
311  // if no reference, remove
312  db.remove( op );
313  }
314  }
315 }
316 
317 // Remove the earliest account history entries if too many.
318 void account_history_plugin_impl::remove_old_histories_by_account( const account_statistics_object& stats_obj,
319  const exceeded_account_object* p_exa_obj )
320 {
322  const account_id_type& account_id = stats_obj.owner;
323  auto max_ops_to_keep = get_max_ops_to_keep( account_id ); // >= 1
324  auto number_of_ops_to_remove = get_biggest_number_to_remove( stats_obj.total_ops, max_ops_to_keep );
325  auto number_of_ops_to_remove_by_blks = get_biggest_number_to_remove( stats_obj.total_ops,
326  _max_ops_per_acc_by_min_blocks );
327 
328  const auto& his_idx = db.get_index_type<account_history_index>();
329  const auto& by_seq_idx = his_idx.indices().get<by_seq>();
330 
331  auto removed_ops = stats_obj.removed_ops;
332  // look for the earliest entry if needed
333  auto aho_itr = ( removed_ops < number_of_ops_to_remove ) ? by_seq_idx.lower_bound( account_id )
334  : by_seq_idx.begin();
335 
336  uint32_t oldest_block_num = _latest_block_number_to_remove;
337  while( removed_ops < number_of_ops_to_remove )
338  {
339  // make sure don't remove the latest one
340  // this should always be false, just check to be safe
341  if( aho_itr == by_seq_idx.end() || aho_itr->account != account_id || aho_itr->id == stats_obj.most_recent_op )
342  break;
343 
344  // if found, check whether to remove
345  const auto& aho_to_remove = *aho_itr;
346  const auto& remove_op = aho_to_remove.operation_id(db);
347  oldest_block_num = remove_op.block_num;
348  if( remove_op.block_num > _latest_block_number_to_remove && removed_ops >= number_of_ops_to_remove_by_blks )
349  break;
350 
351  // remove the entry
352  ++aho_itr;
353  db.remove( aho_to_remove );
354  ++removed_ops;
355 
356  // remove the operation history entry (1.11.x) if configured and no reference left
357  check_and_remove_op_history_obj( remove_op );
358  }
359  // adjust account stats object and the oldest entry
360  if( removed_ops != stats_obj.removed_ops )
361  {
362  db.modify( stats_obj, [removed_ops]( account_statistics_object& obj ){
363  obj.removed_ops = removed_ops;
364  });
365  // modify previous node's next pointer
366  // this should be always true, but just have a check here
367  if( aho_itr != by_seq_idx.end() && aho_itr->account == account_id )
368  {
369  db.modify( *aho_itr, []( account_history_object& obj ){
370  obj.next = account_history_id_type();
371  });
372  }
373  // else need to modify the head pointer, but it shouldn't be true
374  }
375  // deal with exceeded_account_object
376  if( !p_exa_obj )
377  {
378  const auto& exa_idx = db.get_index_type<exceeded_account_index>().indices().get<by_account>();
379  auto exa_itr = exa_idx.find( account_id );
380  if( exa_itr != exa_idx.end() )
381  p_exa_obj = &(*exa_itr);
382  }
383  if( stats_obj.removed_ops < number_of_ops_to_remove )
384  {
385  // create or update exceeded_account_object
386  if( p_exa_obj )
387  db.modify( *p_exa_obj, [oldest_block_num]( exceeded_account_object& obj ){
388  obj.block_num = oldest_block_num;
389  });
390  else
391  db.create<exceeded_account_object>(
392  [&account_id, oldest_block_num]( exceeded_account_object& obj ){
393  obj.account_id = account_id;
394  obj.block_num = oldest_block_num;
395  });
396  }
397  // remove exceeded_account_object if found
398  else if( p_exa_obj )
399  db.remove( *p_exa_obj );
400 }
401 
402 } // end namespace detail
403 
404 
406  plugin(app),
407  my( std::make_unique<detail::account_history_plugin_impl>(*this) )
408 {
409  // Nothing else to do
410 }
411 
413 
415 {
416  return "account_history";
417 }
418 
420  boost::program_options::options_description& cli,
421  boost::program_options::options_description& cfg
422  )
423 {
424  cli.add_options()
425  ("track-account", boost::program_options::value<std::vector<std::string>>()->composing()->multitoken(),
426  "Account ID to track history for (may specify multiple times; if unset will track all accounts)")
427  ("partial-operations", boost::program_options::value<bool>(),
428  "Keep only those operations in memory that are related to account history tracking")
429  ("max-ops-per-account", boost::program_options::value<uint64_t>(),
430  "Maximum number of operations per account that will be kept in memory. "
431  "Note that the actual number may be higher due to the min-blocks-to-keep option.")
432  ("extended-max-ops-per-account", boost::program_options::value<uint64_t>(),
433  "Maximum number of operations to keep for accounts for which extended history is kept. "
434  "This option only takes effect when track-account is not used and max-ops-per-account is not zero.")
435  ("extended-history-by-account",
436  boost::program_options::value<std::vector<std::string>>()->composing()->multitoken(),
437  "Track longer history for these accounts (may specify multiple times)")
438  ("extended-history-by-registrar",
439  boost::program_options::value<std::vector<std::string>>()->composing()->multitoken(),
440  "Track longer history for accounts with this registrar (may specify multiple times)")
441  ("min-blocks-to-keep", boost::program_options::value<uint32_t>(),
442  "Operations which are in the latest X blocks will be kept in memory. "
443  "This option only takes effect when track-account is not used and max-ops-per-account is not zero. "
444  "Note that this option may cause more history records to be kept in memory than the limit defined by the "
445  "max-ops-per-account option, but the amount will be limited by the max-ops-per-acc-by-min-blocks option. "
446  "(default: 30000)")
447  ("max-ops-per-acc-by-min-blocks", boost::program_options::value<uint64_t>(),
448  "A potential higher limit on the maximum number of operations per account to be kept in memory "
449  "when the min-blocks-to-keep option causes the amount to exceed the limit defined by the "
450  "max-ops-per-account option. If this is less than max-ops-per-account, max-ops-per-account will be used. "
451  "(default: 1000)")
452  ;
453  cfg.add(cli);
454 }
455 
456 void account_history_plugin::plugin_initialize(const boost::program_options::variables_map& options)
457 {
458  my->init_program_options( options );
459 
460  // connect with group 0 to process before some special steps (e.g. snapshot or next_object_id)
461  database().applied_block.connect( 0, [this]( const signed_block& b){ my->update_account_histories(b); } );
464 
466 }
467 
468 void detail::account_history_plugin_impl::init_program_options(const boost::program_options::variables_map& options)
469 {
470  LOAD_VALUE_SET(options, "track-account", _tracked_accounts, graphene::chain::account_id_type);
471 
472  utilities::get_program_option( options, "partial-operations", _partial_operations );
473  utilities::get_program_option( options, "max-ops-per-account", _max_ops_per_account );
474  utilities::get_program_option( options, "extended-max-ops-per-account", _extended_max_ops_per_account );
475  if( _extended_max_ops_per_account < _max_ops_per_account )
476  _extended_max_ops_per_account = _max_ops_per_account;
477 
478  LOAD_VALUE_SET(options, "extended-history-by-account", _extended_history_accounts,
479  graphene::chain::account_id_type);
480  LOAD_VALUE_SET(options, "extended-history-by-registrar", _extended_history_registrars,
481  graphene::chain::account_id_type);
482 
483  utilities::get_program_option( options, "min-blocks-to-keep", _min_blocks_to_keep );
484  utilities::get_program_option( options, "max-ops-per-acc-by-min-blocks", _max_ops_per_acc_by_min_blocks );
485  if( _max_ops_per_acc_by_min_blocks < _max_ops_per_account )
486  _max_ops_per_acc_by_min_blocks = _max_ops_per_account;
487 }
488 
490 {
491 }
492 
493 flat_set<account_id_type> account_history_plugin::tracked_accounts() const
494 {
495  return my->_tracked_accounts;
496 }
497 
498 } }
graphene::account_history::account_history_plugin::plugin_startup
void plugin_startup() override
Begin normal runtime operations.
Definition: account_history_plugin.cpp:489
graphene::chain::database
tracks the blockchain state in an extensible manner
Definition: database.hpp:70
graphene::utilities::get_program_option
void get_program_option(const boost::program_options::variables_map &from, const std::string &key, T &to)
Definition: boost_program_options.hpp:30
graphene::account_history::account_history_plugin::~account_history_plugin
~account_history_plugin() override
database.hpp
graphene::db::primary_index
Wraps a derived index to intercept calls to create, modify, and remove so that callbacks may be fired...
Definition: index.hpp:312
graphene::app::plugin::database
chain::database & database()
Definition: plugin.hpp:115
impacted.hpp
graphene::account_history::account_history_plugin::plugin_initialize
void plugin_initialize(const boost::program_options::variables_map &options) override
Perform early startup routines and register plugin indexes, callbacks, etc.
Definition: account_history_plugin.cpp:456
boost_program_options.hpp
account_evaluator.hpp
account_history_plugin.hpp
graphene::db::object_database::create
const T & create(F &&constructor)
Definition: object_database.hpp:63
graphene::chain::database::get_applied_operations
const vector< optional< operation_history_object > > & get_applied_operations() const
Definition: db_block.cpp:566
graphene::account_history::detail::account_history_plugin_impl
Definition: account_history_plugin.cpp:48
graphene::chain::database::applied_block
fc::signal< void(const signed_block &)> applied_block
Definition: database.hpp:624
graphene::chain::account_history_index
generic_index< account_history_object, account_history_multi_idx_type > account_history_index
Definition: operation_history_object.hpp:165
graphene::protocol::block_header::block_num
uint32_t block_num() const
Definition: block.hpp:34
graphene::account_history::account_history_plugin::tracked_accounts
flat_set< account_id_type > tracked_accounts() const
Definition: account_history_plugin.cpp:493
graphene::app::application
Definition: application.hpp:91
graphene::chain::account_statistics_object
Definition: account_object.hpp:46
graphene::db::object_database::add_index
IndexType * add_index()
Definition: object_database.hpp:144
config.hpp
evaluator.hpp
graphene::account_history::detail::account_history_plugin_impl::account_history_plugin_impl
account_history_plugin_impl(account_history_plugin &_plugin)
Definition: account_history_plugin.cpp:51
account_object.hpp
thread.hpp
graphene::account_history::exceeded_account_index
generic_index< exceeded_account_object, exceeded_account_multi_idx_type > exceeded_account_index
Definition: account_history_plugin.hpp:80
graphene::protocol::block_header::timestamp
fc::time_point_sec timestamp
Definition: block.hpp:35
graphene::protocol::extendable_operation_result
extension< extendable_operation_result_dtl > extendable_operation_result
Definition: base.hpp:113
graphene::protocol::operation_get_required_authorities
void operation_get_required_authorities(const operation &op, flat_set< account_id_type > &active, flat_set< account_id_type > &owner, vector< authority > &other, bool ignore_custom_operation_required_auths)
Definition: operations.cpp:103
operation_history_object.hpp
graphene::account_history::account_history_plugin::account_history_plugin
account_history_plugin(graphene::app::application &app)
Definition: account_history_plugin.cpp:405
std
Definition: zeroed_array.hpp:76
graphene::chain::operation_get_impacted_accounts
void operation_get_impacted_accounts(const operation &op, flat_set< account_id_type > &result, bool ignore_custom_op_required_auths)
Definition: db_notify.cpp:403
transaction_evaluation_state.hpp
graphene::chain::database::get_account_stats_by_owner
const account_statistics_object & get_account_stats_by_owner(account_id_type owner) const
Definition: db_getter.cpp:142
graphene::account_history::account_history_plugin
Definition: account_history_plugin.hpp:87
LOAD_VALUE_SET
#define LOAD_VALUE_SET(options, name, container, type)
Definition: plugin.hpp:137
graphene::account_history::account_history_plugin::plugin_name
std::string plugin_name() const override
Get the name of the plugin.
Definition: account_history_plugin.cpp:414
graphene::protocol::signed_block
Definition: block.hpp:64
graphene::db::object_database::remove
void remove(const object &obj)
Definition: object_database.hpp:97
graphene::app::abstract_plugin::app
application & app() const
Get a reference of the application bound to the plugin.
Definition: plugin.hpp:46
graphene::db::object_database::get_index_type
const IndexType & get_index_type() const
Definition: object_database.hpp:77
graphene
Definition: api.cpp:48
graphene::account_history::exceeded_account_object
This struct tracks accounts that have exceeded the max-ops-per-account limit.
Definition: account_history_plugin.hpp:53
graphene::db::object_database::modify
void modify(const T &obj, const Lambda &m)
Definition: object_database.hpp:99
graphene::chain::operation_history_object
tracks the history of all logical operations on blockchain state
Definition: operation_history_object.hpp:48
graphene::account_history::account_history_plugin::plugin_set_program_options
void plugin_set_program_options(boost::program_options::options_description &cli, boost::program_options::options_description &cfg) override
Fill in command line parameters used by the plugin.
Definition: account_history_plugin.cpp:419