diff --git a/Builds/VisualStudio2015/RippleD.vcxproj b/Builds/VisualStudio2015/RippleD.vcxproj index 4413ed27b..b3726c308 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj +++ b/Builds/VisualStudio2015/RippleD.vcxproj @@ -1049,6 +1049,8 @@ True True + + True True diff --git a/Builds/VisualStudio2015/RippleD.vcxproj.filters b/Builds/VisualStudio2015/RippleD.vcxproj.filters index d436b05cf..816ce4bc0 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj.filters +++ b/Builds/VisualStudio2015/RippleD.vcxproj.filters @@ -1524,6 +1524,9 @@ ripple\app\paths\impl + + ripple\app\paths\impl + ripple\app\paths\impl diff --git a/src/ripple/app/paths/Flow.cpp b/src/ripple/app/paths/Flow.cpp index 9e68f8e98..903c70629 100644 --- a/src/ripple/app/paths/Flow.cpp +++ b/src/ripple/app/paths/Flow.cpp @@ -65,7 +65,8 @@ flow ( bool ownerPaysTransferFee, boost::optional const& limitQuality, boost::optional const& sendMax, - beast::Journal j) + beast::Journal j, + path::detail::FlowDebugInfo* flowDebugInfo) { Issue const srcIssue = [&] { if (sendMax) @@ -123,7 +124,7 @@ flow ( return finishFlow (sb, srcIssue, dstIssue, flow ( sb, strands, asDeliver.xrp, defaultPaths, partialPayment, - limitQuality, sendMax, j)); + limitQuality, sendMax, j, flowDebugInfo)); } if (srcIsXRP && !dstIsXRP) @@ -131,7 +132,7 @@ flow ( return finishFlow (sb, srcIssue, dstIssue, flow ( sb, strands, asDeliver.iou, defaultPaths, partialPayment, - limitQuality, sendMax, j)); + limitQuality, sendMax, j, flowDebugInfo)); } if (!srcIsXRP && dstIsXRP) @@ -139,14 +140,14 @@ flow ( return finishFlow (sb, srcIssue, dstIssue, flow ( sb, strands, asDeliver.xrp, defaultPaths, partialPayment, - limitQuality, sendMax, j)); + limitQuality, sendMax, j, flowDebugInfo)); } assert (!srcIsXRP && !dstIsXRP); return finishFlow (sb, srcIssue, dstIssue, flow ( sb, strands, asDeliver.iou, defaultPaths, partialPayment, - limitQuality, sendMax, j)); + limitQuality, sendMax, j, flowDebugInfo)); } diff --git a/src/ripple/app/paths/Flow.h b/src/ripple/app/paths/Flow.h index ecf622585..e6f9087ce 100644 --- a/src/ripple/app/paths/Flow.h +++ b/src/ripple/app/paths/Flow.h @@ -27,6 +27,12 @@ namespace ripple { +namespace path { +namespace detail{ +struct FlowDebugInfo; +} +} + /** Make a payment from the src account to the dst account @@ -54,7 +60,8 @@ flow (PaymentSandbox& view, bool ownerPaysTransferFee, boost::optional const& limitQuality, boost::optional const& sendMax, - beast::Journal j); + beast::Journal j, + path::detail::FlowDebugInfo* flowDebugInfo=nullptr); } // ripple diff --git a/src/ripple/app/paths/RippleCalc.cpp b/src/ripple/app/paths/RippleCalc.cpp index 6c5c930d1..7d6a6fda2 100644 --- a/src/ripple/app/paths/RippleCalc.cpp +++ b/src/ripple/app/paths/RippleCalc.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -70,19 +71,25 @@ RippleCalc::Output RippleCalc::rippleCalculate ( Config const& config, Input const* const pInputs) { + // call flow v1 and v2 so results may be compared + bool const compareFlowV1V2 = + view.rules ().enabled (featureCompareFlowV1V2, config.features); + bool const useFlowV1Output = !flowV2Switchover (view.info ().parentCloseTime) && !view.rules ().enabled (featureFlowV2, config.features); - // When flowV2 is enabled via rules, call old flow so results may be - // compared - bool const callFlowV1 = useFlowV1Output || - view.rules ().enabled (featureFlowV2, config.features); - bool const callFlowV2 = !useFlowV1Output; + bool const callFlowV1 = useFlowV1Output || compareFlowV1V2; + bool const callFlowV2 = !useFlowV1Output || compareFlowV1V2; Output flowV1Out; PaymentSandbox flowV1SB (&view); + + auto const inNative = saMaxAmountReq.native(); + auto const outNative = saDstAmountReq.native(); + detail::FlowDebugInfo flowV1FlowDebugInfo (inNative, outNative); if (callFlowV1) { + auto const timeIt = flowV1FlowDebugInfo.timeBlock ("main"); RippleCalc rc ( flowV1SB, saMaxAmountReq, @@ -96,7 +103,7 @@ RippleCalc::Output RippleCalc::rippleCalculate ( rc.inputFlags = *pInputs; } - auto result = rc.rippleCalculate (); + auto result = rc.rippleCalculate (compareFlowV1V2 ? &flowV1FlowDebugInfo : nullptr); flowV1Out.setResult (result); flowV1Out.actualAmountIn = rc.actualAmountIn_; flowV1Out.actualAmountOut = rc.actualAmountOut_; @@ -106,6 +113,8 @@ RippleCalc::Output RippleCalc::rippleCalculate ( Output flowV2Out; PaymentSandbox flowV2SB (&view); + detail::FlowDebugInfo flowV2FlowDebugInfo (inNative, outNative); + auto j = l.journal ("Flow"); if (callFlowV2) { bool defaultPaths = true; @@ -129,15 +138,15 @@ RippleCalc::Output RippleCalc::rippleCalculate ( sendMax.emplace (saMaxAmountReq); } - auto j = l.journal ("Flow"); - try { bool const ownerPaysTransferFee = view.rules ().enabled (featureOwnerPaysFee, config.features); + auto const timeIt = flowV2FlowDebugInfo.timeBlock ("main"); flowV2Out = flow (flowV2SB, saDstAmountReq, uSrcAccountID, uDstAccountID, spsPaths, defaultPaths, partialPayment, - ownerPaysTransferFee, limitQuality, sendMax, j); + ownerPaysTransferFee, limitQuality, sendMax, j, + compareFlowV1V2 ? &flowV2FlowDebugInfo : nullptr); } catch (std::exception& e) { @@ -145,32 +154,63 @@ RippleCalc::Output RippleCalc::rippleCalculate ( if (!useFlowV1Output) Rethrow(); } + } - if (j.debug()) + if (j.debug()) + { + using BalanceDiffs = detail::BalanceDiffs; + auto logResult = [&](std::string const& algoName, + Output const& result, + detail::FlowDebugInfo const& flowDebugInfo, + boost::optional const& balanceDiffs, + bool outputPassInfo, + bool outputBalanceDiffs) { + j.debug () << "RippleCalc Result> " << + " actualIn: " << result.actualAmountIn << + ", actualOut: " << result.actualAmountOut << + ", result: " << result.result () << + ", dstAmtReq: " << saDstAmountReq << + ", sendMax: " << saMaxAmountReq << + (compareFlowV1V2 ? ", " + flowDebugInfo.to_string (outputPassInfo): "") << + (outputBalanceDiffs && balanceDiffs + ? ", " + detail::balanceDiffsToString(balanceDiffs) : "") << + ", algo: " << algoName; + }; + bool outputPassInfo = false; + bool outputBalanceDiffs = false; + boost::optional bdV1, bdV2; + if (compareFlowV1V2) { - auto logResult = [&] (std::string const& algoName, Output const& result) + auto const v1r = flowV1Out.result (); + auto const v2r = flowV2Out.result (); + if (v1r != v2r || + (((v1r == tesSUCCESS) || (v1r == tecPATH_PARTIAL)) && + ((flowV1Out.actualAmountIn != + flowV2Out.actualAmountIn) || + (flowV1Out.actualAmountOut != + flowV2Out.actualAmountOut)))) { - j.debug() << "RippleCalc Result> " << - " actualIn: " << result.actualAmountIn << - ", actualOut: " << result.actualAmountOut << - ", result: " << result.result () << - ", dstAmtReq: " << saDstAmountReq << - ", sendMax: " << saMaxAmountReq << - ", algo: " << algoName; - }; - if (callFlowV1) - { - logResult ("V1", flowV1Out); - } - if (callFlowV2) - { - logResult ("V2", flowV2Out); + outputPassInfo = true; } + bdV1 = detail::balanceDiffs (flowV1SB, view); + bdV2 = detail::balanceDiffs (flowV2SB, view); + outputBalanceDiffs = bdV1 != bdV2; } - JLOG (j.trace()) << "Using old flow: " << useFlowV1Output; + if (callFlowV1) + { + logResult ("V1", flowV1Out, flowV1FlowDebugInfo, bdV1, + outputPassInfo, outputBalanceDiffs); + } + if (callFlowV2) + { + logResult ("V2", flowV2Out, flowV2FlowDebugInfo, bdV2, + outputPassInfo, outputBalanceDiffs); + } } + JLOG (j.trace()) << "Using old flow: " << useFlowV1Output; + if (!useFlowV1Output) { flowV2SB.apply (view); @@ -232,7 +272,7 @@ bool RippleCalc::addPathState(STPath const& path, TER& resultCode) // liquidity. No need to revisit path in the future if all liquidity is used. // <-- TER: Only returns tepPATH_PARTIAL if partialPaymentAllowed. -TER RippleCalc::rippleCalculate () +TER RippleCalc::rippleCalculate (detail::FlowDebugInfo* flowDebugInfo) { JLOG (j_.trace()) << "rippleCalc>" @@ -301,6 +341,7 @@ TER RippleCalc::rippleCalculate () // True, if ever computed multi-quality. bool multiQuality = false; + if (flowDebugInfo) flowDebugInfo->newLiquidityPass(); // Find the best path. for (auto pathState : pathStateList_) { @@ -328,6 +369,11 @@ TER RippleCalc::rippleCalculate () << " uQuality=" << pathState->quality() << " rate=" << amountFromRate (pathState->quality()); + if (flowDebugInfo) + flowDebugInfo->pushLiquiditySrc ( + toEitherAmount (pathState->inPass ()), + toEitherAmount (pathState->outPass ())); + if (!pathState->quality()) { // Path was dry. @@ -418,13 +464,16 @@ TER RippleCalc::rippleCalculate () // Apply best path. auto pathState = pathStateList_[iBest]; - JLOG (j_.debug()) + if (flowDebugInfo) + flowDebugInfo->pushPass (toEitherAmount (pathState->inPass ()), + toEitherAmount (pathState->outPass ()), + pathStateList_.size () - iDry); + + JLOG (j_.debug ()) << "rippleCalc: best:" - << " uQuality=" - << amountFromRate (pathState->quality()) - << " inPass()=" << pathState->inPass() - << " saOutPass=" << pathState->outPass() - << " iBest=" << iBest; + << " uQuality=" << amountFromRate (pathState->quality ()) + << " inPass()=" << pathState->inPass () + << " saOutPass=" << pathState->outPass () << " iBest=" << iBest; // Record best pass' offers that became unfunded for deletion on // success. diff --git a/src/ripple/app/paths/RippleCalc.h b/src/ripple/app/paths/RippleCalc.h index ff235646e..4f5b53033 100644 --- a/src/ripple/app/paths/RippleCalc.h +++ b/src/ripple/app/paths/RippleCalc.h @@ -32,6 +32,10 @@ namespace ripple { class Config; namespace path { +namespace detail { +struct FlowDebugInfo; +} + /** RippleCalc calculates the quality of a payment path. Quality is the amount of input required to produce a given output along a @@ -145,7 +149,7 @@ private: } /** Compute liquidity through these path sets. */ - TER rippleCalculate (); + TER rippleCalculate (detail::FlowDebugInfo* flowDebugInfo=nullptr); /** Add a single PathState. Returns true on success.*/ bool addPathState(STPath const&, TER&); diff --git a/src/ripple/app/paths/impl/AmountSpec.h b/src/ripple/app/paths/impl/AmountSpec.h index 10ba1336b..57be8e009 100644 --- a/src/ripple/app/paths/impl/AmountSpec.h +++ b/src/ripple/app/paths/impl/AmountSpec.h @@ -182,6 +182,15 @@ toAmountSpec (STAmount const& amt) return result; } +inline +EitherAmount +toEitherAmount (STAmount const& amt) +{ + if (isXRP (amt)) + return EitherAmount{amt.xrp()}; + return EitherAmount{amt.iou()}; +} + inline AmountSpec toAmountSpec ( diff --git a/src/ripple/app/paths/impl/FlowDebugInfo.h b/src/ripple/app/paths/impl/FlowDebugInfo.h new file mode 100644 index 000000000..62d484190 --- /dev/null +++ b/src/ripple/app/paths/impl/FlowDebugInfo.h @@ -0,0 +1,372 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 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. +*/ +//============================================================================== + +#ifndef RIPPLE_PATH_IMPL_FLOWDEBUGINFO_H_INCLUDED +#define RIPPLE_PATH_IMPL_FLOWDEBUGINFO_H_INCLUDED + +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace ripple +{ +namespace path +{ +namespace detail +{ +// Track performance information of a single payment +struct FlowDebugInfo +{ + using clock = std::chrono::high_resolution_clock; + using time_point = clock::time_point; + boost::container::flat_map> + timePoints; + boost::container::flat_map counts; + + struct PassInfo + { + PassInfo () = delete; + PassInfo (bool nativeIn_, bool nativeOut_) + : nativeIn (nativeIn_), nativeOut (nativeOut_) + { + } + bool const nativeIn; + bool const nativeOut; + std::vector in; + std::vector out; + std::vector numActive; + + std::vector> liquiditySrcIn; + std::vector> liquiditySrcOut; + + void + reserve (size_t s) + { + in.reserve (s); + out.reserve (s); + liquiditySrcIn.reserve(s); + liquiditySrcOut.reserve(s); + numActive.reserve (s); + } + + size_t + size () const + { + return in.size (); + } + + void + push_back (EitherAmount const& in_amt, + EitherAmount const& out_amt, + std::size_t active) + { + in.push_back (in_amt); + out.push_back (out_amt); + numActive.push_back (active); + } + + void + pushLiquiditySrc (EitherAmount const& in, EitherAmount const& out) + { + assert(!liquiditySrcIn.empty()); + liquiditySrcIn.back().push_back(in); + liquiditySrcOut.back().push_back(out); + } + + void + newLiquidityPass() + { + auto const s = liquiditySrcIn.size(); + size_t const r = !numActive.empty() ? numActive.back() : 16; + liquiditySrcIn.resize(s+1); + liquiditySrcIn.back().reserve(r); + liquiditySrcOut.resize(s+1); + liquiditySrcOut.back().reserve(r); + } + }; + + PassInfo passInfo; + + FlowDebugInfo () = delete; + FlowDebugInfo (bool nativeIn, bool nativeOut) + : passInfo (nativeIn, nativeOut) + { + timePoints.reserve (16); + counts.reserve (16); + passInfo.reserve (64); + } + + auto + duration (std::string const& tag) const + { + auto i = timePoints.find (tag); + if (i == timePoints.end ()) + { + assert (0); + return std::chrono::duration(0); + } + auto const& t = i->second; + return std::chrono::duration_cast> ( + t.second - t.first); + } + + std::size_t + count (std::string const& tag) const + { + auto i = counts.find (tag); + if (i == counts.end ()) + return 0; + return i->second; + } + + // Time the duration of the existence of the result + auto + timeBlock (std::string name) + { + struct Stopper + { + std::string tag; + FlowDebugInfo* info; + Stopper (std::string name, FlowDebugInfo& pi) + : tag (std::move (name)), info (&pi) + { + auto const start = FlowDebugInfo::clock::now (); + info->timePoints.emplace (tag, std::make_pair (start, start)); + } + ~Stopper () + { + auto const end = FlowDebugInfo::clock::now (); + info->timePoints[tag].second = end; + } + }; + return Stopper (std::move (name), *this); + } + + void + inc (std::string const& tag) + { + auto i = counts.find (tag); + if (i == counts.end ()) + { + counts[tag] = 1; + } + ++i->second; + } + + void + setCount (std::string const& tag, std::size_t c) + { + counts[tag] = c; + } + + std::size_t + passCount () const + { + return passInfo.size (); + } + + void + pushPass (EitherAmount const& in, + EitherAmount const& out, + std::size_t activeStrands) + { + passInfo.push_back (in, out, activeStrands); + } + + void + pushLiquiditySrc (EitherAmount const& in, EitherAmount const& out) + { + passInfo.pushLiquiditySrc (in, out); + } + + void + newLiquidityPass () + { + passInfo.newLiquidityPass (); + } + + std::string + to_string (bool writePassInfo) const + { + std::ostringstream ostr; + + auto const d = duration ("main"); + + ostr << "duration: " << d.count () << ", pass_count: " << passCount (); + + if (writePassInfo) + { + auto write_list = [&ostr](auto const& vals, auto&& fun, char delim=';') { + ostr << '['; + if (!vals.empty ()) + { + ostr << fun (vals[0]); + for (size_t i = 1, e = vals.size (); i < e; ++i) + ostr << delim << fun (vals[i]); + } + ostr << ']'; + }; + auto writeXrpAmtList = [&ostr, &write_list]( + std::vector const& amts, char delim=';') { + auto get_val = [](EitherAmount const& a) -> std::string { + return ripple::to_string (a.xrp); + }; + write_list (amts, get_val, delim); + }; + auto writeIouAmtList = [&ostr, &write_list]( + std::vector const& amts, char delim=';') { + auto get_val = [](EitherAmount const& a) -> std::string { + return ripple::to_string (a.iou); + }; + write_list (amts, get_val, delim); + }; + auto writeIntList = [&ostr, &write_list]( + std::vector const& vals, char delim=';') { + auto get_val = []( + size_t const& v) -> size_t const& { return v; }; + write_list (vals, get_val); + }; + auto writeNestedIouAmtList = [&ostr, &writeIouAmtList]( + std::vector> const& amts) { + ostr << '['; + if (!amts.empty ()) + { + writeIouAmtList(amts[0], '|'); + for (size_t i = 1, e = amts.size (); i < e; ++i) + { + ostr << ';'; + writeIouAmtList(amts[i], '|'); + } + } + ostr << ']'; + }; + auto writeNestedXrpAmtList = [&ostr, &writeXrpAmtList]( + std::vector> const& amts) { + ostr << '['; + if (!amts.empty ()) + { + writeXrpAmtList(amts[0], '|'); + for (size_t i = 1, e = amts.size (); i < e; ++i) + { + ostr << ';'; + writeXrpAmtList(amts[i], '|'); + } + } + ostr << ']'; + }; + + ostr << ", in_pass: "; + if (passInfo.nativeIn) + writeXrpAmtList (passInfo.in); + else + writeIouAmtList (passInfo.in); + ostr << ", out_pass: "; + if (passInfo.nativeOut) + writeXrpAmtList (passInfo.out); + else + writeIouAmtList (passInfo.out); + ostr << ", num_active: "; + writeIntList (passInfo.numActive); + if (!passInfo.liquiditySrcIn.empty () && + !passInfo.liquiditySrcIn.back ().empty ()) + { + ostr << ", l_src_in: "; + if (passInfo.nativeIn) + writeNestedXrpAmtList (passInfo.liquiditySrcIn); + else + writeNestedIouAmtList (passInfo.liquiditySrcIn); + ostr << ", l_src_out: "; + if (passInfo.nativeOut) + writeNestedXrpAmtList (passInfo.liquiditySrcOut); + else + writeNestedIouAmtList (passInfo.liquiditySrcOut); + } + } + + return ostr.str (); + } +}; + +inline +void +writeDiffElement (std::ostringstream& ostr, + std::pair, STAmount> const& elem) +{ + using namespace std; + auto const k = elem.first; + auto const v = elem.second; + ostr << '[' << get<0> (k) << '|' << get<1> (k) << '|' << get<2> (k) << '|' + << v << ']'; +}; + +template +void +writeDiffs (std::ostringstream& ostr, Iter begin, Iter end) +{ + ostr << '['; + if (begin != end) + { + writeDiffElement (ostr, *begin); + ++begin; + } + for (; begin != end; ++begin) + { + ostr << ';'; + writeDiffElement (ostr, *begin); + } + ostr << ']'; +}; + +using BalanceDiffs = std::pair< + std::map, STAmount>, + XRPAmount>; + +inline +BalanceDiffs +balanceDiffs(PaymentSandbox const& sb, ReadView const& rv) +{ + return {sb.balanceChanges (rv), sb.xrpDestroyed ()}; +} + +inline +std::string +balanceDiffsToString (boost::optional const& bd) +{ + if (!bd) + return std::string{}; + auto const& diffs = bd->first; + auto const& xrpDestroyed = bd->second; + std::ostringstream ostr; + ostr << ", xrpDestroyed: " << to_string (xrpDestroyed); + ostr << ", balanceDiffs: "; + writeDiffs (ostr, diffs.begin (), diffs.end ()); + return ostr.str (); +}; + +} // detail +} // path +} // ripple +#endif diff --git a/src/ripple/app/paths/impl/StrandFlow.h b/src/ripple/app/paths/impl/StrandFlow.h index 464321dc4..9b1f62cfd 100644 --- a/src/ripple/app/paths/impl/StrandFlow.h +++ b/src/ripple/app/paths/impl/StrandFlow.h @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -330,6 +331,11 @@ public: { return cur_.end (); } + + auto size () const + { + return cur_.size (); + } }; /* @@ -355,7 +361,8 @@ flow (PaymentSandbox const& baseView, bool partialPayment, boost::optional const& limitQuality, boost::optional const& sendMaxST, - beast::Journal j) + beast::Journal j, + path::detail::FlowDebugInfo* flowDebugInfo=nullptr) { using Result = FlowResult; @@ -434,6 +441,7 @@ flow (PaymentSandbox const& baseView, boost::container::flat_set ofrsToRm; boost::optional best; + if (flowDebugInfo) flowDebugInfo->newLiquidityPass(); for (auto strand : activeStrands) { auto f = flow ( @@ -446,6 +454,9 @@ flow (PaymentSandbox const& baseView, if (f.ter != tesSUCCESS || f.out == beast::zero) continue; + if (flowDebugInfo) + flowDebugInfo->pushLiquiditySrc(EitherAmount(f.in), EitherAmount(f.out)); + assert (f.out <= remainingOut && f.sandbox && (!remainingIn || f.in <= *remainingIn)); @@ -483,6 +494,10 @@ flow (PaymentSandbox const& baseView, if (sendMax) remainingIn = *sendMax - sum (savedIns); + if (flowDebugInfo) + flowDebugInfo->pushPass (EitherAmount (best->in), + EitherAmount (best->out), activeStrands.size ()); + JLOG (j.trace()) << "Best path: in: " << to_string (best->in) << " out: " << to_string (best->out) diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 7acd6daac..8b867f7a4 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -41,6 +41,7 @@ extern uint256 const featureTrustSetAuth; extern uint256 const featureFeeEscalation; extern uint256 const featureFlowV2; extern uint256 const featureOwnerPaysFee; +extern uint256 const featureCompareFlowV1V2; } // ripple diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index dfe15456b..cd1b1f510 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -52,5 +52,6 @@ uint256 const featureTrustSetAuth = feature("TrustSetAuth"); uint256 const featureFeeEscalation = feature("FeeEscalation"); uint256 const featureFlowV2 = feature("FlowV2"); uint256 const featureOwnerPaysFee = feature("OwnerPaysFee"); +uint256 const featureCompareFlowV1V2 = feature("CompareFlowV1V2"); } // ripple