Compare commits

..
21 Commits
Author SHA1 Message Date
Richard Holland a88a441305 make 20260929 default yes 2026-10-12 13:37:40 +11:00
Richard Holland 4a9493c05f test coverage 2026-10-12 13:23:21 +11:00
Richard Holland 604ed0ca25 retire old fix 2026-10-12 12:58:37 +11:00
Richard Holland 527a65b449 Merge branch 'dev' into fixes 2026-10-12 11:32:47 +10:00
Richard Holland 68dec561b3 various fixes 2026-10-12 12:30:08 +11:00
Richard Holland 8c66209cd6 flakey test fix 2026-10-12 11:11:46 +11:00
Richard Holland a6cc9fde1d tequ review fixes 2026-10-12 10:48:02 +11:00
Richard Holland 3cb3ca85c4 bug fixes 2026-10-12 09:56:55 +11:00
Richard Holland c75702a69e Merge branch 'dev' into newrng 2026-10-12 08:23:04 +10:00
Richard Holland 3d1df72c29 Merge branch 'dev' into newrng 2026-10-12 08:08:51 +10:00
Richard Holland a246998076 Merge branch 'dev' into newrng 2026-10-11 23:09:30 +10:00
tequ 5c9beffdf8 Merge branch 'dev' into newrng 2026-10-11 22:02:15 +09:00
Richard Holland 5080a7f05d Merge branch 'dev' into newrng 2026-10-11 16:23:18 +10:00
Richard Holland 6c76e020bb Merge branch 'dev' into newrng 2026-10-11 15:51:24 +10:00
Richard Holland 541ce8dcc1 fee bug 2026-10-11 16:47:05 +11:00
Richard Holland bb57635fd6 clang 2026-10-11 15:37:08 +11:00
Richard Holland 737c24982a Merge branch 'dev' into newrng 2026-10-11 14:31:50 +10:00
Richard Holland e0e7e6bb5d test coverage, rpc parse 2026-10-11 13:39:29 +11:00
Richard Holland 3b20c779b8 rng fixes 2026-10-06 18:38:20 +11:00
Richard Holland a981ee56f1 Merge branch 'dev' into newrng 2026-10-06 12:30:29 +10:00
Richard Holland 2653b33565 init 2026-10-06 13:24:51 +11:00
74 changed files with 425 additions and 3253 deletions

No files matched your search

-1
View File
@@ -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
View File
@@ -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
-8
View File
@@ -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
-5
View File
@@ -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)
-1
View File
@@ -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
-1
View File
@@ -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 {
-4
View File
@@ -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
-6
View File
@@ -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>
-120
View File
@@ -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
-4
View File
@@ -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
{
-29
View File
@@ -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, ({
-2
View File
@@ -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
+1 -10
View File
@@ -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
{
+4 -70
View File
@@ -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>
-74
View File
@@ -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);
}
-19
View File
@@ -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);
-146
View File
@@ -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();
}
};
-112
View File
@@ -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";
-31
View File
@@ -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();
-8
View File
@@ -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;
+1 -640
View File
@@ -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);
}
};
-50
View File
@@ -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.",
-79
View File
@@ -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)
{
-8
View File
@@ -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;
+1 -1
View File
@@ -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);
-41
View File
@@ -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
{
+3 -37
View File
@@ -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,
+2 -3
View File
@@ -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);
+1 -7
View File
@@ -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
+1 -7
View File
@@ -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);
+1 -3
View File
@@ -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
{
-67
View File
@@ -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
{
-7
View File
@@ -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;
-13
View File
@@ -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
+1 -8
View File
@@ -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);
});
}
+1 -16
View File
@@ -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)
{
+1 -17
View File
@@ -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();
+1 -2
View File
@@ -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
+9 -51
View File
@@ -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
+3 -5
View File
@@ -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
+6 -78
View File
@@ -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
+20 -28
View File
@@ -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;
}
{
+2 -3
View File
@@ -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))
{
-159
View File
@@ -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
-66
View File
@@ -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:
+1 -9
View File
@@ -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
{
+2 -6
View File
@@ -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(
-1
View File
@@ -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>
-3
View File
@@ -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
+1 -5
View File
@@ -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;
+1 -6
View File
@@ -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
+1 -2
View File
@@ -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)
-17
View File
@@ -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},
-16
View File
@@ -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
+2 -5
View File
@@ -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);
+13 -65
View File
@@ -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
-18
View File
@@ -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
-1
View File
@@ -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
-2
View File
@@ -184,8 +184,6 @@ Json::Value
doCatalogueStatus(RPC::JsonContext&);
Json::Value
doCatalogueLoad(RPC::JsonContext&);
Json::Value
doCatalogueUnpin(RPC::JsonContext&);
} // namespace ripple
#endif
+3 -44
View File
@@ -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
+6 -65
View File
@@ -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;