cc: save events to the db

This makes them retrievable from a snapshot, after a restart,
and after a reorg that crosses a game update
This commit is contained in:
Crypto City 2019-11-15 00:01:30 +00:00
parent b89e696a80
commit 761d212b53
12 changed files with 351 additions and 23 deletions

View File

@ -36,6 +36,7 @@
#include "common/command_line.h"
#include "crypto/hash.h"
#include "cc/cc_config.h"
#include "cc/cc_game_events.h"
#include "cryptonote_basic/blobdatatype.h"
#include "cryptonote_basic/cryptonote_basic.h"
#include "cryptonote_basic/difficulty.h"
@ -1876,6 +1877,10 @@ public:
virtual void remove_cc_shares(uint32_t city, uint64_t height) = 0;
virtual bool get_cc_shares(uint32_t city, uint64_t height, cc_shares_data_t &sd) const = 0;
virtual void add_cc_events(uint64_t height, const std::vector<cc::game_update_event_t> &events) = 0;
virtual void remove_cc_events(uint64_t height) = 0;
virtual bool get_cc_events(uint64_t height, uint32_t account, uint32_t flag, std::vector<cc::game_update_event_t> &events) const = 0;
// CC non virtual helpers, calls the virtual ones
bool does_cc_account_exist(uint32_t id) const;
bool does_cc_city_exist(uint32_t id) const;

View File

