mirror of
https://github.com/Xahau/xahaud.git
synced 2026-10-26 21:57:12 +00:00
Compare commits
2
Commits
jsontx
...
history-tree
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b0c651761 | ||
|
|
c6173a74ff |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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, ({
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user