Skip to content

Commit 84f2c29

Browse files
Merge pull request #5520 from knst/asset-lock-ranges
feat!: improves Credit Pool to protect from cumulative increasing amount of gaps
2 parents 690f47c + aeef1a6 commit 84f2c29

File tree

9 files changed

+208
-152
lines changed

9 files changed

+208
-152
lines changed

src/Makefile.am

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ BITCOIN_CORE_H = \
326326
util/underlying.h \
327327
util/serfloat.h \
328328
util/settings.h \
329-
util/skip_set.h \
329+
util/ranges_set.h \
330330
util/sock.h \
331331
util/string.h \
332332
util/time.h \
@@ -744,7 +744,7 @@ libbitcoin_util_a_SOURCES = \
744744
util/moneystr.cpp \
745745
util/readwritefile.cpp \
746746
util/settings.cpp \
747-
util/skip_set.cpp \
747+
util/ranges_set.cpp \
748748
util/spanparsing.cpp \
749749
util/strencodings.cpp \
750750
util/time.cpp \

src/evo/creditpool.cpp

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ static UnlockDataPerBlock GetDataFromUnlockTxes(const std::vector<CTransactionRe
7070

7171
std::string CCreditPool::ToString() const
7272
{
73-
return strprintf("CCreditPool(locked=%lld, currentLimit=%lld, nIndexes=%lld)",
74-
locked, currentLimit, indexes.Size());
73+
return strprintf("CCreditPool(locked=%lld, currentLimit=%lld)",
74+
locked, currentLimit);
7575
}
7676

7777
std::optional<CCreditPool> CCreditPoolManager::GetFromCache(const CBlockIndex* const block_index)
@@ -130,7 +130,7 @@ CCreditPool CCreditPoolManager::ConstructCreditPool(const CBlockIndex* const blo
130130
// If reading of previous block is not successfully, but
131131
// prev contains credit pool related data, something strange happened
132132
assert(prev.locked == 0);
133-
assert(prev.indexes.Size() == 0);
133+
assert(prev.indexes.IsEmpty());
134134

135135
CCreditPool emptyPool;
136136
AddToCache(block_index->GetBlockHash(), block_index->nHeight, emptyPool);
@@ -150,9 +150,9 @@ CCreditPool CCreditPoolManager::ConstructCreditPool(const CBlockIndex* const blo
150150
// Indexes should not be duplicated since genesis block, but the Unlock Amount
151151
// of withdrawal transaction is limited only by this window
152152
UnlockDataPerBlock blockData = GetDataFromUnlockTxes(block->vtx);
153-
CSkipSet indexes{std::move(prev.indexes)};
153+
CRangesSet indexes{std::move(prev.indexes)};
154154
if (std::any_of(blockData.indexes.begin(), blockData.indexes.end(), [&](const uint64_t index) { return !indexes.Add(index); })) {
155-
throw std::runtime_error(strprintf("%s: failed-getcreditpool-index-exceed", __func__));
155+
throw std::runtime_error(strprintf("%s: failed-getcreditpool-index-duplicated", __func__));
156156
}
157157

158158
const CBlockIndex* distant_block_index = block_index;
@@ -266,10 +266,6 @@ bool CCreditPoolDiff::Unlock(const CTransaction& tx, TxValidationState& state)
266266
return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-creditpool-unlock-duplicated-index");
267267
}
268268

269-
if (!pool.indexes.CanBeAdded(index)) {
270-
return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-creditpool-unlock-cant-add");
271-
}
272-
273269
newIndexes.insert(index);
274270
sessionUnlocked += toUnlock;
275271
return true;

src/evo/creditpool.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
#include <sync.h>
1616
#include <threadsafety.h>
1717
#include <unordered_lru_cache.h>
18-
#include <util/skip_set.h>
18+
#include <util/ranges_set.h>
1919

2020
#include <optional>
2121
#include <unordered_set>
@@ -34,7 +34,7 @@ struct CCreditPool {
3434
// needs for logic of limits of unlocks
3535
CAmount currentLimit{0};
3636
CAmount latelyUnlocked{0};
37-
CSkipSet indexes{};
37+
CRangesSet indexes{};
3838

3939
std::string ToString() const;
4040

src/test/util_tests.cpp

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
#include <util/getuniquepath.h>
1616
#include <util/message.h> // For MessageSign(), MessageVerify(), MESSAGE_MAGIC
1717
#include <util/moneystr.h>
18-
#include <util/skip_set.h>
18+
#include <util/ranges_set.h>
1919
#include <util/spanparsing.h>
2020
#include <util/strencodings.h>
2121
#include <util/string.h>
@@ -2167,27 +2167,28 @@ BOOST_AUTO_TEST_CASE(test_Capitalize)
21672167
BOOST_CHECK_EQUAL(Capitalize("\x00\xfe\xff"), "\x00\xfe\xff");
21682168
}
21692169

2170-
BOOST_AUTO_TEST_CASE(test_SkipSet)
2170+
BOOST_AUTO_TEST_CASE(test_CRanges)
21712171
{
21722172
std::mt19937 gen;
21732173
for (size_t test = 0; test < 17; ++test) {
21742174
std::uniform_int_distribution<uint64_t> dist_value(0, (1 << test));
2175-
size_t skip_size = test ? (1 << (test - 1)) : 1;
2176-
CSkipSet set_1{skip_size};
2175+
CRangesSet ranges;
21772176
std::unordered_set<uint64_t> set_2;
21782177
for (size_t iter = 0; iter < (1 << test) * 2; ++iter) {
21792178
uint64_t value = dist_value(gen);
2180-
BOOST_CHECK(set_1.Contains(value) == !!set_2.count(value));
2181-
if (!set_1.Contains(value) && set_1.CanBeAdded(value)) {
2182-
BOOST_CHECK(!set_1.Contains(value));
2183-
BOOST_CHECK(set_1.Add(value));
2179+
BOOST_CHECK_EQUAL(ranges.Contains(value), !!set_2.count(value));
2180+
if (!ranges.Contains(value)) {
2181+
BOOST_CHECK(ranges.Add(value));
21842182
set_2.insert(value);
2183+
} else {
2184+
BOOST_CHECK(ranges.Remove(value));
2185+
set_2.erase(set_2.find(value));
21852186
}
2186-
BOOST_CHECK(set_1.Contains(value) == !!set_2.count(value));
2187-
BOOST_CHECK(set_1.Size() == set_2.size());
2187+
BOOST_CHECK_EQUAL(ranges.Contains(value), !!set_2.count(value));
2188+
BOOST_CHECK_EQUAL(ranges.Size(), set_2.size());
21882189
}
21892190
if (test > 4) {
2190-
BOOST_CHECK(set_1.Size() > ((1 << test) / 4));
2191+
BOOST_CHECK(ranges.Size() > ((1 << test) / 4));
21912192
}
21922193
}
21932194
}

src/util/ranges_set.cpp

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright (c) 2023 The Dash Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <util/ranges_set.h>
6+
7+
CRangesSet::Range::Range() : CRangesSet::Range::Range(0, 0) {}
8+
9+
CRangesSet::Range::Range(uint64_t begin, uint64_t end) :
10+
begin(begin),
11+
end(end)
12+
{
13+
}
14+
15+
bool CRangesSet::Add(uint64_t value)
16+
{
17+
if (Contains(value)) return false;
18+
19+
// If element is not in CRangesSet, we need to add a new range [value, value + 1)
20+
// But this operation can cause 2 operation of merge (total 3 cases):
21+
// - if there're exist ranges [x, value) and [value + 1, y) than
22+
// all 3 of them should be merged in one range [x, y)
23+
// - if there's exist a range [x, value) - we need to replace it to new range [x, value + 1)
24+
// - if there's exist a range [value + 1, y) - we need to replace it to new range [value, y)
25+
Range new_range{value, value + 1};
26+
auto it = ranges.lower_bound({value, value});
27+
if (it != ranges.begin()) {
28+
auto prev = it;
29+
--prev;
30+
31+
if (prev->end == value) {
32+
new_range.begin = prev->begin;
33+
it = ranges.erase(prev);
34+
}
35+
}
36+
const auto next = it;
37+
if (next != ranges.end()) {
38+
if (next->begin == value + 1) {
39+
new_range.end = next->end;
40+
it = ranges.erase(next);
41+
}
42+
}
43+
const auto ret = ranges.insert(new_range);
44+
assert(ret.second);
45+
return true;
46+
}
47+
48+
bool CRangesSet::Remove(uint64_t value)
49+
{
50+
if (!Contains(value)) return false;
51+
52+
// If element is in CRangesSet, there's possible 2 cases:
53+
// - value in the range that is lower-bound (if begin of that range equals to `value`)
54+
// - value in the previous range (otherwise)
55+
auto it = ranges.lower_bound({value, value});
56+
57+
if (it == ranges.end() || it->begin != value) {
58+
assert(it != ranges.begin());
59+
--it;
60+
}
61+
62+
const Range current_range = *it;
63+
ranges.erase(it);
64+
if (current_range.begin != value) {
65+
const auto ret = ranges.insert({current_range.begin, value});
66+
assert(ret.second);
67+
}
68+
if (value + 1 != current_range.end) {
69+
const auto ret = ranges.insert({value + 1, current_range.end});
70+
assert(ret.second);
71+
}
72+
return true;
73+
}
74+
75+
bool CRangesSet::IsEmpty() const noexcept
76+
{
77+
return ranges.empty();
78+
}
79+
80+
size_t CRangesSet::Size() const noexcept
81+
{
82+
size_t result{0};
83+
for (auto i : ranges) {
84+
result += i.end - i.begin;
85+
}
86+
return result;
87+
}
88+
89+
bool CRangesSet::Contains(uint64_t value) const noexcept
90+
{
91+
const auto it = ranges.lower_bound({value, value});
92+
if (it != ranges.end() && it->begin == value) return true;
93+
if (it == ranges.begin()) return false;
94+
auto prev = it;
95+
--prev;
96+
return prev->begin <= value && prev->end > value;
97+
}
98+

src/util/ranges_set.h

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright (c) 2023 The Dash Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#ifndef BITCOIN_UTIL_RANGES_SET_H
6+
#define BITCOIN_UTIL_RANGES_SET_H
7+
8+
#include <hash.h>
9+
#include <saltedhasher.h>
10+
#include <serialize.h>
11+
12+
#include <set>
13+
14+
/**
15+
* The CRangesSet is a datastructure that keeps efficiently numbers as set of
16+
* continuous ranges of numbers.
17+
* CRangesSet let's to keep elements with gaps of any size while CSkipList has
18+
* limited capacity (total size of all gaps)
19+
*
20+
* The CRangesSet provides transaction guarantees: element can be added or
21+
* removed and data structure will be consistent. For case if any of these
22+
* operation failed (out-memory for example), the `assert` will be called to
23+
* terminate program.
24+
*/
25+
class CRangesSet
26+
{
27+
// internal datastructure, doesn't have a reason to be publicly available
28+
struct Range
29+
{
30+
uint64_t begin;
31+
uint64_t end;
32+
Range();
33+
Range(uint64_t begin, uint64_t end);
34+
bool operator<(const Range& other) const
35+
{
36+
if (begin != other.begin) return begin < other.begin;
37+
return end < other.end;
38+
}
39+
40+
SERIALIZE_METHODS(Range, obj)
41+
{
42+
READWRITE(obj.begin);
43+
READWRITE(obj.end);
44+
}
45+
};
46+
47+
std::set<Range> ranges;
48+
49+
public:
50+
/**
51+
* this function adds `value` to the datastructure.
52+
* it returns true if `add` succeed
53+
*/
54+
[[nodiscard]] bool Add(uint64_t value);
55+
56+
/**
57+
* this function returns true if `value` exists in the datastructure
58+
*/
59+
[[nodiscard]] bool Contains(uint64_t value) const noexcept;
60+
61+
/**
62+
* this function removes `value` from the datastructure.
63+
* it returns `false` if element didn't existed or removing failed by any reason
64+
*/
65+
[[nodiscard]] bool Remove(uint64_t value);
66+
67+
/**
68+
* Size() works with complexity O(N) times, avoid calling it without a good reason
69+
* Instead prefer to use IsEmpty()
70+
*/
71+
[[nodiscard]] size_t Size() const noexcept;
72+
73+
/**
74+
* IsEmpty() returns true if there's no any elements added
75+
*/
76+
[[nodiscard]] bool IsEmpty() const noexcept;
77+
78+
SERIALIZE_METHODS(CRangesSet, obj)
79+
{
80+
READWRITE(obj.ranges);
81+
}
82+
};
83+
84+
#endif // BITCOIN_UTIL_RANGES_SET_H

src/util/skip_set.cpp

Lines changed: 0 additions & 53 deletions
This file was deleted.

0 commit comments

Comments
 (0)