Payment Channels (RIPD-1224):

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
give 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.
This commit is contained in:
seelabs
2016-07-19 15:27:56 -04:00
parent 2e7f5502bf
commit d4a56f223a
35 changed files with 2015 additions and 5 deletions

View File

@@ -1179,6 +1179,10 @@
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\ripple\app\tests\PayChan_test.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\ripple\app\tests\Regression_test.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
@@ -1273,6 +1277,12 @@
</ClCompile>
<ClInclude Include="..\..\src\ripple\app\tx\impl\OfferStream.h">
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\tx\impl\PayChan.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClInclude Include="..\..\src\ripple\app\tx\impl\PayChan.h">
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\tx\impl\Payment.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
@@ -2854,6 +2864,8 @@
</ClInclude>
<ClInclude Include="..\..\src\ripple\protocol\LedgerFormats.h">
</ClInclude>
<ClInclude Include="..\..\src\ripple\protocol\PayChan.h">
</ClInclude>
<ClInclude Include="..\..\src\ripple\protocol\Protocol.h">
</ClInclude>
<ClInclude Include="..\..\src\ripple\protocol\PublicKey.h">
@@ -3051,6 +3063,10 @@
</ClCompile>
<ClInclude Include="..\..\src\ripple\rpc\Context.h">
</ClInclude>
<ClCompile Include="..\..\src\ripple\rpc\handlers\AccountChannels.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\ripple\rpc\handlers\AccountCurrenciesHandler.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
@@ -3183,6 +3199,10 @@
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\ripple\rpc\handlers\PayChanClaim.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="..\..\src\ripple\rpc\handlers\Peers.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>

View File

@@ -1647,6 +1647,9 @@
<ClCompile Include="..\..\src\ripple\app\tests\Path_test.cpp">
<Filter>ripple\app\tests</Filter>
</ClCompile>
<ClCompile Include="..\..\src\ripple\app\tests\PayChan_test.cpp">
<Filter>ripple\app\tests</Filter>
</ClCompile>
<ClCompile Include="..\..\src\ripple\app\tests\Regression_test.cpp">
<Filter>ripple\app\tests</Filter>
</ClCompile>
@@ -1734,6 +1737,12 @@
<ClInclude Include="..\..\src\ripple\app\tx\impl\OfferStream.h">
<Filter>ripple\app\tx\impl</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\tx\impl\PayChan.cpp">
<Filter>ripple\app\tx\impl</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ripple\app\tx\impl\PayChan.h">
<Filter>ripple\app\tx\impl</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\tx\impl\Payment.cpp">
<Filter>ripple\app\tx\impl</Filter>
</ClCompile>
@@ -3351,6 +3360,9 @@
<ClInclude Include="..\..\src\ripple\protocol\LedgerFormats.h">
<Filter>ripple\protocol</Filter>
</ClInclude>
<ClInclude Include="..\..\src\ripple\protocol\PayChan.h">
<Filter>ripple\protocol</Filter>
</ClInclude>
<ClInclude Include="..\..\src\ripple\protocol\Protocol.h">
<Filter>ripple\protocol</Filter>
</ClInclude>
@@ -3564,6 +3576,9 @@
<ClInclude Include="..\..\src\ripple\rpc\Context.h">
<Filter>ripple\rpc</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ripple\rpc\handlers\AccountChannels.cpp">
<Filter>ripple\rpc\handlers</Filter>
</ClCompile>
<ClCompile Include="..\..\src\ripple\rpc\handlers\AccountCurrenciesHandler.cpp">
<Filter>ripple\rpc\handlers</Filter>
</ClCompile>
@@ -3666,6 +3681,9 @@
<ClCompile Include="..\..\src\ripple\rpc\handlers\PathFind.cpp">
<Filter>ripple\rpc\handlers</Filter>
</ClCompile>
<ClCompile Include="..\..\src\ripple\rpc\handlers\PayChanClaim.cpp">
<Filter>ripple\rpc\handlers</Filter>
</ClCompile>
<ClCompile Include="..\..\src\ripple\rpc\handlers\Peers.cpp">
<Filter>ripple\rpc\handlers</Filter>
</ClCompile>

