mirror of
https://github.com/Xahau/xahaud.git
synced 2026-07-31 19:10:10 +00:00
3452 lines
128 KiB
C++
3452 lines
128 KiB
C++
//------------------------------------------------------------------------------
|
|
/*
|
|
This file is part of rippled: https://github.com/ripple/rippled
|
|
Copyright (c) 2012, 2013 Ripple Labs Inc.
|
|
|
|
Permission to use, copy, modify, and/or distribute this software for any
|
|
purpose with or without fee is hereby granted, provided that the above
|
|
copyright notice and this permission notice appear in all copies.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
//==============================================================================
|
|
|
|
#include <xrpld/app/consensus/ConsensusExtensions.h>
|
|
#include <xrpld/app/consensus/ExportSignatureHarvester.h>
|
|
#include <xrpld/app/ledger/InboundTransactions.h>
|
|
#include <xrpld/app/ledger/Ledger.h>
|
|
#include <xrpld/app/ledger/LedgerMaster.h>
|
|
#include <xrpld/app/ledger/OpenLedger.h>
|
|
#include <xrpld/app/misc/AmendmentTable.h>
|
|
#include <xrpld/app/misc/CanonicalTXSet.h>
|
|
#include <xrpld/app/misc/Manifest.h>
|
|
#include <xrpld/app/misc/NetworkOPs.h>
|
|
#include <xrpld/app/misc/RuntimeConfig.h>
|
|
#include <xrpld/app/misc/ValidatorKeys.h>
|
|
#include <xrpld/app/misc/ValidatorList.h>
|
|
#include <xrpld/app/tx/detail/ExportLedgerOps.h>
|
|
#include <xrpld/app/tx/detail/ExportResultBuilder.h>
|
|
#include <xrpld/consensus/Consensus.h>
|
|
#include <xrpld/consensus/ConsensusExtensionsTick.h>
|
|
#include <xrpld/overlay/Overlay.h>
|
|
#include <xrpld/shamap/SHAMap.h>
|
|
#include <xrpl/basics/contract.h>
|
|
#include <xrpl/basics/random.h>
|
|
#include <xrpl/basics/strHex.h>
|
|
#include <xrpl/crypto/csprng.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/Indexes.h>
|
|
#include <xrpl/protocol/SidecarType.h>
|
|
#include <xrpl/protocol/Sign.h>
|
|
#include <xrpl/protocol/TxFlags.h>
|
|
#include <xrpl/protocol/TxFormats.h>
|
|
#include <xrpl/protocol/ValidatorBitset.h>
|
|
#include <xrpl/protocol/digest.h>
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <iterator>
|
|
#include <limits>
|
|
#include <random>
|
|
|
|
namespace ripple {
|
|
|
|
ConsensusExtensions::ConsensusExtensions(Application& app, beast::Journal j)
|
|
: app_(app), j_(j)
|
|
{
|
|
}
|
|
|
|
namespace {
|
|
|
|
struct ResolvedExportShare
|
|
{
|
|
std::shared_ptr<SLE const> latch;
|
|
STTx releaseTarget;
|
|
};
|
|
|
|
enum class ExportShareResolutionStatus {
|
|
resolved,
|
|
deferred,
|
|
duplicate,
|
|
invalid
|
|
};
|
|
|
|
struct ExportShareResolution
|
|
{
|
|
ExportShareResolutionStatus status;
|
|
std::optional<ResolvedExportShare> value;
|
|
};
|
|
|
|
bool
|
|
isPendingExportShare(
|
|
ReadView const& view,
|
|
ExportShare const& share,
|
|
beast::Journal j)
|
|
{
|
|
if (share.originLedgerSeq > view.info().seq)
|
|
return false;
|
|
|
|
auto const originHash = share.originLedgerSeq == view.info().seq
|
|
? std::optional<uint256>{view.info().hash}
|
|
: hashOfSeq(view, share.originLedgerSeq, j);
|
|
if (!originHash || *originHash != share.originLedgerHash)
|
|
return false;
|
|
|
|
auto const latch =
|
|
view.read(keylet::shadowTicket(share.owner, share.originTxn));
|
|
return latch && latch->getType() == ltSHADOW_TICKET &&
|
|
latch->isFieldPresent(sfTransactionHash) &&
|
|
latch->isFieldPresent(sfExportUniverseHash) &&
|
|
latch->isFieldPresent(sfExportCommittee) &&
|
|
latch->isFieldPresent(sfLastLedgerSequence) &&
|
|
latch->isFieldPresent(sfExportNode) &&
|
|
!latch->isFieldPresent(sfExportSignatureHash) &&
|
|
latch->getAccountID(sfAccount) == share.owner &&
|
|
latch->getFieldH256(sfTransactionHash) == share.originTxn &&
|
|
latch->getFieldU32(sfLedgerSequence) == share.originLedgerSeq &&
|
|
view.info().seq <= latch->getFieldU32(sfLastLedgerSequence);
|
|
}
|
|
|
|
ExportShareResolution
|
|
resolveExportShare(
|
|
Application& app,
|
|
ConsensusExtensions const& extensions,
|
|
ExportShare const& share,
|
|
std::shared_ptr<Ledger const> const& validated,
|
|
beast::Journal j)
|
|
{
|
|
if (!validated)
|
|
return {ExportShareResolutionStatus::deferred, std::nullopt};
|
|
if (!validated->rules().enabled(featureExport) ||
|
|
share.triggerTxn != share.originTxn)
|
|
return {ExportShareResolutionStatus::invalid, std::nullopt};
|
|
if (share.originLedgerSeq > validated->info().seq)
|
|
return {ExportShareResolutionStatus::deferred, std::nullopt};
|
|
|
|
auto const canonicalOriginHash =
|
|
share.originLedgerSeq == validated->info().seq
|
|
? std::optional<uint256>{validated->info().hash}
|
|
: hashOfSeq(*validated, share.originLedgerSeq, j);
|
|
if (!canonicalOriginHash)
|
|
return {ExportShareResolutionStatus::deferred, std::nullopt};
|
|
if (*canonicalOriginHash != share.originLedgerHash)
|
|
return {ExportShareResolutionStatus::invalid, std::nullopt};
|
|
|
|
auto const latchKey = keylet::shadowTicket(share.owner, share.originTxn);
|
|
auto const latch = validated->read(latchKey);
|
|
if (!latch)
|
|
return {ExportShareResolutionStatus::invalid, std::nullopt};
|
|
if (latch->getType() != ltSHADOW_TICKET ||
|
|
!latch->isFieldPresent(sfTransactionHash) ||
|
|
!latch->isFieldPresent(sfExportUniverseHash) ||
|
|
!latch->isFieldPresent(sfExportCommittee) ||
|
|
!latch->isFieldPresent(sfLastLedgerSequence) ||
|
|
!latch->isFieldPresent(sfAccount) ||
|
|
!latch->isFieldPresent(sfLedgerSequence) ||
|
|
latch->getAccountID(sfAccount) != share.owner ||
|
|
latch->getFieldH256(sfTransactionHash) != share.originTxn ||
|
|
latch->getFieldU32(sfLedgerSequence) != share.originLedgerSeq)
|
|
return {ExportShareResolutionStatus::invalid, std::nullopt};
|
|
if (!latch->isFieldPresent(sfExportNode) ||
|
|
latch->isFieldPresent(sfExportSignatureHash) ||
|
|
validated->info().seq > latch->getFieldU32(sfLastLedgerSequence))
|
|
return {ExportShareResolutionStatus::duplicate, std::nullopt};
|
|
|
|
auto const originLedger =
|
|
app.getLedgerMaster().getLedgerByHash(share.originLedgerHash);
|
|
if (!originLedger)
|
|
return {ExportShareResolutionStatus::deferred, std::nullopt};
|
|
if (originLedger->info().seq != share.originLedgerSeq)
|
|
return {ExportShareResolutionStatus::invalid, std::nullopt};
|
|
|
|
auto const [outer, _] = originLedger->txRead(share.originTxn);
|
|
if (!outer || outer->getTxnType() != ttEXPORT ||
|
|
!outer->isFieldPresent(sfExportedTxn) ||
|
|
outer->getAccountID(sfAccount) != share.owner)
|
|
return {ExportShareResolutionStatus::invalid, std::nullopt};
|
|
|
|
bool const hasUniverse = outer->isFieldPresent(sfExportUniverseHash);
|
|
bool const hasCommittee = outer->isFieldPresent(sfExportCommittee);
|
|
if (hasUniverse != hasCommittee ||
|
|
(!hasUniverse && !outer->isFieldPresent(sfEmitDetails)) ||
|
|
(hasUniverse &&
|
|
(outer->getFieldH256(sfExportUniverseHash) !=
|
|
latch->getFieldH256(sfExportUniverseHash) ||
|
|
outer->getFieldVL(sfExportCommittee) !=
|
|
latch->getFieldVL(sfExportCommittee))))
|
|
return {ExportShareResolutionStatus::invalid, std::nullopt};
|
|
|
|
auto const universeHash = latch->getFieldH256(sfExportUniverseHash);
|
|
if (originLedger->info().parentHash != universeHash)
|
|
return {ExportShareResolutionStatus::invalid, std::nullopt};
|
|
auto const universeLedger =
|
|
app.getLedgerMaster().getLedgerByHash(universeHash);
|
|
if (!universeLedger)
|
|
return {ExportShareResolutionStatus::deferred, std::nullopt};
|
|
|
|
auto const validatorView =
|
|
extensions.makeActiveValidatorView(universeLedger);
|
|
if (!validatorView->fromUNLReport || !validatorView->sourceLedgerHash ||
|
|
*validatorView->sourceLedgerHash != universeHash ||
|
|
share.universePosition >=
|
|
validatorView->orderedOriginalMasterKeys.size())
|
|
return {ExportShareResolutionStatus::invalid, std::nullopt};
|
|
|
|
auto const committee = resolveExportCommittee(
|
|
makeSlice(latch->getFieldVL(sfExportCommittee)),
|
|
validatorView->orderedOriginalMasterKeys.size());
|
|
if (!committee || !committee->members.contains(share.universePosition))
|
|
return {ExportShareResolutionStatus::invalid, std::nullopt};
|
|
|
|
auto const& expectedMaster =
|
|
validatorView->orderedOriginalMasterKeys[share.universePosition];
|
|
if (app.validatorManifests().getMasterKey(share.signingKey) !=
|
|
expectedMaster)
|
|
return {ExportShareResolutionStatus::duplicate, std::nullopt};
|
|
|
|
auto const inner = ExportLedgerOps::innerExportedTx(*outer);
|
|
if (!inner)
|
|
return {ExportShareResolutionStatus::invalid, std::nullopt};
|
|
auto const targetNetworkID = inner->isFieldPresent(sfNetworkID)
|
|
? inner->getFieldU32(sfNetworkID)
|
|
: std::uint32_t{0};
|
|
ExportOriginMemo::Origin const origin{
|
|
app.config().NETWORK_ID, targetNetworkID, share.originTxn};
|
|
auto const identity = ExportOriginMemo::identityForm(*inner, origin);
|
|
auto release = ExportOriginMemo::releaseForm(
|
|
*inner,
|
|
origin,
|
|
ExportOriginMemo::Anchor{
|
|
share.originLedgerSeq, share.originLedgerHash});
|
|
if (!identity || !release ||
|
|
ExportResultBuilder::exportIntentHash(identity.value()) !=
|
|
latch->getFieldH256(sfDigest))
|
|
return {ExportShareResolutionStatus::invalid, std::nullopt};
|
|
|
|
return {
|
|
ExportShareResolutionStatus::resolved,
|
|
ResolvedExportShare{latch, std::move(release.value())}};
|
|
}
|
|
|
|
ExportShare
|
|
withContribution(
|
|
ExportShare const& context,
|
|
ExportSigCollectorV2::Contribution const& contribution)
|
|
{
|
|
return ExportShare{
|
|
context.version,
|
|
context.owner,
|
|
context.originTxn,
|
|
context.originLedgerSeq,
|
|
context.originLedgerHash,
|
|
context.triggerTxn,
|
|
contribution.position,
|
|
contribution.signingKey,
|
|
contribution.signature};
|
|
}
|
|
|
|
std::map<uint256, std::shared_ptr<SLE const>>
|
|
pendingExportLatches(ReadView const& view, LedgerIndex eligibleSeq)
|
|
{
|
|
std::map<uint256, std::shared_ptr<SLE const>> result;
|
|
forEachItem(
|
|
view,
|
|
keylet::pendingExports(),
|
|
[&](std::shared_ptr<SLE const> const& latch) {
|
|
if (!latch || latch->getType() != ltSHADOW_TICKET ||
|
|
!latch->isFieldPresent(sfTransactionHash) ||
|
|
!latch->isFieldPresent(sfLedgerSequence) ||
|
|
!latch->isFieldPresent(sfAccount) ||
|
|
!latch->isFieldPresent(sfLastLedgerSequence) ||
|
|
eligibleSeq > latch->getFieldU32(sfLastLedgerSequence) ||
|
|
result.size() >= ExportLimits::maxLiveExportLatches)
|
|
return;
|
|
result.emplace(latch->getFieldH256(sfTransactionHash), latch);
|
|
});
|
|
return result;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool
|
|
ConsensusExtensions::publishExportShareLocked(
|
|
ExportShare const& share,
|
|
LedgerIndex const validatedLedgerSeq,
|
|
uint256 const& validatedLedgerHash)
|
|
{
|
|
if (!exportShareServiceStarted_.load(std::memory_order_acquire))
|
|
return false;
|
|
|
|
if (exportStreamEmissionSeq_ != validatedLedgerSeq)
|
|
{
|
|
exportStreamEmissionSeq_ = validatedLedgerSeq;
|
|
exportStreamEmittedShares_.clear();
|
|
}
|
|
|
|
auto const identity = std::pair{share.originTxn, share.universePosition};
|
|
if (!exportStreamEmittedShares_.insert(identity).second)
|
|
return false;
|
|
|
|
app_.getOPs().pubExportSignature(
|
|
share, validatedLedgerSeq, validatedLedgerHash);
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
ConsensusExtensions::onExportShare(ExportShare const& share)
|
|
{
|
|
return admitExportShare(share, {}, true).isAccepted();
|
|
}
|
|
|
|
ExportShareAdmission
|
|
ConsensusExtensions::onExportShare(
|
|
ExportShare const& share,
|
|
ExportShareChargeHandler deferredCharge)
|
|
{
|
|
return admitExportShare(share, std::move(deferredCharge), true);
|
|
}
|
|
|
|
ExportShareAdmission
|
|
ConsensusExtensions::deferExportShare(
|
|
ExportShare const& share,
|
|
ExportShareChargeHandler deferredCharge,
|
|
LedgerIndex const validatedLedgerSeq)
|
|
{
|
|
if (share.originLedgerSeq <= validatedLedgerSeq ||
|
|
share.originLedgerSeq - validatedLedgerSeq >
|
|
maxDeferredExportShareFutureLedgers_)
|
|
return {ExportShareDisposition::deferred, ExportShareCharge::none};
|
|
|
|
std::size_t const serializedBytes = share.serialize().size();
|
|
auto const wireHash = share.wireHash();
|
|
|
|
std::lock_guard lock(deferredExportSharesMutex_);
|
|
if (!exportShareServiceStarted_.load(std::memory_order_acquire))
|
|
return {ExportShareDisposition::duplicate, ExportShareCharge::none};
|
|
|
|
if (auto const it = deferredExportShares_.find(wireHash);
|
|
it != deferredExportShares_.end())
|
|
{
|
|
if (!it->second.charge && deferredCharge)
|
|
it->second.charge = std::move(deferredCharge);
|
|
return {ExportShareDisposition::duplicate, ExportShareCharge::none};
|
|
}
|
|
|
|
bool const newOrigin =
|
|
!deferredExportShareOrigins_.contains(share.originTxn);
|
|
if (deferredExportShares_.size() >= maxDeferredExportShares_ ||
|
|
serializedBytes > maxDeferredExportShareBytes_ ||
|
|
deferredExportShareBytes_ >
|
|
maxDeferredExportShareBytes_ - serializedBytes ||
|
|
(newOrigin &&
|
|
deferredExportShareOrigins_.size() >= maxDeferredExportShareOrigins_))
|
|
return {ExportShareDisposition::deferred, ExportShareCharge::none};
|
|
|
|
deferredExportShareBytes_ += serializedBytes;
|
|
++deferredExportShareOrigins_[share.originTxn];
|
|
deferredExportShares_.emplace(
|
|
wireHash,
|
|
DeferredExportShare{share, std::move(deferredCharge), serializedBytes});
|
|
return {ExportShareDisposition::deferred, ExportShareCharge::none};
|
|
}
|
|
|
|
void
|
|
ConsensusExtensions::retryDeferredExportShares(
|
|
LedgerIndex const validatedLedgerSeq)
|
|
{
|
|
std::vector<DeferredExportShare> retry;
|
|
{
|
|
std::lock_guard lock(deferredExportSharesMutex_);
|
|
for (auto it = deferredExportShares_.begin();
|
|
it != deferredExportShares_.end();)
|
|
{
|
|
if (it->second.share.originLedgerSeq > validatedLedgerSeq)
|
|
{
|
|
++it;
|
|
continue;
|
|
}
|
|
|
|
auto const origin = it->second.share.originTxn;
|
|
deferredExportShareBytes_ -= it->second.serializedBytes;
|
|
auto const originIt = deferredExportShareOrigins_.find(origin);
|
|
if (originIt != deferredExportShareOrigins_.end() &&
|
|
--originIt->second == 0)
|
|
deferredExportShareOrigins_.erase(originIt);
|
|
retry.push_back(std::move(it->second));
|
|
it = deferredExportShares_.erase(it);
|
|
}
|
|
}
|
|
|
|
for (auto& deferred : retry)
|
|
{
|
|
auto const result = admitExportShare(deferred.share, {}, false);
|
|
if (result.isAccepted())
|
|
{
|
|
auto const frame = deferred.share.serialize();
|
|
protocol::TMExportShares message;
|
|
message.add_shares(frame.data(), frame.size());
|
|
app_.overlay().broadcast(message);
|
|
}
|
|
else if (
|
|
result.disposition == ExportShareDisposition::invalid &&
|
|
result.charge != ExportShareCharge::none && deferred.charge)
|
|
{
|
|
deferred.charge(result.charge);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
ConsensusExtensions::clearDeferredExportShares()
|
|
{
|
|
std::lock_guard lock(deferredExportSharesMutex_);
|
|
deferredExportShares_.clear();
|
|
deferredExportShareOrigins_.clear();
|
|
deferredExportShareBytes_ = 0;
|
|
}
|
|
|
|
ExportShareAdmission
|
|
ConsensusExtensions::admitExportShare(
|
|
ExportShare const& share,
|
|
ExportShareChargeHandler deferredCharge,
|
|
bool const allowDeferral)
|
|
{
|
|
if (!exportShareServiceStarted_.load(std::memory_order_acquire))
|
|
return {ExportShareDisposition::duplicate, ExportShareCharge::none};
|
|
|
|
auto const validated = app_.getLedgerMaster().getValidatedLedger();
|
|
if (allowDeferral && validated &&
|
|
share.originLedgerSeq > validated->info().seq)
|
|
return deferExportShare(
|
|
share, std::move(deferredCharge), validated->info().seq);
|
|
|
|
auto resolved = resolveExportShare(app_, *this, share, validated, j_);
|
|
if (resolved.status == ExportShareResolutionStatus::deferred)
|
|
return {ExportShareDisposition::deferred, ExportShareCharge::none};
|
|
if (resolved.status == ExportShareResolutionStatus::duplicate)
|
|
return {ExportShareDisposition::duplicate, ExportShareCharge::none};
|
|
if (resolved.status == ExportShareResolutionStatus::invalid ||
|
|
!resolved.value || !validated)
|
|
return {
|
|
ExportShareDisposition::invalid, ExportShareCharge::invalidData};
|
|
|
|
if (!postValidationExportSigCollector_.reopenPublication(
|
|
share.originTxn, share.triggerTxn, validated->info().seq))
|
|
return {ExportShareDisposition::deferred, ExportShareCharge::none};
|
|
|
|
ExportSigCollectorV2::Contribution contribution{
|
|
share.universePosition, share.signingKey, share.signature};
|
|
auto admission = postValidationExportSigCollector_.beginAttributedAdmission(
|
|
share.originTxn, std::move(contribution), validated->info().seq);
|
|
if (admission.result != ExportSigCollectorV2::BeginResult::verify ||
|
|
!admission.ticket)
|
|
{
|
|
switch (admission.result)
|
|
{
|
|
case ExportSigCollectorV2::BeginResult::duplicate:
|
|
case ExportSigCollectorV2::BeginResult::conflicted:
|
|
return {
|
|
ExportShareDisposition::duplicate, ExportShareCharge::none};
|
|
case ExportSigCollectorV2::BeginResult::unknownOrigin:
|
|
case ExportSigCollectorV2::BeginResult::capacity:
|
|
return {
|
|
ExportShareDisposition::deferred, ExportShareCharge::none};
|
|
case ExportSigCollectorV2::BeginResult::malformed:
|
|
return {
|
|
ExportShareDisposition::invalid,
|
|
ExportShareCharge::invalidData};
|
|
case ExportSigCollectorV2::BeginResult::verify:
|
|
break;
|
|
}
|
|
return {
|
|
ExportShareDisposition::invalid, ExportShareCharge::invalidData};
|
|
}
|
|
|
|
auto const signer = calcAccountID(share.signingKey);
|
|
auto const data =
|
|
buildMultiSigningData(resolved.value->releaseTarget, signer);
|
|
auto const signatureVerified = verify(
|
|
share.signingKey,
|
|
data.slice(),
|
|
Slice{share.signature.data(), share.signature.size()});
|
|
auto outcome = postValidationExportSigCollector_.admitContribution(
|
|
std::move(*admission.ticket), signatureVerified, validated->info().seq);
|
|
if (outcome.result == ExportSigCollectorV2::AdmitResult::accepted)
|
|
{
|
|
std::lock_guard streamLock(exportStreamMutex_);
|
|
if (!exportShareServiceStarted_.load(std::memory_order_acquire))
|
|
return {ExportShareDisposition::duplicate, ExportShareCharge::none};
|
|
auto const latest = app_.getLedgerMaster().getValidatedLedger();
|
|
if (!latest || !isPendingExportShare(*latest, share, j_))
|
|
return {ExportShareDisposition::duplicate, ExportShareCharge::none};
|
|
publishExportShareLocked(
|
|
share, latest->info().seq, latest->info().hash);
|
|
return {ExportShareDisposition::accepted, ExportShareCharge::none};
|
|
}
|
|
if (outcome.result == ExportSigCollectorV2::AdmitResult::invalid)
|
|
return {
|
|
ExportShareDisposition::invalid,
|
|
ExportShareCharge::invalidSignature};
|
|
if (outcome.result != ExportSigCollectorV2::AdmitResult::conflicted ||
|
|
!outcome.priorContribution || !outcome.conflictingContribution)
|
|
return {ExportShareDisposition::duplicate, ExportShareCharge::none};
|
|
|
|
// Propagate both valid encodings so every honest collector can observe the
|
|
// same absorbing conflict even if it missed the first frame.
|
|
protocol::TMExportShares conflict;
|
|
for (auto const* contribution :
|
|
{&*outcome.priorContribution, &*outcome.conflictingContribution})
|
|
{
|
|
auto const frame = withContribution(share, *contribution).serialize();
|
|
conflict.add_shares(frame.data(), frame.size());
|
|
}
|
|
app_.overlay().broadcast(conflict);
|
|
return {ExportShareDisposition::duplicate, ExportShareCharge::none};
|
|
}
|
|
|
|
void
|
|
ConsensusExtensions::onValidatedLedger(
|
|
LedgerIndex const seq,
|
|
uint256 const& hash) noexcept
|
|
{
|
|
if (!exportShareServiceStarted_.load(std::memory_order_acquire))
|
|
return;
|
|
|
|
try
|
|
{
|
|
auto const eventLedger = app_.getLedgerMaster().getLedgerByHash(hash);
|
|
auto const validated = app_.getLedgerMaster().getValidatedLedger();
|
|
if (!eventLedger || eventLedger->info().seq != seq || !validated ||
|
|
validated->info().seq < seq ||
|
|
!validated->rules().enabled(featureExport))
|
|
return;
|
|
auto const canonicalEventHash = seq == validated->info().seq
|
|
? std::optional<uint256>{validated->info().hash}
|
|
: hashOfSeq(*validated, seq, j_);
|
|
if (!canonicalEventHash || *canonicalEventHash != hash)
|
|
return;
|
|
|
|
auto const cfg = app_.getRuntimeConfig().getConsensusTestConfig();
|
|
if (cfg && cfg->noExportSig && *cfg->noExportSig)
|
|
return;
|
|
|
|
postValidationExportSigCollector_.cleanupStale(validated->info().seq);
|
|
|
|
// Replay the retained share view once at each validated cursor. Shares
|
|
// admitted concurrently are serialized by exportStreamMutex_: they
|
|
// either appear in this replay or publish afterward as edge events.
|
|
{
|
|
std::lock_guard streamLock(exportStreamMutex_);
|
|
if (!exportShareServiceStarted_.load(std::memory_order_acquire))
|
|
return;
|
|
if (lastExportReplaySeq_.load(std::memory_order_relaxed) <
|
|
validated->info().seq)
|
|
{
|
|
lastExportReplaySeq_.store(
|
|
validated->info().seq, std::memory_order_relaxed);
|
|
auto const live =
|
|
pendingExportLatches(*validated, validated->info().seq);
|
|
auto const unionSnapshot =
|
|
postValidationExportSigCollector_.fullUnionSnapshot();
|
|
for (auto const& [origin, latch] : live)
|
|
{
|
|
auto const it = unionSnapshot.find(origin);
|
|
if (it == unionSnapshot.end())
|
|
continue;
|
|
|
|
auto const originSeq = latch->getFieldU32(sfLedgerSequence);
|
|
auto const originHash = originSeq == validated->info().seq
|
|
? std::optional<uint256>{validated->info().hash}
|
|
: hashOfSeq(*validated, originSeq, j_);
|
|
if (!originHash)
|
|
continue;
|
|
|
|
for (auto const& contribution : it->second)
|
|
{
|
|
publishExportShareLocked(
|
|
ExportShare{
|
|
ExportShare::currentVersion,
|
|
latch->getAccountID(sfAccount),
|
|
origin,
|
|
originSeq,
|
|
*originHash,
|
|
origin,
|
|
contribution.position,
|
|
contribution.signingKey,
|
|
contribution.signature},
|
|
validated->info().seq,
|
|
validated->info().hash);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Retained replay owns the start of this cursor. Deferred relay shares
|
|
// are reconsidered afterward, so newly accepted entries publish as
|
|
// edge events before this node authors its own shares below.
|
|
retryDeferredExportShares(validated->info().seq);
|
|
|
|
std::vector<ExportShare> shares;
|
|
shares.reserve(ExportLimits::maxLiveExportLatches);
|
|
auto const& keys = app_.getValidatorKeys();
|
|
if (keys.keys && keys.nodeID != beast::zero)
|
|
{
|
|
forEachItem(
|
|
*validated,
|
|
keylet::pendingExports(),
|
|
[&](std::shared_ptr<SLE const> const& latch) {
|
|
try
|
|
{
|
|
if (!latch ||
|
|
shares.size() >=
|
|
ExportLimits::maxLiveExportLatches ||
|
|
latch->getType() != ltSHADOW_TICKET ||
|
|
!latch->isFieldPresent(sfTransactionHash) ||
|
|
!latch->isFieldPresent(sfExportUniverseHash) ||
|
|
!latch->isFieldPresent(sfExportCommittee) ||
|
|
!latch->isFieldPresent(sfLastLedgerSequence) ||
|
|
validated->info().seq >
|
|
latch->getFieldU32(sfLastLedgerSequence))
|
|
return;
|
|
|
|
auto const origin =
|
|
latch->getFieldH256(sfTransactionHash);
|
|
auto const originSeq =
|
|
latch->getFieldU32(sfLedgerSequence);
|
|
auto const originHash =
|
|
originSeq == validated->info().seq
|
|
? std::optional<uint256>{validated->info().hash}
|
|
: hashOfSeq(*validated, originSeq, j_);
|
|
if (!originHash)
|
|
return;
|
|
|
|
auto const universeHash =
|
|
latch->getFieldH256(sfExportUniverseHash);
|
|
auto const universeLedger =
|
|
app_.getLedgerMaster().getLedgerByHash(
|
|
universeHash);
|
|
if (!universeLedger)
|
|
return;
|
|
auto const validatorView =
|
|
makeActiveValidatorView(universeLedger);
|
|
if (!validatorView->fromUNLReport ||
|
|
!validatorView->sourceLedgerHash ||
|
|
*validatorView->sourceLedgerHash != universeHash)
|
|
return;
|
|
|
|
auto const committee = resolveExportCommittee(
|
|
makeSlice(latch->getFieldVL(sfExportCommittee)),
|
|
validatorView->orderedOriginalMasterKeys.size());
|
|
auto const position = std::find(
|
|
validatorView->orderedOriginalMasterKeys.begin(),
|
|
validatorView->orderedOriginalMasterKeys.end(),
|
|
keys.keys->masterPublicKey);
|
|
if (!committee ||
|
|
position ==
|
|
validatorView->orderedOriginalMasterKeys.end())
|
|
return;
|
|
auto const index =
|
|
static_cast<std::size_t>(std::distance(
|
|
validatorView->orderedOriginalMasterKeys
|
|
.begin(),
|
|
position));
|
|
if (!committee->members.contains(index) ||
|
|
postValidationExportSigCollector_.positionStatus(
|
|
origin, static_cast<std::uint16_t>(index)) !=
|
|
ExportSigCollectorV2::PositionStatus::empty)
|
|
return;
|
|
|
|
auto const originLedger =
|
|
app_.getLedgerMaster().getLedgerByHash(*originHash);
|
|
if (!originLedger ||
|
|
originLedger->info().parentHash != universeHash)
|
|
return;
|
|
auto const [outer, _] = originLedger->txRead(origin);
|
|
if (!outer)
|
|
return;
|
|
auto const inner =
|
|
ExportLedgerOps::innerExportedTx(*outer);
|
|
if (!inner)
|
|
return;
|
|
|
|
auto const targetNetworkID =
|
|
inner->isFieldPresent(sfNetworkID)
|
|
? inner->getFieldU32(sfNetworkID)
|
|
: std::uint32_t{0};
|
|
auto release = ExportOriginMemo::releaseForm(
|
|
*inner,
|
|
ExportOriginMemo::Origin{
|
|
app_.config().NETWORK_ID,
|
|
targetNetworkID,
|
|
origin},
|
|
ExportOriginMemo::Anchor{originSeq, *originHash});
|
|
if (!release)
|
|
return;
|
|
|
|
auto const signer = calcAccountID(keys.keys->publicKey);
|
|
auto const data =
|
|
buildMultiSigningData(release.value(), signer);
|
|
auto const signature = sign(
|
|
keys.keys->publicKey,
|
|
keys.keys->secretKey,
|
|
data.slice());
|
|
ExportShare share{
|
|
ExportShare::currentVersion,
|
|
latch->getAccountID(sfAccount),
|
|
origin,
|
|
originSeq,
|
|
*originHash,
|
|
origin,
|
|
static_cast<std::uint16_t>(index),
|
|
keys.keys->publicKey,
|
|
signature};
|
|
if (onExportShare(share))
|
|
shares.push_back(std::move(share));
|
|
}
|
|
catch (std::exception const& e)
|
|
{
|
|
JLOG(j_.warn())
|
|
<< "Export: skipped malformed pending latch"
|
|
<< " ledgerSeq=" << validated->info().seq
|
|
<< " error=" << e.what();
|
|
}
|
|
});
|
|
}
|
|
|
|
for (std::size_t first = 0; first < shares.size();
|
|
first += ExportLimits::maxExportSharesPerRelay)
|
|
{
|
|
protocol::TMExportShares message;
|
|
auto const last = std::min(
|
|
shares.size(), first + ExportLimits::maxExportSharesPerRelay);
|
|
for (auto i = first; i < last; ++i)
|
|
{
|
|
auto const frame = shares[i].serialize();
|
|
message.add_shares(frame.data(), frame.size());
|
|
}
|
|
app_.overlay().broadcast(message);
|
|
}
|
|
}
|
|
catch (std::exception const& e)
|
|
{
|
|
JLOG(j_.error()) << "Export: validated release failed seq=" << seq
|
|
<< " hash=" << hash << " error=" << e.what();
|
|
}
|
|
catch (...)
|
|
{
|
|
JLOG(j_.error()) << "Export: validated release failed seq=" << seq
|
|
<< " hash=" << hash << " error=unknown";
|
|
}
|
|
}
|
|
|
|
void
|
|
ConsensusExtensions::startExportShareService()
|
|
{
|
|
clearDeferredExportShares();
|
|
std::lock_guard streamLock(exportStreamMutex_);
|
|
exportStreamEmissionSeq_ = 0;
|
|
exportStreamEmittedShares_.clear();
|
|
lastExportReplaySeq_.store(0, std::memory_order_relaxed);
|
|
exportShareServiceStarted_.store(true, std::memory_order_release);
|
|
}
|
|
|
|
void
|
|
ConsensusExtensions::stopExportShareService() noexcept
|
|
{
|
|
exportShareServiceStarted_.store(false, std::memory_order_release);
|
|
{
|
|
std::lock_guard streamLock(exportStreamMutex_);
|
|
exportStreamEmissionSeq_ = 0;
|
|
exportStreamEmittedShares_.clear();
|
|
}
|
|
clearDeferredExportShares();
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// RNG Helper Methods
|
|
|
|
namespace {
|
|
|
|
std::string
|
|
buildObservedParticipantBitmap(
|
|
std::vector<NodeID> const& activeSorted,
|
|
hash_set<NodeID> const& observed)
|
|
{
|
|
std::string bitmapBin;
|
|
bitmapBin.reserve(activeSorted.size());
|
|
|
|
for (std::size_t i = 0; i < activeSorted.size(); ++i)
|
|
{
|
|
bool const seen = observed.count(activeSorted[i]) != 0;
|
|
bitmapBin.push_back(seen ? '1' : '0');
|
|
}
|
|
|
|
return bitmapBin;
|
|
}
|
|
|
|
Blob
|
|
buildEntropyContributorMask(
|
|
std::vector<PublicKey> const& orderedMasterKeys,
|
|
hash_set<NodeID> const& contributors)
|
|
{
|
|
return makeValidatorBitset(orderedMasterKeys.size(), [&](std::size_t i) {
|
|
return contributors.count(calcNodeID(orderedMasterKeys[i])) != 0;
|
|
});
|
|
}
|
|
|
|
uint256
|
|
entropyCommitment(
|
|
uint256 const& reveal,
|
|
PublicKey const& validatorKey,
|
|
LedgerIndex seq)
|
|
{
|
|
return sha512Half(reveal, validatorKey, seq);
|
|
}
|
|
|
|
bool
|
|
verifyProposalDigest(
|
|
PublicKey const& publicKey,
|
|
uint256 const& signingHash,
|
|
Slice const& signature)
|
|
{
|
|
// Proposal/validation signatures are secp256k1 digest signatures today.
|
|
// Proposal-carried sidecars can still name a valid ed25519 manifest/master
|
|
// key, so reject non-secp keys before verifyDigest, which aborts on them.
|
|
auto const type = publicKeyType(publicKey);
|
|
return type && *type == KeyType::secp256k1 &&
|
|
verifyDigest(publicKey, signingHash, signature);
|
|
}
|
|
|
|
struct AdmittedSidecarLeaf
|
|
{
|
|
STObject sidecar;
|
|
std::uint8_t type;
|
|
};
|
|
|
|
std::optional<AdmittedSidecarLeaf>
|
|
admitSidecarLeaf(
|
|
uint256 const& itemKey,
|
|
Slice const& entry,
|
|
uint256 const& setHash,
|
|
beast::Journal j,
|
|
char const* owner,
|
|
char const* kind,
|
|
char const* source,
|
|
std::optional<std::size_t> maxEntryBytes = std::nullopt,
|
|
bool* malformed = nullptr)
|
|
{
|
|
if (maxEntryBytes && entry.size() > *maxEntryBytes)
|
|
{
|
|
if (malformed)
|
|
*malformed = true;
|
|
JLOG(j.warn()) << owner << ": rejecting sidecar entry"
|
|
<< " reason=entry-too-large"
|
|
<< " kind=" << kind << " source=" << source
|
|
<< " setHash=" << setHash << " itemKey=" << itemKey
|
|
<< " entryBytes=" << entry.size()
|
|
<< " maxEntryBytes=" << *maxEntryBytes;
|
|
return std::nullopt;
|
|
}
|
|
|
|
SerialIter sit(entry);
|
|
STObject sidecar(sit, sfGeneric);
|
|
|
|
if (!sidecar.isFieldPresent(sfSidecarType))
|
|
return std::nullopt;
|
|
|
|
auto const sidecarHash = sidecar.getHash(HashPrefix::sidecar);
|
|
if (sidecarHash != itemKey)
|
|
{
|
|
if (malformed)
|
|
*malformed = true;
|
|
JLOG(j.warn()) << owner << ": rejecting sidecar entry"
|
|
<< " reason=item-key-mismatch"
|
|
<< " kind=" << kind << " source=" << source
|
|
<< " setHash=" << setHash << " itemKey=" << itemKey
|
|
<< " sidecarHash=" << sidecarHash;
|
|
return std::nullopt;
|
|
}
|
|
|
|
auto const type = sidecar.getFieldU8(sfSidecarType);
|
|
return AdmittedSidecarLeaf{std::move(sidecar), type};
|
|
}
|
|
|
|
boost::intrusive_ptr<SHAMapItem>
|
|
makeSidecarItem(STObject const& sidecar)
|
|
{
|
|
Serializer s;
|
|
sidecar.add(s);
|
|
auto const itemKey = sidecar.getHash(HashPrefix::sidecar);
|
|
XRPL_ASSERT(
|
|
itemKey == sha512Half(HashPrefix::sidecar, s.slice()),
|
|
"ripple::makeSidecarItem : sidecar hash matches serialized bytes");
|
|
return make_shamapitem(itemKey, s.slice());
|
|
}
|
|
|
|
//@@start active-validator-view-build
|
|
ActiveValidatorViewSource
|
|
buildActiveValidatorViewSource(
|
|
std::shared_ptr<Ledger const> const& sourceLedger)
|
|
{
|
|
ActiveValidatorViewSource source;
|
|
|
|
if (!sourceLedger)
|
|
return source;
|
|
|
|
source.sourceLedgerHash = sourceLedger->info().hash;
|
|
|
|
if (auto const sle = sourceLedger->read(keylet::UNLReport()))
|
|
{
|
|
if (sle->isFieldPresent(sfActiveValidators))
|
|
{
|
|
hash_set<PublicKey> reportKeys;
|
|
for (auto const& obj : sle->getFieldArray(sfActiveValidators))
|
|
{
|
|
auto const pk = obj.getFieldVL(sfPublicKey);
|
|
if (!publicKeyType(makeSlice(pk)))
|
|
continue;
|
|
|
|
reportKeys.insert(PublicKey{makeSlice(pk)});
|
|
}
|
|
|
|
if (!reportKeys.empty())
|
|
source.unlReportMasterKeys = std::move(reportKeys);
|
|
}
|
|
}
|
|
|
|
// UNLReport records recently active validators; NegativeUNL is the separate
|
|
// ledger policy overlay that core consensus applies to quorum.
|
|
source.negativeUNLEnabled =
|
|
sourceLedger->rules().enabled(featureNegativeUNL);
|
|
if (source.negativeUNLEnabled)
|
|
source.negativeUNL = sourceLedger->negativeUNL();
|
|
|
|
return source;
|
|
}
|
|
|
|
ActiveValidatorViewFallback
|
|
buildActiveValidatorViewFallback(Application& app)
|
|
{
|
|
ActiveValidatorViewFallback fallback;
|
|
|
|
// Fallback exists for early ledgers and dev/test networks before the report
|
|
// object is available. It is deliberately the configured trusted master-key
|
|
// set so manifest signing keys still resolve through trust.
|
|
fallback.trustedMasterKeys = app.validators().getTrustedMasterKeys();
|
|
|
|
// Some standalone/dev configurations trust local validation implicitly. The
|
|
// builder dedupes this if self is already in the trusted set.
|
|
auto const& valKeys = app.getValidatorKeys();
|
|
if (valKeys.keys && valKeys.nodeID != beast::zero)
|
|
fallback.localMasterKey = valKeys.keys->masterPublicKey;
|
|
|
|
return fallback;
|
|
}
|
|
//@@end active-validator-view-build
|
|
|
|
} // namespace
|
|
|
|
std::size_t
|
|
ConsensusExtensions::quorumThreshold() const
|
|
{
|
|
// Validator_quorum entropy uses a fixed 80% threshold over the effective
|
|
// active UNL snapshot. Tier 2 participant_aligned entropy has its own
|
|
// lower intersection-safe floor; recent proposers are useful for liveness
|
|
// heuristics, but they do not lower either threshold.
|
|
// Use the shared validator view so Tier 3 RNG and Export use the same
|
|
// denominator.
|
|
auto const base = activeValidatorView()->size();
|
|
return safeQuorumThreshold(base);
|
|
}
|
|
|
|
std::size_t
|
|
ConsensusExtensions::exportRootAlignmentThreshold() const
|
|
{
|
|
return exportRootAlignmentThreshold(*activeValidatorView());
|
|
}
|
|
|
|
std::size_t
|
|
ConsensusExtensions::exportRootAlignmentThreshold(
|
|
ActiveValidatorView const& validatorView)
|
|
{
|
|
auto const base = validatorView.size();
|
|
|
|
// Export sidecar hashes are signed through ExtendedPosition even when RNG
|
|
// is disabled, so a quorum-aligned exportSigSetHash is deterministic
|
|
// enough for Export-only mode. Unanimity would let one active validator
|
|
// veto an otherwise converged export round.
|
|
return safeQuorumThreshold(base);
|
|
}
|
|
|
|
std::size_t
|
|
ConsensusExtensions::exportWitnessThreshold() const
|
|
{
|
|
return exportWitnessThreshold(*activeValidatorView());
|
|
}
|
|
|
|
std::size_t
|
|
ConsensusExtensions::exportWitnessThreshold(
|
|
ActiveValidatorView const& validatorView)
|
|
{
|
|
// The full active view is also the witness authority today. Keep this
|
|
// authorization threshold separate from root alignment so a bounded
|
|
// committee can replace the witness authority without weakening qV.
|
|
return safeQuorumThreshold(validatorView.size());
|
|
}
|
|
|
|
bool
|
|
ConsensusExtensions::exportAuthorityFitsTargetSignerCap(
|
|
ActiveValidatorView const& validatorView,
|
|
std::size_t targetSignerCap)
|
|
{
|
|
// NegativeUNL is temporary. The destination signer policy must be able to
|
|
// represent every validator that can return to the effective source view.
|
|
return validatorView.originalViewSize <= targetSignerCap;
|
|
}
|
|
|
|
std::size_t
|
|
ConsensusExtensions::tier2Threshold() const
|
|
{
|
|
// Tier 2 (participant_aligned) lowers the alignment bar from the 80%
|
|
// validator-quorum gate to the quorum-intersection floor over the ORIGINAL
|
|
// (pre-nUNL) view (~60%; exact value from calculateParticipantThreshold,
|
|
// which keeps two aligned cohorts sharing an honest validator so an
|
|
// equivocator cannot mint two distinct digests). Anchored to
|
|
// originalViewSize, not size(): nUNL can shrink the effective view while
|
|
// leaving faulty nodes in it, so a fraction of the effective view could
|
|
// exceed the Byzantine fraction (which is bounded over the original UNL).
|
|
// Regressing this to size() is a consensus fork under nUNL and is pinned by
|
|
// ConsensusExtensions_test::testTier2ThresholdAnchorsToOriginalView.
|
|
auto const base = activeValidatorView()->originalViewSize;
|
|
return safeParticipantThreshold(base);
|
|
}
|
|
|
|
std::size_t
|
|
ConsensusExtensions::entropyGateThreshold() const
|
|
{
|
|
// The bar at which the commit/reveal/entropy pipeline engages and the
|
|
// entropy conflict gate resolves: the lowest ENABLED accepted tier's
|
|
// threshold. In the normal band tier2Threshold (~0.6*original) < quorum
|
|
// (0.8*effective), so this is the participant-alignment floor and
|
|
// sub-quorum rounds reach injection. Under nUNL the exact integer
|
|
// thresholds can cross either way; that only changes which threshold lets
|
|
// the pipeline proceed. Final tier labels are computed later from the
|
|
// agreed entropy set count in selectEntropy(). Non-fallback tier labels are
|
|
// allowed only when the round view is anchored by UNLReport; the
|
|
// trusted-fallback view is local configuration and selectEntropy() maps it
|
|
// to consensus_fallback.
|
|
auto const view = activeValidatorView();
|
|
return entropyGateThresholdForView(view->size(), view->originalViewSize);
|
|
}
|
|
|
|
std::size_t
|
|
ConsensusExtensions::entropyGateThresholdForView(
|
|
std::size_t effectiveViewSize,
|
|
std::size_t originalViewSize)
|
|
{
|
|
auto const quorum = safeQuorumThreshold(effectiveViewSize);
|
|
auto const tier2 = safeParticipantThreshold(originalViewSize);
|
|
return std::min(quorum, tier2);
|
|
}
|
|
|
|
EntropyTier
|
|
ConsensusExtensions::selectEntropyTierForView(
|
|
bool fromUNLReport,
|
|
std::size_t participantCount,
|
|
std::size_t effectiveViewSize,
|
|
std::size_t originalViewSize)
|
|
{
|
|
if (!fromUNLReport)
|
|
return entropyTierConsensusFallback;
|
|
|
|
if (effectiveViewSize > 0 && participantCount == effectiveViewSize)
|
|
return entropyTierValidatorFull;
|
|
|
|
auto const quorum = safeQuorumThreshold(effectiveViewSize);
|
|
if (participantCount >= quorum)
|
|
return entropyTierValidatorQuorum;
|
|
|
|
auto const tier2 = safeParticipantThreshold(originalViewSize);
|
|
if (participantCount >= tier2)
|
|
return entropyTierParticipantAligned;
|
|
|
|
return entropyTierConsensusFallback;
|
|
}
|
|
|
|
void
|
|
ConsensusExtensions::setExpectedProposers(hash_set<NodeID> proposers)
|
|
{
|
|
bool const includeSelf = mode_ == ConsensusMode::proposing &&
|
|
app_.getValidatorKeys().keys &&
|
|
app_.getValidatorKeys().nodeID != beast::zero;
|
|
|
|
if (!proposers.empty())
|
|
{
|
|
// Intersect recent proposers with the active UNL. This set is used as
|
|
// a liveness hint only; commit quorum itself remains fixed to the
|
|
// active UNL snapshot for the round.
|
|
auto const validatorView = activeValidatorView();
|
|
hash_set<NodeID> filtered;
|
|
for (auto const& id : proposers)
|
|
{
|
|
if (!includeSelf && id == app_.getValidatorKeys().nodeID)
|
|
continue;
|
|
// Recent proposers are only a liveness hint; filter them through
|
|
// the same active view that defines commit quorum membership.
|
|
if (validatorView->containsNode(id))
|
|
filtered.insert(id);
|
|
}
|
|
if (includeSelf)
|
|
filtered.insert(app_.getValidatorKeys().nodeID);
|
|
likelyParticipants_ = std::move(filtered);
|
|
JLOG(j_.trace()) << "RNG: likelyParticipants"
|
|
<< " source=recent-proposers"
|
|
<< " count=" << likelyParticipants_.size()
|
|
<< " input=" << proposers.size()
|
|
<< " includeSelf=" << (includeSelf ? "yes" : "no")
|
|
<< " activeValidators=" << validatorView->size();
|
|
return;
|
|
}
|
|
|
|
// First round (or no recent data): fall back to the active UNL snapshot as
|
|
// our best guess for who may still contribute before timeout.
|
|
auto const validatorView = activeValidatorView();
|
|
if (validatorView->size() > 0)
|
|
{
|
|
likelyParticipants_ = validatorView->nodeIds;
|
|
JLOG(j_.trace()) << "RNG: likelyParticipants"
|
|
<< " source=active-validator-view"
|
|
<< " count=" << likelyParticipants_.size()
|
|
<< " activeValidators=" << validatorView->size()
|
|
<< " viewSource="
|
|
<< (validatorView->fromUNLReport ? "UNLReport"
|
|
: "trusted-fallback");
|
|
return;
|
|
}
|
|
|
|
// No data at all (shouldn't happen — cacheUNLReport falls back to
|
|
// trusted keys). Leave empty; diagnostics will show no liveness hint.
|
|
JLOG(j_.warn()) << "RNG: likelyParticipants unavailable"
|
|
<< " reason=empty-active-validator-view";
|
|
}
|
|
|
|
std::size_t
|
|
ConsensusExtensions::pendingCommitCount() const
|
|
{
|
|
return pendingCommits_.size();
|
|
}
|
|
|
|
bool
|
|
ConsensusExtensions::hasProofedCommit(NodeID const& nodeId) const
|
|
{
|
|
return pendingCommits_.count(nodeId) > 0 && commitProofs_.count(nodeId) > 0;
|
|
}
|
|
|
|
bool
|
|
ConsensusExtensions::hasActiveProofedCommit(
|
|
NodeID const& nodeId,
|
|
ActiveValidatorView const& validatorView) const
|
|
{
|
|
return validatorView.containsNode(nodeId) &&
|
|
nodeIdToKey_.count(nodeId) > 0 && hasProofedCommit(nodeId);
|
|
}
|
|
|
|
std::size_t
|
|
ConsensusExtensions::proofedCommitCount() const
|
|
{
|
|
auto const validatorView = activeValidatorView();
|
|
return std::count_if(
|
|
pendingCommits_.begin(),
|
|
pendingCommits_.end(),
|
|
[this, validatorView](auto const& entry) {
|
|
auto const& nid = entry.first;
|
|
// Match buildCommitSet(): only commits that can be emitted as
|
|
// verifiable sidecar leaves count toward any commit threshold.
|
|
return hasActiveProofedCommit(nid, *validatorView);
|
|
});
|
|
}
|
|
|
|
std::size_t
|
|
ConsensusExtensions::pendingRevealCount() const
|
|
{
|
|
return pendingReveals_.size();
|
|
}
|
|
|
|
std::size_t
|
|
ConsensusExtensions::proofedRevealCount() const
|
|
{
|
|
auto const validatorView = activeValidatorView();
|
|
return std::count_if(
|
|
pendingReveals_.begin(),
|
|
pendingReveals_.end(),
|
|
[this, validatorView](auto const& entry) {
|
|
auto const& nid = entry.first;
|
|
return hasActiveProofedCommit(nid, *validatorView);
|
|
});
|
|
}
|
|
|
|
bool
|
|
ConsensusExtensions::ingestRngContribution(
|
|
NodeID const& nodeId,
|
|
PublicKey const& publicKey,
|
|
RngContributionKind kind,
|
|
uint256 const& digest,
|
|
std::optional<LedgerIndex> seq,
|
|
std::optional<ProposalProof> const& proof,
|
|
char const* sourceTag,
|
|
RngProofCachePolicy proofCachePolicy)
|
|
{
|
|
bool const isCommit = kind == RngContributionKind::commit;
|
|
char const* kindName = isCommit ? "commit" : "reveal";
|
|
|
|
if (!isUNLReportMember(nodeId))
|
|
{
|
|
JLOG(j_.trace()) << "RNG: rejecting contribution"
|
|
<< " reason=non-active-validator"
|
|
<< " kind=" << kindName << " source=" << sourceTag
|
|
<< " node=" << nodeId;
|
|
return false;
|
|
}
|
|
|
|
//@@start rng-contribution-identity-gate
|
|
auto const directMaster = calcNodeID(publicKey) == nodeId;
|
|
auto const trustedMaster = directMaster
|
|
? std::optional<PublicKey>{publicKey}
|
|
: app_.validators().getTrustedKey(publicKey);
|
|
if (!trustedMaster || calcNodeID(*trustedMaster) != nodeId)
|
|
{
|
|
JLOG(j_.warn()) << "RNG: rejecting contribution"
|
|
<< " reason=node-key-mismatch"
|
|
<< " kind=" << kindName << " source=" << sourceTag
|
|
<< " node=" << nodeId;
|
|
return false;
|
|
}
|
|
//@@end rng-contribution-identity-gate
|
|
|
|
if (isCommit)
|
|
{
|
|
auto const existing = pendingCommits_.find(nodeId);
|
|
bool const hasSeq0Proof = proof && proof->proposeSeq == 0;
|
|
if (commitSetFrozen_)
|
|
{
|
|
if (existing != pendingCommits_.end() && existing->second == digest)
|
|
{
|
|
JLOG(j_.trace())
|
|
<< "RNG: ignoring duplicate commitment after freeze"
|
|
<< " source=" << sourceTag << " node=" << nodeId
|
|
<< " commit=" << digest;
|
|
}
|
|
else
|
|
{
|
|
JLOG(j_.warn())
|
|
<< "RNG: rejecting changed commitment after freeze"
|
|
<< " source=" << sourceTag << " node=" << nodeId
|
|
<< " new=" << digest << " existing="
|
|
<< (existing != pendingCommits_.end()
|
|
? to_string(existing->second)
|
|
: std::string{"none"});
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (existing != pendingCommits_.end() && existing->second != digest)
|
|
{
|
|
if (hasProofedCommit(nodeId) &&
|
|
proofCachePolicy == RngProofCachePolicy::keepExisting)
|
|
{
|
|
JLOG(j_.warn())
|
|
<< "RNG: ignoring changed commitment"
|
|
<< " reason=existing-proofed-commit"
|
|
<< " source=" << sourceTag << " node=" << nodeId
|
|
<< " old=" << existing->second << " new=" << digest;
|
|
return false;
|
|
}
|
|
|
|
JLOG(j_.warn()) << "RNG: replacing changed commitment"
|
|
<< " source=" << sourceTag << " node=" << nodeId
|
|
<< " old=" << existing->second << " new=" << digest
|
|
<< " proofed=" << (hasSeq0Proof ? "yes" : "no");
|
|
|
|
existing->second = digest;
|
|
pendingReveals_.erase(nodeId);
|
|
if (!hasSeq0Proof)
|
|
commitProofs_.erase(nodeId);
|
|
}
|
|
else
|
|
{
|
|
pendingCommits_.emplace(nodeId, digest);
|
|
}
|
|
|
|
nodeIdToKey_.insert_or_assign(nodeId, publicKey);
|
|
|
|
if (proof)
|
|
{
|
|
if (proof->proposeSeq == 0)
|
|
{
|
|
if (proofCachePolicy == RngProofCachePolicy::replaceExisting)
|
|
commitProofs_.insert_or_assign(nodeId, *proof);
|
|
else
|
|
commitProofs_.emplace(nodeId, *proof);
|
|
}
|
|
else
|
|
{
|
|
JLOG(j_.debug())
|
|
<< "RNG: commit proof not cached"
|
|
<< " reason=nonzero-propose-seq"
|
|
<< " source=" << sourceTag << " node=" << nodeId
|
|
<< " proposeSeq=" << proof->proposeSeq << " seq="
|
|
<< (seq ? std::to_string(*seq) : std::string{"unknown"});
|
|
}
|
|
}
|
|
|
|
JLOG(j_.trace()) << "RNG: admitted contribution"
|
|
<< " kind=commit"
|
|
<< " source=" << sourceTag << " node=" << nodeId
|
|
<< " proofed="
|
|
<< (commitProofs_.count(nodeId) ? "yes" : "no");
|
|
return true;
|
|
}
|
|
|
|
if (!seq)
|
|
{
|
|
JLOG(j_.warn()) << "RNG: rejecting contribution"
|
|
<< " reason=unknown-sequence"
|
|
<< " kind=reveal"
|
|
<< " source=" << sourceTag << " node=" << nodeId;
|
|
return false;
|
|
}
|
|
|
|
//@@start rng-reveal-commitment-proof-gate
|
|
auto const commitIt = pendingCommits_.find(nodeId);
|
|
if (commitIt == pendingCommits_.end())
|
|
{
|
|
JLOG(j_.debug()) << "RNG: rejecting contribution"
|
|
<< " reason=reveal-without-commitment"
|
|
<< " kind=reveal"
|
|
<< " source=" << sourceTag << " node=" << nodeId
|
|
<< " seq=" << *seq;
|
|
return false;
|
|
}
|
|
if (!hasProofedCommit(nodeId))
|
|
{
|
|
JLOG(j_.debug()) << "RNG: rejecting contribution"
|
|
<< " reason=reveal-without-proofed-commit"
|
|
<< " kind=reveal"
|
|
<< " source=" << sourceTag << " node=" << nodeId
|
|
<< " seq=" << *seq;
|
|
return false;
|
|
}
|
|
|
|
auto const expectedCommit = entropyCommitment(digest, publicKey, *seq);
|
|
if (expectedCommit != commitIt->second)
|
|
{
|
|
JLOG(j_.warn()) << "RNG: rejecting contribution"
|
|
<< " reason=reveal-commitment-mismatch"
|
|
<< " kind=reveal"
|
|
<< " source=" << sourceTag << " node=" << nodeId
|
|
<< " seq=" << *seq << " expected=" << commitIt->second
|
|
<< " calculated=" << expectedCommit;
|
|
return false;
|
|
}
|
|
//@@end rng-reveal-commitment-proof-gate
|
|
|
|
auto [it, inserted] = pendingReveals_.emplace(nodeId, digest);
|
|
if (!inserted && it->second != digest)
|
|
{
|
|
JLOG(j_.warn()) << "RNG: validator changed reveal"
|
|
<< " source=" << sourceTag << " node=" << nodeId
|
|
<< " old=" << it->second << " new=" << digest;
|
|
it->second = digest;
|
|
}
|
|
nodeIdToKey_.insert_or_assign(nodeId, publicKey);
|
|
JLOG(j_.trace()) << "RNG: admitted contribution"
|
|
<< " kind=reveal"
|
|
<< " source=" << sourceTag << " node=" << nodeId
|
|
<< " seq=" << *seq;
|
|
return true;
|
|
}
|
|
|
|
std::size_t
|
|
ConsensusExtensions::expectedProposerCount() const
|
|
{
|
|
return likelyParticipants_.size();
|
|
}
|
|
|
|
bool
|
|
ConsensusExtensions::hasQuorumOfCommits() const
|
|
{
|
|
auto const threshold = quorumThreshold();
|
|
auto const activeValidators = activeValidatorView()->size();
|
|
auto const proofedCommitCount = this->proofedCommitCount();
|
|
bool result = static_cast<std::size_t>(proofedCommitCount) >= threshold;
|
|
JLOG(j_.trace()) << "RNG: commit quorum check"
|
|
<< " proofedCommits=" << proofedCommitCount
|
|
<< " threshold=" << threshold
|
|
<< " result=" << (result ? "yes" : "no")
|
|
<< " pendingCommits=" << pendingCommits_.size()
|
|
<< " activeValidators=" << activeValidators
|
|
<< " likelyParticipants=" << likelyParticipants_.size();
|
|
return result;
|
|
}
|
|
|
|
bool
|
|
ConsensusExtensions::hasMinimumReveals() const
|
|
{
|
|
// Wait for reveals from ALL committers, not just 80%. The commit
|
|
// set is deterministic (SHAMap agreed), so we know exactly which
|
|
// validators should reveal. Waiting for all of them ensures every
|
|
// node builds the same entropy set. rngPIPELINE_TIMEOUT in
|
|
// Consensus.h is the safety valve for nodes that crash/partition
|
|
// between commit and reveal.
|
|
// Reveal quorum targets the commit sidecar set, not every later proposal
|
|
// commitment we heard. That keeps proofless late commits from extending
|
|
// the reveal wait after they were excluded from buildCommitSet().
|
|
auto const expected = proofedCommitCount();
|
|
auto const revealCount = proofedRevealCount();
|
|
auto const activeValidators = activeValidatorView()->size();
|
|
bool result = revealCount >= expected;
|
|
JLOG(j_.trace()) << "RNG: reveal quorum check"
|
|
<< " reveals=" << revealCount << " expected=" << expected
|
|
<< " result=" << (result ? "yes" : "no")
|
|
<< " pendingReveals=" << pendingReveals_.size()
|
|
<< " pendingCommits=" << pendingCommits_.size()
|
|
<< " activeValidators=" << activeValidators;
|
|
return result;
|
|
}
|
|
|
|
bool
|
|
ConsensusExtensions::hasAnyReveals() const
|
|
{
|
|
return !pendingReveals_.empty();
|
|
}
|
|
|
|
void
|
|
ConsensusExtensions::acceptEntropySet(uint256 const& hash)
|
|
{
|
|
acceptedEntropySetHash_ = hash;
|
|
}
|
|
|
|
void
|
|
ConsensusExtensions::clearAcceptedEntropySet()
|
|
{
|
|
acceptedEntropySetHash_.reset();
|
|
}
|
|
|
|
ConsensusExtensions::EntropySelection
|
|
ConsensusExtensions::selectEntropy(
|
|
uint256 const& agreedTxSetHash,
|
|
LedgerIndex seq) const
|
|
{
|
|
//@@start entropy-selector-fallback
|
|
// Tier 1 fallback: consensus-bound deterministic digest over already-agreed
|
|
// round inputs. agreedTxSetHash is the pre-injection consensus tx set hash:
|
|
// the digest must never depend on a set that could contain the pseudo-tx
|
|
// carrying it (circular).
|
|
auto const fallback = [&]() -> EntropySelection {
|
|
return {
|
|
sha512Half(
|
|
HashPrefix::entropyFallback,
|
|
roundPrevLedgerHash_,
|
|
agreedTxSetHash,
|
|
seq),
|
|
entropyTierConsensusFallback,
|
|
0,
|
|
0,
|
|
{}};
|
|
};
|
|
//@@end entropy-selector-fallback
|
|
|
|
// Standalone/dev: synthetic deterministic entropy so hook dice/random work.
|
|
//@@start entropy-selector-standalone
|
|
if (app_.config().standalone())
|
|
{
|
|
auto const cfg = app_.getRuntimeConfig().getConsensusTestConfig();
|
|
auto const tier = static_cast<std::uint8_t>(std::clamp(
|
|
cfg && cfg->standaloneEntropyTier ? *cfg->standaloneEntropyTier
|
|
: entropyTierValidatorFull,
|
|
0,
|
|
static_cast<int>(entropyTierValidatorFull)));
|
|
auto const toU16 = [](int value) {
|
|
return static_cast<std::uint16_t>(std::clamp(
|
|
value,
|
|
0,
|
|
static_cast<int>(std::numeric_limits<std::uint16_t>::max())));
|
|
};
|
|
auto const count = toU16(
|
|
cfg && cfg->standaloneEntropyCount ? *cfg->standaloneEntropyCount
|
|
: 20);
|
|
auto const denominator = toU16(
|
|
cfg && cfg->standaloneEntropyDenominator
|
|
? *cfg->standaloneEntropyDenominator
|
|
: 20);
|
|
Blob contributors((denominator + 7) / 8, 0);
|
|
auto const contributorCount = std::min(count, denominator);
|
|
for (std::uint16_t i = 0; i < contributorCount; ++i)
|
|
contributors[i / 8] |= static_cast<std::uint8_t>(1u << (i % 8));
|
|
return {
|
|
sha512Half(std::string("standalone-entropy"), seq),
|
|
tier,
|
|
count,
|
|
denominator,
|
|
contributors};
|
|
}
|
|
//@@end entropy-selector-standalone
|
|
|
|
// Non-fallback entropy labels depend on validator-view thresholds. Without
|
|
// an on-ledger UNLReport, that view is derived from local trusted config,
|
|
// so two nodes can agree on the same entropy set but label it with
|
|
// different tiers. A non-standalone node therefore mints only
|
|
// consensus_fallback until the round view is ledger-anchored.
|
|
auto const validatorView = activeValidatorView();
|
|
//@@start entropy-selector-unlreport-gate
|
|
if (!validatorView->fromUNLReport)
|
|
{
|
|
if (validatorView->sourceLedgerHash)
|
|
{
|
|
XRPL_ASSERT(
|
|
*validatorView->sourceLedgerHash == roundPrevLedgerHash_,
|
|
"ripple::ConsensusExtensions::selectEntropy : "
|
|
"active view source matches round parent");
|
|
}
|
|
JLOG(j_.warn()) << "RNG: using consensus fallback entropy"
|
|
<< " reason=no-unl-report"
|
|
<< " seq=" << seq
|
|
<< " activeValidators=" << validatorView->size()
|
|
<< " originalView=" << validatorView->originalViewSize
|
|
<< " sourceLedgerHash="
|
|
<< (validatorView->sourceLedgerHash
|
|
? to_string(*validatorView->sourceLedgerHash)
|
|
: std::string{"none"})
|
|
<< " roundPrevLedgerHash=" << roundPrevLedgerHash_;
|
|
return fallback();
|
|
}
|
|
//@@end entropy-selector-unlreport-gate
|
|
|
|
//@@start entropy-selector-accepted-map-consumption
|
|
// A cached entropySetMap_ is only candidate material. The tick gate marks
|
|
// the sidecar hash accepted after peer-observation/alignment checks have
|
|
// completed; injection never consults local timeout state directly.
|
|
if (!acceptedEntropySetHash_ || !entropySetMap_ ||
|
|
entropySetMap_->getHash().as_uint256() != *acceptedEntropySetHash_)
|
|
return fallback();
|
|
auto const agreedHash = *acceptedEntropySetHash_;
|
|
|
|
// Derive from the AGREED entropySetMap_ — NOT local pendingReveals_. The
|
|
// map's hash was published in proposals and accepted by the gate, so every
|
|
// node holding the same entropySetHash produces byte-identical entropy and
|
|
// the same tier/count/denominator labels. Read leaves through the shared
|
|
// sidecar admission helper so accepted-map consumption enforces the same
|
|
// content-address/type contract as snapshot construction. Do not resolve
|
|
// sfSigningPubKey through live manifests here: ingress already
|
|
// authenticated the signing-key -> master-NodeID binding before the local
|
|
// snapshot was built, and re-reading mutable manifests at materialization
|
|
// would reintroduce a local-state fork surface.
|
|
std::vector<std::pair<PublicKey, uint256>> sorted;
|
|
hash_set<NodeID> contributors;
|
|
hash_set<PublicKey> signingKeys;
|
|
bool malformedContributorSet = false;
|
|
entropySetMap_->visitLeaves(
|
|
[&](boost::intrusive_ptr<SHAMapItem const> const& item) {
|
|
try
|
|
{
|
|
auto admitted = admitSidecarLeaf(
|
|
item->key(),
|
|
item->slice(),
|
|
agreedHash,
|
|
j_,
|
|
"RNG",
|
|
"entropySet",
|
|
"select");
|
|
if (!admitted)
|
|
{
|
|
malformedContributorSet = true;
|
|
return;
|
|
}
|
|
if (admitted->type != sidecarRngReveal)
|
|
{
|
|
malformedContributorSet = true;
|
|
return;
|
|
}
|
|
auto const& sidecar = admitted->sidecar;
|
|
if (!sidecar.isFieldPresent(sfAccount))
|
|
{
|
|
malformedContributorSet = true;
|
|
return;
|
|
}
|
|
auto const account = sidecar.getAccountID(sfAccount);
|
|
auto const nodeId = NodeID::fromVoid(account.data());
|
|
if (!validatorView->containsNode(nodeId))
|
|
{
|
|
malformedContributorSet = true;
|
|
return;
|
|
}
|
|
if (!contributors.insert(nodeId).second)
|
|
{
|
|
malformedContributorSet = true;
|
|
return;
|
|
}
|
|
|
|
auto const pk = sidecar.getFieldVL(sfSigningPubKey);
|
|
if (!publicKeyType(makeSlice(pk)))
|
|
{
|
|
malformedContributorSet = true;
|
|
return;
|
|
}
|
|
PublicKey const signingKey{makeSlice(pk)};
|
|
if (!signingKeys.insert(signingKey).second)
|
|
{
|
|
malformedContributorSet = true;
|
|
return;
|
|
}
|
|
sorted.emplace_back(signingKey, sidecar.getFieldH256(sfDigest));
|
|
}
|
|
catch (...)
|
|
{
|
|
malformedContributorSet = true;
|
|
}
|
|
});
|
|
|
|
// Residual: the gate passed but no leaf parsed — fall back rather than
|
|
// skip, so a fresh ConsensusEntropy entry always exists.
|
|
if (sorted.empty())
|
|
return fallback();
|
|
if (malformedContributorSet || contributors.size() != sorted.size())
|
|
return fallback();
|
|
//@@end entropy-selector-accepted-map-consumption
|
|
|
|
//@@start entropy-selector-normalize-digest-mask
|
|
std::sort(sorted.begin(), sorted.end(), [](auto const& a, auto const& b) {
|
|
if (a.first.slice() < b.first.slice())
|
|
return true;
|
|
if (b.first.slice() < a.first.slice())
|
|
return false;
|
|
return a.second < b.second;
|
|
});
|
|
|
|
Serializer s;
|
|
for (auto const& [key, reveal] : sorted)
|
|
{
|
|
s.addVL(key.slice());
|
|
s.addBitString(reveal);
|
|
}
|
|
auto const digest = sha512Half(s.slice());
|
|
auto const count = static_cast<std::uint16_t>(sorted.size());
|
|
auto const denominator = static_cast<std::uint16_t>(validatorView->size());
|
|
auto const contributorMask = buildEntropyContributorMask(
|
|
validatorView->orderedMasterKeys, contributors);
|
|
//@@end entropy-selector-normalize-digest-mask
|
|
|
|
//@@start entropy-selector-tier-ladder
|
|
// Tier ladder over the AGREED participant count — deterministic on every
|
|
// node holding this entropySetHash. quorumThreshold() = ceil(0.8 *
|
|
// effective view); tier2Threshold() = the equivocation-safe intersection
|
|
// floor over the original view (~0.6*n; see calculateParticipantThreshold).
|
|
// Below tier2Threshold too few aligned participants contributed to trust
|
|
// the result — fall back.
|
|
auto const tier = selectEntropyTierForView(
|
|
validatorView->fromUNLReport,
|
|
count,
|
|
validatorView->size(),
|
|
validatorView->originalViewSize);
|
|
if (tier != entropyTierConsensusFallback)
|
|
return {
|
|
digest,
|
|
static_cast<std::uint8_t>(tier),
|
|
count,
|
|
denominator,
|
|
contributorMask};
|
|
return fallback();
|
|
//@@end entropy-selector-tier-ladder
|
|
}
|
|
|
|
bool
|
|
ConsensusExtensions::rngEnabled() const
|
|
{
|
|
return rngEnabledThisRound_.load(std::memory_order_relaxed);
|
|
}
|
|
|
|
uint256
|
|
ConsensusExtensions::txnOrderingSalt(
|
|
uint256 const& agreedTxSetHash,
|
|
LedgerIndex seq) const
|
|
{
|
|
if (!rngEnabled())
|
|
return agreedTxSetHash;
|
|
|
|
auto const selection = selectEntropy(agreedTxSetHash, seq);
|
|
return sha512Half(
|
|
HashPrefix::entropyTxnOrder,
|
|
agreedTxSetHash,
|
|
selection.digest,
|
|
selection.tier,
|
|
selection.count,
|
|
selection.denominator);
|
|
}
|
|
|
|
bool
|
|
ConsensusExtensions::exportEnabled() const
|
|
{
|
|
return exportEnabledThisRound_.load(std::memory_order_relaxed);
|
|
}
|
|
|
|
bool
|
|
ConsensusExtensions::testSuppressExportSigSetHash() const
|
|
{
|
|
auto const cfg = app_.getRuntimeConfig().getConsensusTestConfig();
|
|
return cfg && cfg->noExportSigHash.has_value() && *cfg->noExportSigHash;
|
|
}
|
|
|
|
bool
|
|
ConsensusExtensions::testBootstrapFastStartEnabled() const
|
|
{
|
|
auto const cfg = app_.getRuntimeConfig().getConsensusTestConfig();
|
|
if (cfg && cfg->bootstrapFastStart.has_value())
|
|
return *cfg->bootstrapFastStart;
|
|
return false;
|
|
}
|
|
|
|
uint256
|
|
ConsensusExtensions::buildCommitSet(LedgerIndex seq)
|
|
{
|
|
// Track the active RNG round explicitly. Nodes in observing/switching
|
|
// mode can have a closed ledger index behind the consensus round while
|
|
// still building that round's local RNG snapshots.
|
|
rngRoundSeq_ = seq;
|
|
|
|
auto map =
|
|
std::make_shared<SHAMap>(SHAMapType::SIDECAR, app_.getNodeFamily());
|
|
map->setUnbacked();
|
|
|
|
auto const validatorView = activeValidatorView();
|
|
std::size_t entryCount = 0;
|
|
// NOTE: avoid structured bindings in for-loops containing lambdas —
|
|
// clang-14 (CI) rejects capturing them (P2036R3 not implemented).
|
|
for (auto const& entry : pendingCommits_)
|
|
{
|
|
auto const& nid = entry.first;
|
|
auto const& commit = entry.second;
|
|
|
|
// Commit sidecars are consensus inputs, so only publish leaves from
|
|
// the proofed commit set over the frozen quorum validator view.
|
|
if (!hasActiveProofedCommit(nid, *validatorView))
|
|
continue;
|
|
|
|
auto kit = nodeIdToKey_.find(nid);
|
|
if (kit == nodeIdToKey_.end())
|
|
continue;
|
|
auto proofIt = commitProofs_.find(nid);
|
|
if (proofIt == commitProofs_.end())
|
|
continue;
|
|
|
|
// Encode the NodeID into sfAccount so the local snapshot can be
|
|
// replayed without recomputing master-vs-signing key identity.
|
|
AccountID acctId;
|
|
std::memcpy(acctId.data(), nid.data(), acctId.size());
|
|
|
|
STObject sidecar(sfGeneric);
|
|
sidecar.setFieldU8(sfSidecarType, sidecarRngCommit);
|
|
sidecar.setFieldU32(sfLedgerSequence, seq);
|
|
sidecar.setAccountID(sfAccount, acctId);
|
|
sidecar.setFieldH256(sfDigest, commit);
|
|
sidecar.setFieldVL(sfSigningPubKey, kit->second.slice());
|
|
sidecar.setFieldVL(sfBlob, serializeProof(proofIt->second));
|
|
|
|
map->addItem(SHAMapNodeType::tnSIDECAR, makeSidecarItem(sidecar));
|
|
++entryCount;
|
|
}
|
|
|
|
map = map->snapShot(false);
|
|
commitSetMap_ = map;
|
|
|
|
auto const hash = map->getHash().as_uint256();
|
|
// TODO: move consensus-extension snapshots out of InboundTransactions.
|
|
// They are same-process materialization caches only; sidecar roots are no
|
|
// longer advertised, fetched, served, or merged from peers.
|
|
app_.getInboundTransactions().giveSet(hash, map, false);
|
|
|
|
JLOG(j_.debug()) << "RNG: built commitSet SHAMap"
|
|
<< " hash=" << hash << " seq=" << seq
|
|
<< " entries=" << entryCount
|
|
<< " pendingCommits=" << pendingCommits_.size()
|
|
<< " activeValidators=" << validatorView->size();
|
|
return hash;
|
|
}
|
|
|
|
uint256
|
|
ConsensusExtensions::buildEntropySet(LedgerIndex seq)
|
|
{
|
|
rngRoundSeq_ = seq;
|
|
|
|
auto map =
|
|
std::make_shared<SHAMap>(SHAMapType::SIDECAR, app_.getNodeFamily());
|
|
map->setUnbacked();
|
|
|
|
auto const validatorView = activeValidatorView();
|
|
std::size_t entryCount = 0;
|
|
// NOTE: avoid structured bindings — clang-14 can't capture them (P2036R3).
|
|
for (auto const& entry : pendingReveals_)
|
|
{
|
|
auto const& nid = entry.first;
|
|
auto const& reveal = entry.second;
|
|
|
|
// The entropy set is the reveal side of the proofed commit set. Late
|
|
// proofless commits/reveals may sit in the local harvest cache, but
|
|
// they must not affect the agreed digest/count/tier.
|
|
if (!hasActiveProofedCommit(nid, *validatorView))
|
|
continue;
|
|
|
|
auto kit = nodeIdToKey_.find(nid);
|
|
if (kit == nodeIdToKey_.end())
|
|
continue;
|
|
|
|
AccountID acctId;
|
|
std::memcpy(acctId.data(), nid.data(), acctId.size());
|
|
|
|
STObject sidecar(sfGeneric);
|
|
sidecar.setFieldU8(sfSidecarType, sidecarRngReveal);
|
|
sidecar.setFieldU32(sfLedgerSequence, seq);
|
|
sidecar.setAccountID(sfAccount, acctId);
|
|
sidecar.setFieldH256(sfDigest, reveal);
|
|
sidecar.setFieldVL(sfSigningPubKey, kit->second.slice());
|
|
// Intentionally omit sfBlob for reveal-set entries.
|
|
//
|
|
// Reveal proofs are timing-dependent (seq/closeTime/signature can
|
|
// differ while the reveal digest is identical), which makes the
|
|
// entropy-set hash non-deterministic across nodes under packet
|
|
// loss/reordering. We only need deterministic reveal material
|
|
// (validator identity + digest) for accepted-root replay and entropy
|
|
// calculation.
|
|
|
|
map->addItem(SHAMapNodeType::tnSIDECAR, makeSidecarItem(sidecar));
|
|
++entryCount;
|
|
}
|
|
|
|
map = map->snapShot(false);
|
|
entropySetMap_ = map;
|
|
|
|
auto const hash = map->getHash().as_uint256();
|
|
// TODO: move consensus-extension snapshots out of InboundTransactions.
|
|
// They are same-process materialization caches only; sidecar roots are no
|
|
// longer advertised, fetched, served, or merged from peers.
|
|
app_.getInboundTransactions().giveSet(hash, map, false);
|
|
|
|
JLOG(j_.debug()) << "RNG: built entropySet SHAMap"
|
|
<< " hash=" << hash << " seq=" << seq
|
|
<< " entries=" << entryCount
|
|
<< " pendingReveals=" << pendingReveals_.size()
|
|
<< " activeValidators=" << validatorView->size();
|
|
return hash;
|
|
}
|
|
|
|
uint256
|
|
ConsensusExtensions::buildExportSigSet(LedgerIndex seq)
|
|
{
|
|
auto map =
|
|
std::make_shared<SHAMap>(SHAMapType::SIDECAR, app_.getNodeFamily());
|
|
map->setUnbacked();
|
|
|
|
auto const validated = app_.getLedgerMaster().getValidatedLedger();
|
|
auto const live = validated
|
|
? pendingExportLatches(*validated, seq)
|
|
: std::map<uint256, std::shared_ptr<SLE const>>{};
|
|
auto const allSigs = postValidationExportSigCollector_.fullUnionSnapshot();
|
|
std::size_t entryCount = 0;
|
|
|
|
for (auto const& [origin, contributions] : allSigs)
|
|
{
|
|
if (live.find(origin) == live.end())
|
|
continue;
|
|
|
|
for (auto const& contribution : contributions)
|
|
{
|
|
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()});
|
|
|
|
map->addItem(SHAMapNodeType::tnSIDECAR, makeSidecarItem(sidecar));
|
|
++entryCount;
|
|
}
|
|
}
|
|
|
|
auto const maxExportSidecarLeaves =
|
|
ExportLimits::maxLiveExportLatches * ExportLimits::maxCommitteeMembers;
|
|
XRPL_ASSERT(
|
|
entryCount <= maxExportSidecarLeaves,
|
|
"ripple::ConsensusExtensions::buildExportSigSet : "
|
|
"export sidecar leaf count must stay within bounded local cap");
|
|
|
|
map = map->snapShot(false);
|
|
exportSigSetMap_ = map;
|
|
|
|
auto const hash = map->getHash().as_uint256();
|
|
// TODO: move consensus-extension snapshots out of InboundTransactions.
|
|
// They are same-process materialization caches only; sidecar roots are no
|
|
// longer advertised, fetched, served, or merged from peers.
|
|
app_.getInboundTransactions().giveSet(hash, map, false);
|
|
|
|
JLOG(j_.debug()) << "Export: built exportSigSet SHAMap"
|
|
<< " hash=" << hash << " seq=" << seq
|
|
<< " entries=" << entryCount
|
|
<< " liveOrigins=" << live.size();
|
|
return hash;
|
|
}
|
|
|
|
bool
|
|
ConsensusExtensions::hasPendingExportSigs() const
|
|
{
|
|
auto const validated = app_.getLedgerMaster().getValidatedLedger();
|
|
if (!validated)
|
|
return false;
|
|
auto candidateSeq = rngRoundSeq_;
|
|
if (!candidateSeq)
|
|
{
|
|
if (validated->info().seq == std::numeric_limits<LedgerIndex>::max())
|
|
return false;
|
|
candidateSeq = validated->info().seq + 1;
|
|
}
|
|
auto const live = pendingExportLatches(*validated, *candidateSeq);
|
|
auto const allSigs = postValidationExportSigCollector_.fullUnionSnapshot();
|
|
return std::any_of(allSigs.begin(), allSigs.end(), [&](auto const& entry) {
|
|
return live.find(entry.first) != live.end();
|
|
});
|
|
}
|
|
|
|
bool
|
|
ConsensusExtensions::hasConsensusExportTxns() const
|
|
{
|
|
auto const validated = app_.getLedgerMaster().getValidatedLedger();
|
|
if (!validated)
|
|
return false;
|
|
auto candidateSeq = rngRoundSeq_;
|
|
if (!candidateSeq)
|
|
{
|
|
if (validated->info().seq == std::numeric_limits<LedgerIndex>::max())
|
|
return false;
|
|
candidateSeq = validated->info().seq + 1;
|
|
}
|
|
return !pendingExportLatches(*validated, *candidateSeq).empty();
|
|
}
|
|
|
|
void
|
|
ConsensusExtensions::setExportSigConvergenceFailed()
|
|
{
|
|
exportSigConvergenceFailed_ = true;
|
|
}
|
|
|
|
bool
|
|
ConsensusExtensions::exportSigConvergenceFailed() const
|
|
{
|
|
return exportSigConvergenceFailed_;
|
|
}
|
|
|
|
void
|
|
ConsensusExtensions::acceptExportSigSet(uint256 const& hash)
|
|
{
|
|
acceptedExportSigSetHash_ = hash;
|
|
}
|
|
|
|
void
|
|
ConsensusExtensions::clearAcceptedExportSigSet()
|
|
{
|
|
acceptedExportSigSetHash_.reset();
|
|
}
|
|
|
|
std::optional<ConsensusExtensions::ExportSignatureSnapshot>
|
|
ConsensusExtensions::agreedExportSignatures(
|
|
STTx const& exportTx,
|
|
uint256 const& txHash,
|
|
std::size_t threshold) const
|
|
{
|
|
// A local exportSigSetMap_ is only candidate material until the sidecar
|
|
// gate accepts its root. Without this guard, a timed-out node with a local
|
|
// partial-but-quorum map could mint a different signed export blob from
|
|
// the quorum-aligned nodes.
|
|
if (!acceptedExportSigSetHash_)
|
|
{
|
|
JLOG(j_.warn()) << "Export: exportSigSet not accepted"
|
|
<< " txHash=" << txHash << " threshold=" << threshold;
|
|
return std::nullopt;
|
|
}
|
|
|
|
auto const acceptedHash = *acceptedExportSigSetHash_;
|
|
std::shared_ptr<SHAMap> agreedMap;
|
|
if (exportSigSetMap_ &&
|
|
exportSigSetMap_->getHash().as_uint256() == acceptedHash)
|
|
{
|
|
agreedMap = exportSigSetMap_;
|
|
}
|
|
else
|
|
{
|
|
agreedMap = app_.getInboundTransactions().getSet(acceptedHash, false);
|
|
}
|
|
|
|
if (!agreedMap)
|
|
{
|
|
JLOG(j_.warn()) << "Export: accepted exportSigSet missing"
|
|
<< " acceptedHash=" << acceptedHash
|
|
<< " txHash=" << txHash << " threshold=" << threshold;
|
|
return std::nullopt;
|
|
}
|
|
if (agreedMap->mapType() != SHAMapType::SIDECAR)
|
|
{
|
|
JLOG(j_.warn()) << "Export: accepted exportSigSet has wrong map type"
|
|
<< " acceptedHash=" << acceptedHash
|
|
<< " txHash=" << txHash;
|
|
return std::nullopt;
|
|
}
|
|
|
|
auto const agreedHash = agreedMap->getHash().as_uint256();
|
|
if (agreedHash != acceptedHash)
|
|
{
|
|
JLOG(j_.warn()) << "Export: accepted exportSigSet hash mismatch"
|
|
<< " setHash=" << agreedHash
|
|
<< " acceptedHash=" << acceptedHash
|
|
<< " txHash=" << txHash;
|
|
return std::nullopt;
|
|
}
|
|
|
|
// The accepted root is the membership decision. Candidate construction
|
|
// filters live active signers, but materialization must not re-resolve
|
|
// signer keys through mutable manifests or nodes can diverge after a
|
|
// rotation. Keep cryptographic verification below; drop only the live
|
|
// membership re-filter.
|
|
ExportSignatureSnapshot signatures;
|
|
bool invalid = false;
|
|
agreedMap->visitLeaves(
|
|
[&](boost::intrusive_ptr<SHAMapItem const> const& item) {
|
|
if (invalid)
|
|
return;
|
|
|
|
try
|
|
{
|
|
auto admitted = admitSidecarLeaf(
|
|
item->key(),
|
|
item->slice(),
|
|
agreedHash,
|
|
j_,
|
|
"Export",
|
|
"exportSigSet",
|
|
"agreed",
|
|
ExportLimits::maxExportSignatureSidecarBytes,
|
|
&invalid);
|
|
if (!admitted || admitted->type != sidecarExportSig)
|
|
return;
|
|
auto const& sidecar = admitted->sidecar;
|
|
|
|
if (!sidecar.isFieldPresent(sfTransactionHash) ||
|
|
!sidecar.isFieldPresent(sfSigningPubKey) ||
|
|
!sidecar.isFieldPresent(sfTxnSignature))
|
|
return;
|
|
|
|
if (sidecar.getFieldH256(sfTransactionHash) != txHash)
|
|
return;
|
|
|
|
auto const pk = sidecar.getFieldVL(sfSigningPubKey);
|
|
if (!publicKeyType(makeSlice(pk)))
|
|
return;
|
|
|
|
PublicKey const valPK{makeSlice(pk)};
|
|
|
|
auto const sigVL = sidecar.getFieldVL(sfTxnSignature);
|
|
auto const sigSlice = makeSlice(sigVL);
|
|
if (!verifyExportSignatureAgainstTx(
|
|
exportTx,
|
|
valPK,
|
|
sigSlice,
|
|
txHash,
|
|
j_,
|
|
"agreed exportSigSet"))
|
|
{
|
|
invalid = true;
|
|
return;
|
|
}
|
|
|
|
Buffer sigBuf(sigSlice.data(), sigSlice.size());
|
|
if (auto const [_, inserted] =
|
|
signatures.emplace(valPK, std::move(sigBuf));
|
|
!inserted)
|
|
{
|
|
JLOG(j_.warn())
|
|
<< "Export: accepted exportSigSet duplicate signer"
|
|
<< " setHash=" << agreedHash << " txHash=" << txHash
|
|
<< " signer=" << toBase58(TokenType::NodePublic, valPK);
|
|
invalid = true;
|
|
}
|
|
}
|
|
catch (std::exception const& e)
|
|
{
|
|
JLOG(j_.warn())
|
|
<< "Export: agreed exportSigSet parse failed"
|
|
<< " setHash=" << agreedHash << " txHash=" << txHash
|
|
<< " error=" << e.what();
|
|
invalid = true;
|
|
}
|
|
});
|
|
|
|
if (invalid)
|
|
return std::nullopt;
|
|
|
|
if (signatures.size() < threshold)
|
|
{
|
|
JLOG(j_.info()) << "Export: accepted exportSigSet below quorum"
|
|
<< " setHash=" << agreedHash << " txHash=" << txHash
|
|
<< " signers=" << signatures.size()
|
|
<< " threshold=" << threshold;
|
|
return std::nullopt;
|
|
}
|
|
|
|
return signatures;
|
|
}
|
|
|
|
std::optional<ConsensusExtensions::ExportWitnessMaterial>
|
|
ConsensusExtensions::agreedExportWitness(
|
|
STTx const& releaseTarget,
|
|
uint256 const& origin,
|
|
Blob const& committeeBitmap,
|
|
std::size_t universeSize,
|
|
std::size_t threshold) const
|
|
{
|
|
auto const committee =
|
|
resolveExportCommittee(makeSlice(committeeBitmap), universeSize);
|
|
if (!committee || committee->quorum != threshold)
|
|
return std::nullopt;
|
|
|
|
auto const acceptedHash = acceptedExportSigSetHash_
|
|
? *acceptedExportSigSetHash_
|
|
: app_.config().standalone() && exportSigSetMap_
|
|
? exportSigSetMap_->getHash().as_uint256()
|
|
: uint256{};
|
|
if (acceptedHash.isZero())
|
|
return std::nullopt;
|
|
std::shared_ptr<SHAMap> agreedMap;
|
|
if (exportSigSetMap_ &&
|
|
exportSigSetMap_->getHash().as_uint256() == acceptedHash)
|
|
agreedMap = exportSigSetMap_;
|
|
else
|
|
agreedMap = app_.getInboundTransactions().getSet(acceptedHash, false);
|
|
|
|
if (!agreedMap || agreedMap->mapType() != SHAMapType::SIDECAR ||
|
|
agreedMap->getHash().as_uint256() != acceptedHash)
|
|
return std::nullopt;
|
|
|
|
ExportWitnessMaterial material;
|
|
std::set<std::uint32_t> positions;
|
|
hash_set<AccountID> signerAccounts;
|
|
bool invalid = false;
|
|
agreedMap->visitLeaves(
|
|
[&](boost::intrusive_ptr<SHAMapItem const> const& item) {
|
|
if (invalid)
|
|
return;
|
|
try
|
|
{
|
|
auto admitted = admitSidecarLeaf(
|
|
item->key(),
|
|
item->slice(),
|
|
acceptedHash,
|
|
j_,
|
|
"Export",
|
|
"exportSigSet",
|
|
"witness",
|
|
ExportLimits::maxExportSignatureSidecarBytes,
|
|
&invalid);
|
|
if (!admitted || admitted->type != sidecarExportSig)
|
|
{
|
|
invalid = true;
|
|
return;
|
|
}
|
|
|
|
auto const& sidecar = admitted->sidecar;
|
|
if (!sidecar.isFieldPresent(sfTransactionHash) ||
|
|
!sidecar.isFieldPresent(sfTransactionIndex) ||
|
|
!sidecar.isFieldPresent(sfSigningPubKey) ||
|
|
!sidecar.isFieldPresent(sfTxnSignature))
|
|
{
|
|
invalid = true;
|
|
return;
|
|
}
|
|
if (sidecar.getFieldH256(sfTransactionHash) != origin)
|
|
return;
|
|
|
|
auto const position = sidecar.getFieldU32(sfTransactionIndex);
|
|
if (position >= universeSize ||
|
|
!committee->members.contains(position) ||
|
|
!positions.insert(position).second)
|
|
{
|
|
invalid = true;
|
|
return;
|
|
}
|
|
|
|
auto const keyBytes = sidecar.getFieldVL(sfSigningPubKey);
|
|
if (!publicKeyType(makeSlice(keyBytes)))
|
|
{
|
|
invalid = true;
|
|
return;
|
|
}
|
|
PublicKey const key{makeSlice(keyBytes)};
|
|
auto const signer = calcAccountID(key);
|
|
if (!signerAccounts.insert(signer).second)
|
|
{
|
|
invalid = true;
|
|
return;
|
|
}
|
|
|
|
auto const signature = sidecar.getFieldVL(sfTxnSignature);
|
|
auto const data = buildMultiSigningData(releaseTarget, signer);
|
|
if (signature.empty() ||
|
|
signature.size() >
|
|
ExportSigCollectorV2::maxSignatureBytes ||
|
|
!verify(key, data.slice(), makeSlice(signature)))
|
|
{
|
|
invalid = true;
|
|
return;
|
|
}
|
|
|
|
auto const [_, inserted] = material.signatures.emplace(
|
|
static_cast<std::uint16_t>(position),
|
|
ExportResultBuilder::PositionedSignature{
|
|
key, Buffer{signature.data(), signature.size()}});
|
|
if (!inserted)
|
|
{
|
|
invalid = true;
|
|
return;
|
|
}
|
|
}
|
|
catch (std::exception const& e)
|
|
{
|
|
JLOG(j_.warn())
|
|
<< "Export: witness sidecar parse failed"
|
|
<< " origin=" << origin << " setHash=" << acceptedHash
|
|
<< " error=" << e.what();
|
|
invalid = true;
|
|
}
|
|
});
|
|
|
|
if (invalid || material.signatures.size() < threshold ||
|
|
material.signatures.size() > ExportLimits::maxCommitteeMembers)
|
|
return std::nullopt;
|
|
return material;
|
|
}
|
|
|
|
void
|
|
ConsensusExtensions::generateEntropySecret()
|
|
{
|
|
// Generate cryptographically secure random entropy
|
|
crypto_prng()(myEntropySecret_.data(), myEntropySecret_.size());
|
|
entropyFailed_ = false;
|
|
}
|
|
|
|
uint256
|
|
ConsensusExtensions::getEntropySecret() const
|
|
{
|
|
return myEntropySecret_;
|
|
}
|
|
|
|
void
|
|
ConsensusExtensions::setEntropyFailed()
|
|
{
|
|
entropyFailed_ = true;
|
|
}
|
|
|
|
void
|
|
ConsensusExtensions::freezeRngCommitSet()
|
|
{
|
|
commitSetFrozen_ = true;
|
|
}
|
|
|
|
void
|
|
ConsensusExtensions::selfSeedReveal()
|
|
{
|
|
auto const& valKeys = app_.getValidatorKeys();
|
|
if (!valKeys.keys || valKeys.nodeID == beast::zero)
|
|
return;
|
|
|
|
if (myEntropySecret_ != uint256{})
|
|
{
|
|
pendingReveals_[valKeys.nodeID] = myEntropySecret_;
|
|
nodeIdToKey_.insert_or_assign(valKeys.nodeID, valKeys.keys->publicKey);
|
|
}
|
|
}
|
|
|
|
//@@start clear-rng-state
|
|
void
|
|
ConsensusExtensions::clearRngStatePreservingExport()
|
|
{
|
|
//@@start round-stop-rng-reset
|
|
pendingCommits_.clear();
|
|
pendingReveals_.clear();
|
|
nodeIdToKey_.clear();
|
|
myEntropySecret_ = uint256{};
|
|
entropyFailed_ = false;
|
|
commitSetFrozen_ = false;
|
|
commitSetMap_.reset();
|
|
entropySetMap_.reset();
|
|
acceptedEntropySetHash_.reset();
|
|
rngRoundSeq_.reset();
|
|
roundPrevLedgerHash_ = uint256{};
|
|
observedParticipantsHash_.reset();
|
|
observedParticipantsCount_ = 0;
|
|
observedParticipantsBitmapBin_.clear();
|
|
likelyParticipants_.clear();
|
|
commitProofs_.clear();
|
|
//@@end round-stop-rng-reset
|
|
// Keep the round-level enable latches intact here. onRoundStart() refreshes
|
|
// them from the consensus parent before clearing so boundary cleanup, such
|
|
// as the export-disabled signature wipe, observes the new round's rules.
|
|
}
|
|
|
|
void
|
|
ConsensusExtensions::clearRngState()
|
|
{
|
|
//@@start round-stop-export-reset
|
|
if (!exportEnabled())
|
|
{
|
|
// Export disabled is an amendment boundary, not a retry boundary.
|
|
// Drop cached signatures so an emergency stop cannot leave old quorum
|
|
// material waiting for a later re-enable.
|
|
postValidationExportSigCollector_.clearAll();
|
|
}
|
|
exportSigSetMap_.reset();
|
|
acceptedExportSigSetHash_.reset();
|
|
proposalPublishedExportShares_.clear();
|
|
exportSigGateStarted_ = false;
|
|
exportSigGateStart_ = {};
|
|
exportSigConvergenceFailed_ = false;
|
|
//@@end round-stop-export-reset
|
|
|
|
clearRngStatePreservingExport();
|
|
}
|
|
//@@end clear-rng-state
|
|
|
|
void
|
|
ConsensusExtensions::cacheUNLReport(
|
|
std::shared_ptr<Ledger const> const& prevLedger)
|
|
{
|
|
auto view = makeActiveValidatorView(prevLedger);
|
|
auto const size = view->size();
|
|
auto const fromUNLReport = view->fromUNLReport;
|
|
|
|
{
|
|
std::lock_guard lock(activeValidatorViewMutex_);
|
|
activeValidatorView_ = std::move(view);
|
|
}
|
|
|
|
JLOG(j_.trace()) << "RNG: cached active validator view"
|
|
<< " activeValidators=" << size << " source="
|
|
<< (fromUNLReport ? "UNLReport" : "trusted-fallback");
|
|
}
|
|
|
|
bool
|
|
ConsensusExtensions::isUNLReportMember(NodeID const& nodeId) const
|
|
{
|
|
// RNG commit/reveal sidecars identify validators by master-key NodeID, so
|
|
// use the shared active view instead of a separate RNG-only membership set.
|
|
return activeValidatorView()->containsNode(nodeId);
|
|
}
|
|
|
|
bool
|
|
ConsensusExtensions::localIsActiveValidator() const
|
|
{
|
|
// Our own sidecar position only counts toward alignment when this validator
|
|
// is itself in the active view — the same universe as the peer-membership
|
|
// filter and the entropy/export thresholds.
|
|
auto const& valKeys = app_.getValidatorKeys();
|
|
if (!valKeys.keys || valKeys.nodeID == beast::zero)
|
|
return false;
|
|
return activeValidatorView()->containsNode(valKeys.nodeID);
|
|
}
|
|
|
|
ConsensusExtensions::ActiveValidatorViewPtr
|
|
ConsensusExtensions::activeValidatorView() const
|
|
{
|
|
std::lock_guard lock(activeValidatorViewMutex_);
|
|
return activeValidatorView_;
|
|
}
|
|
|
|
ConsensusExtensions::ActiveValidatorViewPtr
|
|
ConsensusExtensions::makeActiveValidatorView(
|
|
std::shared_ptr<Ledger const> const& prevLedger) const
|
|
{
|
|
// Prefer the consensus parent ledger so all validators evaluate the round
|
|
// against the same frozen UNLReport, not a local latest-validated ledger.
|
|
if (!prevLedger)
|
|
{
|
|
XRPL_ASSERT(
|
|
roundPrevLedgerHash_.isZero(),
|
|
"ripple::ConsensusExtensions::makeActiveValidatorView : "
|
|
"null parent is outside an active consensus round");
|
|
}
|
|
auto const sourceLedger =
|
|
prevLedger ? prevLedger : app_.getLedgerMaster().getValidatedLedger();
|
|
XRPL_ASSERT(
|
|
sourceLedger,
|
|
"ripple::ConsensusExtensions::makeActiveValidatorView : "
|
|
"source ledger is available");
|
|
return std::make_shared<ActiveValidatorView const>(buildActiveValidatorView(
|
|
buildActiveValidatorViewSource(sourceLedger),
|
|
buildActiveValidatorViewFallback(app_)));
|
|
}
|
|
|
|
bool
|
|
ConsensusExtensions::isActiveValidator(PublicKey const& validationKey) const
|
|
{
|
|
return isActiveValidator(validationKey, *activeValidatorView());
|
|
}
|
|
|
|
bool
|
|
ConsensusExtensions::isActiveValidator(
|
|
PublicKey const& validationKey,
|
|
ActiveValidatorView const& view) const
|
|
{
|
|
auto const trustedMaster = app_.validators().getTrustedKey(validationKey);
|
|
if (!trustedMaster)
|
|
return false;
|
|
|
|
return view.containsMaster(*trustedMaster);
|
|
}
|
|
|
|
void
|
|
ConsensusExtensions::recordParticipantDiagnostics(
|
|
ConsensusMode mode,
|
|
std::vector<NodeID> peerNodeIds)
|
|
{
|
|
auto const view = activeValidatorView();
|
|
hash_set<NodeID> observed;
|
|
observed.reserve(peerNodeIds.size() + 1);
|
|
|
|
for (auto const& nodeId : peerNodeIds)
|
|
{
|
|
if (view->containsNode(nodeId))
|
|
observed.insert(nodeId);
|
|
}
|
|
|
|
auto const& valKeys = app_.getValidatorKeys();
|
|
if (mode == ConsensusMode::proposing && valKeys.nodeID != beast::zero &&
|
|
view->containsNode(valKeys.nodeID))
|
|
{
|
|
observed.insert(valKeys.nodeID);
|
|
}
|
|
|
|
std::vector<NodeID> activeSorted(
|
|
view->nodeIds.begin(), view->nodeIds.end());
|
|
std::sort(activeSorted.begin(), activeSorted.end());
|
|
std::vector<NodeID> observedSorted(observed.begin(), observed.end());
|
|
std::sort(observedSorted.begin(), observedSorted.end());
|
|
auto bitmapBin = buildObservedParticipantBitmap(activeSorted, observed);
|
|
|
|
Serializer s(512);
|
|
if (view->sourceLedgerHash)
|
|
s.addBitString(*view->sourceLedgerHash);
|
|
else
|
|
{
|
|
uint256 noSource;
|
|
noSource.zero();
|
|
s.addBitString(noSource);
|
|
}
|
|
s.add32(static_cast<std::uint32_t>(view->size()));
|
|
for (auto const& nodeId : activeSorted)
|
|
s.addBitString(nodeId);
|
|
s.add32(static_cast<std::uint32_t>(observedSorted.size()));
|
|
for (auto const& nodeId : observedSorted)
|
|
s.addBitString(nodeId);
|
|
|
|
auto const hash = sha512Half(HashPrefix::observedParticipants, s.slice());
|
|
bool const changed = !observedParticipantsHash_ ||
|
|
*observedParticipantsHash_ != hash ||
|
|
observedParticipantsCount_ != observedSorted.size() ||
|
|
observedParticipantsBitmapBin_ != bitmapBin;
|
|
|
|
observedParticipantsHash_ = hash;
|
|
observedParticipantsCount_ = observedSorted.size();
|
|
observedParticipantsBitmapBin_ = std::move(bitmapBin);
|
|
|
|
if (changed)
|
|
{
|
|
JLOG(j_.debug()) << "STALLDIAG: observed-active-participants"
|
|
<< " count=" << observedParticipantsCount_
|
|
<< " activeView=" << view->size()
|
|
<< " quorum=" << quorumThreshold() << " hash=" << hash
|
|
<< " source="
|
|
<< (view->fromUNLReport ? "UNLReport"
|
|
: "trusted-fallback")
|
|
<< " mode=" << to_string(mode)
|
|
<< " peerPositions=" << peerNodeIds.size()
|
|
<< " bitmapBin=" << observedParticipantsBitmapBin_;
|
|
}
|
|
}
|
|
|
|
void
|
|
ConsensusExtensions::attachParticipantDiagnostics(ExtendedPosition& pos) const
|
|
{
|
|
//@@start participant-diagnostics-feature-gate
|
|
if (!rngEnabled() && !exportEnabled())
|
|
return;
|
|
|
|
if (observedParticipantsHash_)
|
|
pos.observedParticipantsHash = observedParticipantsHash_;
|
|
//@@end participant-diagnostics-feature-gate
|
|
}
|
|
|
|
std::size_t
|
|
ConsensusExtensions::observedParticipantCount() const
|
|
{
|
|
return observedParticipantsCount_;
|
|
}
|
|
|
|
std::optional<uint256>
|
|
ConsensusExtensions::observedParticipantsHash() const
|
|
{
|
|
return observedParticipantsHash_;
|
|
}
|
|
|
|
std::string const&
|
|
ConsensusExtensions::observedParticipantsBitmapBin() const
|
|
{
|
|
return observedParticipantsBitmapBin_;
|
|
}
|
|
|
|
void
|
|
ConsensusExtensions::onPreBuild(
|
|
CanonicalTXSet& retriableTxs,
|
|
LedgerIndex seq,
|
|
uint256 const& txSetHash)
|
|
{
|
|
if (rngEnabled())
|
|
{
|
|
JLOG(j_.info()) << "RNG: injectEntropy"
|
|
<< " seq=" << seq
|
|
<< " commits=" << pendingCommits_.size()
|
|
<< " reveals=" << pendingReveals_.size()
|
|
<< " entropyFailed=" << (entropyFailed_ ? "yes" : "no")
|
|
<< " quorum=" << quorumThreshold() << " entropySetHash="
|
|
<< (entropySetMap_
|
|
? to_string(
|
|
entropySetMap_->getHash().as_uint256())
|
|
: std::string{"none"});
|
|
|
|
//@@start rng-inject-entropy-selection
|
|
// One deterministic selector over the AGREED entropySetMap_ chooses the
|
|
// digest and its tier/count/denominator labels. Every node derives the
|
|
// same entropy for the same agreed round inputs. txSetHash is the
|
|
// agreed pre-injection consensus tx set hash.
|
|
auto const selection = selectEntropy(txSetHash, seq);
|
|
uint256 const finalEntropy = selection.digest;
|
|
std::uint8_t const entropyTier = selection.tier;
|
|
std::uint16_t const entropyCount = selection.count;
|
|
std::uint16_t const entropyDenominator = selection.denominator;
|
|
Blob const& entropyContributors = selection.contributors;
|
|
//@@end rng-inject-entropy-selection
|
|
|
|
JLOG(j_.info()) << "RNG: entropy selected"
|
|
<< " seq=" << seq
|
|
<< " tier=" << static_cast<int>(entropyTier)
|
|
<< " count=" << entropyCount
|
|
<< " denominator=" << entropyDenominator
|
|
<< " digest=" << finalEntropy;
|
|
|
|
//@@start rng-inject-pseudotx
|
|
// Synthesize and inject the pseudo-transaction. The selector always
|
|
// yields a digest (fallback when there is no validator entropy), so
|
|
// injection is unconditional — every RNG-enabled ledger carries a
|
|
// ConsensusEntropy tx.
|
|
{
|
|
// Design note: this is the canonical path that materializes
|
|
// the synthetic entropy-bearing tx-set in production.
|
|
//
|
|
// Why here (onAccept/buildLCL) instead of mutating proposals
|
|
// earlier?
|
|
// - Consensus agreement is keyed by proposal txSetHash during
|
|
// establish. Late mutation of txSetHash in establish can fragment
|
|
// votes under loss/reordering (base hash vs synthetic hash).
|
|
// - Injecting at accept preserves robust convergence semantics:
|
|
// peers agree on the base transaction set first, then
|
|
// deterministically derive/apply the entropy pseudo-tx for ledger
|
|
// construction.
|
|
//
|
|
//@@start rng-inject-pseudotx-core
|
|
// Account Zero convention for pseudo-transactions (same as ttFEE,
|
|
// etc)
|
|
STTx tx(ttCONSENSUS_ENTROPY, [&](auto& obj) {
|
|
obj.setFieldU32(sfLedgerSequence, seq);
|
|
obj.setAccountID(sfAccount, AccountID{});
|
|
obj.setFieldU32(sfSequence, 0);
|
|
obj.setFieldAmount(sfFee, STAmount{});
|
|
obj.setFieldH256(sfDigest, finalEntropy);
|
|
obj.setFieldU16(sfEntropyCount, entropyCount);
|
|
obj.setFieldU16(sfEntropyDenominator, entropyDenominator);
|
|
obj.setFieldVL(sfEntropyContributors, entropyContributors);
|
|
obj.setFieldU8(sfEntropyTier, entropyTier);
|
|
});
|
|
|
|
auto const txID = tx.getTransactionID();
|
|
// Value-based dedup. There must never be two entropy pseudo-txs,
|
|
// but when one is already present in the agreed set it must be
|
|
// VALIDATED as the exact pseudo-tx we would have produced — not
|
|
// merely "same type". Injection is deterministic, so every honest
|
|
// node derives the identical pseudo-tx (identical txID) for the
|
|
// same agreed inputs. A present-but-different entropy pseudo-tx is
|
|
// therefore a determinism violation (version skew or a
|
|
// divergent/malicious peer) and must be surfaced, not silently
|
|
// trusted.
|
|
auto const existing = std::find_if(
|
|
retriableTxs.begin(),
|
|
retriableTxs.end(),
|
|
[](auto const& entry) {
|
|
return entry.second->getTxnType() == ttCONSENSUS_ENTROPY;
|
|
});
|
|
if (existing != retriableTxs.end())
|
|
{
|
|
auto const existingID = existing->second->getTransactionID();
|
|
if (existingID == txID)
|
|
{
|
|
JLOG(j_.debug()) << "RNG: entropy pseudo-tx already present"
|
|
<< " txHash=" << txID << " seq=" << seq
|
|
<< " action=skip-duplicate-verified";
|
|
}
|
|
else
|
|
{
|
|
// The agreed tx set's hash already commits to the existing
|
|
// pseudo-tx, so we cannot replace it without forking off
|
|
// the agreed ledger. This is detect-and-log only: the
|
|
// existing pseudo-tx is KEPT and is still applied at ledger
|
|
// build (BuildLedger applyTransactions). A hard-fail/reject
|
|
// policy on mismatch is a deliberate future decision (it
|
|
// trades a determinism violation for a halt risk under
|
|
// benign skew).
|
|
//
|
|
// Read present fields defensively: the mismatching
|
|
// pseudo-tx may be exactly the old/malformed (pre-tier)
|
|
// entry we are guarding against, and getField...() on a
|
|
// missing required field would throw here, inside
|
|
// onPreBuild during build.
|
|
auto const& pres = *existing->second;
|
|
JLOG(j_.error())
|
|
<< "RNG: entropy pseudo-tx MISMATCH"
|
|
<< " seq=" << seq << " reason=determinism-violation"
|
|
<< " action=keep-agreed-and-flag"
|
|
<< " ourTxHash=" << txID
|
|
<< " ourDigest=" << finalEntropy
|
|
<< " ourTier=" << static_cast<int>(entropyTier)
|
|
<< " ourCount=" << entropyCount
|
|
<< " ourDenominator=" << entropyDenominator
|
|
<< " ourContributors=" << strHex(entropyContributors)
|
|
<< " presentTxHash=" << existingID << " presentDigest="
|
|
<< (pres.isFieldPresent(sfDigest)
|
|
? to_string(pres.getFieldH256(sfDigest))
|
|
: std::string{"<missing>"})
|
|
<< " presentTier="
|
|
<< (pres.isFieldPresent(sfEntropyTier)
|
|
? std::to_string(pres.getFieldU8(sfEntropyTier))
|
|
: std::string{"<missing>"})
|
|
<< " presentCount="
|
|
<< (pres.isFieldPresent(sfEntropyCount)
|
|
? std::to_string(
|
|
pres.getFieldU16(sfEntropyCount))
|
|
: std::string{"<missing>"})
|
|
<< " presentDenominator="
|
|
<< (pres.isFieldPresent(sfEntropyDenominator)
|
|
? std::to_string(
|
|
pres.getFieldU16(sfEntropyDenominator))
|
|
: std::string{"<missing>"})
|
|
<< " presentContributors="
|
|
<< (pres.isFieldPresent(sfEntropyContributors)
|
|
? strHex(pres.getFieldVL(sfEntropyContributors))
|
|
: std::string{"<missing>"});
|
|
}
|
|
}
|
|
else
|
|
{
|
|
retriableTxs.insert(std::make_shared<STTx>(std::move(tx)));
|
|
}
|
|
//@@end rng-inject-pseudotx-core
|
|
}
|
|
//@@end rng-inject-pseudotx
|
|
}
|
|
|
|
if (exportEnabled())
|
|
{
|
|
//@@start export-later-ledger-witness-materialization
|
|
// Standalone has no peer-position gate to build and accept a sidecar
|
|
// root. Materialize the same locally verified union that
|
|
// agreedExportWitness() explicitly accepts in standalone mode.
|
|
if (app_.config().standalone() && hasPendingExportSigs())
|
|
buildExportSigSet(seq);
|
|
|
|
// Export witnesses are synthetic consequences of the accepted sidecar
|
|
// root. Never preserve a transaction-set supplied variant.
|
|
for (auto it = retriableTxs.begin(); it != retriableTxs.end();)
|
|
{
|
|
auto const& tx = it->second;
|
|
if (tx && tx->getTxnType() == ttEXPORT_SIGNATURES)
|
|
it = retriableTxs.erase(it);
|
|
else
|
|
++it;
|
|
}
|
|
|
|
auto const parent =
|
|
app_.getLedgerMaster().getLedgerByHash(roundPrevLedgerHash_);
|
|
auto const validated = app_.getLedgerMaster().getValidatedLedger();
|
|
bool parentExtendsValidated =
|
|
parent && validated && validated->info().seq <= parent->info().seq;
|
|
if (parentExtendsValidated &&
|
|
validated->info().seq < parent->info().seq)
|
|
{
|
|
auto const validatedHash =
|
|
hashOfSeq(*parent, validated->info().seq, j_);
|
|
parentExtendsValidated =
|
|
validatedHash && *validatedHash == validated->info().hash;
|
|
}
|
|
else if (parentExtendsValidated)
|
|
{
|
|
parentExtendsValidated =
|
|
parent->info().hash == validated->info().hash;
|
|
}
|
|
|
|
if (parentExtendsValidated)
|
|
{
|
|
auto const pending = pendingExportLatches(*parent, seq);
|
|
for (auto const& [origin, latch] : pending)
|
|
{
|
|
if (!latch->isFieldPresent(sfExportUniverseHash) ||
|
|
!latch->isFieldPresent(sfExportCommittee) ||
|
|
!latch->isFieldPresent(sfLastLedgerSequence) ||
|
|
seq > latch->getFieldU32(sfLastLedgerSequence))
|
|
continue;
|
|
|
|
auto const originSeq = latch->getFieldU32(sfLedgerSequence);
|
|
auto const originHash = hashOfSeq(*parent, originSeq, j_);
|
|
auto const validatedOriginHash =
|
|
originSeq == validated->info().seq
|
|
? std::optional<uint256>{validated->info().hash}
|
|
: hashOfSeq(*validated, originSeq, j_);
|
|
if (!originHash || !validatedOriginHash ||
|
|
*originHash != *validatedOriginHash)
|
|
continue;
|
|
|
|
auto const originLedger =
|
|
app_.getLedgerMaster().getLedgerByHash(*originHash);
|
|
if (!originLedger)
|
|
continue;
|
|
auto const [outer, _] = originLedger->txRead(origin);
|
|
if (!outer || outer->getTxnType() != ttEXPORT)
|
|
continue;
|
|
auto const inner = ExportLedgerOps::innerExportedTx(*outer);
|
|
if (!inner)
|
|
continue;
|
|
|
|
auto const universeHash =
|
|
latch->getFieldH256(sfExportUniverseHash);
|
|
if (originLedger->info().parentHash != universeHash)
|
|
continue;
|
|
auto const universeLedger =
|
|
app_.getLedgerMaster().getLedgerByHash(universeHash);
|
|
if (!universeLedger)
|
|
continue;
|
|
auto const validatorView =
|
|
makeActiveValidatorView(universeLedger);
|
|
if (!validatorView->fromUNLReport ||
|
|
!validatorView->sourceLedgerHash ||
|
|
*validatorView->sourceLedgerHash != universeHash)
|
|
continue;
|
|
|
|
auto const committeeBitmap =
|
|
latch->getFieldVL(sfExportCommittee);
|
|
auto const committee = resolveExportCommittee(
|
|
makeSlice(committeeBitmap),
|
|
validatorView->orderedOriginalMasterKeys.size());
|
|
if (!committee)
|
|
continue;
|
|
|
|
auto const targetNetworkID = inner->isFieldPresent(sfNetworkID)
|
|
? inner->getFieldU32(sfNetworkID)
|
|
: std::uint32_t{0};
|
|
auto const release = ExportOriginMemo::releaseForm(
|
|
*inner,
|
|
ExportOriginMemo::Origin{
|
|
app_.config().NETWORK_ID, targetNetworkID, origin},
|
|
ExportOriginMemo::Anchor{originSeq, *originHash});
|
|
if (!release)
|
|
continue;
|
|
|
|
auto material = agreedExportWitness(
|
|
release.value(),
|
|
origin,
|
|
committeeBitmap,
|
|
validatorView->orderedOriginalMasterKeys.size(),
|
|
committee->quorum);
|
|
if (!material)
|
|
continue;
|
|
|
|
auto witness = ExportResultBuilder::buildSignatureWitness(
|
|
origin,
|
|
release.value(),
|
|
material->signatures,
|
|
validatorView->orderedOriginalMasterKeys.size(),
|
|
seq);
|
|
retriableTxs.insert(std::make_shared<STTx>(std::move(witness)));
|
|
}
|
|
}
|
|
//@@end export-later-ledger-witness-materialization
|
|
}
|
|
|
|
//@@start accept-time-cleanup-success
|
|
// Export's ledger-defining signature witnesses are now self-contained
|
|
// transactions in the stream; no later apply step reads sidecar memory.
|
|
clearRngStatePreservingExport();
|
|
//@@end accept-time-cleanup-success
|
|
}
|
|
|
|
void
|
|
ConsensusExtensions::harvestRngData(
|
|
NodeID const& nodeId,
|
|
PublicKey const& publicKey,
|
|
ExtendedPosition const& position,
|
|
std::uint32_t proposeSeq,
|
|
NetClock::time_point closeTime,
|
|
uint256 const& prevLedger,
|
|
Slice const& signature)
|
|
{
|
|
JLOG(j_.trace()) << "RNG: harvestRngData"
|
|
<< " node=" << nodeId
|
|
<< " commit=" << (position.myCommitment ? "yes" : "no")
|
|
<< " reveal=" << (position.myReveal ? "yes" : "no")
|
|
<< " proposeSeq=" << proposeSeq
|
|
<< " prevLedger=" << prevLedger;
|
|
|
|
//@@start runtime-rng-claim-drop
|
|
// RuntimeConfig: randomly drop RNG claims for testing
|
|
auto& rc = app_.getRuntimeConfig();
|
|
if (rc.active())
|
|
{
|
|
if (auto cfg = rc.getConsensusTestConfig())
|
|
{
|
|
if (cfg->rngClaimDropPctX100 && *cfg->rngClaimDropPctX100 > 0)
|
|
{
|
|
static thread_local std::mt19937 rng{std::random_device{}()};
|
|
if (std::uniform_int_distribution<int>{0, 9999}(rng) <
|
|
*cfg->rngClaimDropPctX100)
|
|
{
|
|
JLOG(j_.warn())
|
|
<< "RNG: TESTING dropping claim"
|
|
<< " node=" << nodeId
|
|
<< " dropPctX100=" << *cfg->rngClaimDropPctX100
|
|
<< " proposeSeq=" << proposeSeq;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//@@end runtime-rng-claim-drop
|
|
|
|
//@@start rng-harvest-commit
|
|
// Harvest commitment if present
|
|
if (position.myCommitment)
|
|
{
|
|
std::optional<ProposalProof> proof;
|
|
if (proposeSeq == 0)
|
|
{
|
|
ProposalProof p;
|
|
p.proposeSeq = proposeSeq;
|
|
p.closeTime = static_cast<std::uint32_t>(
|
|
closeTime.time_since_epoch().count());
|
|
p.prevLedger = prevLedger;
|
|
Serializer s;
|
|
position.add(s);
|
|
p.positionData = std::move(s);
|
|
p.signature = Buffer(signature.data(), signature.size());
|
|
proof = std::move(p);
|
|
}
|
|
|
|
if (ingestRngContribution(
|
|
nodeId,
|
|
publicKey,
|
|
RngContributionKind::commit,
|
|
*position.myCommitment,
|
|
std::nullopt,
|
|
proof,
|
|
"proposal",
|
|
RngProofCachePolicy::keepExisting))
|
|
{
|
|
JLOG(j_.trace())
|
|
<< "RNG: harvested commitment"
|
|
<< " node=" << nodeId << " proposeSeq=" << proposeSeq
|
|
<< " commitment=" << *position.myCommitment;
|
|
}
|
|
}
|
|
//@@end rng-harvest-commit
|
|
|
|
//@@start rng-harvest-reveal-verification
|
|
// Harvest reveal if present — verify it matches the stored commitment
|
|
if (position.myReveal)
|
|
{
|
|
if (rc.active())
|
|
{
|
|
if (auto cfg = rc.getConsensusTestConfig())
|
|
{
|
|
if (cfg->rngRevealDropPctX100 && *cfg->rngRevealDropPctX100 > 0)
|
|
{
|
|
static thread_local std::mt19937 rng{
|
|
std::random_device{}()};
|
|
if (std::uniform_int_distribution<int>{0, 9999}(rng) <
|
|
*cfg->rngRevealDropPctX100)
|
|
{
|
|
JLOG(j_.warn())
|
|
<< "RNG: TESTING dropping reveal claim"
|
|
<< " node=" << nodeId
|
|
<< " dropPctX100=" << *cfg->rngRevealDropPctX100
|
|
<< " proposeSeq=" << proposeSeq;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Verify Hash(reveal | pubKey | seq) == commitment
|
|
auto const prevLgr = app_.getLedgerMaster().getLedgerByHash(prevLedger);
|
|
if (!prevLgr)
|
|
{
|
|
JLOG(j_.warn())
|
|
<< "RNG: cannot verify reveal"
|
|
<< " reason=prev-ledger-unavailable"
|
|
<< " node=" << nodeId << " proposeSeq=" << proposeSeq
|
|
<< " prevLedger=" << prevLedger;
|
|
return;
|
|
}
|
|
|
|
auto const seq = prevLgr->info().seq + 1;
|
|
|
|
if (ingestRngContribution(
|
|
nodeId,
|
|
publicKey,
|
|
RngContributionKind::reveal,
|
|
*position.myReveal,
|
|
seq,
|
|
std::nullopt,
|
|
"proposal",
|
|
RngProofCachePolicy::keepExisting))
|
|
{
|
|
JLOG(j_.trace())
|
|
<< "RNG: harvested reveal"
|
|
<< " node=" << nodeId << " proposeSeq=" << proposeSeq
|
|
<< " seq=" << seq << " reveal=" << *position.myReveal;
|
|
}
|
|
}
|
|
//@@end rng-harvest-reveal-verification
|
|
}
|
|
|
|
Blob
|
|
ConsensusExtensions::serializeProof(ProposalProof const& proof)
|
|
{
|
|
Serializer s;
|
|
s.add32(proof.proposeSeq);
|
|
s.add32(proof.closeTime);
|
|
s.addBitString(proof.prevLedger);
|
|
s.addVL(proof.positionData.slice());
|
|
s.addVL(Slice(proof.signature.data(), proof.signature.size()));
|
|
return s.getData();
|
|
}
|
|
|
|
std::optional<ConsensusExtensions::ProposalProof>
|
|
ConsensusExtensions::deserializeProof(Blob const& proofBlob)
|
|
{
|
|
try
|
|
{
|
|
SerialIter sit(makeSlice(proofBlob));
|
|
|
|
ProposalProof proof;
|
|
proof.proposeSeq = sit.get32();
|
|
proof.closeTime = sit.get32();
|
|
proof.prevLedger = sit.get256();
|
|
|
|
auto const positionData = sit.getVL();
|
|
auto const signature = sit.getVL();
|
|
|
|
if (!sit.empty())
|
|
return std::nullopt;
|
|
|
|
proof.positionData =
|
|
Serializer(positionData.data(), positionData.size());
|
|
proof.signature = Buffer(signature.data(), signature.size());
|
|
return proof;
|
|
}
|
|
catch (std::exception const&)
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
bool
|
|
ConsensusExtensions::verifyProof(
|
|
Blob const& proofBlob,
|
|
PublicKey const& publicKey,
|
|
uint256 const& expectedDigest,
|
|
bool isCommit)
|
|
{
|
|
try
|
|
{
|
|
auto const proof = deserializeProof(proofBlob);
|
|
if (!proof)
|
|
return false;
|
|
|
|
auto const positionData = proof->positionData.slice();
|
|
|
|
// Deserialize ExtendedPosition from the proof
|
|
SerialIter posIter(positionData);
|
|
auto maybePos =
|
|
ExtendedPosition::fromSerialIter(posIter, positionData.size());
|
|
if (!maybePos)
|
|
return false;
|
|
auto position = std::move(*maybePos);
|
|
|
|
// Verify the expected digest matches the position's leaf
|
|
if (isCommit)
|
|
{
|
|
if (!position.myCommitment ||
|
|
*position.myCommitment != expectedDigest)
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if (!position.myReveal || *position.myReveal != expectedDigest)
|
|
return false;
|
|
}
|
|
|
|
// Recompute the signing hash (must match
|
|
// ConsensusProposal::signingHash)
|
|
auto signingHash = sha512Half(
|
|
HashPrefix::proposal,
|
|
proof->proposeSeq,
|
|
proof->closeTime,
|
|
proof->prevLedger,
|
|
position);
|
|
|
|
// Use the proposal verifier rather than calling verifyDigest directly:
|
|
// proposal-proof bytes are part of the authenticated snapshot.
|
|
return verifyProposalDigest(
|
|
publicKey,
|
|
signingHash,
|
|
Slice(proof->signature.data(), proof->signature.size()));
|
|
}
|
|
catch (std::exception const&)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void
|
|
ConsensusExtensions::onRoundStart(
|
|
RCLCxLedger const& prevLedger,
|
|
hash_set<NodeID> lastProposers)
|
|
{
|
|
auto const& rules = prevLedger.ledger_->rules();
|
|
//@@start round-extension-feature-latches
|
|
setRngEnabledThisRound(rules.enabled(featureConsensusEntropy));
|
|
setExportEnabledThisRound(rules.enabled(featureExport));
|
|
//@@end round-extension-feature-latches
|
|
clearRngState();
|
|
|
|
roundPrevLedgerHash_ = prevLedger.ledger_->info().hash;
|
|
rngRoundSeq_ = prevLedger.ledger_->info().seq + 1;
|
|
cacheUNLReport(prevLedger.ledger_);
|
|
auto const validatorView = activeValidatorView();
|
|
if (validatorView->sourceLedgerHash)
|
|
{
|
|
XRPL_ASSERT(
|
|
*validatorView->sourceLedgerHash == roundPrevLedgerHash_,
|
|
"ripple::ConsensusExtensions::onRoundStart : "
|
|
"active view source matches round parent");
|
|
}
|
|
setExpectedProposers(std::move(lastProposers));
|
|
resetSubState();
|
|
}
|
|
|
|
void
|
|
ConsensusExtensions::onTrustedPeerProposal(
|
|
NodeID const& nodeId,
|
|
PublicKey const& publicKey,
|
|
ExtendedPosition const& position,
|
|
std::uint32_t proposeSeq,
|
|
NetClock::time_point closeTime,
|
|
uint256 const& prevLedger,
|
|
Slice const& signature,
|
|
std::vector<std::string> const& exportSignatures)
|
|
{
|
|
// Cluster peers may relay proposals that failed the overlay signature
|
|
// check. Extension sidecars become ledger inputs, so harvest them only
|
|
// after re-checking the proposal proof against the claimed validator key.
|
|
auto const signingHash = sha512Half(
|
|
HashPrefix::proposal,
|
|
proposeSeq,
|
|
closeTime.time_since_epoch().count(),
|
|
prevLedger,
|
|
position);
|
|
if (!verifyProposalDigest(publicKey, signingHash, signature))
|
|
{
|
|
JLOG(j_.debug()) << "ConsensusExtensions: ignoring unsigned proposal "
|
|
"sidecars"
|
|
<< " node=" << nodeId << " proposeSeq=" << proposeSeq;
|
|
return;
|
|
}
|
|
|
|
harvestRngData(
|
|
nodeId,
|
|
publicKey,
|
|
position,
|
|
proposeSeq,
|
|
closeTime,
|
|
prevLedger,
|
|
signature);
|
|
|
|
// Stored future/wrong-ledger proposals are replayed through this path, not
|
|
// through PeerImp's original protobuf packet. Re-harvest signed export
|
|
// blobs here so they are evaluated against the active view for this parent.
|
|
if (!exportSignatures.empty() && position.exportSignaturesHash &&
|
|
proposalExportSignaturesHash(exportSignatures) ==
|
|
*position.exportSignaturesHash)
|
|
{
|
|
harvestExportSignatures(
|
|
publicKey, prevLedger, exportSignatures, "stored proposal");
|
|
}
|
|
}
|
|
|
|
void
|
|
ConsensusExtensions::appendJson(Json::Value& ret) const
|
|
{
|
|
using Int = Json::Value::Int;
|
|
Json::Value rng(Json::objectValue);
|
|
|
|
rng["enabled"] = rngEnabled();
|
|
|
|
auto estStateName = [&]() -> char const* {
|
|
switch (estState_)
|
|
{
|
|
case EstablishState::ConvergingTx:
|
|
return "ConvergingTx";
|
|
case EstablishState::ConvergingCommit:
|
|
return "ConvergingCommit";
|
|
case EstablishState::ConvergingReveal:
|
|
return "ConvergingReveal";
|
|
}
|
|
return "Unknown";
|
|
};
|
|
rng["est_state"] = estStateName();
|
|
rng["commits"] = static_cast<Int>(pendingCommitCount());
|
|
rng["quorum"] = static_cast<Int>(quorumThreshold());
|
|
rng["commit_quorum"] = hasQuorumOfCommits();
|
|
rng["min_reveals"] = hasMinimumReveals();
|
|
rng["any_reveals"] = hasAnyReveals();
|
|
rng["reveals"] = static_cast<Int>(pendingRevealCount());
|
|
rng["likely_participants"] = static_cast<Int>(expectedProposerCount());
|
|
rng["observed_active_participants"] =
|
|
static_cast<Int>(observedParticipantCount());
|
|
if (observedParticipantsHash_)
|
|
{
|
|
rng["observed_participants"] = to_string(*observedParticipantsHash_);
|
|
rng["observed_participants_bitmap"] = observedParticipantsBitmapBin_;
|
|
}
|
|
|
|
ret["rng"] = std::move(rng);
|
|
}
|
|
|
|
void
|
|
ConsensusExtensions::logPosition(
|
|
ExtendedPosition const& pos,
|
|
beast::Journal j,
|
|
beast::severities::Severity level) const
|
|
{
|
|
if (!j.active(level))
|
|
return;
|
|
|
|
j.stream(level) << "STALLDIAG: position-sidecar"
|
|
<< " commitSetHash="
|
|
<< (pos.commitSetHash ? to_string(*pos.commitSetHash)
|
|
: std::string{"none"})
|
|
<< " entropySetHash="
|
|
<< (pos.entropySetHash ? to_string(*pos.entropySetHash)
|
|
: std::string{"none"})
|
|
<< " exportSigSetHash="
|
|
<< (pos.exportSigSetHash ? to_string(*pos.exportSigSetHash)
|
|
: std::string{"none"})
|
|
<< " exportSignaturesHash="
|
|
<< (pos.exportSignaturesHash
|
|
? to_string(*pos.exportSignaturesHash)
|
|
: std::string{"none"})
|
|
<< " observedParticipantsHash="
|
|
<< (pos.observedParticipantsHash
|
|
? to_string(*pos.observedParticipantsHash)
|
|
: std::string{"none"})
|
|
<< " myCommitment=" << (pos.myCommitment ? "yes" : "no")
|
|
<< " myReveal=" << (pos.myReveal ? "yes" : "no");
|
|
}
|
|
|
|
std::size_t
|
|
ConsensusExtensions::harvestExportSignatures(
|
|
PublicKey const& senderPK,
|
|
uint256 const&,
|
|
std::vector<std::string> const& exportSignatures,
|
|
char const*)
|
|
{
|
|
if (!exportEnabled() || exportSignatures.empty() ||
|
|
exportSignatures.size() > ExportLimits::maxExportSharesPerRelay)
|
|
return 0;
|
|
|
|
std::size_t accepted = 0;
|
|
for (auto const& bytes : exportSignatures)
|
|
{
|
|
auto const share = ExportShare::parse(makeSlice(bytes));
|
|
if (!share || share->signingKey != senderPK ||
|
|
share->serialize().slice() != makeSlice(bytes))
|
|
continue;
|
|
if (onExportShare(*share))
|
|
++accepted;
|
|
}
|
|
return accepted;
|
|
}
|
|
|
|
//@@start peer-harvest-export-sigs
|
|
void
|
|
ConsensusExtensions::onTrustedPeerMessage(
|
|
::protocol::TMProposeSet const& wireMsg)
|
|
{
|
|
if (wireMsg.exportsignatures_size() == 0)
|
|
return;
|
|
|
|
auto const senderSlice = makeSlice(wireMsg.nodepubkey());
|
|
if (!publicKeyType(senderSlice))
|
|
return;
|
|
PublicKey const senderPK{senderSlice};
|
|
|
|
if (wireMsg.previousledger().size() != uint256::size())
|
|
return;
|
|
|
|
auto const positionSlice = makeSlice(wireMsg.currenttxhash());
|
|
SerialIter positionSit{positionSlice};
|
|
auto const position = ExtendedPosition::fromSerialIter(
|
|
positionSit, wireMsg.currenttxhash().size());
|
|
if (!position || !position->exportSignaturesHash)
|
|
return;
|
|
|
|
uint256 proposalPrevLedger;
|
|
std::memcpy(
|
|
proposalPrevLedger.data(),
|
|
wireMsg.previousledger().data(),
|
|
uint256::size());
|
|
|
|
std::vector<std::string> exportSignatures;
|
|
exportSignatures.reserve(wireMsg.exportsignatures_size());
|
|
for (int i = 0; i < wireMsg.exportsignatures_size(); ++i)
|
|
{
|
|
if (wireMsg.exportsignatures(i).size() >
|
|
ExportLimits::maxSerializedExportShareBytes)
|
|
return;
|
|
exportSignatures.push_back(wireMsg.exportsignatures(i));
|
|
}
|
|
|
|
// The raw protobuf field is not signed directly; the ExtendedPosition
|
|
// digest is. Keep this check local so every harvesting path enforces the
|
|
// same binding, even when called outside PeerImp's proposal precheck.
|
|
if (proposalExportSignaturesHash(exportSignatures) !=
|
|
*position->exportSignaturesHash)
|
|
return;
|
|
|
|
harvestExportSignatures(
|
|
senderPK, proposalPrevLedger, exportSignatures, "wire proposal");
|
|
}
|
|
//@@end peer-harvest-export-sigs
|
|
|
|
//@@start rng-bootstrap-commitment
|
|
void
|
|
ConsensusExtensions::decoratePosition(
|
|
ExtendedPosition& pos,
|
|
std::shared_ptr<Ledger const> const& prevLedger,
|
|
bool proposing)
|
|
{
|
|
//@@start rng-decorate-position-feature-gate
|
|
if (!proposing || !prevLedger->rules().enabled(featureConsensusEntropy))
|
|
{
|
|
JLOG(j_.debug())
|
|
<< "RNG: decoratePosition skipped"
|
|
<< " proposing=" << (proposing ? "yes" : "no") << " amendment="
|
|
<< (prevLedger->rules().enabled(featureConsensusEntropy) ? "yes"
|
|
: "no")
|
|
<< " prevLedgerSeq=" << prevLedger->info().seq
|
|
<< " prevLedger=" << prevLedger->info().hash;
|
|
return;
|
|
}
|
|
//@@end rng-decorate-position-feature-gate
|
|
|
|
auto const& valKeys = app_.getValidatorKeys();
|
|
if (!valKeys.keys || valKeys.nodeID == beast::zero)
|
|
{
|
|
// Only validators author RNG sidecars. Observers still consume peer
|
|
// sidecars and diagnostics, but never seed local commit/reveal state.
|
|
JLOG(j_.debug()) << "RNG: decoratePosition skipped"
|
|
<< " reason=no-validator-key"
|
|
<< " prevLedgerSeq=" << prevLedger->info().seq
|
|
<< " prevLedger=" << prevLedger->info().hash;
|
|
return;
|
|
}
|
|
|
|
setMode(ConsensusMode::proposing);
|
|
cacheUNLReport(prevLedger);
|
|
generateEntropySecret();
|
|
|
|
pos.myCommitment = entropyCommitment(
|
|
getEntropySecret(),
|
|
valKeys.keys->publicKey,
|
|
prevLedger->info().seq + 1);
|
|
|
|
// Seed our own commitment into pendingCommits_ so we count
|
|
// toward quorum (harvestRngData only sees peer proposals).
|
|
pendingCommits_[valKeys.nodeID] = *pos.myCommitment;
|
|
nodeIdToKey_.insert_or_assign(valKeys.nodeID, valKeys.keys->publicKey);
|
|
|
|
JLOG(j_.info()) << "RNG: decoratePosition bootstrap"
|
|
<< " seq=" << (prevLedger->info().seq + 1)
|
|
<< " prevLedger=" << prevLedger->info().hash
|
|
<< " node=" << valKeys.nodeID
|
|
<< " commitment=" << *pos.myCommitment;
|
|
}
|
|
//@@end rng-bootstrap-commitment
|
|
|
|
//@@start export-sig-attachment
|
|
void
|
|
ConsensusExtensions::attachExportSignatures(
|
|
protocol::TMProposeSet& prop,
|
|
RCLCxPeerPos::Proposal const& proposal)
|
|
{
|
|
if (!exportEnabled())
|
|
return;
|
|
|
|
auto const cfg = app_.getRuntimeConfig().getConsensusTestConfig();
|
|
if (cfg && cfg->noExportSig && *cfg->noExportSig)
|
|
return;
|
|
|
|
auto const& keys = app_.getValidatorKeys();
|
|
auto const validated = app_.getLedgerMaster().getValidatedLedger();
|
|
if (!keys.keys || keys.nodeID == beast::zero || !validated ||
|
|
!validated->rules().enabled(featureExport) || !rngRoundSeq_ ||
|
|
proposal.prevLedger() != roundPrevLedgerHash_)
|
|
return;
|
|
|
|
auto const live = pendingExportLatches(*validated, *rngRoundSeq_);
|
|
auto const snapshot = postValidationExportSigCollector_.fullUnionSnapshot();
|
|
std::size_t attached = 0;
|
|
for (auto const& [origin, contributions] : snapshot)
|
|
{
|
|
auto const latchIt = live.find(origin);
|
|
if (latchIt == live.end())
|
|
continue;
|
|
auto const& latch = latchIt->second;
|
|
auto const originSeq = latch->getFieldU32(sfLedgerSequence);
|
|
auto const originHash = originSeq == validated->info().seq
|
|
? std::optional<uint256>{validated->info().hash}
|
|
: hashOfSeq(*validated, originSeq, j_);
|
|
if (!originHash)
|
|
continue;
|
|
|
|
for (auto const& contribution : contributions)
|
|
{
|
|
if (attached >= ExportLimits::maxExportSharesPerRelay)
|
|
return;
|
|
if (contribution.signingKey != keys.keys->publicKey)
|
|
continue;
|
|
auto const identity =
|
|
std::pair<uint256, ExportSigCollectorV2::Position>{
|
|
origin, contribution.position};
|
|
if (proposalPublishedExportShares_.count(identity) != 0)
|
|
continue;
|
|
|
|
ExportShare share{
|
|
ExportShare::currentVersion,
|
|
latch->getAccountID(sfAccount),
|
|
origin,
|
|
originSeq,
|
|
*originHash,
|
|
origin,
|
|
contribution.position,
|
|
contribution.signingKey,
|
|
contribution.signature};
|
|
if (resolveExportShare(app_, *this, share, validated, j_).status !=
|
|
ExportShareResolutionStatus::resolved)
|
|
continue;
|
|
|
|
auto const frame = share.serialize();
|
|
prop.add_exportsignatures(frame.data(), frame.size());
|
|
proposalPublishedExportShares_.insert(identity);
|
|
++attached;
|
|
}
|
|
}
|
|
}
|
|
//@@end export-sig-attachment
|
|
|
|
void
|
|
ConsensusExtensions::decorateMessage(
|
|
protocol::TMProposeSet&,
|
|
RCLCxPeerPos::Proposal const& proposal,
|
|
ExtendedPosition const& signedPosition,
|
|
Buffer const& proposalSig)
|
|
{
|
|
auto const& valKeys = app_.getValidatorKeys();
|
|
if (!valKeys.keys || valKeys.nodeID == beast::zero)
|
|
return;
|
|
|
|
// Self-seed our own reveal so we count toward reveal quorum
|
|
// (harvestRngData only sees peer proposals, not our own).
|
|
if (signedPosition.myReveal)
|
|
{
|
|
pendingReveals_[valKeys.nodeID] = *signedPosition.myReveal;
|
|
nodeIdToKey_.insert_or_assign(valKeys.nodeID, valKeys.keys->publicKey);
|
|
JLOG(j_.trace()) << "RNG: self-seeded reveal"
|
|
<< " node=" << valKeys.nodeID
|
|
<< " proposeSeq=" << proposal.proposeSeq()
|
|
<< " prevLedger=" << proposal.prevLedger();
|
|
}
|
|
|
|
// Store our own deterministic commit proof for commitSet entries.
|
|
// Reveal sidecars deliberately omit proofs.
|
|
if (signedPosition.myCommitment)
|
|
{
|
|
auto makeProof = [&]() {
|
|
ProposalProof proof;
|
|
proof.proposeSeq = proposal.proposeSeq();
|
|
proof.closeTime = static_cast<std::uint32_t>(
|
|
proposal.closeTime().time_since_epoch().count());
|
|
proof.prevLedger = proposal.prevLedger();
|
|
Serializer s;
|
|
signedPosition.add(s);
|
|
proof.positionData = std::move(s);
|
|
proof.signature = Buffer(proposalSig.data(), proposalSig.size());
|
|
return proof;
|
|
};
|
|
|
|
if (signedPosition.myCommitment && proposal.proposeSeq() == 0)
|
|
commitProofs_.emplace(valKeys.nodeID, makeProof());
|
|
}
|
|
}
|
|
|
|
ExtensionTickResult
|
|
ConsensusExtensions::onTick(TickContext const& ctx)
|
|
{
|
|
return extensionsTick(*this, ctx);
|
|
}
|
|
|
|
} // namespace ripple
|