Compare commits

...

1 Commits

Author SHA1 Message Date
Richard Holland
dc17cb44a7 partial 2026-01-29 12:05:07 +11:00
5 changed files with 198 additions and 10 deletions

View File

@@ -479,11 +479,24 @@ private:
to reach consensus. Update our position only on the timer, and in this to reach consensus. Update our position only on the timer, and in this
phase. phase.
If we have consensus, move to the accepted phase. If we have consensus, move to the shuffle phase.
*/ */
void void
phaseEstablish(); phaseEstablish();
/** Handle shuffle phase.
In the shuffle phase, UNLReport nodes exchange entropy to build
a consensus entropy that is then used as an RNG source for Hooks.
The entropy is injected as a ttSHUFFLE psuedo into the final ledger
If we have consensus, move to the accepted phase.
*/
void
phaseShuffle();
/** Evaluate whether pausing increases likelihood of validation. /** Evaluate whether pausing increases likelihood of validation.
* *
* As a validator that has previously synced to the network, if our most * As a validator that has previously synced to the network, if our most
@@ -588,6 +601,10 @@ private:
// Peer proposed positions for the current round // Peer proposed positions for the current round
hash_map<NodeID_t, PeerPosition_t> currPeerPositions_; hash_map<NodeID_t, PeerPosition_t> currPeerPositions_;
// our and our peers' entropy as per TMShuffle, used in phaseShuffle
std::optional<uint256> ourEntropy_;
hash_map<NodeID_t, std::pair<uint256, uint256>> currPeerEntropy_;
// Recently received peer positions, available when transitioning between // Recently received peer positions, available when transitioning between
// ledgers or rounds // ledgers or rounds
hash_map<NodeID_t, std::deque<PeerPosition_t>> recentPeerPositions_; hash_map<NodeID_t, std::deque<PeerPosition_t>> recentPeerPositions_;
@@ -832,6 +849,10 @@ Consensus<Adaptor>::timerEntry(NetClock::time_point const& now)
{ {
phaseEstablish(); phaseEstablish();
} }
else if (phase_ == ConsensusPhase::shuffle)
{
phaseShuffle();
}
} }
template <class Adaptor> template <class Adaptor>
@@ -1291,8 +1312,12 @@ Consensus<Adaptor>::phaseEstablish()
adaptor_.updateOperatingMode(currPeerPositions_.size()); adaptor_.updateOperatingMode(currPeerPositions_.size());
prevProposers_ = currPeerPositions_.size(); prevProposers_ = currPeerPositions_.size();
prevRoundTime_ = result_->roundTime.read(); prevRoundTime_ = result_->roundTime.read();
phase_ = ConsensusPhase::accepted;
JLOG(j_.debug()) << "transitioned to ConsensusPhase::accepted"; // RHTODO: guard with amendment
phase_ = ConsensusPhase::shuffle;
JLOG(j_.debug()) << "transitioned to ConsensusPhase::shuffle";
/*
adaptor_.onAccept( adaptor_.onAccept(
*result_, *result_,
previousLedger_, previousLedger_,
@@ -1300,6 +1325,60 @@ Consensus<Adaptor>::phaseEstablish()
rawCloseTimes_, rawCloseTimes_,
mode_.get(), mode_.get(),
getJson(true)); getJson(true));
*/
}
template <class Adaptor>
void
Consensus<Adaptor>::phaseShuffle()
{
// can only establish consensus if we already took a stance
assert(result_);
using namespace std::chrono;
ConsensusParms const& parms = adaptor_.parms();
result_->roundTime.tick(clock_.now());
result_->proposers = currPeerPositions_.size();
convergePercent_ = result_->roundTime.read() * 100 /
std::max<milliseconds>(prevRoundTime_, parms.avMIN_CONSENSUS_TIME);
// Give everyone a chance to take an initial position
if (result_->roundTime.read() < parms.ledgerMIN_CONSENSUS)
return;
updateOurPositions();
// Nothing to do if too many laggards or we don't have consensus.
if (shouldPause() || !haveConsensus())
return;
if (!haveCloseTimeConsensus_)
{
JLOG(j_.info()) << "We have TX consensus but not CT consensus";
return;
}
JLOG(j_.info()) << "Converge cutoff (" << currPeerPositions_.size()
<< " participants)";
adaptor_.updateOperatingMode(currPeerPositions_.size());
prevProposers_ = currPeerPositions_.size();
prevRoundTime_ = result_->roundTime.read();
// RHTODO: guard with amendment
phase_ = ConsensusPhase::shuffle;
JLOG(j_.debug()) << "transitioned to ConsensusPhase::shuffle";
/*
adaptor_.onAccept(
*result_,
previousLedger_,
closeResolution_,
rawCloseTimes_,
mode_.get(),
getJson(true));
*/
} }
template <class Adaptor> template <class Adaptor>

View File