View File

@@ -45,7 +45,8 @@ supportedAmendments ()
{ "6781F8368C4771B83E8B821D88F580202BCB4228075297B19E4FDC5233F1EFDC TrustSetAuth" },
{ "42426C4D4F1009EE67080A9B7965B44656D7714D104A72F9B4369F97ABF044EE FeeEscalation" },
{ "5CC22CFF2864B020BD79E0E1F048F63EF3594F95E650E43B3F837EF1DF5F4B26 FlowV2"},
{ "9178256A980A86CF3D70D0260A7DA6402AAFE43632FDBCB88037978404188871 OwnerPaysFee"}
{ "9178256A980A86CF3D70D0260A7DA6402AAFE43632FDBCB88037978404188871 OwnerPaysFee"},
{ "08DE7D96082187F6E6578530258C77FAABABE4C20474BDB82F04B021F1A68647 PayChan"}
};
}

View File

@@ -121,11 +121,14 @@ void printHelp (const po::options_description& desc)
" account_currencies <account> [<ledger>] [strict]\n"
" account_info <account>|<seed>|<pass_phrase>|<key> [<ledger>] [strict]\n"
" account_lines <account> <account>|\"\" [<ledger>]\n"
" account_channels <account> <account>|\"\" [<ledger>]\n"
" account_objects <account> [<ledger>] [strict]\n"
" account_offers <account>|<account_public_key> [<ledger>]\n"
" account_tx accountID [ledger_min [ledger_max [limit [offset]]]] [binary] [count] [descending]\n"
" book_offers <taker_pays> <taker_gets> [<taker [<ledger> [<limit> [<proof> [<marker>]]]]]\n"
" can_delete [<ledgerid>|<ledgerhash>|now|always|never]\n"
" channel_authorize <private_key> <channel_id> <drops>\n"
" channel_verify <public_key> <channel_id> <drops> <signature>\n"
" connect <ip> [<port>]\n"
" consensus_info\n"
" feature [<feature> [accept|reject]]\n"

View File

@@ -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 <BeastConfig.h>
#include <ripple/basics/chrono.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/JsonFields.h>
#include <ripple/protocol/PayChan.h>
#include <ripple/protocol/TxFlags.h>
#include <ripple/test/jtx.h>
#include <chrono>
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<std::int64_t>
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<NetClock::time_point> const& cancelAfter = boost::none,
boost::optional<std::uint32_t> 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<NetClock::time_point> 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<STAmount> const& balance = boost::none,
boost::optional<STAmount> const& amount = boost::none,
boost::optional<Slice> const& signature = boost::none,
boost::optional<PublicKey> 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<NetClock::time_point> 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

View File

