Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
59bc2ea
feat: add the evo snapshot v3 canonical bounded codec and context-fre…
PastaPastaPasta Aug 13, 2026
00228ce
fix: build the ReadFixedBitSet trailing-bits mask without an implicit…
PastaPastaPasta Aug 13, 2026
4488b64
test: cover the bounded CRangesSet unserializer
PastaPastaPasta Aug 13, 2026
0765965
fix: allow partial evo snapshot quorum history
PastaPastaPasta Aug 13, 2026
5aea447
fix: reject full-domain ranges during deserialization
PastaPastaPasta Aug 13, 2026
aea2e8c
fix: reject noncanonical MNHF signal order during deserialization
PastaPastaPasta Aug 20, 2026
5967566
fix: bound commitment bitsets by a format ceiling instead of static L…
PastaPastaPasta Aug 20, 2026
fdaff57
fix: validate credit-pool amounts and MNHF signal semantics context-free
PastaPastaPasta Aug 20, 2026
8d4ac9b
fix: replace previous contents when deserializing an evo snapshot
PastaPastaPasta Aug 20, 2026
8512a65
fix: bound same-prefix proTxHash runs against HAMT collision blowup
PastaPastaPasta Aug 20, 2026
776188b
fix: bind the CbTx height in VerifyEvoSnapshotCbTx
PastaPastaPasta Aug 20, 2026
1ea1e01
fix: replace per-quorum contents when deserializing CQuorumSnapshotData
PastaPastaPasta Aug 20, 2026
ec1ef2d
fix: bound cumulative historical MN-list reconstruction work
PastaPastaPasta Aug 20, 2026
1a90e7e
refactor: drop the C prefix from the new evo snapshot types
PastaPastaPasta Aug 28, 2026
babf47e
refactor: give every evo snapshot collection one explicit canonical c…
PastaPastaPasta Aug 28, 2026
da25ab6
build: list evo/snapshot.cpp where it is actually used
PastaPastaPasta Aug 28, 2026
7183a4d
fix: enforce the decoder's historical operation budget during validation
PastaPastaPasta Aug 28, 2026
c94bb8f
test: exercise evo per-quorum replacement for every cleared vector
PastaPastaPasta Aug 28, 2026
ccf37fe
test: name the noncanonical CRangesSet case for its actual defect
PastaPastaPasta Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 5 additions & 46 deletions src/evo/snapshot.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,55 +23,14 @@
#include <map>
#include <set>
#include <stdexcept>
#include <tuple>
#include <type_traits>

