diff --git a/Builds/VisualStudio2015/RippleD.vcxproj b/Builds/VisualStudio2015/RippleD.vcxproj index 04e8a4d23..c2acc96da 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj +++ b/Builds/VisualStudio2015/RippleD.vcxproj @@ -1179,6 +1179,10 @@ True True + + True + True + True True @@ -1273,6 +1277,12 @@ + + True + True + + + True True @@ -2854,6 +2864,8 @@ + + @@ -3051,6 +3063,10 @@ + + True + True + True True @@ -3183,6 +3199,10 @@ True True + + True + True + True True diff --git a/Builds/VisualStudio2015/RippleD.vcxproj.filters b/Builds/VisualStudio2015/RippleD.vcxproj.filters index 493226509..cb10cbe3e 100644 --- a/Builds/VisualStudio2015/RippleD.vcxproj.filters +++ b/Builds/VisualStudio2015/RippleD.vcxproj.filters @@ -1647,6 +1647,9 @@ ripple\app\tests + + ripple\app\tests + ripple\app\tests @@ -1734,6 +1737,12 @@ ripple\app\tx\impl + + ripple\app\tx\impl + + + ripple\app\tx\impl + ripple\app\tx\impl @@ -3351,6 +3360,9 @@ ripple\protocol + + ripple\protocol + ripple\protocol @@ -3564,6 +3576,9 @@ ripple\rpc + + ripple\rpc\handlers + ripple\rpc\handlers @@ -3666,6 +3681,9 @@ ripple\rpc\handlers + + ripple\rpc\handlers + ripple\rpc\handlers diff --git a/src/ripple/app/main/Amendments.cpp b/src/ripple/app/main/Amendments.cpp index 2d15c9691..157114538 100644 --- a/src/ripple/app/main/Amendments.cpp +++ b/src/ripple/app/main/Amendments.cpp @@ -45,7 +45,8 @@ supportedAmendments () { "6781F8368C4771B83E8B821D88F580202BCB4228075297B19E4FDC5233F1EFDC TrustSetAuth" }, { "42426C4D4F1009EE67080A9B7965B44656D7714D104A72F9B4369F97ABF044EE FeeEscalation" }, { "5CC22CFF2864B020BD79E0E1F048F63EF3594F95E650E43B3F837EF1DF5F4B26 FlowV2"}, - { "9178256A980A86CF3D70D0260A7DA6402AAFE43632FDBCB88037978404188871 OwnerPaysFee"} + { "9178256A980A86CF3D70D0260A7DA6402AAFE43632FDBCB88037978404188871 OwnerPaysFee"}, + { "08DE7D96082187F6E6578530258C77FAABABE4C20474BDB82F04B021F1A68647 PayChan"} }; } diff --git a/src/ripple/app/main/Main.cpp b/src/ripple/app/main/Main.cpp index 0df70a6f8..d0386a104 100644 --- a/src/ripple/app/main/Main.cpp +++ b/src/ripple/app/main/Main.cpp @@ -121,11 +121,14 @@ void printHelp (const po::options_description& desc) " account_currencies [] [strict]\n" " account_info ||| [] [strict]\n" " account_lines |\"\" []\n" + " account_channels |\"\" []\n" " account_objects [] [strict]\n" " account_offers | []\n" " account_tx accountID [ledger_min [ledger_max [limit [offset]]]] [binary] [count] [descending]\n" " book_offers [ [ [ []]]]]\n" " can_delete [||now|always|never]\n" + " channel_authorize \n" + " channel_verify \n" " connect []\n" " consensus_info\n" " feature [ [accept|reject]]\n" diff --git a/src/ripple/app/tests/PayChan_test.cpp b/src/ripple/app/tests/PayChan_test.cpp new file mode 100644 index 000000000..d9b667c3c --- /dev/null +++ b/src/ripple/app/tests/PayChan_test.cpp @@ -0,0 +1,840 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace ripple +{ +namespace test +{ +struct PayChan_test : public beast::unit_test::suite +{ + static + uint256 + channel (ReadView const& view, + jtx::Account const& account, + jtx::Account const& dst) + { + auto const sle = view.read (keylet::account (account)); + if (!sle) + return beast::zero; + auto const k = keylet::payChan (account, dst, (*sle)[sfSequence] - 1); + return k.key; + } + + static Buffer + signClaimAuth (PublicKey const& pk, + SecretKey const& sk, + uint256 const& channel, + STAmount const& authAmt) + { + Serializer msg; + serializePayChanAuthorization (msg, channel, authAmt.xrp ()); + return sign (pk, sk, msg.slice ()); + } + + static + STAmount + channelBalance (ReadView const& view, uint256 const& chan) + { + auto const slep = view.read ({ltPAYCHAN, chan}); + if (!slep) + return XRPAmount{-1}; + return (*slep)[sfBalance]; + } + + static + bool + channelExists (ReadView const& view, uint256 const& chan) + { + auto const slep = view.read ({ltPAYCHAN, chan}); + return bool(slep); + } + + static + STAmount + channelAmount (ReadView const& view, uint256 const& chan) + { + auto const slep = view.read ({ltPAYCHAN, chan}); + if (!slep) + return XRPAmount{-1}; + return (*slep)[sfAmount]; + } + + static + boost::optional + channelExpiration (ReadView const& view, uint256 const& chan) + { + auto const slep = view.read ({ltPAYCHAN, chan}); + if (!slep) + return boost::none; + if (auto const r = (*slep)[~sfExpiration]) + return r.value(); + return boost::none; + + } + + static Json::Value + create (jtx::Account const& account, + jtx::Account const& to, + STAmount const& amount, + NetClock::duration const& settleDelay, + PublicKey const& pk, + boost::optional const& cancelAfter = boost::none, + boost::optional const& dstTag = boost::none) + { + using namespace jtx; + Json::Value jv; + jv[jss::TransactionType] = "PaymentChannelCreate"; + jv[jss::Flags] = tfUniversal; + jv[jss::Account] = account.human (); + jv[jss::Destination] = to.human (); + jv[jss::Amount] = amount.getJson (0); + jv["SettleDelay"] = settleDelay.count (); + jv["PublicKey"] = strHex (pk.slice ()); + if (cancelAfter) + jv["CancelAfter"] = cancelAfter->time_since_epoch ().count (); + if (dstTag) + jv["DestinationTag"] = *dstTag; + return jv; + } + + static + Json::Value + fund (jtx::Account const& account, + uint256 const& channel, + STAmount const& amount, + boost::optional const& expiration = boost::none) + { + using namespace jtx; + Json::Value jv; + jv[jss::TransactionType] = "PaymentChannelFund"; + jv[jss::Flags] = tfUniversal; + jv[jss::Account] = account.human (); + jv["Channel"] = to_string (channel); + jv[jss::Amount] = amount.getJson (0); + if (expiration) + jv["Expiration"] = expiration->time_since_epoch ().count (); + return jv; + } + + static + Json::Value + claim (jtx::Account const& account, + uint256 const& channel, + boost::optional const& balance = boost::none, + boost::optional const& amount = boost::none, + boost::optional const& signature = boost::none, + boost::optional const& pk = boost::none) + { + using namespace jtx; + Json::Value jv; + jv[jss::TransactionType] = "PaymentChannelClaim"; + jv[jss::Flags] = tfUniversal; + jv[jss::Account] = account.human (); + jv["Channel"] = to_string (channel); + if (amount) + jv[jss::Amount] = amount->getJson (0); + if (balance) + jv["Balance"] = balance->getJson (0); + if (signature) + jv["Signature"] = strHex (*signature); + if (pk) + jv["PublicKey"] = strHex (pk->slice ()); + return jv; + } + + void + testSimple () + { + testcase ("simple"); + using namespace jtx; + using namespace std::literals::chrono_literals; + Env env (*this, features (featurePayChan)); + auto const alice = Account ("alice"); + auto const bob = Account ("bob"); + auto USDA = alice["USD"]; + env.fund (XRP (10000), alice, bob); + auto const pk = alice.pk (); + auto const settleDelay = 100s; + env (create (alice, bob, XRP (1000), settleDelay, pk)); + auto const chan = channel (*env.current (), alice, bob); + BEAST_EXPECT (channelBalance (*env.current (), chan) == XRP (0)); + BEAST_EXPECT (channelAmount (*env.current (), chan) == XRP (1000)); + + { + auto const preAlice = env.balance (alice); + env (fund (alice, chan, XRP (1000))); + auto const feeDrops = env.current ()->fees ().base; + BEAST_EXPECT (env.balance (alice) == preAlice - XRP (1000) - feeDrops); + } + + auto chanBal = channelBalance (*env.current (), chan); + auto chanAmt = channelAmount (*env.current (), chan); + BEAST_EXPECT (chanBal == XRP (0)); + BEAST_EXPECT (chanAmt == XRP (2000)); + + { + // bad amounts (non-xrp, negative amounts) + env (create (alice, bob, USDA (1000), settleDelay, pk), + ter (temBAD_AMOUNT)); + env (fund (alice, chan, USDA (1000)), + ter (temBAD_AMOUNT)); + env (create (alice, bob, XRP (-1000), settleDelay, pk), + ter (temBAD_AMOUNT)); + env (fund (alice, chan, XRP (-1000)), + ter (temBAD_AMOUNT)); + } + + // invalid account + env (create (alice, "noAccount", XRP (1000), settleDelay, pk), + ter (tecNO_DST)); + // can't create channel to the same account + env (create (alice, alice, XRP (1000), settleDelay, pk), + ter (temDST_IS_SRC)); + // invalid channel + env (fund (alice, channel (*env.current (), alice, "noAccount"), XRP (1000)), + ter (tecNO_ENTRY)); + // not enough funds + env (create (alice, bob, XRP (10000), settleDelay, pk), + ter (tecUNFUNDED)); + + { + // No signature claim with bad amounts (negative and non-xrp) + auto const iou = USDA (100).value (); + auto const negXRP = XRP (-100).value (); + auto const posXRP = XRP (100).value (); + env (claim (alice, chan, iou, iou), ter (temBAD_AMOUNT)); + env (claim (alice, chan, posXRP, iou), ter (temBAD_AMOUNT)); + env (claim (alice, chan, iou, posXRP), ter (temBAD_AMOUNT)); + env (claim (alice, chan, negXRP, negXRP), ter (temBAD_AMOUNT)); + env (claim (alice, chan, posXRP, negXRP), ter (temBAD_AMOUNT)); + env (claim (alice, chan, negXRP, posXRP), ter (temBAD_AMOUNT)); + } + { + // No signature claim more than authorized + auto const delta = XRP (500); + auto const reqBal = chanBal + delta; + auto const authAmt = reqBal + XRP (-100); + assert (reqBal <= chanAmt); + env (claim (alice, chan, reqBal, authAmt), ter (tecNO_PERMISSION)); + } + { + // No signature needed since the owner is claiming + auto const preBob = env.balance (bob); + auto const delta = XRP (500); + auto const reqBal = chanBal + delta; + auto const authAmt = reqBal + XRP (100); + assert (reqBal <= chanAmt); + env (claim (alice, chan, reqBal, authAmt)); + BEAST_EXPECT (channelBalance (*env.current (), chan) == reqBal); + BEAST_EXPECT (channelAmount (*env.current (), chan) == chanAmt); + BEAST_EXPECT (env.balance (bob) == preBob + delta); + chanBal = reqBal; + } + { + // Claim with signature + auto preBob = env.balance (bob); + auto const delta = XRP (500); + auto const reqBal = chanBal + delta; + auto const authAmt = reqBal + XRP (100); + assert (reqBal <= chanAmt); + auto const sig = + signClaimAuth (alice.pk (), alice.sk (), chan, authAmt); + env (claim (bob, chan, reqBal, authAmt, Slice (sig), alice.pk ())); + BEAST_EXPECT (channelBalance (*env.current (), chan) == reqBal); + BEAST_EXPECT (channelAmount (*env.current (), chan) == chanAmt); + auto const feeDrops = env.current ()->fees ().base; + BEAST_EXPECT (env.balance (bob) == preBob + delta - feeDrops); + chanBal = reqBal; + + // claim again + preBob = env.balance (bob); + env (claim (bob, chan, reqBal, authAmt, Slice (sig), alice.pk ()), + ter (tecUNFUNDED_PAYMENT)); + BEAST_EXPECT (channelBalance (*env.current (), chan) == chanBal); + BEAST_EXPECT (channelAmount (*env.current (), chan) == chanAmt); + BEAST_EXPECT (env.balance (bob) == preBob - feeDrops); + } + { + // Try to claim more than authorized + auto const preBob = env.balance (bob); + STAmount const authAmt = chanBal + XRP (500); + STAmount const reqAmt = authAmt + 1; + assert (reqAmt <= chanAmt); + auto const sig = + signClaimAuth (alice.pk (), alice.sk (), chan, authAmt); + env (claim (bob, chan, reqAmt, authAmt, Slice (sig), alice.pk ()), + ter (tecNO_PERMISSION)); + BEAST_EXPECT (channelBalance (*env.current (), chan) == chanBal); + BEAST_EXPECT (channelAmount (*env.current (), chan) == chanAmt); + auto const feeDrops = env.current ()->fees ().base; + BEAST_EXPECT (env.balance (bob) == preBob - feeDrops); + } + + // Dst tries to fund the channel + env (fund (bob, chan, XRP (1000)), ter (tecNO_PERMISSION)); + BEAST_EXPECT (channelBalance (*env.current (), chan) == chanBal); + BEAST_EXPECT (channelAmount (*env.current (), chan) == chanAmt); + + { + // Wrong signing key + auto const sig = + signClaimAuth (bob.pk (), bob.sk (), chan, XRP (1500)); + env (claim (bob, chan, XRP (1500).value (), XRP (1500).value (), + Slice (sig), bob.pk ()), + ter (temBAD_SIGNER)); + BEAST_EXPECT (channelBalance (*env.current (), chan) == chanBal); + BEAST_EXPECT (channelAmount (*env.current (), chan) == chanAmt); + } + { + // Bad signature + auto const sig = + signClaimAuth (bob.pk (), bob.sk (), chan, XRP (1500)); + env (claim (bob, chan, XRP (1500).value (), XRP (1500).value (), + Slice (sig), alice.pk ()), + ter (temBAD_SIGNATURE)); + BEAST_EXPECT (channelBalance (*env.current (), chan) == chanBal); + BEAST_EXPECT (channelAmount (*env.current (), chan) == chanAmt); + } + { + // Dst closes channel + auto const preAlice = env.balance (alice); + auto const preBob = env.balance (bob); + env (claim (bob, chan), txflags (tfClose)); + BEAST_EXPECT (!channelExists (*env.current (), chan)); + auto const feeDrops = env.current ()->fees ().base; + auto const delta = chanAmt - chanBal; + assert (delta > beast::zero); + BEAST_EXPECT (env.balance (alice) == preAlice + delta); + BEAST_EXPECT (env.balance (bob) == preBob - feeDrops); + } + } + + void + testCancelAfter () + { + testcase ("cancel after"); + using namespace jtx; + using namespace std::literals::chrono_literals; + auto const alice = Account ("alice"); + auto const bob = Account ("bob"); + auto const carol = Account ("carol"); + { + // If dst claims after cancel after, channel closes + Env env (*this, features (featurePayChan)); + env.fund (XRP (10000), alice, bob); + auto const pk = alice.pk (); + auto const settleDelay = 100s; + NetClock::time_point const cancelAfter = + env.current ()->info ().parentCloseTime + 3600s; + auto const channelFunds = XRP (1000); + env (create ( + alice, bob, channelFunds, settleDelay, pk, cancelAfter)); + auto const chan = channel (*env.current (), alice, bob); + if (!chan) + { + fail (); + return; + } + BEAST_EXPECT (channelExists (*env.current (), chan)); + env.close (cancelAfter); + { + // dst cannot claim after cancelAfter + auto const chanBal = channelBalance (*env.current (), chan); + auto const chanAmt = channelAmount (*env.current (), chan); + auto preAlice = env.balance (alice); + auto preBob = env.balance (bob); + auto const delta = XRP (500); + auto const reqBal = chanBal + delta; + auto const authAmt = reqBal + XRP (100); + assert (reqBal <= chanAmt); + auto const sig = + signClaimAuth (alice.pk (), alice.sk (), chan, authAmt); + env (claim ( + bob, chan, reqBal, authAmt, Slice (sig), alice.pk ())); + auto const feeDrops = env.current ()->fees ().base; + BEAST_EXPECT (!channelExists (*env.current (), chan)); + BEAST_EXPECT (env.balance (bob) == preBob - feeDrops); + BEAST_EXPECT (env.balance (alice) == preAlice + channelFunds); + } + } + { + // Third party can close after cancel after + Env env (*this, features (featurePayChan)); + env.fund (XRP (10000), alice, bob, carol); + auto const pk = alice.pk (); + auto const settleDelay = 100s; + NetClock::time_point const cancelAfter = + env.current ()->info ().parentCloseTime + 3600s; + auto const channelFunds = XRP (1000); + env (create ( + alice, bob, channelFunds, settleDelay, pk, cancelAfter)); + auto const chan = channel (*env.current (), alice, bob); + BEAST_EXPECT (channelExists (*env.current (), chan)); + // third party close before cancelAfter + env (claim (carol, chan), txflags (tfClose), + ter (tecNO_PERMISSION)); + BEAST_EXPECT (channelExists (*env.current (), chan)); + env.close (cancelAfter); + // third party close after cancelAfter + auto const preAlice = env.balance (alice); + env (claim (carol, chan), txflags (tfClose)); + BEAST_EXPECT (!channelExists (*env.current (), chan)); + BEAST_EXPECT (env.balance (alice) == preAlice + channelFunds); + } + } + + void + testExpiration () + { + testcase ("expiration"); + using namespace jtx; + using namespace std::literals::chrono_literals; + Env env (*this, features (featurePayChan)); + auto const alice = Account ("alice"); + auto const bob = Account ("bob"); + auto const carol = Account ("carol"); + env.fund (XRP (10000), alice, bob, carol); + auto const pk = alice.pk (); + auto const settleDelay = 3600s; + auto const closeTime = env.current ()->info ().parentCloseTime; + auto const minExpiration = closeTime + settleDelay; + NetClock::time_point const cancelAfter = closeTime + 7200s; + auto const channelFunds = XRP (1000); + env (create (alice, bob, channelFunds, settleDelay, pk, cancelAfter)); + auto const chan = channel (*env.current (), alice, bob); + BEAST_EXPECT (channelExists (*env.current (), chan)); + BEAST_EXPECT (!channelExpiration (*env.current (), chan)); + // Owner closes, will close after settleDelay + env (claim (alice, chan), txflags (tfClose)); + auto counts = []( + auto const& t) { return t.time_since_epoch ().count (); }; + BEAST_EXPECT (*channelExpiration (*env.current (), chan) == + counts (minExpiration)); + // increase the expiration time + env (fund ( + alice, chan, XRP (1), NetClock::time_point{minExpiration + 100s})); + BEAST_EXPECT (*channelExpiration (*env.current (), chan) == + counts (minExpiration) + 100); + // decrease the expiration, but still above minExpiration + env (fund ( + alice, chan, XRP (1), NetClock::time_point{minExpiration + 50s})); + BEAST_EXPECT (*channelExpiration (*env.current (), chan) == + counts (minExpiration) + 50); + // decrease the expiration below minExpiration + env (fund (alice, chan, XRP (1), + NetClock::time_point{minExpiration - 50s}), + ter (temBAD_EXPIRATION)); + BEAST_EXPECT (*channelExpiration (*env.current (), chan) == + counts (minExpiration) + 50); + env (claim (bob, chan), txflags (tfRenew), ter (tecNO_PERMISSION)); + BEAST_EXPECT (*channelExpiration (*env.current (), chan) == + counts (minExpiration) + 50); + env (claim (alice, chan), txflags (tfRenew)); + BEAST_EXPECT (!channelExpiration (*env.current (), chan)); + // decrease the expiration below minExpiration + env (fund (alice, chan, XRP (1), + NetClock::time_point{minExpiration - 50s}), + ter (temBAD_EXPIRATION)); + BEAST_EXPECT (!channelExpiration (*env.current (), chan)); + env (fund (alice, chan, XRP (1), NetClock::time_point{minExpiration})); + env.close (minExpiration); + // Try to extend the expiration after the expiration has already passed + env (fund ( + alice, chan, XRP (1), NetClock::time_point{minExpiration + 1000s})); + BEAST_EXPECT (!channelExists (*env.current (), chan)); + } + + void + testSettleDelay () + { + testcase ("settle delay"); + using namespace jtx; + using namespace std::literals::chrono_literals; + Env env (*this, features (featurePayChan)); + auto const alice = Account ("alice"); + auto const bob = Account ("bob"); + env.fund (XRP (10000), alice, bob); + auto const pk = alice.pk (); + auto const settleDelay = 3600s; + NetClock::time_point const settleTimepoint = + env.current ()->info ().parentCloseTime + settleDelay; + auto const channelFunds = XRP (1000); + env (create (alice, bob, channelFunds, settleDelay, pk)); + auto const chan = channel (*env.current (), alice, bob); + BEAST_EXPECT (channelExists (*env.current (), chan)); + // Owner closes, will close after settleDelay + env (claim (alice, chan), txflags (tfClose)); + BEAST_EXPECT (channelExists (*env.current (), chan)); + env.close (settleTimepoint-settleDelay/2); + { + // receiver can still claim + auto const chanBal = channelBalance (*env.current (), chan); + auto const chanAmt = channelAmount (*env.current (), chan); + auto preBob = env.balance (bob); + auto const delta = XRP (500); + auto const reqBal = chanBal + delta; + auto const authAmt = reqBal + XRP (100); + assert (reqBal <= chanAmt); + auto const sig = + signClaimAuth (alice.pk (), alice.sk (), chan, authAmt); + env (claim (bob, chan, reqBal, authAmt, Slice (sig), alice.pk ())); + BEAST_EXPECT (channelBalance (*env.current (), chan) == reqBal); + BEAST_EXPECT (channelAmount (*env.current (), chan) == chanAmt); + auto const feeDrops = env.current ()->fees ().base; + BEAST_EXPECT (env.balance (bob) == preBob + delta - feeDrops); + } + env.close (settleTimepoint); + { + // past settleTime, channel will close + auto const chanBal = channelBalance (*env.current (), chan); + auto const chanAmt = channelAmount (*env.current (), chan); + auto const preAlice = env.balance (alice); + auto preBob = env.balance (bob); + auto const delta = XRP (500); + auto const reqBal = chanBal + delta; + auto const authAmt = reqBal + XRP (100); + assert (reqBal <= chanAmt); + auto const sig = + signClaimAuth (alice.pk (), alice.sk (), chan, authAmt); + env (claim (bob, chan, reqBal, authAmt, Slice (sig), alice.pk ())); + BEAST_EXPECT (!channelExists (*env.current (), chan)); + auto const feeDrops = env.current ()->fees ().base; + BEAST_EXPECT (env.balance (alice) == preAlice + chanAmt - chanBal); + BEAST_EXPECT (env.balance (bob) == preBob - feeDrops); + } + } + + void + testCloseDry () + { + testcase ("close dry"); + using namespace jtx; + using namespace std::literals::chrono_literals; + Env env (*this, features (featurePayChan)); + auto const alice = Account ("alice"); + auto const bob = Account ("bob"); + env.fund (XRP (10000), alice, bob); + auto const pk = alice.pk (); + auto const settleDelay = 3600s; + auto const channelFunds = XRP (1000); + env (create (alice, bob, channelFunds, settleDelay, pk)); + auto const chan = channel (*env.current (), alice, bob); + BEAST_EXPECT (channelExists (*env.current (), chan)); + // Owner tries to close channel, but it will remain open (settle delay) + env (claim (alice, chan), txflags (tfClose)); + BEAST_EXPECT (channelExists (*env.current (), chan)); + { + // claim the entire amount + auto const preBob = env.balance (bob); + env (claim ( + alice, chan, channelFunds.value (), channelFunds.value ())); + BEAST_EXPECT (channelBalance (*env.current (), chan) == channelFunds); + BEAST_EXPECT (env.balance (bob) == preBob + channelFunds); + } + auto const preAlice = env.balance (alice); + // Channel is now dry, can close before expiration date + env (claim (alice, chan), txflags (tfClose)); + BEAST_EXPECT (!channelExists (*env.current (), chan)); + auto const feeDrops = env.current ()->fees ().base; + BEAST_EXPECT (env.balance (alice) == preAlice - feeDrops); + } + + void + testDefaultAmount () + { + // auth amount defaults to balance if not present + testcase ("default amount"); + using namespace jtx; + using namespace std::literals::chrono_literals; + Env env (*this, features (featurePayChan)); + auto const alice = Account ("alice"); + auto const bob = Account ("bob"); + env.fund (XRP (10000), alice, bob); + auto const pk = alice.pk (); + auto const settleDelay = 3600s; + auto const channelFunds = XRP (1000); + env (create (alice, bob, channelFunds, settleDelay, pk)); + auto const chan = channel (*env.current (), alice, bob); + BEAST_EXPECT (channelExists (*env.current (), chan)); + // Owner tries to close channel, but it will remain open (settle delay) + env (claim (alice, chan), txflags (tfClose)); + BEAST_EXPECT (channelExists (*env.current (), chan)); + { + auto chanBal = channelBalance (*env.current (), chan); + auto chanAmt = channelAmount (*env.current (), chan); + auto const preBob = env.balance (bob); + + auto const delta = XRP (500); + auto const reqBal = chanBal + delta; + assert (reqBal <= chanAmt); + auto const sig = + signClaimAuth (alice.pk (), alice.sk (), chan, reqBal); + env (claim ( + bob, chan, reqBal, boost::none, Slice (sig), alice.pk ())); + BEAST_EXPECT (channelBalance (*env.current (), chan) == reqBal); + auto const feeDrops = env.current ()->fees ().base; + BEAST_EXPECT (env.balance (bob) == preBob + delta - feeDrops); + chanBal = reqBal; + } + { + // Claim again + auto chanBal = channelBalance (*env.current (), chan); + auto chanAmt = channelAmount (*env.current (), chan); + auto const preBob = env.balance (bob); + + auto const delta = XRP (500); + auto const reqBal = chanBal + delta; + assert (reqBal <= chanAmt); + auto const sig = + signClaimAuth (alice.pk (), alice.sk (), chan, reqBal); + env (claim ( + bob, chan, reqBal, boost::none, Slice (sig), alice.pk ())); + BEAST_EXPECT (channelBalance (*env.current (), chan) == reqBal); + auto const feeDrops = env.current ()->fees ().base; + BEAST_EXPECT (env.balance (bob) == preBob + delta - feeDrops); + chanBal = reqBal; + } + } + + void + testDisallowXRP () + { + // auth amount defaults to balance if not present + testcase ("Disallow XRP"); + using namespace jtx; + using namespace std::literals::chrono_literals; + { + // Create a channel where dst disallows XRP + Env env (*this, features (featurePayChan)); + auto const alice = Account ("alice"); + auto const bob = Account ("bob"); + env.fund (XRP (10000), alice, bob); + env (fset (bob, asfDisallowXRP)); + auto const pk = alice.pk (); + auto const settleDelay = 3600s; + auto const channelFunds = XRP (1000); + env (create (alice, bob, channelFunds, settleDelay, pk), + ter (tecNO_TARGET)); + auto const chan = channel (*env.current (), alice, bob); + BEAST_EXPECT (!channelExists (*env.current (), chan)); + } + { + // Claim to a channel where dst disallows XRP + // (channel is created before disallow xrp is set) + Env env (*this, features (featurePayChan)); + auto const alice = Account ("alice"); + auto const bob = Account ("bob"); + env.fund (XRP (10000), alice, bob); + auto const pk = alice.pk (); + auto const settleDelay = 3600s; + auto const channelFunds = XRP (1000); + env (create (alice, bob, channelFunds, settleDelay, pk)); + auto const chan = channel (*env.current (), alice, bob); + BEAST_EXPECT (channelExists (*env.current (), chan)); + + env (fset (bob, asfDisallowXRP)); + auto const preBob = env.balance (bob); + auto const reqBal = XRP (500).value(); + env (claim (alice, chan, reqBal, reqBal), ter(tecNO_TARGET)); + } + } + + void + testDstTag () + { + // auth amount defaults to balance if not present + testcase ("Dst Tag"); + using namespace jtx; + using namespace std::literals::chrono_literals; + // Create a channel where dst disallows XRP + Env env (*this, features (featurePayChan)); + auto const alice = Account ("alice"); + auto const bob = Account ("bob"); + env.fund (XRP (10000), alice, bob); + env (fset (bob, asfRequireDest)); + auto const pk = alice.pk (); + auto const settleDelay = 3600s; + auto const channelFunds = XRP (1000); + env (create (alice, bob, channelFunds, settleDelay, pk), + ter (tecDST_TAG_NEEDED)); + BEAST_EXPECT (!channelExists ( + *env.current (), channel (*env.current (), alice, bob))); + env ( + create (alice, bob, channelFunds, settleDelay, pk, boost::none, 1)); + BEAST_EXPECT (channelExists ( + *env.current (), channel (*env.current (), alice, bob))); + } + + void + testMultiple () + { + // auth amount defaults to balance if not present + testcase ("Multiple channels to the same account"); + using namespace jtx; + using namespace std::literals::chrono_literals; + Env env (*this, features (featurePayChan)); + auto const alice = Account ("alice"); + auto const bob = Account ("bob"); + env.fund (XRP (10000), alice, bob); + auto const pk = alice.pk (); + auto const settleDelay = 3600s; + auto const channelFunds = XRP (1000); + env (create (alice, bob, channelFunds, settleDelay, pk)); + auto const chan1 = channel (*env.current (), alice, bob); + BEAST_EXPECT (channelExists (*env.current (), chan1)); + env (create (alice, bob, channelFunds, settleDelay, pk)); + auto const chan2 = channel (*env.current (), alice, bob); + BEAST_EXPECT (channelExists (*env.current (), chan2)); + BEAST_EXPECT (chan1 != chan2); + } + + void + testRPC () + { + testcase ("RPC"); + using namespace jtx; + using namespace std::literals::chrono_literals; + Env env (*this, features (featurePayChan)); + auto const alice = Account ("alice"); + auto const bob = Account ("bob"); + env.fund (XRP (10000), alice, bob); + auto const pk = alice.pk (); + auto const settleDelay = 3600s; + auto const channelFunds = XRP (1000); + env (create (alice, bob, channelFunds, settleDelay, pk)); + env.close(); + auto const chan1Str = to_string (channel (*env.current (), alice, bob)); + std::string chan1PkStr; + { + auto const r = + env.rpc ("account_channels", alice.human (), bob.human ()); + BEAST_EXPECT (r[jss::result][jss::channels].size () == 1); + BEAST_EXPECT (r[jss::result][jss::channels][0u][jss::channel_id] == chan1Str); + chan1PkStr = r[jss::result][jss::channels][0u][jss::public_key].asString(); + } + { + auto const r = + env.rpc ("account_channels", bob.human (), alice.human ()); + BEAST_EXPECT (r[jss::result][jss::channels].size () == 0); + } + env (create (alice, bob, channelFunds, settleDelay, pk)); + env.close(); + auto const chan2Str = to_string (channel (*env.current (), alice, bob)); + { + auto const r = + env.rpc ("account_channels", alice.human (), bob.human ()); + BEAST_EXPECT (r[jss::result][jss::channels].size () == 2); + BEAST_EXPECT (chan1Str != chan2Str); + for (auto const& c : {chan1Str, chan2Str}) + BEAST_EXPECT (r[jss::result][jss::channels][0u][jss::channel_id] == c || + r[jss::result][jss::channels][1u][jss::channel_id] == c); + } + { + // Verify chan1 auth + auto const rs = + env.rpc ("channel_authorize", "alice", chan1Str, "1000"); + auto const sig = rs[jss::result][jss::signature].asString (); + BEAST_EXPECT (!sig.empty ()); + auto const rv = env.rpc ( + "channel_verify", chan1PkStr, chan1Str, "1000", sig); + BEAST_EXPECT (rv[jss::result][jss::signature_verified].asBool ()); + } + { + // Try to verify chan2 auth with chan1 key + auto const rs = + env.rpc ("channel_authorize", "alice", chan2Str, "1000"); + auto const sig = rs[jss::result][jss::signature].asString (); + BEAST_EXPECT (!sig.empty ()); + auto const rv = + env.rpc ("channel_verify", chan1PkStr, chan1Str, "1000", sig); + BEAST_EXPECT (!rv[jss::result][jss::signature_verified].asBool ()); + } + } + + void + testOptionalFields () + { + testcase ("Optional Fields"); + using namespace jtx; + using namespace std::literals::chrono_literals; + Env env (*this, features (featurePayChan)); + auto const alice = Account ("alice"); + auto const bob = Account ("bob"); + auto const carol = Account ("carol"); + auto const dan = Account ("dan"); + env.fund (XRP (10000), alice, bob, carol, dan); + auto const pk = alice.pk (); + auto const settleDelay = 3600s; + auto const channelFunds = XRP (1000); + + boost::optional cancelAfter; + + { + env (create (alice, bob, channelFunds, settleDelay, pk)); + auto const chan = to_string (channel (*env.current (), alice, bob)); + auto const r = + env.rpc ("account_channels", alice.human (), bob.human ()); + BEAST_EXPECT (r[jss::result][jss::channels].size () == 1); + BEAST_EXPECT (r[jss::result][jss::channels][0u][jss::channel_id] == chan); + BEAST_EXPECT (!r[jss::result][jss::channels][0u].isMember(jss::destination_tag)); + } + { + std::uint32_t dstTag=42; + env (create ( + alice, carol, channelFunds, settleDelay, pk, cancelAfter, dstTag)); + auto const chan = to_string (channel (*env.current (), alice, carol)); + auto const r = + env.rpc ("account_channels", alice.human (), carol.human ()); + BEAST_EXPECT (r[jss::result][jss::channels].size () == 1); + BEAST_EXPECT (r[jss::result][jss::channels][0u][jss::channel_id] == chan); + BEAST_EXPECT (r[jss::result][jss::channels][0u][jss::destination_tag] == dstTag); + } + } + + void + run () override + { + testSimple (); + testCancelAfter (); + testSettleDelay (); + testExpiration (); + testCloseDry (); + testDefaultAmount (); + testDisallowXRP (); + testDstTag (); + testMultiple (); + testRPC (); + testOptionalFields (); + } +}; + +BEAST_DEFINE_TESTSUITE (PayChan, app, ripple); +} // test +} // ripple diff --git a/src/ripple/app/tx/impl/PayChan.cpp b/src/ripple/app/tx/impl/PayChan.cpp new file mode 100644 index 000000000..000a3606c --- /dev/null +++ b/src/ripple/app/tx/impl/PayChan.cpp @@ -0,0 +1,486 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include + +namespace ripple { + +/* + PaymentChannel + + Payment channels permit off-ledger checkpoints of XRP payments flowing + in a single direction. A channel sequesters the owner's XRP in its own + ledger entry. The owner can authorize the recipient to claim up to a + given balance by giving the receiver a signed message (off-ledger). The + recipient can use this signed message to claim any unpaid balance while + the channel remains open. The owner can top off the line as needed. If + the channel has not paid out all its funds, the owner must wait out a + delay to close the channel to give the recipient a chance to supply any + claims. The recipient can close the channel at any time. Any transaction + that touches the channel after the expiration time will close the + channel. The total amount paid increases monotonically as newer claims + are issued. When the channel is closed any remaining balance is returned + to the owner. Channels are intended to permit intermittent off-ledger + settlement of ILP trust lines as balances get substantial. For + bidirectional channels, a payment channel can be used in each direction. + + PaymentChannelCreate + + Create a unidirectional channel. The parameters are: + Destination + The recipient at the end of the channel. + Amount + The amount of XRP to deposit in the channel immediately. + SettleDelay + The amount of time everyone but the recipient must wait for a + superior claim. + PublicKey + The key that will sign claims against the channel. + CancelAfter (optional) + Any channel transaction that touches this channel after the + `CancelAfter` time will close it. + DestinationTag (optional) + Destination tags allow the different accounts inside of a Hosted + Wallet to be mapped back onto the Ripple ledger. The destination tag + tells the server to which account in the Hosted Wallet the funds are + intended to go to. Required if the destination has lsfRequireDestTag + set. + SourceTag (optional) + Source tags allow the different accounts inside of a Hosted Wallet + to be mapped back onto the Ripple ledger. Source tags are similar to + destination tags but are for the channel owner to identify their own + transactions. + + PaymentChannelFund + + Add additional funds to the payment channel. Only the channel owner may + use this transaction. The parameters are: + Channel + The 256-bit ID of the channel. + Amount + The amount of XRP to add. + Expiration (optional) + Time the channel closes. The transaction will fail if the expiration + times does not satisfy the SettleDelay constraints. + + PaymentChannelClaim + + Place a claim against an existing channel. The parameters are: + Channel + The 256-bit ID of the channel. + Balance (optional) + The total amount of XRP delivered after this claim is processed (optional, not + needed if just closing). + Amount (optional) + The amount of XRP the signature is for (not needed if equal to Balance or just + closing the line). + Signature (optional) + Authorization for the balance above, signed by the owner (optional, + not needed if closing or owner is performing the transaction). The + signature if for the following message: CLM\0 followed by the + 256-bit channel ID, and a 64-bit integer drops. + PublicKey (optional) + The public key that made the signature (optional, required if a signature + is present) + Flags + tfCloseChannel + Request that the channel be closed + tfRenewChannel + Request that the channel's expiration be reset. Only the owner + may renew a channel. + +*/ + +//------------------------------------------------------------------------------ + +static +TER +closeChannel ( + std::shared_ptr const& slep, + ApplyView& view, + uint256 const& key, + beast::Journal j) +{ + AccountID const src = (*slep)[sfAccount]; + // Remove PayChan from owner directory + { + auto const page = (*slep)[sfOwnerNode]; + TER const ter = dirDelete (view, true, page, keylet::ownerDir (src).key, + key, false, page == 0, j); + if (!isTesSuccess (ter)) + return ter; + } + + // Transfer amount back to owner, decrement owner count + auto const sle = view.peek (keylet::account (src)); + assert ((*slep)[sfAmount] >= (*slep)[sfBalance]); + (*sle)[sfBalance] = + (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance]; + (*sle)[sfOwnerCount] = (*sle)[sfOwnerCount] - 1; + view.update (sle); + + // Remove PayChan from ledger + view.erase (slep); + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + +TER +PayChanCreate::preflight (PreflightContext const& ctx) +{ + if (!ctx.rules.enabled (featurePayChan, ctx.app.config ().features)) + return temDISABLED; + + auto const ret = preflight1 (ctx); + if (!isTesSuccess (ret)) + return ret; + + if (!isXRP (ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero)) + return temBAD_AMOUNT; + + if (ctx.tx[sfAccount] == ctx.tx[sfDestination]) + return temDST_IS_SRC; + + return preflight2 (ctx); +} + +TER +PayChanCreate::preclaim(PreclaimContext const &ctx) +{ + auto const account = ctx.tx[sfAccount]; + auto const sle = ctx.view.read (keylet::account (account)); + + // Check reserve and funds availability + { + auto const balance = (*sle)[sfBalance]; + auto const reserve = + ctx.view.fees ().accountReserve ((*sle)[sfOwnerCount] + 1); + + if (balance < reserve) + return tecINSUFFICIENT_RESERVE; + + if (balance < reserve + ctx.tx[sfAmount]) + return tecUNFUNDED; + } + + auto const dst = ctx.tx[sfDestination]; + + { + // Check destination account + auto const sled = ctx.view.read (keylet::account (dst)); + if (!sled) + return tecNO_DST; + if (((*sled)[sfFlags] & lsfRequireDestTag) && + !ctx.tx[~sfDestinationTag]) + return tecDST_TAG_NEEDED; + if ((*sled)[sfFlags] & lsfDisallowXRP) + return tecNO_TARGET; + } + + return tesSUCCESS; +} + +TER +PayChanCreate::doApply() +{ + auto const account = ctx_.tx[sfAccount]; + auto const sle = ctx_.view ().peek (keylet::account (account)); + auto const dst = ctx_.tx[sfDestination]; + + // Create PayChan in ledger + auto const slep = std::make_shared ( + keylet::payChan (account, dst, (*sle)[sfSequence] - 1)); + // Funds held in this channel + (*slep)[sfAmount] = ctx_.tx[sfAmount]; + // Amount channel has already paid + (*slep)[sfBalance] = ctx_.tx[sfAmount].zeroed (); + (*slep)[sfAccount] = account; + (*slep)[sfDestination] = dst; + (*slep)[sfSettleDelay] = ctx_.tx[sfSettleDelay]; + (*slep)[sfPublicKey] = ctx_.tx[sfPublicKey]; + (*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter]; + (*slep)[~sfSourceTag] = ctx_.tx[~sfSourceTag]; + (*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag]; + + ctx_.view ().insert (slep); + + // Add PayChan to owner directory + { + uint64_t page; + auto result = dirAdd (ctx_.view (), page, keylet::ownerDir (account), + slep->key (), describeOwnerDir (account), + ctx_.app.journal ("View")); + if (!isTesSuccess (result.first)) + return result.first; + (*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 +PayChanFund::preflight (PreflightContext const& ctx) +{ + if (!ctx.rules.enabled (featurePayChan, ctx.app.config ().features)) + return temDISABLED; + + auto const ret = preflight1 (ctx); + if (!isTesSuccess (ret)) + return ret; + + if (!isXRP (ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero)) + return temBAD_AMOUNT; + + return preflight2 (ctx); +} + +TER +PayChanFund::doApply() +{ + Keylet const k (ltPAYCHAN, ctx_.tx[sfPayChannel]); + auto const slep = ctx_.view ().peek (k); + if (!slep) + return tecNO_ENTRY; + + AccountID const src = (*slep)[sfAccount]; + auto const txAccount = ctx_.tx[sfAccount]; + auto const expiration = (*slep)[~sfExpiration]; + + { + auto const cancelAfter = (*slep)[~sfCancelAfter]; + auto const closeTime = + ctx_.view ().info ().parentCloseTime.time_since_epoch ().count (); + if ((cancelAfter && closeTime >= *cancelAfter) || + (expiration && closeTime >= *expiration)) + return closeChannel ( + slep, ctx_.view (), k.key, ctx_.app.journal ("View")); + } + + if (src != txAccount) + // only the owner can add funds or extend + return tecNO_PERMISSION; + + if (auto extend = ctx_.tx[~sfExpiration]) + { + auto minExpiration = + ctx_.view ().info ().parentCloseTime.time_since_epoch ().count () + + (*slep)[sfSettleDelay]; + if (expiration && *expiration < minExpiration) + minExpiration = *expiration; + + if (*extend < minExpiration) + return temBAD_EXPIRATION; + (*slep)[~sfExpiration] = *extend; + ctx_.view ().update (slep); + } + + auto const sle = ctx_.view ().peek (keylet::account (txAccount)); + + { + // Check reserve and funds availability + auto const balance = (*sle)[sfBalance]; + auto const reserve = + ctx_.view ().fees ().accountReserve ((*sle)[sfOwnerCount]); + + if (balance < reserve) + return tecINSUFFICIENT_RESERVE; + + if (balance < reserve + ctx_.tx[sfAmount]) + return tecUNFUNDED; + } + + (*slep)[sfAmount] = (*slep)[sfAmount] + ctx_.tx[sfAmount]; + ctx_.view ().update (slep); + + (*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount]; + ctx_.view ().update (sle); + + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + +TER +PayChanClaim::preflight (PreflightContext const& ctx) +{ + if (! ctx.rules.enabled(featurePayChan, + ctx.app.config().features)) + return temDISABLED; + + auto const ret = preflight1 (ctx); + if (!isTesSuccess (ret)) + return ret; + + auto const bal = ctx.tx[~sfBalance]; + if (bal && (!isXRP (*bal) || *bal <= beast::zero)) + return temBAD_AMOUNT; + + auto const amt = ctx.tx[~sfAmount]; + if (amt && (!isXRP (*amt) || *amt <= beast::zero)) + return temBAD_AMOUNT; + + if (bal && amt && *bal > *amt) + return tecNO_PERMISSION; + + auto const flags = ctx.tx.getFlags (); + if ((flags & tfClose) && (flags & tfRenew)) + return temMALFORMED; + + if (auto const sig = ctx.tx[~sfSignature]) + { + if (!(ctx.tx[~sfPublicKey] && bal)) + return temMALFORMED; + + // Check the signature + // The signature isn't needed if txAccount == src, but if it's + // present, check it + + auto const reqBalance = bal->xrp (); + auto const authAmt = amt ? amt->xrp() : reqBalance; + + if (reqBalance > authAmt) + return tecNO_PERMISSION; + + Keylet const k (ltPAYCHAN, ctx.tx[sfPayChannel]); + PublicKey const pk (ctx.tx[sfPublicKey]); + Serializer msg; + serializePayChanAuthorization (msg, k.key, authAmt); + if (!verify (pk, msg.slice (), *sig, /*canonical*/ true)) + return temBAD_SIGNATURE; + } + + return preflight2 (ctx); +} + +TER +PayChanClaim::doApply() +{ + Keylet const k (ltPAYCHAN, ctx_.tx[sfPayChannel]); + auto const slep = ctx_.view ().peek (k); + if (!slep) + return tecNO_TARGET; + + AccountID const src = (*slep)[sfAccount]; + AccountID const dst = (*slep)[sfDestination]; + AccountID const txAccount = ctx_.tx[sfAccount]; + + auto const curExpiration = (*slep)[~sfExpiration]; + { + auto const cancelAfter = (*slep)[~sfCancelAfter]; + auto const closeTime = + ctx_.view ().info ().parentCloseTime.time_since_epoch ().count (); + if ((cancelAfter && closeTime >= *cancelAfter) || + (curExpiration && closeTime >= *curExpiration)) + return closeChannel ( + slep, ctx_.view (), k.key, ctx_.app.journal ("View")); + } + + if (txAccount != src && txAccount != dst) + return tecNO_PERMISSION; + + if (ctx_.tx[~sfBalance]) + { + auto const chanBalance = slep->getFieldAmount (sfBalance).xrp (); + auto const chanFunds = slep->getFieldAmount (sfAmount).xrp (); + auto const reqBalance = ctx_.tx[sfBalance].xrp (); + + if (txAccount == dst && !ctx_.tx[~sfSignature]) + return temBAD_SIGNATURE; + + if (ctx_.tx[~sfSignature]) + { + PublicKey const pk ((*slep)[sfPublicKey]); + if (ctx_.tx[sfPublicKey] != pk) + return temBAD_SIGNER; + } + + if (reqBalance > chanFunds) + return tecUNFUNDED_PAYMENT; + + if (reqBalance <= chanBalance) + // nothing requested + return tecUNFUNDED_PAYMENT; + + auto const sled = ctx_.view ().peek (keylet::account (dst)); + if (!sled) + return terNO_ACCOUNT; + + if (txAccount == src && ((*sled)[sfFlags] & lsfDisallowXRP)) + return tecNO_TARGET; + + (*slep)[sfBalance] = ctx_.tx[sfBalance]; + XRPAmount const reqDelta = reqBalance - chanBalance; + assert (reqDelta >= beast::zero); + (*sled)[sfBalance] = (*sled)[sfBalance] + reqDelta; + ctx_.view ().update (sled); + ctx_.view ().update (slep); + } + + if (ctx_.tx.getFlags () & tfRenew) + { + if (src != txAccount) + return tecNO_PERMISSION; + (*slep)[~sfExpiration] = boost::none; + ctx_.view ().update (slep); + } + + if (ctx_.tx.getFlags () & tfClose) + { + // Channel will close immediately if dry or the receiver closes + if (dst == txAccount || (*slep)[sfBalance] == (*slep)[sfAmount]) + return closeChannel ( + slep, ctx_.view (), k.key, ctx_.app.journal ("View")); + + auto const settleExpiration = + ctx_.view ().info ().parentCloseTime.time_since_epoch ().count () + + (*slep)[sfSettleDelay]; + + if (!curExpiration || *curExpiration > settleExpiration) + { + (*slep)[~sfExpiration] = settleExpiration; + ctx_.view ().update (slep); + } + } + + return tesSUCCESS; +} + +} // ripple + diff --git a/src/ripple/app/tx/impl/PayChan.h b/src/ripple/app/tx/impl/PayChan.h new file mode 100644 index 000000000..d04687ac3 --- /dev/null +++ b/src/ripple/app/tx/impl/PayChan.h @@ -0,0 +1,91 @@ +//------------------------------------------------------------------------------ +/* + 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_PAYCHAN_H_INCLUDED +#define RIPPLE_TX_PAYCHAN_H_INCLUDED + +#include + +namespace ripple { + +class PayChanCreate + : public Transactor +{ +public: + explicit + PayChanCreate (ApplyContext& ctx) + : Transactor(ctx) + { + } + + static + TER + preflight (PreflightContext const& ctx); + + static + TER + preclaim(PreclaimContext const &ctx); + + TER + doApply() override; +}; + +//------------------------------------------------------------------------------ + +class PayChanFund + : public Transactor +{ +public: + explicit + PayChanFund (ApplyContext& ctx) + : Transactor(ctx) + { + } + + static + TER + preflight (PreflightContext const& ctx); + + TER + doApply() override; +}; + +//------------------------------------------------------------------------------ + +class PayChanClaim + : public Transactor +{ +public: + explicit + PayChanClaim (ApplyContext& ctx) + : Transactor(ctx) + { + } + + static + TER + preflight (PreflightContext const& ctx); + + TER + doApply() override; +}; + +} // ripple + +#endif diff --git a/src/ripple/app/tx/impl/applySteps.cpp b/src/ripple/app/tx/impl/applySteps.cpp index 3885d4cf0..9a1e98d49 100644 --- a/src/ripple/app/tx/impl/applySteps.cpp +++ b/src/ripple/app/tx/impl/applySteps.cpp @@ -31,6 +31,7 @@ #include #include #include +#include namespace ripple { @@ -54,6 +55,9 @@ invoke_preflight (PreflightContext const& ctx) case ttTRUST_SET: return SetTrust ::preflight(ctx); case ttAMENDMENT: case ttFEE: return Change ::preflight(ctx); + case ttPAYCHAN_CREATE: return PayChanCreate ::preflight(ctx); + case ttPAYCHAN_FUND: return PayChanFund ::preflight(ctx); + case ttPAYCHAN_CLAIM: return PayChanClaim ::preflight(ctx); default: assert(false); return temUNKNOWN; @@ -124,6 +128,9 @@ invoke_preclaim (PreclaimContext const& ctx) case ttTRUST_SET: return invoke_preclaim(ctx); case ttAMENDMENT: case ttFEE: return invoke_preclaim(ctx); + case ttPAYCHAN_CREATE: return invoke_preclaim(ctx); + case ttPAYCHAN_FUND: return invoke_preclaim(ctx); + case ttPAYCHAN_CLAIM: return invoke_preclaim(ctx); default: assert(false); return { temUNKNOWN, 0 }; @@ -150,6 +157,9 @@ invoke_calculateBaseFee(PreclaimContext const& ctx) case ttTRUST_SET: return SetTrust::calculateBaseFee(ctx); case ttAMENDMENT: case ttFEE: return Change::calculateBaseFee(ctx); + case ttPAYCHAN_CREATE: return PayChanCreate::calculateBaseFee(ctx); + case ttPAYCHAN_FUND: return PayChanFund::calculateBaseFee(ctx); + case ttPAYCHAN_CLAIM: return PayChanClaim::calculateBaseFee(ctx); default: assert(false); return 0; @@ -187,6 +197,9 @@ invoke_calculateConsequences(STTx const& tx) case ttTICKET_CANCEL: return invoke_calculateConsequences(tx); case ttTICKET_CREATE: return invoke_calculateConsequences(tx); case ttTRUST_SET: return invoke_calculateConsequences(tx); + case ttPAYCHAN_CREATE: return invoke_calculateConsequences(tx); + case ttPAYCHAN_FUND: return invoke_calculateConsequences(tx); + case ttPAYCHAN_CLAIM: return invoke_calculateConsequences(tx); case ttAMENDMENT: case ttFEE: // fall through to default @@ -217,6 +230,9 @@ invoke_apply (ApplyContext& ctx) case ttTRUST_SET: { SetTrust p(ctx); return p(); } case ttAMENDMENT: case ttFEE: { Change p(ctx); return p(); } + case ttPAYCHAN_CREATE: { PayChanCreate p(ctx); return p(); } + case ttPAYCHAN_FUND: { PayChanFund p(ctx); return p(); } + case ttPAYCHAN_CLAIM: { PayChanClaim p(ctx); return p(); } default: assert(false); return { temUNKNOWN, false }; diff --git a/src/ripple/ledger/impl/ApplyStateTable.cpp b/src/ripple/ledger/impl/ApplyStateTable.cpp index 9687b672c..ee0b46f86 100644 --- a/src/ripple/ledger/impl/ApplyStateTable.cpp +++ b/src/ripple/ledger/impl/ApplyStateTable.cpp @@ -629,6 +629,12 @@ ApplyStateTable::threadOwners (ReadView const& base, threadTx (base, meta, (*sle)[sfDestination], mods, j); break; } + case ltPAYCHAN: + { + threadTx (base, meta, (*sle)[sfAccount], mods, j); + threadTx (base, meta, (*sle)[sfDestination], mods, j); + break; + } case ltRIPPLE_STATE: { threadTx (base, meta, (*sle)[sfLowLimit].getIssuer(), mods, j); diff --git a/src/ripple/net/impl/RPCCall.cpp b/src/ripple/net/impl/RPCCall.cpp index 1c872732b..d8be72d76 100644 --- a/src/ripple/net/impl/RPCCall.cpp +++ b/src/ripple/net/impl/RPCCall.cpp @@ -583,6 +583,73 @@ private: return parseAccountRaw2 (jvParams, "peer"); } + // account_channels |"" [] + Json::Value parseAccountChannels (Json::Value const& jvParams) + { + return parseAccountRaw2 (jvParams, jss::destination_account); + } + + // channel_authorize + Json::Value parseChannelAuthorize (Json::Value const& jvParams) + { + Json::Value jvRequest (Json::objectValue); + + jvRequest[jss::secret] = jvParams[0u]; + { + // verify the channel id is a valid 256 bit number + uint256 channelId; + if (!channelId.SetHexExact (jvParams[1u].asString ())) + return rpcError (rpcCHANNEL_MALFORMED); + } + jvRequest[jss::channel_id] = jvParams[1u].asString (); + + try + { + auto const drops = std::stoul (jvParams[2u].asString ()); + (void)drops; // just used for error checking + jvRequest[jss::amount] = jvParams[2u]; + } + catch (std::exception const&) + { + return rpcError (rpcCHANNEL_AMT_MALFORMED); + } + + return jvRequest; + } + + // channel_verify + Json::Value parseChannelVerify (Json::Value const& jvParams) + { + std::string const strPk = jvParams[0u].asString (); + + if (!parseBase58 (TokenType::TOKEN_ACCOUNT_PUBLIC, strPk)) + return rpcError (rpcPUBLIC_MALFORMED); + + Json::Value jvRequest (Json::objectValue); + + jvRequest[jss::public_key] = strPk; + { + // verify the channel id is a valid 256 bit number + uint256 channelId; + if (!channelId.SetHexExact (jvParams[1u].asString ())) + return rpcError (rpcCHANNEL_MALFORMED); + } + jvRequest[jss::channel_id] = jvParams[1u].asString (); + try + { + auto const drops = std::stoul (jvParams[2u].asString ()); + (void)drops; // just used for error checking + jvRequest[jss::amount] = jvParams[2u]; + } + catch (std::exception const&) + { + return rpcError (rpcCHANNEL_AMT_MALFORMED); + } + jvRequest[jss::signature] = jvParams[3u].asString (); + + return jvRequest; + } + Json::Value parseAccountRaw2 (Json::Value const& jvParams, char const * const acc2Field) { @@ -916,11 +983,14 @@ public: { "account_currencies", &RPCParser::parseAccountCurrencies, 1, 2 }, { "account_info", &RPCParser::parseAccountItems, 1, 2 }, { "account_lines", &RPCParser::parseAccountLines, 1, 5 }, + { "account_channels", &RPCParser::parseAccountChannels, 1, 3 }, { "account_objects", &RPCParser::parseAccountItems, 1, 5 }, { "account_offers", &RPCParser::parseAccountItems, 1, 4 }, { "account_tx", &RPCParser::parseAccountTransactions, 1, 8 }, { "book_offers", &RPCParser::parseBookOffers, 2, 7 }, { "can_delete", &RPCParser::parseCanDelete, 0, 1 }, + { "channel_authorize", &RPCParser::parseChannelAuthorize, 3, 3 }, + { "channel_verify", &RPCParser::parseChannelVerify, 4, 4 }, { "connect", &RPCParser::parseConnect, 1, 2 }, { "consensus_info", &RPCParser::parseAsIs, 0, 0 }, { "feature", &RPCParser::parseFeature, 0, 2 }, diff --git a/src/ripple/protocol/ErrorCodes.h b/src/ripple/protocol/ErrorCodes.h index 0034cb81b..e9e31f3ac 100644 --- a/src/ripple/protocol/ErrorCodes.h +++ b/src/ripple/protocol/ErrorCodes.h @@ -87,6 +87,8 @@ enum error_code_i rpcBAD_MARKET, rpcBAD_SECRET, rpcBAD_SEED, + rpcCHANNEL_MALFORMED, + rpcCHANNEL_AMT_MALFORMED, rpcCOMMAND_MISSING, rpcDST_ACT_MALFORMED, rpcDST_ACT_MISSING, diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 33ec8ef5b..607fe1f1c 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -43,6 +43,7 @@ extern uint256 const featureFlowV2; extern uint256 const featureOwnerPaysFee; extern uint256 const featureCompareFlowV1V2; extern uint256 const featureSHAMapV2; +extern uint256 const featurePayChan; } // ripple diff --git a/src/ripple/protocol/HashPrefix.h b/src/ripple/protocol/HashPrefix.h index 814256d99..0e0c8f6d7 100644 --- a/src/ripple/protocol/HashPrefix.h +++ b/src/ripple/protocol/HashPrefix.h @@ -99,6 +99,9 @@ public: /** Manifest */ static HashPrefix const manifest; + + /** Payment Channel Claim */ + static HashPrefix const paymentChannelClaim; }; template diff --git a/src/ripple/protocol/Indexes.h b/src/ripple/protocol/Indexes.h index b1dd42b9a..b82d9850f 100644 --- a/src/ripple/protocol/Indexes.h +++ b/src/ripple/protocol/Indexes.h @@ -238,6 +238,10 @@ Keylet page (uint256 const& key) Keylet susPay (AccountID const& source, std::uint32_t seq); +/** A PaymentChannel */ +Keylet +payChan (AccountID const& source, AccountID const& dst, std::uint32_t seq); + } // keylet } diff --git a/src/ripple/protocol/JsonFields.h b/src/ripple/protocol/JsonFields.h index 9aa4c5763..cde457a0f 100644 --- a/src/ripple/protocol/JsonFields.h +++ b/src/ripple/protocol/JsonFields.h @@ -50,6 +50,7 @@ JSS ( Paths ); // in/out: TransactionSign JSS ( TransferRate ); // in: TransferRate JSS ( historical_perminute ); // historical_perminute JSS ( SLE_hit_rate ); // out: GetCounts +JSS ( SettleDelay ); // in: TransactionSign JSS ( SendMax ); // in: TransactionSign JSS ( Sequence ); // in/out: TransactionSign; field. JSS ( SetFlag ); // field. @@ -80,6 +81,7 @@ JSS ( age ); // out: NetworkOPs, Peers JSS ( alternatives ); // out: PathRequest, RipplePathFind JSS ( amendment_blocked ); // out: NetworkOPs JSS ( amendments ); // in: AccountObjects, out: NetworkOPs +JSS ( amount ); // out: AccountChannels JSS ( asks ); // out: Subscribe JSS ( assets ); // out: GatewayBalances JSS ( authorized ); // out: AccountLines @@ -98,7 +100,10 @@ JSS ( both ); // in: Subscribe, Unsubscribe JSS ( both_sides ); // in: Subscribe, Unsubscribe JSS ( build_path ); // in: TransactionSign JSS ( build_version ); // out: NetworkOPs +JSS ( cancel_after ); // out: AccountChannels JSS ( can_delete ); // out: CanDelete +JSS ( channel_id ); // out: AccountChannels +JSS ( channels ); // out: AccountChannels JSS ( check_nodes ); // in: LedgerCleaner JSS ( clear ); // in/out: FetchInfo JSS ( close_flags ); // out: LedgerToJson @@ -135,10 +140,12 @@ JSS ( debug_signing ); // in: TransactionSign JSS ( delivered_amount ); // out: addPaymentDeliveredAmount JSS ( deprecated ); // out: WalletSeed JSS ( descending ); // in: AccountTx* -JSS ( destination_account ); // in: PathRequest, RipplePathFind +JSS ( destination_account ); // in: PathRequest, RipplePathFind, account_lines + // out: AccountChannels JSS ( destination_amount ); // in: PathRequest, RipplePathFind JSS ( destination_currencies ); // in: PathRequest, RipplePathFind JSS ( destination_tag ); // in: PathRequest + // out: AccountChannels JSS ( dir_entry ); // out: DirectoryEntryIterator JSS ( dir_index ); // out: DirectoryEntryIterator JSS ( dir_root ); // out: DirectoryEntryIterator @@ -156,7 +163,7 @@ JSS ( error_exception ); // out: Submit JSS ( error_message ); // out: error JSS ( expand ); // in: handler/Ledger JSS ( expected_ledger_size ); // out: TxQ -JSS ( expiration ); // out: AccountOffers +JSS ( expiration ); // out: AccountOffers, AccountChannels JSS ( fail_hard ); // in: Sign, Submit JSS ( failed ); // out: InboundLedger JSS ( feature ); // in: Feature @@ -347,7 +354,8 @@ JSS ( rt_accounts ); // in: Subscribe, Unsubscribe JSS ( sanity ); // out: PeerImp JSS ( search_depth ); // in: RipplePathFind JSS ( secret ); // in: TransactionSign, WalletSeed, - // ValidationCreate, ValidationSeed + // ValidationCreate, ValidationSeed, + // channel_authorize JSS ( seed ); // in: WalletAccounts, out: WalletSeed JSS ( seed_hex ); // in: WalletPropose, TransactionSign JSS ( send_currencies ); // out: AccountCurrencies @@ -357,8 +365,10 @@ JSS ( seq ); // in: LedgerEntry; JSS ( seqNum ); // out: LedgerToJson JSS ( server_state ); // out: NetworkOPs JSS ( server_status ); // out: NetworkOPs +JSS ( settle_delay ); // out: AccountChannels JSS ( severity ); // in: LogLevel -JSS ( signature ); // out: NetworkOPs +JSS ( signature ); // out: NetworkOPs, ChannelAuthorize +JSS ( signature_verified ); // out: ChannelVerify JSS ( signing_key ); // out: NetworkOPs JSS ( signing_time ); // out: NetworkOPs JSS ( signer_list ); // in: AccountObjects @@ -367,6 +377,7 @@ JSS ( snapshot ); // in: Subscribe JSS ( source_account ); // in: PathRequest, RipplePathFind JSS ( source_amount ); // in: PathRequest, RipplePathFind JSS ( source_currencies ); // in: PathRequest, RipplePathFind +JSS ( source_tag ); // out: AccountChannels JSS ( stand_alone ); // out: NetworkOPs JSS ( start ); // in: TxHistory JSS ( state ); // out: Logic.h, ServerState, LedgerData diff --git a/src/ripple/protocol/LedgerFormats.h b/src/ripple/protocol/LedgerFormats.h index 49e57e612..856c90ad1 100644 --- a/src/ripple/protocol/LedgerFormats.h +++ b/src/ripple/protocol/LedgerFormats.h @@ -80,6 +80,9 @@ enum LedgerEntryType ltSUSPAY = 'u', + // Simple unidirection xrp channel + ltPAYCHAN = 'x', + // No longer used or supported. Left here to prevent accidental // reassignment of the ledger type. ltNICKNAME = 'n', @@ -107,6 +110,7 @@ enum LedgerNameSpace spaceFee = 'e', spaceTicket = 'T', spaceSignerList = 'S', + spaceXRPUChannel = 'x', // No longer used or supported. Left here to reserve the space and // avoid accidental reuse of the space. diff --git a/src/ripple/protocol/PayChan.h b/src/ripple/protocol/PayChan.h new file mode 100644 index 000000000..90352b8a8 --- /dev/null +++ b/src/ripple/protocol/PayChan.h @@ -0,0 +1,44 @@ +//------------------------------------------------------------------------------ +/* + 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_PROTOCOL_PAYCHAN_H_INCLUDED +#define RIPPLE_PROTOCOL_PAYCHAN_H_INCLUDED + +#include +#include +#include +#include + +namespace ripple { + +inline +void +serializePayChanAuthorization ( + Serializer& msg, + uint256 const& key, + XRPAmount const& amt) +{ + msg.add32 (HashPrefix::paymentChannelClaim); + msg.add256 (key); + msg.add64 (amt.drops ()); +} + +} // ripple + +#endif diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index c31c1fb9b..802d7cd67 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -375,6 +375,7 @@ extern SF_U32 const sfSignerQuorum; extern SF_U32 const sfCancelAfter; extern SF_U32 const sfFinishAfter; extern SF_U32 const sfSignerListID; +extern SF_U32 const sfSettleDelay; // 64-bit integers extern SF_U64 const sfIndexNext; @@ -413,6 +414,7 @@ extern SF_U256 const sfNickname; extern SF_U256 const sfAmendment; extern SF_U256 const sfTicketID; extern SF_U256 const sfDigest; +extern SF_U256 const sfPayChannel; // currency amount (common) extern SF_Amount const sfAmount; diff --git a/src/ripple/protocol/TxFlags.h b/src/ripple/protocol/TxFlags.h index bd67b15ac..d2b2eeaf7 100644 --- a/src/ripple/protocol/TxFlags.h +++ b/src/ripple/protocol/TxFlags.h @@ -95,6 +95,10 @@ const std::uint32_t tfTrustSetMask = ~ (tfUniversal | tfSetfAuth | tfSet const std::uint32_t tfGotMajority = 0x00010000; const std::uint32_t tfLostMajority = 0x00020000; +// PaymentChannel flags: +const std::uint32_t tfRenew = 0x00010000; +const std::uint32_t tfClose = 0x00020000; + } // ripple #endif diff --git a/src/ripple/protocol/TxFormats.h b/src/ripple/protocol/TxFormats.h index 74dccc0f4..36b23c049 100644 --- a/src/ripple/protocol/TxFormats.h +++ b/src/ripple/protocol/TxFormats.h @@ -47,6 +47,9 @@ enum TxType ttTICKET_CREATE = 10, ttTICKET_CANCEL = 11, ttSIGNER_LIST_SET = 12, + ttPAYCHAN_CREATE = 13, + ttPAYCHAN_FUND = 14, + ttPAYCHAN_CLAIM = 15, ttTRUST_SET = 20, diff --git a/src/ripple/protocol/impl/ErrorCodes.cpp b/src/ripple/protocol/impl/ErrorCodes.cpp index aebab6f0e..4a9253872 100644 --- a/src/ripple/protocol/impl/ErrorCodes.cpp +++ b/src/ripple/protocol/impl/ErrorCodes.cpp @@ -61,6 +61,8 @@ public: add (rpcBAD_SECRET, "badSecret", "Secret does not match account."); add (rpcBAD_SEED, "badSeed", "Disallowed seed."); add (rpcBAD_SYNTAX, "badSyntax", "Syntax error."); + add (rpcCHANNEL_MALFORMED, "channelMalformed", "Payment channel is malformed."); + add (rpcCHANNEL_AMT_MALFORMED, "channelAmtMalformed","Payment channel amount is malformed."); add (rpcCOMMAND_MISSING, "commandMissing", "Missing command entry."); add (rpcDST_ACT_MALFORMED, "dstActMalformed", "Destination account is malformed."); add (rpcDST_ACT_MISSING, "dstActMissing", "Destination account does not exist."); diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index b43e9edf6..b2e5d5f18 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -54,5 +54,6 @@ uint256 const featureFlowV2 = feature("FlowV2"); uint256 const featureOwnerPaysFee = feature("OwnerPaysFee"); uint256 const featureCompareFlowV1V2 = feature("CompareFlowV1V2"); uint256 const featureSHAMapV2 = feature("SHAMapV2"); +uint256 const featurePayChan = feature("PayChan"); } // ripple diff --git a/src/ripple/protocol/impl/HashPrefix.cpp b/src/ripple/protocol/impl/HashPrefix.cpp index e4c42fff1..2f33bda0c 100644 --- a/src/ripple/protocol/impl/HashPrefix.cpp +++ b/src/ripple/protocol/impl/HashPrefix.cpp @@ -36,5 +36,6 @@ HashPrefix const HashPrefix::txMultiSign ('S', 'M', 'T'); HashPrefix const HashPrefix::validation ('V', 'A', 'L'); HashPrefix const HashPrefix::proposal ('P', 'R', 'P'); HashPrefix const HashPrefix::manifest ('M', 'A', 'N'); +HashPrefix const HashPrefix::paymentChannelClaim ('C', 'L', 'M'); } // ripple diff --git a/src/ripple/protocol/impl/Indexes.cpp b/src/ripple/protocol/impl/Indexes.cpp index a10071c97..169d6e578 100644 --- a/src/ripple/protocol/impl/Indexes.cpp +++ b/src/ripple/protocol/impl/Indexes.cpp @@ -323,6 +323,18 @@ susPay (AccountID const& source, std::uint32_t seq) return { ltSUSPAY, static_cast(h) }; } +Keylet +payChan (AccountID const& source, AccountID const& dst, std::uint32_t seq) +{ + sha512_half_hasher h; + using beast::hash_append; + hash_append(h, spaceXRPUChannel); + hash_append(h, source); + hash_append(h, dst); + hash_append(h, seq); + return { ltPAYCHAN, static_cast(h) }; +} + } // keylet } // ripple diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index 9a1e65b27..062e40ac1 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -130,6 +130,22 @@ LedgerFormats::LedgerFormats () << SOElement (sfPreviousTxnID, SOE_REQUIRED) << SOElement (sfPreviousTxnLgrSeq, SOE_REQUIRED) ; + + add ("PayChannel", ltPAYCHAN) + << SOElement (sfAccount, SOE_REQUIRED) + << SOElement (sfDestination, SOE_REQUIRED) + << SOElement (sfAmount, SOE_REQUIRED) + << SOElement (sfBalance, SOE_REQUIRED) + << SOElement (sfPublicKey, SOE_REQUIRED) + << SOElement (sfSettleDelay, SOE_REQUIRED) + << SOElement (sfExpiration, SOE_OPTIONAL) + << SOElement (sfCancelAfter, SOE_OPTIONAL) + << SOElement (sfSourceTag, SOE_OPTIONAL) + << SOElement (sfDestinationTag, SOE_OPTIONAL) + << SOElement (sfOwnerNode, SOE_REQUIRED) + << SOElement (sfPreviousTxnID, SOE_REQUIRED) + << SOElement (sfPreviousTxnLgrSeq, SOE_REQUIRED) + ; } void LedgerFormats::addCommonFields (Item& item) diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index 44a809c54..2983da0ca 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -126,6 +126,7 @@ SF_U32 const sfSignerQuorum = make::one(&sfSignerQuorum, SF_U32 const sfCancelAfter = make::one(&sfCancelAfter, STI_UINT32, 36, "CancelAfter"); SF_U32 const sfFinishAfter = make::one(&sfFinishAfter, STI_UINT32, 37, "FinishAfter"); SF_U32 const sfSignerListID = make::one(&sfSignerListID, STI_UINT32, 38, "SignerListID"); +SF_U32 const sfSettleDelay = make::one(&sfSettleDelay, STI_UINT32, 39, "SettleDelay"); // 64-bit integers SF_U64 const sfIndexNext = make::one(&sfIndexNext, STI_UINT64, 1, "IndexNext"); @@ -164,6 +165,7 @@ SF_U256 const sfNickname = make::one(&sfNickname, STI_H SF_U256 const sfAmendment = make::one(&sfAmendment, STI_HASH256, 19, "Amendment"); SF_U256 const sfTicketID = make::one(&sfTicketID, STI_HASH256, 20, "TicketID"); SF_U256 const sfDigest = make::one(&sfDigest, STI_HASH256, 21, "Digest"); +SF_U256 const sfPayChannel = make::one(&sfPayChannel, STI_HASH256, 22, "Channel"); // currency amount (common) SF_Amount const sfAmount = make::one(&sfAmount, STI_AMOUNT, 1, "Amount"); diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index 85e213b83..2263d660e 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -113,6 +113,26 @@ TxFormats::TxFormats () << SOElement (sfSignerQuorum, SOE_REQUIRED) << SOElement (sfSignerEntries, SOE_OPTIONAL) ; + + add ("PaymentChannelCreate", ttPAYCHAN_CREATE) << + SOElement (sfDestination, SOE_REQUIRED) << + SOElement (sfAmount, SOE_REQUIRED) << + SOElement (sfSettleDelay, SOE_REQUIRED) << + SOElement (sfPublicKey, SOE_REQUIRED) << + SOElement (sfCancelAfter, SOE_OPTIONAL) << + SOElement (sfDestinationTag, SOE_OPTIONAL); + + add ("PaymentChannelFund", ttPAYCHAN_FUND) << + SOElement (sfPayChannel, SOE_REQUIRED) << + SOElement (sfAmount, SOE_REQUIRED) << + SOElement (sfExpiration, SOE_OPTIONAL); + + add ("PaymentChannelClaim", ttPAYCHAN_CLAIM) << + SOElement (sfPayChannel, SOE_REQUIRED) << + SOElement (sfAmount, SOE_OPTIONAL) << + SOElement (sfBalance, SOE_OPTIONAL) << + SOElement (sfSignature, SOE_OPTIONAL) << + SOElement (sfPublicKey, SOE_OPTIONAL); } void TxFormats::addCommonFields (Item& item) diff --git a/src/ripple/rpc/handlers/AccountChannels.cpp b/src/ripple/rpc/handlers/AccountChannels.cpp new file mode 100644 index 000000000..6a82aa00f --- /dev/null +++ b/src/ripple/rpc/handlers/AccountChannels.cpp @@ -0,0 +1,184 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2014 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 +#include +#include +namespace ripple { + +void addChannel (Json::Value& jsonLines, SLE const& line) +{ + Json::Value& jDst (jsonLines.append (Json::objectValue)); + jDst[jss::channel_id] = to_string (line.key ()); + jDst[jss::account] = to_string (line[sfAccount]); + jDst[jss::destination_account] = to_string (line[sfDestination]); + jDst[jss::amount] = line[sfAmount].getText (); + jDst[jss::balance] = line[sfBalance].getText (); + PublicKey const pk (line[sfPublicKey]); + jDst[jss::public_key] = toBase58 (TokenType::TOKEN_ACCOUNT_PUBLIC, pk); + jDst[jss::public_key_hex] = strHex (pk); + jDst[jss::settle_delay] = line[sfSettleDelay]; + if (auto const& v = line[~sfExpiration]) + jDst[jss::expiration] = *v; + if (auto const& v = line[~sfCancelAfter]) + jDst[jss::cancel_after] = *v; + if (auto const& v = line[~sfSourceTag]) + jDst[jss::source_tag] = *v; + if (auto const& v = line[~sfDestinationTag]) + jDst[jss::destination_tag] = *v; +} + +// { +// account: | +// ledger_hash : +// ledger_index : +// limit: integer // optional +// marker: opaque // optional, resume previous query +// } +Json::Value doAccountChannels (RPC::Context& context) +{ + auto const& params (context.params); + if (! params.isMember (jss::account)) + return RPC::missing_field_error (jss::account); + + std::shared_ptr ledger; + auto result = RPC::lookupLedger (ledger, context); + if (! ledger) + return result; + + std::string strIdent (params[jss::account].asString ()); + AccountID accountID; + + result = RPC::accountFromString (accountID, strIdent); + if (result) + return result; + + if (! ledger->exists(keylet::account (accountID))) + return rpcError (rpcACT_NOT_FOUND); + + std::string strDst; + if (params.isMember (jss::destination_account)) + strDst = params[jss::destination_account].asString (); + auto hasDst = ! strDst.empty (); + + AccountID raDstAccount; + if (hasDst) + { + result = RPC::accountFromString (raDstAccount, strDst); + if (result) + return result; + } + + unsigned int limit; + if (auto err = readLimitField(limit, RPC::Tuning::accountChannels, context)) + return *err; + + Json::Value jsonChannels{Json::arrayValue}; + struct VisitData + { + std::vector > items; + AccountID const& accountID; + bool hasDst; + AccountID const& raDstAccount; + }; + VisitData visitData = {{}, accountID, hasDst, raDstAccount}; + unsigned int reserve (limit); + uint256 startAfter; + std::uint64_t startHint; + + if (params.isMember (jss::marker)) + { + // We have a start point. Use limit - 1 from the result and use the + // very last one for the resume. + Json::Value const& marker (params[jss::marker]); + + if (! marker.isString ()) + return RPC::expected_field_error (jss::marker, "string"); + + startAfter.SetHex (marker.asString ()); + auto const sleChannel = ledger->read({ltPAYCHAN, startAfter}); + + if (! sleChannel) + return rpcError (rpcINVALID_PARAMS); + + if (sleChannel->getFieldAmount (sfLowLimit).getIssuer () == accountID) + startHint = sleChannel->getFieldU64 (sfLowNode); + else if (sleChannel->getFieldAmount (sfHighLimit).getIssuer () == accountID) + startHint = sleChannel->getFieldU64 (sfHighNode); + else + return rpcError (rpcINVALID_PARAMS); + + addChannel (jsonChannels, *sleChannel); + visitData.items.reserve (reserve); + } + else + { + startHint = 0; + // We have no start point, limit should be one higher than requested. + visitData.items.reserve (++reserve); + } + + if (! forEachItemAfter(*ledger, accountID, + startAfter, startHint, reserve, + [&visitData](std::shared_ptr const& sleCur) + { + + if (sleCur && sleCur->getType () == ltPAYCHAN && + (! visitData.hasDst || + visitData.raDstAccount == (*sleCur)[sfDestination])) + { + visitData.items.emplace_back (sleCur); + return true; + } + + return false; + })) + { + return rpcError (rpcINVALID_PARAMS); + } + + if (visitData.items.size () == reserve) + { + result[jss::limit] = limit; + + result[jss::marker] = to_string (visitData.items.back()->getIndex()); + visitData.items.pop_back (); + } + + result[jss::account] = context.app.accountIDCache().toBase58 (accountID); + + for (auto const& item : visitData.items) + addChannel (jsonChannels, *item); + + context.loadType = Resource::feeMediumBurdenRPC; + result[jss::channels] = std::move(jsonChannels); + return result; +} + +} // ripple diff --git a/src/ripple/rpc/handlers/Handlers.h b/src/ripple/rpc/handlers/Handlers.h index edb2ee06e..d2b932da5 100644 --- a/src/ripple/rpc/handlers/Handlers.h +++ b/src/ripple/rpc/handlers/Handlers.h @@ -27,6 +27,7 @@ namespace ripple { Json::Value doAccountCurrencies (RPC::Context&); Json::Value doAccountInfo (RPC::Context&); Json::Value doAccountLines (RPC::Context&); +Json::Value doAccountChannels (RPC::Context&); Json::Value doAccountObjects (RPC::Context&); Json::Value doAccountOffers (RPC::Context&); Json::Value doAccountTx (RPC::Context&); @@ -35,6 +36,8 @@ Json::Value doAccountTxOld (RPC::Context&); Json::Value doBookOffers (RPC::Context&); Json::Value doBlackList (RPC::Context&); Json::Value doCanDelete (RPC::Context&); +Json::Value doChannelAuthorize (RPC::Context&); +Json::Value doChannelVerify (RPC::Context&); Json::Value doConnect (RPC::Context&); Json::Value doConsensusInfo (RPC::Context&); Json::Value doFeature (RPC::Context&); diff --git a/src/ripple/rpc/handlers/PayChanClaim.cpp b/src/ripple/rpc/handlers/PayChanClaim.cpp new file mode 100644 index 000000000..01a80af0a --- /dev/null +++ b/src/ripple/rpc/handlers/PayChanClaim.cpp @@ -0,0 +1,130 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2014 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 +#include +#include + +namespace ripple { + +// { +// secret_key: +// channel_id: 256-bit channel id +// drops: 64-bit uint (as string) +// } +Json::Value doChannelAuthorize (RPC::Context& context) +{ + auto const& params (context.params); + for (auto const& p : {jss::secret, jss::channel_id, jss::amount}) + if (!params.isMember (p)) + return RPC::missing_field_error (p); + + Json::Value result; + auto const keypair = RPC::keypairForSignature (params, result); + if (RPC::contains_error (result)) + return result; + + uint256 channelId; + if (!channelId.SetHexExact (params[jss::channel_id].asString ())) + return rpcError (rpcCHANNEL_MALFORMED); + + std::uint64_t drops = 0; + try + { + drops = std::stoul (params[jss::amount].asString ()); + } + catch (std::exception const&) + { + return rpcError (rpcCHANNEL_AMT_MALFORMED); + } + + Serializer msg; + serializePayChanAuthorization (msg, channelId, XRPAmount (drops)); + + try + { + auto const buf = sign (keypair.first, keypair.second, msg.slice ()); + result[jss::signature] = strHex (buf); + } + catch (std::exception&) + { + result = + RPC::make_error (rpcINTERNAL, "Exception occurred during signing."); + } + return result; +} + +// { +// public_key: +// channel_id: 256-bit channel id +// drops: 64-bit uint (as string) +// signature: signature to verify +// } +Json::Value doChannelVerify (RPC::Context& context) +{ + auto const& params (context.params); + for (auto const& p : + {jss::public_key, jss::channel_id, jss::amount, jss::signature}) + if (!params.isMember (p)) + return RPC::missing_field_error (p); + + std::string const strPk = params[jss::public_key].asString (); + auto const pk = + parseBase58 (TokenType::TOKEN_ACCOUNT_PUBLIC, strPk); + if (!pk) + return rpcError (rpcPUBLIC_MALFORMED); + + uint256 channelId; + if (!channelId.SetHexExact (params[jss::channel_id].asString ())) + return rpcError (rpcCHANNEL_MALFORMED); + + std::uint64_t drops = 0; + try + { + drops = std::stoul (params[jss::amount].asString ()); + } + catch (std::exception const&) + { + return rpcError (rpcCHANNEL_AMT_MALFORMED); + } + + std::pair sig(strUnHex (params[jss::signature].asString ())); + if (!sig.second || !sig.first.size ()) + return rpcError (rpcINVALID_PARAMS); + + Serializer msg; + serializePayChanAuthorization (msg, channelId, XRPAmount (drops)); + + Json::Value result; + result[jss::signature_verified] = + verify (*pk, msg.slice (), makeSlice (sig.first), /*canonical*/ true); + return result; +} + +} // ripple diff --git a/src/ripple/rpc/impl/Handler.cpp b/src/ripple/rpc/impl/Handler.cpp index f0ddeda4d..e4e32ee94 100644 --- a/src/ripple/rpc/impl/Handler.cpp +++ b/src/ripple/rpc/impl/Handler.cpp @@ -104,12 +104,15 @@ Handler handlerArray[] { { "account_info", byRef (&doAccountInfo), Role::USER, NO_CONDITION }, { "account_currencies", byRef (&doAccountCurrencies), Role::USER, NO_CONDITION }, { "account_lines", byRef (&doAccountLines), Role::USER, NO_CONDITION }, + { "account_channels", byRef (&doAccountChannels), Role::USER, NO_CONDITION }, { "account_objects", byRef (&doAccountObjects), Role::USER, NO_CONDITION }, { "account_offers", byRef (&doAccountOffers), Role::USER, NO_CONDITION }, { "account_tx", byRef (&doAccountTxSwitch), Role::USER, NO_CONDITION }, { "blacklist", byRef (&doBlackList), Role::ADMIN, NO_CONDITION }, { "book_offers", byRef (&doBookOffers), Role::USER, NO_CONDITION }, { "can_delete", byRef (&doCanDelete), Role::ADMIN, NO_CONDITION }, + { "channel_authorize", byRef (&doChannelAuthorize), Role::USER, NO_CONDITION }, + { "channel_verify", byRef (&doChannelVerify), Role::USER, NO_CONDITION }, { "connect", byRef (&doConnect), Role::ADMIN, NO_CONDITION }, { "consensus_info", byRef (&doConsensusInfo), Role::ADMIN, NO_CONDITION }, { "gateway_balances", byRef (&doGatewayBalances), Role::USER, NO_CONDITION }, diff --git a/src/ripple/rpc/impl/Tuning.h b/src/ripple/rpc/impl/Tuning.h index 14693e8e9..75f658aec 100644 --- a/src/ripple/rpc/impl/Tuning.h +++ b/src/ripple/rpc/impl/Tuning.h @@ -35,6 +35,9 @@ struct LimitRange { /** Limits for the account_lines command. */ static LimitRange const accountLines = {10, 200, 400}; +/** Limits for the account_channels command. */ +static LimitRange const accountChannels = {10, 200, 400}; + /** Limits for the account_objects command. */ static LimitRange const accountObjects = {10, 200, 400}; diff --git a/src/ripple/unity/app_tests.cpp b/src/ripple/unity/app_tests.cpp index 5439e4a19..8bdd2097e 100644 --- a/src/ripple/unity/app_tests.cpp +++ b/src/ripple/unity/app_tests.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include diff --git a/src/ripple/unity/app_tx.cpp b/src/ripple/unity/app_tx.cpp index 8e6bb61c8..e84af3d91 100644 --- a/src/ripple/unity/app_tx.cpp +++ b/src/ripple/unity/app_tx.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include diff --git a/src/ripple/unity/rpcx.cpp b/src/ripple/unity/rpcx.cpp index 401848f4a..acd466910 100644 --- a/src/ripple/unity/rpcx.cpp +++ b/src/ripple/unity/rpcx.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -62,6 +63,7 @@ #include #include #include +#include #include #include #include