@@ -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 <BeastConfig.h>
#include <ripple/app/tx/impl/PayChan.h>
#include <ripple/basics/chrono.h>
#include <ripple/basics/Log.h>
#include <ripple/protocol/digest.h>
#include <ripple/protocol/st.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/PayChan.h>
#include <ripple/protocol/PublicKey.h>
#include <ripple/protocol/TxFlags.h>
#include <ripple/protocol/XRPAmount.h>
#include <ripple/ledger/View.h>
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<SLE> 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<SLE> (
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

View File

@@ -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 <ripple/app/tx/impl/Transactor.h>
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

View File

@@ -31,6 +31,7 @@
#include <ripple/app/tx/impl/SetSignerList.h>
#include <ripple/app/tx/impl/SetTrust.h>
#include <ripple/app/tx/impl/SusPay.h>
#include <ripple/app/tx/impl/PayChan.h>
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<SetTrust>(ctx);
case ttAMENDMENT:
case ttFEE: return invoke_preclaim<Change>(ctx);
case ttPAYCHAN_CREATE: return invoke_preclaim<PayChanCreate>(ctx);
case ttPAYCHAN_FUND: return invoke_preclaim<PayChanFund>(ctx);
case ttPAYCHAN_CLAIM: return invoke_preclaim<PayChanClaim>(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<CancelTicket>(tx);
case ttTICKET_CREATE: return invoke_calculateConsequences<CreateTicket>(tx);
case ttTRUST_SET: return invoke_calculateConsequences<SetTrust>(tx);
case ttPAYCHAN_CREATE: return invoke_calculateConsequences<PayChanCreate>(tx);
case ttPAYCHAN_FUND: return invoke_calculateConsequences<PayChanFund>(tx);
case ttPAYCHAN_CLAIM: return invoke_calculateConsequences<PayChanClaim>(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 };

View File

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

View File

@@ -583,6 +583,73 @@ private:
return parseAccountRaw2 (jvParams, "peer");
}
// account_channels <account> <account>|"" [<ledger>]
Json::Value parseAccountChannels (Json::Value const& jvParams)
{
return parseAccountRaw2 (jvParams, jss::destination_account);
}
// channel_authorize <private_key> <channel_id> <drops>
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 <public_key> <channel_id> <drops> <signature>
Json::Value parseChannelVerify (Json::Value const& jvParams)
{
std::string const strPk = jvParams[0u].asString ();
if (!parseBase58<PublicKey> (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 },

View File

@@ -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,

View File

@@ -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

View File

@@ -99,6 +99,9 @@ public:
/** Manifest */
static HashPrefix const manifest;
/** Payment Channel Claim */
static HashPrefix const paymentChannelClaim;
};
template <class Hasher>

View File

@@ -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
}

View File

@@ -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

View File

@@ -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.

View File

@@ -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 <ripple/basics/base_uint.h>
#include <ripple/protocol/HashPrefix.h>
#include <ripple/protocol/Serializer.h>
#include <ripple/protocol/XRPAmount.h>
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

View File

@@ -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;

View File

@@ -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

View File

@@ -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,

View File

@@ -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.");

View File

@@ -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

View File

@@ -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

View File

@@ -323,6 +323,18 @@ susPay (AccountID const& source, std::uint32_t seq)
return { ltSUSPAY, static_cast<uint256>(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<uint256>(h) };
}
} // keylet
} // ripple

View File

@@ -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)

View File

@@ -126,6 +126,7 @@ SF_U32 const sfSignerQuorum = make::one<SF_U32::type>(&sfSignerQuorum,
SF_U32 const sfCancelAfter = make::one<SF_U32::type>(&sfCancelAfter, STI_UINT32, 36, "CancelAfter");
SF_U32 const sfFinishAfter = make::one<SF_U32::type>(&sfFinishAfter, STI_UINT32, 37, "FinishAfter");
SF_U32 const sfSignerListID = make::one<SF_U32::type>(&sfSignerListID, STI_UINT32, 38, "SignerListID");
SF_U32 const sfSettleDelay = make::one<SF_U32::type>(&sfSettleDelay, STI_UINT32, 39, "SettleDelay");
// 64-bit integers
SF_U64 const sfIndexNext = make::one<SF_U64::type>(&sfIndexNext, STI_UINT64, 1, "IndexNext");
@@ -164,6 +165,7 @@ SF_U256 const sfNickname = make::one<SF_U256::type>(&sfNickname, STI_H
SF_U256 const sfAmendment = make::one<SF_U256::type>(&sfAmendment, STI_HASH256, 19, "Amendment");
SF_U256 const sfTicketID = make::one<SF_U256::type>(&sfTicketID, STI_HASH256, 20, "TicketID");
SF_U256 const sfDigest = make::one<SF_U256::type>(&sfDigest, STI_HASH256, 21, "Digest");
SF_U256 const sfPayChannel = make::one<SF_U256::type>(&sfPayChannel, STI_HASH256, 22, "Channel");
// currency amount (common)
SF_Amount const sfAmount = make::one<SF_Amount::type>(&sfAmount, STI_AMOUNT, 1, "Amount");

View File

@@ -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)

View File

@@ -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 <BeastConfig.h>
#include <ripple/app/main/Application.h>
#include <ripple/ledger/ReadView.h>
#include <ripple/ledger/View.h>
#include <ripple/net/RPCErr.h>
#include <ripple/protocol/ErrorCodes.h>
#include <ripple/protocol/JsonFields.h>
#include <ripple/protocol/PublicKey.h>
#include <ripple/protocol/STAccount.h>
#include <ripple/resource/Fees.h>
#include <ripple/rpc/Context.h>
#include <ripple/rpc/impl/RPCHelpers.h>
#include <ripple/rpc/impl/Tuning.h>
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: <account>|<account_public_key>
// ledger_hash : <ledger>
// ledger_index : <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<ReadView const> 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 <std::shared_ptr<SLE const>> 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<SLE const> 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

View File

@@ -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&);

