From 8d9dffcf846c3737e9150acf0cf3940fcfab4aa4 Mon Sep 17 00:00:00 2001 From: "Nikolaos D. Bougalis" Date: Fri, 2 Mar 2018 06:23:17 -0800 Subject: [PATCH] Clarify Escrow semantics (RIPD-1571): When creating an escrow, if the `CancelAfter` time is specified but the `FinishAfter` is not, the resulting escrow can be immediately completed using `EscrowFinish`. While this behavior is documented, it is unintuitive and can be confusing for users. This commit introduces a new fix amendment (fix1571) which prevents the creation of new Escrow entries that can be finished immediately and without any requirements. Once the amendment is activated, creating a new Escrow will require specifying the `FinishAfter` time explicitly or requires that a cryptocondition be specified. --- src/ripple/app/tx/impl/Escrow.cpp | 232 +++---- src/ripple/protocol/Feature.h | 6 +- src/ripple/protocol/impl/Feature.cpp | 4 +- src/test/app/Escrow_test.cpp | 883 +++++++++++++++++---------- 4 files changed, 685 insertions(+), 440 deletions(-) diff --git a/src/ripple/app/tx/impl/Escrow.cpp b/src/ripple/app/tx/impl/Escrow.cpp index 32434e2bfb..2c47a85e6f 100644 --- a/src/ripple/app/tx/impl/Escrow.cpp +++ b/src/ripple/app/tx/impl/Escrow.cpp @@ -41,99 +41,52 @@ namespace ripple { /* - Escrow allows an account holder to sequester any amount - of XRP in its own ledger entry, until the escrow process - either finishes or is canceled. + Escrow + ====== - If the escrow process finishes successfully, then the - destination account (which must exist) will receives the - sequestered XRP. If the escrow is, instead, canceled, - the account which created the escrow will receive the - sequestered XRP back instead. + Escrow is a feature of the XRP Ledger that allows you to send conditional + XRP payments. These conditional payments, called escrows, set aside XRP and + deliver it later when certain conditions are met. Conditions to successfully + finish an escrow include time-based unlocks and crypto-conditions. Escrows + can also be set to expire if not finished in time. - EscrowCreate + The XRP set aside in an escrow is locked up. No one can use or destroy the + XRP until the escrow has been successfully finished or canceled. Before the + expiration time, only the intended receiver can get the XRP. After the + expiration time, the XRP can only be returned to the sender. - When an escrow is created, an optional condition may - be attached. If present, that condition must be - fulfilled for the escrow to successfully finish. + For more details on escrow, including examples, diagrams and more please + visit https://ripple.com/build/escrow/#escrow - At the time of creation, one or both of the fields - sfCancelAfter and sfFinishAfter may be provided. If - neither field is specified, the transaction is - malformed. + For details on specific transactions, including fields and validation rules + please see: - Since the escrow eventually becomes a payment, an - optional DestinationTag and an optional SourceTag - are supported in the EscrowCreate transaction. + `EscrowCreate` + -------------- + See: https://ripple.com/build/transactions/#escrowcreate - Validation rules: + `EscrowFinish` + -------------- + See: https://ripple.com/build/transactions/#escrowfinish - sfCondition - If present, specifies a condition; the same - condition along with its matching fulfillment - are required during EscrowFinish. - - sfCancelAfter - If present, escrow may be canceled after the - specified time (seconds after the Ripple epoch). - - sfFinishAfter - If present, must be prior to sfCancelAfter. - A EscrowFinish succeeds only in ledgers after - sfFinishAfter but before sfCancelAfter. - - If absent, same as parentCloseTime - - Malformed if both sfCancelAfter, sfFinishAfter - are absent. - - Malformed if both sfFinishAfter, sfCancelAfter - specified and sfCancelAfter <= sfFinishAfter - - EscrowFinish - - Any account may submit a EscrowFinish. If the escrow - ledger entry specifies a condition, the EscrowFinish - must provide the same condition and its associated - fulfillment in the sfCondition and sfFulfillment - fields, or else the EscrowFinish will fail. - - If the escrow ledger entry specifies sfFinishAfter, the - transaction will fail if parentCloseTime <= sfFinishAfter. - - EscrowFinish transactions must be submitted before - the escrow's sfCancelAfter if present. - - If the escrow ledger entry specifies sfCancelAfter, the - transaction will fail if sfCancelAfter <= parentCloseTime. - - NOTE: The reason the condition must be specified again - is because it must always be possible to verify - the condition without retrieving the escrow - ledger entry. - - EscrowCancel - - Any account may submit a EscrowCancel transaction. - - If the escrow ledger entry does not specify a - sfCancelAfter, the cancel transaction will fail. - - If parentCloseTime <= sfCancelAfter, the transaction - will fail. - - When a escrow is canceled, the funds are returned to - the source account. - - By careful selection of fields in each transaction, - these operations may be achieved: - - * Lock up XRP for a time period - * Execute a payment conditionally + `EscrowCancel` + -------------- + See: https://ripple.com/build/transactions/#escrowcancel */ //------------------------------------------------------------------------------ +/** Has the specified time passed? + + @param now the current time + @param mark the cutoff point + @return true if \a now refers to a time strictly after \a mark, false otherwise. +*/ +static inline bool after (NetClock::time_point now, std::uint32_t mark) +{ + return now.time_since_epoch().count() > mark; +} + XRPAmount EscrowCreate::calculateMaxSpend(STTx const& tx) { @@ -156,14 +109,26 @@ EscrowCreate::preflight (PreflightContext const& ctx) if (ctx.tx[sfAmount] <= beast::zero) return temBAD_AMOUNT; - if (! ctx.tx[~sfCancelAfter] && - ! ctx.tx[~sfFinishAfter]) - return temBAD_EXPIRATION; + // We must specify at least one timeout value + if (! ctx.tx[~sfCancelAfter] && ! ctx.tx[~sfFinishAfter]) + return temBAD_EXPIRATION; + // If both finish and cancel times are specified then the cancel time must + // be strictly after the finish time. if (ctx.tx[~sfCancelAfter] && ctx.tx[~sfFinishAfter] && ctx.tx[sfCancelAfter] <= ctx.tx[sfFinishAfter]) return temBAD_EXPIRATION; + if (ctx.rules.enabled(fix1571)) + { + // In the absence of a FinishAfter, the escrow can be finished + // immediately, which can be confusing. When creating an escrow, + // we want to ensure that either a FinishAfter time is explicitly + // specified or a completion condition is attached. + if (! ctx.tx[~sfFinishAfter] && ! ctx.tx[~sfCondition]) + return temMALFORMED; + } + if (auto const cb = ctx.tx[~sfCondition]) { using namespace ripple::cryptoconditions; @@ -193,20 +158,36 @@ EscrowCreate::doApply() { auto const closeTime = ctx_.view ().info ().parentCloseTime; - if (ctx_.tx[~sfCancelAfter]) + // Prior to fix1571, the cancel and finish times could be greater + // than or equal to the parent ledgers' close time. + // + // With fix1571, we require that they both be strictly greater + // than the parent ledgers' close time. + if (ctx_.view ().rules().enabled(fix1571)) { - auto const cancelAfter = ctx_.tx[sfCancelAfter]; + if (ctx_.tx[~sfCancelAfter] && after(closeTime, ctx_.tx[sfCancelAfter])) + return tecNO_PERMISSION; - if (closeTime.time_since_epoch().count() >= cancelAfter) + if (ctx_.tx[~sfFinishAfter] && after(closeTime, ctx_.tx[sfFinishAfter])) return tecNO_PERMISSION; } - - if (ctx_.tx[~sfFinishAfter]) + else { - auto const finishAfter = ctx_.tx[sfFinishAfter]; + if (ctx_.tx[~sfCancelAfter]) + { + auto const cancelAfter = ctx_.tx[sfCancelAfter]; - if (closeTime.time_since_epoch().count() >= finishAfter) - return tecNO_PERMISSION; + if (closeTime.time_since_epoch().count() >= cancelAfter) + return tecNO_PERMISSION; + } + + if (ctx_.tx[~sfFinishAfter]) + { + auto const finishAfter = ctx_.tx[sfFinishAfter]; + + if (closeTime.time_since_epoch().count() >= finishAfter) + return tecNO_PERMISSION; + } } auto const account = ctx_.tx[sfAccount]; @@ -383,17 +364,35 @@ EscrowFinish::doApply() if (! slep) return tecNO_TARGET; - // Too soon? - if ((*slep)[~sfFinishAfter] && - ctx_.view().info().parentCloseTime.time_since_epoch().count() <= - (*slep)[sfFinishAfter]) - return tecNO_PERMISSION; + // If a cancel time is present, a finish operation should only succeed prior + // to that time. fix1571 corrects a logic error in the check that would make + // a finish only succeed strictly after the cancel time. + if (ctx_.view ().rules().enabled(fix1571)) + { + auto const now = ctx_.view().info().parentCloseTime; - // Too late? - if ((*slep)[~sfCancelAfter] && - (*slep)[sfCancelAfter] <= - ctx_.view().info().parentCloseTime.time_since_epoch().count()) - return tecNO_PERMISSION; + // Too soon: can't execute before the finish time + if ((*slep)[~sfFinishAfter] && ! after(now, (*slep)[sfFinishAfter])) + return tecNO_PERMISSION; + + // Too late: can't execute after the cancel time + if ((*slep)[~sfCancelAfter] && after(now, (*slep)[sfCancelAfter])) + return tecNO_PERMISSION; + } + else + { + // Too soon? + if ((*slep)[~sfFinishAfter] && + ctx_.view().info().parentCloseTime.time_since_epoch().count() <= + (*slep)[sfFinishAfter]) + return tecNO_PERMISSION; + + // Too late? + if ((*slep)[~sfCancelAfter] && + ctx_.view().info().parentCloseTime.time_since_epoch().count() <= + (*slep)[sfCancelAfter]) + return tecNO_PERMISSION; + } // Check cryptocondition fulfillment { @@ -515,17 +514,32 @@ EscrowCancel::preflight (PreflightContext const& ctx) TER EscrowCancel::doApply() { - auto const k = keylet::escrow( - ctx_.tx[sfOwner], ctx_.tx[sfOfferSequence]); + auto const k = keylet::escrow(ctx_.tx[sfOwner], ctx_.tx[sfOfferSequence]); auto const slep = ctx_.view().peek(k); if (! slep) return tecNO_TARGET; - // Too soon? - if (! (*slep)[~sfCancelAfter] || - ctx_.view().info().parentCloseTime.time_since_epoch().count() <= + if (ctx_.view ().rules().enabled(fix1571)) + { + auto const now = ctx_.view().info().parentCloseTime; + + // No cancel time specified: can't execute at all. + if (! (*slep)[~sfCancelAfter]) + return tecNO_PERMISSION; + + // Too soon: can't execute before the cancel time. + if (! after(now, (*slep)[sfCancelAfter])) + return tecNO_PERMISSION; + } + else + { + // Too soon? + if (!(*slep)[~sfCancelAfter] || + ctx_.view().info().parentCloseTime.time_since_epoch().count() <= (*slep)[sfCancelAfter]) - return tecNO_PERMISSION; + return tecNO_PERMISSION; + } + AccountID const account = (*slep)[sfAccount]; // Remove escrow from owner directory diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index a6c9c050bf..16c3abc359 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -75,7 +75,8 @@ class FeatureCollections "fix1523", "fix1528", "DepositAuth", - "Checks" + "Checks", + "fix1571" }; std::vector features; @@ -135,7 +136,7 @@ public: using base::bitset; using base::operator==; using base::operator!=; - + using base::test; using base::all; using base::any; @@ -357,6 +358,7 @@ extern uint256 const fix1523; extern uint256 const fix1528; extern uint256 const featureDepositAuth; extern uint256 const featureChecks; +extern uint256 const fix1571; } // ripple diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index e7b8b38609..0d6389b8df 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -108,7 +108,8 @@ detail::supportedAmendments () { "B9E739B8296B4A1BB29BE990B17D66E21B62A300A909F25AC55C22D6C72E1F9D fix1523" }, { "1D3463A5891F9E589C5AE839FFAC4A917CE96197098A1EF22304E1BC5B98A454 fix1528" }, { "F64E1EABBE79D55B3BB82020516CEC2C582A98A6BFE20FBE9BB6A0D233418064 DepositAuth"}, - { "157D2D480E006395B76F948E3E07A45A05FE10230D88A7993C71F97AE4B1F2D1 Checks"} + { "157D2D480E006395B76F948E3E07A45A05FE10230D88A7993C71F97AE4B1F2D1 Checks"}, + { "7117E2EC2DBF119CA55181D69819F1999ECEE1A0225A7FD2B9ED47940968479C fix1571" } }; return supported; } @@ -158,5 +159,6 @@ uint256 const fix1523 = *getRegisteredFeature("fix1523"); uint256 const fix1528 = *getRegisteredFeature("fix1528"); uint256 const featureDepositAuth = *getRegisteredFeature("DepositAuth"); uint256 const featureChecks = *getRegisteredFeature("Checks"); +uint256 const fix1571 = *getRegisteredFeature("fix1571"); } // ripple diff --git a/src/test/app/Escrow_test.cpp b/src/test/app/Escrow_test.cpp index debcfdcc6c..c500340a1c 100644 --- a/src/test/app/Escrow_test.cpp +++ b/src/test/app/Escrow_test.cpp @@ -75,11 +75,100 @@ struct Escrow_test : public beast::unit_test::suite 0x81, 0x01, 0x04 }}; + /** Set the "FinishAfter" time tag on a JTx */ + struct finish_time + { + private: + NetClock::time_point value_; + + public: + explicit + finish_time (NetClock::time_point const& value) + : value_ (value) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jt) const + { + jt.jv["FinishAfter"] = value_.time_since_epoch().count(); + } + }; + + /** Set the "CancelAfter" time tag on a JTx */ + struct cancel_time + { + private: + NetClock::time_point value_; + + public: + explicit + cancel_time (NetClock::time_point const& value) + : value_ (value) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jt) const + { + jt.jv["CancelAfter"] = value_.time_since_epoch().count(); + } + }; + + struct condition + { + private: + std::string value_; + + public: + explicit + condition (Slice condition) + : value_ (strHex(condition)) + { + } + + template + condition (std::array c) + : condition(makeSlice(c)) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jt) const + { + jt.jv["Condition"] = value_; + } + }; + + struct fulfillment + { + private: + std::string value_; + + public: + explicit + fulfillment (Slice condition) + : value_ (strHex(condition)) + { + } + + template + fulfillment (std::array f) + : fulfillment(makeSlice(f)) + { + } + + void + operator()(jtx::Env&, jtx::JTx& jt) const + { + jt.jv["Fulfillment"] = value_; + } + }; + static Json::Value - condpay (jtx::Account const& account, jtx::Account const& to, - STAmount const& amount, Slice condition, - NetClock::time_point const& cancelAfter) + escrow ( + jtx::Account const& account, jtx::Account const& to, STAmount const& amount) { using namespace jtx; Json::Value jv; @@ -88,68 +177,6 @@ struct Escrow_test : public beast::unit_test::suite jv[jss::Account] = account.human(); jv[jss::Destination] = to.human(); jv[jss::Amount] = amount.getJson(0); - jv["CancelAfter"] = - cancelAfter.time_since_epoch().count(); - jv["Condition"] = strHex(condition); - return jv; - } - - static - Json::Value - condpay (jtx::Account const& account, jtx::Account const& to, - STAmount const& amount, Slice condition, - NetClock::time_point const& cancelAfter, - NetClock::time_point const& finishAfter) - { - auto jv = condpay (account, to, amount, condition, cancelAfter); - jv ["FinishAfter"] = finishAfter.time_since_epoch().count(); - return jv; - } - - static - Json::Value - lockup (jtx::Account const& account, jtx::Account const& to, - STAmount const& amount, NetClock::time_point const& expiry) - { - using namespace jtx; - Json::Value jv; - jv[jss::TransactionType] = "EscrowCreate"; - jv[jss::Flags] = tfUniversal; - jv[jss::Account] = account.human(); - jv[jss::Destination] = to.human(); - jv[jss::Amount] = amount.getJson(0); - jv["FinishAfter"] = - expiry.time_since_epoch().count(); - return jv; - } - - static - Json::Value - lockup (jtx::Account const& account, jtx::Account const& to, - STAmount const& amount, NetClock::time_point const& expiry, - NetClock::time_point const& cancel) - { - Json::Value jv = lockup (account, to, amount, expiry); - jv["CancelAfter"] = cancel.time_since_epoch().count(); - return jv; - } - - static - Json::Value - lockup (jtx::Account const& account, jtx::Account const& to, - STAmount const& amount, Slice condition, - NetClock::time_point const& expiry) - { - using namespace jtx; - Json::Value jv; - jv[jss::TransactionType] = "EscrowCreate"; - jv[jss::Flags] = tfUniversal; - jv[jss::Account] = account.human(); - jv[jss::Destination] = to.human(); - jv[jss::Amount] = amount.getJson(0); - jv["FinishAfter"] = - expiry.time_since_epoch().count(); - jv["Condition"] = strHex(condition); return jv; } @@ -167,23 +194,6 @@ struct Escrow_test : public beast::unit_test::suite return jv; } - static - Json::Value - finish (jtx::Account const& account, - jtx::Account const& from, std::uint32_t seq, - Slice condition, Slice fulfillment) - { - Json::Value jv; - jv[jss::TransactionType] = "EscrowFinish"; - jv[jss::Flags] = tfUniversal; - jv[jss::Account] = account.human(); - jv["Owner"] = from.human(); - jv["OfferSequence"] = seq; - jv["Condition"] = strHex(condition); - jv["Fulfillment"] = strHex(fulfillment); - return jv; - } - static Json::Value cancel (jtx::Account const& account, @@ -209,23 +219,146 @@ struct Escrow_test : public beast::unit_test::suite { // Escrow not enabled Env env(*this, supported_amendments() - featureEscrow); env.fund(XRP(5000), "alice", "bob"); - env(lockup("alice", "bob", XRP(1000), env.now() + 1s), ter(temDISABLED)); - env(finish("bob", "alice", 1), ter(temDISABLED)); - env(cancel("bob", "alice", 1), ter(temDISABLED)); + env(escrow("alice", "bob", XRP(1000)), + finish_time(env.now() + 1s), ter(temDISABLED)); + env(finish("bob", "alice", 1), ter(temDISABLED)); + env(cancel("bob", "alice", 1), ter(temDISABLED)); } { // Escrow enabled Env env(*this); env.fund(XRP(5000), "alice", "bob"); - env(lockup("alice", "bob", XRP(1000), env.now() + 1s)); + env(escrow("alice", "bob", XRP(1000)), finish_time(env.now() + 1s)); env.close(); - auto const seq = env.seq("alice"); + auto const seq1 = env.seq("alice"); - env(condpay("alice", "bob", XRP(1000), - makeSlice (cb1), env.now() + 1s), fee(1500)); - env(finish("bob", "alice", seq, - makeSlice(cb1), makeSlice(fb1)), fee(1500)); + env(escrow("alice", "bob", XRP(1000)), condition (cb1), + finish_time(env.now() + 1s), fee(1500)); + env.close(); + env(finish("bob", "alice", seq1), + condition(cb1), fulfillment(fb1), fee(1500)); + + auto const seq2 = env.seq("alice"); + + env(escrow("alice", "bob", XRP(1000)), condition(cb2), + finish_time(env.now() + 1s), cancel_time(env.now() + 2s), fee(1500)); + env.close(); + env(cancel("bob", "alice", seq2), fee(1500)); + } + } + + void + testTiming() + { + using namespace jtx; + using namespace std::chrono; + + { + testcase("Timing: Finish Only"); + Env env(*this); + env.fund(XRP(5000), "alice", "bob"); + env.close(); + + // We create an escrow that can be finished in the future + auto const ts = env.now() + 97s; + + auto const seq = env.seq("alice"); + env(escrow("alice", "bob", XRP(1000)), finish_time(ts)); + + // Advance the ledger, verifying that the finish won't complete + // prematurely. + for ( ; env.now() < ts; env.close()) + env(finish("bob", "alice", seq), fee(1500), ter(tecNO_PERMISSION)); + + env(finish("bob", "alice", seq), fee(1500)); + } + + { + testcase("Timing: Cancel Only"); + Env env(*this); + env.fund(XRP(5000), "alice", "bob"); + env.close(); + + // We create an escrow that can be cancelled in the future + auto const ts = env.now() + 117s; + + auto const seq = env.seq("alice"); + env(escrow("alice", "bob", XRP(1000)), condition(cb1), cancel_time(ts)); + + // Advance the ledger, verifying that the cancel won't complete + // prematurely. + for ( ; env.now() < ts; env.close()) + env(cancel("bob", "alice", seq), fee(1500), ter(tecNO_PERMISSION)); + + // Verify that a finish won't work anymore. + env(finish("bob", "alice", seq), condition(cb1), + fulfillment(fb1), fee(1500), ter(tecNO_PERMISSION)); + + // Verify that the cancel will succeed + env(cancel("bob", "alice", seq), fee(1500)); + } + + { + testcase("Timing: Finish and Cancel -> Finish"); + Env env(*this); + env.fund(XRP(5000), "alice", "bob"); + env.close(); + + // We create an escrow that can be cancelled in the future + auto const fts = env.now() + 117s; + auto const cts = env.now() + 192s; + + auto const seq = env.seq("alice"); + env(escrow("alice", "bob", XRP(1000)), finish_time(fts), cancel_time(cts)); + + // Advance the ledger, verifying that the finish and cancel won't + // complete prematurely. + for ( ; env.now() < fts; env.close()) + { + env(finish("bob", "alice", seq), fee(1500), ter(tecNO_PERMISSION)); + env(cancel("bob", "alice", seq), fee(1500), ter(tecNO_PERMISSION)); + } + + // Verify that a cancel still won't work + env(cancel("bob", "alice", seq), fee(1500), ter(tecNO_PERMISSION)); + + // And verify that a finish will + env(finish("bob", "alice", seq), fee(1500)); + } + + { + testcase("Timing: Finish and Cancel -> Cancel"); + Env env(*this); + env.fund(XRP(5000), "alice", "bob"); + env.close(); + + // We create an escrow that can be cancelled in the future + auto const fts = env.now() + 109s; + auto const cts = env.now() + 184s; + + auto const seq = env.seq("alice"); + env(escrow("alice", "bob", XRP(1000)), finish_time(fts), cancel_time(cts)); + + // Advance the ledger, verifying that the finish and cancel won't + // complete prematurely. + for ( ; env.now() < fts; env.close()) + { + env(finish("bob", "alice", seq), fee(1500), ter(tecNO_PERMISSION)); + env(cancel("bob", "alice", seq), fee(1500), ter(tecNO_PERMISSION)); + } + + // Continue advancing, verifying that the cancel won't complete + // prematurely. At this point a finish would succeed. + for ( ; env.now() < cts; env.close()) + env(cancel("bob", "alice", seq), fee(1500), ter(tecNO_PERMISSION)); + + // Verify that finish will no longer work, since we are past the + // cancel activation time. + env(finish("bob", "alice", seq), fee(1500), ter(tecNO_PERMISSION)); + + // And verify that a cancel will succeed. + env(cancel("bob", "alice", seq), fee(1500)); } } @@ -240,13 +373,20 @@ struct Escrow_test : public beast::unit_test::suite Env env(*this); auto const alice = Account("alice"); - env.fund(XRP(5000), alice, "bob"); + auto const bob = Account("bob"); + + env.fund(XRP(5000), alice, bob); + + // Check to make sure that we correctly detect if tags are really + // required: + env(fset(bob, asfRequireDest)); + env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 1s), ter(tecDST_TAG_NEEDED)); - auto const seq = env.seq(alice); // set source and dest tags - env(condpay(alice, "bob", XRP(1000), - makeSlice (cb1), env.now() + 1s), - stag(1), dtag(2)); + auto const seq = env.seq(alice); + + env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 1s), stag(1), dtag(2)); + auto const sle = env.le(keylet::escrow(alice.id(), seq)); BEAST_EXPECT(sle); BEAST_EXPECT((*sle)[sfSourceTag] == 1); @@ -267,7 +407,7 @@ struct Escrow_test : public beast::unit_test::suite env.fund(XRP(5000), "bob", "george"); env(fset("george", asfDisallowXRP)); - env(lockup("bob", "george", XRP(10), env.now() + 1s), + env(escrow("bob", "george", XRP(10)), finish_time(env.now() + 1s), ter (tecNO_TARGET)); } { @@ -277,9 +417,64 @@ struct Escrow_test : public beast::unit_test::suite env.fund(XRP(5000), "bob", "george"); env(fset("george", asfDisallowXRP)); - env(lockup("bob", "george", XRP(10), env.now() + 1s)); + env(escrow("bob", "george", XRP(10)), finish_time(env.now() + 1s)); + } + } + + void + test1571() + { + using namespace jtx; + using namespace std::chrono; + + { + testcase ("Implied Finish Time (without fix1571)"); + + Env env(*this, supported_amendments() - fix1571); + env.fund(XRP(5000), "alice", "bob", "carol"); + env.close(); + + // Creating an escrow without a finish time and finishing it + // is allowed without fix1571: + auto const seq1 = env.seq("alice"); + env(escrow("alice", "bob", XRP(100)), cancel_time(env.now() + 1s), fee(1500)); + env.close(); + env(finish("carol", "alice", seq1), fee(1500)); + BEAST_EXPECT (env.balance ("bob") == XRP(5100)); + + env.close(); + + // Creating an escrow without a finish time and a condition is + // also allowed without fix1571: + auto const seq2 = env.seq("alice"); + env(escrow("alice", "bob", XRP(100)), cancel_time(env.now() + 1s), + condition(cb1), fee(1500)); + env.close(); + env(finish("carol", "alice", seq2), condition(cb1), + fulfillment(fb1), fee(1500)); + BEAST_EXPECT (env.balance ("bob") == XRP(5200)); } + { + testcase ("Implied Finish Time (with fix1571)"); + + Env env(*this); + env.fund(XRP(5000), "alice", "bob", "carol"); + env.close(); + + // Creating an escrow with only a cancel time is not allowed: + env (escrow("alice", "bob", XRP(100)), cancel_time(env.now() + 90s), + fee(1500), ter(temMALFORMED)); + + // Creating an escrow with only a cancel time and a condition is allowed: + auto const seq = env.seq("alice"); + env (escrow("alice", "bob", XRP(100)), cancel_time(env.now() + 90s), + condition(cb1), fee(1500)); + env.close(); + env (finish("carol", "alice", seq), condition(cb1), + fulfillment(fb1), fee(1500)); + BEAST_EXPECT (env.balance ("bob") == XRP(5100)); + } } void @@ -294,88 +489,94 @@ struct Escrow_test : public beast::unit_test::suite env.fund(XRP(5000), "alice", "bob"); env.close(); - // Expiration in the past - env(condpay("alice", "bob", XRP(1000), - makeSlice(cb1), env.now() - 1s), ter(tecNO_PERMISSION)); + // Finish time is in the past + env(escrow("alice", "bob", XRP(1000)), + finish_time(env.now() - 5s), ter(tecNO_PERMISSION)); + + // Cancel time is in the past + env(escrow("alice", "bob", XRP(1000)), condition(cb1), + cancel_time(env.now() - 5s), ter(tecNO_PERMISSION)); // no destination account - env(condpay("alice", "carol", XRP(1000), - makeSlice(cb1), env.now() + 1s), ter(tecNO_DST)); + env(escrow("alice", "carol", XRP(1000)), + finish_time(env.now() + 1s), ter(tecNO_DST)); env.fund(XRP(5000), "carol"); - env(condpay("alice", "carol", XRP(1000), - makeSlice(cb1), env.now() + 1s), stag(2)); - env(condpay("alice", "carol", XRP(1000), - makeSlice(cb1), env.now() + 1s), stag(3), dtag(4)); + + // Using non-XRP: + env (escrow("alice", "carol", Account("alice")["USD"](500)), + finish_time(env.now() + 1s), ter(temBAD_AMOUNT)); + + // Sending zero or no XRP: + env (escrow("alice", "carol", XRP(0)), + finish_time(env.now() + 1s), ter(temBAD_AMOUNT)); + env (escrow("alice", "carol", XRP(-1000)), + finish_time(env.now() + 1s), ter(temBAD_AMOUNT)); + + // Fail if neither CancelAfter nor FinishAfter are specified: + env (escrow("alice", "carol", XRP(1)), ter(temBAD_EXPIRATION)); + + // Fail if neither a FinishTime nor a condition are attached: + env (escrow("alice", "carol", XRP(1)), + cancel_time(env.now() + 1s), ter(temMALFORMED)); + + // Fail if FinishAfter has already passed: + env (escrow("alice", "carol", XRP(1)), + finish_time(env.now() - 1s), ter(tecNO_PERMISSION)); + + // If both CancelAfter and FinishAfter are set, then CancelAfter must + // be strictly later than FinishAfter. + env(escrow("alice", "carol", XRP(1)), condition(cb1), + finish_time(env.now() + 10s), cancel_time(env.now() + 10s), ter(temBAD_EXPIRATION)); + + env(escrow("alice", "carol", XRP(1)), condition(cb1), + finish_time(env.now() + 10s), cancel_time(env.now() + 5s), ter(temBAD_EXPIRATION)); + + // Carol now requires the use of a destination tag env(fset("carol", asfRequireDest)); // missing destination tag - env(condpay("alice", "carol", XRP(1000), - makeSlice(cb1), env.now() + 1s), ter(tecDST_TAG_NEEDED)); - env(condpay("alice", "carol", XRP(1000), - makeSlice(cb1), env.now() + 1s), dtag(1)); + env(escrow("alice", "carol", XRP(1)), + condition(cb1), cancel_time(env.now() + 1s), ter(tecDST_TAG_NEEDED)); - // Using non-XRP: - env (lockup("alice", "carol", Account("alice")["USD"](500), - env.now() + 1s), ter(temBAD_AMOUNT)); + // Success! + env(escrow("alice", "carol", XRP(1)), + condition(cb1), cancel_time(env.now() + 1s), dtag(1)); - // Sending zero or no XRP: - env (lockup("alice", "carol", XRP(0), - env.now() + 1s), ter(temBAD_AMOUNT)); - env (lockup("alice", "carol", XRP(-1000), - env.now() + 1s), ter(temBAD_AMOUNT)); + { // Fail if the sender wants to send more than he has: + auto const accountReserve = drops(env.current()->fees().reserve); + auto const accountIncrement = drops(env.current()->fees().increment); - // Fail if neither CancelAfter nor FinishAfter are specified: - { - auto j1 = lockup("alice", "carol", XRP(1), env.now() + 1s); - j1.removeMember ("FinishAfter"); - env (j1, ter(temBAD_EXPIRATION)); + env.fund (accountReserve + accountIncrement + XRP(50), "daniel"); + env(escrow("daniel", "bob", XRP(51)), + finish_time(env.now() + 1s), ter(tecUNFUNDED)); - auto j2 = condpay("alice", "carol", XRP(1), makeSlice(cb1), env.now() + 1s); - j2.removeMember ("CancelAfter"); - env (j2, ter(temBAD_EXPIRATION)); + env.fund (accountReserve + accountIncrement + XRP(50), "evan"); + env(escrow("evan", "bob", XRP(50)), + finish_time(env.now() + 1s), ter(tecUNFUNDED)); + + env.fund (accountReserve, "frank"); + env(escrow("frank", "bob", XRP(1)), + finish_time(env.now() + 1s), ter(tecINSUFFICIENT_RESERVE)); } - // Fail if FinishAfter has already passed: - env (lockup("alice", "carol", XRP(1), env.now() - 1s), ter (tecNO_PERMISSION)); - - // Both CancelAfter and FinishAfter - env(condpay("alice", "carol", XRP(1), makeSlice(cb1), - env.now() + 10s, env.now() + 10s), ter (temBAD_EXPIRATION)); - env(condpay("alice", "carol", XRP(1), makeSlice(cb1), - env.now() + 10s, env.now() + 15s), ter (temBAD_EXPIRATION)); - - // Fail if the sender wants to send more than he has: - auto const accountReserve = - drops(env.current()->fees().reserve); - auto const accountIncrement = - drops(env.current()->fees().increment); - - env.fund (accountReserve + accountIncrement + XRP(50), "daniel"); - env(lockup("daniel", "bob", XRP(51), env.now() + 1s), ter (tecUNFUNDED)); - - env.fund (accountReserve + accountIncrement + XRP(50), "evan"); - env(lockup("evan", "bob", XRP(50), env.now() + 1s), ter (tecUNFUNDED)); - - env.fund (accountReserve, "frank"); - env(lockup("frank", "bob", XRP(1), env.now() + 1s), ter (tecINSUFFICIENT_RESERVE)); - { // Specify incorrect sequence number env.fund (XRP(5000), "hannah"); auto const seq = env.seq("hannah"); - env(lockup("hannah", "hannah", XRP(10), env.now() + 1s)); - env(finish ("hannah", "hannah", seq + 7), ter (tecNO_TARGET)); + env(escrow("hannah", "hannah", XRP(10)), + finish_time(env.now() + 1s), fee(1500)); + env.close(); + env(finish ("hannah", "hannah", seq + 7), fee(1500), ter(tecNO_TARGET)); } { // Try to specify a condition for a non-conditional payment env.fund (XRP(5000), "ivan"); auto const seq = env.seq("ivan"); - auto j = lockup("ivan", "ivan", XRP(10), env.now() + 1s); - j["CancelAfter"] = j.removeMember ("FinishAfter"); - env (j); - env(finish("ivan", "ivan", seq, - makeSlice(cb1), makeSlice(fb1)), fee(1500), ter (tecCRYPTOCONDITION_ERROR)); + env(escrow("ivan", "ivan", XRP(10)), finish_time(env.now() + 1s)); + env.close(); + env(finish("ivan", "ivan", seq), condition(cb1), fulfillment(fb1), + fee(1500), ter(tecCRYPTOCONDITION_ERROR)); } } @@ -388,18 +589,25 @@ struct Escrow_test : public beast::unit_test::suite using namespace std::chrono; { // Unconditional + Env env(*this); env.fund(XRP(5000), "alice", "bob"); auto const seq = env.seq("alice"); - env(lockup("alice", "alice", XRP(1000), env.now() + 1s)); + env(escrow("alice", "alice", XRP(1000)), finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(10))); + // Not enough time has elapsed for a finish and cancelling isn't + // possible. env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); env(finish("bob", "alice", seq), ter(tecNO_PERMISSION)); env.close(); + // Cancel continues to not be possible env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + + // Finish should succeed. Verify funds. env(finish("bob", "alice", seq)); + env.require(balance("alice", XRP(5000) - drops(10))); } { // Unconditionally pay from alice to bob. jack (neither source nor @@ -408,16 +616,22 @@ struct Escrow_test : public beast::unit_test::suite Env env(*this); env.fund(XRP(5000), "alice", "bob", "jack"); auto const seq = env.seq("alice"); - env(lockup("alice", "bob", XRP(1000), env.now() + 1s)); + env(escrow("alice", "bob", XRP(1000)), finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(10))); + // Not enough time has elapsed for a finish and cancelling isn't + // possible. env(cancel("jack", "alice", seq), ter(tecNO_PERMISSION)); env(finish("jack", "alice", seq), ter(tecNO_PERMISSION)); env.close(); + // Cancel continues to not be possible env(cancel("jack", "alice", seq), ter(tecNO_PERMISSION)); + + // Finish should succeed. Verify funds. env(finish("jack", "alice", seq)); env.close(); + env.require(balance("alice", XRP(4000) - drops(10))); env.require(balance("bob", XRP(6000))); env.require(balance("jack", XRP(5000) - drops(40))); @@ -431,24 +645,32 @@ struct Escrow_test : public beast::unit_test::suite env.close(); auto const seq = env.seq("alice"); - env(lockup("alice", "bob", XRP(1000), env.now() + 1s)); + env(escrow("alice", "bob", XRP(1000)), finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(10))); + // Not enough time has elapsed for a finish and cancelling isn't + // possible. env(cancel("jack", "alice", seq), ter(tecNO_PERMISSION)); + env(cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); env(finish("jack", "alice", seq), ter(tecNO_PERMISSION)); env(finish("alice", "alice", seq), ter(tecNO_PERMISSION)); env(finish("bob", "alice", seq), ter(tecNO_PERMISSION)); env.close(); + // Cancel continues to not be possible. Finish will only succeed for + // Bob, because of PaymentAuth. env(cancel("jack", "alice", seq), ter(tecNO_PERMISSION)); + env(cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); env(finish("jack", "alice", seq), ter(tecNO_PERMISSION)); env(finish("alice", "alice", seq), ter(tecNO_PERMISSION)); - env.close(); env(finish("bob", "alice", seq)); env.close(); + auto const baseFee = env.current()->fees().base; - env.require(balance("alice", XRP(4000) - (baseFee * 3))); - env.require(balance("bob", XRP(6000) - (baseFee * 3))); + env.require(balance("alice", XRP(4000) - (baseFee * 5))); + env.require(balance("bob", XRP(6000) - (baseFee * 5))); env.require(balance("jack", XRP(5000) - (baseFee * 4))); } { @@ -456,22 +678,31 @@ struct Escrow_test : public beast::unit_test::suite Env env(*this); env.fund(XRP(5000), "alice", "bob"); auto const seq = env.seq("alice"); - env(lockup("alice", "alice", XRP(1000), makeSlice(cb2), env.now() + 1s)); + env(escrow("alice", "alice", XRP(1000)), condition(cb2), finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(10))); - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq, - makeSlice(cb2), makeSlice(fb2)), fee(1500), ter(tecNO_PERMISSION)); + // Not enough time has elapsed for a finish and cancelling isn't + // possible. + env(cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(finish("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(finish("alice", "alice", seq), + condition(cb2), fulfillment(fb2), fee(1500), ter(tecNO_PERMISSION)); + env(finish("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(finish("bob", "alice", seq), + condition(cb2), fulfillment(fb2), fee(1500), ter(tecNO_PERMISSION)); env.close(); - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("alice", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); + // Cancel continues to not be possible. Finish is possible but + // requires the fulfillment associated with the escrow. + env(cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(finish("bob", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); + env(finish("alice", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); env.close(); - env(finish("bob", "alice", seq, - makeSlice(cb2), makeSlice(fb2)), fee(1500)); + env(finish("bob", "alice", seq), + condition(cb2), fulfillment(fb2), fee(1500)); } { // Self-escrowed conditional with PaymentAuth @@ -479,92 +710,109 @@ struct Escrow_test : public beast::unit_test::suite env.fund(XRP(5000), "alice", "bob"); auto const seq = env.seq("alice"); - env(lockup("alice", "alice", XRP(1000), makeSlice(cb2), env.now() + 1s)); + env(escrow("alice", "alice", XRP(1000)), condition(cb3), finish_time(env.now() + 5s)); env.require(balance("alice", XRP(4000) - drops(10))); + // Not enough time has elapsed for a finish and cancelling isn't + // possible. + env(cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq), ter(tecNO_PERMISSION)); - env(finish("bob", "alice", seq, - makeSlice(cb2), makeSlice(fb2)), fee(1500), ter(tecNO_PERMISSION)); + env(finish("alice", "alice", seq), ter(tecNO_PERMISSION)); + env(finish("alice", "alice", seq), + condition(cb3), fulfillment(fb3), fee(1500), ter(tecNO_PERMISSION)); + env(finish("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(finish("bob", "alice", seq), + condition(cb3), fulfillment(fb3), fee(1500), ter(tecNO_PERMISSION)); env.close(); + // Cancel continues to not be possible. Finish is now possible but + // requires the associated cryptocondition. + env(cancel("alice", "alice", seq), ter(tecNO_PERMISSION)); env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); env(finish("bob", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); env(finish("alice", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); + + // Enable deposit authorization. After this, only Alice can finish + // the escrow. env(fset ("alice", asfDepositAuth)); env.close(); - env(finish("bob", "alice", seq, - makeSlice(cb2), makeSlice(fb2)), fee(1500), ter(tecNO_PERMISSION)); - env(finish("alice", "alice", seq, - makeSlice(cb2), makeSlice(fb2)), fee(1500)); + env(finish("bob", "alice", seq), condition(cb2), + fulfillment(fb2), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); + env(finish("alice", "alice", seq), condition(cb2), + fulfillment(fb2), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); + env(finish("alice", "alice", seq), condition(cb3), + fulfillment(fb3), fee(1500)); } } void testEscrowConditions() { - testcase ("Escrow Conditions"); + testcase ("Escrow with CryptoConditions"); using namespace jtx; using namespace std::chrono; - using S = seconds; { // Test cryptoconditions Env env(*this); - auto T = [&env](NetClock::duration const& d) - { return env.now() + d; }; env.fund(XRP(5000), "alice", "bob", "carol"); auto const seq = env.seq("alice"); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); - env(condpay("alice", "carol", XRP(1000), makeSlice(cb1), T(S{1}))); + env(escrow("alice", "carol", XRP(1000)), condition(cb1), + cancel_time(env.now() + 1s)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); env.require(balance("alice", XRP(4000) - drops(10))); env.require(balance("carol", XRP(5000))); - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); // Attempt to finish without a fulfillment - env(finish("bob", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); + env(finish("bob", "alice", seq), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); // Attempt to finish with a condition instead of a fulfillment - env(finish("bob", "alice", seq, makeSlice(cb1), makeSlice(cb1)), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); + env(finish("bob", "alice", seq), condition(cb1), + fulfillment(cb1), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); - env(finish("bob", "alice", seq, makeSlice(cb1), makeSlice(cb2)), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); + env(finish("bob", "alice", seq), condition(cb1), + fulfillment(cb2), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); - env(finish("bob", "alice", seq, makeSlice(cb1), makeSlice(cb3)), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); + env(finish("bob", "alice", seq), condition(cb1), + fulfillment(cb3), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); // Attempt to finish with an incorrect condition and various // combinations of correct and incorrect fulfillments. - env(finish("bob", "alice", seq, makeSlice(cb2), makeSlice(fb1)), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); + env(finish("bob", "alice", seq), condition(cb2), + fulfillment(fb1), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); - env(finish("bob", "alice", seq, makeSlice(cb2), makeSlice(fb2)), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); + env(finish("bob", "alice", seq), condition(cb2), + fulfillment(fb2), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); - env(finish("bob", "alice", seq, makeSlice(cb2), makeSlice(fb3)), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); + env(finish("bob", "alice", seq), condition(cb2), + fulfillment(fb3), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); // Attempt to finish with the correct condition & fulfillment - env(finish("bob", "alice", seq, makeSlice(cb1), makeSlice(fb1)), fee(1500)); + env(finish("bob", "alice", seq), condition(cb1), + fulfillment(fb1), fee(1500)); + // SLE removed on finish BEAST_EXPECT(! env.le(keylet::escrow(Account("alice").id(), seq))); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); env.require(balance("carol", XRP(6000))); - env(cancel("bob", "alice", seq), ter(tecNO_TARGET)); + env(cancel("bob", "alice", seq), ter(tecNO_TARGET)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); - env(cancel("bob", "carol", 1), ter(tecNO_TARGET)); - env.close(); + env(cancel("bob", "carol", 1), ter(tecNO_TARGET)); } - { // Test cancel when condition is present Env env(*this); - auto T = [&env](NetClock::duration const& d) - { return env.now() + d; }; env.fund(XRP(5000), "alice", "bob", "carol"); auto const seq = env.seq("alice"); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); - env(condpay("alice", "carol", XRP(1000), makeSlice(cb2), T(S{1}))); + env(escrow("alice", "carol", XRP(1000)), condition(cb2), + cancel_time(env.now() + 1s)); env.close(); env.require(balance("alice", XRP(4000) - drops(10))); // balance restored on cancel @@ -573,31 +821,26 @@ struct Escrow_test : public beast::unit_test::suite // SLE removed on cancel BEAST_EXPECT(! env.le(keylet::escrow(Account("alice").id(), seq))); } - { Env env(*this); - auto T = [&env](NetClock::duration const& d) - { return env.now() + d; }; env.fund(XRP(5000), "alice", "bob", "carol"); env.close(); auto const seq = env.seq("alice"); - env(condpay("alice", "carol", XRP(1000), makeSlice(cb3), T(S{1}))); + env(escrow("alice", "carol", XRP(1000)), condition(cb3), + cancel_time(env.now() + 1s)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); // cancel fails before expiration - env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); env.close(); // finish fails after expiration - env(finish("bob", "alice", seq, makeSlice(cb3), makeSlice(fb3)), - fee(1500), ter(tecNO_PERMISSION)); + env(finish("bob", "alice", seq), condition(cb3), + fulfillment(fb3), fee(1500), ter(tecNO_PERMISSION)); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); env.require(balance("carol", XRP(5000))); } - { // Test long & short conditions during creation Env env(*this); - auto T = [&env](NetClock::duration const& d) - { return env.now() + d; }; env.fund(XRP(5000), "alice", "bob", "carol"); std::vector v; @@ -607,37 +850,36 @@ struct Escrow_test : public beast::unit_test::suite auto const p = v.data(); auto const s = v.size(); + auto const ts = env.now() + 1s; + // All these are expected to fail, because the // condition we pass in is malformed in some way - env(condpay("alice", "carol", XRP(1000), - Slice{p, s}, T(S{1})), ter(temMALFORMED)); - env(condpay("alice", "carol", XRP(1000), - Slice{p, s - 1}, T(S{1})), ter(temMALFORMED)); - env(condpay("alice", "carol", XRP(1000), - Slice{p, s - 2}, T(S{1})), ter(temMALFORMED)); - env(condpay("alice", "carol", XRP(1000), - Slice{p + 1, s - 1}, T(S{1})), ter(temMALFORMED)); - env(condpay("alice", "carol", XRP(1000), - Slice{p + 1, s - 3}, T(S{1})), ter(temMALFORMED)); - env(condpay("alice", "carol", XRP(1000), - Slice{p + 2, s - 2}, T(S{1})), ter(temMALFORMED)); - env(condpay("alice", "carol", XRP(1000), - Slice{p + 2, s - 3}, T(S{1})), ter(temMALFORMED)); + env(escrow("alice", "carol", XRP(1000)), condition(Slice{p, s}), + cancel_time(ts), ter(temMALFORMED)); + env(escrow("alice", "carol", XRP(1000)), condition(Slice{p, s - 1}), + cancel_time(ts), ter(temMALFORMED)); + env(escrow("alice", "carol", XRP(1000)), condition(Slice{p, s - 2}), + cancel_time(ts), ter(temMALFORMED)); + env(escrow("alice", "carol", XRP(1000)), condition(Slice{p + 1, s - 1}), + cancel_time(ts), ter(temMALFORMED)); + env(escrow("alice", "carol", XRP(1000)), condition(Slice{p + 1, s - 3}), + cancel_time(ts), ter(temMALFORMED)); + env(escrow("alice", "carol", XRP(1000)), condition(Slice{p + 2, s - 2}), + cancel_time(ts), ter(temMALFORMED)); + env(escrow("alice", "carol", XRP(1000)), condition(Slice{p + 2, s - 3}), + cancel_time(ts), ter(temMALFORMED)); auto const seq = env.seq("alice"); - env(condpay("alice", "carol", XRP(1000), - Slice{p + 1, s - 2}, T(S{1})), fee(100)); - env(finish("bob", "alice", seq, - makeSlice(cb1), makeSlice(fb1)), fee(1500)); + env(escrow("alice", "carol", XRP(1000)), condition(Slice{p + 1, s - 2}), + cancel_time(ts), fee(100)); + env(finish("bob", "alice", seq), + condition(cb1), fulfillment(fb1), fee(1500)); env.require(balance("alice", XRP(4000) - drops(100))); env.require(balance("bob", XRP(5000) - drops(1500))); env.require(balance("carol", XRP(6000))); } - { // Test long and short conditions & fulfillments during finish Env env(*this); - auto T = [&env](NetClock::duration const& d) - { return env.now() + d; }; env.fund(XRP(5000), "alice", "bob", "carol"); std::vector cv; @@ -654,116 +896,104 @@ struct Escrow_test : public beast::unit_test::suite auto const fp = fv.data(); auto const fs = fv.size(); + auto const ts = env.now() + 1s; + // All these are expected to fail, because the // condition we pass in is malformed in some way - env(condpay("alice", "carol", XRP(1000), - Slice{cp, cs}, T(S{1})), ter(temMALFORMED)); - env(condpay("alice", "carol", XRP(1000), - Slice{cp, cs - 1}, T(S{1})), ter(temMALFORMED)); - env(condpay("alice", "carol", XRP(1000), - Slice{cp, cs - 2}, T(S{1})), ter(temMALFORMED)); - env(condpay("alice", "carol", XRP(1000), - Slice{cp + 1, cs - 1}, T(S{1})), ter(temMALFORMED)); - env(condpay("alice", "carol", XRP(1000), - Slice{cp + 1, cs - 3}, T(S{1})), ter(temMALFORMED)); - env(condpay("alice", "carol", XRP(1000), - Slice{cp + 2, cs - 2}, T(S{1})), ter(temMALFORMED)); - env(condpay("alice", "carol", XRP(1000), - Slice{cp + 2, cs - 3}, T(S{1})), ter(temMALFORMED)); + env(escrow("alice", "carol", XRP(1000)), condition(Slice{cp, cs}), + cancel_time(ts), ter(temMALFORMED)); + env(escrow("alice", "carol", XRP(1000)), condition(Slice{cp, cs - 1}), + cancel_time(ts), ter(temMALFORMED)); + env(escrow("alice", "carol", XRP(1000)), condition(Slice{cp, cs - 2}), + cancel_time(ts), ter(temMALFORMED)); + env(escrow("alice", "carol", XRP(1000)), condition(Slice{cp + 1, cs - 1}), + cancel_time(ts), ter(temMALFORMED)); + env(escrow("alice", "carol", XRP(1000)), condition(Slice{cp + 1, cs - 3}), + cancel_time(ts), ter(temMALFORMED)); + env(escrow("alice", "carol", XRP(1000)), condition(Slice{cp + 2, cs - 2}), + cancel_time(ts), ter(temMALFORMED)); + env(escrow("alice", "carol", XRP(1000)), condition(Slice{cp + 2, cs - 3}), + cancel_time(ts), ter(temMALFORMED)); auto const seq = env.seq("alice"); - env(condpay("alice", "carol", XRP(1000), - Slice{cp + 1, cs - 2}, T(S{1})), fee(100)); + env(escrow("alice", "carol", XRP(1000)), condition(Slice{cp + 1, cs - 2}), + cancel_time(ts), fee(100)); // Now, try to fulfill using the same sequence of // malformed conditions. - env(finish("bob", "alice", seq, Slice{cp, cs}, Slice{fp, fs}), + env(finish("bob", "alice", seq), condition(Slice{cp, cs}), fulfillment(Slice{fp, fs}), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq, Slice{cp, cs - 1}, Slice{fp, fs}), + env(finish("bob", "alice", seq), condition(Slice{cp, cs - 1}), fulfillment(Slice{fp, fs}), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq, Slice{cp, cs - 2}, Slice{fp, fs}), + env(finish("bob", "alice", seq), condition(Slice{cp, cs - 2}), fulfillment(Slice{fp, fs}), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq, Slice{cp + 1, cs - 1}, Slice{fp, fs}), + env(finish("bob", "alice", seq), condition(Slice{cp + 1, cs - 1}), fulfillment(Slice{fp, fs}), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq, Slice{cp + 1, cs - 3}, Slice{fp, fs}), + env(finish("bob", "alice", seq), condition(Slice{cp + 1, cs - 3}), fulfillment(Slice{fp, fs}), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq, Slice{cp + 2, cs - 2}, Slice{fp, fs}), + env(finish("bob", "alice", seq), condition(Slice{cp + 2, cs - 2}), fulfillment(Slice{fp, fs}), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq, Slice{cp + 2, cs - 3}, Slice{fp, fs}), + env(finish("bob", "alice", seq), condition(Slice{cp + 2, cs - 3}), fulfillment(Slice{fp, fs}), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); - // Now, using the correct condition, try malformed - // fulfillments: - env(finish("bob", "alice", seq, Slice{cp + 1, cs - 2}, Slice{fp, fs}), - fee(1500), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq, Slice{cp + 1, cs - 2}, Slice{fp, fs - 1}), - fee(1500), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq, Slice{cp + 1, cs - 2}, Slice{fp, fs - 2}), - fee(1500), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq, Slice{cp + 1, cs - 2}, Slice{fp + 1, fs - 1}), - fee(1500), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq, Slice{cp + 1, cs - 2}, Slice{fp + 1, fs - 3}), - fee(1500), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq, Slice{cp + 1, cs - 2}, Slice{fp + 1, fs - 3}), - fee(1500), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq, Slice{cp + 1, cs - 2}, Slice{fp + 2, fs - 2}), - fee(1500), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq, Slice{cp + 1, cs - 2}, Slice{fp + 2, fs - 3}), - fee(1500), ter(tecCRYPTOCONDITION_ERROR)); + // Now, using the correct condition, try malformed fulfillments: + env(finish("bob", "alice", seq), condition(Slice{cp + 1, cs - 2}), + fulfillment(Slice{fp, fs}), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); + env(finish("bob", "alice", seq), condition(Slice{cp + 1, cs - 2}), + fulfillment(Slice{fp, fs - 1}), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); + env(finish("bob", "alice", seq), condition(Slice{cp + 1, cs - 2}), + fulfillment(Slice{fp, fs - 2}), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); + env(finish("bob", "alice", seq), condition(Slice{cp + 1, cs - 2}), + fulfillment(Slice{fp + 1, fs - 1}), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); + env(finish("bob", "alice", seq), condition(Slice{cp + 1, cs - 2}), + fulfillment(Slice{fp + 1, fs - 3}), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); + env(finish("bob", "alice", seq), condition(Slice{cp + 1, cs - 2}), + fulfillment(Slice{fp + 1, fs - 3}), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); + env(finish("bob", "alice", seq), condition(Slice{cp + 1, cs - 2}), + fulfillment(Slice{fp + 2, fs - 2}), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); + env(finish("bob", "alice", seq), condition(Slice{cp + 1, cs - 2}), + fulfillment(Slice{fp + 2, fs - 3}), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); // Now try for the right one - env(finish("bob", "alice", seq, - makeSlice(cb2), makeSlice(fb2)), fee(1500)); + env(finish("bob", "alice", seq), condition(cb2), + fulfillment(fb2), fee(1500)); env.require(balance("alice", XRP(4000) - drops(100))); env.require(balance("carol", XRP(6000))); } - { // Test empty condition during creation and // empty condition & fulfillment during finish Env env(*this); - auto T = [&env](NetClock::duration const& d) - { return env.now() + d; }; env.fund(XRP(5000), "alice", "bob", "carol"); - env(condpay("alice", "carol", XRP(1000), {}, T(S{1})), ter(temMALFORMED)); + env(escrow("alice", "carol", XRP(1000)), condition(Slice{}), + cancel_time(env.now() + 1s), ter(temMALFORMED)); auto const seq = env.seq("alice"); - env(condpay("alice", "carol", XRP(1000), makeSlice(cb3), T(S{1}))); + env(escrow("alice", "carol", XRP(1000)), + condition(cb3), cancel_time(env.now() + 1s)); - env(finish("bob", "alice", seq, {}, {}), - fee(1500), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq, makeSlice(cb3), {}), - fee(1500), ter(tecCRYPTOCONDITION_ERROR)); - env(finish("bob", "alice", seq, {}, makeSlice(fb3)), - fee(1500), ter(tecCRYPTOCONDITION_ERROR)); + env(finish("bob", "alice", seq), condition(Slice{}), + fulfillment(Slice{}), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); + env(finish("bob", "alice", seq), condition(cb3), + fulfillment(Slice{}), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); + env(finish("bob", "alice", seq), condition(Slice{}), + fulfillment(fb3), fee(1500), ter(tecCRYPTOCONDITION_ERROR)); - auto correctFinish = finish("bob", "alice", seq, - makeSlice(cb3), makeSlice(fb3)); + // Assemble finish that is missing the Condition or the Fulfillment + // since either both must be present, or neither can: + env (finish("bob", "alice", seq), condition(cb3), ter(temMALFORMED)); + env (finish("bob", "alice", seq), fulfillment(fb3), ter(temMALFORMED)); - // Manually assemble finish that is missing the - // Condition or the Fulfillment (either both must - // be present, or neither can): - { - auto finishNoCondition = correctFinish; - finishNoCondition.removeMember ("Condition"); - env (finishNoCondition, ter(temMALFORMED)); - - auto finishNoFulfillment = correctFinish; - finishNoFulfillment.removeMember ("Fulfillment"); - env (finishNoFulfillment, ter(temMALFORMED)); - } - - env(correctFinish, fee(1500)); + // Now finish it. + env(finish("bob", "alice", seq), condition(cb3), + fulfillment(fb3), fee(1500)); env.require(balance ("carol", XRP(6000))); env.require(balance ("alice", XRP(4000) - drops(10))); } - { // Test a condition other than PreimageSha256, which // would require a separate amendment Env env(*this); - auto T = [&env](NetClock::duration const& d) - { return env.now() + d; }; - env.fund(XRP(5000), "alice", "bob", "carol"); + env.fund(XRP(5000), "alice", "bob"); std::array cb = {{ @@ -776,8 +1006,8 @@ struct Escrow_test : public beast::unit_test::suite // FIXME: this transaction should, eventually, return temDISABLED // instead of temMALFORMED. - env(condpay("alice", "carol", XRP(1000), makeSlice(cb), T(S{1})), - ter(temMALFORMED)); + env(escrow("alice", "bob", XRP(1000)), condition(cb), + cancel_time(env.now() + 1s), ter(temMALFORMED)); } } @@ -795,9 +1025,9 @@ struct Escrow_test : public beast::unit_test::suite testcase ("Metadata & Ownership (without fix1523)"); Env env(*this, supported_amendments() - fix1523); env.fund(XRP(5000), alice, bruce, carol); - auto const seq = env.seq(alice); - env(lockup(alice, carol, XRP(1000), env.now() + 1s)); + auto const seq = env.seq(alice); + env(escrow(alice, carol, XRP(1000)), finish_time(env.now() + 1s)); BEAST_EXPECT((*env.meta())[sfTransactionResult] == tesSUCCESS); @@ -811,7 +1041,6 @@ struct Escrow_test : public beast::unit_test::suite ripple::Dir cod (*env.current(), keylet::ownerDir(carol.id())); BEAST_EXPECT(cod.begin() == cod.end()); } - { testcase ("Metadata (with fix1523, to self)"); @@ -820,7 +1049,8 @@ struct Escrow_test : public beast::unit_test::suite auto const aseq = env.seq(alice); auto const bseq = env.seq(bruce); - env(lockup(alice, alice, XRP(1000), env.now() + 1s, env.now() + 500s)); + env(escrow(alice, alice, XRP(1000)), + finish_time(env.now() + 1s), cancel_time(env.now() + 500s)); BEAST_EXPECT((*env.meta())[sfTransactionResult] == tesSUCCESS); env.close(5s); auto const aa = env.le(keylet::escrow(alice.id(), aseq)); @@ -832,7 +1062,8 @@ struct Escrow_test : public beast::unit_test::suite BEAST_EXPECT(std::find(aod.begin(), aod.end(), aa) != aod.end()); } - env(lockup(bruce, bruce, XRP(1000), env.now() + 1s, env.now() + 2s)); + env(escrow(bruce, bruce, XRP(1000)), + finish_time(env.now() + 1s), cancel_time(env.now() + 2s)); BEAST_EXPECT((*env.meta())[sfTransactionResult] == tesSUCCESS); env.close(5s); auto const bb = env.le(keylet::escrow(bruce.id(), bseq)); @@ -870,7 +1101,6 @@ struct Escrow_test : public beast::unit_test::suite BEAST_EXPECT(std::find(bod.begin(), bod.end(), bb) == bod.end()); } } - { testcase ("Metadata (with fix1523, to other)"); @@ -879,10 +1109,11 @@ struct Escrow_test : public beast::unit_test::suite auto const aseq = env.seq(alice); auto const bseq = env.seq(bruce); - env(lockup(alice, bruce, XRP(1000), env.now() + 1s)); + env(escrow(alice, bruce, XRP(1000)), finish_time(env.now() + 1s)); BEAST_EXPECT((*env.meta())[sfTransactionResult] == tesSUCCESS); env.close(5s); - env(lockup(bruce, carol, XRP(1000), env.now() + 1s, env.now() + 2s)); + env(escrow(bruce, carol, XRP(1000)), + finish_time(env.now() + 1s), cancel_time(env.now() + 2s)); BEAST_EXPECT((*env.meta())[sfTransactionResult] == tesSUCCESS); env.close(5s); @@ -960,10 +1191,8 @@ struct Escrow_test : public beast::unit_test::suite env.memoize("carol"); { - auto const jtx = env.jt( - condpay("alice", "carol", XRP(1000), - makeSlice(cb1), env.now() + 1s), - seq(1), fee(10)); + auto const jtx = env.jt(escrow("alice", "carol", XRP(1000)), + finish_time(env.now() + 1s), seq(1), fee(10)); auto const pf = preflight(env.app(), env.current()->rules(), *jtx.stx, tapNONE, env.journal); BEAST_EXPECT(pf.ter == tesSUCCESS); @@ -974,8 +1203,7 @@ struct Escrow_test : public beast::unit_test::suite } { - auto const jtx = env.jt(cancel("bob", "alice", 3), - seq(1), fee(10)); + auto const jtx = env.jt(cancel("bob", "alice", 3), seq(1), fee(10)); auto const pf = preflight(env.app(), env.current()->rules(), *jtx.stx, tapNONE, env.journal); BEAST_EXPECT(pf.ter == tesSUCCESS); @@ -986,10 +1214,7 @@ struct Escrow_test : public beast::unit_test::suite } { - auto const jtx = env.jt( - finish("bob", "alice", 3, - makeSlice(cb1), makeSlice(fb1)), - seq(1), fee(10)); + auto const jtx = env.jt(finish("bob", "alice", 3), seq(1), fee(10)); auto const pf = preflight(env.app(), env.current()->rules(), *jtx.stx, tapNONE, env.journal); BEAST_EXPECT(pf.ter == tesSUCCESS); @@ -1003,8 +1228,10 @@ struct Escrow_test : public beast::unit_test::suite void run() override { testEnablement(); + testTiming(); testTags(); testDisallowXRP(); + test1571(); testFails(); testLockup(); testEscrowConditions();