@ -190,6 +190,55 @@ int BlockchainLMDB::compare_string(const MDB_val *a, const MDB_val *b)
return strcmp(va, vb);
}
enum event_match_t: uint32_t { em_account, em_account_flag, em_account_flag_event };
int BlockchainLMDB::compare_events(const MDB_val *a, const MDB_val *b)
{
// a[1] size is an int which tells is how much of the record to match against
const uint32_t match = a[1].mv_size;
const char *aptr = (const char*)a->mv_data;
const char *bptr = (const char*)b->mv_data;
uint32_t va32, vb32;
memcpy(&va32, aptr, sizeof(va32));
memcpy(&vb32, bptr, sizeof(vb32));
if (va32 < vb32)
return -1;
if (va32 > vb32)
return 1;
if (match == em_account)
return 0;
aptr += sizeof(va32);
bptr += sizeof(vb32);
memcpy(&va32, aptr, sizeof(va32));
memcpy(&vb32, bptr, sizeof(vb32));
if (va32 < vb32)
return -1;
if (va32 > vb32)
return 1;
if (match == em_account_flag)
return 0;
aptr += sizeof(va32);
bptr += sizeof(vb32);
const ssize_t a_size = a->mv_size - (aptr - (const char*)a->mv_data);
const ssize_t b_size = b->mv_size - (bptr - (const char*)b->mv_data);
unsigned int len = a_size;
ssize_t len_diff = (ssize_t) a_size - (ssize_t) b_size;
if (len_diff > 0)
{
len = b_size;
len_diff = 1;
}
int diff = memcmp(aptr, bptr, len);
return diff ? diff : len_diff<0 ? -1 : len_diff;
}
}
namespace
@ -229,6 +278,7 @@ namespace
* cc_trade_used nonce uint64_t
* cc_orders nonce {order metadata}
* cc_shares city/height {share metadata}
* cc_events height account/flag/string
*
* Note: where the data items are of uniform size, DUPFIXED tables have
* been used to save space. In most of these cases, a dummy "zerokval"
@ -271,6 +321,7 @@ const char* const LMDB_CC_USED_NONCES = "cc_used_nonces";
const char* const LMDB_CC_TRADE_USED = "cc_trade_used";
const char* const LMDB_CC_ORDERS = "cc_orders";
const char* const LMDB_CC_SHARES = "cc_shares";
const char* const LMDB_CC_EVENTS = "cc_events";
const char* const LMDB_PROPERTIES = "properties";
@ -447,6 +498,12 @@ typedef struct mdb_order_t
uint64_t expiration;
} mdb_order_t;
struct cc_events_data_t
{
uint32_t account;
uint32_t flag;
};
std::atomic<uint64_t> mdb_txn_safe::num_active_txns{0};
std::atomic_flag mdb_txn_safe::creation_gate = ATOMIC_FLAG_INIT;
@ -1527,6 +1584,7 @@ void BlockchainLMDB::open(const std::string& filename, const int db_flags)
lmdb_db_open(txn, LMDB_CC_TRADE_USED, MDB_INTEGERKEY | MDB_CREATE, m_cc_trade_used, "Failed to open db handle for m_cc_trade_used");
lmdb_db_open(txn, LMDB_CC_ORDERS, MDB_INTEGERKEY | MDB_CREATE | MDB_DUPSORT | MDB_DUPFIXED, m_cc_orders, "Failed to open db handle for m_cc_orders");
lmdb_db_open(txn, LMDB_CC_SHARES, MDB_INTEGERKEY | MDB_CREATE | MDB_DUPSORT | MDB_DUPFIXED, m_cc_shares, "Failed to open db handle for m_cc_shares");
lmdb_db_open(txn, LMDB_CC_EVENTS, MDB_INTEGERKEY | MDB_CREATE | MDB_DUPSORT, m_cc_events, "Failed to open db handle for m_cc_events");
lmdb_db_open(txn, LMDB_PROPERTIES, MDB_CREATE, m_properties, "Failed to open db handle for m_properties");
@ -1555,6 +1613,8 @@ void BlockchainLMDB::open(const std::string& filename, const int db_flags)
mdb_set_dupsort(txn, m_cc_trade_used, compare_uint64);
mdb_set_dupsort(txn, m_cc_orders, compare_uint64);
mdb_set_dupsort(txn, m_cc_shares, compare_uint32_uint64);
mdb_set_compare(txn, m_cc_events, compare_uint64);
mdb_set_dupsort(txn, m_cc_events, compare_events);
if (!(mdb_flags & MDB_RDONLY))
{
@ -1745,6 +1805,8 @@ void BlockchainLMDB::reset()
throw0(DB_ERROR(lmdb_error("Failed to drop m_cc_orders: ", result).c_str()));
if (auto result = mdb_drop(txn, m_cc_shares, 0))
throw0(DB_ERROR(lmdb_error("Failed to drop m_cc_shares: ", result).c_str()));
if (auto result = mdb_drop(txn, m_cc_events, 0))
throw0(DB_ERROR(lmdb_error("Failed to drop m_cc_events: ", result).c_str()));
// init with current version
MDB_val_str(k, "version");
@ -5964,6 +6026,134 @@ bool BlockchainLMDB::get_cc_shares(uint32_t city, uint64_t height, cc_shares_dat
return true;
}
void BlockchainLMDB::add_cc_events(uint64_t height, const std::vector<cc::game_update_event_t> &events)
{
LOG_PRINT_L3("BlockchainLMDB::" << __func__);
check_open();
mdb_txn_cursors *m_cursors = &m_wcursors;
CURSOR(cc_events)
for (const auto &e: events)
{
cc_events_data_t *data = (cc_events_data_t*)alloca(sizeof(cc_events_data_t) + e.event.size());
data->account = e.account;
data->flag = e.flag;
memcpy(data + 1, e.event.data(), e.event.size());
MDB_val k = {sizeof(uint64_t), (void *)&height};
MDB_val v[2] = {{sizeof(cc_events_data_t) + e.event.size(), (void *)data}, {em_account_flag_event, NULL}};
if (auto result = mdb_cursor_put(m_cur_cc_events, &k, v, 0)) {
throw0(DB_ERROR(lmdb_error("Error adding event data to db transaction: ", result).c_str()));
}
}
}
void BlockchainLMDB::remove_cc_events(uint64_t height)
{
LOG_PRINT_L3("BlockchainLMDB::" << __func__);
check_open();
mdb_txn_cursors *m_cursors = &m_wcursors;
CURSOR(cc_events)
MDB_val k = {sizeof(height), (void*)&height}, v;
int result = mdb_cursor_get(m_cur_cc_events, &k, &v, MDB_SET);
if (result == MDB_NOTFOUND)
return;
if (result)
throw0(DB_ERROR(lmdb_error("Error getting events data from db: ", result).c_str()));
mdb_size_t count;
result = mdb_cursor_count(m_cur_cc_events, &count);
if (result)
throw0(DB_ERROR(lmdb_error("Error counting events data from db: ", result).c_str()));
MDEBUG("deleting " << count << " events at height " << height);
result = mdb_cursor_del(m_cur_cc_events, MDB_NODUPDATA);
if (result)
throw0(DB_ERROR(lmdb_error("Error deleting events data from db: ", result).c_str()));
}
bool BlockchainLMDB::get_cc_events(uint64_t height, uint32_t account, uint32_t flag, std::vector<cc::game_update_event_t> &events) const
{
LOG_PRINT_L3("BlockchainLMDB::" << __func__);
check_open();
events.clear();
// we can lookup by flag, but the search has to be hierarchical, so fill account in first
if (flag > 0 && account == 0)
{
cc_flag_data_t cfd;
if (!get_cc_flag_data(flag, cfd, NULL))
{
// flag does not exist
return true;
}
account = cfd.owner;
}
TXN_PREFIX_RDONLY();
RCURSOR(cc_events)
const event_match_t match = flag ? em_account_flag : em_account;
MDB_cursor_op op = (account || flag) ? MDB_GET_BOTH : MDB_SET;
cc_events_data_t ed{account, flag};
MDB_val k = {sizeof(uint64_t), (void*)&height};
MDB_val v[2] = {{sizeof(ed), (void*)&ed}, {match, NULL}};
int result = mdb_cursor_get(m_cur_cc_events, &k, v, op);
if (result == MDB_NOTFOUND)
return true;
if (result)
{
MERROR("Events data not found for height " << height << ", account " << account << ", flag " << flag << ": " << mdb_strerror(result));
return false;
}
// get the first record since we can end up in the middle :/
while (1)
{
MDB_val k2, v2;
result = mdb_cursor_get(m_cur_cc_events, &k2, &v2, MDB_PREV_DUP);
if (result == MDB_NOTFOUND)
break;
if (result)
throw0(DB_ERROR(lmdb_error("Error getting event: ", result).c_str()));
const cc_events_data_t *ed = (const cc_events_data_t*)v2.mv_data;
const bool match = (account == 0 || account == ed->account) && (flag == 0 || flag == ed->flag);
if (!match)
{
result = mdb_cursor_get(m_cur_cc_events, &k2, &v2, MDB_NEXT_DUP);
if (result)
throw0(DB_ERROR(lmdb_error("Error getting event: ", result).c_str()));
break;
}
}
// walk till the first non matching record
while (1)
{
MDB_val k2, v2;
result = mdb_cursor_get(m_cur_cc_events, &k2, &v2, MDB_GET_CURRENT);
if (result)
throw0(DB_ERROR(lmdb_error("Error getting event: ", result).c_str()));
const cc_events_data_t *ed = (const cc_events_data_t*)v2.mv_data;
const bool match = (account == 0 || account == ed->account) && (flag == 0 || flag == ed->flag);
if (!match)
break;
events.push_back({ed->account, ed->flag, std::string((const char*)(ed+1), v2.mv_size - sizeof(cc_events_data_t))});
result = mdb_cursor_get(m_cur_cc_events, &k2, &v2, MDB_NEXT_DUP);
if (result == MDB_NOTFOUND)
break;
if (result)
throw0(DB_ERROR(lmdb_error("Error getting event: ", result).c_str()));
}
TXN_POSTFIX_RDONLY();
return true;
}
uint64_t BlockchainLMDB::get_database_size() const
{
uint64_t size = 0;

View File

@ -82,6 +82,7 @@ typedef struct mdb_txn_cursors
MDB_cursor *m_txc_cc_trade_used;
MDB_cursor *m_txc_cc_orders;
MDB_cursor *m_txc_cc_shares;
MDB_cursor *m_txc_cc_events;
} mdb_txn_cursors;
#define m_cur_blocks m_cursors->m_txc_blocks
@ -111,6 +112,7 @@ typedef struct mdb_txn_cursors
#define m_cur_cc_trade_used m_cursors->m_txc_cc_trade_used
#define m_cur_cc_orders m_cursors->m_txc_cc_orders
#define m_cur_cc_shares m_cursors->m_txc_cc_shares
#define m_cur_cc_events m_cursors->m_txc_cc_events
typedef struct mdb_rflags
{
@ -142,6 +144,7 @@ typedef struct mdb_rflags
bool m_rf_cc_trade_used;
bool m_rf_cc_orders;
bool m_rf_cc_shares;
bool m_rf_cc_events;
} mdb_rflags;
typedef struct mdb_threadinfo
@ -384,6 +387,7 @@ public:
static int compare_hash32(const MDB_val *a, const MDB_val *b);
static int compare_uint32_uint64(const MDB_val *a, const MDB_val *b);
static int compare_string(const MDB_val *a, const MDB_val *b);
static int compare_events(const MDB_val *a, const MDB_val *b);
private:
void do_resize(uint64_t size_increase=0);
@ -500,6 +504,10 @@ private:
void remove_cc_shares(uint32_t city, uint64_t height);
bool get_cc_shares(uint32_t city, uint64_t height, cc_shares_data_t &sd) const;
void add_cc_events(uint64_t height, const std::vector<cc::game_update_event_t> &events);
void remove_cc_events(uint64_t height);
bool get_cc_events(uint64_t height, uint32_t account, uint32_t flag, std::vector<cc::game_update_event_t> &events) const;
// fix up anything that may be wrong due to past bugs
virtual void fixup();
@ -562,6 +570,7 @@ private:
MDB_dbi m_cc_trade_used;
MDB_dbi m_cc_orders;
MDB_dbi m_cc_shares;
MDB_dbi m_cc_events;
mutable uint64_t m_cum_size; // used in batch size estimation
mutable unsigned int m_cum_count;

View File

@ -215,6 +215,10 @@ public:
virtual void remove_cc_shares(uint32_t city, uint64_t height) {}
virtual bool get_cc_shares(uint32_t city, uint64_t height, cc_shares_data_t &sd) const { return false; }
virtual void add_cc_events(uint64_t height, const std::vector<cc::game_update_event_t> &events) {}
virtual void remove_cc_events(uint64_t height) {}
virtual bool get_cc_events(uint64_t height, uint32_t account, uint32_t flag, std::vector<cc::game_update_event_t> &events) const { return false; }
private:
uint32_t n_accounts;
};

