Add invariant check tests (RIPD-1493):

Add coverage for a few invariant checks. Handle exception in invariant
checking code so that an check that throws an exception will still
properly return tef/tecINVARIANT_FAILED.
This commit is contained in:
Mike Ellery
2017-07-17 15:35:34 -07:00
committed by Nik Bougalis
parent 36423a5f77
commit c00341a97e
2 changed files with 244 additions and 38 deletions

View File

@@ -78,7 +78,8 @@ ApplyContext::checkInvariantsHelper(TER terResult, std::index_sequence<Is...>)
if (view_->rules().enabled(featureEnforceInvariants)) if (view_->rules().enabled(featureEnforceInvariants))
{ {
auto checkers = getInvariantChecks(); auto checkers = getInvariantChecks();
try
{
// call each check's per-entry method // call each check's per-entry method
visit ( visit (
[&checkers]( [&checkers](
@@ -94,9 +95,8 @@ ApplyContext::checkInvariantsHelper(TER terResult, std::index_sequence<Is...>)
}; };
}); });
// Sean Parent for_each_argument trick // Sean Parent for_each_argument trick (a fold expression with `&&`
// (a fold expression with `&&` would be really nice here when we move // would be really nice here when we move to C++-17)
// to C++-17)
std::array<bool, sizeof...(Is)> finalizers {{ std::array<bool, sizeof...(Is)> finalizers {{
std::get<Is>(checkers).finalize(tx, terResult, journal)...}}; std::get<Is>(checkers).finalize(tx, terResult, journal)...}};
@@ -112,6 +112,17 @@ ApplyContext::checkInvariantsHelper(TER terResult, std::index_sequence<Is...>)
to_string(tx.getJson (0)); to_string(tx.getJson (0));
} }
} }
catch(std::exception const& ex)
{
terResult = (terResult == tecINVARIANT_FAILED) ?
tefINVARIANT_FAILED :
tecINVARIANT_FAILED ;
JLOG(journal.fatal()) <<
"Transaction caused an exception in an invariant" <<
", ex: " << ex.what() <<
", tx: " << to_string(tx.getJson (0));
}
}
return terResult; return terResult;
} }

View File

