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:
Nik Bougalis
2016-10-26 01:18:31 -07:00
parent d198b439fd
commit d69b16895c
14 changed files with 772 additions and 224 deletions

View File

@@ -45,9 +45,10 @@ supportedAmendments ()
{ "DA1BD556B42D85EA9C84066D028D355B52416734D3283F85E216EA5DA6DB7E13 SusPay" },
{ "6781F8368C4771B83E8B821D88F580202BCB4228075297B19E4FDC5233F1EFDC TrustSetAuth" },
{ "42426C4D4F1009EE67080A9B7965B44656D7714D104A72F9B4369F97ABF044EE FeeEscalation" },
{ "9178256A980A86CF3D70D0260A7DA6402AAFE43632FDBCB88037978404188871 OwnerPaysFee"},
{ "08DE7D96082187F6E6578530258C77FAABABE4C20474BDB82F04B021F1A68647 PayChan"},
{ "740352F2412A9909880C23A559FCECEDA3BE2126FED62FC7660D628A06927F11 Flow"}
{ "9178256A980A86CF3D70D0260A7DA6402AAFE43632FDBCB88037978404188871 OwnerPaysFee" },
{ "08DE7D96082187F6E6578530258C77FAABABE4C20474BDB82F04B021F1A68647 PayChan" },
{ "740352F2412A9909880C23A559FCECEDA3BE2126FED62FC7660D628A06927F11 Flow" },
{ "1562511F573A19AE9BD103B5D6B9E01B3B46805AEC5D3C4805C902B514399146 CryptoConditions" }
};
}

View File

