diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 8e6ed5765..1f889d21b 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -428,6 +428,7 @@ target_sources (rippled PRIVATE src/ripple/app/rdb/impl/Vacuum.cpp src/ripple/app/rdb/impl/Wallet.cpp src/ripple/app/tx/impl/ApplyContext.cpp + src/ripple/app/tx/impl/Batch.cpp src/ripple/app/tx/impl/BookTip.cpp src/ripple/app/tx/impl/CancelCheck.cpp src/ripple/app/tx/impl/CancelOffer.cpp @@ -703,6 +704,7 @@ if (tests) src/test/app/AccountDelete_test.cpp src/test/app/AccountTxPaging_test.cpp src/test/app/AmendmentTable_test.cpp + src/test/app/Batch_test.cpp src/test/app/BaseFee_test.cpp src/test/app/Check_test.cpp src/test/app/ClaimReward_test.cpp diff --git a/src/ripple/app/tx/impl/ApplyContext.h b/src/ripple/app/tx/impl/ApplyContext.h index 82cfa1b26..eed91eb50 100644 --- a/src/ripple/app/tx/impl/ApplyContext.h +++ b/src/ripple/app/tx/impl/ApplyContext.h @@ -49,6 +49,7 @@ public: TER const preclaimResult; XRPAmount const baseFee; beast::Journal const journal; + OpenView& base_; ApplyView& view() @@ -139,7 +140,7 @@ private: XRPAmount const fee, std::index_sequence); - OpenView& base_; + // OpenView& base_; ApplyFlags flags_; std::optional view_; }; diff --git a/src/ripple/app/tx/impl/Batch.cpp b/src/ripple/app/tx/impl/Batch.cpp new file mode 100644 index 000000000..3067b2412 --- /dev/null +++ b/src/ripple/app/tx/impl/Batch.cpp @@ -0,0 +1,196 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 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 + +namespace ripple { + +PreclaimContext +makePreclaimTx(PreclaimContext const& ctx, TxType const& tt, STObject const& txn) +{ + auto const stx = STTx(tt, [&txn](STObject& obj){ + obj = std::move(txn); + }); + PreclaimContext const pcctx(ctx.app, ctx.view, tesSUCCESS, stx, ctx.flags, ctx.j); + return pcctx; +} + +TxConsequences +Batch::makeTxConsequences(PreflightContext const& ctx) +{ + return TxConsequences{ctx.tx, TxConsequences::normal}; +} + +NotTEC +Batch::preflight(PreflightContext const& ctx) +{ + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + auto& tx = ctx.tx; + + auto const& txns = tx.getFieldArray(sfEmittedTxns); + if (txns.empty()) + { + JLOG(ctx.j.warn()) << "Batch: txns array empty."; + return temMALFORMED; + } + + if (txns.size() > 400) + { + JLOG(ctx.j.warn()) + << "Batch: txns array exceeds 400 entries."; + return temMALFORMED; + } + + for (auto const& txn : txns) + { + if (!txn.isFieldPresent(sfTransactionType)) + { + JLOG(ctx.j.warn()) + << "Batch: TransactionType missing in array entry."; + return temMALFORMED; + } + + auto const tt = txn.getFieldU16(sfTransactionType); + auto const account = txn.getAccountID(sfAccount); + std::cout << "account: " << account << "\n"; + auto const stx = STTx(ttINVOKE, [&txn](STObject& obj){ + obj = std::move(txn); + }); + + auto const txBlob = strHex(stx.getSerializer().slice()); + std::cout << "txBlob: " << txBlob << "\n"; + + PreflightContext const pfctx(ctx.app, stx, ctx.rules, ctx.flags, ctx.j); + + switch (tt) + { + case ttINVOKE: + std::cout << "tt: " << "ttINVOKE" << "\n"; + // DA: Create array of responses + Invoke::preflight(pfctx); + default: + std::cout << "tt: " << "temUNKNOWN" << "\n"; + } + + } + + return preflight2(ctx); +} + +TER +Batch::preclaim(PreclaimContext const& ctx) +{ + if (!ctx.view.rules().enabled(featureHooks)) + return temDISABLED; + + auto const& txns = ctx.tx.getFieldArray(sfEmittedTxns); + for (auto const& txn : txns) + { + if (!txn.isFieldPresent(sfTransactionType)) + { + JLOG(ctx.j.warn()) + << "Batch: TransactionType missing in array entry."; + return temMALFORMED; + } + + auto const tt = txn.getFieldU16(sfTransactionType); + + auto const stx = STTx(ttINVOKE, [&txn](STObject& obj){ + obj = std::move(txn); + }); + PreclaimContext const pcctx(ctx.app, ctx.view, tesSUCCESS, stx, ctx.flags, ctx.j); + + switch (tt) + { + case ttINVOKE: + std::cout << "tt: " << "ttINVOKE" << "\n"; + // auto const pcctx1 = makePreclaimTx(ctx, ttINVOKE, txn); + // DA: Create array of responses + Invoke::preclaim(pcctx); + break; + default: + std::cout << "tt: " << "temUNKNOWN" << "\n"; + } + + } + + return tesSUCCESS; +} + +TER +Batch::doApply() +{ + auto const& txns = ctx_.tx.getFieldArray(sfEmittedTxns); + for (auto const& txn : txns) + { + if (!txn.isFieldPresent(sfTransactionType)) + { + JLOG(ctx_.journal.warn()) + << "Batch: TransactionType missing in array entry."; + return temMALFORMED; + } + + auto const tt = txn.getFieldU16(sfTransactionType); + auto const stx = STTx(ttINVOKE, [&txn](STObject& obj){ + obj = std::move(txn); + }); + ApplyContext actx(ctx_.app, ctx_.base_, stx, tesSUCCESS, XRPAmount(1), view().flags(), ctx_.journal); + Invoke p(actx); + + switch (tt) + { + case ttINVOKE: + std::cout << "tt: " << "ttINVOKE" << "\n"; + // DA: Create array of responses + p(); + default: + std::cout << "tt: " << "temUNKNOWN" << "\n"; + } + + } + return tesSUCCESS; +} + +XRPAmount +Batch::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + XRPAmount extraFee{0}; + // if (tx.isFieldPresent(sfEmittedTxns)) + // { + // XRPAmount txFees{0}; + // auto const& txns = tx.getFieldArray(sfEmittedTxns); + // for (auto const& txn : txns) + // { + // txFees += txn.isFieldPresent(sfFee) ? txn.getFieldAmount(sfFee) : XRPAmount{0}; + // } + // extraFee += txFees; + // } + return Transactor::calculateBaseFee(view, tx) + extraFee; +} + +} // namespace ripple diff --git a/src/ripple/app/tx/impl/Batch.h b/src/ripple/app/tx/impl/Batch.h new file mode 100644 index 000000000..ccaf88d20 --- /dev/null +++ b/src/ripple/app/tx/impl/Batch.h @@ -0,0 +1,57 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 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{Custom}; + + explicit Batch(ApplyContext& ctx) : Transactor(ctx) + { + } + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static TxConsequences + makeTxConsequences(PreflightContext const& ctx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/ripple/app/tx/impl/InvariantCheck.cpp b/src/ripple/app/tx/impl/InvariantCheck.cpp index 84a5ff582..db477084d 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.cpp +++ b/src/ripple/app/tx/impl/InvariantCheck.cpp @@ -241,6 +241,9 @@ XRPNotCreated::finalize( return drops_ == drops; } + if (tt == ttBATCH) + return true; + // The net change should never be positive, as this would mean that the // transaction created XRP out of thin air. That's not possible. if (drops_ > 0) diff --git a/src/ripple/app/tx/impl/Invoke.cpp b/src/ripple/app/tx/impl/Invoke.cpp index aeabb4226..ef32ed4a8 100644 --- a/src/ripple/app/tx/impl/Invoke.cpp +++ b/src/ripple/app/tx/impl/Invoke.cpp @@ -34,24 +34,27 @@ Invoke::makeTxConsequences(PreflightContext const& ctx) NotTEC Invoke::preflight(PreflightContext const& ctx) { + std::cout << "Invoke::preflight" << "\n"; if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; auto& tx = ctx.tx; - if (tx.getFieldVL(sfBlob).size() > (128 * 1024)) + if (tx.isFieldPresent(sfBlob) && tx.getFieldVL(sfBlob).size() > (128 * 1024)) { JLOG(ctx.j.warn()) << "Invoke: blob was more than 128kib " << tx.getTransactionID(); return temMALFORMED; } + std::cout << "Invoke::preflight" << "FINISHED" << "\n"; return preflight2(ctx); } TER Invoke::preclaim(PreclaimContext const& ctx) { + std::cout << "Invoke::preclaim" << "\n"; if (!ctx.view.rules().enabled(featureHooks)) return temDISABLED; @@ -67,12 +70,14 @@ Invoke::preclaim(PreclaimContext const& ctx) return tecNO_TARGET; } + std::cout << "Invoke::preclaim" << "FINISHED" << "\n"; return tesSUCCESS; } TER Invoke::doApply() { + std::cout << "Invoke::doApply" << "\n"; // everything happens in the hooks! return tesSUCCESS; } diff --git a/src/ripple/app/tx/impl/applySteps.cpp b/src/ripple/app/tx/impl/applySteps.cpp index fa3a17c36..16ade014b 100644 --- a/src/ripple/app/tx/impl/applySteps.cpp +++ b/src/ripple/app/tx/impl/applySteps.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -104,6 +105,8 @@ invoke_preflight(PreflightContext const& ctx) return invoke_preflight_helper(ctx); case ttACCOUNT_SET: return invoke_preflight_helper(ctx); + case ttBATCH: + return invoke_preflight_helper(ctx); case ttCHECK_CANCEL: return invoke_preflight_helper(ctx); case ttCHECK_CASH: @@ -223,6 +226,8 @@ invoke_preclaim(PreclaimContext const& ctx) return invoke_preclaim(ctx); case ttACCOUNT_SET: return invoke_preclaim(ctx); + case ttBATCH: + return invoke_preclaim(ctx); case ttCHECK_CANCEL: return invoke_preclaim(ctx); case ttCHECK_CASH: @@ -304,6 +309,8 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx) return DeleteAccount::calculateBaseFee(view, tx); case ttACCOUNT_SET: return SetAccount::calculateBaseFee(view, tx); + case ttBATCH: + return Batch::calculateBaseFee(view, tx); case ttCHECK_CANCEL: return CancelCheck::calculateBaseFee(view, tx); case ttCHECK_CASH: @@ -428,6 +435,10 @@ invoke_apply(ApplyContext& ctx) SetAccount p(ctx); return p(); } + case ttBATCH: { + Batch p(ctx); + return p(); + } case ttCHECK_CANCEL: { CancelCheck p(ctx); return p(); diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index cd5dda504..c732bd684 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -604,6 +604,7 @@ extern SField const sfMemos; extern SField const sfNFTokens; extern SField const sfHooks; extern SField const sfGenesisMint; +extern SField const sfEmittedTxns; // array of objects (uncommon) extern SField const sfMajorities; diff --git a/src/ripple/protocol/TxFormats.h b/src/ripple/protocol/TxFormats.h index 7999ee5b4..086cb16a8 100644 --- a/src/ripple/protocol/TxFormats.h +++ b/src/ripple/protocol/TxFormats.h @@ -181,6 +181,8 @@ enum TxType : std::uint16_t ttUNL_MODIFY = 102, ttEMIT_FAILURE = 103, ttUNL_REPORT = 104, + + ttBATCH = 105, }; // clang-format on diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index 889885442..654298dbe 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -366,6 +366,7 @@ CONSTRUCT_UNTYPED_SFIELD(sfDisabledValidators, "DisabledValidators", ARRAY, CONSTRUCT_UNTYPED_SFIELD(sfHookExecutions, "HookExecutions", ARRAY, 18); CONSTRUCT_UNTYPED_SFIELD(sfHookParameters, "HookParameters", ARRAY, 19); CONSTRUCT_UNTYPED_SFIELD(sfHookGrants, "HookGrants", ARRAY, 20); +CONSTRUCT_UNTYPED_SFIELD(sfEmittedTxns, "EmittedTxns", ARRAY, 97); CONSTRUCT_UNTYPED_SFIELD(sfGenesisMints, "GenesisMints", ARRAY, 96); CONSTRUCT_UNTYPED_SFIELD(sfActiveValidators, "ActiveValidators", ARRAY, 95); CONSTRUCT_UNTYPED_SFIELD(sfImportVLKeys, "ImportVLKeys", ARRAY, 94); diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index 23c39e3cd..8c5db62b5 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -441,6 +441,13 @@ TxFormats::TxFormats() {sfTicketSequence, soeOPTIONAL}, }, commonFields); + + add(jss::Batch, + ttBATCH, + { + {sfEmittedTxns, soeOPTIONAL}, + }, + commonFields); } TxFormats const& diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index 9671a8399..2d10a7c29 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -50,6 +50,7 @@ JSS(AccountSet); // transaction type. JSS(Amendments); // ledger type. JSS(Amount); // in: TransactionSign; field. JSS(Authorize); // field +JSS(Batch); // transaction type. JSS(Blob); JSS(Check); // ledger type. JSS(CheckCancel); // transaction type. diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp new file mode 100644 index 000000000..a3ee824ee --- /dev/null +++ b/src/test/app/Batch_test.cpp @@ -0,0 +1,94 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 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 + +namespace ripple { +namespace test { + +class Batch_test : public beast::unit_test::suite +{ + void + testBatch(FeatureBitset features) + { + testcase("batch"); + + using namespace test::jtx; + using namespace std::literals; + + // test::jtx::Env env{*this, network::makeNetworkConfig(21337)}; + Env env{*this, envconfig(), features, nullptr, + // beast::severities::kWarning + beast::severities::kTrace + }; + + 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 seq = env.seq("alice"); + Json::Value jv; + jv[jss::TransactionType] = jss::Batch; + jv[jss::Account] = alice.human(); + jv[sfEmittedTxns.jsonName] = Json::Value{Json::arrayValue}; + jv[sfEmittedTxns.jsonName][0U] = Json::Value{}; + jv[sfEmittedTxns.jsonName][0U][jss::EmittedTxn] = Json::Value{}; + jv[sfEmittedTxns.jsonName][0U][jss::EmittedTxn][jss::TransactionType] = jss::Invoke; + jv[sfEmittedTxns.jsonName][0U][jss::EmittedTxn][sfAccount.jsonName] = alice.human(); + jv[sfEmittedTxns.jsonName][0U][jss::EmittedTxn][sfDestination.jsonName] = bob.human(); + jv[sfEmittedTxns.jsonName][0U][jss::EmittedTxn][sfFee.jsonName] = "1000000"; + jv[sfEmittedTxns.jsonName][0U][jss::EmittedTxn][jss::Sequence] = seq + 1; + jv[sfEmittedTxns.jsonName][0U][jss::EmittedTxn][jss::SigningPubKey] = strHex(alice.pk()); + jv[sfEmittedTxns.jsonName][1U] = Json::Value{}; + jv[sfEmittedTxns.jsonName][1U][jss::EmittedTxn] = Json::Value{}; + jv[sfEmittedTxns.jsonName][1U][jss::EmittedTxn][jss::TransactionType] = jss::Invoke; + jv[sfEmittedTxns.jsonName][1U][jss::EmittedTxn][sfAccount.jsonName] = alice.human(); + jv[sfEmittedTxns.jsonName][1U][jss::EmittedTxn][sfDestination.jsonName] = carol.human(); + jv[sfEmittedTxns.jsonName][1U][jss::EmittedTxn][sfFee.jsonName] = "1000000"; + jv[sfEmittedTxns.jsonName][1U][jss::EmittedTxn][jss::Sequence] = seq + 2; + jv[sfEmittedTxns.jsonName][1U][jss::EmittedTxn][jss::SigningPubKey] = strHex(alice.pk()); + env(jv, fee(XRP(3)), ter(tesSUCCESS)); + env.close(); + } + + void + testWithFeats(FeatureBitset features) + { + testBatch(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