@@ -55,6 +55,7 @@ class Invariants_test : public beast::unit_test::suite
// changes that will cause the check to fail. // changes that will cause the check to fail.
void void
doInvariantCheck( bool enabled, doInvariantCheck( bool enabled,
std::vector<std::string> const& expect_logs,
std::function < std::function <
bool ( bool (
test::jtx::Account const& a, test::jtx::Account const& a,
@@ -94,13 +95,25 @@ class Invariants_test : public beast::unit_test::suite
BEAST_EXPECT(precheck(A1, A2, ac)); BEAST_EXPECT(precheck(A1, A2, ac));
auto tr = ac.checkInvariants(tesSUCCESS); auto tr = tesSUCCESS;
// invoke check twice to cover tec and tef cases
for (auto i : {0,1})
{
tr = ac.checkInvariants(tr);
if (enabled) if (enabled)
{ {
BEAST_EXPECT(tr == tecINVARIANT_FAILED); BEAST_EXPECT(
BEAST_EXPECT(boost::starts_with(sink.strm_.str(), "Invariant failed:")); tr == (i == 0 ? tecINVARIANT_FAILED : tefINVARIANT_FAILED));
BEAST_EXPECT(
boost::starts_with(sink.strm_.str(), "Invariant failed:") ||
boost::starts_with(sink.strm_.str(),
"Transaction caused an exception"));
//uncomment if you want to log the invariant failure message //uncomment if you want to log the invariant failure message
//log << " --> " << sink.strm_.str() << std::endl; //log << " --> " << sink.strm_.str() << std::endl;
for (auto const& m : expect_logs)
{
BEAST_EXPECT(sink.strm_.str().find(m) != std::string::npos);
}
} }
else else
{ {
@@ -108,6 +121,7 @@ class Invariants_test : public beast::unit_test::suite
BEAST_EXPECT(sink.strm_.str().empty()); BEAST_EXPECT(sink.strm_.str().empty());
} }
} }
}
void void
testEnabled () testEnabled ()
@@ -130,6 +144,7 @@ class Invariants_test : public beast::unit_test::suite
testcase << "checks " << (enabled ? "enabled" : "disabled") << testcase << "checks " << (enabled ? "enabled" : "disabled") <<
" - XRP created"; " - XRP created";
doInvariantCheck (enabled, doInvariantCheck (enabled,
{{ "XRP net change was 500 on a fee of 0" }},
[](Account const& A1, Account const&, ApplyContext& ac) [](Account const& A1, Account const&, ApplyContext& ac)
{ {
// put a single account in the view and "manufacture" some XRP // put a single account in the view and "manufacture" some XRP
@@ -150,6 +165,7 @@ class Invariants_test : public beast::unit_test::suite
testcase << "checks " << (enabled ? "enabled" : "disabled") << testcase << "checks " << (enabled ? "enabled" : "disabled") <<
" - account root removed"; " - account root removed";
doInvariantCheck (enabled, doInvariantCheck (enabled,
{{ "an account root was deleted" }},
[](Account const& A1, Account const&, ApplyContext& ac) [](Account const& A1, Account const&, ApplyContext& ac)
{ {
// remove an account from the view // remove an account from the view
@@ -168,6 +184,8 @@ class Invariants_test : public beast::unit_test::suite
testcase << "checks " << (enabled ? "enabled" : "disabled") << testcase << "checks " << (enabled ? "enabled" : "disabled") <<
" - LE types don't match"; " - LE types don't match";
doInvariantCheck (enabled, doInvariantCheck (enabled,
{{ "ledger entry type mismatch" },
{ "XRP net change was -1000000000 on a fee of 0" }},
[](Account const& A1, Account const&, ApplyContext& ac) [](Account const& A1, Account const&, ApplyContext& ac)
{ {
// replace an entry in the table with an SLE of a different type // replace an entry in the table with an SLE of a different type
@@ -180,6 +198,7 @@ class Invariants_test : public beast::unit_test::suite
}); });
doInvariantCheck (enabled, doInvariantCheck (enabled,
{{ "invalid ledger entry type added" }},
[](Account const& A1, Account const&, ApplyContext& ac) [](Account const& A1, Account const&, ApplyContext& ac)
{ {
// add an entry in the table with an SLE of an invalid type // add an entry in the table with an SLE of an invalid type
@@ -204,6 +223,7 @@ class Invariants_test : public beast::unit_test::suite
testcase << "checks " << (enabled ? "enabled" : "disabled") << testcase << "checks " << (enabled ? "enabled" : "disabled") <<
" - trust lines with XRP not allowed"; " - trust lines with XRP not allowed";
doInvariantCheck (enabled, doInvariantCheck (enabled,
{{ "an XRP trust line was created" }},
[](Account const& A1, Account const& A2, ApplyContext& ac) [](Account const& A1, Account const& A2, ApplyContext& ac)
{ {
// create simple trust SLE with xrp currency // create simple trust SLE with xrp currency
@@ -215,18 +235,192 @@ class Invariants_test : public beast::unit_test::suite
}); });
} }
void
testXRPBalanceCheck(bool enabled)
{
using namespace test::jtx;
testcase << "checks " << (enabled ? "enabled" : "disabled") <<
" - XRP balance checks";
doInvariantCheck (enabled,
{{ "Cannot return non-native STAmount as XRPAmount" }},
[](Account const& A1, Account const& A2, ApplyContext& ac)
{
//non-native balance
auto const sle = ac.view().peek (keylet::account(A1.id()));
if(! sle)
return false;
STAmount nonNative {A2["USD"](51)};
sle->setFieldAmount (sfBalance, nonNative);
ac.view().update (sle);
return true;
});
doInvariantCheck (enabled,
{{ "incorrect account XRP balance" },
{ "XRP net change was 99999999000000001 on a fee of 0" }},
[](Account const& A1, Account const&, ApplyContext& ac)
{
// balance exceeds genesis amount
auto const sle = ac.view().peek (keylet::account(A1.id()));
if(! sle)
return false;
sle->setFieldAmount (sfBalance, SYSTEM_CURRENCY_START + 1);
ac.view().update (sle);
return true;
});
doInvariantCheck (enabled,
{{ "incorrect account XRP balance" },
{ "XRP net change was -1000000001 on a fee of 0" }},
[](Account const& A1, Account const&, ApplyContext& ac)
{
// balance is negative
auto const sle = ac.view().peek (keylet::account(A1.id()));
if(! sle)
return false;
sle->setFieldAmount (sfBalance, -1);
ac.view().update (sle);
return true;
});
}
void
testNoBadOffers(bool enabled)
{
using namespace test::jtx;
testcase << "checks " << (enabled ? "enabled" : "disabled") <<
" - no bad offers";
doInvariantCheck (enabled,
{{ "offer with a bad amount" }},
[](Account const& A1, Account const&, ApplyContext& ac)
{
// offer with negative takerpays
auto const sle = ac.view().peek (keylet::account(A1.id()));
if(! sle)
return false;
auto const offer_index =
getOfferIndex (A1.id(), (*sle)[sfSequence]);
auto sleNew = std::make_shared<SLE> (ltOFFER, offer_index);
sleNew->setAccountID (sfAccount, A1.id());
sleNew->setFieldU32 (sfSequence, (*sle)[sfSequence]);
sleNew->setFieldAmount (sfTakerPays, XRP(-1));
ac.view().insert (sleNew);
return true;
});
doInvariantCheck (enabled,
{{ "offer with a bad amount" }},
[](Account const& A1, Account const&, ApplyContext& ac)
{
// offer with negative takergets
auto const sle = ac.view().peek (keylet::account(A1.id()));
if(! sle)
return false;
auto const offer_index =
getOfferIndex (A1.id(), (*sle)[sfSequence]);
auto sleNew = std::make_shared<SLE> (ltOFFER, offer_index);
sleNew->setAccountID (sfAccount, A1.id());
sleNew->setFieldU32 (sfSequence, (*sle)[sfSequence]);
sleNew->setFieldAmount (sfTakerPays, A1["USD"](10));
sleNew->setFieldAmount (sfTakerGets, XRP(-1));
ac.view().insert (sleNew);
return true;
});
doInvariantCheck (enabled,
{{ "offer with a bad amount" }},
[](Account const& A1, Account const&, ApplyContext& ac)
{
// offer XRP to XRP
auto const sle = ac.view().peek (keylet::account(A1.id()));
if(! sle)
return false;
auto const offer_index =
getOfferIndex (A1.id(), (*sle)[sfSequence]);
auto sleNew = std::make_shared<SLE> (ltOFFER, offer_index);
sleNew->setAccountID (sfAccount, A1.id());
sleNew->setFieldU32 (sfSequence, (*sle)[sfSequence]);
sleNew->setFieldAmount (sfTakerPays, XRP(10));
sleNew->setFieldAmount (sfTakerGets, XRP(11));
ac.view().insert (sleNew);
return true;
});
}
void
testNoZeroEscrow(bool enabled)
{
using namespace test::jtx;
testcase << "checks " << (enabled ? "enabled" : "disabled") <<
" - no zero escrow";
doInvariantCheck (enabled,
{{ "Cannot return non-native STAmount as XRPAmount" }},
[](Account const& A1, Account const& A2, ApplyContext& ac)
{
// escrow with nonnative amount
auto const sle = ac.view().peek (keylet::account(A1.id()));
if(! sle)
return false;
auto sleNew = std::make_shared<SLE> (
keylet::escrow(A1, (*sle)[sfSequence] + 2));
STAmount nonNative {A2["USD"](51)};
sleNew->setFieldAmount (sfAmount, nonNative);
ac.view().insert (sleNew);
return true;
});
doInvariantCheck (enabled,
{{ "XRP net change was -1000000 on a fee of 0"},
{ "escrow specifies invalid amount" }},
[](Account const& A1, Account const&, ApplyContext& ac)
{
// escrow with negative amount
auto const sle = ac.view().peek (keylet::account(A1.id()));
if(! sle)
return false;
auto sleNew = std::make_shared<SLE> (
keylet::escrow(A1, (*sle)[sfSequence] + 2));
sleNew->setFieldAmount (sfAmount, XRP(-1));
ac.view().insert (sleNew);
return true;
});
doInvariantCheck (enabled,
{{ "XRP net change was 100000000000000001 on a fee of 0" },
{ "escrow specifies invalid amount" }},
[](Account const& A1, Account const&, ApplyContext& ac)
{
// escrow with too-large amount
auto const sle = ac.view().peek (keylet::account(A1.id()));
if(! sle)
return false;
auto sleNew = std::make_shared<SLE> (
keylet::escrow(A1, (*sle)[sfSequence] + 2));
sleNew->setFieldAmount (sfAmount, SYSTEM_CURRENCY_START + 1);
ac.view().insert (sleNew);
return true;
});
}
public: public:
void run () void run ()
{ {
testEnabled (); testEnabled ();
// all invariant checks are run with
// the checks enabled and disabled // now run each invariant check test with
for(auto const& b : {true, false}) // the feature enabled and disabled
for(auto const& b : {false, true})
{ {
testXRPNotCreated (b); testXRPNotCreated (b);
testAccountsNotRemoved (b); testAccountsNotRemoved (b);
testTypesMatch (b); testTypesMatch (b);
testNoXRPTrustLine (b); testNoXRPTrustLine (b);
testXRPBalanceCheck (b);
testNoBadOffers (b);
testNoZeroEscrow (b);
} }
} }
}; };
@@ -234,3 +428,4 @@ public:
BEAST_DEFINE_TESTSUITE (Invariants, ledger, ripple); BEAST_DEFINE_TESTSUITE (Invariants, ledger, ripple);
} // ripple } // ripple