View File

@ -41,7 +41,7 @@
namespace cc
{
cc::game_update_events_t cc_command_handler_game_update::events = {};
cc::game_update_events_t cc_command_handler_game_update::last_events = {};
cc_command_handler_game_update cc_command_handler_game_update::instance;
@ -100,7 +100,7 @@ bool cc_command_handler_game_update::check(const cryptonote::BlockchainDB &db, c
}
}
events = std::move(new_events);
last_events = std::move(new_events);
return true;
}
@ -184,6 +184,7 @@ bool cc_command_handler_game_update::execute(cryptonote::BlockchainDB &db, const
if (!execute_city(db, game.cities[i]))
return false;
}
db.add_cc_events(db.height() - 1, last_events);
return true;
}
@ -273,15 +274,15 @@ bool cc_command_handler_game_update::revert(cryptonote::BlockchainDB &db, const
{
const cryptonote::cc_command_game_update_t &game = boost::get<cryptonote::cc_command_game_update_t>(cmd);
db.remove_cc_events(db.height() - 1);
for (size_t i = 0; i < game.cities.size(); ++i)
{
if (!revert_city(db, game.cities[game.cities.size() - 1 - i]))
const cryptonote::cc_command_game_update_t::city_t &city = game.cities[game.cities.size() - 1 - i];
if (!revert_city(db, city))
return false;
}
// technically we should fill up with previous game update's events, but no way to easily do that
events.clear();
return true;
}

View File

@ -45,7 +45,8 @@ public:
static cc_command_handler_game_update instance;
static cc::game_update_events_t events;
private:
static cc::game_update_events_t last_events;
};
}