@@ -87,15 +87,15 @@ to_string(ConsensusMode m)
/** Phases of consensus for a single ledger round. /** Phases of consensus for a single ledger round.
@code @code
"close" "accept" "close" "shuffle" "accept"
open ------- > establish ---------> accepted open ------- > establish -------> shuffle ---------> accepted
^ | | ^ | |
|---------------| | |---------------| |
^ "startRound" | ^ "startRound" |
|------------------------------------| |-----------------------------------------------------|
@endcode @endcode
The typical transition goes from open to establish to accepted and The typical transition goes from open to establish to shuffle to accepted and
then a call to startRound begins the process anew. However, if a wrong prior then a call to startRound begins the process anew. However, if a wrong prior
ledger is detected and recovered during the establish or accept phase, ledger is detected and recovered during the establish or accept phase,
consensus will internally go back to open (see Consensus::handleWrongLedger). consensus will internally go back to open (see Consensus::handleWrongLedger).
@@ -107,6 +107,9 @@ enum class ConsensusPhase {
//! Establishing consensus by exchanging proposals with our peers //! Establishing consensus by exchanging proposals with our peers
establish, establish,
//! Negotitate featureRNG entropy
shuffle,
//! We have accepted a new last closed ledger and are waiting on a call //! We have accepted a new last closed ledger and are waiting on a call
//! to startRound to begin the next consensus round. No changes //! to startRound to begin the next consensus round. No changes
//! to consensus phase occur while in this phase. //! to consensus phase occur while in this phase.
@@ -122,6 +125,8 @@ to_string(ConsensusPhase p)
return "open"; return "open";
case ConsensusPhase::establish: case ConsensusPhase::establish:
return "establish"; return "establish";
case ConsensusPhase::shuffle:
return "shuffle";
case ConsensusPhase::accepted: case ConsensusPhase::accepted:
return "accepted"; return "accepted";
default: default:

View File

@@ -1094,6 +1094,7 @@ trustTransferLockedBalance(
} }
return tesSUCCESS; return tesSUCCESS;
} }
} // namespace ripple } // namespace ripple
#endif #endif

View File

@@ -1918,6 +1918,100 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMLedgerData> const& m)
app_.getInboundLedgers().gotLedgerData(ledgerHash, shared_from_this(), m); app_.getInboundLedgers().gotLedgerData(ledgerHash, shared_from_this(), m);
} }
void
PeerImp::onMessage(std::shared_ptr<protocol::TMShuffle> const& m)
{
protocol::TMShuffle& shuf = *m;
auto const sig = makeSlice(shf.signature());
// Preliminary check for the validity of the signature: A DER encoded
// signature can't be longer than 72 bytes.
if ((std::clamp<std::size_t>(sig.size(), 64, 72) != sig.size()) ||
(publicKeyType(makeSlice(shf.nodepubkey())) != KeyType::secp256k1))
{
JLOG(p_journal_.warn()) << "Shuffle: malformed";
fee_ = Resource::feeInvalidSignature;
return;
}
if (!stringIsUint256Sized(shf.nodeentropy()) ||
!stringIsUint256Sized(shf.consensusentropy()) ||
!stringIsUint256Sized(shf.previousledger()))
{
JLOG(p_journal_.warn()) << "Shuffle: malformed";
fee_ = Resource::feeInvalidRequest;
return;
}
PublicKey const publicKey{makeSlice(shf.nodepubkey())};
auto const isTrusted = app_.validators().trusted(publicKey);
if (!isTrusted)
return;
uint256 const prevLedger{shf.previousledger()};
uint32_t const shuffleSeq{shf.shuffleseq()};
uint256 const nodeEntropy{shf.nodeentropy()};
uint256 const consensusEntropy{shf.consensusentropy()};
uint256 const suppression = sha512Half(std::string("TMShuffle", sig));
if (auto [added, relayed] =
app_.getHashRouter().addSuppressionPeerWithStatus(suppression, id_);
!added)
{
// Count unique messages (Slots has it's own 'HashRouter'), which a peer
// receives within IDLED seconds since the message has been relayed.
if (reduceRelayReady() && relayed &&
(stopwatch().now() - *relayed) < reduce_relay::IDLED)
overlay_.updateSlotAndSquelch(
suppression, publicKey, id_, protocol::mtSHUFFLE);
JLOG(p_journal_.trace()) << "Shuffle: duplicate";
return;
}
if (!isTrusted)
{
if (tracking_.load() == Tracking::diverged)
{
JLOG(p_journal_.debug())
<< "Proposal: Dropping untrusted (peer divergence)";
return;
}
if (!cluster() && app_.getFeeTrack().isLoadedLocal())
{
JLOG(p_journal_.debug()) << "Proposal: Dropping untrusted (load)";
return;
}
}
JLOG(p_journal_.trace())
<< "Proposal: " << (isTrusted ? "trusted" : "untrusted");
auto proposal = RCLCxPeerPos(
publicKey,
sig,
suppression,
RCLCxPeerPos::Proposal{
prevLedger,
set.proposeseq(),
proposeHash,
closeTime,
app_.timeKeeper().closeTime(),
calcNodeID(app_.validatorManifests().getMasterKey(publicKey))});
std::weak_ptr<PeerImp> weak = shared_from_this();
app_.getJobQueue().addJob(
isTrusted ? jtPROPOSAL_t : jtPROPOSAL_ut,
"recvPropose->checkPropose",
[weak, isTrusted, m, proposal]() {
if (auto peer = weak.lock())
peer->checkPropose(isTrusted, m, proposal);
});
}
void void
PeerImp::onMessage(std::shared_ptr<protocol::TMProposeSet> const& m) PeerImp::onMessage(std::shared_ptr<protocol::TMProposeSet> const& m)
{ {

View File

@@ -450,3 +450,12 @@ message TMHaveTransactions
repeated bytes hashes = 1; repeated bytes hashes = 1;
} }
message TMShuffle
{
required bytes nodeEntropy = 1;
required bytes consensusEntropy = 2;
required uint32 shuffleSeq = 3;
required bytes nodePubKey = 4;
required bytes previousledger = 5;
required bytes signature = 6; // signature of above fields
}