Compare commits

..

1 Commits

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

View File

@@ -192,7 +192,6 @@
#define sfNFTokenMinter ((8U << 16U) + 9U)
#define sfEmitCallback ((8U << 16U) + 10U)
#define sfHookAccount ((8U << 16U) + 16U)
#define sfHookAdministrator ((8U << 16U) + 98U)
#define sfInform ((8U << 16U) + 99U)
#define sfIndexes ((19U << 16U) + 1U)
#define sfHashes ((19U << 16U) + 2U)

View File

@@ -267,9 +267,6 @@ DeleteAccount::preclaim(PreclaimContext const& ctx)
if (sleAccount->isFieldPresent(sfHookNamespaces) ||
sleAccount->isFieldPresent(sfHooks))
return tecHAS_OBLIGATIONS;
if (sleAccount->isFieldPresent(sfHookAdministrator))
return tecHAS_OBLIGATIONS;
}
// When fixNFTokenRemint is enabled, we don't allow an account to be

View File

@@ -897,9 +897,7 @@ ValidNewAccountRoot::finalize(
}
if ((tt == ttPAYMENT || tt == ttIMPORT || tt == ttGENESIS_MINT ||
tt == ttREMIT ||
(tt == ttHOOK_SET &&
view.rules().enabled(featureHookAdministrator))) &&
tt == ttREMIT) &&
isTesSuccess(result))
{
std::uint32_t const startingSeq{

View File

@@ -628,21 +628,6 @@ SetHook::calculateBaseFee(ReadView const& view, STTx const& tx)
TER
SetHook::preclaim(ripple::PreclaimContext const& ctx)
{
if (ctx.tx.isFieldPresent(sfHookAdministrator))
{
auto const& administrator = ctx.tx.getAccountID(sfHookAdministrator);
auto const& sle = ctx.view.read(keylet::account(administrator));
if (!sle)
return tecNO_DST;
if (!sle->isFieldPresent(sfHookAdministrator))
return tecNO_PERMISSION;
if (sle->getAccountID(sfHookAdministrator) !=
ctx.tx.getAccountID(sfAccount))
return tecNO_PERMISSION;
}
auto const& hookSets = ctx.tx.getFieldArray(sfHooks);
for (auto const& hookSetObj : hookSets)
@@ -682,46 +667,12 @@ SetHook::preflight(PreflightContext const& ctx)
return ret;
if (ctx.rules.enabled(fixInvalidTxFlags) &&
ctx.tx.getFlags() & tfSetHookMask)
ctx.tx.getFlags() & tfUniversalMask)
{
JLOG(ctx.j.trace()) << "SetHook: Invalid flags set.";
return temINVALID_FLAG;
}
if (ctx.tx.isFlag(tfNewAccount) &&
!ctx.rules.enabled(featureHookAdministrator))
{
JLOG(ctx.j.trace()) << "SetHook: New account flag set but hook "
"administrator amendment is not enabled.";
return temDISABLED;
}
if (ctx.tx.isFieldPresent(sfDestination))
{
if (!ctx.rules.enabled(featureHookAdministrator))
{
JLOG(ctx.j.trace())
<< "HookSet: Hook administrator amendment not enabled.";
return temDISABLED;
}
if (ctx.tx.isFlag(tfNewAccount))
{
JLOG(ctx.j.trace())
<< "HookSet: Both new account flag and destination set. "
"New account flag and destination cannot be set at the same "
"time.";
return temMALFORMED;
}
if (ctx.tx.getAccountID(sfDestination) ==
ctx.tx.getAccountID(sfAccount))
{
JLOG(ctx.j.trace()) << "HookSet: Redundant hook administrator.";
return temREDUNDANT;
}
}
if (!ctx.tx.isFieldPresent(sfHooks))
{
JLOG(ctx.j.trace())
@@ -1232,23 +1183,6 @@ struct KeyletComparator
}
};
AccountID
randomAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey)
{
// This number must not be changed without an amendment
constexpr std::uint16_t maxAccountAttempts = 256;
for (std::uint16_t i = 0; i < maxAccountAttempts; ++i)
{
ripesha_hasher rsh;
auto const hash = sha512Half(i, view.info().parentHash, pseudoOwnerKey);
rsh(hash.data(), hash.size());
AccountID const ret{static_cast<ripesha_hasher::result_type>(rsh)};
if (!view.read(keylet::account(ret)))
return ret;
}
return beast::zero;
}
TER
SetHook::setHook()
{
@@ -1268,69 +1202,11 @@ SetHook::setHook()
.app = ctx_.app,
.rules = ctx_.view().rules()};
auto targetAccount = ctx.tx[~sfDestination].value_or(account_);
if (ctx_.tx.isFlag(tfNewAccount))
{
// create the new account
auto const newAccount = randomAccountAddress(ctx_.view(), uint256{});
if (newAccount == beast::zero)
return tecDUPLICATE;
auto sleNewAccount = std::make_shared<SLE>(keylet::account(newAccount));
sleNewAccount->setAccountID(sfAccount, newAccount);
sleNewAccount->setFieldAmount(sfBalance, STAmount{});
sleNewAccount->setFieldU32(sfOwnerCount, 1); // ltHook
std::uint32_t const seqno{
ctx_.view().rules().enabled(featureXahauGenesis)
? ctx_.view().info().parentCloseTime.time_since_epoch().count()
: ctx_.view().rules().enabled(featureDeletableAccounts)
? ctx_.view().seq()
: 1};
sleNewAccount->setFieldU32(sfSequence, seqno);
sleNewAccount->setFieldU32(sfFlags, lsfDisableMaster);
sleNewAccount->setAccountID(sfHookAdministrator, account_);
auto sleFees = view().peek(keylet::fees());
if (sleFees && view().rules().enabled(featureXahauGenesis))
{
auto actIdx = sleFees->isFieldPresent(sfAccountCount)
? sleFees->getFieldU64(sfAccountCount)
: 0;
sleNewAccount->setFieldU64(sfAccountIndex, actIdx);
sleFees->setFieldU64(sfAccountCount, actIdx + 1);
view().update(sleFees);
}
// fund AccountReserve + ObjectReserve (ltHook)
auto const requiredDrops = ctx_.view().fees().accountReserve(1);
auto sourceSle = ctx_.view().peek(keylet::account(account_));
if (!sourceSle)
return tefINTERNAL;
auto const sourceCurrentReserve = ctx_.view().fees().accountReserve(
sourceSle->getFieldU32(sfOwnerCount));
auto const sourceBalance = sourceSle->getFieldAmount(sfBalance).xrp();
if (sourceBalance < sourceCurrentReserve + requiredDrops)
return tecUNFUNDED;
sourceSle->setFieldAmount(sfBalance, sourceBalance - requiredDrops);
ctx_.view().update(sourceSle);
sleNewAccount->setFieldAmount(sfBalance, requiredDrops);
ctx_.view().insert(sleNewAccount);
targetAccount = newAccount;
}
const int blobMax = hook::maxHookWasmSize();
auto const accountKeylet = keylet::account(account_);
auto const hookKeylet = keylet::hook(account_);
auto const hookKeylet = keylet::hook(targetAccount);
auto accountSLE = view().peek(keylet::account(targetAccount));
auto accountSLE = view().peek(accountKeylet);
ripple::STArray newHooks{sfHooks, 8};
auto newHookSLE = std::make_shared<SLE>(hookKeylet);

