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" }, { "DA1BD556B42D85EA9C84066D028D355B52416734D3283F85E216EA5DA6DB7E13 SusPay" },
{ "6781F8368C4771B83E8B821D88F580202BCB4228075297B19E4FDC5233F1EFDC TrustSetAuth" }, { "6781F8368C4771B83E8B821D88F580202BCB4228075297B19E4FDC5233F1EFDC TrustSetAuth" },
{ "42426C4D4F1009EE67080A9B7965B44656D7714D104A72F9B4369F97ABF044EE FeeEscalation" }, { "42426C4D4F1009EE67080A9B7965B44656D7714D104A72F9B4369F97ABF044EE FeeEscalation" },
{ "9178256A980A86CF3D70D0260A7DA6402AAFE43632FDBCB88037978404188871 OwnerPaysFee"}, { "9178256A980A86CF3D70D0260A7DA6402AAFE43632FDBCB88037978404188871 OwnerPaysFee" },
{ "08DE7D96082187F6E6578530258C77FAABABE4C20474BDB82F04B021F1A68647 PayChan"}, { "08DE7D96082187F6E6578530258C77FAABABE4C20474BDB82F04B021F1A68647 PayChan" },
{ "740352F2412A9909880C23A559FCECEDA3BE2126FED62FC7660D628A06927F11 Flow"} { "740352F2412A9909880C23A559FCECEDA3BE2126FED62FC7660D628A06927F11 Flow" },
{ "1562511F573A19AE9BD103B5D6B9E01B3B46805AEC5D3C4805C902B514399146 CryptoConditions" }
}; };
} }

View File

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

View File

