mirror of
https://github.com/Xahau/xahaud.git
synced 2025-11-30 07:25:50 +00:00
Implement transaction invariant checks (RIPD-1425):
Add new functionality to enforce one or more sanity checks (invariants) on transactions. Add tests for each new invariant check. Allow for easily adding additional invariant checks in the future. Also Resolves ------------- - RIPD-1426 - RIPD-1427 - RIPD-1428 - RIPD-1429 - RIPD-1430 - RIPD-1431 - RIPD-1432 Release Notes ------------- Creates a new ammendment named "EnforceInvariants" which must be enabled in order for these new checks to run on each transaction.
This commit is contained in:
committed by
Nik Bougalis
parent
e52614ac81
commit
026a249173
@@ -1311,6 +1311,12 @@
|
||||
</ClCompile>
|
||||
<ClInclude Include="..\..\src\ripple\app\tx\impl\Escrow.h">
|
||||
</ClInclude>
|
||||
<ClCompile Include="..\..\src\ripple\app\tx\impl\InvariantCheck.cpp">
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
|
||||
</ClCompile>
|
||||
<ClInclude Include="..\..\src\ripple\app\tx\impl\InvariantCheck.h">
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\..\src\ripple\app\tx\impl\Offer.h">
|
||||
</ClInclude>
|
||||
<ClCompile Include="..\..\src\ripple\app\tx\impl\OfferStream.cpp">
|
||||
@@ -4699,6 +4705,10 @@
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\src\test\ledger\Invariants_test.cpp">
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\src\test\ledger\PaymentSandbox_test.cpp">
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
|
||||
|
||||
@@ -1833,6 +1833,12 @@
|
||||
<ClInclude Include="..\..\src\ripple\app\tx\impl\Escrow.h">
|
||||
<Filter>ripple\app\tx\impl</Filter>
|
||||
</ClInclude>
|
||||
<ClCompile Include="..\..\src\ripple\app\tx\impl\InvariantCheck.cpp">
|
||||
<Filter>ripple\app\tx\impl</Filter>
|
||||
</ClCompile>
|
||||
<ClInclude Include="..\..\src\ripple\app\tx\impl\InvariantCheck.h">
|
||||
<Filter>ripple\app\tx\impl</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\..\src\ripple\app\tx\impl\Offer.h">
|
||||
<Filter>ripple\app\tx\impl</Filter>
|
||||
</ClInclude>
|
||||
@@ -5448,6 +5454,9 @@
|
||||
<ClCompile Include="..\..\src\test\ledger\Directory_test.cpp">
|
||||
<Filter>test\ledger</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\src\test\ledger\Invariants_test.cpp">
|
||||
<Filter>test\ledger</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\src\test\ledger\PaymentSandbox_test.cpp">
|
||||
<Filter>test\ledger</Filter>
|
||||
</ClCompile>
|
||||
|
||||
@@ -117,6 +117,7 @@ INPUT = \
|
||||
../src/ripple/app/consensus/RCLCxLedger.h \
|
||||
../src/ripple/app/consensus/RCLConsensus.h \
|
||||
../src/ripple/app/consensus/RCLCxPeerPos.h \
|
||||
../src/ripple/app/tx/impl/InvariantCheck.h \
|
||||
|
||||
INPUT_ENCODING = UTF-8
|
||||
FILE_PATTERNS =
|
||||
|
||||
@@ -52,7 +52,8 @@ supportedAmendments ()
|
||||
{ "E2E6F2866106419B88C50045ACE96368558C345566AC8F2BDF5A5B5587F0E6FA fix1368" },
|
||||
{ "07D43DCE529B15A10827E5E04943B496762F9A88E3268269D69C44BE49E21104 Escrow" },
|
||||
{ "86E83A7D2ECE3AD5FA87AB2195AE015C950469ABF0B72EAACED318F74886AE90 CryptoConditionsSuite" },
|
||||
{ "48C4451D6C6A138453F056EB6793AFF4B5C57457A37BA63EF3541FF8CE873DC2 ToStrandV2"}
|
||||
{ "48C4451D6C6A138453F056EB6793AFF4B5C57457A37BA63EF3541FF8CE873DC2 ToStrandV2"},
|
||||
{ "DC9CA96AEA1DCF83E527D1AFC916EFAF5D27388ECA4060A88817C1238CAEE0BF EnforceInvariants" }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,10 +19,12 @@
|
||||
|
||||
#include <BeastConfig.h>
|
||||
#include <ripple/app/tx/impl/ApplyContext.h>
|
||||
#include <ripple/app/tx/impl/InvariantCheck.h>
|
||||
#include <ripple/app/tx/impl/Transactor.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/json/to_string.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <cassert>
|
||||
|
||||
namespace ripple {
|
||||
@@ -69,4 +71,56 @@ ApplyContext::visit (std::function <void (
|
||||
view_->visit(base_, func);
|
||||
}
|
||||
|
||||
template<std::size_t... Is>
|
||||
TER
|
||||
ApplyContext::checkInvariantsHelper(TER terResult, std::index_sequence<Is...>)
|
||||
{
|
||||
if (view_->rules().enabled(featureEnforceInvariants))
|
||||
{
|
||||
auto checkers = getInvariantChecks();
|
||||
|
||||
// call each check's per-entry method
|
||||
visit (
|
||||
[&checkers](
|
||||
uint256 const& index,
|
||||
bool isDelete,
|
||||
std::shared_ptr <SLE const> const& before,
|
||||
std::shared_ptr <SLE const> const& after)
|
||||
{
|
||||
// Sean Parent for_each_argument trick
|
||||
(void)std::array<int, sizeof...(Is)>{
|
||||
{((std::get<Is>(checkers).
|
||||
visitEntry(index, isDelete, before, after)), 0)...}
|
||||
};
|
||||
});
|
||||
|
||||
// Sean Parent for_each_argument trick
|
||||
// (a fold expression with `&&` would be really nice here when we move
|
||||
// to C++-17)
|
||||
std::array<bool, sizeof...(Is)> finalizers {{
|
||||
std::get<Is>(checkers).finalize(tx, terResult, journal)...}};
|
||||
|
||||
// call each check's finalizer to see that it passes
|
||||
if (! std::all_of( finalizers.cbegin(), finalizers.cend(),
|
||||
[](auto const& b) { return b; }))
|
||||
{
|
||||
terResult = (terResult == tecINVARIANT_FAILED) ?
|
||||
tefINVARIANT_FAILED :
|
||||
tecINVARIANT_FAILED ;
|
||||
JLOG(journal.error()) <<
|
||||
"Transaction has failed one or more invariants: " <<
|
||||
to_string(tx.getJson (0));
|
||||
}
|
||||
}
|
||||
|
||||
return terResult;
|
||||
}
|
||||
|
||||
TER
|
||||
ApplyContext::checkInvariants(TER terResult)
|
||||
{
|
||||
return checkInvariantsHelper(
|
||||
terResult, std::make_index_sequence<std::tuple_size<InvariantChecks>::value>{});
|
||||
}
|
||||
|
||||
} // ripple
|
||||
|
||||
@@ -101,7 +101,13 @@ public:
|
||||
view_->rawDestroyXRP(fee);
|
||||
}
|
||||
|
||||
TER
|
||||
checkInvariants(TER);
|
||||
|
||||
private:
|
||||
template<std::size_t... Is>
|
||||
TER checkInvariantsHelper(TER terResult, std::index_sequence<Is...>);
|
||||
|
||||
OpenView& base_;
|
||||
ApplyFlags flags_;
|
||||
boost::optional<ApplyViewImpl> view_;
|
||||
|
||||
191
src/ripple/app/tx/impl/InvariantCheck.cpp
Normal file
191
src/ripple/app/tx/impl/InvariantCheck.cpp
Normal file
@@ -0,0 +1,191 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2012-2016 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/app/tx/impl/InvariantCheck.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
void
|
||||
XRPNotCreated::visitEntry(
|
||||
uint256 const&,
|
||||
bool isDelete,
|
||||
std::shared_ptr <SLE const> const& before,
|
||||
std::shared_ptr <SLE const> const& after)
|
||||
{
|
||||
if(before)
|
||||
{
|
||||
switch (before->getType())
|
||||
{
|
||||
case ltACCOUNT_ROOT:
|
||||
drops_ -= (*before)[sfBalance].xrp().drops();
|
||||
break;
|
||||
case ltPAYCHAN:
|
||||
drops_ -= ((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops();
|
||||
break;
|
||||
case ltESCROW:
|
||||
drops_ -= (*before)[sfAmount].xrp().drops();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(after)
|
||||
{
|
||||
switch (after->getType())
|
||||
{
|
||||
case ltACCOUNT_ROOT:
|
||||
drops_ += (*after)[sfBalance].xrp().drops();
|
||||
break;
|
||||
case ltPAYCHAN:
|
||||
if (! isDelete)
|
||||
drops_ += ((*after)[sfAmount] - (*after)[sfBalance]).xrp().drops();
|
||||
break;
|
||||
case ltESCROW:
|
||||
if (! isDelete)
|
||||
drops_ += (*after)[sfAmount].xrp().drops();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
XRPNotCreated::finalize(STTx const& tx, TER /*tec*/, beast::Journal const& j)
|
||||
{
|
||||
auto fee = tx.getFieldAmount(sfFee).xrp().drops();
|
||||
if(-1*fee <= drops_ && drops_ <= 0)
|
||||
return true;
|
||||
|
||||
JLOG(j.fatal()) << "Invariant failed: XRP net change was " << drops_ <<
|
||||
" on a fee of " << fee;
|
||||
return false;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
void
|
||||
AccountRootsNotDeleted::visitEntry(
|
||||
uint256 const&,
|
||||
bool isDelete,
|
||||
std::shared_ptr <SLE const> const& before,
|
||||
std::shared_ptr <SLE const> const&)
|
||||
{
|
||||
if (isDelete && before && before->getType() == ltACCOUNT_ROOT)
|
||||
accountDeleted_ = true;
|
||||
}
|
||||
|
||||
bool
|
||||
AccountRootsNotDeleted::finalize(STTx const&, TER, beast::Journal const& j)
|
||||
{
|
||||
if (! accountDeleted_)
|
||||
return true;
|
||||
|
||||
JLOG(j.fatal()) << "Invariant failed: an account root was deleted";
|
||||
return false;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
void
|
||||
LedgerEntryTypesMatch::visitEntry(
|
||||
uint256 const&,
|
||||
bool,
|
||||
std::shared_ptr <SLE const> const& before,
|
||||
std::shared_ptr <SLE const> const& after)
|
||||
{
|
||||
if (before && after && before->getType() != after->getType())
|
||||
typeMismatch_ = true;
|
||||
|
||||
if (after)
|
||||
{
|
||||
switch (after->getType())
|
||||
{
|
||||
case ltACCOUNT_ROOT:
|
||||
case ltDIR_NODE:
|
||||
case ltRIPPLE_STATE:
|
||||
case ltTICKET:
|
||||
case ltSIGNER_LIST:
|
||||
case ltOFFER:
|
||||
case ltLEDGER_HASHES:
|
||||
case ltAMENDMENTS:
|
||||
case ltFEE_SETTINGS:
|
||||
case ltESCROW:
|
||||
case ltPAYCHAN:
|
||||
break;
|
||||
default:
|
||||
invalidTypeAdded_ = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
LedgerEntryTypesMatch::finalize(STTx const&, TER, beast::Journal const& j)
|
||||
{
|
||||
if ((! typeMismatch_) && (! invalidTypeAdded_))
|
||||
return true;
|
||||
|
||||
if (typeMismatch_)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: ledger entry type mismatch";
|
||||
}
|
||||
|
||||
if (invalidTypeAdded_)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: invalid ledger entry type added";
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
void
|
||||
NoXRPTrustLines::visitEntry(
|
||||
uint256 const&,
|
||||
bool,
|
||||
std::shared_ptr <SLE const> const&,
|
||||
std::shared_ptr <SLE const> const& after)
|
||||
{
|
||||
if (after && after->getType() == ltRIPPLE_STATE)
|
||||
{
|
||||
// checking the issue directly here instead of
|
||||
// relying on .native() just in case native somehow
|
||||
// were systematically incorrect
|
||||
xrpTrustLine_ =
|
||||
after->getFieldAmount (sfLowLimit).issue() == xrpIssue() ||
|
||||
after->getFieldAmount (sfHighLimit).issue() == xrpIssue();
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
NoXRPTrustLines::finalize(STTx const&, TER, beast::Journal const& j)
|
||||
{
|
||||
if (! xrpTrustLine_)
|
||||
return true;
|
||||
|
||||
JLOG(j.fatal()) << "Invariant failed: an XRP trust line was created";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // ripple
|
||||
|
||||
204
src/ripple/app/tx/impl/InvariantCheck.h
Normal file
204
src/ripple/app/tx/impl/InvariantCheck.h
Normal file
@@ -0,0 +1,204 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2012-2017 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_APP_TX_INVARIANTCHECK_H_INCLUDED
|
||||
#define RIPPLE_APP_TX_INVARIANTCHECK_H_INCLUDED
|
||||
|
||||
#include <ripple/basics/base_uint.h>
|
||||
#include <ripple/protocol/STLedgerEntry.h>
|
||||
#include <ripple/protocol/STTx.h>
|
||||
#include <ripple/protocol/TER.h>
|
||||
#include <ripple/beast/utility/Journal.h>
|
||||
#include <tuple>
|
||||
#include <cstdint>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
#if GENERATING_DOCS
|
||||
/**
|
||||
* @brief Prototype for invariant check implementations.
|
||||
*
|
||||
* __THIS CLASS DOES NOT EXIST__ - or rather it exists in documentation only to
|
||||
* communicate the interface required of any invariant checker. Any invariant
|
||||
* check implementation should implement the public methods documented here.
|
||||
*
|
||||
*/
|
||||
class InvariantChecker_PROTOTYPE
|
||||
{
|
||||
public:
|
||||
|
||||
/**
|
||||
* @brief called for each ledger entry in the current transaction.
|
||||
*
|
||||
* @param index the key (identifier) for the ledger entry
|
||||
* @param isDelete true if the SLE is being deleted
|
||||
* @param before ledger entry before modification by the transaction
|
||||
* @param after ledger entry after modification by the transaction
|
||||
*/
|
||||
void
|
||||
visitEntry(
|
||||
uint256 const& index,
|
||||
bool isDelete,
|
||||
std::shared_ptr<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> const& after);
|
||||
|
||||
/**
|
||||
* @brief called after all ledger entries have been visited to determine
|
||||
* the final status of the check
|
||||
*
|
||||
* @param tx the transaction being applied
|
||||
* @param tec the current TER result of the transaction
|
||||
* @param j journal for logging
|
||||
*
|
||||
* @return true if check passes, false if it fails
|
||||
*/
|
||||
bool
|
||||
finalize(
|
||||
STTx const& tx,
|
||||
TER tec,
|
||||
beast::Journal const& j);
|
||||
};
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Invariant: A transaction must not create XRP and should only destroy
|
||||
* XRP, up to the transaction fee.
|
||||
*
|
||||
* For this check, we start with a signed 64-bit integer set to zero. As we go
|
||||
* through the ledger entries, look only at account roots, escrow payments,
|
||||
* and payment channels. Remove from the total any previous XRP values and add
|
||||
* to the total any new XRP values. The net balance of a payment channel is
|
||||
* computed from two fields (amount and balance) and deletions are ignored
|
||||
* for paychan and escrow because the amount fields have not been adjusted for
|
||||
* those in the case of deletion.
|
||||
*
|
||||
* The final total must be less than or equal to zero and greater than or equal
|
||||
* to the negative of the tx fee.
|
||||
*
|
||||
*/
|
||||
class XRPNotCreated
|
||||
{
|
||||
std::int64_t drops_ = 0;
|
||||
|
||||
public:
|
||||
|
||||
void
|
||||
visitEntry(
|
||||
uint256 const&,
|
||||
bool,
|
||||
std::shared_ptr<SLE const> const&,
|
||||
std::shared_ptr<SLE const> const&);
|
||||
|
||||
bool
|
||||
finalize(STTx const&, TER, beast::Journal const&);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Invariant: we cannot remove an account ledger entry
|
||||
*
|
||||
* an account root should never be the target of a delete
|
||||
*/
|
||||
class AccountRootsNotDeleted
|
||||
{
|
||||
bool accountDeleted_ = false;
|
||||
|
||||
public:
|
||||
|
||||
void
|
||||
visitEntry(
|
||||
uint256 const&,
|
||||
bool,
|
||||
std::shared_ptr<SLE const> const&,
|
||||
std::shared_ptr<SLE const> const&);
|
||||
|
||||
bool
|
||||
finalize(STTx const&, TER, beast::Journal const&);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Invariant: corresponding modified ledger entries should match in type and
|
||||
* added entries should be a valid type.
|
||||
*
|
||||
*/
|
||||
class LedgerEntryTypesMatch
|
||||
{
|
||||
bool typeMismatch_ = false;
|
||||
bool invalidTypeAdded_ = false;
|
||||
|
||||
public:
|
||||
|
||||
void
|
||||
visitEntry(
|
||||
uint256 const&,
|
||||
bool,
|
||||
std::shared_ptr<SLE const> const&,
|
||||
std::shared_ptr<SLE const> const&);
|
||||
|
||||
bool
|
||||
finalize(STTx const&, TER, beast::Journal const&);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Invariant: Trust lines using XRP are not allowed.
|
||||
*
|
||||
*/
|
||||
class NoXRPTrustLines
|
||||
{
|
||||
bool xrpTrustLine_ = false;
|
||||
|
||||
public:
|
||||
|
||||
void
|
||||
visitEntry(
|
||||
uint256 const&,
|
||||
bool,
|
||||
std::shared_ptr<SLE const> const&,
|
||||
std::shared_ptr<SLE const> const&);
|
||||
|
||||
bool
|
||||
finalize(STTx const&, TER, beast::Journal const&);
|
||||
};
|
||||
|
||||
// additional invariant checks can be declared above and then added to this
|
||||
// tuple
|
||||
using InvariantChecks = std::tuple<
|
||||
AccountRootsNotDeleted,
|
||||
LedgerEntryTypesMatch,
|
||||
XRPNotCreated,
|
||||
NoXRPTrustLines
|
||||
>;
|
||||
|
||||
/**
|
||||
* @brief get a tuple of all invariant checks
|
||||
*
|
||||
* @return std::tuple of instances that implement the required invariant check
|
||||
* methods
|
||||
*
|
||||
* @see ripple::InvariantChecker_PROTOTYPE
|
||||
*/
|
||||
inline
|
||||
InvariantChecks
|
||||
getInvariantChecks()
|
||||
{
|
||||
return InvariantChecks{};
|
||||
}
|
||||
|
||||
} //ripple
|
||||
|
||||
#endif
|
||||
@@ -568,6 +568,33 @@ void removeUnfundedOffers (ApplyView& view, std::vector<uint256> const& offers,
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Transactor::claimFee (XRPAmount& fee, TER terResult, std::vector<uint256> const& removedOffers)
|
||||
{
|
||||
ctx_.discard();
|
||||
|
||||
auto const txnAcct = view().peek(
|
||||
keylet::account(ctx_.tx.getAccountID(sfAccount)));
|
||||
|
||||
auto const balance = txnAcct->getFieldAmount (sfBalance).xrp ();
|
||||
|
||||
// balance should have already been
|
||||
// checked in checkFee / preFlight.
|
||||
assert(balance != zero && (!view().open() || balance >= fee));
|
||||
// We retry/reject the transaction if the account
|
||||
// balance is zero or we're applying against an open
|
||||
// ledger and the balance is less than the fee
|
||||
if (fee > balance)
|
||||
fee = balance;
|
||||
txnAcct->setFieldAmount (sfBalance, balance - fee);
|
||||
txnAcct->setFieldU32 (sfSequence, ctx_.tx.getSequence() + 1);
|
||||
|
||||
if (terResult == tecOVERSIZE)
|
||||
removeUnfundedOffers (view(), removedOffers, ctx_.app.journal ("View"));
|
||||
|
||||
view().update (txnAcct);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
std::pair<TER, bool>
|
||||
Transactor::operator()()
|
||||
@@ -655,35 +682,24 @@ Transactor::operator()()
|
||||
});
|
||||
}
|
||||
|
||||
ctx_.discard();
|
||||
|
||||
auto const txnAcct = view().peek(
|
||||
keylet::account(ctx_.tx.getAccountID(sfAccount)));
|
||||
|
||||
std::uint32_t t_seq = ctx_.tx.getSequence ();
|
||||
|
||||
auto const balance = txnAcct->getFieldAmount (sfBalance).xrp ();
|
||||
|
||||
// balance should have already been
|
||||
// checked in checkFee / preFlight.
|
||||
assert(balance != zero && (!view().open() || balance >= fee));
|
||||
// We retry/reject the transaction if the account
|
||||
// balance is zero or we're applying against an open
|
||||
// ledger and the balance is less than the fee
|
||||
if (fee > balance)
|
||||
fee = balance;
|
||||
txnAcct->setFieldAmount (sfBalance, balance - fee);
|
||||
txnAcct->setFieldU32 (sfSequence, t_seq + 1);
|
||||
|
||||
if (terResult == tecOVERSIZE)
|
||||
removeUnfundedOffers (view(), removedOffers, ctx_.app.journal ("View"));
|
||||
|
||||
view().update (txnAcct);
|
||||
claimFee(fee, terResult, removedOffers);
|
||||
didApply = true;
|
||||
}
|
||||
else if (!didApply)
|
||||
|
||||
if (didApply)
|
||||
{
|
||||
JLOG(j_.debug()) << "Not applying transaction " << txID;
|
||||
// Check invariants
|
||||
// if `tecINVARIANT_FAILED` not returned, we can proceed to apply the tx
|
||||
terResult = ctx_.checkInvariants(terResult);
|
||||
if (terResult == tecINVARIANT_FAILED)
|
||||
{
|
||||
// if invariants failed, claim a fee still
|
||||
claimFee(fee, terResult, {});
|
||||
//Check invariants *again* to ensure the fee claiming doesn't
|
||||
//violate invariants.
|
||||
terResult = ctx_.checkInvariants(terResult);
|
||||
didApply = isTecClaim(terResult);
|
||||
}
|
||||
}
|
||||
|
||||
if (didApply)
|
||||
@@ -691,7 +707,7 @@ Transactor::operator()()
|
||||
// Transaction succeeded fully or (retries are
|
||||
// not allowed and the transaction could claim a fee)
|
||||
|
||||
if(!view().open())
|
||||
if (!view().open())
|
||||
{
|
||||
// Charge whatever fee they specified.
|
||||
|
||||
@@ -711,6 +727,11 @@ Transactor::operator()()
|
||||
// since we called apply(), it is not okay to look
|
||||
// at view() past this point.
|
||||
}
|
||||
else
|
||||
{
|
||||
JLOG(j_.debug()) << "Not applying transaction " << txID;
|
||||
}
|
||||
|
||||
|
||||
JLOG(j_.trace()) <<
|
||||
"apply: " << transToken(terResult) <<
|
||||
|
||||
@@ -168,6 +168,7 @@ protected:
|
||||
private:
|
||||
void setSeq ();
|
||||
TER payFee ();
|
||||
void claimFee (XRPAmount& fee, TER terResult, std::vector<uint256> const& removedOffers);
|
||||
static TER checkSingleSign (PreclaimContext const& ctx);
|
||||
static TER checkMultiSign (PreclaimContext const& ctx);
|
||||
};
|
||||
|
||||
@@ -49,6 +49,7 @@ extern uint256 const fix1368;
|
||||
extern uint256 const featureEscrow;
|
||||
extern uint256 const featureCryptoConditionsSuite;
|
||||
extern uint256 const featureToStrandV2;
|
||||
extern uint256 const featureEnforceInvariants;
|
||||
|
||||
} // ripple
|
||||
|
||||
|
||||
@@ -25,10 +25,14 @@
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class Invariants_test;
|
||||
|
||||
class STLedgerEntry final
|
||||
: public STObject
|
||||
, public CountedObject <STLedgerEntry>
|
||||
{
|
||||
friend Invariants_test; // this test wants access to the private type_
|
||||
|
||||
public:
|
||||
static char const* getCountedObjectName () { return "STLedgerEntry"; }
|
||||
|
||||
|
||||
@@ -120,6 +120,7 @@ enum TER
|
||||
tefBAD_QUORUM,
|
||||
tefNOT_MULTI_SIGNING,
|
||||
tefBAD_AUTH_MASTER,
|
||||
tefINVARIANT_FAILED,
|
||||
|
||||
// -99 .. -1: R Retry
|
||||
// sequence too high, no funds for txn fee, originating -account
|
||||
@@ -206,7 +207,8 @@ enum TER
|
||||
tecDST_TAG_NEEDED = 143,
|
||||
tecINTERNAL = 144,
|
||||
tecOVERSIZE = 145,
|
||||
tecCRYPTOCONDITION_ERROR = 146
|
||||
tecCRYPTOCONDITION_ERROR = 146,
|
||||
tecINVARIANT_FAILED = 147
|
||||
};
|
||||
|
||||
inline bool isTelLocal(TER x)
|
||||
|
||||
@@ -60,5 +60,6 @@ uint256 const fix1368 = feature("fix1368");
|
||||
uint256 const featureEscrow = feature("Escrow");
|
||||
uint256 const featureCryptoConditionsSuite = feature("CryptoConditionsSuite");
|
||||
uint256 const featureToStrandV2 = feature("ToStrandV2");
|
||||
uint256 const featureEnforceInvariants = feature("EnforceInvariants");
|
||||
|
||||
} // ripple
|
||||
|
||||
@@ -64,6 +64,7 @@ bool transResultInfo (TER code, std::string& token, std::string& text)
|
||||
{ tecDST_TAG_NEEDED, { "tecDST_TAG_NEEDED", "A destination tag is required." } },
|
||||
{ tecINTERNAL, { "tecINTERNAL", "An internal error has occurred during processing." } },
|
||||
{ tecCRYPTOCONDITION_ERROR, { "tecCRYPTOCONDITION_ERROR", "Malformed, invalid, or mismatched conditional or fulfillment." } },
|
||||
{ tecINVARIANT_FAILED, { "tecINVARIANT_FAILED", "One or more invariants for the transaction were not satisfied." } },
|
||||
|
||||
{ tefALREADY, { "tefALREADY", "The exact transaction was already in this ledger." } },
|
||||
{ tefBAD_ADD_AUTH, { "tefBAD_ADD_AUTH", "Not authorized to add account." } },
|
||||
@@ -79,9 +80,10 @@ bool transResultInfo (TER code, std::string& token, std::string& text)
|
||||
{ tefMAX_LEDGER, { "tefMAX_LEDGER", "Ledger sequence too high." } },
|
||||
{ tefNO_AUTH_REQUIRED, { "tefNO_AUTH_REQUIRED", "Auth is not required." } },
|
||||
{ tefNOT_MULTI_SIGNING, { "tefNOT_MULTI_SIGNING", "Account has no appropriate list of multi-signers." } },
|
||||
{ tefPAST_SEQ, { "tefPAST_SEQ", "This sequence number has already past." } },
|
||||
{ tefPAST_SEQ, { "tefPAST_SEQ", "This sequence number has already passed." } },
|
||||
{ tefWRONG_PRIOR, { "tefWRONG_PRIOR", "This previous transaction does not match." } },
|
||||
{ tefBAD_AUTH_MASTER, { "tefBAD_AUTH_MASTER", "Auth for unclaimed account needs correct master key." } },
|
||||
{ tefINVARIANT_FAILED, { "tefINVARIANT_FAILED", "Fee claim violated invariants for the transaction." } },
|
||||
|
||||
{ telLOCAL_ERROR, { "telLOCAL_ERROR", "Local failure." } },
|
||||
{ telBAD_DOMAIN, { "telBAD_DOMAIN", "Domain too long." } },
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <ripple/app/tx/impl/CreateOffer.cpp>
|
||||
#include <ripple/app/tx/impl/CreateTicket.cpp>
|
||||
#include <ripple/app/tx/impl/Escrow.cpp>
|
||||
#include <ripple/app/tx/impl/InvariantCheck.cpp>
|
||||
#include <ripple/app/tx/impl/OfferStream.cpp>
|
||||
#include <ripple/app/tx/impl/Payment.cpp>
|
||||
#include <ripple/app/tx/impl/PayChan.cpp>
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#include <ripple/json/json_value.h>
|
||||
#include <ripple/json/to_string.h>
|
||||
#include <ripple/ledger/CachedSLEs.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
#include <ripple/protocol/Issue.h>
|
||||
#include <ripple/protocol/STAmount.h>
|
||||
@@ -53,6 +54,7 @@
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace jtx {
|
||||
@@ -156,7 +158,10 @@ public:
|
||||
{
|
||||
memoize(Account::master);
|
||||
Pathfinder::initPathTable();
|
||||
construct(std::forward<Args>(args)...);
|
||||
// enable the the invariant enforcement amendment by default.
|
||||
construct(
|
||||
features(featureEnforceInvariants),
|
||||
std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
template <class... Args>
|
||||
|
||||
236
src/test/ledger/Invariants_test.cpp
Normal file
236
src/test/ledger/Invariants_test.cpp
Normal file
@@ -0,0 +1,236 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2012-2017 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 <BeastConfig.h>
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <ripple/beast/utility/Journal.h>
|
||||
#include <ripple/app/tx/apply.h>
|
||||
#include <ripple/app/tx/impl/Transactor.h>
|
||||
#include <ripple/app/tx/impl/ApplyContext.h>
|
||||
#include <ripple/protocol/STLedgerEntry.h>
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class Invariants_test : public beast::unit_test::suite
|
||||
{
|
||||
|
||||
class TestSink : public beast::Journal::Sink
|
||||
{
|
||||
public:
|
||||
std::stringstream strm_;
|
||||
|
||||
TestSink () : Sink (beast::severities::kWarning, false) { }
|
||||
|
||||
void
|
||||
write (beast::severities::Severity level,
|
||||
std::string const& text) override
|
||||
{
|
||||
if (level < threshold())
|
||||
return;
|
||||
|
||||
strm_ << text << std::endl;
|
||||
}
|
||||
};
|
||||
|
||||
// this is common setup/method for running a failing invariant check. The
|
||||
// precheck function is used to manipulate the ApplyContext with view
|
||||
// changes that will cause the check to fail.
|
||||
void
|
||||
doInvariantCheck( bool enabled,
|
||||
std::function <
|
||||
bool (
|
||||
test::jtx::Account const& a,
|
||||
test::jtx::Account const& b,
|
||||
ApplyContext& ac)>
|
||||
const& precheck )
|
||||
{
|
||||
using namespace test::jtx;
|
||||
Env env {*this};
|
||||
if (! enabled)
|
||||
{
|
||||
auto& features = env.app().config().features;
|
||||
auto it = features.find(featureEnforceInvariants);
|
||||
if (it != features.end())
|
||||
features.erase(it);
|
||||
}
|
||||
|
||||
Account A1 {"A1"};
|
||||
Account A2 {"A2"};
|
||||
env.fund (XRP (1000), A1, A2);
|
||||
env.close();
|
||||
|
||||
// dummy/empty tx to setup the AccountContext
|
||||
auto tx = STTx {ttACCOUNT_SET, [](STObject&){ } };
|
||||
OpenView ov {*env.current()};
|
||||
TestSink sink;
|
||||
beast::Journal jlog {sink};
|
||||
ApplyContext ac {
|
||||
env.app(),
|
||||
ov,
|
||||
tx,
|
||||
tesSUCCESS,
|
||||
env.current()->fees().base,
|
||||
tapNONE,
|
||||
jlog
|
||||
};
|
||||
|
||||
BEAST_EXPECT(precheck(A1, A2, ac));
|
||||
|
||||
auto tr = ac.checkInvariants(tesSUCCESS);
|
||||
if (enabled)
|
||||
{
|
||||
BEAST_EXPECT(tr == tecINVARIANT_FAILED);
|
||||
BEAST_EXPECT(boost::starts_with(sink.strm_.str(), "Invariant failed:"));
|
||||
//uncomment if you want to log the invariant failure message
|
||||
//log << " --> " << sink.strm_.str() << std::endl;
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(tr == tesSUCCESS);
|
||||
BEAST_EXPECT(sink.strm_.str().empty());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testEnabled ()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
testcase ("feature enabled");
|
||||
Env env {*this};
|
||||
|
||||
auto hasInvariants =
|
||||
env.app().config().features.find (featureEnforceInvariants);
|
||||
BEAST_EXPECT(hasInvariants != env.app().config().features.end());
|
||||
|
||||
BEAST_EXPECT(env.current()->rules().enabled(featureEnforceInvariants));
|
||||
}
|
||||
|
||||
void
|
||||
testXRPNotCreated (bool enabled)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
testcase << "checks " << (enabled ? "enabled" : "disabled") <<
|
||||
" - XRP created";
|
||||
doInvariantCheck (enabled,
|
||||
[](Account const& A1, Account const&, ApplyContext& ac)
|
||||
{
|
||||
// put a single account in the view and "manufacture" some XRP
|
||||
auto const sle = ac.view().peek (keylet::account(A1.id()));
|
||||
if(! sle)
|
||||
return false;
|
||||
auto amt = sle->getFieldAmount (sfBalance);
|
||||
sle->setFieldAmount (sfBalance, amt + 500);
|
||||
ac.view().update (sle);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
testAccountsNotRemoved(bool enabled)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
testcase << "checks " << (enabled ? "enabled" : "disabled") <<
|
||||
" - account root removed";
|
||||
doInvariantCheck (enabled,
|
||||
[](Account const& A1, Account const&, ApplyContext& ac)
|
||||
{
|
||||
// remove an account from the view
|
||||
auto const sle = ac.view().peek (keylet::account(A1.id()));
|
||||
if(! sle)
|
||||
return false;
|
||||
ac.view().erase (sle);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
testTypesMatch(bool enabled)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
testcase << "checks " << (enabled ? "enabled" : "disabled") <<
|
||||
" - LE types don't match";
|
||||
doInvariantCheck (enabled,
|
||||
[](Account const& A1, Account const&, ApplyContext& ac)
|
||||
{
|
||||
// replace an entry in the table with an SLE of a different type
|
||||
auto const sle = ac.view().peek (keylet::account(A1.id()));
|
||||
if(! sle)
|
||||
return false;
|
||||
auto sleNew = std::make_shared<SLE> (ltTICKET, sle->key());
|
||||
ac.rawView().rawReplace (sleNew);
|
||||
return true;
|
||||
});
|
||||
|
||||
doInvariantCheck (enabled,
|
||||
[](Account const& A1, Account const&, ApplyContext& ac)
|
||||
{
|
||||
// add an entry in the table with an SLE of an invalid type
|
||||
auto const sle = ac.view().peek (keylet::account(A1.id()));
|
||||
if(! sle)
|
||||
return false;
|
||||
// make a dummy escrow ledger entry, then change the type to an
|
||||
// unsupported value so that the valid type invariant check
|
||||
// will fail.
|
||||
auto sleNew = std::make_shared<SLE> (
|
||||
keylet::escrow(A1, (*sle)[sfSequence] + 2));
|
||||
sleNew->type_ = ltNICKNAME;
|
||||
ac.view().insert (sleNew);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
testNoXRPTrustLine(bool enabled)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
testcase << "checks " << (enabled ? "enabled" : "disabled") <<
|
||||
" - trust lines with XRP not allowed";
|
||||
doInvariantCheck (enabled,
|
||||
[](Account const& A1, Account const& A2, ApplyContext& ac)
|
||||
{
|
||||
// create simple trust SLE with xrp currency
|
||||
auto index = getRippleStateIndex (A1, A2, xrpIssue().currency);
|
||||
auto const sleNew = std::make_shared<SLE>(
|
||||
ltRIPPLE_STATE, index);
|
||||
ac.view().insert (sleNew);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public:
|
||||
void run ()
|
||||
{
|
||||
testEnabled ();
|
||||
// all invariant checks are run with
|
||||
// the checks enabled and disabled
|
||||
for(auto const& b : {true, false})
|
||||
{
|
||||
testXRPNotCreated (b);
|
||||
testAccountsNotRemoved (b);
|
||||
testTypesMatch (b);
|
||||
testNoXRPTrustLine (b);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE (Invariants, ledger, ripple);
|
||||
|
||||
} // ripple
|
||||
@@ -470,15 +470,13 @@ class LedgerRPC_test : public beast::unit_test::suite
|
||||
{
|
||||
testcase("Ledger with Queued Transactions");
|
||||
using namespace test::jtx;
|
||||
Env env{ *this, []()
|
||||
{
|
||||
auto p = std::make_unique<Config>();
|
||||
test::setupConfigForUnitTests(*p);
|
||||
auto& section = p->section("transaction_queue");
|
||||
section.set("minimum_txn_in_ledger_standalone", "3");
|
||||
return p;
|
||||
}(),
|
||||
features(featureFeeEscalation) };
|
||||
Env env { *this,
|
||||
envconfig([](std::unique_ptr<Config> cfg) {
|
||||
cfg->section("transaction_queue")
|
||||
.set("minimum_txn_in_ledger_standalone", "3");
|
||||
return cfg;
|
||||
}),
|
||||
features(featureFeeEscalation)};
|
||||
|
||||
Json::Value jv;
|
||||
jv[jss::ledger_index] = "current";
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
#include <test/ledger/BookDirs_test.cpp>
|
||||
#include <test/ledger/Directory_test.cpp>
|
||||
#include <test/ledger/Invariants_test.cpp>
|
||||
#include <test/ledger/PaymentSandbox_test.cpp>
|
||||
#include <test/ledger/PendingSaves_test.cpp>
|
||||
#include <test/ledger/SHAMapV2_test.cpp>
|
||||
|
||||
Reference in New Issue
Block a user