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

OrderBookIsNotCrossed invariant #2210

Closed
wants to merge 24 commits into from
Closed

OrderBookIsNotCrossed invariant #2210

wants to merge 24 commits into from

Conversation

robertDurst
Copy link
Contributor

Description

This PR introduces a new invariant, named OrderBookIsNotCrossed.

Background:

Pre-protocol 10 was allowing in some cases the order book to be in a crossed state. The example from CAP-0004 reads:

“In transaction 1, account A creates an offer selling 100 X at a price of 10 Y / 1 X.
In transaction 2, account B creates an offer selling 1 stroop Y at a price of 1 X / 10 Y.
As of protocol version 9, nothing is exchanged and the book remains crossed.”

Put very simply, a crossed state is one such that the top of the book contains a highest bid higher than the lowest ask.

Implementation Overview

This invariant, unlike the previous five, requires some state to determine the highest bid and the lowest ask. For this, I utilize a nested map data structure to simulate an order book, which is updated upon each call of checkOnOperationApply based on the offer entries in LedgerTxnDelta. Since this invariant will be used by the fuzzing code in #2182, which requires a reset of state between execution cycles, I have introduced a resetForFuzzer method to the base invariant class.

Open Questions

  • Naming -- IsNot does not flow off the tongue
  • Method of determining best order -- for now I just iterate through all orders for a given asset pair and take the highest/lowest (O(N)). This should be fine for small order books in fuzzing, but is not production ready.
  • Initial in-memory order book state -- for now there is no way to initialize an order book. For fuzzing I believe this is fine, however it may be something we want (however, this in-memory structure in general is certainly not a production approach).

Checklist

  • Reviewed the contributing document
  • Rebased on top of master (no merge commits)
  • Ran clang-format v5.0.0 (via make format or the Visual Studio extension)
  • Compiles
  • Ran all tests

Copy link
Contributor

@jonjove jonjove left a comment

Choose a reason for hiding this comment

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

The ordering of the commits doesn't make that much sense to me and should probably be revisited (for example, the first commit registers an invariant that isn't even defined yet).

src/invariant/test/InvariantTestUtils.cpp Show resolved Hide resolved
// Orders is a map of OfferId (int64_t) --> OfferEntry
using Orders = std::unordered_map<int64_t, OfferEntry>;
// AssetOrders, orders by asset, a map of AssetId --> map of Orders
using AssetOrders = std::unordered_map<AssetId, Orders>;
Copy link
Contributor

Choose a reason for hiding this comment

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

For both AssetOrders and OrderBook, serializing the key to string is not a C++ idiom. Write a template specialization of std::hash instead. See ledger/LedgerHashUtils.h for inspiration.

Copy link
Contributor

Choose a reason for hiding this comment

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

The change you made is not what I meant, let's discuss further offline.

src/invariant/OrderBookIsNotCrossed.h Outdated Show resolved Hide resolved

// AssetId is defined as a concatenation of issuer and asset code:
// ASSET_CODE | '-' | ISSUER_ACCOUNT_ID
using AssetId = std::string;
Copy link
Contributor

Choose a reason for hiding this comment

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

Not needed (along with getAssetID) in accordance with the comment about specializing std::hash. As a general remark: if you are writing a comment to describe the specific structure that a string must have, then you would probably be better off using a struct instead.

src/invariant/OrderBookIsNotCrossed.cpp Outdated Show resolved Hide resolved