@@ -19,8 +19,11 @@
#include <BeastConfig.h> #include <BeastConfig.h>
#include <ripple/app/tx/impl/SusPay.h> #include <ripple/app/tx/impl/SusPay.h>
#include <ripple/app/misc/HashRouter.h>
#include <ripple/basics/chrono.h> #include <ripple/basics/chrono.h>
#include <ripple/basics/Log.h> #include <ripple/basics/Log.h>
#include <ripple/conditions/Condition.h>
#include <ripple/conditions/Fulfillment.h>
#include <ripple/protocol/digest.h> #include <ripple/protocol/digest.h>
#include <ripple/protocol/st.h> #include <ripple/protocol/st.h>
#include <ripple/protocol/Feature.h> #include <ripple/protocol/Feature.h>
@@ -29,6 +32,12 @@
#include <ripple/protocol/XRPAmount.h> #include <ripple/protocol/XRPAmount.h>
#include <ripple/ledger/View.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 { namespace ripple {
/* /*
@@ -59,8 +68,10 @@ namespace ripple {
Validation rules: Validation rules:
sfDigest sfCondition
If present, proof is required on a SusPayFinish. If present, specifies a condition, and the
condition along with its matching fulfillment
is required on a SusPayFinish.
sfCancelAfter sfCancelAfter
If present, SusPay may be canceled after the If present, SusPay may be canceled after the
@@ -83,16 +94,9 @@ namespace ripple {
Any account may submit a SusPayFinish. If the SusPay Any account may submit a SusPayFinish. If the SusPay
ledger entry specifies a condition, the SusPayFinish ledger entry specifies a condition, the SusPayFinish
must provide the sfMethod, original sfDigest, and must provide the same condition and its associated
sfProof fields. Depending on the method, a fulfillment in the sfFulfillment field, or else the
cryptographic operation will be performed on sfProof SusPayFinish will fail.
and the result must match the sfDigest or else the
SusPayFinish is considered as having an invalid
signature.
Only sfMethod==1 is supported, where sfProof must be a
256-bit unsigned big-endian integer which when hashed
using SHA256 produces digest == sfDigest.
If the SusPay ledger entry specifies sfFinishAfter, the If the SusPay ledger entry specifies sfFinishAfter, the
transaction will fail if parentCloseTime <= sfFinishAfter. transaction will fail if parentCloseTime <= sfFinishAfter.
@@ -103,8 +107,10 @@ namespace ripple {
If the SusPay ledger entry specifies sfCancelAfter, the If the SusPay ledger entry specifies sfCancelAfter, the
transaction will fail if sfCancelAfter <= parentCloseTime. transaction will fail if sfCancelAfter <= parentCloseTime.
NOTE: It must always be possible to verify the condition NOTE: The reason the condition must be specified again
without retrieving the SusPay ledger entry. is because it must always be possible to verify
the condition without retrieving the SusPay
ledger entry.
SusPayCancel SusPayCancel
@@ -159,32 +165,62 @@ SusPayCreate::preflight (PreflightContext const& ctx)
ctx.tx[sfCancelAfter] <= ctx.tx[sfFinishAfter]) ctx.tx[sfCancelAfter] <= ctx.tx[sfFinishAfter])
return temBAD_EXPIRATION; 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); return preflight2 (ctx);
} }
TER TER
SusPayCreate::doApply() SusPayCreate::doApply()
{ {
// For now, require that all operations can be auto const closeTime = ctx_.view ().info ().parentCloseTime;
// canceled, or finished without proof, within a
// reasonable period of time for the first release. if (ctx_.tx[~sfCancelAfter])
using namespace std::chrono;
auto const maxExpire = (ctx_.view().info().parentCloseTime +
weeks{1}).time_since_epoch().count();
if (ctx_.tx[~sfDigest])
{ {
if (! ctx_.tx[~sfCancelAfter]) auto const cancelAfter = ctx_.tx[sfCancelAfter];
return tecNO_PERMISSION;
if (maxExpire <= ctx_.tx[sfCancelAfter]) if (closeTime.time_since_epoch().count() >= cancelAfter)
return tecNO_PERMISSION; return tecNO_PERMISSION;
} }
else
if (ctx_.tx[~sfFinishAfter])
{ {
if (ctx_.tx[~sfCancelAfter] && auto const finishAfter = ctx_.tx[sfFinishAfter];
maxExpire <= ctx_.tx[sfCancelAfter])
return tecNO_PERMISSION; if (closeTime.time_since_epoch().count() >= finishAfter)
if (ctx_.tx[~sfFinishAfter] &&
maxExpire <= ctx_.tx[sfFinishAfter])
return tecNO_PERMISSION; return tecNO_PERMISSION;
} }
@@ -223,7 +259,7 @@ SusPayCreate::doApply()
keylet::susPay(account, (*sle)[sfSequence] - 1)); keylet::susPay(account, (*sle)[sfSequence] - 1));
(*slep)[sfAmount] = ctx_.tx[sfAmount]; (*slep)[sfAmount] = ctx_.tx[sfAmount];
(*slep)[sfAccount] = account; (*slep)[sfAccount] = account;
(*slep)[~sfDigest] = ctx_.tx[~sfDigest]; (*slep)[~sfCondition] = ctx_.tx[~sfCondition];
(*slep)[~sfSourceTag] = ctx_.tx[~sfSourceTag]; (*slep)[~sfSourceTag] = ctx_.tx[~sfSourceTag];
(*slep)[sfDestination] = ctx_.tx[sfDestination]; (*slep)[sfDestination] = ctx_.tx[sfDestination];
(*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter]; (*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 TER
SusPayFinish::preflight (PreflightContext const& ctx) SusPayFinish::preflight (PreflightContext const& ctx)
{ {
@@ -260,53 +340,72 @@ SusPayFinish::preflight (PreflightContext const& ctx)
ctx.app.config().features)) ctx.app.config().features))
return temDISABLED; return temDISABLED;
auto const ret = preflight1 (ctx);
if (!isTesSuccess (ret))
return ret;
if (ctx.tx[~sfMethod])
{ {
// Condition auto const ret = preflight1 (ctx);
switch(ctx.tx[sfMethod]) if (!isTesSuccess (ret))
{ return ret;
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;
} }
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 TER
SusPayFinish::doApply() SusPayFinish::doApply()
{ {
@@ -329,10 +428,52 @@ SusPayFinish::doApply()
ctx_.view().info().parentCloseTime.time_since_epoch().count()) ctx_.view().info().parentCloseTime.time_since_epoch().count())
return tecNO_PERMISSION; return tecNO_PERMISSION;
// Same digest? // Check cryptocondition fulfillment
if ((*slep)[~sfDigest] && (! ctx_.tx[~sfMethod] || {
(ctx_.tx[~sfDigest] != (*slep)[~sfDigest]))) auto const id = ctx_.tx.getTransactionID();
return tecNO_PERMISSION; 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]; AccountID const account = (*slep)[sfAccount];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -85,7 +85,7 @@ LedgerFormats::LedgerFormats ()
SOElement (sfAccount, SOE_REQUIRED) << SOElement (sfAccount, SOE_REQUIRED) <<
SOElement (sfDestination, SOE_REQUIRED) << SOElement (sfDestination, SOE_REQUIRED) <<
SOElement (sfAmount, SOE_REQUIRED) << SOElement (sfAmount, SOE_REQUIRED) <<
SOElement (sfDigest, SOE_OPTIONAL) << SOElement (sfCondition, SOE_OPTIONAL) <<
SOElement (sfCancelAfter, SOE_OPTIONAL) << SOElement (sfCancelAfter, SOE_OPTIONAL) <<
SOElement (sfFinishAfter, SOE_OPTIONAL) << SOElement (sfFinishAfter, SOE_OPTIONAL) <<
SOElement (sfSourceTag, 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) // variable length (uncommon)
// 16 has not been used yet... SF_Blob const sfFulfillment = make::one<SF_Blob::type>(&sfFulfillment, STI_VL, 16, "Fulfillment");
SF_Blob const sfProof = make::one<SF_Blob::type>(&sfProof, STI_VL, 17, "Proof"); 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); SF_Blob const sfMasterSignature = make::one<SF_Blob::type>(&sfMasterSignature, STI_VL, 18, "MasterSignature", SField::sMD_Default, SField::notSigning);
// account // account
SF_Account const sfAccount = make::one<SF_Account::type>(&sfAccount, STI_ACCOUNT, 1, "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"); 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." } }, { 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." } }, { tecDST_TAG_NEEDED, { "tecDST_TAG_NEEDED", "A destination tag is required." } },
{ tecINTERNAL, { "tecINTERNAL", "An internal error has occurred during processing." } }, { 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." } }, { tefALREADY, { "tefALREADY", "The exact transaction was already in this ledger." } },
{ tefBAD_ADD_AUTH, { "tefBAD_ADD_AUTH", "Not authorized to add account." } }, { tefBAD_ADD_AUTH, { "tefBAD_ADD_AUTH", "Not authorized to add account." } },

View File

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

View File

@@ -20,7 +20,6 @@
#include <BeastConfig.h> #include <BeastConfig.h>
#include <ripple/test/jtx.h> #include <ripple/test/jtx.h>
#include <ripple/app/tx/applySteps.h> #include <ripple/app/tx/applySteps.h>
#include <ripple/protocol/digest.h>
#include <ripple/protocol/Feature.h> #include <ripple/protocol/Feature.h>
#include <ripple/protocol/Indexes.h> #include <ripple/protocol/Indexes.h>
#include <ripple/protocol/JsonFields.h> #include <ripple/protocol/JsonFields.h>
@@ -31,38 +30,72 @@ namespace test {
struct SusPay_test : public beast::unit_test::suite struct SusPay_test : public beast::unit_test::suite
{ {
template <class... Args> // An Ed25519 conditional trigger fulfillment and its
static // condition
uint256 std::array<std::uint8_t, 99> const fb1 =
digest (Args&&... args) {{
{ 0x00, 0x04, 0x60, 0x3B, 0x6A, 0x27, 0xBC, 0xCE, 0xB6, 0xA4, 0x2D, 0x62,
sha256_hasher h; 0xA3, 0xA8, 0xD0, 0x2A, 0x6F, 0x0D, 0x73, 0x65, 0x32, 0x15, 0x77, 0x1D,
using beast::hash_append; 0xE2, 0x43, 0xA6, 0x3A, 0xC0, 0x48, 0xA1, 0x8B, 0x59, 0xDA, 0x29, 0x8F,
hash_append(h, args...); 0x89, 0x5B, 0x3C, 0xAF, 0xE2, 0xC9, 0x50, 0x60, 0x39, 0xD0, 0xE2, 0xA6,
auto const d = static_cast< 0x63, 0x82, 0x56, 0x80, 0x04, 0x67, 0x4F, 0xE8, 0xD2, 0x37, 0x78, 0x50,
sha256_hasher::result_type>(h); 0x92, 0xE4, 0x0D, 0x6A, 0xAF, 0x48, 0x3E, 0x4F, 0xC6, 0x01, 0x68, 0x70,
uint256 result; 0x5F, 0x31, 0xF1, 0x01, 0x59, 0x61, 0x38, 0xCE, 0x21, 0xAA, 0x35, 0x7C,
std::memcpy(result.data(), d.data(), d.size()); 0x0D, 0x32, 0xA0, 0x64, 0xF4, 0x23, 0xDC, 0x3E, 0xE4, 0xAA, 0x3A, 0xBF,
return result; 0x53, 0xF8, 0x03,
} }};
// Create condition std::array<std::uint8_t, 39> const cb1 =
// First is digest, second is pre-image {{
static 0x00, 0x04, 0x01, 0x20, 0x20, 0x3B, 0x6A, 0x27, 0xBC, 0xCE, 0xB6, 0xA4,
std::pair<uint256, uint256> 0x2D, 0x62, 0xA3, 0xA8, 0xD0, 0x2A, 0x6F, 0x0D, 0x73, 0x65, 0x32, 0x15,
cond (std::string const& receipt) 0x77, 0x1D, 0xE2, 0x43, 0xA6, 0x3A, 0xC0, 0x48, 0xA1, 0x8B, 0x59, 0xDA,
{ 0x29, 0x01, 0x60
std::pair<uint256, uint256> result; }};
result.second = digest(receipt);
result.first = digest(result.second); // A prefix.prefix.ed25519 conditional trigger fulfillment:
return result; 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 static
Json::Value Json::Value
condpay (jtx::Account const& account, jtx::Account const& to, condpay (jtx::Account const& account, jtx::Account const& to,
STAmount const& amount, uint256 const& digest, STAmount const& amount, Slice condition,
NetClock::time_point const& expiry) NetClock::time_point const& cancelAfter)
{ {
using namespace jtx; using namespace jtx;
Json::Value jv; Json::Value jv;
@@ -72,8 +105,20 @@ struct SusPay_test : public beast::unit_test::suite
jv[jss::Destination] = to.human(); jv[jss::Destination] = to.human();
jv[jss::Amount] = amount.getJson(0); jv[jss::Amount] = amount.getJson(0);
jv["CancelAfter"] = jv["CancelAfter"] =
expiry.time_since_epoch().count(); cancelAfter.time_since_epoch().count();
jv["Digest"] = to_string(digest); 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; return jv;
} }
@@ -94,6 +139,25 @@ struct SusPay_test : public beast::unit_test::suite
return jv; 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 static
Json::Value Json::Value
finish (jtx::Account const& account, finish (jtx::Account const& account,
@@ -108,12 +172,11 @@ struct SusPay_test : public beast::unit_test::suite
return jv; return jv;
} }
template <class Proof>
static static
Json::Value Json::Value
finish (jtx::Account const& account, finish (jtx::Account const& account,
jtx::Account const& from, std::uint32_t seq, jtx::Account const& from, std::uint32_t seq,
uint256 const& digest, Proof const& proof) Slice condition, Slice fulfillment)
{ {
Json::Value jv; Json::Value jv;
jv[jss::TransactionType] = "SuspendedPaymentFinish"; jv[jss::TransactionType] = "SuspendedPaymentFinish";
@@ -121,9 +184,8 @@ struct SusPay_test : public beast::unit_test::suite
jv[jss::Account] = account.human(); jv[jss::Account] = account.human();
jv["Owner"] = from.human(); jv["Owner"] = from.human();
jv["OfferSequence"] = seq; jv["OfferSequence"] = seq;
jv["Method"] = 1; jv["Condition"] = strHex(condition);
jv["Digest"] = to_string(digest); jv["Fulfillment"] = strHex(fulfillment);
jv["Proof"] = to_string(proof);
return jv; return jv;
} }
@@ -144,46 +206,100 @@ struct SusPay_test : public beast::unit_test::suite
void void
testEnablement() testEnablement()
{ {
testcase ("Enablement");
using namespace jtx; using namespace jtx;
using namespace std::chrono; using namespace std::chrono;
using S = seconds;
auto const c = cond("receipt"); { // SusPay enabled
{
Env env(*this, features(featureSusPay)); Env env(*this, features(featureSusPay));
auto T = [&env](NetClock::duration const& d)
{ return env.now() + d; };
env.fund(XRP(5000), "alice", "bob"); env.fund(XRP(5000), "alice", "bob");
// syntax env(lockup("alice", "bob", XRP(1000), env.now() + 1s));
env(condpay("alice", "bob", XRP(1000), c.first, T(S{1})));
} }
{
{ // SusPay not enabled
Env env(*this); Env env(*this);
auto T = [&env](NetClock::duration const& d)
{ return env.now() + d; };
env.fund(XRP(5000), "alice", "bob"); env.fund(XRP(5000), "alice", "bob");
// disabled in production env(lockup("alice", "bob", XRP(1000), env.now() + 1s), ter(temDISABLED));
env(condpay("alice", "bob", XRP(1000), c.first, T(S{1})), ter(temDISABLED)); env(finish("bob", "alice", 1), ter(temDISABLED));
env(finish("bob", "alice", 1), ter(temDISABLED)); env(cancel("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 void
testTags() testTags()
{ {
testcase ("Tags");
using namespace jtx; using namespace jtx;
using namespace std::chrono; 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 const alice = Account("alice");
auto T = [&env](NetClock::duration const& d)
{ return env.now() + d; };
env.fund(XRP(5000), alice, "bob"); env.fund(XRP(5000), alice, "bob");
auto const c = cond("receipt");
auto const seq = env.seq(alice); auto const seq = env.seq(alice);
// set source and dest tags // 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)); auto const sle = env.le(keylet::susPay(alice.id(), seq));
BEAST_EXPECT((*sle)[sfSourceTag] == 1); BEAST_EXPECT((*sle)[sfSourceTag] == 1);
BEAST_EXPECT((*sle)[sfDestinationTag] == 2); BEAST_EXPECT((*sle)[sfDestinationTag] == 2);
@@ -193,80 +309,203 @@ struct SusPay_test : public beast::unit_test::suite
void void
testFails() testFails()
{ {
testcase ("Failure Cases");
using namespace jtx; using namespace jtx;
using namespace std::chrono; 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 j1 = lockup("alice", "carol", XRP(1), env.now() + 1s);
auto T = [&env](NetClock::duration const& d) j1.removeMember ("FinishAfter");
{ return env.now() + d; }; env (j1, ter(temBAD_EXPIRATION));
env.fund(XRP(5000), "alice", "bob");
auto const c = cond("receipt"); auto j2 = condpay("alice", "carol", XRP(1), makeSlice(cb1), env.now() + 1s);
// VFALCO Should we enforce this? j2.removeMember ("CancelAfter");
// expiration in the past env (j2, ter(temBAD_EXPIRATION));
//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)); // Fail if FinishAfter has already passed:
// no destination account env (lockup("alice", "carol", XRP(1), env.now() - 1s), ter (tecNO_PERMISSION));
env(condpay("alice", "carol", XRP(1000), c.first, T(S{1})), ter(tecNO_DST));
env.fund(XRP(5000), "carol"); // Both CancelAfter and FinishAfter
env(condpay("alice", "carol", env(condpay("alice", "carol", XRP(1), makeSlice(cb1),
XRP(1000), c.first, T(S{1})), stag(2)); env.now() + 10s, env.now() + 10s), ter (temBAD_EXPIRATION));
env(condpay("alice", "carol", env(condpay("alice", "carol", XRP(1), makeSlice(cb1),
XRP(1000), c.first, T(S{1})), stag(3), dtag(4)); env.now() + 10s, env.now() + 15s), ter (temBAD_EXPIRATION));
env(fset("carol", asfRequireDest));
// missing destination tag // Fail if the sender wants to send more than he has:
env(condpay("alice", "carol", XRP(1000), c.first, T(S{1})), ter(tecDST_TAG_NEEDED)); auto const accountReserve =
env(condpay("alice", "carol", drops(env.current()->fees().reserve);
XRP(1000), c.first, T(S{1})), dtag(1)); 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 void
testLockup() testLockup()
{ {
testcase ("Lockup");
using namespace jtx; using namespace jtx;
using namespace std::chrono; using namespace std::chrono;
using S = seconds;
{ { // Unconditional
Env env(*this, features(featureSusPay)); Env env(*this, features(featureSusPay));
auto T = [&env](NetClock::duration const& d)
{ return env.now() + d; };
env.fund(XRP(5000), "alice", "bob"); env.fund(XRP(5000), "alice", "bob");
auto const seq = env.seq("alice"); 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.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.close();
env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION));
env(cancel("bob", "alice", seq), ter(tecNO_PERMISSION));
env(finish("bob", "alice", seq)); 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 void
testCondPay() testCondPay()
{ {
testcase ("Conditional Payments");
using namespace jtx; using namespace jtx;
using namespace std::chrono; using namespace std::chrono;
using S = seconds; using S = seconds;
{
Env env(*this, features(featureSusPay)); { // Test cryptoconditions
Env env(*this,
features(featureSusPay),
features(featureCryptoConditions));
auto T = [&env](NetClock::duration const& d) auto T = [&env](NetClock::duration const& d)
{ return env.now() + d; }; { return env.now() + d; };
env.fund(XRP(5000), "alice", "bob", "carol"); env.fund(XRP(5000), "alice", "bob", "carol");
auto const c = cond("receipt");
auto const seq = env.seq("alice"); auto const seq = env.seq("alice");
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); 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); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
env.require(balance("alice", XRP(4000) - drops(10))); env.require(balance("alice", XRP(4000) - drops(10)));
env.require(balance("carol", XRP(5000))); 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); 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); 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 // SLE removed on finish
BEAST_EXPECT(! env.le(keylet::susPay(Account("alice").id(), seq))); BEAST_EXPECT(! env.le(keylet::susPay(Account("alice").id(), seq)));
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); 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(cancel("bob", "carol", 1), ter(tecNO_TARGET));
env.close(); 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) auto T = [&env](NetClock::duration const& d)
{ return env.now() + d; }; { return env.now() + d; };
env.fund(XRP(5000), "alice", "bob", "carol"); env.fund(XRP(5000), "alice", "bob", "carol");
auto const c = cond("receipt");
auto const seq = env.seq("alice"); auto const seq = env.seq("alice");
BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 0); 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.close();
env.require(balance("alice", XRP(4000) - drops(10))); env.require(balance("alice", XRP(4000) - drops(10)));
// balance restored on cancel // balance restored on cancel
@@ -293,83 +534,235 @@ struct SusPay_test : public beast::unit_test::suite
// SLE removed on cancel // SLE removed on cancel
BEAST_EXPECT(! env.le(keylet::susPay(Account("alice").id(), seq))); 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) auto T = [&env](NetClock::duration const& d)
{ return env.now() + d; }; { return env.now() + d; };
env.fund(XRP(5000), "alice", "bob", "carol"); env.fund(XRP(5000), "alice", "bob", "carol");
env.close(); env.close();
auto const c = cond("receipt");
auto const seq = env.seq("alice"); 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); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
// cancel fails before expiration // 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); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
env.close(); env.close();
// finish fails after expiration // 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); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1);
env.require(balance("carol", XRP(5000))); 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) auto T = [&env](NetClock::duration const& d)
{ return env.now() + d; }; { return env.now() + d; };
env.fund(XRP(5000), "alice", "bob", "carol"); 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"); auto const seq = env.seq("alice");
env(condpay("alice", "carol", XRP(1000), c.first, T(S{1}))); env(condpay("alice", "carol", XRP(1000),
// wrong digest Slice{p + 1, s - 2}, T(S{1})), fee(100));
auto const cx = cond("bad"); env(finish("bob", "alice", seq,
env(finish("bob", "alice", seq, cx.first, cx.second), ter(tecNO_PERMISSION)); 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) auto T = [&env](NetClock::duration const& d)
{ return env.now() + d; }; { return env.now() + d; };
env.fund(XRP(5000), "alice", "bob", "carol"); env.fund(XRP(5000), "alice", "bob", "carol");
auto const p = from_hex_text<uint128>(
"0102030405060708090A0B0C0D0E0F"); std::vector<std::uint8_t> cv;
auto const d = digest(p); 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"); auto const seq = env.seq("alice");
env(condpay("alice", "carol", XRP(1000), d, T(S{1}))); env(condpay("alice", "carol", XRP(1000),
// bad digest size Slice{cp + 1, cs - 2}, T(S{1})), fee(100));
env(finish("bob", "alice", seq, d, p), ter(temMALFORMED));
// 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 void
testMeta() testMeta()
{ {
testcase ("Metadata");
using namespace jtx; using namespace jtx;
using namespace std::chrono; using namespace std::chrono;
Env env(*this, features(featureSusPay)); Env env(*this,
auto T = [&env](NetClock::duration const& d) features(featureSusPay),
{ return env.now() + d; }; features(featureCryptoConditions));
env.fund(XRP(5000), "alice", "bob", "carol"); env.fund(XRP(5000), "alice", "bob", "carol");
auto const c = cond("receipt"); env(condpay("alice", "carol", XRP(1000), makeSlice(cb1), env.now() + 1s));
env(condpay("alice", "carol", XRP(1000), c.first, T(1s)));
auto const m = env.meta(); auto const m = env.meta();
BEAST_EXPECT((*m)[sfTransactionResult] == tesSUCCESS); BEAST_EXPECT((*m)[sfTransactionResult] == tesSUCCESS);
} }
void testConsequences() void testConsequences()
{ {
testcase ("Consequences");
using namespace jtx; using namespace jtx;
using namespace std::chrono; using namespace std::chrono;
Env env(*this, features(featureSusPay)); Env env(*this,
auto T = [&env](NetClock::duration const& d) features(featureSusPay),
{ features(featureCryptoConditions));
return env.now() + d;
};
env.memoize("alice"); env.memoize("alice");
env.memoize("bob"); env.memoize("bob");
env.memoize("carol"); env.memoize("carol");
auto const c = cond("receipt");
{ {
auto const jtx = env.jt( 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)); seq(1), fee(10));
auto const pf = preflight(env.app(), env.current()->rules(), auto const pf = preflight(env.app(), env.current()->rules(),
*jtx.stx, tapNONE, env.journal); *jtx.stx, tapNONE, env.journal);
@@ -394,7 +787,8 @@ struct SusPay_test : public beast::unit_test::suite
{ {
auto const jtx = env.jt( 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)); seq(1), fee(10));
auto const pf = preflight(env.app(), env.current()->rules(), auto const pf = preflight(env.app(), env.current()->rules(),
*jtx.stx, tapNONE, env.journal); *jtx.stx, tapNONE, env.journal);