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); - }); - -});