Compare commits

...
3 Commits
Author SHA1 Message Date
Richard Holland 2b0c651761 history tree 2026-10-20 17:22:40 +11:00
Richard Holland c6173a74ff init history tree 2026-10-20 17:10:35 +11:00
Richard Holland 3dc41b2a59 ignore amendment cfg option (#900)
Use like so, on testnet:
[ignore_amendments]
HookOnV2
2026-10-17 09:19:39 +10:00
32 changed files with 873 additions and 17 deletions

No files matched your search

+8
View File
@@ -345,6 +345,14 @@ dice(uint32_t sides);
extern int64_t
util_random(uint32_t write_ptr, uint32_t write_len);
extern int64_t
history_verify(
uint32_t write_ptr,
uint32_t write_len,
uint32_t read_ptr,
uint32_t read_len,
uint64_t leaf_index);
#ifdef __cplusplus
}
#endif
+5
View File
@@ -112,6 +112,8 @@
#define sfAccountIndex ((3U << 16U) + 98U)
#define sfAccountCount ((3U << 16U) + 99U)
#define sfRewardAccumulator ((3U << 16U) + 100U)
#define sfHistoryLeafIndex ((3U << 16U) + 101U)
#define sfHistoryLeafCount ((3U << 16U) + 102U)
#define sfEmailHash ((4U << 16U) + 1U)
#define sfTakerPaysCurrency ((17U << 16U) + 1U)
#define sfTakerPaysIssuer ((17U << 16U) + 2U)
@@ -165,6 +167,7 @@
#define sfEmittedTxnID ((5U << 16U) + 97U)
#define sfGovernanceMarks ((5U << 16U) + 98U)
#define sfGovernanceFlags ((5U << 16U) + 99U)
#define sfHistoryProofDigest ((5U << 16U) + 100U)
#define sfNumber ((9U << 16U) + 1U)
#define sfAmount ((6U << 16U) + 1U)
#define sfBalance ((6U << 16U) + 2U)
@@ -231,6 +234,7 @@
#define sfHookName ((7U << 16U) + 97U)
#define sfRemarkValue ((7U << 16U) + 98U)
#define sfRemarkName ((7U << 16U) + 99U)
#define sfHistoryProof ((7U << 16U) + 100U)
#define sfAccount ((8U << 16U) + 1U)
#define sfOwner ((8U << 16U) + 2U)
#define sfDestination ((8U << 16U) + 3U)
@@ -257,6 +261,7 @@
#define sfHookNamespaces ((19U << 16U) + 5U)
#define sfCredentialIDs ((19U << 16U) + 6U)
#define sfURITokenIDs ((19U << 16U) + 99U)
#define sfHistoryPeaks ((19U << 16U) + 100U)
#define sfPaths ((18U << 16U) + 1U)
#define sfBaseAsset ((26U << 16U) + 1U)
#define sfQuoteAsset ((26U << 16U) + 2U)
+1
View File
@@ -62,6 +62,7 @@
#define ttPERMISSIONED_DOMAIN_SET 71
#define ttPERMISSIONED_DOMAIN_DELETE 72
#define ttENTROPY 89
#define ttHISTORY_PROOF 90
#define ttMANIFEST_SET 91
#define ttCRON 92
#define ttCRON_SET 93
+1
View File
@@ -16,6 +16,7 @@
#define featureHooksUpdate1 "1"
#define featureHooksUpdate2 "1"
#define featureRNG "1"
#define featureHistoryTree "1"
#define fix20250131 "1"
#define fixGuardDepth32 "1"
namespace hook_api {
+4
View File
@@ -306,4 +306,8 @@ HOOK_API_DEFINITION(
HOOK_API_DEFINITION(
int64_t, util_random, (uint32_t write_ptr, uint32_t write_len),
featureRNG)
HOOK_API_DEFINITION(
int64_t, history_verify, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len, uint64_t leaf_index),
featureHistoryTree)
// clang-format on
+6
View File
@@ -96,6 +96,12 @@ enum class HashPrefix : std::uint32_t {
/** Credentials signature */
credential = detail::make_hash_prefix('C', 'R', 'D'),
/** featureHistoryTree: history tree leaf */
historyLeaf = detail::make_hash_prefix('H', 'L', 'F'),
/** featureHistoryTree: history tree interior node */
historyNode = detail::make_hash_prefix('H', 'N', 'D'),
};
template <class Hasher>
+120
View File
@@ -0,0 +1,120 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2026 XRPL-Labs
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.
*/
//==============================================================================
#ifndef RIPPLE_PROTOCOL_HISTORYTREE_H_INCLUDED
#define RIPPLE_PROTOCOL_HISTORYTREE_H_INCLUDED
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <cstdint>
#include <optional>
#include <vector>
namespace ripple {
namespace history {
/** featureHistoryTree: an append-only Merkle Mountain Range over every
transaction ever applied, in ledger order then TransactionIndex order.
Validators keep only the peaks (one hash per set bit of the leaf count)
in the ltHISTORY_TREE singleton; nobody needs a leaf to append or verify.
Leaf i for a transaction applied at TransactionIndex t of ledger s is
sha512Half(HashPrefix::historyLeaf, u32 s, u32 t, VL tx, VL meta)
where tx is the stored (storable, see STTx::addStorable) serialization and
meta is the stored metadata. Interior nodes are
sha512Half(HashPrefix::historyNode, left, right).
Peaks are kept tallest-first, i.e. in the order of the set bits of the
leaf count from most to least significant, so that peak p covers the
p-th set bit.
*/
/// Hash of one leaf.
uint256
leafHash(std::uint32_t ledgerSeq, std::uint32_t txIndex, Slice tx, Slice meta);
/// Hash of one interior node.
uint256
nodeHash(uint256 const& left, uint256 const& right);
/// Append a leaf, merging peaks as the binary carry of count demands.
void
append(std::vector<uint256>& peaks, std::uint64_t& count, uint256 const& leaf);
/// True if peaks could be the peaks of a tree with count leaves.
bool
peaksValid(std::vector<uint256> const& peaks, std::uint64_t count);
/** Which mountain holds a leaf.
peak: index into the peaks vector; height: path length to that peak;
offset: the leaf's position within the mountain (its path bits).
*/
struct Position
{
std::size_t peak;
unsigned height;
std::uint64_t offset;
};
std::optional<Position>
locate(std::uint64_t count, std::uint64_t leafIndex);
/** The proof a host submits in sfHistoryProof.
Wire format (big endian, VL = XRPL variable-length prefix):
u8 version (currently 1)
u32 ledger sequence
u32 transaction index
VL transaction (storable serialization)
VL metadata
u8 path length
32 * path length bytes of sibling hashes, leaf-side first
*/
constexpr std::uint8_t proofVersion = 1;
constexpr std::size_t maxProofPath = 64;
struct Proof
{
std::uint32_t ledgerSeq;
std::uint32_t txIndex;
Slice tx;
Slice meta;
std::vector<uint256> path;
};
std::optional<Proof>
parseProof(Slice blob);
/** Verify that proof places its leaf at leafIndex under the current peaks.
On success the leaf's hash is written to leaf (if non-null).
*/
bool
verifyProof(
std::vector<uint256> const& peaks,
std::uint64_t count,
std::uint64_t leafIndex,
Proof const& proof,
uint256* leaf = nullptr);
} // namespace history
} // namespace ripple
#endif
+4
View File
@@ -122,6 +122,10 @@ UNLReport() noexcept;
Keylet const&
random() noexcept;
/** The (fixed) index of the featureHistoryTree history tree object. */
Keylet const&
historyTree() noexcept;
/** The beginning of an order book */
struct book_t
{
+29
View File
@@ -48,6 +48,9 @@ class STTx final : public STObject, public CountedObject<STTx>
uint256 tid_;
TxType tx_type_;
uint256
computeTransactionID() const;
public:
static std::size_t const minMultiSigners = 1;
@@ -107,6 +110,32 @@ public:
uint256
getTransactionID() const;
/** featureHistoryTree: transient fields.
A transient field travels with the transaction on the wire and is
visible to transactors and Hooks while it applies, but is not signed,
is not part of the transaction ID, and is clipped from the form stored
in the ledger (see addStorable). Its content is bound to the ID by a
digest field that is signed. Only sfHistoryProof is transient.
*/
static bool
isTransientField(SField const& f);
/// True if this transaction carries any transient field.
bool
hasTransientFields() const;
/// Serialize the storable form: everything except transient fields.
void
addStorable(Serializer& s) const;
/** Throw if a transient field is present and its digest does not match.
Called by the constructors so a tampered relay copy never reaches the
HashRouter under the real transaction's ID.
*/
void
checkTransientFields() const;
Json::Value
getJson(JsonOptions options) const override;
@@ -34,6 +34,7 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FEATURE(HistoryTree, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(RNG, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(PWALoader, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (20261005, Supported::yes, VoteBehavior::DefaultYes)
@@ -216,6 +216,20 @@ LEDGER_ENTRY(ltRANDOM, 0x526E, Random, random, ({
{sfPreviousTxnLgrSeq, soeREQUIRED},
}))
/** featureHistoryTree: the history Merkle Mountain Range.
A singleton. Validators append one leaf per applied transaction at ledger
close (Ledger::updateHistoryTree) and keep only the peaks. Hooks may load
it; the genesis Hook drives the retention game from sfHistoryLeafCount.
\sa keylet::historyTree
*/
LEDGER_ENTRY(ltHISTORY_TREE, 0x4874, HistoryTree, history_tree, ({
{sfLedgerSequence, soeREQUIRED},
{sfHistoryLeafCount, soeREQUIRED},
{sfHistoryPeaks, soeOPTIONAL},
}))
/** A ledger object which contains a signer list for an account.
\sa keylet::signers
@@ -160,6 +160,9 @@ TYPED_SFIELD(sfTouchCount, UINT64, 97)
TYPED_SFIELD(sfAccountIndex, UINT64, 98)
TYPED_SFIELD(sfAccountCount, UINT64, 99)
TYPED_SFIELD(sfRewardAccumulator, UINT64, 100)
// featureHistoryTree
TYPED_SFIELD(sfHistoryLeafIndex, UINT64, 101)
TYPED_SFIELD(sfHistoryLeafCount, UINT64, 102)
// 128-bit
TYPED_SFIELD(sfEmailHash, UINT128, 1)
@@ -224,6 +227,8 @@ TYPED_SFIELD(sfHookCanEmit, UINT256, 96)
TYPED_SFIELD(sfEmittedTxnID, UINT256, 97)
TYPED_SFIELD(sfGovernanceMarks, UINT256, 98)
TYPED_SFIELD(sfGovernanceFlags, UINT256, 99)
// featureHistoryTree: sha512Half of sfHistoryProof, signed and part of the txid
TYPED_SFIELD(sfHistoryProofDigest, UINT256, 100)
// number (common)
TYPED_SFIELD(sfNumber, NUMBER, 1)
@@ -304,6 +309,9 @@ TYPED_SFIELD(sfAppLoader, VL, 96)
TYPED_SFIELD(sfHookName, VL, 97)
TYPED_SFIELD(sfRemarkValue, VL, 98)
TYPED_SFIELD(sfRemarkName, VL, 99)
// featureHistoryTree: transient. Not signed, not part of the txid, clipped from
// the stored transaction (see STTx::isTransientField).
TYPED_SFIELD(sfHistoryProof, VL, 100, SField::sMD_Default, SField::notSigning)
// account (common)
TYPED_SFIELD(sfAccount, ACCOUNT, 1)
@@ -337,6 +345,7 @@ TYPED_SFIELD(sfNFTokenOffers, VECTOR256, 4)
TYPED_SFIELD(sfHookNamespaces, VECTOR256, 5)
TYPED_SFIELD(sfCredentialIDs, VECTOR256, 6)
TYPED_SFIELD(sfURITokenIDs, VECTOR256, 99)
TYPED_SFIELD(sfHistoryPeaks, VECTOR256, 100)
// path set
UNTYPED_SFIELD(sfPaths, PATHSET, 1)
@@ -517,6 +517,22 @@ TRANSACTION(ttENTROPY, 89, Entropy, ({
{sfRandomData, soeOPTIONAL},
}))
/** featureHistoryTree: a host proves it holds one leaf of the history tree.
sfHistoryProof carries the leaf (tx + meta) and its sibling path and is
verified against the ltHISTORY_TREE peaks in preclaim. It is transient:
excluded from the signature and the txid (sfHistoryProofDigest stands in
for it) and clipped from the stored transaction, so history does not grow
with proofs of history. Hooks on sfDestination (strong TSH) see the full
transaction and may call history_verify on it.
*/
TRANSACTION(ttHISTORY_PROOF, 90, HistoryProof, ({
{sfHistoryLeafIndex, soeREQUIRED},
{sfHistoryProofDigest, soeREQUIRED},
{sfHistoryProof, soeOPTIONAL},
{sfDestination, soeOPTIONAL},
}))
/* sfAccount is supplied by TxFormats::commonFields; listing it here as well
makes the SOTemplate reject the format at construction. */
TRANSACTION(ttMANIFEST_SET, 91, SetManifest, ({
+10 -1
View File
@@ -94,7 +94,8 @@ enum class LedgerNameSpace : std::uint16_t {
MANIFEST_DIR = LEDGER_NAMESPACE2(0x01, 'M'),
CREDENTIAL = LEDGER_NAMESPACE2(0x01, 'D'),
PERMISSIONED_DOMAIN = 'm',
RANDOM = 0x526E, // Rn
RANDOM = 0x526E, // Rn
HISTORY_TREE = 0x4874, // Ht
// No longer used or supported. Left here to reserve the space
// to avoid accidental reuse.
@@ -286,6 +287,14 @@ random() noexcept
return ret;
}
Keylet const&
historyTree() noexcept
{
static Keylet const ret{
ltHISTORY_TREE, indexHash(LedgerNameSpace::HISTORY_TREE)};
return ret;
}
Keylet
book_t::operator()(Book const& b) const
{
+70 -4
View File
@@ -33,6 +33,7 @@
#include <xrpl/protocol/Sign.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/digest.h>
#include <xrpl/protocol/jss.h>
#include <boost/format.hpp>
@@ -62,7 +63,8 @@ STTx::STTx(STObject&& object) : STObject(std::move(object))
{
tx_type_ = safe_cast<TxType>(getFieldU16(sfTransactionType));
applyTemplate(getTxFormat(tx_type_)->getSOTemplate()); // may throw
tid_ = getHash(HashPrefix::transactionID);
checkTransientFields();
tid_ = computeTransactionID();
}
STTx::STTx(SerialIter& sit) : STObject(sfTransaction)
@@ -78,7 +80,8 @@ STTx::STTx(SerialIter& sit) : STObject(sfTransaction)
tx_type_ = safe_cast<TxType>(getFieldU16(sfTransactionType));
applyTemplate(getTxFormat(tx_type_)->getSOTemplate()); // May throw
tid_ = getHash(HashPrefix::transactionID);
checkTransientFields();
tid_ = computeTransactionID();
}
STTx::STTx(TxType type, std::function<void(STObject&)> assembler)
@@ -96,7 +99,70 @@ STTx::STTx(TxType type, std::function<void(STObject&)> assembler)
if (tx_type_ != type)
LogicError("Transaction type was mutated during assembly");
tid_ = getHash(HashPrefix::transactionID);
checkTransientFields();
tid_ = computeTransactionID();
}
namespace {
uint256
computeStorableTxID(STTx const& tx)
{
if (!tx.hasTransientFields())
return tx.getHash(HashPrefix::transactionID);
// The ID is the hash of the storable form, so the clipped transaction
// stored in the ledger hashes to the same ID it was relayed and applied
// under. The digest field inside that form commits to the clipped data.
Serializer s;
s.add32(HashPrefix::transactionID);
tx.addStorable(s);
return s.getSHA512Half();
}
} // namespace
bool
STTx::isTransientField(SField const& f)
{
return f == sfHistoryProof;
}
bool
STTx::hasTransientFields() const
{
return isFieldPresent(sfHistoryProof);
}
void
STTx::addStorable(Serializer& s) const
{
if (!hasTransientFields())
{
add(s);
return;
}
STObject clipped(*this);
clipped.makeFieldAbsent(sfHistoryProof);
clipped.add(s);
}
void
STTx::checkTransientFields() const
{
if (!isFieldPresent(sfHistoryProof))
return;
if (!isFieldPresent(sfHistoryProofDigest) ||
sha512Half(makeSlice(getFieldVL(sfHistoryProof))) !=
getFieldH256(sfHistoryProofDigest))
Throw<std::runtime_error>(
"Transaction transient field digest mismatch");
}
uint256
STTx::computeTransactionID() const
{
return computeStorableTxID(*this);
}
STBase*
@@ -206,7 +272,7 @@ STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey)
auto const sig = ripple::sign(publicKey, secretKey, makeSlice(data));
setFieldVL(sfTxnSignature, sig);
tid_ = getHash(HashPrefix::transactionID);
tid_ = computeTransactionID();
}
Expected<void, std::string>
+74
View File
@@ -1271,6 +1271,79 @@ public:
BEAST_EXPECT(table->needValidatedLedger(257));
}
void
testIgnoredUnsupported()
{
testcase("ignore_amendments");
using namespace std::chrono_literals;
weeks constexpr w(1);
test::jtx::Env env{*this, makeConfig()};
static std::vector<AmendmentTable::FeatureInfo> const supported =
makeDefaultYes(yes_);
// Ignore every unsupported_ amendment except the first one.
BEAST_EXPECT(unsupported_.size() > 1);
std::vector<std::string> const ignoredNames(
unsupported_.begin() + 1, unsupported_.end());
auto table = make_AmendmentTable(
env.app(),
w,
supported,
emptySection_,
emptySection_,
journal_,
makeSection(ignoredNames));
for (auto const& a : ignoredNames)
BEAST_EXPECT(table->isSupported(amendmentId(a)));
BEAST_EXPECT(!table->isSupported(amendmentId(unsupported_[0])));
majorityAmendments_t const majority;
// Enabling only ignored amendments must not block.
std::set<uint256> enabled;
for (auto const& a : ignoredNames)
enabled.insert(amendmentId(a));
table->doValidatedLedger(1, enabled, majority);
BEAST_EXPECT(!table->hasUnsupportedEnabled());
BEAST_EXPECT(table->unsupportedAmendments().empty());
// An ignored amendment must never be voted for.
auto const desired = table->getDesired();
for (auto const& a : ignoredNames)
BEAST_EXPECT(
std::find(desired.begin(), desired.end(), amendmentId(a)) ==
desired.end());
// A non-ignored unsupported amendment still blocks.
enabled.insert(amendmentId(unsupported_[0]));
table->doValidatedLedger(2, enabled, majority);
BEAST_EXPECT(table->hasUnsupportedEnabled());
// Bad entries are rejected.
Section bad("ignore_amendments");
bad.append("NotARealFeatureName");
try
{
make_AmendmentTable(
env.app(),
w,
supported,
emptySection_,
emptySection_,
journal_,
bad);
fail("expected bad [ignore_amendments] entry to throw");
}
catch (std::runtime_error const&)
{
pass();
}
}
void
testFeature(FeatureBitset const& feat)
{
@@ -1294,6 +1367,7 @@ public:
testBadConfig();
testEnableVeto();
testHasUnsupported();
testIgnoredUnsupported();
testFeature(all - fixMajorityCalc);
testFeature(all);
}
+8
View File
@@ -501,6 +501,14 @@ public:
Expected<Bytes, HookReturnCode>
util_random(uint32_t len) const;
/// featureHistoryTree (genesis Hook only until per-API billing exists)
// Verify a history tree proof against the current ltHISTORY_TREE peaks
// and return the leaf hash. An empty blob means "the originating
// transaction's own sfHistoryProof and sfHistoryLeafIndex".
Expected<uint256, HookReturnCode>
history_verify(Slice blob, uint64_t leafIndex) const;
Expected<Keylet, HookReturnCode>
ledger_keylet(Keylet const& klLo, Keylet const& klHi) const;
+41
View File
@@ -3,9 +3,13 @@
#include <xrpld/app/hook/HookAPI.h>
#include <xrpld/app/ledger/OpenLedger.h>
#include <xrpld/app/ledger/TransactionMaster.h>
#include <xrpld/app/tx/detail/HistoryProof.h>
#include <xrpld/app/tx/detail/Import.h>
#include <xrpld/ledger/ApplyViewImpl.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <algorithm>
#include <limits>
@@ -1991,6 +1995,43 @@ HookAPI::rng_next() const
return out;
}
// featureHistoryTree
Expected<uint256, HookReturnCode>
HookAPI::history_verify(Slice blob, uint64_t leafIndex) const
{
auto& applyCtx = hookCtx.applyCtx;
auto const& view = applyCtx.view();
if (!view.rules().enabled(featureHistoryTree))
return Unexpected(NOT_IMPLEMENTED);
// Until per-API billing exists only the genesis account's Hooks may pay
// for a native verification.
static auto const genesisAccountId = calcAccountID(
generateKeyPair(KeyType::secp256k1, generateSeed("masterpassphrase"))
.first);
if (hookCtx.result.account != genesisAccountId)
return Unexpected(NOT_AUTHORIZED);
Blob own;
if (blob.empty())
{
auto const& tx = applyCtx.tx;
if (tx.getTxnType() != ttHISTORY_PROOF ||
!tx.isFieldPresent(sfHistoryProof))
return Unexpected(INVALID_TXN);
own = tx.getFieldVL(sfHistoryProof);
blob = makeSlice(own);
leafIndex = tx.getFieldU64(sfHistoryLeafIndex);
}
auto const leaf = HistoryProof::verify(view, leafIndex, blob);
if (!leaf)
return Unexpected(DOES_NOT_MATCH);
return *leaf;
}
Expected<uint64_t, HookReturnCode>
HookAPI::dice(uint32_t sides) const
{
+35 -1
View File
@@ -340,7 +340,8 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
case ttCHECK_CREATE:
case ttACCOUNT_DELETE:
case ttPAYCHAN_CREATE:
case ttINVOKE: {
case ttINVOKE:
case ttHISTORY_PROOF: {
if (destAcc)
ADD_TSH(*destAcc, tshSTRONG);
break;
@@ -2926,6 +2927,39 @@ DEFINE_HOOK_FUNCTION(
HOOK_TEARDOWN();
}
// featureHistoryTree: verify a proof (read_len == 0: the otxn's own proof)
// and write the 32 byte leaf hash
DEFINE_HOOK_FUNCTION(
int64_t,
history_verify,
uint32_t write_ptr,
uint32_t write_len,
uint32_t read_ptr,
uint32_t read_len,
uint64_t leaf_index)
{
HOOK_SETUP();
if (write_len < 32)
return TOO_SMALL;
if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length) ||
NOT_IN_BOUNDS(read_ptr, read_len, memory_length))
return OUT_OF_BOUNDS;
auto const result = api.history_verify(
ripple::Slice{memory + read_ptr, read_len}, leaf_index);
if (!result)
return result.error();
auto const& leaf = result.value();
WRITE_WASM_MEMORY_AND_RETURN(
write_ptr, 32, leaf.data(), 32, memory, memory_length);
HOOK_TEARDOWN();
}
DEFINE_HOOK_FUNCTION(
int64_t,
ledger_keylet,
+67
View File
@@ -43,6 +43,7 @@
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/HistoryTree.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SecretKey.h>
@@ -50,6 +51,7 @@
#include <xrpl/protocol/digest.h>
#include <xrpl/protocol/jss.h>
#include <boost/optional.hpp>
#include <algorithm>
#include <utility>
#include <vector>
@@ -992,6 +994,71 @@ Ledger::updateSkipList()
rawReplace(sle);
}
// featureHistoryTree
void
Ledger::updateHistoryTree()
{
if (!rules().enabled(featureHistoryTree))
return;
auto const k = keylet::historyTree();
auto sle = peek(k);
bool const created = !sle;
if (created)
{
sle = std::make_shared<SLE>(k);
sle->setFieldU64(sfHistoryLeafCount, 0);
}
// Leaves are appended in TransactionIndex order, which is the order the
// transactions were applied, so every node computes the same tree.
std::vector<std::pair<std::uint32_t, uint256>> leaves;
for (auto const& item : txMap_)
{
SerialIter sit(item.slice());
Slice const tx = sit.getSlice(sit.getVLDataLength());
Slice const meta = sit.getSlice(sit.getVLDataLength());
std::uint32_t index = 0;
{
SerialIter ms(meta);
STObject const m(ms, sfMetadata);
index = m.getFieldU32(sfTransactionIndex);
}
leaves.emplace_back(
index, history::leafHash(info_.seq, index, tx, meta));
}
if (leaves.empty() && !created)
return;
std::sort(leaves.begin(), leaves.end(), [](auto const& a, auto const& b) {
return a.first < b.first;
});
std::vector<uint256> peaks;
if (sle->isFieldPresent(sfHistoryPeaks))
peaks = static_cast<std::vector<uint256>>(
sle->getFieldV256(sfHistoryPeaks));
std::uint64_t count = sle->getFieldU64(sfHistoryLeafCount);
for (auto const& [index, leaf] : leaves)
history::append(peaks, count, leaf);
sle->setFieldU64(sfHistoryLeafCount, count);
sle->setFieldU32(sfLedgerSequence, info_.seq);
if (peaks.empty())
sle->makeFieldAbsent(sfHistoryPeaks);
else
sle->setFieldV256(sfHistoryPeaks, STVector256(peaks));
if (created)
rawInsert(sle);
else
rawReplace(sle);
}
bool
Ledger::isFlagLedger() const
{
+7
View File
@@ -352,6 +352,13 @@ public:
void
updateSkipList();
/** featureHistoryTree: append this ledger's transactions, in
TransactionIndex order, to the history MMR kept in ltHISTORY_TREE.
Called once at close after all transactions are applied.
*/
void
updateHistoryTree();
bool
walkLedger(beast::Journal j, bool parallel = false) const;
+8 -1
View File
@@ -64,6 +64,7 @@ buildLedgerImpl(
}
built->updateSkipList();
built->updateHistoryTree();
{
// Write the final version of all modified SHAMap
// nodes to the node store to preserve the new LCL
@@ -271,7 +272,13 @@ buildLedger(
j,
[&](OpenView& accum, std::shared_ptr<Ledger> const& built) {
for (auto& tx : replayData.orderedTxns())
applyTransaction(app, accum, *tx.second, false, applyFlags, j);
applyTransaction(
app,
accum,
*tx.second,
false,
applyFlags | tapHISTORY_REPLAY,
j);
});
}
+17 -1
View File
@@ -1260,13 +1260,29 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline)
Section const& upVoted = config_->section(SECTION_AMENDMENTS);
Section const& ignored = config_->section(SECTION_IGNORE_AMENDMENTS);
// [ignore_amendments] exists to keep long-lived test networks syncable
// after an amendment they activated is removed from the codebase. It
// must never be usable on a production network.
if (!ignored.lines().empty() &&
(config_->NETWORK_ID == 0 || config_->NETWORK_ID == 21337))
{
JLOG(m_journal.fatal()) << "[" SECTION_IGNORE_AMENDMENTS
"] is not permitted on "
"network_id "
<< config_->NETWORK_ID;
return false;
}
m_amendmentTable = make_AmendmentTable(
*this,
config().AMENDMENT_MAJORITY_TIME,
supported,
upVoted,
downVoted,
logs_->journal("Amendments"));
logs_->journal("Amendments"),
ignored);
}
Pathfinder::initPathTable();
+2 -1
View File
@@ -211,7 +211,8 @@ make_AmendmentTable(
std::vector<AmendmentTable::FeatureInfo> const& supported,
Section const& enabled,
Section const& vetoed,
beast::Journal journal);
beast::Journal journal,
Section const& ignored = Section{});
} // namespace ripple
+78 -6
View File
@@ -302,6 +302,15 @@ struct AmendmentState
/** Indicates an amendment that this server has code support for. */
bool supported = false;
/** Indicates an amendment listed in [ignore_amendments]. The server has
no code for it, but must not become amendment blocked if it is (or
becomes) enabled on the ledger. Such an amendment is reported as
`supported` internally so every blocking check (LedgerMaster,
NetworkOPs, Change) passes, but is never voted for and is shown as
unsupported + ignored over RPC.
*/
bool ignored = false;
/** The name of this amendment, possibly empty. */
std::string name;
@@ -476,7 +485,8 @@ public:
std::vector<FeatureInfo> const& supported,
Section const& enabled,
Section const& vetoed,
beast::Journal journal);
beast::Journal journal,
Section const& ignored);
uint256
find(std::string const& name) const override;
@@ -543,7 +553,8 @@ AmendmentTableImpl::AmendmentTableImpl(
std::vector<FeatureInfo> const& supported,
Section const& enabled,
Section const& vetoed,
beast::Journal journal)
beast::Journal journal,
Section const& ignored)
: lastUpdateSeq_(0)
, majorityTime_(majorityTime)
, unsupportedEnabled_(false)
@@ -586,6 +597,59 @@ AmendmentTableImpl::AmendmentTableImpl(
<< " voted by default if not enabled on the ledger.";
}
// Parse ignored amendments from config. Each line is either a registered
// feature name (e.g. "HookOnV2") or "<64 hex amendment ID> [name]". These
// are deliberately NOT persisted to wallet.db: removing the line from the
// config must restore normal amendment-blocking behaviour.
for (auto const& line : ignored.lines())
{
std::string const trimmed = boost::algorithm::trim_copy(line);
if (trimmed.empty())
continue;
std::vector<std::string> parts;
boost::algorithm::split(
parts,
trimmed,
boost::algorithm::is_space(),
boost::algorithm::token_compress_on);
uint256 id;
std::string name;
if (id.parseHex(parts[0]))
name = parts.size() > 1 ? parts[1] : std::string{};
else if (auto const f = getRegisteredFeature(parts[0]))
{
id = *f;
name = parts[0];
}
else
Throw<std::runtime_error>(
"Invalid entry '" + line + "' in [" + ignored.name() +
"]: expected a registered feature name or amendment ID");
AmendmentState& s = add(id, lock);
if (s.supported)
{
JLOG(j_.warn()) << "[" << ignored.name() << "] lists amendment "
<< id << " (" << s.name
<< ") which this server supports; entry has no "
"effect.";
continue;
}
if (!name.empty())
s.name = name;
s.supported = true;
s.ignored = true;
s.vote = AmendmentVote::obsolete;
JLOG(j_.warn()) << "Amendment " << id << " (" << s.name
<< ") is unsupported but listed in [" << ignored.name()
<< "]: this server will NOT become amendment blocked "
"if it is enabled on the ledger.";
}
hash_set<uint256> detect_conflict;
// Parse enabled amendments from config
for (std::pair<uint256, std::string> const& a : parseSection(enabled))
@@ -775,7 +839,12 @@ AmendmentTableImpl::enable(uint256 const& amendment)
s.enabled = true;
if (!s.supported)
if (s.ignored)
{
JLOG(j_.warn()) << "Ignored unsupported amendment " << amendment << " ("
<< s.name << ") is enabled on the ledger.";
}
else if (!s.supported)
{
JLOG(j_.error()) << "Unsupported amendment " << amendment
<< " activated.";
@@ -1044,7 +1113,9 @@ AmendmentTableImpl::injectJson(
if (!fs.name.empty())
v[jss::name] = fs.name;
v[jss::supported] = fs.supported;
v[jss::supported] = fs.supported && !fs.ignored;
if (fs.ignored)
v["ignored"] = true;
if (!fs.enabled && isAdmin)
{
if (fs.vote == AmendmentVote::obsolete)
@@ -1113,10 +1184,11 @@ make_AmendmentTable(
std::vector<AmendmentTable::FeatureInfo> const& supported,
Section const& enabled,
Section const& vetoed,
beast::Journal journal)
beast::Journal journal,
Section const& ignored)
{
return std::make_unique<AmendmentTableImpl>(
app, majorityTime, supported, enabled, vetoed, journal);
app, majorityTime, supported, enabled, vetoed, journal, ignored);
}
} // namespace ripple
+159
View File
@@ -0,0 +1,159 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2026 XRPL-Labs
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/tx/detail/HistoryProof.h>
#include <xrpl/basics/Log.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/digest.h>
#include <xrpl/protocol/st.h>
namespace ripple {
std::optional<uint256>
HistoryProof::verify(ReadView const& view, std::uint64_t leafIndex, Slice blob)
{
auto const sle = view.read(keylet::historyTree());
if (!sle)
return std::nullopt;
auto const proof = history::parseProof(blob);
if (!proof)
return std::nullopt;
std::vector<uint256> peaks;
if (sle->isFieldPresent(sfHistoryPeaks))
peaks = static_cast<std::vector<uint256>>(
sle->getFieldV256(sfHistoryPeaks));
uint256 leaf;
if (!history::verifyProof(
peaks,
sle->getFieldU64(sfHistoryLeafCount),
leafIndex,
*proof,
&leaf))
return std::nullopt;
return leaf;
}
NotTEC
HistoryProof::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureHistoryTree))
return temDISABLED;
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
return ret;
auto const& tx = ctx.tx;
if (tx.getFlags() & tfUniversalMask)
return temINVALID_FLAG;
bool const replay = (ctx.flags & tapHISTORY_REPLAY) != 0;
if (!tx.isFieldPresent(sfHistoryProof))
{
// Only the stored, clipped form lacks the proof, and only a replay of
// an already validated ledger may apply that form.
if (!replay)
{
JLOG(ctx.j.warn()) << "HistoryProof: missing proof.";
return temMALFORMED;
}
}
else
{
auto const blob = tx.getFieldVL(sfHistoryProof);
// STTx's constructors already enforce this; keep it as a belt for
// transactions assembled in-process.
if (sha512Half(makeSlice(blob)) !=
tx.getFieldH256(sfHistoryProofDigest))
return temMALFORMED;
if (!history::parseProof(makeSlice(blob)))
{
JLOG(ctx.j.warn()) << "HistoryProof: unparseable proof.";
return temMALFORMED;
}
}
return preflight2(ctx);
}
TER
HistoryProof::preclaim(PreclaimContext const& ctx)
{
auto const& tx = ctx.tx;
if (tx.isFieldPresent(sfDestination))
{
auto const sleDest = ctx.view.read(keylet::account(tx[sfDestination]));
if (!sleDest)
return tecNO_TARGET;
}
if (!tx.isFieldPresent(sfHistoryProof))
{
// Replay of a validated ledger (preflight admitted this form only
// under tapHISTORY_REPLAY): the proof was checked when the ledger
// was first built, trust it.
return tesSUCCESS;
}
if (!ctx.view.read(keylet::historyTree()))
return tecINTERNAL; // feature enabled, object missing
if (!verify(
ctx.view,
tx.getFieldU64(sfHistoryLeafIndex),
makeSlice(tx.getFieldVL(sfHistoryProof))))
{
JLOG(ctx.j.debug()) << "HistoryProof: proof does not verify.";
return tecNO_PERMISSION;
}
return tesSUCCESS;
}
TER
HistoryProof::doApply()
{
// The proof was verified in preclaim; everything else is the Hook's.
return tesSUCCESS;
}
XRPAmount
HistoryProof::calculateBaseFee(ReadView const& view, STTx const& tx)
{
// The proof is not stored, so it costs relay bandwidth and a verification
// rather than ledger space: one drop per 32 bytes.
XRPAmount extra{0};
if (tx.isFieldPresent(sfHistoryProof))
extra = XRPAmount{static_cast<std::int64_t>(
tx.getFieldVL(sfHistoryProof).size() / 32)};
return Transactor::calculateBaseFee(view, tx) + extra;
}
} // namespace ripple
+66
View File
@@ -0,0 +1,66 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2026 XRPL-Labs
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.
*/
//==============================================================================
#ifndef RIPPLE_TX_HISTORYPROOF_H_INCLUDED
#define RIPPLE_TX_HISTORYPROOF_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
#include <xrpl/protocol/HistoryTree.h>
namespace ripple {
/** featureHistoryTree: prove possession of one history tree leaf.
The transactor only establishes that the proof is genuine against the
current ltHISTORY_TREE peaks. Who was challenged, who gets paid and who is
slashed is the genesis Hook's business; it runs as the strong TSH on
sfDestination and can read the full transaction, including the transient
sfHistoryProof, before the stored form is clipped.
*/
class HistoryProof : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit HistoryProof(ApplyContext& ctx) : Transactor(ctx)
{
}
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
/** Verify a proof blob against the tree in view. Shared with the
history_verify Hook API. Returns the leaf hash on success.
*/
static std::optional<uint256>
verify(ReadView const& view, std::uint64_t leafIndex, Slice blob);
};
} // namespace ripple
#endif
@@ -605,6 +605,7 @@ LedgerEntryTypesMatch::visitEntry(
case ltIMPORT_VLSEQ:
case ltUNL_REPORT:
case ltRANDOM:
case ltHISTORY_TREE:
case ltAMM:
case ltBRIDGE:
case ltXCHAIN_OWNED_CLAIM_ID:
+1
View File
@@ -44,6 +44,7 @@
#include <xrpld/app/tx/detail/DepositPreauth.h>
#include <xrpld/app/tx/detail/Escrow.h>
#include <xrpld/app/tx/detail/GenesisMint.h>
#include <xrpld/app/tx/detail/HistoryProof.h>
#include <xrpld/app/tx/detail/Import.h>
#include <xrpld/app/tx/detail/Invoke.h>
#include <xrpld/app/tx/detail/LedgerStateFix.h>
+3
View File
@@ -102,6 +102,9 @@ struct ConfigSection
#define SECTION_VALIDATORS "validators"
#define SECTION_VALIDATOR_TOKEN "validator_token"
#define SECTION_VETO_AMENDMENTS "veto_amendments"
// Testnet-only: amendments enabled on-ledger that this node should not treat
// as unsupported (i.e. should not become amendment blocked over).
#define SECTION_IGNORE_AMENDMENTS "ignore_amendments"
#define SECTION_WORKERS "workers"
} // namespace ripple
+6 -1
View File
@@ -46,7 +46,12 @@ enum ApplyFlags : std::uint32_t {
// Transaction shouldn't be applied
// Signatures shouldn't be checked
tapDRY_RUN = 0x1000
tapDRY_RUN = 0x1000,
// featureHistoryTree: a validated ledger is being replayed from its
// stored (clipped) transactions; trust that transient fields were present
// and verified when the ledger was first built.
tapHISTORY_REPLAY = 0x2000
};
constexpr ApplyFlags
+2 -1
View File
@@ -313,8 +313,9 @@ ApplyStateTable::apply(
beast::Journal j)
{
// Build metadata and insert
// featureHistoryTree: transient fields are clipped from the stored form.
auto const sTx = std::make_shared<Serializer>();
tx.add(*sTx);
tx.addStorable(*sTx);
std::shared_ptr<Serializer> sMeta;
std::optional<TxMeta> metadata;
if (!to.open() || isDryRun)