mirror of
				https://github.com/Xahau/xahaud.git
				synced 2025-11-04 02:35:48 +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