mirror of
https://github.com/Xahau/xahaud.git
synced 2026-10-28 06:37:07 +00:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a88a441305 | ||
|
|
4a9493c05f | ||
|
|
604ed0ca25 | ||
|
|
527a65b449 | ||
|
|
68dec561b3 | ||
|
|
8c66209cd6 | ||
|
|
a6cc9fde1d | ||
|
|
3cb3ca85c4 | ||
|
|
c75702a69e | ||
|
|
3d1df72c29 | ||
|
|
a246998076 | ||
|
|
5c9beffdf8 | ||
|
|
5080a7f05d | ||
|
|
6c76e020bb | ||
|
|
541ce8dcc1 | ||
|
|
bb57635fd6 | ||
|
|
737c24982a | ||
|
|
e0e7e6bb5d | ||
|
|
3b20c779b8 | ||
|
|
a981ee56f1 | ||
|
|
2653b33565 |
No files matched your search
@@ -127,7 +127,6 @@ test.rpc > xrpl.basics
|
||||
test.rpc > xrpld.app
|
||||
test.rpc > xrpld.core
|
||||
test.rpc > xrpld.net
|
||||
test.rpc > xrpld.nodestore
|
||||
test.rpc > xrpld.overlay
|
||||
test.rpc > xrpld.rpc
|
||||
test.rpc > xrpld.shamap
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ else
|
||||
# GH Action, runner
|
||||
if [[ "$(git rev-parse --abbrev-ref HEAD)" == "release" ]]; then
|
||||
echo "building on the release branch... placing it in builds/candidate"
|
||||
mkdir -p /data/builds/candidate
|
||||
mkdir /data/builds/candidate
|
||||
cp /io/release-build/xahaud /data/builds/candidate/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
|
||||
cp /io/release-build/release.info /data/builds/candidate/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4.releaseinfo
|
||||
else
|
||||
|
||||
@@ -345,14 +345,6 @@ 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,8 +112,6 @@
|
||||
#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)
|
||||
@@ -167,7 +165,6 @@
|
||||
#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)
|
||||
@@ -234,7 +231,6 @@
|
||||
#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)
|
||||
@@ -261,7 +257,6 @@
|
||||
#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,7 +62,6 @@
|
||||
#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,7 +16,6 @@
|
||||
#define featureHooksUpdate1 "1"
|
||||
#define featureHooksUpdate2 "1"
|
||||
#define featureRNG "1"
|
||||
#define featureHistoryTree "1"
|
||||
#define fix20250131 "1"
|
||||
#define fixGuardDepth32 "1"
|
||||
namespace hook_api {
|
||||
|
||||
@@ -306,8 +306,4 @@ 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,12 +96,6 @@ 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>
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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,10 +122,6 @@ 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,9 +48,6 @@ class STTx final : public STObject, public CountedObject<STTx>
|
||||
uint256 tid_;
|
||||
TxType tx_type_;
|
||||
|
||||
uint256
|
||||
computeTransactionID() const;
|
||||
|
||||
public:
|
||||
static std::size_t const minMultiSigners = 1;
|
||||
|
||||
@@ -110,32 +107,6 @@ 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,7 +34,6 @@
|
||||
// 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,20 +216,6 @@ 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,9 +160,6 @@ 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)
|
||||
@@ -227,8 +224,6 @@ 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)
|
||||
@@ -309,9 +304,6 @@ 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)
|
||||
@@ -345,7 +337,6 @@ 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,22 +517,6 @@ 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, ({
|
||||
|
||||
@@ -171,7 +171,6 @@ JSS(amendment_blocked); // out: NetworkOPs
|
||||
JSS(amm_account); // in: amm_info
|
||||
JSS(amount); // out: AccountChannels, amm_info
|
||||
JSS(amount2); // out: amm_info
|
||||
JSS(anchored_through); // out: CatalogueLoad
|
||||
JSS(api_version); // in: many, out: Version
|
||||
JSS(api_version_low); // out: Version
|
||||
JSS(applied); // out: SubmitTransaction
|
||||
@@ -745,7 +744,6 @@ JSS(TRANSACTION_FLAGS_INDICES); // out: RPC server_definitions
|
||||
JSS(type_hex); // out: STPathSet
|
||||
JSS(unl); // out: UnlList
|
||||
JSS(unlimited); // out: Connection.h
|
||||
JSS(unpinned); // out: CatalogueUnpin
|
||||
JSS(uptime); // out: GetCounts
|
||||
JSS(uri); // out: ValidatorSites
|
||||
JSS(url); // in/out: Subscribe, Unsubscribe
|
||||
|
||||
@@ -94,8 +94,7 @@ enum class LedgerNameSpace : std::uint16_t {
|
||||
MANIFEST_DIR = LEDGER_NAMESPACE2(0x01, 'M'),
|
||||
CREDENTIAL = LEDGER_NAMESPACE2(0x01, 'D'),
|
||||
PERMISSIONED_DOMAIN = 'm',
|
||||
RANDOM = 0x526E, // Rn
|
||||
HISTORY_TREE = 0x4874, // Ht
|
||||
RANDOM = 0x526E, // Rn
|
||||
|
||||
// No longer used or supported. Left here to reserve the space
|
||||
// to avoid accidental reuse.
|
||||
@@ -287,14 +286,6 @@ 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,7 +33,6 @@
|
||||
#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>
|
||||
|
||||
@@ -63,8 +62,7 @@ STTx::STTx(STObject&& object) : STObject(std::move(object))
|
||||
{
|
||||
tx_type_ = safe_cast<TxType>(getFieldU16(sfTransactionType));
|
||||
applyTemplate(getTxFormat(tx_type_)->getSOTemplate()); // may throw
|
||||
checkTransientFields();
|
||||
tid_ = computeTransactionID();
|
||||
tid_ = getHash(HashPrefix::transactionID);
|
||||
}
|
||||
|
||||
STTx::STTx(SerialIter& sit) : STObject(sfTransaction)
|
||||
@@ -80,8 +78,7 @@ STTx::STTx(SerialIter& sit) : STObject(sfTransaction)
|
||||
tx_type_ = safe_cast<TxType>(getFieldU16(sfTransactionType));
|
||||
|
||||
applyTemplate(getTxFormat(tx_type_)->getSOTemplate()); // May throw
|
||||
checkTransientFields();
|
||||
tid_ = computeTransactionID();
|
||||
tid_ = getHash(HashPrefix::transactionID);
|
||||
}
|
||||
|
||||
STTx::STTx(TxType type, std::function<void(STObject&)> assembler)
|
||||
@@ -99,70 +96,7 @@ STTx::STTx(TxType type, std::function<void(STObject&)> assembler)
|
||||
if (tx_type_ != type)
|
||||
LogicError("Transaction type was mutated during assembly");
|
||||
|
||||
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);
|
||||
tid_ = getHash(HashPrefix::transactionID);
|
||||
}
|
||||
|
||||
STBase*
|
||||
@@ -272,7 +206,7 @@ STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey)
|
||||
auto const sig = ripple::sign(publicKey, secretKey, makeSlice(data));
|
||||
|
||||
setFieldVL(sfTxnSignature, sig);
|
||||
tid_ = computeTransactionID();
|
||||
tid_ = getHash(HashPrefix::transactionID);
|
||||
}
|
||||
|
||||
Expected<void, std::string>
|
||||
|
||||
@@ -1271,79 +1271,6 @@ 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)
|
||||
{
|
||||
@@ -1367,7 +1294,6 @@ public:
|
||||
testBadConfig();
|
||||
testEnableVeto();
|
||||
testHasUnsupported();
|
||||
testIgnoredUnsupported();
|
||||
testFeature(all - fixMajorityCalc);
|
||||
testFeature(all);
|
||||
}
|
||||
|
||||
@@ -578,25 +578,6 @@ public:
|
||||
ter(tecNO_TARGET));
|
||||
env.close();
|
||||
}
|
||||
// HookName removed with a HookName-only update
|
||||
// The sfHooks entry keeps an STI_NOTPRESENT placeholder for
|
||||
// sfHookName, which preclaim must treat as "no name".
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::Account] = issuer.human();
|
||||
jv[jss::TransactionType] = jss::SetHook;
|
||||
jv[jss::Flags] = 0;
|
||||
jv[jss::Hooks] = Json::Value{Json::arrayValue};
|
||||
jv[jss::Hooks][0U][jss::Hook][jss::HookName] = "";
|
||||
env(jv, fee(XRP(1)));
|
||||
env.close();
|
||||
|
||||
env(reward::claim(alice),
|
||||
reward::issuer(issuer),
|
||||
fee(XRP(1)),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
}
|
||||
// Vaild HookOn
|
||||
{
|
||||
auto hookObj = hso(jtx::genesis::AcceptHook, overrideFlag);
|
||||
|
||||
@@ -33,11 +33,9 @@
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <exception>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
@@ -95,27 +93,6 @@ class SHAMapStore_test : public beast::unit_test::suite
|
||||
return cfg;
|
||||
}
|
||||
|
||||
// Online delete with DatabasePinned over in-memory backends. Unlike
|
||||
// pinnedPersistent, the hot store keeps nodes (type=rwdb is the null
|
||||
// node store), so ledgers closed in the test can be fetched. There is
|
||||
// no database_path, so every SHAMapStoreImp opens its own private
|
||||
// state.db: a second instance built by a test does not see (or trip
|
||||
// over) the rotation state of the Env's own.
|
||||
static auto
|
||||
pinnedInMemory(
|
||||
std::unique_ptr<Config> cfg,
|
||||
std::string const& nodePath,
|
||||
std::string const& pinnedPath)
|
||||
{
|
||||
cfg = onlineDelete(std::move(cfg));
|
||||
auto& section = cfg->section(ConfigSection::nodeDatabase());
|
||||
section.set("type", "memory");
|
||||
section.set("path", nodePath);
|
||||
section.set("pinned_type", "memory");
|
||||
section.set("pinned_path", pinnedPath);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
bool
|
||||
goodLedger(
|
||||
jtx::Env& env,
|
||||
@@ -1176,127 +1153,6 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// catalogue_unpin of everything (or a rolled-back first load) leaves the
|
||||
// pinned set empty. That must persist as "no pins", not as a string that
|
||||
// fails to parse on the next start.
|
||||
void
|
||||
testPinnedRangeRestoreEmpty()
|
||||
{
|
||||
testcase("pinned range restore of an empty set");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
beast::temp_dir tempDir;
|
||||
auto const tempPath = boost::filesystem::path(tempDir.path());
|
||||
auto const nodePath = (tempPath / "node").string();
|
||||
auto const pinnedPath = (tempPath / "pinned").string();
|
||||
|
||||
Env env(*this, envconfig(pinnedInMemory, nodePath, pinnedPath));
|
||||
|
||||
NodeStoreScheduler scheduler(env.app().getJobQueue());
|
||||
auto const journal = env.app().journal("SHAMapStoreTest");
|
||||
try
|
||||
{
|
||||
SHAMapStoreImp restoreCheck(env.app(), scheduler, journal);
|
||||
|
||||
RangeSet<std::uint32_t> ranges;
|
||||
ranges.insert(range(100u, 200u));
|
||||
restoreCheck.setPinnedRanges(ranges);
|
||||
restoreCheck.setPinnedRanges(RangeSet<std::uint32_t>{});
|
||||
|
||||
restoreCheck.start();
|
||||
restoreCheck.stop();
|
||||
pass();
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
fail(
|
||||
std::string("restart with no pinned ranges threw: ") +
|
||||
e.what());
|
||||
}
|
||||
BEAST_EXPECT(
|
||||
env.app().getLedgerMaster().getPinnedLedgersRangeSet().empty());
|
||||
}
|
||||
|
||||
// A crash between a pinned-range checkpoint and the backend committing
|
||||
// the last ledgers leaves the recorded interval's tail missing. Startup
|
||||
// trims it back to the last ledger present instead of refusing to start.
|
||||
void
|
||||
testPinnedRangeRestoreTrimsMissingTail()
|
||||
{
|
||||
testcase("pinned range restore trims a missing tail");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
beast::temp_dir tempDir;
|
||||
auto const tempPath = boost::filesystem::path(tempDir.path());
|
||||
auto const nodePath = (tempPath / "node").string();
|
||||
auto const pinnedPath = (tempPath / "pinned").string();
|
||||
|
||||
Env env(*this, envconfig(pinnedInMemory, nodePath, pinnedPath));
|
||||
for (int i = 0; i < 4; ++i)
|
||||
env.close();
|
||||
|
||||
auto& rdb = env.app().getRelationalDatabase();
|
||||
auto const last = env.app().getLedgerMaster().getValidLedgerIndex();
|
||||
auto const first = last - 2;
|
||||
|
||||
// Ledger saves may complete asynchronously; wait for the SQL rows.
|
||||
for (int i = 0; i < 100 && rdb.getHashByIndex(last).isZero(); ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
if (!BEAST_EXPECT(
|
||||
rdb.getHashByIndex(first).isNonZero() &&
|
||||
rdb.getHashByIndex(last).isNonZero()))
|
||||
return;
|
||||
|
||||
NodeStoreScheduler scheduler(env.app().getJobQueue());
|
||||
auto const journal = env.app().journal("SHAMapStoreTest");
|
||||
try
|
||||
{
|
||||
SHAMapStoreImp restoreCheck(env.app(), scheduler, journal);
|
||||
|
||||
// Recorded through last + 10, but only [first, last] exist.
|
||||
RangeSet<std::uint32_t> recorded;
|
||||
recorded.insert(range(first, last + 10));
|
||||
restoreCheck.setPinnedRanges(recorded);
|
||||
|
||||
restoreCheck.start();
|
||||
restoreCheck.stop();
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
fail(std::string("restart with a missing tail threw: ") + e.what());
|
||||
return;
|
||||
}
|
||||
|
||||
RangeSet<std::uint32_t> expected;
|
||||
expected.insert(range(first, last));
|
||||
BEAST_EXPECT(
|
||||
env.app().getLedgerMaster().getPinnedLedgersRangeSet() == expected);
|
||||
|
||||
// A missing tail longer than the trim limit (65536 ledgers) is not
|
||||
// an interrupted load: it means the wrong pinned store, so startup
|
||||
// still refuses.
|
||||
try
|
||||
{
|
||||
SHAMapStoreImp restoreCheck(env.app(), scheduler, journal);
|
||||
|
||||
RangeSet<std::uint32_t> recorded;
|
||||
recorded.insert(range(first, last + 70000));
|
||||
restoreCheck.setPinnedRanges(recorded);
|
||||
|
||||
restoreCheck.start();
|
||||
restoreCheck.stop();
|
||||
fail("Expected startup failure for an over-long missing tail");
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
std::string(e.what()).find("Persisted pinned interval") !=
|
||||
std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: compare two RangeSet<uint32_t> for set-equality. The
|
||||
// test cares about which sequences are in the result, not the
|
||||
// particular interval representation.
|
||||
@@ -1446,8 +1302,6 @@ public:
|
||||
testNullFactoryDropsWrites();
|
||||
testRotate();
|
||||
testPinnedRangeRestoreRequiresPinnedData();
|
||||
testPinnedRangeRestoreEmpty();
|
||||
testPinnedRangeRestoreTrimsMissingTail();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2134,118 +2134,6 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
/// Rename and remove the HookName with HookName-only updates
|
||||
Env env{*this, features};
|
||||
|
||||
env.fund(XRP(10000), alice);
|
||||
env.close();
|
||||
|
||||
// Create with hook name
|
||||
auto jvh = hso(accept_wasm);
|
||||
jvh[jss::HookName] = "41424344";
|
||||
env(ripple::test::jtx::hook(alice, {{jvh}}, 0),
|
||||
M("Create named hook"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
|
||||
auto const hookNameOnlyUpdate = [&](std::string const& name) {
|
||||
Json::Value jv;
|
||||
jv[jss::Account] = alice.human();
|
||||
jv[jss::TransactionType] = jss::SetHook;
|
||||
jv[jss::Flags] = 0;
|
||||
jv[jss::Hooks] = Json::Value{Json::arrayValue};
|
||||
Json::Value iv;
|
||||
iv[jss::HookName] = name;
|
||||
jv[jss::Hooks][0U][jss::Hook] = iv;
|
||||
return jv;
|
||||
};
|
||||
|
||||
// Rename: update carrying only a non-empty sfHookName
|
||||
env(hookNameOnlyUpdate("4142434445"),
|
||||
M("Rename hook with a HookName-only update"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
{
|
||||
auto const hooks = env.le(keylet::hook(alice));
|
||||
BEAST_EXPECT(hooks && hooks->isFieldPresent(sfHooks));
|
||||
auto const& hooksArray = hooks->getFieldArray(sfHooks);
|
||||
BEAST_EXPECT(hooksArray.size() == 1);
|
||||
BEAST_EXPECT(hooksArray[0].isFieldPresent(sfHookName));
|
||||
BEAST_EXPECT(
|
||||
strHex(hooksArray[0].getFieldVL(sfHookName)) ==
|
||||
"4142434445");
|
||||
}
|
||||
|
||||
// Remove: update carrying only an empty sfHookName
|
||||
env(hookNameOnlyUpdate(""),
|
||||
M("Remove hook name with a HookName-only update"),
|
||||
HSFEE);
|
||||
|
||||
// In the same open ledger as the removal: the open view hands
|
||||
// the next transaction the very sfHooks entry the SetHook just
|
||||
// modified, so the fee calculation and the hook chain must
|
||||
// tolerate the removed name. This is the path that crashed the
|
||||
// node at submit, and crashed every validator replaying the
|
||||
// pair while building the ledger.
|
||||
{
|
||||
auto jv = invoke::invoke(alice);
|
||||
auto const expectedFee =
|
||||
calculateBaseFee(*env.current(), *env.jt(jv).stx);
|
||||
BEAST_EXPECT(expectedFee == drops(19));
|
||||
env(jv,
|
||||
M("Invoke in the same open ledger as the removal"),
|
||||
HSFEE);
|
||||
}
|
||||
env.close();
|
||||
{
|
||||
auto const hookExecutions =
|
||||
env.meta()->getFieldArray(sfHookExecutions);
|
||||
BEAST_EXPECT(hookExecutions.size() == 1);
|
||||
BEAST_EXPECT(
|
||||
hookExecutions[0].getFieldH256(sfHookHash) == accept_hash);
|
||||
}
|
||||
{
|
||||
auto const hooks = env.le(keylet::hook(alice));
|
||||
BEAST_EXPECT(hooks && hooks->isFieldPresent(sfHooks));
|
||||
auto const& hooksArray = hooks->getFieldArray(sfHooks);
|
||||
BEAST_EXPECT(hooksArray.size() == 1);
|
||||
BEAST_EXPECT(!hooksArray[0].isFieldPresent(sfHookName));
|
||||
}
|
||||
|
||||
// The now non-named hook must still be charged for and
|
||||
// executed by transactions in later ledgers, where the entry
|
||||
// is re-read from its serialized form.
|
||||
{
|
||||
auto jv = invoke::invoke(alice);
|
||||
auto const expectedFee =
|
||||
calculateBaseFee(*env.current(), *env.jt(jv).stx);
|
||||
BEAST_EXPECT(expectedFee == drops(19));
|
||||
env(jv, M("Invoke after removing the hook name"), HSFEE);
|
||||
env.close();
|
||||
auto const hookExecutions =
|
||||
env.meta()->getFieldArray(sfHookExecutions);
|
||||
BEAST_EXPECT(hookExecutions.size() == 1);
|
||||
BEAST_EXPECT(
|
||||
hookExecutions[0].getFieldH256(sfHookHash) == accept_hash);
|
||||
}
|
||||
|
||||
// Removing the name of a hook that has none must be a no-op,
|
||||
// not a thrown FieldErr (-> tefEXCEPTION): makeFieldAbsent on
|
||||
// the untemplated entry throws when the field is missing.
|
||||
env(hookNameOnlyUpdate(""),
|
||||
M("Remove hook name again (no-op)"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
{
|
||||
auto const hooks = env.le(keylet::hook(alice));
|
||||
BEAST_EXPECT(hooks && hooks->isFieldPresent(sfHooks));
|
||||
auto const& hooksArray = hooks->getFieldArray(sfHooks);
|
||||
BEAST_EXPECT(hooksArray.size() == 1);
|
||||
BEAST_EXPECT(!hooksArray[0].isFieldPresent(sfHookName));
|
||||
}
|
||||
}
|
||||
|
||||
// Install named hook
|
||||
auto jvh = hso(accept_wasm);
|
||||
jvh[jss::HookName] = "41424344";
|
||||
|
||||
@@ -1122,36 +1122,6 @@ trustthesevalidators.gov
|
||||
}
|
||||
}
|
||||
|
||||
// Standalone forces ledger_history to zero, but [node_db] type=rwdb
|
||||
// (the null node store) sizes its retained-ledger window from it and
|
||||
// SHAMapStoreImp throws on zero. cfg/rippled.cfg ships exactly this
|
||||
// configuration, and jtx never calls Config::setup, so pin it here.
|
||||
void
|
||||
testSetupStandaloneRwdb()
|
||||
{
|
||||
testcase("setup standalone rwdb keeps ledger_history");
|
||||
|
||||
for (std::string const type : {"rwdb", "NuDB"})
|
||||
{
|
||||
detail::RippledCfgGuard const cfg(
|
||||
*this,
|
||||
"testSetupStandaloneRwdb_" + type,
|
||||
"",
|
||||
"",
|
||||
true,
|
||||
"[node_db]\ntype=" + type + "\npath=main\n");
|
||||
Config config;
|
||||
config.setup(
|
||||
cfg.configFile(),
|
||||
/*bQuiet*/ true,
|
||||
/* bSilent */ true,
|
||||
/* bStandalone */ true);
|
||||
BEAST_EXPECT(config.standalone());
|
||||
BEAST_EXPECT(config.nullBackend() == (type == "rwdb"));
|
||||
BEAST_EXPECT(config.LEDGER_HISTORY == (type == "rwdb" ? 256 : 0));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testPort()
|
||||
{
|
||||
@@ -1843,7 +1813,6 @@ r.ripple.com:51235
|
||||
testValidatorsFile();
|
||||
testSetup(false);
|
||||
testSetup(true);
|
||||
testSetupStandaloneRwdb();
|
||||
testPort();
|
||||
testZeroPort();
|
||||
testWhitespace();
|
||||
|
||||
@@ -66,14 +66,6 @@ public:
|
||||
// Write the batch
|
||||
storeBatch(*backend, batch);
|
||||
|
||||
// Durability on demand: RocksDB drains its write queue and
|
||||
// syncs its WAL; in-memory backends have nothing to wait for;
|
||||
// NuDB commits on its own thread and cannot promise it.
|
||||
backend->store(batch.front());
|
||||
bool const durable = backend->syncDurable();
|
||||
BEAST_EXPECT(durable == (type != "nudb"));
|
||||
backend->sync();
|
||||
|
||||
{
|
||||
// Read it back in
|
||||
Batch copy;
|
||||
|
||||
@@ -18,22 +18,16 @@
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <xrpld/app/ledger/Ledger.h>
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/core/ConfigSections.h>
|
||||
#include <xrpld/nodestore/detail/DatabasePinnedImp.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/beast/utility/temp_dir.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <grpc/impl/codegen/compression_types.h>
|
||||
#include <iterator>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
|
||||
namespace ripple {
|
||||
@@ -108,28 +102,14 @@ struct CatalogueEnvOpts
|
||||
};
|
||||
|
||||
// envconfig with pinned_type set so catalogue_load passes the config guard.
|
||||
//
|
||||
// envconfig() selects [node_db] type=rwdb, which is the tree-only null node
|
||||
// store: SHAMapStore never builds DatabasePinned on it, so catalogue_load
|
||||
// would refuse to run. Use the in-memory "memory" store for the hot side
|
||||
// instead. It is shared by path within the process, so each config gets its
|
||||
// own path (SHAMapStore creates the directory; nothing is written to it).
|
||||
// The pinned side uses rwdb, an in-memory persistent backend.
|
||||
// Uses rwdb for in-process speed (allowed in standalone).
|
||||
inline std::unique_ptr<Config>
|
||||
catalogueEnvconfig(CatalogueEnvOpts const& opts = {})
|
||||
{
|
||||
auto cfg = test::jtx::envconfig();
|
||||
auto& nodeDb = cfg->section(ConfigSection::nodeDatabase());
|
||||
if (opts.withPinnedType)
|
||||
{
|
||||
nodeDb.set("type", "memory");
|
||||
nodeDb.set(
|
||||
"path",
|
||||
(boost::filesystem::temp_directory_path() /
|
||||
boost::filesystem::unique_path("catalogue-test-%%%%-%%%%-%%%%"))
|
||||
.string());
|
||||
nodeDb.set("pinned_type", "rwdb");
|
||||
}
|
||||
nodeDb.set("online_delete", opts.onlineDelete);
|
||||
return cfg;
|
||||
}
|
||||
@@ -1087,621 +1067,6 @@ class Catalogue_test : public beast::unit_test::suite
|
||||
}
|
||||
|
||||
public:
|
||||
// ---- Editing uncompressed catalogues -----------------------------------
|
||||
//
|
||||
// Ledger record: seq, hash, txHash, accountHash, parentHash, drops,
|
||||
// closeFlags, closeTimeResolution, closeTime, parentCloseTime; then the
|
||||
// state map and the tx map, each a run of leaf records ending in
|
||||
// tnTERMINAL. A leaf record is type (1), key (32), and, unless it is a
|
||||
// tnREMOVE, size (4) and data.
|
||||
|
||||
static constexpr std::size_t catLedgerHeaderSize =
|
||||
4 + 32 * 4 + 8 + sizeof(int) + 4 + 8 + 8;
|
||||
|
||||
struct CatLedgerRecord
|
||||
{
|
||||
std::size_t header; // first byte of the ledger header
|
||||
std::size_t state; // first byte of the state map
|
||||
std::size_t tx; // first byte of the tx map
|
||||
std::size_t end; // one past the tx map's tnTERMINAL
|
||||
};
|
||||
|
||||
// Position just past the map that starts at `pos`.
|
||||
static std::size_t
|
||||
skipCatalogueMap(std::vector<std::uint8_t> const& bytes, std::size_t pos)
|
||||
{
|
||||
while (pos < bytes.size())
|
||||
{
|
||||
auto const type = bytes[pos++];
|
||||
if (type == 255) // tnTERMINAL
|
||||
return pos;
|
||||
pos += 32;
|
||||
if (type == 254) // tnREMOVE
|
||||
continue;
|
||||
if (pos + 4 > bytes.size())
|
||||
break;
|
||||
std::uint32_t size = 0;
|
||||
std::memcpy(&size, &bytes[pos], 4);
|
||||
pos += 4 + size;
|
||||
}
|
||||
return bytes.size();
|
||||
}
|
||||
|
||||
// Position of the size field of the first leaf (not a removal) in the
|
||||
// map that starts at `pos`, if the map has one.
|
||||
static std::optional<std::size_t>
|
||||
firstLeafSizeField(std::vector<std::uint8_t> const& bytes, std::size_t pos)
|
||||
{
|
||||
while (pos < bytes.size())
|
||||
{
|
||||
auto const type = bytes[pos];
|
||||
if (type == 255)
|
||||
return std::nullopt;
|
||||
if (type != 254)
|
||||
return pos + 1 + 32;
|
||||
pos += 1 + 32;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static std::vector<CatLedgerRecord>
|
||||
indexCatalogue(std::vector<std::uint8_t> const& bytes)
|
||||
{
|
||||
std::vector<CatLedgerRecord> records;
|
||||
std::size_t pos = sizeof(TestCATLHeader);
|
||||
while (pos + catLedgerHeaderSize <= bytes.size())
|
||||
{
|
||||
CatLedgerRecord r;
|
||||
r.header = pos;
|
||||
r.state = pos + catLedgerHeaderSize;
|
||||
r.tx = skipCatalogueMap(bytes, r.state);
|
||||
r.end = skipCatalogueMap(bytes, r.tx);
|
||||
records.push_back(r);
|
||||
pos = r.end;
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
static std::vector<std::uint8_t>
|
||||
readCatalogue(std::string const& path)
|
||||
{
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
return {
|
||||
std::istreambuf_iterator<char>(in),
|
||||
std::istreambuf_iterator<char>()};
|
||||
}
|
||||
|
||||
static void
|
||||
writeCatalogue(std::string const& path, std::vector<std::uint8_t> const& b)
|
||||
{
|
||||
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
||||
out.write(reinterpret_cast<char const*>(b.data()), b.size());
|
||||
}
|
||||
|
||||
static Json::Value
|
||||
loadCatalogue(test::jtx::Env& env, std::string const& path, bool ignoreHash)
|
||||
{
|
||||
Json::Value params{Json::objectValue};
|
||||
params[jss::input_file] = path;
|
||||
if (ignoreHash)
|
||||
params[jss::ignore_hash] = true;
|
||||
return env.client().invoke("catalogue_load", params)[jss::result];
|
||||
}
|
||||
|
||||
static bool
|
||||
errorContains(Json::Value const& result, std::string const& what)
|
||||
{
|
||||
return result[jss::status] == "error" &&
|
||||
result[jss::error_message].asString().find(what) !=
|
||||
std::string::npos;
|
||||
}
|
||||
|
||||
void
|
||||
testCatalogueLoadFailures(FeatureBitset features)
|
||||
{
|
||||
testcase("catalogue_load: failures part way through a file");
|
||||
using namespace test::jtx;
|
||||
using beast::severities::kDisabled;
|
||||
|
||||
boost::filesystem::path tempDir =
|
||||
boost::filesystem::temp_directory_path() /
|
||||
boost::filesystem::unique_path();
|
||||
boost::filesystem::create_directories(tempDir);
|
||||
auto const goodPath = (tempDir / "good.catl").string();
|
||||
|
||||
// An uncompressed catalogue of ledgers 3-6, so records can be edited.
|
||||
{
|
||||
Env env{*this, catalogueEnvconfig(), features, nullptr, kDisabled};
|
||||
prepareLedgerData(env, 3);
|
||||
|
||||
Json::Value params{Json::objectValue};
|
||||
params[jss::min_ledger] = 3;
|
||||
params[jss::max_ledger] = 6;
|
||||
params[jss::output_file] = goodPath;
|
||||
params[jss::compression_level] = 0;
|
||||
auto const result =
|
||||
env.client().invoke("catalogue_create", params)[jss::result];
|
||||
BEAST_EXPECT(result[jss::status] == jss::success);
|
||||
}
|
||||
|
||||
auto const good = readCatalogue(goodPath);
|
||||
auto const records = indexCatalogue(good);
|
||||
if (!BEAST_EXPECT(
|
||||
records.size() == 4 && records.back().end == good.size()))
|
||||
return;
|
||||
|
||||
// Write `bytes` to a new file, load it into a fresh server, and hand
|
||||
// the result and the resulting pinned set to `check`.
|
||||
int fileNo = 0;
|
||||
auto loadEdited = [&](std::vector<std::uint8_t> const& bytes,
|
||||
bool ignoreHash,
|
||||
auto&& check) {
|
||||
auto const path =
|
||||
(tempDir / ("edited" + std::to_string(++fileNo) + ".catl"))
|
||||
.string();
|
||||
writeCatalogue(path, bytes);
|
||||
Env env{*this, catalogueEnvconfig(), features, nullptr, kDisabled};
|
||||
auto const result = loadCatalogue(env, path, ignoreHash);
|
||||
check(
|
||||
result, env.app().getLedgerMaster().getPinnedLedgersRangeSet());
|
||||
};
|
||||
|
||||
auto const hashField = offsetof(TestCATLHeader, hash);
|
||||
auto const maxLedgerField = offsetof(TestCATLHeader, max_ledger);
|
||||
// Offsets within a ledger header record.
|
||||
constexpr std::size_t accountHashAt = 4 + 32 * 2;
|
||||
constexpr std::size_t dropsAt = 4 + 32 * 4;
|
||||
|
||||
// The stored checksum is wrong but every ledger is intact: all four
|
||||
// load, then the whole-file check fails and the run is rolled back.
|
||||
{
|
||||
auto bytes = good;
|
||||
bytes[hashField] ^= 0xff;
|
||||
loadEdited(
|
||||
bytes, false, [&](auto const& result, auto const& pinned) {
|
||||
BEAST_EXPECT(errorContains(
|
||||
result, "catalogue hash verification failed"));
|
||||
BEAST_EXPECT(pinned.empty());
|
||||
});
|
||||
}
|
||||
|
||||
// A ledger part way through is corrupt. The load stops there, the
|
||||
// rest of the file is still hashed, the checksum fails, and the
|
||||
// ledgers loaded before it are rolled back too.
|
||||
{
|
||||
auto bytes = good;
|
||||
bytes[records[2].header + dropsAt] ^= 0x01;
|
||||
loadEdited(
|
||||
bytes, false, [&](auto const& result, auto const& pinned) {
|
||||
BEAST_EXPECT(errorContains(
|
||||
result, "catalogue hash verification failed"));
|
||||
BEAST_EXPECT(pinned.empty());
|
||||
});
|
||||
}
|
||||
|
||||
// A checksum failure only rolls back what this run added: ledgers
|
||||
// that were already pinned stay pinned.
|
||||
{
|
||||
auto bytes = good;
|
||||
bytes[hashField] ^= 0xff;
|
||||
auto const badPath = (tempDir / "badsum.catl").string();
|
||||
writeCatalogue(badPath, bytes);
|
||||
|
||||
Env env{*this, catalogueEnvconfig(), features, nullptr, kDisabled};
|
||||
BEAST_EXPECT(
|
||||
loadCatalogue(env, goodPath, false)[jss::status] ==
|
||||
jss::success);
|
||||
BEAST_EXPECT(errorContains(
|
||||
loadCatalogue(env, badPath, false),
|
||||
"catalogue hash verification failed"));
|
||||
auto& lm = env.app().getLedgerMaster();
|
||||
BEAST_EXPECT(to_string(lm.getPinnedLedgersRangeSet()) == "3-6");
|
||||
|
||||
// The pinned store: an empty batch is a no-op, an in-memory
|
||||
// backend is durable on demand, and a fetch made for
|
||||
// online_delete's copy pass (duplicate == true) finds nodes
|
||||
// that exist only in the pinned store.
|
||||
auto* const pinnedDb = dynamic_cast<NodeStore::DatabasePinnedImp*>(
|
||||
&env.app().getNodeStore());
|
||||
if (BEAST_EXPECT(pinnedDb))
|
||||
{
|
||||
pinnedDb->storePinnedBatch({});
|
||||
BEAST_EXPECT(pinnedDb->syncPinned());
|
||||
auto const stateRoot =
|
||||
uint256::fromVoid(&good[records[0].header + accountHashAt]);
|
||||
BEAST_EXPECT(env.app().getNodeStore().fetchNodeObject(
|
||||
stateRoot, 0, NodeStore::FetchType::synchronous, true));
|
||||
}
|
||||
}
|
||||
|
||||
// Out of sequence: ledger 4's record claims to be ledger 9.
|
||||
{
|
||||
auto bytes = good;
|
||||
std::uint32_t const nine = 9;
|
||||
std::memcpy(&bytes[records[1].header], &nine, 4);
|
||||
loadEdited(
|
||||
bytes, true, [&](auto const& result, auto const& pinned) {
|
||||
BEAST_EXPECT(errorContains(result, "out of sequence"));
|
||||
BEAST_EXPECT(boost::icl::contains(pinned, 3u));
|
||||
BEAST_EXPECT(!boost::icl::contains(pinned, 4u));
|
||||
});
|
||||
}
|
||||
|
||||
// The header promises a ledger 7 the file does not contain. Ledgers
|
||||
// 3-6 load (hash checks are off) before the file runs out.
|
||||
{
|
||||
auto bytes = good;
|
||||
std::uint32_t const seven = 7;
|
||||
std::memcpy(&bytes[maxLedgerField], &seven, 4);
|
||||
loadEdited(
|
||||
bytes, true, [&](auto const& result, auto const& pinned) {
|
||||
BEAST_EXPECT(errorContains(
|
||||
result, "Unexpected end of catalogue file"));
|
||||
BEAST_EXPECT(to_string(pinned) == "3-6");
|
||||
});
|
||||
}
|
||||
|
||||
// An absurd leaf size in each kind of map is rejected while parsing.
|
||||
auto withHugeLeaf = [&](std::size_t sizeField) {
|
||||
auto bytes = good;
|
||||
std::uint32_t const huge = 0xFFFFFFFF;
|
||||
std::memcpy(&bytes[sizeField], &huge, 4);
|
||||
return bytes;
|
||||
};
|
||||
|
||||
// ... in the base ledger's state map.
|
||||
if (auto const field = firstLeafSizeField(good, records[0].state);
|
||||
BEAST_EXPECT(field))
|
||||
{
|
||||
loadEdited(
|
||||
withHugeLeaf(*field),
|
||||
true,
|
||||
[&](auto const& result, auto const& pinned) {
|
||||
BEAST_EXPECT(errorContains(
|
||||
result, "Failed to load base ledger state"));
|
||||
BEAST_EXPECT(pinned.empty());
|
||||
});
|
||||
}
|
||||
|
||||
// ... in a later ledger's state delta.
|
||||
{
|
||||
std::optional<std::size_t> index;
|
||||
std::optional<std::size_t> field;
|
||||
for (std::size_t i = 1; i < records.size() && !field; ++i)
|
||||
if ((field = firstLeafSizeField(good, records[i].state)))
|
||||
index = i;
|
||||
if (BEAST_EXPECT(field))
|
||||
{
|
||||
std::uint32_t const seq = 3 + *index;
|
||||
loadEdited(
|
||||
withHugeLeaf(*field),
|
||||
true,
|
||||
[&](auto const& result, auto const& pinned) {
|
||||
BEAST_EXPECT(errorContains(
|
||||
result, "Failed to apply ledger delta"));
|
||||
BEAST_EXPECT(boost::icl::contains(pinned, seq - 1));
|
||||
BEAST_EXPECT(!boost::icl::contains(pinned, seq));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ... in a ledger's transaction map.
|
||||
{
|
||||
std::optional<std::size_t> index;
|
||||
std::optional<std::size_t> field;
|
||||
for (std::size_t i = 0; i < records.size() && !field; ++i)
|
||||
if ((field = firstLeafSizeField(good, records[i].tx)))
|
||||
index = i;
|
||||
if (BEAST_EXPECT(field))
|
||||
{
|
||||
std::uint32_t const seq = 3 + *index;
|
||||
loadEdited(
|
||||
withHugeLeaf(*field),
|
||||
true,
|
||||
[&](auto const& result, auto const& pinned) {
|
||||
BEAST_EXPECT(errorContains(
|
||||
result, "Failed to apply ledger delta"));
|
||||
BEAST_EXPECT(!boost::icl::contains(pinned, seq));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
boost::filesystem::remove_all(tempDir);
|
||||
}
|
||||
|
||||
void
|
||||
testCataloguePinnedStoreRequired(FeatureBitset features)
|
||||
{
|
||||
testcase("catalogue_load / catalogue_unpin: pinned store required");
|
||||
using namespace test::jtx;
|
||||
using beast::severities::kDisabled;
|
||||
|
||||
// pinned_type is set, but [node_db] type=rwdb is the null node
|
||||
// store, on which SHAMapStore never builds the pinned store.
|
||||
{
|
||||
auto cfg = test::jtx::envconfig();
|
||||
auto& nodeDb = cfg->section(ConfigSection::nodeDatabase());
|
||||
nodeDb.set("pinned_type", "rwdb");
|
||||
nodeDb.set("online_delete", "256");
|
||||
Env env{*this, std::move(cfg), features, nullptr, kDisabled};
|
||||
|
||||
Json::Value params{Json::objectValue};
|
||||
params[jss::input_file] = "/nonexistent.catl";
|
||||
auto const result =
|
||||
env.client().invoke("catalogue_load", params)[jss::result];
|
||||
BEAST_EXPECT(
|
||||
errorContains(result, "requires the pinned node store"));
|
||||
}
|
||||
|
||||
// catalogue_unpin needs pinned_type configured at all.
|
||||
{
|
||||
Env env{
|
||||
*this,
|
||||
catalogueEnvconfig({.withPinnedType = false}),
|
||||
features,
|
||||
nullptr,
|
||||
kDisabled};
|
||||
|
||||
Json::Value params{Json::objectValue};
|
||||
params[jss::ledger_index_min] = 1;
|
||||
params[jss::ledger_index_max] = 2;
|
||||
auto const result =
|
||||
env.client().invoke("catalogue_unpin", params)[jss::result];
|
||||
BEAST_EXPECT(errorContains(result, "pinned_type"));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testCatalogueChainLinkage(FeatureBitset features)
|
||||
{
|
||||
testcase("catalogue_load: Ledgers must chain");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{
|
||||
*this,
|
||||
catalogueEnvconfig(),
|
||||
features,
|
||||
nullptr,
|
||||
beast::severities::kDisabled,
|
||||
};
|
||||
prepareLedgerData(env, 3);
|
||||
|
||||
boost::filesystem::path tempDir =
|
||||
boost::filesystem::temp_directory_path() /
|
||||
boost::filesystem::unique_path();
|
||||
boost::filesystem::create_directories(tempDir);
|
||||
auto cataloguePath = (tempDir / "test.catl").string();
|
||||
|
||||
// Uncompressed, so ledger records can be located and edited.
|
||||
{
|
||||
Json::Value params{Json::objectValue};
|
||||
params[jss::min_ledger] = 3;
|
||||
params[jss::max_ledger] = 6;
|
||||
params[jss::output_file] = cataloguePath;
|
||||
params[jss::compression_level] = 0;
|
||||
auto const result =
|
||||
env.client().invoke("catalogue_create", params)[jss::result];
|
||||
BEAST_EXPECT(result[jss::status] == jss::success);
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> bytes;
|
||||
{
|
||||
std::ifstream in(cataloguePath, std::ios::binary);
|
||||
bytes.assign(
|
||||
std::istreambuf_iterator<char>(in),
|
||||
std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
// Ledger record: seq, hash, txHash, accountHash, parentHash, drops,
|
||||
// closeFlags, closeTimeResolution, closeTime, parentCloseTime; then
|
||||
// the state map and tx map as leaf records ending in tnTERMINAL.
|
||||
constexpr std::size_t ledgerHeaderSize =
|
||||
4 + 32 * 4 + 8 + sizeof(int) + 4 + 8 + 8;
|
||||
auto skipMap = [&](std::size_t pos) -> std::size_t {
|
||||
while (pos < bytes.size())
|
||||
{
|
||||
auto const type = bytes[pos++];
|
||||
if (type == 255) // tnTERMINAL
|
||||
return pos;
|
||||
pos += 32;
|
||||
if (type == 254) // tnREMOVE
|
||||
continue;
|
||||
if (pos + 4 > bytes.size())
|
||||
break;
|
||||
std::uint32_t size = 0;
|
||||
std::memcpy(&size, &bytes[pos], 4);
|
||||
pos += 4 + size;
|
||||
}
|
||||
return bytes.size();
|
||||
};
|
||||
|
||||
// Skip the base ledger (3) to reach ledger 4.
|
||||
std::size_t pos = sizeof(TestCATLHeader) + ledgerHeaderSize;
|
||||
pos = skipMap(skipMap(pos));
|
||||
if (!BEAST_EXPECT(pos + ledgerHeaderSize <= bytes.size()))
|
||||
return;
|
||||
|
||||
LedgerInfo info;
|
||||
std::size_t p = pos;
|
||||
auto take = [&](void* dst, std::size_t n) {
|
||||
std::memcpy(dst, &bytes[p], n);
|
||||
p += n;
|
||||
};
|
||||
std::uint64_t drops = 0, closeTime = 0, parentCloseTime = 0;
|
||||
std::uint32_t resolution = 0;
|
||||
take(&info.seq, 4);
|
||||
std::size_t const hashOffset = p;
|
||||
take(info.hash.data(), 32);
|
||||
take(info.txHash.data(), 32);
|
||||
take(info.accountHash.data(), 32);
|
||||
std::size_t const parentOffset = p;
|
||||
take(info.parentHash.data(), 32);
|
||||
take(&drops, 8);
|
||||
take(&info.closeFlags, sizeof(info.closeFlags));
|
||||
take(&resolution, 4);
|
||||
take(&closeTime, 8);
|
||||
take(&parentCloseTime, 8);
|
||||
info.drops = drops;
|
||||
info.closeTime = NetClock::time_point{NetClock::duration{closeTime}};
|
||||
info.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{parentCloseTime}};
|
||||
info.closeTimeResolution = NetClock::duration{resolution};
|
||||
|
||||
BEAST_EXPECT(info.seq == 4);
|
||||
// Sanity: the record parses to a ledger whose hash checks out.
|
||||
BEAST_EXPECT(calculateLedgerHash(info) == info.hash);
|
||||
|
||||
// Re-parent ledger 4 and fix up its hash, so the file stays
|
||||
// internally consistent ledger by ledger; only the chain check can
|
||||
// catch it.
|
||||
info.parentHash = ~info.parentHash;
|
||||
info.hash = calculateLedgerHash(info);
|
||||
std::memcpy(&bytes[parentOffset], info.parentHash.data(), 32);
|
||||
std::memcpy(&bytes[hashOffset], info.hash.data(), 32);
|
||||
{
|
||||
std::ofstream out(
|
||||
cataloguePath, std::ios::binary | std::ios::trunc);
|
||||
out.write(
|
||||
reinterpret_cast<char const*>(bytes.data()), bytes.size());
|
||||
}
|
||||
|
||||
Env loadEnv{
|
||||
*this,
|
||||
catalogueEnvconfig(),
|
||||
features,
|
||||
nullptr,
|
||||
beast::severities::kDisabled,
|
||||
};
|
||||
|
||||
Json::Value params{Json::objectValue};
|
||||
params[jss::input_file] = cataloguePath;
|
||||
params[jss::ignore_hash] = true; // the file was edited
|
||||
auto const result =
|
||||
loadEnv.client().invoke("catalogue_load", params)[jss::result];
|
||||
BEAST_EXPECT(result[jss::status] == "error");
|
||||
BEAST_EXPECT(
|
||||
result[jss::error_message].asString().find(
|
||||
"does not chain to its predecessor") != std::string::npos);
|
||||
|
||||
// The base ledger verified and stays pinned; nothing after it does.
|
||||
auto const pinned =
|
||||
loadEnv.app().getLedgerMaster().getPinnedLedgersRangeSet();
|
||||
BEAST_EXPECT(boost::icl::contains(pinned, 3u));
|
||||
BEAST_EXPECT(!boost::icl::contains(pinned, 4u));
|
||||
|
||||
boost::filesystem::remove_all(tempDir);
|
||||
}
|
||||
|
||||
void
|
||||
testCatalogueUnpin(FeatureBitset features)
|
||||
{
|
||||
testcase("catalogue_unpin");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{
|
||||
*this,
|
||||
catalogueEnvconfig(),
|
||||
features,
|
||||
nullptr,
|
||||
beast::severities::kDisabled,
|
||||
};
|
||||
prepareLedgerData(env, 5);
|
||||
|
||||
boost::filesystem::path tempDir =
|
||||
boost::filesystem::temp_directory_path() /
|
||||
boost::filesystem::unique_path();
|
||||
boost::filesystem::create_directories(tempDir);
|
||||
auto cataloguePath = (tempDir / "test.catl").string();
|
||||
|
||||
{
|
||||
Json::Value params{Json::objectValue};
|
||||
params[jss::min_ledger] = 3;
|
||||
params[jss::max_ledger] = 8;
|
||||
params[jss::output_file] = cataloguePath;
|
||||
auto const result =
|
||||
env.client().invoke("catalogue_create", params)[jss::result];
|
||||
BEAST_EXPECT(result[jss::status] == jss::success);
|
||||
}
|
||||
{
|
||||
Json::Value params{Json::objectValue};
|
||||
params[jss::input_file] = cataloguePath;
|
||||
auto const result =
|
||||
env.client().invoke("catalogue_load", params)[jss::result];
|
||||
BEAST_EXPECT(result[jss::status] == jss::success);
|
||||
}
|
||||
|
||||
auto& lm = env.app().getLedgerMaster();
|
||||
BEAST_EXPECT(to_string(lm.getPinnedLedgersRangeSet()) == "3-8");
|
||||
|
||||
auto unpin = [&](std::optional<Json::Value> min,
|
||||
std::optional<Json::Value> max) {
|
||||
Json::Value params{Json::objectValue};
|
||||
if (min)
|
||||
params[jss::ledger_index_min] = *min;
|
||||
if (max)
|
||||
params[jss::ledger_index_max] = *max;
|
||||
return env.client().invoke("catalogue_unpin", params)[jss::result];
|
||||
};
|
||||
|
||||
// Bad parameters
|
||||
BEAST_EXPECT(unpin({}, {})[jss::status] == "error");
|
||||
BEAST_EXPECT(unpin(5, {})[jss::status] == "error");
|
||||
BEAST_EXPECT(unpin(6, 5)[jss::status] == "error");
|
||||
BEAST_EXPECT(unpin(-1, 5)[jss::status] == "error");
|
||||
BEAST_EXPECT(unpin("x", 5)[jss::status] == "error");
|
||||
BEAST_EXPECT(to_string(lm.getPinnedLedgersRangeSet()) == "3-8");
|
||||
|
||||
// Cut a hole
|
||||
{
|
||||
auto const result = unpin(5, 6);
|
||||
BEAST_EXPECT(result[jss::status] == jss::success);
|
||||
BEAST_EXPECT(result[jss::unpinned] == "5-6");
|
||||
BEAST_EXPECT(result[jss::complete_ledgers_pinned] == "3-4,7-8");
|
||||
BEAST_EXPECT(lm.isPinned(4) && lm.isPinned(7));
|
||||
BEAST_EXPECT(!lm.isPinned(5) && !lm.isPinned(6));
|
||||
}
|
||||
|
||||
// A range only partly pinned unpins just the pinned part
|
||||
{
|
||||
auto const result = unpin(1, 4);
|
||||
BEAST_EXPECT(result[jss::status] == jss::success);
|
||||
BEAST_EXPECT(result[jss::unpinned] == "3-4");
|
||||
BEAST_EXPECT(result[jss::complete_ledgers_pinned] == "7-8");
|
||||
}
|
||||
|
||||
// Nothing pinned in range: success, nothing unpinned. (Unsigned
|
||||
// JSON values are accepted as well as non-negative signed ones.)
|
||||
{
|
||||
auto const result = unpin(Json::UInt(5), Json::UInt(6));
|
||||
BEAST_EXPECT(result[jss::status] == jss::success);
|
||||
BEAST_EXPECT(result[jss::unpinned] == "empty");
|
||||
BEAST_EXPECT(result[jss::complete_ledgers_pinned] == "7-8");
|
||||
}
|
||||
|
||||
// Unpinning the rest leaves nothing pinned. (state.db then records
|
||||
// an empty set, which must not stop the next start; see
|
||||
// SHAMapStore_test.)
|
||||
{
|
||||
auto const result = unpin(1, 100);
|
||||
BEAST_EXPECT(result[jss::status] == jss::success);
|
||||
BEAST_EXPECT(result[jss::unpinned] == "7-8");
|
||||
BEAST_EXPECT(result[jss::complete_ledgers_pinned] == "empty");
|
||||
BEAST_EXPECT(lm.getPinnedLedgersRangeSet().empty());
|
||||
}
|
||||
|
||||
// Re-loading the file pins it again
|
||||
{
|
||||
Json::Value params{Json::objectValue};
|
||||
params[jss::input_file] = cataloguePath;
|
||||
auto const result =
|
||||
env.client().invoke("catalogue_load", params)[jss::result];
|
||||
BEAST_EXPECT(result[jss::status] == jss::success);
|
||||
BEAST_EXPECT(to_string(lm.getPinnedLedgersRangeSet()) == "3-8");
|
||||
}
|
||||
|
||||
boost::filesystem::remove_all(tempDir);
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
@@ -1716,10 +1081,6 @@ public:
|
||||
testCatalogueFileSize(all);
|
||||
testCatalogueCompression(all);
|
||||
testCatalogueStatus(all);
|
||||
testCatalogueChainLinkage(all);
|
||||
testCatalogueUnpin(all);
|
||||
testCatalogueLoadFailures(all);
|
||||
testCataloguePinnedStoreRequired(all);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1934,56 +1934,6 @@ static RPCCallTestData const rpcCallTestArray[] = {
|
||||
RPCCallTestData::bad_cast,
|
||||
R"()"},
|
||||
|
||||
// catalogue_unpin
|
||||
// ------------------------------------------------------------
|
||||
{"catalogue_unpin: minimal.",
|
||||
__LINE__,
|
||||
{"catalogue_unpin", "5000001", "6000000"},
|
||||
RPCCallTestData::no_exception,
|
||||
R"({
|
||||
"method" : "catalogue_unpin",
|
||||
"params" : [
|
||||
{
|
||||
"api_version" : %API_VER%,
|
||||
"ledger_index_max" : 6000000,
|
||||
"ledger_index_min" : 5000001
|
||||
}
|
||||
]
|
||||
})"},
|
||||
{"catalogue_unpin: too few arguments.",
|
||||
__LINE__,
|
||||
{"catalogue_unpin", "5000001"},
|
||||
RPCCallTestData::no_exception,
|
||||
R"({
|
||||
"method" : "catalogue_unpin",
|
||||
"params" : [
|
||||
{
|
||||
"error" : "badSyntax",
|
||||
"error_code" : 1,
|
||||
"error_message" : "Syntax error."
|
||||
}
|
||||
]
|
||||
})"},
|
||||
{"catalogue_unpin: too many arguments.",
|
||||
__LINE__,
|
||||
{"catalogue_unpin", "1", "2", "3"},
|
||||
RPCCallTestData::no_exception,
|
||||
R"({
|
||||
"method" : "catalogue_unpin",
|
||||
"params" : [
|
||||
{
|
||||
"error" : "badSyntax",
|
||||
"error_code" : 1,
|
||||
"error_message" : "Syntax error."
|
||||
}
|
||||
]
|
||||
})"},
|
||||
{"catalogue_unpin: non-integer bound.",
|
||||
__LINE__,
|
||||
{"catalogue_unpin", "first", "6000000"},
|
||||
RPCCallTestData::bad_cast,
|
||||
R"()"},
|
||||
|
||||
// channel_authorize
|
||||
// -----------------------------------------------------------
|
||||
{"channel_authorize: minimal.",
|
||||
|
||||
@@ -29,9 +29,7 @@
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
namespace tests {
|
||||
@@ -133,7 +131,6 @@ public:
|
||||
testCanonicalizeMergesInnerChildren(journal);
|
||||
testCanonicalizeSharesDuplicateContent(journal);
|
||||
testGetLinkedNode(journal);
|
||||
testUpsertAndFlushToSink(journal);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -287,82 +284,6 @@ public:
|
||||
BEAST_EXPECT(liveLinked > 0);
|
||||
}
|
||||
|
||||
void
|
||||
testUpsertAndFlushToSink(beast::Journal const& journal)
|
||||
{
|
||||
testcase("upsertGiveItem and flushDirty to a sink");
|
||||
|
||||
tests::TestNodeFamily f(journal);
|
||||
SHAMap a{SHAMapType::FREE, f}; // built with upsertGiveItem
|
||||
SHAMap b{SHAMapType::FREE, f}; // reference: addItem/updateGiveItem
|
||||
|
||||
// Distinct first nibbles: a root with three leaf children.
|
||||
constexpr uint256 h1(
|
||||
"092891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
|
||||
constexpr uint256 h2(
|
||||
"436ccbac3347baa1f1e53baeef1f43334da88f1f6d70d963b833afd6dfa289fe");
|
||||
constexpr uint256 h3(
|
||||
"b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8");
|
||||
auto const nm = SHAMapNodeType::tnTRANSACTION_NM;
|
||||
|
||||
// Absent keys are added.
|
||||
BEAST_EXPECT(a.upsertGiveItem(nm, make_shamapitem(h1, IntToVUC(1))));
|
||||
BEAST_EXPECT(a.upsertGiveItem(nm, make_shamapitem(h2, IntToVUC(2))));
|
||||
BEAST_EXPECT(a.upsertGiveItem(nm, make_shamapitem(h3, IntToVUC(3))));
|
||||
// A present key has its data replaced.
|
||||
BEAST_EXPECT(a.upsertGiveItem(nm, make_shamapitem(h2, IntToVUC(20))));
|
||||
// A change of leaf type is refused and leaves the item untouched.
|
||||
BEAST_EXPECT(!a.upsertGiveItem(
|
||||
SHAMapNodeType::tnTRANSACTION_MD,
|
||||
make_shamapitem(h1, IntToVUC(10))));
|
||||
|
||||
BEAST_EXPECT(b.addItem(nm, make_shamapitem(h1, IntToVUC(1))));
|
||||
BEAST_EXPECT(b.addItem(nm, make_shamapitem(h2, IntToVUC(2))));
|
||||
BEAST_EXPECT(b.addItem(nm, make_shamapitem(h3, IntToVUC(3))));
|
||||
BEAST_EXPECT(b.updateGiveItem(nm, make_shamapitem(h2, IntToVUC(20))));
|
||||
|
||||
{
|
||||
auto const v20 = IntToVUC(20);
|
||||
auto const& item = a.peekItem(h2);
|
||||
BEAST_EXPECT(
|
||||
item && item->slice() == Slice(v20.data(), v20.size()));
|
||||
auto const v1 = IntToVUC(1);
|
||||
auto const& item1 = a.peekItem(h1);
|
||||
BEAST_EXPECT(
|
||||
item1 && item1->slice() == Slice(v1.data(), v1.size()));
|
||||
}
|
||||
|
||||
// Flush to a sink. Do not call getHash() on `a` first: it unshares
|
||||
// the map and the flush would then have nothing to write.
|
||||
auto const cacheBefore = f.getTreeNodeCache()->size();
|
||||
std::vector<uint256> written;
|
||||
bool sinkArgsOk = true;
|
||||
SHAMap::FlushSink const sink =
|
||||
[&](NodeObjectType t, Blob&& data, uint256 const& hash) {
|
||||
sinkArgsOk =
|
||||
sinkArgsOk && t == hotTRANSACTION_NODE && !data.empty();
|
||||
written.push_back(hash);
|
||||
};
|
||||
a.flushDirty(hotTRANSACTION_NODE, sink);
|
||||
|
||||
BEAST_EXPECT(sinkArgsOk);
|
||||
BEAST_EXPECT(written.size() == 4); // root + three leaves
|
||||
// Unlike flushDirty(t), nothing was canonicalized into the cache.
|
||||
BEAST_EXPECT(f.getTreeNodeCache()->size() == cacheBefore);
|
||||
|
||||
// The upserted map matches the reference, and its root was written.
|
||||
BEAST_EXPECT(a.getHash() == b.getHash());
|
||||
BEAST_EXPECT(
|
||||
std::find(
|
||||
written.begin(), written.end(), a.getHash().as_uint256()) !=
|
||||
written.end());
|
||||
|
||||
// Everything is now shared: a second flush writes nothing.
|
||||
written.clear();
|
||||
a.flushDirty(hotTRANSACTION_NODE, sink);
|
||||
BEAST_EXPECT(written.empty());
|
||||
}
|
||||
|
||||
void
|
||||
testGetLinkedNode(beast::Journal const& journal)
|
||||
{
|
||||
|
||||
@@ -501,14 +501,6 @@ 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;
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ bool
|
||||
canHook(
|
||||
STTx const& tx,
|
||||
ripple::uint256 hookOn,
|
||||
std::optional<ripple::Blob> hookName);
|
||||
std::optional<ripple::Slice> hookName);
|
||||
|
||||
bool
|
||||
canEmit(ripple::TxType txType, ripple::uint256 hookCanEmit);
|
||||
|
||||
@@ -3,13 +3,9 @@
|
||||
#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>
|
||||
|
||||
@@ -1995,43 +1991,6 @@ 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,8 +340,7 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
|
||||
case ttCHECK_CREATE:
|
||||
case ttACCOUNT_DELETE:
|
||||
case ttPAYCHAN_CREATE:
|
||||
case ttINVOKE:
|
||||
case ttHISTORY_PROOF: {
|
||||
case ttINVOKE: {
|
||||
if (destAcc)
|
||||
ADD_TSH(*destAcc, tshSTRONG);
|
||||
break;
|
||||
@@ -815,7 +814,7 @@ bool
|
||||
hook::canHook(
|
||||
STTx const& tx,
|
||||
ripple::uint256 hookOn,
|
||||
std::optional<ripple::Blob> hookName)
|
||||
std::optional<ripple::Slice> hookName)
|
||||
{
|
||||
if (!canHookTT(tx.getTxnType(), hookOn))
|
||||
return false;
|
||||
@@ -829,7 +828,7 @@ hook::canHook(
|
||||
// transaction, so we can't hook without the hook name
|
||||
return false;
|
||||
|
||||
return tx.getFieldVL(sfHookName) == *hookName;
|
||||
return tx[sfHookName] == *hookName;
|
||||
}
|
||||
|
||||
ripple::uint256
|
||||
@@ -2927,39 +2926,6 @@ 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,
|
||||
|
||||
@@ -25,8 +25,7 @@ namespace ripple {
|
||||
|
||||
AcceptedLedger::AcceptedLedger(
|
||||
std::shared_ptr<ReadView const> const& ledger,
|
||||
Application& app,
|
||||
bool withJson)
|
||||
Application& app)
|
||||
: mLedger(ledger)
|
||||
{
|
||||
transactions_.reserve(256);
|
||||
@@ -34,7 +33,7 @@ AcceptedLedger::AcceptedLedger(
|
||||
auto insertAll = [&](auto const& txns) {
|
||||
for (auto const& item : txns)
|
||||
transactions_.emplace_back(std::make_unique<AcceptedLedgerTx>(
|
||||
ledger, item.first, item.second, withJson));
|
||||
ledger, item.first, item.second));
|
||||
};
|
||||
|
||||
transactions_.reserve(256);
|
||||
|
||||
@@ -44,15 +44,9 @@ namespace ripple {
|
||||
class AcceptedLedger : public CountedObject<AcceptedLedger>
|
||||
{
|
||||
public:
|
||||
/** @param withJson build each transaction's publishable JSON. Pass
|
||||
false only for ledgers that will never be published from this
|
||||
object (e.g. pinned catalogue history being saved to SQL);
|
||||
getJson() on such transactions returns a null value.
|
||||
*/
|
||||
AcceptedLedger(
|
||||
std::shared_ptr<ReadView const> const& ledger,
|
||||
Application& app,
|
||||
bool withJson = true);
|
||||
Application& app);
|
||||
|
||||
std::shared_ptr<ReadView const> const&
|
||||
getLedger() const
|
||||
|
||||
@@ -28,8 +28,7 @@ namespace ripple {
|
||||
AcceptedLedgerTx::AcceptedLedgerTx(
|
||||
std::shared_ptr<ReadView const> const& ledger,
|
||||
std::shared_ptr<STTx const> const& txn,
|
||||
std::shared_ptr<STObject const> const& met,
|
||||
bool withJson)
|
||||
std::shared_ptr<STObject const> const& met)
|
||||
: mTxn(txn)
|
||||
, mMeta(txn->getTransactionID(), ledger->seq(), *met)
|
||||
, mAffected(mMeta.getAffectedAccounts())
|
||||
@@ -42,11 +41,6 @@ AcceptedLedgerTx::AcceptedLedgerTx(
|
||||
met->add(s);
|
||||
mRawMeta = std::move(s.modData());
|
||||
|
||||
// The JSON (full tx + meta + hex raw meta per transaction) is only
|
||||
// needed for publishing. Bulk SQL saves of pinned history skip it.
|
||||
if (!withJson)
|
||||
return;
|
||||
|
||||
mJson = Json::objectValue;
|
||||
mJson[jss::transaction] = mTxn->getJson(JsonOptions::none);
|
||||
|
||||
|
||||
@@ -46,8 +46,7 @@ public:
|
||||
AcceptedLedgerTx(
|
||||
std::shared_ptr<ReadView const> const& ledger,
|
||||
std::shared_ptr<STTx const> const&,
|
||||
std::shared_ptr<STObject const> const&,
|
||||
bool withJson = true);
|
||||
std::shared_ptr<STObject const> const&);
|
||||
|
||||
std::shared_ptr<STTx const> const&
|
||||
getTxn() const
|
||||
@@ -89,7 +88,6 @@ public:
|
||||
std::string
|
||||
getEscMeta() const;
|
||||
|
||||
/** Null if constructed with withJson == false. */
|
||||
Json::Value const&
|
||||
getJson() const
|
||||
{
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
#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>
|
||||
@@ -51,7 +50,6 @@
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <boost/optional.hpp>
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -994,71 +992,6 @@ 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,13 +352,6 @@ 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;
|
||||
|
||||
|
||||
@@ -228,19 +228,6 @@ public:
|
||||
isPinned(std::uint32_t seq);
|
||||
void
|
||||
unpinLedger(std::uint32_t seq);
|
||||
|
||||
/** Remove `ranges` from the pinned set.
|
||||
|
||||
@param dropFromComplete of the ledgers actually unpinned, those to
|
||||
also remove from complete_ledgers now (used to roll back a
|
||||
load). Otherwise unpinned ledgers stay complete until online
|
||||
delete releases them.
|
||||
@return the ledgers that were pinned and no longer are.
|
||||
*/
|
||||
RangeSet<std::uint32_t>
|
||||
unpinLedgers(
|
||||
RangeSet<std::uint32_t> const& ranges,
|
||||
RangeSet<std::uint32_t> const& dropFromComplete = {});
|
||||
bool
|
||||
isValidated(ReadView const& ledger);
|
||||
bool
|
||||
|
||||
@@ -64,7 +64,6 @@ buildLedgerImpl(
|
||||
}
|
||||
|
||||
built->updateSkipList();
|
||||
built->updateHistoryTree();
|
||||
{
|
||||
// Write the final version of all modified SHAMap
|
||||
// nodes to the node store to preserve the new LCL
|
||||
@@ -272,13 +271,7 @@ buildLedger(
|
||||
j,
|
||||
[&](OpenView& accum, std::shared_ptr<Ledger> const& built) {
|
||||
for (auto& tx : replayData.orderedTxns())
|
||||
applyTransaction(
|
||||
app,
|
||||
accum,
|
||||
*tx.second,
|
||||
false,
|
||||
applyFlags | tapHISTORY_REPLAY,
|
||||
j);
|
||||
applyTransaction(app, accum, *tx.second, false, applyFlags, j);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -501,9 +501,7 @@ LedgerMaster::storeLedger(std::shared_ptr<Ledger const> ledger, bool pin)
|
||||
std::lock_guard sl(mPinnedLock);
|
||||
mPinnedLedgers.insert(range(seq, seq));
|
||||
}
|
||||
// catalogue_load pins every ledger it imports; at info this would
|
||||
// emit one line per ledger for the whole file.
|
||||
JLOG(m_journal.debug()) << "Pinned ledger : " << seq;
|
||||
JLOG(m_journal.info()) << "Pinned ledger : " << seq;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -588,19 +586,6 @@ LedgerMaster::unpinLedger(std::uint32_t seq)
|
||||
mPinnedLedgers.erase(range(seq, seq));
|
||||
}
|
||||
|
||||
RangeSet<std::uint32_t>
|
||||
LedgerMaster::unpinLedgers(
|
||||
RangeSet<std::uint32_t> const& ranges,
|
||||
RangeSet<std::uint32_t> const& dropFromComplete)
|
||||
{
|
||||
std::scoped_lock lock(mCompleteLock, mPinnedLock);
|
||||
RangeSet<std::uint32_t> removed = mPinnedLedgers & ranges;
|
||||
mPinnedLedgers -= removed;
|
||||
if (!dropFromComplete.empty())
|
||||
mCompleteLedgers -= (removed & dropFromComplete);
|
||||
return removed;
|
||||
}
|
||||
|
||||
bool
|
||||
LedgerMaster::isValidated(ReadView const& ledger)
|
||||
{
|
||||
|
||||
@@ -1260,29 +1260,13 @@ 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"),
|
||||
ignored);
|
||||
logs_->journal("Amendments"));
|
||||
}
|
||||
|
||||
Pathfinder::initPathTable();
|
||||
|
||||
@@ -211,8 +211,7 @@ make_AmendmentTable(
|
||||
std::vector<AmendmentTable::FeatureInfo> const& supported,
|
||||
Section const& enabled,
|
||||
Section const& vetoed,
|
||||
beast::Journal journal,
|
||||
Section const& ignored = Section{});
|
||||
beast::Journal journal);
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
|
||||
@@ -381,9 +381,7 @@ void
|
||||
SHAMapStoreImp::loadPinnedRanges()
|
||||
{
|
||||
auto rangesStr = state_db_.getPinnedRanges();
|
||||
// "empty" is what to_string() renders for an empty set; accept it in
|
||||
// case it was stored before setPinnedRanges() learned to write "".
|
||||
if (rangesStr.empty() || rangesStr == "empty")
|
||||
if (rangesStr.empty())
|
||||
return;
|
||||
|
||||
auto const& nscfg = app_.config().section(ConfigSection::nodeDatabase());
|
||||
@@ -413,62 +411,22 @@ SHAMapStoreImp::loadPinnedRanges()
|
||||
static_cast<bool>(app_.getNodeStore().fetchNodeObject(hash, seq));
|
||||
};
|
||||
|
||||
//
|
||||
// catalogue_load records ranges at checkpoints, after handing the pinned
|
||||
// backend every node of the recorded ledgers, but not every backend can
|
||||
// make those writes durable on demand (NuDB commits on its own thread).
|
||||
// A crash can therefore leave the tail of an interval recorded but not
|
||||
// on disk. Each ledger's nodes reach the backend before its header, and
|
||||
// ledgers in sequence, so what survives is a prefix: trim the upper bound
|
||||
// back to the last ledger whose header is present instead of refusing to
|
||||
// start. A missing lower bound still means the configured pinned store
|
||||
// is not the one these ranges were loaded into.
|
||||
constexpr std::uint32_t maxTailTrim = 65536;
|
||||
|
||||
RangeSet<std::uint32_t> verified;
|
||||
for (auto const& interval : persistedRanges)
|
||||
{
|
||||
auto const lower = interval.lower();
|
||||
auto upper = interval.upper();
|
||||
|
||||
auto const notPresent = [&]() {
|
||||
return std::runtime_error(
|
||||
"Persisted pinned interval " + std::to_string(lower) + "-" +
|
||||
if (!hasLedgerData(interval.lower()) ||
|
||||
!hasLedgerData(interval.upper()))
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
"Persisted pinned interval " +
|
||||
std::to_string(interval.lower()) + "-" +
|
||||
std::to_string(interval.upper()) +
|
||||
" is not present in the currently configured pinned store");
|
||||
};
|
||||
|
||||
if (!hasLedgerData(lower))
|
||||
Throw<std::runtime_error>(notPresent());
|
||||
|
||||
std::uint32_t trimmed = 0;
|
||||
while (upper > lower && !hasLedgerData(upper))
|
||||
{
|
||||
if (++trimmed > maxTailTrim)
|
||||
Throw<std::runtime_error>(notPresent());
|
||||
--upper;
|
||||
}
|
||||
|
||||
if (trimmed)
|
||||
{
|
||||
JLOG(journal_.warn())
|
||||
<< "Pinned interval " << lower << "-" << interval.upper()
|
||||
<< " was recorded but its last " << trimmed
|
||||
<< " ledger(s) are not in the pinned store (likely an "
|
||||
"interrupted catalogue_load); trimmed to "
|
||||
<< lower << "-" << upper
|
||||
<< ". Re-run catalogue_load to restore them.";
|
||||
}
|
||||
|
||||
verified.insert(range(lower, upper));
|
||||
}
|
||||
|
||||
if (verified != persistedRanges)
|
||||
state_db_.setPinnedRanges(to_string(verified));
|
||||
|
||||
JLOG(journal_.info()) << "Loaded pinned ranges from database: "
|
||||
<< to_string(verified);
|
||||
app_.getLedgerMaster().setPinnedLedgersRangeSet(verified);
|
||||
<< rangesStr;
|
||||
app_.getLedgerMaster().setPinnedLedgersRangeSet(persistedRanges);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -184,11 +184,9 @@ public:
|
||||
void
|
||||
setPinnedRanges(RangeSet<std::uint32_t> const& ranges) override
|
||||
{
|
||||
// Save pinned ranges to database for persistence. to_string()
|
||||
// renders an empty set as "empty", which from_string() rejects, so
|
||||
// store "" instead: loadPinnedRanges() treats that as nothing pinned.
|
||||
state_db_.setPinnedRanges(
|
||||
ranges.empty() ? std::string{} : to_string(ranges));
|
||||
// Save pinned ranges to database for persistence
|
||||
std::string rangesStr = to_string(ranges);
|
||||
state_db_.setPinnedRanges(rangesStr);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -302,15 +302,6 @@ 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;
|
||||
|
||||
@@ -485,8 +476,7 @@ public:
|
||||
std::vector<FeatureInfo> const& supported,
|
||||
Section const& enabled,
|
||||
Section const& vetoed,
|
||||
beast::Journal journal,
|
||||
Section const& ignored);
|
||||
beast::Journal journal);
|
||||
|
||||
uint256
|
||||
find(std::string const& name) const override;
|
||||
@@ -553,8 +543,7 @@ AmendmentTableImpl::AmendmentTableImpl(
|
||||
std::vector<FeatureInfo> const& supported,
|
||||
Section const& enabled,
|
||||
Section const& vetoed,
|
||||
beast::Journal journal,
|
||||
Section const& ignored)
|
||||
beast::Journal journal)
|
||||
: lastUpdateSeq_(0)
|
||||
, majorityTime_(majorityTime)
|
||||
, unsupportedEnabled_(false)
|
||||
@@ -597,59 +586,6 @@ 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))
|
||||
@@ -839,12 +775,7 @@ AmendmentTableImpl::enable(uint256 const& amendment)
|
||||
|
||||
s.enabled = true;
|
||||
|
||||
if (s.ignored)
|
||||
{
|
||||
JLOG(j_.warn()) << "Ignored unsupported amendment " << amendment << " ("
|
||||
<< s.name << ") is enabled on the ledger.";
|
||||
}
|
||||
else if (!s.supported)
|
||||
if (!s.supported)
|
||||
{
|
||||
JLOG(j_.error()) << "Unsupported amendment " << amendment
|
||||
<< " activated.";
|
||||
@@ -1113,9 +1044,7 @@ AmendmentTableImpl::injectJson(
|
||||
if (!fs.name.empty())
|
||||
v[jss::name] = fs.name;
|
||||
|
||||
v[jss::supported] = fs.supported && !fs.ignored;
|
||||
if (fs.ignored)
|
||||
v["ignored"] = true;
|
||||
v[jss::supported] = fs.supported;
|
||||
if (!fs.enabled && isAdmin)
|
||||
{
|
||||
if (fs.vote == AmendmentVote::obsolete)
|
||||
@@ -1184,11 +1113,10 @@ make_AmendmentTable(
|
||||
std::vector<AmendmentTable::FeatureInfo> const& supported,
|
||||
Section const& enabled,
|
||||
Section const& vetoed,
|
||||
beast::Journal journal,
|
||||
Section const& ignored)
|
||||
beast::Journal journal)
|
||||
{
|
||||
return std::make_unique<AmendmentTableImpl>(
|
||||
app, majorityTime, supported, enabled, vetoed, journal, ignored);
|
||||
app, majorityTime, supported, enabled, vetoed, journal);
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -258,50 +258,42 @@ saveValidatedLedger(
|
||||
ledger->info().txHash == ledger->txMap().getHash().as_uint256(),
|
||||
"ripple::detail::saveValidatedLedger : transaction hash match");
|
||||
|
||||
bool const pinned = app.getLedgerMaster().isPinned(seq);
|
||||
|
||||
// Save the ledger header in the hashed object store
|
||||
{
|
||||
Serializer s(128);
|
||||
s.add32(HashPrefix::ledgerMaster);
|
||||
addRaw(ledger->info(), s);
|
||||
// Use pinnedLEDGER type for pinned ledgers, hotLEDGER for others
|
||||
auto ledgerType = pinned ? pinnedLEDGER : hotLEDGER;
|
||||
auto ledgerType =
|
||||
app.getLedgerMaster().isPinned(seq) ? pinnedLEDGER : hotLEDGER;
|
||||
app.getNodeStore().store(
|
||||
ledgerType, std::move(s.modData()), ledger->info().hash, seq);
|
||||
}
|
||||
|
||||
// Pinned (catalogue) ledgers are saved in bulk: they are never cached
|
||||
// or published from here, so their AcceptedLedger skips the per-tx JSON
|
||||
// and is not built at all when there are no tx tables to fill.
|
||||
std::shared_ptr<AcceptedLedger> aLedger;
|
||||
if (!pinned || app.config().useTxTables())
|
||||
try
|
||||
{
|
||||
try
|
||||
aLedger = app.getAcceptedLedgerCache().fetch(ledger->info().hash);
|
||||
if (!aLedger)
|
||||
{
|
||||
aLedger = app.getAcceptedLedgerCache().fetch(ledger->info().hash);
|
||||
if (!aLedger)
|
||||
{
|
||||
aLedger = std::make_shared<AcceptedLedger>(
|
||||
ledger, app, /*withJson=*/!pinned);
|
||||
aLedger = std::make_shared<AcceptedLedger>(ledger, app);
|
||||
|
||||
// Only cache if the ledger is NOT in the pinned range
|
||||
if (!pinned)
|
||||
{
|
||||
app.getAcceptedLedgerCache().canonicalize_replace_client(
|
||||
ledger->info().hash, aLedger);
|
||||
}
|
||||
// Only cache if the ledger is NOT in the pinned range
|
||||
if (!app.getLedgerMaster().isPinned(ledger->info().seq))
|
||||
{
|
||||
app.getAcceptedLedgerCache().canonicalize_replace_client(
|
||||
ledger->info().hash, aLedger);
|
||||
}
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
JLOG(j.warn()) << "An accepted ledger was missing nodes";
|
||||
app.getLedgerMaster().failedSave(seq, ledger->info().hash);
|
||||
// Clients can now trust the database for information about this
|
||||
// ledger sequence.
|
||||
app.pendingSaves().finishWork(seq);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
JLOG(j.warn()) << "An accepted ledger was missing nodes";
|
||||
app.getLedgerMaster().failedSave(seq, ledger->info().hash);
|
||||
// Clients can now trust the database for information about this
|
||||
// ledger sequence.
|
||||
app.pendingSaves().finishWork(seq);
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -158,9 +158,8 @@ ClaimReward::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
auto const& hookOn =
|
||||
hook::getHookOn(hook, sleDef, sfHookOnIncoming);
|
||||
auto const hookName = ctx.view.rules().enabled(fix20260929) &&
|
||||
hook.isFieldPresent(sfHookName)
|
||||
? std::optional<Blob>(hook.getFieldVL(sfHookName))
|
||||
auto const hookName = ctx.view.rules().enabled(fix20260929)
|
||||
? hook[~sfHookName]
|
||||
: std::nullopt;
|
||||
if (hook::canHook(ctx.tx, hookOn, hookName))
|
||||
{
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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
|
||||
@@ -1,66 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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,7 +605,6 @@ LedgerEntryTypesMatch::visitEntry(
|
||||
case ltIMPORT_VLSEQ:
|
||||
case ltUNL_REPORT:
|
||||
case ltRANDOM:
|
||||
case ltHISTORY_TREE:
|
||||
case ltAMM:
|
||||
case ltBRIDGE:
|
||||
case ltXCHAIN_OWNED_CLAIM_ID:
|
||||
|
||||
@@ -1740,15 +1740,7 @@ SetHook::setHook()
|
||||
{
|
||||
if (newHookName->size() == 0)
|
||||
{
|
||||
// Erase the field outright rather than
|
||||
// makeFieldAbsent: newHook carries no template, so
|
||||
// makeFieldAbsent leaves an STI_NOTPRESENT
|
||||
// placeholder in the in-memory entry that every
|
||||
// reader must then special-case, and it throws
|
||||
// (-> tefEXCEPTION) when the hook has no name to
|
||||
// remove. delField matches the serialized form of
|
||||
// the entry and is a no-op on a nameless hook.
|
||||
newHook.delField(sfHookName);
|
||||
newHook.makeFieldAbsent(sfHookName);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -287,9 +287,7 @@ Transactor::calculateHookChainFee(
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
std::optional<Blob> const hookName = hookObj.isFieldPresent(sfHookName)
|
||||
? std::optional<Blob>(hookObj.getFieldVL(sfHookName))
|
||||
: std::nullopt;
|
||||
auto const hookName = hookObj[~sfHookName];
|
||||
|
||||
uint32_t flags = 0;
|
||||
if (hookObj.isFieldPresent(sfFlags))
|
||||
@@ -1411,9 +1409,7 @@ Transactor::executeHookChain(
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
std::optional<Blob> const hookName = hookObj.isFieldPresent(sfHookName)
|
||||
? std::optional<Blob>(hookObj.getFieldVL(sfHookName))
|
||||
: std::nullopt;
|
||||
auto const hookName = hookObj[~sfHookName];
|
||||
|
||||
// check if the hook can fire
|
||||
uint256 hookOn = hook::getHookOn(
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
#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>
|
||||
|
||||
@@ -102,9 +102,6 @@ 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
|
||||
|
||||
@@ -405,11 +405,7 @@ Config::setup(
|
||||
|
||||
HTTPClient::initializeSSLContext(*this, j_);
|
||||
|
||||
// Standalone never fetches history, so ledger_history is normally
|
||||
// meaningless there. The rwdb null node store (nullBackend) uses it
|
||||
// to size the in-memory retained-ledger window, and SHAMapStoreImp
|
||||
// rejects zero for it, so keep the configured value in that case.
|
||||
if (RUN_STANDALONE && !nullBackend())
|
||||
if (RUN_STANDALONE)
|
||||
LEDGER_HISTORY = 0;
|
||||
|
||||
std::string ledgerTxDbType;
|
||||
|
||||
@@ -46,12 +46,7 @@ enum ApplyFlags : std::uint32_t {
|
||||
|
||||
// Transaction shouldn't be applied
|
||||
// Signatures shouldn't be checked
|
||||
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
|
||||
tapDRY_RUN = 0x1000
|
||||
};
|
||||
|
||||
constexpr ApplyFlags
|
||||
|
||||
@@ -313,9 +313,8 @@ 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.addStorable(*sTx);
|
||||
tx.add(*sTx);
|
||||
std::shared_ptr<Serializer> sMeta;
|
||||
std::optional<TxMeta> metadata;
|
||||
if (!to.open() || isDryRun)
|
||||
|
||||
@@ -851,22 +851,6 @@ private:
|
||||
return jvRequest;
|
||||
}
|
||||
|
||||
// catalogue_unpin <ledger_index_min> <ledger_index_max>
|
||||
Json::Value
|
||||
parseCatalogueUnpin(Json::Value const& jvParams)
|
||||
{
|
||||
Json::Value jvRequest(Json::objectValue);
|
||||
|
||||
if (jvParams.size() >= 2)
|
||||
{
|
||||
// asUInt() throws on non-numeric input rather than yielding 0
|
||||
jvRequest[jss::ledger_index_min] = jvParams[0u].asUInt();
|
||||
jvRequest[jss::ledger_index_max] = jvParams[1u].asUInt();
|
||||
}
|
||||
|
||||
return jvRequest;
|
||||
}
|
||||
|
||||
// catalogue_status - no parameters required
|
||||
Json::Value
|
||||
parseCatalogueStatus(Json::Value const& jvParams)
|
||||
@@ -1486,7 +1470,6 @@ public:
|
||||
{"catalogue_create", &RPCParser::parseCatalogueCreate, 3, 4},
|
||||
{"catalogue_load", &RPCParser::parseCatalogueLoad, 1, 2},
|
||||
{"catalogue_status", &RPCParser::parseCatalogueStatus, 0, 0},
|
||||
{"catalogue_unpin", &RPCParser::parseCatalogueUnpin, 2, 2},
|
||||
{"channel_authorize", &RPCParser::parseChannelAuthorize, 3, 4},
|
||||
{"channel_verify", &RPCParser::parseChannelVerify, 4, 4},
|
||||
{"connect", &RPCParser::parseConnect, 1, 2},
|
||||
|
||||
@@ -121,22 +121,6 @@ public:
|
||||
virtual void
|
||||
sync() = 0;
|
||||
|
||||
/** Make every object previously passed to store() or storeBatch()
|
||||
durable before returning.
|
||||
|
||||
@return true if, on return, those writes are as durable as this
|
||||
backend ever makes them (trivially so for in-memory
|
||||
backends); false if the backend cannot do this on demand and
|
||||
they become durable later on its own schedule (e.g. NuDB's
|
||||
commit thread).
|
||||
*/
|
||||
virtual bool
|
||||
syncDurable()
|
||||
{
|
||||
sync();
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Visit every object in the database
|
||||
This is usually called during import.
|
||||
@note This routine will not be called concurrently with itself
|
||||
|
||||
@@ -186,13 +186,6 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
bool
|
||||
syncDurable() override
|
||||
{
|
||||
// Nothing here outlives the process, so there is nothing to wait for.
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
for_each(std::function<void(std::shared_ptr<NodeObject>)> f) override
|
||||
{
|
||||
|
||||
@@ -291,9 +291,6 @@ public:
|
||||
void
|
||||
sync() override
|
||||
{
|
||||
// NuDB commits on its context thread (about once a second) and
|
||||
// offers no public barrier while that thread runs, so this cannot
|
||||
// force durability; syncDurable() therefore reports false.
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -81,13 +81,6 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
bool
|
||||
syncDurable() override
|
||||
{
|
||||
// Nothing here outlives the process, so there is nothing to wait for.
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
for_each(std::function<void(std::shared_ptr<NodeObject>)> f) override
|
||||
{
|
||||
|
||||
@@ -164,13 +164,6 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
bool
|
||||
syncDurable() override
|
||||
{
|
||||
// Nothing here outlives the process, so there is nothing to wait for.
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
for_each(std::function<void(std::shared_ptr<NodeObject>)> f) override
|
||||
{
|
||||
|
||||
@@ -383,33 +383,6 @@ public:
|
||||
void
|
||||
sync() override
|
||||
{
|
||||
(void)syncDurable();
|
||||
}
|
||||
|
||||
bool
|
||||
syncDurable() override
|
||||
{
|
||||
// Drain anything queued through store() into RocksDB, then flush
|
||||
// and fsync the WAL so every write so far survives a crash.
|
||||
m_batch.waitForWriting();
|
||||
if (!m_db)
|
||||
return false;
|
||||
|
||||
auto const ret = m_db->FlushWAL(/*sync=*/true);
|
||||
if (ret.ok())
|
||||
return true;
|
||||
|
||||
// Some user-supplied options (e.g. allow_mmap_writes) make WAL sync
|
||||
// unavailable. Report "not durable on demand" rather than failing:
|
||||
// callers then fall back to the non-durable checkpoint path.
|
||||
if (ret.IsNotSupported())
|
||||
{
|
||||
JLOG(m_journal.warn())
|
||||
<< "RocksDB cannot sync its WAL on demand: " << ret.ToString();
|
||||
return false;
|
||||
}
|
||||
|
||||
Throw<std::runtime_error>("sync failed: " + ret.ToString());
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -74,16 +74,13 @@ public:
|
||||
int
|
||||
getWriteLoad();
|
||||
|
||||
/** Block until every object passed to store() so far has been handed
|
||||
to the callback's writeBatch(). */
|
||||
void
|
||||
waitForWriting();
|
||||
|
||||
private:
|
||||
void
|
||||
performScheduledTask() override;
|
||||
void
|
||||
writeBatch();
|
||||
void
|
||||
waitForWriting();
|
||||
|
||||
private:
|
||||
using LockType = std::recursive_mutex;
|
||||
|
||||
@@ -91,36 +91,6 @@ DatabasePinnedImp::store(
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
DatabasePinnedImp::storePinnedBatch(Batch const& batch)
|
||||
{
|
||||
if (batch.empty())
|
||||
return;
|
||||
|
||||
std::uint64_t bytes = 0;
|
||||
for (auto const& obj : batch)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
obj && !isPinnedType(obj->getType()),
|
||||
"ripple::NodeStore::DatabasePinnedImp::storePinnedBatch : "
|
||||
"serializable type");
|
||||
bytes += obj->getData().size();
|
||||
}
|
||||
|
||||
// The persistent backends allowed for pinned_type (NuDB, RocksDB, and
|
||||
// rwdb/memory in standalone) are safe to write from several threads,
|
||||
// so this may overlap a BatchWriter flush of single store() calls.
|
||||
persistent_->storeBatch(batch);
|
||||
pinnedStoreCount_ += batch.size();
|
||||
storeStats(batch.size(), bytes);
|
||||
}
|
||||
|
||||
bool
|
||||
DatabasePinnedImp::syncPinned()
|
||||
{
|
||||
return persistent_->syncDurable();
|
||||
}
|
||||
|
||||
std::shared_ptr<NodeObject>
|
||||
DatabasePinnedImp::fetchNodeObject(
|
||||
uint256 const& hash,
|
||||
@@ -142,11 +112,7 @@ DatabasePinnedImp::fetchNodeObject(
|
||||
{
|
||||
// Try persistent backend first (pinned data)
|
||||
if (auto obj = tryPersistent(hash, fetchReport))
|
||||
{
|
||||
if (duplicate)
|
||||
copyToRotating(*obj, hash, ledgerSeq);
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Fallback: try rotating backends
|
||||
return rotating_.fetchNodeObject(
|
||||
@@ -164,30 +130,7 @@ DatabasePinnedImp::fetchNodeObject(
|
||||
return obj;
|
||||
|
||||
// Fallback: try persistent backend (pinned data)
|
||||
auto obj = tryPersistent(hash, fetchReport);
|
||||
|
||||
// `duplicate` is set by online_delete's copy of the current validated
|
||||
// state into the new writable backend. A hot ledger can share nodes
|
||||
// that exist only in persistent storage (e.g. after catalogue_load
|
||||
// switched the LCL to its last ledger); copy those too, so the hot
|
||||
// store becomes self-contained after one rotation and pinned history
|
||||
// can be unpinned and its store discarded without breaking live state.
|
||||
// The pinned-first path above does the same, so the copy happens
|
||||
// whichever backend is tried first.
|
||||
if (obj && duplicate)
|
||||
copyToRotating(*obj, hash, ledgerSeq);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
void
|
||||
DatabasePinnedImp::copyToRotating(
|
||||
NodeObject const& obj,
|
||||
uint256 const& hash,
|
||||
std::uint32_t ledgerSeq)
|
||||
{
|
||||
Blob data(obj.getData());
|
||||
rotating_.store(obj.getType(), std::move(data), hash, ledgerSeq);
|
||||
return tryPersistent(hash, fetchReport);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -319,8 +262,8 @@ DatabasePinnedImp::tryPersistent(uint256 const& hash, FetchReport& fetchReport)
|
||||
if (status == ok && nodeObject)
|
||||
{
|
||||
fetchReport.wasFound = true;
|
||||
// Copying into rotating storage for duplicate=true is handled by
|
||||
// the caller (see fetchNodeObject's default path).
|
||||
// Note: We do NOT copy pinned data to rotating storage even if
|
||||
// duplicate=true. Pinned data stays in persistent storage.
|
||||
}
|
||||
return nodeObject;
|
||||
}
|
||||
|
||||
@@ -121,20 +121,6 @@ public:
|
||||
std::uint32_t ledgerSeq) override;
|
||||
void
|
||||
sync() override;
|
||||
|
||||
/** Write node objects of pinned history straight to the persistent
|
||||
backend in one batch. Objects must carry hot (serializable) types.
|
||||
Used by catalogue_load, which builds whole batches per ledger.
|
||||
*/
|
||||
void
|
||||
storePinnedBatch(Batch const& batch);
|
||||
|
||||
/** Make everything written to the persistent backend durable.
|
||||
@return false if the backend cannot guarantee that on demand.
|
||||
*/
|
||||
bool
|
||||
syncPinned();
|
||||
|
||||
bool
|
||||
storeLedger(std::shared_ptr<Ledger const> const& srcLedger) override;
|
||||
void
|
||||
@@ -155,14 +141,6 @@ private:
|
||||
bool
|
||||
likelyPinned(std::uint32_t ledgerSeq) const;
|
||||
|
||||
/** Copy an object found only in persistent storage into the writable
|
||||
rotating backend (the `duplicate` path of fetchNodeObject). */
|
||||
void
|
||||
copyToRotating(
|
||||
NodeObject const& obj,
|
||||
uint256 const& hash,
|
||||
std::uint32_t ledgerSeq);
|
||||
|
||||
std::shared_ptr<NodeObject>
|
||||
tryPersistent(uint256 const& hash, FetchReport& fetchReport);
|
||||
|
||||
|
||||
@@ -213,19 +213,13 @@ deserializeSHAMapFromStream(
|
||||
CatalogueInputStream& stream,
|
||||
SHAMapNodeType nodeType,
|
||||
NodeObjectType flushType,
|
||||
SHAMap::FlushSink const* sink,
|
||||
bool allowRemoval,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Reused across leaves: resize() keeps capacity, so steady-state
|
||||
// parsing does no per-leaf allocation beyond the SHAMapItem itself.
|
||||
std::vector<std::uint8_t> data;
|
||||
|
||||
auto deserializeLeaf =
|
||||
[&shaMap, &stream, &j, &data, nodeType, allowRemoval](
|
||||
SHAMapNodeType& parsedType) -> bool {
|
||||
auto deserializeLeaf = [&shaMap, &stream, &j, nodeType, allowRemoval](
|
||||
SHAMapNodeType& parsedType) -> bool {
|
||||
stream.read(reinterpret_cast<char*>(&parsedType), 1);
|
||||
|
||||
if (parsedType == SHAMapNodeType::tnTERMINAL)
|
||||
@@ -254,8 +248,7 @@ deserializeSHAMapFromStream(
|
||||
return false;
|
||||
}
|
||||
|
||||
// delItem walks once and reports absence itself.
|
||||
if (!shaMap.delItem(key))
|
||||
if (!shaMap.hasItem(key))
|
||||
{
|
||||
JLOG(j.error())
|
||||
<< "Deserialization: removal of key " << to_string(key)
|
||||
@@ -263,6 +256,7 @@ deserializeSHAMapFromStream(
|
||||
return false;
|
||||
}
|
||||
|
||||
shaMap.delItem(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -285,7 +279,7 @@ deserializeSHAMapFromStream(
|
||||
return false;
|
||||
}
|
||||
|
||||
data.resize(size);
|
||||
std::vector<std::uint8_t> data(size);
|
||||
stream.read(reinterpret_cast<char*>(data.data()), size);
|
||||
|
||||
if (stream.fail())
|
||||
@@ -296,8 +290,11 @@ deserializeSHAMapFromStream(
|
||||
return false;
|
||||
}
|
||||
|
||||
auto item = make_shamapitem(key, Slice(data.data(), size));
|
||||
return shaMap.upsertGiveItem(nodeType, std::move(item));
|
||||
auto item = make_shamapitem(key, makeSlice(data));
|
||||
if (shaMap.hasItem(key))
|
||||
return shaMap.updateGiveItem(nodeType, std::move(item));
|
||||
|
||||
return shaMap.addGiveItem(nodeType, std::move(item));
|
||||
};
|
||||
|
||||
auto lastParsed = SHAMapNodeType::tnREMOVE;
|
||||
@@ -311,10 +308,7 @@ deserializeSHAMapFromStream(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sink)
|
||||
shaMap.flushDirty(flushType, *sink);
|
||||
else
|
||||
shaMap.flushDirty(flushType);
|
||||
shaMap.flushDirty(flushType);
|
||||
return true;
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
@@ -351,13 +345,7 @@ deserializeStateMapFromStream(
|
||||
beast::Journal const& j)
|
||||
{
|
||||
return deserializeSHAMapFromStream(
|
||||
stateMap,
|
||||
stream,
|
||||
SHAMapNodeType::tnACCOUNT_STATE,
|
||||
flushType,
|
||||
nullptr,
|
||||
true,
|
||||
j);
|
||||
stateMap, stream, SHAMapNodeType::tnACCOUNT_STATE, flushType, true, j);
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -368,47 +356,7 @@ deserializeTxMapFromStream(
|
||||
beast::Journal const& j)
|
||||
{
|
||||
return deserializeSHAMapFromStream(
|
||||
txMap,
|
||||
stream,
|
||||
SHAMapNodeType::tnTRANSACTION_MD,
|
||||
flushType,
|
||||
nullptr,
|
||||
false,
|
||||
j);
|
||||
}
|
||||
|
||||
bool
|
||||
deserializeStateMapFromStream(
|
||||
SHAMap& stateMap,
|
||||
CatalogueInputStream& stream,
|
||||
SHAMap::FlushSink const& sink,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
return deserializeSHAMapFromStream(
|
||||
stateMap,
|
||||
stream,
|
||||
SHAMapNodeType::tnACCOUNT_STATE,
|
||||
hotACCOUNT_NODE,
|
||||
&sink,
|
||||
true,
|
||||
j);
|
||||
}
|
||||
|
||||
bool
|
||||
deserializeTxMapFromStream(
|
||||
SHAMap& txMap,
|
||||
CatalogueInputStream& stream,
|
||||
SHAMap::FlushSink const& sink,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
return deserializeSHAMapFromStream(
|
||||
txMap,
|
||||
stream,
|
||||
SHAMapNodeType::tnTRANSACTION_MD,
|
||||
hotTRANSACTION_NODE,
|
||||
&sink,
|
||||
false,
|
||||
j);
|
||||
txMap, stream, SHAMapNodeType::tnTRANSACTION_MD, flushType, false, j);
|
||||
}
|
||||
|
||||
} // namespace RPC
|
||||
|
||||
@@ -59,24 +59,6 @@ deserializeTxMapFromStream(
|
||||
NodeObjectType flushType,
|
||||
beast::Journal const& j);
|
||||
|
||||
/** As above, but dirty nodes are flushed to `sink` (tagged with
|
||||
hotACCOUNT_NODE / hotTRANSACTION_NODE) instead of the nodestore, and are
|
||||
not canonicalized into the TreeNodeCache. See SHAMap::flushDirty(t, sink).
|
||||
*/
|
||||
bool
|
||||
deserializeStateMapFromStream(
|
||||
SHAMap& stateMap,
|
||||
CatalogueInputStream& stream,
|
||||
SHAMap::FlushSink const& sink,
|
||||
beast::Journal const& j);
|
||||
|
||||
bool
|
||||
deserializeTxMapFromStream(
|
||||
SHAMap& txMap,
|
||||
CatalogueInputStream& stream,
|
||||
SHAMap::FlushSink const& sink,
|
||||
beast::Journal const& j);
|
||||
|
||||
} // namespace RPC
|
||||
} // namespace ripple
|
||||
|
||||
|
||||
@@ -199,7 +199,6 @@ Handler const handlerArray[]{
|
||||
{"catalogue_create", byRef(&doCatalogueCreate), Role::ADMIN, NO_CONDITION},
|
||||
{"catalogue_status", byRef(&doCatalogueStatus), Role::ADMIN, NO_CONDITION},
|
||||
{"catalogue_load", byRef(&doCatalogueLoad), Role::ADMIN, NO_CONDITION},
|
||||
{"catalogue_unpin", byRef(&doCatalogueUnpin), Role::ADMIN, NO_CONDITION},
|
||||
};
|
||||
|
||||
class HandlerTable
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -184,8 +184,6 @@ Json::Value
|
||||
doCatalogueStatus(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doCatalogueLoad(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doCatalogueUnpin(RPC::JsonContext&);
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -36,7 +36,6 @@
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <boost/iostreams/filtering_stream.hpp>
|
||||
#include <cassert>
|
||||
#include <functional>
|
||||
#include <stack>
|
||||
#include <vector>
|
||||
|
||||
@@ -213,19 +212,6 @@ public:
|
||||
SHAMapNodeType type,
|
||||
boost::intrusive_ptr<SHAMapItem const> item);
|
||||
|
||||
/** Replace the item with the same key if present, otherwise add it.
|
||||
|
||||
Equivalent to `hasItem(key) ? updateGiveItem(..) : addGiveItem(..)`
|
||||
but walks the tree once when the key already exists.
|
||||
|
||||
@return false if the key exists with a different leaf type, or if
|
||||
the add fails.
|
||||
*/
|
||||
bool
|
||||
upsertGiveItem(
|
||||
SHAMapNodeType type,
|
||||
boost::intrusive_ptr<SHAMapItem const> item);
|
||||
|
||||
// Save a copy if you need to extend the life
|
||||
// of the SHAMapItem beyond this SHAMap
|
||||
boost::intrusive_ptr<SHAMapItem const> const&
|
||||
@@ -381,26 +367,6 @@ public:
|
||||
int
|
||||
flushDirty(NodeObjectType t);
|
||||
|
||||
/** Receives each node written by flushDirty(t, sink): the type passed
|
||||
to flushDirty, the node serialized with its hash prefix, and its
|
||||
hash.
|
||||
*/
|
||||
using FlushSink =
|
||||
std::function<void(NodeObjectType, Blob&&, uint256 const&)>;
|
||||
|
||||
/** Flush modified nodes to `sink` instead of the nodestore and convert
|
||||
them to shared.
|
||||
|
||||
Unlike flushDirty(t), written nodes are NOT canonicalized into the
|
||||
family's TreeNodeCache. Bulk writers of history (catalogue_load)
|
||||
must use this: every cached inner node holds strong references to
|
||||
its children, so canonicalizing pins every superseded node version
|
||||
in memory until the cache sweeps it, which on a fast import grows
|
||||
without practical bound.
|
||||
*/
|
||||
int
|
||||
flushDirty(NodeObjectType t, FlushSink const& sink);
|
||||
|
||||
void
|
||||
walkMap(std::vector<SHAMapMissingNode>& missingNodes, int maxMissing) const;
|
||||
bool
|
||||
@@ -483,13 +449,9 @@ private:
|
||||
std::shared_ptr<Node>
|
||||
preFlushNode(std::shared_ptr<Node> node) const;
|
||||
|
||||
/** write and canonicalize modified node; if `sink` is set, hand the
|
||||
serialized node to it instead and do not canonicalize */
|
||||
/** write and canonicalize modified node */
|
||||
std::shared_ptr<SHAMapTreeNode>
|
||||
writeNode(
|
||||
NodeObjectType t,
|
||||
std::shared_ptr<SHAMapTreeNode> node,
|
||||
FlushSink const* sink = nullptr) const;
|
||||
writeNode(NodeObjectType t, std::shared_ptr<SHAMapTreeNode> node) const;
|
||||
|
||||
// returns the first item at or below this node
|
||||
SHAMapLeafNode*
|
||||
@@ -572,10 +534,7 @@ private:
|
||||
Delta& differences,
|
||||
int& maxCount) const;
|
||||
int
|
||||
walkSubTree(
|
||||
bool doWrite,
|
||||
NodeObjectType t,
|
||||
FlushSink const* sink = nullptr);
|
||||
walkSubTree(bool doWrite, NodeObjectType t);
|
||||
|
||||
// Structure to track information about call to
|
||||
// getMissingNodes while it's in progress
|
||||
|
||||
@@ -1059,42 +1059,6 @@ SHAMap::updateGiveItem(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
SHAMap::upsertGiveItem(
|
||||
SHAMapNodeType type,
|
||||
boost::intrusive_ptr<SHAMapItem const> item)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
state_ != SHAMapState::Immutable,
|
||||
"ripple::SHAMap::upsertGiveItem : not immutable");
|
||||
|
||||
uint256 const tag = item->key();
|
||||
|
||||
SharedPtrNodeStack stack;
|
||||
auto const leaf = walkTowardsKey(tag, &stack);
|
||||
|
||||
if (!leaf || leaf->peekItem()->key() != tag)
|
||||
return addGiveItem(type, std::move(item));
|
||||
|
||||
// From here this mirrors updateGiveItem, reusing the walk above.
|
||||
auto node = std::static_pointer_cast<SHAMapLeafNode>(stack.top().first);
|
||||
auto const nodeID = stack.top().second;
|
||||
stack.pop();
|
||||
|
||||
if (node->getType() != type)
|
||||
{
|
||||
JLOG(journal_.fatal()) << "SHAMap::upsertGiveItem: cross-type change!";
|
||||
return false;
|
||||
}
|
||||
|
||||
node = unshareNode(std::move(node), nodeID);
|
||||
|
||||
if (node->setItem(std::move(item)))
|
||||
dirtyUp(stack, tag, node);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
SHAMap::fetchRoot(SHAMapHash const& hash, SHAMapSyncFilter* filter)
|
||||
{
|
||||
@@ -1144,24 +1108,10 @@ SHAMap::fetchRoot(SHAMapHash const& hash, SHAMapSyncFilter* filter)
|
||||
first call SHAMapTreeNode::unshare().
|
||||
*/
|
||||
std::shared_ptr<SHAMapTreeNode>
|
||||
SHAMap::writeNode(
|
||||
NodeObjectType t,
|
||||
std::shared_ptr<SHAMapTreeNode> node,
|
||||
FlushSink const* sink) const
|
||||
SHAMap::writeNode(NodeObjectType t, std::shared_ptr<SHAMapTreeNode> node) const
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
node->cowid() == 0, "ripple::SHAMap::writeNode : valid input node");
|
||||
|
||||
if (sink)
|
||||
{
|
||||
// Bulk-write path: no TreeNodeCache, no direct store. The caller
|
||||
// owns persistence (see flushDirty(t, sink)).
|
||||
Serializer s;
|
||||
node->serializeWithPrefix(s);
|
||||
(*sink)(t, std::move(s.modData()), node->getHash().as_uint256());
|
||||
return node;
|
||||
}
|
||||
|
||||
XRPL_ASSERT(backed_, "ripple::SHAMap::writeNode : is backed");
|
||||
|
||||
canonicalize(node->getHash(), node);
|
||||
@@ -1209,19 +1159,10 @@ SHAMap::flushDirty(NodeObjectType t)
|
||||
}
|
||||
|
||||
int
|
||||
SHAMap::flushDirty(NodeObjectType t, FlushSink const& sink)
|
||||
{
|
||||
// The sink decides where nodes go, so this writes whether or not the
|
||||
// map is backed.
|
||||
return walkSubTree(true, t, &sink);
|
||||
}
|
||||
|
||||
int
|
||||
SHAMap::walkSubTree(bool doWrite, NodeObjectType t, FlushSink const* sink)
|
||||
SHAMap::walkSubTree(bool doWrite, NodeObjectType t)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
!doWrite || backed_ || sink,
|
||||
"ripple::SHAMap::walkSubTree : valid input");
|
||||
!doWrite || backed_, "ripple::SHAMap::walkSubTree : valid input");
|
||||
|
||||
int flushed = 0;
|
||||
|
||||
@@ -1235,7 +1176,7 @@ SHAMap::walkSubTree(bool doWrite, NodeObjectType t, FlushSink const* sink)
|
||||
root_->unshare();
|
||||
|
||||
if (doWrite)
|
||||
root_ = writeNode(t, std::move(root_), sink);
|
||||
root_ = writeNode(t, std::move(root_));
|
||||
|
||||
return 1;
|
||||
}
|
||||
@@ -1304,7 +1245,7 @@ SHAMap::walkSubTree(bool doWrite, NodeObjectType t, FlushSink const* sink)
|
||||
child->unshare();
|
||||
|
||||
if (doWrite)
|
||||
child = writeNode(t, std::move(child), sink);
|
||||
child = writeNode(t, std::move(child));
|
||||
|
||||
node->shareChild(branch, child);
|
||||
}
|
||||
@@ -1320,7 +1261,7 @@ SHAMap::walkSubTree(bool doWrite, NodeObjectType t, FlushSink const* sink)
|
||||
|
||||
if (doWrite)
|
||||
node = std::static_pointer_cast<SHAMapInnerNode>(
|
||||
writeNode(t, std::move(node), sink));
|
||||
writeNode(t, std::move(node)));
|
||||
|
||||
++flushed;
|
||||
|
||||
|
||||
Reference in new issue
Block a user