From d69b16895c620b9eab9b8d0da654d3b734249612 Mon Sep 17 00:00:00 2001 From: Nik Bougalis Date: Wed, 26 Oct 2016 01:18:31 -0700 Subject: [PATCH] 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. --- src/ripple/app/main/Amendments.cpp | 7 +- src/ripple/app/misc/HashRouter.h | 10 +- src/ripple/app/tx/impl/SusPay.cpp | 295 +++++++--- src/ripple/app/tx/impl/SusPay.h | 4 + src/ripple/app/tx/impl/Transactor.cpp | 2 +- src/ripple/protocol/Feature.h | 1 + src/ripple/protocol/SField.h | 6 +- src/ripple/protocol/TER.h | 1 + src/ripple/protocol/impl/Feature.cpp | 1 + src/ripple/protocol/impl/LedgerFormats.cpp | 2 +- src/ripple/protocol/impl/SField.cpp | 5 +- src/ripple/protocol/impl/TER.cpp | 1 + src/ripple/protocol/impl/TxFormats.cpp | 7 +- src/test/app/SusPay_test.cpp | 654 +++++++++++++++++---- 14 files changed, 772 insertions(+), 224 deletions(-) diff --git a/src/ripple/app/main/Amendments.cpp b/src/ripple/app/main/Amendments.cpp index 8f10fce473..20b676e25b 100644 --- a/src/ripple/app/main/Amendments.cpp +++ b/src/ripple/app/main/Amendments.cpp @@ -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" } }; } diff --git a/src/ripple/app/misc/HashRouter.h b/src/ripple/app/misc/HashRouter.h index adc2d3ab5e..ef8dafe2eb 100644 --- a/src/ripple/app/misc/HashRouter.h +++ b/src/ripple/app/misc/HashRouter.h @@ -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. diff --git a/src/ripple/app/tx/impl/SusPay.cpp b/src/ripple/app/tx/impl/SusPay.cpp index 8f8a198032..f549470e67 100644 --- a/src/ripple/app/tx/impl/SusPay.cpp +++ b/src/ripple/app/tx/impl/SusPay.cpp @@ -19,8 +19,11 @@ #include #include +#include #include #include +#include +#include #include #include #include @@ -29,6 +32,12 @@ #include #include +// 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(cb) != static_cast(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 (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]; diff --git a/src/ripple/app/tx/impl/SusPay.h b/src/ripple/app/tx/impl/SusPay.h index 46c9bc7b72..aa52fae7f0 100644 --- a/src/ripple/app/tx/impl/SusPay.h +++ b/src/ripple/app/tx/impl/SusPay.h @@ -62,6 +62,10 @@ public: TER preflight (PreflightContext const& ctx); + static + std::uint64_t + calculateBaseFee (PreclaimContext const& ctx); + TER doApply() override; }; diff --git a/src/ripple/app/tx/impl/Transactor.cpp b/src/ripple/app/tx/impl/Transactor.cpp index abc282d71d..6d9eed55d5 100644 --- a/src/ripple/app/tx/impl/Transactor.cpp +++ b/src/ripple/app/tx/impl/Transactor.cpp @@ -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; diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 286e259808..4d5a4bc1cb 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -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 diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index ecb86f8e55..be21253365 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -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; diff --git a/src/ripple/protocol/TER.h b/src/ripple/protocol/TER.h index 9a74bc8339..e41dcaf544 100644 --- a/src/ripple/protocol/TER.h +++ b/src/ripple/protocol/TER.h @@ -205,6 +205,7 @@ enum TER tecDST_TAG_NEEDED = 143, tecINTERNAL = 144, tecOVERSIZE = 145, + tecCRYPTOCONDITION_ERROR = 146 }; inline bool isTelLocal(TER x) diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index ff4112a900..a3d4814dc7 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -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 diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index 062e40ac13..0a83ff7c8c 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -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) << diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index fa7fbff10e..98c7cf90c2 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -201,10 +201,11 @@ SF_Blob const sfMemoFormat = make::one(&sfMemoFormat, STI // variable length (uncommon) -// 16 has not been used yet... -SF_Blob const sfProof = make::one(&sfProof, STI_VL, 17, "Proof"); +SF_Blob const sfFulfillment = make::one(&sfFulfillment, STI_VL, 16, "Fulfillment"); +SF_Blob const sfCondition = make::one(&sfCondition, STI_VL, 17, "Condition"); SF_Blob const sfMasterSignature = make::one(&sfMasterSignature, STI_VL, 18, "MasterSignature", SField::sMD_Default, SField::notSigning); + // account SF_Account const sfAccount = make::one(&sfAccount, STI_ACCOUNT, 1, "Account"); SF_Account const sfOwner = make::one(&sfOwner, STI_ACCOUNT, 2, "Owner"); diff --git a/src/ripple/protocol/impl/TER.cpp b/src/ripple/protocol/impl/TER.cpp index 8a5b2c42ba..458b1c7e98 100644 --- a/src/ripple/protocol/impl/TER.cpp +++ b/src/ripple/protocol/impl/TER.cpp @@ -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." } }, diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index 2263d660ec..982ed1c06a 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -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) << diff --git a/src/test/app/SusPay_test.cpp b/src/test/app/SusPay_test.cpp index 62445cc581..efb3488394 100644 --- a/src/test/app/SusPay_test.cpp +++ b/src/test/app/SusPay_test.cpp @@ -20,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -31,38 +30,72 @@ namespace test { struct SusPay_test : public beast::unit_test::suite { - template - 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 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 - cond (std::string const& receipt) - { - std::pair result; - result.second = digest(receipt); - result.first = digest(result.second); - return result; - } + std::array 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 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 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 const fb3 = + {{ + 0x00, 0x01, 0x04, 0x00, 0x00, 0x00, 0x00, + }}; + + std::array 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 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 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( - "0102030405060708090A0B0C0D0E0F"); - auto const d = digest(p); + + std::vector 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 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);