diff --git a/Builds/VisualStudio2015/RippleD.vcxproj b/Builds/VisualStudio2015/RippleD.vcxproj
index 3f4a031ac..c4c5f47d3 100644
--- a/Builds/VisualStudio2015/RippleD.vcxproj
+++ b/Builds/VisualStudio2015/RippleD.vcxproj
@@ -1736,6 +1736,10 @@
True
True
+
+ True
+ True
+
True
True
diff --git a/Builds/VisualStudio2015/RippleD.vcxproj.filters b/Builds/VisualStudio2015/RippleD.vcxproj.filters
index 0fde47e54..b0fb1b993 100644
--- a/Builds/VisualStudio2015/RippleD.vcxproj.filters
+++ b/Builds/VisualStudio2015/RippleD.vcxproj.filters
@@ -2460,6 +2460,9 @@
ripple\app\tests
+
+ ripple\app\tests
+
ripple\app\tests
diff --git a/src/ripple/app/tests/Transaction_ordering_test.cpp b/src/ripple/app/tests/Transaction_ordering_test.cpp
new file mode 100644
index 000000000..c897a6e0a
--- /dev/null
+++ b/src/ripple/app/tests/Transaction_ordering_test.cpp
@@ -0,0 +1,179 @@
+//------------------------------------------------------------------------------
+/*
+ This file is part of rippled: https://github.com/ripple/rippled
+ Copyright (c) 2012, 2013 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
+
+namespace ripple {
+namespace test {
+
+struct Transaction_ordering_test : public beast::unit_test::suite
+{
+ template
+ void submitWait(jtx::Env& env, jtx::JTx const& tx, Ftest&& test)
+ {
+ using namespace std::chrono_literals;
+
+ std::condition_variable cv;
+ env.app().getJobQueue().postCoro(
+ jtCLIENT, "Coroutine-Test",
+ [&](std::shared_ptr jc)
+ {
+ env(tx);
+ cv.notify_one();
+ });
+
+ {
+ std::mutex m;
+ std::unique_lock lk(m);
+ // If stepping through this test in a debugger,
+ // make the timeout much longer, or use
+ //cv.wait(lk, test);
+ expect(cv.wait_for(lk, 2s, test));
+ }
+ }
+
+ void testCorrectOrder()
+ {
+ using namespace jtx;
+
+ Env env(*this);
+ auto const alice = Account("alice");
+ env.fund(XRP(1000), noripple(alice));
+
+ auto const aliceSequence = env.seq(alice);
+
+ auto const tx1 = env.jt(noop(alice), seq(aliceSequence));
+ auto const tx2 = env.jt(noop(alice), seq(aliceSequence + 1),
+ json(R"({"LastLedgerSequence":7})"));
+
+ env(tx1);
+ env.close();
+ expect(env.seq(alice) == aliceSequence + 1);
+ env(tx2);
+ env.close();
+ expect(env.seq(alice) == aliceSequence + 2);
+
+ env.close();
+
+ {
+ auto const result = env.rpc("tx", to_string(tx1.stx->getTransactionID()));
+ expect(result.first == rpcSUCCESS);
+ expect(result.second["result"]["meta"]["TransactionResult"] == "tesSUCCESS");
+ }
+ {
+ auto const result = env.rpc("tx", to_string(tx2.stx->getTransactionID()));
+ expect(result.first == rpcSUCCESS);
+ expect(result.second["result"]["meta"]["TransactionResult"] == "tesSUCCESS");
+ }
+ }
+
+ void testIncorrectOrder()
+ {
+ using namespace jtx;
+
+ Env env(*this);
+ env.app().getJobQueue().setThreadCount(0, false);
+ auto const alice = Account("alice");
+ env.fund(XRP(1000), noripple(alice));
+
+ auto const aliceSequence = env.seq(alice);
+
+ auto const tx1 = env.jt(noop(alice), seq(aliceSequence));
+ auto const tx2 = env.jt(noop(alice), seq(aliceSequence + 1),
+ json(R"({"LastLedgerSequence":7})"));
+
+ env(tx2, ter(terPRE_SEQ));
+ expect(env.seq(alice) == aliceSequence);
+ submitWait(env, tx1,
+ [&]()
+ {
+ return env.seq(alice) == aliceSequence + 2;
+ });
+ expect(env.seq(alice) == aliceSequence + 2);
+
+ env.close();
+
+ {
+ auto const result = env.rpc("tx", to_string(tx1.stx->getTransactionID()));
+ expect(result.first == rpcSUCCESS);
+ expect(result.second["result"]["meta"]["TransactionResult"] == "tesSUCCESS");
+ }
+ {
+ auto const result = env.rpc("tx", to_string(tx2.stx->getTransactionID()));
+ expect(result.first == rpcSUCCESS);
+ expect(result.second["result"]["meta"]["TransactionResult"] == "tesSUCCESS");
+ }
+ }
+
+ void testIncorrectOrderMultipleIntermediaries()
+ {
+ using namespace jtx;
+
+ Env env(*this);
+ env.app().getJobQueue().setThreadCount(0, false);
+ auto const alice = Account("alice");
+ env.fund(XRP(1000), noripple(alice));
+
+ auto const aliceSequence = env.seq(alice);
+
+ std::vector tx;
+ for (auto i = 0; i < 5; ++i)
+ {
+ tx.emplace_back(
+ env.jt(noop(alice), seq(aliceSequence + i),
+ json(R"({"LastLedgerSequence":7})"))
+ );
+ }
+
+ for (auto i = 1; i < 5; ++i)
+ {
+ env(tx[i], ter(terPRE_SEQ));
+ expect(env.seq(alice) == aliceSequence);
+ }
+
+ submitWait(env, tx[0],
+ [&]()
+ {
+ return env.seq(alice) == aliceSequence + 5;
+ });
+ expect(env.seq(alice) == aliceSequence + 5);
+
+ env.close();
+
+ for (auto i = 0; i < 5; ++i)
+ {
+ auto const result = env.rpc("tx", to_string(tx[i].stx->getTransactionID()));
+ expect(result.first == rpcSUCCESS);
+ expect(result.second["result"]["meta"]["TransactionResult"] == "tesSUCCESS");
+ }
+ }
+
+ void run() override
+ {
+ testCorrectOrder();
+ testIncorrectOrder();
+ testIncorrectOrderMultipleIntermediaries();
+ }
+};
+
+BEAST_DEFINE_TESTSUITE(Transaction_ordering,app,ripple);
+
+} // test
+} // ripple
diff --git a/src/ripple/unity/app_tests.cpp b/src/ripple/unity/app_tests.cpp
index 96626d9ca..ab7d17a15 100644
--- a/src/ripple/unity/app_tests.cpp
+++ b/src/ripple/unity/app_tests.cpp
@@ -33,5 +33,6 @@
#include
#include
#include
+#include
#include
#include
diff --git a/test/transaction-ordering-test.js b/test/transaction-ordering-test.js
deleted file mode 100644
index c60621aa7..000000000
--- a/test/transaction-ordering-test.js
+++ /dev/null
@@ -1,255 +0,0 @@
-let assert = require('assert');
-let _ = require('lodash');
-let async = require('async');
-let testutils = require('./testutils');
-let config = testutils.init_config();
-let accounts = require('./testconfig').accounts;
-let Amount = require('ripple-lib').Amount;
-let Transaction = require('ripple-lib').Transaction;
-
-suite('Transaction Ordering', function() {
- let $ = {};
- let opts = {};
-
- setup(function(done) {
- testutils.build_setup(opts).call($, done);
- });
-
- setup(function(done) {
- $.remote.local_signing = true;
- testutils.create_accounts(
- $.remote,
- 'root',
- Amount.from_human('1000 XRP'),
- ['alice'],
- done);
- });
-
- teardown(function(done) {
- testutils.build_teardown().call($, done);
- });
-
- function getAliceSequence() {
- return $.remote.account('alice')._entry.Sequence + 1;
- }
-
- function confirmAliceSeq(expected, callback) {
- let request = $.remote.requestAccountInfo(
- { account: accounts.alice.account, ledger: 'current' },
- function(err, res) {
- assert(!err);
- // console.log("Alice seq: ", res.account_data.Sequence, " Want: ", expected);
- assert.strictEqual(res.account_data.Sequence, expected);
-
- if(callback) {
- callback();
- }
- });
- }
-
- test('correct order', function(done) {
- let aliceSequence = getAliceSequence();
- confirmAliceSeq(aliceSequence);
-
- let tx1 = $.remote.createTransaction('AccountSet',
- {account: accounts.alice.account});
- tx1.setSequence(aliceSequence);
-
- let tx2 = $.remote.createTransaction('AccountSet',
- {account: accounts.alice.account});
- tx2.setSequence(aliceSequence + 1);
- tx2.setLastLedgerSequenceOffset(5);
-
- async.series([
- function(callback) {
- tx1.once('submitted', function(res) {
- // console.log("tx1 submitted: ", res.engine_result);
- assert.strictEqual(res.engine_result, 'tesSUCCESS');
-
- $.remote.ledger_accept();
- });
- tx1.once('final', function(res) {
- // console.log("tx1 final: ", res.metadata.TransactionResult);
- assert.strictEqual(res.metadata.TransactionResult,
- 'tesSUCCESS');
-
- callback();
- });
-
- tx1.submit();
- },
- function(callback) {
- confirmAliceSeq(aliceSequence + 1, callback);
- },
- function(callback) {
- tx2.once('submitted', function(res) {
- // console.log("tx2 submitted: ", res.engine_result);
- assert.strictEqual(res.engine_result, 'tesSUCCESS');
-
- $.remote.ledger_accept();
- });
- tx2.once('final', function(res) {
- // console.log("tx2 final: ", res.metadata.TransactionResult);
- assert.strictEqual(res.metadata.TransactionResult,
- 'tesSUCCESS');
-
- callback();
- });
-
- tx2.submit();
- },
- function(callback) {
- confirmAliceSeq(aliceSequence + 2, callback);
- }
- ], done);
- });
-
- test('incorrect order', function(done) {
- let aliceSequence = getAliceSequence();
- confirmAliceSeq(aliceSequence);
-
- let tx1 = $.remote.createTransaction('AccountSet',
- {account: accounts.alice.account});
- tx1.setSequence(aliceSequence);
-
- let tx2 = $.remote.createTransaction('AccountSet',
- {account: accounts.alice.account});
- tx2.setSequence(aliceSequence + 1);
- tx2.setLastLedgerSequenceOffset(5);
-
- async.series([
- function(callback) {
- // Use `on` instead of `once` for this event, because
- // we want it to fail if ripple-lib resubmits on our
- // behalf, especially if it succeeds.
- tx2.on('submitted', function(res) {
- // console.log("tx2 submitted: ", res.engine_result);
- assert.strictEqual(res.engine_result, 'terPRE_SEQ');
-
- callback();
- });
-
- tx2.submit();
- },
- function(callback) {
- confirmAliceSeq(aliceSequence, callback);
- },
- function(callback) {
- tx1.once('submitted', function(res) {
- // console.log("tx1 submitted: ", res.engine_result);
- assert.strictEqual(res.engine_result, 'tesSUCCESS');
-
- callback();
- });
-
- tx1.submit();
- },
- function(callback) {
- confirmAliceSeq(aliceSequence + 2, callback);
- },
- function(callback) {
- tx1.once('final', function(res) {
- // console.log("tx1 final: ", res.metadata.TransactionResult);
- assert.strictEqual(res.metadata.TransactionResult,
- 'tesSUCCESS');
- });
- tx2.once('final', function(res) {
- // console.log("tx2 final: ", res.metadata.TransactionResult);
- assert.strictEqual(res.metadata.TransactionResult,
- 'tesSUCCESS');
-
- callback();
- });
-
- $.remote.ledger_accept();
- }
- ], done);
- });
-
- test('incorrect order, multiple intermediaries', function(done) {
- let aliceSequence = getAliceSequence();
- confirmAliceSeq(aliceSequence);
-
- let tx = [];
- for(let i = 0; i < 5; ++i) {
- tx[i] = $.remote.createTransaction('AccountSet',
- {account: accounts.alice.account});
- tx[i].setSequence(aliceSequence + i);
- tx[i].setLastLedgerSequenceOffset(5);
- }
-
- let submits = [];
- for(let i = 0; i < 3; ++i) {
- submits = submits.concat([
- function(callback) {
- tx[i].once('submitted', function(res) {
- // console.log("tx" + i + " submitted: ", res.engine_result);
- assert.strictEqual(res.engine_result, 'tesSUCCESS');
-
- callback();
- });
-
- tx[i].submit();
- },
- function(callback) {
- confirmAliceSeq(aliceSequence + i + 1, callback);
- },
- ]);
- }
-
- async.series([
- function(callback) {
- // Use `on` instead of `once` for this event, because
- // we want it to fail if ripple-lib resubmits on our
- // behalf, especially if it succeeds.
- tx[4].on('submitted', function(res) {
- // console.log("tx4 submitted: ", res.engine_result);
- assert.strictEqual(res.engine_result, 'terPRE_SEQ');
-
- callback();
- });
-
- tx[4].submit();
- // Note ripple-lib has a bug/feature that it'll create
- // transactions to fill in sequence gaps if more than
- // one "ter" is received, so this is the best we can do.
- },
- function(callback) {
- confirmAliceSeq(aliceSequence, callback);
- },
- ].concat(submits).concat([
- function(callback) {
- tx[3].once('submitted', function(res) {
- // console.log("tx3 submitted: ", res.engine_result);
- assert.strictEqual(res.engine_result, 'tesSUCCESS');
-
- callback();
- });
-
- tx[3].submit();
- },
- function(callback) {
- confirmAliceSeq(aliceSequence + 5, callback);
- },
- function(callback) {
- for(let i = 0; i < 4; ++i) {
- tx[i].once('final', function(res) {
- // console.log("tx" + i + " final: ", res.metadata.TransactionResult);
- assert.strictEqual(res.metadata.TransactionResult,
- 'tesSUCCESS');
- });
- }
- tx[4].once('final', function(res) {
- // console.log("tx4 final: ", res.metadata.TransactionResult);
- assert.strictEqual(res.metadata.TransactionResult,
- 'tesSUCCESS');
-
- callback();
- });
-
- $.remote.ledger_accept();
- }
- ]), done);
- });
-
-});