View File

@@ -479,11 +479,24 @@ private:
to reach consensus. Update our position only on the timer, and in this
phase.
If we have consensus, move to the accepted phase.
If we have consensus, move to the shuffle phase.
*/
void
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.
*
* 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
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
// ledgers or rounds
hash_map<NodeID_t, std::deque<PeerPosition_t>> recentPeerPositions_;
@@ -832,6 +849,10 @@ Consensus<Adaptor>::timerEntry(NetClock::time_point const& now)
{
phaseEstablish();
}
else if (phase_ == ConsensusPhase::shuffle)
{
phaseShuffle();
}
}
template <class Adaptor>
@@ -1291,8 +1312,12 @@ Consensus<Adaptor>::phaseEstablish()
adaptor_.updateOperatingMode(currPeerPositions_.size());
prevProposers_ = currPeerPositions_.size();
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(
*result_,
previousLedger_,
@@ -1300,6 +1325,60 @@ Consensus<Adaptor>::phaseEstablish()
rawCloseTimes_,
mode_.get(),
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>

View File

@@ -87,15 +87,15 @@ to_string(ConsensusMode m)
/** Phases of consensus for a single ledger round.
@code
"close" "accept"
open ------- > establish ---------> accepted
^ | |
|---------------| |
^ "startRound" |
|------------------------------------|
"close" "shuffle" "accept"
open ------- > establish -------> shuffle ---------> accepted
^ | |
|---------------| |
^ "startRound" |
|-----------------------------------------------------|
@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
ledger is detected and recovered during the establish or accept phase,
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
establish,
//! Negotitate featureRNG entropy
shuffle,
//! 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 consensus phase occur while in this phase.
@@ -122,6 +125,8 @@ to_string(ConsensusPhase p)
return "open";
case ConsensusPhase::establish:
return "establish";
case ConsensusPhase::shuffle:
return "shuffle";
case ConsensusPhase::accepted:
return "accepted";
default:

View File

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

View File

@@ -1918,6 +1918,100 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMLedgerData> const& 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
PeerImp::onMessage(std::shared_ptr<protocol::TMProposeSet> const& m)
{

View File

@@ -450,3 +450,12 @@ message TMHaveTransactions
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
}

View File

@@ -74,7 +74,7 @@ namespace detail {
// Feature.cpp. Because it's only used to reserve storage, and determine how
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
// the actual number of amendments. A LogicError on startup will verify this.
static constexpr std::size_t numFeatures = 91;
static constexpr std::size_t numFeatures = 90;
/** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated
@@ -378,7 +378,6 @@ extern uint256 const fixInvalidTxFlags;
extern uint256 const featureExtendedHookState;
extern uint256 const fixCronStacking;
extern uint256 const fixHookAPI20251128;
extern uint256 const featureHookAdministrator;
} // namespace ripple
#endif

View File

@@ -563,7 +563,6 @@ extern SF_ACCOUNT const sfEmitCallback;
extern SF_ACCOUNT const sfHookAccount;
extern SF_ACCOUNT const sfNFTokenMinter;
extern SF_ACCOUNT const sfInform;
extern SF_ACCOUNT const sfHookAdministrator;
// path set
extern SField const sfPaths;

View File

@@ -184,11 +184,6 @@ constexpr std::uint32_t const tfNFTokenCancelOfferMask = ~(tfUniversal);
// NFTokenAcceptOffer flags:
constexpr std::uint32_t const tfNFTokenAcceptOfferMask = ~tfUniversal;
enum SetHookFlags : uint32_t {
tfNewAccount = 0x00000001,
};
constexpr std::uint32_t const tfSetHookMask = ~(tfUniversal | tfNewAccount);
// URIToken mask
constexpr std::uint32_t const tfURITokenMintMask = ~(tfUniversal | tfBurnable);
constexpr std::uint32_t const tfURITokenNonMintMask = ~tfUniversal;

View File

@@ -484,7 +484,6 @@ REGISTER_FIX (fixInvalidTxFlags, Supported::yes, VoteBehavior::De
REGISTER_FEATURE(ExtendedHookState, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FIX (fixCronStacking, Supported::yes, VoteBehavior::DefaultYes);
REGISTER_FIX (fixHookAPI20251128, Supported::yes, VoteBehavior::DefaultYes);
REGISTER_FEATURE(HookAdministrator, Supported::yes, VoteBehavior::DefaultNo);
// The following amendments are obsolete, but must remain supported
// because they could potentially get enabled.

View File

@@ -70,7 +70,6 @@ LedgerFormats::LedgerFormats()
{sfTouchCount, soeOPTIONAL},
{sfHookStateScale, soeOPTIONAL},
{sfCron, soeOPTIONAL},
{sfHookAdministrator, soeOPTIONAL},
},
commonFields);

View File

@@ -315,7 +315,6 @@ CONSTRUCT_TYPED_SFIELD(sfEmitCallback, "EmitCallback", ACCOUNT,
// account (uncommon)
CONSTRUCT_TYPED_SFIELD(sfHookAccount, "HookAccount", ACCOUNT, 16);
CONSTRUCT_TYPED_SFIELD(sfHookAdministrator, "HookAdministrator", ACCOUNT, 98);
CONSTRUCT_TYPED_SFIELD(sfInform, "Inform", ACCOUNT, 99);
// vector of 256-bit

View File

@@ -324,7 +324,6 @@ TxFormats::TxFormats()
{
{sfHooks, soeREQUIRED},
{sfTicketSequence, soeOPTIONAL},
{sfDestination, soeOPTIONAL},
},
commonFields);