From 3f0eacf5e70c43715a1e726d7a228d0ee307ee4e Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Sun, 26 Jul 2015 14:25:59 -0700 Subject: [PATCH] Add SuspendedPayment feature (RIPD-992): The code is enabled in jtx::Env, and enabled in production ledgers only if the SuspendedPayment amendment is voted into a ledger. --- Builds/VisualStudio2013/RippleD.vcxproj | 18 +- .../VisualStudio2013/RippleD.vcxproj.filters | 17 +- src/ripple/app/tests/SusPay_test.cpp | 322 ++++++++++++++ src/ripple/app/tx/impl/SusPay.cpp | 410 ++++++++++++++++++ src/ripple/app/tx/impl/SusPay.h | 87 ++++ src/ripple/app/tx/impl/apply.cpp | 7 + src/ripple/protocol/Feature.h | 2 + src/ripple/protocol/Indexes.h | 5 + src/ripple/protocol/LedgerFormats.h | 6 +- src/ripple/protocol/SField.h | 6 +- src/ripple/protocol/TxFormats.h | 6 +- src/ripple/protocol/impl/Feature.cpp | 2 + src/ripple/protocol/impl/Indexes.cpp | 11 + src/ripple/protocol/impl/LedgerFormats.cpp | 13 + src/ripple/protocol/impl/SField.cpp | 6 +- src/ripple/protocol/impl/TxFormats.cpp | 19 + .../handlers/{Feature.cpp => Feature1.cpp} | 0 src/ripple/test/jtx.h | 2 + src/ripple/test/jtx/impl/tag.cpp | 42 ++ src/ripple/test/jtx/pay.h | 3 +- src/ripple/test/jtx/tag.h | 69 +++ src/ripple/unity/app_tests.cpp | 1 + src/ripple/unity/app_tx.cpp | 1 + src/ripple/unity/rpcx.cpp | 2 +- src/ripple/unity/test.cpp | 1 + 25 files changed, 1043 insertions(+), 15 deletions(-) create mode 100644 src/ripple/app/tests/SusPay_test.cpp create mode 100644 src/ripple/app/tx/impl/SusPay.cpp create mode 100644 src/ripple/app/tx/impl/SusPay.h rename src/ripple/rpc/handlers/{Feature.cpp => Feature1.cpp} (100%) create mode 100644 src/ripple/test/jtx/impl/tag.cpp create mode 100644 src/ripple/test/jtx/tag.h diff --git a/Builds/VisualStudio2013/RippleD.vcxproj b/Builds/VisualStudio2013/RippleD.vcxproj index da4cdac1c..cb3e963e4 100644 --- a/Builds/VisualStudio2013/RippleD.vcxproj +++ b/Builds/VisualStudio2013/RippleD.vcxproj @@ -1709,6 +1709,10 @@ True True + + True + True + @@ -1809,6 +1813,12 @@ + + True + True + + + True True @@ -3168,7 +3178,7 @@ True True - + True True @@ -3696,6 +3706,10 @@ True True + + True + True + True True @@ -3746,6 +3760,8 @@ + + diff --git a/Builds/VisualStudio2013/RippleD.vcxproj.filters b/Builds/VisualStudio2013/RippleD.vcxproj.filters index 997ec9222..7d252272f 100644 --- a/Builds/VisualStudio2013/RippleD.vcxproj.filters +++ b/Builds/VisualStudio2013/RippleD.vcxproj.filters @@ -2457,6 +2457,9 @@ ripple\app\tests + + ripple\app\tests + ripple\app\tx @@ -2556,6 +2559,12 @@ ripple\app\tx\impl + + ripple\app\tx\impl + + + ripple\app\tx\impl + ripple\app\tx\impl @@ -3870,7 +3879,7 @@ ripple\rpc\handlers - + ripple\rpc\handlers @@ -4362,6 +4371,9 @@ ripple\test\jtx\impl + + ripple\test\jtx\impl + ripple\test\jtx\impl @@ -4425,6 +4437,9 @@ ripple\test\jtx + + ripple\test\jtx + ripple\test\jtx diff --git a/src/ripple/app/tests/SusPay_test.cpp b/src/ripple/app/tests/SusPay_test.cpp new file mode 100644 index 000000000..d61354f1e --- /dev/null +++ b/src/ripple/app/tests/SusPay_test.cpp @@ -0,0 +1,322 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { + +struct SusPay_test : public beast::unit_test::suite +{ + template + static + uint256 + digest (Args&&... args) + { + sha256_hasher h; + using beast::hash_append; + hash_append(h, args...); + auto const d = static_cast< + sha256_hasher::result_type>(h); + uint256 result; + std::memcpy(result.data(), d.data(), d.size()); + return result; + } + + // Create condition + // First is digest, second is pre-image + static + std::pair + cond (std::string const& receipt) + { + std::pair result; + result.second = digest(receipt); + result.first = digest(result.second); + return result; + } + + static + Json::Value + condpay (jtx::Account const& account, jtx::Account const& to, + STAmount const& amount, uint256 const& digest, + NetClock::time_point const& expiry) + { + using namespace jtx; + Json::Value jv; + jv[jss::TransactionType] = "SuspendedPaymentCreate"; + jv[jss::Flags] = tfUniversal; + jv[jss::Account] = account.human(); + jv[jss::Destination] = to.human(); + jv[jss::Amount] = amount.getJson(0); + jv["CancelAfter"] = + expiry.time_since_epoch().count(); + jv["Digest"] = to_string(digest); + 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] = "SuspendedPaymentCreate"; + 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 + finish (jtx::Account const& account, + jtx::Account const& from, std::uint32_t seq) + { + Json::Value jv; + jv[jss::TransactionType] = "SuspendedPaymentFinish"; + jv[jss::Flags] = tfUniversal; + jv[jss::Account] = account.human(); + jv["Owner"] = from.human(); + jv["OfferSequence"] = seq; + return jv; + } + + static + Json::Value + finish (jtx::Account const& account, + jtx::Account const& from, std::uint32_t seq, + uint256 const& digest, uint256 const& preimage) + { + Json::Value jv; + jv[jss::TransactionType] = "SuspendedPaymentFinish"; + jv[jss::Flags] = tfUniversal; + jv[jss::Account] = account.human(); + jv["Owner"] = from.human(); + jv["OfferSequence"] = seq; + jv["Method"] = 1; + jv["Digest"] = to_string(digest); + jv["Proof"] = to_string(preimage); + return jv; + } + + static + Json::Value + cancel (jtx::Account const& account, + jtx::Account const& from, std::uint32_t seq) + { + Json::Value jv; + jv[jss::TransactionType] = "SuspendedPaymentCancel"; + jv[jss::Flags] = tfUniversal; + jv[jss::Account] = account.human(); + jv["Owner"] = from.human(); + jv["OfferSequence"] = seq; + return jv; + } + + void + testEnablement() + { + using namespace jtx; + using namespace std::chrono; + using S = seconds; + { + Env env(*this); + auto const T = [&env](NetClock::duration const& d) + { return env.clock.now() + d; }; + env.fund(XRP(5000), "alice", "bob"); + auto const c = cond("receipt"); + // syntax + env(condpay("alice", "bob", XRP(1000), c.first, T(S{1}))); + env.disable_testing(); + // disabled in production + env(condpay("alice", "bob", XRP(1000), c.first, T(S{1})), ter(temDISABLED)); + env(finish("bob", "alice", 1), ter(temDISABLED)); + env(cancel("bob", "alice", 1), ter(temDISABLED)); + } + } + + void + testTags() + { + using namespace jtx; + using namespace std::chrono; + using S = seconds; + { + Env env(*this); + auto const alice = Account("alice"); + auto const T = [&env](NetClock::duration const& d) + { return env.clock.now() + d; }; + env.fund(XRP(5000), alice, "bob"); + auto const c = cond("receipt"); + auto const seq = env.seq(alice); + // set source and dest tags + env(condpay(alice, "bob", XRP(1000), c.first, T(S{1})), stag(1), dtag(2)); + auto const sle = env.le(keylet::susPay(alice.id(), seq)); + expect((*sle)[sfSourceTag] == 1); + expect((*sle)[sfDestinationTag] == 2); + } + } + + void + testFails() + { + using namespace jtx; + using namespace std::chrono; + using S = seconds; + { + Env env(*this); + auto const T = [&env](NetClock::duration const& d) + { return env.clock.now() + d; }; + env.fund(XRP(5000), "alice", "bob"); + auto const c = cond("receipt"); + // VFALCO Should we enforce this? + // expiration in the past + //env(condpay("alice", "bob", XRP(1000), c.first, T(S{-1})), ter(tecNO_PERMISSION)); + // expiration beyond the limit + env(condpay("alice", "bob", XRP(1000), c.first, T(days(7+1))), ter(tecNO_PERMISSION)); + // no destination account + env(condpay("alice", "carol", XRP(1000), c.first, T(S{1})), ter(tecNO_DST)); + env.fund(XRP(5000), "carol"); + env(condpay("alice", "carol", + XRP(1000), c.first, T(S{1})), stag(2)); + env(condpay("alice", "carol", + XRP(1000), c.first, T(S{1})), stag(3), dtag(4)); + env(fset("carol", asfRequireDest)); + // missing destination tag + env(condpay("alice", "carol", XRP(1000), c.first, T(S{1})), ter(tecDST_TAG_NEEDED)); + env(condpay("alice", "carol", + XRP(1000), c.first, T(S{1})), dtag(1)); + } + } + + void + testLockup() + { + using namespace jtx; + using namespace std::chrono; + using S = seconds; + { + Env env(*this); + auto const T = [&env](NetClock::duration const& d) + { return env.clock.now() + d; }; + env.fund(XRP(5000), "alice", "bob"); + auto const seq = env.seq("alice"); + env(lockup("alice", "alice", XRP(1000), T(S{1}))); + 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.close(); + env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + env(finish("bob", "alice", seq)); + } + } + + void + testCondPay() + { + using namespace jtx; + using namespace std::chrono; + using S = seconds; + { + Env env(*this); + auto const T = [&env](NetClock::duration const& d) + { return env.clock.now() + d; }; + env.fund(XRP(5000), "alice", "bob", "carol"); + auto const c = cond("receipt"); + auto const seq = env.seq("alice"); + expect((*env.le("alice"))[sfOwnerCount] == 0); + env(condpay("alice", "carol", XRP(1000), c.first, T(S{1}))); + 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)); + expect((*env.le("alice"))[sfOwnerCount] == 1); + env(finish("bob", "alice", seq, c.first, c.first), ter(temBAD_SIGNATURE)); + expect((*env.le("alice"))[sfOwnerCount] == 1); + env(finish("bob", "alice", seq, c.first, c.second)); + // SLE removed on finish + expect(! env.le(keylet::susPay(Account("alice").id(), seq))); + expect((*env.le("alice"))[sfOwnerCount] == 0); + env.require(balance("carol", XRP(6000))); + env(cancel("bob", "alice", seq), ter(tecNO_TARGET)); + expect((*env.le("alice"))[sfOwnerCount] == 0); + env(cancel("bob", "carol", 1), ter(tecNO_TARGET)); + env.close(); + } + { + Env env(*this); + auto const T = [&env](NetClock::duration const& d) + { return env.clock.now() + d; }; + env.fund(XRP(5000), "alice", "bob", "carol"); + auto const c = cond("receipt"); + auto const seq = env.seq("alice"); + expect((*env.le("alice"))[sfOwnerCount] == 0); + env(condpay("alice", "carol", XRP(1000), c.first, T(S{1}))); + env.close(); + env.require(balance("alice", XRP(4000) - drops(10))); + // balance restored on cancel + env(cancel("bob", "alice", seq)); + env.require(balance("alice", XRP(5000) - drops(10))); + // SLE removed on cancel + expect(! env.le(keylet::susPay(Account("alice").id(), seq))); + } + { + Env env(*this); + auto const T = [&env](NetClock::duration const& d) + { return env.clock.now() + d; }; + env.fund(XRP(5000), "alice", "bob", "carol"); + env.close(); + auto const c = cond("receipt"); + auto const seq = env.seq("alice"); + env(condpay("alice", "carol", XRP(1000), c.first, T(S{1}))); + expect((*env.le("alice"))[sfOwnerCount] == 1); + // cancel fails before expiration + env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION)); + expect((*env.le("alice"))[sfOwnerCount] == 1); + env.close(); + // finish fails after expiration + env(finish("bob", "alice", seq, c.first, c.second), ter(tecNO_PERMISSION)); + expect((*env.le("alice"))[sfOwnerCount] == 1); + env.require(balance("carol", XRP(5000))); + } + } + + void run() override + { + testEnablement(); + testTags(); + testFails(); + testLockup(); + testCondPay(); + } +}; + +BEAST_DEFINE_TESTSUITE(SusPay,app,ripple); + +} // test +} // ripple diff --git a/src/ripple/app/tx/impl/SusPay.cpp b/src/ripple/app/tx/impl/SusPay.cpp new file mode 100644 index 000000000..7db0263cb --- /dev/null +++ b/src/ripple/app/tx/impl/SusPay.cpp @@ -0,0 +1,410 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +/* + SuspendedPayment + + A suspended payment ("SusPay") sequesters XRP in its + own ledger entry until a SusPayFinish or a SusPayCancel + transaction mentioning the ledger entry is successfully + applied to the ledger. If the SusPayFinish succeeds, + the destination account (which must exist) receives the + XRP. If the SusPayCancel succeeds, the account which + created the SusPay is credited the XRP. + + SusPayCreate + + When the SusPay is created, an optional condition may + be attached. The condition is specified by providing + the cryptographic digest of the condition to be met. + + 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. + + Since the SusPay eventually becomes a payment, an + optional DestinationTag and an optional SourceTag + is supported in the SusPayCreate transaction. + + Validation rules: + + sfDigest + If present, proof is required on a SusPayFinish. + + sfCancelAfter + If present, SusPay may be canceled after the + specified time (seconds after the Ripple epoch). + + sfFinishAfter + If present, must be prior to sfCancelAfter. + A SusPayFinish 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 + + SusPayFinish + + Any account may submit a SusPayFinish. If the SusPay + ledger entry specifies a condition, the SusPayFinish + must provide the sfMethod, original sfDigest, and + sfProof fields. Depending on the method, a + cryptographic operation will be performed on sfProof + and the result must match the sfDigest or else the + SusPayFinish is considered as having an invalid + signature. + + Only sfMethod==1 is supported, where sfProof must be a + 256-bit unsigned big-endian integer which when hashed + using SHA256 produces digest == sfDigest. + + If the SusPay ledger entry specifies sfFinishAfter, the + transaction will fail if parentCloseTime <= sfFinishAfter. + + SusPayFinish transactions must be submitted before a + SusPay's sfCancelAfter if present. + + If the SusPay ledger entry specifies sfCancelAfter, the + transaction will fail if sfCancelAfter <= parentCloseTime. + + NOTE: It must always be possible to verify the condition + without retrieving the SusPay ledger entry. + + SusPayCancel + + Any account may submit a SusPayCancel transaction. + + If the SusPay ledger entry does not specify a + sfCancelAfter, the cancel transaction will fail. + + If parentCloseTime <= sfCancelAfter, the transaction + will fail. + + When a SusPay 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 +*/ + +//------------------------------------------------------------------------------ + +TER +SusPayCreate::preflight (PreflightContext const& ctx) +{ + if (! (ctx.flags & tapENABLE_TESTING) && + ! ctx.rules.enabled(featureSusPay, + ctx.config.features)) + return temDISABLED; + + if (! isXRP(ctx.tx[sfAmount])) + return temBAD_AMOUNT; + + if (ctx.tx[sfAmount] <= beast::zero) + return temBAD_AMOUNT; + + if (! ctx.tx[~sfCancelAfter] && + ! ctx.tx[~sfFinishAfter]) + return temBAD_EXPIRATION; + + if (ctx.tx[~sfCancelAfter] && ctx.tx[~sfFinishAfter] && + ctx.tx[sfCancelAfter] <= ctx.tx[sfFinishAfter]) + return temBAD_EXPIRATION; + + return Transactor::preflight(ctx); +} + +TER +SusPayCreate::doApply() +{ + // For now, require that all operations can be + // canceled, or finished without proof, within a + // reasonable period of time for the first release. + using namespace std::chrono; + auto const maxExpire = + ctx_.view().info().parentCloseTime + + seconds{days(7)}.count(); + if (ctx_.tx[~sfDigest]) + { + if (! ctx_.tx[~sfCancelAfter] || + maxExpire <= ctx_.tx[sfCancelAfter]) + return tecNO_PERMISSION; + } + else + { + if (ctx_.tx[~sfCancelAfter] && + maxExpire <= ctx_.tx[sfCancelAfter]) + return tecNO_PERMISSION; + if (ctx_.tx[~sfFinishAfter] && + maxExpire <= ctx_.tx[sfFinishAfter]) + return tecNO_PERMISSION; + } + + XRPAmount const amount = + STAmount(ctx_.tx[sfAmount]).mantissa(); + + auto const account = ctx_.tx[sfAccount]; + + auto const sle = ctx_.view().peek( + keylet::account(account)); + + if (XRPAmount(STAmount((*sle)[sfBalance]).mantissa()) < + XRPAmount(ctx_.view().fees().accountReserve( + (*sle)[sfOwnerCount] + 1))) + return tecINSUFFICIENT_RESERVE; + + if (XRPAmount(STAmount((*sle)[sfBalance]).mantissa()) < + amount + XRPAmount(ctx_.view().fees().accountReserve( + (*sle)[sfOwnerCount] + 1))) + return tecUNFUNDED; + + // Check destination account + { + auto const sled = ctx_.view().read( + keylet::account(ctx_.tx[sfDestination])); + if (! sled) + return tecNO_DST; + if (((*sled)[sfFlags] & lsfRequireDestTag) && + ! ctx_.tx[~sfDestinationTag]) + return tecDST_TAG_NEEDED; + if ((*sled)[sfFlags] & lsfDisallowXRP) + return tecNO_TARGET; + } + + // Create SusPay in ledger + auto const slep = std::make_shared( + keylet::susPay(account, (*sle)[sfSequence] - 1)); + (*slep)[sfAmount] = ctx_.tx[sfAmount]; + (*slep)[sfAccount] = account; + (*slep)[~sfDigest] = ctx_.tx[~sfDigest]; + (*slep)[~sfSourceTag] = ctx_.tx[~sfSourceTag]; + (*slep)[sfDestination] = ctx_.tx[sfDestination]; + (*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter]; + (*slep)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter]; + (*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag]; + + ctx_.view().insert(slep); + + // Add SusPay to owner directory + { + uint64_t page; + TER ter = dirAdd(ctx_.view(), page, + keylet::ownerDir(account).key, + slep->key(), describeOwnerDir(account)); + if (! isTesSuccess(ter)) + return ter; + (*slep)[sfOwnerNode] = page; + } + + // Deduct owner's balance, increment owner count + (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; + (*sle)[sfOwnerCount] = (*sle)[sfOwnerCount] + 1; + ctx_.view().update(sle); + + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + +TER +SusPayFinish::preflight (PreflightContext const& ctx) +{ + if (! (ctx.flags & tapENABLE_TESTING) && + ! ctx.rules.enabled(featureSusPay, + ctx.config.features)) + return temDISABLED; + + if (ctx.tx[~sfMethod]) + { + // Condition + switch(ctx.tx[sfMethod]) + { + case 1: + { + if (! ctx.tx[~sfDigest]) + return temMALFORMED; + if (! ctx.tx[~sfProof]) + return temMALFORMED; + sha256_hasher h; + using beast::hash_append; + hash_append(h, ctx.tx[sfProof]); + uint256 digest; + { + auto const result = static_cast< + sha256_hasher::result_type>(h); + std::memcpy(digest.data(), + result.data(), result.size()); + } + if (digest != ctx.tx[sfDigest]) + return temBAD_SIGNATURE; + break; + } + default: + return temMALFORMED; + } + } + else + { + // No Condition + if (ctx.tx[~sfDigest]) + return temMALFORMED; + if (ctx.tx[~sfProof]) + return temMALFORMED; + } + + return Transactor::preflight(ctx); +} + +TER +SusPayFinish::doApply() +{ + // peek SusPay SLE + auto const k = keylet::susPay( + ctx_.tx[sfOwner], ctx_.tx[sfOfferSequence]); + auto const slep = ctx_.view().peek(k); + if (! slep) + return tecNO_TARGET; + + // Too soon? + if ((*slep)[~sfFinishAfter] && + ctx_.view().info().parentCloseTime <= + (*slep)[sfFinishAfter]) + return tecNO_PERMISSION; + + // Too late? + if ((*slep)[~sfCancelAfter] && + (*slep)[sfCancelAfter] <= + ctx_.view().info().parentCloseTime) + return tecNO_PERMISSION; + + AccountID const account = (*slep)[sfAccount]; + + // Remove SusPay from owner directory + { + auto const page = (*slep)[sfOwnerNode]; + TER const ter = dirDelete(ctx_.view(), true, + page, keylet::ownerDir(account).key, + k.key, false, page == 0); + if (! isTesSuccess(ter)) + return ter; + } + + // NOTE: These payments cannot be used to fund accounts + + // Fetch Destination SLE + auto const sled = ctx_.view().peek( + keylet::account((*slep)[sfDestination])); + if (! sled) + return tecNO_DST; + + // Transfer amount to destination + (*sled)[sfBalance] = (*sled)[sfBalance] + (*slep)[sfAmount]; + ctx_.view().update(sled); + + // Adjust source owner count + auto const sle = ctx_.view().peek( + keylet::account(account)); + (*sle)[sfOwnerCount] = (*sle)[sfOwnerCount] - 1; + ctx_.view().update(sle); + + // Remove SusPay from ledger + ctx_.view().erase(slep); + + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + +TER +SusPayCancel::preflight (PreflightContext const& ctx) +{ + if (! (ctx.flags & tapENABLE_TESTING) && + ! ctx.rules.enabled(featureSusPay, + ctx.config.features)) + return temDISABLED; + + return Transactor::preflight(ctx); +} + +TER +SusPayCancel::doApply() +{ + // peek SusPay SLE + auto const k = keylet::susPay( + 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 <= + (*slep)[sfCancelAfter]) + return tecNO_PERMISSION; + + AccountID const account = (*slep)[sfAccount]; + + // Remove SusPay from owner directory + { + auto const page = (*slep)[sfOwnerNode]; + TER const ter = dirDelete(ctx_.view(), true, + page, keylet::ownerDir(account).key, + k.key, false, page == 0); + if (! isTesSuccess(ter)) + return ter; + } + + // Transfer amount back to owner, decrement owner count + auto const sle = ctx_.view().peek( + keylet::account(account)); + (*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount]; + (*sle)[sfOwnerCount] = (*sle)[sfOwnerCount] - 1; + ctx_.view().update(sle); + + // Remove SusPay from ledger + ctx_.view().erase(slep); + + return tesSUCCESS; +} + +} // ripple + diff --git a/src/ripple/app/tx/impl/SusPay.h b/src/ripple/app/tx/impl/SusPay.h new file mode 100644 index 000000000..2263990c6 --- /dev/null +++ b/src/ripple/app/tx/impl/SusPay.h @@ -0,0 +1,87 @@ +//------------------------------------------------------------------------------ +/* + 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. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_SUSPAY_H_INCLUDED +#define RIPPLE_TX_SUSPAY_H_INCLUDED + +#include + +namespace ripple { + +class SusPayCreate + : public Transactor +{ +public: + explicit + SusPayCreate (ApplyContext& ctx) + : Transactor(ctx) + { + } + + static + TER + preflight (PreflightContext const& ctx); + + TER + doApply() override; +}; + +//------------------------------------------------------------------------------ + +class SusPayFinish + : public Transactor +{ +public: + explicit + SusPayFinish (ApplyContext& ctx) + : Transactor(ctx) + { + } + + static + TER + preflight (PreflightContext const& ctx); + + TER + doApply() override; +}; + +//------------------------------------------------------------------------------ + +class SusPayCancel + : public Transactor +{ +public: + explicit + SusPayCancel (ApplyContext& ctx) + : Transactor(ctx) + { + } + + static + TER + preflight (PreflightContext const& ctx); + + TER + doApply() override; +}; + +} // ripple + +#endif diff --git a/src/ripple/app/tx/impl/apply.cpp b/src/ripple/app/tx/impl/apply.cpp index dddbf14f2..c350cbd91 100644 --- a/src/ripple/app/tx/impl/apply.cpp +++ b/src/ripple/app/tx/impl/apply.cpp @@ -30,6 +30,7 @@ #include #include #include +#include namespace ripple { @@ -43,6 +44,9 @@ invoke_preflight (PreflightContext const& ctx) case ttOFFER_CANCEL: return CancelOffer ::preflight(ctx); case ttOFFER_CREATE: return CreateOffer ::preflight(ctx); case ttPAYMENT: return Payment ::preflight(ctx); + case ttSUSPAY_CREATE: return SusPayCreate ::preflight(ctx); + case ttSUSPAY_FINISH: return SusPayFinish ::preflight(ctx); + case ttSUSPAY_CANCEL: return SusPayCancel ::preflight(ctx); case ttREGULAR_KEY_SET: return SetRegularKey ::preflight(ctx); case ttSIGNER_LIST_SET: return SetSignerList ::preflight(ctx); case ttTICKET_CANCEL: return CancelTicket ::preflight(ctx); @@ -65,6 +69,9 @@ invoke_apply (ApplyContext& ctx) case ttOFFER_CANCEL: { CancelOffer p(ctx); return p(); } case ttOFFER_CREATE: { CreateOffer p(ctx); return p(); } case ttPAYMENT: { Payment p(ctx); return p(); } + case ttSUSPAY_CREATE: { SusPayCreate p(ctx); return p(); } + case ttSUSPAY_FINISH: { SusPayFinish p(ctx); return p(); } + case ttSUSPAY_CANCEL: { SusPayCancel p(ctx); return p(); } case ttREGULAR_KEY_SET: { SetRegularKey p(ctx); return p(); } case ttSIGNER_LIST_SET: { SetSignerList p(ctx); return p(); } case ttTICKET_CANCEL: { CancelTicket p(ctx); return p(); } diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index ec91f7c91..3519bd932 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -34,6 +34,8 @@ uint256 feature (const char* name); /** @} */ +extern uint256 const featureSusPay; + } // ripple #endif diff --git a/src/ripple/protocol/Indexes.h b/src/ripple/protocol/Indexes.h index 9951da794..01140148c 100644 --- a/src/ripple/protocol/Indexes.h +++ b/src/ripple/protocol/Indexes.h @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -234,6 +235,10 @@ Keylet page (uint256 const& key) return { ltDIR_NODE, key }; } +/** A SuspendedPayment */ +Keylet +susPay (AccountID const& source, std::uint32_t seq); + } // keylet } diff --git a/src/ripple/protocol/LedgerFormats.h b/src/ripple/protocol/LedgerFormats.h index 1ee4a0ba3..49e57e612 100644 --- a/src/ripple/protocol/LedgerFormats.h +++ b/src/ripple/protocol/LedgerFormats.h @@ -64,15 +64,12 @@ enum LedgerEntryType */ ltDIR_NODE = 'd', - /** Describes a trust line. - */ ltRIPPLE_STATE = 'r', ltTICKET = 'T', ltSIGNER_LIST = 'S', - /* Deprecated. */ ltOFFER = 'o', ltLEDGER_HASHES = 'h', @@ -81,6 +78,8 @@ enum LedgerEntryType ltFEE_SETTINGS = 's', + ltSUSPAY = 'u', + // No longer used or supported. Left here to prevent accidental // reassignment of the ledger type. ltNICKNAME = 'n', @@ -103,6 +102,7 @@ enum LedgerNameSpace spaceBookDir = 'B', // Directory of order books. spaceContract = 'c', spaceSkipList = 's', + spaceSusPay = 'u', spaceAmendment = 'f', spaceFee = 'e', spaceTicket = 'T', diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index 154517778..d962d32fc 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -344,7 +344,7 @@ extern SField const sfMetadata; // 8-bit integers extern SF_U8 const sfCloseResolution; -extern SF_U8 const sfTemplateEntryType; +extern SF_U8 const sfMethod; extern SF_U8 const sfTransactionResult; // 16-bit integers @@ -388,6 +388,8 @@ extern SF_U32 const sfReserveIncrement; extern SF_U32 const sfSetFlag; extern SF_U32 const sfClearFlag; extern SF_U32 const sfSignerQuorum; +extern SF_U32 const sfCancelAfter; +extern SF_U32 const sfFinishAfter; // 64-bit integers extern SF_U64 const sfIndexNext; @@ -461,7 +463,7 @@ extern SF_Blob const sfMemoFormat; // variable length (uncommon) extern SF_Blob const sfMultiSignature; -extern SF_Blob const sfInnerSig; +extern SF_Blob const sfProof; // account extern SF_Account const sfAccount; diff --git a/src/ripple/protocol/TxFormats.h b/src/ripple/protocol/TxFormats.h index 12ae39b96..74dccc0f4 100644 --- a/src/ripple/protocol/TxFormats.h +++ b/src/ripple/protocol/TxFormats.h @@ -35,10 +35,10 @@ enum TxType ttINVALID = -1, ttPAYMENT = 0, - ttCLAIM = 1, // open - ttWALLET_ADD = 2, // unused + ttSUSPAY_CREATE = 1, + ttSUSPAY_FINISH = 2, ttACCOUNT_SET = 3, - ttPASSWORD_FUND = 4, // open + ttSUSPAY_CANCEL = 4, ttREGULAR_KEY_SET = 5, ttNICKNAME_SET = 6, // open ttOFFER_CREATE = 7, diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index bb58be4b8..5825de38b 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -45,4 +45,6 @@ feature (const char* name) return feature(name, std::strlen(name)); } +uint256 const featureSusPay = feature("SusPay"); + } // ripple diff --git a/src/ripple/protocol/impl/Indexes.cpp b/src/ripple/protocol/impl/Indexes.cpp index 4f6342176..9987d0799 100644 --- a/src/ripple/protocol/impl/Indexes.cpp +++ b/src/ripple/protocol/impl/Indexes.cpp @@ -308,6 +308,17 @@ Keylet page(Keylet const& root, return page(root.key, index); } +Keylet +susPay (AccountID const& source, std::uint32_t seq) +{ + sha512_half_hasher h; + using beast::hash_append; + hash_append(h, spaceSusPay); + hash_append(h, source); + hash_append(h, seq); + return { ltSUSPAY, static_cast(h) }; +} + } // keylet } // ripple diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index 225ae9fef..54b19d65d 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -81,6 +81,19 @@ LedgerFormats::LedgerFormats () << SOElement (sfHighQualityOut, SOE_OPTIONAL) ; + add ("SusPay", ltSUSPAY) << + SOElement (sfAccount, SOE_REQUIRED) << + SOElement (sfDestination, SOE_REQUIRED) << + SOElement (sfAmount, SOE_REQUIRED) << + SOElement (sfDigest, SOE_OPTIONAL) << + SOElement (sfCancelAfter, SOE_OPTIONAL) << + SOElement (sfFinishAfter, SOE_OPTIONAL) << + SOElement (sfSourceTag, SOE_OPTIONAL) << + SOElement (sfDestinationTag, SOE_OPTIONAL) << + SOElement (sfOwnerNode, SOE_REQUIRED) << + SOElement (sfPreviousTxnID, SOE_REQUIRED) << + SOElement (sfPreviousTxnLgrSeq, SOE_REQUIRED); + add ("LedgerHashes", ltLEDGER_HASHES) << SOElement (sfFirstLedgerSequence, SOE_OPTIONAL) // Remove if we do a ledger restart << SOElement (sfLastLedgerSequence, SOE_OPTIONAL) diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index 27332a324..44fdc86cd 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -77,7 +77,7 @@ SField const sfIndex = make::one(&sfIndex, STI_HASH256, 258, "in // 8-bit integers SF_U8 const sfCloseResolution = make::one(&sfCloseResolution, STI_UINT8, 1, "CloseResolution"); -SF_U8 const sfTemplateEntryType = make::one(&sfTemplateEntryType, STI_UINT8, 2, "TemplateEntryType"); +SF_U8 const sfMethod = make::one(&sfMethod, STI_UINT8, 2, "Method"); SF_U8 const sfTransactionResult = make::one(&sfTransactionResult, STI_UINT8, 3, "TransactionResult"); // 16-bit integers @@ -121,6 +121,8 @@ SF_U32 const sfReserveIncrement = make::one(&sfReserveIncrement SF_U32 const sfSetFlag = make::one(&sfSetFlag, STI_UINT32, 33, "SetFlag"); SF_U32 const sfClearFlag = make::one(&sfClearFlag, STI_UINT32, 34, "ClearFlag"); SF_U32 const sfSignerQuorum = make::one(&sfSignerQuorum, STI_UINT32, 35, "SignerQuorum"); +SF_U32 const sfCancelAfter = make::one(&sfCancelAfter, STI_UINT32, 36, "CancelAfter"); +SF_U32 const sfFinishAfter = make::one(&sfFinishAfter, STI_UINT32, 37, "FinishAfter"); // 64-bit integers SF_U64 const sfIndexNext = make::one(&sfIndexNext, STI_UINT64, 1, "IndexNext"); @@ -194,7 +196,7 @@ SF_Blob const sfMemoFormat = make::one(&sfMemoFormat, STI // variable length (uncommon) SF_Blob const sfMultiSignature = make::one(&sfMultiSignature, STI_VL, 16, "MultiSignature"); -SF_Blob const sfInnerSig = make::one(&sfInnerSig, STI_VL, 17, "InnerSig"); +SF_Blob const sfProof = make::one(&sfProof, STI_VL, 17, "Proof"); // account SF_Account const sfAccount = make::one(&sfAccount, STI_ACCOUNT, 1, "Account"); diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index 8b5c5b11c..450885ffe 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -67,6 +67,25 @@ TxFormats::TxFormats () << SOElement (sfDeliverMin, SOE_OPTIONAL) ; + add ("SuspendedPaymentCreate", ttSUSPAY_CREATE) << + SOElement (sfDestination, SOE_REQUIRED) << + SOElement (sfAmount, SOE_REQUIRED) << + SOElement (sfDigest, SOE_OPTIONAL) << + SOElement (sfCancelAfter, SOE_OPTIONAL) << + SOElement (sfFinishAfter, SOE_OPTIONAL) << + SOElement (sfDestinationTag, SOE_OPTIONAL); + + add ("SuspendedPaymentFinish", ttSUSPAY_FINISH) << + SOElement (sfOwner, SOE_REQUIRED) << + SOElement (sfOfferSequence, SOE_REQUIRED) << + SOElement (sfMethod, SOE_OPTIONAL) << + SOElement (sfDigest, SOE_OPTIONAL) << + SOElement (sfProof, SOE_OPTIONAL); + + add ("SuspendedPaymentCancel", ttSUSPAY_CANCEL) << + SOElement (sfOwner, SOE_REQUIRED) << + SOElement (sfOfferSequence, SOE_REQUIRED); + add ("EnableAmendment", ttAMENDMENT) << SOElement (sfLedgerSequence, SOE_OPTIONAL) << SOElement (sfAmendment, SOE_REQUIRED) diff --git a/src/ripple/rpc/handlers/Feature.cpp b/src/ripple/rpc/handlers/Feature1.cpp similarity index 100% rename from src/ripple/rpc/handlers/Feature.cpp rename to src/ripple/rpc/handlers/Feature1.cpp diff --git a/src/ripple/test/jtx.h b/src/ripple/test/jtx.h index 8bf47e841..821716eaf 100644 --- a/src/ripple/test/jtx.h +++ b/src/ripple/test/jtx.h @@ -22,6 +22,7 @@ // Convenience header that includes everything +#include #include #include #include @@ -46,6 +47,7 @@ #include #include #include +#include #include #include #include diff --git a/src/ripple/test/jtx/impl/tag.cpp b/src/ripple/test/jtx/impl/tag.cpp new file mode 100644 index 000000000..ee51d0504 --- /dev/null +++ b/src/ripple/test/jtx/impl/tag.cpp @@ -0,0 +1,42 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +void +dtag::operator()(Env const&, JTx& jt) const +{ + jt.jv["DestinationTag"] = value_; +} + +void +stag::operator()(Env const&, JTx& jt) const +{ + jt.jv["SourceTag"] = value_; +} + +} // jtx +} // test +} // ripple diff --git a/src/ripple/test/jtx/pay.h b/src/ripple/test/jtx/pay.h index 60c513c3d..467c0a547 100644 --- a/src/ripple/test/jtx/pay.h +++ b/src/ripple/test/jtx/pay.h @@ -31,8 +31,7 @@ namespace jtx { /** Create a payment. */ Json::Value pay (Account const& account, - Account const& to, - AnyAmount amount); + Account const& to, AnyAmount amount); } // jtx } // test diff --git a/src/ripple/test/jtx/tag.h b/src/ripple/test/jtx/tag.h new file mode 100644 index 000000000..94e13f2c0 --- /dev/null +++ b/src/ripple/test/jtx/tag.h @@ -0,0 +1,69 @@ +//------------------------------------------------------------------------------ +/* + 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. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_JTX_TAG_H_INCLUDED +#define RIPPLE_TEST_JTX_TAG_H_INCLUDED + +#include + +namespace ripple { +namespace test { + +namespace jtx { + +/** Set the destination tag on a JTx*/ +struct dtag +{ +private: + std::uint32_t value_; + +public: + explicit + dtag (std::uint32_t value) + : value_ (value) + { + } + + void + operator()(Env const&, JTx& jt) const; +}; + +/** Set the source tag on a JTx*/ +struct stag +{ +private: + std::uint32_t value_; + +public: + explicit + stag (std::uint32_t value) + : value_ (value) + { + } + + void + operator()(Env const&, JTx& jt) const; +}; + +} // jtx + +} // test +} // ripple + +#endif diff --git a/src/ripple/unity/app_tests.cpp b/src/ripple/unity/app_tests.cpp index f27ccf031..dad4846a1 100644 --- a/src/ripple/unity/app_tests.cpp +++ b/src/ripple/unity/app_tests.cpp @@ -19,4 +19,5 @@ #include +#include #include diff --git a/src/ripple/unity/app_tx.cpp b/src/ripple/unity/app_tx.cpp index 02195db84..cb874838a 100644 --- a/src/ripple/unity/app_tx.cpp +++ b/src/ripple/unity/app_tx.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include diff --git a/src/ripple/unity/rpcx.cpp b/src/ripple/unity/rpcx.cpp index 79cf6b87d..580dd4b8b 100644 --- a/src/ripple/unity/rpcx.cpp +++ b/src/ripple/unity/rpcx.cpp @@ -46,7 +46,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/src/ripple/unity/test.cpp b/src/ripple/unity/test.cpp index 932b9dcfa..d766d5475 100644 --- a/src/ripple/unity/test.cpp +++ b/src/ripple/unity/test.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include