@@ -38,10 +38,12 @@ namespace ripple {
#define SF_TRUSTED 0x10 // comes from trusted source
// Private flags, used internally in apply.cpp.
// Do not attempt to read, set, or reuse.
#define SF_PRIVATE1 0x100
#define SF_PRIVATE2 0x200
#define SF_PRIVATE3 0x400
#define SF_PRIVATE4 0x800
#define SF_PRIVATE1 0x0100
#define SF_PRIVATE2 0x0200
#define SF_PRIVATE3 0x0400
#define SF_PRIVATE4 0x0800
#define SF_PRIVATE5 0x1000
#define SF_PRIVATE6 0x2000
/** Routing table for objects identified by hash.

View File

@@ -19,8 +19,11 @@
#include <BeastConfig.h>
#include <ripple/app/tx/impl/SusPay.h>
#include <ripple/app/misc/HashRouter.h>
#include <ripple/basics/chrono.h>
#include <ripple/basics/Log.h>
#include <ripple/conditions/Condition.h>
#include <ripple/conditions/Fulfillment.h>
#include <ripple/protocol/digest.h>
#include <ripple/protocol/st.h>
#include <ripple/protocol/Feature.h>
@@ -29,6 +32,12 @@
#include <ripple/protocol/XRPAmount.h>
#include <ripple/ledger/View.h>
// During a SusPayFinish, the transaction must specify both
// a condition and a fulfillment. We track whether that
// fulfillment matches and validates the condition.
#define SF_CF_INVALID SF_PRIVATE5
#define SF_CF_VALID SF_PRIVATE6
namespace ripple {
/*
@@ -59,8 +68,10 @@ namespace ripple {
Validation rules:
sfDigest
If present, proof is required on a SusPayFinish.
sfCondition
If present, specifies a condition, and the
condition along with its matching fulfillment
is required on a SusPayFinish.
sfCancelAfter
If present, SusPay may be canceled after the
@@ -83,16 +94,9 @@ namespace ripple {
Any account may submit a SusPayFinish. If the SusPay
ledger entry specifies a condition, the SusPayFinish
must provide the sfMethod, original sfDigest, and
sfProof fields. Depending on the method, a
cryptographic operation will be performed on sfProof
and the result must match the sfDigest or else the
SusPayFinish is considered as having an invalid
signature.
Only sfMethod==1 is supported, where sfProof must be a
256-bit unsigned big-endian integer which when hashed
using SHA256 produces digest == sfDigest.
must provide the same condition and its associated
fulfillment in the sfFulfillment field, or else the
SusPayFinish will fail.
If the SusPay ledger entry specifies sfFinishAfter, the
transaction will fail if parentCloseTime <= sfFinishAfter.
@@ -103,8 +107,10 @@ namespace ripple {
If the SusPay ledger entry specifies sfCancelAfter, the
transaction will fail if sfCancelAfter <= parentCloseTime.
NOTE: It must always be possible to verify the condition
without retrieving the SusPay ledger entry.
NOTE: The reason the condition must be specified again
is because it must always be possible to verify
the condition without retrieving the SusPay
ledger entry.
SusPayCancel
@@ -159,32 +165,62 @@ SusPayCreate::preflight (PreflightContext const& ctx)
ctx.tx[sfCancelAfter] <= ctx.tx[sfFinishAfter])
return temBAD_EXPIRATION;
if (auto const cb = ctx.tx[~sfCondition])
{
if (! ctx.rules.enabled(featureCryptoConditions,
ctx.app.config().features))
return temDISABLED;
using namespace ripple::cryptoconditions;
// TODO: Remove this try/catch once cryptoconditions
// no longer use exceptions.
try
{
auto condition = loadCondition(*cb);
if (!condition)
return temMALFORMED;
{
// TODO: This is here temporarily to ensure
// that the condition given doesn't
// contain unnecessary trailing junk.
// The new parsing API will simplify
// the checking here.
auto b = to_blob(*condition);
if (*cb != makeSlice(b))
return temMALFORMED;
}
}
catch (...)
{
return temMALFORMED;
}
}
return preflight2 (ctx);
}
TER
SusPayCreate::doApply()
{
// For now, require that all operations can be
// canceled, or finished without proof, within a
// reasonable period of time for the first release.
using namespace std::chrono;
auto const maxExpire = (ctx_.view().info().parentCloseTime +
weeks{1}).time_since_epoch().count();
if (ctx_.tx[~sfDigest])
auto const closeTime = ctx_.view ().info ().parentCloseTime;
if (ctx_.tx[~sfCancelAfter])
{
if (! ctx_.tx[~sfCancelAfter])
return tecNO_PERMISSION;
if (maxExpire <= ctx_.tx[sfCancelAfter])
auto const cancelAfter = ctx_.tx[sfCancelAfter];
if (closeTime.time_since_epoch().count() >= cancelAfter)
return tecNO_PERMISSION;
}
else
if (ctx_.tx[~sfFinishAfter])
{
if (ctx_.tx[~sfCancelAfter] &&
maxExpire <= ctx_.tx[sfCancelAfter])
return tecNO_PERMISSION;
if (ctx_.tx[~sfFinishAfter] &&
maxExpire <= ctx_.tx[sfFinishAfter])
auto const finishAfter = ctx_.tx[sfFinishAfter];
if (closeTime.time_since_epoch().count() >= finishAfter)
return tecNO_PERMISSION;
}
@@ -223,7 +259,7 @@ SusPayCreate::doApply()
keylet::susPay(account, (*sle)[sfSequence] - 1));
(*slep)[sfAmount] = ctx_.tx[sfAmount];
(*slep)[sfAccount] = account;
(*slep)[~sfDigest] = ctx_.tx[~sfDigest];
(*slep)[~sfCondition] = ctx_.tx[~sfCondition];
(*slep)[~sfSourceTag] = ctx_.tx[~sfSourceTag];
(*slep)[sfDestination] = ctx_.tx[sfDestination];
(*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter];
@@ -253,6 +289,50 @@ SusPayCreate::doApply()
//------------------------------------------------------------------------------
static
bool
checkCondition (Slice f, Slice c)
{
using namespace ripple::cryptoconditions;
// TODO: Remove this try/catch once cryptoconditions
// no longer use exceptions.
try
{
auto condition = loadCondition(c);
if (!condition)
return false;
auto fulfillment = loadFulfillment(f);
if (!fulfillment)
return false;
{
// TODO: This is here temporarily to ensure
// that the condition & fulfillment
// given don't contain unnecessary
// trailing junk. The new parsing API
// will simplify the checking here.
auto cb = to_blob(*condition);
if (c != makeSlice(cb))
return false;
auto fb = to_blob(*fulfillment);
if (f != makeSlice(fb))
return false;
}
return validateTrigger (*fulfillment, *condition);
}
catch (...)
{
return false;
}
}
TER
SusPayFinish::preflight (PreflightContext const& ctx)
{
@@ -260,53 +340,72 @@ SusPayFinish::preflight (PreflightContext const& ctx)
ctx.app.config().features))
return temDISABLED;
auto const ret = preflight1 (ctx);
if (!isTesSuccess (ret))
return ret;
if (ctx.tx[~sfMethod])
{
// Condition
switch(ctx.tx[sfMethod])
{
case 1:
{
if (! ctx.tx[~sfDigest])
return temMALFORMED;
if (! ctx.tx[~sfProof])
return temMALFORMED;
if (ctx.tx[~sfProof]->size() != 32)
return temMALFORMED;
sha256_hasher h;
using beast::hash_append;
hash_append(h, ctx.tx[sfProof]);
uint256 digest;
{
auto const result = static_cast<
sha256_hasher::result_type>(h);
std::memcpy(digest.data(),
result.data(), result.size());
}
if (digest != ctx.tx[sfDigest])
return temBAD_SIGNATURE;
break;
}
default:
return temMALFORMED;
}
}
else
{
// No Condition
if (ctx.tx[~sfDigest])
return temMALFORMED;
if (ctx.tx[~sfProof])
return temMALFORMED;
auto const ret = preflight1 (ctx);
if (!isTesSuccess (ret))
return ret;
}
return preflight2 (ctx);
auto const cb = ctx.tx[~sfCondition];
auto const fb = ctx.tx[~sfFulfillment];
if (cb || fb)
{
if (! ctx.rules.enabled(featureCryptoConditions,
ctx.app.config().features))
return temDISABLED;
}
// If you specify a condition, then you must also specify
// a fulfillment.
if (static_cast<bool>(cb) != static_cast<bool>(fb))
return temMALFORMED;
// Verify the transaction signature. If it doesn't work
// then don't do any more work.
{
auto const ret = preflight2 (ctx);
if (!isTesSuccess (ret))
return ret;
}
if (cb && fb)
{
auto& router = ctx.app.getHashRouter();
auto const id = ctx.tx.getTransactionID();
auto const flags = router.getFlags (id);
// If we haven't checked the condition, check it
// now. Whether it passes or not isn't important
// in preflight.
if (!(flags & (SF_CF_INVALID | SF_CF_VALID)))
{
if (checkCondition (*fb, *cb))
router.setFlags (id, SF_CF_VALID);
else
router.setFlags (id, SF_CF_INVALID);
}
}
return tesSUCCESS;
}
std::uint64_t
SusPayFinish::calculateBaseFee (PreclaimContext const& ctx)
{
std::uint64_t extraFee = 0;
if (auto const fb = ctx.tx[~sfFulfillment])
{
extraFee += ctx.view.fees().units *
(32 + static_cast<std::uint64_t> (fb->size() / 16));
}
return Transactor::calculateBaseFee (ctx) + extraFee;
}
TER
SusPayFinish::doApply()
{
@@ -329,10 +428,52 @@ SusPayFinish::doApply()
ctx_.view().info().parentCloseTime.time_since_epoch().count())
return tecNO_PERMISSION;
// Same digest?
if ((*slep)[~sfDigest] && (! ctx_.tx[~sfMethod] ||
(ctx_.tx[~sfDigest] != (*slep)[~sfDigest])))
return tecNO_PERMISSION;
// Check cryptocondition fulfillment
{
auto const id = ctx_.tx.getTransactionID();
auto flags = ctx_.app.getHashRouter().getFlags (id);
auto const cb = ctx_.tx[~sfCondition];
// It's unlikely that the results of the check will
// expire from the hash router, but if it happens,
// simply re-run the check.
if (cb && ! (flags & (SF_CF_INVALID | SF_CF_VALID)))
{
auto const fb = ctx_.tx[~sfFulfillment];
if (!fb)
return tecINTERNAL;
if (checkCondition (*fb, *cb))
flags = SF_CF_VALID;
else
flags = SF_CF_INVALID;
ctx_.app.getHashRouter().setFlags (id, flags);
}
// If the check failed, then simply return an error
// and don't look at anything else.
if (flags & SF_CF_INVALID)
return tecCRYPTOCONDITION_ERROR;
// Check against condition in the ledger entry:
auto const cond = (*slep)[~sfCondition];
// If a condition wasn't specified during creation,
// one shouldn't be included now.
if (!cond && cb)
return tecCRYPTOCONDITION_ERROR;
// If a condition was specified during creation of
// the suspended payment, the identical condition
// must be presented again. We don't check if the
// fulfillment matches the condition since we did
// that in preflight.
if (cond && (cond != cb))
return tecCRYPTOCONDITION_ERROR;
}
AccountID const account = (*slep)[sfAccount];

View File

@@ -62,6 +62,10 @@ public:
TER
preflight (PreflightContext const& ctx);
static
std::uint64_t
calculateBaseFee (PreclaimContext const& ctx);
TER
doApply() override;
};

View File

@@ -93,7 +93,7 @@ preflight2 (PreflightContext const& ctx)
auto const sigValid = checkValidity(ctx.app.getHashRouter(),
ctx.tx, ctx.rules, ctx.app.config());
if (sigValid.first == Validity::SigBad)
{
{
JLOG(ctx.j.debug()) <<
"preflight2: bad signature. " << sigValid.second;
return temINVALID;

View File

@@ -44,6 +44,7 @@ extern uint256 const featureCompareFlowV1V2;
extern uint256 const featureSHAMapV2;
extern uint256 const featurePayChan;
extern uint256 const featureFlow;
extern uint256 const featureCryptoConditions;
} // ripple

View File

@@ -447,10 +447,12 @@ extern SF_Blob const sfCreateCode;
extern SF_Blob const sfMemoType;
extern SF_Blob const sfMemoData;
extern SF_Blob const sfMemoFormat;
extern SF_Blob const sfMasterSignature;
// variable length (uncommon)
extern SF_Blob const sfProof;
extern SF_Blob const sfFulfillment;
extern SF_Blob const sfCondition;
extern SF_Blob const sfMasterSignature;
// account
extern SF_Account const sfAccount;
extern SF_Account const sfOwner;

View File

@@ -205,6 +205,7 @@ enum TER
tecDST_TAG_NEEDED = 143,
tecINTERNAL = 144,
tecOVERSIZE = 145,
tecCRYPTOCONDITION_ERROR = 146
};
inline bool isTelLocal(TER x)

View File

@@ -55,5 +55,6 @@ uint256 const featureCompareFlowV1V2 = feature("CompareFlowV1V2");
uint256 const featureSHAMapV2 = feature("SHAMapV2");
uint256 const featurePayChan = feature("PayChan");
uint256 const featureFlow = feature("Flow");
uint256 const featureCryptoConditions = feature("CryptoConditions");
} // ripple

View File

@@ -85,7 +85,7 @@ LedgerFormats::LedgerFormats ()
SOElement (sfAccount, SOE_REQUIRED) <<
SOElement (sfDestination, SOE_REQUIRED) <<
SOElement (sfAmount, SOE_REQUIRED) <<
SOElement (sfDigest, SOE_OPTIONAL) <<
SOElement (sfCondition, SOE_OPTIONAL) <<
SOElement (sfCancelAfter, SOE_OPTIONAL) <<
SOElement (sfFinishAfter, SOE_OPTIONAL) <<
SOElement (sfSourceTag, SOE_OPTIONAL) <<

View File

@@ -201,10 +201,11 @@ SF_Blob const sfMemoFormat = make::one<SF_Blob::type>(&sfMemoFormat, STI
// variable length (uncommon)
// 16 has not been used yet...
SF_Blob const sfProof = make::one<SF_Blob::type>(&sfProof, STI_VL, 17, "Proof");
SF_Blob const sfFulfillment = make::one<SF_Blob::type>(&sfFulfillment, STI_VL, 16, "Fulfillment");
SF_Blob const sfCondition = make::one<SF_Blob::type>(&sfCondition, STI_VL, 17, "Condition");
SF_Blob const sfMasterSignature = make::one<SF_Blob::type>(&sfMasterSignature, STI_VL, 18, "MasterSignature", SField::sMD_Default, SField::notSigning);
// account
SF_Account const sfAccount = make::one<SF_Account::type>(&sfAccount, STI_ACCOUNT, 1, "Account");
SF_Account const sfOwner = make::one<SF_Account::type>(&sfOwner, STI_ACCOUNT, 2, "Owner");

View File

@@ -63,6 +63,7 @@ bool transResultInfo (TER code, std::string& token, std::string& text)
{ tecNEED_MASTER_KEY, { "tecNEED_MASTER_KEY", "The operation requires the use of the Master Key." } },
{ tecDST_TAG_NEEDED, { "tecDST_TAG_NEEDED", "A destination tag is required." } },
{ tecINTERNAL, { "tecINTERNAL", "An internal error has occurred during processing." } },
{ tecCRYPTOCONDITION_ERROR, { "tecCRYPTOCONDITION_ERROR", "Malformed, invalid, or mismatched conditional or fulfillment." } },
{ tefALREADY, { "tefALREADY", "The exact transaction was already in this ledger." } },
{ tefBAD_ADD_AUTH, { "tefBAD_ADD_AUTH", "Not authorized to add account." } },

View File

@@ -69,7 +69,7 @@ TxFormats::TxFormats ()
add ("SuspendedPaymentCreate", ttSUSPAY_CREATE) <<
SOElement (sfDestination, SOE_REQUIRED) <<
SOElement (sfAmount, SOE_REQUIRED) <<
SOElement (sfDigest, SOE_OPTIONAL) <<
SOElement (sfCondition, SOE_OPTIONAL) <<
SOElement (sfCancelAfter, SOE_OPTIONAL) <<
SOElement (sfFinishAfter, SOE_OPTIONAL) <<
SOElement (sfDestinationTag, SOE_OPTIONAL);
@@ -77,9 +77,8 @@ TxFormats::TxFormats ()
add ("SuspendedPaymentFinish", ttSUSPAY_FINISH) <<
SOElement (sfOwner, SOE_REQUIRED) <<
SOElement (sfOfferSequence, SOE_REQUIRED) <<
SOElement (sfMethod, SOE_OPTIONAL) <<
SOElement (sfDigest, SOE_OPTIONAL) <<
SOElement (sfProof, SOE_OPTIONAL);
SOElement (sfFulfillment, SOE_OPTIONAL) <<
SOElement (sfCondition, SOE_OPTIONAL);
add ("SuspendedPaymentCancel", ttSUSPAY_CANCEL) <<
SOElement (sfOwner, SOE_REQUIRED) <<

View File

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