SECTION("Then modify offer")
{
auto offer2 = generateOffer(cur1, cur2, 2, Price{5, 3});
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just copy offer1 and modify it directly?

auto entry = ltxPtr->create(offer1);

invariant->checkOnOperationApply(op, opRes, ltxPtr->getDelta());
auto orderbook = invariant->getOrderBook();
Copy link
Contributor

Choose a reason for hiding this comment

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

You want auto const& here and many other places in this file.

SECTION("Create offer")
{
// Offer 5: 7 A @ 8/5 (B/A)
auto offer5 = generateOffer(cur1, cur2, 7, Price{8, 5});
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not use genApplyCheckCreateOffer here? (This question applies in multiple places in this file)

}

void
genApplyCheckModifyOffer(Asset& ask, Asset& bid, int64 amount, Price price,
Copy link
Contributor

Choose a reason for hiding this comment

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

Passing around the operations is kind of clunky. The invariant only depends on the assets in the operation, so you should be able to prepare them in these functions.


SECTION("Not crossed when highest bid < lowest ask")
{
SECTION("Create offer")
Copy link
Contributor

Choose a reason for hiding this comment

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

Why put a section directly inside another section? (This applies in multiple places in this file)

src/invariant/test/OrderBookIsNotCrossedTests.cpp Outdated Show resolved Hide resolved
src/invariant/test/OrderBookIsNotCrossedTests.cpp Outdated Show resolved Hide resolved
src/invariant/InvariantManagerImpl.cpp Outdated Show resolved Hide resolved
src/invariant/OrderBookIsNotCrossed.cpp Outdated Show resolved Hide resolved
#include "invariant/Invariant.h"
#include "xdr/Stellar-ledger-entries.h"

#include <unordered_map>
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think you need this include

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 think I do. Invariant.h only includes:

#include <memory>
#include <string>

src/invariant/OrderBookIsNotCrossed.h Outdated Show resolved Hide resolved
#include "test/test.h"

using namespace stellar;
using namespace stellar::InvariantTestUtils;
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think you need this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As far as I can tell I do, won't compile without and it is also in most of the other invariant tests.

src/main/ApplicationImpl.cpp Show resolved Hide resolved
@robertDurst
Copy link
Contributor Author

@marta-lokhova and @jonjove I have refactored per your feedback. Thanks -- I think this is quite a bit cleaner now!

Since some of the refactors were more involved, the diffs may make review a bit harder. To address this I tried to sensibly organize my changes into a few commits labeled [pr fix], however if a) this organization does not make sense or b) it would be easier if I just re-org'd everything together, let me know and I can fix up ASAP.

@robertDurst
Copy link
Contributor Author

Updated for proper template specialization for std::hash for Asset instead of getAssetId--> std::string approach.

@robertDurst robertDurst changed the title OrderBookIsNotCrossed invariant [WIP] OrderBookIsNotCrossed invariant Aug 13, 2019
@robertDurst
Copy link
Contributor Author

Changed header to WIP -- realize this does not properly account for PathPayment. It makes the assumption only a single asset pair will be modified and thus only checks if a single asset pair's orders are crossed. Will change header back when fixed.

@robertDurst
Copy link
Contributor Author

Latest commit 86e3826 is a bit substantial (hence WIP in header). The most notable changes from the initial PR include:

  • utilizing Operation to derive asset pairs for which to check order book is crossed
  • proper accounting for all operations that touch the order book (ManageBuyOffer, ManageSellOffer, CreatePassiveOffer, PathPayment, and AllowTrust)
  • utilize set for Orders (std::set<OfferEntry, OfferEntryCmp) instead of std::unordered_map<offerId, OfferEntry> since I do not need to look up individual offers by id, but rather am interested in the least priced, or best offers (thinking ahead a bit here to BestOffersTakenInvariant)
  • decoupled genApplyCheck methods in testing to gen and applyCheck allowing for multiple offers to be applyCheck'd and also separated updateOrderBook and check to allow for more complex checking later on... again thinking about BestOffersTakenInvariant

@robertDurst robertDurst changed the title [WIP] OrderBookIsNotCrossed invariant OrderBookIsNotCrossed invariant Aug 19, 2019
case MANAGE_BUY_OFFER:
{
auto const& offer = op.body.manageBuyOfferOp();
return {std::pair<Asset, Asset>(offer.selling, offer.buying)};
Copy link
Contributor

Choose a reason for hiding this comment

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

std::make_pair can infer types for arguments, so until C++17 please use that

Choose a reason for hiding this comment

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

S

Copy link
Contributor Author

Choose a reason for hiding this comment

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

for (int i = 0; i < pp.path.size(); ++i)
{
// beginning: send -> A
if (i == 0)
Copy link
Contributor

Choose a reason for hiding this comment

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

is does not make sense for those conditions to be inside loop
you can loop from 1 to pp.path.size() - 1 and have those special cases outside of loop

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. Fixed, simpler now.

@robertDurst
Copy link
Contributor Author

While this works great for a persistent order book, the fuzzer does not want the order book to persist between fuzzing executions (not even between operations within a single transaction). So I will need to properly reset this. Have it reset properly on a local branch.

Will push this soon.

src/invariant/InvariantManagerImpl.cpp Show resolved Hide resolved
src/invariant/Invariant.h Show resolved Hide resolved

namespace std
{
template <> class hash<stellar::Asset>
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe this should be in ledger/LedgerHashUtils.h?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved

bool
operator()(OfferEntry const& a, OfferEntry const& b) const
{
auto const& price = [](OfferEntry const& offer) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Using a lambda here is unnecessary. I would just express it directly, as doing so would be shorter than the lambda. An alternative approach would be to remove the lambda, implement this in the .cpp file (leaving the declaration in the .h file), and use the static price method in that translation unit.

{

std::vector<std::pair<Asset, Asset>>
extractAssetPairs(Operation const& op, LedgerTxnDelta const& ltxd)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just check the asset-pair for every offer that appears in ltxd, similar to what you did for ALLOW_TRUST but incorporating created offers as well? I think this would be simpler and more reliable than handling each operation type specifically.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This simplification is great, and at this point it may seem like I do not utilize operations at all now, which is true. However, I do not believe I should remove the operation inclusion code from the tests because future order book invariants, specifically the one I have locally, wip, BestOffersTaken, relies on the operation for certain parts of its check.


private:
// as of right now, since this is only used in fuzzing, the mOrderBook will
// be empty to start. If used in production, since invraiants are
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: invraiants -> invariants

// dest: C
// path: {Bs}
// where Bs represents from 0 to 5 offers inclusive
Asset send = les.at(0).data.offer().selling;
Copy link
Contributor

Choose a reason for hiding this comment

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

Use front instead of at(0) and back instead of at(size()-1). Also these should be const references.


// convert a single offer into an offer operation
Operation
opFromLedgerEntries(LedgerEntry le)
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: le and offer should be const references. This comment applies many places in this file, so I won't post it everywhere but it would be worthwhile for you to look over the file.

}
}

return assets;
Copy link
Contributor

Choose a reason for hiding this comment

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

assets contains duplicates, which will be wasteful when checking crossed later. (I believe this is also possible for PATH_PAYMENT)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As part of an above change, I loop through Offers and only grab unique asset pairs.

@@ -50,6 +50,11 @@ class InvariantManagerImpl : public InvariantManager

virtual void enableInvariant(std::string const& name) override;

#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
Copy link
Contributor

Choose a reason for hiding this comment

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

As far as I can tell, I don't think you are ever actually using these functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

rebased so now I can add it to the appropriate place in the fuzzing code, before and after:

// attempt to apply transaction
txFramePtr->attemptApplication(*mApp, ltx);

auto assetPair = std::make_pair(offer.selling, offer.buying);
auto oppositeAssetPair =
std::make_pair(offer.buying, offer.selling);
if (assets.find(oppositeAssetPair) == assets.end() &&
Copy link
Contributor

Choose a reason for hiding this comment

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

Technically, you only need to check oppositeAssetPair here. The insert on line 35 will do nothing if assetPair is already included.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point -- fixed.

// we derive the rest of the information from the
// LedgerTxnDelta entries
for (auto const& entry : ltxd.entry)
auto const& offer = entry.second.previous
Copy link
Contributor

Choose a reason for hiding this comment

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

Looking closely at this, I'm not sure that this makes sense as implemented. An important piece of context here: it is possible to modify the buying or selling assets on an offer. In those cases, previous and current won't have the same asset pairs.

But in any case, I'm wondering why we should ever care about the previous state? If an order book wasn't crossed and an order was deleted, then it still isn't crossed. If an order book wasn't crossed and an order was modified such that it has the same asset pair, then it will be gathered from the current state. If an order book wasn't crossed and an order was modified such that it doesn't have the same asset pair, then it still isn't crossed. In any of these cases, the previous asset pair isn't useful. Am I missing something 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.

Thanks, I did not realize you can change the asset, I thought you could only change the price and amount. Good catch.

Very good point -- if a set A of orders on an order book is not crossed, there is no subset B such that it may become crossed. The spread between the lowest ask and the highest bid for the subset B is >= the spread before the deletion of any order A.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TLDR they can only be in a less or equally crossed state so if the state was not crossed before it is not crossed now.

@@ -83,7 +83,7 @@ class OrderBookIsNotCrossed : public Invariant
void deleteFromOrderBook(OfferEntry const& oe);
void addToOrderBook(OfferEntry const& oe);
void updateOrderBook(LedgerTxnDelta const& ltxd);
std::string check(std::vector<std::pair<Asset, Asset>> assetPairs);
std::string check(std::set<std::pair<Asset, Asset>> assetPairs);
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: ... const& assetPairs

std::string
checkCrossed(Asset const& a, Asset const& b, OrderBook const& orderBook)
{
// if either side of order book for asset pair empty or does not yet exist,
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you forgot to fill in the rest of the code starting from // ... (I only gave you the first part of it). Should need to make some changes through auto const& bids = ....

@MonsieurNicolas
Copy link
Contributor

will reopen later

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants