mirror of
https://github.com/Xahau/xahaud.git
synced 2026-09-27 07:36:52 +00:00
5516 lines
208 KiB
C++
5516 lines
208 KiB
C++
//------------------------------------------------------------------------------
|
|
/*
|
|
This file is part of rippled: https://github.com/ripple/rippled
|
|
|
|
Permission to use, copy, modify, and/or distribute this software for any
|
|
purpose with or without fee is hereby granted, provided that the above
|
|
copyright notice and this permission notice appear in all copies.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
//==============================================================================
|
|
|
|
#include <test/jtx.h>
|
|
#include <test/jtx/WSClient.h>
|
|
#include <xrpld/app/consensus/ActiveValidatorView.h>
|
|
#include <xrpld/app/consensus/ConsensusExtensions.h>
|
|
#include <xrpld/app/consensus/ProposalPrecheck.h>
|
|
#include <xrpld/app/ledger/BuildLedger.h>
|
|
#include <xrpld/app/ledger/InboundTransactions.h>
|
|
#include <xrpld/app/ledger/Ledger.h>
|
|
#include <xrpld/app/ledger/detail/TransactionAcquire.h>
|
|
#include <xrpld/app/main/CollectorManager.h>
|
|
#include <xrpld/app/misc/CanonicalTXSet.h>
|
|
#include <xrpld/app/misc/HashRouter.h>
|
|
#include <xrpld/app/misc/Manifest.h>
|
|
#include <xrpld/app/misc/NegativeUNLVote.h>
|
|
#include <xrpld/app/misc/RuntimeConfig.h>
|
|
#include <xrpld/app/misc/ValidatorKeys.h>
|
|
#include <xrpld/app/tx/detail/ExportLedgerOps.h>
|
|
#include <xrpld/app/tx/detail/ExportResultBuilder.h>
|
|
#include <xrpld/consensus/ConsensusExtensionsTick.h>
|
|
#include <xrpld/consensus/ConsensusProposal.h>
|
|
#include <xrpld/ledger/Sandbox.h>
|
|
#include <xrpld/overlay/PeerSet.h>
|
|
#include <xrpld/shamap/SHAMapSidecarLeafNode.h>
|
|
#include <xrpl/basics/StringUtilities.h>
|
|
#include <xrpl/beast/unit_test.h>
|
|
#include <xrpl/protocol/EntropyTier.h>
|
|
#include <xrpl/protocol/ExportCommittee.h>
|
|
#include <xrpl/protocol/ExportLimits.h>
|
|
#include <xrpl/protocol/ExportOriginMemo.h>
|
|
#include <xrpl/protocol/ExportShare.h>
|
|
#include <xrpl/protocol/Feature.h>
|
|
#include <xrpl/protocol/HashPrefix.h>
|
|
#include <xrpl/protocol/Indexes.h>
|
|
#include <xrpl/protocol/STAmount.h>
|
|
#include <xrpl/protocol/STObject.h>
|
|
#include <xrpl/protocol/STTx.h>
|
|
#include <xrpl/protocol/SidecarType.h>
|
|
#include <xrpl/protocol/Sign.h>
|
|
#include <xrpl/protocol/TxFlags.h>
|
|
#include <xrpl/protocol/TxFormats.h>
|
|
#include <xrpl/protocol/digest.h>
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cstring>
|
|
#include <deque>
|
|
#include <future>
|
|
#include <limits>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <tuple>
|
|
|
|
namespace ripple {
|
|
namespace test {
|
|
|
|
namespace {
|
|
|
|
Manifest
|
|
makeValidatorManifest(
|
|
SecretKey const& masterSecret,
|
|
SecretKey const& signingSecret,
|
|
std::uint32_t sequence = 0)
|
|
{
|
|
auto const master = derivePublicKey(KeyType::secp256k1, masterSecret);
|
|
auto const signing = derivePublicKey(KeyType::secp256k1, signingSecret);
|
|
STObject object{sfGeneric};
|
|
object.setFieldU32(sfSequence, sequence);
|
|
object.setFieldVL(sfPublicKey, master.slice());
|
|
object.setFieldVL(sfSigningPubKey, signing.slice());
|
|
sign(object, HashPrefix::manifest, KeyType::secp256k1, signingSecret);
|
|
sign(
|
|
object,
|
|
HashPrefix::manifest,
|
|
KeyType::secp256k1,
|
|
masterSecret,
|
|
sfMasterSignature);
|
|
|
|
Serializer serialized;
|
|
object.add(serialized);
|
|
auto manifest = deserializeManifest(std::string{
|
|
static_cast<char const*>(serialized.data()), serialized.size()});
|
|
if (!manifest)
|
|
Throw<std::runtime_error>("failed to build validator manifest");
|
|
return std::move(*manifest);
|
|
}
|
|
|
|
class ActiveNoopSink : public beast::Journal::Sink
|
|
{
|
|
public:
|
|
ActiveNoopSink() : Sink(beast::severities::kTrace, false)
|
|
{
|
|
}
|
|
|
|
bool
|
|
active(beast::severities::Severity) const override
|
|
{
|
|
return true;
|
|
}
|
|
|
|
void
|
|
write(beast::severities::Severity, std::string const&) override
|
|
{
|
|
}
|
|
|
|
void
|
|
writeAlways(beast::severities::Severity, std::string const&) override
|
|
{
|
|
}
|
|
};
|
|
|
|
beast::Journal
|
|
activeNoopJournal()
|
|
{
|
|
static ActiveNoopSink sink;
|
|
return beast::Journal{sink};
|
|
}
|
|
|
|
uint256
|
|
makeHash(char const* label)
|
|
{
|
|
return sha512Half(Slice(label, std::strlen(label)));
|
|
}
|
|
|
|
NodeID
|
|
makeNode(std::uint8_t id)
|
|
{
|
|
NodeID node;
|
|
node.zero();
|
|
node.data()[NodeID::size() - 1] = id;
|
|
return node;
|
|
}
|
|
|
|
AccountID
|
|
accountFromNode(NodeID const& nodeId)
|
|
{
|
|
AccountID account;
|
|
std::memcpy(account.data(), nodeId.data(), account.size());
|
|
return account;
|
|
}
|
|
|
|
Blob
|
|
expectedContributorMask(
|
|
std::vector<PublicKey> activeMasterKeys,
|
|
std::vector<NodeID> const& contributorNodes)
|
|
{
|
|
std::sort(activeMasterKeys.begin(), activeMasterKeys.end());
|
|
Blob mask((activeMasterKeys.size() + 7) / 8, 0);
|
|
for (std::size_t i = 0; i < activeMasterKeys.size(); ++i)
|
|
{
|
|
auto const nodeId = calcNodeID(activeMasterKeys[i]);
|
|
if (std::find(
|
|
contributorNodes.begin(), contributorNodes.end(), nodeId) ==
|
|
contributorNodes.end())
|
|
continue;
|
|
mask[i / 8] |= static_cast<std::uint8_t>(1u << (i % 8));
|
|
}
|
|
return mask;
|
|
}
|
|
|
|
Blob
|
|
standaloneContributorMask(std::uint16_t denominator, std::uint16_t count)
|
|
{
|
|
Blob mask((denominator + 7) / 8, 0);
|
|
for (std::uint16_t i = 0; i < std::min(denominator, count); ++i)
|
|
mask[i / 8] |= static_cast<std::uint8_t>(1u << (i % 8));
|
|
return mask;
|
|
}
|
|
|
|
std::string
|
|
makeExportSigBlob(uint256 const& txHash, PublicKey const& publicKey)
|
|
{
|
|
std::string blob;
|
|
blob.append(reinterpret_cast<char const*>(txHash.data()), uint256::size());
|
|
blob.append(
|
|
reinterpret_cast<char const*>(publicKey.data()), publicKey.size());
|
|
blob.push_back('\x30');
|
|
return blob;
|
|
}
|
|
|
|
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,
|
|
std::uint32_t ticketSequence = 1)
|
|
{
|
|
STObject obj(sfExportedTxn);
|
|
obj.setFieldU16(sfTransactionType, ttPAYMENT);
|
|
obj.setFieldU32(sfFlags, tfFullyCanonicalSig);
|
|
obj.setFieldU32(sfSequence, 0);
|
|
obj.setFieldU32(sfTicketSequence, ticketSequence);
|
|
obj.setFieldU32(sfFirstLedgerSequence, 2);
|
|
obj.setFieldU32(sfLastLedgerSequence, 6);
|
|
obj.setFieldAmount(sfAmount, XRPAmount{1000000});
|
|
obj.setFieldAmount(sfFee, XRPAmount{10});
|
|
obj.setFieldVL(sfSigningPubKey, Blob{});
|
|
obj.setAccountID(sfAccount, src);
|
|
obj.setAccountID(sfDestination, dst);
|
|
return obj;
|
|
}
|
|
|
|
std::shared_ptr<STTx const>
|
|
makeConsensusEntropyTx(
|
|
std::uint32_t ledgerSeq,
|
|
uint256 const& digest,
|
|
std::uint16_t count)
|
|
{
|
|
STObject obj(sfGeneric);
|
|
obj.setFieldU16(sfTransactionType, ttCONSENSUS_ENTROPY);
|
|
obj.setFieldU32(sfLedgerSequence, ledgerSeq);
|
|
obj.setAccountID(sfAccount, AccountID{});
|
|
obj.setFieldU32(sfSequence, 0);
|
|
obj.setFieldAmount(sfFee, STAmount{});
|
|
obj.setFieldVL(sfSigningPubKey, Slice{});
|
|
obj.setFieldH256(sfDigest, digest);
|
|
obj.setFieldU16(sfEntropyCount, count);
|
|
obj.setFieldU16(sfEntropyDenominator, count);
|
|
obj.setFieldVL(
|
|
sfEntropyContributors, standaloneContributorMask(count, count));
|
|
obj.setFieldU8(sfEntropyTier, entropyTierValidatorFull);
|
|
return std::make_shared<STTx const>(makeSTTx(obj));
|
|
}
|
|
|
|
std::shared_ptr<STTx const>
|
|
makeExportSignaturesTx(std::uint32_t ledgerSeq, uint256 const& exportTxHash)
|
|
{
|
|
auto const signer = randomKeyPair(KeyType::secp256k1);
|
|
auto const dst = calcAccountID(randomKeyPair(KeyType::secp256k1).first);
|
|
auto const innerObj = makeExportedPayment(calcAccountID(signer.first), dst);
|
|
auto const innerTx = makeSTTx(innerObj);
|
|
|
|
ExportResultBuilder::PositionedSignatureSnapshot signatures;
|
|
auto sig = ExportResultBuilder::signExportedTxn(
|
|
innerTx, signer.first, signer.second);
|
|
signatures.emplace(
|
|
0,
|
|
ExportResultBuilder::PositionedSignature{
|
|
signer.first, Buffer(sig.data(), sig.size())});
|
|
|
|
return std::make_shared<STTx const>(
|
|
ExportResultBuilder::buildSignatureWitness(
|
|
exportTxHash, innerTx, signatures, 1, ledgerSeq));
|
|
}
|
|
|
|
RCLTxSet
|
|
makeRCLTxSet(Application& app, std::vector<std::shared_ptr<STTx const>> txns)
|
|
{
|
|
auto map =
|
|
std::make_shared<SHAMap>(SHAMapType::TRANSACTION, app.getNodeFamily());
|
|
map->setUnbacked();
|
|
|
|
for (auto const& tx : txns)
|
|
{
|
|
Serializer s;
|
|
tx->add(s);
|
|
map->addItem(
|
|
SHAMapNodeType::tnTRANSACTION_NM,
|
|
make_shamapitem(tx->getTransactionID(), s.slice()));
|
|
}
|
|
|
|
return RCLTxSet{map->snapShot(false)};
|
|
}
|
|
|
|
void
|
|
forceNonStandalone(Application& app)
|
|
{
|
|
const_cast<Config&>(app.config()).setupControl(true, true, false);
|
|
}
|
|
|
|
std::shared_ptr<STTx const>
|
|
singleCanonicalTx(CanonicalTXSet const& txs)
|
|
{
|
|
if (std::distance(txs.begin(), txs.end()) != 1)
|
|
return {};
|
|
return txs.begin()->second;
|
|
}
|
|
|
|
uint256
|
|
expectedEntropy(
|
|
std::vector<std::pair<PublicKey, uint256>> const& orderedContributions)
|
|
{
|
|
Serializer s;
|
|
for (auto const& [key, reveal] : orderedContributions)
|
|
{
|
|
s.addVL(key.slice());
|
|
s.addBitString(reveal);
|
|
}
|
|
return sha512Half(s.slice());
|
|
}
|
|
|
|
uint256
|
|
expectedEntropy(PublicKey const& key, uint256 const& reveal)
|
|
{
|
|
return expectedEntropy({{key, reveal}});
|
|
}
|
|
|
|
Buffer
|
|
signPosition(
|
|
PublicKey const& publicKey,
|
|
SecretKey const& secretKey,
|
|
ExtendedPosition const& position,
|
|
std::uint32_t proposeSeq,
|
|
NetClock::time_point closeTime,
|
|
uint256 const& prevLedger)
|
|
{
|
|
using Proposal = ConsensusProposal<NodeID, uint256, ExtendedPosition>;
|
|
Proposal proposal{
|
|
prevLedger,
|
|
proposeSeq,
|
|
position,
|
|
closeTime,
|
|
NetClock::time_point{},
|
|
calcNodeID(publicKey)};
|
|
|
|
auto const sig = signDigest(publicKey, secretKey, proposal.signingHash());
|
|
return Buffer(sig.data(), sig.size());
|
|
}
|
|
|
|
struct FakeTxSet
|
|
{
|
|
using ID = uint256;
|
|
|
|
uint256 hash;
|
|
|
|
uint256
|
|
id() const
|
|
{
|
|
return hash;
|
|
}
|
|
};
|
|
|
|
class FakePeerPosition
|
|
{
|
|
public:
|
|
using Proposal = ConsensusProposal<NodeID, uint256, ExtendedPosition>;
|
|
|
|
FakePeerPosition(NodeID const& nodeId, ExtendedPosition const& position)
|
|
: proposal_(
|
|
uint256{},
|
|
Proposal::seqJoin,
|
|
position,
|
|
NetClock::time_point{},
|
|
NetClock::time_point{},
|
|
nodeId)
|
|
{
|
|
}
|
|
|
|
Proposal const&
|
|
proposal() const
|
|
{
|
|
return proposal_;
|
|
}
|
|
|
|
private:
|
|
Proposal proposal_;
|
|
};
|
|
|
|
struct FakeExtensions
|
|
{
|
|
beast::Journal j_{activeNoopJournal()};
|
|
EstablishState estState_{EstablishState::ConvergingTx};
|
|
std::chrono::steady_clock::time_point revealPhaseStart_{};
|
|
std::chrono::steady_clock::time_point commitHashConflictStart_{};
|
|
bool entropySetPublished_{false};
|
|
std::chrono::steady_clock::time_point entropyPublishStart_{};
|
|
bool exportSigGateStarted_{false};
|
|
std::chrono::steady_clock::time_point exportSigGateStart_{};
|
|
bool exportSigConvergenceFailed_{false};
|
|
bool rngOn{false};
|
|
bool localExportSigs{true};
|
|
bool livePendingExportLatches{false};
|
|
bool exportOn{true};
|
|
bool exportViewAnchored{true};
|
|
bool entropyFailed{false};
|
|
bool commitFrozen{false};
|
|
std::size_t sidecarQuorum{4};
|
|
std::size_t commits{4};
|
|
std::size_t proofedCommits{4};
|
|
std::size_t reveals{4};
|
|
bool commitQuorum{true};
|
|
bool minimumReveals{true};
|
|
bool anyReveals{true};
|
|
uint256 exportHash{makeHash("local-export-sig-set")};
|
|
uint256 commitHash{makeHash("local-commit-set")};
|
|
uint256 entropyHash{makeHash("local-entropy-set")};
|
|
std::deque<uint256> exportHashSequence;
|
|
std::deque<uint256> commitHashSequence;
|
|
std::deque<uint256> entropyHashSequence;
|
|
int commitBuilds = 0;
|
|
int exportBuilds = 0;
|
|
int acceptedExportClears = 0;
|
|
std::optional<uint256> acceptedExportHash;
|
|
std::optional<uint256> acceptedEntropyHash;
|
|
int entropyBuilds = 0;
|
|
int participantDiagnostics = 0;
|
|
int selfSeeds = 0;
|
|
|
|
bool
|
|
rngEnabled() const
|
|
{
|
|
return rngOn;
|
|
}
|
|
|
|
bool
|
|
exportEnabled() const
|
|
{
|
|
return exportOn;
|
|
}
|
|
|
|
bool
|
|
exportFinalizationViewAnchored() const
|
|
{
|
|
return exportViewAnchored;
|
|
}
|
|
|
|
bool
|
|
testSuppressExportSigSetHash() const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
std::size_t
|
|
quorumThreshold() const
|
|
{
|
|
return sidecarQuorum;
|
|
}
|
|
|
|
std::size_t
|
|
entropyGateThreshold() const
|
|
{
|
|
// Stub default mirrors quorumThreshold (tier-2 band collapsed); the
|
|
// tier-2 step-down is exercised end-to-end in the CSF sims.
|
|
return sidecarQuorum;
|
|
}
|
|
|
|
std::size_t
|
|
exportRootAlignmentThreshold() const
|
|
{
|
|
return sidecarQuorum;
|
|
}
|
|
|
|
// Membership is a no-op in the FakeExtensions tick tests (every peer
|
|
// counts, local counts) so the gate behavior is unchanged; F1 active-view
|
|
// filtering is exercised directly against inspectTxConvergedSidecarPeers.
|
|
template <class Id>
|
|
bool
|
|
isUNLReportMember(Id const&) const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
localIsActiveValidator() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
std::size_t
|
|
pendingCommitCount() const
|
|
{
|
|
return rngOn ? commits : 0;
|
|
}
|
|
|
|
std::size_t
|
|
proofedCommitCount() const
|
|
{
|
|
return rngOn ? proofedCommits : 0;
|
|
}
|
|
|
|
std::size_t
|
|
pendingRevealCount() const
|
|
{
|
|
return rngOn ? reveals : 0;
|
|
}
|
|
|
|
std::size_t
|
|
expectedProposerCount() const
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
bool
|
|
hasQuorumOfCommits() const
|
|
{
|
|
return rngOn && commitQuorum;
|
|
}
|
|
|
|
bool
|
|
hasMinimumReveals() const
|
|
{
|
|
return rngOn && minimumReveals;
|
|
}
|
|
|
|
bool
|
|
hasAnyReveals() const
|
|
{
|
|
return rngOn && anyReveals;
|
|
}
|
|
|
|
uint256
|
|
buildCommitSet(LedgerIndex)
|
|
{
|
|
++commitBuilds;
|
|
if (!commitHashSequence.empty())
|
|
{
|
|
auto ret = commitHashSequence.front();
|
|
commitHashSequence.pop_front();
|
|
return ret;
|
|
}
|
|
return commitHash;
|
|
}
|
|
|
|
uint256
|
|
buildEntropySet(LedgerIndex)
|
|
{
|
|
++entropyBuilds;
|
|
if (!entropyHashSequence.empty())
|
|
{
|
|
auto ret = entropyHashSequence.front();
|
|
entropyHashSequence.pop_front();
|
|
return ret;
|
|
}
|
|
return entropyHash;
|
|
}
|
|
|
|
void
|
|
acceptEntropySet(uint256 const& hash)
|
|
{
|
|
acceptedEntropyHash = hash;
|
|
}
|
|
|
|
void
|
|
clearAcceptedEntropySet()
|
|
{
|
|
acceptedEntropyHash.reset();
|
|
}
|
|
|
|
uint256
|
|
getEntropySecret() const
|
|
{
|
|
return makeHash("entropy-secret");
|
|
}
|
|
|
|
void
|
|
selfSeedReveal()
|
|
{
|
|
++selfSeeds;
|
|
}
|
|
|
|
void
|
|
setEntropyFailed()
|
|
{
|
|
entropyFailed = true;
|
|
}
|
|
|
|
void
|
|
freezeRngCommitSet()
|
|
{
|
|
commitFrozen = true;
|
|
}
|
|
|
|
bool
|
|
hasPendingExportSigs() const
|
|
{
|
|
return localExportSigs;
|
|
}
|
|
|
|
bool
|
|
hasEligiblePendingExports() const
|
|
{
|
|
return livePendingExportLatches;
|
|
}
|
|
|
|
uint256
|
|
buildExportSigSet(LedgerIndex)
|
|
{
|
|
++exportBuilds;
|
|
if (!exportHashSequence.empty())
|
|
{
|
|
auto ret = exportHashSequence.front();
|
|
exportHashSequence.pop_front();
|
|
return ret;
|
|
}
|
|
return exportHash;
|
|
}
|
|
|
|
void
|
|
setExportSigConvergenceFailed()
|
|
{
|
|
exportSigConvergenceFailed_ = true;
|
|
}
|
|
|
|
void
|
|
acceptExportSigSet(uint256 const& hash)
|
|
{
|
|
acceptedExportHash = hash;
|
|
}
|
|
|
|
void
|
|
clearAcceptedExportSigSet()
|
|
{
|
|
++acceptedExportClears;
|
|
acceptedExportHash.reset();
|
|
}
|
|
|
|
template <class PeerPositions>
|
|
void
|
|
recordParticipantDiagnostics(ConsensusMode, PeerPositions const&)
|
|
{
|
|
++participantDiagnostics;
|
|
}
|
|
|
|
std::size_t
|
|
observedParticipantCount() const
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
std::optional<uint256>
|
|
observedParticipantsHash() const
|
|
{
|
|
return makeHash("observed-participants");
|
|
}
|
|
|
|
std::string
|
|
observedParticipantsBitmapBin() const
|
|
{
|
|
return "1";
|
|
}
|
|
};
|
|
|
|
struct ExtensionTickHarness
|
|
{
|
|
ExtendedPosition position{makeHash("tx-set")};
|
|
FakeTxSet txns{position.txSetHash};
|
|
hash_map<NodeID, FakePeerPosition> peers;
|
|
ConsensusParms parms;
|
|
NetClock::time_point netNow{NetClock::duration{123}};
|
|
std::chrono::steady_clock::time_point start{};
|
|
ConsensusMode mode = ConsensusMode::proposing;
|
|
std::size_t prevProposers = 4;
|
|
LedgerIndex buildSeq = 2;
|
|
int updates = 0;
|
|
int proposes = 0;
|
|
|
|
void
|
|
addPeer(
|
|
std::uint8_t id,
|
|
std::optional<uint256> exportSigSetHash,
|
|
uint256 txSetHash = makeHash("tx-set"))
|
|
{
|
|
ExtendedPosition peerPosition{txSetHash};
|
|
peerPosition.exportSigSetHash = exportSigSetHash;
|
|
peers.emplace(
|
|
makeNode(id), FakePeerPosition{makeNode(id), peerPosition});
|
|
}
|
|
|
|
void
|
|
addBothPeer(std::uint8_t id, FakeExtensions const& ext)
|
|
{
|
|
ExtendedPosition peerPosition{position.txSetHash};
|
|
peerPosition.commitSetHash = ext.commitHash;
|
|
peerPosition.entropySetHash = ext.entropyHash;
|
|
peerPosition.exportSigSetHash = ext.exportHash;
|
|
peers.emplace(
|
|
makeNode(id), FakePeerPosition{makeNode(id), peerPosition});
|
|
}
|
|
|
|
void
|
|
addEntropyPeer(
|
|
std::uint8_t id,
|
|
std::optional<uint256> entropySetHash,
|
|
uint256 txSetHash = makeHash("tx-set"))
|
|
{
|
|
ExtendedPosition peerPosition{txSetHash};
|
|
peerPosition.entropySetHash = entropySetHash;
|
|
peers.emplace(
|
|
makeNode(id), FakePeerPosition{makeNode(id), peerPosition});
|
|
}
|
|
|
|
void
|
|
addCommitPeer(
|
|
std::uint8_t id,
|
|
std::optional<uint256> commitSetHash,
|
|
uint256 txSetHash = makeHash("tx-set"))
|
|
{
|
|
ExtendedPosition peerPosition{txSetHash};
|
|
peerPosition.commitSetHash = commitSetHash;
|
|
peers.emplace(
|
|
makeNode(id), FakePeerPosition{makeNode(id), peerPosition});
|
|
}
|
|
|
|
template <class Extensions>
|
|
ExtensionTickResult
|
|
tick(Extensions& ext, std::chrono::milliseconds elapsed = {})
|
|
{
|
|
ConsensusTick<ExtendedPosition, FakePeerPosition, FakeTxSet> ctx{
|
|
.buildSeq = buildSeq,
|
|
.now = netNow,
|
|
.nowSteady = start + elapsed,
|
|
.roundTime = elapsed,
|
|
.mode = mode,
|
|
.prevProposers = prevProposers,
|
|
.peerPositions = peers,
|
|
.parms = parms,
|
|
.haveCloseTimeConsensus = true,
|
|
.convergePercent = 100,
|
|
.j = activeNoopJournal(),
|
|
.getPosition = [&]() -> ExtendedPosition const& {
|
|
return position;
|
|
},
|
|
.updatePosition =
|
|
[&](ExtendedPosition const& newPosition) {
|
|
position = newPosition;
|
|
++updates;
|
|
},
|
|
.propose = [&]() { ++proposes; },
|
|
.haveConsensus = []() { return true; },
|
|
.getTxns = [&]() -> FakeTxSet const& { return txns; }};
|
|
|
|
return extensionsTick(ext, ctx);
|
|
}
|
|
};
|
|
|
|
// Build an in-memory ledger carrying a UNLReport with the given active
|
|
// validator keys (optionally disabling some via NegativeUNL). Drives the RNG
|
|
// active-validator view. NOTE: it is not registered with the LedgerMaster, so
|
|
// it suits cacheUNLReport()/makeActiveValidatorView() (which read its SLEs
|
|
// directly) but NOT reveal verification (which resolves the round's prev ledger
|
|
// through the LedgerMaster — anchor harvests to a real closed ledger instead).
|
|
std::shared_ptr<Ledger>
|
|
makeUNLReportLedger(
|
|
jtx::Env& env,
|
|
std::vector<PublicKey> const& activeKeys,
|
|
std::vector<PublicKey> const& disabledKeys = {})
|
|
{
|
|
auto const genesis = std::make_shared<Ledger>(
|
|
create_genesis,
|
|
env.app().config(),
|
|
std::vector<uint256>{},
|
|
env.app().getNodeFamily());
|
|
auto ledger =
|
|
std::make_shared<Ledger>(*genesis, env.app().timeKeeper().closeTime());
|
|
|
|
auto report = std::make_shared<SLE>(keylet::UNLReport());
|
|
std::vector<STObject> active;
|
|
active.reserve(activeKeys.size());
|
|
for (auto const& pk : activeKeys)
|
|
{
|
|
active.push_back(STObject::makeInnerObject(sfActiveValidator));
|
|
active.back().setFieldVL(sfPublicKey, pk);
|
|
}
|
|
report->setFieldArray(
|
|
sfActiveValidators, STArray(active, sfActiveValidators));
|
|
|
|
OpenView accum(&*ledger);
|
|
accum.rawInsert(report);
|
|
|
|
if (!disabledKeys.empty())
|
|
{
|
|
auto negUnl = std::make_shared<SLE>(keylet::negativeUNL());
|
|
std::vector<STObject> disabled;
|
|
disabled.reserve(disabledKeys.size());
|
|
for (auto const& pk : disabledKeys)
|
|
{
|
|
disabled.push_back(STObject::makeInnerObject(sfDisabledValidator));
|
|
disabled.back().setFieldVL(sfPublicKey, pk);
|
|
disabled.back().setFieldU32(sfFirstLedgerSequence, ledger->seq());
|
|
}
|
|
negUnl->setFieldArray(
|
|
sfDisabledValidators, STArray(disabled, sfDisabledValidators));
|
|
accum.rawInsert(negUnl);
|
|
}
|
|
|
|
accum.apply(*ledger);
|
|
return ledger;
|
|
}
|
|
|
|
// Harvest a commit (proposeSeq 0) + matching reveal (proposeSeq 1) from one
|
|
// validator into `ce`. The commitment binds seq, which reveal verification
|
|
// recomputes as prevLedger->seq()+1 (resolved through the LedgerMaster), so
|
|
// pass a prevLedger that is actually stored and a matching seq.
|
|
void
|
|
harvestCommitReveal(
|
|
ConsensusExtensions& ce,
|
|
NodeID const& nodeId,
|
|
PublicKey const& pk,
|
|
SecretKey const& sk,
|
|
uint256 const& txSetHash,
|
|
LedgerIndex seq,
|
|
NetClock::time_point closeTime,
|
|
uint256 const& prevLedger,
|
|
uint256 const& reveal)
|
|
{
|
|
// nodeId is explicit (not calcNodeID(pk)): a validator's view identity is
|
|
// its master-key NodeID, which can differ from its signing pubkey.
|
|
auto const commitment = sha512Half(reveal, pk, seq);
|
|
|
|
ExtendedPosition commitPos{txSetHash};
|
|
commitPos.myCommitment = commitment;
|
|
auto const commitSig =
|
|
signPosition(pk, sk, commitPos, 0, closeTime, prevLedger);
|
|
ce.harvestRngData(
|
|
nodeId,
|
|
pk,
|
|
commitPos,
|
|
0,
|
|
closeTime,
|
|
prevLedger,
|
|
Slice(commitSig.data(), commitSig.size()));
|
|
|
|
ExtendedPosition revealPos{txSetHash};
|
|
revealPos.myReveal = reveal;
|
|
auto const revealSig =
|
|
signPosition(pk, sk, revealPos, 1, closeTime, prevLedger);
|
|
ce.harvestRngData(
|
|
nodeId,
|
|
pk,
|
|
revealPos,
|
|
1,
|
|
closeTime,
|
|
prevLedger,
|
|
Slice(revealSig.data(), revealSig.size()));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
class ConsensusExtensions_test : public beast::unit_test::suite
|
|
{
|
|
std::vector<PublicKey>
|
|
makeValidatorKeys() const
|
|
{
|
|
std::vector<std::string> const rawKeys = {
|
|
"0388935426E0D08083314842EDFBB2D517BD47699F9A4527318A8E10468C97C05"
|
|
"2",
|
|
"02691AC5AE1C4C333AE5DF8A93BDC495F0EEBFC6DB0DA7EB6EF808F3AFC006E3F"
|
|
"E"};
|
|
|
|
std::vector<PublicKey> keys;
|
|
keys.reserve(rawKeys.size());
|
|
for (auto const& rawKey : rawKeys)
|
|
{
|
|
auto const pkHex = strUnHex(rawKey);
|
|
keys.emplace_back(makeSlice(*pkHex));
|
|
}
|
|
return keys;
|
|
}
|
|
|
|
void
|
|
testSidecarPeerAlignmentHelper()
|
|
{
|
|
testcase("Sidecar peer alignment helper");
|
|
|
|
BEAST_EXPECT(detail::sidecarLocalContribution(true) == 1);
|
|
BEAST_EXPECT(detail::sidecarLocalContribution(false) == 0);
|
|
BEAST_EXPECT(detail::sidecarLocalContribution(true, true) == 1);
|
|
BEAST_EXPECT(detail::sidecarLocalContribution(true, false) == 0);
|
|
BEAST_EXPECT(detail::sidecarLocalContribution(false, true) == 0);
|
|
BEAST_EXPECT(detail::sidecarAlignedParticipants(2, true) == 3);
|
|
BEAST_EXPECT(detail::sidecarAlignedParticipants(2, false) == 2);
|
|
BEAST_EXPECT(detail::sidecarQuorumAligned(2, true, 3));
|
|
BEAST_EXPECT(!detail::sidecarQuorumAligned(2, true, 4));
|
|
BEAST_EXPECT(detail::sidecarFullObservation(2, 2));
|
|
BEAST_EXPECT(!detail::sidecarFullObservation(2, 3));
|
|
|
|
ExtensionTickHarness harness;
|
|
auto const localHash = makeHash("sidecar-local");
|
|
auto const conflictHash = makeHash("sidecar-conflict");
|
|
harness.position.exportSigSetHash = localHash;
|
|
harness.addPeer(1, localHash);
|
|
harness.addPeer(2, conflictHash);
|
|
harness.addPeer(3, std::nullopt);
|
|
harness.addPeer(4, localHash, makeHash("other-tx-set"));
|
|
|
|
auto const exportHashOf = [](auto const& position) {
|
|
return position.exportSigSetHash;
|
|
};
|
|
auto const allMembers = [](auto const&) { return true; };
|
|
|
|
std::vector<uint256> mismatches;
|
|
auto const state = detail::inspectTxConvergedSidecarPeers(
|
|
harness.peers,
|
|
harness.position,
|
|
true,
|
|
true,
|
|
exportHashOf,
|
|
allMembers,
|
|
[&](auto const& hash) {
|
|
if (hash)
|
|
mismatches.push_back(*hash);
|
|
});
|
|
|
|
BEAST_EXPECT(state.localCounts);
|
|
BEAST_EXPECT(state.conflict);
|
|
BEAST_EXPECT(state.aligned == 1);
|
|
BEAST_EXPECT(state.alignedParticipants() == 2);
|
|
BEAST_EXPECT(state.peersSeen == 2);
|
|
BEAST_EXPECT(state.txConverged == 3);
|
|
BEAST_EXPECT(state.quorumAligned(2));
|
|
BEAST_EXPECT(!state.quorumAligned(3));
|
|
BEAST_EXPECT(!state.fullObservation());
|
|
BEAST_EXPECT(mismatches.size() == 1);
|
|
if (!mismatches.empty())
|
|
BEAST_EXPECT(mismatches.front() == conflictHash);
|
|
|
|
harness.position.exportSigSetHash.reset();
|
|
auto const unpublishedState = detail::inspectTxConvergedSidecarPeers(
|
|
harness.peers,
|
|
harness.position,
|
|
true,
|
|
false,
|
|
exportHashOf,
|
|
allMembers,
|
|
[](auto const&) {});
|
|
BEAST_EXPECT(!unpublishedState.localCounts);
|
|
BEAST_EXPECT(unpublishedState.alignedParticipants() == 0);
|
|
BEAST_EXPECT(!unpublishedState.quorumAligned(1));
|
|
BEAST_EXPECT(unpublishedState.fullObservation());
|
|
|
|
//@@start test-sidecar-active-view-filter
|
|
// F1: the alignment-counting universe must be the active validator
|
|
// view, not the full trusted-proposer set. A trusted-but-non-active
|
|
// proposer (node 5) that tx-converges and aligns on the SAME hash must
|
|
// NOT inflate alignedParticipants() — otherwise two equivocation
|
|
// cohorts padded by non-active peers could each clear the gate.
|
|
harness.position.exportSigSetHash = localHash;
|
|
harness.addPeer(5, localHash); // trusted, but outside the active view
|
|
auto const activeOnly = [](auto const& id) {
|
|
return id != makeNode(5); // nodes 1..4 active; 5 is not
|
|
};
|
|
|
|
auto const padded = detail::inspectTxConvergedSidecarPeers(
|
|
harness.peers,
|
|
harness.position,
|
|
true,
|
|
true,
|
|
exportHashOf,
|
|
allMembers,
|
|
[](auto const&) {});
|
|
BEAST_EXPECT(padded.aligned == 2); // node 1 + node 5, unfiltered
|
|
|
|
auto const filtered = detail::inspectTxConvergedSidecarPeers(
|
|
harness.peers,
|
|
harness.position,
|
|
true,
|
|
true,
|
|
exportHashOf,
|
|
activeOnly,
|
|
[](auto const&) {});
|
|
BEAST_EXPECT(filtered.aligned == 1); // node 5 excluded
|
|
BEAST_EXPECT(filtered.alignedParticipants() == 2); // node 1 + local
|
|
BEAST_EXPECT(!filtered.quorumAligned(3)); // padding can't reach quorum
|
|
|
|
// The local +1 is likewise gated on local active-view membership.
|
|
auto const nonActiveLocal = detail::inspectTxConvergedSidecarPeers(
|
|
harness.peers,
|
|
harness.position,
|
|
false,
|
|
true,
|
|
exportHashOf,
|
|
activeOnly,
|
|
[](auto const&) {});
|
|
BEAST_EXPECT(!nonActiveLocal.localCounts);
|
|
BEAST_EXPECT(nonActiveLocal.alignedParticipants() == 1); // node 1 only
|
|
//@@end test-sidecar-active-view-filter
|
|
}
|
|
|
|
void
|
|
testSidecarSplitBrainEquivocationThreshold()
|
|
{
|
|
testcase("Sidecar split-brain equivocation threshold");
|
|
|
|
auto const hashA = makeHash("split-brain-sidecar-a");
|
|
auto const hashB = makeHash("split-brain-sidecar-b");
|
|
auto const exportHashOf = [](auto const& position) {
|
|
return position.exportSigSetHash;
|
|
};
|
|
auto const allMembers = [](auto const&) { return true; };
|
|
|
|
auto inspect = [&](std::uint8_t localId,
|
|
uint256 const& localHash,
|
|
std::vector<std::uint8_t> const& hashANodes,
|
|
std::vector<std::uint8_t> const& hashBNodes) {
|
|
ExtensionTickHarness harness;
|
|
harness.position.exportSigSetHash = localHash;
|
|
auto addPeer = [&](std::uint8_t id, uint256 const& hash) {
|
|
if (id != localId)
|
|
harness.addPeer(id, hash);
|
|
};
|
|
for (auto id : hashANodes)
|
|
addPeer(id, hashA);
|
|
for (auto id : hashBNodes)
|
|
addPeer(id, hashB);
|
|
|
|
return detail::inspectTxConvergedSidecarPeers(
|
|
harness.peers,
|
|
harness.position,
|
|
true,
|
|
true,
|
|
exportHashOf,
|
|
allMembers,
|
|
[](auto const&) {});
|
|
};
|
|
|
|
// n=9, f=floor(n/5)=1. One equivocator (node 8) can show hashA to
|
|
// nodes 0..3 and hashB to nodes 4..7. A plain strict-majority gate
|
|
// (5/9) would let both local views proceed on different hashes. The
|
|
// participant threshold is 6, so neither split cohort can pass.
|
|
{
|
|
auto const viewA = inspect(0, hashA, {0, 1, 2, 3, 8}, {4, 5, 6, 7});
|
|
auto const viewB = inspect(4, hashB, {0, 1, 2, 3}, {4, 5, 6, 7, 8});
|
|
auto const strictMajority = std::size_t{5};
|
|
auto const participant = calculateParticipantThreshold(9);
|
|
|
|
BEAST_EXPECT(viewA.alignedParticipants() == strictMajority);
|
|
BEAST_EXPECT(viewB.alignedParticipants() == strictMajority);
|
|
BEAST_EXPECT(viewA.quorumAligned(strictMajority));
|
|
BEAST_EXPECT(viewB.quorumAligned(strictMajority));
|
|
BEAST_EXPECT(participant == 6);
|
|
BEAST_EXPECT(!viewA.quorumAligned(participant));
|
|
BEAST_EXPECT(!viewB.quorumAligned(participant));
|
|
}
|
|
|
|
// n=10, f=2. Two equivocators (8,9) make two disjoint honest cohorts
|
|
// appear as 6/10 each. Naive ceil(0.6n) would fork at this exact
|
|
// multiple of five; calculateParticipantThreshold bumps the floor to 7.
|
|
{
|
|
auto const viewA =
|
|
inspect(0, hashA, {0, 1, 2, 3, 8, 9}, {4, 5, 6, 7});
|
|
auto const viewB =
|
|
inspect(4, hashB, {0, 1, 2, 3}, {4, 5, 6, 7, 8, 9});
|
|
auto const naiveSixty = std::size_t{6};
|
|
auto const participant = calculateParticipantThreshold(10);
|
|
|
|
BEAST_EXPECT(viewA.alignedParticipants() == naiveSixty);
|
|
BEAST_EXPECT(viewB.alignedParticipants() == naiveSixty);
|
|
BEAST_EXPECT(viewA.quorumAligned(naiveSixty));
|
|
BEAST_EXPECT(viewB.quorumAligned(naiveSixty));
|
|
BEAST_EXPECT(participant == 7);
|
|
BEAST_EXPECT(!viewA.quorumAligned(participant));
|
|
BEAST_EXPECT(!viewB.quorumAligned(participant));
|
|
}
|
|
}
|
|
|
|
void
|
|
testActiveValidatorViewBuilderPrefersUNLReport()
|
|
{
|
|
testcase("Active validator view builder prefers UNLReport");
|
|
|
|
auto const keys = makeValidatorKeys();
|
|
ActiveValidatorViewSource source;
|
|
source.sourceLedgerHash = makeHash("active-validator-view-source");
|
|
source.unlReportMasterKeys.emplace();
|
|
source.unlReportMasterKeys->insert(keys[0]);
|
|
source.negativeUNLEnabled = true;
|
|
source.negativeUNL.insert(keys[1]);
|
|
|
|
ActiveValidatorViewFallback fallback;
|
|
fallback.trustedMasterKeys.insert(keys[1]);
|
|
fallback.localMasterKey = keys[1];
|
|
|
|
auto const view = buildActiveValidatorView(source, fallback);
|
|
BEAST_EXPECT(view.fromUNLReport);
|
|
BEAST_EXPECT(view.sourceLedgerHash == source.sourceLedgerHash);
|
|
BEAST_EXPECT(view.size() == 1);
|
|
BEAST_EXPECT(view.originalViewSize == 1);
|
|
BEAST_EXPECT(view.containsMaster(keys[0]));
|
|
BEAST_EXPECT(view.containsOriginalMaster(keys[0]));
|
|
BEAST_EXPECT(!view.containsMaster(keys[1]));
|
|
BEAST_EXPECT(!view.containsOriginalMaster(keys[1]));
|
|
BEAST_EXPECT(
|
|
view.orderedOriginalMasterKeys == std::vector<PublicKey>{keys[0]});
|
|
BEAST_EXPECT(view.containsNode(calcNodeID(keys[0])));
|
|
}
|
|
|
|
void
|
|
testActiveValidatorViewBuilderFallback()
|
|
{
|
|
testcase("Active validator view builder fallback");
|
|
|
|
auto const keys = makeValidatorKeys();
|
|
ActiveValidatorViewSource source;
|
|
source.sourceLedgerHash = makeHash("active-validator-view-fallback");
|
|
|
|
ActiveValidatorViewFallback fallback;
|
|
fallback.trustedMasterKeys.insert(keys[0]);
|
|
fallback.trustedMasterKeys.insert(keys[1]);
|
|
fallback.localMasterKey = keys[1];
|
|
|
|
auto const view = buildActiveValidatorView(source, fallback);
|
|
BEAST_EXPECT(!view.fromUNLReport);
|
|
BEAST_EXPECT(view.sourceLedgerHash == source.sourceLedgerHash);
|
|
BEAST_EXPECT(view.size() == 2);
|
|
BEAST_EXPECT(view.originalViewSize == 2);
|
|
BEAST_EXPECT(view.containsMaster(keys[0]));
|
|
BEAST_EXPECT(view.containsMaster(keys[1]));
|
|
BEAST_EXPECT(view.containsOriginalMaster(keys[0]));
|
|
BEAST_EXPECT(view.containsOriginalMaster(keys[1]));
|
|
BEAST_EXPECT(std::is_sorted(
|
|
view.orderedOriginalMasterKeys.begin(),
|
|
view.orderedOriginalMasterKeys.end()));
|
|
|
|
source.negativeUNLEnabled = true;
|
|
source.negativeUNL.insert(keys[0]);
|
|
|
|
auto const negativeView = buildActiveValidatorView(source, fallback);
|
|
BEAST_EXPECT(!negativeView.fromUNLReport);
|
|
BEAST_EXPECT(negativeView.size() == 1);
|
|
// nUNL shrinks the effective view but NOT the original-UNL denominator.
|
|
BEAST_EXPECT(negativeView.originalViewSize == 2);
|
|
BEAST_EXPECT(!negativeView.containsMaster(keys[0]));
|
|
BEAST_EXPECT(negativeView.containsMaster(keys[1]));
|
|
BEAST_EXPECT(negativeView.containsOriginalMaster(keys[0]));
|
|
BEAST_EXPECT(negativeView.containsOriginalMaster(keys[1]));
|
|
BEAST_EXPECT(negativeView.orderedOriginalMasterKeys.size() == 2);
|
|
}
|
|
|
|
void
|
|
testActiveValidatorViewAppliesNegativeUNL()
|
|
{
|
|
testcase("Active validator view applies NegativeUNL");
|
|
|
|
using namespace jtx;
|
|
Env env{
|
|
*this,
|
|
envconfig(),
|
|
supported_amendments() | featureNegativeUNL,
|
|
nullptr};
|
|
|
|
auto const vlKeys = makeValidatorKeys();
|
|
auto const l = makeUNLReportLedger(env, vlKeys, {vlKeys[0]});
|
|
BEAST_EXPECT(l->rules().enabled(featureNegativeUNL));
|
|
|
|
ConsensusExtensions ce{env.app(), env.journal};
|
|
auto const view = ce.makeActiveValidatorView(l);
|
|
|
|
BEAST_EXPECT(view->fromUNLReport);
|
|
BEAST_EXPECT(view->size() == 1);
|
|
// Effective view is 1 (nUNL disabled one), but the original-UNL
|
|
// denominator that Tier 2's intersection floor anchors to stays 2.
|
|
BEAST_EXPECT(view->originalViewSize == 2);
|
|
BEAST_EXPECT(!view->containsMaster(vlKeys[0]));
|
|
BEAST_EXPECT(!view->containsNode(calcNodeID(vlKeys[0])));
|
|
BEAST_EXPECT(view->containsMaster(vlKeys[1]));
|
|
BEAST_EXPECT(view->containsNode(calcNodeID(vlKeys[1])));
|
|
}
|
|
|
|
void
|
|
testActiveValidatorViewCapsNegativeUNL()
|
|
{
|
|
testcase("Active validator view caps NegativeUNL against active set");
|
|
|
|
constexpr std::size_t kOriginal = 10;
|
|
constexpr auto kLegalDisable =
|
|
NegativeUNLVote::maxNegativeUNLListed(kOriginal);
|
|
constexpr std::size_t kOverCapDisable = 8;
|
|
|
|
std::vector<PublicKey> activeKeys;
|
|
activeKeys.reserve(kOriginal);
|
|
for (std::size_t i = 0; i < kOriginal; ++i)
|
|
activeKeys.push_back(randomKeyPair(KeyType::secp256k1).first);
|
|
|
|
auto makeSource = [&](std::size_t disabledCount) {
|
|
ActiveValidatorViewSource source;
|
|
source.sourceLedgerHash = makeHash("active-validator-view-cap");
|
|
source.unlReportMasterKeys.emplace();
|
|
for (auto const& masterKey : activeKeys)
|
|
source.unlReportMasterKeys->insert(masterKey);
|
|
source.negativeUNLEnabled = true;
|
|
for (std::size_t i = 0; i < disabledCount; ++i)
|
|
source.negativeUNL.insert(activeKeys[i]);
|
|
return source;
|
|
};
|
|
|
|
ActiveValidatorViewFallback fallback;
|
|
|
|
auto const legalView =
|
|
buildActiveValidatorView(makeSource(kLegalDisable), fallback);
|
|
BEAST_EXPECT(legalView.fromUNLReport);
|
|
BEAST_EXPECT(legalView.originalViewSize == kOriginal);
|
|
BEAST_EXPECT(legalView.size() == kOriginal - kLegalDisable);
|
|
for (std::size_t i = 0; i < kLegalDisable; ++i)
|
|
BEAST_EXPECT(!legalView.containsMaster(activeKeys[i]));
|
|
|
|
auto const source = makeSource(kOverCapDisable);
|
|
auto const cappedView = buildActiveValidatorView(source, fallback);
|
|
|
|
BEAST_EXPECT(cappedView.fromUNLReport);
|
|
BEAST_EXPECT(cappedView.sourceLedgerHash == source.sourceLedgerHash);
|
|
BEAST_EXPECT(cappedView.originalViewSize == kOriginal);
|
|
BEAST_EXPECT(cappedView.size() == kOriginal - kLegalDisable);
|
|
|
|
std::vector<PublicKey> cappedDisabled(
|
|
activeKeys.begin(), activeKeys.begin() + kOverCapDisable);
|
|
std::sort(cappedDisabled.begin(), cappedDisabled.end());
|
|
|
|
for (std::size_t i = 0; i < cappedDisabled.size(); ++i)
|
|
{
|
|
auto const wasCanonicallyRemoved = i < kLegalDisable;
|
|
if (wasCanonicallyRemoved)
|
|
BEAST_EXPECT(!cappedView.containsMaster(cappedDisabled[i]));
|
|
else
|
|
BEAST_EXPECT(cappedView.containsMaster(cappedDisabled[i]));
|
|
}
|
|
|
|
BEAST_EXPECT(
|
|
ConsensusExtensions::entropyGateThresholdForView(2, kOriginal) ==
|
|
2);
|
|
BEAST_EXPECT(
|
|
ConsensusExtensions::entropyGateThresholdForView(
|
|
cappedView.size(), cappedView.originalViewSize) == 6);
|
|
BEAST_EXPECT(
|
|
ConsensusExtensions::selectEntropyTierForView(
|
|
true, 2, cappedView.size(), cappedView.originalViewSize) ==
|
|
entropyTierConsensusFallback);
|
|
BEAST_EXPECT(
|
|
ConsensusExtensions::exportRootAlignmentThreshold(cappedView) == 6);
|
|
}
|
|
|
|
void
|
|
testActiveValidatorViewNullSourceAndExpectedProposers()
|
|
{
|
|
testcase("Active validator view null source and expected proposers");
|
|
|
|
using namespace jtx;
|
|
Env env{
|
|
*this, envconfig(validator, ""), supported_amendments(), nullptr};
|
|
auto const& valKeys = env.app().getValidatorKeys();
|
|
BEAST_EXPECT(valKeys.keys);
|
|
if (!valKeys.keys)
|
|
return;
|
|
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
auto const view = ce.makeActiveValidatorView({});
|
|
BEAST_EXPECT(!view->fromUNLReport);
|
|
BEAST_EXPECT(view->containsNode(valKeys.nodeID));
|
|
|
|
ce.cacheUNLReport();
|
|
ce.setMode(ConsensusMode::proposing);
|
|
ce.setExpectedProposers(hash_set<NodeID>{valKeys.nodeID, makeNode(99)});
|
|
BEAST_EXPECT(ce.expectedProposerCount() == 1);
|
|
|
|
ce.setExpectedProposers({});
|
|
BEAST_EXPECT(
|
|
ce.expectedProposerCount() == ce.activeValidatorView()->size());
|
|
}
|
|
|
|
void
|
|
testParticipantThreshold()
|
|
{
|
|
testcase("participant-alignment threshold arithmetic");
|
|
|
|
// Smallest cohort t with 2t - n > floor(n/5) tolerated faults: ~0.6 n,
|
|
// but bumped at multiples of 5 where plain ceil(0.6 n) leaves the
|
|
// cohort overlap exactly == f (forkable).
|
|
BEAST_EXPECT(calculateParticipantThreshold(1) == 1);
|
|
BEAST_EXPECT(
|
|
calculateParticipantThreshold(5) == 4); // not 3 (n=5 forks)
|
|
BEAST_EXPECT(calculateParticipantThreshold(6) == 4); // == ceil(0.6*6)
|
|
BEAST_EXPECT(calculateParticipantThreshold(7) == 5);
|
|
BEAST_EXPECT(calculateParticipantThreshold(8) == 5);
|
|
BEAST_EXPECT(
|
|
calculateParticipantThreshold(10) == 7); // not 6 (n=10 forks)
|
|
BEAST_EXPECT(calculateParticipantThreshold(15) == 10);
|
|
BEAST_EXPECT(calculateParticipantThreshold(20) == 13);
|
|
BEAST_EXPECT(NegativeUNLVote::maxNegativeUNLListed(10) == 3);
|
|
|
|
// The defining safety invariant, for EVERY view size: two aligned
|
|
// cohorts always share an honest validator, i.e. their overlap (2t - n)
|
|
// strictly exceeds the tolerated Byzantine count f = floor(n/5). And
|
|
// the Tier 2 bar is never stricter than the Tier 3 validator_quorum
|
|
// bar.
|
|
for (std::size_t n = 1; n <= 256; ++n)
|
|
{
|
|
auto const t = calculateParticipantThreshold(n);
|
|
BEAST_EXPECT(2 * t > n + n / 5); // overlap 2t-n > floor(n/5)
|
|
BEAST_EXPECT(t <= calculateQuorumThreshold(n));
|
|
}
|
|
|
|
// The active-view nUNL cap must preserve the same honest-intersection
|
|
// invariant when the entropy gate takes the effective-view quorum
|
|
// branch. This pins the proof obligation from the walkthrough:
|
|
// for active-disabled d <= ceil(n/4), two gate-sized cohorts inside the
|
|
// effective view n-d still overlap by more than floor(n/5).
|
|
for (std::size_t n = 1; n <= 256; ++n)
|
|
{
|
|
auto const maxDisabled = NegativeUNLVote::maxNegativeUNLListed(n);
|
|
for (std::size_t d = 0; d <= maxDisabled; ++d)
|
|
{
|
|
auto const effective = n - d;
|
|
auto const t = ConsensusExtensions::entropyGateThresholdForView(
|
|
effective, n);
|
|
BEAST_EXPECT(2 * t > effective + n / 5);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
testThresholdPolicyHelpers()
|
|
{
|
|
testcase("consensus-extension threshold policy helpers");
|
|
|
|
// Empty views fail closed at one participant. The raw formulas stay
|
|
// mathematical; sidecar gates use the safe wrappers.
|
|
BEAST_EXPECT(calculateQuorumThreshold(0) == 0);
|
|
BEAST_EXPECT(safeQuorumThreshold(0) == 1);
|
|
BEAST_EXPECT(safeQuorumThreshold(6) == calculateQuorumThreshold(6));
|
|
BEAST_EXPECT(calculateParticipantThreshold(0) == 1);
|
|
BEAST_EXPECT(safeParticipantThreshold(0) == 1);
|
|
BEAST_EXPECT(
|
|
safeParticipantThreshold(10) == calculateParticipantThreshold(10));
|
|
auto const max = std::numeric_limits<std::size_t>::max();
|
|
auto const maxByzantine = max / 5;
|
|
BEAST_EXPECT(detail::floorHalfSum(max, max) == max);
|
|
BEAST_EXPECT(detail::floorHalfSum(max, max - 1) == max - 1);
|
|
BEAST_EXPECT(detail::floorHalfSum(max, 0) == max / 2);
|
|
BEAST_EXPECT(
|
|
calculateQuorumThreshold(max) ==
|
|
max / 100 * 80 + (max % 100 * 80 + 99) / 100);
|
|
BEAST_EXPECT(
|
|
calculateParticipantThreshold(max) ==
|
|
detail::floorHalfSum(max, maxByzantine) + 1);
|
|
|
|
// Gate threshold uses effective view for the 80% quorum and original
|
|
// view for the Tier-2 floor, then takes the lower enabled bar.
|
|
BEAST_EXPECT(
|
|
ConsensusExtensions::entropyGateThresholdForView(0, 0) == 1);
|
|
BEAST_EXPECT(
|
|
ConsensusExtensions::entropyGateThresholdForView(6, 6) == 4);
|
|
BEAST_EXPECT(
|
|
ConsensusExtensions::entropyGateThresholdForView(8, 10) == 7);
|
|
BEAST_EXPECT(
|
|
ConsensusExtensions::entropyGateThresholdForView(6, 10) == 5);
|
|
|
|
// Tier labels require a ledger-anchored UNLReport view. With one, the
|
|
// ladder is validator_full first, then validator_quorum, then
|
|
// participant_aligned, then fallback.
|
|
BEAST_EXPECT(
|
|
ConsensusExtensions::selectEntropyTierForView(false, 99, 8, 10) ==
|
|
entropyTierConsensusFallback);
|
|
BEAST_EXPECT(
|
|
ConsensusExtensions::selectEntropyTierForView(true, 8, 8, 10) ==
|
|
entropyTierValidatorFull);
|
|
BEAST_EXPECT(
|
|
ConsensusExtensions::selectEntropyTierForView(true, 7, 8, 10) ==
|
|
entropyTierValidatorQuorum);
|
|
BEAST_EXPECT(
|
|
ConsensusExtensions::selectEntropyTierForView(true, 6, 8, 10) ==
|
|
entropyTierConsensusFallback);
|
|
BEAST_EXPECT(
|
|
ConsensusExtensions::selectEntropyTierForView(true, 5, 8, 8) ==
|
|
entropyTierParticipantAligned);
|
|
BEAST_EXPECT(
|
|
ConsensusExtensions::selectEntropyTierForView(true, 4, 8, 8) ==
|
|
entropyTierConsensusFallback);
|
|
BEAST_EXPECT(
|
|
ConsensusExtensions::selectEntropyTierForView(true, 0, 0, 0) ==
|
|
entropyTierConsensusFallback);
|
|
}
|
|
|
|
void
|
|
testRuntimeConfigPolicyAccessors()
|
|
{
|
|
testcase("runtime config policy accessors");
|
|
|
|
using namespace jtx;
|
|
Env env{
|
|
*this, envconfig(validator, ""), supported_amendments(), nullptr};
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
|
|
BEAST_EXPECT(!ce.testBootstrapFastStartEnabled());
|
|
BEAST_EXPECT(!ce.testSuppressExportSigSetHash());
|
|
|
|
ConsensusTestConfig cfg;
|
|
cfg.bootstrapFastStart = true;
|
|
cfg.noExportSigHash = true;
|
|
env.app().getRuntimeConfig().setGlobalConfig(cfg);
|
|
BEAST_EXPECT(ce.testBootstrapFastStartEnabled());
|
|
BEAST_EXPECT(ce.testSuppressExportSigSetHash());
|
|
|
|
cfg.bootstrapFastStart = false;
|
|
cfg.noExportSigHash = false;
|
|
env.app().getRuntimeConfig().setGlobalConfig(cfg);
|
|
BEAST_EXPECT(!ce.testBootstrapFastStartEnabled());
|
|
BEAST_EXPECT(!ce.testSuppressExportSigSetHash());
|
|
}
|
|
|
|
void
|
|
testOnRoundStartRefreshesFeatureLatches()
|
|
{
|
|
testcase("onRoundStart refreshes extension feature latches");
|
|
|
|
//@@start test-round-extension-feature-latches
|
|
using namespace jtx;
|
|
Env enabledEnv{
|
|
*this,
|
|
envconfig(validator, ""),
|
|
supported_amendments() | featureConsensusEntropy | featureExport,
|
|
nullptr};
|
|
Env disabledEnv{
|
|
*this,
|
|
envconfig(validator, ""),
|
|
supported_amendments() - featureConsensusEntropy - featureExport,
|
|
nullptr};
|
|
|
|
auto const enabledLedger =
|
|
enabledEnv.app().getLedgerMaster().getClosedLedger();
|
|
auto const disabledLedger =
|
|
disabledEnv.app().getLedgerMaster().getClosedLedger();
|
|
|
|
ConsensusExtensions ce{enabledEnv.app(), activeNoopJournal()};
|
|
auto const origin = makeHash("on-round-start-export-latch");
|
|
auto const pk = makeValidatorKeys().front();
|
|
std::uint8_t const sigBytes[] = {1, 2, 3};
|
|
Buffer const sig{sigBytes, sizeof(sigBytes)};
|
|
|
|
ce.setRngEnabledThisRound(false);
|
|
ce.setExportEnabledThisRound(false);
|
|
ce.onRoundStart(RCLCxLedger{enabledLedger}, {});
|
|
BEAST_EXPECT(ce.rngEnabled());
|
|
BEAST_EXPECT(ce.exportEnabled());
|
|
|
|
BEAST_EXPECT(
|
|
ce.postValidationExportSigCollector().registerOrigin(origin, 10));
|
|
auto admission =
|
|
ce.postValidationExportSigCollector().beginAttributedAdmission(
|
|
origin, ExportSigCollector::Contribution{0, pk, sig}, 10);
|
|
BEAST_EXPECT(
|
|
admission.result == ExportSigCollector::BeginResult::verify);
|
|
BEAST_EXPECT(admission.ticket);
|
|
if (admission.ticket)
|
|
BEAST_EXPECT(
|
|
ce.postValidationExportSigCollector()
|
|
.admitContribution(std::move(*admission.ticket), true, 10)
|
|
.result == ExportSigCollector::AdmitResult::accepted);
|
|
BEAST_EXPECT(
|
|
ce.postValidationExportSigCollector().fullUnionSnapshot().size() ==
|
|
1);
|
|
ce.onRoundStart(RCLCxLedger{enabledLedger}, {});
|
|
BEAST_EXPECT(
|
|
ce.postValidationExportSigCollector().fullUnionSnapshot().size() ==
|
|
1);
|
|
|
|
ce.setRngEnabledThisRound(true);
|
|
ce.setExportEnabledThisRound(true);
|
|
ce.onRoundStart(RCLCxLedger{disabledLedger}, {});
|
|
BEAST_EXPECT(!ce.rngEnabled());
|
|
BEAST_EXPECT(!ce.exportEnabled());
|
|
BEAST_EXPECT(
|
|
ce.postValidationExportSigCollector().fullUnionSnapshot().empty());
|
|
//@@end test-round-extension-feature-latches
|
|
}
|
|
|
|
void
|
|
testDecoratePositionGeneratesCommitment()
|
|
{
|
|
testcase("decoratePosition generates commitment");
|
|
|
|
using namespace jtx;
|
|
Env env{
|
|
*this,
|
|
envconfig(validator, ""),
|
|
supported_amendments() | featureConsensusEntropy,
|
|
nullptr};
|
|
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
|
|
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
ExtendedPosition pos{makeHash("decorate-position-enabled")};
|
|
ce.decoratePosition(pos, ledger, true);
|
|
|
|
BEAST_EXPECT(pos.myCommitment);
|
|
BEAST_EXPECT(ce.pendingCommitCount() == 1);
|
|
BEAST_EXPECT(ce.getEntropySecret() != uint256{});
|
|
BEAST_EXPECT(ce.isUNLReportMember(env.app().getValidatorKeys().nodeID));
|
|
}
|
|
|
|
void
|
|
testOnPreBuildInjectsZeroEntropyFallback()
|
|
{
|
|
testcase("onPreBuild injects consensus-bound fallback entropy");
|
|
|
|
using namespace jtx;
|
|
Env env{
|
|
*this,
|
|
envconfig(validator, ""),
|
|
supported_amendments() | featureConsensusEntropy,
|
|
nullptr};
|
|
forceNonStandalone(env.app());
|
|
BEAST_EXPECT(!env.app().config().standalone());
|
|
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
|
|
ce.onRoundStart(RCLCxLedger{ledger}, {});
|
|
ce.setRngEnabledThisRound(true);
|
|
|
|
CanonicalTXSet retriableTxs{makeHash("rng-zero-fallback-salt")};
|
|
auto const seq = ledger->seq() + 1;
|
|
auto const txSetHash = makeHash("rng-fallback-txset");
|
|
ce.onPreBuild(retriableTxs, seq, txSetHash);
|
|
|
|
auto const tx = singleCanonicalTx(retriableTxs);
|
|
BEAST_EXPECT(tx);
|
|
if (!tx)
|
|
return;
|
|
BEAST_EXPECT(tx->getTxnType() == ttCONSENSUS_ENTROPY);
|
|
BEAST_EXPECT(tx->getFieldU32(sfLedgerSequence) == seq);
|
|
|
|
// Tier 1: deterministic, consensus-bound, non-zero, fallback-labeled.
|
|
auto const expected = sha512Half(
|
|
HashPrefix::entropyFallback, ledger->info().hash, txSetHash, seq);
|
|
BEAST_EXPECT(tx->getFieldH256(sfDigest) == expected);
|
|
BEAST_EXPECT(tx->getFieldH256(sfDigest) != uint256{});
|
|
BEAST_EXPECT(tx->getFieldU16(sfEntropyCount) == 0);
|
|
BEAST_EXPECT(tx->getFieldU16(sfEntropyDenominator) == 0);
|
|
BEAST_EXPECT(tx->getFieldVL(sfEntropyContributors).empty());
|
|
BEAST_EXPECT(
|
|
tx->getFieldU8(sfEntropyTier) == entropyTierConsensusFallback);
|
|
|
|
// Same agreed inputs => identical digest on an independent instance.
|
|
ConsensusExtensions other{env.app(), activeNoopJournal()};
|
|
other.onRoundStart(RCLCxLedger{ledger}, {});
|
|
other.setRngEnabledThisRound(true);
|
|
CanonicalTXSet otherTxs{makeHash("rng-zero-fallback-salt-2")};
|
|
other.onPreBuild(otherTxs, seq, txSetHash);
|
|
auto const otherTx = singleCanonicalTx(otherTxs);
|
|
BEAST_EXPECT(otherTx);
|
|
if (otherTx)
|
|
BEAST_EXPECT(otherTx->getFieldH256(sfDigest) == expected);
|
|
}
|
|
|
|
void
|
|
testTxnOrderingSaltExtendsLegacySalt()
|
|
{
|
|
testcase("transaction ordering salt extends legacy salt");
|
|
|
|
using namespace jtx;
|
|
Env env{
|
|
*this,
|
|
envconfig(validator, ""),
|
|
supported_amendments() | featureConsensusEntropy,
|
|
nullptr};
|
|
forceNonStandalone(env.app());
|
|
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
|
|
auto const seq = ledger->seq() + 1;
|
|
auto const txSetHash = makeHash("txn-order-base-set");
|
|
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
ce.onRoundStart(RCLCxLedger{ledger}, {});
|
|
|
|
ce.setRngEnabledThisRound(false);
|
|
BEAST_EXPECT(ce.txnOrderingSalt(txSetHash, seq) == txSetHash);
|
|
|
|
ce.setRngEnabledThisRound(true);
|
|
auto const fallbackDigest = sha512Half(
|
|
HashPrefix::entropyFallback, ledger->info().hash, txSetHash, seq);
|
|
auto const expected = sha512Half(
|
|
HashPrefix::entropyTxnOrder,
|
|
txSetHash,
|
|
fallbackDigest,
|
|
static_cast<std::uint8_t>(entropyTierConsensusFallback),
|
|
static_cast<std::uint16_t>(0),
|
|
static_cast<std::uint16_t>(0));
|
|
|
|
auto const salt = ce.txnOrderingSalt(txSetHash, seq);
|
|
BEAST_EXPECT(salt == expected);
|
|
BEAST_EXPECT(salt != txSetHash);
|
|
}
|
|
|
|
void
|
|
testOnPreBuildInjectsEntropySetEntropy()
|
|
{
|
|
testcase("onPreBuild injects entropy-set entropy");
|
|
|
|
using namespace jtx;
|
|
Env env{
|
|
*this,
|
|
envconfig(validator, ""),
|
|
supported_amendments() | featureConsensusEntropy,
|
|
nullptr};
|
|
forceNonStandalone(env.app());
|
|
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
|
|
auto const& valKeys = env.app().getValidatorKeys();
|
|
BEAST_EXPECT(valKeys.keys);
|
|
if (!valKeys.keys)
|
|
return;
|
|
|
|
auto const& publicKey = valKeys.keys->publicKey;
|
|
auto const& secretKey = valKeys.keys->secretKey;
|
|
auto const nodeId = valKeys.nodeID;
|
|
auto const prevLedger = ledger->info().hash;
|
|
auto const seq = ledger->seq() + 1;
|
|
auto const closeTime = NetClock::time_point{NetClock::duration{321}};
|
|
auto const txSetHash = makeHash("prebuild-entropy-txset");
|
|
auto const reveal = makeHash("prebuild-entropy-reveal");
|
|
auto const viewLedger =
|
|
makeUNLReportLedger(env, std::vector<PublicKey>{publicKey});
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
ce.onRoundStart(RCLCxLedger{ledger}, {});
|
|
ce.cacheUNLReport(viewLedger);
|
|
ce.setRngEnabledThisRound(true);
|
|
harvestCommitReveal(
|
|
ce,
|
|
nodeId,
|
|
publicKey,
|
|
secretKey,
|
|
txSetHash,
|
|
seq,
|
|
closeTime,
|
|
prevLedger,
|
|
reveal);
|
|
BEAST_EXPECT(ce.hasQuorumOfCommits());
|
|
BEAST_EXPECT(ce.hasMinimumReveals());
|
|
|
|
auto const entropySetHash = ce.buildEntropySet(seq);
|
|
ce.acceptEntropySet(entropySetHash);
|
|
BEAST_EXPECT(
|
|
env.app().getInboundTransactions().getSet(entropySetHash, false));
|
|
|
|
CanonicalTXSet retriableTxs{makeHash("entropy-set-prebuild-salt")};
|
|
ce.onPreBuild(retriableTxs, seq, txSetHash);
|
|
|
|
auto const tx = singleCanonicalTx(retriableTxs);
|
|
BEAST_EXPECT(tx);
|
|
if (!tx)
|
|
return;
|
|
BEAST_EXPECT(tx->getTxnType() == ttCONSENSUS_ENTROPY);
|
|
BEAST_EXPECT(
|
|
tx->getFieldH256(sfDigest) == expectedEntropy(publicKey, reveal));
|
|
BEAST_EXPECT(tx->getFieldU16(sfEntropyCount) == 1);
|
|
BEAST_EXPECT(tx->getFieldU16(sfEntropyDenominator) == 1);
|
|
BEAST_EXPECT(tx->getFieldVL(sfEntropyContributors) == Blob{0x01});
|
|
BEAST_EXPECT(tx->getFieldU8(sfEntropyTier) == entropyTierValidatorFull);
|
|
}
|
|
|
|
void
|
|
testOnPreBuildCanonicalizesMultiContributorEntropy()
|
|
{
|
|
testcase("onPreBuild canonicalizes multi-contributor entropy");
|
|
|
|
using namespace jtx;
|
|
Env env{
|
|
*this,
|
|
envconfig(validator, ""),
|
|
supported_amendments() | featureConsensusEntropy,
|
|
nullptr};
|
|
forceNonStandalone(env.app());
|
|
|
|
std::array<std::uint8_t, 32> firstSecretBytes{};
|
|
std::array<std::uint8_t, 32> secondSecretBytes{};
|
|
firstSecretBytes.back() = 1;
|
|
secondSecretBytes.back() = 2;
|
|
SecretKey const firstSecret{firstSecretBytes};
|
|
SecretKey const secondSecret{secondSecretBytes};
|
|
auto const firstKey = derivePublicKey(KeyType::secp256k1, firstSecret);
|
|
auto const secondKey =
|
|
derivePublicKey(KeyType::secp256k1, secondSecret);
|
|
BEAST_EXPECT(firstKey.slice() < secondKey.slice());
|
|
|
|
std::vector<PublicKey> const activeKeys{firstKey, secondKey};
|
|
auto const viewLedger = makeUNLReportLedger(env, activeKeys);
|
|
auto const anchor = env.app().getLedgerMaster().getClosedLedger();
|
|
auto const seq = anchor->info().seq + 1;
|
|
auto const closeTime = NetClock::time_point{NetClock::duration{322}};
|
|
auto const txSetHash = makeHash("multi-contributor-entropy-txset");
|
|
std::vector<uint256> const reveals{
|
|
makeHash("multi-contributor-reveal-a"),
|
|
makeHash("multi-contributor-reveal-b")};
|
|
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
ce.onRoundStart(RCLCxLedger{anchor}, {});
|
|
ce.cacheUNLReport(viewLedger);
|
|
ce.setRngEnabledThisRound(true);
|
|
harvestCommitReveal(
|
|
ce,
|
|
calcNodeID(firstKey),
|
|
firstKey,
|
|
firstSecret,
|
|
txSetHash,
|
|
seq,
|
|
closeTime,
|
|
anchor->info().hash,
|
|
reveals[0]);
|
|
harvestCommitReveal(
|
|
ce,
|
|
calcNodeID(secondKey),
|
|
secondKey,
|
|
secondSecret,
|
|
txSetHash,
|
|
seq,
|
|
closeTime,
|
|
anchor->info().hash,
|
|
reveals[1]);
|
|
BEAST_EXPECT(ce.hasQuorumOfCommits());
|
|
BEAST_EXPECT(ce.hasMinimumReveals());
|
|
|
|
auto const entropySetHash = ce.buildEntropySet(seq);
|
|
BEAST_EXPECT(ce.entropySetMap_);
|
|
if (!ce.entropySetMap_)
|
|
return;
|
|
|
|
std::vector<PublicKey> traversal;
|
|
ce.entropySetMap_->visitLeaves(
|
|
[&](boost::intrusive_ptr<SHAMapItem const> const& item) {
|
|
SerialIter sit{item->slice()};
|
|
STObject const sidecar{sit, sfGeneric};
|
|
traversal.emplace_back(
|
|
makeSlice(sidecar.getFieldVL(sfSigningPubKey)));
|
|
});
|
|
BEAST_EXPECT(traversal.size() == 2);
|
|
if (traversal.size() != 2)
|
|
return;
|
|
BEAST_EXPECT(traversal[0] == secondKey);
|
|
BEAST_EXPECT(traversal[1] == firstKey);
|
|
|
|
ce.acceptEntropySet(entropySetHash);
|
|
CanonicalTXSet txs{makeHash("multi-contributor-entropy-salt")};
|
|
ce.onPreBuild(txs, seq, txSetHash);
|
|
|
|
auto const tx = singleCanonicalTx(txs);
|
|
BEAST_EXPECT(tx);
|
|
if (!tx)
|
|
return;
|
|
|
|
uint256 canonicalDigest;
|
|
BEAST_EXPECT(
|
|
canonicalDigest.parseHex("9C919A4A003154100E550CFC86FCF0731E7E95F43"
|
|
"C3F680E4FE2B16AD8D5AF84"));
|
|
BEAST_EXPECT(tx->getTxnType() == ttCONSENSUS_ENTROPY);
|
|
BEAST_EXPECT(tx->getFieldH256(sfDigest) == canonicalDigest);
|
|
BEAST_EXPECT(tx->getFieldU16(sfEntropyCount) == 2);
|
|
BEAST_EXPECT(tx->getFieldU16(sfEntropyDenominator) == 2);
|
|
BEAST_EXPECT(tx->getFieldVL(sfEntropyContributors) == Blob{0x03});
|
|
BEAST_EXPECT(tx->getFieldU8(sfEntropyTier) == entropyTierValidatorFull);
|
|
}
|
|
|
|
void
|
|
testOnPreBuildAcceptedEntropySetOverridesLocalFailureFlag()
|
|
{
|
|
testcase(
|
|
"onPreBuild accepted entropy set overrides local failure flag");
|
|
|
|
using namespace jtx;
|
|
Env env{
|
|
*this,
|
|
envconfig(validator, ""),
|
|
supported_amendments() | featureConsensusEntropy,
|
|
nullptr};
|
|
forceNonStandalone(env.app());
|
|
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
|
|
auto const& valKeys = env.app().getValidatorKeys();
|
|
BEAST_EXPECT(valKeys.keys);
|
|
if (!valKeys.keys)
|
|
return;
|
|
|
|
auto const& publicKey = valKeys.keys->publicKey;
|
|
auto const& secretKey = valKeys.keys->secretKey;
|
|
auto const nodeId = valKeys.nodeID;
|
|
auto const prevLedger = ledger->info().hash;
|
|
auto const seq = ledger->seq() + 1;
|
|
auto const closeTime = NetClock::time_point{NetClock::duration{654}};
|
|
auto const txSetHash = makeHash("prebuild-failed-entropy-txset");
|
|
auto const reveal = makeHash("prebuild-failed-entropy-reveal");
|
|
auto const viewLedger =
|
|
makeUNLReportLedger(env, std::vector<PublicKey>{publicKey});
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
ce.onRoundStart(RCLCxLedger{ledger}, {});
|
|
ce.cacheUNLReport(viewLedger);
|
|
ce.setRngEnabledThisRound(true);
|
|
harvestCommitReveal(
|
|
ce,
|
|
nodeId,
|
|
publicKey,
|
|
secretKey,
|
|
txSetHash,
|
|
seq,
|
|
closeTime,
|
|
prevLedger,
|
|
reveal);
|
|
BEAST_EXPECT(ce.hasQuorumOfCommits());
|
|
BEAST_EXPECT(ce.hasMinimumReveals());
|
|
|
|
auto const entropySetHash = ce.buildEntropySet(seq);
|
|
ce.acceptEntropySet(entropySetHash);
|
|
BEAST_EXPECT(
|
|
env.app().getInboundTransactions().getSet(entropySetHash, false));
|
|
|
|
ce.setEntropyFailed();
|
|
|
|
CanonicalTXSet retriableTxs{makeHash("failed-entropy-prebuild-salt")};
|
|
ce.onPreBuild(retriableTxs, seq, txSetHash);
|
|
|
|
auto const tx = singleCanonicalTx(retriableTxs);
|
|
BEAST_EXPECT(tx);
|
|
if (!tx)
|
|
return;
|
|
|
|
BEAST_EXPECT(tx->getTxnType() == ttCONSENSUS_ENTROPY);
|
|
BEAST_EXPECT(
|
|
tx->getFieldH256(sfDigest) == expectedEntropy(publicKey, reveal));
|
|
BEAST_EXPECT(tx->getFieldU16(sfEntropyCount) == 1);
|
|
BEAST_EXPECT(tx->getFieldU16(sfEntropyDenominator) == 1);
|
|
BEAST_EXPECT(tx->getFieldVL(sfEntropyContributors) == Blob{0x01});
|
|
BEAST_EXPECT(tx->getFieldU8(sfEntropyTier) == entropyTierValidatorFull);
|
|
}
|
|
|
|
void
|
|
testOnPreBuildRejectsForgedEntropyContributorAttribution()
|
|
{
|
|
testcase("onPreBuild rejects forged entropy contributor attribution");
|
|
|
|
using namespace jtx;
|
|
Env env{
|
|
*this,
|
|
envconfig(validator, ""),
|
|
supported_amendments() | featureConsensusEntropy,
|
|
nullptr};
|
|
forceNonStandalone(env.app());
|
|
|
|
auto const first = randomKeyPair(KeyType::secp256k1);
|
|
auto const second = randomKeyPair(KeyType::secp256k1);
|
|
auto const viewLedger =
|
|
makeUNLReportLedger(env, {first.first, second.first});
|
|
auto const anchor = env.app().getLedgerMaster().getClosedLedger();
|
|
auto const seq = anchor->info().seq + 1;
|
|
auto const txSetHash = makeHash("forged-contributor-txset");
|
|
auto const reveal = makeHash("forged-contributor-reveal");
|
|
|
|
auto makeRevealSidecar = [&](NodeID const& claimedNode) {
|
|
STObject sidecar(sfGeneric);
|
|
sidecar.setFieldU8(sfSidecarType, sidecarRngReveal);
|
|
sidecar.setFieldU32(sfLedgerSequence, seq);
|
|
sidecar.setAccountID(sfAccount, accountFromNode(claimedNode));
|
|
sidecar.setFieldH256(sfDigest, reveal);
|
|
sidecar.setFieldVL(sfSigningPubKey, first.first.slice());
|
|
|
|
Serializer s;
|
|
sidecar.add(s);
|
|
return make_shamapitem(
|
|
sidecar.getHash(HashPrefix::sidecar), s.slice());
|
|
};
|
|
|
|
auto map = std::make_shared<SHAMap>(
|
|
SHAMapType::SIDECAR, env.app().getNodeFamily());
|
|
map->setUnbacked();
|
|
map->addItem(
|
|
SHAMapNodeType::tnSIDECAR,
|
|
makeRevealSidecar(calcNodeID(first.first)));
|
|
map->addItem(
|
|
SHAMapNodeType::tnSIDECAR,
|
|
makeRevealSidecar(calcNodeID(second.first)));
|
|
map = map->snapShot(false);
|
|
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
ce.onRoundStart(RCLCxLedger{anchor}, {});
|
|
ce.cacheUNLReport(viewLedger);
|
|
ce.setRngEnabledThisRound(true);
|
|
ce.entropySetMap_ = map;
|
|
ce.acceptEntropySet(map->getHash().as_uint256());
|
|
|
|
CanonicalTXSet txs{makeHash("forged-contributor-salt")};
|
|
ce.onPreBuild(txs, seq, txSetHash);
|
|
|
|
auto const tx = singleCanonicalTx(txs);
|
|
BEAST_EXPECT(tx);
|
|
if (!tx)
|
|
return;
|
|
|
|
auto const expectedFallback = sha512Half(
|
|
HashPrefix::entropyFallback, anchor->info().hash, txSetHash, seq);
|
|
BEAST_EXPECT(tx->getTxnType() == ttCONSENSUS_ENTROPY);
|
|
BEAST_EXPECT(tx->getFieldH256(sfDigest) == expectedFallback);
|
|
BEAST_EXPECT(tx->getFieldU16(sfEntropyCount) == 0);
|
|
BEAST_EXPECT(tx->getFieldU16(sfEntropyDenominator) == 0);
|
|
BEAST_EXPECT(tx->getFieldVL(sfEntropyContributors).empty());
|
|
BEAST_EXPECT(
|
|
tx->getFieldU8(sfEntropyTier) == entropyTierConsensusFallback);
|
|
}
|
|
|
|
void
|
|
testOnPreBuildTier2ParticipantAligned()
|
|
{
|
|
testcase(
|
|
"onPreBuild labels sub-quorum aligned set participant_aligned");
|
|
|
|
using namespace jtx;
|
|
Env env{
|
|
*this,
|
|
envconfig(validator, ""),
|
|
supported_amendments() | featureConsensusEntropy,
|
|
nullptr};
|
|
forceNonStandalone(env.app());
|
|
|
|
// A 6-validator UNLReport opens the tier-2 band:
|
|
// tier2Threshold = 4 (smallest cohort whose overlap exceeds f=1)
|
|
// quorumThreshold = ceil(0.8 * 6) = 5
|
|
// so an agreed aligned count of 4 is participant_aligned, 5+ is
|
|
// validator_quorum, and < 4 falls back. (n=5 has no band -- there
|
|
// tier2 == quorum -- so 6 is the smallest size that exercises tier 2.)
|
|
constexpr std::size_t kValidators = 6;
|
|
std::vector<std::pair<PublicKey, SecretKey>> vals;
|
|
vals.reserve(kValidators);
|
|
std::vector<PublicKey> activeKeys;
|
|
activeKeys.reserve(kValidators);
|
|
for (std::size_t i = 0; i < kValidators; ++i)
|
|
{
|
|
vals.push_back(randomKeyPair(KeyType::secp256k1));
|
|
activeKeys.push_back(vals.back().first);
|
|
}
|
|
auto const viewLedger = makeUNLReportLedger(env, activeKeys);
|
|
|
|
// Thresholds derive from the cached pre-nUNL view (originalViewSize=6).
|
|
{
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
ce.cacheUNLReport(viewLedger);
|
|
BEAST_EXPECT(ce.activeValidatorView()->originalViewSize == 6);
|
|
BEAST_EXPECT(ce.quorumThreshold() == 5);
|
|
BEAST_EXPECT(ce.tier2Threshold() == 4);
|
|
BEAST_EXPECT(ce.entropyGateThreshold() == 4);
|
|
}
|
|
|
|
// Reveal verification resolves the round's prev ledger through the
|
|
// LedgerMaster, so anchor commitments to a real closed ledger (the view
|
|
// above comes from the in-memory UNLReport ledger).
|
|
auto const anchor = env.app().getLedgerMaster().getClosedLedger();
|
|
auto const prevLedger = anchor->info().hash;
|
|
auto const seq = anchor->info().seq + 1;
|
|
auto const closeTime = NetClock::time_point{NetClock::duration{777}};
|
|
auto const txSetHash = makeHash("tier2-txset");
|
|
|
|
std::vector<PublicKey> activeMasterKeys;
|
|
activeMasterKeys.reserve(vals.size());
|
|
for (auto const& [pk, _] : vals)
|
|
activeMasterKeys.push_back(pk);
|
|
|
|
// Harvest commit+reveal from `revealers` of the 6 validators, build the
|
|
// agreed entropy set, inject, and return the labelled
|
|
// (tier, count, denominator, contributor mask).
|
|
auto runWith = [&](std::size_t revealers) {
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
ce.cacheUNLReport(viewLedger);
|
|
ce.setRngEnabledThisRound(true);
|
|
for (std::size_t i = 0; i < revealers; ++i)
|
|
{
|
|
auto const nodeId = calcNodeID(vals[i].first);
|
|
auto const reveal =
|
|
sha512Half(vals[i].first, makeHash("tier2-reveal"));
|
|
harvestCommitReveal(
|
|
ce,
|
|
nodeId,
|
|
vals[i].first,
|
|
vals[i].second,
|
|
txSetHash,
|
|
seq,
|
|
closeTime,
|
|
prevLedger,
|
|
reveal);
|
|
}
|
|
auto const entropySetHash = ce.buildEntropySet(seq);
|
|
ce.acceptEntropySet(entropySetHash);
|
|
CanonicalTXSet txs{makeHash("tier2-salt")};
|
|
ce.onPreBuild(txs, seq, txSetHash);
|
|
auto const tx = singleCanonicalTx(txs);
|
|
std::tuple<int, std::uint16_t, std::uint16_t, Blob> out{
|
|
-1, 0, 0, Blob{}};
|
|
if (tx)
|
|
out = {
|
|
tx->getFieldU8(sfEntropyTier),
|
|
tx->getFieldU16(sfEntropyCount),
|
|
tx->getFieldU16(sfEntropyDenominator),
|
|
tx->getFieldVL(sfEntropyContributors)};
|
|
return out;
|
|
};
|
|
|
|
// 5 of 6 aligned -> validator_quorum (count >= quorum 5).
|
|
auto const q = runWith(5);
|
|
BEAST_EXPECT(std::get<0>(q) == entropyTierValidatorQuorum);
|
|
BEAST_EXPECT(std::get<1>(q) == 5);
|
|
BEAST_EXPECT(std::get<2>(q) == kValidators);
|
|
BEAST_EXPECT(
|
|
std::get<3>(q) ==
|
|
expectedContributorMask(
|
|
activeMasterKeys,
|
|
{calcNodeID(vals[0].first),
|
|
calcNodeID(vals[1].first),
|
|
calcNodeID(vals[2].first),
|
|
calcNodeID(vals[3].first),
|
|
calcNodeID(vals[4].first)}));
|
|
|
|
// 4 of 6 aligned -> participant_aligned (count >= tier2 4, < quorum 5).
|
|
auto const p = runWith(4);
|
|
BEAST_EXPECT(std::get<0>(p) == entropyTierParticipantAligned);
|
|
BEAST_EXPECT(std::get<1>(p) == 4);
|
|
BEAST_EXPECT(std::get<2>(p) == kValidators);
|
|
BEAST_EXPECT(
|
|
std::get<3>(p) ==
|
|
expectedContributorMask(
|
|
activeMasterKeys,
|
|
{calcNodeID(vals[0].first),
|
|
calcNodeID(vals[1].first),
|
|
calcNodeID(vals[2].first),
|
|
calcNodeID(vals[3].first)}));
|
|
|
|
// 3 of 6 aligned -> below the tier-2 floor -> consensus_fallback.
|
|
auto const f = runWith(3);
|
|
BEAST_EXPECT(std::get<0>(f) == entropyTierConsensusFallback);
|
|
BEAST_EXPECT(std::get<1>(f) == 0);
|
|
BEAST_EXPECT(std::get<2>(f) == 0);
|
|
BEAST_EXPECT(std::get<3>(f).empty());
|
|
}
|
|
|
|
void
|
|
testTier2ThresholdAnchorsToOriginalView()
|
|
{
|
|
testcase(
|
|
"Tier 2 threshold anchors to pre-nUNL original view under "
|
|
"NegativeUNL");
|
|
|
|
using namespace jtx;
|
|
Env env{
|
|
*this,
|
|
envconfig(validator, ""),
|
|
supported_amendments() | featureConsensusEntropy |
|
|
featureNegativeUNL,
|
|
nullptr};
|
|
forceNonStandalone(env.app());
|
|
|
|
// Ten active validators with two disabled via NegativeUNL: the
|
|
// original-UNL denominator is 10 (Byzantine bound f = floor(10/5) = 2)
|
|
// while the effective view is 8. Aligned cohorts form among the 8
|
|
// effective validators, so two t-cohorts overlap by 2t - 8; safety
|
|
// needs that overlap to exceed f so the two cohorts share an honest
|
|
// validator. Tier 2 MUST key off the original view:
|
|
// tier2Threshold(original 10) == 7 -> overlap 2*7 - 8 = 6 > f: safe
|
|
// tier2Threshold(effective 8) == 5 -> overlap 2*5 - 8 = 2, NOT > f
|
|
// A regression to size() would admit a 5-of-8 cohort as
|
|
// participant_aligned, but its overlap (2) does not exceed the two
|
|
// faulty validators the original UNL still tolerates, so an equivocator
|
|
// could mint two distinct tier-2 digests: a fork. nUNL drops
|
|
// honest-but-down nodes while leaving faulty ones, so the fault bound
|
|
// stays anchored to the original UNL, not the shrunk effective view.
|
|
// Under the correct anchor the band collapses (tier2 == quorum == 7)
|
|
// and no tier-2 mint happens at all; this test pins the anchor so a
|
|
// refactor to size() -- which re-opens the forkable [5, 7) band --
|
|
// fails loudly.
|
|
constexpr std::size_t kActive = 10;
|
|
constexpr std::size_t kDisabled = 2;
|
|
std::vector<PublicKey> activeKeys;
|
|
activeKeys.reserve(kActive);
|
|
for (std::size_t i = 0; i < kActive; ++i)
|
|
activeKeys.push_back(randomKeyPair(KeyType::secp256k1).first);
|
|
std::vector<PublicKey> const disabledKeys(
|
|
activeKeys.begin(), activeKeys.begin() + kDisabled);
|
|
|
|
auto const viewLedger =
|
|
makeUNLReportLedger(env, activeKeys, disabledKeys);
|
|
BEAST_EXPECT(viewLedger->rules().enabled(featureNegativeUNL));
|
|
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
ce.cacheUNLReport(viewLedger);
|
|
auto const view = ce.activeValidatorView();
|
|
|
|
BEAST_EXPECT(view->fromUNLReport);
|
|
BEAST_EXPECT(view->originalViewSize == kActive); // 10, pre-nUNL
|
|
BEAST_EXPECT(view->size() == kActive - kDisabled); // 8, effective
|
|
|
|
// The anchor. Tier 2 derives from originalViewSize (10 -> 7), NOT the
|
|
// effective size (8 -> 5). The gate is min(quorum, tier2); quorum
|
|
// tracks the effective view (8 -> 7), so under the correct anchor gate
|
|
// == 7 and the band is closed, while a regression to size() would drop
|
|
// the floor to 5 and the gate to min(7, 5) == 5.
|
|
BEAST_EXPECT(calculateParticipantThreshold(kActive) == 7);
|
|
BEAST_EXPECT(calculateParticipantThreshold(kActive - kDisabled) == 5);
|
|
BEAST_EXPECT(ce.tier2Threshold() == 7);
|
|
BEAST_EXPECT(ce.quorumThreshold() == 7);
|
|
BEAST_EXPECT(ce.entropyGateThreshold() == 7);
|
|
}
|
|
|
|
void
|
|
testOnPreBuildTier2WithNegativeUNL()
|
|
{
|
|
testcase("onPreBuild mints Tier 2 with active NegativeUNL");
|
|
|
|
using namespace jtx;
|
|
Env env{
|
|
*this,
|
|
envconfig(validator, ""),
|
|
supported_amendments() | featureConsensusEntropy |
|
|
featureNegativeUNL,
|
|
nullptr};
|
|
forceNonStandalone(env.app());
|
|
|
|
// 20 original active validators, 1 disabled by nUNL:
|
|
// originalViewSize = 20 -> tier2Threshold = 13
|
|
// effective size = 19 -> quorumThreshold = 16
|
|
// so counts 13..15 are genuinely participant_aligned under active nUNL.
|
|
constexpr std::size_t kOriginal = 20;
|
|
constexpr std::size_t kDisabled = 1;
|
|
std::vector<std::pair<PublicKey, SecretKey>> vals;
|
|
vals.reserve(kOriginal);
|
|
std::vector<PublicKey> activeKeys;
|
|
activeKeys.reserve(kOriginal);
|
|
for (std::size_t i = 0; i < kOriginal; ++i)
|
|
{
|
|
vals.push_back(randomKeyPair(KeyType::secp256k1));
|
|
activeKeys.push_back(vals.back().first);
|
|
}
|
|
std::vector<PublicKey> const disabledKeys(
|
|
activeKeys.begin(), activeKeys.begin() + kDisabled);
|
|
auto const viewLedger =
|
|
makeUNLReportLedger(env, activeKeys, disabledKeys);
|
|
|
|
{
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
ce.cacheUNLReport(viewLedger);
|
|
auto const view = ce.activeValidatorView();
|
|
BEAST_EXPECT(view->fromUNLReport);
|
|
BEAST_EXPECT(view->originalViewSize == kOriginal);
|
|
BEAST_EXPECT(view->size() == kOriginal - kDisabled);
|
|
BEAST_EXPECT(ce.tier2Threshold() == 13);
|
|
BEAST_EXPECT(ce.quorumThreshold() == 16);
|
|
BEAST_EXPECT(ce.entropyGateThreshold() == 13);
|
|
}
|
|
|
|
auto const anchor = env.app().getLedgerMaster().getClosedLedger();
|
|
auto const prevLedger = anchor->info().hash;
|
|
auto const seq = anchor->info().seq + 1;
|
|
auto const closeTime = NetClock::time_point{NetClock::duration{778}};
|
|
auto const txSetHash = makeHash("tier2-nunl-txset");
|
|
std::vector<PublicKey> effectiveMasterKeys(
|
|
activeKeys.begin() + kDisabled, activeKeys.end());
|
|
|
|
auto runWith = [&](std::size_t revealers) {
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
ce.cacheUNLReport(viewLedger);
|
|
ce.setRngEnabledThisRound(true);
|
|
for (std::size_t i = 0; i < revealers; ++i)
|
|
{
|
|
auto const idx = i + kDisabled; // skip disabled validator
|
|
auto const nodeId = calcNodeID(vals[idx].first);
|
|
auto const reveal =
|
|
sha512Half(vals[idx].first, makeHash("tier2-nunl-reveal"));
|
|
harvestCommitReveal(
|
|
ce,
|
|
nodeId,
|
|
vals[idx].first,
|
|
vals[idx].second,
|
|
txSetHash,
|
|
seq,
|
|
closeTime,
|
|
prevLedger,
|
|
reveal);
|
|
}
|
|
auto const entropySetHash = ce.buildEntropySet(seq);
|
|
ce.acceptEntropySet(entropySetHash);
|
|
CanonicalTXSet txs{makeHash("tier2-nunl-salt")};
|
|
ce.onPreBuild(txs, seq, txSetHash);
|
|
auto const tx = singleCanonicalTx(txs);
|
|
std::tuple<int, std::uint16_t, std::uint16_t, Blob> out{
|
|
-1, 0, 0, Blob{}};
|
|
if (tx)
|
|
out = {
|
|
tx->getFieldU8(sfEntropyTier),
|
|
tx->getFieldU16(sfEntropyCount),
|
|
tx->getFieldU16(sfEntropyDenominator),
|
|
tx->getFieldVL(sfEntropyContributors)};
|
|
return out;
|
|
};
|
|
|
|
auto const q = runWith(16);
|
|
BEAST_EXPECT(std::get<0>(q) == entropyTierValidatorQuorum);
|
|
BEAST_EXPECT(std::get<1>(q) == 16);
|
|
BEAST_EXPECT(std::get<2>(q) == kOriginal - kDisabled);
|
|
{
|
|
std::vector<NodeID> expectedNodes;
|
|
expectedNodes.reserve(16);
|
|
for (std::size_t i = 0; i < 16; ++i)
|
|
expectedNodes.push_back(calcNodeID(vals[i + kDisabled].first));
|
|
BEAST_EXPECT(
|
|
std::get<3>(q) ==
|
|
expectedContributorMask(effectiveMasterKeys, expectedNodes));
|
|
}
|
|
|
|
auto const p = runWith(15);
|
|
BEAST_EXPECT(std::get<0>(p) == entropyTierParticipantAligned);
|
|
BEAST_EXPECT(std::get<1>(p) == 15);
|
|
BEAST_EXPECT(std::get<2>(p) == kOriginal - kDisabled);
|
|
{
|
|
std::vector<NodeID> expectedNodes;
|
|
expectedNodes.reserve(15);
|
|
for (std::size_t i = 0; i < 15; ++i)
|
|
expectedNodes.push_back(calcNodeID(vals[i + kDisabled].first));
|
|
BEAST_EXPECT(
|
|
std::get<3>(p) ==
|
|
expectedContributorMask(effectiveMasterKeys, expectedNodes));
|
|
}
|
|
|
|
auto const f = runWith(12);
|
|
BEAST_EXPECT(std::get<0>(f) == entropyTierConsensusFallback);
|
|
BEAST_EXPECT(std::get<1>(f) == 0);
|
|
BEAST_EXPECT(std::get<2>(f) == 0);
|
|
BEAST_EXPECT(std::get<3>(f).empty());
|
|
}
|
|
|
|
void
|
|
testProposalProofRoundTrip()
|
|
{
|
|
testcase("proposal proof round-trip");
|
|
|
|
auto const [publicKey, secretKey] = randomKeyPair(KeyType::secp256k1);
|
|
auto const prevLedger = makeHash("proof-prev-ledger");
|
|
auto const closeTime = NetClock::time_point{NetClock::duration{99}};
|
|
ExtendedPosition position{makeHash("proof-tx-set")};
|
|
position.myCommitment = makeHash("proof-commitment");
|
|
|
|
auto const signature = signPosition(
|
|
publicKey, secretKey, position, 0, closeTime, prevLedger);
|
|
Serializer positionData;
|
|
position.add(positionData);
|
|
|
|
ConsensusExtensions::ProposalProof proof{
|
|
0,
|
|
static_cast<std::uint32_t>(closeTime.time_since_epoch().count()),
|
|
prevLedger,
|
|
std::move(positionData),
|
|
signature};
|
|
|
|
auto const blob = ConsensusExtensions::serializeProof(proof);
|
|
auto parsed = ConsensusExtensions::deserializeProof(blob);
|
|
BEAST_EXPECT(parsed);
|
|
if (!parsed)
|
|
return;
|
|
BEAST_EXPECT(parsed->proposeSeq == proof.proposeSeq);
|
|
BEAST_EXPECT(parsed->closeTime == proof.closeTime);
|
|
BEAST_EXPECT(parsed->prevLedger == proof.prevLedger);
|
|
BEAST_EXPECT(parsed->signature == proof.signature);
|
|
BEAST_EXPECT(ConsensusExtensions::verifyProof(
|
|
blob, publicKey, *position.myCommitment, true));
|
|
BEAST_EXPECT(!ConsensusExtensions::verifyProof(
|
|
blob, publicKey, makeHash("wrong-commitment"), true));
|
|
|
|
auto const edKeys = randomKeyPair(KeyType::ed25519);
|
|
BEAST_EXPECT(!ConsensusExtensions::verifyProof(
|
|
blob, edKeys.first, *position.myCommitment, true));
|
|
|
|
Blob malformed{1, 2, 3};
|
|
BEAST_EXPECT(!ConsensusExtensions::deserializeProof(malformed));
|
|
BEAST_EXPECT(!ConsensusExtensions::verifyProof(
|
|
malformed, publicKey, *position.myCommitment, true));
|
|
}
|
|
|
|
void
|
|
testProposalPrecheckUsesExportShareRelayLimits()
|
|
{
|
|
testcase("proposal precheck uses serialized ExportShare limits");
|
|
|
|
using enum detail::ProposalPrecheckResult;
|
|
|
|
auto const signer = randomKeyPair(KeyType::secp256k1);
|
|
auto const owner = calcAccountID(signer.first);
|
|
auto const destination =
|
|
calcAccountID(randomKeyPair(KeyType::secp256k1).first);
|
|
auto const releaseTarget =
|
|
makeSTTx(makeExportedPayment(owner, destination));
|
|
auto signature = ExportResultBuilder::signExportedTxn(
|
|
releaseTarget, signer.first, signer.second);
|
|
ExportShare const share{
|
|
ExportShare::currentVersion,
|
|
owner,
|
|
makeHash("precheck-export-origin"),
|
|
10,
|
|
makeHash("precheck-export-origin-ledger"),
|
|
3,
|
|
signer.first,
|
|
std::move(signature)};
|
|
auto const serialized = share.serialize();
|
|
std::string const frame{
|
|
reinterpret_cast<char const*>(serialized.data()),
|
|
serialized.size()};
|
|
|
|
BEAST_EXPECT(frame.size() > 137);
|
|
BEAST_EXPECT(
|
|
frame.size() <= ExportLimits::maxSerializedExportShareBytes);
|
|
BEAST_EXPECT(ExportShare::parse(makeSlice(frame)));
|
|
|
|
auto const makeProposal = [&](std::vector<std::string> const& frames) {
|
|
protocol::TMProposeSet set;
|
|
auto const previous = makeHash("precheck-previous-ledger");
|
|
set.set_previousledger(previous.data(), previous.size());
|
|
for (auto const& bytes : frames)
|
|
set.add_exportsignatures(bytes);
|
|
|
|
ExtendedPosition position{makeHash("precheck-position")};
|
|
position.exportSignaturesHash =
|
|
proposalExportSignaturesHash(frames);
|
|
Serializer positionData;
|
|
position.add(positionData);
|
|
set.set_currenttxhash(positionData.data(), positionData.size());
|
|
return set;
|
|
};
|
|
|
|
std::vector<std::string> atLimit(
|
|
ExportLimits::maxExportSharesPerRelay, frame);
|
|
auto const accepted = makeProposal(atLimit);
|
|
BEAST_EXPECT(
|
|
detail::checkProposalExtensions(accepted, true, true).result == ok);
|
|
|
|
auto overLimit = atLimit;
|
|
overLimit.push_back(frame);
|
|
auto const rejectedCount = makeProposal(overLimit);
|
|
BEAST_EXPECT(
|
|
detail::checkProposalExtensions(rejectedCount, true, true).result ==
|
|
tooManyExportSignatures);
|
|
|
|
std::string oversized = frame;
|
|
oversized.resize(ExportLimits::maxSerializedExportShareBytes + 1, '\0');
|
|
auto const rejectedSize =
|
|
makeProposal(std::vector<std::string>{std::move(oversized)});
|
|
BEAST_EXPECT(
|
|
detail::checkProposalExtensions(rejectedSize, true, true).result ==
|
|
oversizedExportSignature);
|
|
}
|
|
|
|
void
|
|
testHarvestRngDataReplacementAndRejection()
|
|
{
|
|
testcase("harvestRngData replacement and rejection");
|
|
|
|
using namespace jtx;
|
|
Env env{
|
|
*this, envconfig(validator, ""), supported_amendments(), nullptr};
|
|
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
|
|
auto const& valKeys = env.app().getValidatorKeys();
|
|
BEAST_EXPECT(valKeys.keys);
|
|
if (!valKeys.keys)
|
|
return;
|
|
|
|
auto const& publicKey = valKeys.keys->publicKey;
|
|
auto const& secretKey = valKeys.keys->secretKey;
|
|
auto const nodeId = valKeys.nodeID;
|
|
auto const prevLedger = ledger->info().hash;
|
|
auto const seq = ledger->seq() + 1;
|
|
auto const closeTime = NetClock::time_point{NetClock::duration{654}};
|
|
auto const txSetHash = makeHash("harvest-replace-txset");
|
|
auto const reveal1 = makeHash("harvest-reveal-1");
|
|
auto const reveal2 = makeHash("harvest-reveal-2");
|
|
auto const commitment1 = sha512Half(reveal1, publicKey, seq);
|
|
auto const commitment2 = sha512Half(reveal2, publicKey, seq);
|
|
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
ce.cacheUNLReport(ledger);
|
|
|
|
ExtendedPosition inactiveCommit{txSetHash};
|
|
inactiveCommit.myCommitment = commitment1;
|
|
ce.harvestRngData(
|
|
makeNode(99),
|
|
publicKey,
|
|
inactiveCommit,
|
|
0,
|
|
closeTime,
|
|
prevLedger,
|
|
Slice{});
|
|
BEAST_EXPECT(ce.pendingCommitCount() == 0);
|
|
|
|
ExtendedPosition earlyReveal{txSetHash};
|
|
earlyReveal.myReveal = reveal1;
|
|
ce.harvestRngData(
|
|
nodeId, publicKey, earlyReveal, 1, closeTime, prevLedger, Slice{});
|
|
BEAST_EXPECT(ce.pendingRevealCount() == 0);
|
|
|
|
ExtendedPosition commitPos{txSetHash};
|
|
commitPos.myCommitment = commitment1;
|
|
auto commitSig = signPosition(
|
|
publicKey, secretKey, commitPos, 0, closeTime, prevLedger);
|
|
ce.harvestRngData(
|
|
nodeId,
|
|
publicKey,
|
|
commitPos,
|
|
0,
|
|
closeTime,
|
|
prevLedger,
|
|
Slice(commitSig.data(), commitSig.size()));
|
|
BEAST_EXPECT(ce.pendingCommitCount() == 1);
|
|
BEAST_EXPECT(ce.hasQuorumOfCommits());
|
|
|
|
auto revealSig = signPosition(
|
|
publicKey, secretKey, earlyReveal, 1, closeTime, prevLedger);
|
|
ce.harvestRngData(
|
|
nodeId,
|
|
publicKey,
|
|
earlyReveal,
|
|
1,
|
|
closeTime,
|
|
prevLedger,
|
|
Slice(revealSig.data(), revealSig.size()));
|
|
BEAST_EXPECT(ce.pendingRevealCount() == 1);
|
|
BEAST_EXPECT(ce.hasMinimumReveals());
|
|
|
|
commitPos.myCommitment = commitment2;
|
|
commitSig = signPosition(
|
|
publicKey, secretKey, commitPos, 2, closeTime, prevLedger);
|
|
ce.harvestRngData(
|
|
nodeId,
|
|
publicKey,
|
|
commitPos,
|
|
2,
|
|
closeTime,
|
|
prevLedger,
|
|
Slice(commitSig.data(), commitSig.size()));
|
|
BEAST_EXPECT(ce.pendingCommitCount() == 1);
|
|
BEAST_EXPECT(ce.pendingRevealCount() == 1);
|
|
BEAST_EXPECT(ce.hasQuorumOfCommits());
|
|
|
|
ce.harvestRngData(
|
|
nodeId, publicKey, earlyReveal, 3, closeTime, prevLedger, Slice{});
|
|
BEAST_EXPECT(ce.pendingRevealCount() == 1);
|
|
|
|
ExtendedPosition reveal2Pos{txSetHash};
|
|
reveal2Pos.myReveal = reveal2;
|
|
revealSig = signPosition(
|
|
publicKey, secretKey, reveal2Pos, 4, closeTime, prevLedger);
|
|
ce.harvestRngData(
|
|
nodeId,
|
|
publicKey,
|
|
reveal2Pos,
|
|
4,
|
|
closeTime,
|
|
prevLedger,
|
|
Slice(revealSig.data(), revealSig.size()));
|
|
// The later proofless commitment was ignored, so the original proofed
|
|
// commit remains authoritative and the mismatched reveal cannot replace
|
|
// the already-accepted reveal.
|
|
BEAST_EXPECT(ce.pendingRevealCount() == 1);
|
|
}
|
|
|
|
void
|
|
testRngManifestArrivalDoesNotRetargetProofedCommit()
|
|
{
|
|
testcase("RNG manifest arrival does not retarget proofed commit");
|
|
|
|
using namespace jtx;
|
|
auto const masterSeed = randomSeed();
|
|
Env env{
|
|
*this,
|
|
envconfig(validator, toBase58(masterSeed)),
|
|
supported_amendments(),
|
|
nullptr};
|
|
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
|
|
auto const masterSecret =
|
|
generateSecretKey(KeyType::secp256k1, masterSeed);
|
|
auto const signingSecret2 =
|
|
generateSecretKey(KeyType::secp256k1, randomSeed());
|
|
auto const& valKeys = env.app().getValidatorKeys();
|
|
BEAST_EXPECT(valKeys.keys);
|
|
if (!valKeys.keys)
|
|
return;
|
|
auto const& masterKey = valKeys.keys->masterPublicKey;
|
|
auto const& signingKey1 = valKeys.keys->publicKey;
|
|
auto const& signingSecret1 = valKeys.keys->secretKey;
|
|
auto const signingKey2 =
|
|
derivePublicKey(KeyType::secp256k1, signingSecret2);
|
|
auto const nodeId = valKeys.nodeID;
|
|
BEAST_EXPECT(masterKey == signingKey1);
|
|
|
|
ConsensusExtensions manifestFirst{env.app(), activeNoopJournal()};
|
|
ConsensusExtensions proposalFirst{env.app(), activeNoopJournal()};
|
|
manifestFirst.cacheUNLReport(ledger);
|
|
proposalFirst.cacheUNLReport(ledger);
|
|
|
|
auto const seq = ledger->seq() + 1;
|
|
auto const closeTime = NetClock::time_point{NetClock::duration{655}};
|
|
auto const txSetHash = makeHash("manifest-arrival-txset");
|
|
auto const reveal = makeHash("manifest-arrival-reveal");
|
|
auto const commitment = sha512Half(reveal, signingKey1, seq);
|
|
for (auto* ce : {&manifestFirst, &proposalFirst})
|
|
harvestCommitReveal(
|
|
*ce,
|
|
nodeId,
|
|
signingKey1,
|
|
signingSecret1,
|
|
txSetHash,
|
|
seq,
|
|
closeTime,
|
|
ledger->info().hash,
|
|
reveal);
|
|
|
|
ExtendedPosition rotated{txSetHash};
|
|
rotated.myCommitment = commitment;
|
|
auto const rotatedSig = signPosition(
|
|
signingKey2,
|
|
signingSecret2,
|
|
rotated,
|
|
2,
|
|
closeTime,
|
|
ledger->info().hash);
|
|
|
|
// Before the manifest arrives, the rotated key is not trusted and the
|
|
// proposal is ignored. Installing the manifest later does not replay
|
|
// the packet.
|
|
proposalFirst.harvestRngData(
|
|
nodeId,
|
|
signingKey2,
|
|
rotated,
|
|
2,
|
|
closeTime,
|
|
ledger->info().hash,
|
|
Slice(rotatedSig.data(), rotatedSig.size()));
|
|
BEAST_EXPECT(
|
|
env.app().validatorManifests().applyManifest(
|
|
makeValidatorManifest(masterSecret, signingSecret2, 1)) ==
|
|
ManifestDisposition::accepted);
|
|
|
|
// The same packet is trusted when the manifest arrives first, but the
|
|
// proofed K1 commit must keep K1 as its per-round signing key.
|
|
manifestFirst.harvestRngData(
|
|
nodeId,
|
|
signingKey2,
|
|
rotated,
|
|
2,
|
|
closeTime,
|
|
ledger->info().hash,
|
|
Slice(rotatedSig.data(), rotatedSig.size()));
|
|
|
|
BEAST_EXPECT(
|
|
manifestFirst.pendingCommits_ == proposalFirst.pendingCommits_);
|
|
BEAST_EXPECT(
|
|
manifestFirst.pendingReveals_ == proposalFirst.pendingReveals_);
|
|
BEAST_EXPECT(manifestFirst.nodeIdToKey_.at(nodeId) == signingKey1);
|
|
BEAST_EXPECT(proposalFirst.nodeIdToKey_.at(nodeId) == signingKey1);
|
|
BEAST_EXPECT(
|
|
manifestFirst.buildEntropySet(seq) ==
|
|
proposalFirst.buildEntropySet(seq));
|
|
}
|
|
|
|
void
|
|
testRngManifestRotationDropsPinnedContributor()
|
|
{
|
|
testcase("RNG manifest rotation drops pinned contributor");
|
|
|
|
using namespace jtx;
|
|
auto const masterSeed = randomSeed();
|
|
Env env{
|
|
*this,
|
|
envconfig(validator, toBase58(masterSeed)),
|
|
supported_amendments(),
|
|
nullptr};
|
|
auto const anchor = env.app().getLedgerMaster().getClosedLedger();
|
|
auto const masterSecret =
|
|
generateSecretKey(KeyType::secp256k1, masterSeed);
|
|
auto const masterKey =
|
|
derivePublicKey(KeyType::secp256k1, masterSecret);
|
|
auto const signingSecret1 =
|
|
generateSecretKey(KeyType::secp256k1, randomSeed());
|
|
auto const signingSecret2 =
|
|
generateSecretKey(KeyType::secp256k1, randomSeed());
|
|
auto const signingKey1 =
|
|
derivePublicKey(KeyType::secp256k1, signingSecret1);
|
|
auto const signingKey2 =
|
|
derivePublicKey(KeyType::secp256k1, signingSecret2);
|
|
auto const nodeId = calcNodeID(masterKey);
|
|
|
|
BEAST_EXPECT(
|
|
env.app().validatorManifests().applyManifest(
|
|
makeValidatorManifest(masterSecret, signingSecret1, 1)) ==
|
|
ManifestDisposition::accepted);
|
|
|
|
auto const viewLedger =
|
|
makeUNLReportLedger(env, std::vector<PublicKey>{masterKey});
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
ce.cacheUNLReport(viewLedger);
|
|
|
|
auto const seq = anchor->info().seq + 1;
|
|
auto const closeTime = NetClock::time_point{NetClock::duration{656}};
|
|
auto const txSetHash = makeHash("manifest-rotation-txset");
|
|
auto const reveal = makeHash("manifest-rotation-reveal");
|
|
auto const commitment = sha512Half(reveal, signingKey1, seq);
|
|
|
|
ExtendedPosition commitPos{txSetHash};
|
|
commitPos.myCommitment = commitment;
|
|
auto const commitSig = signPosition(
|
|
signingKey1,
|
|
signingSecret1,
|
|
commitPos,
|
|
0,
|
|
closeTime,
|
|
anchor->info().hash);
|
|
ce.harvestRngData(
|
|
nodeId,
|
|
signingKey1,
|
|
commitPos,
|
|
0,
|
|
closeTime,
|
|
anchor->info().hash,
|
|
Slice(commitSig.data(), commitSig.size()));
|
|
BEAST_EXPECT(ce.pendingCommitCount() == 1);
|
|
BEAST_EXPECT(ce.hasQuorumOfCommits());
|
|
|
|
BEAST_EXPECT(
|
|
env.app().validatorManifests().applyManifest(
|
|
makeValidatorManifest(masterSecret, signingSecret2, 2)) ==
|
|
ManifestDisposition::accepted);
|
|
|
|
auto const pinnedProof = ce.commitProofs_.at(nodeId);
|
|
auto const retargetCommitSig = signPosition(
|
|
signingKey2,
|
|
signingSecret2,
|
|
commitPos,
|
|
1,
|
|
closeTime,
|
|
anchor->info().hash);
|
|
ce.harvestRngData(
|
|
nodeId,
|
|
signingKey2,
|
|
commitPos,
|
|
1,
|
|
closeTime,
|
|
anchor->info().hash,
|
|
Slice(retargetCommitSig.data(), retargetCommitSig.size()));
|
|
|
|
BEAST_EXPECT(ce.pendingCommits_.at(nodeId) == commitment);
|
|
BEAST_EXPECT(ce.nodeIdToKey_.at(nodeId) == signingKey1);
|
|
BEAST_EXPECT(ce.commitProofs_.at(nodeId).proposeSeq == 0);
|
|
BEAST_EXPECT(
|
|
ce.commitProofs_.at(nodeId).positionData ==
|
|
pinnedProof.positionData);
|
|
BEAST_EXPECT(
|
|
ce.commitProofs_.at(nodeId).signature == pinnedProof.signature);
|
|
BEAST_EXPECT(ce.proofedCommitCount() == 1);
|
|
BEAST_EXPECT(ce.hasQuorumOfCommits());
|
|
|
|
ExtendedPosition revealPos{txSetHash};
|
|
revealPos.myReveal = reveal;
|
|
auto const retiredKeySig = signPosition(
|
|
signingKey1,
|
|
signingSecret1,
|
|
revealPos,
|
|
1,
|
|
closeTime,
|
|
anchor->info().hash);
|
|
ce.harvestRngData(
|
|
nodeId,
|
|
signingKey1,
|
|
revealPos,
|
|
1,
|
|
closeTime,
|
|
anchor->info().hash,
|
|
Slice(retiredKeySig.data(), retiredKeySig.size()));
|
|
|
|
auto const substitutedKeySig = signPosition(
|
|
signingKey2,
|
|
signingSecret2,
|
|
revealPos,
|
|
2,
|
|
closeTime,
|
|
anchor->info().hash);
|
|
ce.harvestRngData(
|
|
nodeId,
|
|
signingKey2,
|
|
revealPos,
|
|
2,
|
|
closeTime,
|
|
anchor->info().hash,
|
|
Slice(substitutedKeySig.data(), substitutedKeySig.size()));
|
|
|
|
BEAST_EXPECT(ce.pendingRevealCount() == 0);
|
|
BEAST_EXPECT(!ce.hasMinimumReveals());
|
|
auto const entropyHash = ce.buildEntropySet(seq);
|
|
auto const entropySet =
|
|
env.app().getInboundTransactions().getSet(entropyHash, false);
|
|
BEAST_EXPECT(entropySet);
|
|
BEAST_EXPECT(entropySet && entropySet->getHash().isZero());
|
|
}
|
|
|
|
void
|
|
testExportCollectorBuildsAttributedUnion()
|
|
{
|
|
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("attributed-origin");
|
|
auto const signerA = randomKeyPair(KeyType::secp256k1).first;
|
|
auto const signerB = randomKeyPair(KeyType::secp256k1).first;
|
|
std::uint8_t const signatureABytes[] = {1, 2};
|
|
std::uint8_t const signatureBBytes[] = {3, 4};
|
|
Buffer const signatureA{signatureABytes, sizeof(signatureABytes)};
|
|
Buffer const signatureB{signatureBBytes, sizeof(signatureBBytes)};
|
|
|
|
BEAST_EXPECT(collector.registerOrigin(origin, 10));
|
|
auto admit = [&](ExportSigCollector::Position position,
|
|
PublicKey const& key,
|
|
Buffer const& signature) {
|
|
auto admission = collector.beginAttributedAdmission(
|
|
origin,
|
|
ExportSigCollector::Contribution{position, key, signature},
|
|
10);
|
|
BEAST_EXPECT(
|
|
admission.result == ExportSigCollector::BeginResult::verify);
|
|
BEAST_EXPECT(admission.ticket);
|
|
if (!admission.ticket)
|
|
return;
|
|
BEAST_EXPECT(
|
|
collector
|
|
.admitContribution(std::move(*admission.ticket), true, 10)
|
|
.result == ExportSigCollector::AdmitResult::accepted);
|
|
};
|
|
admit(0, signerA, signatureA);
|
|
admit(2, signerB, signatureB);
|
|
|
|
auto const snapshot = collector.fullUnionSnapshot();
|
|
auto const found = snapshot.find(origin);
|
|
BEAST_EXPECT(found != snapshot.end());
|
|
if (found != snapshot.end())
|
|
{
|
|
BEAST_EXPECT(found->second.size() == 2);
|
|
BEAST_EXPECT(found->second[0].position == 0);
|
|
BEAST_EXPECT(found->second[0].signingKey == signerA);
|
|
BEAST_EXPECT(found->second[0].signature == signatureA);
|
|
BEAST_EXPECT(found->second[1].position == 2);
|
|
BEAST_EXPECT(found->second[1].signingKey == signerB);
|
|
BEAST_EXPECT(found->second[1].signature == signatureB);
|
|
}
|
|
}
|
|
|
|
void
|
|
testExportSidecarCandidateDeadline()
|
|
{
|
|
testcase("Export sidecar candidate deadline is inclusive");
|
|
|
|
using namespace jtx;
|
|
Env env{
|
|
*this,
|
|
envconfig(validator, ""),
|
|
supported_amendments() | featureExport,
|
|
nullptr};
|
|
Account const alice{"alice"};
|
|
env.fund(XRP(1000), alice);
|
|
env.close();
|
|
|
|
auto const parent = env.app().getLedgerMaster().getClosedLedger();
|
|
BEAST_EXPECT(parent);
|
|
if (!parent)
|
|
return;
|
|
auto validated = std::make_shared<Ledger>(
|
|
*parent, env.app().timeKeeper().closeTime());
|
|
auto const deadline = validated->info().seq + 1;
|
|
auto const origin = makeHash("export-sidecar-deadline-origin");
|
|
auto latch =
|
|
std::make_shared<SLE>(keylet::exportLatch(alice.id(), origin));
|
|
latch->setAccountID(sfAccount, alice.id());
|
|
latch->setFieldU32(sfTicketSequence, 1);
|
|
latch->setFieldH256(sfTransactionHash, origin);
|
|
latch->setFieldH256(sfDigest, makeHash("export-sidecar-intent"));
|
|
latch->setFieldU32(sfLedgerSequence, validated->info().seq);
|
|
latch->setFieldH256(
|
|
sfExportCommitteeHash, validated->info().parentHash);
|
|
latch->setFieldU32(sfLastLedgerSequence, deadline);
|
|
|
|
Sandbox sandbox{validated.get(), tapNONE};
|
|
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::insertPendingExportLatch(
|
|
sandbox, sandbox, latch, env.journal)));
|
|
sandbox.apply(*validated);
|
|
validated->updateSkipList();
|
|
validated->setAccepted(
|
|
validated->info().closeTime,
|
|
validated->info().closeTimeResolution,
|
|
true);
|
|
env.app().getLedgerMaster().setFullLedger(validated, false, false);
|
|
auto const currentValidated =
|
|
env.app().getLedgerMaster().getValidatedLedger();
|
|
BEAST_EXPECT(
|
|
currentValidated && currentValidated->info().seq == deadline - 1);
|
|
if (!currentValidated || currentValidated->info().seq != deadline - 1)
|
|
return;
|
|
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
ce.onRoundStart(RCLCxLedger{validated}, {});
|
|
auto& collector = ce.postValidationExportSigCollector();
|
|
auto const signer = randomKeyPair(KeyType::secp256k1).first;
|
|
std::uint8_t const signatureBytes[] = {1, 2, 3};
|
|
Buffer const signature{signatureBytes, sizeof(signatureBytes)};
|
|
BEAST_EXPECT(collector.registerOrigin(origin, deadline));
|
|
auto admission = collector.beginAttributedAdmission(
|
|
origin,
|
|
ExportSigCollector::Contribution{0, signer, signature},
|
|
deadline);
|
|
BEAST_EXPECT(admission.ticket);
|
|
if (!admission.ticket)
|
|
return;
|
|
BEAST_EXPECT(
|
|
collector
|
|
.admitContribution(std::move(*admission.ticket), true, deadline)
|
|
.result == ExportSigCollector::AdmitResult::accepted);
|
|
|
|
auto const leafCount = [&](uint256 const& hash) {
|
|
auto const map =
|
|
env.app().getInboundTransactions().getSet(hash, false);
|
|
BEAST_EXPECT(map);
|
|
std::size_t count = 0;
|
|
if (map)
|
|
map->visitLeaves([&](auto const&) { ++count; });
|
|
return count;
|
|
};
|
|
|
|
// The validated view remains D-1 in both cases. Candidate D is the
|
|
// inclusive final publication opportunity; candidate D+1 is expired.
|
|
BEAST_EXPECT(ce.hasEligiblePendingExports());
|
|
BEAST_EXPECT(ce.hasPendingExportSigs());
|
|
BEAST_EXPECT(leafCount(ce.buildExportSigSet(deadline)) == 1);
|
|
|
|
auto nextParent = std::make_shared<Ledger>(
|
|
*validated, env.app().timeKeeper().closeTime());
|
|
nextParent->updateSkipList();
|
|
nextParent->setAccepted(
|
|
nextParent->info().closeTime,
|
|
nextParent->info().closeTimeResolution,
|
|
true);
|
|
ce.onRoundStart(RCLCxLedger{nextParent}, {});
|
|
BEAST_EXPECT(!ce.hasEligiblePendingExports());
|
|
BEAST_EXPECT(!ce.hasPendingExportSigs());
|
|
BEAST_EXPECT(leafCount(ce.buildExportSigSet(deadline + 1)) == 0);
|
|
}
|
|
|
|
void
|
|
testTransactionAcquireRejectsSidecarWireNodes()
|
|
{
|
|
testcase("Transaction acquire rejects sidecar wire nodes");
|
|
|
|
jtx::Env env{*this};
|
|
auto const publicKey = makeValidatorKeys().front();
|
|
|
|
STObject sidecar(sfGeneric);
|
|
sidecar.setFieldU8(sfSidecarType, sidecarExportSig);
|
|
sidecar.setFieldH256(sfTransactionHash, makeHash("export-tx"));
|
|
sidecar.setFieldVL(sfSigningPubKey, publicKey.slice());
|
|
sidecar.setFieldVL(sfTxnSignature, Slice("sig", 3));
|
|
|
|
Serializer itemSer;
|
|
sidecar.add(itemSer);
|
|
auto item = make_shamapitem(
|
|
sidecar.getHash(HashPrefix::sidecar), itemSer.slice());
|
|
SHAMapSidecarLeafNode leaf{std::move(item), 0};
|
|
|
|
Serializer wire;
|
|
leaf.serializeForWire(wire);
|
|
|
|
TransactionAcquire acquire{
|
|
env.app(),
|
|
leaf.getHash().as_uint256(),
|
|
make_DummyPeerSet(env.app())};
|
|
auto const result = acquire.takeNodes(
|
|
{{SHAMapNodeID(), wire.slice()}}, std::shared_ptr<Peer>{});
|
|
|
|
BEAST_EXPECT(result.isInvalid());
|
|
}
|
|
|
|
void
|
|
testAcquiredSetsRejectConsensusExtensionPseudos()
|
|
{
|
|
testcase("Network-acquired sets cannot contain CE pseudo txs");
|
|
|
|
//@@start test-acquired-ce-pseudo-reject
|
|
using namespace jtx;
|
|
Env env{
|
|
*this, envconfig(validator, ""), supported_amendments(), nullptr};
|
|
|
|
std::size_t callbacks = 0;
|
|
bool lastFromAcquire = false;
|
|
auto inbound = make_InboundTransactions(
|
|
env.app(),
|
|
env.app().getCollectorManager().collector(),
|
|
[&](std::shared_ptr<SHAMap> const&, bool fromAcquire) {
|
|
++callbacks;
|
|
lastFromAcquire = fromAcquire;
|
|
});
|
|
|
|
auto const seq = env.closed()->seq() + 1;
|
|
auto const expectRejectedFromAcquire =
|
|
[&](std::shared_ptr<STTx const> const& pseudo) {
|
|
auto const set = makeRCLTxSet(env.app(), {pseudo});
|
|
auto const setHash = set.id();
|
|
auto const beforeCallbacks = callbacks;
|
|
|
|
// A proposal only commits to a candidate tx-set hash. Once
|
|
// the set is acquired, CE-local pseudos must still be
|
|
// unvotable network ingress material, not merely DisputedTxs
|
|
// that an honest minority votes out.
|
|
inbound->giveSet(setHash, set.map_, true);
|
|
BEAST_EXPECT(callbacks == beforeCallbacks);
|
|
BEAST_EXPECT(!inbound->getSet(setHash, false));
|
|
|
|
// Local post-agreement CE materialization still uses the same
|
|
// cache. The acquired-set rejection must not break that
|
|
// local-only path.
|
|
inbound->giveSet(setHash, set.map_, false);
|
|
BEAST_EXPECT(callbacks == beforeCallbacks + 1);
|
|
BEAST_EXPECT(!lastFromAcquire);
|
|
auto const localSet = inbound->getSet(setHash, false);
|
|
BEAST_EXPECT(localSet);
|
|
if (localSet)
|
|
BEAST_EXPECT(localSet->getHash().as_uint256() == setHash);
|
|
};
|
|
|
|
expectRejectedFromAcquire(
|
|
makeConsensusEntropyTx(seq, makeHash("hostile-peer-entropy"), 4));
|
|
expectRejectedFromAcquire(makeExportSignaturesTx(
|
|
seq, makeHash("hostile-peer-export-signatures")));
|
|
//@@end test-acquired-ce-pseudo-reject
|
|
}
|
|
|
|
void
|
|
testExportWitnessRebuildValidationCursor()
|
|
{
|
|
testcase(
|
|
"accepted Export witness rebuild is independent of validation "
|
|
"progress");
|
|
using namespace jtx;
|
|
using namespace std::chrono_literals;
|
|
auto config = envconfig();
|
|
config->NETWORK_ID = 21337;
|
|
Env env{
|
|
*this,
|
|
std::move(config),
|
|
supported_amendments() | featureExport,
|
|
nullptr};
|
|
Account const alice{"cursor-alice"};
|
|
Account const bob{"cursor-bob"};
|
|
env.fund(XRP(1000), alice, bob);
|
|
env.close();
|
|
// A real keyless observer. The test holds the remote validator's key
|
|
// only to construct its independently signed report/share fixtures.
|
|
if (!BEAST_EXPECT(!env.app().getValidatorKeys().keys))
|
|
return;
|
|
auto const signerKeys = randomKeyPair(KeyType::secp256k1);
|
|
auto const& signer = signerKeys.first;
|
|
auto const& secret = signerKeys.second;
|
|
env.app().openLedger().modify([&](OpenView& view, beast::Journal) {
|
|
STTx report(ttUNL_REPORT, [&](auto& obj) {
|
|
obj.setFieldU32(sfLedgerSequence, env.current()->seq());
|
|
auto active = std::make_unique<STObject>(sfActiveValidator);
|
|
active->setFieldVL(sfPublicKey, signer);
|
|
obj.set(std::move(active));
|
|
});
|
|
auto bytes = std::make_shared<Serializer>();
|
|
report.add(*bytes);
|
|
env.app().getHashRouter().setFlags(
|
|
report.getTransactionID(), SF_PRIVATE2);
|
|
view.rawTxInsert(
|
|
report.getTransactionID(), std::move(bytes), nullptr);
|
|
return true;
|
|
});
|
|
if (!BEAST_EXPECT(env.close(
|
|
env.now() + std::chrono::seconds{5},
|
|
std::chrono::milliseconds{0})))
|
|
return;
|
|
forceNonStandalone(env.app());
|
|
auto& master = env.app().getLedgerMaster();
|
|
auto const beforeOrigin = master.getClosedLedger();
|
|
if (!BEAST_EXPECT(
|
|
beforeOrigin && beforeOrigin->read(keylet::UNLReport())))
|
|
return;
|
|
auto const roster = serializeExportCommittee({signer});
|
|
auto target = makeExportedPayment(alice.id(), bob.id());
|
|
target.setFieldU32(sfLastLedgerSequence, beforeOrigin->info().seq + 20);
|
|
Json::Value json;
|
|
json[jss::TransactionType] = jss::Export;
|
|
json[jss::Account] = alice.human();
|
|
json[jss::LastLedgerSequence] = beforeOrigin->info().seq + 5;
|
|
json[sfExportedTxn.jsonName] = target.getJson(JsonOptions::none);
|
|
json[sfExportCommitteeHash.jsonName] =
|
|
to_string(exportCommitteeHash(makeSlice(roster)));
|
|
json[sfExportCommittee.jsonName] = strHex(roster);
|
|
auto const intent = env.jt(json, fee(XRP(1)), ter(tesSUCCESS)).stx;
|
|
if (!BEAST_EXPECT(intent))
|
|
return;
|
|
CanonicalTXSet txs{beforeOrigin->info().hash};
|
|
txs.insert(intent);
|
|
std::set<TxID> failed;
|
|
auto parent = buildLedger(
|
|
beforeOrigin,
|
|
env.app().timeKeeper().closeTime(),
|
|
true,
|
|
beforeOrigin->info().closeTimeResolution,
|
|
env.app(),
|
|
txs,
|
|
failed,
|
|
env.journal);
|
|
if (!BEAST_EXPECT(txs.empty() && failed.empty()))
|
|
return;
|
|
master.storeLedger(parent); // Possessed, not yet locally validated.
|
|
auto const origin = intent->getTransactionID();
|
|
auto const latch = keylet::exportLatch(alice.id(), origin);
|
|
if (!BEAST_EXPECT(parent->read(latch)))
|
|
return;
|
|
auto release = ExportOriginMemo::releaseForm(
|
|
makeSTTx(target),
|
|
ExportOriginMemo::Origin{env.app().config().NETWORK_ID, 0, origin},
|
|
ExportOriginMemo::Anchor{parent->info().seq, parent->info().hash});
|
|
if (!BEAST_EXPECT(release))
|
|
return;
|
|
auto signature = ExportResultBuilder::signExportedTxn(
|
|
release.value(), signer, secret);
|
|
ExportShare share{
|
|
ExportShare::currentVersion,
|
|
alice.id(),
|
|
origin,
|
|
parent->info().seq,
|
|
parent->info().hash,
|
|
0,
|
|
signer,
|
|
signature};
|
|
|
|
// Install an authenticated accepted-root fixture directly. This
|
|
// isolates rebuild determinism, not the separate question of how a
|
|
// lagged live observer obtains and aligns the material in the first
|
|
// place.
|
|
STObject sidecar(sfGeneric);
|
|
sidecar.setFieldU8(sfSidecarType, sidecarExportSig);
|
|
sidecar.setFieldH256(sfTransactionHash, origin);
|
|
sidecar.setFieldU32(sfTransactionIndex, 0);
|
|
sidecar.setFieldVL(sfSigningPubKey, signer.slice());
|
|
sidecar.setFieldVL(
|
|
sfTxnSignature, Slice{signature.data(), signature.size()});
|
|
Serializer bytes;
|
|
sidecar.add(bytes);
|
|
auto map = std::make_shared<SHAMap>(
|
|
SHAMapType::SIDECAR, env.app().getNodeFamily());
|
|
map->setUnbacked();
|
|
BEAST_EXPECT(map->addItem(
|
|
SHAMapNodeType::tnSIDECAR,
|
|
make_shamapitem(
|
|
sidecar.getHash(HashPrefix::sidecar), bytes.slice())));
|
|
map = map->snapShot(false);
|
|
auto const root = map->getHash().as_uint256();
|
|
|
|
auto rebuild = [&](ConsensusMode mode,
|
|
bool acceptRoot =
|
|
true) -> std::shared_ptr<STTx const> {
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
ce.onRoundStart(RCLCxLedger{parent}, {});
|
|
ce.setMode(mode);
|
|
ce.setRngEnabledThisRound(false);
|
|
ce.exportSigSetMap_ = map;
|
|
if (acceptRoot)
|
|
ce.acceptExportSigSet(root);
|
|
CanonicalTXSet result{uint256{}};
|
|
ce.onPreBuild(result, parent->info().seq + 1, uint256{});
|
|
BEAST_EXPECT(result.size() <= 1);
|
|
return result.empty() ? nullptr : result.begin()->second;
|
|
};
|
|
auto installValidated = [&](std::shared_ptr<Ledger> const& ledger) {
|
|
master.storeLedger(ledger);
|
|
master.setFullLedger(ledger, false, false);
|
|
auto current = master.getValidatedLedger();
|
|
BEAST_EXPECT(
|
|
current && current->info().hash == ledger->info().hash);
|
|
};
|
|
auto const modes = {ConsensusMode::observing, ConsensusMode::proposing};
|
|
BEAST_EXPECT(
|
|
master.getValidatedLedger()->info().seq < parent->info().seq);
|
|
{
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
// Working rounds may be several ledgers ahead of validation.
|
|
// The parent contains an intent even though live share admission
|
|
// must still defer it. No accepted-root fixture is installed here.
|
|
auto ahead = parent;
|
|
for (int i = 0; i < 3; ++i)
|
|
{
|
|
ahead = std::make_shared<Ledger>(
|
|
*ahead, env.app().timeKeeper().closeTime());
|
|
ahead->updateSkipList();
|
|
ahead->setAccepted(
|
|
ahead->info().closeTime,
|
|
ahead->info().closeTimeResolution,
|
|
true);
|
|
master.storeLedger(ahead);
|
|
}
|
|
ce.onRoundStart(RCLCxLedger{ahead}, {});
|
|
ce.setRngEnabledThisRound(false);
|
|
ce.setMode(ConsensusMode::observing);
|
|
ce.startExportShareService();
|
|
BEAST_EXPECT(
|
|
ce.onExportShare(share, {}).disposition ==
|
|
ExportShareDisposition::deferred);
|
|
BEAST_EXPECT(ce.hasEligiblePendingExports());
|
|
BEAST_EXPECT(!ce.hasPendingExportSigs());
|
|
ExtensionTickHarness lagged;
|
|
lagged.mode = ConsensusMode::observing;
|
|
lagged.buildSeq = ahead->info().seq + 1;
|
|
BEAST_EXPECT(!lagged.tick(ce).readyForAccept);
|
|
BEAST_EXPECT(ce.exportSigGateStarted_);
|
|
BEAST_EXPECT(!ce.acceptedExportSigSetHash_);
|
|
auto const deadline =
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
ripple::detail::sidecarConvergenceTimeout(lagged.parms));
|
|
BEAST_EXPECT(lagged.tick(ce, deadline + 1ms).readyForAccept);
|
|
BEAST_EXPECT(ce.exportSigConvergenceFailed());
|
|
BEAST_EXPECT(!ce.acceptedExportSigSetHash_);
|
|
BEAST_EXPECT(lagged.proposes == 0);
|
|
ce.stopExportShareService();
|
|
}
|
|
for (auto mode : modes)
|
|
{
|
|
BEAST_EXPECT(rebuild(mode)); // Origin ahead of local validation.
|
|
BEAST_EXPECT(!rebuild(mode, false)); // No root is not permission.
|
|
}
|
|
|
|
installValidated(parent);
|
|
auto expected = rebuild(ConsensusMode::observing);
|
|
if (!BEAST_EXPECT(
|
|
expected && expected->getTxnType() == ttEXPORT_SIGNATURES))
|
|
return;
|
|
BEAST_EXPECT(
|
|
rebuild(ConsensusMode::proposing)->getTransactionID() ==
|
|
expected->getTransactionID());
|
|
|
|
// Production admission and production alignment, not a manually
|
|
// accepted map: validation may overtake a round between candidate
|
|
// publication and the next peer-alignment tick.
|
|
ConsensusExtensions aligning{env.app(), activeNoopJournal()};
|
|
aligning.onRoundStart(RCLCxLedger{parent}, {});
|
|
aligning.setRngEnabledThisRound(false);
|
|
aligning.setMode(ConsensusMode::observing);
|
|
aligning.startExportShareService();
|
|
BEAST_EXPECT(aligning.onExportShare(share, {}).isAccepted());
|
|
BEAST_EXPECT(!aligning.localIsActiveValidator());
|
|
ExtensionTickHarness observation;
|
|
observation.mode = ConsensusMode::observing;
|
|
observation.buildSeq = parent->info().seq + 1;
|
|
observation.prevProposers = 1;
|
|
ExtendedPosition peer{observation.position.txSetHash};
|
|
peer.exportSigSetHash = root;
|
|
auto const signerNode = calcNodeID(signer);
|
|
observation.peers.emplace(
|
|
signerNode, FakePeerPosition{signerNode, peer});
|
|
BEAST_EXPECT(!observation.tick(aligning).readyForAccept);
|
|
BEAST_EXPECT(observation.position.exportSigSetHash == root);
|
|
BEAST_EXPECT(!aligning.acceptedExportSigSetHash_);
|
|
|
|
// Advance local validation past this round's parent, retaining its
|
|
// chain.
|
|
auto next = std::make_shared<Ledger>(
|
|
*parent, env.app().timeKeeper().closeTime());
|
|
Sandbox changes{next.get(), tapNONE};
|
|
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::recordExportWitness(
|
|
changes,
|
|
changes,
|
|
latch,
|
|
expected->getTransactionID(),
|
|
env.journal)));
|
|
changes.apply(*next);
|
|
next->updateSkipList();
|
|
next->setAccepted(
|
|
next->info().closeTime, next->info().closeTimeResolution, true);
|
|
installValidated(next);
|
|
|
|
// Local validation now includes the witness, but this in-flight
|
|
// round still reconstructs the child of the earlier parent. Its
|
|
// candidacy and verified share set must not disappear between ticks.
|
|
BEAST_EXPECT(aligning.hasEligiblePendingExports());
|
|
BEAST_EXPECT(aligning.hasPendingExportSigs());
|
|
BEAST_EXPECT(observation.tick(aligning, 1ms).readyForAccept);
|
|
BEAST_EXPECT(aligning.acceptedExportSigSetHash_ == root);
|
|
BEAST_EXPECT(
|
|
aligning.exportSigSetMap_ &&
|
|
aligning.exportSigSetMap_->getHash().as_uint256() == root);
|
|
BEAST_EXPECT(!aligning.exportSigConvergenceFailed());
|
|
BEAST_EXPECT(observation.proposes == 0);
|
|
CanonicalTXSet alignedResult{uint256{}};
|
|
aligning.onPreBuild(alignedResult, parent->info().seq + 1, uint256{});
|
|
BEAST_EXPECT(alignedResult.size() == 1);
|
|
if (!alignedResult.empty())
|
|
BEAST_EXPECT(
|
|
alignedResult.begin()->second->getTransactionID() ==
|
|
expected->getTransactionID());
|
|
|
|
// Keeping round-local candidacy does not reopen live admission for
|
|
// an already witnessed intent, or carry candidacy into a later round.
|
|
BEAST_EXPECT(
|
|
aligning.onExportShare(share, {}).disposition ==
|
|
ExportShareDisposition::duplicate);
|
|
aligning.onRoundStart(RCLCxLedger{next}, {});
|
|
BEAST_EXPECT(!aligning.acceptedExportSigSetHash_);
|
|
BEAST_EXPECT(!aligning.hasEligiblePendingExports());
|
|
BEAST_EXPECT(!aligning.hasPendingExportSigs());
|
|
|
|
// A local root is still not permission without peer alignment. The
|
|
// old round's already-admitted share survives, but cannot self-count.
|
|
aligning.onRoundStart(RCLCxLedger{parent}, {});
|
|
aligning.setRngEnabledThisRound(false);
|
|
ExtensionTickHarness unaligned;
|
|
unaligned.mode = ConsensusMode::observing;
|
|
unaligned.buildSeq = parent->info().seq + 1;
|
|
BEAST_EXPECT(!unaligned.tick(aligning).readyForAccept);
|
|
BEAST_EXPECT(!unaligned.tick(aligning, 1ms).readyForAccept);
|
|
auto const deadline =
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
ripple::detail::sidecarConvergenceTimeout(unaligned.parms));
|
|
BEAST_EXPECT(unaligned.tick(aligning, deadline + 1ms).readyForAccept);
|
|
BEAST_EXPECT(aligning.exportSigConvergenceFailed());
|
|
BEAST_EXPECT(!aligning.acceptedExportSigSetHash_);
|
|
aligning.stopExportShareService();
|
|
for (auto mode : modes)
|
|
{
|
|
auto actual = rebuild(mode);
|
|
BEAST_EXPECT(actual);
|
|
if (actual)
|
|
BEAST_EXPECT(
|
|
actual->getTransactionID() == expected->getTransactionID());
|
|
}
|
|
|
|
// A newer validated ledger on a competing ancestry must not authorize
|
|
// reconstruction from this round's old parent.
|
|
auto fork = std::make_shared<Ledger>(
|
|
*beforeOrigin, env.app().timeKeeper().closeTime());
|
|
while (fork->info().seq <= next->info().seq)
|
|
{
|
|
fork->updateSkipList();
|
|
fork->setAccepted(
|
|
fork->info().closeTime, fork->info().closeTimeResolution, true);
|
|
fork = std::make_shared<Ledger>(
|
|
*fork, env.app().timeKeeper().closeTime());
|
|
}
|
|
fork->updateSkipList();
|
|
fork->setAccepted(
|
|
fork->info().closeTime, fork->info().closeTimeResolution, true);
|
|
installValidated(fork);
|
|
for (auto mode : modes)
|
|
BEAST_EXPECT(!rebuild(mode));
|
|
}
|
|
|
|
void
|
|
testAgreedExportWitnessBuildsContributorBitmap()
|
|
{
|
|
testcase(
|
|
"agreed Export witness materialization is insertion-order "
|
|
"independent");
|
|
|
|
using namespace jtx;
|
|
auto const signerA = randomKeyPair(KeyType::secp256k1);
|
|
auto const signerB = randomKeyPair(KeyType::secp256k1);
|
|
auto const signerC = randomKeyPair(KeyType::secp256k1);
|
|
auto const dst = calcAccountID(randomKeyPair(KeyType::secp256k1).first);
|
|
auto const releaseTarget =
|
|
makeSTTx(makeExportedPayment(calcAccountID(signerA.first), dst));
|
|
auto const origin = makeHash("agreed-export-origin");
|
|
Blob const contributors{0x07};
|
|
constexpr std::size_t committeeSize = 3;
|
|
auto const threshold = ExportLimits::committeeQuorumThreshold(3);
|
|
BEAST_EXPECT(threshold == 3);
|
|
|
|
struct Contribution
|
|
{
|
|
std::uint32_t position;
|
|
PublicKey signingKey;
|
|
Buffer signature;
|
|
};
|
|
|
|
std::vector<Contribution> const contributions = {
|
|
{0,
|
|
signerA.first,
|
|
ExportResultBuilder::signExportedTxn(
|
|
releaseTarget, signerA.first, signerA.second)},
|
|
{1,
|
|
signerB.first,
|
|
ExportResultBuilder::signExportedTxn(
|
|
releaseTarget, signerB.first, signerB.second)},
|
|
{2,
|
|
signerC.first,
|
|
ExportResultBuilder::signExportedTxn(
|
|
releaseTarget, signerC.first, signerC.second)}};
|
|
|
|
struct MaterializedWitness
|
|
{
|
|
uint256 root;
|
|
ExportResultBuilder::PositionedSignatureSnapshot signatures;
|
|
Blob contributors;
|
|
std::vector<std::uint8_t> serialized;
|
|
uint256 txid;
|
|
};
|
|
|
|
auto const materialize =
|
|
[&](std::vector<std::size_t> const& insertionOrder)
|
|
-> std::optional<MaterializedWitness> {
|
|
Env env{
|
|
*this,
|
|
envconfig(validator, ""),
|
|
supported_amendments(),
|
|
nullptr};
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
|
|
auto map = std::make_shared<SHAMap>(
|
|
SHAMapType::SIDECAR, env.app().getNodeFamily());
|
|
map->setUnbacked();
|
|
for (auto const index : insertionOrder)
|
|
{
|
|
auto const& contribution = contributions[index];
|
|
STObject sidecar(sfGeneric);
|
|
sidecar.setFieldU8(sfSidecarType, sidecarExportSig);
|
|
sidecar.setFieldH256(sfTransactionHash, origin);
|
|
sidecar.setFieldU32(sfTransactionIndex, contribution.position);
|
|
sidecar.setFieldVL(
|
|
sfSigningPubKey, contribution.signingKey.slice());
|
|
sidecar.setFieldVL(
|
|
sfTxnSignature,
|
|
Slice{
|
|
contribution.signature.data(),
|
|
contribution.signature.size()});
|
|
Serializer serialized;
|
|
sidecar.add(serialized);
|
|
BEAST_EXPECT(map->addItem(
|
|
SHAMapNodeType::tnSIDECAR,
|
|
make_shamapitem(
|
|
sidecar.getHash(HashPrefix::sidecar),
|
|
serialized.slice())));
|
|
}
|
|
map = map->snapShot(false);
|
|
|
|
auto const acceptedHash = map->getHash().as_uint256();
|
|
env.app().getInboundTransactions().giveSet(
|
|
acceptedHash, map, false);
|
|
BEAST_EXPECT(!ce.agreedExportWitness(
|
|
releaseTarget, origin, committeeSize, threshold));
|
|
ce.acceptExportSigSet(acceptedHash);
|
|
|
|
auto const material = ce.agreedExportWitness(
|
|
releaseTarget, origin, committeeSize, threshold);
|
|
BEAST_EXPECT(material);
|
|
if (!material)
|
|
return std::nullopt;
|
|
|
|
auto const witness = ExportResultBuilder::buildSignatureWitness(
|
|
origin, releaseTarget, material->signatures, committeeSize, 20);
|
|
BEAST_EXPECT(!witness.isFieldPresent(sfSigners));
|
|
BEAST_EXPECT(
|
|
witness.getFieldVL(sfExportContributors) == contributors);
|
|
auto const& assembled =
|
|
witness.peekAtField(sfExportedTxn).downcast<STObject>();
|
|
BEAST_EXPECT(!assembled.isFieldPresent(sfSigners));
|
|
BEAST_EXPECT(
|
|
witness.getFieldArray(sfExportSigners).size() ==
|
|
contributions.size());
|
|
|
|
Serializer serialized;
|
|
witness.add(serialized);
|
|
return MaterializedWitness{
|
|
acceptedHash,
|
|
material->signatures,
|
|
witness.getFieldVL(sfExportContributors),
|
|
serialized.peekData(),
|
|
witness.getTransactionID()};
|
|
};
|
|
|
|
auto const forward = materialize({0, 1, 2});
|
|
auto const reverse = materialize({2, 1, 0});
|
|
BEAST_EXPECT(forward && reverse);
|
|
if (!forward || !reverse)
|
|
return;
|
|
|
|
auto const expectMapping =
|
|
[&](ExportResultBuilder::PositionedSignatureSnapshot const&
|
|
signatures) {
|
|
BEAST_EXPECT(signatures.size() == contributions.size());
|
|
for (auto const& expected : contributions)
|
|
{
|
|
auto const found = signatures.find(expected.position);
|
|
BEAST_EXPECT(found != signatures.end());
|
|
if (found != signatures.end())
|
|
{
|
|
BEAST_EXPECT(
|
|
found->second.signingKey == expected.signingKey);
|
|
BEAST_EXPECT(
|
|
found->second.signature == expected.signature);
|
|
}
|
|
}
|
|
};
|
|
expectMapping(forward->signatures);
|
|
expectMapping(reverse->signatures);
|
|
|
|
BEAST_EXPECT(forward->root == reverse->root);
|
|
BEAST_EXPECT(forward->contributors == contributors);
|
|
BEAST_EXPECT(reverse->contributors == contributors);
|
|
BEAST_EXPECT(forward->serialized == reverse->serialized);
|
|
BEAST_EXPECT(forward->txid == reverse->txid);
|
|
}
|
|
|
|
void
|
|
testRngSidecarBuildsLocalSnapshots()
|
|
{
|
|
testcase("RNG sidecar builds local snapshots");
|
|
|
|
using namespace jtx;
|
|
Env env{
|
|
*this, envconfig(validator, ""), supported_amendments(), nullptr};
|
|
auto const& valKeys = env.app().getValidatorKeys();
|
|
BEAST_EXPECT(valKeys.keys);
|
|
if (!valKeys.keys)
|
|
return;
|
|
|
|
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
|
|
auto const& publicKey = valKeys.keys->publicKey;
|
|
auto const& secretKey = valKeys.keys->secretKey;
|
|
auto const nodeId = valKeys.nodeID;
|
|
auto const prevLedger = ledger->info().hash;
|
|
auto const seq = ledger->seq() + 1;
|
|
auto const closeTime = NetClock::time_point{NetClock::duration{777}};
|
|
auto const txSetHash = makeHash("rng-sidecar-txset");
|
|
auto const reveal = makeHash("rng-sidecar-reveal");
|
|
auto const commitment = sha512Half(reveal, publicKey, seq);
|
|
|
|
ConsensusExtensions source{env.app(), env.journal};
|
|
source.cacheUNLReport(ledger);
|
|
|
|
ExtendedPosition commitPos{txSetHash};
|
|
commitPos.myCommitment = commitment;
|
|
auto const commitSig = signPosition(
|
|
publicKey, secretKey, commitPos, 0, closeTime, prevLedger);
|
|
source.harvestRngData(
|
|
nodeId,
|
|
publicKey,
|
|
commitPos,
|
|
0,
|
|
closeTime,
|
|
prevLedger,
|
|
Slice(commitSig.data(), commitSig.size()));
|
|
BEAST_EXPECT(source.pendingCommitCount() == 1);
|
|
|
|
ExtendedPosition revealPos{txSetHash};
|
|
revealPos.myReveal = reveal;
|
|
auto const revealSig = signPosition(
|
|
publicKey, secretKey, revealPos, 1, closeTime, prevLedger);
|
|
source.harvestRngData(
|
|
nodeId,
|
|
publicKey,
|
|
revealPos,
|
|
1,
|
|
closeTime,
|
|
prevLedger,
|
|
Slice(revealSig.data(), revealSig.size()));
|
|
BEAST_EXPECT(source.pendingRevealCount() == 1);
|
|
|
|
auto const commitSetHash = source.buildCommitSet(seq);
|
|
auto const revealSetHash = source.buildEntropySet(seq);
|
|
BEAST_EXPECT(
|
|
env.app().getInboundTransactions().getSet(commitSetHash, false));
|
|
BEAST_EXPECT(
|
|
env.app().getInboundTransactions().getSet(revealSetHash, false));
|
|
|
|
source.setRngEnabledThisRound(true);
|
|
source.recordParticipantDiagnostics(
|
|
ConsensusMode::proposing, std::vector<NodeID>{nodeId});
|
|
BEAST_EXPECT(source.observedParticipantCount() == 1);
|
|
BEAST_EXPECT(source.observedParticipantsHash());
|
|
BEAST_EXPECT(!source.observedParticipantsBitmapBin().empty());
|
|
|
|
ExtendedPosition diagnosticPos{txSetHash};
|
|
source.attachParticipantDiagnostics(diagnosticPos);
|
|
BEAST_EXPECT(diagnosticPos.observedParticipantsHash);
|
|
}
|
|
|
|
void
|
|
testOnPreBuildInjectsStandaloneEntropy()
|
|
{
|
|
testcase("onPreBuild injects standalone entropy pseudo-tx");
|
|
|
|
using namespace jtx;
|
|
Env env{
|
|
*this, envconfig(validator, ""), supported_amendments(), nullptr};
|
|
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
ce.setRngEnabledThisRound(true);
|
|
CanonicalTXSet retriableTxs{makeHash("rng-on-pre-build-salt")};
|
|
auto const seq = env.closed()->seq() + 1;
|
|
|
|
ce.onPreBuild(retriableTxs, seq, makeHash("standalone-txset"));
|
|
BEAST_EXPECT(
|
|
std::distance(retriableTxs.begin(), retriableTxs.end()) == 1);
|
|
|
|
auto const tx = retriableTxs.begin()->second;
|
|
BEAST_EXPECT(tx);
|
|
BEAST_EXPECT(tx->getTxnType() == ttCONSENSUS_ENTROPY);
|
|
BEAST_EXPECT(tx->getFieldU32(sfLedgerSequence) == seq);
|
|
BEAST_EXPECT(tx->getAccountID(sfAccount) == AccountID{});
|
|
BEAST_EXPECT(
|
|
tx->getFieldH256(sfDigest) ==
|
|
sha512Half(std::string("standalone-entropy"), seq));
|
|
BEAST_EXPECT(tx->getFieldU16(sfEntropyCount) == 20);
|
|
BEAST_EXPECT(tx->getFieldU16(sfEntropyDenominator) == 20);
|
|
BEAST_EXPECT(
|
|
tx->getFieldVL(sfEntropyContributors) ==
|
|
standaloneContributorMask(20, 20));
|
|
BEAST_EXPECT(tx->getFieldU8(sfEntropyTier) == entropyTierValidatorFull);
|
|
|
|
// Live materialization is reconstructive: a second call removes the
|
|
// prior synthetic entry and derives the same canonical entry again.
|
|
ce.onPreBuild(retriableTxs, seq, makeHash("standalone-txset"));
|
|
BEAST_EXPECT(
|
|
std::distance(retriableTxs.begin(), retriableTxs.end()) == 1);
|
|
}
|
|
|
|
void
|
|
testOnPreBuildStripsSuppliedExtensionPseudos()
|
|
{
|
|
testcase("onPreBuild strips supplied extension pseudo-txs");
|
|
|
|
using namespace jtx;
|
|
Env env{
|
|
*this, envconfig(validator, ""), supported_amendments(), nullptr};
|
|
auto const seq = env.closed()->seq() + 1;
|
|
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
ce.setRngEnabledThisRound(true);
|
|
CanonicalTXSet txs{makeHash("supplied-extension-pseudos-salt")};
|
|
auto const suppliedEntropy =
|
|
makeConsensusEntropyTx(seq, makeHash("supplied-digest"), 7);
|
|
auto const suppliedEntropyID = suppliedEntropy->getTransactionID();
|
|
auto const suppliedWitness =
|
|
makeExportSignaturesTx(seq, makeHash("supplied-export-origin"));
|
|
auto const suppliedWitnessID = suppliedWitness->getTransactionID();
|
|
txs.insert(suppliedEntropy);
|
|
txs.insert(suppliedWitness);
|
|
|
|
ce.onPreBuild(txs, seq, makeHash("supplied-extension-pseudos-txset"));
|
|
|
|
BEAST_EXPECT(std::distance(txs.begin(), txs.end()) == 1);
|
|
auto const derived = singleCanonicalTx(txs);
|
|
BEAST_EXPECT(derived);
|
|
if (derived)
|
|
{
|
|
BEAST_EXPECT(derived->getTxnType() == ttCONSENSUS_ENTROPY);
|
|
BEAST_EXPECT(derived->getTransactionID() != suppliedEntropyID);
|
|
BEAST_EXPECT(derived->getTransactionID() != suppliedWitnessID);
|
|
BEAST_EXPECT(
|
|
derived->getFieldH256(sfDigest) ==
|
|
sha512Half(std::string("standalone-entropy"), seq));
|
|
}
|
|
}
|
|
|
|
void
|
|
testLiveBuildSetStripsOnlyExtensionPseudos()
|
|
{
|
|
testcase("live build set strips only extension pseudo-txs");
|
|
|
|
using namespace jtx;
|
|
Env env{
|
|
*this,
|
|
envconfig(validator, ""),
|
|
supported_amendments() | featureConsensusEntropy,
|
|
nullptr};
|
|
auto const seq = env.closed()->seq() + 1;
|
|
auto const validatorKey = makeValidatorKeys().front();
|
|
|
|
auto const fee = std::make_shared<STTx const>(ttFEE, [&](auto& obj) {
|
|
obj.setAccountID(sfAccount, AccountID{});
|
|
obj.setFieldU32(sfLedgerSequence, seq);
|
|
});
|
|
auto const amendment =
|
|
std::make_shared<STTx const>(ttAMENDMENT, [&](auto& obj) {
|
|
obj.setAccountID(sfAccount, AccountID{});
|
|
obj.setFieldU32(sfLedgerSequence, seq);
|
|
obj.setFieldH256(sfAmendment, makeHash("legacy-amendment"));
|
|
});
|
|
auto const negativeUNL =
|
|
std::make_shared<STTx const>(ttUNL_MODIFY, [&](auto& obj) {
|
|
obj.setAccountID(sfAccount, AccountID{});
|
|
obj.setFieldU8(sfUNLModifyDisabling, 1);
|
|
obj.setFieldU32(sfLedgerSequence, seq);
|
|
obj.setFieldVL(sfUNLModifyValidator, validatorKey);
|
|
});
|
|
auto const suppliedEntropy =
|
|
makeConsensusEntropyTx(seq, makeHash("supplied-digest"), 7);
|
|
auto const suppliedWitness =
|
|
makeExportSignaturesTx(seq, makeHash("supplied-export-origin"));
|
|
|
|
auto const agreed = makeRCLTxSet(
|
|
env.app(),
|
|
{fee, amendment, negativeUNL, suppliedEntropy, suppliedWitness});
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
auto const build = ce.makeLiveBuildTxSet(agreed);
|
|
|
|
BEAST_EXPECT(build.suppliedEntropy == 1);
|
|
BEAST_EXPECT(build.suppliedExportWitnesses == 1);
|
|
BEAST_EXPECT(build.txns.exists(fee->getTransactionID()));
|
|
BEAST_EXPECT(build.txns.exists(amendment->getTransactionID()));
|
|
BEAST_EXPECT(build.txns.exists(negativeUNL->getTransactionID()));
|
|
BEAST_EXPECT(!build.txns.exists(suppliedEntropy->getTransactionID()));
|
|
BEAST_EXPECT(!build.txns.exists(suppliedWitness->getTransactionID()));
|
|
|
|
auto const expected =
|
|
makeRCLTxSet(env.app(), {fee, amendment, negativeUNL});
|
|
BEAST_EXPECT(build.txns.id() == expected.id());
|
|
BEAST_EXPECT(agreed.exists(suppliedEntropy->getTransactionID()));
|
|
BEAST_EXPECT(agreed.exists(suppliedWitness->getTransactionID()));
|
|
|
|
auto const alternateAgreed = makeRCLTxSet(
|
|
env.app(),
|
|
{fee,
|
|
amendment,
|
|
negativeUNL,
|
|
makeConsensusEntropyTx(
|
|
seq, makeHash("alternate-supplied-digest"), 3),
|
|
makeExportSignaturesTx(
|
|
seq, makeHash("alternate-supplied-export-origin"))});
|
|
auto const alternateBuild = ce.makeLiveBuildTxSet(alternateAgreed);
|
|
BEAST_EXPECT(alternateAgreed.id() != agreed.id());
|
|
BEAST_EXPECT(alternateBuild.txns.id() == build.txns.id());
|
|
|
|
forceNonStandalone(env.app());
|
|
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
|
|
ConsensusExtensions derivation{env.app(), activeNoopJournal()};
|
|
ConsensusExtensions alternateDerivation{env.app(), activeNoopJournal()};
|
|
derivation.onRoundStart(RCLCxLedger{ledger}, {});
|
|
alternateDerivation.onRoundStart(RCLCxLedger{ledger}, {});
|
|
derivation.setRngEnabledThisRound(true);
|
|
alternateDerivation.setRngEnabledThisRound(true);
|
|
|
|
auto const salt = derivation.txnOrderingSalt(build.txns.id(), seq);
|
|
auto const alternateSalt =
|
|
alternateDerivation.txnOrderingSalt(alternateBuild.txns.id(), seq);
|
|
BEAST_EXPECT(alternateSalt == salt);
|
|
|
|
CanonicalTXSet derived{salt};
|
|
CanonicalTXSet alternateDerived{alternateSalt};
|
|
derivation.onPreBuild(derived, seq, build.txns.id());
|
|
alternateDerivation.onPreBuild(
|
|
alternateDerived, seq, alternateBuild.txns.id());
|
|
auto const derivedEntropy = singleCanonicalTx(derived);
|
|
auto const alternateEntropy = singleCanonicalTx(alternateDerived);
|
|
BEAST_EXPECT(derivedEntropy);
|
|
BEAST_EXPECT(alternateEntropy);
|
|
if (derivedEntropy && alternateEntropy)
|
|
BEAST_EXPECT(
|
|
derivedEntropy->getFieldH256(sfDigest) ==
|
|
alternateEntropy->getFieldH256(sfDigest));
|
|
}
|
|
|
|
void
|
|
testDiagnosticsJsonAndPositionLogging()
|
|
{
|
|
testcase("diagnostics JSON and position logging");
|
|
|
|
using namespace jtx;
|
|
Env env{
|
|
*this, envconfig(validator, ""), supported_amendments(), nullptr};
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
auto const& valKeys = env.app().getValidatorKeys();
|
|
BEAST_EXPECT(valKeys.keys);
|
|
if (!valKeys.keys)
|
|
return;
|
|
|
|
ce.setRngEnabledThisRound(true);
|
|
ce.recordParticipantDiagnostics(
|
|
ConsensusMode::proposing, std::vector<NodeID>{valKeys.nodeID});
|
|
|
|
Json::Value json;
|
|
ce.appendJson(json);
|
|
BEAST_EXPECT(json.isMember("rng"));
|
|
BEAST_EXPECT(json["rng"]["enabled"].asBool());
|
|
BEAST_EXPECT(json["rng"]["est_state"].asString() == "ConvergingTx");
|
|
BEAST_EXPECT(
|
|
json["rng"]["observed_active_participants"].asInt() ==
|
|
static_cast<int>(ce.observedParticipantCount()));
|
|
BEAST_EXPECT(json["rng"].isMember("observed_participants"));
|
|
BEAST_EXPECT(json["rng"].isMember("observed_participants_bitmap"));
|
|
|
|
ExtendedPosition pos{makeHash("diagnostic-tx-set")};
|
|
pos.commitSetHash = makeHash("diagnostic-commit-set");
|
|
pos.entropySetHash = makeHash("diagnostic-entropy-set");
|
|
pos.exportSigSetHash = makeHash("diagnostic-export-set");
|
|
pos.exportSignaturesHash = makeHash("diagnostic-export-signatures");
|
|
pos.observedParticipantsHash = ce.observedParticipantsHash();
|
|
pos.myCommitment = makeHash("diagnostic-commitment");
|
|
pos.myReveal = makeHash("diagnostic-reveal");
|
|
ce.logPosition(pos, activeNoopJournal());
|
|
}
|
|
|
|
void
|
|
testDecoratePositionSkipsWhenDisabled()
|
|
{
|
|
testcase("decoratePosition skips without amendment");
|
|
|
|
//@@start test-decorate-position-disabled-legacy
|
|
using namespace jtx;
|
|
Env env{
|
|
*this, envconfig(validator, ""), supported_amendments(), nullptr};
|
|
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
|
|
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
ExtendedPosition pos{makeHash("decorate-position-tx-set")};
|
|
|
|
ce.decoratePosition(pos, ledger, true);
|
|
BEAST_EXPECT(!pos.myCommitment);
|
|
BEAST_EXPECT(ce.pendingCommitCount() == 0);
|
|
|
|
ExtendedPosition skipped{makeHash("decorate-position-skipped")};
|
|
ce.decoratePosition(skipped, ledger, false);
|
|
BEAST_EXPECT(!skipped.myCommitment);
|
|
//@@end test-decorate-position-disabled-legacy
|
|
}
|
|
|
|
void
|
|
testExportAdvancesDuringRngWaits()
|
|
{
|
|
testcase(
|
|
"Export advances during each RNG wait without accepting early");
|
|
using namespace std::chrono_literals;
|
|
for (auto mode : {ConsensusMode::proposing, ConsensusMode::observing})
|
|
{
|
|
for (auto state :
|
|
{EstablishState::ConvergingTx,
|
|
EstablishState::ConvergingCommit,
|
|
EstablishState::ConvergingReveal})
|
|
{
|
|
FakeExtensions ext;
|
|
ext.rngOn = true;
|
|
ext.estState_ = state;
|
|
ext.commitQuorum = false;
|
|
ext.commits = ext.proofedCommits = 0;
|
|
ext.minimumReveals = ext.anyReveals = false;
|
|
ExtensionTickHarness h;
|
|
h.mode = mode;
|
|
for (std::uint8_t i = 1; i <= 4; ++i)
|
|
h.addBothPeer(i, ext);
|
|
|
|
BEAST_EXPECT(!h.tick(ext).readyForAccept);
|
|
BEAST_EXPECT(h.position.exportSigSetHash == ext.exportHash);
|
|
BEAST_EXPECT(ext.exportSigGateStarted_);
|
|
BEAST_EXPECT(!ext.acceptedExportHash);
|
|
BEAST_EXPECT(!h.tick(ext, 100ms).readyForAccept);
|
|
BEAST_EXPECT(ext.acceptedExportHash == ext.exportHash);
|
|
BEAST_EXPECT(!ext.acceptedEntropyHash);
|
|
|
|
ext.commitQuorum = true;
|
|
ext.commits = ext.proofedCommits = 4;
|
|
ext.minimumReveals = ext.anyReveals = true;
|
|
bool ready = false;
|
|
for (int tick = 2; tick <= 5 && !ready; ++tick)
|
|
ready = h.tick(ext, tick * 100ms).readyForAccept;
|
|
BEAST_EXPECT(ready);
|
|
BEAST_EXPECT(ext.acceptedExportHash == ext.exportHash);
|
|
BEAST_EXPECT(ext.acceptedEntropyHash == ext.entropyHash);
|
|
BEAST_EXPECT(h.position.exportSigSetHash == ext.exportHash);
|
|
BEAST_EXPECT(h.position.entropySetHash == ext.entropyHash);
|
|
if (mode == ConsensusMode::observing)
|
|
BEAST_EXPECT(h.proposes == 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
testExportExpiryDoesNotEndRngWait()
|
|
{
|
|
testcase("Export expires independently while RNG continues");
|
|
using namespace std::chrono_literals;
|
|
FakeExtensions ext;
|
|
ext.rngOn = true;
|
|
ext.commitQuorum = false;
|
|
ext.commits = ext.proofedCommits = 0;
|
|
ext.minimumReveals = ext.anyReveals = false;
|
|
ext.localExportSigs = false;
|
|
ext.livePendingExportLatches = true;
|
|
ExtensionTickHarness h;
|
|
for (std::uint8_t i = 1; i <= 4; ++i)
|
|
h.addBothPeer(i, ext);
|
|
auto const deadline = detail::sidecarConvergenceTimeout(h.parms);
|
|
BEAST_EXPECT(!h.tick(ext).readyForAccept);
|
|
BEAST_EXPECT(ext.exportSigGateStarted_);
|
|
auto const start = ext.exportSigGateStart_;
|
|
|
|
ext.commitQuorum = true;
|
|
ext.commits = ext.proofedCommits = 4;
|
|
BEAST_EXPECT(!h.tick(ext, deadline / 2).readyForAccept);
|
|
BEAST_EXPECT(!h.tick(ext, deadline / 2 + 1ms).readyForAccept);
|
|
BEAST_EXPECT(!h.tick(ext, deadline + 1ms).readyForAccept);
|
|
BEAST_EXPECT(ext.exportSigConvergenceFailed_);
|
|
BEAST_EXPECT(!ext.acceptedExportHash);
|
|
BEAST_EXPECT(!ext.acceptedEntropyHash);
|
|
|
|
ext.localExportSigs = true;
|
|
ext.minimumReveals = ext.anyReveals = true;
|
|
BEAST_EXPECT(!h.tick(ext, deadline + 2ms).readyForAccept);
|
|
BEAST_EXPECT(h.tick(ext, deadline + 3ms).readyForAccept);
|
|
BEAST_EXPECT(ext.exportSigConvergenceFailed_);
|
|
BEAST_EXPECT(ext.exportSigGateStart_ == start);
|
|
BEAST_EXPECT(!ext.acceptedExportHash);
|
|
BEAST_EXPECT(!h.position.exportSigSetHash);
|
|
BEAST_EXPECT(ext.acceptedEntropyHash == ext.entropyHash);
|
|
}
|
|
|
|
void
|
|
testRngFallbackPreservesAlignedExport()
|
|
{
|
|
testcase("RNG fallback does not suppress concurrently aligned Export");
|
|
using namespace std::chrono_literals;
|
|
FakeExtensions ext;
|
|
ext.rngOn = true;
|
|
ext.commitQuorum = false;
|
|
ext.commits = ext.proofedCommits = 0;
|
|
ExtensionTickHarness h;
|
|
for (std::uint8_t i = 1; i <= 4; ++i)
|
|
h.addBothPeer(i, ext);
|
|
BEAST_EXPECT(!h.tick(ext).readyForAccept);
|
|
BEAST_EXPECT(!h.tick(ext, 100ms).readyForAccept);
|
|
BEAST_EXPECT(ext.acceptedExportHash == ext.exportHash);
|
|
BEAST_EXPECT(
|
|
h.tick(ext, h.parms.rngPIPELINE_TIMEOUT + 1ms).readyForAccept);
|
|
BEAST_EXPECT(ext.acceptedExportHash == ext.exportHash);
|
|
BEAST_EXPECT(!ext.acceptedEntropyHash);
|
|
}
|
|
|
|
void
|
|
testParallelGateRechecksOrdinarySet()
|
|
{
|
|
testcase("early Export alignment cannot bypass a changed ordinary set");
|
|
using namespace std::chrono_literals;
|
|
FakeExtensions ext;
|
|
ext.rngOn = true;
|
|
ext.commitQuorum = false;
|
|
ext.commits = ext.proofedCommits = 0;
|
|
ExtensionTickHarness h;
|
|
for (std::uint8_t i = 1; i <= 4; ++i)
|
|
h.addBothPeer(i, ext);
|
|
BEAST_EXPECT(!h.tick(ext).readyForAccept);
|
|
BEAST_EXPECT(!h.tick(ext, 100ms).readyForAccept);
|
|
BEAST_EXPECT(ext.acceptedExportHash == ext.exportHash);
|
|
auto const start = ext.exportSigGateStart_;
|
|
|
|
h.position.txSetHash = makeHash("changed-ordinary-set");
|
|
h.txns.hash = h.position.txSetHash;
|
|
h.position.entropySetHash = ext.entropyHash;
|
|
ext.estState_ = EstablishState::ConvergingReveal;
|
|
ext.entropySetPublished_ = true;
|
|
h.peers.clear();
|
|
for (std::uint8_t i = 1; i <= 4; ++i)
|
|
h.addEntropyPeer(i, ext.entropyHash, h.position.txSetHash);
|
|
BEAST_EXPECT(!h.tick(ext, 200ms).readyForAccept);
|
|
BEAST_EXPECT(ext.acceptedEntropyHash == ext.entropyHash);
|
|
BEAST_EXPECT(ext.exportSigGateStart_ == start);
|
|
|
|
h.peers.clear();
|
|
for (std::uint8_t i = 1; i <= 4; ++i)
|
|
h.addBothPeer(i, ext);
|
|
BEAST_EXPECT(h.tick(ext, 300ms).readyForAccept);
|
|
BEAST_EXPECT(ext.acceptedExportHash == ext.exportHash);
|
|
BEAST_EXPECT(ext.acceptedEntropyHash == ext.entropyHash);
|
|
}
|
|
|
|
void
|
|
testExportSigGateRequiresQuorumAlignment()
|
|
{
|
|
testcase("Export sig gate requires quorum alignment");
|
|
|
|
FakeExtensions ext;
|
|
ExtensionTickHarness harness;
|
|
auto const localHash = ext.exportHash;
|
|
|
|
harness.addPeer(1, localHash);
|
|
harness.addPeer(2, localHash);
|
|
|
|
auto result = harness.tick(ext);
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
BEAST_EXPECT(harness.position.exportSigSetHash == localHash);
|
|
BEAST_EXPECT(ext.exportSigGateStarted_);
|
|
|
|
result = harness.tick(ext, std::chrono::milliseconds{100});
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
BEAST_EXPECT(!ext.exportSigConvergenceFailed_);
|
|
|
|
result = harness.tick(
|
|
ext,
|
|
harness.parms.rngREVEAL_TIMEOUT * 2 + std::chrono::milliseconds{1});
|
|
BEAST_EXPECT(result.readyForAccept);
|
|
BEAST_EXPECT(ext.exportSigConvergenceFailed_);
|
|
}
|
|
|
|
void
|
|
testRngEntropyGateAllowsQuorumDespiteMissingObservation()
|
|
{
|
|
testcase("RNG entropy gate allows quorum despite missing observation");
|
|
|
|
FakeExtensions ext;
|
|
ext.rngOn = true;
|
|
ext.exportOn = false;
|
|
ext.estState_ = EstablishState::ConvergingReveal;
|
|
|
|
ExtensionTickHarness harness;
|
|
auto const localHash = ext.entropyHash;
|
|
|
|
harness.addEntropyPeer(1, localHash);
|
|
harness.addEntropyPeer(2, localHash);
|
|
harness.addEntropyPeer(3, localHash);
|
|
harness.addEntropyPeer(4, std::nullopt);
|
|
|
|
auto result = harness.tick(ext);
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
BEAST_EXPECT(harness.position.entropySetHash == localHash);
|
|
BEAST_EXPECT(ext.entropySetPublished_);
|
|
|
|
// A silent peer did not advertise a conflicting value; once a quorum
|
|
// has signed the same entropySetHash, waiting for that peer only gives
|
|
// it a veto over otherwise healthy validator entropy.
|
|
result = harness.tick(ext, std::chrono::milliseconds{100});
|
|
BEAST_EXPECT(result.readyForAccept);
|
|
BEAST_EXPECT(!ext.entropyFailed);
|
|
BEAST_EXPECT(harness.position.entropySetHash == localHash);
|
|
}
|
|
|
|
void
|
|
testRngEntropyGateDoesNotCountUnpublishedObserverRoot()
|
|
{
|
|
testcase("RNG entropy gate excludes unpublished observer root");
|
|
|
|
FakeExtensions ext;
|
|
ext.rngOn = true;
|
|
ext.exportOn = false;
|
|
ext.estState_ = EstablishState::ConvergingReveal;
|
|
|
|
ExtensionTickHarness harness;
|
|
harness.mode = ConsensusMode::observing;
|
|
|
|
auto result = harness.tick(ext);
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
BEAST_EXPECT(harness.position.entropySetHash == ext.entropyHash);
|
|
BEAST_EXPECT(harness.proposes == 0);
|
|
|
|
harness.addEntropyPeer(1, ext.entropyHash);
|
|
harness.addEntropyPeer(2, ext.entropyHash);
|
|
harness.addEntropyPeer(3, ext.entropyHash);
|
|
result = harness.tick(ext, std::chrono::milliseconds{100});
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
}
|
|
|
|
void
|
|
testRngEntropyConflictAllowsQuorumDespiteMissingObservation()
|
|
{
|
|
testcase(
|
|
"RNG entropy conflict allows quorum despite missing observation");
|
|
|
|
FakeExtensions ext;
|
|
ext.rngOn = true;
|
|
ext.exportOn = false;
|
|
ext.estState_ = EstablishState::ConvergingReveal;
|
|
|
|
ExtensionTickHarness harness;
|
|
auto const localHash = ext.entropyHash;
|
|
auto const conflictingHash = makeHash("conflicting-entropy-set");
|
|
|
|
harness.addEntropyPeer(1, localHash);
|
|
harness.addEntropyPeer(2, localHash);
|
|
harness.addEntropyPeer(3, localHash);
|
|
harness.addEntropyPeer(4, conflictingHash);
|
|
harness.addEntropyPeer(5, std::nullopt);
|
|
|
|
auto result = harness.tick(ext);
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
BEAST_EXPECT(harness.position.entropySetHash == localHash);
|
|
BEAST_EXPECT(ext.entropySetPublished_);
|
|
|
|
// A quorum-aligned hash is unique over the fixed active-view
|
|
// denominator. A below-quorum conflicting minority plus a silent peer
|
|
// must not veto otherwise healthy validator entropy.
|
|
result = harness.tick(ext, std::chrono::milliseconds{100});
|
|
BEAST_EXPECT(result.readyForAccept);
|
|
BEAST_EXPECT(!ext.entropyFailed);
|
|
BEAST_EXPECT(harness.position.entropySetHash == localHash);
|
|
}
|
|
|
|
void
|
|
testRngFastPathWaitsAfterEntropyPublish()
|
|
{
|
|
testcase("RNG fast path waits after entropy publish");
|
|
|
|
FakeExtensions ext;
|
|
ext.rngOn = true;
|
|
ext.exportOn = false;
|
|
ext.estState_ = EstablishState::ConvergingCommit;
|
|
|
|
ExtensionTickHarness harness;
|
|
auto const localHash = ext.entropyHash;
|
|
|
|
harness.addEntropyPeer(1, localHash);
|
|
harness.addEntropyPeer(2, localHash);
|
|
harness.addEntropyPeer(3, localHash);
|
|
harness.addEntropyPeer(4, localHash);
|
|
|
|
auto result = harness.tick(ext);
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
BEAST_EXPECT(ext.estState_ == EstablishState::ConvergingReveal);
|
|
BEAST_EXPECT(ext.entropySetPublished_);
|
|
BEAST_EXPECT(harness.position.entropySetHash == localHash);
|
|
|
|
result = harness.tick(ext, std::chrono::milliseconds{100});
|
|
BEAST_EXPECT(result.readyForAccept);
|
|
BEAST_EXPECT(!ext.entropyFailed);
|
|
BEAST_EXPECT(harness.position.entropySetHash == localHash);
|
|
}
|
|
|
|
void
|
|
testRngPrevProposerUnderObservationDoesNotSuppressCommitQuorum()
|
|
{
|
|
testcase(
|
|
"RNG previous proposer under-observation does not suppress commit "
|
|
"quorum");
|
|
|
|
FakeExtensions ext;
|
|
ext.rngOn = true;
|
|
ext.exportOn = false;
|
|
|
|
ExtensionTickHarness harness;
|
|
harness.prevProposers = 2;
|
|
|
|
auto result = harness.tick(ext);
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
BEAST_EXPECT(ext.estState_ == EstablishState::ConvergingCommit);
|
|
BEAST_EXPECT(ext.commitBuilds == 1);
|
|
BEAST_EXPECT(harness.position.commitSetHash == ext.commitHash);
|
|
}
|
|
|
|
void
|
|
testRngCommitWaitsWhenQuorumPossible()
|
|
{
|
|
testcase("RNG commit waits when quorum is still possible");
|
|
|
|
FakeExtensions ext;
|
|
ext.rngOn = true;
|
|
ext.exportOn = false;
|
|
ext.commitQuorum = false;
|
|
ext.commits = 2;
|
|
|
|
ExtensionTickHarness harness;
|
|
harness.addCommitPeer(1, std::nullopt);
|
|
harness.addCommitPeer(2, std::nullopt);
|
|
harness.addCommitPeer(3, std::nullopt);
|
|
|
|
auto result = harness.tick(ext, std::chrono::milliseconds{100});
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
BEAST_EXPECT(ext.estState_ == EstablishState::ConvergingTx);
|
|
BEAST_EXPECT(ext.commitBuilds == 0);
|
|
BEAST_EXPECT(harness.updates == 0);
|
|
}
|
|
|
|
void
|
|
testRngCommitTimeoutWithEntropyGatePublishesCommitSet()
|
|
{
|
|
testcase("RNG commit timeout with entropy gate publishes commit set");
|
|
|
|
FakeExtensions ext;
|
|
ext.rngOn = true;
|
|
ext.exportOn = false;
|
|
ext.commitQuorum = false;
|
|
ext.commits = 4;
|
|
|
|
ExtensionTickHarness harness;
|
|
harness.addCommitPeer(1, std::nullopt);
|
|
harness.addCommitPeer(2, std::nullopt);
|
|
harness.addCommitPeer(3, std::nullopt);
|
|
|
|
auto result = harness.tick(
|
|
ext,
|
|
harness.parms.rngPIPELINE_TIMEOUT + std::chrono::milliseconds{1});
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
BEAST_EXPECT(ext.estState_ == EstablishState::ConvergingCommit);
|
|
BEAST_EXPECT(ext.commitBuilds == 1);
|
|
BEAST_EXPECT(harness.position.commitSetHash == ext.commitHash);
|
|
BEAST_EXPECT(harness.updates == 1);
|
|
BEAST_EXPECT(harness.proposes == 1);
|
|
}
|
|
|
|
void
|
|
testRngCommitTimeoutUsesProofedCommitsNotVisiblePeers()
|
|
{
|
|
testcase(
|
|
"RNG commit timeout uses proofed commits, not visible peer "
|
|
"count");
|
|
|
|
FakeExtensions ext;
|
|
ext.rngOn = true;
|
|
ext.exportOn = false;
|
|
ext.commitQuorum = false;
|
|
ext.commits = 4;
|
|
|
|
ExtensionTickHarness harness;
|
|
|
|
auto result = harness.tick(
|
|
ext,
|
|
harness.parms.rngPIPELINE_TIMEOUT + std::chrono::milliseconds{1});
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
BEAST_EXPECT(ext.estState_ == EstablishState::ConvergingCommit);
|
|
BEAST_EXPECT(ext.commitBuilds == 1);
|
|
BEAST_EXPECT(harness.position.commitSetHash == ext.commitHash);
|
|
BEAST_EXPECT(harness.updates == 1);
|
|
BEAST_EXPECT(harness.proposes == 1);
|
|
}
|
|
|
|
void
|
|
testRngCommitTimeoutRejectsUnproofedCommits()
|
|
{
|
|
testcase("RNG commit timeout rejects unproofed commits");
|
|
|
|
FakeExtensions ext;
|
|
ext.rngOn = true;
|
|
ext.exportOn = false;
|
|
ext.commitQuorum = false;
|
|
ext.commits = 4;
|
|
ext.proofedCommits = 3;
|
|
|
|
ExtensionTickHarness harness;
|
|
|
|
auto result = harness.tick(
|
|
ext,
|
|
harness.parms.rngPIPELINE_TIMEOUT + std::chrono::milliseconds{1});
|
|
BEAST_EXPECT(result.readyForAccept);
|
|
BEAST_EXPECT(ext.estState_ == EstablishState::ConvergingTx);
|
|
BEAST_EXPECT(ext.commitBuilds == 0);
|
|
BEAST_EXPECT(!harness.position.commitSetHash);
|
|
}
|
|
|
|
void
|
|
testRngCommitQuorumInObservingModeDoesNotPropose()
|
|
{
|
|
testcase("RNG commit quorum in observing mode does not propose");
|
|
|
|
FakeExtensions ext;
|
|
ext.rngOn = true;
|
|
ext.exportOn = false;
|
|
|
|
ExtensionTickHarness harness;
|
|
harness.mode = ConsensusMode::observing;
|
|
|
|
auto result = harness.tick(ext);
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
BEAST_EXPECT(ext.estState_ == EstablishState::ConvergingCommit);
|
|
BEAST_EXPECT(harness.position.commitSetHash == ext.commitHash);
|
|
BEAST_EXPECT(harness.updates == 1);
|
|
BEAST_EXPECT(harness.proposes == 0);
|
|
BEAST_EXPECT(ext.participantDiagnostics == 1);
|
|
}
|
|
|
|
void
|
|
testRngCommitConflictRefreshesHashBeforeWaiting()
|
|
{
|
|
testcase("RNG commit conflict refreshes hash before waiting");
|
|
|
|
FakeExtensions ext;
|
|
ext.rngOn = true;
|
|
ext.exportOn = false;
|
|
ext.estState_ = EstablishState::ConvergingCommit;
|
|
auto const staleHash = makeHash("stale-commit-set");
|
|
auto const refreshedHash = makeHash("refreshed-commit-set");
|
|
auto const conflictHash = makeHash("conflicting-commit-set");
|
|
ext.commitHashSequence.push_back(refreshedHash);
|
|
|
|
ExtensionTickHarness harness;
|
|
harness.start =
|
|
std::chrono::steady_clock::time_point{} + std::chrono::seconds{1};
|
|
harness.position.commitSetHash = staleHash;
|
|
harness.addCommitPeer(1, conflictHash);
|
|
|
|
auto result = harness.tick(ext);
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
BEAST_EXPECT(ext.estState_ == EstablishState::ConvergingCommit);
|
|
BEAST_EXPECT(ext.commitBuilds == 1);
|
|
BEAST_EXPECT(harness.position.commitSetHash == refreshedHash);
|
|
BEAST_EXPECT(harness.updates == 1);
|
|
BEAST_EXPECT(harness.proposes == 1);
|
|
BEAST_EXPECT(ext.commitHashConflictStart_ == harness.start);
|
|
}
|
|
|
|
void
|
|
testRngCommitHashConflictTimeoutPublishesReveal()
|
|
{
|
|
testcase("RNG commit hash conflict timeout publishes reveal");
|
|
|
|
FakeExtensions ext;
|
|
ext.rngOn = true;
|
|
ext.exportOn = false;
|
|
ext.estState_ = EstablishState::ConvergingCommit;
|
|
ext.commitHash = makeHash("commit-local");
|
|
ext.minimumReveals = false;
|
|
|
|
ExtensionTickHarness harness;
|
|
harness.start =
|
|
std::chrono::steady_clock::time_point{} + std::chrono::seconds{1};
|
|
harness.position.commitSetHash = ext.commitHash;
|
|
auto const conflictHash = makeHash("commit-conflict");
|
|
harness.addCommitPeer(1, conflictHash);
|
|
|
|
auto result = harness.tick(ext);
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
BEAST_EXPECT(!ext.entropyFailed);
|
|
BEAST_EXPECT(ext.commitHashConflictStart_ == harness.start);
|
|
|
|
result = harness.tick(
|
|
ext,
|
|
harness.parms.rngREVEAL_TIMEOUT + std::chrono::milliseconds{1});
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
BEAST_EXPECT(!ext.entropyFailed);
|
|
BEAST_EXPECT(ext.estState_ == EstablishState::ConvergingReveal);
|
|
BEAST_EXPECT(ext.commitFrozen);
|
|
BEAST_EXPECT(ext.selfSeeds == 1);
|
|
BEAST_EXPECT(harness.position.myReveal == ext.getEntropySecret());
|
|
BEAST_EXPECT(!harness.position.entropySetHash);
|
|
BEAST_EXPECT(ext.entropyBuilds == 0);
|
|
BEAST_EXPECT(harness.updates == 1);
|
|
BEAST_EXPECT(harness.proposes == 1);
|
|
BEAST_EXPECT(
|
|
ext.revealPhaseStart_ ==
|
|
harness.start + harness.parms.rngREVEAL_TIMEOUT +
|
|
std::chrono::milliseconds{1});
|
|
BEAST_EXPECT(
|
|
ext.commitHashConflictStart_ ==
|
|
std::chrono::steady_clock::time_point{});
|
|
}
|
|
|
|
void
|
|
testRngRevealTransitionWaitsWhenRevealsIncomplete()
|
|
{
|
|
testcase("RNG reveal transition waits when reveals are incomplete");
|
|
|
|
FakeExtensions ext;
|
|
ext.rngOn = true;
|
|
ext.exportOn = false;
|
|
ext.estState_ = EstablishState::ConvergingCommit;
|
|
ext.minimumReveals = false;
|
|
ext.reveals = 1;
|
|
|
|
ExtensionTickHarness harness;
|
|
harness.position.commitSetHash = ext.commitHash;
|
|
|
|
auto result = harness.tick(ext);
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
BEAST_EXPECT(ext.estState_ == EstablishState::ConvergingReveal);
|
|
BEAST_EXPECT(ext.commitFrozen);
|
|
BEAST_EXPECT(ext.selfSeeds == 1);
|
|
BEAST_EXPECT(ext.entropyBuilds == 0);
|
|
BEAST_EXPECT(harness.position.myReveal);
|
|
BEAST_EXPECT(!harness.position.entropySetHash);
|
|
BEAST_EXPECT(harness.updates == 1);
|
|
BEAST_EXPECT(harness.proposes == 1);
|
|
}
|
|
|
|
void
|
|
testRngRevealTimeoutWithoutRevealsFallsBack()
|
|
{
|
|
testcase("RNG reveal timeout without reveals falls back");
|
|
|
|
FakeExtensions ext;
|
|
ext.rngOn = true;
|
|
ext.exportOn = false;
|
|
ext.estState_ = EstablishState::ConvergingReveal;
|
|
ext.minimumReveals = false;
|
|
ext.anyReveals = false;
|
|
|
|
ExtensionTickHarness harness;
|
|
|
|
auto result = harness.tick(
|
|
ext,
|
|
harness.parms.rngREVEAL_TIMEOUT + std::chrono::milliseconds{1});
|
|
BEAST_EXPECT(result.readyForAccept);
|
|
BEAST_EXPECT(ext.entropyFailed);
|
|
BEAST_EXPECT(!harness.position.entropySetHash);
|
|
}
|
|
|
|
void
|
|
testRngEntropyPublishIsIdempotent()
|
|
{
|
|
testcase("RNG entropy publish is idempotent");
|
|
|
|
FakeExtensions ext;
|
|
ext.rngOn = true;
|
|
ext.exportOn = false;
|
|
ext.estState_ = EstablishState::ConvergingReveal;
|
|
|
|
ExtensionTickHarness harness;
|
|
harness.position.entropySetHash = ext.entropyHash;
|
|
|
|
auto result = harness.tick(ext);
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
BEAST_EXPECT(ext.entropyBuilds == 1);
|
|
BEAST_EXPECT(ext.entropySetPublished_);
|
|
BEAST_EXPECT(harness.position.entropySetHash == ext.entropyHash);
|
|
BEAST_EXPECT(harness.updates == 0);
|
|
BEAST_EXPECT(harness.proposes == 0);
|
|
}
|
|
|
|
void
|
|
testRngEntropyConflictTimeoutClearsHash()
|
|
{
|
|
testcase("RNG entropy conflict timeout clears hash");
|
|
|
|
FakeExtensions ext;
|
|
ext.rngOn = true;
|
|
ext.exportOn = false;
|
|
ext.estState_ = EstablishState::ConvergingReveal;
|
|
|
|
ExtensionTickHarness harness;
|
|
harness.position.entropySetHash = ext.entropyHash;
|
|
ext.entropySetPublished_ = true;
|
|
ext.entropyPublishStart_ = harness.start;
|
|
auto const conflictHash = makeHash("entropy-conflict");
|
|
harness.addEntropyPeer(1, conflictHash);
|
|
|
|
auto result = harness.tick(ext, std::chrono::milliseconds{100});
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
BEAST_EXPECT(!ext.entropyFailed);
|
|
BEAST_EXPECT(harness.position.entropySetHash == ext.entropyHash);
|
|
|
|
result = harness.tick(
|
|
ext,
|
|
harness.parms.rngREVEAL_TIMEOUT * 2 + std::chrono::milliseconds{1});
|
|
BEAST_EXPECT(result.readyForAccept);
|
|
BEAST_EXPECT(ext.entropyFailed);
|
|
BEAST_EXPECT(!harness.position.entropySetHash);
|
|
}
|
|
|
|
void
|
|
testRngEntropyConflictRefreshesHashBeforeWaiting()
|
|
{
|
|
testcase("RNG entropy conflict refreshes hash before waiting");
|
|
|
|
FakeExtensions ext;
|
|
ext.rngOn = true;
|
|
ext.exportOn = false;
|
|
ext.estState_ = EstablishState::ConvergingReveal;
|
|
auto const staleHash = makeHash("stale-entropy-set");
|
|
auto const refreshedHash = makeHash("refreshed-entropy-set");
|
|
auto const conflictHash = makeHash("conflicting-entropy-set");
|
|
ext.entropyHashSequence.push_back(staleHash);
|
|
ext.entropyHashSequence.push_back(refreshedHash);
|
|
|
|
ExtensionTickHarness harness;
|
|
harness.start =
|
|
std::chrono::steady_clock::time_point{} + std::chrono::seconds{1};
|
|
harness.position.entropySetHash = staleHash;
|
|
ext.entropySetPublished_ = true;
|
|
ext.entropyPublishStart_ = harness.start;
|
|
harness.addEntropyPeer(1, conflictHash);
|
|
|
|
auto result = harness.tick(ext, std::chrono::milliseconds{100});
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
BEAST_EXPECT(!ext.entropyFailed);
|
|
BEAST_EXPECT(ext.entropyBuilds == 2);
|
|
BEAST_EXPECT(harness.position.entropySetHash == refreshedHash);
|
|
BEAST_EXPECT(harness.updates == 1);
|
|
BEAST_EXPECT(harness.proposes == 1);
|
|
}
|
|
|
|
void
|
|
testRngEntropyConflictIgnoredWithQuorumAlignment()
|
|
{
|
|
testcase("RNG entropy conflict ignored with quorum alignment");
|
|
|
|
FakeExtensions ext;
|
|
ext.rngOn = true;
|
|
ext.exportOn = false;
|
|
ext.estState_ = EstablishState::ConvergingReveal;
|
|
|
|
ExtensionTickHarness harness;
|
|
auto const localHash = ext.entropyHash;
|
|
harness.position.entropySetHash = localHash;
|
|
ext.entropySetPublished_ = true;
|
|
ext.entropyPublishStart_ = harness.start;
|
|
|
|
harness.addEntropyPeer(1, localHash);
|
|
harness.addEntropyPeer(2, localHash);
|
|
harness.addEntropyPeer(3, localHash);
|
|
harness.addEntropyPeer(4, makeHash("entropy-minority-conflict"));
|
|
|
|
auto result = harness.tick(ext, std::chrono::milliseconds{100});
|
|
BEAST_EXPECT(result.readyForAccept);
|
|
BEAST_EXPECT(!ext.entropyFailed);
|
|
BEAST_EXPECT(harness.position.entropySetHash == localHash);
|
|
}
|
|
|
|
void
|
|
testExportSigGateAllowsAlignedQuorumDespiteMinorityConflict()
|
|
{
|
|
testcase("Export sig gate ignores minority conflict after quorum");
|
|
|
|
FakeExtensions ext;
|
|
ExtensionTickHarness harness;
|
|
auto const localHash = ext.exportHash;
|
|
auto const conflictHash = makeHash("conflicting-export-sig-set");
|
|
|
|
harness.addPeer(1, localHash);
|
|
harness.addPeer(2, localHash);
|
|
harness.addPeer(3, localHash);
|
|
harness.addPeer(4, conflictHash);
|
|
|
|
auto result = harness.tick(ext);
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
|
|
result = harness.tick(ext, std::chrono::milliseconds{100});
|
|
BEAST_EXPECT(result.readyForAccept);
|
|
BEAST_EXPECT(!ext.exportSigConvergenceFailed_);
|
|
}
|
|
|
|
void
|
|
testExportSigGateAllowsQuorumDespiteMissingObservation()
|
|
{
|
|
testcase(
|
|
"Export sig gate allows quorum despite missing sidecar "
|
|
"observation");
|
|
|
|
FakeExtensions ext;
|
|
ExtensionTickHarness harness;
|
|
auto const localHash = ext.exportHash;
|
|
|
|
harness.addPeer(1, localHash);
|
|
harness.addPeer(2, localHash);
|
|
harness.addPeer(3, localHash);
|
|
harness.addPeer(4, std::nullopt);
|
|
|
|
auto result = harness.tick(ext);
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
BEAST_EXPECT(harness.position.exportSigSetHash == localHash);
|
|
BEAST_EXPECT(ext.exportSigGateStarted_);
|
|
|
|
// A quorum-aligned signed exportSigSetHash is enough even if a
|
|
// tx-converged minority peer has not advertised any exportSigSetHash.
|
|
result = harness.tick(ext, std::chrono::milliseconds{100});
|
|
BEAST_EXPECT(result.readyForAccept);
|
|
BEAST_EXPECT(!ext.exportSigConvergenceFailed_);
|
|
}
|
|
|
|
void
|
|
testExportSigGateObservesAdvertisedPeerSets()
|
|
{
|
|
testcase("Export sig gate observes advertised peer sets");
|
|
|
|
FakeExtensions ext;
|
|
ext.localExportSigs = false;
|
|
ext.livePendingExportLatches = true;
|
|
ExtensionTickHarness harness;
|
|
auto const peerHash = makeHash("peer-export-sig-set");
|
|
|
|
harness.addPeer(1, peerHash);
|
|
|
|
auto result = harness.tick(ext);
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
BEAST_EXPECT(ext.exportSigGateStarted_);
|
|
BEAST_EXPECT(!harness.position.exportSigSetHash);
|
|
|
|
result = harness.tick(
|
|
ext,
|
|
harness.parms.rngREVEAL_TIMEOUT * 2 + std::chrono::milliseconds{1});
|
|
BEAST_EXPECT(result.readyForAccept);
|
|
BEAST_EXPECT(ext.exportSigConvergenceFailed_);
|
|
}
|
|
|
|
void
|
|
testExportSigGateIgnoresAdvertisedSetsWithoutExportTxns()
|
|
{
|
|
testcase("Export sig gate ignores advertised sets without export txns");
|
|
|
|
FakeExtensions ext;
|
|
ext.localExportSigs = false;
|
|
ext.livePendingExportLatches = false;
|
|
ExtensionTickHarness harness;
|
|
auto const peerHash = makeHash("empty-round-export-sig-set");
|
|
|
|
harness.addPeer(1, peerHash);
|
|
|
|
auto result = harness.tick(ext);
|
|
BEAST_EXPECT(result.readyForAccept);
|
|
BEAST_EXPECT(!ext.exportSigGateStarted_);
|
|
BEAST_EXPECT(!ext.exportSigConvergenceFailed_);
|
|
}
|
|
|
|
void
|
|
testExportSigGateObservingModeDoesNotPropose()
|
|
{
|
|
testcase("Export sig gate observing mode does not propose");
|
|
|
|
FakeExtensions ext;
|
|
ExtensionTickHarness harness;
|
|
harness.mode = ConsensusMode::observing;
|
|
|
|
auto result = harness.tick(ext);
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
BEAST_EXPECT(harness.position.exportSigSetHash == ext.exportHash);
|
|
BEAST_EXPECT(harness.updates == 1);
|
|
BEAST_EXPECT(harness.proposes == 0);
|
|
|
|
harness.addPeer(1, ext.exportHash);
|
|
harness.addPeer(2, ext.exportHash);
|
|
harness.addPeer(3, ext.exportHash);
|
|
result = harness.tick(ext, std::chrono::milliseconds{100});
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
|
|
// An observer's unpublished local root does not count, but a complete
|
|
// peer-only qV can still authorize materialization of its matching
|
|
// locally held candidate.
|
|
harness.addPeer(4, ext.exportHash);
|
|
result = harness.tick(ext, std::chrono::milliseconds{200});
|
|
BEAST_EXPECT(result.readyForAccept);
|
|
BEAST_EXPECT(!ext.exportSigConvergenceFailed_);
|
|
BEAST_EXPECT(harness.proposes == 0);
|
|
}
|
|
|
|
void
|
|
testExportSigGateRefreshesHashBeforeWaiting()
|
|
{
|
|
testcase("Export sig gate refreshes hash before waiting");
|
|
|
|
FakeExtensions ext;
|
|
auto const staleHash = makeHash("stale-export-sig-set");
|
|
auto const refreshedHash = makeHash("refreshed-export-sig-set");
|
|
auto const conflictHash = makeHash("conflicting-export-sig-set");
|
|
ext.exportHashSequence.push_back(staleHash);
|
|
ext.exportHashSequence.push_back(refreshedHash);
|
|
|
|
ExtensionTickHarness harness;
|
|
harness.start =
|
|
std::chrono::steady_clock::time_point{} + std::chrono::seconds{1};
|
|
harness.position.exportSigSetHash = staleHash;
|
|
ext.exportSigGateStarted_ = true;
|
|
ext.exportSigGateStart_ = harness.start;
|
|
harness.addPeer(1, conflictHash);
|
|
|
|
auto result = harness.tick(ext, std::chrono::milliseconds{100});
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
BEAST_EXPECT(!ext.exportSigConvergenceFailed_);
|
|
BEAST_EXPECT(ext.exportBuilds == 2);
|
|
BEAST_EXPECT(harness.position.exportSigSetHash == refreshedHash);
|
|
BEAST_EXPECT(harness.updates == 1);
|
|
BEAST_EXPECT(harness.proposes == 1);
|
|
}
|
|
|
|
void
|
|
testExportSigGateBoundsCandidateObservationWindow()
|
|
{
|
|
testcase("Export sig gate bounds candidate observation window");
|
|
|
|
FakeExtensions ext;
|
|
ext.localExportSigs = false;
|
|
ext.livePendingExportLatches = true;
|
|
ExtensionTickHarness harness;
|
|
|
|
auto result = harness.tick(ext);
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
BEAST_EXPECT(ext.exportSigGateStarted_);
|
|
BEAST_EXPECT(!harness.position.exportSigSetHash);
|
|
BEAST_EXPECT(!ext.exportSigConvergenceFailed_);
|
|
|
|
result = harness.tick(ext, std::chrono::milliseconds{100});
|
|
BEAST_EXPECT(!result.readyForAccept);
|
|
BEAST_EXPECT(!ext.exportSigConvergenceFailed_);
|
|
|
|
result = harness.tick(
|
|
ext,
|
|
harness.parms.rngREVEAL_TIMEOUT * 2 + std::chrono::milliseconds{1});
|
|
BEAST_EXPECT(result.readyForAccept);
|
|
BEAST_EXPECT(ext.exportSigConvergenceFailed_);
|
|
}
|
|
|
|
void
|
|
testExportSigGateDeadlineSurvivesRootChanges()
|
|
{
|
|
testcase("Export root changes cannot extend or reopen the wait window");
|
|
using namespace std::chrono_literals;
|
|
for (auto mode : {ConsensusMode::proposing, ConsensusMode::observing})
|
|
{
|
|
FakeExtensions ext;
|
|
ExtensionTickHarness harness;
|
|
harness.mode = mode;
|
|
auto const deadline = harness.parms.rngREVEAL_TIMEOUT * 2;
|
|
BEAST_EXPECT(!harness.tick(ext).readyForAccept);
|
|
|
|
// The existing deadline is inclusive. A changed root can still
|
|
// request observation at the boundary, but never after it.
|
|
ext.exportHash = makeHash("export-root-before-deadline");
|
|
BEAST_EXPECT(!harness.tick(ext, deadline - 1ms).readyForAccept);
|
|
ext.exportHash = makeHash("export-root-at-deadline");
|
|
BEAST_EXPECT(!harness.tick(ext, deadline).readyForAccept);
|
|
auto const start = ext.exportSigGateStart_;
|
|
auto const updates = harness.updates;
|
|
auto const proposes = harness.proposes;
|
|
ext.exportHash = makeHash("export-root-after-deadline");
|
|
BEAST_EXPECT(harness.tick(ext, deadline + 1ms).readyForAccept);
|
|
BEAST_EXPECT(ext.exportSigGateStart_ == start);
|
|
BEAST_EXPECT(harness.updates == updates);
|
|
BEAST_EXPECT(harness.proposes == proposes);
|
|
BEAST_EXPECT(ext.exportSigConvergenceFailed_);
|
|
BEAST_EXPECT(!ext.acceptedExportHash);
|
|
|
|
// Matching support arriving after an expiry decision must not
|
|
// reopen this round or install accepted evidence after omission.
|
|
for (std::uint8_t peer = 1; peer <= 4; ++peer)
|
|
harness.addPeer(peer, ext.exportHash);
|
|
BEAST_EXPECT(harness.tick(ext, deadline + 2ms).readyForAccept);
|
|
BEAST_EXPECT(ext.exportSigConvergenceFailed_);
|
|
BEAST_EXPECT(!ext.acceptedExportHash);
|
|
ext.exportHash = makeHash("export-root-after-expiry-decision");
|
|
BEAST_EXPECT(harness.tick(ext, deadline + 250ms).readyForAccept);
|
|
BEAST_EXPECT(!ext.acceptedExportHash);
|
|
if (mode == ConsensusMode::observing)
|
|
BEAST_EXPECT(harness.proposes == 0);
|
|
}
|
|
}
|
|
|
|
void
|
|
testExportSigGateLateFirstMaterial()
|
|
{
|
|
testcase("Export first material cannot restart an expired window");
|
|
using namespace std::chrono_literals;
|
|
for (bool peerAdvertised : {false, true})
|
|
{
|
|
FakeExtensions ext;
|
|
ext.localExportSigs = false;
|
|
ext.livePendingExportLatches = true;
|
|
ExtensionTickHarness harness;
|
|
if (peerAdvertised)
|
|
harness.addPeer(1, ext.exportHash);
|
|
BEAST_EXPECT(!harness.tick(ext).readyForAccept);
|
|
auto const start = ext.exportSigGateStart_;
|
|
auto const deadline = harness.parms.rngREVEAL_TIMEOUT * 2;
|
|
ext.localExportSigs = true;
|
|
BEAST_EXPECT(harness.tick(ext, deadline + 1ms).readyForAccept);
|
|
BEAST_EXPECT(ext.exportSigGateStart_ == start);
|
|
BEAST_EXPECT(ext.exportSigConvergenceFailed_);
|
|
BEAST_EXPECT(!ext.acceptedExportHash);
|
|
BEAST_EXPECT(!harness.position.exportSigSetHash);
|
|
}
|
|
|
|
// A stable, previously published root that aligns on the first late
|
|
// tick need not be discarded: the contract bounds further waiting,
|
|
// not evidence age. No earlier expiry decision exists in this case.
|
|
FakeExtensions ext;
|
|
ExtensionTickHarness harness;
|
|
BEAST_EXPECT(!harness.tick(ext).readyForAccept);
|
|
for (std::uint8_t peer = 1; peer <= 4; ++peer)
|
|
harness.addPeer(peer, ext.exportHash);
|
|
auto const deadline = harness.parms.rngREVEAL_TIMEOUT * 2;
|
|
BEAST_EXPECT(harness.tick(ext, deadline + 1ms).readyForAccept);
|
|
BEAST_EXPECT(!ext.exportSigConvergenceFailed_);
|
|
BEAST_EXPECT(ext.acceptedExportHash == ext.exportHash);
|
|
}
|
|
|
|
void
|
|
testExportSigGateSkipsWhenExportDisabled()
|
|
{
|
|
testcase("Export sig gate skips when Export disabled");
|
|
|
|
FakeExtensions ext;
|
|
ext.exportOn = false;
|
|
ExtensionTickHarness harness;
|
|
|
|
harness.addPeer(1, ext.exportHash);
|
|
|
|
auto result = harness.tick(ext);
|
|
BEAST_EXPECT(result.readyForAccept);
|
|
BEAST_EXPECT(!ext.exportSigGateStarted_);
|
|
BEAST_EXPECT(!harness.position.exportSigSetHash);
|
|
BEAST_EXPECT(ext.exportBuilds == 0);
|
|
}
|
|
|
|
void
|
|
testExportSigGateSkipsWithoutAnchoredView()
|
|
{
|
|
testcase("Export sig gate skips without ledger-anchored view");
|
|
|
|
FakeExtensions ext;
|
|
ext.exportViewAnchored = false;
|
|
ExtensionTickHarness harness;
|
|
|
|
harness.addPeer(1, ext.exportHash);
|
|
|
|
auto const result = harness.tick(ext);
|
|
BEAST_EXPECT(result.readyForAccept);
|
|
BEAST_EXPECT(!ext.exportSigGateStarted_);
|
|
BEAST_EXPECT(!harness.position.exportSigSetHash);
|
|
BEAST_EXPECT(ext.exportBuilds == 0);
|
|
BEAST_EXPECT(ext.acceptedExportClears == 1);
|
|
}
|
|
|
|
void
|
|
testParticipantDiagnosticsOnlyWhenExtensionEnabled()
|
|
{
|
|
testcase("Participant diagnostics only when extension enabled");
|
|
|
|
FakeExtensions ext;
|
|
ext.rngOn = false;
|
|
ext.exportOn = false;
|
|
ExtensionTickHarness harness;
|
|
|
|
auto result = harness.tick(ext);
|
|
BEAST_EXPECT(result.readyForAccept);
|
|
BEAST_EXPECT(ext.participantDiagnostics == 0);
|
|
|
|
ext.exportOn = true;
|
|
ext.localExportSigs = false;
|
|
result = harness.tick(ext);
|
|
BEAST_EXPECT(result.readyForAccept);
|
|
BEAST_EXPECT(ext.participantDiagnostics == 1);
|
|
}
|
|
|
|
void
|
|
testExportDisabledRoundClearsCollector()
|
|
{
|
|
testcase("Export disabled round clears collector");
|
|
|
|
using namespace jtx;
|
|
Env env{*this, envconfig(), supported_amendments(), nullptr};
|
|
ConsensusExtensions ce{env.app(), env.journal};
|
|
auto const origin = makeHash("export-disabled-clears-collector");
|
|
auto const pk = makeValidatorKeys().front();
|
|
std::uint8_t const sigBytes[] = {1, 2, 3};
|
|
Buffer const sig{sigBytes, sizeof(sigBytes)};
|
|
|
|
ce.setExportEnabledThisRound(true);
|
|
BEAST_EXPECT(
|
|
ce.postValidationExportSigCollector().registerOrigin(origin, 10));
|
|
auto admission =
|
|
ce.postValidationExportSigCollector().beginAttributedAdmission(
|
|
origin, ExportSigCollector::Contribution{0, pk, sig}, 10);
|
|
BEAST_EXPECT(admission.ticket);
|
|
if (admission.ticket)
|
|
ce.postValidationExportSigCollector().admitContribution(
|
|
std::move(*admission.ticket), true, 10);
|
|
ce.clearRngState();
|
|
BEAST_EXPECT(
|
|
ce.postValidationExportSigCollector().fullUnionSnapshot().size() ==
|
|
1);
|
|
|
|
ce.setExportEnabledThisRound(false);
|
|
ce.clearRngState();
|
|
BEAST_EXPECT(
|
|
ce.postValidationExportSigCollector().fullUnionSnapshot().empty());
|
|
}
|
|
|
|
void
|
|
testValidatorKeylessAuthoringNoops()
|
|
{
|
|
testcase("validator-keyless extension authoring no-ops");
|
|
|
|
using namespace jtx;
|
|
Env env{
|
|
*this,
|
|
envconfig(),
|
|
supported_amendments() | featureConsensusEntropy | featureExport,
|
|
nullptr};
|
|
auto const& valKeys = env.app().getValidatorKeys();
|
|
BEAST_EXPECT(!valKeys.keys);
|
|
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
|
|
|
|
ExtendedPosition position{makeHash("keyless-authoring")};
|
|
ce.decoratePosition(position, ledger, true);
|
|
BEAST_EXPECT(!position.myCommitment);
|
|
BEAST_EXPECT(ce.pendingCommitCount() == 0);
|
|
|
|
ce.generateEntropySecret();
|
|
ce.selfSeedReveal();
|
|
BEAST_EXPECT(ce.pendingRevealCount() == 0);
|
|
|
|
protocol::TMProposeSet prop;
|
|
RCLCxPeerPos::Proposal proposal{
|
|
ledger->info().hash,
|
|
0,
|
|
position,
|
|
NetClock::time_point{},
|
|
NetClock::time_point{},
|
|
beast::zero};
|
|
ce.setExportEnabledThisRound(true);
|
|
ce.attachExportSignatures(prop, proposal);
|
|
BEAST_EXPECT(prop.exportsignatures_size() == 0);
|
|
|
|
position.myCommitment = makeHash("keyless-commitment");
|
|
position.myReveal = makeHash("keyless-reveal");
|
|
ce.decorateMessage(prop, proposal, position, Buffer{});
|
|
BEAST_EXPECT(ce.pendingRevealCount() == 0);
|
|
BEAST_EXPECT(ce.pendingCommitCount() == 0);
|
|
}
|
|
|
|
void
|
|
runExportStreamRetainedReplayRace(bool replayWins)
|
|
{
|
|
using namespace std::chrono_literals;
|
|
using namespace jtx;
|
|
|
|
testcase(
|
|
replayWins ? "Export stream replay wins before an accepted edge"
|
|
: "Export stream accepted edge wins before replay");
|
|
|
|
auto config = envconfig(validator, "");
|
|
config->NETWORK_ID = 21337;
|
|
Env env{
|
|
*this,
|
|
std::move(config),
|
|
supported_amendments() | featureExport,
|
|
nullptr};
|
|
Account const alice{"alice"};
|
|
Account const carol{"carol"};
|
|
env.fund(XRP(1000), alice, carol);
|
|
env.close();
|
|
|
|
auto const& valKeys = env.app().getValidatorKeys();
|
|
if (!BEAST_EXPECT(valKeys.keys))
|
|
return;
|
|
|
|
env.app().openLedger().modify(
|
|
[&](OpenView& view, beast::Journal) -> bool {
|
|
STTx tx(ttUNL_REPORT, [&](auto& obj) {
|
|
obj.setFieldU32(sfLedgerSequence, env.current()->seq());
|
|
auto active = std::make_unique<STObject>(sfActiveValidator);
|
|
active->setFieldVL(
|
|
sfPublicKey, valKeys.keys->masterPublicKey);
|
|
obj.set(std::move(active));
|
|
});
|
|
auto const txID = tx.getTransactionID();
|
|
auto serializer = std::make_shared<Serializer>();
|
|
tx.add(*serializer);
|
|
env.app().getHashRouter().setFlags(txID, SF_PRIVATE2);
|
|
view.rawTxInsert(txID, std::move(serializer), nullptr);
|
|
return true;
|
|
});
|
|
if (!BEAST_EXPECT(env.close(
|
|
env.now() + std::chrono::seconds{5},
|
|
std::chrono::milliseconds{0})))
|
|
return;
|
|
forceNonStandalone(env.app());
|
|
|
|
auto& ledgerMaster = env.app().getLedgerMaster();
|
|
auto const universe = ledgerMaster.getClosedLedger();
|
|
if (!BEAST_EXPECT(universe && universe->read(keylet::UNLReport())))
|
|
return;
|
|
auto const committeeRoster =
|
|
serializeExportCommittee({valKeys.keys->masterPublicKey});
|
|
auto const committeeDigest =
|
|
exportCommitteeHash(makeSlice(committeeRoster));
|
|
|
|
auto installValidated = [&](std::shared_ptr<Ledger> const& ledger) {
|
|
ledgerMaster.storeLedger(ledger);
|
|
ledgerMaster.setFullLedger(ledger, false, false);
|
|
auto const current = ledgerMaster.getValidatedLedger();
|
|
BEAST_EXPECT(
|
|
current && current->info().seq == ledger->info().seq &&
|
|
current->info().hash == ledger->info().hash);
|
|
};
|
|
|
|
auto const originSeq = universe->info().seq + 1;
|
|
auto innerObj = makeExportedPayment(alice.id(), carol.id());
|
|
innerObj.setFieldU32(sfFirstLedgerSequence, originSeq);
|
|
innerObj.setFieldU32(sfLastLedgerSequence, originSeq + 5);
|
|
auto const innerTx = makeSTTx(innerObj);
|
|
|
|
Json::Value exportJson;
|
|
exportJson[jss::TransactionType] = jss::Export;
|
|
exportJson[jss::Account] = alice.human();
|
|
exportJson[jss::LastLedgerSequence] = originSeq;
|
|
exportJson[sfExportedTxn.jsonName] =
|
|
innerObj.getJson(JsonOptions::none);
|
|
exportJson[sfExportCommitteeHash.jsonName] = to_string(committeeDigest);
|
|
exportJson[sfExportCommittee.jsonName] = strHex(committeeRoster);
|
|
auto const exportTx =
|
|
env.jt(exportJson, fee(XRP(1)), ter(tesSUCCESS)).stx;
|
|
if (!BEAST_EXPECT(exportTx))
|
|
return;
|
|
|
|
CanonicalTXSet originTxs{universe->info().hash};
|
|
originTxs.insert(exportTx);
|
|
std::set<TxID> failed;
|
|
auto originLedger = buildLedger(
|
|
universe,
|
|
env.app().timeKeeper().closeTime(),
|
|
true,
|
|
universe->info().closeTimeResolution,
|
|
env.app(),
|
|
originTxs,
|
|
failed,
|
|
env.journal);
|
|
BEAST_EXPECT(originTxs.empty());
|
|
BEAST_EXPECT(failed.empty());
|
|
auto const origin = exportTx->getTransactionID();
|
|
auto const latchKey = keylet::exportLatch(alice.id(), origin);
|
|
auto const pendingLatch = originLedger->read(latchKey);
|
|
if (!BEAST_EXPECT(
|
|
pendingLatch && pendingLatch->isFieldPresent(sfExportNode) &&
|
|
!pendingLatch->isFieldPresent(sfExportSignatureHash) &&
|
|
!pendingLatch->isFieldPresent(sfExportCommittee)))
|
|
return;
|
|
|
|
auto release = ExportOriginMemo::releaseForm(
|
|
innerTx,
|
|
ExportOriginMemo::Origin{env.app().config().NETWORK_ID, 0, origin},
|
|
ExportOriginMemo::Anchor{
|
|
originLedger->info().seq, originLedger->info().hash});
|
|
if (!BEAST_EXPECT(release))
|
|
return;
|
|
auto const signature = ExportResultBuilder::signExportedTxn(
|
|
release.value(), valKeys.keys->publicKey, valKeys.keys->secretKey);
|
|
ExportShare const share{
|
|
ExportShare::currentVersion,
|
|
alice.id(),
|
|
origin,
|
|
originLedger->info().seq,
|
|
originLedger->info().hash,
|
|
0,
|
|
valKeys.keys->publicKey,
|
|
signature};
|
|
|
|
auto const foreignMasterSecret =
|
|
generateSecretKey(KeyType::secp256k1, randomSeed());
|
|
auto const foreignSigningSecret =
|
|
generateSecretKey(KeyType::secp256k1, randomSeed());
|
|
auto const foreignMasterKey =
|
|
derivePublicKey(KeyType::secp256k1, foreignMasterSecret);
|
|
auto const foreignSigningKey =
|
|
derivePublicKey(KeyType::secp256k1, foreignSigningSecret);
|
|
BEAST_EXPECT(
|
|
env.app().validatorManifests().applyManifest(makeValidatorManifest(
|
|
foreignMasterSecret, foreignSigningSecret)) ==
|
|
ManifestDisposition::accepted);
|
|
auto wrongSigner = share;
|
|
wrongSigner.signingKey = foreignSigningKey;
|
|
wrongSigner.signature = ExportResultBuilder::signExportedTxn(
|
|
release.value(), foreignSigningKey, foreignSigningSecret);
|
|
auto wrongMaster = share;
|
|
wrongMaster.signingKey = foreignMasterKey;
|
|
wrongMaster.signature = ExportResultBuilder::signExportedTxn(
|
|
release.value(), foreignMasterKey, foreignMasterSecret);
|
|
|
|
auto wsc = makeWSClient(env.app().config());
|
|
Json::Value stream;
|
|
stream[jss::streams] = Json::arrayValue;
|
|
stream[jss::streams].append("export_signatures");
|
|
BEAST_EXPECT(
|
|
wsc->invoke("subscribe", stream)[jss::status] == "success");
|
|
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
ce.startExportShareService();
|
|
|
|
auto hasRetainedContribution = [&] {
|
|
auto const snapshot =
|
|
ce.postValidationExportSigCollector().fullUnionSnapshot();
|
|
auto const found = snapshot.find(origin);
|
|
return found != snapshot.end() && found->second.size() == 1 &&
|
|
found->second.front().position == share.committeePosition &&
|
|
found->second.front().signingKey == share.signingKey &&
|
|
found->second.front().signature == share.signature;
|
|
};
|
|
auto expectNoShareEvent = [&] {
|
|
BEAST_EXPECT(!wsc->findMsg(250ms, [](Json::Value const& event) {
|
|
return event[jss::type] == "exportSignatureReceived";
|
|
}));
|
|
};
|
|
auto expectOneShareEvent = [&](Ledger const& cursor) {
|
|
auto const event =
|
|
wsc->findMsg(5s, [&](Json::Value const& message) {
|
|
return message[jss::type] == "exportSignatureReceived" &&
|
|
message[jss::origin_txid] == to_string(origin);
|
|
});
|
|
BEAST_EXPECT(event);
|
|
if (event)
|
|
{
|
|
BEAST_EXPECT((*event)[jss::stream] == "export_signatures");
|
|
BEAST_EXPECT(
|
|
(*event)[jss::ledger_index].asUInt() == cursor.info().seq);
|
|
BEAST_EXPECT(
|
|
(*event)[jss::ledger_hash] ==
|
|
to_string(cursor.info().hash));
|
|
BEAST_EXPECT(
|
|
(*event)[jss::committee_position].asUInt() ==
|
|
share.committeePosition);
|
|
}
|
|
expectNoShareEvent();
|
|
};
|
|
|
|
auto deferredCount = [&] {
|
|
std::lock_guard lock(ce.deferredExportSharesMutex_);
|
|
return ce.deferredExportShares_.size();
|
|
};
|
|
std::atomic<std::size_t> deferredCharges{0};
|
|
std::atomic<ExportShareCharge> lastDeferredCharge{
|
|
ExportShareCharge::none};
|
|
auto const deferredCharge = [&](ExportShareCharge const charge) {
|
|
lastDeferredCharge.store(charge, std::memory_order_relaxed);
|
|
deferredCharges.fetch_add(1, std::memory_order_relaxed);
|
|
};
|
|
|
|
auto admission = ce.onExportShare(share, deferredCharge);
|
|
BEAST_EXPECT(admission.disposition == ExportShareDisposition::deferred);
|
|
BEAST_EXPECT(admission.charge == ExportShareCharge::none);
|
|
BEAST_EXPECT(deferredCount() == 1);
|
|
BEAST_EXPECT(!hasRetainedContribution());
|
|
|
|
admission = ce.onExportShare(share, deferredCharge);
|
|
BEAST_EXPECT(
|
|
admission.disposition == ExportShareDisposition::duplicate);
|
|
BEAST_EXPECT(deferredCount() == 1);
|
|
|
|
auto wrongBranch = share;
|
|
wrongBranch.originLedgerHash = makeHash("wrong-export-origin");
|
|
admission = ce.onExportShare(wrongBranch, deferredCharge);
|
|
BEAST_EXPECT(admission.disposition == ExportShareDisposition::deferred);
|
|
BEAST_EXPECT(deferredCount() == 2);
|
|
|
|
admission = ce.onExportShare(wrongSigner, deferredCharge);
|
|
BEAST_EXPECT(admission.disposition == ExportShareDisposition::deferred);
|
|
BEAST_EXPECT(deferredCount() == 3);
|
|
|
|
admission = ce.onExportShare(wrongMaster, deferredCharge);
|
|
BEAST_EXPECT(admission.disposition == ExportShareDisposition::deferred);
|
|
BEAST_EXPECT(deferredCount() == 4);
|
|
|
|
auto beyondHorizon = share;
|
|
beyondHorizon.originLedgerSeq = universe->info().seq +
|
|
ConsensusExtensions::maxDeferredExportShareFutureLedgers_ + 1;
|
|
admission = ce.onExportShare(beyondHorizon, deferredCharge);
|
|
BEAST_EXPECT(admission.disposition == ExportShareDisposition::deferred);
|
|
BEAST_EXPECT(deferredCount() == 4);
|
|
|
|
installValidated(originLedger);
|
|
|
|
if (replayWins)
|
|
{
|
|
std::unique_lock streamLock{ce.exportStreamMutex_};
|
|
std::promise<void> callbackStarted;
|
|
auto callbackStartedFuture = callbackStarted.get_future();
|
|
auto callback = std::async(std::launch::async, [&] {
|
|
callbackStarted.set_value();
|
|
ce.onValidatedLedger(
|
|
originLedger->info().seq, originLedger->info().hash);
|
|
});
|
|
callbackStartedFuture.wait();
|
|
BEAST_EXPECT(
|
|
callback.wait_for(50ms) == std::future_status::timeout);
|
|
// The callback must reach retained replay before it authors this
|
|
// node's share. Reordering local authoring ahead of replay would
|
|
// admit the contribution while blocked on exportStreamMutex_.
|
|
BEAST_EXPECT(!hasRetainedContribution());
|
|
streamLock.unlock();
|
|
auto const status = callback.wait_for(5s);
|
|
BEAST_EXPECT(status == std::future_status::ready);
|
|
if (status != std::future_status::ready)
|
|
return;
|
|
callback.get();
|
|
|
|
// Replay snapshots first; local authoring then admits and publishes
|
|
// the accepted edge in the same validated-ledger callback.
|
|
BEAST_EXPECT(hasRetainedContribution());
|
|
}
|
|
else
|
|
{
|
|
std::unique_lock streamLock{ce.exportStreamMutex_};
|
|
std::promise<void> edgeStarted;
|
|
auto edgeStartedFuture = edgeStarted.get_future();
|
|
auto edge = std::async(std::launch::async, [&] {
|
|
edgeStarted.set_value();
|
|
return ce.onExportShare(share);
|
|
});
|
|
edgeStartedFuture.wait();
|
|
auto const deadline = std::chrono::steady_clock::now() + 5s;
|
|
while (!hasRetainedContribution() &&
|
|
edge.wait_for(0ms) == std::future_status::timeout &&
|
|
std::chrono::steady_clock::now() < deadline)
|
|
std::this_thread::sleep_for(1ms);
|
|
BEAST_EXPECT(hasRetainedContribution());
|
|
BEAST_EXPECT(edge.wait_for(0ms) == std::future_status::timeout);
|
|
streamLock.unlock();
|
|
auto const status = edge.wait_for(5s);
|
|
BEAST_EXPECT(status == std::future_status::ready);
|
|
if (status != std::future_status::ready)
|
|
return;
|
|
BEAST_EXPECT(edge.get());
|
|
|
|
ce.onValidatedLedger(
|
|
originLedger->info().seq, originLedger->info().hash);
|
|
}
|
|
|
|
BEAST_EXPECT(hasRetainedContribution());
|
|
admission = ce.onExportShare(share, {});
|
|
BEAST_EXPECT(
|
|
admission.disposition == ExportShareDisposition::duplicate);
|
|
BEAST_EXPECT(admission.charge == ExportShareCharge::none);
|
|
|
|
admission = ce.onExportShare(wrongBranch, {});
|
|
BEAST_EXPECT(admission.disposition == ExportShareDisposition::invalid);
|
|
BEAST_EXPECT(admission.charge == ExportShareCharge::invalidData);
|
|
|
|
admission = ce.onExportShare(wrongSigner, {});
|
|
BEAST_EXPECT(admission.disposition == ExportShareDisposition::invalid);
|
|
BEAST_EXPECT(admission.charge == ExportShareCharge::invalidData);
|
|
|
|
admission = ce.onExportShare(wrongMaster, {});
|
|
BEAST_EXPECT(admission.disposition == ExportShareDisposition::invalid);
|
|
BEAST_EXPECT(admission.charge == ExportShareCharge::invalidData);
|
|
|
|
auto invalidSignature = share;
|
|
invalidSignature.signature = sign(
|
|
valKeys.keys->publicKey,
|
|
valKeys.keys->secretKey,
|
|
Slice{"invalid-export-share", 20});
|
|
admission = ce.onExportShare(invalidSignature, {});
|
|
BEAST_EXPECT(admission.disposition == ExportShareDisposition::invalid);
|
|
BEAST_EXPECT(admission.charge == ExportShareCharge::invalidSignature);
|
|
|
|
BEAST_EXPECT(deferredCount() == 0);
|
|
BEAST_EXPECT(deferredCharges.load(std::memory_order_relaxed) == 3);
|
|
BEAST_EXPECT(
|
|
lastDeferredCharge.load(std::memory_order_relaxed) ==
|
|
ExportShareCharge::invalidData);
|
|
BEAST_EXPECT(
|
|
ce.lastExportReplaySeq_.load(std::memory_order_relaxed) ==
|
|
originLedger->info().seq);
|
|
expectOneShareEvent(*originLedger);
|
|
|
|
// Model admission retaining the preceding validated cursor while the
|
|
// validated callback has already completed this origin's retry pass.
|
|
// The share must be admitted directly rather than queued behind that
|
|
// completed pass.
|
|
ce.postValidationExportSigCollector().clear(origin);
|
|
{
|
|
std::lock_guard lock(ce.deferredExportSharesMutex_);
|
|
ce.deferredExportShareRetrySeq_ = originLedger->info().seq;
|
|
}
|
|
admission = ce.deferExportShare(share, {}, universe->info().seq);
|
|
BEAST_EXPECT(admission.disposition == ExportShareDisposition::accepted);
|
|
BEAST_EXPECT(deferredCount() == 0);
|
|
BEAST_EXPECT(hasRetainedContribution());
|
|
|
|
auto next = std::make_shared<Ledger>(
|
|
*originLedger, env.app().timeKeeper().closeTime());
|
|
next->updateSkipList();
|
|
next->setAccepted(
|
|
next->info().closeTime, next->info().closeTimeResolution, true);
|
|
installValidated(next);
|
|
ce.onValidatedLedger(next->info().seq, next->info().hash);
|
|
expectOneShareEvent(*next);
|
|
ce.onValidatedLedger(next->info().seq, next->info().hash);
|
|
expectNoShareEvent();
|
|
|
|
auto witnessed =
|
|
std::make_shared<Ledger>(*next, env.app().timeKeeper().closeTime());
|
|
Sandbox witnessChanges{witnessed.get(), tapNONE};
|
|
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::recordExportWitness(
|
|
witnessChanges,
|
|
witnessChanges,
|
|
latchKey,
|
|
makeHash("stream-witness"),
|
|
env.journal)));
|
|
witnessChanges.apply(*witnessed);
|
|
witnessed->updateSkipList();
|
|
witnessed->setAccepted(
|
|
witnessed->info().closeTime,
|
|
witnessed->info().closeTimeResolution,
|
|
true);
|
|
installValidated(witnessed);
|
|
|
|
auto const terminalLatch = witnessed->read(latchKey);
|
|
BEAST_EXPECT(
|
|
terminalLatch && !terminalLatch->isFieldPresent(sfExportNode) &&
|
|
terminalLatch->isFieldPresent(sfExportSignatureHash));
|
|
BEAST_EXPECT(hasRetainedContribution());
|
|
ce.onValidatedLedger(witnessed->info().seq, witnessed->info().hash);
|
|
BEAST_EXPECT(!ce.onExportShare(share));
|
|
BEAST_EXPECT(hasRetainedContribution());
|
|
expectNoShareEvent();
|
|
|
|
auto pendingAtStop = share;
|
|
pendingAtStop.originLedgerSeq = witnessed->info().seq + 1;
|
|
pendingAtStop.originLedgerHash = makeHash("pending-at-stop");
|
|
admission = ce.onExportShare(pendingAtStop, deferredCharge);
|
|
BEAST_EXPECT(admission.disposition == ExportShareDisposition::deferred);
|
|
BEAST_EXPECT(deferredCount() == 1);
|
|
|
|
ce.stopExportShareService();
|
|
BEAST_EXPECT(deferredCount() == 0);
|
|
BEAST_EXPECT(
|
|
wsc->invoke("unsubscribe", stream)[jss::status] == "success");
|
|
}
|
|
|
|
void
|
|
testExportStreamRetainedReplayRace()
|
|
{
|
|
runExportStreamRetainedReplayRace(false);
|
|
runExportStreamRetainedReplayRace(true);
|
|
}
|
|
|
|
void
|
|
testExportStreamStopFence()
|
|
{
|
|
using namespace std::chrono_literals;
|
|
using namespace jtx;
|
|
|
|
testcase("Export stream stop fences a waiting validated callback");
|
|
|
|
Env env{
|
|
*this,
|
|
envconfig(),
|
|
supported_amendments() | featureExport,
|
|
nullptr};
|
|
auto wsc = makeWSClient(env.app().config());
|
|
Json::Value stream;
|
|
stream[jss::streams] = Json::arrayValue;
|
|
stream[jss::streams].append("export_signatures");
|
|
BEAST_EXPECT(
|
|
wsc->invoke("subscribe", stream)[jss::status] == "success");
|
|
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
ce.startExportShareService();
|
|
auto const validated = env.app().getLedgerMaster().getValidatedLedger();
|
|
if (!BEAST_EXPECT(validated))
|
|
return;
|
|
|
|
auto const origin = makeHash("stop-fenced-pending-export");
|
|
ce.exportStreamEmissionSeq_ = validated->info().seq;
|
|
ce.exportStreamEmittedShares_.emplace(origin, 1);
|
|
|
|
std::unique_lock streamLock{ce.exportStreamMutex_};
|
|
std::promise<void> callbackStarted;
|
|
auto callbackStartedFuture = callbackStarted.get_future();
|
|
auto callback = std::async(std::launch::async, [&] {
|
|
callbackStarted.set_value();
|
|
ce.onValidatedLedger(validated->info().seq, validated->info().hash);
|
|
});
|
|
callbackStartedFuture.wait();
|
|
BEAST_EXPECT(callback.wait_for(50ms) == std::future_status::timeout);
|
|
|
|
auto stop = std::async(
|
|
std::launch::async, [&] { ce.stopExportShareService(); });
|
|
auto const deadline = std::chrono::steady_clock::now() + 1s;
|
|
while (ce.exportShareServiceStarted_.load(std::memory_order_acquire) &&
|
|
std::chrono::steady_clock::now() < deadline)
|
|
std::this_thread::sleep_for(1ms);
|
|
BEAST_EXPECT(
|
|
!ce.exportShareServiceStarted_.load(std::memory_order_acquire));
|
|
BEAST_EXPECT(stop.wait_for(0ms) == std::future_status::timeout);
|
|
|
|
streamLock.unlock();
|
|
BEAST_EXPECT(callback.wait_for(5s) == std::future_status::ready);
|
|
callback.get();
|
|
BEAST_EXPECT(stop.wait_for(5s) == std::future_status::ready);
|
|
stop.get();
|
|
|
|
BEAST_EXPECT(ce.exportStreamEmissionSeq_ == 0);
|
|
BEAST_EXPECT(ce.exportStreamEmittedShares_.empty());
|
|
BEAST_EXPECT(!wsc->findMsg(250ms, [](Json::Value const& event) {
|
|
return event[jss::type] == "exportSignatureReceived";
|
|
}));
|
|
BEAST_EXPECT(
|
|
wsc->invoke("unsubscribe", stream)[jss::status] == "success");
|
|
}
|
|
|
|
void
|
|
testPublicHookNoopAndFailureBranches()
|
|
{
|
|
testcase("public hook no-op and failure branches");
|
|
|
|
using namespace jtx;
|
|
Env env{
|
|
*this, envconfig(validator, ""), supported_amendments(), nullptr};
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
|
|
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
|
|
ce.cacheUNLReport(ledger);
|
|
BEAST_EXPECT(ce.exportRootAlignmentThreshold() == 1);
|
|
|
|
ce.setExportSigConvergenceFailed();
|
|
BEAST_EXPECT(ce.exportSigConvergenceFailed());
|
|
|
|
ce.setEntropyFailed();
|
|
|
|
ce.generateEntropySecret();
|
|
BEAST_EXPECT(!ce.hasAnyReveals());
|
|
ce.selfSeedReveal();
|
|
BEAST_EXPECT(ce.pendingRevealCount() == 1);
|
|
|
|
auto const [pk, _] = randomKeyPair(KeyType::secp256k1);
|
|
std::vector<std::string> const noSigs;
|
|
BEAST_EXPECT(
|
|
ce.harvestExportSignatures(
|
|
pk, ledger->info().hash, noSigs, "disabled") == 0);
|
|
|
|
ce.setExportEnabledThisRound(true);
|
|
BEAST_EXPECT(
|
|
ce.harvestExportSignatures(
|
|
pk, ledger->info().hash, noSigs, "empty") == 0);
|
|
|
|
protocol::TMProposeSet wire;
|
|
ce.onTrustedPeerMessage(wire);
|
|
|
|
wire.add_exportsignatures(
|
|
makeExportSigBlob(makeHash("wire-no-prev"), pk));
|
|
wire.set_nodepubkey(pk.data(), pk.size());
|
|
ce.onTrustedPeerMessage(wire);
|
|
|
|
Json::Value json;
|
|
ce.estState_ = EstablishState::ConvergingCommit;
|
|
ce.appendJson(json);
|
|
BEAST_EXPECT(json["rng"]["est_state"].asString() == "ConvergingCommit");
|
|
ce.estState_ = EstablishState::ConvergingReveal;
|
|
ce.appendJson(json);
|
|
BEAST_EXPECT(json["rng"]["est_state"].asString() == "ConvergingReveal");
|
|
|
|
ExtendedPosition logPos{makeHash("log-inactive-position")};
|
|
ce.logPosition(
|
|
logPos,
|
|
beast::Journal{beast::Journal::getNullSink()},
|
|
beast::severities::kTrace);
|
|
|
|
protocol::TMProposeSet prop;
|
|
RCLCxPeerPos::Proposal proposal{
|
|
ledger->info().hash,
|
|
0,
|
|
ExtendedPosition{makeHash("attach-export-disabled")},
|
|
NetClock::time_point{},
|
|
NetClock::time_point{},
|
|
env.app().getValidatorKeys().nodeID};
|
|
ConsensusExtensions disabled{env.app(), activeNoopJournal()};
|
|
//@@start test-export-attachment-disabled
|
|
disabled.attachExportSignatures(prop, proposal);
|
|
BEAST_EXPECT(prop.exportsignatures_size() == 0);
|
|
|
|
ConsensusTestConfig cfg;
|
|
cfg.noExportSig = true;
|
|
env.app().getRuntimeConfig().setGlobalConfig(cfg);
|
|
ce.attachExportSignatures(prop, proposal);
|
|
BEAST_EXPECT(prop.exportsignatures_size() == 0);
|
|
//@@end test-export-attachment-disabled
|
|
}
|
|
|
|
void
|
|
testDecorateMessageStoresSelfProofs()
|
|
{
|
|
testcase("decorateMessage stores self proofs");
|
|
|
|
using namespace jtx;
|
|
Env env{
|
|
*this,
|
|
envconfig(validator, ""),
|
|
supported_amendments() | featureConsensusEntropy,
|
|
nullptr};
|
|
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
|
|
auto const& valKeys = env.app().getValidatorKeys();
|
|
BEAST_EXPECT(valKeys.keys);
|
|
if (!valKeys.keys)
|
|
return;
|
|
|
|
ConsensusExtensions ce{env.app(), activeNoopJournal()};
|
|
ExtendedPosition commitPos{makeHash("decorate-message-txset")};
|
|
ce.decoratePosition(commitPos, ledger, true);
|
|
BEAST_EXPECT(commitPos.myCommitment);
|
|
if (!commitPos.myCommitment)
|
|
return;
|
|
|
|
auto const closeTime = NetClock::time_point{NetClock::duration{12345}};
|
|
RCLCxPeerPos::Proposal commitProposal{
|
|
ledger->info().hash,
|
|
0,
|
|
commitPos,
|
|
closeTime,
|
|
NetClock::time_point{},
|
|
valKeys.nodeID};
|
|
auto const commitSig = signDigest(
|
|
valKeys.keys->publicKey,
|
|
valKeys.keys->secretKey,
|
|
commitProposal.signingHash());
|
|
protocol::TMProposeSet prop;
|
|
ce.decorateMessage(
|
|
prop,
|
|
commitProposal,
|
|
commitPos,
|
|
Buffer(commitSig.data(), commitSig.size()));
|
|
|
|
auto const seq = ledger->info().seq + 1;
|
|
auto const commitHash = ce.buildCommitSet(seq);
|
|
BEAST_EXPECT(
|
|
env.app().getInboundTransactions().getSet(commitHash, false));
|
|
|
|
ExtendedPosition revealPos{commitPos.txSetHash};
|
|
revealPos.myReveal = ce.getEntropySecret();
|
|
RCLCxPeerPos::Proposal revealProposal{
|
|
ledger->info().hash,
|
|
1,
|
|
revealPos,
|
|
closeTime,
|
|
NetClock::time_point{},
|
|
valKeys.nodeID};
|
|
auto const revealSig = signDigest(
|
|
valKeys.keys->publicKey,
|
|
valKeys.keys->secretKey,
|
|
revealProposal.signingHash());
|
|
ce.decorateMessage(
|
|
prop,
|
|
revealProposal,
|
|
revealPos,
|
|
Buffer(revealSig.data(), revealSig.size()));
|
|
|
|
BEAST_EXPECT(ce.pendingRevealCount() == 1);
|
|
BEAST_EXPECT(ce.hasMinimumReveals());
|
|
auto const entropyHash = ce.buildEntropySet(seq);
|
|
BEAST_EXPECT(
|
|
env.app().getInboundTransactions().getSet(entropyHash, false));
|
|
}
|
|
|
|
public:
|
|
void
|
|
run() override
|
|
{
|
|
testSidecarPeerAlignmentHelper();
|
|
testSidecarSplitBrainEquivocationThreshold();
|
|
testActiveValidatorViewBuilderPrefersUNLReport();
|
|
testActiveValidatorViewBuilderFallback();
|
|
testActiveValidatorViewAppliesNegativeUNL();
|
|
testActiveValidatorViewCapsNegativeUNL();
|
|
testActiveValidatorViewNullSourceAndExpectedProposers();
|
|
testParticipantThreshold();
|
|
testThresholdPolicyHelpers();
|
|
testRuntimeConfigPolicyAccessors();
|
|
testOnRoundStartRefreshesFeatureLatches();
|
|
testDecoratePositionGeneratesCommitment();
|
|
testOnPreBuildInjectsZeroEntropyFallback();
|
|
testTxnOrderingSaltExtendsLegacySalt();
|
|
testOnPreBuildInjectsEntropySetEntropy();
|
|
testOnPreBuildCanonicalizesMultiContributorEntropy();
|
|
testOnPreBuildAcceptedEntropySetOverridesLocalFailureFlag();
|
|
testOnPreBuildRejectsForgedEntropyContributorAttribution();
|
|
testOnPreBuildTier2ParticipantAligned();
|
|
testTier2ThresholdAnchorsToOriginalView();
|
|
testOnPreBuildTier2WithNegativeUNL();
|
|
testProposalProofRoundTrip();
|
|
testProposalPrecheckUsesExportShareRelayLimits();
|
|
testHarvestRngDataReplacementAndRejection();
|
|
testRngManifestArrivalDoesNotRetargetProofedCommit();
|
|
testRngManifestRotationDropsPinnedContributor();
|
|
testExportCollectorBuildsAttributedUnion();
|
|
testExportSidecarCandidateDeadline();
|
|
testTransactionAcquireRejectsSidecarWireNodes();
|
|
testAcquiredSetsRejectConsensusExtensionPseudos();
|
|
testAgreedExportWitnessBuildsContributorBitmap();
|
|
testExportWitnessRebuildValidationCursor();
|
|
testRngSidecarBuildsLocalSnapshots();
|
|
testOnPreBuildInjectsStandaloneEntropy();
|
|
testOnPreBuildStripsSuppliedExtensionPseudos();
|
|
testLiveBuildSetStripsOnlyExtensionPseudos();
|
|
testDiagnosticsJsonAndPositionLogging();
|
|
testDecoratePositionSkipsWhenDisabled();
|
|
testExportSigGateRequiresQuorumAlignment();
|
|
testExportAdvancesDuringRngWaits();
|
|
testExportExpiryDoesNotEndRngWait();
|
|
testRngFallbackPreservesAlignedExport();
|
|
testParallelGateRechecksOrdinarySet();
|
|
testRngEntropyGateAllowsQuorumDespiteMissingObservation();
|
|
testRngEntropyGateDoesNotCountUnpublishedObserverRoot();
|
|
testRngEntropyConflictAllowsQuorumDespiteMissingObservation();
|
|
testRngFastPathWaitsAfterEntropyPublish();
|
|
testRngPrevProposerUnderObservationDoesNotSuppressCommitQuorum();
|
|
testRngCommitWaitsWhenQuorumPossible();
|
|
testRngCommitTimeoutWithEntropyGatePublishesCommitSet();
|
|
testRngCommitTimeoutUsesProofedCommitsNotVisiblePeers();
|
|
testRngCommitTimeoutRejectsUnproofedCommits();
|
|
testRngCommitQuorumInObservingModeDoesNotPropose();
|
|
testRngCommitConflictRefreshesHashBeforeWaiting();
|
|
testRngCommitHashConflictTimeoutPublishesReveal();
|
|
testRngRevealTransitionWaitsWhenRevealsIncomplete();
|
|
testRngRevealTimeoutWithoutRevealsFallsBack();
|
|
testRngEntropyPublishIsIdempotent();
|
|
testRngEntropyConflictTimeoutClearsHash();
|
|
testRngEntropyConflictRefreshesHashBeforeWaiting();
|
|
testRngEntropyConflictIgnoredWithQuorumAlignment();
|
|
testExportSigGateAllowsAlignedQuorumDespiteMinorityConflict();
|
|
testExportSigGateAllowsQuorumDespiteMissingObservation();
|
|
testExportSigGateObservesAdvertisedPeerSets();
|
|
testExportSigGateIgnoresAdvertisedSetsWithoutExportTxns();
|
|
testExportSigGateObservingModeDoesNotPropose();
|
|
testExportSigGateRefreshesHashBeforeWaiting();
|
|
testExportSigGateBoundsCandidateObservationWindow();
|
|
testExportSigGateDeadlineSurvivesRootChanges();
|
|
testExportSigGateLateFirstMaterial();
|
|
testExportSigGateSkipsWhenExportDisabled();
|
|
testExportSigGateSkipsWithoutAnchoredView();
|
|
testParticipantDiagnosticsOnlyWhenExtensionEnabled();
|
|
testExportDisabledRoundClearsCollector();
|
|
testValidatorKeylessAuthoringNoops();
|
|
testExportStreamRetainedReplayRace();
|
|
testExportStreamStopFence();
|
|
testPublicHookNoopAndFailureBranches();
|
|
testDecorateMessageStoresSelfProofs();
|
|
}
|
|
};
|
|
|
|
BEAST_DEFINE_TESTSUITE(ConsensusExtensions, consensus, ripple);
|
|
|
|
} // namespace test
|
|
} // namespace ripple
|