diff --git a/src/test/app/ExportSigCollectorV2_test.cpp b/src/test/app/ExportSigCollectorV2_test.cpp deleted file mode 100644 index e08dcdab5..000000000 --- a/src/test/app/ExportSigCollectorV2_test.cpp +++ /dev/null @@ -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 -#include -#include -#include -#include -#include - -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 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 diff --git a/src/test/app/ExportSigCollector_test.cpp b/src/test/app/ExportSigCollector_test.cpp index 71c604d35..b1a6c8f87 100644 --- a/src/test/app/ExportSigCollector_test.cpp +++ b/src/test/app/ExportSigCollector_test.cpp @@ -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 #include #include -#include 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(seed + 1), - static_cast(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 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(maxTrackedTxns - 1)); - auto const rejectedTx = - makeHashFromSeed(static_cast(maxTrackedTxns)); - auto const verifiedTx = - makeHashFromSeed(static_cast(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(); } }; diff --git a/src/test/app/ExportSignatureHarvester_test.cpp b/src/test/app/ExportSignatureHarvester_test.cpp deleted file mode 100644 index 03389a25c..000000000 --- a/src/test/app/ExportSignatureHarvester_test.cpp +++ /dev/null @@ -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 -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -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 -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(inner)); - - return std::make_shared(makeSTTx(exportObj)); -} - -std::shared_ptr -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(makeSTTx(exportObj)); -} - -std::string -makeBlob(uint256 const& txHash, PublicKey const& pk, Slice sig) -{ - std::string blob; - blob.append(reinterpret_cast(txHash.data()), txHash.size()); - blob.append(reinterpret_cast(pk.data()), pk.size()); - blob.append(reinterpret_cast(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 const sender_ = - randomKeyPair(KeyType::secp256k1); - std::pair const other_ = - randomKeyPair(KeyType::secp256k1); - uint256 const prevLedger_ = makeHash("export-harvester-prev-ledger"); - char const* source_ = "unit-test"; - - ExportSignatureHarvestInput - makeInput( - std::vector const& blobs, - ExportTxnLookup const& exportTxns, - bool active = true, - std::optional 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 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 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(txHash.data()), txHash.size()); - invalidPubkeyBlob.append(33, '\0'); - invalidPubkeyBlob.append("sig", 3); - - std::vector 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 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 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 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 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 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 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 diff --git a/src/test/consensus/ConsensusExtensions_test.cpp b/src/test/consensus/ConsensusExtensions_test.cpp index 98d98683f..483968c3d 100644 --- a/src/test/consensus/ConsensusExtensions_test.cpp +++ b/src/test/consensus/ConsensusExtensions_test.cpp @@ -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(); diff --git a/src/xrpld/app/consensus/ConsensusExtensions.cpp b/src/xrpld/app/consensus/ConsensusExtensions.cpp index 45876e9cc..220ea5f8e 100644 --- a/src/xrpld/app/consensus/ConsensusExtensions.cpp +++ b/src/xrpld/app/consensus/ConsensusExtensions.cpp @@ -18,7 +18,6 @@ //============================================================================== #include -#include #include #include #include @@ -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{ + std::pair{ origin, contribution.position}; if (proposalPublishedExportShares_.count(identity) != 0) continue; diff --git a/src/xrpld/app/consensus/ConsensusExtensions.h b/src/xrpld/app/consensus/ConsensusExtensions.h index eedbcd750..301daa06b 100644 --- a/src/xrpld/app/consensus/ConsensusExtensions.h +++ b/src/xrpld/app/consensus/ConsensusExtensions.h @@ -5,7 +5,7 @@ #include #include #include -#include +#include #include #include #include @@ -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::set> exportStreamEmittedShares_; - std::set> + std::set> proposalPublishedExportShares_; std::atomic exportShareServiceStarted_{false}; std::atomic 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_; diff --git a/src/xrpld/app/consensus/ExportSignatureHarvester.cpp b/src/xrpld/app/consensus/ExportSignatureHarvester.cpp deleted file mode 100644 index 481f690ab..000000000 --- a/src/xrpld/app/consensus/ExportSignatureHarvester.cpp +++ /dev/null @@ -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 -#include - -#include -#include -#include -#include -#include -#include - -#include - -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 diff --git a/src/xrpld/app/consensus/ExportSignatureHarvester.h b/src/xrpld/app/consensus/ExportSignatureHarvester.h deleted file mode 100644 index 20b0db983..000000000 --- a/src/xrpld/app/consensus/ExportSignatureHarvester.h +++ /dev/null @@ -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 -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -namespace ripple { - -using ExportTxnLookup = hash_map>; - -struct ExportSignatureHarvestInput -{ - PublicKey const& senderPK; - uint256 const& proposalPrevLedger; - std::vector const& exportSignatures; - std::optional activeViewSourceLedgerHash; - std::function 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 diff --git a/src/xrpld/app/misc/ExportSigCollector.h b/src/xrpld/app/misc/ExportSigCollector.h index 3632140ec..7a1619dcb 100644 --- a/src/xrpld/app/misc/ExportSigCollector.h +++ b/src/xrpld/app/misc/ExportSigCollector.h @@ -2,429 +2,471 @@ #define RIPPLE_APP_MISC_EXPORTSIGCOLLECTOR_H_INCLUDED #include -#include +#include #include -#include + +#include #include #include #include #include #include +#include +#include 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 validators; - /// Actual multisign signature bytes keyed by validator pubkey. - /// Empty buffers mean pubkey-only (standalone mode). - std::map signatures; - /// Validators whose sigs have been cryptographically verified. - /// Only these count toward quorum and appear in SHAMap/snapshot. - std::set 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 sigs_; - std::set 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 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 priorContribution; + std::optional conflictingContribution; + }; + + using UnionSnapshot = std::map>; + + // 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 unique; + bool conflicted{false}; + std::map reservations; + }; + + struct OriginEntry + { + std::map positions; + std::set published; + std::set publicationTriggers; + uint256 publicationTrigger; + std::uint64_t publicationGeneration{0}; + std::uint32_t lastTouchedSeq{0}; + }; + + mutable std::mutex mutex_; + std::unordered_map 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(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 - unverifiedSignatures(uint256 const& txnHash) const - { - std::lock_guard lock(mutex_); - std::map 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 + 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 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 - 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> - snapshot() const - { - std::lock_guard lock(mutex_); - std::unordered_map> 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 - std::unordered_map> - snapshotWithSigs(IncludeValidator includeValidator) const - { - std::lock_guard lock(mutex_); - std::unordered_map> result; - for (auto const& [hash, entry] : sigs_) - { - std::map 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> - 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 - std::optional> - 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 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> - 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 diff --git a/src/xrpld/app/misc/ExportSigCollectorV2.h b/src/xrpld/app/misc/ExportSigCollectorV2.h deleted file mode 100644 index 5a4075cf6..000000000 --- a/src/xrpld/app/misc/ExportSigCollectorV2.h +++ /dev/null @@ -1,475 +0,0 @@ -#ifndef RIPPLE_APP_MISC_EXPORTSIGCOLLECTORV2_H_INCLUDED -#define RIPPLE_APP_MISC_EXPORTSIGCOLLECTORV2_H_INCLUDED - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -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 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 priorContribution; - std::optional conflictingContribution; - }; - - using UnionSnapshot = std::map>; - - // 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 unique; - bool conflicted{false}; - std::map reservations; - }; - - struct OriginEntry - { - std::map positions; - std::set published; - std::set publicationTriggers; - uint256 publicationTrigger; - std::uint64_t publicationGeneration{0}; - std::uint32_t lastTouchedSeq{0}; - }; - - mutable std::mutex mutex_; - std::unordered_map 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(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 - 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 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