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