RPC robust transaction unit test (RIPD-1079)

This commit is contained in:
Miguel Portilla
2016-02-10 13:21:03 -05:00
committed by Vinnie Falco
parent 1d0ca51c88
commit 8f83f69325
5 changed files with 329 additions and 463 deletions

View File

@@ -3283,6 +3283,10 @@
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\ripple\rpc\tests\RobustTransaction.test.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\ripple\rpc\tests\Status.test.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>

View File

@@ -3765,6 +3765,9 @@
<ClCompile Include="..\..\src\ripple\rpc\tests\LedgerRequestRPC.test.cpp">
<Filter>ripple\rpc\tests</Filter>
</ClCompile>
<ClCompile Include="..\..\src\ripple\rpc\tests\RobustTransaction.test.cpp">
<Filter>ripple\rpc\tests</Filter>
</ClCompile>
<ClCompile Include="..\..\src\ripple\rpc\tests\Status.test.cpp">
<Filter>ripple\rpc\tests</Filter>
</ClCompile>

View File

@@ -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 <BeastConfig.h>
#include <ripple/core/JobQueue.h>
#include <ripple/protocol/JsonFields.h>
#include <ripple/test/jtx.h>
#include <ripple/test/WSClient.h>
#include <beast/unit_test/suite.h>
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

View File

@@ -105,5 +105,6 @@
#include <ripple/rpc/tests/JSONRPC.test.cpp>
#include <ripple/rpc/tests/LedgerRequestRPC.test.cpp>
#include <ripple/rpc/tests/KeyGeneration.test.cpp>
#include <ripple/rpc/tests/RobustTransaction.test.cpp>
#include <ripple/rpc/tests/Status.test.cpp>
#include <ripple/rpc/tests/Subscribe.test.cpp>

View File

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