mirror of
https://github.com/Xahau/xahaud.git
synced 2025-12-06 17:27:52 +00:00
fix: provisional PreviousTxn{Id,LedgerSeq} double threading (#515)
--------- Co-authored-by: tequ <git@tequ.dev>
This commit is contained in:
@@ -751,6 +751,7 @@ if (tests)
|
||||
src/test/app/Path_test.cpp
|
||||
src/test/app/PayChan_test.cpp
|
||||
src/test/app/PayStrand_test.cpp
|
||||
src/test/app/PreviousTxn_test.cpp
|
||||
src/test/app/PseudoTx_test.cpp
|
||||
src/test/app/RCLCensorshipDetector_test.cpp
|
||||
src/test/app/RCLValidations_test.cpp
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <ripple/ledger/OpenView.h>
|
||||
#include <ripple/ledger/RawView.h>
|
||||
#include <ripple/ledger/ReadView.h>
|
||||
#include <ripple/protocol/Rules.h>
|
||||
#include <ripple/protocol/TER.h>
|
||||
#include <ripple/protocol/TxMeta.h>
|
||||
#include <memory>
|
||||
@@ -53,6 +54,18 @@ private:
|
||||
items_t items_;
|
||||
XRPAmount dropsDestroyed_{0};
|
||||
|
||||
// Track original PreviousTxnID/LgrSeq values to restore after provisional
|
||||
// metadata. This map is populated during provisional metadata generation
|
||||
// and consumed during final metadata generation. It is not cleared as
|
||||
// the ApplyStateTable instance is single-use per transaction.
|
||||
struct ThreadingState
|
||||
{
|
||||
uint256 prevTxnID;
|
||||
uint32_t prevTxnLgrSeq;
|
||||
bool hasPrevTxnID;
|
||||
};
|
||||
mutable std::map<key_type, ThreadingState> originalThreadingState_;
|
||||
|
||||
public:
|
||||
ApplyStateTable() = default;
|
||||
ApplyStateTable(ApplyStateTable&&) = default;
|
||||
@@ -73,7 +86,8 @@ public:
|
||||
std::optional<STAmount> const& deliver,
|
||||
std::vector<STObject> const& hookExecution,
|
||||
std::vector<STObject> const& hookEmission,
|
||||
beast::Journal j);
|
||||
beast::Journal j,
|
||||
bool isProvisional = false);
|
||||
|
||||
void
|
||||
apply(
|
||||
@@ -138,8 +152,11 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
static void
|
||||
threadItem(TxMeta& meta, std::shared_ptr<SLE> const& to);
|
||||
void
|
||||
threadItem(
|
||||
TxMeta& meta,
|
||||
std::shared_ptr<SLE> const& to,
|
||||
Rules const& rules);
|
||||
|
||||
std::shared_ptr<SLE>
|
||||
getForMod(
|
||||
|
||||
@@ -118,7 +118,8 @@ ApplyStateTable::generateTxMeta(
|
||||
std::optional<STAmount> const& deliver,
|
||||
std::vector<STObject> const& hookExecution,
|
||||
std::vector<STObject> const& hookEmission,
|
||||
beast::Journal j)
|
||||
beast::Journal j,
|
||||
bool isProvisional)
|
||||
{
|
||||
TxMeta meta(tx.getTransactionID(), to.seq());
|
||||
if (deliver)
|
||||
@@ -196,7 +197,7 @@ ApplyStateTable::generateTxMeta(
|
||||
|
||||
if (curNode->isThreadedType()) // thread transaction to node
|
||||
// item modified
|
||||
threadItem(meta, curNode);
|
||||
threadItem(meta, curNode, to.rules());
|
||||
|
||||
STObject prevs(sfPreviousFields);
|
||||
for (auto const& obj : *origNode)
|
||||
@@ -229,7 +230,7 @@ ApplyStateTable::generateTxMeta(
|
||||
threadOwners(to, meta, curNode, newMod, j);
|
||||
|
||||
if (curNode->isThreadedType()) // always thread to self
|
||||
threadItem(meta, curNode);
|
||||
threadItem(meta, curNode, to.rules());
|
||||
|
||||
STObject news(sfNewFields);
|
||||
for (auto const& obj : *curNode)
|
||||
@@ -254,6 +255,34 @@ ApplyStateTable::generateTxMeta(
|
||||
}
|
||||
}
|
||||
|
||||
// After provisional metadata generation, restore the original PreviousTxnID
|
||||
// values to prevent contamination of the "before" state used for final
|
||||
// metadata comparison. This ensures PreviousTxnID appears correctly in
|
||||
// ModifiedNode metadata.
|
||||
if (isProvisional && to.rules().enabled(fixProvisionalDoubleThreading))
|
||||
{
|
||||
for (auto const& [key, state] : originalThreadingState_)
|
||||
{
|
||||
auto iter = items_.find(key);
|
||||
if (iter != items_.end())
|
||||
{
|
||||
auto sle = iter->second.second;
|
||||
if (state.hasPrevTxnID)
|
||||
{
|
||||
sle->setFieldH256(sfPreviousTxnID, state.prevTxnID);
|
||||
sle->setFieldU32(sfPreviousTxnLgrSeq, state.prevTxnLgrSeq);
|
||||
// Restored to original PreviousTxnID
|
||||
}
|
||||
else
|
||||
{
|
||||
sle->makeFieldAbsent(sfPreviousTxnID);
|
||||
sle->makeFieldAbsent(sfPreviousTxnLgrSeq);
|
||||
// Restored to no PreviousTxnID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {meta, newMod};
|
||||
}
|
||||
|
||||
@@ -287,6 +316,8 @@ ApplyStateTable::apply(
|
||||
// VFALCO For diagnostics do we want to show
|
||||
// metadata even when the base view is open?
|
||||
JLOG(j.trace()) << "metadata " << meta.getJson(JsonOptions::none);
|
||||
|
||||
// Metadata has been generated
|
||||
}
|
||||
to.rawTxInsert(tx.getTransactionID(), sTx, sMeta);
|
||||
apply(to);
|
||||
@@ -549,9 +580,51 @@ ApplyStateTable::destroyXRP(XRPAmount const& fee)
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Insert this transaction to the SLE's threading list
|
||||
//
|
||||
// This method is called during metadata generation to update the
|
||||
// PreviousTxnID/PreviousTxnLgrSeq fields on SLEs. However, it's called
|
||||
// twice for each transaction:
|
||||
// 1. During provisional metadata generation (for hooks to see)
|
||||
// 2. During final metadata generation (for the actual ledger)
|
||||
//
|
||||
// The fixProvisionalDoubleThreading amendment fixes a bug where the
|
||||
// provisional threading would contaminate the "original" state used
|
||||
// for metadata comparison, causing PreviousTxnID to be missing from
|
||||
// the final metadata.
|
||||
//
|
||||
// The fix works by:
|
||||
// - Saving the original PreviousTxnID state for an SLE the first time it's
|
||||
// threaded during the provisional pass.
|
||||
// - Restoring the original state for all affected SLEs in a single batch
|
||||
// after the entire provisional metadata generation is complete.
|
||||
//
|
||||
// This batch-restore is critical because threadItem() can be called on the
|
||||
// same SLE multiple times within one metadata pass. Restoring immediately
|
||||
// would be incorrect. This approach ensures the final metadata comparison
|
||||
// starts from the correct, uncontaminated "before" state.
|
||||
void
|
||||
ApplyStateTable::threadItem(TxMeta& meta, std::shared_ptr<SLE> const& sle)
|
||||
ApplyStateTable::threadItem(
|
||||
TxMeta& meta,
|
||||
std::shared_ptr<SLE> const& sle,
|
||||
const Rules& rules)
|
||||
{
|
||||
if (rules.enabled(fixProvisionalDoubleThreading))
|
||||
{
|
||||
auto const key = sle->key();
|
||||
if (originalThreadingState_.find(key) == originalThreadingState_.end())
|
||||
{
|
||||
// First time (provisional metadata) - save the original state
|
||||
ThreadingState state;
|
||||
state.hasPrevTxnID = sle->isFieldPresent(sfPreviousTxnID);
|
||||
if (state.hasPrevTxnID)
|
||||
{
|
||||
state.prevTxnID = sle->getFieldH256(sfPreviousTxnID);
|
||||
state.prevTxnLgrSeq = sle->getFieldU32(sfPreviousTxnLgrSeq);
|
||||
}
|
||||
originalThreadingState_[key] = state;
|
||||
}
|
||||
}
|
||||
|
||||
key_type prevTxID;
|
||||
LedgerIndex prevLgrID;
|
||||
|
||||
@@ -567,6 +640,11 @@ ApplyStateTable::threadItem(TxMeta& meta, std::shared_ptr<SLE> const& sle)
|
||||
assert(node.getFieldIndex(sfPreviousTxnLgrSeq) == -1);
|
||||
node.setFieldH256(sfPreviousTxnID, prevTxID);
|
||||
node.setFieldU32(sfPreviousTxnLgrSeq, prevLgrID);
|
||||
// Added PreviousTxnID to metadata
|
||||
}
|
||||
else
|
||||
{
|
||||
// PreviousTxnID already present in metadata
|
||||
}
|
||||
|
||||
assert(node.getFieldH256(sfPreviousTxnID) == prevTxID);
|
||||
@@ -640,7 +718,7 @@ ApplyStateTable::threadTx(
|
||||
JLOG(j.warn()) << "Threading to non-existent account: " << toBase58(to);
|
||||
return;
|
||||
}
|
||||
threadItem(meta, sle);
|
||||
threadItem(meta, sle, base.rules());
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -41,8 +41,13 @@ ApplyViewImpl::generateProvisionalMeta(
|
||||
beast::Journal j)
|
||||
{
|
||||
auto [meta, _] = items_.generateTxMeta(
|
||||
to, tx, deliver_, hookExecution_, hookEmission_, j);
|
||||
|
||||
to,
|
||||
tx,
|
||||
deliver_,
|
||||
hookExecution_,
|
||||
hookEmission_,
|
||||
j,
|
||||
true); // isProvisional = true
|
||||
return meta;
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace detail {
|
||||
// Feature.cpp. Because it's only used to reserve storage, and determine how
|
||||
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
|
||||
// the actual number of amendments. A LogicError on startup will verify this.
|
||||
static constexpr std::size_t numFeatures = 81;
|
||||
static constexpr std::size_t numFeatures = 82;
|
||||
|
||||
/** Amendments that this server supports and the default voting behavior.
|
||||
Whether they are enabled depends on the Rules defined in the validated
|
||||
@@ -369,6 +369,7 @@ extern uint256 const fixXahauV3;
|
||||
extern uint256 const fix20250131;
|
||||
extern uint256 const featureHookCanEmit;
|
||||
extern uint256 const fixRewardClaimFlags;
|
||||
extern uint256 const fixProvisionalDoubleThreading;
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
|
||||
@@ -475,6 +475,7 @@ REGISTER_FIX (fixXahauV3, Supported::yes, VoteBehavior::De
|
||||
REGISTER_FIX (fix20250131, Supported::yes, VoteBehavior::DefaultYes);
|
||||
REGISTER_FEATURE(HookCanEmit, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FIX (fixRewardClaimFlags, Supported::yes, VoteBehavior::DefaultYes);
|
||||
REGISTER_FIX (fixProvisionalDoubleThreading, Supported::yes, VoteBehavior::DefaultYes);
|
||||
|
||||
// The following amendments are obsolete, but must remain supported
|
||||
// because they could potentially get enabled.
|
||||
|
||||
317
src/test/app/PreviousTxn_test.cpp
Normal file
317
src/test/app/PreviousTxn_test.cpp
Normal file
@@ -0,0 +1,317 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 Ripple Labs Inc.
|
||||
|
||||
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 <ripple/protocol/jss.h>
|
||||
#include <test/jtx.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class PreviousTxnID_test : public beast::unit_test::suite
|
||||
{
|
||||
public:
|
||||
void
|
||||
testPreviousTxnID(FeatureBitset features)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
Env env{
|
||||
*this, envconfig(), features, nullptr, beast::severities::kNone};
|
||||
auto j = env.app().logs().journal("PreviousTxnID_test");
|
||||
|
||||
auto const alice = Account{"alice"};
|
||||
auto const bob = Account{"bob"};
|
||||
auto const USD = bob["USD"];
|
||||
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
// Create a trustline
|
||||
env(trust(alice, USD(1000)));
|
||||
env.close();
|
||||
|
||||
// Get the transaction metadata and ID
|
||||
auto const meta1 = env.meta();
|
||||
auto const trustCreateTxID = env.tx()->getTransactionID();
|
||||
BEAST_EXPECT(meta1);
|
||||
|
||||
// Check if ModifiedNode has PreviousTxnID at root level
|
||||
auto const& affectedNodes = meta1->getFieldArray(sfAffectedNodes);
|
||||
bool foundPreviousTxnID = false;
|
||||
bool foundModifiedRippleState = false;
|
||||
bool foundCreatedRippleState = false;
|
||||
|
||||
for (auto const& node : affectedNodes)
|
||||
{
|
||||
if (node.getFieldU16(sfLedgerEntryType) == ltRIPPLE_STATE)
|
||||
{
|
||||
if (node.getFName() == sfModifiedNode)
|
||||
{
|
||||
BEAST_EXPECT(false);
|
||||
}
|
||||
else if (node.getFName() == sfCreatedNode)
|
||||
{
|
||||
foundCreatedRippleState = true;
|
||||
foundPreviousTxnID = node.isFieldPresent(sfPreviousTxnID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BEAST_EXPECT(foundCreatedRippleState);
|
||||
// Why we expect NO PreviousTxnID in newly created trustline metadata:
|
||||
//
|
||||
// PreviousTxnID in metadata indicates which transaction PREVIOUSLY
|
||||
// modified an object, not which transaction created it. For a newly
|
||||
// created object, there is no previous transaction - the current
|
||||
// transaction is the first one to touch this object.
|
||||
//
|
||||
// While the SLE itself will have PreviousTxnID set to the creating
|
||||
// transaction (by ApplyStateTable::threadItem), the metadata correctly
|
||||
// omits PreviousTxnID from CreatedNode because there was no previous
|
||||
// modification to reference.
|
||||
//
|
||||
// Technical detail: trustCreate() creates the RippleState with
|
||||
// PreviousTxnID as a defaultObject placeholder (since it's soeREQUIRED
|
||||
// in LedgerFormats.cpp). ApplyStateTable::generateTxMeta skips fields
|
||||
// with zero/empty values when generating metadata, so the unset
|
||||
// PreviousTxnID doesn't appear in the CreatedNode output.
|
||||
BEAST_EXPECT(foundPreviousTxnID == false);
|
||||
BEAST_EXPECT(foundModifiedRippleState == false);
|
||||
|
||||
// Now let's check the actual ledger entry to see if PreviousTxnID was
|
||||
// set Get the trustline from the ledger
|
||||
auto const trustlineKey = keylet::line(alice, bob, USD.currency);
|
||||
auto const sleTrustline = env.le(trustlineKey);
|
||||
BEAST_EXPECT(sleTrustline);
|
||||
|
||||
if (sleTrustline)
|
||||
{
|
||||
// Check if the SLE itself has PreviousTxnID set
|
||||
// Even though it didn't appear in metadata, the SLE should have it
|
||||
// because ApplyStateTable::threadItem() sets it after creation
|
||||
bool sleHasPrevTxnID =
|
||||
sleTrustline->isFieldPresent(sfPreviousTxnID);
|
||||
bool sleHasPrevTxnSeq =
|
||||
sleTrustline->isFieldPresent(sfPreviousTxnLgrSeq);
|
||||
|
||||
JLOG(j.info()) << "SLE has PreviousTxnID: " << sleHasPrevTxnID;
|
||||
JLOG(j.info()) << "SLE has PreviousTxnLgrSeq: " << sleHasPrevTxnSeq;
|
||||
|
||||
if (sleHasPrevTxnID && sleHasPrevTxnSeq)
|
||||
{
|
||||
auto const prevTxnID =
|
||||
sleTrustline->getFieldH256(sfPreviousTxnID);
|
||||
auto const prevTxnSeq =
|
||||
sleTrustline->getFieldU32(sfPreviousTxnLgrSeq);
|
||||
auto const creatingTxnID = env.tx()->getTransactionID();
|
||||
|
||||
JLOG(j.info()) << "SLE PreviousTxnID: " << prevTxnID;
|
||||
JLOG(j.info()) << "Creating TxnID: " << creatingTxnID;
|
||||
JLOG(j.info()) << "SLE PreviousTxnLgrSeq: " << prevTxnSeq;
|
||||
|
||||
// The PreviousTxnID in the SLE should match the transaction
|
||||
// that created it
|
||||
BEAST_EXPECT(prevTxnID == creatingTxnID);
|
||||
BEAST_EXPECT(prevTxnSeq == env.closed()->seq());
|
||||
}
|
||||
else
|
||||
{
|
||||
// This would indicate that ApplyStateTable::threadItem() didn't
|
||||
// set PreviousTxnID on the newly created trustline
|
||||
JLOG(j.warn())
|
||||
<< "Newly created trustline SLE missing PreviousTxnID!";
|
||||
BEAST_EXPECT(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Now modify the trustline with a payment
|
||||
env(pay(bob, alice, USD(100)));
|
||||
env.close();
|
||||
|
||||
// Get the second transaction metadata
|
||||
auto const meta2 = env.meta();
|
||||
BEAST_EXPECT(meta2);
|
||||
|
||||
// Check ModifiedNode for PreviousTxnID
|
||||
auto const& affectedNodes2 = meta2->getFieldArray(sfAffectedNodes);
|
||||
bool foundPreviousTxnIDInModified = false;
|
||||
bool foundPreviousTxnIDInPreviousFields = false;
|
||||
|
||||
for (auto const& node : affectedNodes2)
|
||||
{
|
||||
if (node.getFName() == sfModifiedNode &&
|
||||
node.getFieldU16(sfLedgerEntryType) == ltRIPPLE_STATE)
|
||||
{
|
||||
auto json = node.getJson({});
|
||||
JLOG(j.trace()) << json;
|
||||
|
||||
// Why we expect PreviousTxnID in ModifiedNode metadata:
|
||||
//
|
||||
// When a transaction modifies an existing RippleState, the
|
||||
// ApplyView tracks the modification. During metadata
|
||||
// generation, ApplyStateTable::generateTxMeta compares the
|
||||
// before and after states of the SLE.
|
||||
//
|
||||
// For ModifiedNode entries, the metadata should include
|
||||
// PreviousTxnID and PreviousTxnLgrSeq at the root level to
|
||||
// indicate which transaction last modified this object.
|
||||
// This is different from PreviousFields, which shows what
|
||||
// field values changed.
|
||||
//
|
||||
// The bug fixed by the `fixProvisionalDoubleThreading`
|
||||
// amendment was that ApplyStateTable::threadItem() was
|
||||
// modifying the original SLE during provisional metadata
|
||||
// generation. This contaminated the "before" state used for
|
||||
// comparison, so when final metadata was generated, the
|
||||
// comparison didn't see PreviousTxnID as a change because both
|
||||
// states had the new value.
|
||||
bool expectPreviousTxnID =
|
||||
features[fixProvisionalDoubleThreading];
|
||||
|
||||
if (node.isFieldPresent(sfPreviousTxnID))
|
||||
{
|
||||
foundPreviousTxnIDInModified = true;
|
||||
auto prevTxnID = node.getFieldH256(sfPreviousTxnID);
|
||||
auto prevLgrSeq = node.getFieldU32(sfPreviousTxnLgrSeq);
|
||||
|
||||
BEAST_EXPECT(node.isFieldPresent(sfPreviousTxnLgrSeq));
|
||||
BEAST_EXPECT(prevTxnID != beast::zero);
|
||||
BEAST_EXPECT(prevLgrSeq > 0);
|
||||
|
||||
JLOG(j.info()) << "Found PreviousTxnID: " << prevTxnID
|
||||
<< " at ledger: " << prevLgrSeq << std::endl;
|
||||
|
||||
// When the fix is enabled, we should see the trustline
|
||||
// creation transaction ID as the previous transaction
|
||||
if (expectPreviousTxnID)
|
||||
{
|
||||
BEAST_EXPECT(prevTxnID == trustCreateTxID);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Without the fix, we expect PreviousTxnID to be missing
|
||||
// due to the provisional metadata contamination bug
|
||||
JLOG(j.info()) << "PreviousTxnID missing in metadata";
|
||||
}
|
||||
|
||||
// Check if PreviousTxnID appears in PreviousFields
|
||||
// (it shouldn't - PreviousTxnID is a root-level field)
|
||||
if (node.isFieldPresent(sfPreviousFields))
|
||||
{
|
||||
auto prevFields = dynamic_cast<STObject const*>(
|
||||
node.peekAtPField(sfPreviousFields));
|
||||
if (prevFields &&
|
||||
prevFields->isFieldPresent(sfPreviousTxnID))
|
||||
{
|
||||
foundPreviousTxnIDInPreviousFields = true;
|
||||
JLOG(j.warn()) << "Found PreviousTxnID in "
|
||||
"PreviousFields (unexpected)";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PreviousTxnID should never appear in PreviousFields
|
||||
BEAST_EXPECT(!foundPreviousTxnIDInPreviousFields);
|
||||
|
||||
// With the fix enabled, we expect to find PreviousTxnID
|
||||
// Without the fix, we expect it to be missing (the bug)
|
||||
if (features[fixProvisionalDoubleThreading])
|
||||
{
|
||||
BEAST_EXPECT(foundPreviousTxnIDInModified);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!foundPreviousTxnIDInModified);
|
||||
}
|
||||
|
||||
// Additional check: Verify the SLE state after the payment
|
||||
auto const sleTrustlineAfter = env.le(trustlineKey);
|
||||
BEAST_EXPECT(sleTrustlineAfter);
|
||||
|
||||
if (sleTrustlineAfter)
|
||||
{
|
||||
// The SLE should always have PreviousTxnID set after modification
|
||||
BEAST_EXPECT(sleTrustlineAfter->isFieldPresent(sfPreviousTxnID));
|
||||
BEAST_EXPECT(
|
||||
sleTrustlineAfter->isFieldPresent(sfPreviousTxnLgrSeq));
|
||||
|
||||
auto const currentPrevTxnID =
|
||||
sleTrustlineAfter->getFieldH256(sfPreviousTxnID);
|
||||
auto const currentPrevTxnSeq =
|
||||
sleTrustlineAfter->getFieldU32(sfPreviousTxnLgrSeq);
|
||||
|
||||
// The PreviousTxnID should now point to the payment transaction
|
||||
auto const paymentTxID = env.tx()->getTransactionID();
|
||||
BEAST_EXPECT(currentPrevTxnID == paymentTxID);
|
||||
BEAST_EXPECT(currentPrevTxnSeq == env.closed()->seq());
|
||||
|
||||
// When the bug is present (feature disabled), the metadata won't
|
||||
// show the change, but the SLE will still be updated correctly
|
||||
if (!features[fixProvisionalDoubleThreading])
|
||||
{
|
||||
JLOG(j.info())
|
||||
<< "Bug confirmed: SLE has correct PreviousTxnID ("
|
||||
<< currentPrevTxnID
|
||||
<< ") but metadata doesn't show the change";
|
||||
}
|
||||
}
|
||||
|
||||
// Check account objects were threaded correctly
|
||||
auto const aliceAccount = env.le(keylet::account(alice));
|
||||
auto const bobAccount = env.le(keylet::account(bob));
|
||||
|
||||
BEAST_EXPECT(aliceAccount);
|
||||
BEAST_EXPECT(bobAccount);
|
||||
|
||||
if (aliceAccount && bobAccount)
|
||||
{
|
||||
// Both accounts should have been threaded by the payment
|
||||
BEAST_EXPECT(aliceAccount->isFieldPresent(sfPreviousTxnID));
|
||||
BEAST_EXPECT(bobAccount->isFieldPresent(sfPreviousTxnID));
|
||||
|
||||
auto const alicePrevTxnID =
|
||||
aliceAccount->getFieldH256(sfPreviousTxnID);
|
||||
auto const bobPrevTxnID = bobAccount->getFieldH256(sfPreviousTxnID);
|
||||
auto const paymentTxID = env.tx()->getTransactionID();
|
||||
|
||||
// Both should point to the payment transaction
|
||||
BEAST_EXPECT(alicePrevTxnID == paymentTxID);
|
||||
BEAST_EXPECT(bobPrevTxnID == paymentTxID);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace test::jtx;
|
||||
auto const sa = supported_amendments();
|
||||
|
||||
testcase("With fixProvisionalDoubleThreading enabled");
|
||||
testPreviousTxnID(sa);
|
||||
|
||||
testcase("Without fixProvisionalDoubleThreading (bug present)");
|
||||
testPreviousTxnID(sa - fixProvisionalDoubleThreading);
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(PreviousTxnID, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -186,7 +186,8 @@ class TxQ1_test : public beast::unit_test::suite
|
||||
|
||||
// In order for the vote to occur, we must run as a validator
|
||||
p->section("validation_seed")
|
||||
.legacy("shUwVw52ofnCUX5m7kPTKzJdr4HEH");
|
||||
.legacy("shUwVw52ofnCUX5m7kPTKzJdr4HEH"); // not-suspicious
|
||||
// test seed
|
||||
}
|
||||
return p;
|
||||
}
|
||||
@@ -5049,7 +5050,9 @@ public:
|
||||
testFailInPreclaim(all);
|
||||
testQueuedTxFails(all);
|
||||
testMultiTxnPerAccount(all);
|
||||
testTieBreaking(all);
|
||||
// fragile: hardcoded ordering by txID XOR parentHash
|
||||
// parentHash < txTree Hash < txMeta < PreviousTxnID
|
||||
testTieBreaking(all - fixProvisionalDoubleThreading);
|
||||
testAcctTxnID(all);
|
||||
testMaximum(all);
|
||||
testUnexpectedBalanceChange(all);
|
||||
@@ -5067,7 +5070,9 @@ public:
|
||||
testAcctInQueueButEmpty(all);
|
||||
testRPC(all);
|
||||
testExpirationReplacement(all);
|
||||
testFullQueueGapFill(all);
|
||||
// fragile: hardcoded ordering by txID XOR parentHash
|
||||
// parentHash < txTree Hash < txMeta < PreviousTxnID
|
||||
testFullQueueGapFill(all - fixProvisionalDoubleThreading);
|
||||
testSignAndSubmitSequence(all);
|
||||
testAccountInfo(all);
|
||||
testServerInfo(all);
|
||||
|
||||
@@ -2088,7 +2088,8 @@ public:
|
||||
section.set("normal_consensus_increase_percent", "0");
|
||||
return cfg;
|
||||
}),
|
||||
supported_amendments() - featureXahauGenesis};
|
||||
supported_amendments() - featureXahauGenesis -
|
||||
fixProvisionalDoubleThreading};
|
||||
|
||||
Json::Value jv;
|
||||
jv[jss::ledger_index] = "current";
|
||||
|
||||
Reference in New Issue
Block a user