diff --git a/Builds/VisualStudio2015/RippleD.vcxproj b/Builds/VisualStudio2015/RippleD.vcxproj index a739952c9..5efa6a773 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj +++ b/Builds/VisualStudio2015/RippleD.vcxproj @@ -3283,6 +3283,10 @@ True True + + True + True + True True diff --git a/Builds/VisualStudio2015/RippleD.vcxproj.filters b/Builds/VisualStudio2015/RippleD.vcxproj.filters index 35c0ed33a..a7b487a98 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj.filters +++ b/Builds/VisualStudio2015/RippleD.vcxproj.filters @@ -3765,6 +3765,9 @@ ripple\rpc\tests + + ripple\rpc\tests + ripple\rpc\tests diff --git a/src/ripple/rpc/tests/RobustTransaction.test.cpp b/src/ripple/rpc/tests/RobustTransaction.test.cpp new file mode 100644 index 000000000..86e7e3d80 --- /dev/null +++ b/src/ripple/rpc/tests/RobustTransaction.test.cpp @@ -0,0 +1,321 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include + +namespace ripple { +namespace test { + +class RobustTransaction_test : public beast::unit_test::suite +{ +public: + void + testSequenceRealignment() + { + using namespace std::chrono_literals; + using namespace jtx; + Env env(*this); + env.fund(XRP(10000), "alice", "bob"); + env.close(); + auto wsc = makeWSClient(env.app().config()); + + { + // RPC subscribe to transactions stream + Json::Value jv; + jv[jss::streams] = Json::arrayValue; + jv[jss::streams].append("transactions"); + jv = wsc->invoke("subscribe", jv); + expect(jv[jss::status] == "success"); + } + + { + // Submit past ledger sequence transaction + Json::Value payment; + payment[jss::secret] = toBase58(generateSeed("alice")); + payment[jss::tx_json] = pay("alice", "bob", XRP(1)); + payment[jss::tx_json][sfLastLedgerSequence.fieldName] = 1; + auto jv = wsc->invoke("submit", payment); + expect(jv[jss::result][jss::engine_result] == + "tefMAX_LEDGER"); + + // Submit past sequence transaction + payment[jss::tx_json] = pay("alice", "bob", XRP(1)); + payment[jss::tx_json][sfSequence.fieldName] = + env.seq("alice") - 1; + jv = wsc->invoke("submit", payment); + expect(jv[jss::result][jss::engine_result] == + "tefPAST_SEQ"); + + // Submit future sequence transaction + payment[jss::tx_json][sfSequence.fieldName] = + env.seq("alice") + 1; + jv = wsc->invoke("submit", payment); + expect(jv[jss::result][jss::engine_result] == + "terPRE_SEQ"); + + // Submit transaction to bridge the sequence gap + payment[jss::tx_json][sfSequence.fieldName] = + env.seq("alice"); + jv = wsc->invoke("submit", payment); + expect(jv[jss::result][jss::engine_result] == + "tesSUCCESS"); + + // Wait for the jobqueue to process everything + env.app().getJobQueue().rendezvous(); + + // Finalize transactions + jv = wsc->invoke("ledger_accept"); + expect(jv[jss::result].isMember( + jss::ledger_current_index)); + } + + { + // Check balances + expect(wsc->findMsg(5s, + [&](auto const& jv) + { + auto const& ff = jv[jss::meta]["AffectedNodes"] + [1u]["ModifiedNode"]["FinalFields"]; + return ff[jss::Account] == Account("bob").human() && + ff["Balance"] == "10001000000"; + })); + + expect(wsc->findMsg(5s, + [&](auto const& jv) + { + auto const& ff = jv[jss::meta]["AffectedNodes"] + [1u]["ModifiedNode"]["FinalFields"]; + return ff[jss::Account] == Account("bob").human() && + ff["Balance"] == "10002000000"; + })); + } + } + + /* + Submit a normal payment. Client disconnects after the proposed + transaction result is received. + + Client reconnects in the future. During this time it is presumed that the + transaction should have succeeded. + + Upon reconnection, recent account transaction history is loaded. + The submitted transaction should be detected, and the transaction should + ultimately succeed. + */ + void + testReconnect() + { + using namespace jtx; + Env env(*this); + env.fund(XRP(10000), "alice", "bob"); + env.close(); + auto wsc = makeWSClient(env.app().config()); + + { + // Submit normal payment + Json::Value jv; + jv[jss::secret] = toBase58(generateSeed("alice")); + jv[jss::tx_json] = pay("alice", "bob", XRP(1)); + jv = wsc->invoke("submit", jv); + expect(jv[jss::result][jss::engine_result] == + "tesSUCCESS"); + + // Disconnect + wsc.reset(); + + // Server finalizes transaction + env.close(); + } + + { + // RPC account_tx + Json::Value jv; + jv[jss::account] = Account("bob").human(); + jv[jss::ledger_index_min] = -1; + jv[jss::ledger_index_max] = -1; + wsc = makeWSClient(env.app().config()); + jv = wsc->invoke("account_tx", jv); + + // Check balance + auto ff = jv[jss::result][jss::transactions][0u][jss::meta] + ["AffectedNodes"][1u]["ModifiedNode"]["FinalFields"]; + expect(ff[jss::Account] == + Account("bob").human()); + expect(ff["Balance"] == "10001000000"); + } + } + + void + testReconnectAfterWait() + { + using namespace std::chrono_literals; + using namespace jtx; + Env env(*this); + env.fund(XRP(10000), "alice", "bob"); + env.close(); + auto wsc = makeWSClient(env.app().config()); + + { + // Submit normal payment + Json::Value jv; + jv[jss::secret] = toBase58(generateSeed("alice")); + jv[jss::tx_json] = pay("alice", "bob", XRP(1)); + jv = wsc->invoke("submit", jv); + expect(jv[jss::result][jss::engine_result] == + "tesSUCCESS"); + + // Finalize transaction + jv = wsc->invoke("ledger_accept"); + expect(jv[jss::result].isMember( + jss::ledger_current_index)); + + // Wait for the jobqueue to process everything + env.app().getJobQueue().rendezvous(); + } + + { + // RPC subscribe to ledger stream + Json::Value jv; + jv[jss::streams] = Json::arrayValue; + jv[jss::streams].append("ledger"); + jv = wsc->invoke("subscribe", jv); + expect(jv[jss::status] == "success"); + + // Close ledgers + for(auto i = 0; i < 8; ++i) + { + expect(wsc->invoke("ledger_accept")[jss::result]. + isMember(jss::ledger_current_index)); + + // Wait for the jobqueue to process everything + env.app().getJobQueue().rendezvous(); + + expect(wsc->findMsg(5s, + [&](auto const& jv) + { + return jv[jss::type] == "ledgerClosed"; + })); + } + } + + { + // Disconnect, reconnect + wsc = makeWSClient(env.app().config()); + + // RPC subscribe to ledger stream + Json::Value jv; + jv[jss::streams] = Json::arrayValue; + jv[jss::streams].append("ledger"); + jv = wsc->invoke("subscribe", jv); + expect(jv[jss::status] == "success"); + + // Close ledgers + for (auto i = 0; i < 2; ++i) + { + expect(wsc->invoke("ledger_accept")[jss::result]. + isMember(jss::ledger_current_index)); + + // Wait for the jobqueue to process everything + env.app().getJobQueue().rendezvous(); + + expect(wsc->findMsg(5s, + [&](auto const& jv) + { + return jv[jss::type] == "ledgerClosed"; + })); + } + } + + { + // RPC account_tx + Json::Value jv; + jv[jss::account] = Account("bob").human(); + jv[jss::ledger_index_min] = -1; + jv[jss::ledger_index_max] = -1; + wsc = makeWSClient(env.app().config()); + jv = wsc->invoke("account_tx", jv); + + // Check balance + auto ff = jv[jss::result][jss::transactions][0u][jss::meta] + ["AffectedNodes"][1u]["ModifiedNode"]["FinalFields"]; + expect(ff[jss::Account] == + Account("bob").human()); + expect(ff["Balance"] == "10001000000"); + } + } + + void + testAccountsProposed() + { + using namespace std::chrono_literals; + using namespace jtx; + Env env(*this); + env.fund(XRP(10000), "alice"); + env.close(); + auto wsc = makeWSClient(env.app().config()); + + { + // RPC subscribe to accounts_proposed stream + Json::Value jv; + jv[jss::accounts_proposed] = Json::arrayValue; + jv[jss::accounts_proposed].append( + Account("alice").human()); + jv = wsc->invoke("subscribe", jv); + expect(jv[jss::status] == "success"); + } + + { + // Submit account_set transaction + Json::Value jv; + jv[jss::secret] = toBase58(generateSeed("alice")); + jv[jss::tx_json] = fset("alice", 0); + jv[jss::tx_json][jss::Fee] = 10; + jv = wsc->invoke("submit", jv); + expect(jv[jss::result][jss::engine_result] == + "tesSUCCESS"); + } + + { + // Check stream update + expect(wsc->findMsg(5s, + [&](auto const& jv) + { + return jv[jss::transaction][jss::TransactionType] == + "AccountSet"; + })); + } + } + + void + run() override + { + testSequenceRealignment(); + testReconnect(); + testReconnectAfterWait(); + testAccountsProposed(); + } +}; + +BEAST_DEFINE_TESTSUITE(RobustTransaction,app,ripple); + +} // test +} // ripple diff --git a/src/ripple/unity/rpcx.cpp b/src/ripple/unity/rpcx.cpp index e226ce3d6..8d6ef5e90 100644 --- a/src/ripple/unity/rpcx.cpp +++ b/src/ripple/unity/rpcx.cpp @@ -105,5 +105,6 @@ #include #include #include +#include #include #include diff --git a/test/robust-transaction-test.js b/test/robust-transaction-test.js deleted file mode 100644 index a5824dbee..000000000 --- a/test/robust-transaction-test.js +++ /dev/null @@ -1,463 +0,0 @@ -var async = require('async'); -var assert = require('assert'); -var ripple = require('ripple-lib'); -var Amount = require('ripple-lib').Amount; -var Remote = require('ripple-lib').Remote; -var Transaction = require('ripple-lib').Transaction; -var Server = require('./server').Server; -var testutils = require('./testutils'); -var config = testutils.init_config(); - -var make_suite = process.env.CI ? suite.skip : suite; -make_suite('Robust transaction submission', function() { - var $ = { }; - - setup(function(done) { - testutils.build_setup().call($, function() { - $.remote.local_signing = true; - - $.remote.request_subscribe() - .accounts($.remote.account('root')._account_id) - .callback(done); - }); - }); - - teardown(function(done) { - testutils.build_teardown().call($, done); - }); - - // Payment is submitted (without a destination tag) - // to a destination which requires one. - // - // The sequence is now in the future. - // - // Immediately subsequent transactions should err - // with terPRE_SEQ. - // - // Gaps in the sequence should be filled with an - // empty transaction. - // - // Transaction should ultimately succeed. - // - // Subsequent transactions should be submitted with - // an up-to-date transction sequence. i.e. the - // internal sequence should always catch up. - - test('sequence realignment', function(done) { - var self = this; - - var steps = [ - - function createAccounts(callback) { - self.what = 'Create accounts'; - testutils.create_accounts($.remote, 'root', '20000.0', [ 'alice', 'bob' ], callback); - }, - - function sendInvalidTransaction(callback) { - self.what = 'Send transaction without a destination tag'; - - var tx = $.remote.transaction().payment({ - from: 'root', - to: 'alice', - amount: Amount.from_human('1 XRP') - }); - - tx.once('submitted', function(m) { - assert.strictEqual('tefMAX_LEDGER', m.engine_result); - }); - tx.once('error', function(m) { - assert.strictEqual('tejMaxLedger', m.engine_result); - }); - - // Standalone mode starts with the open ledger as 3, so there's no way - // for this to be anything other than tefMAX_LEDGER. - tx.lastLedger(1); - tx.submit(); - - //Invoke callback immediately - callback(null, tx); - }, - - function sendValidTransaction(previousTx, callback) { - self.what = 'Send normal transaction which should succeed'; - - var tx = $.remote.transaction().payment({ - from: 'root', - to: 'bob', - amount: Amount.from_human('1 XRP') - }); - - tx.on('submitted', function(m) { - //console.log('Submitted', m); - }); - - tx.once('resubmitted', function() { - self.resubmitted = true; - }); - - //First attempt at submission should result in - //terPRE_SEQ as the sequence is still in the future - tx.once('submitted', function(m) { - assert.strictEqual('terPRE_SEQ', m.engine_result); - }); - tx.once('final', function() { - assert(previousTx.finalized, - "Expected lastLedger 1 transaction to be finalized"); - callback(); - }); - - tx.submit(); - - testutils.ledger_wait($.remote, tx); - }, - - function checkPending(callback) { - self.what = 'Check pending'; - var pending = $.remote.getAccount('root')._transactionManager._pending; - assert.strictEqual(pending._queue.length, 0, 'Pending transactions persisting'); - callback(); - }, - - function verifyBalance(callback) { - self.what = 'Verify balance'; - testutils.verify_balance($.remote, 'bob', '20000999988', callback); - } - - ] - - async.waterfall(steps, function(err) { - assert(!err, self.what + ': ' + err); - assert(self.resubmitted, 'Transaction failed to resubmit'); - done(); - }); - }); - - // Submit a normal payment which should succeed. - // - // Remote disconnects immediately after submission - // and before the validated transaction result is - // received. - // - // Remote reconnects in the future. During this - // time it is presumed that the transaction should - // have succeeded, but an immediate response was - // not possible, as the server was disconnected. - // - // Upon reconnection, recent account transaction - // history is loaded. - // - // The submitted transaction should be detected, - // and the transaction should ultimately succeed. - - test('temporary server disconnection', function(done) { - var self = this; - - var steps = [ - - function createAccounts(callback) { - self.what = 'Create accounts'; - testutils.create_accounts($.remote, 'root', '20000.0', [ 'alice' ], callback); - }, - - function submitTransaction(callback) { - self.what = 'Submit a transaction'; - - var tx = $.remote.transaction().payment({ - from: 'root', - to: 'alice', - amount: Amount.from_human('1 XRP') - }); - - tx.submit(); - - setImmediate(function() { - $.remote.once('disconnect', function remoteDisconnected() { - assert(!$.remote._connected); - - tx.once('final', function(m) { - assert.strictEqual(m.engine_result, 'tesSUCCESS'); - callback(); - }); - - $.remote.connect(function() { - testutils.ledger_wait($.remote, tx); - }); - }); - - $.remote.disconnect(); - }); - }, - - function waitLedger(callback) { - self.what = 'Wait ledger'; - $.remote.once('ledger_closed', function() { - callback(); - }); - $.remote.ledger_accept(); - }, - - function checkPending(callback) { - self.what = 'Check pending'; - var pending = $.remote.getAccount('root')._transactionManager._pending; - assert.strictEqual(pending._queue.length, 0, 'Pending transactions persisting'); - callback(); - }, - - function verifyBalance(callback) { - self.what = 'Verify balance'; - testutils.verify_balance($.remote, 'alice', '20000999988', callback); - } - - ] - - async.series(steps, function(err) { - assert(!err, self.what + ': ' + err); - done(); - }); - }); - - test('temporary server disconnection -- reconnect after max ledger wait', function(done) { - var self = this; - - var steps = [ - - function createAccounts(callback) { - self.what = 'Create accounts'; - testutils.create_accounts($.remote, 'root', '20000.0', [ 'alice' ], callback); - }, - - function waitLedgers(callback) { - self.what = 'Wait ledger'; - $.remote.once('ledger_closed', function() { - callback(); - }); - $.remote.ledger_accept(); - }, - - function verifyBalance(callback) { - self.what = 'Verify balance'; - testutils.verify_balance($.remote, 'alice', '19999999988', callback); - }, - - function submitTransaction(callback) { - self.what = 'Submit a transaction'; - - var tx = $.remote.transaction().payment({ - from: 'root', - to: 'alice', - amount: Amount.from_human('1 XRP') - }); - - tx.once('submitted', function(m) { - assert.strictEqual(m.engine_result, 'tesSUCCESS'); - - var handleMessage = $.remote._handleMessage; - $.remote._handleMessage = function(){}; - - var ledgers = 0; - - ;(function nextLedger() { - if (++ledgers > 8) { - tx.once('final', function() { callback(); }); - $.remote._handleMessage = handleMessage; - $.remote.disconnect(function() { - assert(!$.remote._connected); - var pending = $.remote.getAccount('root')._transactionManager._pending; - assert.strictEqual(pending._queue.length, 1, 'Pending transactions persisting'); - $.remote.connect(); - }); - } else { - $.remote._getServer().once('ledger_closed', function() { - setTimeout(nextLedger, 20); - }); - $.remote.ledger_accept(); - } - })(); - }); - - tx.submit(); - }, - - function waitLedgers(callback) { - self.what = 'Wait ledgers'; - - var ledgers = 0; - - ;(function nextLedger() { - $.remote.once('ledger_closed', function() { - if (++ledgers === 3) { - callback(); - } else { - setTimeout(nextLedger, process.env.CI ? 400 : 100 ); - } - }); - $.remote.ledger_accept(); - })(); - }, - - function checkPending(callback) { - self.what = 'Check pending'; - var pending = $.remote.getAccount('root')._transactionManager._pending; - assert.strictEqual(pending._queue.length, 0, 'Pending transactions persisting'); - callback(); - }, - - function verifyBalance(callback) { - self.what = 'Verify balance'; - testutils.verify_balance($.remote, 'alice', '20000999988', callback); - } - - ] - - async.series(steps, function(err) { - assert(!err, self.what + ': ' + err); - done(); - }); - }); - - // Submit request times out - // - // Since the transaction ID is generated locally, the - // transaction should still validate from the account - // transaction stream, even without a response to the - // original submit request - - test('submission timeout', function(done) { - var self = this; - - var steps = [ - - function createAccounts(callback) { - self.what = 'Create accounts'; - testutils.create_accounts($.remote, 'root', '20000.0', [ 'alice' ], callback); - }, - - function submitTransaction(callback) { - self.what = 'Submit a transaction whose submit request times out'; - - var tx = $.remote.transaction().payment({ - from: 'root', - to: 'alice', - amount: Amount.from_human('1 XRP') - }); - - var timed_out = false; - - $.remote.getAccount('root')._transactionManager._submissionTimeout = 0; - - // A response from transaction submission should never - // actually be received - tx.once('timeout', function() { timed_out = true; }); - - tx.once('final', function(m) { - assert(timed_out, 'Transaction submission failed to time out'); - assert.strictEqual(m.engine_result, 'tesSUCCESS'); - callback(); - }); - - tx.submit(); - - testutils.ledger_wait($.remote, tx); - }, - - function checkPending(callback) { - self.what = 'Check pending'; - assert.strictEqual($.remote.getAccount('root')._transactionManager._pending.length(), 0, 'Pending transactions persisting'); - callback(); - }, - - function verifyBalance(callback) { - self.what = 'Verify balance'; - testutils.verify_balance($.remote, 'alice', '20000999988', callback); - } - - ] - - async.series(steps, function(err) { - assert(!err, self.what + ': ' + err); - done(); - }); - }); - - // Subscribing to accounts_proposed will result in ripple-lib - // being streamed non-validated (proposed) transactions - // - // This test ensures that only validated transactions will - // trigger a transaction success event - - test('subscribe to accounts_proposed', function(done) { - var self = this; - - var series = [ - - function subscribeToAccountsProposed(callback) { - self.what = 'Subscribe to accounts_proposed'; - - $.remote.requestSubscribe() - .addAccountProposed('root') - .callback(callback); - }, - - function submitTransaction(callback) { - self.what = 'Submit a transaction'; - - var tx = $.remote.transaction().accountSet('root'); - - var receivedProposedTransaction = false; - - $.remote.on('transaction', function(tx) { - if (tx.status === 'proposed') { - receivedProposedTransaction = true; - } - }); - - tx.submit(function(err, m) { - assert(!err, err); - assert(receivedProposedTransaction, 'Did not received proposed transaction from stream'); - assert(m.engine_result, 'tesSUCCESS'); - assert(m.validated, 'Transaction is finalized with invalidated transaction stream response'); - done(); - }); - - testutils.ledger_wait($.remote, tx); - } - - ] - - async.series(series, function(err, m) { - assert(!err, self.what + ': ' + err); - done(); - }); - }); - - // Validate that LastLedgerSequence works - test('set LastLedgerSequence', function(done) { - var self = this; - - var series = [ - - function createAccounts(callback) { - self.what = 'Create accounts'; - testutils.create_accounts($.remote, 'root', '20000.0', [ 'alice' ], callback); - }, - - function submitTransaction(callback) { - var tx = $.remote.transaction().payment('root', 'alice', '1'); - tx.lastLedger(0); - - tx.once('submitted', function(m) { - assert.strictEqual(m.engine_result, 'tefMAX_LEDGER'); - callback(); - }); - - tx.submit(); - } - - ] - - async.series(series, function(err) { - assert(!err, self.what); - done(); - }); - }); -});