View File

@ -269,7 +269,7 @@ namespace cryptonote
return is_v1_tx(blobdata_ref{tx_blob.data(), tx_blob.size()});
}
//---------------------------------------------------------------
bool is_game_update_block(uint64_t height, const crypto::hash &prev_hash)
bool is_game_update_block(uint64_t height)
{
return height % GAME_UPDATE_FREQUENCY == GAME_UPDATE_FREQUENCY-1;
}

View File

@ -56,7 +56,7 @@ namespace cryptonote
bool parse_and_validate_tx_base_from_blob(const blobdata& tx_blob, transaction& tx);
bool is_v1_tx(const blobdata_ref& tx_blob);
bool is_v1_tx(const blobdata& tx_blob);
bool is_game_update_block(uint64_t height, const crypto::hash &prev_hash);
bool is_game_update_block(uint64_t height);
template<typename T>
bool find_tx_extra_field_by_type(const std::vector<tx_extra_field>& tx_extra_fields, T& field, size_t index = 0)

View File

@ -1184,7 +1184,7 @@ bool Blockchain::prevalidate_miner_transaction(const block& b, uint64_t height,
if (b.miner_tx.version == 2)
{
CHECK_AND_ASSERT_MES(b.miner_tx.extra.size() <= 256, false, "v2 coinbase extra is too large");
const bool is_update = b.major_version >= 12 && cryptonote::is_game_update_block(height, b.prev_id);
const bool is_update = b.major_version >= 12 && cryptonote::is_game_update_block(height);
CHECK_AND_ASSERT_MES((b.miner_tx.minor_version > 0) == is_update, false, "Update block does not match minor version");
if (b.miner_tx.minor_version == 0)
{
@ -1279,7 +1279,7 @@ bool Blockchain::validate_miner_transaction(const block& b, size_t cumulative_bl
base_reward = 0;
}
const bool is_game_update = version >= 12 && cryptonote::is_game_update_block(cryptonote::get_block_height(b), get_tail_id());
const bool is_game_update = version >= 12 && cryptonote::is_game_update_block(cryptonote::get_block_height(b));
if (is_game_update)
{
CHECK_AND_ASSERT_MES(b.miner_tx.cc_cmd.type() == typeid(cryptonote::cc_command_game_update_t), false, "Game update command has wrong type");
@ -1556,7 +1556,7 @@ bool Blockchain::create_block_template(block& b, const crypto::hash *from_block,
uint8_t hf_version = b.major_version;
size_t max_outs = hf_version >= 4 ? 1 : 11;
cryptonote::cc_command_t cc_cmd;
const bool is_game_update = hf_version >= 12 && cryptonote::is_game_update_block(height, get_tail_id());
const bool is_game_update = hf_version >= 12 && cryptonote::is_game_update_block(height);
if (is_game_update)
{
cc_cmd = cc::create_cc_game_update_command(*m_db, false);

View File

@ -3728,6 +3728,8 @@ namespace cryptonote
++flag_id;
}
res.top_hash = db.top_block_hash(&res.height);
cryptonote::cc_shares_data_t sd;
if (!db.get_cc_shares(req.city, db.height() - 1, sd))
{
@ -3740,15 +3742,21 @@ namespace cryptonote
res.snapshot.shares.push_back({(uint8_t)role, sd.payout[role], sd.shares[role]});
}
const auto &events = cc::cc_command_handler_game_update::events;
res.snapshot.last_update_events.reserve(events.size());
std::vector<cc::game_update_event_t> events;
uint64_t height = res.height;
while (height > 0 && !cryptonote::is_game_update_block(height))
--height;
if (!db.get_cc_events(height, 0, 0, events))
{
res.status = "Internal error: can't get events data";
MERROR(res.status);
return false;
}
for (const auto &e: events)
{
res.snapshot.last_update_events.push_back({e.account, e.flag, e.event});
}
res.top_hash = db.top_block_hash(&res.height);
block blk;
bool orphan = false;
bool have_block = m_core.get_block_by_hash(res.top_hash, blk, &orphan);
@ -3919,15 +3927,26 @@ namespace cryptonote
try
{
res.events.reserve(cc::cc_command_handler_game_update::events.size());
for (const auto &e: cc::cc_command_handler_game_update::events)
{
res.events.push_back({e.account, e.flag, e.event});
}
crypto::hash top_hash;
m_core.get_blockchain_top(res.height, top_hash);
++res.height; // turn top block height into blockchain height
res.top_hash = epee::string_tools::pod_to_hex(top_hash);
BlockchainDB &db = m_core.get_blockchain_storage().get_db();
std::vector<cc::game_update_event_t> events;
uint64_t height = res.height;
while (height > 0 && !cryptonote::is_game_update_block(height))
--height;
if (!db.get_cc_events(height, 0, 0, events))
{
res.status = "Internal error: can't get events data";
MERROR(res.status);
return false;
}
for (const auto &e: events)
{
res.events.push_back({e.account, e.flag, e.event});
}
}
catch (const std::exception &e)
{

View File

@ -532,7 +532,7 @@ bool gen_cc_tx_validation_base::generate_with_full(std::vector<test_event_entry>
12, 12, blk_last.timestamp + DIFFICULTY_BLOCKS_ESTIMATE_TIMESPAN * 2, // v2 has blocks twice as long
crypto::hash(), 0, transaction(), std::vector<crypto::hash>(), 0, 0, 12),
false, "Failed to generate block");
if (cryptonote::is_game_update_block(cryptonote::get_block_height(blk), blk.prev_id))
if (cryptonote::is_game_update_block(cryptonote::get_block_height(blk)))
{
blk.miner_tx.minor_version = 1;
DO_CALLBACK(events, "generate_game_update_command");
@ -540,7 +540,7 @@ bool gen_cc_tx_validation_base::generate_with_full(std::vector<test_event_entry>
events.push_back(blk);
blk_last = blk;
blockchain.push_back(blk);
if (cryptonote::is_game_update_block(cryptonote::get_block_height(blk), blk.prev_id))
if (cryptonote::is_game_update_block(cryptonote::get_block_height(blk)))
break;
}
}

View File

@ -2256,3 +2256,102 @@ TEST(cc_influence, roles)
ASSERT_EQ(100, cc::calculate_influence(ROLE_RESIDENTIAL1, std::vector<uint32_t>{0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0}.data(), 0, 0, events));
ASSERT_EQ( 90, cc::calculate_influence(ROLE_RESIDENTIAL1, std::vector<uint32_t>{0, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0}.data(), 0, 0, events));
}
TEST(cc, events)
{
cryptonote::BlockchainDB *db = cryptonote::new_db();
ASSERT_TRUE(db);
boost::filesystem::path path = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path();
const std::string filename = (path / db->get_db_name()).string();
db->open(filename, 0);
cryptonote::db_wtxn_guard guard(db);
// starts off empty
std::vector<cc::game_update_event_t> events;
ASSERT_TRUE(db->get_cc_events(0, 0, 0, events));
ASSERT_TRUE(events.empty());
// can delete nothing
db->remove_cc_events(7434);
ASSERT_TRUE(db->get_cc_events(0, 0, 0, events));
ASSERT_TRUE(events.empty());
// add a couple at height 127
db->add_cc_events(127, {{4, 1, "building 1"}, {4, 0, "no building"}});
ASSERT_TRUE(db->get_cc_events(127, 0, 0, events));
ASSERT_EQ(events.size(), 2);
// add a bundle at 200
db->add_cc_events(200, {
{2, 0, "account 2"},
{4, 1, "account 4, building 1, event 0"},
{5, 2, "account 5, building 2, event 0"},
{4, 1, "account 4, building 1, event 1"},
{4, 1, "account 4, building 1, event 2"},
{5, 3, "account 5, building 3, event 0"},
{6, 4, "account 6, building 4, event 0"},
{4, 5, "account 4, building 5, event 0"},
{4, 0, "account 4"},
{1, 0, "account 1"}
});
ASSERT_TRUE(db->get_cc_events(200, 0, 0, events));
ASSERT_EQ(events.size(), 10);
ASSERT_TRUE(db->get_cc_events(200, 4, 0, events));
ASSERT_EQ(events.size(), 5);
ASSERT_TRUE(db->get_cc_events(200, 5, 0, events));
ASSERT_EQ(events.size(), 2);
ASSERT_TRUE(db->get_cc_events(200, 6, 0, events));
ASSERT_EQ(events.size(), 1);
ASSERT_TRUE(db->get_cc_events(200, 7, 6, events));
ASSERT_EQ(events.size(), 0);
ASSERT_TRUE(db->get_cc_events(200, 8, 0, events));
ASSERT_EQ(events.size(), 0);
ASSERT_TRUE(db->get_cc_events(200, 4, 4, events));
ASSERT_EQ(events.size(), 0);
ASSERT_TRUE(db->get_cc_events(200, 4, 1, events));
ASSERT_EQ(events.size(), 3);
ASSERT_TRUE(db->get_cc_events(200, 4, 5, events));
ASSERT_EQ(events.size(), 1);
ASSERT_TRUE(db->get_cc_events(200, 5, 1, events));
ASSERT_EQ(events.size(), 0);
ASSERT_TRUE(db->get_cc_events(200, 1, 0, events));
ASSERT_EQ(events.size(), 1);
// 127 still exists, unchanged
ASSERT_TRUE(db->get_cc_events(127, 0, 0, events));
ASSERT_EQ(events.size(), 2);
// add at 240
db->add_cc_events(240, {{4, 1, "building 1"}, {4, 0, "no building"}});
ASSERT_TRUE(db->get_cc_events(240, 0, 0, events));
ASSERT_EQ(events.size(), 2);
ASSERT_TRUE(db->get_cc_events(200, 0, 0, events));
ASSERT_EQ(events.size(), 10);
// delete 200
db->remove_cc_events(200);
ASSERT_TRUE(db->get_cc_events(240, 0, 0, events));
ASSERT_EQ(events.size(), 2);
ASSERT_TRUE(db->get_cc_events(200, 0, 0, events));
ASSERT_EQ(events.size(), 0);
ASSERT_TRUE(db->get_cc_events(127, 0, 0, events));
ASSERT_EQ(events.size(), 2);
// we can add 200 back
db->add_cc_events(200, {
{2, 0, "account 2"},
{4, 1, "account 4, building 1, event 0"},
{5, 2, "account 5, building 2, event 0"},
{4, 1, "account 4, building 1, event 1"},
{4, 1, "account 4, building 1, event 2"},
{5, 3, "account 5, building 3, event 0"},
{6, 4, "account 6, building 4, event 0"},
{4, 5, "account 4, building 5, event 0"},
{4, 0, "account 4"},
{1, 0, "account 1"}
});
ASSERT_TRUE(db->get_cc_events(200, 0, 0, events));
ASSERT_EQ(events.size(), 10);
}