mirror of
https://github.com/Xahau/xahaud.git
synced 2026-08-23 08:20:52 +00:00
feat(consensus): add ExtendedPosition for RNG entropy support
Introduce data structures for consensus-derived randomness using commit-reveal scheme: - Add ExtendedPosition struct with consensus targets (txSetHash, commitSetHash, entropySetHash) and pipelined leaves (myCommitment, myReveal) - operator== excludes leaves to allow convergence with unique leaves - add() includes ALL fields to prevent signature stripping attacks - Add EstablishState enum for sub-phases: ConvergingTx, ConvergingCommit, ConvergingReveal - Update Consensus template to use Adaptor::Position_t - Add Position_t typedef to RCLConsensus::Adaptor and test CSF Peer This is the foundational data structure work for the RNG implementation. The gating logic and entropy computation will follow.
This commit is contained in:
@@ -166,9 +166,10 @@ RCLConsensus::Adaptor::share(RCLCxPeerPos const& peerPos)
|
||||
prop.set_closetime(proposal.closeTime().time_since_epoch().count());
|
||||
|
||||
prop.set_currenttxhash(
|
||||
proposal.position().begin(), proposal.position().size());
|
||||
proposal.position().txSetHash.begin(),
|
||||
proposal.position().txSetHash.size());
|
||||
prop.set_previousledger(
|
||||
proposal.prevLedger().begin(), proposal.position().size());
|
||||
proposal.prevLedger().begin(), proposal.prevLedger().size());
|
||||
|
||||
auto const pk = peerPos.publicKey().slice();
|
||||
prop.set_nodepubkey(pk.data(), pk.size());
|
||||
@@ -210,7 +211,8 @@ RCLConsensus::Adaptor::propose(RCLCxPeerPos::Proposal const& proposal)
|
||||
protocol::TMProposeSet prop;
|
||||
|
||||
prop.set_currenttxhash(
|
||||
proposal.position().begin(), proposal.position().size());
|
||||
proposal.position().txSetHash.begin(),
|
||||
proposal.position().txSetHash.size());
|
||||
prop.set_previousledger(
|
||||
proposal.prevLedger().begin(), proposal.prevLedger().size());
|
||||
prop.set_proposeseq(proposal.proposeSeq());
|
||||
@@ -390,7 +392,7 @@ RCLConsensus::Adaptor::onClose(
|
||||
RCLCxPeerPos::Proposal{
|
||||
initialLedger->info().parentHash,
|
||||
RCLCxPeerPos::Proposal::seqJoin,
|
||||
setHash,
|
||||
ExtendedPosition{setHash},
|
||||
closeTime,
|
||||
app_.timeKeeper().closeTime(),
|
||||
validatorKeys_.nodeID}};
|
||||
|
||||
@@ -93,6 +93,7 @@ class RCLConsensus
|
||||
using NodeKey_t = PublicKey;
|
||||
using TxSet_t = RCLTxSet;
|
||||
using PeerPosition_t = RCLCxPeerPos;
|
||||
using Position_t = ExtendedPosition;
|
||||
|
||||
using Result = ConsensusResult<Adaptor>;
|
||||
|
||||
|
||||
@@ -47,8 +47,16 @@ RCLCxPeerPos::RCLCxPeerPos(
|
||||
bool
|
||||
RCLCxPeerPos::checkSign() const
|
||||
{
|
||||
return verifyDigest(
|
||||
publicKey(), proposal_.signingHash(), signature(), false);
|
||||
// Use proposalUniqueId to ensure full ExtendedPosition is covered
|
||||
auto const signingHash = proposalUniqueId(
|
||||
proposal_.position(),
|
||||
proposal_.prevLedger(),
|
||||
proposal_.proposeSeq(),
|
||||
proposal_.closeTime(),
|
||||
publicKey_.slice(),
|
||||
Slice{nullptr, 0}); // Exclude signature for signing hash
|
||||
|
||||
return verifyDigest(publicKey(), signingHash, signature(), false);
|
||||
}
|
||||
|
||||
Json::Value
|
||||
@@ -64,7 +72,7 @@ RCLCxPeerPos::getJson() const
|
||||
|
||||
uint256
|
||||
proposalUniqueId(
|
||||
uint256 const& proposeHash,
|
||||
ExtendedPosition const& position,
|
||||
uint256 const& previousLedger,
|
||||
std::uint32_t proposeSeq,
|
||||
NetClock::time_point closeTime,
|
||||
@@ -72,10 +80,14 @@ proposalUniqueId(
|
||||
Slice const& signature)
|
||||
{
|
||||
Serializer s(512);
|
||||
s.addBitString(proposeHash);
|
||||
s.addBitString(previousLedger);
|
||||
s.add32(HashPrefix::proposal);
|
||||
s.add32(proposeSeq);
|
||||
s.add32(closeTime.time_since_epoch().count());
|
||||
s.addBitString(previousLedger);
|
||||
|
||||
// Serialize full ExtendedPosition (TxSet + Sets + Leaves)
|
||||
position.add(s);
|
||||
|
||||
s.addVL(publicKey);
|
||||
s.addVL(signature);
|
||||
|
||||
|
||||
@@ -28,13 +28,160 @@
|
||||
#include <ripple/protocol/HashPrefix.h>
|
||||
#include <ripple/protocol/PublicKey.h>
|
||||
#include <ripple/protocol/SecretKey.h>
|
||||
#include <ripple/protocol/Serializer.h>
|
||||
#include <boost/container/static_vector.hpp>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** Extended position for consensus with RNG entropy support.
|
||||
|
||||
Carries both the consensus targets (set hashes that require agreement)
|
||||
and pipelined leaves (per-validator data transported via gossip).
|
||||
|
||||
Critical design:
|
||||
- operator== excludes leaves (allows convergence with unique leaves)
|
||||
- add() includes ALL fields (prevents signature stripping attacks)
|
||||
*/
|
||||
struct ExtendedPosition
|
||||
{
|
||||
// === Consensus Targets (Agreement Required) ===
|
||||
uint256 txSetHash;
|
||||
std::optional<uint256> commitSetHash;
|
||||
std::optional<uint256> entropySetHash;
|
||||
|
||||
// === Pipelined Leaves (No Agreement Required) ===
|
||||
std::optional<uint256> myCommitment;
|
||||
std::optional<uint256> myReveal;
|
||||
|
||||
ExtendedPosition() = default;
|
||||
explicit ExtendedPosition(uint256 const& txSet) : txSetHash(txSet)
|
||||
{
|
||||
}
|
||||
|
||||
// Implicit conversion for legacy compatibility
|
||||
operator uint256() const
|
||||
{
|
||||
return txSetHash;
|
||||
}
|
||||
|
||||
// Helper to update TxSet while preserving sidecar data
|
||||
void
|
||||
updateTxSet(uint256 const& set)
|
||||
{
|
||||
txSetHash = set;
|
||||
}
|
||||
|
||||
// CRITICAL: Exclude leaves from equality - consensus only on set hashes
|
||||
bool
|
||||
operator==(ExtendedPosition const& other) const
|
||||
{
|
||||
return txSetHash == other.txSetHash &&
|
||||
commitSetHash == other.commitSetHash &&
|
||||
entropySetHash == other.entropySetHash;
|
||||
}
|
||||
|
||||
bool
|
||||
operator!=(ExtendedPosition const& other) const
|
||||
{
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
// Comparison with uint256 (compares txSetHash only)
|
||||
bool
|
||||
operator==(uint256 const& hash) const
|
||||
{
|
||||
return txSetHash == hash;
|
||||
}
|
||||
|
||||
bool
|
||||
operator!=(uint256 const& hash) const
|
||||
{
|
||||
return txSetHash != hash;
|
||||
}
|
||||
|
||||
friend bool
|
||||
operator==(uint256 const& hash, ExtendedPosition const& pos)
|
||||
{
|
||||
return pos.txSetHash == hash;
|
||||
}
|
||||
|
||||
friend bool
|
||||
operator!=(uint256 const& hash, ExtendedPosition const& pos)
|
||||
{
|
||||
return pos.txSetHash != hash;
|
||||
}
|
||||
|
||||
// CRITICAL: Include ALL fields for signing (prevents stripping attacks)
|
||||
void
|
||||
add(Serializer& s) const
|
||||
{
|
||||
s.addBitString(txSetHash);
|
||||
|
||||
std::uint8_t flags = 0;
|
||||
if (commitSetHash)
|
||||
flags |= 0x01;
|
||||
if (entropySetHash)
|
||||
flags |= 0x02;
|
||||
if (myCommitment)
|
||||
flags |= 0x04;
|
||||
if (myReveal)
|
||||
flags |= 0x08;
|
||||
s.add8(flags);
|
||||
|
||||
if (commitSetHash)
|
||||
s.addBitString(*commitSetHash);
|
||||
if (entropySetHash)
|
||||
s.addBitString(*entropySetHash);
|
||||
if (myCommitment)
|
||||
s.addBitString(*myCommitment);
|
||||
if (myReveal)
|
||||
s.addBitString(*myReveal);
|
||||
}
|
||||
|
||||
Json::Value
|
||||
getJson() const
|
||||
{
|
||||
Json::Value ret = Json::objectValue;
|
||||
ret["tx_set"] = to_string(txSetHash);
|
||||
if (commitSetHash)
|
||||
ret["commit_set"] = to_string(*commitSetHash);
|
||||
if (entropySetHash)
|
||||
ret["entropy_set"] = to_string(*entropySetHash);
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
// For logging/debugging - returns txSetHash as string
|
||||
inline std::string
|
||||
to_string(ExtendedPosition const& pos)
|
||||
{
|
||||
return to_string(pos.txSetHash);
|
||||
}
|
||||
|
||||
// Stream output for logging
|
||||
inline std::ostream&
|
||||
operator<<(std::ostream& os, ExtendedPosition const& pos)
|
||||
{
|
||||
return os << pos.txSetHash;
|
||||
}
|
||||
|
||||
// For hash_append (used in sha512Half and similar)
|
||||
template <class Hasher>
|
||||
void
|
||||
hash_append(Hasher& h, ExtendedPosition const& pos)
|
||||
{
|
||||
using beast::hash_append;
|
||||
// Serialize full position including all fields
|
||||
Serializer s;
|
||||
pos.add(s);
|
||||
hash_append(h, s.slice());
|
||||
}
|
||||
|
||||
/** A peer's signed, proposed position for use in RCLConsensus.
|
||||
|
||||
Carries a ConsensusProposal signed by a peer. Provides value semantics
|
||||
@@ -43,8 +190,9 @@ namespace ripple {
|
||||
class RCLCxPeerPos
|
||||
{
|
||||
public:
|
||||
//< The type of the proposed position
|
||||
using Proposal = ConsensusProposal<NodeID, uint256, uint256>;
|
||||
//< The type of the proposed position (uses ExtendedPosition for RNG
|
||||
//support)
|
||||
using Proposal = ConsensusProposal<NodeID, uint256, ExtendedPosition>;
|
||||
|
||||
/** Constructor
|
||||
|
||||
@@ -112,7 +260,10 @@ private:
|
||||
hash_append(h, std::uint32_t(proposal().proposeSeq()));
|
||||
hash_append(h, proposal().closeTime());
|
||||
hash_append(h, proposal().prevLedger());
|
||||
hash_append(h, proposal().position());
|
||||
// Serialize full ExtendedPosition for hashing
|
||||
Serializer s;
|
||||
proposal().position().add(s);
|
||||
hash_append(h, s.slice());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -125,7 +276,7 @@ private:
|
||||
order to validate the signature. If the last closed ledger is left out, then
|
||||
it is considered as all zeroes for the purposes of signing.
|
||||
|
||||
@param proposeHash The hash of the proposed position
|
||||
@param position The extended position (includes entropy fields)
|
||||
@param previousLedger The hash of the ledger the proposal is based upon
|
||||
@param proposeSeq Sequence number of the proposal
|
||||
@param closeTime Close time of the proposal
|
||||
@@ -134,7 +285,7 @@ private:
|
||||
*/
|
||||
uint256
|
||||
proposalUniqueId(
|
||||
uint256 const& proposeHash,
|
||||
ExtendedPosition const& position,
|
||||
uint256 const& previousLedger,
|
||||
std::uint32_t proposeSeq,
|
||||
NetClock::time_point closeTime,
|
||||
|
||||
@@ -36,6 +36,21 @@
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** Sub-states for pipelined consensus with RNG entropy support.
|
||||
|
||||
The establish phase is divided into sub-states to support commit-reveal
|
||||
for consensus-derived randomness while maintaining low latency through
|
||||
pipelining.
|
||||
|
||||
@note Data collection (commits, reveals) happens continuously via proposal
|
||||
leaves. Sub-states are checkpoints, not serial waits.
|
||||
*/
|
||||
enum class EstablishState {
|
||||
ConvergingTx, ///< Normal txset convergence + harvesting commits
|
||||
ConvergingCommit, ///< Confirming commitSet agreement (near-instant)
|
||||
ConvergingReveal ///< Collecting reveals + confirming entropySet
|
||||
};
|
||||
|
||||
/** Determines whether the current ledger should close at this time.
|
||||
|
||||
This function should be called when a ledger is open and there is no close
|
||||
@@ -289,10 +304,11 @@ class Consensus
|
||||
using NodeID_t = typename Adaptor::NodeID_t;
|
||||
using Tx_t = typename TxSet_t::Tx;
|
||||
using PeerPosition_t = typename Adaptor::PeerPosition_t;
|
||||
// Use Adaptor::Position_t for RNG support (ExtendedPosition)
|
||||
using Proposal_t = ConsensusProposal<
|
||||
NodeID_t,
|
||||
typename Ledger_t::ID,
|
||||
typename TxSet_t::ID>;
|
||||
typename Adaptor::Position_t>;
|
||||
|
||||
using Result = ConsensusResult<Adaptor>;
|
||||
|
||||
@@ -542,6 +558,7 @@ private:
|
||||
Adaptor& adaptor_;
|
||||
|
||||
ConsensusPhase phase_{ConsensusPhase::accepted};
|
||||
EstablishState estState_{EstablishState::ConvergingTx};
|
||||
MonitoredMode mode_{ConsensusMode::observing};
|
||||
bool firstRound_ = true;
|
||||
bool haveCloseTimeConsensus_ = false;
|
||||
@@ -1515,7 +1532,21 @@ Consensus<Adaptor>::updateOurPositions()
|
||||
<< consensusCloseTime.time_since_epoch().count()
|
||||
<< ", tx " << newID;
|
||||
|
||||
result_->position.changePosition(newID, consensusCloseTime, now_);
|
||||
// Preserve sidecar data (RNG fields), only update txSetHash
|
||||
// Use type traits to conditionally handle ExtendedPosition vs simple ID
|
||||
if constexpr (requires(typename Adaptor::Position_t p) {
|
||||
p.updateTxSet(newID);
|
||||
})
|
||||
{
|
||||
auto currentPos = result_->position.position();
|
||||
currentPos.updateTxSet(newID);
|
||||
result_->position.changePosition(
|
||||
currentPos, consensusCloseTime, now_);
|
||||
}
|
||||
else
|
||||
{
|
||||
result_->position.changePosition(newID, consensusCloseTime, now_);
|
||||
}
|
||||
|
||||
// Share our new transaction set and update disputes
|
||||
// if we haven't already received it
|
||||
|
||||
@@ -205,16 +205,20 @@ struct ConsensusResult
|
||||
using NodeID_t = typename Traits::NodeID_t;
|
||||
|
||||
using Tx_t = typename TxSet_t::Tx;
|
||||
// Use Traits::Position_t for RNG support (defaults to TxSet_t::ID)
|
||||
using Proposal_t = ConsensusProposal<
|
||||
NodeID_t,
|
||||
typename Ledger_t::ID,
|
||||
typename TxSet_t::ID>;
|
||||
typename Traits::Position_t>;
|
||||
using Dispute_t = DisputedTx<Tx_t, NodeID_t>;
|
||||
|
||||
ConsensusResult(TxSet_t&& s, Proposal_t&& p)
|
||||
: txns{std::move(s)}, position{std::move(p)}
|
||||
{
|
||||
assert(txns.id() == position.position());
|
||||
// Use implicit conversion to uint256 for ExtendedPosition
|
||||
assert(
|
||||
txns.id() ==
|
||||
static_cast<typename TxSet_t::ID>(position.position()));
|
||||
}
|
||||
|
||||
//! The set of transactions consensus agrees go in the ledger
|
||||
|
||||
@@ -1961,7 +1961,7 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMProposeSet> const& m)
|
||||
NetClock::time_point const closeTime{NetClock::duration{set.closetime()}};
|
||||
|
||||
uint256 const suppression = proposalUniqueId(
|
||||
proposeHash,
|
||||
ExtendedPosition{proposeHash},
|
||||
prevLedger,
|
||||
set.proposeseq(),
|
||||
closeTime,
|
||||
@@ -2008,7 +2008,7 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMProposeSet> const& m)
|
||||
RCLCxPeerPos::Proposal{
|
||||
prevLedger,
|
||||
set.proposeseq(),
|
||||
proposeHash,
|
||||
ExtendedPosition{proposeHash},
|
||||
closeTime,
|
||||
app_.timeKeeper().closeTime(),
|
||||
calcNodeID(app_.validatorManifests().getMasterKey(publicKey))});
|
||||
|
||||
@@ -145,6 +145,7 @@ public:
|
||||
using namespace csf;
|
||||
using namespace std::chrono;
|
||||
|
||||
//@@start peers-agree
|
||||
ConsensusParms const parms{};
|
||||
Sim sim;
|
||||
PeerGroup peers = sim.createGroup(5);
|
||||
@@ -174,6 +175,7 @@ public:
|
||||
BEAST_EXPECT(lcl.txs().find(Tx{i}) != lcl.txs().end());
|
||||
}
|
||||
}
|
||||
//@@end peers-agree
|
||||
}
|
||||
|
||||
void
|
||||
@@ -186,6 +188,7 @@ public:
|
||||
// that have significantly longer network delays to the rest of the
|
||||
// network
|
||||
|
||||
//@@start slow-peer-scenario
|
||||
// Test when a slow peer doesn't delay a consensus quorum (4/5 agree)
|
||||
{
|
||||
ConsensusParms const parms{};
|
||||
@@ -224,16 +227,18 @@ public:
|
||||
BEAST_EXPECT(
|
||||
peer->prevRoundTime == network[0]->prevRoundTime);
|
||||
|
||||
// Slow peer's transaction (Tx{0}) didn't make it in time
|
||||
BEAST_EXPECT(lcl.txs().find(Tx{0}) == lcl.txs().end());
|
||||
for (std::uint32_t i = 2; i < network.size(); ++i)
|
||||
BEAST_EXPECT(lcl.txs().find(Tx{i}) != lcl.txs().end());
|
||||
|
||||
// Tx 0 didn't make it
|
||||
// Tx 0 is still in the open transaction set for next round
|
||||
BEAST_EXPECT(
|
||||
peer->openTxs.find(Tx{0}) != peer->openTxs.end());
|
||||
}
|
||||
}
|
||||
}
|
||||
//@@end slow-peer-scenario
|
||||
|
||||
// Test when the slow peers delay a consensus quorum (4/6 agree)
|
||||
{
|
||||
@@ -421,6 +426,7 @@ public:
|
||||
// the wrong LCL at different phases of consensus
|
||||
for (auto validationDelay : {0ms, parms.ledgerMIN_CLOSE})
|
||||
{
|
||||
//@@start wrong-lcl-scenario
|
||||
// Consider 10 peers:
|
||||
// 0 1 2 3 4 5 6 7 8 9
|
||||
// minority majorityA majorityB
|
||||
@@ -441,6 +447,7 @@ public:
|
||||
|
||||
// This topology can potentially fork with the above trust relations
|
||||
// but that is intended for this test.
|
||||
//@@end wrong-lcl-scenario
|
||||
|
||||
Sim sim;
|
||||
|
||||
@@ -724,6 +731,7 @@ public:
|
||||
}
|
||||
sim.run(1);
|
||||
|
||||
//@@start fork-threshold
|
||||
// Fork should not happen for 40% or greater overlap
|
||||
// Since the overlapped nodes have a UNL that is the union of the
|
||||
// two cliques, the maximum sized UNL list is the number of peers
|
||||
@@ -735,6 +743,7 @@ public:
|
||||
// One for cliqueA, one for cliqueB and one for nodes in both
|
||||
BEAST_EXPECT(sim.branches() <= 3);
|
||||
}
|
||||
//@@end fork-threshold
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -159,6 +159,8 @@ struct Peer
|
||||
using NodeKey_t = PeerKey;
|
||||
using TxSet_t = TxSet;
|
||||
using PeerPosition_t = Position;
|
||||
using Position_t =
|
||||
typename TxSet_t::ID; // Use TxSet::ID for test framework
|
||||
using Result = ConsensusResult<Peer>;
|
||||
using NodeKey = Validation::NodeKey;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user