mirror of
https://github.com/Xahau/xahaud.git
synced 2026-07-23 15:10:10 +00:00
refactor(export): remove legacy collector lane
This commit is contained in:
@@ -1,271 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES WHATSOEVER
|
||||
RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||||
CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/misc/ExportSigCollectorV2.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
namespace {
|
||||
|
||||
uint256
|
||||
origin(std::uint32_t value)
|
||||
{
|
||||
Serializer s;
|
||||
s.add32(value);
|
||||
return sha512Half(s.slice());
|
||||
}
|
||||
|
||||
PublicKey
|
||||
publicKey(char const* hex)
|
||||
{
|
||||
auto const raw = strUnHex(hex);
|
||||
return PublicKey{makeSlice(*raw)};
|
||||
}
|
||||
|
||||
Buffer
|
||||
signature(std::uint8_t value)
|
||||
{
|
||||
std::uint8_t bytes[] = {value, std::uint8_t(value + 1)};
|
||||
return Buffer{bytes, sizeof(bytes)};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class ExportSigCollectorV2_test : public beast::unit_test::suite
|
||||
{
|
||||
PublicKey const keyA_ = publicKey(
|
||||
"0388935426E0D08083314842EDFBB2D517BD47699F9A4527318A8E10468C97C05"
|
||||
"2");
|
||||
PublicKey const keyB_ = randomKeyPair(KeyType::secp256k1).first;
|
||||
|
||||
static ExportSigCollectorV2::Contribution
|
||||
contribution(
|
||||
ExportSigCollectorV2::Position position,
|
||||
PublicKey const& key,
|
||||
std::uint8_t sig)
|
||||
{
|
||||
return {position, key, signature(sig)};
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
testAdmissionAndConflict()
|
||||
{
|
||||
testcase("v2 admission and absorbing conflict");
|
||||
|
||||
ExportSigCollectorV2 collector;
|
||||
auto const w = origin(1);
|
||||
auto const publication = collector.reopenPublication(w, w, 9);
|
||||
BEAST_EXPECT(publication.has_value());
|
||||
auto first = collector.beginAttributedAdmission(
|
||||
w, contribution(7, keyA_, 1), 10);
|
||||
BEAST_EXPECT(first.result == ExportSigCollectorV2::BeginResult::verify);
|
||||
BEAST_EXPECT(first.ticket.has_value());
|
||||
BEAST_EXPECT(collector.fullUnionSnapshot().empty());
|
||||
if (!first.ticket)
|
||||
return;
|
||||
|
||||
auto accepted =
|
||||
collector.admitContribution(std::move(*first.ticket), true, 10);
|
||||
BEAST_EXPECT(
|
||||
accepted.result == ExportSigCollectorV2::AdmitResult::accepted);
|
||||
auto snapshot = collector.fullUnionSnapshot();
|
||||
BEAST_EXPECT(snapshot.at(w).size() == 1);
|
||||
BEAST_EXPECT(snapshot.at(w).front().position == 7);
|
||||
|
||||
auto duplicate = collector.beginAttributedAdmission(
|
||||
w, contribution(7, keyA_, 1), 11);
|
||||
BEAST_EXPECT(
|
||||
duplicate.result == ExportSigCollectorV2::BeginResult::duplicate);
|
||||
|
||||
auto second = collector.beginAttributedAdmission(
|
||||
w, contribution(7, keyB_, 2), 11);
|
||||
auto third = collector.beginAttributedAdmission(
|
||||
w, contribution(7, keyA_, 3), 11);
|
||||
BEAST_EXPECT(second.ticket.has_value());
|
||||
BEAST_EXPECT(
|
||||
third.result == ExportSigCollectorV2::BeginResult::capacity);
|
||||
if (!second.ticket)
|
||||
return;
|
||||
|
||||
auto conflicted =
|
||||
collector.admitContribution(std::move(*second.ticket), true, 11);
|
||||
BEAST_EXPECT(
|
||||
conflicted.result == ExportSigCollectorV2::AdmitResult::conflicted);
|
||||
BEAST_EXPECT(conflicted.priorContribution.has_value());
|
||||
BEAST_EXPECT(conflicted.conflictingContribution.has_value());
|
||||
BEAST_EXPECT(
|
||||
collector.positionStatus(w, 7) ==
|
||||
ExportSigCollectorV2::PositionStatus::conflicted);
|
||||
BEAST_EXPECT(collector.fullUnionSnapshot().empty());
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(w, contribution(7, keyA_, 4), 12)
|
||||
.result == ExportSigCollectorV2::BeginResult::conflicted);
|
||||
}
|
||||
|
||||
void
|
||||
testReservationAndPublicationLifecycle()
|
||||
{
|
||||
testcase("v2 verification reservations and publication reopening");
|
||||
|
||||
ExportSigCollectorV2 collector;
|
||||
auto const w = origin(2);
|
||||
auto publication = collector.reopenPublication(w, w, 19);
|
||||
BEAST_EXPECT(publication.has_value());
|
||||
auto invalid = collector.beginAttributedAdmission(
|
||||
w, contribution(3, keyA_, 10), 20);
|
||||
BEAST_EXPECT(invalid.ticket.has_value());
|
||||
if (!invalid.ticket)
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
collector.admitContribution(std::move(*invalid.ticket), false, 20)
|
||||
.result == ExportSigCollectorV2::AdmitResult::invalid);
|
||||
|
||||
auto valid = collector.beginAttributedAdmission(
|
||||
w, contribution(3, keyA_, 11), 20);
|
||||
BEAST_EXPECT(valid.ticket.has_value());
|
||||
if (!valid.ticket)
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
collector.admitContribution(std::move(*valid.ticket), true, 20)
|
||||
.result == ExportSigCollectorV2::AdmitResult::accepted);
|
||||
|
||||
auto other = collector.beginAttributedAdmission(
|
||||
w, contribution(4, keyB_, 12), 20);
|
||||
BEAST_EXPECT(other.ticket.has_value());
|
||||
if (!other.ticket)
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
collector.admitContribution(std::move(*other.ticket), true, 20)
|
||||
.result == ExportSigCollectorV2::AdmitResult::accepted);
|
||||
|
||||
BEAST_EXPECT(publication.has_value());
|
||||
if (!publication)
|
||||
return;
|
||||
BEAST_EXPECT(collector.claimPublication(*publication, 3, 2));
|
||||
BEAST_EXPECT(collector.publicationGeneration(w) == 1);
|
||||
BEAST_EXPECT(!collector.claimPublication(*publication, 3, 2));
|
||||
|
||||
// Reopening the same trigger is idempotent and does not reset slots.
|
||||
auto samePublication = collector.reopenPublication(w, w, 21);
|
||||
BEAST_EXPECT(samePublication.has_value());
|
||||
if (!samePublication)
|
||||
return;
|
||||
BEAST_EXPECT(collector.publicationGeneration(w) == 1);
|
||||
BEAST_EXPECT(!collector.claimPublication(*samePublication, 3, 2));
|
||||
|
||||
auto const nextTrigger = origin(22);
|
||||
auto nextPublication = collector.reopenPublication(w, nextTrigger, 22);
|
||||
BEAST_EXPECT(nextPublication.has_value());
|
||||
if (!nextPublication)
|
||||
return;
|
||||
BEAST_EXPECT(collector.publicationGeneration(w) == 2);
|
||||
BEAST_EXPECT(!collector.claimPublication(*publication, 4, 2));
|
||||
BEAST_EXPECT(collector.claimPublication(*nextPublication, 4, 1));
|
||||
BEAST_EXPECT(!collector.reopenPublication(w, w, 23));
|
||||
BEAST_EXPECT(
|
||||
collector.positionStatus(w, 3) ==
|
||||
ExportSigCollectorV2::PositionStatus::unique);
|
||||
BEAST_EXPECT(collector.fullUnionSnapshot().at(w).size() == 2);
|
||||
|
||||
collector.cleanupStale(278);
|
||||
BEAST_EXPECT(!collector.fullUnionSnapshot().empty());
|
||||
collector.cleanupStale(279);
|
||||
BEAST_EXPECT(collector.fullUnionSnapshot().empty());
|
||||
}
|
||||
|
||||
void
|
||||
testMalformedBoundaries()
|
||||
{
|
||||
testcase("v2 malformed contribution boundaries");
|
||||
|
||||
ExportSigCollectorV2 collector;
|
||||
auto good = contribution(0, keyA_, 1);
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(uint256{}, good).result ==
|
||||
ExportSigCollectorV2::BeginResult::malformed);
|
||||
|
||||
auto const w = origin(3);
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(w, good, 1).result ==
|
||||
ExportSigCollectorV2::BeginResult::unknownOrigin);
|
||||
BEAST_EXPECT(collector.reopenPublication(w, w, 1).has_value());
|
||||
|
||||
good.position = ExportLimits::maxCommitteeMembers;
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(w, good, 1).result ==
|
||||
ExportSigCollectorV2::BeginResult::malformed);
|
||||
|
||||
good.position = 0;
|
||||
good.signature = Buffer{};
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(w, good, 1).result ==
|
||||
ExportSigCollectorV2::BeginResult::malformed);
|
||||
|
||||
std::vector<std::uint8_t> oversized(
|
||||
ExportSigCollectorV2::maxSignatureBytes + 1, 0xAB);
|
||||
good.signature = Buffer{oversized.data(), oversized.size()};
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(w, good, 1).result ==
|
||||
ExportSigCollectorV2::BeginResult::malformed);
|
||||
|
||||
good = contribution(5, keyA_, 5);
|
||||
auto abandoned = collector.beginAttributedAdmission(w, good, 2);
|
||||
BEAST_EXPECT(abandoned.ticket.has_value());
|
||||
if (!abandoned.ticket)
|
||||
return;
|
||||
BEAST_EXPECT(collector.cancelAdmission(std::move(*abandoned.ticket)));
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(w, good, 2).result ==
|
||||
ExportSigCollectorV2::BeginResult::verify);
|
||||
|
||||
auto expired =
|
||||
collector.beginAttributedAdmission(w, contribution(6, keyA_, 6), 3);
|
||||
BEAST_EXPECT(expired.ticket.has_value());
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(w, contribution(6, keyB_, 7), 5)
|
||||
.result == ExportSigCollectorV2::BeginResult::verify);
|
||||
|
||||
auto oldToken = collector.reopenPublication(w, origin(30), 6);
|
||||
BEAST_EXPECT(oldToken.has_value());
|
||||
collector.clear(w);
|
||||
auto newToken = collector.reopenPublication(w, origin(31), 7);
|
||||
BEAST_EXPECT(newToken.has_value());
|
||||
if (oldToken)
|
||||
BEAST_EXPECT(!collector.claimPublication(*oldToken, 0, 1));
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testAdmissionAndConflict();
|
||||
testReservationAndPublicationLifecycle();
|
||||
testMalformedBoundaries();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(ExportSigCollectorV2, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -3,16 +3,16 @@
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES WHATSOEVER
|
||||
RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||||
CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <cstring>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
@@ -30,444 +29,238 @@ namespace test {
|
||||
namespace {
|
||||
|
||||
uint256
|
||||
makeHash(char const* label)
|
||||
{
|
||||
return sha512Half(Slice(label, std::strlen(label)));
|
||||
}
|
||||
|
||||
uint256
|
||||
makeHashFromSeed(std::uint32_t seed)
|
||||
origin(std::uint32_t value)
|
||||
{
|
||||
Serializer s;
|
||||
s.add32(seed);
|
||||
s.add32(value);
|
||||
return sha512Half(s.slice());
|
||||
}
|
||||
|
||||
PublicKey
|
||||
makePublicKey(char const* hex)
|
||||
publicKey(char const* hex)
|
||||
{
|
||||
auto const raw = strUnHex(hex);
|
||||
return PublicKey{makeSlice(*raw)};
|
||||
}
|
||||
|
||||
Buffer
|
||||
makeSignature(std::uint8_t seed)
|
||||
signature(std::uint8_t value)
|
||||
{
|
||||
std::uint8_t bytes[] = {
|
||||
seed,
|
||||
static_cast<std::uint8_t>(seed + 1),
|
||||
static_cast<std::uint8_t>(seed + 2)};
|
||||
return Buffer(bytes, sizeof(bytes));
|
||||
std::uint8_t bytes[] = {value, std::uint8_t(value + 1)};
|
||||
return Buffer{bytes, sizeof(bytes)};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class ExportSigCollector_test : public beast::unit_test::suite
|
||||
{
|
||||
PublicKey const validator_ = makePublicKey(
|
||||
PublicKey const keyA_ = publicKey(
|
||||
"0388935426E0D08083314842EDFBB2D517BD47699F9A4527318A8E10468C97C05"
|
||||
"2");
|
||||
PublicKey const keyB_ = randomKeyPair(KeyType::secp256k1).first;
|
||||
|
||||
static ExportSigCollector::Contribution
|
||||
contribution(
|
||||
ExportSigCollector::Position position,
|
||||
PublicKey const& key,
|
||||
std::uint8_t sig)
|
||||
{
|
||||
return {position, key, signature(sig)};
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
testCleanupUsesFirstSeenSeq()
|
||||
testAdmissionAndConflict()
|
||||
{
|
||||
testcase("cleanup uses first seen sequence");
|
||||
testcase("admission and absorbing conflict");
|
||||
|
||||
ExportSigCollector collector;
|
||||
auto const tx = makeHash("cleanup-verified");
|
||||
auto const sig = makeSignature(1);
|
||||
auto const w = origin(1);
|
||||
auto const publication = collector.reopenPublication(w, w, 9);
|
||||
BEAST_EXPECT(publication.has_value());
|
||||
auto first = collector.beginAttributedAdmission(
|
||||
w, contribution(7, keyA_, 1), 10);
|
||||
BEAST_EXPECT(first.result == ExportSigCollector::BeginResult::verify);
|
||||
BEAST_EXPECT(first.ticket.has_value());
|
||||
BEAST_EXPECT(collector.fullUnionSnapshot().empty());
|
||||
if (!first.ticket)
|
||||
return;
|
||||
|
||||
collector.addVerifiedSignature(tx, validator_, sig, 10);
|
||||
BEAST_EXPECT(collector.signatureCount(tx) == 1);
|
||||
auto accepted =
|
||||
collector.admitContribution(std::move(*first.ticket), true, 10);
|
||||
BEAST_EXPECT(
|
||||
accepted.result == ExportSigCollector::AdmitResult::accepted);
|
||||
auto snapshot = collector.fullUnionSnapshot();
|
||||
BEAST_EXPECT(snapshot.at(w).size() == 1);
|
||||
BEAST_EXPECT(snapshot.at(w).front().position == 7);
|
||||
|
||||
collector.cleanupStale(266);
|
||||
BEAST_EXPECT(collector.signatureCount(tx) == 1);
|
||||
auto duplicate = collector.beginAttributedAdmission(
|
||||
w, contribution(7, keyA_, 1), 11);
|
||||
BEAST_EXPECT(
|
||||
duplicate.result == ExportSigCollector::BeginResult::duplicate);
|
||||
|
||||
collector.cleanupStale(267);
|
||||
BEAST_EXPECT(collector.signatureCount(tx) == 0);
|
||||
auto second = collector.beginAttributedAdmission(
|
||||
w, contribution(7, keyB_, 2), 11);
|
||||
auto third = collector.beginAttributedAdmission(
|
||||
w, contribution(7, keyA_, 3), 11);
|
||||
BEAST_EXPECT(second.ticket.has_value());
|
||||
BEAST_EXPECT(third.result == ExportSigCollector::BeginResult::capacity);
|
||||
if (!second.ticket)
|
||||
return;
|
||||
|
||||
auto conflicted =
|
||||
collector.admitContribution(std::move(*second.ticket), true, 11);
|
||||
BEAST_EXPECT(
|
||||
conflicted.result == ExportSigCollector::AdmitResult::conflicted);
|
||||
BEAST_EXPECT(conflicted.priorContribution.has_value());
|
||||
BEAST_EXPECT(conflicted.conflictingContribution.has_value());
|
||||
BEAST_EXPECT(
|
||||
collector.positionStatus(w, 7) ==
|
||||
ExportSigCollector::PositionStatus::conflicted);
|
||||
BEAST_EXPECT(collector.fullUnionSnapshot().empty());
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(w, contribution(7, keyA_, 4), 12)
|
||||
.result == ExportSigCollector::BeginResult::conflicted);
|
||||
}
|
||||
|
||||
void
|
||||
testUpgradeSetsFirstSeenSeq()
|
||||
testReservationAndPublicationLifecycle()
|
||||
{
|
||||
testcase("upgrade sets first seen sequence");
|
||||
testcase("verification reservations and publication reopening");
|
||||
|
||||
ExportSigCollector collector;
|
||||
auto const tx = makeHash("cleanup-upgraded");
|
||||
auto const sig = makeSignature(5);
|
||||
auto const w = origin(2);
|
||||
auto publication = collector.reopenPublication(w, w, 19);
|
||||
BEAST_EXPECT(publication.has_value());
|
||||
auto invalid = collector.beginAttributedAdmission(
|
||||
w, contribution(3, keyA_, 10), 20);
|
||||
BEAST_EXPECT(invalid.ticket.has_value());
|
||||
if (!invalid.ticket)
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
collector.admitContribution(std::move(*invalid.ticket), false, 20)
|
||||
.result == ExportSigCollector::AdmitResult::invalid);
|
||||
|
||||
collector.addUnverifiedSignature(tx, validator_, sig);
|
||||
BEAST_EXPECT(collector.hasUnverifiedSignatures());
|
||||
auto valid = collector.beginAttributedAdmission(
|
||||
w, contribution(3, keyA_, 11), 20);
|
||||
BEAST_EXPECT(valid.ticket.has_value());
|
||||
if (!valid.ticket)
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
collector.admitContribution(std::move(*valid.ticket), true, 20)
|
||||
.result == ExportSigCollector::AdmitResult::accepted);
|
||||
|
||||
collector.upgradeSignature(tx, validator_, sig, 10);
|
||||
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
|
||||
BEAST_EXPECT(collector.signatureCount(tx) == 1);
|
||||
auto other = collector.beginAttributedAdmission(
|
||||
w, contribution(4, keyB_, 12), 20);
|
||||
BEAST_EXPECT(other.ticket.has_value());
|
||||
if (!other.ticket)
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
collector.admitContribution(std::move(*other.ticket), true, 20)
|
||||
.result == ExportSigCollector::AdmitResult::accepted);
|
||||
|
||||
collector.cleanupStale(266);
|
||||
BEAST_EXPECT(collector.signatureCount(tx) == 1);
|
||||
BEAST_EXPECT(publication.has_value());
|
||||
if (!publication)
|
||||
return;
|
||||
BEAST_EXPECT(collector.claimPublication(*publication, 3, 2));
|
||||
BEAST_EXPECT(collector.publicationGeneration(w) == 1);
|
||||
BEAST_EXPECT(!collector.claimPublication(*publication, 3, 2));
|
||||
|
||||
collector.cleanupStale(267);
|
||||
BEAST_EXPECT(collector.signatureCount(tx) == 0);
|
||||
// Reopening the same trigger is idempotent and does not reset slots.
|
||||
auto samePublication = collector.reopenPublication(w, w, 21);
|
||||
BEAST_EXPECT(samePublication.has_value());
|
||||
if (!samePublication)
|
||||
return;
|
||||
BEAST_EXPECT(collector.publicationGeneration(w) == 1);
|
||||
BEAST_EXPECT(!collector.claimPublication(*samePublication, 3, 2));
|
||||
|
||||
auto const nextTrigger = origin(22);
|
||||
auto nextPublication = collector.reopenPublication(w, nextTrigger, 22);
|
||||
BEAST_EXPECT(nextPublication.has_value());
|
||||
if (!nextPublication)
|
||||
return;
|
||||
BEAST_EXPECT(collector.publicationGeneration(w) == 2);
|
||||
BEAST_EXPECT(!collector.claimPublication(*publication, 4, 2));
|
||||
BEAST_EXPECT(collector.claimPublication(*nextPublication, 4, 1));
|
||||
BEAST_EXPECT(!collector.reopenPublication(w, w, 23));
|
||||
BEAST_EXPECT(
|
||||
collector.positionStatus(w, 3) ==
|
||||
ExportSigCollector::PositionStatus::unique);
|
||||
BEAST_EXPECT(collector.fullUnionSnapshot().at(w).size() == 2);
|
||||
|
||||
collector.cleanupStale(278);
|
||||
BEAST_EXPECT(!collector.fullUnionSnapshot().empty());
|
||||
collector.cleanupStale(279);
|
||||
BEAST_EXPECT(collector.fullUnionSnapshot().empty());
|
||||
}
|
||||
|
||||
void
|
||||
testRemoveInvalidUnverifiedSignature()
|
||||
testMalformedBoundaries()
|
||||
{
|
||||
testcase("remove invalid unverified signature");
|
||||
testcase("malformed contribution boundaries");
|
||||
|
||||
ExportSigCollector collector;
|
||||
auto const tx = makeHash("remove-invalid");
|
||||
auto const sig = makeSignature(9);
|
||||
auto const otherSig = makeSignature(10);
|
||||
auto good = contribution(0, keyA_, 1);
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(uint256{}, good).result ==
|
||||
ExportSigCollector::BeginResult::malformed);
|
||||
|
||||
collector.addUnverifiedSignature(tx, validator_, sig, 10);
|
||||
BEAST_EXPECT(collector.hasUnverifiedSignatures());
|
||||
auto const w = origin(3);
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(w, good, 1).result ==
|
||||
ExportSigCollector::BeginResult::unknownOrigin);
|
||||
BEAST_EXPECT(collector.reopenPublication(w, w, 1).has_value());
|
||||
|
||||
BEAST_EXPECT(!collector.removeSignature(tx, validator_, otherSig));
|
||||
BEAST_EXPECT(collector.hasUnverifiedSignatures());
|
||||
good.position = ExportLimits::maxCommitteeMembers;
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(w, good, 1).result ==
|
||||
ExportSigCollector::BeginResult::malformed);
|
||||
|
||||
BEAST_EXPECT(collector.removeSignature(tx, validator_, sig));
|
||||
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
|
||||
BEAST_EXPECT(collector.signatureCount(tx) == 0);
|
||||
}
|
||||
good.position = 0;
|
||||
good.signature = Buffer{};
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(w, good, 1).result ==
|
||||
ExportSigCollector::BeginResult::malformed);
|
||||
|
||||
void
|
||||
testSnapshotsAndFilteredCounts()
|
||||
{
|
||||
testcase("snapshots and filtered counts use verified signatures only");
|
||||
std::vector<std::uint8_t> oversized(
|
||||
ExportSigCollector::maxSignatureBytes + 1, 0xAB);
|
||||
good.signature = Buffer{oversized.data(), oversized.size()};
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(w, good, 1).result ==
|
||||
ExportSigCollector::BeginResult::malformed);
|
||||
|
||||
auto const other = randomKeyPair(KeyType::secp256k1).first;
|
||||
ExportSigCollector collector;
|
||||
auto const tx = makeHash("snapshot-filtered");
|
||||
auto const verifiedSig = makeSignature(20);
|
||||
auto const unverifiedSig = makeSignature(30);
|
||||
good = contribution(5, keyA_, 5);
|
||||
auto abandoned = collector.beginAttributedAdmission(w, good, 2);
|
||||
BEAST_EXPECT(abandoned.ticket.has_value());
|
||||
if (!abandoned.ticket)
|
||||
return;
|
||||
BEAST_EXPECT(collector.cancelAdmission(std::move(*abandoned.ticket)));
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(w, good, 2).result ==
|
||||
ExportSigCollector::BeginResult::verify);
|
||||
|
||||
BEAST_EXPECT(!collector.hasVerifiedSignature(tx, validator_));
|
||||
BEAST_EXPECT(collector.unverifiedSignatures(tx).empty());
|
||||
BEAST_EXPECT(!collector.checkQuorumAndSnapshot(tx, 1));
|
||||
auto expired =
|
||||
collector.beginAttributedAdmission(w, contribution(6, keyA_, 6), 3);
|
||||
BEAST_EXPECT(expired.ticket.has_value());
|
||||
BEAST_EXPECT(
|
||||
collector.beginAttributedAdmission(w, contribution(6, keyB_, 7), 5)
|
||||
.result == ExportSigCollector::BeginResult::verify);
|
||||
|
||||
collector.addVerifiedSignature(tx, validator_, verifiedSig, 10);
|
||||
collector.addUnverifiedSignature(tx, other, unverifiedSig, 11);
|
||||
|
||||
BEAST_EXPECT(collector.hasVerifiedSignature(tx, validator_));
|
||||
BEAST_EXPECT(!collector.hasVerifiedSignature(tx, other));
|
||||
BEAST_EXPECT(collector.signatureCount(tx) == 1);
|
||||
BEAST_EXPECT(collector.signatureCount(tx, [&](PublicKey const& pk) {
|
||||
return pk == validator_;
|
||||
}) == 1);
|
||||
BEAST_EXPECT(collector.signatureCount(tx, [&](PublicKey const& pk) {
|
||||
return pk == other;
|
||||
}) == 0);
|
||||
|
||||
auto unverified = collector.unverifiedSignatures(tx);
|
||||
BEAST_EXPECT(unverified.size() == 1);
|
||||
BEAST_EXPECT(unverified.count(other) == 1);
|
||||
|
||||
auto snapshot = collector.snapshot();
|
||||
BEAST_EXPECT(snapshot.size() == 1);
|
||||
BEAST_EXPECT(snapshot[tx].count(validator_) == 1);
|
||||
BEAST_EXPECT(snapshot[tx].count(other) == 0);
|
||||
|
||||
auto sigSnapshot = collector.snapshotWithSigs();
|
||||
BEAST_EXPECT(sigSnapshot[tx].size() == 1);
|
||||
BEAST_EXPECT(sigSnapshot[tx][validator_] == verifiedSig);
|
||||
|
||||
auto filteredSnapshot = collector.snapshotWithSigs(
|
||||
[&](PublicKey const& pk) { return pk == other; });
|
||||
BEAST_EXPECT(filteredSnapshot.empty());
|
||||
|
||||
BEAST_EXPECT(!collector.checkQuorumAndSnapshot(tx, 2));
|
||||
auto quorum = collector.checkQuorumAndSnapshot(tx, 1);
|
||||
BEAST_EXPECT(quorum.has_value());
|
||||
if (quorum)
|
||||
{
|
||||
BEAST_EXPECT(quorum->size() == 1);
|
||||
BEAST_EXPECT((*quorum)[validator_] == verifiedSig);
|
||||
}
|
||||
|
||||
collector.upgradeSignature(tx, other, makeSignature(31), 12);
|
||||
BEAST_EXPECT(collector.signatureCount(tx) == 1);
|
||||
|
||||
collector.upgradeSignature(tx, other, unverifiedSig, 12);
|
||||
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
|
||||
BEAST_EXPECT(collector.signatureCount(tx) == 2);
|
||||
|
||||
auto filteredQuorum = collector.checkQuorumAndSnapshot(
|
||||
tx, 1, [&](PublicKey const& pk) { return pk == other; });
|
||||
BEAST_EXPECT(filteredQuorum.has_value());
|
||||
if (filteredQuorum)
|
||||
BEAST_EXPECT((*filteredQuorum)[other] == unverifiedSig);
|
||||
|
||||
collector.clear(tx);
|
||||
BEAST_EXPECT(collector.signatureCount(tx) == 0);
|
||||
BEAST_EXPECT(collector.snapshot().empty());
|
||||
}
|
||||
|
||||
void
|
||||
testLegacyReplacementSemantics()
|
||||
{
|
||||
testcase("legacy replacement semantics");
|
||||
|
||||
auto const sigA = makeSignature(60);
|
||||
auto const sigB = makeSignature(70);
|
||||
|
||||
{
|
||||
ExportSigCollector collector;
|
||||
auto const tx = makeHash("unverified-replacement");
|
||||
collector.addUnverifiedSignature(tx, validator_, sigA, 10);
|
||||
collector.addUnverifiedSignature(tx, validator_, sigB, 20);
|
||||
BEAST_EXPECT(
|
||||
collector.unverifiedSignatures(tx).at(validator_) == sigB);
|
||||
|
||||
// Replacement does not refresh firstSeenSeq.
|
||||
collector.cleanupStale(266);
|
||||
BEAST_EXPECT(collector.hasUnverifiedSignatures());
|
||||
collector.cleanupStale(267);
|
||||
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
|
||||
}
|
||||
|
||||
{
|
||||
ExportSigCollector collector;
|
||||
auto const tx = makeHash("verified-replacement");
|
||||
collector.addVerifiedSignature(tx, validator_, sigA, 10);
|
||||
collector.addVerifiedSignature(tx, validator_, sigB, 20);
|
||||
BEAST_EXPECT(
|
||||
collector.snapshotWithSigs().at(tx).at(validator_) == sigB);
|
||||
|
||||
// Unverified input never overwrites an already verified value.
|
||||
collector.addUnverifiedSignature(tx, validator_, sigA, 30);
|
||||
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
|
||||
BEAST_EXPECT(
|
||||
collector.snapshotWithSigs().at(tx).at(validator_) == sigB);
|
||||
|
||||
collector.cleanupStale(266);
|
||||
BEAST_EXPECT(collector.signatureCount(tx) == 1);
|
||||
collector.cleanupStale(267);
|
||||
BEAST_EXPECT(collector.signatureCount(tx) == 0);
|
||||
}
|
||||
|
||||
{
|
||||
ExportSigCollector collector;
|
||||
auto const tx = makeHash("unverified-to-verified");
|
||||
collector.addUnverifiedSignature(tx, validator_, sigA, 10);
|
||||
collector.addVerifiedSignature(tx, validator_, sigB, 20);
|
||||
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
|
||||
BEAST_EXPECT(
|
||||
collector.snapshotWithSigs().at(tx).at(validator_) == sigB);
|
||||
collector.cleanupStale(266);
|
||||
BEAST_EXPECT(collector.signatureCount(tx) == 1);
|
||||
collector.cleanupStale(267);
|
||||
BEAST_EXPECT(collector.signatureCount(tx) == 0);
|
||||
}
|
||||
|
||||
{
|
||||
ExportSigCollector collector;
|
||||
auto const tx = makeHash("unverified-to-standalone");
|
||||
collector.addUnverifiedSignature(tx, validator_, sigA, 10);
|
||||
collector.addStandaloneSignature(tx, validator_, 20);
|
||||
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
|
||||
BEAST_EXPECT(
|
||||
collector.snapshotWithSigs().at(tx).at(validator_) == sigA);
|
||||
collector.cleanupStale(266);
|
||||
BEAST_EXPECT(collector.signatureCount(tx) == 1);
|
||||
collector.cleanupStale(267);
|
||||
BEAST_EXPECT(collector.signatureCount(tx) == 0);
|
||||
}
|
||||
|
||||
{
|
||||
ExportSigCollector collector;
|
||||
auto const tx = makeHash("standalone-preserves-buffer");
|
||||
collector.addVerifiedSignature(tx, validator_, sigB, 10);
|
||||
collector.addStandaloneSignature(tx, validator_, 20);
|
||||
BEAST_EXPECT(
|
||||
collector.snapshotWithSigs().at(tx).at(validator_) == sigB);
|
||||
}
|
||||
|
||||
{
|
||||
ExportSigCollector collector;
|
||||
auto const tx = makeHash("standalone-empty-transitions");
|
||||
collector.addStandaloneSignature(tx, validator_, 10);
|
||||
collector.addUnverifiedSignature(tx, validator_, sigA, 20);
|
||||
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
|
||||
BEAST_EXPECT(
|
||||
collector.snapshotWithSigs().at(tx).at(validator_).empty());
|
||||
|
||||
collector.addVerifiedSignature(tx, validator_, sigB, 30);
|
||||
BEAST_EXPECT(
|
||||
collector.snapshotWithSigs().at(tx).at(validator_) == sigB);
|
||||
}
|
||||
|
||||
{
|
||||
ExportSigCollector collector;
|
||||
auto const tx = makeHash("upgrade-preserves-age");
|
||||
collector.addUnverifiedSignature(tx, validator_, sigA, 10);
|
||||
collector.upgradeSignature(tx, validator_, sigA, 20);
|
||||
BEAST_EXPECT(collector.signatureCount(tx) == 1);
|
||||
collector.cleanupStale(266);
|
||||
BEAST_EXPECT(collector.signatureCount(tx) == 1);
|
||||
collector.cleanupStale(267);
|
||||
BEAST_EXPECT(collector.signatureCount(tx) == 0);
|
||||
}
|
||||
|
||||
{
|
||||
ExportSigCollector collector;
|
||||
auto const tx = makeHash("ignored-unverified-initializes-age");
|
||||
collector.addVerifiedSignature(tx, validator_, sigA);
|
||||
collector.addUnverifiedSignature(tx, validator_, sigB, 10);
|
||||
BEAST_EXPECT(
|
||||
collector.snapshotWithSigs().at(tx).at(validator_) == sigA);
|
||||
collector.cleanupStale(266);
|
||||
BEAST_EXPECT(collector.signatureCount(tx) == 1);
|
||||
collector.cleanupStale(267);
|
||||
BEAST_EXPECT(collector.signatureCount(tx) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testStandaloneAndRoundState()
|
||||
{
|
||||
testcase("standalone signatures and round state");
|
||||
|
||||
ExportSigCollector collector;
|
||||
auto const tx = makeHash("standalone-round");
|
||||
|
||||
collector.addStandaloneSignature(tx, validator_, 10);
|
||||
BEAST_EXPECT(collector.hasVerifiedSignature(tx, validator_));
|
||||
BEAST_EXPECT(collector.signatureCount(tx) == 1);
|
||||
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
|
||||
|
||||
auto snapshot = collector.snapshot();
|
||||
BEAST_EXPECT(snapshot.size() == 1);
|
||||
BEAST_EXPECT(snapshot[tx].count(validator_) == 1);
|
||||
|
||||
auto sigSnapshot = collector.snapshotWithSigs();
|
||||
BEAST_EXPECT(sigSnapshot.size() == 1);
|
||||
BEAST_EXPECT(sigSnapshot[tx].count(validator_) == 1);
|
||||
BEAST_EXPECT(sigSnapshot[tx][validator_].empty());
|
||||
|
||||
BEAST_EXPECT(collector.markSent(tx));
|
||||
BEAST_EXPECT(!collector.markSent(tx));
|
||||
collector.clearRound();
|
||||
BEAST_EXPECT(collector.markSent(tx));
|
||||
|
||||
auto const tx2 = makeHash("standalone-round-2");
|
||||
auto const tx3 = makeHash("standalone-round-3");
|
||||
BEAST_EXPECT(collector.markSent(tx2, 2));
|
||||
BEAST_EXPECT(!collector.markSent(tx3, 2));
|
||||
BEAST_EXPECT(!collector.markSent(tx2, 2));
|
||||
collector.clearRound();
|
||||
BEAST_EXPECT(collector.markSent(tx3, 2));
|
||||
}
|
||||
|
||||
void
|
||||
testClearAll()
|
||||
{
|
||||
testcase("clear all signatures and round state");
|
||||
|
||||
ExportSigCollector collector;
|
||||
auto const verifiedTx = makeHash("clear-all-verified");
|
||||
auto const unverifiedTx = makeHash("clear-all-unverified");
|
||||
auto const sig = makeSignature(12);
|
||||
|
||||
collector.addVerifiedSignature(verifiedTx, validator_, sig, 10);
|
||||
collector.addUnverifiedSignature(unverifiedTx, validator_, sig, 10);
|
||||
BEAST_EXPECT(collector.signatureCount(verifiedTx) == 1);
|
||||
BEAST_EXPECT(collector.hasUnverifiedSignatures());
|
||||
BEAST_EXPECT(collector.markSent(verifiedTx));
|
||||
BEAST_EXPECT(!collector.markSent(verifiedTx));
|
||||
|
||||
collector.clearAll();
|
||||
|
||||
BEAST_EXPECT(collector.signatureCount(verifiedTx) == 0);
|
||||
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
|
||||
BEAST_EXPECT(collector.markSent(verifiedTx));
|
||||
}
|
||||
|
||||
void
|
||||
testUnverifiedCacheCap()
|
||||
{
|
||||
testcase("unverified cache cap");
|
||||
|
||||
constexpr std::size_t maxTrackedTxns = 4096;
|
||||
ExportSigCollector collector;
|
||||
auto const sig = makeSignature(50);
|
||||
|
||||
for (std::uint32_t i = 0; i < maxTrackedTxns; ++i)
|
||||
collector.addUnverifiedSignature(
|
||||
makeHashFromSeed(i), validator_, sig, 10);
|
||||
|
||||
auto const firstTx = makeHashFromSeed(0);
|
||||
auto const lastTrackedTx =
|
||||
makeHashFromSeed(static_cast<std::uint32_t>(maxTrackedTxns - 1));
|
||||
auto const rejectedTx =
|
||||
makeHashFromSeed(static_cast<std::uint32_t>(maxTrackedTxns));
|
||||
auto const verifiedTx =
|
||||
makeHashFromSeed(static_cast<std::uint32_t>(maxTrackedTxns + 1));
|
||||
auto const other = randomKeyPair(KeyType::secp256k1).first;
|
||||
|
||||
BEAST_EXPECT(collector.hasUnverifiedSignatures());
|
||||
BEAST_EXPECT(collector.unverifiedSignatures(firstTx).size() == 1);
|
||||
BEAST_EXPECT(collector.unverifiedSignatures(lastTrackedTx).size() == 1);
|
||||
|
||||
collector.addUnverifiedSignature(rejectedTx, validator_, sig, 10);
|
||||
BEAST_EXPECT(collector.unverifiedSignatures(rejectedTx).empty());
|
||||
|
||||
// Existing entries remain updateable at the cap.
|
||||
collector.addUnverifiedSignature(firstTx, other, sig, 10);
|
||||
BEAST_EXPECT(collector.unverifiedSignatures(firstTx).size() == 2);
|
||||
|
||||
// Verified entries are real in-ledger exports and are not gated by the
|
||||
// unverified relay-ordering cache cap.
|
||||
collector.addVerifiedSignature(verifiedTx, validator_, sig, 10);
|
||||
BEAST_EXPECT(collector.signatureCount(verifiedTx) == 1);
|
||||
}
|
||||
|
||||
void
|
||||
testDefensiveNoOps()
|
||||
{
|
||||
testcase("defensive no-op paths");
|
||||
|
||||
ExportSigCollector collector;
|
||||
auto const missingTx = makeHash("missing-defensive");
|
||||
auto const standaloneTx = makeHash("standalone-defensive");
|
||||
auto const sig = makeSignature(40);
|
||||
|
||||
collector.upgradeSignature(missingTx, validator_, sig, 10);
|
||||
BEAST_EXPECT(collector.signatureCount(missingTx) == 0);
|
||||
BEAST_EXPECT(!collector.removeSignature(missingTx, validator_, sig));
|
||||
BEAST_EXPECT(!collector.checkQuorumAndSnapshot(missingTx, 1));
|
||||
BEAST_EXPECT(collector.signatureCount(missingTx, [](PublicKey const&) {
|
||||
return true;
|
||||
}) == 0);
|
||||
|
||||
collector.addStandaloneSignature(standaloneTx, validator_, 10);
|
||||
collector.upgradeSignature(standaloneTx, validator_, Buffer{}, 11);
|
||||
BEAST_EXPECT(collector.signatureCount(standaloneTx) == 1);
|
||||
BEAST_EXPECT(collector.snapshotWithSigs()
|
||||
.at(standaloneTx)
|
||||
.at(validator_)
|
||||
.empty());
|
||||
|
||||
auto filtered =
|
||||
collector.snapshotWithSigs([](PublicKey const&) { return false; });
|
||||
BEAST_EXPECT(filtered.empty());
|
||||
BEAST_EXPECT(!collector.checkQuorumAndSnapshot(
|
||||
standaloneTx, 1, [](PublicKey const&) { return false; }));
|
||||
auto oldToken = collector.reopenPublication(w, origin(30), 6);
|
||||
BEAST_EXPECT(oldToken.has_value());
|
||||
collector.clear(w);
|
||||
auto newToken = collector.reopenPublication(w, origin(31), 7);
|
||||
BEAST_EXPECT(newToken.has_value());
|
||||
if (oldToken)
|
||||
BEAST_EXPECT(!collector.claimPublication(*oldToken, 0, 1));
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testCleanupUsesFirstSeenSeq();
|
||||
testUpgradeSetsFirstSeenSeq();
|
||||
testRemoveInvalidUnverifiedSignature();
|
||||
testSnapshotsAndFilteredCounts();
|
||||
testLegacyReplacementSemantics();
|
||||
testStandaloneAndRoundState();
|
||||
testClearAll();
|
||||
testUnverifiedCacheCap();
|
||||
testDefensiveNoOps();
|
||||
testAdmissionAndConflict();
|
||||
testReservationAndPublicationLifecycle();
|
||||
testMalformedBoundaries();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,400 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/unit_test/SuiteJournal.h>
|
||||
#include <xrpld/app/consensus/ExportSignatureHarvester.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
namespace {
|
||||
|
||||
uint256
|
||||
makeHash(char const* label)
|
||||
{
|
||||
return sha512Half(Slice(label, std::strlen(label)));
|
||||
}
|
||||
|
||||
STTx
|
||||
makeSTTx(STObject const& obj)
|
||||
{
|
||||
Serializer s;
|
||||
obj.add(s);
|
||||
SerialIter sit{s.slice()};
|
||||
return STTx{std::ref(sit)};
|
||||
}
|
||||
|
||||
STObject
|
||||
makeExportedPayment(AccountID const& src, AccountID const& dst)
|
||||
{
|
||||
STObject obj(sfExportedTxn);
|
||||
obj.setFieldU16(sfTransactionType, ttPAYMENT);
|
||||
obj.setFieldU32(sfFlags, tfFullyCanonicalSig);
|
||||
obj.setFieldU32(sfSequence, 0);
|
||||
obj.setFieldU32(sfTicketSequence, 1);
|
||||
obj.setFieldU32(sfFirstLedgerSequence, 2);
|
||||
obj.setFieldU32(sfLastLedgerSequence, 6);
|
||||
obj.setFieldAmount(sfAmount, XRPAmount{1000000});
|
||||
obj.setFieldAmount(sfFee, XRPAmount{10});
|
||||
obj.setFieldVL(sfSigningPubKey, Blob{});
|
||||
obj.setAccountID(sfAccount, src);
|
||||
obj.setAccountID(sfDestination, dst);
|
||||
return obj;
|
||||
}
|
||||
|
||||
std::shared_ptr<STTx const>
|
||||
makeExportTx(STObject const& inner, AccountID const& account)
|
||||
{
|
||||
STObject exportObj(sfGeneric);
|
||||
exportObj.setFieldU16(sfTransactionType, ttEXPORT);
|
||||
exportObj.setAccountID(sfAccount, account);
|
||||
exportObj.setFieldU32(sfSequence, 0);
|
||||
exportObj.setFieldVL(sfSigningPubKey, Blob{});
|
||||
exportObj.setFieldU32(sfFirstLedgerSequence, 2);
|
||||
exportObj.setFieldU32(sfLastLedgerSequence, 6);
|
||||
exportObj.setFieldAmount(sfFee, XRPAmount{0});
|
||||
exportObj.set(std::make_unique<STObject>(inner));
|
||||
|
||||
return std::make_shared<STTx const>(makeSTTx(exportObj));
|
||||
}
|
||||
|
||||
std::shared_ptr<STTx const>
|
||||
makeExportTxWithoutInner(AccountID const& account)
|
||||
{
|
||||
STObject exportObj(sfGeneric);
|
||||
exportObj.setFieldU16(sfTransactionType, ttEXPORT);
|
||||
exportObj.setAccountID(sfAccount, account);
|
||||
exportObj.setFieldU32(sfSequence, 0);
|
||||
exportObj.setFieldVL(sfSigningPubKey, Blob{});
|
||||
exportObj.setFieldU32(sfFirstLedgerSequence, 2);
|
||||
exportObj.setFieldU32(sfLastLedgerSequence, 6);
|
||||
exportObj.setFieldAmount(sfFee, XRPAmount{0});
|
||||
|
||||
return std::make_shared<STTx const>(makeSTTx(exportObj));
|
||||
}
|
||||
|
||||
std::string
|
||||
makeBlob(uint256 const& txHash, PublicKey const& pk, Slice sig)
|
||||
{
|
||||
std::string blob;
|
||||
blob.append(reinterpret_cast<char const*>(txHash.data()), txHash.size());
|
||||
blob.append(reinterpret_cast<char const*>(pk.data()), pk.size());
|
||||
blob.append(reinterpret_cast<char const*>(sig.data()), sig.size());
|
||||
return blob;
|
||||
}
|
||||
|
||||
std::string
|
||||
makeBlob(uint256 const& txHash, PublicKey const& pk, Buffer const& sig)
|
||||
{
|
||||
return makeBlob(txHash, pk, Slice(sig.data(), sig.size()));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class ExportSignatureHarvester_test : public beast::unit_test::suite
|
||||
{
|
||||
SuiteJournal journal_{"ExportSignatureHarvester_test", *this};
|
||||
std::pair<PublicKey, SecretKey> const sender_ =
|
||||
randomKeyPair(KeyType::secp256k1);
|
||||
std::pair<PublicKey, SecretKey> const other_ =
|
||||
randomKeyPair(KeyType::secp256k1);
|
||||
uint256 const prevLedger_ = makeHash("export-harvester-prev-ledger");
|
||||
char const* source_ = "unit-test";
|
||||
|
||||
ExportSignatureHarvestInput
|
||||
makeInput(
|
||||
std::vector<std::string> const& blobs,
|
||||
ExportTxnLookup const& exportTxns,
|
||||
bool active = true,
|
||||
std::optional<uint256> sourceLedgerHash = std::nullopt,
|
||||
PublicKey const* sender = nullptr,
|
||||
std::size_t maxEntries = 2) const
|
||||
{
|
||||
return ExportSignatureHarvestInput{
|
||||
sender ? *sender : sender_.first,
|
||||
prevLedger_,
|
||||
blobs,
|
||||
sourceLedgerHash,
|
||||
[active](PublicKey const&) { return active; },
|
||||
exportTxns,
|
||||
42,
|
||||
source_,
|
||||
maxEntries};
|
||||
}
|
||||
|
||||
beast::Journal&
|
||||
journal()
|
||||
{
|
||||
return journal_;
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
testRejectsTooManyEntries()
|
||||
{
|
||||
testcase("rejects too many entries");
|
||||
|
||||
auto const txHash = makeHash("too-many");
|
||||
auto const blob = makeBlob(txHash, sender_.first, Slice("sig", 3));
|
||||
std::vector<std::string> const blobs{blob, blob, blob};
|
||||
ExportTxnLookup lookup;
|
||||
ExportSigCollector collector;
|
||||
|
||||
auto input = makeInput(blobs, lookup);
|
||||
BEAST_EXPECT(harvestExportSignatures(input, collector, journal()) == 0);
|
||||
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
|
||||
BEAST_EXPECT(collector.signatureCount(txHash) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testEmptyInputAndDirectVerification()
|
||||
{
|
||||
testcase("empty input and direct verification");
|
||||
|
||||
std::vector<std::string> const empty;
|
||||
ExportTxnLookup lookup;
|
||||
ExportSigCollector collector;
|
||||
|
||||
auto input = makeInput(empty, lookup, true, prevLedger_);
|
||||
BEAST_EXPECT(harvestExportSignatures(input, collector, journal()) == 0);
|
||||
|
||||
auto const senderAccount = calcAccountID(sender_.first);
|
||||
auto const dstAccount = calcAccountID(other_.first);
|
||||
auto const innerObj = makeExportedPayment(senderAccount, dstAccount);
|
||||
auto const innerTx = makeSTTx(innerObj);
|
||||
auto const sigData = buildMultiSigningData(innerTx, senderAccount);
|
||||
auto const sig = sign(sender_.first, sender_.second, sigData.slice());
|
||||
auto const exportTx = makeExportTx(innerObj, senderAccount);
|
||||
auto const txHash = exportTx->getTransactionID();
|
||||
|
||||
BEAST_EXPECT(verifyExportSignatureAgainstTx(
|
||||
*exportTx,
|
||||
sender_.first,
|
||||
Slice(sig.data(), sig.size()),
|
||||
txHash,
|
||||
journal(),
|
||||
source_));
|
||||
|
||||
BEAST_EXPECT(!verifyExportSignatureAgainstTx(
|
||||
*exportTx,
|
||||
sender_.first,
|
||||
Slice("bad-sig", 7),
|
||||
txHash,
|
||||
journal(),
|
||||
source_));
|
||||
|
||||
auto const noInner = makeExportTxWithoutInner(senderAccount);
|
||||
BEAST_EXPECT(!verifyExportSignatureAgainstTx(
|
||||
*noInner,
|
||||
sender_.first,
|
||||
Slice(sig.data(), sig.size()),
|
||||
noInner->getTransactionID(),
|
||||
journal(),
|
||||
source_));
|
||||
}
|
||||
|
||||
void
|
||||
testIgnoresEmptyAndMalformedEntries()
|
||||
{
|
||||
testcase("ignores empty and malformed entries");
|
||||
|
||||
auto const txHash = makeHash("malformed");
|
||||
std::string invalidPubkeyBlob;
|
||||
invalidPubkeyBlob.append(
|
||||
reinterpret_cast<char const*>(txHash.data()), txHash.size());
|
||||
invalidPubkeyBlob.append(33, '\0');
|
||||
invalidPubkeyBlob.append("sig", 3);
|
||||
|
||||
std::vector<std::string> const blobs{
|
||||
"",
|
||||
std::string(64, 'x'),
|
||||
makeBlob(txHash, sender_.first, Slice{}),
|
||||
invalidPubkeyBlob};
|
||||
ExportTxnLookup lookup;
|
||||
ExportSigCollector collector;
|
||||
|
||||
auto input =
|
||||
makeInput(blobs, lookup, true, prevLedger_, nullptr, blobs.size());
|
||||
BEAST_EXPECT(harvestExportSignatures(input, collector, journal()) == 0);
|
||||
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
|
||||
BEAST_EXPECT(collector.signatureCount(txHash) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testRejectsInactiveOrWrongParent()
|
||||
{
|
||||
testcase("rejects inactive or wrong-parent senders");
|
||||
|
||||
auto const txHash = makeHash("inactive");
|
||||
std::vector<std::string> const blobs{
|
||||
makeBlob(txHash, sender_.first, Slice("sig", 3))};
|
||||
ExportTxnLookup lookup;
|
||||
ExportSigCollector collector;
|
||||
|
||||
auto inactive = makeInput(blobs, lookup, false);
|
||||
BEAST_EXPECT(
|
||||
harvestExportSignatures(inactive, collector, journal()) == 0);
|
||||
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
|
||||
|
||||
auto wrongParent =
|
||||
makeInput(blobs, lookup, true, makeHash("different-parent"));
|
||||
BEAST_EXPECT(
|
||||
harvestExportSignatures(wrongParent, collector, journal()) == 0);
|
||||
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
|
||||
}
|
||||
|
||||
void
|
||||
testRejectsPubkeyMismatchAtomically()
|
||||
{
|
||||
testcase("rejects embedded pubkey mismatch atomically");
|
||||
|
||||
auto const txHash = makeHash("mismatch");
|
||||
std::vector<std::string> const blobs{
|
||||
makeBlob(txHash, sender_.first, Slice("sig-a", 5)),
|
||||
makeBlob(txHash, other_.first, Slice("sig-b", 5))};
|
||||
ExportTxnLookup lookup;
|
||||
ExportSigCollector collector;
|
||||
|
||||
auto input = makeInput(blobs, lookup, true, prevLedger_);
|
||||
BEAST_EXPECT(harvestExportSignatures(input, collector, journal()) == 0);
|
||||
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
|
||||
BEAST_EXPECT(collector.signatureCount(txHash) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testMissingTxStoresUnverified()
|
||||
{
|
||||
testcase("missing tx stores unverified");
|
||||
|
||||
auto const txHash = makeHash("missing-tx");
|
||||
std::vector<std::string> const blobs{
|
||||
makeBlob(txHash, sender_.first, Slice("sig", 3))};
|
||||
ExportTxnLookup lookup;
|
||||
ExportSigCollector collector;
|
||||
|
||||
auto input = makeInput(blobs, lookup, true, prevLedger_);
|
||||
BEAST_EXPECT(harvestExportSignatures(input, collector, journal()) == 1);
|
||||
BEAST_EXPECT(collector.hasUnverifiedSignatures());
|
||||
BEAST_EXPECT(collector.signatureCount(txHash) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testOpenLedgerTxStoresVerifiedAndSkipsDuplicate()
|
||||
{
|
||||
testcase("open-ledger tx stores verified and skips duplicate");
|
||||
|
||||
auto const senderAccount = calcAccountID(sender_.first);
|
||||
auto const dstAccount = calcAccountID(other_.first);
|
||||
auto const innerObj = makeExportedPayment(senderAccount, dstAccount);
|
||||
auto const innerTx = makeSTTx(innerObj);
|
||||
auto const sigData = buildMultiSigningData(innerTx, senderAccount);
|
||||
auto const sig = sign(sender_.first, sender_.second, sigData.slice());
|
||||
auto const exportTx = makeExportTx(innerObj, senderAccount);
|
||||
auto const txHash = exportTx->getTransactionID();
|
||||
|
||||
ExportTxnLookup lookup;
|
||||
lookup.emplace(txHash, exportTx);
|
||||
std::vector<std::string> const blobs{
|
||||
makeBlob(txHash, sender_.first, sig)};
|
||||
ExportSigCollector collector;
|
||||
|
||||
auto input = makeInput(blobs, lookup, true, prevLedger_);
|
||||
BEAST_EXPECT(harvestExportSignatures(input, collector, journal()) == 1);
|
||||
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
|
||||
BEAST_EXPECT(collector.signatureCount(txHash) == 1);
|
||||
|
||||
BEAST_EXPECT(harvestExportSignatures(input, collector, journal()) == 0);
|
||||
BEAST_EXPECT(collector.signatureCount(txHash) == 1);
|
||||
}
|
||||
|
||||
void
|
||||
testRejectsInvalidOpenLedgerSignatures()
|
||||
{
|
||||
testcase("rejects invalid open-ledger signatures");
|
||||
|
||||
auto const senderAccount = calcAccountID(sender_.first);
|
||||
auto const dstAccount = calcAccountID(other_.first);
|
||||
auto const innerObj = makeExportedPayment(senderAccount, dstAccount);
|
||||
auto const exportTx = makeExportTx(innerObj, senderAccount);
|
||||
auto const txHash = exportTx->getTransactionID();
|
||||
|
||||
ExportTxnLookup lookup;
|
||||
lookup.emplace(txHash, exportTx);
|
||||
std::vector<std::string> const blobs{
|
||||
makeBlob(txHash, sender_.first, Slice("bad-sig", 7))};
|
||||
ExportSigCollector collector;
|
||||
|
||||
auto input = makeInput(blobs, lookup, true, prevLedger_);
|
||||
BEAST_EXPECT(harvestExportSignatures(input, collector, journal()) == 0);
|
||||
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
|
||||
BEAST_EXPECT(collector.signatureCount(txHash) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testRejectsOpenLedgerTxWithoutExportedTxn()
|
||||
{
|
||||
testcase("rejects open-ledger tx without exported transaction");
|
||||
|
||||
auto const senderAccount = calcAccountID(sender_.first);
|
||||
auto const exportTx = makeExportTxWithoutInner(senderAccount);
|
||||
auto const txHash = exportTx->getTransactionID();
|
||||
|
||||
ExportTxnLookup lookup;
|
||||
lookup.emplace(txHash, exportTx);
|
||||
std::vector<std::string> const blobs{
|
||||
makeBlob(txHash, sender_.first, Slice("sig", 3))};
|
||||
ExportSigCollector collector;
|
||||
|
||||
auto input = makeInput(blobs, lookup, true, prevLedger_);
|
||||
BEAST_EXPECT(harvestExportSignatures(input, collector, journal()) == 0);
|
||||
BEAST_EXPECT(!collector.hasUnverifiedSignatures());
|
||||
BEAST_EXPECT(collector.signatureCount(txHash) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testRejectsTooManyEntries();
|
||||
testEmptyInputAndDirectVerification();
|
||||
testIgnoresEmptyAndMalformedEntries();
|
||||
testRejectsInactiveOrWrongParent();
|
||||
testRejectsPubkeyMismatchAtomically();
|
||||
testMissingTxStoresUnverified();
|
||||
testOpenLedgerTxStoresVerifiedAndSkipsDuplicate();
|
||||
testRejectsInvalidOpenLedgerSignatures();
|
||||
testRejectsOpenLedgerTxWithoutExportedTxn();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(ExportSignatureHarvester, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -1431,15 +1431,15 @@ class ConsensusExtensions_test : public beast::unit_test::suite
|
||||
origin, trigger, 10));
|
||||
auto admission =
|
||||
ce.postValidationExportSigCollector().beginAttributedAdmission(
|
||||
origin, ExportSigCollectorV2::Contribution{0, pk, sig}, 10);
|
||||
origin, ExportSigCollector::Contribution{0, pk, sig}, 10);
|
||||
BEAST_EXPECT(
|
||||
admission.result == ExportSigCollectorV2::BeginResult::verify);
|
||||
admission.result == ExportSigCollector::BeginResult::verify);
|
||||
BEAST_EXPECT(admission.ticket);
|
||||
if (admission.ticket)
|
||||
BEAST_EXPECT(
|
||||
ce.postValidationExportSigCollector()
|
||||
.admitContribution(std::move(*admission.ticket), true, 10)
|
||||
.result == ExportSigCollectorV2::AdmitResult::accepted);
|
||||
.result == ExportSigCollector::AdmitResult::accepted);
|
||||
BEAST_EXPECT(
|
||||
ce.postValidationExportSigCollector().fullUnionSnapshot().size() ==
|
||||
1);
|
||||
@@ -2340,17 +2340,17 @@ class ConsensusExtensions_test : public beast::unit_test::suite
|
||||
}
|
||||
|
||||
void
|
||||
testExportV2CollectorBuildsAttributedUnion()
|
||||
testExportCollectorBuildsAttributedUnion()
|
||||
{
|
||||
testcase("Export V2 collector builds attributed union");
|
||||
testcase("Export collector builds attributed union");
|
||||
|
||||
using namespace jtx;
|
||||
Env env{
|
||||
*this, envconfig(validator, ""), supported_amendments(), nullptr};
|
||||
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
||||
auto& collector = ce.postValidationExportSigCollector();
|
||||
auto const origin = makeHash("v2-attributed-origin");
|
||||
auto const trigger = makeHash("v2-attributed-trigger");
|
||||
auto const origin = makeHash("attributed-origin");
|
||||
auto const trigger = makeHash("attributed-trigger");
|
||||
auto const signerA = randomKeyPair(KeyType::secp256k1).first;
|
||||
auto const signerB = randomKeyPair(KeyType::secp256k1).first;
|
||||
std::uint8_t const signatureABytes[] = {1, 2};
|
||||
@@ -2359,22 +2359,22 @@ class ConsensusExtensions_test : public beast::unit_test::suite
|
||||
Buffer const signatureB{signatureBBytes, sizeof(signatureBBytes)};
|
||||
|
||||
BEAST_EXPECT(collector.reopenPublication(origin, trigger, 10));
|
||||
auto admit = [&](ExportSigCollectorV2::Position position,
|
||||
auto admit = [&](ExportSigCollector::Position position,
|
||||
PublicKey const& key,
|
||||
Buffer const& signature) {
|
||||
auto admission = collector.beginAttributedAdmission(
|
||||
origin,
|
||||
ExportSigCollectorV2::Contribution{position, key, signature},
|
||||
ExportSigCollector::Contribution{position, key, signature},
|
||||
10);
|
||||
BEAST_EXPECT(
|
||||
admission.result == ExportSigCollectorV2::BeginResult::verify);
|
||||
admission.result == ExportSigCollector::BeginResult::verify);
|
||||
BEAST_EXPECT(admission.ticket);
|
||||
if (!admission.ticket)
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
collector
|
||||
.admitContribution(std::move(*admission.ticket), true, 10)
|
||||
.result == ExportSigCollectorV2::AdmitResult::accepted);
|
||||
.result == ExportSigCollector::AdmitResult::accepted);
|
||||
};
|
||||
admit(0, signerA, signatureA);
|
||||
admit(2, signerB, signatureB);
|
||||
@@ -2453,7 +2453,7 @@ class ConsensusExtensions_test : public beast::unit_test::suite
|
||||
BEAST_EXPECT(collector.reopenPublication(origin, origin, deadline));
|
||||
auto admission = collector.beginAttributedAdmission(
|
||||
origin,
|
||||
ExportSigCollectorV2::Contribution{0, signer, signature},
|
||||
ExportSigCollector::Contribution{0, signer, signature},
|
||||
deadline);
|
||||
BEAST_EXPECT(admission.ticket);
|
||||
if (!admission.ticket)
|
||||
@@ -2461,7 +2461,7 @@ class ConsensusExtensions_test : public beast::unit_test::suite
|
||||
BEAST_EXPECT(
|
||||
collector
|
||||
.admitContribution(std::move(*admission.ticket), true, deadline)
|
||||
.result == ExportSigCollectorV2::AdmitResult::accepted);
|
||||
.result == ExportSigCollector::AdmitResult::accepted);
|
||||
|
||||
auto const leafCount = [&](uint256 const& hash) {
|
||||
auto const map =
|
||||
@@ -2587,7 +2587,7 @@ class ConsensusExtensions_test : public beast::unit_test::suite
|
||||
auto const dst = calcAccountID(randomKeyPair(KeyType::secp256k1).first);
|
||||
auto const releaseTarget =
|
||||
makeSTTx(makeExportedPayment(calcAccountID(signerA.first), dst));
|
||||
auto const origin = makeHash("agreed-export-v2-origin");
|
||||
auto const origin = makeHash("agreed-export-origin");
|
||||
Blob const contributors{0x07};
|
||||
constexpr std::size_t committeeSize = 3;
|
||||
auto const threshold = ExportLimits::committeeQuorumThreshold(3);
|
||||
@@ -3664,7 +3664,7 @@ class ConsensusExtensions_test : public beast::unit_test::suite
|
||||
origin, trigger, 10));
|
||||
auto admission =
|
||||
ce.postValidationExportSigCollector().beginAttributedAdmission(
|
||||
origin, ExportSigCollectorV2::Contribution{0, pk, sig}, 10);
|
||||
origin, ExportSigCollector::Contribution{0, pk, sig}, 10);
|
||||
BEAST_EXPECT(admission.ticket);
|
||||
if (admission.ticket)
|
||||
ce.postValidationExportSigCollector().admitContribution(
|
||||
@@ -4377,7 +4377,7 @@ public:
|
||||
testProposalProofRoundTrip();
|
||||
testProposalPrecheckUsesExportShareRelayLimits();
|
||||
testHarvestRngDataReplacementAndRejection();
|
||||
testExportV2CollectorBuildsAttributedUnion();
|
||||
testExportCollectorBuildsAttributedUnion();
|
||||
testExportSidecarCandidateDeadline();
|
||||
testTransactionAcquireRejectsSidecarWireNodes();
|
||||
testAcquiredSetsRejectConsensusExtensionPseudos();
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/consensus/ConsensusExtensions.h>
|
||||
#include <xrpld/app/consensus/ExportSignatureHarvester.h>
|
||||
#include <xrpld/app/ledger/InboundTransactions.h>
|
||||
#include <xrpld/app/ledger/Ledger.h>
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
@@ -238,7 +237,7 @@ resolveExportShare(
|
||||
ExportShare
|
||||
withContribution(
|
||||
ExportShare const& context,
|
||||
ExportSigCollectorV2::Contribution const& contribution)
|
||||
ExportSigCollector::Contribution const& contribution)
|
||||
{
|
||||
return ExportShare{
|
||||
context.version,
|
||||
@@ -470,28 +469,28 @@ ConsensusExtensions::admitExportShare(
|
||||
share.originTxn, share.triggerTxn, validated->info().seq))
|
||||
return {ExportShareDisposition::deferred, ExportShareCharge::none};
|
||||
|
||||
ExportSigCollectorV2::Contribution contribution{
|
||||
ExportSigCollector::Contribution contribution{
|
||||
share.committeePosition, share.signingKey, share.signature};
|
||||
auto admission = postValidationExportSigCollector_.beginAttributedAdmission(
|
||||
share.originTxn, std::move(contribution), validated->info().seq);
|
||||
if (admission.result != ExportSigCollectorV2::BeginResult::verify ||
|
||||
if (admission.result != ExportSigCollector::BeginResult::verify ||
|
||||
!admission.ticket)
|
||||
{
|
||||
switch (admission.result)
|
||||
{
|
||||
case ExportSigCollectorV2::BeginResult::duplicate:
|
||||
case ExportSigCollectorV2::BeginResult::conflicted:
|
||||
case ExportSigCollector::BeginResult::duplicate:
|
||||
case ExportSigCollector::BeginResult::conflicted:
|
||||
return {
|
||||
ExportShareDisposition::duplicate, ExportShareCharge::none};
|
||||
case ExportSigCollectorV2::BeginResult::unknownOrigin:
|
||||
case ExportSigCollectorV2::BeginResult::capacity:
|
||||
case ExportSigCollector::BeginResult::unknownOrigin:
|
||||
case ExportSigCollector::BeginResult::capacity:
|
||||
return {
|
||||
ExportShareDisposition::deferred, ExportShareCharge::none};
|
||||
case ExportSigCollectorV2::BeginResult::malformed:
|
||||
case ExportSigCollector::BeginResult::malformed:
|
||||
return {
|
||||
ExportShareDisposition::invalid,
|
||||
ExportShareCharge::invalidData};
|
||||
case ExportSigCollectorV2::BeginResult::verify:
|
||||
case ExportSigCollector::BeginResult::verify:
|
||||
break;
|
||||
}
|
||||
return {
|
||||
@@ -507,7 +506,7 @@ ConsensusExtensions::admitExportShare(
|
||||
Slice{share.signature.data(), share.signature.size()});
|
||||
auto outcome = postValidationExportSigCollector_.admitContribution(
|
||||
std::move(*admission.ticket), signatureVerified, validated->info().seq);
|
||||
if (outcome.result == ExportSigCollectorV2::AdmitResult::accepted)
|
||||
if (outcome.result == ExportSigCollector::AdmitResult::accepted)
|
||||
{
|
||||
std::lock_guard streamLock(exportStreamMutex_);
|
||||
if (!exportShareServiceStarted_.load(std::memory_order_acquire))
|
||||
@@ -519,11 +518,11 @@ ConsensusExtensions::admitExportShare(
|
||||
share, latest->info().seq, latest->info().hash);
|
||||
return {ExportShareDisposition::accepted, ExportShareCharge::none};
|
||||
}
|
||||
if (outcome.result == ExportSigCollectorV2::AdmitResult::invalid)
|
||||
if (outcome.result == ExportSigCollector::AdmitResult::invalid)
|
||||
return {
|
||||
ExportShareDisposition::invalid,
|
||||
ExportShareCharge::invalidSignature};
|
||||
if (outcome.result != ExportSigCollectorV2::AdmitResult::conflicted ||
|
||||
if (outcome.result != ExportSigCollector::AdmitResult::conflicted ||
|
||||
!outcome.priorContribution || !outcome.conflictingContribution)
|
||||
return {ExportShareDisposition::duplicate, ExportShareCharge::none};
|
||||
|
||||
@@ -681,7 +680,7 @@ ConsensusExtensions::onValidatedLedger(
|
||||
return;
|
||||
if (postValidationExportSigCollector_.positionStatus(
|
||||
origin, *position) !=
|
||||
ExportSigCollectorV2::PositionStatus::empty)
|
||||
ExportSigCollector::PositionStatus::empty)
|
||||
return;
|
||||
|
||||
auto const originLedger =
|
||||
@@ -2092,8 +2091,7 @@ ConsensusExtensions::agreedExportWitness(
|
||||
auto const data =
|
||||
buildMultiSigningData(exportSigningPayload, signer);
|
||||
if (signature.empty() ||
|
||||
signature.size() >
|
||||
ExportSigCollectorV2::maxSignatureBytes ||
|
||||
signature.size() > ExportSigCollector::maxSignatureBytes ||
|
||||
!verify(key, data.slice(), makeSlice(signature)))
|
||||
{
|
||||
invalid = true;
|
||||
@@ -3248,7 +3246,7 @@ ConsensusExtensions::attachExportSignatures(
|
||||
if (contribution.signingKey != keys.keys->publicKey)
|
||||
continue;
|
||||
auto const identity =
|
||||
std::pair<uint256, ExportSigCollectorV2::Position>{
|
||||
std::pair<uint256, ExportSigCollector::Position>{
|
||||
origin, contribution.position};
|
||||
if (proposalPublishedExportShares_.count(identity) != 0)
|
||||
continue;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include <xrpld/app/consensus/RCLCxLedger.h>
|
||||
#include <xrpld/app/consensus/RCLCxPeerPos.h>
|
||||
#include <xrpld/app/consensus/RCLCxTx.h>
|
||||
#include <xrpld/app/misc/ExportSigCollectorV2.h>
|
||||
#include <xrpld/app/misc/ExportSigCollector.h>
|
||||
#include <xrpld/app/tx/detail/ExportResultBuilder.h>
|
||||
#include <xrpld/consensus/ConsensusParms.h>
|
||||
#include <xrpld/consensus/ConsensusTypes.h>
|
||||
@@ -54,12 +54,12 @@ class ConsensusExtensions
|
||||
friend class test::ConsensusExtensions_test;
|
||||
|
||||
Application& app_;
|
||||
ExportSigCollectorV2 postValidationExportSigCollector_;
|
||||
ExportSigCollector postValidationExportSigCollector_;
|
||||
std::mutex exportStreamMutex_;
|
||||
LedgerIndex exportStreamEmissionSeq_{0};
|
||||
std::set<std::pair<uint256, ExportSigCollectorV2::Position>>
|
||||
std::set<std::pair<uint256, ExportSigCollector::Position>>
|
||||
exportStreamEmittedShares_;
|
||||
std::set<std::pair<uint256, ExportSigCollectorV2::Position>>
|
||||
std::set<std::pair<uint256, ExportSigCollector::Position>>
|
||||
proposalPublishedExportShares_;
|
||||
std::atomic<bool> exportShareServiceStarted_{false};
|
||||
std::atomic<LedgerIndex> lastExportReplaySeq_{0};
|
||||
@@ -223,13 +223,13 @@ private:
|
||||
public:
|
||||
ConsensusExtensions(Application& app, beast::Journal j);
|
||||
|
||||
ExportSigCollectorV2&
|
||||
ExportSigCollector&
|
||||
postValidationExportSigCollector()
|
||||
{
|
||||
return postValidationExportSigCollector_;
|
||||
}
|
||||
|
||||
ExportSigCollectorV2 const&
|
||||
ExportSigCollector const&
|
||||
postValidationExportSigCollector() const
|
||||
{
|
||||
return postValidationExportSigCollector_;
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/XRPLF/rippled
|
||||
Copyright 2026 Xahau
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/consensus/ExportSignatureHarvester.h>
|
||||
#include <xrpld/app/tx/detail/ExportLedgerOps.h>
|
||||
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ExportLimits.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
bool
|
||||
verifyExportSignatureAgainstTx(
|
||||
STTx const& exportTx,
|
||||
PublicKey const& validator,
|
||||
Slice sigSlice,
|
||||
uint256 const& txHash,
|
||||
beast::Journal j,
|
||||
char const* source)
|
||||
{
|
||||
if (!exportTx.isFieldPresent(sfExportedTxn))
|
||||
{
|
||||
JLOG(j.warn()) << "Export: cannot verify sig"
|
||||
<< " txHash=" << txHash << " source=" << source
|
||||
<< " reason=missing-sfExportedTxn";
|
||||
return false;
|
||||
}
|
||||
|
||||
auto embeddedTarget = ExportLedgerOps::embeddedExportedTxn(exportTx);
|
||||
if (!embeddedTarget)
|
||||
{
|
||||
JLOG(j.warn()) << "Export: failed to verify sig"
|
||||
<< " txHash=" << txHash << " source=" << source
|
||||
<< " validator=" << calcNodeID(validator)
|
||||
<< " reason=inner-tx-parse-failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
auto const signerAcctID = calcAccountID(validator);
|
||||
auto const sigData =
|
||||
buildMultiSigningData(*embeddedTarget, signerAcctID);
|
||||
if (!verify(validator, sigData.slice(), sigSlice))
|
||||
{
|
||||
JLOG(j.warn()) << "Export: invalid multisign sig"
|
||||
<< " txHash=" << txHash << " source=" << source
|
||||
<< " validator=" << calcNodeID(validator)
|
||||
<< " reason=signature-verify-failed";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(j.warn()) << "Export: failed to verify sig"
|
||||
<< " txHash=" << txHash << " source=" << source
|
||||
<< " validator=" << calcNodeID(validator)
|
||||
<< " error=" << e.what();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
harvestExportSignatures(
|
||||
ExportSignatureHarvestInput const& input,
|
||||
ExportSigCollector& collector,
|
||||
beast::Journal j)
|
||||
{
|
||||
if (input.exportSignatures.empty())
|
||||
return 0;
|
||||
|
||||
if (input.exportSignatures.size() > input.maxEntries)
|
||||
{
|
||||
JLOG(j.warn()) << "Export: rejecting proposal signatures"
|
||||
<< " reason=too-many"
|
||||
<< " source=" << input.source
|
||||
<< " count=" << input.exportSignatures.size()
|
||||
<< " max=" << input.maxEntries
|
||||
<< " sender=" << calcNodeID(input.senderPK)
|
||||
<< " prevLedger=" << input.proposalPrevLedger;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!input.isActiveSigner(input.senderPK))
|
||||
return 0;
|
||||
|
||||
if (input.activeViewSourceLedgerHash)
|
||||
{
|
||||
if (input.proposalPrevLedger != *input.activeViewSourceLedgerHash)
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (auto const& blob : input.exportSignatures)
|
||||
{
|
||||
if (blob.size() < 65)
|
||||
continue;
|
||||
|
||||
auto const pkSlice = makeSlice(blob).substr(32, 33);
|
||||
if (!publicKeyType(pkSlice))
|
||||
continue;
|
||||
|
||||
if (PublicKey{pkSlice} != input.senderPK)
|
||||
{
|
||||
JLOG(j.warn()) << "Export: rejecting proposal signatures"
|
||||
<< " reason=embedded-pubkey-mismatch"
|
||||
<< " source=" << input.source
|
||||
<< " sender=" << calcNodeID(input.senderPK)
|
||||
<< " embedded=" << calcNodeID(PublicKey{pkSlice})
|
||||
<< " prevLedger=" << input.proposalPrevLedger;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t stored = 0;
|
||||
|
||||
for (auto const& blob : input.exportSignatures)
|
||||
{
|
||||
if (blob.size() < 65)
|
||||
continue;
|
||||
|
||||
// Defense-in-depth: ProposalPrecheck already rejects oversized blobs at
|
||||
// ingress, but bound the stored signature size here too so the merge/
|
||||
// sidecar path can never cache an over-large buffer.
|
||||
if (blob.size() > ExportLimits::maxExportSignatureBytes)
|
||||
continue;
|
||||
|
||||
uint256 txHash;
|
||||
std::memcpy(txHash.data(), blob.data(), 32);
|
||||
|
||||
if (blob.size() <= 65)
|
||||
continue;
|
||||
|
||||
if (collector.hasVerifiedSignature(txHash, input.senderPK))
|
||||
continue;
|
||||
|
||||
auto const fullSlice = makeSlice(blob);
|
||||
auto const pkSlice = fullSlice.substr(32, 33);
|
||||
if (!publicKeyType(pkSlice))
|
||||
continue;
|
||||
|
||||
auto const sigSlice = fullSlice.substr(65);
|
||||
|
||||
auto const txIt = input.exportTxns.find(txHash);
|
||||
if (txIt == input.exportTxns.end())
|
||||
{
|
||||
JLOG(j.debug())
|
||||
<< "Export: storing unverified sig"
|
||||
<< " txHash=" << txHash << " source=" << input.source
|
||||
<< " signer=" << calcNodeID(input.senderPK)
|
||||
<< " reason=tx-not-in-open-ledger"
|
||||
<< " currentClosedSeq=" << input.currentClosedSeq;
|
||||
Buffer sigBuf(sigSlice.data(), sigSlice.size());
|
||||
collector.addUnverifiedSignature(
|
||||
txHash, input.senderPK, sigBuf, input.currentClosedSeq);
|
||||
++stored;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!verifyExportSignatureAgainstTx(
|
||||
*txIt->second,
|
||||
input.senderPK,
|
||||
sigSlice,
|
||||
txHash,
|
||||
j,
|
||||
"open ledger"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Buffer sigBuf(sigSlice.data(), sigSlice.size());
|
||||
collector.addVerifiedSignature(
|
||||
txHash, input.senderPK, sigBuf, input.currentClosedSeq);
|
||||
++stored;
|
||||
}
|
||||
|
||||
if (stored > 0)
|
||||
{
|
||||
JLOG(j.debug()) << "Export: harvested proposal signatures"
|
||||
<< " stored=" << stored
|
||||
<< " advertised=" << input.exportSignatures.size()
|
||||
<< " source=" << input.source
|
||||
<< " sender=" << calcNodeID(input.senderPK)
|
||||
<< " currentClosedSeq=" << input.currentClosedSeq;
|
||||
}
|
||||
return stored;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -1,73 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/XRPLF/rippled
|
||||
Copyright 2026 Xahau
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_APP_CONSENSUS_EXPORTSIGNATUREHARVESTER_H_INCLUDED
|
||||
#define RIPPLE_APP_CONSENSUS_EXPORTSIGNATUREHARVESTER_H_INCLUDED
|
||||
|
||||
#include <xrpld/app/misc/ExportSigCollector.h>
|
||||
#include <xrpl/basics/Buffer.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
using ExportTxnLookup = hash_map<uint256, std::shared_ptr<STTx const>>;
|
||||
|
||||
struct ExportSignatureHarvestInput
|
||||
{
|
||||
PublicKey const& senderPK;
|
||||
uint256 const& proposalPrevLedger;
|
||||
std::vector<std::string> const& exportSignatures;
|
||||
std::optional<uint256> activeViewSourceLedgerHash;
|
||||
std::function<bool(PublicKey const&)> isActiveSigner;
|
||||
ExportTxnLookup const& exportTxns;
|
||||
LedgerIndex currentClosedSeq = 0;
|
||||
char const* source = "unknown";
|
||||
std::size_t maxEntries = 0;
|
||||
};
|
||||
|
||||
bool
|
||||
verifyExportSignatureAgainstTx(
|
||||
STTx const& exportTx,
|
||||
PublicKey const& validator,
|
||||
Slice sigSlice,
|
||||
uint256 const& txHash,
|
||||
beast::Journal j,
|
||||
char const* source);
|
||||
|
||||
std::size_t
|
||||
harvestExportSignatures(
|
||||
ExportSignatureHarvestInput const& input,
|
||||
ExportSigCollector& collector,
|
||||
beast::Journal j);
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -2,429 +2,471 @@
|
||||
#define RIPPLE_APP_MISC_EXPORTSIGCOLLECTOR_H_INCLUDED
|
||||
|
||||
#include <xrpl/basics/Buffer.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/protocol/ExportLimits.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <algorithm>
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/// Export signature collector for the retriable export approach.
|
||||
///
|
||||
/// Stores multisign signatures from validators for pending ttEXPORT
|
||||
/// transactions. Signatures arrive via proposal ingestion
|
||||
/// (onTrustedPeerMessage) after proposal signature verification; they are
|
||||
/// sender-bound and, when possible, multisign-verified.
|
||||
///
|
||||
/// Signatures are either **verified** (cryptographically checked against
|
||||
/// buildMultiSigningData) or **unverified** (stored on proposal-level
|
||||
/// trust alone, e.g. when the ttEXPORT tx isn't in the open ledger yet
|
||||
/// due to relay ordering).
|
||||
///
|
||||
/// Only verified signatures count toward quorum, appear in the local export
|
||||
/// signature snapshot, and are assembled into the final export blob.
|
||||
/// Unverified sigs are a local cache that can be upgraded to verified
|
||||
/// via `upgradeSignature()` when the tx becomes available (e.g. in
|
||||
/// Export::doApply which always has the tx).
|
||||
///
|
||||
//@@start export-sig-collector-mutex
|
||||
/// Thread-safe.
|
||||
/** Post-validation Export contribution collector.
|
||||
|
||||
Contribution identity is the immutable Export origin plus the position in
|
||||
that origin's immutable validator committee. Publication attempts may
|
||||
reopen, but never reset admitted contributions or conflict state.
|
||||
*/
|
||||
class ExportSigCollector
|
||||
{
|
||||
mutable std::mutex mutex_;
|
||||
//@@end export-sig-collector-mutex
|
||||
public:
|
||||
using Position = std::uint16_t;
|
||||
|
||||
struct SigEntry
|
||||
struct Contribution
|
||||
{
|
||||
/// All validators that have contributed (verified or unverified).
|
||||
std::set<PublicKey> validators;
|
||||
/// Actual multisign signature bytes keyed by validator pubkey.
|
||||
/// Empty buffers mean pubkey-only (standalone mode).
|
||||
std::map<PublicKey, Buffer> signatures;
|
||||
/// Validators whose sigs have been cryptographically verified.
|
||||
/// Only these count toward quorum and appear in SHAMap/snapshot.
|
||||
std::set<PublicKey> verified;
|
||||
std::uint32_t firstSeenSeq{0};
|
||||
Position position;
|
||||
PublicKey signingKey;
|
||||
Buffer signature;
|
||||
|
||||
friend bool
|
||||
operator==(Contribution const& lhs, Contribution const& rhs)
|
||||
{
|
||||
return lhs.position == rhs.position &&
|
||||
lhs.signingKey == rhs.signingKey &&
|
||||
lhs.signature == rhs.signature;
|
||||
}
|
||||
};
|
||||
|
||||
std::unordered_map<uint256, SigEntry> sigs_;
|
||||
std::set<uint256> sentThisRound_;
|
||||
enum class BeginResult {
|
||||
verify,
|
||||
duplicate,
|
||||
conflicted,
|
||||
unknownOrigin,
|
||||
capacity,
|
||||
malformed
|
||||
};
|
||||
|
||||
static constexpr std::uint32_t maxStaleLedgers = 256;
|
||||
enum class AdmitResult { accepted, duplicate, conflicted, invalid, stale };
|
||||
|
||||
// Cap on distinct tracked export txns. Bounds the unverified cache: a
|
||||
// malicious trusted validator can advertise proposal sigs with arbitrary
|
||||
// txHashes for txns not in our open ledger (stored unverified, only TTL-
|
||||
// evicted). Verified entries (real in-ledger exports) are never gated by
|
||||
// this cap, so legitimate quorum collection is unaffected.
|
||||
static constexpr std::size_t maxTrackedTxns = 4096;
|
||||
enum class PositionStatus { empty, unique, conflicted };
|
||||
|
||||
void
|
||||
touchSeq(SigEntry& entry, std::uint32_t seq)
|
||||
class AdmissionTicket
|
||||
{
|
||||
if (entry.firstSeenSeq == 0 && seq > 0)
|
||||
entry.firstSeenSeq = seq;
|
||||
friend class ExportSigCollector;
|
||||
|
||||
uint256 origin_;
|
||||
std::uint64_t reservation_;
|
||||
Contribution contribution_;
|
||||
|
||||
AdmissionTicket(
|
||||
uint256 const& origin,
|
||||
std::uint64_t reservation,
|
||||
Contribution contribution)
|
||||
: origin_(origin)
|
||||
, reservation_(reservation)
|
||||
, contribution_(std::move(contribution))
|
||||
{
|
||||
}
|
||||
|
||||
public:
|
||||
uint256 const&
|
||||
origin() const
|
||||
{
|
||||
return origin_;
|
||||
}
|
||||
|
||||
Contribution const&
|
||||
contribution() const
|
||||
{
|
||||
return contribution_;
|
||||
}
|
||||
};
|
||||
|
||||
struct Admission
|
||||
{
|
||||
BeginResult result;
|
||||
std::optional<AdmissionTicket> ticket;
|
||||
};
|
||||
|
||||
class PublicationToken
|
||||
{
|
||||
friend class ExportSigCollector;
|
||||
|
||||
uint256 origin_;
|
||||
std::uint64_t generation_;
|
||||
|
||||
PublicationToken(uint256 const& origin, std::uint64_t generation)
|
||||
: origin_(origin), generation_(generation)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
struct AdmitOutcome
|
||||
{
|
||||
AdmitResult result;
|
||||
// Present when this admission observed the second valid encoding.
|
||||
std::optional<Contribution> priorContribution;
|
||||
std::optional<Contribution> conflictingContribution;
|
||||
};
|
||||
|
||||
using UnionSnapshot = std::map<uint256, std::vector<Contribution>>;
|
||||
|
||||
// Conservative local bounds. These are tuning values, not wire-format
|
||||
// dimensions, and must be reviewed before feature activation.
|
||||
static constexpr std::size_t maxTrackedOrigins = 4096;
|
||||
static constexpr std::uint32_t maxStaleLedgers = 256;
|
||||
static constexpr std::uint32_t maxReservationLedgers = 1;
|
||||
static constexpr std::size_t maxPublicationTriggers = 64;
|
||||
static constexpr std::size_t maxSignatureBytes = 72;
|
||||
|
||||
private:
|
||||
struct Reservation
|
||||
{
|
||||
Contribution contribution;
|
||||
std::uint32_t reservedAtSeq;
|
||||
};
|
||||
|
||||
struct PositionEntry
|
||||
{
|
||||
std::optional<Contribution> unique;
|
||||
bool conflicted{false};
|
||||
std::map<std::uint64_t, Reservation> reservations;
|
||||
};
|
||||
|
||||
struct OriginEntry
|
||||
{
|
||||
std::map<Position, PositionEntry> positions;
|
||||
std::set<Position> published;
|
||||
std::set<uint256> publicationTriggers;
|
||||
uint256 publicationTrigger;
|
||||
std::uint64_t publicationGeneration{0};
|
||||
std::uint32_t lastTouchedSeq{0};
|
||||
};
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
std::unordered_map<uint256, OriginEntry> origins_;
|
||||
std::uint64_t nextReservation_{1};
|
||||
std::uint64_t nextPublicationGeneration_{1};
|
||||
|
||||
static bool
|
||||
sameEncoding(Contribution const& lhs, Contribution const& rhs)
|
||||
{
|
||||
return lhs.signingKey == rhs.signingKey &&
|
||||
lhs.signature == rhs.signature;
|
||||
}
|
||||
|
||||
static void
|
||||
touch(OriginEntry& entry, std::uint32_t currentSeq)
|
||||
{
|
||||
if (currentSeq > entry.lastTouchedSeq)
|
||||
entry.lastTouchedSeq = currentSeq;
|
||||
}
|
||||
|
||||
static void
|
||||
pruneReservations(PositionEntry& position, std::uint32_t currentSeq)
|
||||
{
|
||||
for (auto it = position.reservations.begin();
|
||||
it != position.reservations.end();)
|
||||
{
|
||||
auto const reserved = it->second.reservedAtSeq;
|
||||
if (currentSeq > reserved &&
|
||||
currentSeq - reserved > maxReservationLedgers)
|
||||
it = position.reservations.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
eraseEmptyPosition(OriginEntry& origin, Position position)
|
||||
{
|
||||
auto const it = origin.positions.find(position);
|
||||
if (it != origin.positions.end() && !it->second.unique &&
|
||||
!it->second.conflicted && it->second.reservations.empty())
|
||||
origin.positions.erase(it);
|
||||
}
|
||||
|
||||
std::uint64_t
|
||||
nextReservation()
|
||||
{
|
||||
auto const result = nextReservation_++;
|
||||
if (nextReservation_ == 0)
|
||||
nextReservation_ = 1;
|
||||
return result;
|
||||
}
|
||||
|
||||
std::uint64_t
|
||||
nextPublicationGeneration()
|
||||
{
|
||||
auto const result = nextPublicationGeneration_++;
|
||||
if (nextPublicationGeneration_ == 0)
|
||||
nextPublicationGeneration_ = 1;
|
||||
return result;
|
||||
}
|
||||
|
||||
public:
|
||||
/// Store a signature that has been cryptographically verified
|
||||
/// against buildMultiSigningData + verify().
|
||||
void
|
||||
addVerifiedSignature(
|
||||
uint256 const& txnHash,
|
||||
PublicKey const& validator,
|
||||
Buffer const& signature,
|
||||
/** Reserve one contribution encoding for verification.
|
||||
|
||||
The caller must first attribute the signing key to this exact selected
|
||||
committee position using the validated latch and current manifest. The
|
||||
reservation is then the cryptographic pre-verification DoS boundary.
|
||||
At most two
|
||||
distinct encodings can be in-flight or admitted at a position. The
|
||||
caller performs cryptographic verification only for `verify`, then
|
||||
returns the ticket through `admitContribution`.
|
||||
*/
|
||||
Admission
|
||||
beginAttributedAdmission(
|
||||
uint256 const& origin,
|
||||
Contribution contribution,
|
||||
std::uint32_t currentSeq = 0)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
signature.size() > 0,
|
||||
"ripple::ExportSigCollector::addVerifiedSignature : "
|
||||
"non-empty signature");
|
||||
if (origin.isZero() || currentSeq == 0 ||
|
||||
contribution.position >= ExportLimits::maxCommitteeMembers ||
|
||||
contribution.signature.empty() ||
|
||||
contribution.signature.size() > maxSignatureBytes)
|
||||
return {BeginResult::malformed, std::nullopt};
|
||||
|
||||
std::lock_guard lock(mutex_);
|
||||
auto& entry = sigs_[txnHash];
|
||||
entry.validators.insert(validator);
|
||||
entry.signatures[validator] = signature;
|
||||
entry.verified.insert(validator);
|
||||
touchSeq(entry, currentSeq);
|
||||
auto originIt = origins_.find(origin);
|
||||
if (originIt == origins_.end())
|
||||
return {BeginResult::unknownOrigin, std::nullopt};
|
||||
|
||||
auto& originEntry = originIt->second;
|
||||
auto& position = originEntry.positions[contribution.position];
|
||||
pruneReservations(position, currentSeq);
|
||||
if (position.conflicted)
|
||||
return {BeginResult::conflicted, std::nullopt};
|
||||
if (position.unique && sameEncoding(*position.unique, contribution))
|
||||
return {BeginResult::duplicate, std::nullopt};
|
||||
|
||||
for (auto const& [_, pending] : position.reservations)
|
||||
{
|
||||
if (sameEncoding(pending.contribution, contribution))
|
||||
return {BeginResult::duplicate, std::nullopt};
|
||||
}
|
||||
|
||||
auto const distinct = position.reservations.size() +
|
||||
static_cast<std::size_t>(position.unique.has_value());
|
||||
if (distinct >= 2)
|
||||
return {BeginResult::capacity, std::nullopt};
|
||||
|
||||
auto const reservation = nextReservation();
|
||||
position.reservations.emplace(
|
||||
reservation, Reservation{contribution, currentSeq});
|
||||
return {
|
||||
BeginResult::verify,
|
||||
AdmissionTicket{origin, reservation, std::move(contribution)}};
|
||||
}
|
||||
|
||||
/// Store a signature from a trusted source (checkSign + sender
|
||||
/// binding passed) but without multisign content verification.
|
||||
/// Used when the ttEXPORT tx isn't in the open ledger yet due
|
||||
/// to relay ordering. Will be upgraded to verified via
|
||||
/// upgradeSignature() when the tx becomes available.
|
||||
///
|
||||
/// Does NOT count toward quorum or appear in SHAMap/snapshot.
|
||||
void
|
||||
addUnverifiedSignature(
|
||||
uint256 const& txnHash,
|
||||
PublicKey const& validator,
|
||||
Buffer const& signature,
|
||||
std::uint32_t currentSeq = 0)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
signature.size() > 0,
|
||||
"ripple::ExportSigCollector::addUnverifiedSignature : "
|
||||
"non-empty signature");
|
||||
std::lock_guard lock(mutex_);
|
||||
// Bound the unverified cache (see maxTrackedTxns). Only gate NEW
|
||||
// txHashes; existing entries and the verified path are never blocked,
|
||||
// so real exports still reach quorum.
|
||||
if (sigs_.find(txnHash) == sigs_.end() &&
|
||||
sigs_.size() >= maxTrackedTxns)
|
||||
return;
|
||||
auto& entry = sigs_[txnHash];
|
||||
entry.validators.insert(validator);
|
||||
// Don't overwrite a verified sig with an unverified one.
|
||||
if (entry.verified.find(validator) == entry.verified.end())
|
||||
entry.signatures[validator] = signature;
|
||||
touchSeq(entry, currentSeq);
|
||||
}
|
||||
|
||||
/// Upgrade a previously unverified sig to verified.
|
||||
/// Called from Export::doApply after verifying against the inner tx.
|
||||
/// The caller passes the exact buffer it verified; we only promote
|
||||
/// if the stored buffer still matches (guards against concurrent
|
||||
/// overwrites between unverifiedSignatures() and this call).
|
||||
void
|
||||
upgradeSignature(
|
||||
uint256 const& txnHash,
|
||||
PublicKey const& validator,
|
||||
Buffer const& verifiedBuf,
|
||||
std::uint32_t currentSeq = 0)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
auto it = sigs_.find(txnHash);
|
||||
if (it == sigs_.end())
|
||||
return;
|
||||
auto sit = it->second.signatures.find(validator);
|
||||
if (sit == it->second.signatures.end() || sit->second.size() == 0)
|
||||
return;
|
||||
// Only promote if the stored buffer is the same one we verified.
|
||||
if (!(sit->second == verifiedBuf))
|
||||
return;
|
||||
it->second.verified.insert(validator);
|
||||
touchSeq(it->second, currentSeq);
|
||||
}
|
||||
|
||||
/// Remove a signature if the stored buffer still matches the caller's
|
||||
/// verified-invalid buffer. This keeps stale unverified data from being
|
||||
/// retried forever while avoiding races with a newer replacement.
|
||||
/** Cancel a reservation when verification cannot be completed. */
|
||||
bool
|
||||
removeSignature(
|
||||
uint256 const& txnHash,
|
||||
PublicKey const& validator,
|
||||
Buffer const& expectedBuf)
|
||||
cancelAdmission(AdmissionTicket ticket)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
auto it = sigs_.find(txnHash);
|
||||
if (it == sigs_.end())
|
||||
auto originIt = origins_.find(ticket.origin_);
|
||||
if (originIt == origins_.end())
|
||||
return false;
|
||||
|
||||
auto& entry = it->second;
|
||||
auto sit = entry.signatures.find(validator);
|
||||
if (sit == entry.signatures.end() || !(sit->second == expectedBuf))
|
||||
auto positionIt =
|
||||
originIt->second.positions.find(ticket.contribution_.position);
|
||||
if (positionIt == originIt->second.positions.end())
|
||||
return false;
|
||||
|
||||
entry.signatures.erase(sit);
|
||||
entry.validators.erase(validator);
|
||||
entry.verified.erase(validator);
|
||||
|
||||
if (entry.signatures.empty() && entry.validators.empty() &&
|
||||
entry.verified.empty())
|
||||
sigs_.erase(it);
|
||||
|
||||
auto reservationIt =
|
||||
positionIt->second.reservations.find(ticket.reservation_);
|
||||
if (reservationIt == positionIt->second.reservations.end() ||
|
||||
!(reservationIt->second.contribution == ticket.contribution_))
|
||||
return false;
|
||||
positionIt->second.reservations.erase(reservationIt);
|
||||
eraseEmptyPosition(originIt->second, ticket.contribution_.position);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Store a pubkey-only entry (no real signature). Used in
|
||||
/// standalone mode where quorum counting is sufficient.
|
||||
/// Treated as verified (standalone has no consensus to verify against).
|
||||
void
|
||||
addStandaloneSignature(
|
||||
uint256 const& txnHash,
|
||||
PublicKey const& validator,
|
||||
/** Complete a reserved admission after signature verification. */
|
||||
AdmitOutcome
|
||||
admitContribution(
|
||||
AdmissionTicket ticket,
|
||||
bool signatureVerified,
|
||||
std::uint32_t currentSeq = 0)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
auto& entry = sigs_[txnHash];
|
||||
entry.validators.insert(validator);
|
||||
if (entry.signatures.find(validator) == entry.signatures.end())
|
||||
entry.signatures[validator] = Buffer{};
|
||||
entry.verified.insert(validator);
|
||||
touchSeq(entry, currentSeq);
|
||||
}
|
||||
auto originIt = origins_.find(ticket.origin_);
|
||||
if (originIt == origins_.end())
|
||||
return {AdmitResult::stale, std::nullopt, std::nullopt};
|
||||
|
||||
/// Check if a cryptographically verified signature exists.
|
||||
/// Used to skip redundant verify() calls when the same sig
|
||||
/// arrives more than once through proposal relay.
|
||||
bool
|
||||
hasVerifiedSignature(uint256 const& txnHash, PublicKey const& validator)
|
||||
const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
auto it = sigs_.find(txnHash);
|
||||
if (it == sigs_.end())
|
||||
return false;
|
||||
return it->second.verified.count(validator) > 0;
|
||||
}
|
||||
auto& originEntry = originIt->second;
|
||||
auto positionIt =
|
||||
originEntry.positions.find(ticket.contribution_.position);
|
||||
if (positionIt == originEntry.positions.end())
|
||||
return {AdmitResult::stale, std::nullopt, std::nullopt};
|
||||
|
||||
/// Return unverified validators for a given txHash.
|
||||
/// Used by Export::doApply to find sigs that need verification.
|
||||
std::map<PublicKey, Buffer>
|
||||
unverifiedSignatures(uint256 const& txnHash) const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
std::map<PublicKey, Buffer> result;
|
||||
auto it = sigs_.find(txnHash);
|
||||
if (it == sigs_.end())
|
||||
return result;
|
||||
for (auto const& [pk, buf] : it->second.signatures)
|
||||
auto& position = positionIt->second;
|
||||
auto reservationIt = position.reservations.find(ticket.reservation_);
|
||||
if (reservationIt == position.reservations.end() ||
|
||||
!(reservationIt->second.contribution == ticket.contribution_))
|
||||
return {AdmitResult::stale, std::nullopt, std::nullopt};
|
||||
position.reservations.erase(reservationIt);
|
||||
|
||||
if (!signatureVerified)
|
||||
{
|
||||
if (buf.size() > 0 && it->second.verified.count(pk) == 0)
|
||||
result[pk] = buf;
|
||||
eraseEmptyPosition(originEntry, ticket.contribution_.position);
|
||||
return {AdmitResult::invalid, std::nullopt, std::nullopt};
|
||||
}
|
||||
if (position.conflicted)
|
||||
return {AdmitResult::stale, std::nullopt, std::nullopt};
|
||||
if (!position.unique)
|
||||
{
|
||||
position.unique = std::move(ticket.contribution_);
|
||||
touch(originEntry, currentSeq);
|
||||
return {AdmitResult::accepted, std::nullopt, std::nullopt};
|
||||
}
|
||||
if (sameEncoding(*position.unique, ticket.contribution_))
|
||||
return {AdmitResult::duplicate, std::nullopt, std::nullopt};
|
||||
|
||||
auto prior = std::move(position.unique);
|
||||
auto conflicting = std::move(ticket.contribution_);
|
||||
position.unique.reset();
|
||||
position.conflicted = true;
|
||||
position.reservations.clear();
|
||||
touch(originEntry, currentSeq);
|
||||
return {
|
||||
AdmitResult::conflicted, std::move(prior), std::move(conflicting)};
|
||||
}
|
||||
|
||||
/** Reopen per-attempt publication without changing contribution state. */
|
||||
std::optional<PublicationToken>
|
||||
reopenPublication(
|
||||
uint256 const& origin,
|
||||
uint256 const& trigger,
|
||||
std::uint32_t currentSeq)
|
||||
{
|
||||
if (origin.isZero() || trigger.isZero() || currentSeq == 0)
|
||||
return std::nullopt;
|
||||
|
||||
std::lock_guard lock(mutex_);
|
||||
auto it = origins_.find(origin);
|
||||
if (it == origins_.end())
|
||||
{
|
||||
if (origins_.size() >= maxTrackedOrigins)
|
||||
return std::nullopt;
|
||||
it = origins_.emplace(origin, OriginEntry{}).first;
|
||||
}
|
||||
|
||||
auto& entry = it->second;
|
||||
if (entry.publicationGeneration != 0 &&
|
||||
entry.publicationTrigger == trigger)
|
||||
return PublicationToken{origin, entry.publicationGeneration};
|
||||
if (entry.publicationTriggers.count(trigger) != 0 ||
|
||||
entry.publicationTriggers.size() >= maxPublicationTriggers)
|
||||
return std::nullopt;
|
||||
|
||||
entry.published.clear();
|
||||
entry.publicationTrigger = trigger;
|
||||
entry.publicationTriggers.insert(trigger);
|
||||
entry.publicationGeneration = nextPublicationGeneration();
|
||||
touch(entry, currentSeq);
|
||||
return PublicationToken{origin, entry.publicationGeneration};
|
||||
}
|
||||
|
||||
/** Claim one position's publication slot in the current attempt. */
|
||||
bool
|
||||
claimPublication(
|
||||
PublicationToken const& token,
|
||||
Position position,
|
||||
std::size_t maxDistinct)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
auto it = origins_.find(token.origin_);
|
||||
if (it == origins_.end() ||
|
||||
it->second.publicationGeneration != token.generation_)
|
||||
return false;
|
||||
auto const positionIt = it->second.positions.find(position);
|
||||
if (positionIt == it->second.positions.end() ||
|
||||
!positionIt->second.unique || positionIt->second.conflicted)
|
||||
return false;
|
||||
if (it->second.published.count(position) != 0 ||
|
||||
it->second.published.size() >= maxDistinct)
|
||||
return false;
|
||||
it->second.published.insert(position);
|
||||
return true;
|
||||
}
|
||||
|
||||
PositionStatus
|
||||
positionStatus(uint256 const& origin, Position position) const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
auto const originIt = origins_.find(origin);
|
||||
if (originIt == origins_.end())
|
||||
return PositionStatus::empty;
|
||||
auto const positionIt = originIt->second.positions.find(position);
|
||||
if (positionIt == originIt->second.positions.end())
|
||||
return PositionStatus::empty;
|
||||
if (positionIt->second.conflicted)
|
||||
return PositionStatus::conflicted;
|
||||
return positionIt->second.unique ? PositionStatus::unique
|
||||
: PositionStatus::empty;
|
||||
}
|
||||
|
||||
std::uint64_t
|
||||
publicationGeneration(uint256 const& origin) const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
auto const it = origins_.find(origin);
|
||||
return it == origins_.end() ? 0 : it->second.publicationGeneration;
|
||||
}
|
||||
|
||||
/** Deterministic full union of every unique, verified contribution. */
|
||||
UnionSnapshot
|
||||
fullUnionSnapshot() const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
UnionSnapshot result;
|
||||
for (auto const& [origin, entry] : origins_)
|
||||
{
|
||||
std::vector<Contribution> contributions;
|
||||
for (auto const& [_, position] : entry.positions)
|
||||
{
|
||||
if (!position.conflicted && position.unique)
|
||||
contributions.push_back(*position.unique);
|
||||
}
|
||||
if (!contributions.empty())
|
||||
result.emplace(origin, std::move(contributions));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool
|
||||
hasUnverifiedSignatures() const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
for (auto const& [_, entry] : sigs_)
|
||||
{
|
||||
for (auto const& [pk, buf] : entry.signatures)
|
||||
{
|
||||
if (buf.size() > 0 && entry.verified.count(pk) == 0)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Count of VERIFIED signatures only.
|
||||
template <class IncludeValidator>
|
||||
std::size_t
|
||||
signatureCount(uint256 const& txnHash, IncludeValidator includeValidator)
|
||||
const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
auto it = sigs_.find(txnHash);
|
||||
if (it == sigs_.end())
|
||||
return 0;
|
||||
|
||||
return std::count_if(
|
||||
it->second.verified.begin(),
|
||||
it->second.verified.end(),
|
||||
includeValidator);
|
||||
}
|
||||
|
||||
/// Count of VERIFIED signatures only.
|
||||
std::size_t
|
||||
signatureCount(uint256 const& txnHash) const
|
||||
{
|
||||
return signatureCount(txnHash, [](PublicKey const&) { return true; });
|
||||
}
|
||||
|
||||
void
|
||||
clear(uint256 const& txnHash)
|
||||
clear(uint256 const& origin)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
sigs_.erase(txnHash);
|
||||
origins_.erase(origin);
|
||||
}
|
||||
|
||||
void
|
||||
clearAll()
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
sigs_.clear();
|
||||
sentThisRound_.clear();
|
||||
origins_.clear();
|
||||
}
|
||||
|
||||
/// Get a snapshot of VERIFIED sigs (pubkeys only) for building
|
||||
/// the SHAMap. Only verified sigs appear in convergence.
|
||||
std::unordered_map<uint256, std::set<PublicKey>>
|
||||
snapshot() const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
std::unordered_map<uint256, std::set<PublicKey>> result;
|
||||
for (auto const& [hash, entry] : sigs_)
|
||||
{
|
||||
if (!entry.verified.empty())
|
||||
result[hash] = entry.verified;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Get a snapshot of VERIFIED signatures including sig buffers.
|
||||
template <class IncludeValidator>
|
||||
std::unordered_map<uint256, std::map<PublicKey, Buffer>>
|
||||
snapshotWithSigs(IncludeValidator includeValidator) const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
std::unordered_map<uint256, std::map<PublicKey, Buffer>> result;
|
||||
for (auto const& [hash, entry] : sigs_)
|
||||
{
|
||||
std::map<PublicKey, Buffer> verifiedSigs;
|
||||
for (auto const& pk : entry.verified)
|
||||
{
|
||||
if (!includeValidator(pk))
|
||||
continue;
|
||||
|
||||
auto sit = entry.signatures.find(pk);
|
||||
if (sit != entry.signatures.end())
|
||||
verifiedSigs[pk] = sit->second;
|
||||
}
|
||||
if (!verifiedSigs.empty())
|
||||
result[hash] = std::move(verifiedSigs);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Get a snapshot of VERIFIED signatures including sig buffers.
|
||||
std::unordered_map<uint256, std::map<PublicKey, Buffer>>
|
||||
snapshotWithSigs() const
|
||||
{
|
||||
return snapshotWithSigs([](PublicKey const&) { return true; });
|
||||
}
|
||||
|
||||
/// Atomic quorum check + snapshot for a single txHash.
|
||||
/// Returns VERIFIED signatures if quorum is met, nullopt otherwise.
|
||||
template <class IncludeValidator>
|
||||
std::optional<std::map<PublicKey, Buffer>>
|
||||
checkQuorumAndSnapshot(
|
||||
uint256 const& txnHash,
|
||||
std::size_t threshold,
|
||||
IncludeValidator includeValidator) const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
auto it = sigs_.find(txnHash);
|
||||
if (it == sigs_.end())
|
||||
return std::nullopt;
|
||||
|
||||
std::map<PublicKey, Buffer> result;
|
||||
for (auto const& pk : it->second.verified)
|
||||
{
|
||||
if (!includeValidator(pk))
|
||||
continue;
|
||||
|
||||
auto sit = it->second.signatures.find(pk);
|
||||
XRPL_ASSERT(
|
||||
sit != it->second.signatures.end(),
|
||||
"ripple::ExportSigCollector::checkQuorumAndSnapshot : "
|
||||
"verified key must exist in signatures map");
|
||||
XRPL_ASSERT(
|
||||
sit->second.size() > 0,
|
||||
"ripple::ExportSigCollector::checkQuorumAndSnapshot : "
|
||||
"verified signature must be non-empty");
|
||||
if (sit != it->second.signatures.end())
|
||||
result[pk] = sit->second;
|
||||
}
|
||||
|
||||
if (result.size() < threshold)
|
||||
return std::nullopt;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Atomic quorum check + snapshot for a single txHash.
|
||||
/// Returns VERIFIED signatures if quorum is met, nullopt otherwise.
|
||||
std::optional<std::map<PublicKey, Buffer>>
|
||||
checkQuorumAndSnapshot(uint256 const& txnHash, std::size_t threshold) const
|
||||
{
|
||||
return checkQuorumAndSnapshot(
|
||||
txnHash, threshold, [](PublicKey const&) { return true; });
|
||||
}
|
||||
|
||||
/// Remove entries older than maxStaleLedgers.
|
||||
void
|
||||
cleanupStale(std::uint32_t currentSeq)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
for (auto it = sigs_.begin(); it != sigs_.end();)
|
||||
for (auto it = origins_.begin(); it != origins_.end();)
|
||||
{
|
||||
if (it->second.firstSeenSeq > 0 &&
|
||||
currentSeq > it->second.firstSeenSeq + maxStaleLedgers)
|
||||
it = sigs_.erase(it);
|
||||
auto const last = it->second.lastTouchedSeq;
|
||||
if (last > 0 && currentSeq > last &&
|
||||
currentSeq - last > maxStaleLedgers)
|
||||
it = origins_.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if we haven't sent our sig for this tx yet this round.
|
||||
/// Marks it as sent on first call.
|
||||
bool
|
||||
markSent(uint256 const& txnHash)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
return sentThisRound_.insert(txnHash).second;
|
||||
}
|
||||
|
||||
/// Returns true if this is a new tx for the round and the distinct sent
|
||||
/// count remains below the caller's per-round publication cap.
|
||||
bool
|
||||
markSent(uint256 const& txnHash, std::size_t maxDistinct)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
if (sentThisRound_.find(txnHash) != sentThisRound_.end())
|
||||
return false;
|
||||
if (sentThisRound_.size() >= maxDistinct)
|
||||
return false;
|
||||
sentThisRound_.insert(txnHash);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Clear per-round state. Call at the start of each consensus round.
|
||||
void
|
||||
clearRound()
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
sentThisRound_.clear();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -1,475 +0,0 @@
|
||||
#ifndef RIPPLE_APP_MISC_EXPORTSIGCOLLECTORV2_H_INCLUDED
|
||||
#define RIPPLE_APP_MISC_EXPORTSIGCOLLECTORV2_H_INCLUDED
|
||||
|
||||
#include <xrpl/basics/Buffer.h>
|
||||
#include <xrpl/protocol/ExportLimits.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** Post-validation Export contribution collector.
|
||||
|
||||
This class is intentionally separate from the live legacy collector. Its
|
||||
contribution identity is the immutable Export origin plus the position in
|
||||
that origin's immutable validator committee. Publication attempts may
|
||||
reopen, but never reset admitted contributions or conflict state.
|
||||
*/
|
||||
class ExportSigCollectorV2
|
||||
{
|
||||
public:
|
||||
using Position = std::uint16_t;
|
||||
|
||||
struct Contribution
|
||||
{
|
||||
Position position;
|
||||
PublicKey signingKey;
|
||||
Buffer signature;
|
||||
|
||||
friend bool
|
||||
operator==(Contribution const& lhs, Contribution const& rhs)
|
||||
{
|
||||
return lhs.position == rhs.position &&
|
||||
lhs.signingKey == rhs.signingKey &&
|
||||
lhs.signature == rhs.signature;
|
||||
}
|
||||
};
|
||||
|
||||
enum class BeginResult {
|
||||
verify,
|
||||
duplicate,
|
||||
conflicted,
|
||||
unknownOrigin,
|
||||
capacity,
|
||||
malformed
|
||||
};
|
||||
|
||||
enum class AdmitResult { accepted, duplicate, conflicted, invalid, stale };
|
||||
|
||||
enum class PositionStatus { empty, unique, conflicted };
|
||||
|
||||
class AdmissionTicket
|
||||
{
|
||||
friend class ExportSigCollectorV2;
|
||||
|
||||
uint256 origin_;
|
||||
std::uint64_t reservation_;
|
||||
Contribution contribution_;
|
||||
|
||||
AdmissionTicket(
|
||||
uint256 const& origin,
|
||||
std::uint64_t reservation,
|
||||
Contribution contribution)
|
||||
: origin_(origin)
|
||||
, reservation_(reservation)
|
||||
, contribution_(std::move(contribution))
|
||||
{
|
||||
}
|
||||
|
||||
public:
|
||||
uint256 const&
|
||||
origin() const
|
||||
{
|
||||
return origin_;
|
||||
}
|
||||
|
||||
Contribution const&
|
||||
contribution() const
|
||||
{
|
||||
return contribution_;
|
||||
}
|
||||
};
|
||||
|
||||
struct Admission
|
||||
{
|
||||
BeginResult result;
|
||||
std::optional<AdmissionTicket> ticket;
|
||||
};
|
||||
|
||||
class PublicationToken
|
||||
{
|
||||
friend class ExportSigCollectorV2;
|
||||
|
||||
uint256 origin_;
|
||||
std::uint64_t generation_;
|
||||
|
||||
PublicationToken(uint256 const& origin, std::uint64_t generation)
|
||||
: origin_(origin), generation_(generation)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
struct AdmitOutcome
|
||||
{
|
||||
AdmitResult result;
|
||||
// Present when this admission observed the second valid encoding.
|
||||
std::optional<Contribution> priorContribution;
|
||||
std::optional<Contribution> conflictingContribution;
|
||||
};
|
||||
|
||||
using UnionSnapshot = std::map<uint256, std::vector<Contribution>>;
|
||||
|
||||
// Conservative local bounds. These are tuning values, not wire-format
|
||||
// dimensions, and must be reviewed before feature activation.
|
||||
static constexpr std::size_t maxTrackedOrigins = 4096;
|
||||
static constexpr std::uint32_t maxStaleLedgers = 256;
|
||||
static constexpr std::uint32_t maxReservationLedgers = 1;
|
||||
static constexpr std::size_t maxPublicationTriggers = 64;
|
||||
static constexpr std::size_t maxSignatureBytes = 72;
|
||||
|
||||
private:
|
||||
struct Reservation
|
||||
{
|
||||
Contribution contribution;
|
||||
std::uint32_t reservedAtSeq;
|
||||
};
|
||||
|
||||
struct PositionEntry
|
||||
{
|
||||
std::optional<Contribution> unique;
|
||||
bool conflicted{false};
|
||||
std::map<std::uint64_t, Reservation> reservations;
|
||||
};
|
||||
|
||||
struct OriginEntry
|
||||
{
|
||||
std::map<Position, PositionEntry> positions;
|
||||
std::set<Position> published;
|
||||
std::set<uint256> publicationTriggers;
|
||||
uint256 publicationTrigger;
|
||||
std::uint64_t publicationGeneration{0};
|
||||
std::uint32_t lastTouchedSeq{0};
|
||||
};
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
std::unordered_map<uint256, OriginEntry> origins_;
|
||||
std::uint64_t nextReservation_{1};
|
||||
std::uint64_t nextPublicationGeneration_{1};
|
||||
|
||||
static bool
|
||||
sameEncoding(Contribution const& lhs, Contribution const& rhs)
|
||||
{
|
||||
return lhs.signingKey == rhs.signingKey &&
|
||||
lhs.signature == rhs.signature;
|
||||
}
|
||||
|
||||
static void
|
||||
touch(OriginEntry& entry, std::uint32_t currentSeq)
|
||||
{
|
||||
if (currentSeq > entry.lastTouchedSeq)
|
||||
entry.lastTouchedSeq = currentSeq;
|
||||
}
|
||||
|
||||
static void
|
||||
pruneReservations(PositionEntry& position, std::uint32_t currentSeq)
|
||||
{
|
||||
for (auto it = position.reservations.begin();
|
||||
it != position.reservations.end();)
|
||||
{
|
||||
auto const reserved = it->second.reservedAtSeq;
|
||||
if (currentSeq > reserved &&
|
||||
currentSeq - reserved > maxReservationLedgers)
|
||||
it = position.reservations.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
eraseEmptyPosition(OriginEntry& origin, Position position)
|
||||
{
|
||||
auto const it = origin.positions.find(position);
|
||||
if (it != origin.positions.end() && !it->second.unique &&
|
||||
!it->second.conflicted && it->second.reservations.empty())
|
||||
origin.positions.erase(it);
|
||||
}
|
||||
|
||||
std::uint64_t
|
||||
nextReservation()
|
||||
{
|
||||
auto const result = nextReservation_++;
|
||||
if (nextReservation_ == 0)
|
||||
nextReservation_ = 1;
|
||||
return result;
|
||||
}
|
||||
|
||||
std::uint64_t
|
||||
nextPublicationGeneration()
|
||||
{
|
||||
auto const result = nextPublicationGeneration_++;
|
||||
if (nextPublicationGeneration_ == 0)
|
||||
nextPublicationGeneration_ = 1;
|
||||
return result;
|
||||
}
|
||||
|
||||
public:
|
||||
/** Reserve one contribution encoding for verification.
|
||||
|
||||
The caller must first attribute the signing key to this exact selected
|
||||
committee position using the validated latch and current manifest. The
|
||||
reservation is then the cryptographic pre-verification DoS boundary.
|
||||
At most two
|
||||
distinct encodings can be in-flight or admitted at a position. The
|
||||
caller performs cryptographic verification only for `verify`, then
|
||||
returns the ticket through `admitContribution`.
|
||||
*/
|
||||
Admission
|
||||
beginAttributedAdmission(
|
||||
uint256 const& origin,
|
||||
Contribution contribution,
|
||||
std::uint32_t currentSeq = 0)
|
||||
{
|
||||
if (origin.isZero() || currentSeq == 0 ||
|
||||
contribution.position >= ExportLimits::maxCommitteeMembers ||
|
||||
contribution.signature.empty() ||
|
||||
contribution.signature.size() > maxSignatureBytes)
|
||||
return {BeginResult::malformed, std::nullopt};
|
||||
|
||||
std::lock_guard lock(mutex_);
|
||||
auto originIt = origins_.find(origin);
|
||||
if (originIt == origins_.end())
|
||||
return {BeginResult::unknownOrigin, std::nullopt};
|
||||
|
||||
auto& originEntry = originIt->second;
|
||||
auto& position = originEntry.positions[contribution.position];
|
||||
pruneReservations(position, currentSeq);
|
||||
if (position.conflicted)
|
||||
return {BeginResult::conflicted, std::nullopt};
|
||||
if (position.unique && sameEncoding(*position.unique, contribution))
|
||||
return {BeginResult::duplicate, std::nullopt};
|
||||
|
||||
for (auto const& [_, pending] : position.reservations)
|
||||
{
|
||||
if (sameEncoding(pending.contribution, contribution))
|
||||
return {BeginResult::duplicate, std::nullopt};
|
||||
}
|
||||
|
||||
auto const distinct = position.reservations.size() +
|
||||
static_cast<std::size_t>(position.unique.has_value());
|
||||
if (distinct >= 2)
|
||||
return {BeginResult::capacity, std::nullopt};
|
||||
|
||||
auto const reservation = nextReservation();
|
||||
position.reservations.emplace(
|
||||
reservation, Reservation{contribution, currentSeq});
|
||||
return {
|
||||
BeginResult::verify,
|
||||
AdmissionTicket{origin, reservation, std::move(contribution)}};
|
||||
}
|
||||
|
||||
/** Cancel a reservation when verification cannot be completed. */
|
||||
bool
|
||||
cancelAdmission(AdmissionTicket ticket)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
auto originIt = origins_.find(ticket.origin_);
|
||||
if (originIt == origins_.end())
|
||||
return false;
|
||||
auto positionIt =
|
||||
originIt->second.positions.find(ticket.contribution_.position);
|
||||
if (positionIt == originIt->second.positions.end())
|
||||
return false;
|
||||
auto reservationIt =
|
||||
positionIt->second.reservations.find(ticket.reservation_);
|
||||
if (reservationIt == positionIt->second.reservations.end() ||
|
||||
!(reservationIt->second.contribution == ticket.contribution_))
|
||||
return false;
|
||||
positionIt->second.reservations.erase(reservationIt);
|
||||
eraseEmptyPosition(originIt->second, ticket.contribution_.position);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Complete a reserved admission after signature verification. */
|
||||
AdmitOutcome
|
||||
admitContribution(
|
||||
AdmissionTicket ticket,
|
||||
bool signatureVerified,
|
||||
std::uint32_t currentSeq = 0)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
auto originIt = origins_.find(ticket.origin_);
|
||||
if (originIt == origins_.end())
|
||||
return {AdmitResult::stale, std::nullopt, std::nullopt};
|
||||
|
||||
auto& originEntry = originIt->second;
|
||||
auto positionIt =
|
||||
originEntry.positions.find(ticket.contribution_.position);
|
||||
if (positionIt == originEntry.positions.end())
|
||||
return {AdmitResult::stale, std::nullopt, std::nullopt};
|
||||
|
||||
auto& position = positionIt->second;
|
||||
auto reservationIt = position.reservations.find(ticket.reservation_);
|
||||
if (reservationIt == position.reservations.end() ||
|
||||
!(reservationIt->second.contribution == ticket.contribution_))
|
||||
return {AdmitResult::stale, std::nullopt, std::nullopt};
|
||||
position.reservations.erase(reservationIt);
|
||||
|
||||
if (!signatureVerified)
|
||||
{
|
||||
eraseEmptyPosition(originEntry, ticket.contribution_.position);
|
||||
return {AdmitResult::invalid, std::nullopt, std::nullopt};
|
||||
}
|
||||
if (position.conflicted)
|
||||
return {AdmitResult::stale, std::nullopt, std::nullopt};
|
||||
if (!position.unique)
|
||||
{
|
||||
position.unique = std::move(ticket.contribution_);
|
||||
touch(originEntry, currentSeq);
|
||||
return {AdmitResult::accepted, std::nullopt, std::nullopt};
|
||||
}
|
||||
if (sameEncoding(*position.unique, ticket.contribution_))
|
||||
return {AdmitResult::duplicate, std::nullopt, std::nullopt};
|
||||
|
||||
auto prior = std::move(position.unique);
|
||||
auto conflicting = std::move(ticket.contribution_);
|
||||
position.unique.reset();
|
||||
position.conflicted = true;
|
||||
position.reservations.clear();
|
||||
touch(originEntry, currentSeq);
|
||||
return {
|
||||
AdmitResult::conflicted, std::move(prior), std::move(conflicting)};
|
||||
}
|
||||
|
||||
/** Reopen per-attempt publication without changing contribution state. */
|
||||
std::optional<PublicationToken>
|
||||
reopenPublication(
|
||||
uint256 const& origin,
|
||||
uint256 const& trigger,
|
||||
std::uint32_t currentSeq)
|
||||
{
|
||||
if (origin.isZero() || trigger.isZero() || currentSeq == 0)
|
||||
return std::nullopt;
|
||||
|
||||
std::lock_guard lock(mutex_);
|
||||
auto it = origins_.find(origin);
|
||||
if (it == origins_.end())
|
||||
{
|
||||
if (origins_.size() >= maxTrackedOrigins)
|
||||
return std::nullopt;
|
||||
it = origins_.emplace(origin, OriginEntry{}).first;
|
||||
}
|
||||
|
||||
auto& entry = it->second;
|
||||
if (entry.publicationGeneration != 0 &&
|
||||
entry.publicationTrigger == trigger)
|
||||
return PublicationToken{origin, entry.publicationGeneration};
|
||||
if (entry.publicationTriggers.count(trigger) != 0 ||
|
||||
entry.publicationTriggers.size() >= maxPublicationTriggers)
|
||||
return std::nullopt;
|
||||
|
||||
entry.published.clear();
|
||||
entry.publicationTrigger = trigger;
|
||||
entry.publicationTriggers.insert(trigger);
|
||||
entry.publicationGeneration = nextPublicationGeneration();
|
||||
touch(entry, currentSeq);
|
||||
return PublicationToken{origin, entry.publicationGeneration};
|
||||
}
|
||||
|
||||
/** Claim one position's publication slot in the current attempt. */
|
||||
bool
|
||||
claimPublication(
|
||||
PublicationToken const& token,
|
||||
Position position,
|
||||
std::size_t maxDistinct)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
auto it = origins_.find(token.origin_);
|
||||
if (it == origins_.end() ||
|
||||
it->second.publicationGeneration != token.generation_)
|
||||
return false;
|
||||
auto const positionIt = it->second.positions.find(position);
|
||||
if (positionIt == it->second.positions.end() ||
|
||||
!positionIt->second.unique || positionIt->second.conflicted)
|
||||
return false;
|
||||
if (it->second.published.count(position) != 0 ||
|
||||
it->second.published.size() >= maxDistinct)
|
||||
return false;
|
||||
it->second.published.insert(position);
|
||||
return true;
|
||||
}
|
||||
|
||||
PositionStatus
|
||||
positionStatus(uint256 const& origin, Position position) const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
auto const originIt = origins_.find(origin);
|
||||
if (originIt == origins_.end())
|
||||
return PositionStatus::empty;
|
||||
auto const positionIt = originIt->second.positions.find(position);
|
||||
if (positionIt == originIt->second.positions.end())
|
||||
return PositionStatus::empty;
|
||||
if (positionIt->second.conflicted)
|
||||
return PositionStatus::conflicted;
|
||||
return positionIt->second.unique ? PositionStatus::unique
|
||||
: PositionStatus::empty;
|
||||
}
|
||||
|
||||
std::uint64_t
|
||||
publicationGeneration(uint256 const& origin) const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
auto const it = origins_.find(origin);
|
||||
return it == origins_.end() ? 0 : it->second.publicationGeneration;
|
||||
}
|
||||
|
||||
/** Deterministic full union of every unique, verified contribution. */
|
||||
UnionSnapshot
|
||||
fullUnionSnapshot() const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
UnionSnapshot result;
|
||||
for (auto const& [origin, entry] : origins_)
|
||||
{
|
||||
std::vector<Contribution> contributions;
|
||||
for (auto const& [_, position] : entry.positions)
|
||||
{
|
||||
if (!position.conflicted && position.unique)
|
||||
contributions.push_back(*position.unique);
|
||||
}
|
||||
if (!contributions.empty())
|
||||
result.emplace(origin, std::move(contributions));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void
|
||||
clear(uint256 const& origin)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
origins_.erase(origin);
|
||||
}
|
||||
|
||||
void
|
||||
clearAll()
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
origins_.clear();
|
||||
}
|
||||
|
||||
void
|
||||
cleanupStale(std::uint32_t currentSeq)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
for (auto it = origins_.begin(); it != origins_.end();)
|
||||
{
|
||||
auto const last = it->second.lastTouchedSeq;
|
||||
if (last > 0 && currentSeq > last &&
|
||||
currentSeq - last > maxStaleLedgers)
|
||||
it = origins_.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user