mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
Conditional Suspended Payments (RIPD-1140):
A conditional suspended payment is a suspended payment where completion of the payment is contingent upon the fulfillment of a condition defined by the sender during creation of the suspended payment. This commit also introduces the "CryptoConditions" amendment which controls whether cryptoconditions will be supported in suspended payments. The existing "SusPay" amendment can be used to enable suspended payments without enabling the cryptoconditions code.
This commit is contained in:
@@ -20,7 +20,6 @@
|
||||
#include <BeastConfig.h>
|
||||
#include <ripple/test/jtx.h>
|
||||
#include <ripple/app/tx/applySteps.h>
|
||||
#include <ripple/protocol/digest.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
#include <ripple/protocol/JsonFields.h>
|
||||
@@ -31,38 +30,72 @@ namespace test {
|
||||
|
||||
struct SusPay_test : public beast::unit_test::suite
|
||||
{
|
||||
template <class... Args>
|
||||
static
|
||||
uint256
|
||||
digest (Args&&... args)
|
||||
{
|
||||
sha256_hasher h;
|
||||
using beast::hash_append;
|
||||
hash_append(h, args...);
|
||||
auto const d = static_cast<
|
||||
sha256_hasher::result_type>(h);
|
||||
uint256 result;
|
||||
std::memcpy(result.data(), d.data(), d.size());
|
||||
return result;
|
||||
}
|
||||
// An Ed25519 conditional trigger fulfillment and its
|
||||
// condition
|
||||
std::array<std::uint8_t, 99> const fb1 =
|
||||
{{
|
||||
0x00, 0x04, 0x60, 0x3B, 0x6A, 0x27, 0xBC, 0xCE, 0xB6, 0xA4, 0x2D, 0x62,
|
||||
0xA3, 0xA8, 0xD0, 0x2A, 0x6F, 0x0D, 0x73, 0x65, 0x32, 0x15, 0x77, 0x1D,
|
||||
0xE2, 0x43, 0xA6, 0x3A, 0xC0, 0x48, 0xA1, 0x8B, 0x59, 0xDA, 0x29, 0x8F,
|
||||
0x89, 0x5B, 0x3C, 0xAF, 0xE2, 0xC9, 0x50, 0x60, 0x39, 0xD0, 0xE2, 0xA6,
|
||||
0x63, 0x82, 0x56, 0x80, 0x04, 0x67, 0x4F, 0xE8, 0xD2, 0x37, 0x78, 0x50,
|
||||
0x92, 0xE4, 0x0D, 0x6A, 0xAF, 0x48, 0x3E, 0x4F, 0xC6, 0x01, 0x68, 0x70,
|
||||
0x5F, 0x31, 0xF1, 0x01, 0x59, 0x61, 0x38, 0xCE, 0x21, 0xAA, 0x35, 0x7C,
|
||||
0x0D, 0x32, 0xA0, 0x64, 0xF4, 0x23, 0xDC, 0x3E, 0xE4, 0xAA, 0x3A, 0xBF,
|
||||
0x53, 0xF8, 0x03,
|
||||
}};
|
||||
|
||||
// Create condition
|
||||
// First is digest, second is pre-image
|
||||
static
|
||||
std::pair<uint256, uint256>
|
||||
cond (std::string const& receipt)
|
||||
{
|
||||
std::pair<uint256, uint256> result;
|
||||
result.second = digest(receipt);
|
||||
result.first = digest(result.second);
|
||||
return result;
|
||||
}
|
||||
std::array<std::uint8_t, 39> const cb1 =
|
||||
{{
|
||||
0x00, 0x04, 0x01, 0x20, 0x20, 0x3B, 0x6A, 0x27, 0xBC, 0xCE, 0xB6, 0xA4,
|
||||
0x2D, 0x62, 0xA3, 0xA8, 0xD0, 0x2A, 0x6F, 0x0D, 0x73, 0x65, 0x32, 0x15,
|
||||
0x77, 0x1D, 0xE2, 0x43, 0xA6, 0x3A, 0xC0, 0x48, 0xA1, 0x8B, 0x59, 0xDA,
|
||||
0x29, 0x01, 0x60
|
||||
}};
|
||||
|
||||
// A prefix.prefix.ed25519 conditional trigger fulfillment:
|
||||
std::array<std::uint8_t, 106> const fb2 =
|
||||
{{
|
||||
0x00, 0x01, 0x67, 0x03, 0x61, 0x62, 0x63, 0x00, 0x04, 0x60, 0x76, 0xA1,
|
||||
0x59, 0x20, 0x44, 0xA6, 0xE4, 0xF5, 0x11, 0x26, 0x5B, 0xCA, 0x73, 0xA6,
|
||||
0x04, 0xD9, 0x0B, 0x05, 0x29, 0xD1, 0xDF, 0x60, 0x2B, 0xE3, 0x0A, 0x19,
|
||||
0xA9, 0x25, 0x76, 0x60, 0xD1, 0xF5, 0xAE, 0xC6, 0xAB, 0x6A, 0x91, 0x22,
|
||||
0xAF, 0xF0, 0xF7, 0xDC, 0xB9, 0x66, 0x7F, 0xF6, 0x13, 0x13, 0x68, 0x94,
|
||||
0x73, 0x2B, 0x6E, 0x78, 0xC2, 0x6F, 0x5B, 0x67, 0x31, 0x01, 0xE2, 0x67,
|
||||
0xFE, 0x2E, 0x2B, 0x65, 0xFA, 0x4D, 0x53, 0xDA, 0xD4, 0x78, 0xA1, 0xAD,
|
||||
0xA6, 0x4D, 0x50, 0xFD, 0x1D, 0xFD, 0xB7, 0xD9, 0x49, 0x20, 0xDC, 0x3E,
|
||||
0x1A, 0x56, 0x4A, 0x64, 0x7B, 0x1C, 0xBA, 0x35, 0x60, 0x01,
|
||||
}};
|
||||
|
||||
std::array<std::uint8_t, 39> const cb2 =
|
||||
{{
|
||||
|
||||
0x00, 0x01, 0x01, 0x25, 0x20, 0x28, 0x7A, 0x8B, 0xD8, 0xAD, 0xAE, 0x8A,
|
||||
0xCA, 0x0C, 0x87, 0x1C, 0xE7, 0xC2, 0x5F, 0xBA, 0xA5, 0xA8, 0xBE, 0x10,
|
||||
0xD0, 0xE4, 0xDB, 0x1F, 0x56, 0xAE, 0xEE, 0x8B, 0xB3, 0xAD, 0xCE, 0xE5,
|
||||
0x5B, 0x01, 0x64
|
||||
}};
|
||||
|
||||
// A prefix+preimage conditional trigger fulfillment
|
||||
std::array<std::uint8_t, 7> const fb3 =
|
||||
{{
|
||||
0x00, 0x01, 0x04, 0x00, 0x00, 0x00, 0x00,
|
||||
}};
|
||||
|
||||
std::array<std::uint8_t, 39> const cb3 =
|
||||
{{
|
||||
|
||||
0x00, 0x01, 0x01, 0x07, 0x20, 0x62, 0x36, 0xB7, 0xA8, 0x58, 0xFB, 0x35,
|
||||
0x2F, 0xD5, 0xC3, 0x01, 0x3B, 0x68, 0x98, 0xCF, 0x26, 0x8B, 0x3E, 0xB8,
|
||||
0x50, 0xB3, 0x4A, 0xD2, 0x65, 0x24, 0xB0, 0xF8, 0x56, 0xC3, 0x72, 0xD9,
|
||||
0x73, 0x01, 0x01
|
||||
}};
|
||||
|
||||
static
|
||||
Json::Value
|
||||
condpay (jtx::Account const& account, jtx::Account const& to,
|
||||
STAmount const& amount, uint256 const& digest,
|
||||
NetClock::time_point const& expiry)
|
||||
STAmount const& amount, Slice condition,
|
||||
NetClock::time_point const& cancelAfter)
|
||||
{
|
||||
using namespace jtx;
|
||||
Json::Value jv;
|
||||
@@ -72,8 +105,20 @@ struct SusPay_test : public beast::unit_test::suite
|
||||
jv[jss::Destination] = to.human();
|
||||
jv[jss::Amount] = amount.getJson(0);
|
||||
jv["CancelAfter"] =
|
||||
expiry.time_since_epoch().count();
|
||||
jv["Digest"] = to_string(digest);
|
||||
cancelAfter.time_since_epoch().count();
|
||||
jv["Condition"] = strHex(condition);
|
||||
return jv;
|
||||
}
|
||||
|
||||
static
|
||||
Json::Value
|
||||
condpay (jtx::Account const& account, jtx::Account const& to,
|
||||
STAmount const& amount, Slice condition,
|
||||
NetClock::time_point const& cancelAfter,
|
||||
NetClock::time_point const& finishAfter)
|
||||
{
|
||||
auto jv = condpay (account, to, amount, condition, cancelAfter);
|
||||
jv ["FinishAfter"] = finishAfter.time_since_epoch().count();
|
||||
return jv;
|
||||
}
|
||||
|
||||
@@ -94,6 +139,25 @@ struct SusPay_test : public beast::unit_test::suite
|
||||
return jv;
|
||||
}
|
||||
|
||||
static
|
||||
Json::Value
|
||||
lockup (jtx::Account const& account, jtx::Account const& to,
|
||||
STAmount const& amount, Slice condition,
|
||||
NetClock::time_point const& expiry)
|
||||
{
|
||||
using namespace jtx;
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = "SuspendedPaymentCreate";
|
||||
jv[jss::Flags] = tfUniversal;
|
||||
jv[jss::Account] = account.human();
|
||||
jv[jss::Destination] = to.human();
|
||||
jv[jss::Amount] = amount.getJson(0);
|
||||
jv["FinishAfter"] =
|
||||
expiry.time_since_epoch().count();
|
||||
jv["Condition"] = strHex(condition);
|
||||
return jv;
|
||||
}
|
||||
|
||||
static
|
||||
Json::Value
|
||||
finish (jtx::Account const& account,
|
||||
@@ -108,12 +172,11 @@ struct SusPay_test : public beast::unit_test::suite
|
||||
return jv;
|
||||
}
|
||||
|
||||
template <class Proof>
|
||||
static
|
||||
Json::Value
|
||||
finish (jtx::Account const& account,
|
||||
jtx::Account const& from, std::uint32_t seq,
|
||||
uint256 const& digest, Proof const& proof)
|
||||
Slice condition, Slice fulfillment)
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = "SuspendedPaymentFinish";
|
||||
@@ -121,9 +184,8 @@ struct SusPay_test : public beast::unit_test::suite
|
||||
jv[jss::Account] = account.human();
|
||||
jv["Owner"] = from.human();
|
||||
jv["OfferSequence"] = seq;
|
||||
jv["Method"] = 1;
|
||||
jv["Digest"] = to_string(digest);
|
||||
jv["Proof"] = to_string(proof);
|
||||
jv["Condition"] = strHex(condition);
|
||||
jv["Fulfillment"] = strHex(fulfillment);
|
||||
return jv;
|
||||
}
|
||||
|
||||
@@ -144,46 +206,100 @@ struct SusPay_test : public beast::unit_test::suite
|
||||
void
|
||||
testEnablement()
|
||||
{
|
||||
testcase ("Enablement");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
using S = seconds;
|
||||
auto const c = cond("receipt");
|
||||
{
|
||||
|
||||
{ // SusPay enabled
|
||||
Env env(*this, features(featureSusPay));
|
||||
auto T = [&env](NetClock::duration const& d)
|
||||
{ return env.now() + d; };
|
||||
env.fund(XRP(5000), "alice", "bob");
|
||||
// syntax
|
||||
env(condpay("alice", "bob", XRP(1000), c.first, T(S{1})));
|
||||
env(lockup("alice", "bob", XRP(1000), env.now() + 1s));
|
||||
}
|
||||
{
|
||||
|
||||
{ // SusPay not enabled
|
||||
Env env(*this);
|
||||
auto T = [&env](NetClock::duration const& d)
|
||||
{ return env.now() + d; };
|
||||
env.fund(XRP(5000), "alice", "bob");
|
||||
// disabled in production
|
||||
env(condpay("alice", "bob", XRP(1000), c.first, T(S{1})), ter(temDISABLED));
|
||||
env(finish("bob", "alice", 1), ter(temDISABLED));
|
||||
env(cancel("bob", "alice", 1), ter(temDISABLED));
|
||||
env(lockup("alice", "bob", XRP(1000), env.now() + 1s), ter(temDISABLED));
|
||||
env(finish("bob", "alice", 1), ter(temDISABLED));
|
||||
env(cancel("bob", "alice", 1), ter(temDISABLED));
|
||||
}
|
||||
|
||||
{ // SusPay enabled, CryptoConditions disabled
|
||||
Env env(*this,
|
||||
features(featureSusPay));
|
||||
|
||||
env.fund(XRP(5000), "alice", "bob");
|
||||
|
||||
auto const seq = env.seq("alice");
|
||||
|
||||
// Fail: no cryptoconditions allowed
|
||||
env(condpay("alice", "bob", XRP(1000),
|
||||
makeSlice (cb1), env.now() + 1s), ter(temDISABLED));
|
||||
|
||||
// Succeed: doesn't have a cryptocondition
|
||||
env(lockup("alice", "bob", XRP(1000),
|
||||
env.now() + 1s));
|
||||
|
||||
// Fail: can't specify conditional finishes if
|
||||
// cryptoconditions aren't allowed.
|
||||
{
|
||||
auto f = finish("bob", "alice", seq,
|
||||
makeSlice(cb1), makeSlice(fb1));
|
||||
env (f, ter(temDISABLED));
|
||||
|
||||
auto fnc = f;
|
||||
fnc.removeMember ("Condition");
|
||||
env (fnc, ter(temDISABLED));
|
||||
|
||||
auto fnf = f;
|
||||
fnf.removeMember ("Fulfillment");
|
||||
env (fnf, ter(temDISABLED));
|
||||
|
||||
}
|
||||
|
||||
// Succeeds
|
||||
env.close();
|
||||
env(finish("bob", "alice", seq));
|
||||
}
|
||||
|
||||
{ // SusPay enabled, CryptoConditions enabled
|
||||
Env env(*this,
|
||||
features(featureSusPay),
|
||||
features(featureCryptoConditions));
|
||||
|
||||
env.fund(XRP(5000), "alice", "bob");
|
||||
|
||||
auto const seq = env.seq("alice");
|
||||
|
||||
env(condpay("alice", "bob", XRP(1000),
|
||||
makeSlice (cb1), env.now() + 1s), fee(1500));
|
||||
env(finish("bob", "alice", seq,
|
||||
makeSlice(cb1), makeSlice(fb1)), fee(1500));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testTags()
|
||||
{
|
||||
testcase ("Tags");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
using S = seconds;
|
||||
|
||||
{
|
||||
Env env(*this, features(featureSusPay));
|
||||
Env env(*this,
|
||||
features(featureSusPay),
|
||||
features(featureCryptoConditions));
|
||||
|
||||
auto const alice = Account("alice");
|
||||
auto T = [&env](NetClock::duration const& d)
|
||||
{ return env.now() + d; };
|
||||
env.fund(XRP(5000), alice, "bob");
|
||||
auto const c = cond("receipt");
|
||||
|
||||
auto const seq = env.seq(alice);
|
||||
// set source and dest tags
|
||||
env(condpay(alice, "bob", XRP(1000), c.first, T(S{1})), stag(1), dtag(2));
|
||||
env(condpay(alice, "bob", XRP(1000),
|
||||
makeSlice (cb1), env.now() + 1s),
|
||||
stag(1), dtag(2));
|
||||
auto const sle = env.le(keylet::susPay(alice.id(), seq));
|
||||
BEAST_EXPECT((*sle)[sfSourceTag] == 1);
|
||||
BEAST_EXPECT((*sle)[sfDestinationTag] == 2);
|
||||
@@ -193,80 +309,203 @@ struct SusPay_test : public beast::unit_test::suite
|
||||
void
|
||||
testFails()
|
||||
{
|
||||
testcase ("Failure Cases");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
using S = seconds;
|
||||
|
||||
Env env(*this,
|
||||
features(featureSusPay),
|
||||
features(featureCryptoConditions));
|
||||
env.fund(XRP(5000), "alice", "bob");
|
||||
env.close();
|
||||
|
||||
// Expiration in the past
|
||||
env(condpay("alice", "bob", XRP(1000),
|
||||
makeSlice(cb1), env.now() - 1s), ter(tecNO_PERMISSION));
|
||||
|
||||
// no destination account
|
||||
env(condpay("alice", "carol", XRP(1000),
|
||||
makeSlice(cb1), env.now() + 1s), ter(tecNO_DST));
|
||||
|
||||
env.fund(XRP(5000), "carol");
|
||||
env(condpay("alice", "carol", XRP(1000),
|
||||
makeSlice(cb1), env.now() + 1s), stag(2));
|
||||
env(condpay("alice", "carol", XRP(1000),
|
||||
makeSlice(cb1), env.now() + 1s), stag(3), dtag(4));
|
||||
env(fset("carol", asfRequireDest));
|
||||
|
||||
// missing destination tag
|
||||
env(condpay("alice", "carol", XRP(1000),
|
||||
makeSlice(cb1), env.now() + 1s), ter(tecDST_TAG_NEEDED));
|
||||
env(condpay("alice", "carol", XRP(1000),
|
||||
makeSlice(cb1), env.now() + 1s), dtag(1));
|
||||
|
||||
// Using non-XRP:
|
||||
env (lockup("alice", "carol", Account("alice")["USD"](500),
|
||||
env.now() + 1s), ter(temBAD_AMOUNT));
|
||||
|
||||
// Sending zero or no XRP:
|
||||
env (lockup("alice", "carol", XRP(0),
|
||||
env.now() + 1s), ter(temBAD_AMOUNT));
|
||||
env (lockup("alice", "carol", XRP(-1000),
|
||||
env.now() + 1s), ter(temBAD_AMOUNT));
|
||||
|
||||
// Fail if neither CancelAfter nor FinishAfter are specified:
|
||||
{
|
||||
Env env(*this, features(featureSusPay));
|
||||
auto T = [&env](NetClock::duration const& d)
|
||||
{ return env.now() + d; };
|
||||
env.fund(XRP(5000), "alice", "bob");
|
||||
auto const c = cond("receipt");
|
||||
// VFALCO Should we enforce this?
|
||||
// expiration in the past
|
||||
//env(condpay("alice", "bob", XRP(1000), c.first, T(S{-1})), ter(tecNO_PERMISSION));
|
||||
// expiration beyond the limit
|
||||
env(condpay("alice", "bob", XRP(1000), c.first, T(days(7+1))), ter(tecNO_PERMISSION));
|
||||
// no destination account
|
||||
env(condpay("alice", "carol", XRP(1000), c.first, T(S{1})), ter(tecNO_DST));
|
||||
env.fund(XRP(5000), "carol");
|
||||
env(condpay("alice", "carol",
|
||||
XRP(1000), c.first, T(S{1})), stag(2));
|
||||
env(condpay("alice", "carol",
|
||||
XRP(1000), c.first, T(S{1})), stag(3), dtag(4));
|
||||
env(fset("carol", asfRequireDest));
|
||||
// missing destination tag
|
||||
env(condpay("alice", "carol", XRP(1000), c.first, T(S{1})), ter(tecDST_TAG_NEEDED));
|
||||
env(condpay("alice", "carol",
|
||||
XRP(1000), c.first, T(S{1})), dtag(1));
|
||||
auto j1 = lockup("alice", "carol", XRP(1), env.now() + 1s);
|
||||
j1.removeMember ("FinishAfter");
|
||||
env (j1, ter(temBAD_EXPIRATION));
|
||||
|
||||
auto j2 = condpay("alice", "carol", XRP(1), makeSlice(cb1), env.now() + 1s);
|
||||
j2.removeMember ("CancelAfter");
|
||||
env (j2, ter(temBAD_EXPIRATION));
|
||||
}
|
||||
|
||||
// Fail if FinishAfter has already passed:
|
||||
env (lockup("alice", "carol", XRP(1), env.now() - 1s), ter (tecNO_PERMISSION));
|
||||
|
||||
// Both CancelAfter and FinishAfter
|
||||
env(condpay("alice", "carol", XRP(1), makeSlice(cb1),
|
||||
env.now() + 10s, env.now() + 10s), ter (temBAD_EXPIRATION));
|
||||
env(condpay("alice", "carol", XRP(1), makeSlice(cb1),
|
||||
env.now() + 10s, env.now() + 15s), ter (temBAD_EXPIRATION));
|
||||
|
||||
// Fail if the sender wants to send more than he has:
|
||||
auto const accountReserve =
|
||||
drops(env.current()->fees().reserve);
|
||||
auto const accountIncrement =
|
||||
drops(env.current()->fees().increment);
|
||||
|
||||
env.fund (accountReserve + accountIncrement + XRP(50), "daniel");
|
||||
env(lockup("daniel", "bob", XRP(51), env.now() + 1s), ter (tecUNFUNDED));
|
||||
|
||||
env.fund (accountReserve + accountIncrement + XRP(50), "evan");
|
||||
env(lockup("evan", "bob", XRP(50), env.now() + 1s), ter (tecUNFUNDED));
|
||||
|
||||
env.fund (accountReserve, "frank");
|
||||
env(lockup("frank", "bob", XRP(1), env.now() + 1s), ter (tecINSUFFICIENT_RESERVE));
|
||||
|
||||
// Respect the "asfDisallowXRP" account flag:
|
||||
env.fund (accountReserve + accountIncrement, "george");
|
||||
env(fset("george", asfDisallowXRP));
|
||||
env(lockup("bob", "george", XRP(10), env.now() + 1s), ter (tecNO_TARGET));
|
||||
|
||||
{ // Specify incorrect sequence number
|
||||
env.fund (XRP(5000), "hannah");
|
||||
auto const seq = env.seq("hannah");
|
||||
env(lockup("hannah", "hannah", XRP(10), env.now() + 1s));
|
||||
env(finish ("hannah", "hannah", seq + 7), ter (tecNO_TARGET));
|
||||
}
|
||||
|
||||
{ // Try to specify a condition for a non-conditional payment
|
||||
env.fund (XRP(5000), "ivan");
|
||||
auto const seq = env.seq("ivan");
|
||||
|
||||
auto j = lockup("ivan", "ivan", XRP(10), env.now() + 1s);
|
||||
j["CancelAfter"] = j.removeMember ("FinishAfter");
|
||||
env (j);
|
||||
env(finish("ivan", "ivan", seq,
|
||||
makeSlice(cb1), makeSlice(fb1)), fee(1500), ter (tecCRYPTOCONDITION_ERROR));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testLockup()
|
||||
{
|
||||
testcase ("Lockup");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
using S = seconds;
|
||||
{
|
||||
|
||||
{ // Unconditional
|
||||
Env env(*this, features(featureSusPay));
|
||||
auto T = [&env](NetClock::duration const& d)
|
||||
{ return env.now() + d; };
|
||||
env.fund(XRP(5000), "alice", "bob");
|
||||
auto const seq = env.seq("alice");
|
||||
env(lockup("alice", "alice", XRP(1000), T(S{1})));
|
||||
env(lockup("alice", "alice", XRP(1000), env.now() + 1s));
|
||||
env.require(balance("alice", XRP(4000) - drops(10)));
|
||||
env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION));
|
||||
env(finish("bob", "alice", seq), ter(tecNO_PERMISSION));
|
||||
|
||||
env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION));
|
||||
env(finish("bob", "alice", seq), ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION));
|
||||
|
||||
env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION));
|
||||
env(finish("bob", "alice", seq));
|
||||
}
|
||||
|
||||
{ // Conditional
|
||||
Env env(*this,
|
||||
features(featureSusPay),
|
||||
features(featureCryptoConditions));
|
||||
env.fund(XRP(5000), "alice", "bob");
|
||||
auto const seq = env.seq("alice");
|
||||
env(lockup("alice", "alice", XRP(1000), makeSlice(cb2), env.now() + 1s));
|
||||
env.require(balance("alice", XRP(4000) - drops(10)));
|
||||
|
||||
env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION));
|
||||
env(finish("bob", "alice", seq), ter(tecNO_PERMISSION));
|
||||
env(finish("bob", "alice", seq,
|
||||
makeSlice(cb2), makeSlice(fb2)), fee(1500), ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
|
||||
env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION));
|
||||
env(finish("bob", "alice", seq), ter(tecCRYPTOCONDITION_ERROR));
|
||||
env(finish("bob", "alice", seq), ter(tecCRYPTOCONDITION_ERROR));
|
||||
env.close();
|
||||
|
||||
env(finish("bob", "alice", seq,
|
||||
makeSlice(cb2), makeSlice(fb2)), fee(1500));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testCondPay()
|
||||
{
|
||||
testcase ("Conditional Payments");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
using S = seconds;
|
||||
{
|
||||
Env env(*this, features(featureSusPay));
|
||||
|
||||
{ // Test cryptoconditions
|
||||
Env env(*this,
|
||||
features(featureSusPay),
|
||||
features(featureCryptoConditions));
|
||||
auto T = [&env](NetClock::duration const& d)
|
||||
{ return env.now() + d; };
|
||||
env.fund(XRP(5000), "alice", "bob", "carol");
|
||||
auto const c = cond("receipt");
|
||||
auto const seq = env.seq("alice");
|
||||
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0);
|
||||
env(condpay("alice", "carol", XRP(1000), c.first, T(S{1})));
|
||||
env(condpay("alice", "carol", XRP(1000), makeSlice(cb1), T(S{1})));
|
||||
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
|
||||
env.require(balance("alice", XRP(4000) - drops(10)));
|
||||
env.require(balance("carol", XRP(5000)));
|
||||
env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION));
|
||||
env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION));
|
||||
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
|
||||
env(finish("bob", "alice", seq, c.first, c.first), ter(temBAD_SIGNATURE));
|
||||
|
||||
// Attempt to finish without a fulfillment
|
||||
env(finish("bob", "alice", seq), ter(tecCRYPTOCONDITION_ERROR));
|
||||
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
|
||||
env(finish("bob", "alice", seq, c.first, c.second));
|
||||
|
||||
// Attempt to finish with a condition instead of a fulfillment
|
||||
env(finish("bob", "alice", seq, makeSlice(cb1), makeSlice(cb1)), fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
||||
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
|
||||
env(finish("bob", "alice", seq, makeSlice(cb1), makeSlice(cb2)), fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
||||
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
|
||||
env(finish("bob", "alice", seq, makeSlice(cb1), makeSlice(cb3)), fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
||||
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
|
||||
|
||||
// Attempt to finish with an incorrect condition and various
|
||||
// combinations of correct and incorrect fulfillments.
|
||||
env(finish("bob", "alice", seq, makeSlice(cb2), makeSlice(fb1)), fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
||||
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
|
||||
env(finish("bob", "alice", seq, makeSlice(cb2), makeSlice(fb2)), fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
||||
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
|
||||
env(finish("bob", "alice", seq, makeSlice(cb2), makeSlice(fb3)), fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
||||
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
|
||||
|
||||
// Attempt to finish with the correct condition & fulfillment
|
||||
env(finish("bob", "alice", seq, makeSlice(cb1), makeSlice(fb1)), fee(1500));
|
||||
// SLE removed on finish
|
||||
BEAST_EXPECT(! env.le(keylet::susPay(Account("alice").id(), seq)));
|
||||
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0);
|
||||
@@ -276,15 +515,17 @@ struct SusPay_test : public beast::unit_test::suite
|
||||
env(cancel("bob", "carol", 1), ter(tecNO_TARGET));
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
Env env(*this, features(featureSusPay));
|
||||
|
||||
{ // Test cancel when condition is present
|
||||
Env env(*this,
|
||||
features(featureSusPay),
|
||||
features(featureCryptoConditions));
|
||||
auto T = [&env](NetClock::duration const& d)
|
||||
{ return env.now() + d; };
|
||||
env.fund(XRP(5000), "alice", "bob", "carol");
|
||||
auto const c = cond("receipt");
|
||||
auto const seq = env.seq("alice");
|
||||
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0);
|
||||
env(condpay("alice", "carol", XRP(1000), c.first, T(S{1})));
|
||||
env(condpay("alice", "carol", XRP(1000), makeSlice(cb2), T(S{1})));
|
||||
env.close();
|
||||
env.require(balance("alice", XRP(4000) - drops(10)));
|
||||
// balance restored on cancel
|
||||
@@ -293,83 +534,235 @@ struct SusPay_test : public beast::unit_test::suite
|
||||
// SLE removed on cancel
|
||||
BEAST_EXPECT(! env.le(keylet::susPay(Account("alice").id(), seq)));
|
||||
}
|
||||
|
||||
{
|
||||
Env env(*this, features(featureSusPay));
|
||||
Env env(*this,
|
||||
features(featureSusPay),
|
||||
features(featureCryptoConditions));
|
||||
auto T = [&env](NetClock::duration const& d)
|
||||
{ return env.now() + d; };
|
||||
env.fund(XRP(5000), "alice", "bob", "carol");
|
||||
env.close();
|
||||
auto const c = cond("receipt");
|
||||
auto const seq = env.seq("alice");
|
||||
env(condpay("alice", "carol", XRP(1000), c.first, T(S{1})));
|
||||
env(condpay("alice", "carol", XRP(1000), makeSlice(cb3), T(S{1})));
|
||||
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
|
||||
// cancel fails before expiration
|
||||
env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION));
|
||||
env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION));
|
||||
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
|
||||
env.close();
|
||||
// finish fails after expiration
|
||||
env(finish("bob", "alice", seq, c.first, c.second), ter(tecNO_PERMISSION));
|
||||
env(finish("bob", "alice", seq, makeSlice(cb3), makeSlice(fb3)),
|
||||
fee(1500), ter(tecNO_PERMISSION));
|
||||
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
|
||||
env.require(balance("carol", XRP(5000)));
|
||||
}
|
||||
{
|
||||
Env env(*this, features(featureSusPay));
|
||||
|
||||
{ // Test long & short conditions during creation
|
||||
Env env(*this,
|
||||
features(featureSusPay),
|
||||
features(featureCryptoConditions));
|
||||
auto T = [&env](NetClock::duration const& d)
|
||||
{ return env.now() + d; };
|
||||
env.fund(XRP(5000), "alice", "bob", "carol");
|
||||
auto const c = cond("receipt");
|
||||
|
||||
std::vector<std::uint8_t> v;
|
||||
v.resize(cb1.size() + 2, 0x78);
|
||||
std::memcpy (v.data() + 1, cb1.data(), cb1.size());
|
||||
|
||||
auto const p = v.data();
|
||||
auto const s = v.size();
|
||||
|
||||
// All these are expected to fail, because the
|
||||
// condition we pass in is malformed in some way
|
||||
env(condpay("alice", "carol", XRP(1000),
|
||||
Slice{p, s}, T(S{1})), ter(temMALFORMED));
|
||||
env(condpay("alice", "carol", XRP(1000),
|
||||
Slice{p, s - 1}, T(S{1})), ter(temMALFORMED));
|
||||
env(condpay("alice", "carol", XRP(1000),
|
||||
Slice{p, s - 2}, T(S{1})), ter(temMALFORMED));
|
||||
env(condpay("alice", "carol", XRP(1000),
|
||||
Slice{p + 1, s - 1}, T(S{1})), ter(temMALFORMED));
|
||||
env(condpay("alice", "carol", XRP(1000),
|
||||
Slice{p + 1, s - 3}, T(S{1})), ter(temMALFORMED));
|
||||
env(condpay("alice", "carol", XRP(1000),
|
||||
Slice{p + 2, s - 2}, T(S{1})), ter(temMALFORMED));
|
||||
env(condpay("alice", "carol", XRP(1000),
|
||||
Slice{p + 2, s - 3}, T(S{1})), ter(temMALFORMED));
|
||||
|
||||
auto const seq = env.seq("alice");
|
||||
env(condpay("alice", "carol", XRP(1000), c.first, T(S{1})));
|
||||
// wrong digest
|
||||
auto const cx = cond("bad");
|
||||
env(finish("bob", "alice", seq, cx.first, cx.second), ter(tecNO_PERMISSION));
|
||||
env(condpay("alice", "carol", XRP(1000),
|
||||
Slice{p + 1, s - 2}, T(S{1})), fee(100));
|
||||
env(finish("bob", "alice", seq,
|
||||
makeSlice(cb1), makeSlice(fb1)), fee(1500));
|
||||
env.require(balance("alice", XRP(4000) - drops(100)));
|
||||
env.require(balance("bob", XRP(5000) - drops(1500)));
|
||||
env.require(balance("carol", XRP(6000)));
|
||||
}
|
||||
{
|
||||
Env env(*this, features(featureSusPay));
|
||||
|
||||
{ // Test long and short conditions & fulfillments during finish
|
||||
Env env(*this,
|
||||
features(featureSusPay),
|
||||
features(featureCryptoConditions));
|
||||
auto T = [&env](NetClock::duration const& d)
|
||||
{ return env.now() + d; };
|
||||
env.fund(XRP(5000), "alice", "bob", "carol");
|
||||
auto const p = from_hex_text<uint128>(
|
||||
"0102030405060708090A0B0C0D0E0F");
|
||||
auto const d = digest(p);
|
||||
|
||||
std::vector<std::uint8_t> cv;
|
||||
cv.resize(cb2.size() + 2, 0x78);
|
||||
std::memcpy (cv.data() + 1, cb2.data(), cb2.size());
|
||||
|
||||
auto const cp = cv.data();
|
||||
auto const cs = cv.size();
|
||||
|
||||
std::vector<std::uint8_t> fv;
|
||||
fv.resize(fb2.size() + 2, 0x13);
|
||||
std::memcpy(fv.data() + 1, fb2.data(), fb2.size());
|
||||
|
||||
auto const fp = fv.data();
|
||||
auto const fs = fv.size();
|
||||
|
||||
// All these are expected to fail, because the
|
||||
// condition we pass in is malformed in some way
|
||||
env(condpay("alice", "carol", XRP(1000),
|
||||
Slice{cp, cs}, T(S{1})), ter(temMALFORMED));
|
||||
env(condpay("alice", "carol", XRP(1000),
|
||||
Slice{cp, cs - 1}, T(S{1})), ter(temMALFORMED));
|
||||
env(condpay("alice", "carol", XRP(1000),
|
||||
Slice{cp, cs - 2}, T(S{1})), ter(temMALFORMED));
|
||||
env(condpay("alice", "carol", XRP(1000),
|
||||
Slice{cp + 1, cs - 1}, T(S{1})), ter(temMALFORMED));
|
||||
env(condpay("alice", "carol", XRP(1000),
|
||||
Slice{cp + 1, cs - 3}, T(S{1})), ter(temMALFORMED));
|
||||
env(condpay("alice", "carol", XRP(1000),
|
||||
Slice{cp + 2, cs - 2}, T(S{1})), ter(temMALFORMED));
|
||||
env(condpay("alice", "carol", XRP(1000),
|
||||
Slice{cp + 2, cs - 3}, T(S{1})), ter(temMALFORMED));
|
||||
|
||||
auto const seq = env.seq("alice");
|
||||
env(condpay("alice", "carol", XRP(1000), d, T(S{1})));
|
||||
// bad digest size
|
||||
env(finish("bob", "alice", seq, d, p), ter(temMALFORMED));
|
||||
env(condpay("alice", "carol", XRP(1000),
|
||||
Slice{cp + 1, cs - 2}, T(S{1})), fee(100));
|
||||
|
||||
// Now, try to fulfill using the same sequence of
|
||||
// malformed conditions.
|
||||
env(finish("bob", "alice", seq, Slice{cp, cs}, Slice{fp, fs}),
|
||||
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
||||
env(finish("bob", "alice", seq, Slice{cp, cs - 1}, Slice{fp, fs}),
|
||||
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
||||
env(finish("bob", "alice", seq, Slice{cp, cs - 2}, Slice{fp, fs}),
|
||||
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
||||
env(finish("bob", "alice", seq, Slice{cp + 1, cs - 1}, Slice{fp, fs}),
|
||||
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
||||
env(finish("bob", "alice", seq, Slice{cp + 1, cs - 3}, Slice{fp, fs}),
|
||||
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
||||
env(finish("bob", "alice", seq, Slice{cp + 2, cs - 2}, Slice{fp, fs}),
|
||||
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
||||
env(finish("bob", "alice", seq, Slice{cp + 2, cs - 3}, Slice{fp, fs}),
|
||||
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
||||
|
||||
// Now, using the correct condition, try malformed
|
||||
// fulfillments:
|
||||
env(finish("bob", "alice", seq, Slice{cp + 1, cs - 2}, Slice{fp, fs}),
|
||||
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
||||
env(finish("bob", "alice", seq, Slice{cp + 1, cs - 2}, Slice{fp, fs - 1}),
|
||||
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
||||
env(finish("bob", "alice", seq, Slice{cp + 1, cs - 2}, Slice{fp, fs - 2}),
|
||||
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
||||
env(finish("bob", "alice", seq, Slice{cp + 1, cs - 2}, Slice{fp + 1, fs - 1}),
|
||||
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
||||
env(finish("bob", "alice", seq, Slice{cp + 1, cs - 2}, Slice{fp + 1, fs - 3}),
|
||||
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
||||
env(finish("bob", "alice", seq, Slice{cp + 1, cs - 2}, Slice{fp + 1, fs - 3}),
|
||||
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
||||
env(finish("bob", "alice", seq, Slice{cp + 1, cs - 2}, Slice{fp + 2, fs - 2}),
|
||||
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
||||
env(finish("bob", "alice", seq, Slice{cp + 1, cs - 2}, Slice{fp + 2, fs - 3}),
|
||||
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
||||
|
||||
// Now try for the right one
|
||||
env(finish("bob", "alice", seq,
|
||||
makeSlice(cb2), makeSlice(fb2)), fee(1500));
|
||||
env.require(balance("alice", XRP(4000) - drops(100)));
|
||||
env.require(balance("carol", XRP(6000)));
|
||||
}
|
||||
|
||||
{ // Test empty condition during creation and
|
||||
// empty condition & fulfillment during finish
|
||||
Env env(*this,
|
||||
features(featureSusPay),
|
||||
features(featureCryptoConditions));
|
||||
auto T = [&env](NetClock::duration const& d)
|
||||
{ return env.now() + d; };
|
||||
env.fund(XRP(5000), "alice", "bob", "carol");
|
||||
|
||||
env(condpay("alice", "carol", XRP(1000), {}, T(S{1})), ter(temMALFORMED));
|
||||
|
||||
auto const seq = env.seq("alice");
|
||||
env(condpay("alice", "carol", XRP(1000), makeSlice(cb3), T(S{1})));
|
||||
|
||||
env(finish("bob", "alice", seq, {}, {}),
|
||||
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
||||
env(finish("bob", "alice", seq, makeSlice(cb3), {}),
|
||||
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
||||
env(finish("bob", "alice", seq, {}, makeSlice(fb3)),
|
||||
fee(1500), ter(tecCRYPTOCONDITION_ERROR));
|
||||
|
||||
auto correctFinish = finish("bob", "alice", seq,
|
||||
makeSlice(cb3), makeSlice(fb3));
|
||||
|
||||
// Manually assemble finish that is missing the
|
||||
// Condition or the Fulfillment (either both must
|
||||
// be present, or neither can):
|
||||
{
|
||||
auto finishNoCondition = correctFinish;
|
||||
finishNoCondition.removeMember ("Condition");
|
||||
env (finishNoCondition, ter(temMALFORMED));
|
||||
|
||||
auto finishNoFulfillment = correctFinish;
|
||||
finishNoFulfillment.removeMember ("Fulfillment");
|
||||
env (finishNoFulfillment, ter(temMALFORMED));
|
||||
}
|
||||
|
||||
env(correctFinish, fee(1500));
|
||||
env.require(balance ("carol", XRP(6000)));
|
||||
env.require(balance ("alice", XRP(4000) - drops(10)));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testMeta()
|
||||
{
|
||||
testcase ("Metadata");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
Env env(*this, features(featureSusPay));
|
||||
auto T = [&env](NetClock::duration const& d)
|
||||
{ return env.now() + d; };
|
||||
Env env(*this,
|
||||
features(featureSusPay),
|
||||
features(featureCryptoConditions));
|
||||
|
||||
env.fund(XRP(5000), "alice", "bob", "carol");
|
||||
auto const c = cond("receipt");
|
||||
env(condpay("alice", "carol", XRP(1000), c.first, T(1s)));
|
||||
env(condpay("alice", "carol", XRP(1000), makeSlice(cb1), env.now() + 1s));
|
||||
auto const m = env.meta();
|
||||
BEAST_EXPECT((*m)[sfTransactionResult] == tesSUCCESS);
|
||||
}
|
||||
|
||||
void testConsequences()
|
||||
{
|
||||
testcase ("Consequences");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
Env env(*this, features(featureSusPay));
|
||||
auto T = [&env](NetClock::duration const& d)
|
||||
{
|
||||
return env.now() + d;
|
||||
};
|
||||
Env env(*this,
|
||||
features(featureSusPay),
|
||||
features(featureCryptoConditions));
|
||||
|
||||
env.memoize("alice");
|
||||
env.memoize("bob");
|
||||
env.memoize("carol");
|
||||
auto const c = cond("receipt");
|
||||
|
||||
{
|
||||
auto const jtx = env.jt(
|
||||
condpay("alice", "carol", XRP(1000), c.first, T(1s)),
|
||||
condpay("alice", "carol", XRP(1000),
|
||||
makeSlice(cb1), env.now() + 1s),
|
||||
seq(1), fee(10));
|
||||
auto const pf = preflight(env.app(), env.current()->rules(),
|
||||
*jtx.stx, tapNONE, env.journal);
|
||||
@@ -394,7 +787,8 @@ struct SusPay_test : public beast::unit_test::suite
|
||||
|
||||
{
|
||||
auto const jtx = env.jt(
|
||||
finish("bob", "alice", 3, c.first, c.second),
|
||||
finish("bob", "alice", 3,
|
||||
makeSlice(cb1), makeSlice(fb1)),
|
||||
seq(1), fee(10));
|
||||
auto const pf = preflight(env.app(), env.current()->rules(),
|
||||
*jtx.stx, tapNONE, env.journal);
|
||||
|
||||
Reference in New Issue
Block a user