View File

@@ -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 <BeastConfig.h>
#include <ripple/app/main/Application.h>
#include <ripple/basics/StringUtilities.h>
#include <ripple/ledger/ReadView.h>
#include <ripple/net/RPCErr.h>
#include <ripple/protocol/ErrorCodes.h>
#include <ripple/protocol/JsonFields.h>
#include <ripple/protocol/PayChan.h>
#include <ripple/protocol/STAccount.h>
#include <ripple/resource/Fees.h>
#include <ripple/rpc/Context.h>
#include <ripple/rpc/impl/RPCHelpers.h>
#include <ripple/rpc/impl/Tuning.h>
namespace ripple {
// {
// secret_key: <signing_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: <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<PublicKey> (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<Blob, bool> 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

View File

@@ -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 },

View File

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

View File

@@ -30,6 +30,7 @@
#include <ripple/app/tests/OfferStream.test.cpp>
#include <ripple/app/tests/Offer.test.cpp>
#include <ripple/app/tests/Path_test.cpp>
#include <ripple/app/tests/PayChan_test.cpp>
#include <ripple/app/tests/Regression_test.cpp>
#include <ripple/app/tests/SHAMapStore_test.cpp>
#include <ripple/app/tests/SusPay_test.cpp>

View File

@@ -29,6 +29,7 @@
#include <ripple/app/tx/impl/CreateTicket.cpp>
#include <ripple/app/tx/impl/OfferStream.cpp>
#include <ripple/app/tx/impl/Payment.cpp>
#include <ripple/app/tx/impl/PayChan.cpp>
#include <ripple/app/tx/impl/SetAccount.cpp>
#include <ripple/app/tx/impl/SetRegularKey.cpp>
#include <ripple/app/tx/impl/SetSignerList.cpp>

View File

@@ -33,6 +33,7 @@
#include <ripple/rpc/handlers/AccountCurrenciesHandler.cpp>
#include <ripple/rpc/handlers/AccountInfo.cpp>
#include <ripple/rpc/handlers/AccountLines.cpp>
#include <ripple/rpc/handlers/AccountChannels.cpp>
#include <ripple/rpc/handlers/AccountObjects.cpp>
#include <ripple/rpc/handlers/AccountOffers.cpp>
#include <ripple/rpc/handlers/AccountTx.cpp>
@@ -62,6 +63,7 @@
#include <ripple/rpc/handlers/NoRippleCheck.cpp>
#include <ripple/rpc/handlers/OwnerInfo.cpp>
#include <ripple/rpc/handlers/PathFind.cpp>
#include <ripple/rpc/handlers/PayChanClaim.cpp>
#include <ripple/rpc/handlers/Peers.cpp>
#include <ripple/rpc/handlers/Ping.cpp>
#include <ripple/rpc/handlers/Print.cpp>