Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Transaction Invariant Checks #2054

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions Builds/VisualStudio2015/RippleD.vcxproj
Expand Up @@ -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">
Expand Down Expand Up @@ -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>
Expand Down
9 changes: 9 additions & 0 deletions Builds/VisualStudio2015/RippleD.vcxproj.filters
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down
1 change: 1 addition & 0 deletions docs/source.dox
Expand Up @@ -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 =
Expand Down
3 changes: 2 additions & 1 deletion src/ripple/app/main/Amendments.cpp
Expand Up @@ -52,7 +52,8 @@ supportedAmendments ()
{ "E2E6F2866106419B88C50045ACE96368558C345566AC8F2BDF5A5B5587F0E6FA fix1368" },
{ "07D43DCE529B15A10827E5E04943B496762F9A88E3268269D69C44BE49E21104 Escrow" },
{ "86E83A7D2ECE3AD5FA87AB2195AE015C950469ABF0B72EAACED318F74886AE90 CryptoConditionsSuite" },
{ "48C4451D6C6A138453F056EB6793AFF4B5C57457A37BA63EF3541FF8CE873DC2 ToStrandV2"}
{ "48C4451D6C6A138453F056EB6793AFF4B5C57457A37BA63EF3541FF8CE873DC2 ToStrandV2"},
{ "DC9CA96AEA1DCF83E527D1AFC916EFAF5D27388ECA4060A88817C1238CAEE0BF EnforceInvariants" }
};
}

Expand Down
54 changes: 54 additions & 0 deletions src/ripple/app/tx/impl/ApplyContext.cpp
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
6 changes: 6 additions & 0 deletions src/ripple/app/tx/impl/ApplyContext.h
Expand Up @@ -101,7 +101,13 @@ class ApplyContext
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_;
Expand Down
191 changes: 191 additions & 0 deletions 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 <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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the whole point here is to check for programming errors elsewhere, I'm thinking we want to explicitly check all the known LedgerEntryTypes here, and fail if we get an unknown type.

case ltDIR_NODE:
case ltRIPPLE_STATE:
[...etc...]
   break;
default:
   // set failure flag, assert, or maybe even throw since this is a programming oversight error
   break;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to add this check for known types of LE, I'd be inclined to create a separate check for this rather than wrapping it into this one. @JoelKatz do you think it's worth adding a LedgerEntryType-is-valid invariant check?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd combine this check into LedgerEntryTypesMatch - in addition to checking that types match, we can ensure that only known LETs are added.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works for me. If catching it in LedgerEntryTypesMatch or similar isn't enough to think through the other invariant classes, then we've got bigger problems.

}
}

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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this is a range because a tx could claim less than the specified fee if the relevant account has a balance lower than that, and is thus zeroed out. Correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds right to me ;)


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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we wanted to be extra nitpicky, we could check if the respective amendment is enabled here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe we're passing rules or the feature set into the invariants right now, so we would have to add that if we wanted to do this. I'm happy to look into it more if there is sufficient value...

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