namespace evo {
namespace {

template <typename T>
std::vector<T> Sorted(std::vector<T> values)
{
std::sort(values.begin(), values.end(), [](const T& a, const T& b) {
if constexpr (std::is_same_v<T, MinedQuorumCommitment>) {
return std::tie(a.quorum_base_block_hash, a.mined_block_hash) <
std::tie(b.quorum_base_block_hash, b.mined_block_hash);
} else if constexpr (std::is_same_v<T, QuorumSnapshotEntry>) {
return a.cycle_base_block_hash < b.cycle_base_block_hash;
} else if constexpr (std::is_same_v<T, HistoricalMNListDiff>) {
return std::tie(a.height, a.block_hash) > std::tie(b.height, b.block_hash);
} else if constexpr (std::is_same_v<T, QuorumModifier>) {
return std::tie(a.llmq_type, a.work_block_hash) < std::tie(b.llmq_type, b.work_block_hash);
} else {
return a.llmq_type < b.llmq_type;
}
});
return values;
}

template <typename T>
bool IsStrictlySorted(const std::vector<T>& values)
{
return std::adjacent_find(values.begin(), values.end(), [](const T& a, const T& b) {
if constexpr (std::is_same_v<T, MinedQuorumCommitment>) {
return std::tie(a.quorum_base_block_hash, a.mined_block_hash) >=
std::tie(b.quorum_base_block_hash, b.mined_block_hash);
} else if constexpr (std::is_same_v<T, QuorumSnapshotEntry>) {
return !(a.cycle_base_block_hash < b.cycle_base_block_hash);
} else if constexpr (std::is_same_v<T, HistoricalMNListDiff>) {
return !(std::tie(a.height, a.block_hash) > std::tie(b.height, b.block_hash));
} else if constexpr (std::is_same_v<T, QuorumModifier>) {
return !(std::tie(a.llmq_type, a.work_block_hash) < std::tie(b.llmq_type, b.work_block_hash));
} else {
return a.llmq_type >= b.llmq_type;
}
}) == values.end();
}

void ValidateCommitments(const QuorumSnapshotData& data, const std::vector<MinedQuorumCommitment>& commitments,
std::set<uint256>& quorum_hashes, bool require_canonical_order)
{
if (require_canonical_order && !IsStrictlySorted(commitments)) {
if (require_canonical_order && !IsCanonicallySorted(commitments)) {
throw std::ios_base::failure("noncanonical evo quorum commitments");
}
std::set<int16_t> quorum_indexes;
Expand Down Expand Up @@ -192,7 +151,7 @@ bool ReconstructHistoricalMNLists(const EvoSnapshot& snapshot, std::map<uint256,
int previous_height{current.GetHeightForSnapshotCodec()};
size_t records_processed{0};
try {
const auto history{Sorted(snapshot.historical_mn_list_diffs)};
const auto history{SortedCanonically(snapshot.historical_mn_list_diffs)};
for (const auto& entry : history) {
if (entry.previous_block_hash != previous_hash || entry.block_hash.IsNull() ||
entry.height < 0 || entry.height >= previous_height || entry.canonical_list_hash.IsNull()) {
Expand Down Expand Up @@ -263,8 +222,8 @@ void EvoSnapshot::Validate(bool require_canonical_order) const
throw std::ios_base::failure("invalid evo snapshot MNHF signal");
}
}
if (require_canonical_order && (!IsStrictlySorted(quorums) || !IsStrictlySorted(historical_mn_list_diffs) ||
!IsStrictlySorted(quorum_modifiers))) {
if (require_canonical_order && (!IsCanonicallySorted(quorums) || !IsCanonicallySorted(historical_mn_list_diffs) ||
!IsCanonicallySorted(quorum_modifiers))) {
throw std::ios_base::failure("noncanonical evo snapshot top-level order");
}

Expand Down Expand Up @@ -305,7 +264,7 @@ void EvoSnapshot::Validate(bool require_canonical_order) const
required_modifiers.emplace(data.llmq_type, entry.work_block_hash);
}
}
if (require_canonical_order && !IsStrictlySorted(data.rotation_snapshots)) {
if (require_canonical_order && !IsCanonicallySorted(data.rotation_snapshots)) {
throw std::ios_base::failure("noncanonical evo quorum rotation snapshots");
}
std::set<uint256> cycle_hashes;
Expand Down
74 changes: 52 additions & 22 deletions src/evo/snapshot.h
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,52 @@ class EvoSnapshot
void Validate(bool require_canonical_order = false) const;
};

/**
* Canonical wire order, one overload per snapshot collection element. The
* serializer, the decode-time order check, and the object-level order check
* all derive from these, so a type without an overload is a compile error
* rather than a silently accepted order.
*/
inline bool IsCanonicallyBefore(const MinedQuorumCommitment& a, const MinedQuorumCommitment& b)
{
return std::tie(a.quorum_base_block_hash, a.mined_block_hash) < std::tie(b.quorum_base_block_hash, b.mined_block_hash);
}

inline bool IsCanonicallyBefore(const QuorumSnapshotEntry& a, const QuorumSnapshotEntry& b)
{
return a.cycle_base_block_hash < b.cycle_base_block_hash;
}

/** Historical diffs descend by height: the chain is replayed newest first. */
inline bool IsCanonicallyBefore(const HistoricalMNListDiff& a, const HistoricalMNListDiff& b)
{
return std::tie(a.height, a.block_hash) > std::tie(b.height, b.block_hash);
}

inline bool IsCanonicallyBefore(const QuorumModifier& a, const QuorumModifier& b)
{
return std::tie(a.llmq_type, a.work_block_hash) < std::tie(b.llmq_type, b.work_block_hash);
}

inline bool IsCanonicallyBefore(const QuorumSnapshotData& a, const QuorumSnapshotData& b)
{
return a.llmq_type < b.llmq_type;
}

template <typename T>
std::vector<T> SortedCanonically(std::vector<T> values)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naming a bit confusing now because it's not only return values as a "sorted copy", but also change value itself.

So,

tmp = v;
u = SortedCanonically(v);
assert(tmp == v); <-- fail

Also consider rewrting code below (see 2nd comment for snapshot.h)

{
std::sort(values.begin(), values.end(), [](const T& a, const T& b) { return IsCanonicallyBefore(a, b); });
return values;
}

template <typename T>
bool IsCanonicallySorted(const std::vector<T>& values)
{
return std::adjacent_find(values.begin(), values.end(),
[](const T& a, const T& b) { return !IsCanonicallyBefore(a, b); }) == values.end();
}

template <typename Stream, typename T, typename WriteOne>
void WriteSnapshotVector(Stream& s, const std::vector<T>& values, WriteOne&& write_one)
{
Expand Down Expand Up @@ -501,17 +547,9 @@ QuorumSnapshotEntry ReadRotationSnapshot(Stream& s, const Consensus::LLMQParams&
template <typename Stream>
void QuorumSnapshotData::Serialize(Stream& s) const
{
auto active{active_commitments};
auto safety{safety_commitments};
auto snapshots{rotation_snapshots};
const auto commitment_less = [](const auto& a, const auto& b) {
return std::tie(a.quorum_base_block_hash, a.mined_block_hash) <
std::tie(b.quorum_base_block_hash, b.mined_block_hash);
};
std::sort(active.begin(), active.end(), commitment_less);
std::sort(safety.begin(), safety.end(), commitment_less);
std::sort(snapshots.begin(), snapshots.end(),
[](const auto& a, const auto& b) { return a.cycle_base_block_hash < b.cycle_base_block_hash; });
const auto active{SortedCanonically(active_commitments)};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SortedCannonically updates value itself.

Instead do:

SortCanonically(active_commitments);
SortCanonically(safety_commitments);
SortCanonically(rotation_snapshots);

s << llmq_type << rotation_enabled << active_commitment << safety_commitment;

and continue so for other members

const auto safety{SortedCanonically(safety_commitments)};
const auto snapshots{SortedCanonically(rotation_snapshots)};
s << llmq_type << rotation_enabled << active << safety;
WriteSnapshotVector(s, snapshots, [&](const auto& entry) { WriteRotationSnapshot(s, entry); });
}
Expand Down Expand Up @@ -547,17 +585,9 @@ void QuorumSnapshotData::Unserialize(Stream& s)
template <typename Stream>
void EvoSnapshot::Serialize(Stream& s) const
{
auto sorted_quorums{quorums};
auto sorted_history{historical_mn_list_diffs};
auto sorted_modifiers{quorum_modifiers};
std::sort(sorted_quorums.begin(), sorted_quorums.end(),
[](const auto& a, const auto& b) { return a.llmq_type < b.llmq_type; });
std::sort(sorted_history.begin(), sorted_history.end(), [](const auto& a, const auto& b) {
return std::tie(a.height, a.block_hash) > std::tie(b.height, b.block_hash);
});
std::sort(sorted_modifiers.begin(), sorted_modifiers.end(), [](const auto& a, const auto& b) {
return std::tie(a.llmq_type, a.work_block_hash) < std::tie(b.llmq_type, b.work_block_hash);
});
const auto sorted_quorums{SortedCanonically(quorums)};
const auto sorted_history{SortedCanonically(historical_mn_list_diffs)};
const auto sorted_modifiers{SortedCanonically(quorum_modifiers)};
s << version << base_block_hash;
SerializeCanonicalMNList(s, mn_list);
s << sorted_quorums;
Expand Down