diff --git a/Builds/VisualStudio2015/RippleD.vcxproj b/Builds/VisualStudio2015/RippleD.vcxproj index 18cb38e2e..1ef62f4db 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj +++ b/Builds/VisualStudio2015/RippleD.vcxproj @@ -1311,6 +1311,12 @@ + + True + True + + + @@ -4699,6 +4705,10 @@ True True + + True + True + True True diff --git a/Builds/VisualStudio2015/RippleD.vcxproj.filters b/Builds/VisualStudio2015/RippleD.vcxproj.filters index 097689aec..b6b237c0b 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj.filters +++ b/Builds/VisualStudio2015/RippleD.vcxproj.filters @@ -1833,6 +1833,12 @@ ripple\app\tx\impl + + ripple\app\tx\impl + + + ripple\app\tx\impl + ripple\app\tx\impl @@ -5448,6 +5454,9 @@ test\ledger + + test\ledger + test\ledger diff --git a/docs/source.dox b/docs/source.dox index 5c5affb66..6dd9f77a6 100644 --- a/docs/source.dox +++ b/docs/source.dox @@ -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 = diff --git a/src/ripple/app/main/Amendments.cpp b/src/ripple/app/main/Amendments.cpp index d470eaf61..11e774d92 100644 --- a/src/ripple/app/main/Amendments.cpp +++ b/src/ripple/app/main/Amendments.cpp @@ -52,7 +52,8 @@ supportedAmendments () { "E2E6F2866106419B88C50045ACE96368558C345566AC8F2BDF5A5B5587F0E6FA fix1368" }, { "07D43DCE529B15A10827E5E04943B496762F9A88E3268269D69C44BE49E21104 Escrow" }, { "86E83A7D2ECE3AD5FA87AB2195AE015C950469ABF0B72EAACED318F74886AE90 CryptoConditionsSuite" }, - { "48C4451D6C6A138453F056EB6793AFF4B5C57457A37BA63EF3541FF8CE873DC2 ToStrandV2"} + { "48C4451D6C6A138453F056EB6793AFF4B5C57457A37BA63EF3541FF8CE873DC2 ToStrandV2"}, + { "DC9CA96AEA1DCF83E527D1AFC916EFAF5D27388ECA4060A88817C1238CAEE0BF EnforceInvariants" } }; } diff --git a/src/ripple/app/tx/impl/ApplyContext.cpp b/src/ripple/app/tx/impl/ApplyContext.cpp index f18e90fa7..31ec11799 100644 --- a/src/ripple/app/tx/impl/ApplyContext.cpp +++ b/src/ripple/app/tx/impl/ApplyContext.cpp @@ -19,10 +19,12 @@ #include #include +#include #include #include #include #include +#include #include namespace ripple { @@ -69,4 +71,56 @@ ApplyContext::visit (std::function visit(base_, func); } +template +TER +ApplyContext::checkInvariantsHelper(TER terResult, std::index_sequence) +{ + 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 const& before, + std::shared_ptr const& after) + { + // Sean Parent for_each_argument trick + (void)std::array{ + {((std::get(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 finalizers {{ + std::get(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::value>{}); +} + } // ripple diff --git a/src/ripple/app/tx/impl/ApplyContext.h b/src/ripple/app/tx/impl/ApplyContext.h index 95e7db829..a4cfb8cf5 100644 --- a/src/ripple/app/tx/impl/ApplyContext.h +++ b/src/ripple/app/tx/impl/ApplyContext.h @@ -101,7 +101,13 @@ public: view_->rawDestroyXRP(fee); } + TER + checkInvariants(TER); + private: + template + TER checkInvariantsHelper(TER terResult, std::index_sequence); + OpenView& base_; ApplyFlags flags_; boost::optional view_; diff --git a/src/ripple/app/tx/impl/InvariantCheck.cpp b/src/ripple/app/tx/impl/InvariantCheck.cpp new file mode 100644 index 000000000..03a15c576 --- /dev/null +++ b/src/ripple/app/tx/impl/InvariantCheck.cpp @@ -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 +#include + +namespace ripple { + +void +XRPNotCreated::visitEntry( + uint256 const&, + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr 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 const& before, + std::shared_ptr 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 const& before, + std::shared_ptr 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 const&, + std::shared_ptr 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 + diff --git a/src/ripple/app/tx/impl/InvariantCheck.h b/src/ripple/app/tx/impl/InvariantCheck.h new file mode 100644 index 000000000..554bb9cf2 --- /dev/null +++ b/src/ripple/app/tx/impl/InvariantCheck.h @@ -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 +#include +#include +#include +#include +#include +#include + +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 const& before, + std::shared_ptr 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 const&, + std::shared_ptr 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 const&, + std::shared_ptr 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 const&, + std::shared_ptr 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 const&, + std::shared_ptr 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 diff --git a/src/ripple/app/tx/impl/Transactor.cpp b/src/ripple/app/tx/impl/Transactor.cpp index 7b62b305f..1cf50da67 100644 --- a/src/ripple/app/tx/impl/Transactor.cpp +++ b/src/ripple/app/tx/impl/Transactor.cpp @@ -568,6 +568,33 @@ void removeUnfundedOffers (ApplyView& view, std::vector const& offers, } } +void +Transactor::claimFee (XRPAmount& fee, TER terResult, std::vector 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 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) << diff --git a/src/ripple/app/tx/impl/Transactor.h b/src/ripple/app/tx/impl/Transactor.h index bb878539c..cf74ee0a9 100644 --- a/src/ripple/app/tx/impl/Transactor.h +++ b/src/ripple/app/tx/impl/Transactor.h @@ -168,6 +168,7 @@ protected: private: void setSeq (); TER payFee (); + void claimFee (XRPAmount& fee, TER terResult, std::vector const& removedOffers); static TER checkSingleSign (PreclaimContext const& ctx); static TER checkMultiSign (PreclaimContext const& ctx); }; diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 352040295..89f4de4d5 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -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 diff --git a/src/ripple/protocol/STLedgerEntry.h b/src/ripple/protocol/STLedgerEntry.h index 24a85f407..83ffdc559 100644 --- a/src/ripple/protocol/STLedgerEntry.h +++ b/src/ripple/protocol/STLedgerEntry.h @@ -25,10 +25,14 @@ namespace ripple { +class Invariants_test; + class STLedgerEntry final : public STObject , public CountedObject { + friend Invariants_test; // this test wants access to the private type_ + public: static char const* getCountedObjectName () { return "STLedgerEntry"; } diff --git a/src/ripple/protocol/TER.h b/src/ripple/protocol/TER.h index 37b893811..811de43e9 100644 --- a/src/ripple/protocol/TER.h +++ b/src/ripple/protocol/TER.h @@ -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) diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index e485daf3f..480126793 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -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 diff --git a/src/ripple/protocol/impl/TER.cpp b/src/ripple/protocol/impl/TER.cpp index fede93f6e..b4674923e 100644 --- a/src/ripple/protocol/impl/TER.cpp +++ b/src/ripple/protocol/impl/TER.cpp @@ -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." } }, diff --git a/src/ripple/unity/app_tx.cpp b/src/ripple/unity/app_tx.cpp index d1a669efa..a676fb5f3 100644 --- a/src/ripple/unity/app_tx.cpp +++ b/src/ripple/unity/app_tx.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index f7db2bbaf..2f7d3ac54 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -53,6 +54,7 @@ #include #include + namespace ripple { namespace test { namespace jtx { @@ -156,7 +158,10 @@ public: { memoize(Account::master); Pathfinder::initPathTable(); - construct(std::forward(args)...); + // enable the the invariant enforcement amendment by default. + construct( + features(featureEnforceInvariants), + std::forward(args)...); } template diff --git a/src/test/ledger/Invariants_test.cpp b/src/test/ledger/Invariants_test.cpp new file mode 100644 index 000000000..520d915a9 --- /dev/null +++ b/src/test/ledger/Invariants_test.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include + +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 (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 ( + 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( + 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 diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index f6ecbd214..596e463e2 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -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(); - 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 cfg) { + cfg->section("transaction_queue") + .set("minimum_txn_in_ledger_standalone", "3"); + return cfg; + }), + features(featureFeeEscalation)}; Json::Value jv; jv[jss::ledger_index] = "current"; diff --git a/src/test/unity/ledger_test_unity.cpp b/src/test/unity/ledger_test_unity.cpp index 9496f523f..6338fa0a7 100644 --- a/src/test/unity/ledger_test_unity.cpp +++ b/src/test/unity/ledger_test_unity.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include #include