diff --git a/include/xrpl/protocol/Batch.h b/include/xrpl/protocol/Batch.h new file mode 100644 index 00000000000..1388bbd2f15 --- /dev/null +++ b/include/xrpl/protocol/Batch.h @@ -0,0 +1,37 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include + +namespace ripple { + +inline void +serializeBatch( + Serializer& msg, + std::uint32_t const& flags, + std::vector const& txids) +{ + msg.add32(HashPrefix::batch); + msg.add32(flags); + msg.add32(std::uint32_t(txids.size())); + for (auto const& txid : txids) + msg.addBitString(txid); +} + +} // namespace ripple \ No newline at end of file diff --git a/include/xrpl/protocol/Feature.h b/include/xrpl/protocol/Feature.h index 369ec3304ef..bff3e57597f 100644 --- a/include/xrpl/protocol/Feature.h +++ b/include/xrpl/protocol/Feature.h @@ -80,7 +80,7 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -static constexpr std::size_t numFeatures = 85; +static constexpr std::size_t numFeatures = 86; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated diff --git a/include/xrpl/protocol/HashPrefix.h b/include/xrpl/protocol/HashPrefix.h index 0b6ddda4921..6adecece4fe 100644 --- a/include/xrpl/protocol/HashPrefix.h +++ b/include/xrpl/protocol/HashPrefix.h @@ -87,6 +87,9 @@ enum class HashPrefix : std::uint32_t { /** Credentials signature */ credential = detail::make_hash_prefix('C', 'R', 'D'), + + /** Batch */ + batch = detail::make_hash_prefix('B', 'C', 'H'), }; template diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 81a45c383fb..e2da39c6509 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -154,6 +154,9 @@ std::size_t constexpr maxPriceScale = 20; */ std::size_t constexpr maxTrim = 25; +/** The maximum number of transactions that can be in a batch. */ +std::size_t constexpr maxBatchTxCount = 8; + } // namespace ripple #endif diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index 4c8db2e01e4..ed427cc509b 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -247,6 +247,8 @@ class STObject : public STBase, public CountedObject getFieldCurrency(SField const& field) const; STNumber const& getFieldNumber(SField const& field) const; + std::vector + getBatchTransactionIDs() const; /** Get the value of a field. @param A TypedField built from an SField value representing the desired @@ -475,6 +477,7 @@ class STObject : public STBase, public CountedObject move(std::size_t n, void* buf) override; friend class detail::STVar; + mutable std::vector batch_txn_ids_; }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h index 08b9a1bad10..627a3518d42 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -120,10 +120,16 @@ class STTx final : public STObject, public CountedObject @return `true` if valid signature. If invalid, the error message string. */ enum class RequireFullyCanonicalSig : bool { no, yes }; + Expected checkSign(RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules) const; + Expected + checkBatchSign( + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const; + // SQL Functions with metadata. static std::string const& getMetaSQLInsertReplaceHeader(); @@ -148,6 +154,17 @@ class STTx final : public STObject, public CountedObject RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules) const; + Expected + checkBatchSingleSign( + STObject const& batchSigner, + RequireFullyCanonicalSig requireCanonicalSig) const; + + Expected + checkBatchMultiSign( + STObject const& batchSigner, + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const; + STBase* copy(std::size_t n, void* buf) const override; STBase* diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 317e9c2c978..cd6fad31961 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -139,8 +139,8 @@ enum TEMcodes : TERUnderlyingType { temARRAY_EMPTY, temARRAY_TOO_LARGE, - temBAD_TRANSFER_FEE, + temINVALID_BATCH, }; //------------------------------------------------------------------------------ @@ -343,7 +343,7 @@ enum TECcodes : TERUnderlyingType { tecARRAY_EMPTY = 190, tecARRAY_TOO_LARGE = 191, tecLOCKED = 192, - tecBAD_CREDENTIALS = 193, + tecBAD_CREDENTIALS = 193 }; //------------------------------------------------------------------------------ @@ -629,37 +629,37 @@ using TER = TERSubset; //------------------------------------------------------------------------------ inline bool -isTelLocal(TER x) +isTelLocal(TER x) noexcept { - return ((x) >= telLOCAL_ERROR && (x) < temMALFORMED); + return (x >= telLOCAL_ERROR && x < temMALFORMED); } inline bool -isTemMalformed(TER x) +isTemMalformed(TER x) noexcept { - return ((x) >= temMALFORMED && (x) < tefFAILURE); + return (x >= temMALFORMED && x < tefFAILURE); } inline bool -isTefFailure(TER x) +isTefFailure(TER x) noexcept { - return ((x) >= tefFAILURE && (x) < terRETRY); + return (x >= tefFAILURE && x < terRETRY); } inline bool -isTerRetry(TER x) +isTerRetry(TER x) noexcept { - return ((x) >= terRETRY && (x) < tesSUCCESS); + return (x >= terRETRY && x < tesSUCCESS); } inline bool -isTesSuccess(TER x) +isTesSuccess(TER x) noexcept { - return ((x) == tesSUCCESS); + return (x == tesSUCCESS); } inline bool -isTecClaim(TER x) +isTecClaim(TER x) noexcept { return ((x) >= tecCLAIM); } diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 9502db7e3ea..3ced3007181 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -58,8 +58,9 @@ namespace ripple { // clang-format off // Universal Transaction flags: constexpr std::uint32_t tfFullyCanonicalSig = 0x80000000; -constexpr std::uint32_t tfUniversal = tfFullyCanonicalSig; -constexpr std::uint32_t tfUniversalMask = ~tfUniversal; +constexpr std::uint32_t tfInnerBatchTxn = 0x40000000; +constexpr std::uint32_t tfUniversal = tfFullyCanonicalSig | tfInnerBatchTxn; +constexpr std::uint32_t tfUniversalMask = ~(tfFullyCanonicalSig | tfInnerBatchTxn); // AccountSet flags: constexpr std::uint32_t tfRequireDestTag = 0x00010000; @@ -97,6 +98,7 @@ constexpr std::uint32_t tfPassive = 0x00010000; constexpr std::uint32_t tfImmediateOrCancel = 0x00020000; constexpr std::uint32_t tfFillOrKill = 0x00040000; constexpr std::uint32_t tfSell = 0x00080000; + constexpr std::uint32_t tfOfferCreateMask = ~(tfUniversal | tfPassive | tfImmediateOrCancel | tfFillOrKill | tfSell); @@ -189,7 +191,7 @@ constexpr std::uint32_t const tfNFTokenCreateOfferMask = ~(tfUniversal | tfSellNFToken); // NFTokenCancelOffer flags: -constexpr std::uint32_t const tfNFTokenCancelOfferMask = ~(tfUniversal); +constexpr std::uint32_t const tfNFTokenCancelOfferMask = ~tfUniversal; // NFTokenAcceptOffer flags: constexpr std::uint32_t const tfNFTokenAcceptOfferMask = ~tfUniversal; @@ -222,6 +224,15 @@ constexpr std::uint32_t tfAMMClawbackMask = ~(tfUniversal | tfClawTwoAssets); // BridgeModify flags: constexpr std::uint32_t tfClearAccountCreateAmount = 0x00010000; constexpr std::uint32_t tfBridgeModifyMask = ~(tfUniversal | tfClearAccountCreateAmount); + +// Batch Flags: +constexpr std::uint32_t tfAllOrNothing = 0x00010000; +constexpr std::uint32_t tfOnlyOne = 0x00020000; +constexpr std::uint32_t tfUntilFailure = 0x00040000; +constexpr std::uint32_t tfIndependent = 0x00080000; +constexpr std::uint32_t const tfBatchMask = + ~(tfUniversal | tfAllOrNothing | tfOnlyOne | tfUntilFailure | tfIndependent) | tfInnerBatchTxn; + // clang-format on } // namespace ripple diff --git a/include/xrpl/protocol/TxMeta.h b/include/xrpl/protocol/TxMeta.h index 44ec8ae93fd..fb806dfb375 100644 --- a/include/xrpl/protocol/TxMeta.h +++ b/include/xrpl/protocol/TxMeta.h @@ -44,7 +44,10 @@ class TxMeta CtorHelper); public: - TxMeta(uint256 const& transactionID, std::uint32_t ledger); + TxMeta( + uint256 const& transactionID, + std::uint32_t ledger, + std::optional batchId = std::nullopt); TxMeta(uint256 const& txID, std::uint32_t ledger, Blob const&); TxMeta(uint256 const& txID, std::uint32_t ledger, std::string const&); TxMeta(uint256 const& txID, std::uint32_t ledger, STObject const&); @@ -135,6 +138,7 @@ class TxMeta int mResult; std::optional mDelivered; + std::optional const mBatchId; STArray mNodes; }; diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index d90dc327780..188287b90c5 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -29,6 +29,7 @@ // If you add an amendment here, then do not forget to increment `numFeatures` // in include/xrpl/protocol/Feature.h. +XRPL_FEATURE(Batch, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionedDomains, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(DynamicNFT, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(Credentials, Supported::yes, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 3217bab9134..5401ac5464e 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -191,6 +191,7 @@ TYPED_SFIELD(sfHookHash, UINT256, 31) TYPED_SFIELD(sfHookNamespace, UINT256, 32) TYPED_SFIELD(sfHookSetTxnID, UINT256, 33) TYPED_SFIELD(sfDomainID, UINT256, 34) +TYPED_SFIELD(sfParentBatchID, UINT256, 35) // number (common) TYPED_SFIELD(sfNumber, NUMBER, 1) @@ -347,6 +348,8 @@ UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement, OBJECT, 30) UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, OBJECT, 31) UNTYPED_SFIELD(sfPriceData, OBJECT, 32) UNTYPED_SFIELD(sfCredential, OBJECT, 33) +UNTYPED_SFIELD(sfRawTransaction, OBJECT, 34) +UNTYPED_SFIELD(sfBatchSigner, OBJECT, 35) // array of objects (common) // ARRAY/1 is reserved for end of array @@ -377,3 +380,5 @@ UNTYPED_SFIELD(sfAuthAccounts, ARRAY, 25) UNTYPED_SFIELD(sfAuthorizeCredentials, ARRAY, 26) UNTYPED_SFIELD(sfUnauthorizeCredentials, ARRAY, 27) UNTYPED_SFIELD(sfAcceptedCredentials, ARRAY, 28) +UNTYPED_SFIELD(sfRawTransactions, ARRAY, 29) +UNTYPED_SFIELD(sfBatchSigners, ARRAY, 30) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index dd3ac42325d..b5a06ce293a 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -465,6 +465,12 @@ TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 63, PermissionedDomainDelete, ({ {sfDomainID, soeREQUIRED}, })) +/** This transaction type wraps inner transactions for batch. */ +TRANSACTION(ttBATCH, 64, Batch, ({ + {sfRawTransactions, soeREQUIRED}, + {sfBatchSigners, soeOPTIONAL}, +})) + /** This system-generated transaction type is used to update the status of the various amendments. For details, see: https://xrpl.org/amendments.html @@ -499,4 +505,3 @@ TRANSACTION(ttUNL_MODIFY, 102, UNLModify, ({ {sfLedgerSequence, soeREQUIRED}, {sfUNLModifyValidator, soeREQUIRED}, })) - diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index c41d7ef2594..866f66b0e7f 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -83,6 +83,8 @@ JSS(PriceDataSeries); // field. JSS(PriceData); // field. JSS(Provider); // field. JSS(QuoteAsset); // in: Oracle. +JSS(RawTransaction); // in: Batch +JSS(RawTransactions); // in: Batch JSS(SLE_hit_rate); // out: GetCounts. JSS(Scale); // field. JSS(SettleDelay); // in: TransactionSign diff --git a/src/libxrpl/protocol/InnerObjectFormats.cpp b/src/libxrpl/protocol/InnerObjectFormats.cpp index 87c42a8085f..dc137b19ed6 100644 --- a/src/libxrpl/protocol/InnerObjectFormats.cpp +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp @@ -154,6 +154,13 @@ InnerObjectFormats::InnerObjectFormats() {sfIssuer, soeREQUIRED}, {sfCredentialType, soeREQUIRED}, }); + + add(sfBatchSigner.jsonName.c_str(), + sfBatchSigner.getCode(), + {{sfAccount, soeREQUIRED}, + {sfSigningPubKey, soeOPTIONAL}, + {sfTxnSignature, soeOPTIONAL}, + {sfSigners, soeOPTIONAL}}); } InnerObjectFormats const& diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp index 821f8f05c96..20a275a8474 100644 --- a/src/libxrpl/protocol/STObject.cpp +++ b/src/libxrpl/protocol/STObject.cpp @@ -898,4 +898,33 @@ STObject::getSortedFields(STObject const& objToSort, WhichFields whichFields) return sf; } +/** + * @brief Retrieves a batch of transaction IDs from the STObject. + * + * This function returns a vector of transaction IDs by extracting them from + * the field array `sfRawTransactions` within the STObject. If the batch + * transaction IDs have already been computed and cached in `batch_txn_ids_`, + * it returns the cached vector. Otherwise, it computes the transaction IDs, + * caches them, and then returns the vector. + * + * @return A vector of `uint256` containing the batch transaction IDs. + * + * @note The function asserts that the `sfRawTransactions` field array is not + * empty and that the size of the computed batch transaction IDs matches the + * size of the `sfRawTransactions` field array. + */ +std::vector +STObject::getBatchTransactionIDs() const +{ + assert(getFieldArray(sfRawTransactions).size() != 0); + if (batch_txn_ids_.size() != 0) + return batch_txn_ids_; + + for (STObject const& rb : getFieldArray(sfRawTransactions)) + batch_txn_ids_.push_back(rb.getHash(HashPrefix::transactionID)); + + assert(batch_txn_ids_.size() == getFieldArray(sfRawTransactions).size()); + return batch_txn_ids_; +} + } // namespace ripple diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index bd1c461c8c7..9b987a4fb5b 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -229,6 +231,32 @@ STTx::checkSign( return Unexpected("Internal signature check failure."); } +Expected +STTx::checkBatchSign( + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const +{ + try + { + STArray const& signers{getFieldArray(sfBatchSigners)}; + for (auto const& signer : signers) + { + Blob const& signingPubKey = signer.getFieldVL(sfSigningPubKey); + auto const result = signingPubKey.empty() + ? checkBatchMultiSign(signer, requireCanonicalSig, rules) + : checkBatchSingleSign(signer, requireCanonicalSig); + + if (!result) + return result; + } + return {}; + } + catch (std::exception const&) + { + } + return Unexpected("Internal signature check failure."); +} + Json::Value STTx::getJson(JsonOptions options) const { @@ -308,80 +336,70 @@ STTx::getMetaSQL( getFieldU32(sfSequence) % inLedger % status % rTxn % escapedMetaData); } -Expected -STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const +static Expected +singleSignHelper( + STObject const& signer, + Slice const& data, + STTx::RequireFullyCanonicalSig requireCanonicalSig, + std::uint32_t flags) { - // We don't allow both a non-empty sfSigningPubKey and an sfSigners. - // That would allow the transaction to be signed two ways. So if both - // fields are present the signature is invalid. - if (isFieldPresent(sfSigners)) + if (signer.isFieldPresent(sfSigners)) return Unexpected("Cannot both single- and multi-sign."); bool validSig = false; try { - bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || - (requireCanonicalSig == RequireFullyCanonicalSig::yes); - - auto const spk = getFieldVL(sfSigningPubKey); + bool const fullyCanonical = (flags & tfFullyCanonicalSig) || + (requireCanonicalSig == STTx::RequireFullyCanonicalSig::yes); + auto const spk = signer.getFieldVL(sfSigningPubKey); if (publicKeyType(makeSlice(spk))) { - Blob const signature = getFieldVL(sfTxnSignature); - Blob const data = getSigningData(*this); - + Blob const signature = signer.getFieldVL(sfTxnSignature); validSig = verify( PublicKey(makeSlice(spk)), - makeSlice(data), + data, makeSlice(signature), fullyCanonical); } } catch (std::exception const&) { - // Assume it was a signature failure. validSig = false; } - if (validSig == false) + + if (!validSig) return Unexpected("Invalid signature."); - // Signature was verified. + return {}; } Expected -STTx::checkMultiSign( - RequireFullyCanonicalSig requireCanonicalSig, - Rules const& rules) const +STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const { - // Make sure the MultiSigners are present. Otherwise they are not - // attempting multi-signing and we just have a bad SigningPubKey. - if (!isFieldPresent(sfSigners)) - return Unexpected("Empty SigningPubKey."); - - // We don't allow both an sfSigners and an sfTxnSignature. Both fields - // being present would indicate that the transaction is signed both ways. - if (isFieldPresent(sfTxnSignature)) - return Unexpected("Cannot both single- and multi-sign."); - - STArray const& signers{getFieldArray(sfSigners)}; - - // There are well known bounds that the number of signers must be within. - if (signers.size() < minMultiSigners || - signers.size() > maxMultiSigners(&rules)) - return Unexpected("Invalid Signers array size."); - - // We can ease the computational load inside the loop a bit by - // pre-constructing part of the data that we hash. Fill a Serializer - // with the stuff that stays constant from signature to signature. - Serializer const dataStart{startMultiSigningData(*this)}; - - // We also use the sfAccount field inside the loop. Get it once. - auto const txnAccountID = getAccountID(sfAccount); + auto const data = getSigningData(*this); + return singleSignHelper( + *this, makeSlice(data), requireCanonicalSig, getFlags()); +} - // Determine whether signatures must be full canonical. - bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || - (requireCanonicalSig == RequireFullyCanonicalSig::yes); +Expected +STTx::checkBatchSingleSign( + STObject const& batchSigner, + RequireFullyCanonicalSig requireCanonicalSig) const +{ + Serializer msg; + serializeBatch(msg, getFlags(), getBatchTransactionIDs()); + return singleSignHelper( + batchSigner, msg.slice(), requireCanonicalSig, getFlags()); +} +Expected +multiSignHelper( + STArray const& signers, + AccountID const& txnAccountID, + bool const fullyCanonical, + std::function(AccountID const&)> makeMsg) +{ // Signers must be in sorted order by AccountID. AccountID lastAccountID(beast::zero); @@ -408,18 +426,16 @@ STTx::checkMultiSign( bool validSig = false; try { - Serializer s = dataStart; - finishMultiSigningData(accountID, s); - + std::vector msgData = makeMsg(accountID); + Slice msgSlice(msgData.data(), msgData.size()); auto spk = signer.getFieldVL(sfSigningPubKey); if (publicKeyType(makeSlice(spk))) { Blob const signature = signer.getFieldVL(sfTxnSignature); - validSig = verify( PublicKey(makeSlice(spk)), - s.slice(), + msgSlice, makeSlice(signature), fullyCanonical); } @@ -438,6 +454,88 @@ STTx::checkMultiSign( return {}; } +Expected +STTx::checkBatchMultiSign( + STObject const& batchSigner, + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const +{ + // Make sure the MultiSigners are present. Otherwise they are not + // attempting multi-signing and we just have a bad SigningPubKey. + if (!batchSigner.isFieldPresent(sfSigners)) + return Unexpected("Empty SigningPubKey."); + + // We don't allow both an sfSigners and an sfTxnSignature. Both fields + // being present would indicate that the transaction is signed both ways. + if (batchSigner.isFieldPresent(sfTxnSignature)) + return Unexpected("Cannot both single- and multi-sign."); + + STArray const& signers{batchSigner.getFieldArray(sfSigners)}; + + // There are well known bounds that the number of signers must be within. + if (signers.size() < minMultiSigners || + signers.size() > maxMultiSigners(&rules)) + return Unexpected("Invalid Signers array size."); + + // We also use the sfAccount field inside the loop. Get it once. + auto const txnAccountID = batchSigner.getAccountID(sfAccount); + + // Determine whether signatures must be full canonical. + bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || + (requireCanonicalSig == RequireFullyCanonicalSig::yes); + + Serializer msg; + serializeBatch(msg, getFlags(), getBatchTransactionIDs()); + + return multiSignHelper( + signers, + txnAccountID, + fullyCanonical, + [&msg](AccountID const&) -> std::vector { + return msg.getData(); + }); +} + +Expected +STTx::checkMultiSign( + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const +{ + // Make sure the MultiSigners are present. Otherwise they are not + // attempting multi-signing and we just have a bad SigningPubKey. + if (!isFieldPresent(sfSigners)) + return Unexpected("Empty SigningPubKey."); + + // We don't allow both an sfSigners and an sfTxnSignature. Both fields + // being present would indicate that the transaction is signed both ways. + if (isFieldPresent(sfTxnSignature)) + return Unexpected("Cannot both single- and multi-sign."); + + STArray const& signers{getFieldArray(sfSigners)}; + + // There are well known bounds that the number of signers must be within. + if (signers.size() < minMultiSigners || + signers.size() > maxMultiSigners(&rules)) + return Unexpected("Invalid Signers array size."); + + // We also use the sfAccount field inside the loop. Get it once. + auto const txnAccountID = getAccountID(sfAccount); + + // Determine whether signatures must be full canonical. + bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || + (requireCanonicalSig == RequireFullyCanonicalSig::yes); + + return multiSignHelper( + signers, + txnAccountID, + fullyCanonical, + [this](AccountID const& accountID) -> std::vector { + Serializer dataStart = startMultiSigningData(*this); + finishMultiSigningData(accountID, dataStart); + return dataStart.getData(); + }); +} + //------------------------------------------------------------------------------ static bool @@ -612,10 +710,13 @@ sterilize(STTx const& stx) bool isPseudoTx(STObject const& tx) { - auto t = tx[~sfTransactionType]; + auto const t = tx[~sfTransactionType]; + if (!t) return false; - auto tt = safe_cast(*t); + + auto const tt = safe_cast(*t); + return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY; } diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 815b27c0018..ca1dfdae359 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -208,6 +208,7 @@ transResults() MAKE_ERROR(temARRAY_EMPTY, "Malformed: Array is empty."), MAKE_ERROR(temARRAY_TOO_LARGE, "Malformed: Array is too large."), MAKE_ERROR(temBAD_TRANSFER_FEE, "Malformed: Transfer fee is outside valid range."), + MAKE_ERROR(temINVALID_BATCH, "Malformed: Invalid inner batch transaction."), MAKE_ERROR(terRETRY, "Retry transaction."), MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."), diff --git a/src/libxrpl/protocol/TxMeta.cpp b/src/libxrpl/protocol/TxMeta.cpp index dad67c13349..7d56e57fda7 100644 --- a/src/libxrpl/protocol/TxMeta.cpp +++ b/src/libxrpl/protocol/TxMeta.cpp @@ -78,11 +78,15 @@ TxMeta::TxMeta( { } -TxMeta::TxMeta(uint256 const& transactionID, std::uint32_t ledger) +TxMeta::TxMeta( + uint256 const& transactionID, + std::uint32_t ledger, + std::optional batchId) : mTransactionID(transactionID) , mLedger(ledger) , mIndex(static_cast(-1)) , mResult(255) + , mBatchId(batchId) , mNodes(sfAffectedNodes) { mNodes.reserve(32); @@ -213,6 +217,8 @@ TxMeta::getAsObject() const { STObject metaData(sfTransactionMetaData); XRPL_ASSERT(mResult != 255, "ripple::TxMeta::getAsObject : result is set"); + if (mBatchId) + metaData.setFieldH256(sfParentBatchID, mBatchId.value()); metaData.setFieldU8(sfTransactionResult, mResult); metaData.setFieldU32(sfTransactionIndex, mIndex); metaData.emplace_back(mNodes); diff --git a/src/test/app/AccountDelete_test.cpp b/src/test/app/AccountDelete_test.cpp index f8d3cf4692a..a900d872710 100644 --- a/src/test/app/AccountDelete_test.cpp +++ b/src/test/app/AccountDelete_test.cpp @@ -367,7 +367,7 @@ class AccountDelete_test : public beast::unit_test::suite NetClock::time_point const& cancelAfter) { Json::Value jv; jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; jv[jss::Account] = account.human(); jv[jss::Destination] = to.human(); jv[jss::Amount] = amount.getJson(JsonOptions::none); @@ -397,7 +397,7 @@ class AccountDelete_test : public beast::unit_test::suite [](Account const& account, Account const& from, std::uint32_t seq) { Json::Value jv; jv[jss::TransactionType] = jss::EscrowCancel; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; jv[jss::Account] = account.human(); jv[sfOwner.jsonName] = from.human(); jv[sfOfferSequence.jsonName] = seq; @@ -535,7 +535,7 @@ class AccountDelete_test : public beast::unit_test::suite auto payChanClaim = [&]() { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelClaim; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; jv[jss::Account] = alice.human(); jv[sfChannel.jsonName] = to_string(payChanKey.key); jv[sfBalance.jsonName] = diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp new file mode 100644 index 00000000000..236e0859a25 --- /dev/null +++ b/src/test/app/Batch_test.cpp @@ -0,0 +1,2153 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { + +class Batch_test : public beast::unit_test::suite +{ + struct TestBatchData + { + std::string result; + std::string txHash; + }; + + struct TestSignData + { + int index; + jtx::Account account; + }; + + Json::Value + getTxByIndex(Json::Value jrr, std::uint8_t index) + { + for (auto const& txn : jrr[jss::result][jss::ledger][jss::transactions]) + { + if (txn[jss::metaData][sfTransactionIndex.jsonName] == index) + return txn; + } + return {}; + } + + void + validateBatch( + jtx::Env& env, + TxID const& batchId, + std::vector const& batchResults) + { + Json::Value params; + params[jss::ledger_index] = env.current()->seq() - 1; + params[jss::transactions] = true; + params[jss::expand] = true; + auto const jrr = env.rpc("json", "ledger", to_string(params)); + // std::cout << "jrr: " << jrr << std::endl; + + // Validate the number of transactions in the ledger + auto const transactions = + jrr[jss::result][jss::ledger][jss::transactions]; + BEAST_EXPECT(transactions.size() == batchResults.size() + 1); + + // Validate ttBatch is correct index + auto const txn = getTxByIndex(jrr, 0); + BEAST_EXPECT(txn.isMember(jss::metaData)); + Json::Value const meta = txn[jss::metaData]; + BEAST_EXPECT(txn[sfTransactionType.jsonName] == "Batch"); + BEAST_EXPECT(meta[sfTransactionResult.jsonName] == "tesSUCCESS"); + + // Validate the inner transactions + for (TestBatchData const& batchResult : batchResults) + { + Json::Value jsonTx; + jsonTx[jss::binary] = false; + jsonTx[jss::transaction] = batchResult.txHash; + jsonTx[jss::id] = 1; + Json::Value const jrr = + env.rpc("json", "tx", to_string(jsonTx))[jss::result]; + BEAST_EXPECT( + jrr[jss::meta][sfTransactionResult.jsonName] == + batchResult.result); + // BEAST_EXPECT(jrr[jss::meta][sfParentBatchID.jsonName] == + // batchId); + } + } + + Json::Value + addBatchTx( + Json::Value jv, + Json::Value const& tx, + std::uint32_t sequence, + std::optional ticket = std::nullopt) + { + std::uint32_t const index = jv[jss::RawTransactions].size(); + Json::Value& batchTransaction = jv[jss::RawTransactions][index]; + + // Initialize the batch transaction + batchTransaction = Json::Value{}; + batchTransaction[jss::RawTransaction] = tx; + batchTransaction[jss::RawTransaction][jss::SigningPubKey] = ""; + batchTransaction[jss::RawTransaction][sfFee.jsonName] = 0; + batchTransaction[jss::RawTransaction][jss::Sequence] = sequence; + batchTransaction[jss::RawTransaction][jss::Flags] = tfInnerBatchTxn; + + // Optionally set ticket sequence + if (ticket.has_value()) + { + batchTransaction[jss::RawTransaction][jss::Sequence] = 0; + batchTransaction[jss::RawTransaction][sfTicketSequence.jsonName] = + *ticket; + } + + return jv; + } + + Json::Value + addBatchSignatures(Json::Value jv, std::vector const& signers) + { + auto const ojv = jv; + for (auto const& signer : signers) + { + Serializer ss{ + buildMultiSigningData(jtx::parse(ojv), signer.account.id())}; + auto const sig = ripple::sign( + signer.account.pk(), signer.account.sk(), ss.slice()); + jv[sfBatchSigners.jsonName][signer.index][sfBatchSigner.jsonName] + [sfAccount.jsonName] = signer.account.human(); + jv[sfBatchSigners.jsonName][signer.index][sfBatchSigner.jsonName] + [sfSigningPubKey.jsonName] = strHex(signer.account.pk()); + jv[sfBatchSigners.jsonName][signer.index][sfBatchSigner.jsonName] + [sfTxnSignature.jsonName] = strHex(Slice{sig.data(), sig.size()}); + } + return jv; + } + + XRPAmount + calcBatchFee( + test::jtx::Env const& env, + uint32_t const& signers, + uint32_t const& txns = 0) + { + XRPAmount const feeDrops = env.current()->fees().base; + return ((signers + 2) * feeDrops) + feeDrops * txns; + } + + void + testEnable(FeatureBitset features) + { + testcase("enabled"); + + using namespace test::jtx; + using namespace std::literals; + + for (bool const withBatch : {true, false}) + { + auto const amend = withBatch ? features : features - featureBatch; + test::jtx::Env env{*this, envconfig(), amend}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(1000), alice, bob, carol); + env.close(); + + // ttBatch + { + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 0, 1); + auto const txResult = + withBatch ? ter(tesSUCCESS) : ter(temDISABLED); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(1)), seq + 1), + txResult); + env.close(); + } + + // tfInnerBatchTxn + { + auto const txResult = + withBatch ? ter(telENV_RPC_FAILED) : ter(temINVALID_FLAG); + env(pay(alice, bob, XRP(1)), + txflags(tfInnerBatchTxn), + txResult); + env.close(); + } + + env.close(); + } + } + + void + testPreflight(FeatureBitset features) + { + testcase("preflight"); + + using namespace test::jtx; + using namespace std::literals; + + //---------------------------------------------------------------------- + // preflight + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(1000), alice, bob, carol); + env.close(); + + // temINVALID_FLAG: Batch: invalid flags. + { + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 0, 0); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + txflags(tfDisallowXRP), + ter(temINVALID_FLAG)); + env.close(); + } + + // temMALFORMED: Batch: too many flags. + { + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 0, 0); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + txflags(tfAllOrNothing | tfOnlyOne), + ter(temMALFORMED)); + env.close(); + } + + // temARRAY_EMPTY: Batch: txns array empty. + { + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 0, 0); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + ter(temARRAY_EMPTY)); + env.close(); + } + + // temARRAY_TOO_LARGE: Batch: txns array exceeds 8 entries. + { + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 0, 9); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(1)), seq + 1), + batch::add(pay(alice, bob, XRP(1)), seq + 2), + batch::add(pay(alice, bob, XRP(1)), seq + 3), + batch::add(pay(alice, bob, XRP(1)), seq + 4), + batch::add(pay(alice, bob, XRP(1)), seq + 5), + batch::add(pay(alice, bob, XRP(1)), seq + 6), + batch::add(pay(alice, bob, XRP(1)), seq + 7), + batch::add(pay(alice, bob, XRP(1)), seq + 8), + batch::add(pay(alice, bob, XRP(1)), seq + 9), + ter(temARRAY_TOO_LARGE)); + env.close(); + } + + // temMALFORMED: // LCOV_EXCL_LINE + + // temMALFORMED: Batch: duplicate TxID found. + { + auto const batchFee = calcBatchFee(env, 1, 2); + Json::Value jv = + batch::batch(alice, env.seq(alice), batchFee, tfAllOrNothing); + + // Tx 1 + Json::Value tx1 = pay(alice, bob, XRP(10)); + jv = addBatchTx(jv, tx1, env.seq(alice) + 1); + auto txn1 = jv[jss::RawTransactions][0u][jss::RawTransaction]; + STParsedJSONObject parsed1(std::string(jss::tx_json), txn1); + STTx const stx1 = STTx{std::move(parsed1.object.value())}; + + // Add a duplicate txn + jv = addBatchTx(jv, tx1, env.seq(alice) + 1); + + env(jv, batch::sig(bob), ter(temMALFORMED)); + env.close(); + } + + // temINVALID_BATCH: Batch: TransactionType missing in array entry. + { + auto const txBlob = + "12003D2200010000240000000468400000000000003273210388935426E0D0" + "8083314842EDFBB2D517BD47699F9A4527318A8E10468C97C0527446304402" + "20280E69E1CD973C909586B3EBF41556F50694F8FE5E905BF6C9E9B6F97417" + "A1D40220509BE54BF5CE3B5D7A989D12F302C486657883629CAF34EC648361" + "6237AFA9C88114AE123A8556F3CF91154711376AFB0F894F832B3DF01DE022" + "22800000002400000005614000000000989680684000000000000000730081" + "14AE123A8556F3CF91154711376AFB0F894F832B3D8314F51DFC2A09D62CBB" + "A1DFBDD4691DAC96AD98B90FE1F1061320B767AB126F2655B1848233CE8952" + "7A1503C7B03A75D8C9DE547FEDB408CA26A1"; + auto const jrr = env.rpc("submit", txBlob)[jss::result]; + BEAST_EXPECT( + jrr[jss::status] == "error" && + jrr[jss::error] == "invalidTransaction"); + + env.close(); + } + + // temINVALID_BATCH: Batch: batch cannot have inner batch txn. + { + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 0, 2); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add( + batch::batch(alice, seq, batchFee, tfAllOrNothing), seq), + batch::add(pay(alice, bob, XRP(1)), seq + 2), + ter(temINVALID_BATCH)); + env.close(); + } + + // temINVALID_BATCH: Batch: inner txn cannot include TxnSignature. + { + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 0, 2); + auto tx1 = pay(alice, bob, XRP(1)); + tx1[jss::TxnSignature] = "DEADBEEF"; + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(tx1, seq + 1), + ter(temINVALID_BATCH)); + env.close(); + } + + // temINVALID_BATCH: Batch: inner txn must include empty SigningPubKey. + { + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 0, 2); + Json::Value jv = batch::batch(alice, seq, batchFee, tfAllOrNothing); + Json::Value tx1 = pay(alice, bob, XRP(10)); + jv = addBatchTx(jv, tx1, env.seq(alice) + 1); + jv[jss::RawTransactions][0u][jss::RawTransaction] + [jss::SigningPubKey] = strHex(alice.pk()); + auto txn1 = jv[jss::RawTransactions][0u][jss::RawTransaction]; + STParsedJSONObject parsed1(std::string(jss::tx_json), txn1); + STTx const stx1 = STTx{std::move(parsed1.object.value())}; + env(jv, ter(temINVALID_BATCH)); + env.close(); + } + + // temINVALID_BATCH: Batch: inner txn cannot include Signers. + { + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 0, 2); + auto tx1 = pay(alice, bob, XRP(1)); + tx1[sfSigners.jsonName] = Json::arrayValue; + tx1[sfSigners.jsonName][0U][sfSigner.jsonName] = Json::objectValue; + tx1[sfSigners.jsonName][0U][sfSigner.jsonName][sfAccount.jsonName] = + alice.human(); + tx1[sfSigners.jsonName][0U][sfSigner.jsonName] + [sfSigningPubKey.jsonName] = strHex(alice.pk()); + tx1[sfSigners.jsonName][0U][sfSigner.jsonName] + [sfTxnSignature.jsonName] = "DEADBEEF"; + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(tx1, seq + 1), + ter(temINVALID_BATCH)); + env.close(); + } + + // temINVALID_BATCH: Batch: inner txn preflight failed. + { + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 0, 2); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(acctdelete(alice, bob), seq + 1), + batch::add(pay(alice, bob, XRP(-1)), seq + 2), + ter(temINVALID_BATCH)); + env.close(); + } + + // temARRAY_TOO_LARGE: Batch: signers array exceeds 8 entries. + { + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 9, 2); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(1)), seq + 1), + batch::add(pay(alice, bob, XRP(1)), seq + 2), + batch::sig( + bob, + carol, + alice, + bob, + carol, + alice, + bob, + carol, + alice, + alice), + ter(temARRAY_TOO_LARGE)); + env.close(); + } + + // temBAD_SIGNER: Batch: signer cannot be the outer account + { + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 2, 2); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(10)), seq + 1), + batch::add(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::sig(alice, bob), + ter(temBAD_SIGNER)); + env.close(); + } + + // temBAD_SIGNER: Batch: duplicate signer found + { + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 2, 2); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(1)), seq + 1), + batch::add(pay(alice, bob, XRP(1)), seq + 2), + batch::sig(bob, bob), + ter(temBAD_SIGNER)); + env.close(); + } + + // temBAD_SIGNER: Batch: no account signature for inner txn. + { + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 1, 2); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(10)), seq + 1), + batch::add(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::sig(carol), + ter(temBAD_SIGNER)); + env.close(); + } + + // temBAD_SIGNATURE: Batch: invalid batch txn signature. + { + std::vector const signers = {{ + {0, bob}, + }}; + + auto const batchFee = calcBatchFee(env, 1, 2); + Json::Value jv = + batch::batch(alice, env.seq(alice), batchFee, tfAllOrNothing); + + // Tx 1 + Json::Value tx1 = pay(alice, bob, XRP(10)); + jv = addBatchTx(jv, tx1, env.seq(alice) + 1); + auto txn1 = jv[jss::RawTransactions][0u][jss::RawTransaction]; + STParsedJSONObject parsed1(std::string(jss::tx_json), txn1); + STTx const stx1 = STTx{std::move(parsed1.object.value())}; + + // Tx 2 + Json::Value const tx2 = pay(bob, alice, XRP(5)); + jv = addBatchTx(jv, tx2, env.seq(bob)); + auto txn2 = jv[jss::RawTransactions][1u][jss::RawTransaction]; + STParsedJSONObject parsed2(std::string(jss::tx_json), txn2); + STTx const stx2 = STTx{std::move(parsed2.object.value())}; + + for (auto const& signer : signers) + { + Serializer msg; + serializeBatch( + msg, + tfAllOrNothing, + {stx1.getTransactionID(), stx2.getTransactionID()}); + auto const sig = ripple::sign( + signer.account.pk(), signer.account.sk(), msg.slice()); + jv[sfBatchSigners.jsonName][signer.index] + [sfBatchSigner.jsonName][sfAccount.jsonName] = + signer.account.human(); + jv[sfBatchSigners.jsonName][signer.index] + [sfBatchSigner.jsonName][sfSigningPubKey.jsonName] = + strHex(alice.pk()); + jv[sfBatchSigners.jsonName][signer.index] + [sfBatchSigner.jsonName][sfTxnSignature.jsonName] = + strHex(Slice{sig.data(), sig.size()}); + } + + jv = addBatchSignatures(jv, signers); + + env(jv, ter(temBAD_SIGNATURE)); + env.close(); + } + + // temBAD_SIGNER: Batch: invalid batch signers. + { + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 2, 2); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(10)), seq + 1), + batch::add(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::sig(bob, carol), + ter(temBAD_SIGNER)); + env.close(); + } + } + + void + testBadSequence(FeatureBitset features) + { + testcase("bad sequence"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(1000), alice, bob, gw); + env.close(); + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + env(pay(gw, bob, USD(100))); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + // Invalid: Bob Sequence is a future sequence + { + auto const preAliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobSeq = env.seq(bob); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = calcBatchFee(env, 1, 2); + env(batch::batch(alice, preAliceSeq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(10)), preAliceSeq + 1), + batch::add(pay(bob, alice, XRP(5)), preBobSeq + 10), + batch::sig(bob), + ter(tesSUCCESS)); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = {}; + env.close(); + validateBatch(env, batchId, testCases); + + // Alice pays fee & Bob should not be affected. + BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD); + BEAST_EXPECT(env.seq(bob) == preBobSeq); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD); + } + + // Invalid: Outer and Inner Sequence are the same + { + auto const preAliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobSeq = env.seq(bob); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = calcBatchFee(env, 1, 2); + env(batch::batch(alice, preAliceSeq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(10)), preAliceSeq), + batch::add(pay(bob, alice, XRP(5)), preBobSeq), + batch::sig(bob), + ter(tesSUCCESS)); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = {}; + env.close(); + validateBatch(env, batchId, testCases); + + // Alice pays fee & Bob should not be affected. + BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD); + BEAST_EXPECT(env.seq(bob) == preBobSeq); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD); + } + } + + void + testBadFeeOuterBatch(FeatureBitset features) + { + testcase("bad fee outer batch"); + + using namespace test::jtx; + using namespace std::literals; + + // Bad Fee Without Signer + { + test::jtx::Env env{*this, envconfig()}; + XRPAmount const feeDrops = env.current()->fees().base; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(1000), alice, bob, gw); + env.close(); + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + env(pay(gw, bob, USD(100))); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + auto const preAliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobSeq = env.seq(bob); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = (0 + 1) * feeDrops; + env(batch::batch(alice, preAliceSeq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(10)), preAliceSeq + 1), + batch::add(pay(bob, alice, XRP(5)), preBobSeq), + batch::sig(bob), + ter(telINSUF_FEE_P)); + env.close(); + + // Alice & Bob should not be affected. + BEAST_EXPECT(env.seq(alice) == preAliceSeq); + BEAST_EXPECT(env.balance(alice) == preAlice); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD); + BEAST_EXPECT(env.seq(bob) == preBobSeq); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD); + } + + // Bad Fee With Signer + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(1000), alice, bob, gw); + env.close(); + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + env(pay(gw, bob, USD(100))); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + auto const preAliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobSeq = env.seq(bob); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + // Bad Fee: Should be (1 + 2) * feeDrops + auto const batchFee = calcBatchFee(env, 0, 2); + env(batch::batch(alice, preAliceSeq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(10)), preAliceSeq + 1), + batch::add(pay(bob, alice, XRP(5)), preBobSeq), + batch::sig(bob), + ter(telINSUF_FEE_P)); + env.close(); + + // Alice & Bob should not be affected. + BEAST_EXPECT(env.seq(alice) == preAliceSeq); + BEAST_EXPECT(env.balance(alice) == preAlice); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD); + BEAST_EXPECT(env.seq(bob) == preBobSeq); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD); + } + + // Bad Fee Dynamic Fee Calculation + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(1000), alice, bob, carol, gw); + env.close(); + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + env(pay(gw, bob, USD(100))); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const seq = env.seq(alice); + auto const ammCreate = + [&alice](STAmount const& amount, STAmount const& amount2) { + Json::Value jv; + jv[jss::Account] = alice.human(); + jv[jss::Amount] = amount.getJson(JsonOptions::none); + jv[jss::Amount2] = amount2.getJson(JsonOptions::none); + jv[jss::TradingFee] = 0; + jv[jss::TransactionType] = jss::AMMCreate; + return jv; + }; + + auto const batchFee = calcBatchFee(env, 0, 2); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(ammCreate(XRP(10), USD(10)), seq + 1), + batch::add(pay(alice, bob, XRP(10)), seq + 2), + ter(telINSUF_FEE_P)); + env.close(); + + BEAST_EXPECT(env.seq(alice) == seq); + BEAST_EXPECT(env.balance(alice) == preAlice); + BEAST_EXPECT(env.balance(bob) == preBob); + } + } + + void + testChangesBetweenViews(FeatureBitset features) + { + testcase("changes between views"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(220), alice, bob, carol); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + // Using 1 XRP to create insufficient reserve result + auto const batchFee = XRP(1); + auto const seq = env.seq(alice); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(10)), seq + 1), + batch::add(pay(alice, bob, XRP(10)), seq + 2), + ter(tesSUCCESS)); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = {}; + env.close(); + validateBatch(env, batchId, testCases); + + // Alice pays fee and sequence; Bob should not be affected. + BEAST_EXPECT(env.seq(alice) == seq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + } + + void + testBadInnerFee(FeatureBitset features) + { + testcase("bad inner fee"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + XRPAmount const feeDrops = env.current()->fees().base; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(1000), alice, bob, carol, gw); + env.close(); + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + env(pay(gw, bob, USD(100))); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 0, 2); + auto tx = pay(alice, bob, XRP(1000)); + tx[jss::Fee] = to_string(feeDrops); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(10)), seq + 1), + batch::add(tx, seq + 2), + ter(tesSUCCESS)); + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = {}; + env.close(); + validateBatch(env, batchId, testCases); + + // Alice pays fee and sequence; Bob should not be affected. + BEAST_EXPECT(env.seq(alice) == seq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + } + + void + testAllOrNothing(FeatureBitset features) + { + testcase("all or nothing"); + + using namespace test::jtx; + using namespace std::literals; + + // all + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(1000), alice, bob, carol); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(1)), seq + 1), + batch::add(pay(alice, bob, XRP(1)), seq + 2), + ter(tesSUCCESS)); + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = { + {"tesSUCCESS", to_string(txIDs[0])}, + {"tesSUCCESS", to_string(txIDs[1])}, + }; + env.close(); + validateBatch(env, batchId, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(2) - batchFee); + + // Bob receives XRP + BEAST_EXPECT(env.balance(bob) == preBob + XRP(2)); + } + + // nothing + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(1000), alice, bob, carol); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(1)), seq + 1), + batch::add(pay(alice, bob, XRP(999)), seq + 2), + ter(tesSUCCESS)); + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = {}; + env.close(); + validateBatch(env, batchId, testCases); + + // Alice consumes sequence + BEAST_EXPECT(env.seq(alice) == seq + 1); + + // Alice pays Fee + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + + // Bob should not be affected + BEAST_EXPECT(env.balance(bob) == preBob); + } + } + + void + testOnlyOne(FeatureBitset features) + { + testcase("only one"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(1000), alice, bob, carol); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = calcBatchFee(env, 0, 3); + auto const seq = env.seq(alice); + env(batch::batch(alice, seq, batchFee, tfOnlyOne), + batch::add(pay(alice, bob, XRP(999)), seq + 1), + batch::add(pay(alice, bob, XRP(1)), seq + 2), + batch::add(pay(alice, bob, XRP(1)), seq + 3), + ter(tesSUCCESS)); + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = { + {"tecUNFUNDED_PAYMENT", to_string(txIDs[0])}, + {"tesSUCCESS", to_string(txIDs[1])}, + }; + env.close(); + validateBatch(env, batchId, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1)); + } + + void + testUntilFailure(FeatureBitset features) + { + testcase("until failure"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(1000), alice, bob, carol); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + env(batch::batch(alice, seq, batchFee, tfUntilFailure), + batch::add(pay(alice, bob, XRP(1)), seq + 1), + batch::add(pay(alice, bob, XRP(1)), seq + 2), + batch::add(pay(alice, bob, XRP(999)), seq + 3), + batch::add(pay(alice, bob, XRP(1)), seq + 4), + ter(tesSUCCESS)); + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = { + {"tesSUCCESS", to_string(txIDs[0])}, + {"tesSUCCESS", to_string(txIDs[1])}, + {"tecUNFUNDED_PAYMENT", to_string(txIDs[2])}, + }; + env.close(); + validateBatch(env, batchId, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 4); + + // Alice pays XRP & Fee + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(2) - batchFee); + + // Bob receives XRP + BEAST_EXPECT(env.balance(bob) == preBob + XRP(2)); + } + + void + testIndependent(FeatureBitset features) + { + testcase("independent"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(1000), alice, bob, carol); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + env(batch::batch(alice, seq, batchFee, tfIndependent), + batch::add(pay(alice, bob, XRP(1)), seq + 1), + batch::add(pay(alice, bob, XRP(1)), seq + 2), + batch::add(pay(alice, bob, XRP(999)), seq + 3), + batch::add(pay(alice, bob, XRP(1)), seq + 4), + ter(tesSUCCESS)); + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = { + {"tesSUCCESS", to_string(txIDs[0])}, + {"tesSUCCESS", to_string(txIDs[1])}, + {"tecUNFUNDED_PAYMENT", to_string(txIDs[2])}, + {"tesSUCCESS", to_string(txIDs[3])}, + }; + env.close(); + validateBatch(env, batchId, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 5); + + // Alice pays XRP & Fee + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + + // Bob receives XRP + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + void + testMultiParty(FeatureBitset features) + { + testcase("multi party"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(1000), alice, bob); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const bobSeq = env.seq(bob); + + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 1, 2); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(10)), seq + 1), + batch::add(pay(bob, alice, XRP(5)), bobSeq), + batch::sig(bob)); + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = { + {"tesSUCCESS", to_string(txIDs[0])}, + {"tesSUCCESS", to_string(txIDs[1])}, + }; + env.close(); + validateBatch(env, batchId, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 2); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(bob) == bobSeq + 1); + + // Alice pays XRP & Fee + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(5) - batchFee); + + // Bob receives XRP + BEAST_EXPECT(env.balance(bob) == preBob + XRP(5)); + } + + void + testRegularKey(FeatureBitset features) + { + testcase("regular key"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(1000), alice, bob, carol); + env.close(); + + env(regkey(alice, carol)); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 0, 2); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(1)), seq + 1), + batch::add(pay(alice, bob, XRP(1)), seq + 2), + sig(carol)); + + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = { + {"tesSUCCESS", to_string(txIDs[0])}, + {"tesSUCCESS", to_string(txIDs[1])}, + }; + env.close(); + validateBatch(env, batchId, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(2) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(2)); + } + + void + testRegularKeyMultiParty(FeatureBitset features) + { + testcase("regular key multi party"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const dave = Account("dave"); + auto const elsa = Account("elsa"); + + env.fund(XRP(1000), alice, bob, carol, dave, elsa); + env.close(); + + env(regkey(bob, carol)); + env.close(); + + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const bobSeq = env.seq(bob); + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 1, 2); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(10)), seq + 1), + batch::add(pay(bob, alice, XRP(5)), bobSeq), + batch::sig(Reg{bob, carol})); + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = { + {"tesSUCCESS", to_string(txIDs[0])}, + {"tesSUCCESS", to_string(txIDs[1])}, + }; + env.close(); + validateBatch(env, batchId, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 2); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(bob) == bobSeq + 1); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(5) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(5)); + } + } + + void + testMultisign(FeatureBitset features) + { + testcase("multisign"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(1000), alice, bob, carol); + env.close(); + + env(signers(alice, 2, {{bob, 1}, {carol, 1}})); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 2, 2); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(1)), seq + 1), + batch::add(pay(alice, bob, XRP(1)), seq + 2), + msig(bob, carol)); + + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = { + {"tesSUCCESS", to_string(txIDs[0])}, + {"tesSUCCESS", to_string(txIDs[1])}, + }; + env.close(); + validateBatch(env, batchId, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(2) - batchFee); + + // Bob receives XRP + BEAST_EXPECT(env.balance(bob) == preBob + XRP(2)); + } + + void + testMultisignMultiParty(FeatureBitset features) + { + testcase("multisign multi party"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const dave = Account("dave"); + auto const elsa = Account("elsa"); + + env.fund(XRP(1000), alice, bob, carol, dave, elsa); + env.close(); + + env(signers(bob, 2, {{carol, 1}, {dave, 1}, {elsa, 1}})); + env.close(); + + // tefBAD_QUORUM + { + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 2, 2); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(10)), seq + 1), + batch::add(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {dave}), + ter(tefBAD_QUORUM)); + env.close(); + } + + // tefBAD_SIGNATURE + { + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 2, 2); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(10)), seq + 1), + batch::add(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {alice, dave}), + ter(tefBAD_SIGNATURE)); + env.close(); + } + + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const bobSeq = env.seq(bob); + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 2, 2); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(10)), seq + 1), + batch::add(pay(bob, alice, XRP(5)), bobSeq), + batch::msig(bob, {dave, carol})); + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = { + {"tesSUCCESS", to_string(txIDs[0])}, + {"tesSUCCESS", to_string(txIDs[1])}, + }; + env.close(); + validateBatch(env, batchId, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 2); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(bob) == bobSeq + 1); + + // Alice pays XRP & Fee + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(5) - batchFee); + + // Bob receives XRP + BEAST_EXPECT(env.balance(bob) == preBob + XRP(5)); + } + } + + void + testBatchType(FeatureBitset features) + { + testcase("batch type"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const eve = Account("eve"); + env.fund(XRP(100000), alice, bob, carol, eve); + env.close(); + + { // All or Nothing: all succeed + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preCarol = env.balance(carol); + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 0, 2); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(100)), seq + 1), + batch::add(pay(alice, carol, XRP(100)), seq + 2)); + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = { + {"tesSUCCESS", to_string(txIDs[0])}, + {"tesSUCCESS", to_string(txIDs[1])}, + }; + env.close(); + validateBatch(env, batchId, testCases); + + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(200) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(100)); + BEAST_EXPECT(env.balance(carol) == preCarol + XRP(100)); + } + + { // All or Nothing: one fails + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preCarol = env.balance(carol); + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 0, 2); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(100)), seq + 1), + batch::add(pay(alice, carol, XRP(747681)), seq + 2)); + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = {}; + env.close(); + validateBatch(env, batchId, testCases); + + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(carol) == preCarol); + } + + { // Independent (one fails) + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preCarol = env.balance(carol); + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 0, 3); + env(batch::batch(alice, seq, batchFee, tfIndependent), + batch::add(pay(alice, bob, XRP(100)), seq + 1), + batch::add(pay(alice, carol, XRP(100)), seq + 2), + batch::add( + offer( + alice, + alice["USD"](100), + XRP(100), + tfImmediateOrCancel), + seq + 3)); + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = { + {"tesSUCCESS", to_string(txIDs[0])}, + {"tesSUCCESS", to_string(txIDs[1])}, + {"tecKILLED", to_string(txIDs[2])}, + }; + env.close(); + validateBatch(env, batchId, testCases); + + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(200) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(100)); + BEAST_EXPECT(env.balance(carol) == preCarol + XRP(100)); + } + + { // Until Failure: one fails, one is not executed + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preCarol = env.balance(carol); + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 0, 4); + env(batch::batch(alice, seq, batchFee, tfUntilFailure), + batch::add(pay(alice, bob, XRP(100)), seq + 1), + batch::add(pay(alice, carol, XRP(100)), seq + 2), + batch::add( + offer( + alice, + alice["USD"](100), + XRP(100), + tfImmediateOrCancel), + seq + 3), + batch::add(pay(alice, eve, XRP(100)), seq + 4)); + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = { + {"tesSUCCESS", to_string(txIDs[0])}, + {"tesSUCCESS", to_string(txIDs[1])}, + {"tecKILLED", to_string(txIDs[2])}, + }; + env.close(); + validateBatch(env, batchId, testCases); + + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(200) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(100)); + BEAST_EXPECT(env.balance(carol) == preCarol + XRP(100)); + } + + { // Only one: the fourth succeeds + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preCarol = env.balance(carol); + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 0, 6); + env(batch::batch(alice, seq, batchFee, tfOnlyOne), + batch::add( + offer( + alice, + alice["USD"](100), + XRP(100), + tfImmediateOrCancel), + seq + 1), + batch::add( + offer( + alice, + alice["USD"](100), + XRP(100), + tfImmediateOrCancel), + seq + 2), + batch::add( + offer( + alice, + alice["USD"](100), + XRP(100), + tfImmediateOrCancel), + seq + 3), + batch::add(pay(alice, bob, XRP(100)), seq + 4), + batch::add(pay(alice, carol, XRP(100)), seq + 5), + batch::add(pay(alice, eve, XRP(100)), seq + 6)); + + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = { + {"tecKILLED", to_string(txIDs[0])}, + {"tecKILLED", to_string(txIDs[1])}, + {"tecKILLED", to_string(txIDs[2])}, + {"tesSUCCESS", to_string(txIDs[3])}, + }; + env.close(); + validateBatch(env, batchId, testCases); + + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(100) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(100)); + BEAST_EXPECT(env.balance(carol) == preCarol); + } + } + + void + testInnerSubmitRPC(FeatureBitset features) + { + testcase("inner submit rpc"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(1000), alice, bob, gw); + env.close(); + + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + env(pay(gw, bob, USD(100))); + env.close(); + + // Invalid RPC Submission: TxnSignature + // - has `TxnSignature` field + // - has no `SigningPubKey` field + // - has no `Signers` field + // - has `tfInnerBatchTxn` flag + { + auto jv = pay(alice, bob, USD(1)); + jv[sfFlags.fieldName] = tfInnerBatchTxn; + Serializer s; + auto jt = env.jt(jv); + jt.stx->add(s); + auto const jrr = env.rpc("submit", strHex(s.slice()))[jss::result]; + BEAST_EXPECT( + jrr[jss::status] == "error" && + jrr[jss::error] == "invalidTransaction" && + jrr[jss::error_exception] == + "fails local checks: Malformed: Invalid inner batch " + "transaction."); + + env.close(); + } + + // Invalid RPC Submission: SigningPubKey + // - has no `TxnSignature` field + // - has `SigningPubKey` field + // - has no `Signers` field + // - has `tfInnerBatchTxn` flag + { + std::string txBlob = + "1200002240000000240000000561D4838D7EA4C68000000000000000000000" + "0000005553440000000000A407AF5856CCF3C42619DAA925813FC955C72983" + "68400000000000000A73210388935426E0D08083314842EDFBB2D517BD4769" + "9F9A4527318A8E10468C97C0528114AE123A8556F3CF91154711376AFB0F89" + "4F832B3D8314F51DFC2A09D62CBBA1DFBDD4691DAC96AD98B90F"; + auto const jrr = env.rpc("submit", txBlob)[jss::result]; + BEAST_EXPECT( + jrr[jss::status] == "error" && + jrr[jss::error] == "invalidTransaction" && + jrr[jss::error_exception] == + "fails local checks: Malformed: Invalid inner batch " + "transaction."); + + env.close(); + } + + // Invalid RPC Submission: Signers + // - has no `TxnSignature` field + // - has empty `SigningPubKey` field + // - has `Signers` field + // - has `tfInnerBatchTxn` flag + { + std::string txBlob = + "1200002240000000240000000561D4838D7EA4C68000000000000000000000" + "0000005553440000000000A407AF5856CCF3C42619DAA925813FC955C72983" + "68400000000000000A73008114AE123A8556F3CF91154711376AFB0F894F83" + "2B3D8314F51DFC2A09D62CBBA1DFBDD4691DAC96AD98B90FF3E01073210289" + "49021029D5CC87E78BCF053AFEC0CAFD15108EC119EAAFEC466F5C095407BF" + "74473045022100EC791DC3306E1784B813CBE275C9A0E2F467EF795E3571AA" + "DB295862F2F316350220668716954E02AF714F119F34D869891C8704A7989B" + "DB0DBA029A7580430BB7138114B389FBCED0AF9DCDFF62900BFAEFA3EB872D" + "8A96E1E010732102691AC5AE1C4C333AE5DF8A93BDC495F0EEBFC6DB0DA7EB" + "6EF808F3AFC006E3FE74473045022100B93117804900BE1E83E5E2B5846642" + "7BBFE2138CDEF5F31F566B4AC49A947C300220463AFD847028A76F3FEC997B" + "56FA4C4E6514A57E77D38AC854A6A2A54DD4DB478114F51DFC2A09D62CBBA1" + "DFBDD4691DAC96AD98B90FE1F1"; + auto const jrr = env.rpc("submit", txBlob)[jss::result]; + BEAST_EXPECT( + jrr[jss::status] == "error" && + jrr[jss::error] == "invalidTransaction" && + jrr[jss::error_exception] == + "fails local checks: Malformed: Invalid inner batch " + "transaction."); + + env.close(); + } + + // Invalid RPC Submission: tfInnerBatchTxn + // - has no `TxnSignature` field + // - has empty `SigningPubKey` field + // - has no `Signers` field + // - has `tfInnerBatchTxn` flag + { + std::string txBlob = + "1200002240000000240000000561D4838D7EA4C68000000000000000000000" + "0000005553440000000000A407AF5856CCF3C42619DAA925813FC955C72983" + "68400000000000000A73008114AE123A8556F3CF91154711376AFB0F894F83" + "2B3D8314F51DFC2A09D62CBBA1DFBDD4691DAC96AD98B90F"; + auto const jrr = env.rpc("submit", txBlob)[jss::result]; + BEAST_EXPECT( + jrr[jss::status] == "success" && + jrr[jss::engine_result] == "temINVALID_FLAG"); + + env.close(); + } + } + + void + testNoAccount(FeatureBitset features) + { + testcase("no account"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice); + env.close(); + env.memoize(bob); + + auto const preAlice = env.balance(alice); + + // Tx 1 + Json::Value tx1 = noop(bob); + tx1[sfSetFlag.fieldName] = asfAllowTrustLineClawback; + + auto const ledSeq = env.current()->seq(); + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 1, 2); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(1000)), seq + 1), + batch::add(tx1, ledSeq), + batch::sig(bob)); + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = { + {"tesSUCCESS", to_string(txIDs[0])}, + {"tesSUCCESS", to_string(txIDs[1])}, + }; + env.close(); + validateBatch(env, batchId, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 2); + + // Bob consumes sequences (# of txns) + BEAST_EXPECT(env.seq(bob) == ledSeq + 1); + + // Alice pays XRP & Fee + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - batchFee); + + // Bob receives XRP + BEAST_EXPECT(env.balance(bob) == XRP(1000)); + } + + void + testAccountSet(FeatureBitset features) + { + testcase("account set"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(1000), alice, bob, carol); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + // Tx 1 + Json::Value tx1 = noop(alice); + std::string const domain = "example.com"; + tx1[sfDomain.fieldName] = strHex(domain); + + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 0, 2); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(tx1, seq + 1), + batch::add(pay(alice, bob, XRP(1)), seq + 2)); + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = { + {"tesSUCCESS", to_string(txIDs[0])}, + {"tesSUCCESS", to_string(txIDs[1])}, + }; + env.close(); + validateBatch(env, batchId, testCases); + + auto const sle = env.le(keylet::account(alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT( + sle->getFieldVL(sfDomain) == Blob(domain.begin(), domain.end())); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1)); + } + + void + testAccountDelete(FeatureBitset features) + { + testcase("account delete"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(1000), alice, bob, carol); + env.close(); + + // Close enough ledgers to delete account + int const delta = [&]() -> int { + if (env.seq(alice) + 300 > env.current()->seq()) + return env.seq(alice) - env.current()->seq() + 300; + return 0; + }(); + for (int i = 0; i < delta; ++i) + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const seq = env.seq(alice); + auto const batchFee = drops(env.current()->fees().reserve); + env(batch::batch(alice, seq, batchFee, tfIndependent), + batch::add(pay(alice, bob, XRP(1)), seq + 1), + batch::add(acctdelete(alice, bob), seq + 2), + batch::add(pay(alice, bob, XRP(1)), seq + 3)); + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = { + {"tesSUCCESS", to_string(txIDs[0])}, + {"tesSUCCESS", to_string(txIDs[1])}, + }; + env.close(); + validateBatch(env, batchId, testCases); + + // Alice does not exist + BEAST_EXPECT(!env.le(keylet::account(alice))); + + // Bob receives Alice's XRP + BEAST_EXPECT(env.balance(bob) == preBob + (preAlice - batchFee)); + } + + static uint256 + getCheckIndex(AccountID const& account, std::uint32_t uSequence) + { + return keylet::check(account, uSequence).key; + } + + void + testObjectCreateSequence(FeatureBitset features) + { + testcase("object create w/ sequence"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(1000), alice, bob, gw); + env.close(); + + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + env(pay(gw, bob, USD(100))); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 1, 2); + uint256 const chkId{getCheckIndex(bob, env.seq(bob))}; + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(check::create(bob, alice, USD(10)), env.seq(bob)), + batch::add(check::cash(alice, chkId, USD(10)), seq + 1), + batch::sig(bob)); + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = { + {"tesSUCCESS", to_string(txIDs[0])}, + {"tesSUCCESS", to_string(txIDs[1])}, + }; + env.close(); + validateBatch(env, batchId, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == 7); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(bob) == 6); + + // Alice pays Fee + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + + // Bob XRP Unchanged + BEAST_EXPECT(env.balance(bob) == preBob); + + // Alice pays USD & Bob receives USD + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD + USD(10)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD - USD(10)); + } + + void + testObjectCreateTicket(FeatureBitset features) + { + testcase("object create w/ ticket"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(1000), alice, bob, gw); + env.close(); + + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + env(pay(gw, bob, USD(100))); + env.close(); + + std::uint32_t bobTicketSeq{env.seq(bob) + 1}; + env(ticket::create(bob, 10)); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 1, 2); + uint256 const chkId{getCheckIndex(bob, bobTicketSeq)}; + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(check::create(bob, alice, USD(10)), 0, bobTicketSeq), + batch::add(check::cash(alice, chkId, USD(10)), seq + 1), + batch::sig(bob)); + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = { + {"tesSUCCESS", to_string(txIDs[0])}, + {"tesSUCCESS", to_string(txIDs[1])}, + }; + env.close(); + validateBatch(env, batchId, testCases); + + BEAST_EXPECT(env.seq(alice) == 7); + BEAST_EXPECT(env.seq(bob) == 16); + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD + USD(10)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD - USD(10)); + } + + void + testObjectCreate3rdParty(FeatureBitset features) + { + testcase("object create w/ 3rd party"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(1000), alice, bob, carol, gw); + env.close(); + + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + env(pay(gw, bob, USD(100))); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preCarol = env.balance(carol); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const seq = env.seq(carol); + auto const batchFee = calcBatchFee(env, 2, 2); + uint256 const chkId{getCheckIndex(bob, env.seq(bob))}; + env(batch::batch(carol, seq, batchFee, tfAllOrNothing), + batch::add(check::create(bob, alice, USD(10)), env.seq(bob)), + batch::add(check::cash(alice, chkId, USD(10)), env.seq(alice)), + batch::sig(alice, bob)); + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = { + {"tesSUCCESS", to_string(txIDs[0])}, + {"tesSUCCESS", to_string(txIDs[1])}, + }; + env.close(); + validateBatch(env, batchId, testCases); + + BEAST_EXPECT(env.seq(alice) == 6); + BEAST_EXPECT(env.seq(bob) == 6); + BEAST_EXPECT(env.seq(carol) == 5); + BEAST_EXPECT(env.balance(alice) == preAlice); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(carol) == preCarol - batchFee); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD + USD(10)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD - USD(10)); + } + + void + testTicketsOuter(FeatureBitset features) + { + testcase("tickets outer"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(1000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 0, 2); + env(batch::batch(alice, 0, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(1)), seq + 0), + batch::add(pay(alice, bob, XRP(1)), seq + 1), + ticket::use(aliceTicketSeq++)); + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = { + {"tesSUCCESS", to_string(txIDs[0])}, + {"tesSUCCESS", to_string(txIDs[1])}, + }; + env.close(); + validateBatch(env, batchId, testCases); + + auto const sle = env.le(keylet::account(alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 9); + BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 9); + + BEAST_EXPECT(env.seq(alice) == 17); + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(2) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(2)); + } + + void + testTicketsInner(FeatureBitset features) + { + testcase("tickets inner"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(1000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 0, 2); + env(batch::batch(alice, seq, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(1)), 0, aliceTicketSeq), + batch::add(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1)); + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = { + {"tesSUCCESS", to_string(txIDs[0])}, + {"tesSUCCESS", to_string(txIDs[1])}, + }; + env.close(); + validateBatch(env, batchId, testCases); + + auto const sle = env.le(keylet::account(alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 8); + BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 8); + + BEAST_EXPECT(env.seq(alice) == 16); + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(2) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(2)); + } + + void + testTicketsOuterInner(FeatureBitset features) + { + testcase("tickets outer inner"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(1000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const seq = env.seq(alice); + auto const batchFee = calcBatchFee(env, 0, 2); + env(batch::batch(alice, 0, batchFee, tfAllOrNothing), + batch::add(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1), + batch::add(pay(alice, bob, XRP(1)), seq + 0), + ticket::use(aliceTicketSeq)); + auto const txIDs = env.tx()->getBatchTransactionIDs(); + TxID const batchId = env.tx()->getTransactionID(); + std::vector testCases = { + {"tesSUCCESS", to_string(txIDs[0])}, + {"tesSUCCESS", to_string(txIDs[1])}, + }; + env.close(); + validateBatch(env, batchId, testCases); + + auto const sle = env.le(keylet::account(alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 8); + BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 8); + + BEAST_EXPECT(env.seq(alice) == 16); + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(2) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(2)); + } + + void + testPseudoTxn(FeatureBitset features) + { + testcase("pseudo txn"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(1000), alice, bob); + env.close(); + + STTx const stx = STTx(ttAMENDMENT, [&](auto& obj) { + obj.setAccountID(sfAccount, AccountID()); + obj.setFieldH256(sfAmendment, uint256(2)); + obj.setFieldU32(sfLedgerSequence, env.seq(alice)); + obj.setFieldU32(sfFlags, tfInnerBatchTxn); + }); + + std::string reason; + BEAST_EXPECT(isPseudoTx(stx)); + BEAST_EXPECT(!passesLocalChecks(stx, reason)); + BEAST_EXPECT(reason == "Cannot submit pseudo transactions."); + env.app().openLedger().modify([&](OpenView& view, beast::Journal j) { + auto const result = ripple::apply(env.app(), view, stx, tapNONE, j); + BEAST_EXPECT(!result.second && result.first == temINVALID_FLAG); + return result.second; + }); + } + + void + testWithFeats(FeatureBitset features) + { + testEnable(features); + testPreflight(features); + testBadSequence(features); + testBadFeeOuterBatch(features); + testChangesBetweenViews(features); + testBadInnerFee(features); + testAllOrNothing(features); + testOnlyOne(features); + testUntilFailure(features); + testIndependent(features); + testMultiParty(features); + testRegularKey(features); + testRegularKeyMultiParty(features); + testMultisign(features); + testMultisignMultiParty(features); + testBatchType(features); + testInnerSubmitRPC(features); + testNoAccount(features); + testAccountSet(features); + testAccountDelete(features); + testObjectCreateSequence(features); + testObjectCreateTicket(features); + testObjectCreate3rdParty(features); + testTicketsOuter(features); + testTicketsInner(features); + testTicketsOuterInner(features); + testPseudoTxn(features); + } + +public: + void + run() override + { + using namespace test::jtx; + auto const sa = supported_amendments(); + testWithFeats(sa); + } +}; + +BEAST_DEFINE_TESTSUITE(Batch, app, ripple); + +} // namespace test +} // namespace ripple \ No newline at end of file diff --git a/src/test/app/MultiSign_test.cpp b/src/test/app/MultiSign_test.cpp index 77d85d9011b..cab703c0a98 100644 --- a/src/test/app/MultiSign_test.cpp +++ b/src/test/app/MultiSign_test.cpp @@ -457,7 +457,7 @@ class MultiSign_test : public beast::unit_test::suite // Attempt a multisigned transaction that meets the quorum. auto const baseFee = env.current()->fees().base; std::uint32_t aliceSeq = env.seq(alice); - env(noop(alice), msig(msig::Reg{cheri, cher}), fee(2 * baseFee)); + env(noop(alice), msig(Reg{cheri, cher}), fee(2 * baseFee)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -477,7 +477,7 @@ class MultiSign_test : public beast::unit_test::suite BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); aliceSeq = env.seq(alice); - env(noop(alice), msig(msig::Reg{becky, beck}), fee(2 * baseFee)); + env(noop(alice), msig(Reg{becky, beck}), fee(2 * baseFee)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -485,7 +485,7 @@ class MultiSign_test : public beast::unit_test::suite aliceSeq = env.seq(alice); env(noop(alice), fee(3 * baseFee), - msig(msig::Reg{becky, beck}, msig::Reg{cheri, cher})); + msig(Reg{becky, beck}, Reg{cheri, cher})); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); } @@ -780,12 +780,12 @@ class MultiSign_test : public beast::unit_test::suite BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); aliceSeq = env.seq(alice); - env(noop(alice), msig(msig::Reg{cheri, cher}), fee(2 * baseFee)); + env(noop(alice), msig(Reg{cheri, cher}), fee(2 * baseFee)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); aliceSeq = env.seq(alice); - env(noop(alice), msig(msig::Reg{daria, dari}), fee(2 * baseFee)); + env(noop(alice), msig(Reg{daria, dari}), fee(2 * baseFee)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -798,7 +798,7 @@ class MultiSign_test : public beast::unit_test::suite aliceSeq = env.seq(alice); env(noop(alice), fee(5 * baseFee), - msig(becky, msig::Reg{cheri, cher}, msig::Reg{daria, dari}, jinni)); + msig(becky, Reg{cheri, cher}, Reg{daria, dari}, jinni)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -817,7 +817,7 @@ class MultiSign_test : public beast::unit_test::suite aliceSeq = env.seq(alice); env(noop(alice), fee(9 * baseFee), - msig(becky, msig::Reg{cheri, cher}, msig::Reg{daria, dari}, jinni)); + msig(becky, Reg{cheri, cher}, Reg{daria, dari}, jinni)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -825,7 +825,7 @@ class MultiSign_test : public beast::unit_test::suite aliceSeq = env.seq(alice); env(noop(alice), fee(5 * baseFee), - msig(becky, cheri, msig::Reg{daria, dari}, jinni)); + msig(becky, cheri, Reg{daria, dari}, jinni)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -850,8 +850,8 @@ class MultiSign_test : public beast::unit_test::suite fee(9 * baseFee), msig( becky, - msig::Reg{cheri, cher}, - msig::Reg{daria, dari}, + Reg{cheri, cher}, + Reg{daria, dari}, haunt, jinni, phase, @@ -1342,7 +1342,7 @@ class MultiSign_test : public beast::unit_test::suite // Becky cannot 2-level multisign for alice. 2-level multisigning // is not supported. env(noop(alice), - msig(msig::Reg{becky, bogie}), + msig(Reg{becky, bogie}), fee(2 * baseFee), ter(tefBAD_SIGNATURE)); env.close(); @@ -1351,7 +1351,7 @@ class MultiSign_test : public beast::unit_test::suite // not yet enabled. Account const beck{"beck", KeyType::ed25519}; env(noop(alice), - msig(msig::Reg{becky, beck}), + msig(Reg{becky, beck}), fee(2 * baseFee), ter(tefBAD_SIGNATURE)); env.close(); @@ -1361,13 +1361,13 @@ class MultiSign_test : public beast::unit_test::suite env(regkey(becky, beck), msig(demon), fee(2 * baseFee)); env.close(); - env(noop(alice), msig(msig::Reg{becky, beck}), fee(2 * baseFee)); + env(noop(alice), msig(Reg{becky, beck}), fee(2 * baseFee)); env.close(); // The presence of becky's regular key does not influence whether she // can 2-level multisign; it still won't work. env(noop(alice), - msig(msig::Reg{becky, demon}), + msig(Reg{becky, demon}), fee(2 * baseFee), ter(tefBAD_SIGNATURE)); env.close(); diff --git a/src/test/jtx.h b/src/test/jtx.h index f3c69ce33c3..2da793f4b4d 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/SignerUtils.h b/src/test/jtx/SignerUtils.h new file mode 100644 index 00000000000..75358f940bb --- /dev/null +++ b/src/test/jtx/SignerUtils.h @@ -0,0 +1,55 @@ +#ifndef RIPPLE_TEST_JTX_SIGNERUTILS_H_INCLUDED +#define RIPPLE_TEST_JTX_SIGNERUTILS_H_INCLUDED + +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +struct Reg +{ + Account acct; + Account sig; + + Reg(Account const& masterSig) : acct(masterSig), sig(masterSig) + { + } + + Reg(Account const& acct_, Account const& regularSig) + : acct(acct_), sig(regularSig) + { + } + + Reg(char const* masterSig) : acct(masterSig), sig(masterSig) + { + } + + Reg(char const* acct_, char const* regularSig) + : acct(acct_), sig(regularSig) + { + } + + bool + operator<(Reg const& rhs) const + { + return acct < rhs.acct; + } +}; + +// Utility function to sort signers +inline void +sortSigners(std::vector& signers) +{ + std::sort( + signers.begin(), signers.end(), [](Reg const& lhs, Reg const& rhs) { + return lhs.acct < rhs.acct; + }); +} + +} // namespace jtx +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index d81551aa840..d2f45a2a661 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -443,7 +443,7 @@ create(A const& account, A const& dest, STAmount const& sendMax) jv[sfSendMax.jsonName] = sendMax.getJson(JsonOptions::none); jv[sfDestination.jsonName] = to_string(dest); jv[sfTransactionType.jsonName] = jss::CheckCreate; - jv[sfFlags.jsonName] = tfUniversal; + jv[sfFlags.jsonName] = tfFullyCanonicalSig; return jv; } // clang-format on diff --git a/src/test/jtx/batch.h b/src/test/jtx/batch.h new file mode 100644 index 00000000000..e8c47506199 --- /dev/null +++ b/src/test/jtx/batch.h @@ -0,0 +1,128 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_JTX_BATCH_H_INCLUDED +#define RIPPLE_TEST_JTX_BATCH_H_INCLUDED + +#include +#include +#include +#include +#include +#include "test/jtx/SignerUtils.h" +#include +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +/** Batch operations */ +namespace batch { + +/** Batch. */ +Json::Value +batch( + jtx::Account const& account, + uint32_t seq, + STAmount const& fee, + std::uint32_t flags); + +/** Adds a new Batch Txn on a JTx. */ +class add +{ +private: + Json::Value txn_; + std::uint32_t seq_; + std::optional ticket_; + +public: + add(Json::Value const& txn, + std::uint32_t const& sequence, + std::optional const& ticket = std::nullopt, + std::optional const& fee = std::nullopt) + : txn_(txn), seq_(sequence), ticket_(ticket) + { + } + + void + operator()(Env&, JTx& jtx) const; +}; + +/** Set a batch signature on a JTx. */ +class sig +{ +public: + std::vector signers; + + sig(std::vector signers_) : signers(std::move(signers_)) + { + sortSigners(signers); + } + + template + requires std::convertible_to + explicit sig(AccountType&& a0, Accounts&&... aN) + : signers{std::forward(a0), std::forward(aN)...} + { + sortSigners(signers); + } + + void + operator()(Env&, JTx& jt) const; +}; + +/** Set a batch nested multi-signature on a JTx. */ +class msig +{ +public: + Account master; + std::vector signers; + + msig(Account const& masterAccount, std::vector signers_) + : master(masterAccount), signers(std::move(signers_)) + { + sortSigners(signers); + } + + template + requires std::convertible_to + explicit msig( + Account const& masterAccount, + AccountType&& a0, + Accounts&&... aN) + : master(masterAccount) + , signers{std::forward(a0), std::forward(aN)...} + { + sortSigners(signers); + } + + void + operator()(Env&, JTx& jt) const; +}; + +} // namespace batch + +} // namespace jtx + +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/jtx/impl/AMM.cpp b/src/test/jtx/impl/AMM.cpp index 089d3508d70..05fefb322cf 100644 --- a/src/test/jtx/impl/AMM.cpp +++ b/src/test/jtx/impl/AMM.cpp @@ -820,7 +820,7 @@ pay(Account const& account, AccountID const& to, STAmount const& amount) jv[jss::Amount] = amount.getJson(JsonOptions::none); jv[jss::Destination] = to_string(to); jv[jss::TransactionType] = jss::Payment; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; return jv; } diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index 3bf69729ab0..4b799d9823d 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -219,7 +219,7 @@ escrow(AccountID const& account, AccountID const& to, STAmount const& amount) { Json::Value jv; jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; jv[jss::Account] = to_string(account); jv[jss::Destination] = to_string(to); jv[jss::Amount] = amount.getJson(JsonOptions::none); @@ -231,7 +231,7 @@ finish(AccountID const& account, AccountID const& from, std::uint32_t seq) { Json::Value jv; jv[jss::TransactionType] = jss::EscrowFinish; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; jv[jss::Account] = to_string(account); jv[sfOwner.jsonName] = to_string(from); jv[sfOfferSequence.jsonName] = seq; @@ -243,7 +243,7 @@ cancel(AccountID const& account, Account const& from, std::uint32_t seq) { Json::Value jv; jv[jss::TransactionType] = jss::EscrowCancel; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; jv[jss::Account] = to_string(account); jv[sfOwner.jsonName] = from.human(); jv[sfOfferSequence.jsonName] = seq; @@ -264,7 +264,7 @@ create( { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelCreate; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; jv[jss::Account] = to_string(account); jv[jss::Destination] = to_string(to); jv[jss::Amount] = amount.getJson(JsonOptions::none); @@ -286,7 +286,7 @@ fund( { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelFund; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; jv[jss::Account] = to_string(account); jv[sfChannel.fieldName] = to_string(channel); jv[jss::Amount] = amount.getJson(JsonOptions::none); @@ -306,7 +306,7 @@ claim( { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelClaim; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; jv[jss::Account] = to_string(account); jv["Channel"] = to_string(channel); if (amount) diff --git a/src/test/jtx/impl/batch.cpp b/src/test/jtx/impl/batch.cpp new file mode 100644 index 00000000000..50fef1cc8bc --- /dev/null +++ b/src/test/jtx/impl/batch.cpp @@ -0,0 +1,150 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +namespace batch { + +// Batch. +Json::Value +batch( + jtx::Account const& account, + uint32_t seq, + STAmount const& fee, + std::uint32_t flags) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::Batch; + jv[jss::Account] = account.human(); + jv[jss::RawTransactions] = Json::Value{Json::arrayValue}; + jv[jss::Sequence] = seq; + jv[jss::Flags] = flags; + jv[jss::Fee] = to_string(fee); + return jv; +} + +void +add::operator()(Env& env, JTx& jt) const +{ + auto const index = jt.jv[jss::RawTransactions].size(); + Json::Value& batchTransaction = jt.jv[jss::RawTransactions][index]; + + // Initialize the batch transaction + batchTransaction = Json::Value{}; + batchTransaction[jss::RawTransaction] = txn_; + batchTransaction[jss::RawTransaction][jss::SigningPubKey] = ""; + batchTransaction[jss::RawTransaction][jss::Sequence] = seq_; + batchTransaction[jss::RawTransaction][jss::Fee] = "0"; + batchTransaction[jss::RawTransaction][jss::Flags] = + batchTransaction[jss::RawTransaction][jss::Flags].asUInt() | + tfInnerBatchTxn; + + // Optionally set ticket sequence + if (ticket_.has_value()) + { + batchTransaction[jss::RawTransaction][jss::Sequence] = 0; + batchTransaction[jss::RawTransaction][sfTicketSequence.jsonName] = + *ticket_; + } +} + +void +sig::operator()(Env& env, JTx& jt) const +{ + auto const mySigners = signers; + std::optional st; + try + { + st = parse(jt.jv); + } + catch (parse_error const&) + { + env.test.log << pretty(jt.jv) << std::endl; + Rethrow(); + } + auto& js = jt[sfBatchSigners.getJsonName()]; + for (std::size_t i = 0; i < mySigners.size(); ++i) + { + auto const& e = mySigners[i]; + auto& jo = js[i][sfBatchSigner.getJsonName()]; + jo[jss::Account] = e.acct.human(); + jo[jss::SigningPubKey] = strHex(e.sig.pk().slice()); + + Serializer msg; + serializeBatch(msg, st->getFlags(), st->getBatchTransactionIDs()); + auto const sig = ripple::sign( + *publicKeyType(e.sig.pk().slice()), e.sig.sk(), msg.slice()); + jo[sfTxnSignature.getJsonName()] = + strHex(Slice{sig.data(), sig.size()}); + } +} + +void +msig::operator()(Env& env, JTx& jt) const +{ + auto const mySigners = signers; + std::optional st; + try + { + st = parse(jt.jv); + } + catch (parse_error const&) + { + env.test.log << pretty(jt.jv) << std::endl; + Rethrow(); + } + auto& bs = jt[sfBatchSigners.getJsonName()]; + auto const index = jt[sfBatchSigners.jsonName].size(); + auto& bso = bs[index][sfBatchSigner.getJsonName()]; + bso[jss::Account] = master.human(); + bso[jss::SigningPubKey] = ""; + auto& is = bso[sfSigners.getJsonName()]; + for (std::size_t i = 0; i < mySigners.size(); ++i) + { + auto const& e = mySigners[i]; + auto& iso = is[i][sfSigner.getJsonName()]; + iso[jss::Account] = e.acct.human(); + iso[jss::SigningPubKey] = strHex(e.sig.pk().slice()); + + Serializer msg; + serializeBatch(msg, st->getFlags(), st->getBatchTransactionIDs()); + auto const sig = ripple::sign( + *publicKeyType(e.sig.pk().slice()), e.sig.sk(), msg.slice()); + iso[sfTxnSignature.getJsonName()] = + strHex(Slice{sig.data(), sig.size()}); + } +} + +} // namespace batch + +} // namespace jtx +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/impl/check.cpp b/src/test/jtx/impl/check.cpp index 21af6c9cc3f..7342e8a174e 100644 --- a/src/test/jtx/impl/check.cpp +++ b/src/test/jtx/impl/check.cpp @@ -36,7 +36,7 @@ cash(jtx::Account const& dest, uint256 const& checkId, STAmount const& amount) jv[sfAmount.jsonName] = amount.getJson(JsonOptions::none); jv[sfCheckID.jsonName] = to_string(checkId); jv[sfTransactionType.jsonName] = jss::CheckCash; - jv[sfFlags.jsonName] = tfUniversal; + jv[sfFlags.jsonName] = tfFullyCanonicalSig; return jv; } @@ -52,7 +52,7 @@ cash( jv[sfDeliverMin.jsonName] = atLeast.value.getJson(JsonOptions::none); jv[sfCheckID.jsonName] = to_string(checkId); jv[sfTransactionType.jsonName] = jss::CheckCash; - jv[sfFlags.jsonName] = tfUniversal; + jv[sfFlags.jsonName] = tfFullyCanonicalSig; return jv; } @@ -64,7 +64,7 @@ cancel(jtx::Account const& dest, uint256 const& checkId) jv[sfAccount.jsonName] = dest.human(); jv[sfCheckID.jsonName] = to_string(checkId); jv[sfTransactionType.jsonName] = jss::CheckCancel; - jv[sfFlags.jsonName] = tfUniversal; + jv[sfFlags.jsonName] = tfFullyCanonicalSig; return jv; } diff --git a/src/test/jtx/impl/credentials.cpp b/src/test/jtx/impl/credentials.cpp index bc7ccf93cd4..f821ef44d3e 100644 --- a/src/test/jtx/impl/credentials.cpp +++ b/src/test/jtx/impl/credentials.cpp @@ -39,7 +39,7 @@ create( jv[jss::Account] = issuer.human(); jv[jss::Subject] = subject.human(); - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; jv[sfCredentialType.jsonName] = strHex(credType); return jv; @@ -56,7 +56,7 @@ accept( jv[jss::Account] = subject.human(); jv[jss::Issuer] = issuer.human(); jv[sfCredentialType.jsonName] = strHex(credType); - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; return jv; } @@ -74,7 +74,7 @@ deleteCred( jv[jss::Subject] = subject.human(); jv[jss::Issuer] = issuer.human(); jv[sfCredentialType.jsonName] = strHex(credType); - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; return jv; } diff --git a/src/test/jtx/impl/did.cpp b/src/test/jtx/impl/did.cpp index a9a6e974ef4..1cce5f251f9 100644 --- a/src/test/jtx/impl/did.cpp +++ b/src/test/jtx/impl/did.cpp @@ -34,7 +34,7 @@ set(jtx::Account const& account) Json::Value jv; jv[jss::TransactionType] = jss::DIDSet; jv[jss::Account] = to_string(account.id()); - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; return jv; } @@ -44,7 +44,7 @@ setValid(jtx::Account const& account) Json::Value jv; jv[jss::TransactionType] = jss::DIDSet; jv[jss::Account] = to_string(account.id()); - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; jv[sfURI.jsonName] = strHex(std::string{"uri"}); return jv; } @@ -55,7 +55,7 @@ del(jtx::Account const& account) Json::Value jv; jv[jss::TransactionType] = jss::DIDDelete; jv[jss::Account] = to_string(account.id()); - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; return jv; } diff --git a/src/test/jtx/impl/ledgerStateFix.cpp b/src/test/jtx/impl/ledgerStateFix.cpp index 2f121dc2671..87408db1d81 100644 --- a/src/test/jtx/impl/ledgerStateFix.cpp +++ b/src/test/jtx/impl/ledgerStateFix.cpp @@ -38,7 +38,7 @@ nftPageLinks(jtx::Account const& acct, jtx::Account const& owner) jv[sfLedgerFixType.jsonName] = LedgerStateFix::nfTokenPageLink; jv[sfOwner.jsonName] = owner.human(); jv[sfTransactionType.jsonName] = jss::LedgerStateFix; - jv[sfFlags.jsonName] = tfUniversal; + jv[sfFlags.jsonName] = tfFullyCanonicalSig; return jv; } diff --git a/src/test/jtx/impl/multisign.cpp b/src/test/jtx/impl/multisign.cpp index 42c3bfc78bf..0b1d192252a 100644 --- a/src/test/jtx/impl/multisign.cpp +++ b/src/test/jtx/impl/multisign.cpp @@ -66,17 +66,6 @@ signers(Account const& account, none_t) //------------------------------------------------------------------------------ -msig::msig(std::vector signers_) : signers(std::move(signers_)) -{ - // Signatures must be applied in sorted order. - std::sort( - signers.begin(), - signers.end(), - [](msig::Reg const& lhs, msig::Reg const& rhs) { - return lhs.acct.id() < rhs.acct.id(); - }); -} - void msig::operator()(Env& env, JTx& jt) const { diff --git a/src/test/jtx/impl/pay.cpp b/src/test/jtx/impl/pay.cpp index 2a627223fdd..5a26463754d 100644 --- a/src/test/jtx/impl/pay.cpp +++ b/src/test/jtx/impl/pay.cpp @@ -34,7 +34,7 @@ pay(AccountID const& account, AccountID const& to, AnyAmount amount) jv[jss::Amount] = amount.value.getJson(JsonOptions::none); jv[jss::Destination] = to_string(to); jv[jss::TransactionType] = jss::Payment; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; return jv; } Json::Value diff --git a/src/test/jtx/impl/txflags.cpp b/src/test/jtx/impl/txflags.cpp index 7bc59876a99..b2e45721f67 100644 --- a/src/test/jtx/impl/txflags.cpp +++ b/src/test/jtx/impl/txflags.cpp @@ -27,7 +27,7 @@ namespace jtx { void txflags::operator()(Env&, JTx& jt) const { - jt[jss::Flags] = v_ /*| tfUniversal*/; + jt[jss::Flags] = v_ /*| tfFullyCanonicalSig*/; } } // namespace jtx diff --git a/src/test/jtx/impl/xchain_bridge.cpp b/src/test/jtx/impl/xchain_bridge.cpp index 43b0e7c2f96..e07f145806b 100644 --- a/src/test/jtx/impl/xchain_bridge.cpp +++ b/src/test/jtx/impl/xchain_bridge.cpp @@ -84,7 +84,7 @@ bridge_create( minAccountCreate->getJson(JsonOptions::none); jv[jss::TransactionType] = jss::XChainCreateBridge; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; return jv; } @@ -107,7 +107,7 @@ bridge_modify( minAccountCreate->getJson(JsonOptions::none); jv[jss::TransactionType] = jss::XChainModifyBridge; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; return jv; } @@ -126,7 +126,7 @@ xchain_create_claim_id( jv[sfOtherChainSource.getJsonName()] = otherChainSource.human(); jv[jss::TransactionType] = jss::XChainCreateClaimID; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; return jv; } @@ -148,7 +148,7 @@ xchain_commit( jv[sfOtherChainDestination.getJsonName()] = dst->human(); jv[jss::TransactionType] = jss::XChainCommit; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; return jv; } @@ -169,7 +169,7 @@ xchain_claim( jv[sfAmount.getJsonName()] = amt.value.getJson(JsonOptions::none); jv[jss::TransactionType] = jss::XChainClaim; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; return jv; } @@ -191,7 +191,7 @@ sidechain_xchain_account_create( reward.value.getJson(JsonOptions::none); jv[jss::TransactionType] = jss::XChainAccountCreateCommit; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; return jv; } @@ -242,7 +242,7 @@ claim_attestation( result[sfDestination.getJsonName()] = toBase58(*dst); result[jss::TransactionType] = jss::XChainAddClaimAttestation; - result[jss::Flags] = tfUniversal; + result[jss::Flags] = tfFullyCanonicalSig; return result; } @@ -297,7 +297,7 @@ create_account_attestation( rewardAmount.value.getJson(JsonOptions::none); result[jss::TransactionType] = jss::XChainAddAccountCreateAttestation; - result[jss::Flags] = tfUniversal; + result[jss::Flags] = tfFullyCanonicalSig; return result; } diff --git a/src/test/jtx/multisign.h b/src/test/jtx/multisign.h index 44cee17b7bf..ac4d0f4b292 100644 --- a/src/test/jtx/multisign.h +++ b/src/test/jtx/multisign.h @@ -21,6 +21,7 @@ #define RIPPLE_TEST_JTX_MULTISIGN_H_INCLUDED #include +#include #include #include #include @@ -64,48 +65,19 @@ signers(Account const& account, none_t); class msig { public: - struct Reg - { - Account acct; - Account sig; - - Reg(Account const& masterSig) : acct(masterSig), sig(masterSig) - { - } - - Reg(Account const& acct_, Account const& regularSig) - : acct(acct_), sig(regularSig) - { - } - - Reg(char const* masterSig) : acct(masterSig), sig(masterSig) - { - } - - Reg(char const* acct_, char const* regularSig) - : acct(acct_), sig(regularSig) - { - } - - bool - operator<(Reg const& rhs) const - { - return acct < rhs.acct; - } - }; - std::vector signers; -public: - msig(std::vector signers_); + msig(std::vector signers_) : signers(std::move(signers_)) + { + sortSigners(signers); + } template requires std::convertible_to explicit msig(AccountType&& a0, Accounts&&... aN) - : msig{std::vector{ - std::forward(a0), - std::forward(aN)...}} + : signers{std::forward(a0), std::forward(aN)...} { + sortSigners(signers); } void diff --git a/src/test/rpc/AccountLines_test.cpp b/src/test/rpc/AccountLines_test.cpp index d104ea14b0a..6ee357117cc 100644 --- a/src/test/rpc/AccountLines_test.cpp +++ b/src/test/rpc/AccountLines_test.cpp @@ -570,7 +570,7 @@ class AccountLines_test : public beast::unit_test::suite STAmount const& amount) { Json::Value jv; jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; jv[jss::Account] = account.human(); jv[jss::Destination] = to.human(); jv[jss::Amount] = amount.getJson(JsonOptions::none); @@ -586,7 +586,7 @@ class AccountLines_test : public beast::unit_test::suite PublicKey const& pk) { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelCreate; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; jv[jss::Account] = account.human(); jv[jss::Destination] = to.human(); jv[jss::Amount] = amount.getJson(JsonOptions::none); diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index ae1e3c195d0..7afc3e1396a 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -696,7 +696,7 @@ class AccountObjects_test : public beast::unit_test::suite // gw creates an escrow that we can look for in the ledger. Json::Value jvEscrow; jvEscrow[jss::TransactionType] = jss::EscrowCreate; - jvEscrow[jss::Flags] = tfUniversal; + jvEscrow[jss::Flags] = tfFullyCanonicalSig; jvEscrow[jss::Account] = gw.human(); jvEscrow[jss::Destination] = gw.human(); jvEscrow[jss::Amount] = XRP(100).value().getJson(JsonOptions::none); @@ -910,7 +910,7 @@ class AccountObjects_test : public beast::unit_test::suite // for. Json::Value jvPayChan; jvPayChan[jss::TransactionType] = jss::PaymentChannelCreate; - jvPayChan[jss::Flags] = tfUniversal; + jvPayChan[jss::Flags] = tfFullyCanonicalSig; jvPayChan[jss::Account] = gw.human(); jvPayChan[jss::Destination] = alice.human(); jvPayChan[jss::Amount] = @@ -936,7 +936,7 @@ class AccountObjects_test : public beast::unit_test::suite // gw creates a DID that we can look for in the ledger. Json::Value jvDID; jvDID[jss::TransactionType] = jss::DIDSet; - jvDID[jss::Flags] = tfUniversal; + jvDID[jss::Flags] = tfFullyCanonicalSig; jvDID[jss::Account] = gw.human(); jvDID[sfURI.jsonName] = strHex(std::string{"uri"}); env(jvDID); diff --git a/src/test/rpc/AccountTx_test.cpp b/src/test/rpc/AccountTx_test.cpp index f6a9225ec48..65cf9ebc276 100644 --- a/src/test/rpc/AccountTx_test.cpp +++ b/src/test/rpc/AccountTx_test.cpp @@ -454,7 +454,7 @@ class AccountTx_test : public beast::unit_test::suite STAmount const& amount) { Json::Value escro; escro[jss::TransactionType] = jss::EscrowCreate; - escro[jss::Flags] = tfUniversal; + escro[jss::Flags] = tfFullyCanonicalSig; escro[jss::Account] = account.human(); escro[jss::Destination] = to.human(); escro[jss::Amount] = amount.getJson(JsonOptions::none); @@ -483,7 +483,7 @@ class AccountTx_test : public beast::unit_test::suite { Json::Value escrowFinish; escrowFinish[jss::TransactionType] = jss::EscrowFinish; - escrowFinish[jss::Flags] = tfUniversal; + escrowFinish[jss::Flags] = tfFullyCanonicalSig; escrowFinish[jss::Account] = alice.human(); escrowFinish[sfOwner.jsonName] = alice.human(); escrowFinish[sfOfferSequence.jsonName] = escrowFinishSeq; @@ -492,7 +492,7 @@ class AccountTx_test : public beast::unit_test::suite { Json::Value escrowCancel; escrowCancel[jss::TransactionType] = jss::EscrowCancel; - escrowCancel[jss::Flags] = tfUniversal; + escrowCancel[jss::Flags] = tfFullyCanonicalSig; escrowCancel[jss::Account] = alice.human(); escrowCancel[sfOwner.jsonName] = alice.human(); escrowCancel[sfOfferSequence.jsonName] = escrowCancelSeq; @@ -506,7 +506,7 @@ class AccountTx_test : public beast::unit_test::suite std::uint32_t payChanSeq{env.seq(alice)}; Json::Value payChanCreate; payChanCreate[jss::TransactionType] = jss::PaymentChannelCreate; - payChanCreate[jss::Flags] = tfUniversal; + payChanCreate[jss::Flags] = tfFullyCanonicalSig; payChanCreate[jss::Account] = alice.human(); payChanCreate[jss::Destination] = gw.human(); payChanCreate[jss::Amount] = @@ -523,7 +523,7 @@ class AccountTx_test : public beast::unit_test::suite { Json::Value payChanFund; payChanFund[jss::TransactionType] = jss::PaymentChannelFund; - payChanFund[jss::Flags] = tfUniversal; + payChanFund[jss::Flags] = tfFullyCanonicalSig; payChanFund[jss::Account] = alice.human(); payChanFund[sfChannel.jsonName] = payChanIndex; payChanFund[jss::Amount] = diff --git a/src/test/rpc/LedgerData_test.cpp b/src/test/rpc/LedgerData_test.cpp index 1e4f97a935f..00a68193120 100644 --- a/src/test/rpc/LedgerData_test.cpp +++ b/src/test/rpc/LedgerData_test.cpp @@ -368,7 +368,7 @@ class LedgerData_test : public beast::unit_test::suite { Json::Value jv; jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; jv[jss::Account] = Account{"bob5"}.human(); jv[jss::Destination] = Account{"bob6"}.human(); jv[jss::Amount] = XRP(50).value().getJson(JsonOptions::none); @@ -382,7 +382,7 @@ class LedgerData_test : public beast::unit_test::suite { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelCreate; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; jv[jss::Account] = Account{"bob6"}.human(); jv[jss::Destination] = Account{"bob7"}.human(); jv[jss::Amount] = XRP(100).value().getJson(JsonOptions::none); diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index 8e28eb98cdd..3234c35a0fe 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -1647,7 +1647,7 @@ class LedgerRPC_test : public beast::unit_test::suite NetClock::time_point const& cancelAfter) { Json::Value jv; jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; jv[jss::Account] = account.human(); jv[jss::Destination] = to.human(); jv[jss::Amount] = amount.getJson(JsonOptions::none); diff --git a/src/xrpld/app/ledger/detail/OpenLedger.cpp b/src/xrpld/app/ledger/detail/OpenLedger.cpp index 461d98ae4ac..520392bce4e 100644 --- a/src/xrpld/app/ledger/detail/OpenLedger.cpp +++ b/src/xrpld/app/ledger/detail/OpenLedger.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include namespace ripple { @@ -121,6 +122,16 @@ OpenLedger::accept( { auto const& tx = txpair.first; auto const txId = tx->getTransactionID(); + + // skip batch txns + if (tx->isFlag(tfInnerBatchTxn)) + { + assert( + txpair.second && + txpair.second->isFieldPresent(sfParentBatchID)); + continue; + } + if (auto const toSkip = app.getHashRouter().shouldRelay(txId)) { JLOG(j_.debug()) << "Relaying recovered tx " << txId; diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index 8e483811145..1d4aa7ed061 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -64,6 +64,7 @@ #include #include #include +#include #include #include #include @@ -1140,6 +1141,15 @@ NetworkOPsImp::submitTransaction(std::shared_ptr const& iTrans) return; } + // Enforce Network bar for batch txn + if (auto const view = m_ledgerMaster.getCurrentLedger(); + view->rules().enabled(featureBatch) && iTrans->isFlag(tfInnerBatchTxn)) + { + JLOG(m_journal.error()) + << "Submitted transaction invalid: tfInnerBatchTxn flag present."; + return; + } + // this is an asynchronous interface auto const trans = sterilize(*iTrans); @@ -1204,15 +1214,25 @@ NetworkOPsImp::processTransaction( return; } + auto const view = m_ledgerMaster.getCurrentLedger(); + + // This function is called by several different parts of the codebase + // under no circumstances will we ever accept an inner txn within a batch + // txn from the network. + auto const tx = *transaction->getSTransaction(); + if (view->rules().enabled(featureBatch) && tx.isFlag(tfInnerBatchTxn)) + { + transaction->setStatus(INVALID); + transaction->setResult(temINVALID_FLAG); + app_.getHashRouter().setFlags(transaction->getID(), SF_BAD); + return; + } + // NOTE eahennis - I think this check is redundant, // but I'm not 100% sure yet. // If so, only cost is looking up HashRouter flags. - auto const view = m_ledgerMaster.getCurrentLedger(); - auto const [validity, reason] = checkValidity( - app_.getHashRouter(), - *transaction->getSTransaction(), - view->rules(), - app_.config()); + auto const [validity, reason] = + checkValidity(app_.getHashRouter(), tx, view->rules(), app_.config()); XRPL_ASSERT( validity == Validity::Valid, "ripple::NetworkOPsImp::processTransaction : valid validity"); @@ -1475,12 +1495,13 @@ NetworkOPsImp::apply(std::unique_lock& batchLock) auto const toSkip = app_.getHashRouter().shouldRelay(e.transaction->getID()); - if (toSkip) + if (auto const txn = *(e.transaction->getSTransaction()); + toSkip && !txn.isFlag(tfInnerBatchTxn)) { protocol::TMTransaction tx; Serializer s; - e.transaction->getSTransaction()->add(s); + txn.add(s); tx.set_rawtransaction(s.data(), s.size()); tx.set_status(protocol::tsCURRENT); tx.set_receivetimestamp( @@ -2777,6 +2798,10 @@ NetworkOPsImp::pubProposedTransaction( std::shared_ptr const& transaction, TER result) { + // never publish an inner txn inside a batch txn + if (transaction->isFlag(tfInnerBatchTxn)) + return; + MultiApiJson jvObj = transJson(transaction, result, false, ledger, std::nullopt); diff --git a/src/xrpld/app/tx/applySteps.h b/src/xrpld/app/tx/applySteps.h index 1df537515e9..9692280db52 100644 --- a/src/xrpld/app/tx/applySteps.h +++ b/src/xrpld/app/tx/applySteps.h @@ -152,6 +152,8 @@ struct PreflightResult public: /// From the input - the transaction STTx const& tx; + /// From the input - the batch identifier, if part of a batch + std::optional const batchId; /// From the input - the rules Rules const rules; /// Consequences of the transaction @@ -170,6 +172,7 @@ struct PreflightResult Context const& ctx_, std::pair const& result) : tx(ctx_.tx) + , batchId(ctx_.batchId) , rules(ctx_.rules) , consequences(result.second) , flags(ctx_.flags) @@ -197,6 +200,8 @@ struct PreclaimResult ReadView const& view; /// From the input - the transaction STTx const& tx; + /// From the input - the batch identifier, if part of a batch + std::optional const batchId; /// From the input - the flags ApplyFlags const flags; /// From the input - the journal @@ -204,6 +209,7 @@ struct PreclaimResult /// Intermediate transaction result TER const ter; + /// Success flag - whether the transaction is likely to /// claim a fee bool const likelyToClaimFee; @@ -213,6 +219,7 @@ struct PreclaimResult PreclaimResult(Context const& ctx_, TER ter_) : view(ctx_.view) , tx(ctx_.tx) + , batchId(ctx_.batchId) , flags(ctx_.flags) , j(ctx_.j) , ter(ter_) @@ -242,13 +249,24 @@ struct PreclaimResult @return A `PreflightResult` object containing, among other things, the `TER` code. */ +/** @{ */ +PreflightResult +preflight( + Application& app, + Rules const& rules, + STTx const& tx, + ApplyFlags flags, + beast::Journal j); + PreflightResult preflight( Application& app, Rules const& rules, + uint256 const& batchId, STTx const& tx, ApplyFlags flags, beast::Journal j); +/** @} */ /** Gate a transaction based on static ledger information. diff --git a/src/xrpld/app/tx/detail/ApplyContext.cpp b/src/xrpld/app/tx/detail/ApplyContext.cpp index 620c286bd73..3f1c2daa21e 100644 --- a/src/xrpld/app/tx/detail/ApplyContext.cpp +++ b/src/xrpld/app/tx/detail/ApplyContext.cpp @@ -31,6 +31,7 @@ namespace ripple { ApplyContext::ApplyContext( Application& app_, OpenView& base, + std::optional const& batchId, STTx const& tx_, TER preclaimResult_, XRPAmount baseFee_, @@ -43,6 +44,7 @@ ApplyContext::ApplyContext( , journal(journal_) , base_(base) , flags_(flags) + , batchId_(batchId) { view_.emplace(&base_, flags_); } @@ -56,7 +58,7 @@ ApplyContext::discard() void ApplyContext::apply(TER ter) { - view_->apply(base_, tx, ter, journal); + view_->apply(base_, tx, ter, batchId_, journal); } std::size_t diff --git a/src/xrpld/app/tx/detail/ApplyContext.h b/src/xrpld/app/tx/detail/ApplyContext.h index 6ffe7a4d576..8f6b1d759c2 100644 --- a/src/xrpld/app/tx/detail/ApplyContext.h +++ b/src/xrpld/app/tx/detail/ApplyContext.h @@ -36,13 +36,35 @@ class ApplyContext { public: explicit ApplyContext( - Application& app, - OpenView& base, - STTx const& tx, - TER preclaimResult, - XRPAmount baseFee, + Application& app_, + OpenView& base_, + std::optional const& batchId, + STTx const& tx_, + TER preclaimResult_, + XRPAmount baseFee_, ApplyFlags flags, - beast::Journal = beast::Journal{beast::Journal::getNullSink()}); + beast::Journal journal_ = beast::Journal{ + beast::Journal::getNullSink()}); + + explicit ApplyContext( + Application& app_, + OpenView& base_, + STTx const& tx_, + TER preclaimResult_, + XRPAmount baseFee_, + ApplyFlags flags, + beast::Journal journal = beast::Journal{beast::Journal::getNullSink()}) + : ApplyContext( + app_, + base_, + std::nullopt, + tx_, + preclaimResult_, + baseFee_, + flags, + journal) + { + } Application& app; STTx const& tx; @@ -124,6 +146,9 @@ class ApplyContext OpenView& base_; ApplyFlags flags_; std::optional view_; + + // The ID of the batch transaction we are executing under, if seated. + std::optional batchId_; }; } // namespace ripple diff --git a/src/xrpld/app/tx/detail/Batch.cpp b/src/xrpld/app/tx/detail/Batch.cpp new file mode 100644 index 00000000000..6289aa91853 --- /dev/null +++ b/src/xrpld/app/tx/detail/Batch.cpp @@ -0,0 +1,251 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +XRPAmount +Batch::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + // Calculate the Inner Txn Fees + XRPAmount txnFees{0}; + if (tx.isFieldPresent(sfRawTransactions)) + { + XRPAmount txFees{0}; + auto const& txns = tx.getFieldArray(sfRawTransactions); + for (STObject txn : txns) + { + STTx const stx = STTx{std::move(txn)}; + txFees += ripple::calculateBaseFee(view, stx); + } + txnFees += txFees; + } + + // Calculate the BatchSigners Fees + std::int32_t signerCount = tx.isFieldPresent(sfBatchSigners) + ? tx.getFieldArray(sfBatchSigners).size() + : 0; + + // sum of inner tx fees + 10 drops per signature + 20 drops for processing + return ((signerCount + 2) * view.fees().base) + txnFees; +} + +NotTEC +Batch::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureBatch)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + auto const batchId = ctx.tx.getTransactionID(); + auto const outerAccount = ctx.tx.getAccountID(sfAccount); + auto const flags = ctx.tx.getFlags(); + + if (flags & tfBatchMask) + { + JLOG(ctx.j.trace()) + << "BatchTrace[" << batchId << "]:" << "invalid flags."; + return temINVALID_FLAG; + } + + if (std::popcount( + flags & + (tfAllOrNothing | tfOnlyOne | tfUntilFailure | tfIndependent)) != 1) + { + JLOG(ctx.j.trace()) + << "BatchTrace[" << batchId << "]:" << "too many flags."; + return temMALFORMED; + } + + auto rawTxns = ctx.tx.getFieldArray(sfRawTransactions); + if (rawTxns.size() == 0) + { + JLOG(ctx.j.trace()) + << "BatchTrace[" << batchId << "]:" << "txns array is empty."; + return temARRAY_EMPTY; + } + + if (rawTxns.size() > maxBatchTxCount) + { + JLOG(ctx.j.trace()) << "BatchTrace[" << batchId + << "]:" << "txns array exceeds 8 entries."; + return temARRAY_TOO_LARGE; + } + + auto const& hashes = ctx.tx.getBatchTransactionIDs(); + + if (hashes.size() != rawTxns.size()) + { + JLOG(ctx.j.trace()) << "BatchTrace[" << batchId + << "]:" << "hashes array size does not match txns."; + return temMALFORMED; // LCOV_EXCL_LINE + } + + if (auto const ret = preflight2(ctx); !isTesSuccess(ret)) + return ret; + + // Validation Inner Batch Txns + std::set requiredSigners; + std::unordered_set> uniqueHashes; + for (STObject rb : rawTxns) + { + STTx const stx = STTx{std::move(rb)}; + auto const hash = stx.getTransactionID(); + if (!uniqueHashes.emplace(hash).second) + { + JLOG(ctx.j.trace()) + << "BatchTrace[" << batchId << "]:" << "duplicate TxID found." + << "txID: " << hash; + return temMALFORMED; + } + + if (!stx.isFieldPresent(sfTransactionType)) + { + JLOG(ctx.j.trace()) + << "BatchTrace[" << batchId + << "]:" << "TransactionType missing in inner txn." + << "txID: " << hash; + return temINVALID_BATCH; // LCOV_EXCL_LINE + } + + if (stx.getFieldU16(sfTransactionType) == ttBATCH) + { + JLOG(ctx.j.trace()) + << "BatchTrace[" << batchId + << "]:" << "batch cannot have an inner batch txn." + << "txID: " << hash; + return temINVALID_BATCH; + } + + if (stx.isFieldPresent(sfTxnSignature) || + stx.isFieldPresent(sfSigners) || !stx.getSigningPubKey().empty()) + { + JLOG(ctx.j.trace()) << "BatchTrace[" << batchId << "]:" + << "inner txn cannot include TxnSignature or " + "Signers and SigningPubKey must be empty." + << "txID: " << hash; + return temINVALID_BATCH; + } + + auto const innerAccount = stx.getAccountID(sfAccount); + if (auto const preflightResult = + ripple::preflight(ctx.app, ctx.rules, stx, tapFAIL_HARD, ctx.j); + preflightResult.ter != tesSUCCESS) + { + JLOG(ctx.j.trace()) << "BatchTrace[" << batchId + << "]:" << "inner txn preflight failed." + << "txID: " << hash; + return temINVALID_BATCH; + } + + // If the inner account is the same as the outer account, continue. + // 1. We do not add it to the required signers set. + // 2. We do check a signature for the inner account does not exist. + if (innerAccount == outerAccount) + continue; + + // Add the inner account to the required signers set. + requiredSigners.insert(innerAccount); + } + + // Validation Batch Signers + std::set batchSigners; + if (ctx.tx.isFieldPresent(sfBatchSigners)) + { + STArray const signers = ctx.tx.getFieldArray(sfBatchSigners); + + // Check that the batch signers array is not too large. + if (signers.size() > 8) + { + JLOG(ctx.j.trace()) << "BatchTrace[" << batchId + << "]:" << "signers array exceeds 8 entries."; + return temARRAY_TOO_LARGE; + } + + // Add the batch signers to the set. + for (auto const& signer : signers) + { + AccountID const signerAccount = signer.getAccountID(sfAccount); + if (signerAccount == outerAccount) + { + JLOG(ctx.j.trace()) + << "BatchTrace[" << batchId << "]:" + << "signer cannot be the outer account: " << signerAccount; + return temBAD_SIGNER; + } + + if (!batchSigners.insert(signerAccount).second) + { + JLOG(ctx.j.trace()) + << "BatchTrace[" << batchId + << "]:" << "duplicate signer found: " << signerAccount; + return temBAD_SIGNER; + } + + // Check that the batch signer is in the required signers set. + if (requiredSigners.erase(signerAccount) == 0) + { + JLOG(ctx.j.trace()) + << "BatchTrace[" << batchId + << "]:" << "no account signature for inner txn."; + return temBAD_SIGNER; + } + } + + // Check the batch signers signatures. + auto const sigResult = ctx.tx.checkBatchSign( + STTx::RequireFullyCanonicalSig::yes, ctx.rules); + + if (!sigResult) + { + JLOG(ctx.j.trace()) << "BatchTrace[" << batchId + << "]:" << "invalid batch txn signature."; + return temBAD_SIGNATURE; + } + } + + if (!requiredSigners.empty()) + { + JLOG(ctx.j.trace()) + << "BatchTrace[" << batchId << "]:" << "invalid batch signers."; + return temBAD_SIGNER; + } + return tesSUCCESS; +} + +TER +Batch::doApply() +{ + // Inner txns are applied in `applyBatchTransactions`, after the outer batch + // txn is applied + return tesSUCCESS; +} + +} // namespace ripple \ No newline at end of file diff --git a/src/xrpld/app/tx/detail/Batch.h b/src/xrpld/app/tx/detail/Batch.h new file mode 100644 index 00000000000..dcf3bc2875e --- /dev/null +++ b/src/xrpld/app/tx/detail/Batch.h @@ -0,0 +1,53 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_BATCH_H_INCLUDED +#define RIPPLE_TX_BATCH_H_INCLUDED + +#include +#include +#include +#include + +namespace ripple { + +class Batch : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit Batch(ApplyContext& ctx) : Transactor(ctx) + { + } + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static NotTEC + preflight(PreflightContext const& ctx); + + TER + doApply() override; +}; + +using Batch = Batch; + +} // namespace ripple + +#endif \ No newline at end of file diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 63794023d40..5046ae90f0a 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -140,7 +140,7 @@ XRPNotCreated::visitEntry( bool XRPNotCreated::finalize( STTx const& tx, - TER const, + TER const res, XRPAmount const fee, ReadView const&, beast::Journal const& j) diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index df3d32f8a58..b6644a90958 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include namespace ripple { @@ -41,6 +42,16 @@ namespace ripple { NotTEC preflight0(PreflightContext const& ctx) { + if (ctx.tx.isFlag(tfInnerBatchTxn) && !ctx.rules.enabled(featureBatch)) + return temINVALID_FLAG; + + if (isPseudoTx(ctx.tx) && ctx.tx.isFlag(tfInnerBatchTxn)) + { + JLOG(ctx.j.warn()) << "Pseudo transactions cannot contain the " + "tfInnerBatchTxn flag."; + return temINVALID_FLAG; + } + if (!isPseudoTx(ctx.tx) || ctx.tx.isFieldPresent(sfNetworkID)) { uint32_t nodeNID = ctx.app.config().NETWORK_ID; @@ -145,18 +156,6 @@ preflight2(PreflightContext const& ctx) //------------------------------------------------------------------------------ -PreflightContext::PreflightContext( - Application& app_, - STTx const& tx_, - Rules const& rules_, - ApplyFlags flags_, - beast::Journal j_) - : app(app_), tx(tx_), rules(rules_), flags(flags_), j(j_) -{ -} - -//------------------------------------------------------------------------------ - Transactor::Transactor(ApplyContext& ctx) : ctx_(ctx), j_(ctx.journal), account_(ctx.tx.getAccountID(sfAccount)) { @@ -197,6 +196,16 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee) return temBAD_FEE; auto const feePaid = ctx.tx[sfFee].xrp(); + + if (ctx.flags & tapBATCH) + { + if (feePaid == beast::zero) + return tesSUCCESS; + + JLOG(ctx.j.warn()) << "Batch: sfFee must be zero."; + return temBAD_FEE; + } + if (!isLegalAmount(feePaid) || feePaid < beast::zero) return temBAD_FEE; @@ -486,17 +495,29 @@ Transactor::apply() NotTEC Transactor::checkSign(PreclaimContext const& ctx) { + // Ignore signature check on batch inner transactions + if (ctx.tx.isFlag(tfInnerBatchTxn)) + { + // Defensive Check: These values are also checked in Batch::preflight + if (ctx.tx.isFieldPresent(sfTxnSignature) || + !ctx.tx.getSigningPubKey().empty() || + ctx.tx.isFieldPresent(sfSigners)) + { + return temINVALID_FLAG; + } + return tesSUCCESS; + } + + auto const idAccount = ctx.tx.getAccountID(sfAccount); + // If the pk is empty, then we must be multi-signing. if (ctx.tx.getSigningPubKey().empty()) - return checkMultiSign(ctx); - - return checkSingleSign(ctx); -} + { + STArray const& txSigners(ctx.tx.getFieldArray(sfSigners)); + return checkMultiSign(ctx.view, idAccount, txSigners, ctx.j); + } -NotTEC -Transactor::checkSingleSign(PreclaimContext const& ctx) -{ - // Check that the value in the signing key slot is a public key. + // Check Single Sign auto const pkSigner = ctx.tx.getSigningPubKey(); if (!publicKeyType(makeSlice(pkSigner))) { @@ -504,17 +525,65 @@ Transactor::checkSingleSign(PreclaimContext const& ctx) << "checkSingleSign: signing public key type is unknown"; return tefBAD_AUTH; // FIXME: should be better error! } - - // Look up the account. auto const idSigner = calcAccountID(PublicKey(makeSlice(pkSigner))); - auto const idAccount = ctx.tx.getAccountID(sfAccount); auto const sleAccount = ctx.view.read(keylet::account(idAccount)); if (!sleAccount) return terNO_ACCOUNT; + return checkSingleSign( + idSigner, idAccount, sleAccount, ctx.view.rules(), ctx.j); +} + +NotTEC +Transactor::checkBatchSign(PreclaimContext const& ctx) +{ + NotTEC ret = tesSUCCESS; + STArray const& signers{ctx.tx.getFieldArray(sfBatchSigners)}; + for (auto const& signer : signers) + { + auto const idAccount = signer.getAccountID(sfAccount); + + Blob const& pkSigner = signer.getFieldVL(sfSigningPubKey); + if (pkSigner.empty()) + { + STArray const& txSigners(signer.getFieldArray(sfSigners)); + if (ret = checkMultiSign(ctx.view, idAccount, txSigners, ctx.j); + !isTesSuccess(ret)) + return ret; + } + else + { + if (!publicKeyType(makeSlice(pkSigner))) + return tefBAD_AUTH; + + auto const idSigner = calcAccountID(PublicKey(makeSlice(pkSigner))); + auto const sleAccount = ctx.view.read(keylet::account(idAccount)); + + // We dont need to check the regular key or multisign here + // because the account does not exist. + if (!sleAccount) + return tesSUCCESS; + + if (ret = checkSingleSign( + idSigner, idAccount, sleAccount, ctx.view.rules(), ctx.j); + !isTesSuccess(ret)) + return ret; + } + } + return ret; +} + +NotTEC +Transactor::checkSingleSign( + AccountID const& idSigner, + AccountID const& idAccount, + std::shared_ptr sleAccount, + Rules const& rules, + beast::Journal j) +{ bool const isMasterDisabled = sleAccount->isFlag(lsfDisableMaster); - if (ctx.view.rules().enabled(fixMasterKeyAsRegularKey)) + if (rules.enabled(fixMasterKeyAsRegularKey)) { // Signed with regular key. if ((*sleAccount)[~sfRegularKey] == idSigner) @@ -551,16 +620,14 @@ Transactor::checkSingleSign(PreclaimContext const& ctx) else if (sleAccount->isFieldPresent(sfRegularKey)) { // Signing key does not match master or regular key. - JLOG(ctx.j.trace()) - << "checkSingleSign: Not authorized to use account."; + JLOG(j.trace()) << "checkSingleSign: Not authorized to use account."; return tefBAD_AUTH; } else { // No regular key on account and signing key does not match master key. // FIXME: Why differentiate this case from tefBAD_AUTH? - JLOG(ctx.j.trace()) - << "checkSingleSign: Not authorized to use account."; + JLOG(j.trace()) << "checkSingleSign: Not authorized to use account."; return tefBAD_AUTH_MASTER; } @@ -568,16 +635,19 @@ Transactor::checkSingleSign(PreclaimContext const& ctx) } NotTEC -Transactor::checkMultiSign(PreclaimContext const& ctx) +Transactor::checkMultiSign( + ReadView const& view, + AccountID const& id, + STArray const& txSigners, + beast::Journal j) { - auto const id = ctx.tx.getAccountID(sfAccount); // Get mTxnAccountID's SignerList and Quorum. std::shared_ptr sleAccountSigners = - ctx.view.read(keylet::signers(id)); + view.read(keylet::signers(id)); // If the signer list doesn't exist the account is not multi-signing. if (!sleAccountSigners) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Invalid: Not a multi-signing account."; return tefNOT_MULTI_SIGNING; } @@ -592,12 +662,11 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) "ripple::Transactor::checkMultiSign : signer list ID is 0"); auto accountSigners = - SignerEntries::deserialize(*sleAccountSigners, ctx.j, "ledger"); + SignerEntries::deserialize(*sleAccountSigners, j, "ledger"); if (!accountSigners) return accountSigners.error(); // Get the array of transaction signers. - STArray const& txSigners(ctx.tx.getFieldArray(sfSigners)); // Walk the accountSigners performing a variety of checks and see if // the quorum is met. @@ -616,7 +685,7 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) { if (++iter == accountSigners->end()) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Invalid SigningAccount.Account."; return tefBAD_SIGNATURE; } @@ -624,7 +693,7 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) if (iter->account != txSignerAcctID) { // The SigningAccount is not in the SignerEntries. - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Invalid SigningAccount.Account."; return tefBAD_SIGNATURE; } @@ -636,7 +705,7 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) if (!publicKeyType(makeSlice(spk))) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "checkMultiSign: signing public key type is unknown"; return tefBAD_SIGNATURE; } @@ -669,7 +738,7 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) // In any of these cases we need to know whether the account is in // the ledger. Determine that now. - auto sleTxSignerRoot = ctx.view.read(keylet::account(txSignerAcctID)); + auto const sleTxSignerRoot = view.read(keylet::account(txSignerAcctID)); if (signingAcctIDFromPubKey == txSignerAcctID) { @@ -682,7 +751,7 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) if (signerAccountFlags & lsfDisableMaster) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Signer:Account lsfDisableMaster."; return tefMASTER_DISABLED; } @@ -694,21 +763,21 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) // Public key must hash to the account's regular key. if (!sleTxSignerRoot) { - JLOG(ctx.j.trace()) << "applyTransaction: Non-phantom signer " - "lacks account root."; + JLOG(j.trace()) << "applyTransaction: Non-phantom signer " + "lacks account root."; return tefBAD_SIGNATURE; } if (!sleTxSignerRoot->isFieldPresent(sfRegularKey)) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Account lacks RegularKey."; return tefBAD_SIGNATURE; } if (signingAcctIDFromPubKey != sleTxSignerRoot->getAccountID(sfRegularKey)) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Account doesn't match RegularKey."; return tefBAD_SIGNATURE; } @@ -720,8 +789,7 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) // Cannot perform transaction if quorum is not met. if (weightSum < sleAccountSigners->getFieldU32(sfSignerQuorum)) { - JLOG(ctx.j.trace()) - << "applyTransaction: Signers failed to meet quorum."; + JLOG(j.trace()) << "applyTransaction: Signers failed to meet quorum."; return tefBAD_QUORUM; } @@ -809,7 +877,11 @@ removeDeletedTrustLines( } } -/** Reset the context, discarding any changes made and adjust the fee */ +/** Reset the context, discarding any changes made and adjust the fee. + + @param fee The transaction fee to be charged. + @return A pair containing the transaction result and the actual fee charged. + */ std::pair Transactor::reset(XRPAmount fee) { @@ -817,9 +889,10 @@ Transactor::reset(XRPAmount fee) auto const txnAcct = view().peek(keylet::account(ctx_.tx.getAccountID(sfAccount))); + + // The account should never be missing from the ledger. But if it + // is missing then we can't very well charge it a fee, can we? if (!txnAcct) - // The account should never be missing from the ledger. But if it - // is missing then we can't very well charge it a fee, can we? return {tefINTERNAL, beast::zero}; auto const balance = txnAcct->getFieldAmount(sfBalance).xrp(); @@ -918,7 +991,6 @@ Transactor::operator()() { // If the tapFAIL_HARD flag is set, a tec result // must not do anything - ctx_.discard(); applied = false; } @@ -1069,7 +1141,7 @@ Transactor::operator()() ctx_.apply(result); } - JLOG(j_.trace()) << (applied ? "applied" : "not applied") + JLOG(j_.trace()) << (applied ? "applied" : "not applied") << ", " << transToken(result); return {result, applied}; diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index d7908749efd..3e90f4f3bb7 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -35,14 +35,36 @@ struct PreflightContext STTx const& tx; Rules const rules; ApplyFlags flags; + std::optional batchId; beast::Journal const j; PreflightContext( Application& app_, STTx const& tx_, + uint256 batchId_, Rules const& rules_, ApplyFlags flags_, - beast::Journal j_); + beast::Journal j_ = beast::Journal{beast::Journal::getNullSink()}) + : app(app_) + , tx(tx_) + , rules(rules_) + , flags(flags_) + , batchId(batchId_) + , j(j_) + { + assert((flags_ & tapBATCH) == tapBATCH); + } + + PreflightContext( + Application& app_, + STTx const& tx_, + Rules const& rules_, + ApplyFlags flags_, + beast::Journal j_ = beast::Journal{beast::Journal::getNullSink()}) + : app(app_), tx(tx_), rules(rules_), flags(flags_), j(j_) + { + assert((flags_ & tapBATCH) == 0); + } PreflightContext& operator=(PreflightContext const&) = delete; @@ -55,8 +77,9 @@ struct PreclaimContext Application& app; ReadView const& view; TER preflightResult; - STTx const& tx; ApplyFlags flags; + STTx const& tx; + std::optional const batchId; beast::Journal const j; PreclaimContext( @@ -65,14 +88,36 @@ struct PreclaimContext TER preflightResult_, STTx const& tx_, ApplyFlags flags_, + std::optional batchId_, beast::Journal j_ = beast::Journal{beast::Journal::getNullSink()}) : app(app_) , view(view_) , preflightResult(preflightResult_) - , tx(tx_) , flags(flags_) + , tx(tx_) + , batchId(batchId_) , j(j_) { + assert(batchId.has_value() == ((flags_ & tapBATCH) == tapBATCH)); + } + + PreclaimContext( + Application& app_, + ReadView const& view_, + TER preflightResult_, + STTx const& tx_, + ApplyFlags flags_, + beast::Journal j_ = beast::Journal{beast::Journal::getNullSink()}) + : PreclaimContext( + app_, + view_, + preflightResult_, + tx_, + flags_, + std::nullopt, + j_) + { + assert((flags_ & tapBATCH) == 0); } PreclaimContext& @@ -137,6 +182,9 @@ class Transactor static NotTEC checkSign(PreclaimContext const& ctx); + static NotTEC + checkBatchSign(PreclaimContext const& ctx); + // Returns the fee in fee units, not scaled for load. static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); @@ -195,9 +243,18 @@ class Transactor TER payFee(); static NotTEC - checkSingleSign(PreclaimContext const& ctx); + checkSingleSign( + AccountID const& idSigner, + AccountID const& idAccount, + std::shared_ptr sleAccount, + Rules const& rules, + beast::Journal j); static NotTEC - checkMultiSign(PreclaimContext const& ctx); + checkMultiSign( + ReadView const& view, + AccountID const& idAccount, + STArray const& txSigners, + beast::Journal j); void trapTransaction(uint256) const; }; diff --git a/src/xrpld/app/tx/detail/apply.cpp b/src/xrpld/app/tx/detail/apply.cpp index 103ec041074..838019e0c31 100644 --- a/src/xrpld/app/tx/detail/apply.cpp +++ b/src/xrpld/app/tx/detail/apply.cpp @@ -22,6 +22,7 @@ #include #include #include +#include namespace ripple { @@ -42,6 +43,28 @@ checkValidity( { auto const id = tx.getTransactionID(); auto const flags = router.getFlags(id); + + // Ignore signature check on batch inner transactions + if (rules.enabled(featureBatch) && tx.isFlag(tfInnerBatchTxn)) + { + // Defensive Check: These values are also checked in Batch::preflight + if (tx.isFieldPresent(sfTxnSignature) || + !tx.getSigningPubKey().empty() || tx.isFieldPresent(sfSigners)) + return { + Validity::SigBad, + "Malformed: Invalid inner batch transaction."}; + + std::string reason; + if (!passesLocalChecks(tx, reason)) + { + router.setFlags(id, SF_LOCALBAD); + return {Validity::SigGoodOnly, reason}; + } + + router.setFlags(id, SF_SIGGOOD); + return {Validity::Valid, ""}; + } + if (flags & SF_SIGBAD) // Signature is known bad return {Validity::SigBad, "Transaction has bad signature."}; @@ -105,6 +128,16 @@ forceValidity(HashRouter& router, uint256 const& txid, Validity validity) router.setFlags(txid, flags); } +template +std::pair +apply(Application& app, OpenView& view, PreflightChecks&& preflightChecks) +{ + STAmountSO stAmountSO{view.rules().enabled(fixSTAmountCanonicalize)}; + NumberSO stNumberSO{view.rules().enabled(fixUniversalNumber)}; + + return doApply(preclaim(preflightChecks(), app, view), app, view); +} + std::pair apply( Application& app, @@ -113,12 +146,85 @@ apply( ApplyFlags flags, beast::Journal j) { - STAmountSO stAmountSO{view.rules().enabled(fixSTAmountCanonicalize)}; - NumberSO stNumberSO{view.rules().enabled(fixUniversalNumber)}; + return apply(app, view, [&]() mutable { + return preflight(app, view.rules(), tx, flags, j); + }); +} - auto pfresult = preflight(app, view.rules(), tx, flags, j); - auto pcresult = preclaim(pfresult, app, view); - return doApply(pcresult, app, view); +std::pair +apply( + Application& app, + OpenView& view, + uint256 const& batchId, + STTx const& tx, + ApplyFlags flags, + beast::Journal j) +{ + return apply(app, view, [&]() mutable { + return preflight(app, view.rules(), batchId, tx, flags, j); + }); +} + +static bool +applyBatchTransactions( + Application& app, + OpenView& batchView, + STTx const& txn, + beast::Journal j) +{ + assert( + txn.getTxnType() == ttBATCH && + !txn.getFieldArray(sfRawTransactions).empty()); + + auto const batchId = txn.getTransactionID(); + auto const mode = txn.getFlags(); + + auto applyOneTransaction = [&app, &j, &batchId, &batchView](STTx&& tx) { + OpenView perTxBatchView(batch_view, batchView); + + auto const ret = apply(app, perTxBatchView, batchId, tx, tapBATCH, j); + assert( + ret.second == (isTesSuccess(ret.first) || isTecClaim(ret.first))); + + JLOG(j.trace()) << "BatchTrace[" << batchId + << "]: " << tx.getTransactionID() << " " + << (ret.second ? "applied" : "failure") << ": " + << transToken(ret.first); + + // If the transaction should be applied push its changes to the + // whole-batch view. + if (ret.second && (isTesSuccess(ret.first) || isTecClaim(ret.first))) + perTxBatchView.apply(batchView); + + return ret; + }; + + int applied = 0; + + for (STObject rb : txn.getFieldArray(sfRawTransactions)) + { + auto const result = applyOneTransaction(STTx{std::move(rb)}); + assert( + result.second == + (isTesSuccess(result.first) || isTecClaim(result.first))); + + if (result.second) + ++applied; + + if (!isTesSuccess(result.first)) + { + if (mode & tfAllOrNothing) + return false; + + if (mode & tfUntilFailure) + break; + } + + if (isTesSuccess(result.first) && (mode & tfOnlyOne)) + break; + } + + return applied != 0; } ApplyResult @@ -140,10 +246,22 @@ applyTransaction( try { auto const result = apply(app, view, txn, flags, j); + if (result.second) { JLOG(j.debug()) - << "Transaction applied: " << transHuman(result.first); + << "Transaction applied: " << transToken(result.first); + + // The batch transaction was just applied; now we need to apply + // its inner transactions as necessary. + if (isTesSuccess(result.first) && txn.getTxnType() == ttBATCH) + { + OpenView wholeBatchView(batch_view, view); + + if (applyBatchTransactions(app, wholeBatchView, txn, j)) + wholeBatchView.apply(view); + } + return ApplyResult::Success; } @@ -152,11 +270,11 @@ applyTransaction( { // failure JLOG(j.debug()) - << "Transaction failure: " << transHuman(result.first); + << "Transaction failure: " << transToken(result.first); return ApplyResult::Fail; } - JLOG(j.debug()) << "Transaction retry: " << transHuman(result.first); + JLOG(j.debug()) << "Transaction retry: " << transToken(result.first); return ApplyResult::Retry; } catch (std::exception const& ex) diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index d17227fa684..8aefd447343 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -96,7 +97,6 @@ with_txn_type(TxType txnType, F&& f) #undef TRANSACTION #pragma pop_macro("TRANSACTION") - default: throw UnknownTxnType(txnType); } @@ -199,6 +199,12 @@ invoke_preclaim(PreclaimContext const& ctx) if (result != tesSUCCESS) return result; + + if (ctx.tx.getTxnType() == ttBATCH) + result = T::checkBatchSign(ctx); + + if (result != tesSUCCESS) + return result; } return T::preclaim(ctx); @@ -307,7 +313,28 @@ preflight( } catch (std::exception const& e) { - JLOG(j.fatal()) << "apply: " << e.what(); + JLOG(j.fatal()) << "apply (preflight): " << e.what(); + return {pfctx, {tefEXCEPTION, TxConsequences{tx}}}; + } +} + +PreflightResult +preflight( + Application& app, + Rules const& rules, + uint256 const& batchId, + STTx const& tx, + ApplyFlags flags, + beast::Journal j) +{ + PreflightContext const pfctx(app, tx, batchId, rules, flags, j); + try + { + return {pfctx, invoke_preflight(pfctx)}; + } + catch (std::exception const& e) + { + JLOG(j.fatal()) << "apply (preflight): " << e.what(); return {pfctx, {tefEXCEPTION, TxConsequences{tx}}}; } } @@ -321,18 +348,31 @@ preclaim( std::optional ctx; if (preflightResult.rules != view.rules()) { - auto secondFlight = preflight( - app, - view.rules(), - preflightResult.tx, - preflightResult.flags, - preflightResult.j); + auto secondFlight = [&]() { + if (preflightResult.batchId) + return preflight( + app, + view.rules(), + preflightResult.batchId.value(), + preflightResult.tx, + preflightResult.flags, + preflightResult.j); + + return preflight( + app, + view.rules(), + preflightResult.tx, + preflightResult.flags, + preflightResult.j); + }(); + ctx.emplace( app, view, secondFlight.ter, secondFlight.tx, secondFlight.flags, + secondFlight.batchId, secondFlight.j); } else @@ -343,8 +383,10 @@ preclaim( preflightResult.ter, preflightResult.tx, preflightResult.flags, + preflightResult.batchId, preflightResult.j); } + try { if (ctx->preflightResult != tesSUCCESS) @@ -353,7 +395,7 @@ preclaim( } catch (std::exception const& e) { - JLOG(ctx->j.fatal()) << "apply: " << e.what(); + JLOG(ctx->j.fatal()) << "apply (preclaim): " << e.what(); return {*ctx, tefEXCEPTION}; } } @@ -386,6 +428,7 @@ doApply(PreclaimResult const& preclaimResult, Application& app, OpenView& view) ApplyContext ctx( app, view, + preclaimResult.batchId, preclaimResult.tx, preclaimResult.ter, calculateBaseFee(view, preclaimResult.tx), diff --git a/src/xrpld/ledger/ApplyView.h b/src/xrpld/ledger/ApplyView.h index 12626824308..93addff9d68 100644 --- a/src/xrpld/ledger/ApplyView.h +++ b/src/xrpld/ledger/ApplyView.h @@ -40,6 +40,9 @@ enum ApplyFlags : std::uint32_t { // Transaction came from a privileged source tapUNLIMITED = 0x400, + + // Transaction is executing as part of a batch + tapBATCH = 0x800 }; constexpr ApplyFlags diff --git a/src/xrpld/ledger/ApplyViewImpl.h b/src/xrpld/ledger/ApplyViewImpl.h index 65678c5a6c4..48b9a268378 100644 --- a/src/xrpld/ledger/ApplyViewImpl.h +++ b/src/xrpld/ledger/ApplyViewImpl.h @@ -53,7 +53,12 @@ class ApplyViewImpl final : public detail::ApplyViewBase destructor. */ void - apply(OpenView& to, STTx const& tx, TER ter, beast::Journal j); + apply( + OpenView& to, + STTx const& tx, + TER ter, + std::optional batchId, + beast::Journal j); /** Set the amount of currency delivered. diff --git a/src/xrpld/ledger/OpenView.h b/src/xrpld/ledger/OpenView.h index c8e1911bf96..6f12a58cdd1 100644 --- a/src/xrpld/ledger/OpenView.h +++ b/src/xrpld/ledger/OpenView.h @@ -38,13 +38,21 @@ namespace ripple { Views constructed with this tag will have the rules of open ledgers applied during transaction processing. -*/ -struct open_ledger_t + */ +inline constexpr struct open_ledger_t { - explicit open_ledger_t() = default; -}; + constexpr open_ledger_t() = default; +} open_ledger{}; -extern open_ledger_t const open_ledger; +/** Batch view construction tag. + + Views constructed with this tag are part of a stack of views + used during batch transaction applied. + */ +inline constexpr struct batch_view_t +{ + constexpr batch_view_t() = default; +} batch_view{}; //------------------------------------------------------------------------------ @@ -96,6 +104,10 @@ class OpenView final : public ReadView, public TxsRawView ReadView const* base_; detail::RawStateTable items_; std::shared_ptr hold_; + + /// In batch mode, the number of transactions already executed. + std::size_t baseTxCount_ = 0; + bool open_ = true; public: @@ -141,7 +153,6 @@ class OpenView final : public ReadView, public TxsRawView The tx list starts empty and will contain all newly inserted tx. */ - /** @{ */ OpenView( open_ledger_t, ReadView const* base, @@ -155,7 +166,11 @@ class OpenView final : public ReadView, public TxsRawView : OpenView(open_ledger, &*base, rules, base) { } - /** @} */ + + OpenView(batch_view_t, OpenView& base) : OpenView(std::addressof(base)) + { + baseTxCount_ = base.txCount(); + } /** Construct a new last closed ledger. diff --git a/src/xrpld/ledger/detail/ApplyStateTable.cpp b/src/xrpld/ledger/detail/ApplyStateTable.cpp index 25ac5d00560..b210bfc0eaf 100644 --- a/src/xrpld/ledger/detail/ApplyStateTable.cpp +++ b/src/xrpld/ledger/detail/ApplyStateTable.cpp @@ -115,6 +115,7 @@ ApplyStateTable::apply( STTx const& tx, TER ter, std::optional const& deliver, + std::optional const& batchId, beast::Journal j) { // Build metadata and insert @@ -123,9 +124,11 @@ ApplyStateTable::apply( std::shared_ptr sMeta; if (!to.open()) { - TxMeta meta(tx.getTransactionID(), to.seq()); + TxMeta meta(tx.getTransactionID(), to.seq(), batchId); + if (deliver) meta.setDeliveredAmount(*deliver); + Mods newMod; for (auto& item : items_) { diff --git a/src/xrpld/ledger/detail/ApplyStateTable.h b/src/xrpld/ledger/detail/ApplyStateTable.h index e4c8b9ad656..5af3bb33b7f 100644 --- a/src/xrpld/ledger/detail/ApplyStateTable.h +++ b/src/xrpld/ledger/detail/ApplyStateTable.h @@ -70,6 +70,7 @@ class ApplyStateTable STTx const& tx, TER ter, std::optional const& deliver, + std::optional const& batchId, beast::Journal j); bool diff --git a/src/xrpld/ledger/detail/ApplyViewImpl.cpp b/src/xrpld/ledger/detail/ApplyViewImpl.cpp index e01b8bfe3ba..1e62f6d003a 100644 --- a/src/xrpld/ledger/detail/ApplyViewImpl.cpp +++ b/src/xrpld/ledger/detail/ApplyViewImpl.cpp @@ -29,9 +29,14 @@ ApplyViewImpl::ApplyViewImpl(ReadView const* base, ApplyFlags flags) } void -ApplyViewImpl::apply(OpenView& to, STTx const& tx, TER ter, beast::Journal j) +ApplyViewImpl::apply( + OpenView& to, + STTx const& tx, + TER ter, + std::optional batchId, + beast::Journal j) { - items_.apply(to, tx, ter, deliver_, j); + items_.apply(to, tx, ter, deliver_, batchId, j); } std::size_t diff --git a/src/xrpld/ledger/detail/OpenView.cpp b/src/xrpld/ledger/detail/OpenView.cpp index 619006161f8..d2be7717234 100644 --- a/src/xrpld/ledger/detail/OpenView.cpp +++ b/src/xrpld/ledger/detail/OpenView.cpp @@ -22,8 +22,6 @@ namespace ripple { -open_ledger_t const open_ledger{}; - class OpenView::txs_iter_impl : public txs_type::iter_base { private: @@ -123,7 +121,7 @@ OpenView::OpenView(ReadView const* base, std::shared_ptr hold) std::size_t OpenView::txCount() const { - return txs_.size(); + return baseTxCount_ + txs_.size(); } void @@ -268,7 +266,7 @@ OpenView::rawTxInsert( std::forward_as_tuple(key), std::forward_as_tuple(txn, metaData)); if (!result.second) - LogicError("rawTxInsert: duplicate TX id" + to_string(key)); + LogicError("rawTxInsert: duplicate TX id: " + to_string(key)); } } // namespace ripple diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index b04e1a6b024..4b145304803 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -39,6 +39,7 @@ #include #include // #include +#include #include #include @@ -1251,6 +1252,15 @@ PeerImp::handleTransaction( auto stx = std::make_shared(sit); uint256 txID = stx->getTransactionID(); + // Charge strongly for attempting to relay a txn with tfInnerBatchTxn + if (stx->isFlag(tfInnerBatchTxn)) + { + JLOG(p_journal_.warn()) << "Ignoring Network relayed Tx containing " + "tfInnerBatchTxn (handleTransaction)."; + fee_ = Resource::feeHighBurdenPeer; + return; + } + int flags; constexpr std::chrono::seconds tx_interval = 10s; @@ -2757,6 +2767,15 @@ PeerImp::checkTransaction( // VFALCO TODO Rewrite to not use exceptions try { + // charge strongly for relaying batch txns + if (stx->isFlag(tfInnerBatchTxn)) + { + JLOG(p_journal_.warn()) << "Ignoring Network relayed Tx containing " + "tfInnerBatchTxn (checkSignature)."; + charge(Resource::feeHighBurdenPeer); + return; + } + // Expired? if (stx->isFieldPresent(sfLastLedgerSequence) && (stx->getFieldU32(sfLastLedgerSequence) <