rippled
RobustTransaction_test.cpp
1 //------------------------------------------------------------------------------
2 /*
3  This file is part of rippled: https://github.com/ripple/rippled
4  Copyright (c) 2012, 2013 Ripple Labs Inc.
5 
6  Permission to use, copy, modify, and/or distribute this software for any
7  purpose with or without fee is hereby granted, provided that the above
8  copyright notice and this permission notice appear in all copies.
9 
10  THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11  WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12  MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13  ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14  WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15  ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16  OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 */
18 //==============================================================================
19 
20 #include <ripple/beast/unit_test.h>
21 #include <ripple/core/JobQueue.h>
22 #include <ripple/protocol/jss.h>
23 #include <test/jtx.h>
24 #include <test/jtx/WSClient.h>
25 
26 namespace ripple {
27 namespace test {
28 
29 class RobustTransaction_test : public beast::unit_test::suite
30 {
31 public:
32  void
34  {
35  using namespace std::chrono_literals;
36  using namespace jtx;
37  Env env(*this);
38  env.fund(XRP(10000), "alice", "bob");
39  env.close();
40  auto wsc = makeWSClient(env.app().config());
41 
42  {
43  // RPC subscribe to transactions stream
44  Json::Value jv;
45  jv[jss::streams] = Json::arrayValue;
46  jv[jss::streams].append("transactions");
47  jv = wsc->invoke("subscribe", jv);
48  BEAST_EXPECT(jv[jss::status] == "success");
49  if (wsc->version() == 2)
50  {
51  BEAST_EXPECT(
52  jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
53  BEAST_EXPECT(
54  jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
55  BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
56  }
57  }
58 
59  {
60  // Submit past ledger sequence transaction
61  Json::Value payment;
62  payment[jss::secret] = toBase58(generateSeed("alice"));
63  payment[jss::tx_json] = pay("alice", "bob", XRP(1));
64  payment[jss::tx_json][sfLastLedgerSequence.fieldName] = 1;
65  auto jv = wsc->invoke("submit", payment);
66  if (wsc->version() == 2)
67  {
68  BEAST_EXPECT(
69  jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
70  BEAST_EXPECT(
71  jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
72  BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
73  }
74  BEAST_EXPECT(
75  jv[jss::result][jss::engine_result] == "tefMAX_LEDGER");
76 
77  // Submit past sequence transaction
78  payment[jss::tx_json] = pay("alice", "bob", XRP(1));
79  payment[jss::tx_json][sfSequence.fieldName] = env.seq("alice") - 1;
80  jv = wsc->invoke("submit", payment);
81  if (wsc->version() == 2)
82  {
83  BEAST_EXPECT(
84  jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
85  BEAST_EXPECT(
86  jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
87  BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
88  }
89  BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tefPAST_SEQ");
90 
91  // Submit future sequence transaction
92  payment[jss::tx_json][sfSequence.fieldName] = env.seq("alice") + 1;
93  jv = wsc->invoke("submit", payment);
94  if (wsc->version() == 2)
95  {
96  BEAST_EXPECT(
97  jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
98  BEAST_EXPECT(
99  jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
100  BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
101  }
102  BEAST_EXPECT(jv[jss::result][jss::engine_result] == "terPRE_SEQ");
103 
104  // Submit transaction to bridge the sequence gap
105  payment[jss::tx_json][sfSequence.fieldName] = env.seq("alice");
106  jv = wsc->invoke("submit", payment);
107  if (wsc->version() == 2)
108  {
109  BEAST_EXPECT(
110  jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
111  BEAST_EXPECT(
112  jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
113  BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
114  }
115  BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tesSUCCESS");
116 
117  // Wait for the jobqueue to process everything
118  env.app().getJobQueue().rendezvous();
119 
120  // Finalize transactions
121  jv = wsc->invoke("ledger_accept");
122  if (wsc->version() == 2)
123  {
124  BEAST_EXPECT(
125  jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
126  BEAST_EXPECT(
127  jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
128  BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
129  }
130  BEAST_EXPECT(jv[jss::result].isMember(jss::ledger_current_index));
131  }
132 
133  {
134  // Check balances
135  BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
136  auto const& ff = jv[jss::meta]["AffectedNodes"][1u]
137  ["ModifiedNode"]["FinalFields"];
138  return ff[jss::Account] == Account("bob").human() &&
139  ff["Balance"] == "10001000000";
140  }));
141 
142  BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
143  auto const& ff = jv[jss::meta]["AffectedNodes"][1u]
144  ["ModifiedNode"]["FinalFields"];
145  return ff[jss::Account] == Account("bob").human() &&
146  ff["Balance"] == "10002000000";
147  }));
148  }
149 
150  {
151  // RPC unsubscribe to transactions stream
152  Json::Value jv;
153  jv[jss::streams] = Json::arrayValue;
154  jv[jss::streams].append("transactions");
155  jv = wsc->invoke("unsubscribe", jv);
156  if (wsc->version() == 2)
157  {
158  BEAST_EXPECT(
159  jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
160  BEAST_EXPECT(
161  jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
162  BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
163  }
164  BEAST_EXPECT(jv[jss::status] == "success");
165  }
166  }
167 
168  /*
169  Submit a normal payment. Client disconnects after the proposed
170  transaction result is received.
171 
172  Client reconnects in the future. During this time it is presumed that the
173  transaction should have succeeded.
174 
175  Upon reconnection, recent account transaction history is loaded.
176  The submitted transaction should be detected, and the transaction should
177  ultimately succeed.
178  */
179  void
181  {
182  using namespace jtx;
183  Env env(*this);
184  env.fund(XRP(10000), "alice", "bob");
185  env.close();
186  auto wsc = makeWSClient(env.app().config());
187 
188  {
189  // Submit normal payment
190  Json::Value jv;
191  jv[jss::secret] = toBase58(generateSeed("alice"));
192  jv[jss::tx_json] = pay("alice", "bob", XRP(1));
193  jv = wsc->invoke("submit", jv);
194  if (wsc->version() == 2)
195  {
196  BEAST_EXPECT(
197  jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
198  BEAST_EXPECT(
199  jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
200  BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
201  }
202  BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tesSUCCESS");
203 
204  // Disconnect
205  wsc.reset();
206 
207  // Server finalizes transaction
208  env.close();
209  }
210 
211  {
212  // RPC account_tx
213  Json::Value jv;
214  jv[jss::account] = Account("bob").human();
215  jv[jss::ledger_index_min] = -1;
216  jv[jss::ledger_index_max] = -1;
217  wsc = makeWSClient(env.app().config());
218  jv = wsc->invoke("account_tx", jv);
219  if (wsc->version() == 2)
220  {
221  BEAST_EXPECT(
222  jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
223  BEAST_EXPECT(
224  jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
225  BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
226  }
227 
228  // Check balance
229  auto ff = jv[jss::result][jss::transactions][0u][jss::meta]
230  ["AffectedNodes"][1u]["ModifiedNode"]["FinalFields"];
231  BEAST_EXPECT(ff[jss::Account] == Account("bob").human());
232  BEAST_EXPECT(ff["Balance"] == "10001000000");
233  }
234  }
235 
236  void
238  {
239  using namespace std::chrono_literals;
240  using namespace jtx;
241  Env env(*this);
242  env.fund(XRP(10000), "alice", "bob");
243  env.close();
244  auto wsc = makeWSClient(env.app().config());
245 
246  {
247  // Submit normal payment
248  Json::Value jv;
249  jv[jss::secret] = toBase58(generateSeed("alice"));
250  jv[jss::tx_json] = pay("alice", "bob", XRP(1));
251  jv = wsc->invoke("submit", jv);
252  if (wsc->version() == 2)
253  {
254  BEAST_EXPECT(
255  jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
256  BEAST_EXPECT(
257  jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
258  BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
259  }
260  BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tesSUCCESS");
261 
262  // Finalize transaction
263  jv = wsc->invoke("ledger_accept");
264  if (wsc->version() == 2)
265  {
266  BEAST_EXPECT(
267  jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
268  BEAST_EXPECT(
269  jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
270  BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
271  }
272  BEAST_EXPECT(jv[jss::result].isMember(jss::ledger_current_index));
273 
274  // Wait for the jobqueue to process everything
275  env.app().getJobQueue().rendezvous();
276  }
277 
278  {
279  {
280  // RPC subscribe to ledger stream
281  Json::Value jv;
282  jv[jss::streams] = Json::arrayValue;
283  jv[jss::streams].append("ledger");
284  jv = wsc->invoke("subscribe", jv);
285  if (wsc->version() == 2)
286  {
287  BEAST_EXPECT(
288  jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
289  BEAST_EXPECT(
290  jv.isMember(jss::ripplerpc) &&
291  jv[jss::ripplerpc] == "2.0");
292  BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
293  }
294  BEAST_EXPECT(jv[jss::status] == "success");
295  }
296 
297  // Close ledgers
298  for (auto i = 0; i < 8; ++i)
299  {
300  auto jv = wsc->invoke("ledger_accept");
301  if (wsc->version() == 2)
302  {
303  BEAST_EXPECT(
304  jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
305  BEAST_EXPECT(
306  jv.isMember(jss::ripplerpc) &&
307  jv[jss::ripplerpc] == "2.0");
308  BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
309  }
310  BEAST_EXPECT(
311  jv[jss::result].isMember(jss::ledger_current_index));
312 
313  // Wait for the jobqueue to process everything
314  env.app().getJobQueue().rendezvous();
315 
316  BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jval) {
317  return jval[jss::type] == "ledgerClosed";
318  }));
319  }
320 
321  {
322  // RPC unsubscribe to ledger stream
323  Json::Value jv;
324  jv[jss::streams] = Json::arrayValue;
325  jv[jss::streams].append("ledger");
326  jv = wsc->invoke("unsubscribe", jv);
327  if (wsc->version() == 2)
328  {
329  BEAST_EXPECT(
330  jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
331  BEAST_EXPECT(
332  jv.isMember(jss::ripplerpc) &&
333  jv[jss::ripplerpc] == "2.0");
334  BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
335  }
336  BEAST_EXPECT(jv[jss::status] == "success");
337  }
338  }
339 
340  {
341  // Disconnect, reconnect
342  wsc = makeWSClient(env.app().config());
343  {
344  // RPC subscribe to ledger stream
345  Json::Value jv;
346  jv[jss::streams] = Json::arrayValue;
347  jv[jss::streams].append("ledger");
348  jv = wsc->invoke("subscribe", jv);
349  if (wsc->version() == 2)
350  {
351  BEAST_EXPECT(
352  jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
353  BEAST_EXPECT(
354  jv.isMember(jss::ripplerpc) &&
355  jv[jss::ripplerpc] == "2.0");
356  BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
357  }
358  BEAST_EXPECT(jv[jss::status] == "success");
359  }
360 
361  // Close ledgers
362  for (auto i = 0; i < 2; ++i)
363  {
364  auto jv = wsc->invoke("ledger_accept");
365  if (wsc->version() == 2)
366  {
367  BEAST_EXPECT(
368  jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
369  BEAST_EXPECT(
370  jv.isMember(jss::ripplerpc) &&
371  jv[jss::ripplerpc] == "2.0");
372  BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
373  }
374  BEAST_EXPECT(
375  jv[jss::result].isMember(jss::ledger_current_index));
376 
377  // Wait for the jobqueue to process everything
378  env.app().getJobQueue().rendezvous();
379 
380  BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jval) {
381  return jval[jss::type] == "ledgerClosed";
382  }));
383  }
384 
385  {
386  // RPC unsubscribe to ledger stream
387  Json::Value jv;
388  jv[jss::streams] = Json::arrayValue;
389  jv[jss::streams].append("ledger");
390  jv = wsc->invoke("unsubscribe", jv);
391  if (wsc->version() == 2)
392  {
393  BEAST_EXPECT(
394  jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
395  BEAST_EXPECT(
396  jv.isMember(jss::ripplerpc) &&
397  jv[jss::ripplerpc] == "2.0");
398  BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
399  }
400  BEAST_EXPECT(jv[jss::status] == "success");
401  }
402  }
403 
404  {
405  // RPC account_tx
406  Json::Value jv;
407  jv[jss::account] = Account("bob").human();
408  jv[jss::ledger_index_min] = -1;
409  jv[jss::ledger_index_max] = -1;
410  wsc = makeWSClient(env.app().config());
411  jv = wsc->invoke("account_tx", jv);
412  if (wsc->version() == 2)
413  {
414  BEAST_EXPECT(
415  jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
416  BEAST_EXPECT(
417  jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
418  BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
419  }
420 
421  // Check balance
422  auto ff = jv[jss::result][jss::transactions][0u][jss::meta]
423  ["AffectedNodes"][1u]["ModifiedNode"]["FinalFields"];
424  BEAST_EXPECT(ff[jss::Account] == Account("bob").human());
425  BEAST_EXPECT(ff["Balance"] == "10001000000");
426  }
427  }
428 
429  void
431  {
432  using namespace std::chrono_literals;
433  using namespace jtx;
434  Env env(*this);
435  env.fund(XRP(10000), "alice");
436  env.close();
437  auto wsc = makeWSClient(env.app().config());
438 
439  {
440  // RPC subscribe to accounts_proposed stream
441  Json::Value jv;
442  jv[jss::accounts_proposed] = Json::arrayValue;
443  jv[jss::accounts_proposed].append(Account("alice").human());
444  jv = wsc->invoke("subscribe", jv);
445  if (wsc->version() == 2)
446  {
447  BEAST_EXPECT(
448  jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
449  BEAST_EXPECT(
450  jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
451  BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
452  }
453  BEAST_EXPECT(jv[jss::status] == "success");
454  }
455 
456  {
457  // Submit account_set transaction
458  Json::Value jv;
459  jv[jss::secret] = toBase58(generateSeed("alice"));
460  jv[jss::tx_json] = fset("alice", 0);
461  jv[jss::tx_json][jss::Fee] = 10;
462  jv = wsc->invoke("submit", jv);
463  if (wsc->version() == 2)
464  {
465  BEAST_EXPECT(
466  jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
467  BEAST_EXPECT(
468  jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
469  BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
470  }
471  BEAST_EXPECT(jv[jss::result][jss::engine_result] == "tesSUCCESS");
472  }
473 
474  {
475  // Check stream update
476  BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) {
477  return jv[jss::transaction][jss::TransactionType] ==
478  jss::AccountSet;
479  }));
480  }
481 
482  {
483  // RPC unsubscribe to accounts_proposed stream
484  Json::Value jv;
485  jv[jss::accounts_proposed] = Json::arrayValue;
486  jv[jss::accounts_proposed].append(Account("alice").human());
487  jv = wsc->invoke("unsubscribe", jv);
488  if (wsc->version() == 2)
489  {
490  BEAST_EXPECT(
491  jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
492  BEAST_EXPECT(
493  jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
494  BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
495  }
496  BEAST_EXPECT(jv[jss::status] == "success");
497  }
498  }
499 
500  void
501  run() override
502  {
504  testReconnect();
507  }
508 };
509 
510 BEAST_DEFINE_TESTSUITE(RobustTransaction, app, ripple);
511 
512 } // namespace test
513 } // namespace ripple
ripple::test::jtx::XRP
const XRP_t XRP
Converts to XRP Issue or STAmount.
Definition: amount.cpp:105
Json::arrayValue
@ arrayValue
array value (ordered list)
Definition: json_value.h:42
ripple::sfSequence
const SF_UINT32 sfSequence
ripple::test::RobustTransaction_test
Definition: RobustTransaction_test.cpp:29
ripple::SField::fieldName
const std::string fieldName
Definition: SField.h:132
ripple::toBase58
std::string toBase58(AccountID const &v)
Convert AccountID to base58 checked string.
Definition: AccountID.cpp:104
ripple::test::jtx::Account::human
std::string const & human() const
Returns the human readable public key.
Definition: Account.h:113
ripple::test::RobustTransaction_test::testSequenceRealignment
void testSequenceRealignment()
Definition: RobustTransaction_test.cpp:33
ripple::test::jtx::Env::app
Application & app()
Definition: Env.h:241
Json::Value::append
Value & append(const Value &value)
Append value to array at the end.
Definition: json_value.cpp:882
ripple::Application::config
virtual Config & config()=0
ripple::test::jtx::fset
Json::Value fset(Account const &account, std::uint32_t on, std::uint32_t off=0)
Add and/or remove flag.
Definition: flags.cpp:28
ripple::test::RobustTransaction_test::run
void run() override
Definition: RobustTransaction_test.cpp:501
ripple::Application::getJobQueue
virtual JobQueue & getJobQueue()=0
ripple::test::jtx::Env::close
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition: Env.cpp:121
Json::Value::isMember
bool isMember(const char *key) const
Return true if the object has a member named key.
Definition: json_value.cpp:932
ripple::test::RobustTransaction_test::testAccountsProposed
void testAccountsProposed()
Definition: RobustTransaction_test.cpp:430
ripple::test::jtx::Env::seq
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition: Env.cpp:207
ripple::JobQueue::rendezvous
void rendezvous()
Block until no jobs running.
Definition: JobQueue.cpp:254
ripple::generateSeed
Seed generateSeed(std::string const &passPhrase)
Generate a seed deterministically.
Definition: Seed.cpp:69
ripple
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition: RCLCensorshipDetector.h:29
ripple::test::jtx::pay
Json::Value pay(Account const &account, Account const &to, AnyAmount amount)
Create a payment.
Definition: pay.cpp:29
ripple::test::jtx::Env::fund
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition: Env.cpp:228
ripple::test::makeWSClient
std::unique_ptr< WSClient > makeWSClient(Config const &cfg, bool v2, unsigned rpc_version, std::unordered_map< std::string, std::string > const &headers)
Returns a client operating through WebSockets/S.
Definition: WSClient.cpp:300
ripple::test::RobustTransaction_test::testReconnectAfterWait
void testReconnectAfterWait()
Definition: RobustTransaction_test.cpp:237
ripple::test::jtx::Account
Immutable cryptographic account descriptor.
Definition: Account.h:37
ripple::test::RobustTransaction_test::testReconnect
void testReconnect()
Definition: RobustTransaction_test.cpp:180
ripple::sfLastLedgerSequence
const SF_UINT32 sfLastLedgerSequence
ripple::test::jtx::Env
A transaction testing environment.
Definition: Env.h:116
Json::Value
Represents a JSON value.
Definition: json_value.h:145
ripple::test::BEAST_DEFINE_TESTSUITE
BEAST_DEFINE_TESTSUITE(DeliverMin, app, ripple)