mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
Prevent duplicate txs in book subscription (RIPD-1465):
If an offer transaction touched multiple ledger entries associated with the same book, that offer transaction would be published multiple times to anyone subscribed to that book stream. Fixes #2095.
This commit is contained in:
@@ -64,7 +64,7 @@ public:
|
||||
// RPC subscribe to books stream
|
||||
books[jss::books] = Json::arrayValue;
|
||||
{
|
||||
auto &j = books[jss::books].append(Json::objectValue);
|
||||
auto& j = books[jss::books].append(Json::objectValue);
|
||||
j[jss::snapshot] = true;
|
||||
j[jss::taker_gets][jss::currency] = "XRP";
|
||||
j[jss::taker_pays][jss::currency] = "USD";
|
||||
@@ -147,7 +147,7 @@ public:
|
||||
// RPC subscribe to books stream
|
||||
books[jss::books] = Json::arrayValue;
|
||||
{
|
||||
auto &j = books[jss::books].append(Json::objectValue);
|
||||
auto& j = books[jss::books].append(Json::objectValue);
|
||||
j[jss::snapshot] = true;
|
||||
j[jss::taker_gets][jss::currency] = "XRP";
|
||||
j[jss::taker_pays][jss::currency] = "USD";
|
||||
@@ -225,7 +225,7 @@ public:
|
||||
// RPC subscribe to books stream
|
||||
books[jss::books] = Json::arrayValue;
|
||||
{
|
||||
auto &j = books[jss::books].append(Json::objectValue);
|
||||
auto& j = books[jss::books].append(Json::objectValue);
|
||||
j[jss::snapshot] = true;
|
||||
j[jss::both] = true;
|
||||
j[jss::taker_gets][jss::currency] = "XRP";
|
||||
@@ -319,7 +319,7 @@ public:
|
||||
// RPC subscribe to books stream
|
||||
books[jss::books] = Json::arrayValue;
|
||||
{
|
||||
auto &j = books[jss::books].append(Json::objectValue);
|
||||
auto& j = books[jss::books].append(Json::objectValue);
|
||||
j[jss::snapshot] = true;
|
||||
j[jss::both] = true;
|
||||
j[jss::taker_gets][jss::currency] = "XRP";
|
||||
@@ -414,14 +414,14 @@ public:
|
||||
// RPC subscribe to books stream
|
||||
books[jss::books] = Json::arrayValue;
|
||||
{
|
||||
auto &j = books[jss::books].append(Json::objectValue);
|
||||
auto& j = books[jss::books].append(Json::objectValue);
|
||||
j[jss::snapshot] = true;
|
||||
j[jss::taker_gets][jss::currency] = "XRP";
|
||||
j[jss::taker_pays][jss::currency] = "USD";
|
||||
j[jss::taker_pays][jss::issuer] = Account("alice").human();
|
||||
}
|
||||
{
|
||||
auto &j = books[jss::books].append(Json::objectValue);
|
||||
auto& j = books[jss::books].append(Json::objectValue);
|
||||
j[jss::snapshot] = true;
|
||||
j[jss::taker_gets][jss::currency] = "CNY";
|
||||
j[jss::taker_gets][jss::issuer] = Account("alice").human();
|
||||
@@ -540,14 +540,14 @@ public:
|
||||
// RPC subscribe to books stream
|
||||
books[jss::books] = Json::arrayValue;
|
||||
{
|
||||
auto &j = books[jss::books].append(Json::objectValue);
|
||||
auto& j = books[jss::books].append(Json::objectValue);
|
||||
j[jss::snapshot] = true;
|
||||
j[jss::taker_gets][jss::currency] = "XRP";
|
||||
j[jss::taker_pays][jss::currency] = "USD";
|
||||
j[jss::taker_pays][jss::issuer] = Account("alice").human();
|
||||
}
|
||||
{
|
||||
auto &j = books[jss::books].append(Json::objectValue);
|
||||
auto& j = books[jss::books].append(Json::objectValue);
|
||||
j[jss::snapshot] = true;
|
||||
j[jss::taker_gets][jss::currency] = "CNY";
|
||||
j[jss::taker_gets][jss::issuer] = Account("alice").human();
|
||||
@@ -657,7 +657,7 @@ public:
|
||||
// RPC subscribe to books stream
|
||||
books[jss::books] = Json::arrayValue;
|
||||
{
|
||||
auto &j = books[jss::books].append(Json::objectValue);
|
||||
auto& j = books[jss::books].append(Json::objectValue);
|
||||
j[jss::snapshot] = true;
|
||||
j[jss::both] = true;
|
||||
j[jss::taker_gets][jss::currency] = "XRP";
|
||||
@@ -665,7 +665,7 @@ public:
|
||||
j[jss::taker_pays][jss::issuer] = Account("alice").human();
|
||||
}
|
||||
{
|
||||
auto &j = books[jss::books].append(Json::objectValue);
|
||||
auto& j = books[jss::books].append(Json::objectValue);
|
||||
j[jss::snapshot] = true;
|
||||
j[jss::both] = true;
|
||||
j[jss::taker_gets][jss::currency] = "CNY";
|
||||
@@ -804,7 +804,7 @@ public:
|
||||
// RPC subscribe to books stream
|
||||
books[jss::books] = Json::arrayValue;
|
||||
{
|
||||
auto &j = books[jss::books].append(Json::objectValue);
|
||||
auto& j = books[jss::books].append(Json::objectValue);
|
||||
j[jss::snapshot] = true;
|
||||
j[jss::both] = true;
|
||||
j[jss::taker_gets][jss::currency] = "XRP";
|
||||
@@ -813,7 +813,7 @@ public:
|
||||
}
|
||||
// RPC subscribe to books stream
|
||||
{
|
||||
auto &j = books[jss::books].append(Json::objectValue);
|
||||
auto& j = books[jss::books].append(Json::objectValue);
|
||||
j[jss::snapshot] = true;
|
||||
j[jss::both] = true;
|
||||
j[jss::taker_gets][jss::currency] = "CNY";
|
||||
@@ -951,7 +951,7 @@ public:
|
||||
{
|
||||
books[jss::books] = Json::arrayValue;
|
||||
{
|
||||
auto &j = books[jss::books].append(Json::objectValue);
|
||||
auto& j = books[jss::books].append(Json::objectValue);
|
||||
j[jss::snapshot] = true;
|
||||
j[jss::taker_gets][jss::currency] = "XRP";
|
||||
j[jss::taker_pays][jss::currency] = "USD";
|
||||
@@ -1072,6 +1072,180 @@ public:
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
}
|
||||
|
||||
// Check that a stream only sees the given OfferCreate once
|
||||
static
|
||||
bool
|
||||
offerOnlyOnceInStream(
|
||||
std::unique_ptr<WSClient> const & wsc,
|
||||
std::chrono::milliseconds const& timeout,
|
||||
jtx::PrettyAmount const& takerGets,
|
||||
jtx::PrettyAmount const& takerPays)
|
||||
{
|
||||
auto maybeJv = wsc->getMsg(timeout);
|
||||
// No message
|
||||
if (!maybeJv)
|
||||
return false;
|
||||
// wrong message
|
||||
if(!(*maybeJv).isMember(jss::transaction))
|
||||
return false;
|
||||
auto const& t = (*maybeJv)[jss::transaction];
|
||||
if (t[jss::TransactionType] != "OfferCreate" ||
|
||||
t[jss::TakerGets] != takerGets.value().getJson(0) ||
|
||||
t[jss::TakerPays] != takerPays.value().getJson(0))
|
||||
return false;
|
||||
// Make sure no other message is waiting
|
||||
return wsc->getMsg(timeout) == boost::none;
|
||||
};
|
||||
|
||||
void
|
||||
testCrossingSingleBookOffer()
|
||||
{
|
||||
testcase("Crossing single book offer");
|
||||
|
||||
// This was added to check that an OfferCreate transaction is only
|
||||
// published once in a stream, even if it updates multiple offer
|
||||
// ledger entries
|
||||
|
||||
using namespace jtx;
|
||||
Env env(*this);
|
||||
|
||||
// Scenario is:
|
||||
// - Alice and Bob place identical offers for USD -> XRP
|
||||
// - Charlie places a crossing order that takes both Alice and Bob's
|
||||
|
||||
auto const gw = Account("gateway");
|
||||
auto const alice = Account("alice");
|
||||
auto const bob = Account("bob");
|
||||
auto const charlie = Account("charlie");
|
||||
auto const USD = gw["USD"];
|
||||
|
||||
env.fund (XRP(1000000), gw, alice, bob, charlie);
|
||||
env.close();
|
||||
|
||||
env (trust(alice, USD(500)));
|
||||
env (trust(bob, USD(500)));
|
||||
env.close();
|
||||
|
||||
env (pay(gw, alice, USD(500)));
|
||||
env (pay(gw, bob, USD(500)));
|
||||
env.close();
|
||||
|
||||
// Alice and Bob offer $500 for 500 XRP
|
||||
env (offer (alice, XRP(500), USD(500)));
|
||||
env (offer (bob, XRP(500), USD(500)));
|
||||
env.close();
|
||||
|
||||
auto wsc = makeWSClient(env.app().config());
|
||||
Json::Value books;
|
||||
{
|
||||
// RPC subscribe to books stream
|
||||
books[jss::books] = Json::arrayValue;
|
||||
{
|
||||
auto& j = books[jss::books].append(Json::objectValue);
|
||||
j[jss::snapshot] = false;
|
||||
j[jss::taker_gets][jss::currency] = "XRP";
|
||||
j[jss::taker_pays][jss::currency] = "USD";
|
||||
j[jss::taker_pays][jss::issuer] = gw.human();
|
||||
}
|
||||
|
||||
auto jv = wsc->invoke("subscribe", books);
|
||||
if (!BEAST_EXPECT(jv[jss::status] == "success"))
|
||||
return;
|
||||
}
|
||||
|
||||
// Charlie places an offer that crosses Alice and Charlie's offers
|
||||
env(offer(charlie, USD(1000), XRP(1000)));
|
||||
env.close();
|
||||
env.require(offers(alice, 0), offers(bob, 0), offers(charlie, 0));
|
||||
BEAST_EXPECT(offerOnlyOnceInStream(wsc, 1s, XRP(1000), USD(1000)));
|
||||
|
||||
// RPC unsubscribe
|
||||
auto jv = wsc->invoke("unsubscribe", books);
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
}
|
||||
|
||||
void
|
||||
testCrossingMultiBookOffer()
|
||||
{
|
||||
testcase("Crossing multi-book offer");
|
||||
|
||||
// This was added to check that an OfferCreate transaction is only
|
||||
// published once in a stream, even if it auto-bridges across several
|
||||
// books that are under subscription
|
||||
|
||||
using namespace jtx;
|
||||
Env env(*this);
|
||||
|
||||
// Scenario is:
|
||||
// - Alice has 1 USD and wants 100 XRP
|
||||
// - Bob has 100 XRP and wants 1 EUR
|
||||
// - Charlie has 1 EUR and wants 1 USD and should auto-bridge through
|
||||
// Alice and Bob
|
||||
|
||||
auto const gw = Account("gateway");
|
||||
auto const alice = Account("alice");
|
||||
auto const bob = Account("bob");
|
||||
auto const charlie = Account("charlie");
|
||||
auto const USD = gw["USD"];
|
||||
auto const EUR = gw["EUR"];
|
||||
|
||||
env.fund(XRP(1000000), gw, alice, bob, charlie);
|
||||
env.close();
|
||||
|
||||
for (auto const& account : {alice, bob, charlie})
|
||||
{
|
||||
for (auto const& iou : {USD, EUR})
|
||||
{
|
||||
env(trust(account, iou(1)));
|
||||
}
|
||||
}
|
||||
env.close();
|
||||
|
||||
env(pay(gw, alice, USD(1)));
|
||||
env(pay(gw, charlie, EUR(1)));
|
||||
env.close();
|
||||
|
||||
env(offer(alice, XRP(100), USD(1)));
|
||||
env(offer(bob, EUR(1), XRP(100)));
|
||||
env.close();
|
||||
|
||||
auto wsc = makeWSClient(env.app().config());
|
||||
Json::Value books;
|
||||
|
||||
{
|
||||
// RPC subscribe to multiple book streams
|
||||
books[jss::books] = Json::arrayValue;
|
||||
{
|
||||
auto& j = books[jss::books].append(Json::objectValue);
|
||||
j[jss::snapshot] = false;
|
||||
j[jss::taker_gets][jss::currency] = "XRP";
|
||||
j[jss::taker_pays][jss::currency] = "USD";
|
||||
j[jss::taker_pays][jss::issuer] = gw.human();
|
||||
}
|
||||
|
||||
{
|
||||
auto& j = books[jss::books].append(Json::objectValue);
|
||||
j[jss::snapshot] = false;
|
||||
j[jss::taker_gets][jss::currency] = "EUR";
|
||||
j[jss::taker_gets][jss::issuer] = gw.human();
|
||||
j[jss::taker_pays][jss::currency] = "XRP";
|
||||
}
|
||||
|
||||
auto jv = wsc->invoke("subscribe", books);
|
||||
if (!BEAST_EXPECT(jv[jss::status] == "success"))
|
||||
return;
|
||||
}
|
||||
|
||||
// Charlies places an on offer for EUR -> USD that should auto-bridge
|
||||
env(offer(charlie, USD(1), EUR(1)));
|
||||
env.close();
|
||||
BEAST_EXPECT(offerOnlyOnceInStream(wsc, 1s, EUR(1), USD(1)));
|
||||
|
||||
// RPC unsubscribe
|
||||
auto jv = wsc->invoke("unsubscribe", books);
|
||||
BEAST_EXPECT(jv[jss::status] == "success");
|
||||
}
|
||||
|
||||
void
|
||||
testBookOfferErrors()
|
||||
{
|
||||
@@ -1466,6 +1640,8 @@ public:
|
||||
testMultipleBooksBothSidesEmptyBook();
|
||||
testMultipleBooksBothSidesOffersInBook();
|
||||
testTrackOffers();
|
||||
testCrossingSingleBookOffer();
|
||||
testCrossingMultiBookOffer();
|
||||
testBookOfferErrors();
|
||||
testBookOfferLimits(true);
|
||||
testBookOfferLimits(false);
|
||||
|
||||
Reference in New Issue
Block a user