From cd288a2a5b9108cdff68d388f535064115bd93cf Mon Sep 17 00:00:00 2001 From: Denis Angell Date: Sat, 20 Jul 2024 08:22:48 +0200 Subject: [PATCH 1/8] featureFirewall --- include/xrpl/protocol/Feature.h | 3 +- include/xrpl/protocol/Firewall.h | 121 +++++++++++ include/xrpl/protocol/Indexes.h | 3 + include/xrpl/protocol/LedgerFormats.h | 5 + include/xrpl/protocol/TER.h | 3 +- include/xrpl/protocol/TxFormats.h | 3 + include/xrpl/protocol/jss.h | 3 + src/libxrpl/protocol/Feature.cpp | 1 + src/libxrpl/protocol/Indexes.cpp | 7 + src/libxrpl/protocol/InnerObjectFormats.cpp | 1 + src/libxrpl/protocol/LedgerFormats.cpp | 16 ++ src/libxrpl/protocol/TER.cpp | 1 + src/libxrpl/protocol/TxFormats.cpp | 12 ++ src/test/app/SetFirewall_test.cpp | 160 +++++++++++++++ src/test/jtx.h | 1 + src/test/jtx/firewall.h | 117 +++++++++++ src/test/jtx/impl/firewall.cpp | 71 +++++++ src/xrpld/app/tx/detail/InvariantCheck.cpp | 1 + src/xrpld/app/tx/detail/SetFirewall.cpp | 216 ++++++++++++++++++++ src/xrpld/app/tx/detail/SetFirewall.h | 51 +++++ src/xrpld/app/tx/detail/Transactor.cpp | 113 ++++++++++ src/xrpld/app/tx/detail/Transactor.h | 5 + src/xrpld/app/tx/detail/applySteps.cpp | 8 + src/xrpld/rpc/detail/RPCHelpers.cpp | 3 +- 24 files changed, 922 insertions(+), 3 deletions(-) create mode 100644 include/xrpl/protocol/Firewall.h create mode 100644 src/test/app/SetFirewall_test.cpp create mode 100644 src/test/jtx/firewall.h create mode 100644 src/test/jtx/impl/firewall.cpp create mode 100644 src/xrpld/app/tx/detail/SetFirewall.cpp create mode 100644 src/xrpld/app/tx/detail/SetFirewall.h diff --git a/include/xrpl/protocol/Feature.h b/include/xrpl/protocol/Feature.h index 7eec46e89eb..8d05c61a67f 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 = 78; +static constexpr std::size_t numFeatures = 79; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -371,6 +371,7 @@ extern uint256 const fixReducedOffersV2; extern uint256 const fixEnforceNFTokenTrustline; extern uint256 const fixInnerObjTemplate2; extern uint256 const featureInvariantsV1_1; +extern uint256 const featureFirewall; } // namespace ripple diff --git a/include/xrpl/protocol/Firewall.h b/include/xrpl/protocol/Firewall.h new file mode 100644 index 00000000000..3e7984dc042 --- /dev/null +++ b/include/xrpl/protocol/Firewall.h @@ -0,0 +1,121 @@ +//------------------------------------------------------------------------------ +/* + 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_PROTOCOL_FIREWALL_H_INCLUDED +#define RIPPLE_PROTOCOL_FIREWALL_H_INCLUDED + +#include +#include +#include +#include + +namespace ripple { + +inline void +serializeFirewallAuthorization(Serializer& msg, Json::Value const& authAccounts) +{ + msg.add32(HashPrefix::shardInfo); + for (auto const& authAccount : authAccounts) + { + auto const account = authAccount[jss::AuthAccount]; + std::optional accountId = + parseBase58(account[jss::Account].asString()); + msg.addBitString(*accountId); + if (account[jss::Amount].isString()) + { + std::cout << "isXRP" << "\n"; + std::optional const optDrops = + to_uint64(account[jss::Amount].asString()); + if (!optDrops) + { + // pass + } + else + { + msg.add64(XRPAmount(*optDrops).drops()); + } + } + else + { + std::cout << "isIOU" << "\n"; + STAmount amt; + bool isAmount = amountFromJsonNoThrow(amt, account[jss::Amount]); + if (!isAmount) + { + // pass + } + else + { + if (amt == beast::zero) + msg.add64(STAmount::cNotNative); + else if (amt.signum() == -1) // Negative amount + msg.add64( + amt.mantissa() | + (static_cast(amt.exponent() + 512 + 97) + << (64 - 10))); + else // Positive amount + msg.add64( + amt.mantissa() | + (static_cast( + amt.exponent() + 512 + 256 + 97) + << (64 - 10))); + msg.addBitString(amt.getCurrency()); + msg.addBitString(amt.getIssuer()); + } + } + } +} + +inline void +serializeFirewallAuthorization(Serializer& msg, STArray const& authAccounts) +{ + msg.add32(HashPrefix::shardInfo); + for (auto const& authAccount : authAccounts) + { + AccountID accountId = authAccount.getAccountID(sfAccount); + msg.addBitString(accountId); + STAmount amt = authAccount.getFieldAmount(sfAmount); + ; + if (isXRP(amt)) + { + msg.add64(amt.mantissa()); + } + else + { + if (amt == beast::zero) + msg.add64(STAmount::cNotNative); + else if (amt.signum() == -1) // Negative amount + msg.add64( + amt.mantissa() | + (static_cast(amt.exponent() + 512 + 97) + << (64 - 10))); + else // Positive amount + msg.add64( + amt.mantissa() | + (static_cast(amt.exponent() + 512 + 256 + 97) + << (64 - 10))); + msg.addBitString(amt.getCurrency()); + msg.addBitString(amt.getIssuer()); + } + } +} + +} // namespace ripple + +#endif diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index f179bbacfab..6cadbabb4ba 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -287,6 +287,9 @@ did(AccountID const& account) noexcept; Keylet oracle(AccountID const& account, std::uint32_t const& documentID) noexcept; +Keylet +firewall(AccountID const& account) noexcept; + } // namespace keylet // Everything below is deprecated and should be removed in favor of keylets: diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 0ee6c992d8d..174c177583d 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -197,6 +197,11 @@ enum LedgerEntryType : std::uint16_t */ ltORACLE = 0x0080, + /** A ledger object which tracks FIrewall + \sa keylet::firewall + */ + ltFIREWALL = 0x0046, + //--------------------------------------------------------------------------- /** A special type, matching any ledger entry type. diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 335ef8de39a..9b3139b4464 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -338,7 +338,8 @@ enum TECcodes : TERUnderlyingType { tecINVALID_UPDATE_TIME = 188, tecTOKEN_PAIR_NOT_FOUND = 189, tecARRAY_EMPTY = 190, - tecARRAY_TOO_LARGE = 191 + tecARRAY_TOO_LARGE = 191, + tecFIREWALL_BLOCK = 192 }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/TxFormats.h b/include/xrpl/protocol/TxFormats.h index bd5dffd94e9..e73137b72df 100644 --- a/include/xrpl/protocol/TxFormats.h +++ b/include/xrpl/protocol/TxFormats.h @@ -196,6 +196,9 @@ enum TxType : std::uint16_t /** This transaction type deletes an Oracle instance */ ttORACLE_DELETE = 52, + + /** This transaction type creates an Firewall instance */ + ttFIREWALL_SET = 53, /** This system-generated transaction type is used to update the status of the various amendments. diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index a46e15f39ef..1475c5de255 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -89,6 +89,8 @@ JSS(EscrowFinish); // transaction type. JSS(Fee); // in/out: TransactionSign; field. JSS(FeeSettings); // ledger type. JSS(Flags); // in/out: TransactionSign; field. +JSS(FirewallSet); // transaction type. +JSS(Firewall); // ledger type. JSS(incomplete_shards); // out: OverlayImpl, PeerImp JSS(Invalid); // JSS(LastLedgerSequence); // in: TransactionSign; field @@ -343,6 +345,7 @@ JSS(fee_ref); // out: NetworkOPs, DEPRECATED JSS(fetch_pack); // out: NetworkOPs JSS(FIELDS); // out: RPC server_definitions // matches definitions.json format +JSS(firewall); // in: LedgerEntry JSS(first); // out: rpc/Version JSS(firstSequence); // out: NodeToShardStatus JSS(firstShardIndex); // out: NodeToShardStatus diff --git a/src/libxrpl/protocol/Feature.cpp b/src/libxrpl/protocol/Feature.cpp index 87395b7e189..0b754302043 100644 --- a/src/libxrpl/protocol/Feature.cpp +++ b/src/libxrpl/protocol/Feature.cpp @@ -500,6 +500,7 @@ REGISTER_FIX (fixInnerObjTemplate2, Supported::yes, VoteBehavior::De // InvariantsV1_1 will be changes to Supported::yes when all the // invariants expected to be included under it are complete. REGISTER_FEATURE(InvariantsV1_1, Supported::no, VoteBehavior::DefaultNo); +REGISTER_FEATURE(Firewall, Supported::yes, VoteBehavior::DefaultNo); // The following amendments are obsolete, but must remain supported // because they could potentially get enabled. diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 30d97416cfa..bed12339a99 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -73,6 +73,7 @@ enum class LedgerNameSpace : std::uint16_t { XCHAIN_CREATE_ACCOUNT_CLAIM_ID = 'K', DID = 'I', ORACLE = 'R', + FIREWALL = 'F', // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -451,6 +452,12 @@ oracle(AccountID const& account, std::uint32_t const& documentID) noexcept return {ltORACLE, indexHash(LedgerNameSpace::ORACLE, account, documentID)}; } +Keylet +firewall(AccountID const& account) noexcept +{ + return {ltFIREWALL, indexHash(LedgerNameSpace::FIREWALL, account)}; +} + } // namespace keylet } // namespace ripple diff --git a/src/libxrpl/protocol/InnerObjectFormats.cpp b/src/libxrpl/protocol/InnerObjectFormats.cpp index 6d7b855d199..21f10d665ca 100644 --- a/src/libxrpl/protocol/InnerObjectFormats.cpp +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp @@ -137,6 +137,7 @@ InnerObjectFormats::InnerObjectFormats() sfAuthAccount.getCode(), { {sfAccount, soeREQUIRED}, + {sfAmount, soeOPTIONAL}, }); add(sfPriceData.jsonName.c_str(), diff --git a/src/libxrpl/protocol/LedgerFormats.cpp b/src/libxrpl/protocol/LedgerFormats.cpp index 9401c00278b..10a1dc2d10b 100644 --- a/src/libxrpl/protocol/LedgerFormats.cpp +++ b/src/libxrpl/protocol/LedgerFormats.cpp @@ -364,6 +364,22 @@ LedgerFormats::LedgerFormats() {sfPreviousTxnLgrSeq, soeREQUIRED} }, commonFields); + + add(jss::Firewall, + ltFIREWALL, + { + {sfOwner, soeREQUIRED}, + {sfPublicKey, soeREQUIRED}, + {sfAmount, soeOPTIONAL}, + {sfAmount2, soeOPTIONAL}, + {sfAuthorize, soeOPTIONAL}, + {sfAuthAccounts, soeOPTIONAL}, + {sfCloseTime, soeOPTIONAL}, + {sfOwnerNode, soeREQUIRED}, + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED} + }, + commonFields); // clang-format on } diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index f452b05464e..e132c81b5e2 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -115,6 +115,7 @@ transResults() MAKE_ERROR(tecTOKEN_PAIR_NOT_FOUND, "Token pair is not found in Oracle object."), MAKE_ERROR(tecARRAY_EMPTY, "Array is empty."), MAKE_ERROR(tecARRAY_TOO_LARGE, "Array is too large."), + MAKE_ERROR(tecFIREWALL_BLOCK, "Transaction was blocked by firewall."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), diff --git a/src/libxrpl/protocol/TxFormats.cpp b/src/libxrpl/protocol/TxFormats.cpp index 71c333dc497..f91d60245ef 100644 --- a/src/libxrpl/protocol/TxFormats.cpp +++ b/src/libxrpl/protocol/TxFormats.cpp @@ -505,6 +505,18 @@ TxFormats::TxFormats() {sfOracleDocumentID, soeREQUIRED}, }, commonFields); + + add(jss::FirewallSet, + ttFIREWALL_SET, + { + {sfAmount, soeOPTIONAL}, + {sfAmount2, soeOPTIONAL}, + {sfPublicKey, soeOPTIONAL}, + {sfAuthorize, soeOPTIONAL}, + {sfAuthAccounts, soeOPTIONAL}, + {sfSignature, soeOPTIONAL}, + }, + commonFields); } TxFormats const& diff --git a/src/test/app/SetFirewall_test.cpp b/src/test/app/SetFirewall_test.cpp new file mode 100644 index 00000000000..a74a766e0ea --- /dev/null +++ b/src/test/app/SetFirewall_test.cpp @@ -0,0 +1,160 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 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 + +namespace ripple { +namespace test { +struct SetFirewall_test : public beast::unit_test::suite +{ + + static std::size_t + ownerDirCount(ReadView const& view, jtx::Account const& acct) + { + ripple::Dir const ownerDir(view, keylet::ownerDir(acct.id())); + return std::distance(ownerDir.begin(), ownerDir.end()); + }; + + static Buffer + sigFirewallAuth( + PublicKey const& pk, + SecretKey const& sk, + Json::Value const& authAccounts) + { + Serializer msg; + serializeFirewallAuthorization(msg, authAccounts); + return sign(pk, sk, msg.slice()); + } + + void + testEnabled(FeatureBitset features) + { + testcase("enabled"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + // setup env + Account const alice = Account("alice"); + + for (bool const withFirewall : {false, true}) + { + // If the Firewall amendment is not enabled, you should not be able + // to set or delete firewall. + auto const amend = withFirewall ? features : features - featureFirewall; + Env env{*this, amend}; + env.fund(XRP(1000), alice); + env.close(); + + auto const txResult = withFirewall ? ter(tesSUCCESS) : ter(temDISABLED); + auto const ownerDir = withFirewall ? 1 : 0; + + // SET + env(firewall::set(alice), txResult); + env.close(); + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == ownerDir); + } + } + + void + testEnabled1(FeatureBitset features) + { + testcase("enabled1"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + // setup env + Account const alice = Account("alice"); + Account const bob = Account("bob"); + Account const carol = Account("carol"); + + { + Env env{*this, features}; + // Env env{*this, envconfig(), features, nullptr, + // // beast::severities::kWarning + // beast::severities::kTrace + // }; + env.fund(XRP(1000), alice, bob, carol); + env.close(); + + // FIREWALL SET + env(firewall::set(alice), + firewall::auth(carol), + firewall::amt(XRP(10)), + firewall::pk(strHex(carol.pk().slice())), + ter(tesSUCCESS)); + env.close(); + + { + Json::Value params; + params[jss::transaction] = env.tx()->getJson(JsonOptions::none)[jss::hash]; + auto jrr = env.rpc("json", "tx", to_string(params))[jss::result]; + std::cout << "RESULT: " << jrr << "\n"; + } + + env(pay(alice, bob, XRP(100)), ter(tecFIREWALL_BLOCK)); + env.close(); + + auto tx = firewall::set(alice); + tx[jss::AuthAccounts] = Json::Value{Json::arrayValue}; + tx[jss::AuthAccounts][0U] = Json::Value{}; + tx[jss::AuthAccounts][0U][jss::AuthAccount] = Json::Value{}; + tx[jss::AuthAccounts][0U][jss::AuthAccount][jss::Account] = bob.human(); + tx[jss::AuthAccounts][0U][jss::AuthAccount][jss::Amount] = "100000000"; + auto const sig = sigFirewallAuth(carol.pk(), carol.sk(), tx[jss::AuthAccounts]); + env(tx, + firewall::sig(strHex(Slice(sig))), + ter(tesSUCCESS)); + env.close(); + + { + Json::Value params; + params[jss::transaction] = env.tx()->getJson(JsonOptions::none)[jss::hash]; + auto jrr = env.rpc("json", "tx", to_string(params))[jss::result]; + std::cout << "RESULT: " << jrr << "\n"; + } + + env(pay(alice, bob, XRP(100)), ter(tesSUCCESS)); + env.close(); + } + } + + void + testWithFeats(FeatureBitset features) + { + // testEnabled(features); + testEnabled1(features); + } + +public: + void + run() override + { + using namespace test::jtx; + FeatureBitset const all{supported_amendments()}; + testWithFeats(all); + } +}; + +BEAST_DEFINE_TESTSUITE(SetFirewall, app, ripple); +} // namespace test +} // namespace ripple diff --git a/src/test/jtx.h b/src/test/jtx.h index a3255ef3af9..56fa4ca82af 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/firewall.h b/src/test/jtx/firewall.h new file mode 100644 index 00000000000..a7f0679b10e --- /dev/null +++ b/src/test/jtx/firewall.h @@ -0,0 +1,117 @@ +//------------------------------------------------------------------------------ +/* + 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_TEST_JTX_FIREWALL_H_INCLUDED +#define RIPPLE_TEST_JTX_FIREWALL_H_INCLUDED + +#include +#include +#include +#include + +namespace ripple { +namespace test { +namespace jtx { +namespace firewall { + +/** Set/Update a firewall. */ +Json::Value +set(Account const& account); + +/** Sets the optional Amount on a JTx. */ +class amt +{ +private: + STAmount amt_; + +public: + explicit amt(STAmount const& amt) : amt_(amt) + { + } + + void + operator()(Env&, JTx& jtx) const; +}; + +/** Sets the optional Amount2 on a JTx. */ +class amt2 +{ +private: + STAmount amt2_; + +public: + explicit amt2(STAmount const& amt2) : amt2_(amt2) + { + } + + void + operator()(Env&, JTx& jtx) const; +}; + +/** Sets the optional Authorize on a JTx. */ +class auth +{ +private: + jtx::Account auth_; + +public: + explicit auth(jtx::Account const& auth) : auth_(auth) + { + } + + void + operator()(Env&, JTx& jtx) const; +}; + +/** Sets the optional PublicKey on a JTx. */ +class pk +{ +private: + std::string pk_; + +public: + explicit pk(std::string const& pk) : pk_(pk) + { + } + + void + operator()(Env&, JTx& jtx) const; +}; + +/** Set the optional Signature on a JTx */ +class sig +{ +private: + std::string sig_; + +public: + explicit sig(std::string const& sig) : sig_(sig) + { + } + + void + operator()(Env&, JTx& jtx) const; +}; + +} // namespace firewall +} // namespace jtx +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/jtx/impl/firewall.cpp b/src/test/jtx/impl/firewall.cpp new file mode 100644 index 00000000000..c7ad8090a8a --- /dev/null +++ b/src/test/jtx/impl/firewall.cpp @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace ripple { +namespace test { +namespace jtx { +namespace firewall { + +Json::Value +set(Account const& account) +{ + Json::Value jv; + jv[jss::Account] = account.human(); + jv[jss::TransactionType] = jss::FirewallSet; + return jv; +} + +void +amt::operator()(Env& env, JTx& jt) const +{ + jt.jv[sfAmount.jsonName] = amt_.getJson(JsonOptions::none); +} + +void +amt2::operator()(Env& env, JTx& jt) const +{ + jt.jv[sfAmount2.jsonName] = amt2_.getJson(JsonOptions::none); +} + +void +auth::operator()(Env& env, JTx& jt) const +{ + jt.jv[sfAuthorize.jsonName] = auth_.human(); +} + +void +pk::operator()(Env& env, JTx& jt) const +{ + jt.jv[sfPublicKey.jsonName] = pk_; +} + +void +sig::operator()(Env& env, JTx& jt) const +{ + jt.jv[sfSignature.jsonName] = sig_; +} + + +} // namespace firewall +} // namespace jtx +} // namespace test +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 70210b90d75..bd230d4dfbd 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -478,6 +478,7 @@ LedgerEntryTypesMatch::visitEntry( case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID: case ltDID: case ltORACLE: + case ltFIREWALL: break; default: invalidTypeAdded_ = true; diff --git a/src/xrpld/app/tx/detail/SetFirewall.cpp b/src/xrpld/app/tx/detail/SetFirewall.cpp new file mode 100644 index 00000000000..f419e17e85c --- /dev/null +++ b/src/xrpld/app/tx/detail/SetFirewall.cpp @@ -0,0 +1,216 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include + +namespace ripple { + +NotTEC +SetFirewall::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureFirewall)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + auto const amount = ctx.tx[~sfAmount]; + auto const amount2 = ctx.tx[~sfAmount2]; + + // if (amount.issue() == amount2.issue()) + // { + // JLOG(ctx.j.debug()) + // << "Firewall: tokens can not have the same currency/issuer."; + // return temBAD_AMM_TOKENS; + // } + + // if (auto const err = invalidAmount(amount)) + // { + // JLOG(ctx.j.debug()) << "Firewall: invalid asset1 amount."; + // return err; + // } + + // if (auto const err = invalidAmount(amount2)) + // { + // JLOG(ctx.j.debug()) << "Firewall: invalid asset2 amount."; + // return err; + // } + + // Validate AuthAccounts Max 8 + if (ctx.tx.isFieldPresent(sfAuthAccounts)) + { + if (auto const authAccounts = ctx.tx.getFieldArray(sfAuthAccounts); + authAccounts.size() > 8) + { + JLOG(ctx.j.debug()) << "Firewall: Invalid number of AuthAccounts."; + return temMALFORMED; + } + } + + return preflight2(ctx); +} + +TER +SetFirewall::preclaim(PreclaimContext const& ctx) +{ + AccountID const accountID = ctx.tx[sfAccount]; + // auto const sequence = ctx.tx[sfSequence]; + auto const amount = ctx.tx[~sfAmount]; + auto const amount2 = ctx.tx[~sfAmount2]; + + // Check if Firewall already exists + ripple::Keylet const firewallKeylet = keylet::firewall(accountID); + auto const sleFirewall = ctx.view.read(firewallKeylet); + + // if (auto const ter = requireAuth(ctx.view, amount.issue(), accountID); + // ter != tesSUCCESS) + // { + // JLOG(ctx.j.debug()) + // << "Firewall: account is not authorized, " << amount.issue(); + // return ter; + // } + + // if (auto const ter = requireAuth(ctx.view, amount2.issue(), accountID); + // ter != tesSUCCESS) + // { + // JLOG(ctx.j.debug()) + // << "Firewall: account is not authorized, " << amount2.issue(); + // return ter; + // } + + // // Globally or individually frozen + // if (isFrozen(ctx.view, accountID, amount.issue()) || + // isFrozen(ctx.view, accountID, amount2.issue())) + // { + // JLOG(ctx.j.debug()) << "Firewall: involves frozen asset."; + // return tecFROZEN; + // } + + // auto noDefaultRipple = [](ReadView const& view, Issue const& issue) { + // if (isXRP(issue)) + // return false; + + // if (auto const issuerAccount = + // view.read(keylet::account(issue.account))) + // return (issuerAccount->getFlags() & lsfDefaultRipple) == 0; + + // return false; + // }; + + // if (noDefaultRipple(ctx.view, amount.issue()) || + // noDefaultRipple(ctx.view, amount2.issue())) + // { + // JLOG(ctx.j.debug()) << "Firewall: DefaultRipple not set"; + // return terNO_RIPPLE; + // } + + // Validate AuthAccounts (Has Account Has Amount Amount > 0) + if (ctx.tx.isFieldPresent(sfSignature) && ctx.tx.isFieldPresent(sfAuthAccounts)) + { + auto const authAccounts = ctx.tx.getFieldArray(sfAuthAccounts); + auto const sig = ctx.tx.getFieldVL(sfSignature); + PublicKey const pk(makeSlice(sleFirewall->getFieldVL(sfPublicKey))); + Serializer msg; + serializeFirewallAuthorization(msg, authAccounts); + if (!verify(pk, msg.slice(), makeSlice(sig), /*canonical*/ true)) + return temBAD_SIGNATURE; + } + + return tesSUCCESS; +} + +TER +SetFirewall::doApply() +{ + Sandbox sb(&ctx_.view()); + + ripple::Keylet const firewallKeylet = keylet::firewall(account_); + auto firewallSle = sb.peek(firewallKeylet); + if (!firewallSle) + { + // Set Firewall + auto const firewallSle = std::make_shared(firewallKeylet); + (*firewallSle)[sfOwner] = account_; + (*firewallSle)[~sfAuthorize] = ctx_.tx[~sfAuthorize]; + (*firewallSle)[~sfPublicKey] = ctx_.tx[~sfPublicKey]; + (*firewallSle)[~sfAmount] = ctx_.tx[~sfAmount]; + (*firewallSle)[~sfAmount2] = ctx_.tx[~sfAmount2]; + if (ctx_.tx.isFieldPresent(sfAuthAccounts)) + { + auto const authAccounts = ctx_.tx.getFieldArray(sfAuthAccounts); + firewallSle->setFieldArray(sfAuthAccounts, authAccounts); + } + + // Add owner directory to link the account and Firewall object. + if (auto const page = sb.dirInsert( + keylet::ownerDir(account_), + firewallSle->key(), + describeOwnerDir(account_))) + { + firewallSle->setFieldU64(sfOwnerNode, *page); + } + else + { + JLOG(j_.error()) << "Firewall: failed to insert owner dir"; + return tecDIR_FULL; + } + + sb.insert(firewallSle); + } + else + { + // Update Firewall + JLOG(j_.error()) << "Firewall: Update Firewall"; + + if (ctx_.tx.isFieldPresent(sfAuthorize)) + firewallSle->setAccountID(sfAuthorize, ctx_.tx.getAccountID(sfAuthorize)); + if (ctx_.tx.isFieldPresent(sfPublicKey)) + firewallSle->setFieldH256(sfPublicKey, ctx_.tx.getFieldH256(sfPublicKey)); + if (ctx_.tx.isFieldPresent(sfAmount)) + firewallSle->setFieldAmount(sfAmount, ctx_.tx.getFieldAmount(sfAmount)); + if (ctx_.tx.isFieldPresent(sfAmount2)) + firewallSle->setFieldAmount(sfAmount2, ctx_.tx.getFieldAmount(sfAmount2)); + + if (ctx_.tx.isFieldPresent(sfAuthAccounts)) + { + auto const authAccounts = ctx_.tx.getFieldArray(sfAuthAccounts); + firewallSle->setFieldArray(sfAuthAccounts, authAccounts); + } + if (ctx_.tx.isFieldPresent(sfAuthAccounts)) + { + auto const authAccounts = ctx_.tx.getFieldArray(sfAuthAccounts); + firewallSle->setFieldArray(sfAuthAccounts, authAccounts); + } + sb.update(firewallSle); + } + + sb.apply(ctx_.rawView()); + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/SetFirewall.h b/src/xrpld/app/tx/detail/SetFirewall.h new file mode 100644 index 00000000000..a7d37fab708 --- /dev/null +++ b/src/xrpld/app/tx/detail/SetFirewall.h @@ -0,0 +1,51 @@ +//------------------------------------------------------------------------------ +/* + 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_SETFIREWALL_H_INCLUDED +#define RIPPLE_TX_SETFIREWALL_H_INCLUDED + +#include +#include +#include +#include + +namespace ripple { + +class SetFirewall : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit SetFirewall(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 42e9f0677ab..0a9936be8b5 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -435,6 +435,119 @@ Transactor::ticketDelete( return tesSUCCESS; } +TER +isWhitelisted( + ReadView const& view, + STAmount const& fireAmount, + STAmount const& txAmount, + STArray const& authAccounts, + AccountID const& dest) +{ + if (txAmount <= fireAmount) + { + return tesSUCCESS; + } + else + { + for (auto const& account : authAccounts) + { + if (dest != account[sfAccount]) + { + // pass + } + else + { + if (!view.read(keylet::account(account[sfAccount]))) + { + // JLOG(ctx.j.debug()) << "Firewall: Invalid Account."; + return tecINTERNAL; + } + auto const authAmount = account[~sfAmount]; + if (authAmount && *authAmount >= fireAmount) + { + // Remove Auth Account? + return tesSUCCESS; + } + } + } + // JLOG(j.debug()) << "checkFirewall: XRP Txn Blocked"; + return tecFIREWALL_BLOCK; + // return tecFIREWALL; + } +} + +// Check for firewall. +TER +Transactor::checkFirewall( + PreclaimContext const& ctx, + beast::Journal j) +{ + JLOG(j.error()) << "checkFirewall: START"; + // if ( + // !ctx.tx.isFieldPresent(sfAmount) + // // !ctx.tx.isFieldPresent(sfAmount2) && + // // !ctx.tx.isFieldPresent(sfTakerGets) && + // // !ctx.tx.isFieldPresent(sfTakerPays) + // ) + // ttPAYMENT + // ttOFFER (Create) + // ttESCROW (Create) + // ttPAYCHAN (Create) + // ttNFTOKEN_MINT (Create) + // ttAMM (Create/Deposit) + if (ctx.tx.getTxnType() != ttPAYMENT) + { + JLOG(j.error()) << "checkFirewall: Not Firewall Txn"; + return tesSUCCESS; + } + + auto const id = ctx.tx.getAccountID(sfAccount); + auto const dest = ctx.tx.getAccountID(sfDestination); + + auto const sleFirewall = ctx.view.read(keylet::firewall(id)); + if (!sleFirewall) + { + JLOG(j.error()) << "checkFirewall: No Firewall Active"; + return tesSUCCESS; + } + + if (sleFirewall->getAccountID(sfAuthorize) == dest) + { + JLOG(j.error()) << "checkFirewall: Destination == Authorize"; + return tesSUCCESS; + } + + // Check Whitelist + if (sleFirewall->isFieldPresent(sfAuthAccounts)) + { + auto const authAccounts = sleFirewall->getFieldArray(sfAuthAccounts); + + auto const firewallXRP = sleFirewall->getFieldAmount(sfAmount); + auto const firewallIOU = sleFirewall->getFieldAmount(sfAmount2); + // auto const txAmount = tx.getFieldAmount(~sfAmount); + + // Check XRP + if (ctx.tx.isFieldPresent(sfAmount) && firewallXRP) + { + auto const txXRP = ctx.tx.getFieldAmount(sfAmount); + if (auto const ter = isWhitelisted(ctx.view, firewallXRP, txXRP, authAccounts, dest); ter != tesSUCCESS) + { + return ter; + } + } + // Check IOU + if (ctx.tx.isFieldPresent(sfAmount) && firewallIOU) + { + auto const txIOU = ctx.tx.getFieldAmount(sfAmount); + if (auto const ter = isWhitelisted(ctx.view, firewallIOU, txIOU, authAccounts, dest); ter != tesSUCCESS) + { + return ter; + } + } + } + return tecFIREWALL_BLOCK; +} + // check stuff before you bother to lock the ledger void Transactor::preCompute() diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index 27f22a0eb2e..657b3b7967c 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -158,6 +158,11 @@ class Transactor uint256 const& ticketIndex, beast::Journal j); + static TER + checkFirewall( + PreclaimContext const& ctx, + beast::Journal j); + protected: TER apply(); diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index 9ddaa3051c4..0c53d48a823 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -50,6 +50,7 @@ #include #include #include +#include #include #include @@ -165,6 +166,8 @@ with_txn_type(TxType txnType, F&& f) return f.template operator()(); case ttORACLE_DELETE: return f.template operator()(); + case ttFIREWALL_SET: + return f.template operator()(); default: throw UnknownTxnType(txnType); } @@ -250,6 +253,11 @@ invoke_preclaim(PreclaimContext const& ctx) { TER result = T::checkSeqProxy(ctx.view, ctx.tx, ctx.j); + if (result != tesSUCCESS) + return result; + + result = T::checkFirewall(ctx, ctx.j); + if (result != tesSUCCESS) return result; diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp index 42bd4faded5..8d9d6367b2b 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -934,7 +934,7 @@ chooseLedgerEntryType(Json::Value const& params) std::pair result{RPC::Status::OK, ltANY}; if (params.isMember(jss::type)) { - static constexpr std::array, 22> + static constexpr std::array, 23> types{ {{jss::account, ltACCOUNT_ROOT}, {jss::amendments, ltAMENDMENTS}, @@ -946,6 +946,7 @@ chooseLedgerEntryType(Json::Value const& params) {jss::directory, ltDIR_NODE}, {jss::escrow, ltESCROW}, {jss::fee, ltFEE_SETTINGS}, + {jss::firewall, ltFIREWALL}, {jss::hashes, ltLEDGER_HASHES}, {jss::nunl, ltNEGATIVE_UNL}, {jss::oracle, ltORACLE}, From 86dd8dc8976ff6f25a65a3d758d3c91172aeff8c Mon Sep 17 00:00:00 2001 From: Denis Angell Date: Sun, 21 Jul 2024 15:16:47 +0200 Subject: [PATCH 2/8] whitelist -> firewallPreauth --- include/xrpl/protocol/Firewall.h | 130 ++++----- include/xrpl/protocol/Indexes.h | 12 + include/xrpl/protocol/LedgerFormats.h | 5 + include/xrpl/protocol/TxFormats.h | 3 + include/xrpl/protocol/jss.h | 2 + src/libxrpl/protocol/Indexes.cpp | 9 + src/libxrpl/protocol/InnerObjectFormats.cpp | 1 - src/libxrpl/protocol/LedgerFormats.cpp | 15 +- src/libxrpl/protocol/TxFormats.cpp | 11 +- src/test/app/SetFirewall_test.cpp | 204 +++++++++++-- src/test/jtx/firewall.h | 15 - src/test/jtx/impl/firewall.cpp | 6 - src/xrpld/app/tx/detail/Firewall.cpp | 273 ++++++++++++++++++ .../tx/detail/{SetFirewall.h => Firewall.h} | 8 +- src/xrpld/app/tx/detail/FirewallPreauth.cpp | 203 +++++++++++++ src/xrpld/app/tx/detail/FirewallPreauth.h | 56 ++++ src/xrpld/app/tx/detail/InvariantCheck.cpp | 1 + src/xrpld/app/tx/detail/SetAccount.cpp | 11 + src/xrpld/app/tx/detail/SetFirewall.cpp | 216 -------------- src/xrpld/app/tx/detail/Transactor.cpp | 124 +++----- src/xrpld/app/tx/detail/applySteps.cpp | 7 +- src/xrpld/rpc/detail/RPCHelpers.cpp | 3 +- 22 files changed, 867 insertions(+), 448 deletions(-) create mode 100644 src/xrpld/app/tx/detail/Firewall.cpp rename src/xrpld/app/tx/detail/{SetFirewall.h => Firewall.h} (89%) create mode 100644 src/xrpld/app/tx/detail/FirewallPreauth.cpp create mode 100644 src/xrpld/app/tx/detail/FirewallPreauth.h delete mode 100644 src/xrpld/app/tx/detail/SetFirewall.cpp diff --git a/include/xrpl/protocol/Firewall.h b/include/xrpl/protocol/Firewall.h index 3e7984dc042..d3df7ae4bce 100644 --- a/include/xrpl/protocol/Firewall.h +++ b/include/xrpl/protocol/Firewall.h @@ -8,7 +8,7 @@ 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 + 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 @@ -27,95 +27,63 @@ namespace ripple { +/** + * @brief Serializes firewall authorization data into a message. + * + * This function serializes the given account and preauthorize account IDs + * into the provided Serializer object. It adds a shardInfo hash prefix, + * followed by the account and preauthorize account IDs. + * + * @param msg The Serializer object to serialize the data into. + * @param account The account ID to be serialized. + * @param preauthorize The preauthorize account ID to be serialized. + */ inline void -serializeFirewallAuthorization(Serializer& msg, Json::Value const& authAccounts) +serializeFirewallAuthorization(Serializer& msg, AccountID const& account, AccountID const& preauthorize) { msg.add32(HashPrefix::shardInfo); - for (auto const& authAccount : authAccounts) - { - auto const account = authAccount[jss::AuthAccount]; - std::optional accountId = - parseBase58(account[jss::Account].asString()); - msg.addBitString(*accountId); - if (account[jss::Amount].isString()) - { - std::cout << "isXRP" << "\n"; - std::optional const optDrops = - to_uint64(account[jss::Amount].asString()); - if (!optDrops) - { - // pass - } - else - { - msg.add64(XRPAmount(*optDrops).drops()); - } - } - else - { - std::cout << "isIOU" << "\n"; - STAmount amt; - bool isAmount = amountFromJsonNoThrow(amt, account[jss::Amount]); - if (!isAmount) - { - // pass - } - else - { - if (amt == beast::zero) - msg.add64(STAmount::cNotNative); - else if (amt.signum() == -1) // Negative amount - msg.add64( - amt.mantissa() | - (static_cast(amt.exponent() + 512 + 97) - << (64 - 10))); - else // Positive amount - msg.add64( - amt.mantissa() | - (static_cast( - amt.exponent() + 512 + 256 + 97) - << (64 - 10))); - msg.addBitString(amt.getCurrency()); - msg.addBitString(amt.getIssuer()); - } - } - } + msg.addBitString(account); + msg.addBitString(preauthorize); } +/** + * @brief Serializes firewall authorization data into a message. + * + * This function serializes the given account ID and amount into the provided + * Serializer object. It adds a shardInfo hash prefix, followed by the account + * ID and the amount's mantissa. + * + * @param msg The Serializer object to serialize the data into. + * @param account The account ID to be serialized. + * @param amount The amount to be serialized. + */ inline void -serializeFirewallAuthorization(Serializer& msg, STArray const& authAccounts) +serializeFirewallAuthorization(Serializer& msg, AccountID const& account, STAmount const& amount) { msg.add32(HashPrefix::shardInfo); - for (auto const& authAccount : authAccounts) - { - AccountID accountId = authAccount.getAccountID(sfAccount); - msg.addBitString(accountId); - STAmount amt = authAccount.getFieldAmount(sfAmount); - ; - if (isXRP(amt)) - { - msg.add64(amt.mantissa()); - } - else - { - if (amt == beast::zero) - msg.add64(STAmount::cNotNative); - else if (amt.signum() == -1) // Negative amount - msg.add64( - amt.mantissa() | - (static_cast(amt.exponent() + 512 + 97) - << (64 - 10))); - else // Positive amount - msg.add64( - amt.mantissa() | - (static_cast(amt.exponent() + 512 + 256 + 97) - << (64 - 10))); - msg.addBitString(amt.getCurrency()); - msg.addBitString(amt.getIssuer()); - } - } + msg.addBitString(account); + msg.add64(amount.mantissa()); +} + +/** + * @brief Serializes firewall authorization data into a message. + * + * This function serializes the given account ID and public key into the + * provided Serializer object. It adds a shardInfo hash prefix, followed by + * the account ID and the raw bytes of the public key. + * + * @param msg The Serializer object to serialize the data into. + * @param account The account ID to be serialized. + * @param pk The public key to be serialized. + */ +inline void +serializeFirewallAuthorization(Serializer& msg, AccountID const& account, PublicKey const& pk) +{ + msg.add32(HashPrefix::shardInfo); + msg.addBitString(account); + msg.addRaw(pk.slice()); } } // namespace ripple -#endif +#endif \ No newline at end of file diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 6cadbabb4ba..257044e43ea 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -290,6 +290,18 @@ oracle(AccountID const& account, std::uint32_t const& documentID) noexcept; Keylet firewall(AccountID const& account) noexcept; +/** A FireallPreauth */ +/** @{ */ +Keylet +firewallPreauth(AccountID const& owner, AccountID const& preauthorized) noexcept; + +inline Keylet +firewallPreauth(uint256 const& key) noexcept +{ + return {ltFIREWALL_PREAUTH, key}; +} +/** @} */ + } // namespace keylet // Everything below is deprecated and should be removed in favor of keylets: diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 174c177583d..2ae61ee2d8c 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -201,6 +201,11 @@ enum LedgerEntryType : std::uint16_t \sa keylet::firewall */ ltFIREWALL = 0x0046, + + /** A ledger object which tracks FIrewall + \sa keylet::firewall + */ + ltFIREWALL_PREAUTH = 0x0047, //--------------------------------------------------------------------------- /** A special type, matching any ledger entry type. diff --git a/include/xrpl/protocol/TxFormats.h b/include/xrpl/protocol/TxFormats.h index e73137b72df..160e5fb776b 100644 --- a/include/xrpl/protocol/TxFormats.h +++ b/include/xrpl/protocol/TxFormats.h @@ -200,6 +200,9 @@ enum TxType : std::uint16_t /** This transaction type creates an Firewall instance */ ttFIREWALL_SET = 53, + /** This transaction type creates an Firewall instance */ + ttFIREWALL_PREAUTH = 54, + /** This system-generated transaction type is used to update the status of the various amendments. For details, see: https://xrpl.org/amendments.html diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 1475c5de255..0825620055b 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -89,6 +89,7 @@ JSS(EscrowFinish); // transaction type. JSS(Fee); // in/out: TransactionSign; field. JSS(FeeSettings); // ledger type. JSS(Flags); // in/out: TransactionSign; field. +JSS(FirewallPreauth); // transaction type. JSS(FirewallSet); // transaction type. JSS(Firewall); // ledger type. JSS(incomplete_shards); // out: OverlayImpl, PeerImp @@ -346,6 +347,7 @@ JSS(fetch_pack); // out: NetworkOPs JSS(FIELDS); // out: RPC server_definitions // matches definitions.json format JSS(firewall); // in: LedgerEntry +JSS(firewall_preauth); // in: LedgerEntry JSS(first); // out: rpc/Version JSS(firstSequence); // out: NodeToShardStatus JSS(firstShardIndex); // out: NodeToShardStatus diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index bed12339a99..03cbdc8ef51 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -74,6 +74,7 @@ enum class LedgerNameSpace : std::uint16_t { DID = 'I', ORACLE = 'R', FIREWALL = 'F', + FIREWALL_PREAUTH = 'G', // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -458,6 +459,14 @@ firewall(AccountID const& account) noexcept return {ltFIREWALL, indexHash(LedgerNameSpace::FIREWALL, account)}; } +Keylet +firewallPreauth(AccountID const& owner, AccountID const& preauthorized) noexcept +{ + return { + ltFIREWALL_PREAUTH, + indexHash(LedgerNameSpace::FIREWALL_PREAUTH, owner, preauthorized)}; +} + } // namespace keylet } // namespace ripple diff --git a/src/libxrpl/protocol/InnerObjectFormats.cpp b/src/libxrpl/protocol/InnerObjectFormats.cpp index 21f10d665ca..6d7b855d199 100644 --- a/src/libxrpl/protocol/InnerObjectFormats.cpp +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp @@ -137,7 +137,6 @@ InnerObjectFormats::InnerObjectFormats() sfAuthAccount.getCode(), { {sfAccount, soeREQUIRED}, - {sfAmount, soeOPTIONAL}, }); add(sfPriceData.jsonName.c_str(), diff --git a/src/libxrpl/protocol/LedgerFormats.cpp b/src/libxrpl/protocol/LedgerFormats.cpp index 10a1dc2d10b..e4c4d07440d 100644 --- a/src/libxrpl/protocol/LedgerFormats.cpp +++ b/src/libxrpl/protocol/LedgerFormats.cpp @@ -371,16 +371,23 @@ LedgerFormats::LedgerFormats() {sfOwner, soeREQUIRED}, {sfPublicKey, soeREQUIRED}, {sfAmount, soeOPTIONAL}, - {sfAmount2, soeOPTIONAL}, - {sfAuthorize, soeOPTIONAL}, - {sfAuthAccounts, soeOPTIONAL}, - {sfCloseTime, soeOPTIONAL}, {sfOwnerNode, soeREQUIRED}, {sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnLgrSeq, soeREQUIRED} }, commonFields); + add(jss::FirewallPreauth, + ltFIREWALL_PREAUTH, + { + {sfAccount, soeREQUIRED}, + {sfAuthorize, soeREQUIRED}, + {sfOwnerNode, soeREQUIRED}, + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED}, + }, + commonFields); + // clang-format on } diff --git a/src/libxrpl/protocol/TxFormats.cpp b/src/libxrpl/protocol/TxFormats.cpp index f91d60245ef..bf802e3b9f4 100644 --- a/src/libxrpl/protocol/TxFormats.cpp +++ b/src/libxrpl/protocol/TxFormats.cpp @@ -510,10 +510,17 @@ TxFormats::TxFormats() ttFIREWALL_SET, { {sfAmount, soeOPTIONAL}, - {sfAmount2, soeOPTIONAL}, {sfPublicKey, soeOPTIONAL}, {sfAuthorize, soeOPTIONAL}, - {sfAuthAccounts, soeOPTIONAL}, + {sfSignature, soeOPTIONAL}, + }, + commonFields); + + add(jss::FirewallPreauth, + ttFIREWALL_PREAUTH, + { + {sfAuthorize, soeOPTIONAL}, + {sfUnauthorize, soeOPTIONAL}, {sfSignature, soeOPTIONAL}, }, commonFields); diff --git a/src/test/app/SetFirewall_test.cpp b/src/test/app/SetFirewall_test.cpp index a74a766e0ea..9248dad926f 100644 --- a/src/test/app/SetFirewall_test.cpp +++ b/src/test/app/SetFirewall_test.cpp @@ -25,7 +25,7 @@ namespace ripple { namespace test { -struct SetFirewall_test : public beast::unit_test::suite +struct FirewallSet_test : public beast::unit_test::suite { static std::size_t @@ -36,16 +36,51 @@ struct SetFirewall_test : public beast::unit_test::suite }; static Buffer - sigFirewallAuth( + sigFirewallAuthAmount( PublicKey const& pk, SecretKey const& sk, - Json::Value const& authAccounts) + AccountID const& account, + STAmount const& amount) { Serializer msg; - serializeFirewallAuthorization(msg, authAccounts); + serializeFirewallAuthorization(msg, account, amount); return sign(pk, sk, msg.slice()); } + static Buffer + sigFirewallAuthPK( + PublicKey const& pk, + SecretKey const& sk, + AccountID const& account, + PublicKey const& _pk) + { + Serializer msg; + serializeFirewallAuthorization(msg, account, _pk); + return sign(pk, sk, msg.slice()); + } + + static std::pair> + firewallKeyAndSle( + ReadView const& view, + jtx::Account const& account) + { + auto const k = keylet::firewall(account); + return {k.key, view.read(k)}; + } + + void + verifyFirewall( + ReadView const& view, + jtx::Account const& account, + STAmount const& amount, + PublicKey const& pk) + { + auto [key, sle] = firewallKeyAndSle(view, account); + BEAST_EXPECT((*sle)[sfOwner] == account.id()); + BEAST_EXPECT((*sle)[sfAmount] == amount); + BEAST_EXPECT(strHex((*sle)[sfPublicKey]) == strHex(pk.slice())); + } + void testEnabled(FeatureBitset features) { @@ -76,27 +111,21 @@ struct SetFirewall_test : public beast::unit_test::suite } void - testEnabled1(FeatureBitset features) + testFirewallSet(FeatureBitset features) { - testcase("enabled1"); + testcase("firewall set"); using namespace jtx; using namespace std::literals::chrono_literals; - // setup env Account const alice = Account("alice"); Account const bob = Account("bob"); Account const carol = Account("carol"); { Env env{*this, features}; - // Env env{*this, envconfig(), features, nullptr, - // // beast::severities::kWarning - // beast::severities::kTrace - // }; env.fund(XRP(1000), alice, bob, carol); env.close(); - // FIREWALL SET env(firewall::set(alice), firewall::auth(carol), firewall::amt(XRP(10)), @@ -104,6 +133,95 @@ struct SetFirewall_test : public beast::unit_test::suite ter(tesSUCCESS)); env.close(); + verifyFirewall(*env.current(), alice, XRP(10), carol.pk()); + + env(pay(alice, bob, XRP(100)), ter(tecFIREWALL_BLOCK)); + env.close(); + } + } + + void + testUpdateAmount(FeatureBitset features) + { + testcase("update amount"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + Account const alice = Account("alice"); + Account const bob = Account("bob"); + Account const carol = Account("carol"); + + { + Env env{*this, features}; + env.fund(XRP(1000), alice, bob, carol); + env.close(); + + env(firewall::set(alice), + firewall::auth(carol), + firewall::amt(XRP(10)), + firewall::pk(strHex(carol.pk().slice())), + ter(tesSUCCESS)); + env.close(); + + verifyFirewall(*env.current(), alice, XRP(10), carol.pk()); + + env(pay(alice, bob, XRP(100)), ter(tecFIREWALL_BLOCK)); + env.close(); + + auto const sig = sigFirewallAuthAmount( + carol.pk(), carol.sk(), alice.id(), XRP(100)); + env(firewall::set(alice), + firewall::amt(XRP(100)), + firewall::sig(strHex(Slice(sig))), + ter(tesSUCCESS)); + env.close(); + + verifyFirewall(*env.current(), alice, XRP(100), carol.pk()); + + env(pay(alice, bob, XRP(100)), ter(tesSUCCESS)); + env.close(); + } + } + + void + testUpdatePK(FeatureBitset features) + { + testcase("update pk"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + Account const alice = Account("alice"); + Account const bob = Account("bob"); + Account const carol = Account("carol"); + Account const dave = Account("dave"); + + { + Env env{*this, features}; + env.fund(XRP(1000), alice, bob, carol, dave); + env.close(); + + env(firewall::set(alice), + firewall::auth(carol), + firewall::amt(XRP(10)), + firewall::pk(strHex(carol.pk().slice())), + ter(tesSUCCESS)); + env.close(); + + verifyFirewall(*env.current(), alice, XRP(10), carol.pk()); + + env(pay(alice, bob, XRP(100)), ter(tecFIREWALL_BLOCK)); + env.close(); + + auto const sig1 = sigFirewallAuthPK( + carol.pk(), carol.sk(), alice.id(), dave.pk()); + env(firewall::set(alice), + firewall::pk(strHex(dave.pk().slice())), + firewall::sig(strHex(Slice(sig1))), + ter(tesSUCCESS)); + env.close(); + + verifyFirewall(*env.current(), alice, XRP(10), dave.pk()); + { Json::Value params; params[jss::transaction] = env.tx()->getJson(JsonOptions::none)[jss::hash]; @@ -111,21 +229,16 @@ struct SetFirewall_test : public beast::unit_test::suite std::cout << "RESULT: " << jrr << "\n"; } - env(pay(alice, bob, XRP(100)), ter(tecFIREWALL_BLOCK)); - env.close(); - - auto tx = firewall::set(alice); - tx[jss::AuthAccounts] = Json::Value{Json::arrayValue}; - tx[jss::AuthAccounts][0U] = Json::Value{}; - tx[jss::AuthAccounts][0U][jss::AuthAccount] = Json::Value{}; - tx[jss::AuthAccounts][0U][jss::AuthAccount][jss::Account] = bob.human(); - tx[jss::AuthAccounts][0U][jss::AuthAccount][jss::Amount] = "100000000"; - auto const sig = sigFirewallAuth(carol.pk(), carol.sk(), tx[jss::AuthAccounts]); - env(tx, - firewall::sig(strHex(Slice(sig))), + auto const sig2 = sigFirewallAuthAmount( + dave.pk(), dave.sk(), alice.id(), XRP(100)); + env(firewall::set(alice), + firewall::amt(XRP(100)), + firewall::sig(strHex(Slice(sig2))), ter(tesSUCCESS)); env.close(); + verifyFirewall(*env.current(), alice, XRP(100), dave.pk()); + { Json::Value params; params[jss::transaction] = env.tx()->getJson(JsonOptions::none)[jss::hash]; @@ -138,11 +251,48 @@ struct SetFirewall_test : public beast::unit_test::suite } } + void + testMasterDisable(FeatureBitset features) + { + testcase("master disable"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + Account const alice = Account("alice"); + Account const bob = Account("bob"); + Account const carol = Account("carol"); + Account const dave = Account("dave"); + + { + Env env{*this, features}; + env.fund(XRP(1000), alice, bob, carol, dave); + env.close(); + + env(firewall::set(alice), + firewall::auth(carol), + firewall::amt(XRP(10)), + firewall::pk(strHex(carol.pk().slice())), + ter(tesSUCCESS)); + env.close(); + + verifyFirewall(*env.current(), alice, XRP(10), carol.pk()); + + env(fset(alice, asfDisableMaster), ter(tecNO_PERMISSION)); + env.close(); + } + } + void testWithFeats(FeatureBitset features) { - // testEnabled(features); - testEnabled1(features); + testEnabled(features); + // testPreflight(features); + // testPreclaim(features); + // testDoApply(features); + testFirewallSet(features); + testUpdateAmount(features); + testUpdatePK(features); + testMasterDisable(features); } public: @@ -155,6 +305,6 @@ struct SetFirewall_test : public beast::unit_test::suite } }; -BEAST_DEFINE_TESTSUITE(SetFirewall, app, ripple); +BEAST_DEFINE_TESTSUITE(FirewallSet, app, ripple); } // namespace test } // namespace ripple diff --git a/src/test/jtx/firewall.h b/src/test/jtx/firewall.h index a7f0679b10e..a7761752b9f 100644 --- a/src/test/jtx/firewall.h +++ b/src/test/jtx/firewall.h @@ -49,21 +49,6 @@ class amt operator()(Env&, JTx& jtx) const; }; -/** Sets the optional Amount2 on a JTx. */ -class amt2 -{ -private: - STAmount amt2_; - -public: - explicit amt2(STAmount const& amt2) : amt2_(amt2) - { - } - - void - operator()(Env&, JTx& jtx) const; -}; - /** Sets the optional Authorize on a JTx. */ class auth { diff --git a/src/test/jtx/impl/firewall.cpp b/src/test/jtx/impl/firewall.cpp index c7ad8090a8a..7725cd26bca 100644 --- a/src/test/jtx/impl/firewall.cpp +++ b/src/test/jtx/impl/firewall.cpp @@ -40,12 +40,6 @@ amt::operator()(Env& env, JTx& jt) const jt.jv[sfAmount.jsonName] = amt_.getJson(JsonOptions::none); } -void -amt2::operator()(Env& env, JTx& jt) const -{ - jt.jv[sfAmount2.jsonName] = amt2_.getJson(JsonOptions::none); -} - void auth::operator()(Env& env, JTx& jt) const { diff --git a/src/xrpld/app/tx/detail/Firewall.cpp b/src/xrpld/app/tx/detail/Firewall.cpp new file mode 100644 index 00000000000..045364e4676 --- /dev/null +++ b/src/xrpld/app/tx/detail/Firewall.cpp @@ -0,0 +1,273 @@ +//------------------------------------------------------------------------------ +/* + 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 +#include +#include + +namespace ripple { + +NotTEC +FirewallSet::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureFirewall)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + // auto const amount = ctx.tx[~sfAmount]; + + // if (amount.issue() == amount2.issue()) + // { + // JLOG(ctx.j.debug()) + // << "Firewall: tokens can not have the same currency/issuer."; + // return temBAD_AMM_TOKENS; + // } + + // if (auto const err = invalidAmount(amount)) + // { + // JLOG(ctx.j.debug()) << "Firewall: invalid asset1 amount."; + // return err; + // } + + // Validate Authorize + if (ctx.tx.isFieldPresent(sfAuthorize)) + { + auto const authorizeID = ctx.tx.getAccountID(sfAuthorize); + // Make sure that the passed account is valid. + if (authorizeID == beast::zero) + { + JLOG(ctx.j.debug()) + << "Malformed transaction: Authorized or Unauthorized " + "field zeroed."; + return temINVALID_ACCOUNT_ID; + } + + // An account may not preauthorize itself. + if (authorizeID == ctx.tx[sfAccount]) + { + JLOG(ctx.j.debug()) + << "Malformed transaction: Attempting to FirewallPreauth self."; + return temCANNOT_PREAUTH_SELF; + } + } + + return preflight2(ctx); +} + +TER +FirewallSet::preclaim(PreclaimContext const& ctx) +{ + AccountID const accountID = ctx.tx[sfAccount]; + auto const amount = ctx.tx[~sfAmount]; + + ripple::Keylet const firewallKeylet = keylet::firewall(accountID); + auto const sleFirewall = ctx.view.read(firewallKeylet); + + if (!sleFirewall && ctx.tx.isFieldPresent(sfSignature)) + { + JLOG(ctx.j.debug()) << "Firewall: Set must not contain a sfSignature"; + return temMALFORMED; + } + + if (sleFirewall && !ctx.tx.isFieldPresent(sfSignature)) + { + JLOG(ctx.j.debug()) << "Firewall: Update must contain a sfSignature"; + return temMALFORMED; + } + + if (sleFirewall && ctx.tx.isFieldPresent(sfAuthorize)) + { + JLOG(ctx.j.debug()) << "Firewall: Update cannot contain a sfAuthorize"; + return temMALFORMED; + } + + if (sleFirewall && ctx.tx.isFieldPresent(sfPublicKey) && + ctx.tx.isFieldPresent(sfAmount)) + { + JLOG(ctx.j.debug()) + << "Firewall: Update cannot contain both sfPublicKey & sfAmount"; + return temMALFORMED; + } + + // if (auto const ter = requireAuth(ctx.view, amount.issue(), accountID); + // ter != tesSUCCESS) + // { + // JLOG(ctx.j.debug()) + // << "Firewall: account is not authorized, " << amount.issue(); + // return ter; + // } + + // // Globally or individually frozen + // if (isFrozen(ctx.view, accountID, amount.issue()) || + // isFrozen(ctx.view, accountID, amount2.issue())) + // { + // JLOG(ctx.j.debug()) << "Firewall: involves frozen asset."; + // return tecFROZEN; + // } + + // auto noDefaultRipple = [](ReadView const& view, Issue const& issue) { + // if (isXRP(issue)) + // return false; + + // if (auto const issuerAccount = + // view.read(keylet::account(issue.account))) + // return (issuerAccount->getFlags() & lsfDefaultRipple) == 0; + + // return false; + // }; + + // if (noDefaultRipple(ctx.view, amount.issue())) + // { + // JLOG(ctx.j.debug()) << "Firewall: DefaultRipple not set"; + // return terNO_RIPPLE; + // } + + if (ctx.tx.isFieldPresent(sfSignature) && + ctx.tx.isFieldPresent(sfPublicKey)) + { + PublicKey const txPK(makeSlice(ctx.tx.getFieldVL(sfPublicKey))); + auto const sig = ctx.tx.getFieldVL(sfSignature); + PublicKey const fPK(makeSlice(sleFirewall->getFieldVL(sfPublicKey))); + Serializer msg; + serializeFirewallAuthorization(msg, accountID, txPK); + if (!verify(fPK, msg.slice(), makeSlice(sig), /*canonical*/ true)) + { + JLOG(ctx.j.debug()) + << "Firewall: Bad Signature for update sfPublicKey"; + return temBAD_SIGNATURE; + } + } + + if (ctx.tx.isFieldPresent(sfSignature) && ctx.tx.isFieldPresent(sfAmount)) + { + auto const amount = ctx.tx.getFieldAmount(sfAmount); + auto const sig = ctx.tx.getFieldVL(sfSignature); + PublicKey const pk(makeSlice(sleFirewall->getFieldVL(sfPublicKey))); + Serializer msg; + serializeFirewallAuthorization(msg, accountID, amount); + if (!verify(pk, msg.slice(), makeSlice(sig), /*canonical*/ true)) + { + JLOG(ctx.j.debug()) + << "Firewall: Bad Signature for update sfAmount"; + return temBAD_SIGNATURE; + } + } + + return tesSUCCESS; +} + +TER +FirewallSet::doApply() +{ + Sandbox sb(&ctx_.view()); + + auto const sleOwner = sb.peek(keylet::account(account_)); + if (!sleOwner) + return tefINTERNAL; + + ripple::Keylet const firewallKeylet = keylet::firewall(account_); + auto sleFirewall = sb.peek(firewallKeylet); + if (!sleFirewall) + { + // Set Firewall + auto const sleFirewall = std::make_shared(firewallKeylet); + (*sleFirewall)[sfOwner] = account_; + (*sleFirewall)[~sfPublicKey] = ctx_.tx[~sfPublicKey]; + (*sleFirewall)[~sfAmount] = ctx_.tx[~sfAmount]; + + // Add owner directory to link the account and Firewall object. + if (auto const page = sb.dirInsert( + keylet::ownerDir(account_), + sleFirewall->key(), + describeOwnerDir(account_))) + { + sleFirewall->setFieldU64(sfOwnerNode, *page); + } + else + { + JLOG(j_.debug()) << "Firewall: failed to insert owner dir"; + return tecDIR_FULL; + } + sb.insert(sleFirewall); + adjustOwnerCount(sb, sleOwner, 1, j_); + + // Add Preauth + // A preauth counts against the reserve of the issuing account, but we + // check the starting balance because we want to allow dipping into the + // reserve to pay fees. + { + STAmount const reserve{view().fees().accountReserve( + sleOwner->getFieldU32(sfOwnerCount) + 1)}; + + if (mPriorBalance < reserve) + return tecINSUFFICIENT_RESERVE; + } + + // Preclaim already verified that the Preauth entry does not yet exist. + // Create and populate the Preauth entry. + AccountID const auth{ctx_.tx[sfAuthorize]}; + Keylet const preauthKeylet = keylet::firewallPreauth(account_, auth); + auto slePreauth = std::make_shared(preauthKeylet); + + slePreauth->setAccountID(sfAccount, account_); + slePreauth->setAccountID(sfAuthorize, auth); + + // Add owner directory to link the account and Firewall object. + if (auto const page = sb.dirInsert( + keylet::ownerDir(account_), + slePreauth->key(), + describeOwnerDir(account_))) + { + sleFirewall->setFieldU64(sfOwnerNode, *page); + } + else + { + JLOG(j_.debug()) << "Firewall: failed to insert owner dir"; + return tecDIR_FULL; + } + sb.insert(slePreauth); + adjustOwnerCount(sb, sleOwner, 1, j_); + } + else + { + // Update Firewall + if (ctx_.tx.isFieldPresent(sfPublicKey)) + sleFirewall->setFieldVL( + sfPublicKey, ctx_.tx.getFieldVL(sfPublicKey)); + if (ctx_.tx.isFieldPresent(sfAmount)) + sleFirewall->setFieldAmount( + sfAmount, ctx_.tx.getFieldAmount(sfAmount)); + + sb.update(sleFirewall); + } + + sb.apply(ctx_.rawView()); + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/SetFirewall.h b/src/xrpld/app/tx/detail/Firewall.h similarity index 89% rename from src/xrpld/app/tx/detail/SetFirewall.h rename to src/xrpld/app/tx/detail/Firewall.h index a7d37fab708..d562574fccd 100644 --- a/src/xrpld/app/tx/detail/SetFirewall.h +++ b/src/xrpld/app/tx/detail/Firewall.h @@ -17,8 +17,8 @@ */ //============================================================================== -#ifndef RIPPLE_TX_SETFIREWALL_H_INCLUDED -#define RIPPLE_TX_SETFIREWALL_H_INCLUDED +#ifndef RIPPLE_TX_FIREWALLSET_H_INCLUDED +#define RIPPLE_TX_FIREWALLSET_H_INCLUDED #include #include @@ -27,12 +27,12 @@ namespace ripple { -class SetFirewall : public Transactor +class FirewallSet : public Transactor { public: static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; - explicit SetFirewall(ApplyContext& ctx) : Transactor(ctx) + explicit FirewallSet(ApplyContext& ctx) : Transactor(ctx) { } diff --git a/src/xrpld/app/tx/detail/FirewallPreauth.cpp b/src/xrpld/app/tx/detail/FirewallPreauth.cpp new file mode 100644 index 00000000000..e658491987e --- /dev/null +++ b/src/xrpld/app/tx/detail/FirewallPreauth.cpp @@ -0,0 +1,203 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2018 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 + +namespace ripple { + +NotTEC +FirewallPreauth::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureFirewall)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + auto& tx = ctx.tx; + auto& j = ctx.j; + + if (tx.getFlags() & tfUniversalMask) + { + JLOG(j.trace()) << "Malformed transaction: Invalid flags set."; + return temINVALID_FLAG; + } + + auto const optAuth = ctx.tx[~sfAuthorize]; + auto const optUnauth = ctx.tx[~sfUnauthorize]; + if (static_cast(optAuth) == static_cast(optUnauth)) + { + // Either both fields are present or neither field is present. In + // either case the transaction is malformed. + JLOG(j.trace()) + << "Malformed transaction: " + "Invalid Authorize and Unauthorize field combination."; + return temMALFORMED; + } + + // Make sure that the passed account is valid. + AccountID const target{optAuth ? *optAuth : *optUnauth}; + if (target == beast::zero) + { + JLOG(j.trace()) << "Malformed transaction: Authorized or Unauthorized " + "field zeroed."; + return temINVALID_ACCOUNT_ID; + } + + // An account may not preauthorize itself. + if (optAuth && (target == ctx.tx[sfAccount])) + { + JLOG(j.trace()) + << "Malformed transaction: Attempting to FirewallPreauth self."; + return temCANNOT_PREAUTH_SELF; + } + + return preflight2(ctx); +} + +TER +FirewallPreauth::preclaim(PreclaimContext const& ctx) +{ + // Determine which operation we're performing: authorizing or unauthorizing. + if (ctx.tx.isFieldPresent(sfAuthorize)) + { + // Verify that the Authorize account is present in the ledger. + AccountID const auth{ctx.tx[sfAuthorize]}; + if (!ctx.view.exists(keylet::account(auth))) + return tecNO_TARGET; + + // Verify that the Preauth entry they asked to add is not already + // in the ledger. + if (ctx.view.exists(keylet::firewallPreauth(ctx.tx[sfAccount], auth))) + return tecDUPLICATE; + } + else + { + // Verify that the Preauth entry they asked to remove is in the ledger. + AccountID const unauth{ctx.tx[sfUnauthorize]}; + if (!ctx.view.exists(keylet::firewallPreauth(ctx.tx[sfAccount], unauth))) + return tecNO_ENTRY; + } + return tesSUCCESS; +} + +TER +FirewallPreauth::doApply() +{ + if (ctx_.tx.isFieldPresent(sfAuthorize)) + { + auto const sleOwner = view().peek(keylet::account(account_)); + if (!sleOwner) + return {tefINTERNAL}; + + // A preauth counts against the reserve of the issuing account, but we + // check the starting balance because we want to allow dipping into the + // reserve to pay fees. + { + STAmount const reserve{view().fees().accountReserve( + sleOwner->getFieldU32(sfOwnerCount) + 1)}; + + if (mPriorBalance < reserve) + return tecINSUFFICIENT_RESERVE; + } + + // Preclaim already verified that the Preauth entry does not yet exist. + // Create and populate the Preauth entry. + AccountID const auth{ctx_.tx[sfAuthorize]}; + Keylet const preauthKeylet = keylet::firewallPreauth(account_, auth); + auto slePreauth = std::make_shared(preauthKeylet); + + slePreauth->setAccountID(sfAccount, account_); + slePreauth->setAccountID(sfAuthorize, auth); + view().insert(slePreauth); + + auto viewJ = ctx_.app.journal("View"); + auto const page = view().dirInsert( + keylet::ownerDir(account_), + preauthKeylet, + describeOwnerDir(account_)); + + JLOG(j_.trace()) << "Adding FirewallPreauth to owner directory " + << to_string(preauthKeylet.key) << ": " + << (page ? "success" : "failure"); + + if (!page) + return tecDIR_FULL; + + slePreauth->setFieldU64(sfOwnerNode, *page); + + // If we succeeded, the new entry counts against the creator's reserve. + adjustOwnerCount(view(), sleOwner, 1, viewJ); + } + else + { + auto const preauth = + keylet::firewallPreauth(account_, ctx_.tx[sfUnauthorize]); + + return FirewallPreauth::removeFromLedger( + ctx_.app, view(), preauth.key, j_); + } + return tesSUCCESS; +} + +TER +FirewallPreauth::removeFromLedger( + Application& app, + ApplyView& view, + uint256 const& preauthIndex, + beast::Journal j) +{ + // Verify that the Preauth entry they asked to remove is + // in the ledger. + std::shared_ptr const slePreauth{ + view.peek(keylet::firewallPreauth(preauthIndex))}; + if (!slePreauth) + { + JLOG(j.warn()) << "Selected FirewallPreauth does not exist."; + return tecNO_ENTRY; + } + + AccountID const account{(*slePreauth)[sfAccount]}; + std::uint64_t const page{(*slePreauth)[sfOwnerNode]}; + if (!view.dirRemove(keylet::ownerDir(account), page, preauthIndex, false)) + { + JLOG(j.fatal()) << "Unable to delete FirewallPreauth from owner."; + return tefBAD_LEDGER; + } + + // If we succeeded, update the FirewallPreauth owner's reserve. + auto const sleOwner = view.peek(keylet::account(account)); + if (!sleOwner) + return tefINTERNAL; + + adjustOwnerCount(view, sleOwner, -1, app.journal("View")); + + // Remove FirewallPreauth from ledger. + view.erase(slePreauth); + + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/FirewallPreauth.h b/src/xrpld/app/tx/detail/FirewallPreauth.h new file mode 100644 index 00000000000..6a7f7cab8fd --- /dev/null +++ b/src/xrpld/app/tx/detail/FirewallPreauth.h @@ -0,0 +1,56 @@ +//------------------------------------------------------------------------------ +/* + 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_FIREWALL_PREAUTH_H_INCLUDED +#define RIPPLE_TX_FIREWALL_PREAUTH_H_INCLUDED + +#include + +namespace ripple { + +class FirewallPreauth : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit FirewallPreauth(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + // Interface used by DeleteAccount + static TER + removeFromLedger( + Application& app, + ApplyView& view, + uint256 const& delIndex, + beast::Journal j); +}; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index bd230d4dfbd..e83e5f10601 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -479,6 +479,7 @@ LedgerEntryTypesMatch::visitEntry( case ltDID: case ltORACLE: case ltFIREWALL: + case ltFIREWALL_PREAUTH: break; default: invalidTypeAdded_ = true; diff --git a/src/xrpld/app/tx/detail/SetAccount.cpp b/src/xrpld/app/tx/detail/SetAccount.cpp index c0e115c2497..e983da04b8d 100644 --- a/src/xrpld/app/tx/detail/SetAccount.cpp +++ b/src/xrpld/app/tx/detail/SetAccount.cpp @@ -249,6 +249,17 @@ SetAccount::preclaim(PreclaimContext const& ctx) } } + // Firewall - Cannot Set Disable Master + if (ctx.view.rules().enabled(featureFirewall)) + { + if (auto const sleFirewall = ctx.view.read(keylet::firewall(id)); + sleFirewall && uSetFlag == asfDisableMaster) + { + JLOG(ctx.j.trace()) << "Blocked by Firewall."; + return tecNO_PERMISSION; + } + } + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/SetFirewall.cpp b/src/xrpld/app/tx/detail/SetFirewall.cpp deleted file mode 100644 index f419e17e85c..00000000000 --- a/src/xrpld/app/tx/detail/SetFirewall.cpp +++ /dev/null @@ -1,216 +0,0 @@ -//------------------------------------------------------------------------------ -/* - 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 -#include -#include - -namespace ripple { - -NotTEC -SetFirewall::preflight(PreflightContext const& ctx) -{ - if (!ctx.rules.enabled(featureFirewall)) - return temDISABLED; - - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - auto const amount = ctx.tx[~sfAmount]; - auto const amount2 = ctx.tx[~sfAmount2]; - - // if (amount.issue() == amount2.issue()) - // { - // JLOG(ctx.j.debug()) - // << "Firewall: tokens can not have the same currency/issuer."; - // return temBAD_AMM_TOKENS; - // } - - // if (auto const err = invalidAmount(amount)) - // { - // JLOG(ctx.j.debug()) << "Firewall: invalid asset1 amount."; - // return err; - // } - - // if (auto const err = invalidAmount(amount2)) - // { - // JLOG(ctx.j.debug()) << "Firewall: invalid asset2 amount."; - // return err; - // } - - // Validate AuthAccounts Max 8 - if (ctx.tx.isFieldPresent(sfAuthAccounts)) - { - if (auto const authAccounts = ctx.tx.getFieldArray(sfAuthAccounts); - authAccounts.size() > 8) - { - JLOG(ctx.j.debug()) << "Firewall: Invalid number of AuthAccounts."; - return temMALFORMED; - } - } - - return preflight2(ctx); -} - -TER -SetFirewall::preclaim(PreclaimContext const& ctx) -{ - AccountID const accountID = ctx.tx[sfAccount]; - // auto const sequence = ctx.tx[sfSequence]; - auto const amount = ctx.tx[~sfAmount]; - auto const amount2 = ctx.tx[~sfAmount2]; - - // Check if Firewall already exists - ripple::Keylet const firewallKeylet = keylet::firewall(accountID); - auto const sleFirewall = ctx.view.read(firewallKeylet); - - // if (auto const ter = requireAuth(ctx.view, amount.issue(), accountID); - // ter != tesSUCCESS) - // { - // JLOG(ctx.j.debug()) - // << "Firewall: account is not authorized, " << amount.issue(); - // return ter; - // } - - // if (auto const ter = requireAuth(ctx.view, amount2.issue(), accountID); - // ter != tesSUCCESS) - // { - // JLOG(ctx.j.debug()) - // << "Firewall: account is not authorized, " << amount2.issue(); - // return ter; - // } - - // // Globally or individually frozen - // if (isFrozen(ctx.view, accountID, amount.issue()) || - // isFrozen(ctx.view, accountID, amount2.issue())) - // { - // JLOG(ctx.j.debug()) << "Firewall: involves frozen asset."; - // return tecFROZEN; - // } - - // auto noDefaultRipple = [](ReadView const& view, Issue const& issue) { - // if (isXRP(issue)) - // return false; - - // if (auto const issuerAccount = - // view.read(keylet::account(issue.account))) - // return (issuerAccount->getFlags() & lsfDefaultRipple) == 0; - - // return false; - // }; - - // if (noDefaultRipple(ctx.view, amount.issue()) || - // noDefaultRipple(ctx.view, amount2.issue())) - // { - // JLOG(ctx.j.debug()) << "Firewall: DefaultRipple not set"; - // return terNO_RIPPLE; - // } - - // Validate AuthAccounts (Has Account Has Amount Amount > 0) - if (ctx.tx.isFieldPresent(sfSignature) && ctx.tx.isFieldPresent(sfAuthAccounts)) - { - auto const authAccounts = ctx.tx.getFieldArray(sfAuthAccounts); - auto const sig = ctx.tx.getFieldVL(sfSignature); - PublicKey const pk(makeSlice(sleFirewall->getFieldVL(sfPublicKey))); - Serializer msg; - serializeFirewallAuthorization(msg, authAccounts); - if (!verify(pk, msg.slice(), makeSlice(sig), /*canonical*/ true)) - return temBAD_SIGNATURE; - } - - return tesSUCCESS; -} - -TER -SetFirewall::doApply() -{ - Sandbox sb(&ctx_.view()); - - ripple::Keylet const firewallKeylet = keylet::firewall(account_); - auto firewallSle = sb.peek(firewallKeylet); - if (!firewallSle) - { - // Set Firewall - auto const firewallSle = std::make_shared(firewallKeylet); - (*firewallSle)[sfOwner] = account_; - (*firewallSle)[~sfAuthorize] = ctx_.tx[~sfAuthorize]; - (*firewallSle)[~sfPublicKey] = ctx_.tx[~sfPublicKey]; - (*firewallSle)[~sfAmount] = ctx_.tx[~sfAmount]; - (*firewallSle)[~sfAmount2] = ctx_.tx[~sfAmount2]; - if (ctx_.tx.isFieldPresent(sfAuthAccounts)) - { - auto const authAccounts = ctx_.tx.getFieldArray(sfAuthAccounts); - firewallSle->setFieldArray(sfAuthAccounts, authAccounts); - } - - // Add owner directory to link the account and Firewall object. - if (auto const page = sb.dirInsert( - keylet::ownerDir(account_), - firewallSle->key(), - describeOwnerDir(account_))) - { - firewallSle->setFieldU64(sfOwnerNode, *page); - } - else - { - JLOG(j_.error()) << "Firewall: failed to insert owner dir"; - return tecDIR_FULL; - } - - sb.insert(firewallSle); - } - else - { - // Update Firewall - JLOG(j_.error()) << "Firewall: Update Firewall"; - - if (ctx_.tx.isFieldPresent(sfAuthorize)) - firewallSle->setAccountID(sfAuthorize, ctx_.tx.getAccountID(sfAuthorize)); - if (ctx_.tx.isFieldPresent(sfPublicKey)) - firewallSle->setFieldH256(sfPublicKey, ctx_.tx.getFieldH256(sfPublicKey)); - if (ctx_.tx.isFieldPresent(sfAmount)) - firewallSle->setFieldAmount(sfAmount, ctx_.tx.getFieldAmount(sfAmount)); - if (ctx_.tx.isFieldPresent(sfAmount2)) - firewallSle->setFieldAmount(sfAmount2, ctx_.tx.getFieldAmount(sfAmount2)); - - if (ctx_.tx.isFieldPresent(sfAuthAccounts)) - { - auto const authAccounts = ctx_.tx.getFieldArray(sfAuthAccounts); - firewallSle->setFieldArray(sfAuthAccounts, authAccounts); - } - if (ctx_.tx.isFieldPresent(sfAuthAccounts)) - { - auto const authAccounts = ctx_.tx.getFieldArray(sfAuthAccounts); - firewallSle->setFieldArray(sfAuthAccounts, authAccounts); - } - sb.update(firewallSle); - } - - sb.apply(ctx_.rawView()); - return tesSUCCESS; -} - -} // namespace ripple diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 0a9936be8b5..829f88eb22f 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -435,116 +435,62 @@ Transactor::ticketDelete( return tesSUCCESS; } +/** + * @brief Checks if a transaction is blocked by a firewall. + * + * This function inspects the transaction to determine if it should be blocked + * by a firewall rule. It only applies to payment transactions. If the firewall + * is active and the transaction does not meet the criteria, it will be blocked. + * + * @param ctx The context of the transaction, including the transaction itself + * and the current view of the ledger. + * @param j The journal for logging. + * @return A status code indicating whether the transaction is allowed or blocked. + */ TER -isWhitelisted( - ReadView const& view, - STAmount const& fireAmount, - STAmount const& txAmount, - STArray const& authAccounts, - AccountID const& dest) +Transactor::checkFirewall(PreclaimContext const& ctx, beast::Journal j) { - if (txAmount <= fireAmount) - { - return tesSUCCESS; - } - else - { - for (auto const& account : authAccounts) - { - if (dest != account[sfAccount]) - { - // pass - } - else - { - if (!view.read(keylet::account(account[sfAccount]))) - { - // JLOG(ctx.j.debug()) << "Firewall: Invalid Account."; - return tecINTERNAL; - } - auto const authAmount = account[~sfAmount]; - if (authAmount && *authAmount >= fireAmount) - { - // Remove Auth Account? - return tesSUCCESS; - } - } - } - // JLOG(j.debug()) << "checkFirewall: XRP Txn Blocked"; - return tecFIREWALL_BLOCK; - // return tecFIREWALL; - } -} - -// Check for firewall. -TER -Transactor::checkFirewall( - PreclaimContext const& ctx, - beast::Journal j) -{ - JLOG(j.error()) << "checkFirewall: START"; - // if ( - // !ctx.tx.isFieldPresent(sfAmount) - // // !ctx.tx.isFieldPresent(sfAmount2) && - // // !ctx.tx.isFieldPresent(sfTakerGets) && - // // !ctx.tx.isFieldPresent(sfTakerPays) - // ) - // ttPAYMENT - // ttOFFER (Create) - // ttESCROW (Create) - // ttPAYCHAN (Create) - // ttNFTOKEN_MINT (Create) - // ttAMM (Create/Deposit) + // Only apply firewall checks to payment transactions if (ctx.tx.getTxnType() != ttPAYMENT) { - JLOG(j.error()) << "checkFirewall: Not Firewall Txn"; + JLOG(j.debug()) << "checkFirewall: Not a payment transaction"; return tesSUCCESS; } + // Get the account ID of the sender and the destination account auto const id = ctx.tx.getAccountID(sfAccount); auto const dest = ctx.tx.getAccountID(sfDestination); + // Read the firewall settings for the sender account auto const sleFirewall = ctx.view.read(keylet::firewall(id)); if (!sleFirewall) { - JLOG(j.error()) << "checkFirewall: No Firewall Active"; + JLOG(j.debug()) << "checkFirewall: No firewall settings found"; return tesSUCCESS; } - if (sleFirewall->getAccountID(sfAuthorize) == dest) + // Check if the transaction amount exceeds the firewall limit + if (ctx.tx.isFieldPresent(sfAmount) && + sleFirewall->isFieldPresent(sfAmount)) { - JLOG(j.error()) << "checkFirewall: Destination == Authorize"; - return tesSUCCESS; + if (ctx.tx.getFieldAmount(sfAmount) <= + sleFirewall->getFieldAmount(sfAmount)) + { + JLOG(j.debug()) << "checkFirewall: Transaction amount within limit"; + return tesSUCCESS; + } } - // Check Whitelist - if (sleFirewall->isFieldPresent(sfAuthAccounts)) + // Check if there is a preauthorization for the destination account + if (auto const sleFirewallPreauth = + ctx.view.read(keylet::firewallPreauth(id, dest)); + !sleFirewallPreauth) { - auto const authAccounts = sleFirewall->getFieldArray(sfAuthAccounts); - - auto const firewallXRP = sleFirewall->getFieldAmount(sfAmount); - auto const firewallIOU = sleFirewall->getFieldAmount(sfAmount2); - // auto const txAmount = tx.getFieldAmount(~sfAmount); - - // Check XRP - if (ctx.tx.isFieldPresent(sfAmount) && firewallXRP) - { - auto const txXRP = ctx.tx.getFieldAmount(sfAmount); - if (auto const ter = isWhitelisted(ctx.view, firewallXRP, txXRP, authAccounts, dest); ter != tesSUCCESS) - { - return ter; - } - } - // Check IOU - if (ctx.tx.isFieldPresent(sfAmount) && firewallIOU) - { - auto const txIOU = ctx.tx.getFieldAmount(sfAmount); - if (auto const ter = isWhitelisted(ctx.view, firewallIOU, txIOU, authAccounts, dest); ter != tesSUCCESS) - { - return ter; - } - } + JLOG(j.debug()) << "checkFirewall: No preauthorization for destination"; + return tecFIREWALL_BLOCK; } + + JLOG(j.debug()) << "checkFirewall: Firewall block due to amount limit"; return tecFIREWALL_BLOCK; } diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index 0c53d48a823..2d19059e807 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -50,7 +50,8 @@ #include #include #include -#include +#include +#include #include #include @@ -167,7 +168,9 @@ with_txn_type(TxType txnType, F&& f) case ttORACLE_DELETE: return f.template operator()(); case ttFIREWALL_SET: - return f.template operator()(); + return f.template operator()(); + case ttFIREWALL_PREAUTH: + return f.template operator()(); default: throw UnknownTxnType(txnType); } diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp index 8d9d6367b2b..b7c873f3617 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -934,7 +934,7 @@ chooseLedgerEntryType(Json::Value const& params) std::pair result{RPC::Status::OK, ltANY}; if (params.isMember(jss::type)) { - static constexpr std::array, 23> + static constexpr std::array, 24> types{ {{jss::account, ltACCOUNT_ROOT}, {jss::amendments, ltAMENDMENTS}, @@ -947,6 +947,7 @@ chooseLedgerEntryType(Json::Value const& params) {jss::escrow, ltESCROW}, {jss::fee, ltFEE_SETTINGS}, {jss::firewall, ltFIREWALL}, + {jss::firewall_preauth, ltFIREWALL_PREAUTH}, {jss::hashes, ltLEDGER_HASHES}, {jss::nunl, ltNEGATIVE_UNL}, {jss::oracle, ltORACLE}, From 0654697a00670d2b3f3f26893702d7fa98fdaebc Mon Sep 17 00:00:00 2001 From: Denis Angell Date: Sun, 21 Jul 2024 15:18:23 +0200 Subject: [PATCH 3/8] clang-format --- include/xrpl/protocol/Firewall.h | 15 +++++++++++--- include/xrpl/protocol/Indexes.h | 4 +++- src/libxrpl/protocol/TxFormats.cpp | 2 +- src/test/app/SetFirewall_test.cpp | 23 ++++++++++++--------- src/test/jtx/impl/firewall.cpp | 1 - src/xrpld/app/tx/detail/FirewallPreauth.cpp | 3 ++- src/xrpld/app/tx/detail/Transactor.cpp | 3 ++- src/xrpld/app/tx/detail/Transactor.h | 4 +--- src/xrpld/app/tx/detail/applySteps.cpp | 4 ++-- 9 files changed, 36 insertions(+), 23 deletions(-) diff --git a/include/xrpl/protocol/Firewall.h b/include/xrpl/protocol/Firewall.h index d3df7ae4bce..ac08057e607 100644 --- a/include/xrpl/protocol/Firewall.h +++ b/include/xrpl/protocol/Firewall.h @@ -39,7 +39,10 @@ namespace ripple { * @param preauthorize The preauthorize account ID to be serialized. */ inline void -serializeFirewallAuthorization(Serializer& msg, AccountID const& account, AccountID const& preauthorize) +serializeFirewallAuthorization( + Serializer& msg, + AccountID const& account, + AccountID const& preauthorize) { msg.add32(HashPrefix::shardInfo); msg.addBitString(account); @@ -58,7 +61,10 @@ serializeFirewallAuthorization(Serializer& msg, AccountID const& account, Accoun * @param amount The amount to be serialized. */ inline void -serializeFirewallAuthorization(Serializer& msg, AccountID const& account, STAmount const& amount) +serializeFirewallAuthorization( + Serializer& msg, + AccountID const& account, + STAmount const& amount) { msg.add32(HashPrefix::shardInfo); msg.addBitString(account); @@ -77,7 +83,10 @@ serializeFirewallAuthorization(Serializer& msg, AccountID const& account, STAmou * @param pk The public key to be serialized. */ inline void -serializeFirewallAuthorization(Serializer& msg, AccountID const& account, PublicKey const& pk) +serializeFirewallAuthorization( + Serializer& msg, + AccountID const& account, + PublicKey const& pk) { msg.add32(HashPrefix::shardInfo); msg.addBitString(account); diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 257044e43ea..44d8c7acf63 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -293,7 +293,9 @@ firewall(AccountID const& account) noexcept; /** A FireallPreauth */ /** @{ */ Keylet -firewallPreauth(AccountID const& owner, AccountID const& preauthorized) noexcept; +firewallPreauth( + AccountID const& owner, + AccountID const& preauthorized) noexcept; inline Keylet firewallPreauth(uint256 const& key) noexcept diff --git a/src/libxrpl/protocol/TxFormats.cpp b/src/libxrpl/protocol/TxFormats.cpp index bf802e3b9f4..6b3367d8e33 100644 --- a/src/libxrpl/protocol/TxFormats.cpp +++ b/src/libxrpl/protocol/TxFormats.cpp @@ -505,7 +505,7 @@ TxFormats::TxFormats() {sfOracleDocumentID, soeREQUIRED}, }, commonFields); - + add(jss::FirewallSet, ttFIREWALL_SET, { diff --git a/src/test/app/SetFirewall_test.cpp b/src/test/app/SetFirewall_test.cpp index 9248dad926f..1f460a5fd24 100644 --- a/src/test/app/SetFirewall_test.cpp +++ b/src/test/app/SetFirewall_test.cpp @@ -27,7 +27,6 @@ namespace ripple { namespace test { struct FirewallSet_test : public beast::unit_test::suite { - static std::size_t ownerDirCount(ReadView const& view, jtx::Account const& acct) { @@ -60,9 +59,7 @@ struct FirewallSet_test : public beast::unit_test::suite } static std::pair> - firewallKeyAndSle( - ReadView const& view, - jtx::Account const& account) + firewallKeyAndSle(ReadView const& view, jtx::Account const& account) { auto const k = keylet::firewall(account); return {k.key, view.read(k)}; @@ -95,12 +92,14 @@ struct FirewallSet_test : public beast::unit_test::suite { // If the Firewall amendment is not enabled, you should not be able // to set or delete firewall. - auto const amend = withFirewall ? features : features - featureFirewall; + auto const amend = + withFirewall ? features : features - featureFirewall; Env env{*this, amend}; env.fund(XRP(1000), alice); env.close(); - auto const txResult = withFirewall ? ter(tesSUCCESS) : ter(temDISABLED); + auto const txResult = + withFirewall ? ter(tesSUCCESS) : ter(temDISABLED); auto const ownerDir = withFirewall ? 1 : 0; // SET @@ -224,8 +223,10 @@ struct FirewallSet_test : public beast::unit_test::suite { Json::Value params; - params[jss::transaction] = env.tx()->getJson(JsonOptions::none)[jss::hash]; - auto jrr = env.rpc("json", "tx", to_string(params))[jss::result]; + params[jss::transaction] = + env.tx()->getJson(JsonOptions::none)[jss::hash]; + auto jrr = + env.rpc("json", "tx", to_string(params))[jss::result]; std::cout << "RESULT: " << jrr << "\n"; } @@ -241,8 +242,10 @@ struct FirewallSet_test : public beast::unit_test::suite { Json::Value params; - params[jss::transaction] = env.tx()->getJson(JsonOptions::none)[jss::hash]; - auto jrr = env.rpc("json", "tx", to_string(params))[jss::result]; + params[jss::transaction] = + env.tx()->getJson(JsonOptions::none)[jss::hash]; + auto jrr = + env.rpc("json", "tx", to_string(params))[jss::result]; std::cout << "RESULT: " << jrr << "\n"; } diff --git a/src/test/jtx/impl/firewall.cpp b/src/test/jtx/impl/firewall.cpp index 7725cd26bca..696e91a4aae 100644 --- a/src/test/jtx/impl/firewall.cpp +++ b/src/test/jtx/impl/firewall.cpp @@ -58,7 +58,6 @@ sig::operator()(Env& env, JTx& jt) const jt.jv[sfSignature.jsonName] = sig_; } - } // namespace firewall } // namespace jtx } // namespace test diff --git a/src/xrpld/app/tx/detail/FirewallPreauth.cpp b/src/xrpld/app/tx/detail/FirewallPreauth.cpp index e658491987e..fd2db1240dc 100644 --- a/src/xrpld/app/tx/detail/FirewallPreauth.cpp +++ b/src/xrpld/app/tx/detail/FirewallPreauth.cpp @@ -97,7 +97,8 @@ FirewallPreauth::preclaim(PreclaimContext const& ctx) { // Verify that the Preauth entry they asked to remove is in the ledger. AccountID const unauth{ctx.tx[sfUnauthorize]}; - if (!ctx.view.exists(keylet::firewallPreauth(ctx.tx[sfAccount], unauth))) + if (!ctx.view.exists( + keylet::firewallPreauth(ctx.tx[sfAccount], unauth))) return tecNO_ENTRY; } return tesSUCCESS; diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 829f88eb22f..b9fc0121f15 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -445,7 +445,8 @@ Transactor::ticketDelete( * @param ctx The context of the transaction, including the transaction itself * and the current view of the ledger. * @param j The journal for logging. - * @return A status code indicating whether the transaction is allowed or blocked. + * @return A status code indicating whether the transaction is allowed or + * blocked. */ TER Transactor::checkFirewall(PreclaimContext const& ctx, beast::Journal j) diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index 657b3b7967c..c3fcefd6bcd 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -159,9 +159,7 @@ class Transactor beast::Journal j); static TER - checkFirewall( - PreclaimContext const& ctx, - beast::Journal j); + checkFirewall(PreclaimContext const& ctx, beast::Journal j); protected: TER diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index 2d19059e807..e575bb43177 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -38,6 +38,8 @@ #include #include #include +#include +#include #include #include #include @@ -50,8 +52,6 @@ #include #include #include -#include -#include #include #include From 694fa3764f70a1ab9982cc4014b2068e65ab9614 Mon Sep 17 00:00:00 2001 From: Denis Angell Date: Sun, 21 Jul 2024 15:42:10 +0200 Subject: [PATCH 4/8] [fold] refactor --- src/test/app/SetFirewall_test.cpp | 51 ++++------ src/test/jtx/firewall.h | 8 +- src/test/jtx/impl/firewall.cpp | 4 +- src/xrpld/app/tx/detail/Firewall.cpp | 139 +++++++++++++++------------ 4 files changed, 99 insertions(+), 103 deletions(-) diff --git a/src/test/app/SetFirewall_test.cpp b/src/test/app/SetFirewall_test.cpp index 1f460a5fd24..7a0e4a78fcd 100644 --- a/src/test/app/SetFirewall_test.cpp +++ b/src/test/app/SetFirewall_test.cpp @@ -85,27 +85,30 @@ struct FirewallSet_test : public beast::unit_test::suite using namespace jtx; using namespace std::literals::chrono_literals; - // setup env Account const alice = Account("alice"); + Account const bob = Account("bob"); + Account const carol = Account("carol"); - for (bool const withFirewall : {false, true}) + for (bool const withFirewall : {true, false}) { // If the Firewall amendment is not enabled, you should not be able // to set or delete firewall. auto const amend = withFirewall ? features : features - featureFirewall; Env env{*this, amend}; - env.fund(XRP(1000), alice); + env.fund(XRP(1000), alice, bob); env.close(); auto const txResult = withFirewall ? ter(tesSUCCESS) : ter(temDISABLED); - auto const ownerDir = withFirewall ? 1 : 0; + auto const dirCount = withFirewall ? 2 : 0; - // SET - env(firewall::set(alice), txResult); + env(firewall::set(alice), + firewall::auth(bob), + firewall::pk(carol.pk()), + txResult); env.close(); - BEAST_EXPECT(ownerDirCount(*env.current(), alice) == ownerDir); + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == dirCount); } } @@ -128,7 +131,7 @@ struct FirewallSet_test : public beast::unit_test::suite env(firewall::set(alice), firewall::auth(carol), firewall::amt(XRP(10)), - firewall::pk(strHex(carol.pk().slice())), + firewall::pk(carol.pk()), ter(tesSUCCESS)); env.close(); @@ -158,7 +161,7 @@ struct FirewallSet_test : public beast::unit_test::suite env(firewall::set(alice), firewall::auth(carol), firewall::amt(XRP(10)), - firewall::pk(strHex(carol.pk().slice())), + firewall::pk(carol.pk()), ter(tesSUCCESS)); env.close(); @@ -171,7 +174,7 @@ struct FirewallSet_test : public beast::unit_test::suite carol.pk(), carol.sk(), alice.id(), XRP(100)); env(firewall::set(alice), firewall::amt(XRP(100)), - firewall::sig(strHex(Slice(sig))), + firewall::sig(sig), ter(tesSUCCESS)); env.close(); @@ -202,7 +205,7 @@ struct FirewallSet_test : public beast::unit_test::suite env(firewall::set(alice), firewall::auth(carol), firewall::amt(XRP(10)), - firewall::pk(strHex(carol.pk().slice())), + firewall::pk(carol.pk()), ter(tesSUCCESS)); env.close(); @@ -214,41 +217,23 @@ struct FirewallSet_test : public beast::unit_test::suite auto const sig1 = sigFirewallAuthPK( carol.pk(), carol.sk(), alice.id(), dave.pk()); env(firewall::set(alice), - firewall::pk(strHex(dave.pk().slice())), - firewall::sig(strHex(Slice(sig1))), + firewall::pk(dave.pk()), + firewall::sig(sig1), ter(tesSUCCESS)); env.close(); verifyFirewall(*env.current(), alice, XRP(10), dave.pk()); - { - Json::Value params; - params[jss::transaction] = - env.tx()->getJson(JsonOptions::none)[jss::hash]; - auto jrr = - env.rpc("json", "tx", to_string(params))[jss::result]; - std::cout << "RESULT: " << jrr << "\n"; - } - auto const sig2 = sigFirewallAuthAmount( dave.pk(), dave.sk(), alice.id(), XRP(100)); env(firewall::set(alice), firewall::amt(XRP(100)), - firewall::sig(strHex(Slice(sig2))), + firewall::sig(sig2), ter(tesSUCCESS)); env.close(); verifyFirewall(*env.current(), alice, XRP(100), dave.pk()); - { - Json::Value params; - params[jss::transaction] = - env.tx()->getJson(JsonOptions::none)[jss::hash]; - auto jrr = - env.rpc("json", "tx", to_string(params))[jss::result]; - std::cout << "RESULT: " << jrr << "\n"; - } - env(pay(alice, bob, XRP(100)), ter(tesSUCCESS)); env.close(); } @@ -274,7 +259,7 @@ struct FirewallSet_test : public beast::unit_test::suite env(firewall::set(alice), firewall::auth(carol), firewall::amt(XRP(10)), - firewall::pk(strHex(carol.pk().slice())), + firewall::pk(carol.pk()), ter(tesSUCCESS)); env.close(); diff --git a/src/test/jtx/firewall.h b/src/test/jtx/firewall.h index a7761752b9f..182b28353fd 100644 --- a/src/test/jtx/firewall.h +++ b/src/test/jtx/firewall.h @@ -68,10 +68,10 @@ class auth class pk { private: - std::string pk_; + PublicKey pk_; public: - explicit pk(std::string const& pk) : pk_(pk) + explicit pk(PublicKey const& pk) : pk_(pk) { } @@ -83,10 +83,10 @@ class pk class sig { private: - std::string sig_; + Buffer sig_; public: - explicit sig(std::string const& sig) : sig_(sig) + explicit sig(Buffer const& sig) : sig_(sig) { } diff --git a/src/test/jtx/impl/firewall.cpp b/src/test/jtx/impl/firewall.cpp index 696e91a4aae..a36d14867d4 100644 --- a/src/test/jtx/impl/firewall.cpp +++ b/src/test/jtx/impl/firewall.cpp @@ -49,13 +49,13 @@ auth::operator()(Env& env, JTx& jt) const void pk::operator()(Env& env, JTx& jt) const { - jt.jv[sfPublicKey.jsonName] = pk_; + jt.jv[sfPublicKey.jsonName] = strHex(pk_.slice()); } void sig::operator()(Env& env, JTx& jt) const { - jt.jv[sfSignature.jsonName] = sig_; + jt.jv[sfSignature.jsonName] = strHex(Slice(sig_)); } } // namespace firewall diff --git a/src/xrpld/app/tx/detail/Firewall.cpp b/src/xrpld/app/tx/detail/Firewall.cpp index 045364e4676..d18591e7507 100644 --- a/src/xrpld/app/tx/detail/Firewall.cpp +++ b/src/xrpld/app/tx/detail/Firewall.cpp @@ -83,37 +83,87 @@ TER FirewallSet::preclaim(PreclaimContext const& ctx) { AccountID const accountID = ctx.tx[sfAccount]; - auto const amount = ctx.tx[~sfAmount]; - ripple::Keylet const firewallKeylet = keylet::firewall(accountID); auto const sleFirewall = ctx.view.read(firewallKeylet); - if (!sleFirewall && ctx.tx.isFieldPresent(sfSignature)) + if (!sleFirewall) { - JLOG(ctx.j.debug()) << "Firewall: Set must not contain a sfSignature"; - return temMALFORMED; + if (ctx.tx.isFieldPresent(sfSignature)) + { + JLOG(ctx.j.debug()) + << "Firewall: Set must not contain a sfSignature"; + return temMALFORMED; + } + if (!ctx.tx.isFieldPresent(sfAuthorize)) + { + JLOG(ctx.j.debug()) << "Firewall: Set must contain a sfAuthorize"; + return temMALFORMED; + } + if (!ctx.tx.isFieldPresent(sfPublicKey)) + { + JLOG(ctx.j.debug()) << "Firewall: Set must contain a sfPublicKey"; + return temMALFORMED; + } } - - if (sleFirewall && !ctx.tx.isFieldPresent(sfSignature)) + else { - JLOG(ctx.j.debug()) << "Firewall: Update must contain a sfSignature"; - return temMALFORMED; - } + if (!ctx.tx.isFieldPresent(sfSignature)) + { + JLOG(ctx.j.debug()) + << "Firewall: Update must contain a sfSignature"; + return temMALFORMED; + } - if (sleFirewall && ctx.tx.isFieldPresent(sfAuthorize)) - { - JLOG(ctx.j.debug()) << "Firewall: Update cannot contain a sfAuthorize"; - return temMALFORMED; - } + if (ctx.tx.isFieldPresent(sfAuthorize)) + { + JLOG(ctx.j.debug()) + << "Firewall: Update cannot contain a sfAuthorize"; + return temMALFORMED; + } - if (sleFirewall && ctx.tx.isFieldPresent(sfPublicKey) && - ctx.tx.isFieldPresent(sfAmount)) - { - JLOG(ctx.j.debug()) - << "Firewall: Update cannot contain both sfPublicKey & sfAmount"; - return temMALFORMED; + if (ctx.tx.isFieldPresent(sfPublicKey) && + ctx.tx.isFieldPresent(sfAmount)) + { + JLOG(ctx.j.debug()) << "Firewall: Update cannot contain both " + "sfPublicKey & sfAmount"; + return temMALFORMED; + } + + if (ctx.tx.isFieldPresent(sfSignature) && + ctx.tx.isFieldPresent(sfPublicKey)) + { + PublicKey const txPK(makeSlice(ctx.tx.getFieldVL(sfPublicKey))); + auto const sig = ctx.tx.getFieldVL(sfSignature); + PublicKey const fPK( + makeSlice(sleFirewall->getFieldVL(sfPublicKey))); + Serializer msg; + serializeFirewallAuthorization(msg, accountID, txPK); + if (!verify(fPK, msg.slice(), makeSlice(sig), /*canonical*/ true)) + { + JLOG(ctx.j.debug()) + << "Firewall: Bad Signature for update sfPublicKey"; + return temBAD_SIGNATURE; + } + } + + if (ctx.tx.isFieldPresent(sfSignature) && + ctx.tx.isFieldPresent(sfAmount)) + { + auto const amount = ctx.tx.getFieldAmount(sfAmount); + auto const sig = ctx.tx.getFieldVL(sfSignature); + PublicKey const pk(makeSlice(sleFirewall->getFieldVL(sfPublicKey))); + Serializer msg; + serializeFirewallAuthorization(msg, accountID, amount); + if (!verify(pk, msg.slice(), makeSlice(sig), /*canonical*/ true)) + { + JLOG(ctx.j.debug()) + << "Firewall: Bad Signature for update sfAmount"; + return temBAD_SIGNATURE; + } + } } + // auto const amount = ctx.tx[~sfAmount]; // if (auto const ter = requireAuth(ctx.view, amount.issue(), accountID); // ter != tesSUCCESS) // { @@ -147,37 +197,6 @@ FirewallSet::preclaim(PreclaimContext const& ctx) // return terNO_RIPPLE; // } - if (ctx.tx.isFieldPresent(sfSignature) && - ctx.tx.isFieldPresent(sfPublicKey)) - { - PublicKey const txPK(makeSlice(ctx.tx.getFieldVL(sfPublicKey))); - auto const sig = ctx.tx.getFieldVL(sfSignature); - PublicKey const fPK(makeSlice(sleFirewall->getFieldVL(sfPublicKey))); - Serializer msg; - serializeFirewallAuthorization(msg, accountID, txPK); - if (!verify(fPK, msg.slice(), makeSlice(sig), /*canonical*/ true)) - { - JLOG(ctx.j.debug()) - << "Firewall: Bad Signature for update sfPublicKey"; - return temBAD_SIGNATURE; - } - } - - if (ctx.tx.isFieldPresent(sfSignature) && ctx.tx.isFieldPresent(sfAmount)) - { - auto const amount = ctx.tx.getFieldAmount(sfAmount); - auto const sig = ctx.tx.getFieldVL(sfSignature); - PublicKey const pk(makeSlice(sleFirewall->getFieldVL(sfPublicKey))); - Serializer msg; - serializeFirewallAuthorization(msg, accountID, amount); - if (!verify(pk, msg.slice(), makeSlice(sig), /*canonical*/ true)) - { - JLOG(ctx.j.debug()) - << "Firewall: Bad Signature for update sfAmount"; - return temBAD_SIGNATURE; - } - } - return tesSUCCESS; } @@ -194,13 +213,13 @@ FirewallSet::doApply() auto sleFirewall = sb.peek(firewallKeylet); if (!sleFirewall) { - // Set Firewall auto const sleFirewall = std::make_shared(firewallKeylet); (*sleFirewall)[sfOwner] = account_; - (*sleFirewall)[~sfPublicKey] = ctx_.tx[~sfPublicKey]; - (*sleFirewall)[~sfAmount] = ctx_.tx[~sfAmount]; + sleFirewall->setFieldVL(sfPublicKey, ctx_.tx.getFieldVL(sfPublicKey)); + if (ctx_.tx.isFieldPresent(sfAmount)) + sleFirewall->setFieldAmount( + sfAmount, ctx_.tx.getFieldAmount(sfAmount)); - // Add owner directory to link the account and Firewall object. if (auto const page = sb.dirInsert( keylet::ownerDir(account_), sleFirewall->key(), @@ -216,10 +235,6 @@ FirewallSet::doApply() sb.insert(sleFirewall); adjustOwnerCount(sb, sleOwner, 1, j_); - // Add Preauth - // A preauth counts against the reserve of the issuing account, but we - // check the starting balance because we want to allow dipping into the - // reserve to pay fees. { STAmount const reserve{view().fees().accountReserve( sleOwner->getFieldU32(sfOwnerCount) + 1)}; @@ -228,8 +243,6 @@ FirewallSet::doApply() return tecINSUFFICIENT_RESERVE; } - // Preclaim already verified that the Preauth entry does not yet exist. - // Create and populate the Preauth entry. AccountID const auth{ctx_.tx[sfAuthorize]}; Keylet const preauthKeylet = keylet::firewallPreauth(account_, auth); auto slePreauth = std::make_shared(preauthKeylet); @@ -237,7 +250,6 @@ FirewallSet::doApply() slePreauth->setAccountID(sfAccount, account_); slePreauth->setAccountID(sfAuthorize, auth); - // Add owner directory to link the account and Firewall object. if (auto const page = sb.dirInsert( keylet::ownerDir(account_), slePreauth->key(), @@ -255,7 +267,6 @@ FirewallSet::doApply() } else { - // Update Firewall if (ctx_.tx.isFieldPresent(sfPublicKey)) sleFirewall->setFieldVL( sfPublicKey, ctx_.tx.getFieldVL(sfPublicKey)); From b88fe3e69769d9015123e35134368100436579da Mon Sep 17 00:00:00 2001 From: Denis Angell Date: Sun, 21 Jul 2024 15:52:13 +0200 Subject: [PATCH 5/8] add signature to preauth --- src/xrpld/app/tx/detail/FirewallPreauth.cpp | 30 +++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/xrpld/app/tx/detail/FirewallPreauth.cpp b/src/xrpld/app/tx/detail/FirewallPreauth.cpp index fd2db1240dc..31e6eb678f1 100644 --- a/src/xrpld/app/tx/detail/FirewallPreauth.cpp +++ b/src/xrpld/app/tx/detail/FirewallPreauth.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -80,6 +81,9 @@ FirewallPreauth::preflight(PreflightContext const& ctx) TER FirewallPreauth::preclaim(PreclaimContext const& ctx) { + Serializer msg; + AccountID const accountID = ctx.tx[sfAccount]; + // Determine which operation we're performing: authorizing or unauthorizing. if (ctx.tx.isFieldPresent(sfAuthorize)) { @@ -92,6 +96,8 @@ FirewallPreauth::preclaim(PreclaimContext const& ctx) // in the ledger. if (ctx.view.exists(keylet::firewallPreauth(ctx.tx[sfAccount], auth))) return tecDUPLICATE; + + serializeFirewallAuthorization(msg, accountID, auth); } else { @@ -100,6 +106,30 @@ FirewallPreauth::preclaim(PreclaimContext const& ctx) if (!ctx.view.exists( keylet::firewallPreauth(ctx.tx[sfAccount], unauth))) return tecNO_ENTRY; + + serializeFirewallAuthorization(msg, accountID, unauth); + } + + // Validate Signature + ripple::Keylet const firewallKeylet = keylet::firewall(accountID); + auto const sleFirewall = ctx.view.read(firewallKeylet); + if (!sleFirewall) + { + JLOG(ctx.j.debug()) << "FirewallPreauth: Firewall does not exist."; + return tecNO_TARGET; + } + if (!sleFirewall->isFieldPresent(sfPublicKey)) + { + JLOG(ctx.j.debug()) << "FirewallPreauth: Missing Firewall Public Key."; + return tecINTERNAL; + } + auto const sig = ctx.tx.getFieldVL(sfSignature); + PublicKey const pk(makeSlice(sleFirewall->getFieldVL(sfPublicKey))); + if (!verify(pk, msg.slice(), makeSlice(sig), /*canonical*/ true)) + { + JLOG(ctx.j.debug()) + << "FirewallPreauth: Bad Signature for update."; + return temBAD_SIGNATURE; } return tesSUCCESS; } From eb0545b65deb815b66232bdf778a110817c67b90 Mon Sep 17 00:00:00 2001 From: Denis Angell Date: Sun, 21 Jul 2024 15:53:55 +0200 Subject: [PATCH 6/8] make signature required --- src/libxrpl/protocol/TxFormats.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libxrpl/protocol/TxFormats.cpp b/src/libxrpl/protocol/TxFormats.cpp index 6b3367d8e33..748317879b5 100644 --- a/src/libxrpl/protocol/TxFormats.cpp +++ b/src/libxrpl/protocol/TxFormats.cpp @@ -521,7 +521,7 @@ TxFormats::TxFormats() { {sfAuthorize, soeOPTIONAL}, {sfUnauthorize, soeOPTIONAL}, - {sfSignature, soeOPTIONAL}, + {sfSignature, soeREQUIRED}, }, commonFields); } From 83260bd21dfbcf47bc3336154d0a56c11d060254 Mon Sep 17 00:00:00 2001 From: Denis Angell Date: Sun, 1 Dec 2024 21:18:10 +0100 Subject: [PATCH 7/8] rename tests --- ...SetFirewall_test.cpp => Firewall_test.cpp} | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) rename src/test/app/{SetFirewall_test.cpp => Firewall_test.cpp} (92%) diff --git a/src/test/app/SetFirewall_test.cpp b/src/test/app/Firewall_test.cpp similarity index 92% rename from src/test/app/SetFirewall_test.cpp rename to src/test/app/Firewall_test.cpp index 7a0e4a78fcd..cd0e9d4bd3d 100644 --- a/src/test/app/SetFirewall_test.cpp +++ b/src/test/app/Firewall_test.cpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2023 Ripple Labs Inc. + 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 @@ -25,7 +25,7 @@ namespace ripple { namespace test { -struct FirewallSet_test : public beast::unit_test::suite +struct Firewall_test : public beast::unit_test::suite { static std::size_t ownerDirCount(ReadView const& view, jtx::Account const& acct) @@ -269,6 +269,24 @@ struct FirewallSet_test : public beast::unit_test::suite env.close(); } } + + void + testTransactionTypes(FeatureBitset features) + { + testcase("transaction types"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + Account const alice = Account("alice"); + Account const bob = Account("bob"); + Account const carol = Account("carol"); + Account const dave = Account("dave"); + + // Payment + { + env(pay(alice, bob, XRP(100)), ter(tesSUCCESS)); + } + } void testWithFeats(FeatureBitset features) @@ -278,9 +296,11 @@ struct FirewallSet_test : public beast::unit_test::suite // testPreclaim(features); // testDoApply(features); testFirewallSet(features); + // testFirewallDelete(features); testUpdateAmount(features); testUpdatePK(features); testMasterDisable(features); + testTransactionTypes(features); } public: @@ -293,6 +313,6 @@ struct FirewallSet_test : public beast::unit_test::suite } }; -BEAST_DEFINE_TESTSUITE(FirewallSet, app, ripple); +BEAST_DEFINE_TESTSUITE(Firewall, app, ripple); } // namespace test } // namespace ripple From a7767364e882b2638411ebcd4698db8eb9ae7a6d Mon Sep 17 00:00:00 2001 From: Denis Angell Date: Mon, 2 Dec 2024 19:56:58 +0100 Subject: [PATCH 8/8] refactor --- include/xrpl/protocol/Firewall.h | 98 ---- include/xrpl/protocol/Indexes.h | 31 +- include/xrpl/protocol/LedgerFormats.h | 11 - include/xrpl/protocol/STTx.h | 7 +- include/xrpl/protocol/detail/features.macro | 1 + .../xrpl/protocol/detail/ledger_entries.macro | 26 + include/xrpl/protocol/detail/sfields.macro | 5 + .../xrpl/protocol/detail/transactions.macro | 22 + include/xrpl/protocol/jss.h | 3 + src/libxrpl/protocol/Indexes.cpp | 31 +- src/libxrpl/protocol/InnerObjectFormats.cpp | 8 + src/libxrpl/protocol/STTx.cpp | 161 ++++--- src/test/app/Firewall_test.cpp | 444 +++++++++++++----- src/test/jtx/firewall.h | 138 +++++- src/test/jtx/impl/firewall.cpp | 110 ++++- src/xrpld/app/tx/detail/Firewall.cpp | 174 ++++--- src/xrpld/app/tx/detail/Firewall.h | 5 +- src/xrpld/app/tx/detail/InvariantCheck.cpp | 2 +- src/xrpld/app/tx/detail/Transactor.cpp | 223 +++++++-- src/xrpld/app/tx/detail/Transactor.h | 19 +- ...irewallPreauth.cpp => WithdrawPreauth.cpp} | 90 ++-- .../{FirewallPreauth.h => WithdrawPreauth.h} | 10 +- src/xrpld/app/tx/detail/applySteps.cpp | 15 +- src/xrpld/rpc/detail/RPCHelpers.cpp | 4 +- src/xrpld/rpc/handlers/AccountObjects.cpp | 1 + 25 files changed, 1100 insertions(+), 539 deletions(-) delete mode 100644 include/xrpl/protocol/Firewall.h rename src/xrpld/app/tx/detail/{FirewallPreauth.cpp => WithdrawPreauth.cpp} (71%) rename src/xrpld/app/tx/detail/{FirewallPreauth.h => WithdrawPreauth.h} (86%) diff --git a/include/xrpl/protocol/Firewall.h b/include/xrpl/protocol/Firewall.h deleted file mode 100644 index ac08057e607..00000000000 --- a/include/xrpl/protocol/Firewall.h +++ /dev/null @@ -1,98 +0,0 @@ -//------------------------------------------------------------------------------ -/* - 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_PROTOCOL_FIREWALL_H_INCLUDED -#define RIPPLE_PROTOCOL_FIREWALL_H_INCLUDED - -#include -#include -#include -#include - -namespace ripple { - -/** - * @brief Serializes firewall authorization data into a message. - * - * This function serializes the given account and preauthorize account IDs - * into the provided Serializer object. It adds a shardInfo hash prefix, - * followed by the account and preauthorize account IDs. - * - * @param msg The Serializer object to serialize the data into. - * @param account The account ID to be serialized. - * @param preauthorize The preauthorize account ID to be serialized. - */ -inline void -serializeFirewallAuthorization( - Serializer& msg, - AccountID const& account, - AccountID const& preauthorize) -{ - msg.add32(HashPrefix::shardInfo); - msg.addBitString(account); - msg.addBitString(preauthorize); -} - -/** - * @brief Serializes firewall authorization data into a message. - * - * This function serializes the given account ID and amount into the provided - * Serializer object. It adds a shardInfo hash prefix, followed by the account - * ID and the amount's mantissa. - * - * @param msg The Serializer object to serialize the data into. - * @param account The account ID to be serialized. - * @param amount The amount to be serialized. - */ -inline void -serializeFirewallAuthorization( - Serializer& msg, - AccountID const& account, - STAmount const& amount) -{ - msg.add32(HashPrefix::shardInfo); - msg.addBitString(account); - msg.add64(amount.mantissa()); -} - -/** - * @brief Serializes firewall authorization data into a message. - * - * This function serializes the given account ID and public key into the - * provided Serializer object. It adds a shardInfo hash prefix, followed by - * the account ID and the raw bytes of the public key. - * - * @param msg The Serializer object to serialize the data into. - * @param account The account ID to be serialized. - * @param pk The public key to be serialized. - */ -inline void -serializeFirewallAuthorization( - Serializer& msg, - AccountID const& account, - PublicKey const& pk) -{ - msg.add32(HashPrefix::shardInfo); - msg.addBitString(account); - msg.addRaw(pk.slice()); -} - -} // namespace ripple - -#endif \ No newline at end of file diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 288e7eddf00..82b1b19a161 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -294,21 +294,6 @@ Keylet oracle(AccountID const& account, std::uint32_t const& documentID) noexcept; Keylet -firewall(AccountID const& account) noexcept; - -/** A FireallPreauth */ -/** @{ */ -Keylet -firewallPreauth( - AccountID const& owner, - AccountID const& preauthorized) noexcept; - -inline Keylet -firewallPreauth(uint256 const& key) noexcept -{ - return {ltFIREWALL_PREAUTH, key}; -} -/** @} */ credential( AccountID const& subject, AccountID const& issuer, @@ -344,6 +329,20 @@ mptoken(uint256 const& mptokenKey) Keylet mptoken(uint256 const& issuanceKey, AccountID const& holder) noexcept; +Keylet +firewall(AccountID const& account) noexcept; + +Keylet +withdrawPreauth( + AccountID const& owner, + AccountID const& preauthorized) noexcept; + +inline Keylet +withdrawPreauth(uint256 const& key) noexcept +{ + return {ltWITHDRAW_PREAUTH, key}; +} + } // namespace keylet // Everything below is deprecated and should be removed in favor of keylets: @@ -389,4 +388,4 @@ makeMptID(std::uint32_t sequence, AccountID const& account); } // namespace ripple -#endif +#endif \ No newline at end of file diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 9fc886fd43a..710370a68f2 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -62,17 +62,6 @@ enum LedgerEntryType : std::uint16_t #undef LEDGER_ENTRY #pragma pop_macro("LEDGER_ENTRY") - - /** A ledger object which tracks FIrewall - \sa keylet::firewall - */ - ltFIREWALL = 0x0046, - - /** A ledger object which tracks FIrewall - \sa keylet::firewall - */ - ltFIREWALL_PREAUTH = 0x0047, - //--------------------------------------------------------------------------- /** A special type, matching any ledger entry type. diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h index 08b9a1bad10..0c148c736ee 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -124,6 +124,10 @@ class STTx final : public STObject, public CountedObject checkSign(RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules) const; + Expected + checkFirewallSign(RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules) + const; + // SQL Functions with metadata. static std::string const& getMetaSQLInsertReplaceHeader(); @@ -141,10 +145,11 @@ class STTx final : public STObject, public CountedObject private: Expected - checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const; + checkSingleSign(STObject const& obj, RequireFullyCanonicalSig requireCanonicalSig) const; Expected checkMultiSign( + STObject const& obj, RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules) const; diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 31fc90cef80..10355eb08e2 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(Firewall, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(Credentials, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(AMMClawback, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (AMMv1_2, Supported::yes, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 0cb1ec3416a..e9d76b7791b 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -436,3 +436,29 @@ LEDGER_ENTRY(ltCREDENTIAL, 0x0081, Credential, ({ {sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnLgrSeq, soeREQUIRED}, })) + +/** A ledger object which tracks Firewall + \sa keylet::firewall + */ +LEDGER_ENTRY(ltFIREWALL, 0x0046, Firewall, ({ + {sfOwner, soeREQUIRED}, + {sfIssuer, soeREQUIRED}, + {sfAmount, soeOPTIONAL}, + {sfTimePeriod, soeOPTIONAL}, + {sfTimePeriodStart, soeOPTIONAL}, + {sfTotalOut, soeOPTIONAL}, + {sfOwnerNode, soeREQUIRED}, + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED} +})) + +/** A ledger object which tracks WithdrawPreauth + \sa keylet::WithdrawPreauth + */ +LEDGER_ENTRY(ltWITHDRAW_PREAUTH, 0x0047, WithdrawPreauth, ({ + {sfAccount, soeREQUIRED}, + {sfAuthorize, soeREQUIRED}, + {sfOwnerNode, soeREQUIRED}, + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED}, +})) diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 8384025ee3b..08e4553c9b9 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -112,6 +112,8 @@ TYPED_SFIELD(sfEmitGeneration, UINT32, 46) TYPED_SFIELD(sfVoteWeight, UINT32, 48) TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50) TYPED_SFIELD(sfOracleDocumentID, UINT32, 51) +TYPED_SFIELD(sfTimePeriod, UINT32, 52) +TYPED_SFIELD(sfTimePeriodStart, UINT32, 53) // 64-bit integers (common) TYPED_SFIELD(sfIndexNext, UINT64, 1) @@ -230,6 +232,7 @@ TYPED_SFIELD(sfPrice, AMOUNT, 28) TYPED_SFIELD(sfSignatureReward, AMOUNT, 29) TYPED_SFIELD(sfMinAccountCreateAmount, AMOUNT, 30) TYPED_SFIELD(sfLPTokenBalance, AMOUNT, 31) +TYPED_SFIELD(sfTotalOut, AMOUNT, 32) // variable length (common) TYPED_SFIELD(sfPublicKey, VL, 1) @@ -346,6 +349,7 @@ UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement, OBJECT, 30) UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, OBJECT, 31) UNTYPED_SFIELD(sfPriceData, OBJECT, 32) UNTYPED_SFIELD(sfCredential, OBJECT, 33) +UNTYPED_SFIELD(sfFirewallSigner, OBJECT, 34) // array of objects (common) // ARRAY/1 is reserved for end of array @@ -375,3 +379,4 @@ UNTYPED_SFIELD(sfPriceDataSeries, ARRAY, 24) UNTYPED_SFIELD(sfAuthAccounts, ARRAY, 25) UNTYPED_SFIELD(sfAuthorizeCredentials, ARRAY, 26) UNTYPED_SFIELD(sfUnauthorizeCredentials, ARRAY, 27) +UNTYPED_SFIELD(sfFirewallSigners, ARRAY, 28, SField::sMD_Default, SField::notSigning) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 4f4c8f12595..52876f0966c 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -447,6 +447,28 @@ TRANSACTION(ttCREDENTIAL_DELETE, 60, CredentialDelete, ({ {sfCredentialType, soeREQUIRED}, })) +/** This transaction type creates an WithdrawPreauth instance */ +TRANSACTION(ttWITHDRAW_PREAUTH, 61, WithdrawPreauth, ({ + {sfAuthorize, soeOPTIONAL}, + {sfUnauthorize, soeOPTIONAL}, + {sfPublicKey, soeREQUIRED}, + {sfSignature, soeREQUIRED}, +})) + +/** This transaction type creates an Firewall instance */ +TRANSACTION(ttFIREWALL_SET, 62, FirewallSet, ({ + {sfIssuer, soeOPTIONAL}, + {sfAuthorize, soeOPTIONAL}, + {sfAmount, soeOPTIONAL}, + {sfTimePeriod, soeOPTIONAL}, + {sfFirewallSigners, soeOPTIONAL}, +})) + +// /** This transaction type deletes an Firewall instance */ +// TRANSACTION(ttFIREWALL_DELETE, 63, FirewallDelete, ({ +// {sfSignature, soeREQUIRED}, +// })) + /** This system-generated transaction type is used to update the status of the various amendments. diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index f37dadd0787..4f9c0152396 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -73,6 +73,7 @@ JSS(EPrice); // in: AMM Deposit option JSS(Escrow); // ledger type. JSS(Fee); // in/out: TransactionSign; field. JSS(FeeSettings); // ledger type. +JSS(Firewall); // ledger type. JSS(Flags); // in/out: TransactionSign; field. JSS(Holder); // field. JSS(Invalid); // @@ -307,6 +308,8 @@ JSS(fee_level); // out: AccountInfo JSS(fee_mult_max); // in: TransactionSign JSS(fee_ref); // out: NetworkOPs, DEPRECATED JSS(fetch_pack); // out: NetworkOPs +JSS(firewall); // in: LedgerEntry +JSS(withdraw_preauth); // in: LedgerEntry JSS(FIELDS); // out: RPC server_definitions // matches definitions.json format JSS(first); // out: rpc/Version diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 3f5395b4e80..c600b9a6372 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -74,11 +74,11 @@ enum class LedgerNameSpace : std::uint16_t { XCHAIN_CREATE_ACCOUNT_CLAIM_ID = 'K', DID = 'I', ORACLE = 'R', - FIREWALL = 'F', - FIREWALL_PREAUTH = 'G', MPTOKEN_ISSUANCE = '~', MPTOKEN = 't', CREDENTIAL = 'D', + FIREWALL = 'F', + WITHDRAW_PREAUTH = 'G', // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -484,18 +484,6 @@ oracle(AccountID const& account, std::uint32_t const& documentID) noexcept } Keylet -firewall(AccountID const& account) noexcept -{ - return {ltFIREWALL, indexHash(LedgerNameSpace::FIREWALL, account)}; -} - -Keylet -firewallPreauth(AccountID const& owner, AccountID const& preauthorized) noexcept -{ - return { - ltFIREWALL_PREAUTH, - indexHash(LedgerNameSpace::FIREWALL_PREAUTH, owner, preauthorized)}; - mptIssuance(std::uint32_t seq, AccountID const& issuer) noexcept { return mptIssuance(makeMptID(seq, issuer)); @@ -531,9 +519,22 @@ credential( return { ltCREDENTIAL, indexHash(LedgerNameSpace::CREDENTIAL, subject, issuer, credType)}; +} + +Keylet +firewall(AccountID const& account) noexcept +{ + return {ltFIREWALL, indexHash(LedgerNameSpace::FIREWALL, account)}; +} +Keylet +withdrawPreauth(AccountID const& owner, AccountID const& preauthorized) noexcept +{ + return { + ltWITHDRAW_PREAUTH, + indexHash(LedgerNameSpace::WITHDRAW_PREAUTH, owner, preauthorized)}; } } // namespace keylet -} // namespace ripple +} // namespace ripple \ No newline at end of file diff --git a/src/libxrpl/protocol/InnerObjectFormats.cpp b/src/libxrpl/protocol/InnerObjectFormats.cpp index 87c42a8085f..69a81132ab4 100644 --- a/src/libxrpl/protocol/InnerObjectFormats.cpp +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp @@ -154,6 +154,14 @@ InnerObjectFormats::InnerObjectFormats() {sfIssuer, soeREQUIRED}, {sfCredentialType, soeREQUIRED}, }); + + add(sfFirewallSigner.jsonName.c_str(), + sfFirewallSigner.getCode(), + { + {sfAccount, soeREQUIRED}, + {sfSigningPubKey, soeREQUIRED}, + {sfTxnSignature, soeREQUIRED}, + }); } InnerObjectFormats const& diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 7bd25246c53..c8dde3ade94 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -218,8 +219,35 @@ STTx::checkSign( // multi-signing. Otherwise we're single-signing. Blob const& signingPubKey = getFieldVL(sfSigningPubKey); return signingPubKey.empty() - ? checkMultiSign(requireCanonicalSig, rules) - : checkSingleSign(requireCanonicalSig); + ? checkMultiSign(*this, requireCanonicalSig, rules) + : checkSingleSign(*this, requireCanonicalSig); + } + catch (std::exception const&) + { + } + return Unexpected("Internal signature check failure."); +} + +Expected +STTx::checkFirewallSign( + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const +{ + try + { + STArray const& signers{getFieldArray(sfFirewallSigners)}; + for (auto const& signer : signers) + { + Blob const& signingPubKey = signer.getFieldVL(sfSigningPubKey); + auto const result = checkSingleSign(signer, requireCanonicalSig); + // auto const result = signingPubKey.empty() + // ? checkMultiSign(signer, requireCanonicalSig, rules) + // : checkSingleSign(signer, requireCanonicalSig); + + if (!result) + return result; + } + return {}; } catch (std::exception const&) { @@ -306,80 +334,59 @@ 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(STObject const& signer, 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); - - // Determine whether signatures must be full canonical. - bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || - (requireCanonicalSig == RequireFullyCanonicalSig::yes); + auto const data = getSigningData(*this); + return singleSignHelper( + signer, makeSlice(data), 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); @@ -406,23 +413,21 @@ 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); } } - catch (std::exception const&) + catch (std::exception const& e) { // We assume any problem lies with the signature. validSig = false; @@ -436,6 +441,48 @@ STTx::checkMultiSign( return {}; } +Expected +STTx::checkMultiSign( + STObject const& obj, + 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 (!obj.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 (obj.isFieldPresent(sfTxnSignature)) + return Unexpected("Cannot both single- and multi-sign."); + + STArray const& signers{obj.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 = obj.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 @@ -615,4 +662,4 @@ isPseudoTx(STObject const& tx) return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY; } -} // namespace ripple +} // namespace ripple \ No newline at end of file diff --git a/src/test/app/Firewall_test.cpp b/src/test/app/Firewall_test.cpp index cd0e9d4bd3d..4d617cb76ec 100644 --- a/src/test/app/Firewall_test.cpp +++ b/src/test/app/Firewall_test.cpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2024 Ripple Labs Inc. + Copyright (c) 2024 Transia, LLC. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -18,9 +18,8 @@ //============================================================================== #include -#include +#include #include -#include #include namespace ripple { @@ -34,30 +33,6 @@ struct Firewall_test : public beast::unit_test::suite return std::distance(ownerDir.begin(), ownerDir.end()); }; - static Buffer - sigFirewallAuthAmount( - PublicKey const& pk, - SecretKey const& sk, - AccountID const& account, - STAmount const& amount) - { - Serializer msg; - serializeFirewallAuthorization(msg, account, amount); - return sign(pk, sk, msg.slice()); - } - - static Buffer - sigFirewallAuthPK( - PublicKey const& pk, - SecretKey const& sk, - AccountID const& account, - PublicKey const& _pk) - { - Serializer msg; - serializeFirewallAuthorization(msg, account, _pk); - return sign(pk, sk, msg.slice()); - } - static std::pair> firewallKeyAndSle(ReadView const& view, jtx::Account const& account) { @@ -66,16 +41,38 @@ struct Firewall_test : public beast::unit_test::suite } void - verifyFirewall( + verifyFirewallSle( ReadView const& view, jtx::Account const& account, - STAmount const& amount, - PublicKey const& pk) + jtx::Account const& issuer, + std::optional const& amount = std::nullopt, + std::optional const& timePeriod = std::nullopt, + std::optional const& timeStart = std::nullopt, + std::optional const& totalOut = std::nullopt) { auto [key, sle] = firewallKeyAndSle(view, account); BEAST_EXPECT((*sle)[sfOwner] == account.id()); - BEAST_EXPECT((*sle)[sfAmount] == amount); - BEAST_EXPECT(strHex((*sle)[sfPublicKey]) == strHex(pk.slice())); + BEAST_EXPECT((*sle)[sfIssuer] == issuer.id()); + if (amount) + { + std::cout << "amount: " << *amount << std::endl; + BEAST_EXPECT((*sle)[sfAmount] == *amount); + } + if (timePeriod) + { + std::cout << "timePeriod: " << *timePeriod << std::endl; + BEAST_EXPECT((*sle)[sfTimePeriod] == *timePeriod); + } + if (timeStart) + { + std::cout << "timeStart: " << *timeStart << std::endl; + BEAST_EXPECT((*sle)[sfTimePeriodStart] == *timeStart); + } + if (totalOut) + { + std::cout << "totalOut: " << *totalOut << std::endl; + BEAST_EXPECT((*sle)[sfTotalOut] == *totalOut); + } } void @@ -103,9 +100,11 @@ struct Firewall_test : public beast::unit_test::suite withFirewall ? ter(tesSUCCESS) : ter(temDISABLED); auto const dirCount = withFirewall ? 2 : 0; - env(firewall::set(alice), + auto const seq = env.seq(alice); + auto const fee = env.current()->fees().base; + env(firewall::set(alice, seq, fee), firewall::auth(bob), - firewall::pk(carol.pk()), + firewall::issuer(carol), txResult); env.close(); BEAST_EXPECT(ownerDirCount(*env.current(), alice) == dirCount); @@ -113,9 +112,9 @@ struct Firewall_test : public beast::unit_test::suite } void - testFirewallSet(FeatureBitset features) + testPreflight(FeatureBitset features) { - testcase("firewall set"); + testcase("preflight"); using namespace jtx; using namespace std::literals::chrono_literals; @@ -123,29 +122,17 @@ struct Firewall_test : public beast::unit_test::suite Account const bob = Account("bob"); Account const carol = Account("carol"); - { - Env env{*this, features}; - env.fund(XRP(1000), alice, bob, carol); - env.close(); - - env(firewall::set(alice), - firewall::auth(carol), - firewall::amt(XRP(10)), - firewall::pk(carol.pk()), - ter(tesSUCCESS)); - env.close(); - - verifyFirewall(*env.current(), alice, XRP(10), carol.pk()); + // preflight + // --------------------------------------------------------- - env(pay(alice, bob, XRP(100)), ter(tecFIREWALL_BLOCK)); - env.close(); - } + // temINVALID_ACCOUNT_ID + // temCANNOT_PREAUTH_SELF } void - testUpdateAmount(FeatureBitset features) + testPreclaim(FeatureBitset features) { - testcase("update amount"); + testcase("preclaim"); using namespace jtx; using namespace std::literals::chrono_literals; @@ -153,96 +140,128 @@ struct Firewall_test : public beast::unit_test::suite Account const bob = Account("bob"); Account const carol = Account("carol"); - { - Env env{*this, features}; - env.fund(XRP(1000), alice, bob, carol); - env.close(); + // preclaim + // --------------------------------------------------------- - env(firewall::set(alice), - firewall::auth(carol), - firewall::amt(XRP(10)), - firewall::pk(carol.pk()), - ter(tesSUCCESS)); - env.close(); + // Set - Create + // temMALFORMED: Firewall: Set must not contain a sfSignature + // temMALFORMED: Firewall: Set must contain a sfAuthorize + // temMALFORMED: Firewall: Set must contain a sfPublicKey - verifyFirewall(*env.current(), alice, XRP(10), carol.pk()); + // Set - Update + // temMALFORMED: Firewall: Update must contain a sfSignature + // temMALFORMED: Firewall: Update cannot contain a sfAuthorize + // temMALFORMED: Firewall: Update cannot contain both sfPublicKey & sfAmount + // temBAD_SIGNATURE: Firewall: Bad Signature for update sfPublicKey + // temBAD_SIGNATURE: Firewall: Bad Signature for update sfAmount + } - env(pay(alice, bob, XRP(100)), ter(tecFIREWALL_BLOCK)); - env.close(); + void + testDoApply(FeatureBitset features) + { + testcase("doApply"); + using namespace jtx; + using namespace std::literals::chrono_literals; - auto const sig = sigFirewallAuthAmount( - carol.pk(), carol.sk(), alice.id(), XRP(100)); - env(firewall::set(alice), - firewall::amt(XRP(100)), - firewall::sig(sig), - ter(tesSUCCESS)); - env.close(); + Account const alice = Account("alice"); + Account const bob = Account("bob"); + Account const carol = Account("carol"); - verifyFirewall(*env.current(), alice, XRP(100), carol.pk()); + // doApply + // --------------------------------------------------------- + + // All + // tefINTERNAL: Firewall: Owner account not found + + // Set - Create + // tecDIR_FULL: Firewall: failed to insert owner dir + // tecINSUFFICIENT_RESERVE: Firewall: Insufficient reserve to set firewall + // tecDIR_FULL: Firewall: failed to insert owner dir + + // Set - Update - env(pay(alice, bob, XRP(100)), ter(tesSUCCESS)); - env.close(); - } } void - testUpdatePK(FeatureBitset features) + testFirewallSet(FeatureBitset features) { - testcase("update pk"); + testcase("firewall set"); using namespace jtx; using namespace std::literals::chrono_literals; Account const alice = Account("alice"); Account const bob = Account("bob"); Account const carol = Account("carol"); - Account const dave = Account("dave"); + // No Amount { Env env{*this, features}; - env.fund(XRP(1000), alice, bob, carol, dave); + env.fund(XRP(1000), alice, bob, carol); env.close(); - env(firewall::set(alice), - firewall::auth(carol), - firewall::amt(XRP(10)), - firewall::pk(carol.pk()), + auto const seq = env.seq(alice); + auto const fee = env.current()->fees().base; + env(firewall::set(alice, seq, fee), + firewall::auth(bob), + firewall::issuer(carol), ter(tesSUCCESS)); env.close(); + verifyFirewallSle(*env.current(), alice, carol); + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 2); + } - verifyFirewall(*env.current(), alice, XRP(10), carol.pk()); - - env(pay(alice, bob, XRP(100)), ter(tecFIREWALL_BLOCK)); + // Amount w/out Time Period + { + Env env{*this, features}; + env.fund(XRP(1000), alice, bob, carol); env.close(); - auto const sig1 = sigFirewallAuthPK( - carol.pk(), carol.sk(), alice.id(), dave.pk()); - env(firewall::set(alice), - firewall::pk(dave.pk()), - firewall::sig(sig1), + auto const seq = env.seq(alice); + auto const fee = env.current()->fees().base; + env(firewall::set(alice, seq, fee), + firewall::auth(bob), + firewall::amt(XRP(10)), + firewall::issuer(carol), ter(tesSUCCESS)); env.close(); - verifyFirewall(*env.current(), alice, XRP(10), dave.pk()); + verifyFirewallSle(*env.current(), alice, carol, XRP(10)); + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 2); + } - auto const sig2 = sigFirewallAuthAmount( - dave.pk(), dave.sk(), alice.id(), XRP(100)); - env(firewall::set(alice), - firewall::amt(XRP(100)), - firewall::sig(sig2), - ter(tesSUCCESS)); + // Amount w/ Time Period + { + Env env{*this, features}; + env.fund(XRP(1000), alice, bob, carol); env.close(); - verifyFirewall(*env.current(), alice, XRP(100), dave.pk()); - - env(pay(alice, bob, XRP(100)), ter(tesSUCCESS)); + auto const timeStart = env.now(); + auto const seq = env.seq(alice); + auto const fee = env.current()->fees().base; + env(firewall::set(alice, seq, fee), + firewall::auth(bob), + firewall::amt(XRP(10)), + firewall::time_period(3600), + firewall::issuer(carol), + ter(tesSUCCESS)); env.close(); + + verifyFirewallSle( + *env.current(), + alice, + carol, + XRP(10), + 3600, + timeStart.time_since_epoch().count(), + STAmount(0)); + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 2); } } void - testMasterDisable(FeatureBitset features) + testFirewallBlock(FeatureBitset features) { - testcase("master disable"); + testcase("firewall block"); using namespace jtx; using namespace std::literals::chrono_literals; @@ -256,24 +275,42 @@ struct Firewall_test : public beast::unit_test::suite env.fund(XRP(1000), alice, bob, carol, dave); env.close(); - env(firewall::set(alice), - firewall::auth(carol), + auto const seq = env.seq(alice); + auto const fee = env.current()->fees().base; + env(firewall::set(alice, seq, fee), + firewall::auth(bob), firewall::amt(XRP(10)), - firewall::pk(carol.pk()), + firewall::issuer(carol), ter(tesSUCCESS)); env.close(); - verifyFirewall(*env.current(), alice, XRP(10), carol.pk()); + { + 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 << "\n"; + } - env(fset(alice, asfDisableMaster), ter(tecNO_PERMISSION)); + env(pay(alice, dave, XRP(100)), ter(tecFIREWALL_BLOCK)); env.close(); + + { + 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 << "\n"; + } } } - + void - testTransactionTypes(FeatureBitset features) + testFirewallSetUpdate(FeatureBitset features) { - testcase("transaction types"); + testcase("set update"); using namespace jtx; using namespace std::literals::chrono_literals; @@ -281,26 +318,189 @@ struct Firewall_test : public beast::unit_test::suite Account const bob = Account("bob"); Account const carol = Account("carol"); Account const dave = Account("dave"); + Account const elsa = Account("elsa"); + + // Update Amount w/out time limit + { + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(1000), alice, bob, carol, dave); + env.close(); + + env(firewall::set(alice, env.seq(alice), baseFee), + firewall::auth(bob), + firewall::amt(XRP(10)), + firewall::issuer(carol), + ter(tesSUCCESS)); + env.close(); + + env(pay(alice, dave, XRP(100)), ter(tecFIREWALL_BLOCK)); + env.close(); + + env(firewall::set(alice, env.seq(alice), baseFee), + firewall::amt(XRP(101)), + firewall::sig(carol), + ter(tesSUCCESS)); + env.close(); - // Payment + verifyFirewallSle(*env.current(), alice, carol, XRP(101)); + env(pay(alice, dave, XRP(100)), ter(tesSUCCESS)); + env.close(); + } + + // Update Amount w/ time limit { - env(pay(alice, bob, XRP(100)), ter(tesSUCCESS)); + Env env{*this, features}; + auto const baseFee = env.current()->fees().base; + env.fund(XRP(1000), alice, bob, carol, dave); + env.close(); + + env(firewall::set(alice, env.seq(alice), baseFee), + firewall::auth(bob), + firewall::amt(XRP(10)), + firewall::time_period(300), + firewall::issuer(carol), + ter(tesSUCCESS)); + env.close(); + + verifyFirewallSle( + *env.current(), + alice, + carol, + XRP(10), + 300, + env.now().time_since_epoch().count(), + STAmount(0)); + + env(pay(alice, dave, XRP(100)), ter(tecFIREWALL_BLOCK)); + env.close(); + + env(firewall::set(alice, env.seq(alice), baseFee), + firewall::amt(XRP(101)), + firewall::time_period(3600), + firewall::sig(carol), + ter(tesSUCCESS)); + env.close(); + + verifyFirewallSle( + *env.current(), + alice, + carol, + XRP(101), + 3600, + env.now().time_since_epoch().count(), + STAmount(0)); + + env(pay(alice, dave, XRP(100)), ter(tesSUCCESS)); + env.close(); } + + // // Update Issuer + // { + // Env env{*this, features}; + // auto const baseFee = env.current()->fees().base; + // env.fund(XRP(1000), alice, bob, carol, dave); + // env.close(); + + // env(firewall::set(alice, env.seq(alice), baseFee), + // firewall::auth(bob), + // firewall::issuer(carol), + // ter(tesSUCCESS)); + // env.close(); + + // verifyFirewallSle( + // *env.current(), + // alice, + // carol); + + // env(pay(alice, bob, XRP(100)), ter(tecFIREWALL_BLOCK)); + // env.close(); + + // env(firewall::set(alice, env.seq(alice), baseFee), + // firewall::issuer(dave), + // firewall::sig(carol), + // ter(tesSUCCESS)); + // env.close(); + + // verifyFirewallSle( + // *env.current(), + // alice, + // dave); + + // env(pay(alice, bob, XRP(100)), ter(tesSUCCESS)); + // env.close(); + // } } + // void + // testMasterDisable(FeatureBitset features) + // { + // testcase("master disable"); + // using namespace jtx; + // using namespace std::literals::chrono_literals; + + // Account const alice = Account("alice"); + // Account const bob = Account("bob"); + // Account const carol = Account("carol"); + // Account const dave = Account("dave"); + + // { + // Env env{*this, features}; + // env.fund(XRP(1000), alice, bob, carol, dave); + // env.close(); + + // env(firewall::set(alice), + // firewall::auth(carol), + // firewall::amt(XRP(10)), + // firewall::issuer(carol), + // ter(tesSUCCESS)); + // env.close(); + + // // verifyFirewall(*env.current(), alice, XRP(10), carol.pk()); + + // env(fset(alice, asfDisableMaster), ter(tecNO_PERMISSION)); + // env.close(); + // } + // } + + // void + // testTransactionTypes(FeatureBitset features) + // { + // testcase("transaction types"); + // using namespace jtx; + // using namespace std::literals::chrono_literals; + + // Account const alice = Account("alice"); + // Account const bob = Account("bob"); + // Account const carol = Account("carol"); + // Account const dave = Account("dave"); + + // // Payment + // { + // env(pay(alice, bob, XRP(100)), ter(tesSUCCESS)); + // } + // } + void testWithFeats(FeatureBitset features) { - testEnabled(features); + // testEnabled(features); // testPreflight(features); // testPreclaim(features); // testDoApply(features); - testFirewallSet(features); + // testFirewallSet(features); + // testFirewallBlock(features); // testFirewallDelete(features); - testUpdateAmount(features); - testUpdatePK(features); - testMasterDisable(features); - testTransactionTypes(features); + testFirewallSetUpdate(features); + // testUpdatePK(features); + // testMasterDisable(features); + // testTransactionTypes(features); + + // // Bad Amount + // { + // env(pay(alice, bob, XRP(100)), ter(tecFIREWALL_BLOCK)); + // env.close(); + // } } public: diff --git a/src/test/jtx/firewall.h b/src/test/jtx/firewall.h index 182b28353fd..5ee1fadd03a 100644 --- a/src/test/jtx/firewall.h +++ b/src/test/jtx/firewall.h @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012, 2013 Ripple Labs Inc. + Copyright (c) 2024 Transia, LLC. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -32,7 +32,22 @@ namespace firewall { /** Set/Update a firewall. */ Json::Value -set(Account const& account); +set(Account const& account, std::uint32_t const& seq, STAmount const& fee); + +/** Sets the optional TimePeriod on a JTx. */ +class time_period +{ +private: + std::uint32_t value_; + +public: + explicit time_period(std::uint32_t const& value) : value_(value) + { + } + + void + operator()(Env&, JTx& jtx) const; +}; /** Sets the optional Amount on a JTx. */ class amt @@ -49,6 +64,21 @@ class amt operator()(Env&, JTx& jtx) const; }; +/** Sets the optional Issuer on a JTx. */ +class issuer +{ +private: + jtx::Account issuer_; + +public: + explicit issuer(jtx::Account const& issuer) : issuer_(issuer) + { + } + + void + operator()(Env&, JTx& jtx) const; +}; + /** Sets the optional Authorize on a JTx. */ class auth { @@ -64,34 +94,114 @@ class auth operator()(Env&, JTx& jtx) const; }; -/** Sets the optional PublicKey on a JTx. */ -class pk +/** Set a firewall signature on a JTx. */ +class sig { -private: - PublicKey pk_; +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: - explicit pk(PublicKey const& pk) : pk_(pk) + sig(std::vector signers_); + + template + requires std::convertible_to + explicit sig(AccountType&& a0, Accounts&&... aN) + : sig{std::vector{ + std::forward(a0), + std::forward(aN)...}} { } void - operator()(Env&, JTx& jtx) const; + operator()(Env&, JTx& jt) const; }; -/** Set the optional Signature on a JTx */ -class sig +/** Set a firewall multi signature on a JTx. */ +class msig { -private: - Buffer sig_; +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; + } + }; + + Account master; // Add a member to hold the master account + std::vector signers; public: - explicit sig(Buffer const& sig) : sig_(sig) + msig(Account const& masterAccount, std::vector signers_); + + template + requires std::convertible_to + explicit msig( + Account const& masterAccount, + AccountType&& a0, + Accounts&&... aN) + : master(masterAccount) + , // Initialize master account + signers{std::vector{ + std::forward(a0), + std::forward(aN)...}} { } void - operator()(Env&, JTx& jtx) const; + operator()(Env&, JTx& jt) const; }; } // namespace firewall diff --git a/src/test/jtx/impl/firewall.cpp b/src/test/jtx/impl/firewall.cpp index a36d14867d4..87d44639a52 100644 --- a/src/test/jtx/impl/firewall.cpp +++ b/src/test/jtx/impl/firewall.cpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012, 2013 Ripple Labs Inc. + Copyright (c) 2024 Transia, LLC. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -18,7 +18,14 @@ //============================================================================== #include +#include +// #include +#include +#include +#include #include +#include +#include namespace ripple { namespace test { @@ -26,36 +33,127 @@ namespace jtx { namespace firewall { Json::Value -set(Account const& account) +set(Account const& account, std::uint32_t const& seq, STAmount const& fee) { Json::Value jv; jv[jss::Account] = account.human(); jv[jss::TransactionType] = jss::FirewallSet; + jv[jss::Sequence] = seq; + jv[jss::Fee] = fee.getJson(JsonOptions::none); + jv[jss::SigningPubKey] = strHex(account.pk().slice()); return jv; } +void +time_period::operator()(Env& env, JTx& jt) const +{ + jt.jv[sfTimePeriod.jsonName] = value_; +} + void amt::operator()(Env& env, JTx& jt) const { jt.jv[sfAmount.jsonName] = amt_.getJson(JsonOptions::none); } +void +issuer::operator()(Env& env, JTx& jt) const +{ + jt.jv[sfIssuer.jsonName] = issuer_.human(); +} + void auth::operator()(Env& env, JTx& jt) const { jt.jv[sfAuthorize.jsonName] = auth_.human(); } -void -pk::operator()(Env& env, JTx& jt) const +sig::sig(std::vector signers_) : signers(std::move(signers_)) { - jt.jv[sfPublicKey.jsonName] = strHex(pk_.slice()); + // Signatures must be applied in sorted order. + std::sort( + signers.begin(), + signers.end(), + [](sig::Reg const& lhs, sig::Reg const& rhs) { + return lhs.acct.id() < rhs.acct.id(); + }); } void sig::operator()(Env& env, JTx& jt) const { - jt.jv[sfSignature.jsonName] = strHex(Slice(sig_)); + std::optional st; + try + { + st = parse(jt.jv); + } + catch (parse_error const&) + { + env.test.log << pretty(jt.jv) << std::endl; + Rethrow(); + } + auto const mySigners = signers; + auto& js = jt[sfFirewallSigners.getJsonName()]; + for (std::size_t i = 0; i < mySigners.size(); ++i) + { + auto const& e = mySigners[i]; + auto& jo = js[i][sfFirewallSigner.getJsonName()]; + jo[jss::Account] = e.acct.human(); + jo[jss::SigningPubKey] = strHex(e.sig.pk().slice()); + + Serializer ss; + ss.add32(HashPrefix::txSign); + st->addWithoutSigningFields(ss); + auto const sig = ripple::sign(*publicKeyType(e.sig.pk().slice()), e.sig.sk(), ss.slice()); + jo[jss::TxnSignature] = strHex(Slice{sig.data(), sig.size()}); + } +} + +msig::msig(Account const& masterAccount, std::vector signers_) + : master(masterAccount), signers(std::move(signers_)) +{ + 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 +{ + 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[sfFirewallSigners.getJsonName()]; + auto const index = jt[sfFirewallSigners.jsonName].size(); + auto& bso = bs[index][sfFirewallSigner.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->getFieldV256(sfTxIDs)); + // 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 firewall diff --git a/src/xrpld/app/tx/detail/Firewall.cpp b/src/xrpld/app/tx/detail/Firewall.cpp index d18591e7507..281e572346d 100644 --- a/src/xrpld/app/tx/detail/Firewall.cpp +++ b/src/xrpld/app/tx/detail/Firewall.cpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012, 2013 Ripple Labs Inc. + Copyright (c) 2024 Transia, LLC. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -22,7 +22,6 @@ #include #include #include -#include #include #include #include @@ -30,6 +29,18 @@ namespace ripple { +XRPAmount +FirewallSet::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + // Calculate the FirewallSigners Fees + // std::int32_t signerCount = tx.isFieldPresent(sfFirewallSigners) + // ? tx.getFieldArray(sfFirewallSigners).size() + // : 0; + + // return ((signerCount + 2) * view.fees().base); + return view.fees().base; +} + NotTEC FirewallSet::preflight(PreflightContext const& ctx) { @@ -44,22 +55,22 @@ FirewallSet::preflight(PreflightContext const& ctx) // if (amount.issue() == amount2.issue()) // { // JLOG(ctx.j.debug()) - // << "Firewall: tokens can not have the same currency/issuer."; + // << "FirewallSet: tokens can not have the same currency/issuer."; // return temBAD_AMM_TOKENS; // } // if (auto const err = invalidAmount(amount)) // { - // JLOG(ctx.j.debug()) << "Firewall: invalid asset1 amount."; + // JLOG(ctx.j.debug()) << "FirewallSet: invalid asset1 amount."; // return err; // } // Validate Authorize if (ctx.tx.isFieldPresent(sfAuthorize)) { - auto const authorizeID = ctx.tx.getAccountID(sfAuthorize); + auto const backupID = ctx.tx.getAccountID(sfAuthorize); // Make sure that the passed account is valid. - if (authorizeID == beast::zero) + if (backupID == beast::zero) { JLOG(ctx.j.debug()) << "Malformed transaction: Authorized or Unauthorized " @@ -68,10 +79,10 @@ FirewallSet::preflight(PreflightContext const& ctx) } // An account may not preauthorize itself. - if (authorizeID == ctx.tx[sfAccount]) + if (backupID == ctx.tx[sfAccount]) { JLOG(ctx.j.debug()) - << "Malformed transaction: Attempting to FirewallPreauth self."; + << "Malformed transaction: Attempting to WithdrawPreauth self."; return temCANNOT_PREAUTH_SELF; } } @@ -88,114 +99,85 @@ FirewallSet::preclaim(PreclaimContext const& ctx) if (!sleFirewall) { - if (ctx.tx.isFieldPresent(sfSignature)) + if (ctx.tx.isFieldPresent(sfFirewallSigners)) { JLOG(ctx.j.debug()) - << "Firewall: Set must not contain a sfSignature"; + << "FirewallSet: Set must not contain a sfFirewallSigners"; return temMALFORMED; } if (!ctx.tx.isFieldPresent(sfAuthorize)) { - JLOG(ctx.j.debug()) << "Firewall: Set must contain a sfAuthorize"; + JLOG(ctx.j.debug()) << "FirewallSet: Set must contain a sfAuthorize"; return temMALFORMED; } - if (!ctx.tx.isFieldPresent(sfPublicKey)) + if (!ctx.tx.isFieldPresent(sfIssuer)) { - JLOG(ctx.j.debug()) << "Firewall: Set must contain a sfPublicKey"; + JLOG(ctx.j.debug()) << "FirewallSet: Set must contain a sfIssuer"; return temMALFORMED; } } else { - if (!ctx.tx.isFieldPresent(sfSignature)) - { - JLOG(ctx.j.debug()) - << "Firewall: Update must contain a sfSignature"; - return temMALFORMED; - } - if (ctx.tx.isFieldPresent(sfAuthorize)) { JLOG(ctx.j.debug()) - << "Firewall: Update cannot contain a sfAuthorize"; + << "FirewallSet: Update cannot contain a sfAuthorize"; return temMALFORMED; } - if (ctx.tx.isFieldPresent(sfPublicKey) && - ctx.tx.isFieldPresent(sfAmount)) + if (!ctx.tx.isFieldPresent(sfFirewallSigners)) { - JLOG(ctx.j.debug()) << "Firewall: Update cannot contain both " - "sfPublicKey & sfAmount"; + JLOG(ctx.j.debug()) << "FirewallSet: Update must contain sfFirewallSigners"; return temMALFORMED; } - if (ctx.tx.isFieldPresent(sfSignature) && - ctx.tx.isFieldPresent(sfPublicKey)) + std::set firewallSignersSet; + if (ctx.tx.isFieldPresent(sfFirewallSigners)) { - PublicKey const txPK(makeSlice(ctx.tx.getFieldVL(sfPublicKey))); - auto const sig = ctx.tx.getFieldVL(sfSignature); - PublicKey const fPK( - makeSlice(sleFirewall->getFieldVL(sfPublicKey))); - Serializer msg; - serializeFirewallAuthorization(msg, accountID, txPK); - if (!verify(fPK, msg.slice(), makeSlice(sig), /*canonical*/ true)) + STArray const signers = ctx.tx.getFieldArray(sfFirewallSigners); + + // Check that the firewall signers array is not too large. + if (signers.size() > 8) { - JLOG(ctx.j.debug()) - << "Firewall: Bad Signature for update sfPublicKey"; - return temBAD_SIGNATURE; + JLOG(ctx.j.trace()) << "FirewallSet: signers array exceeds 8 entries."; + return temARRAY_TOO_LARGE; } - } - if (ctx.tx.isFieldPresent(sfSignature) && - ctx.tx.isFieldPresent(sfAmount)) - { - auto const amount = ctx.tx.getFieldAmount(sfAmount); - auto const sig = ctx.tx.getFieldVL(sfSignature); - PublicKey const pk(makeSlice(sleFirewall->getFieldVL(sfPublicKey))); - Serializer msg; - serializeFirewallAuthorization(msg, accountID, amount); - if (!verify(pk, msg.slice(), makeSlice(sig), /*canonical*/ true)) + // Add the batch signers to the set. + for (auto const& signer : signers) { - JLOG(ctx.j.debug()) - << "Firewall: Bad Signature for update sfAmount"; - return temBAD_SIGNATURE; + AccountID const innerAccount = signer.getAccountID(sfAccount); + if (!firewallSignersSet.insert(innerAccount).second) + { + JLOG(ctx.j.trace()) + << "FirewallSet: Duplicate signer found: " << innerAccount; + return temBAD_SIGNER; + } } - } - } - - // auto const amount = ctx.tx[~sfAmount]; - // if (auto const ter = requireAuth(ctx.view, amount.issue(), accountID); - // ter != tesSUCCESS) - // { - // JLOG(ctx.j.debug()) - // << "Firewall: account is not authorized, " << amount.issue(); - // return ter; - // } - - // // Globally or individually frozen - // if (isFrozen(ctx.view, accountID, amount.issue()) || - // isFrozen(ctx.view, accountID, amount2.issue())) - // { - // JLOG(ctx.j.debug()) << "Firewall: involves frozen asset."; - // return tecFROZEN; - // } - - // auto noDefaultRipple = [](ReadView const& view, Issue const& issue) { - // if (isXRP(issue)) - // return false; - // if (auto const issuerAccount = - // view.read(keylet::account(issue.account))) - // return (issuerAccount->getFlags() & lsfDefaultRipple) == 0; + // Check the batch signers signatures. + auto const requireCanonicalSig = + ctx.view.rules().enabled(featureRequireFullyCanonicalSig) + ? STTx::RequireFullyCanonicalSig::yes + : STTx::RequireFullyCanonicalSig::no; + auto const sigResult = + ctx.tx.checkFirewallSign(requireCanonicalSig, ctx.view.rules()); - // return false; - // }; + if (!sigResult) + { + JLOG(ctx.j.trace()) << "FirewallSet: invalid batch txn signature."; + return temBAD_SIGNATURE; + } + } - // if (noDefaultRipple(ctx.view, amount.issue())) - // { - // JLOG(ctx.j.debug()) << "Firewall: DefaultRipple not set"; - // return terNO_RIPPLE; - // } + if (ctx.tx.isFieldPresent(sfFirewallSigners) && + firewallSignersSet.size() != ctx.tx.getFieldArray(sfFirewallSigners).size()) + { + JLOG(ctx.j.trace()) + << "FirewallSet: unique signers does not match firewall signers."; + return temBAD_SIGNER; + } + } return tesSUCCESS; } @@ -207,7 +189,10 @@ FirewallSet::doApply() auto const sleOwner = sb.peek(keylet::account(account_)); if (!sleOwner) + { + JLOG(j_.debug()) << "FirewallSet: Owner account not found"; return tefINTERNAL; + } ripple::Keylet const firewallKeylet = keylet::firewall(account_); auto sleFirewall = sb.peek(firewallKeylet); @@ -215,11 +200,18 @@ FirewallSet::doApply() { auto const sleFirewall = std::make_shared(firewallKeylet); (*sleFirewall)[sfOwner] = account_; - sleFirewall->setFieldVL(sfPublicKey, ctx_.tx.getFieldVL(sfPublicKey)); + sleFirewall->setAccountID(sfIssuer, ctx_.tx.getAccountID(sfIssuer)); if (ctx_.tx.isFieldPresent(sfAmount)) sleFirewall->setFieldAmount( sfAmount, ctx_.tx.getFieldAmount(sfAmount)); + if (ctx_.tx.isFieldPresent(sfTimePeriod)) + { + sleFirewall->setFieldU32(sfTimePeriod, ctx_.tx.getFieldU32(sfTimePeriod)); + sleFirewall->setFieldU32(sfTimePeriodStart, ctx_.view().parentCloseTime().time_since_epoch().count()); + sleFirewall->setFieldAmount(sfTotalOut, STAmount{0}); + } + if (auto const page = sb.dirInsert( keylet::ownerDir(account_), sleFirewall->key(), @@ -229,7 +221,7 @@ FirewallSet::doApply() } else { - JLOG(j_.debug()) << "Firewall: failed to insert owner dir"; + JLOG(j_.debug()) << "FirewallSet: failed to insert owner dir"; return tecDIR_FULL; } sb.insert(sleFirewall); @@ -243,8 +235,8 @@ FirewallSet::doApply() return tecINSUFFICIENT_RESERVE; } - AccountID const auth{ctx_.tx[sfAuthorize]}; - Keylet const preauthKeylet = keylet::firewallPreauth(account_, auth); + AccountID const auth = ctx_.tx.getAccountID(sfAuthorize); + Keylet const preauthKeylet = keylet::withdrawPreauth(account_, auth); auto slePreauth = std::make_shared(preauthKeylet); slePreauth->setAccountID(sfAccount, account_); @@ -259,7 +251,7 @@ FirewallSet::doApply() } else { - JLOG(j_.debug()) << "Firewall: failed to insert owner dir"; + JLOG(j_.debug()) << "FirewallSet: failed to insert owner dir"; return tecDIR_FULL; } sb.insert(slePreauth); @@ -267,9 +259,9 @@ FirewallSet::doApply() } else { - if (ctx_.tx.isFieldPresent(sfPublicKey)) - sleFirewall->setFieldVL( - sfPublicKey, ctx_.tx.getFieldVL(sfPublicKey)); + if (ctx_.tx.isFieldPresent(sfIssuer)) + sleFirewall->setAccountID( + sfIssuer, ctx_.tx.getAccountID(sfIssuer)); if (ctx_.tx.isFieldPresent(sfAmount)) sleFirewall->setFieldAmount( sfAmount, ctx_.tx.getFieldAmount(sfAmount)); diff --git a/src/xrpld/app/tx/detail/Firewall.h b/src/xrpld/app/tx/detail/Firewall.h index d562574fccd..aef58b1d2dc 100644 --- a/src/xrpld/app/tx/detail/Firewall.h +++ b/src/xrpld/app/tx/detail/Firewall.h @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012, 2013 Ripple Labs Inc. + Copyright (c) 2024 Transia, LLC. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -36,6 +36,9 @@ class FirewallSet : public Transactor { } + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 92b5a7049bf..f49a47827b4 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -483,7 +483,7 @@ LedgerEntryTypesMatch::visitEntry( case ltMPTOKEN: case ltCREDENTIAL: case ltFIREWALL: - case ltFIREWALL_PREAUTH: + case ltWITHDRAW_PREAUTH: break; default: invalidTypeAdded_ = true; diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 1bacfa2b023..3243c47e1a7 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -437,62 +437,169 @@ Transactor::ticketDelete( } /** - * @brief Checks if a transaction is blocked by a firewall. + * @brief Updates the total outgoing amount in the firewall ledger entry. * - * This function inspects the transaction to determine if it should be blocked - * by a firewall rule. It only applies to payment transactions. If the firewall - * is active and the transaction does not meet the criteria, it will be blocked. + * This function sets the total outgoing amount in the specified firewall + * ledger entry and updates the view with the modified entry. * - * @param ctx The context of the transaction, including the transaction itself - * and the current view of the ledger. - * @param j The journal for logging. - * @return A status code indicating whether the transaction is allowed or - * blocked. + * @param view The ApplyView object representing the current view of the ledger. + * @param sleFirewall A pointer to the firewall ledger entry to be updated. + * @param totalOut The total outgoing amount to be set in the firewall ledger + * entry. + */ +static void +updateFirewallOutgoingTotal( + ApplyView& view, + SLE::pointer const& sleFirewall, + STAmount const& totalOut) +{ + sleFirewall->setFieldAmount(sfTotalOut, totalOut); + view.update(sleFirewall); +} + +/** + * @brief Resets the outgoing firewall timer. + * + * This function sets the firewall's time period start field to the current time + * and updates the view with the modified firewall entry. + * + * @param view The ApplyView object representing the current view of the ledger. + * @param sleFirewall A shared pointer to the SLE (Serialized Ledger Entry) + * representing the firewall. + * @param currentTime The current time to set as the start of the firewall's + * time period. + */ +static void +resetFirewallOutgoingTimer( + ApplyView& view, + SLE::pointer const& sleFirewall, + std::uint32_t const& currentTime) +{ + sleFirewall->setFieldU32(sfTimePeriodStart, currentTime); + view.update(sleFirewall); +} + +/** + * @brief Checks if a transaction passes the firewall rules for an account. + * + * This function verifies if a transaction is allowed based on the firewall + * settings associated with the account. It checks for the presence of firewall + * settings, destination account authorization, preauthorization, and amount + * limits. + * + * @param ctx The context of the preclaim, containing the transaction and view. + * @param j The journal for logging. + * @return A TER (Transaction Engine Result) code indicating the result of the + * check. + * - tesSUCCESS: The transaction passes the firewall checks. + * - tecFIREWALL_BLOCK: The transaction is blocked by the firewall. */ TER -Transactor::checkFirewall(PreclaimContext const& ctx, beast::Journal j) +Transactor::checkFirewall() { - // Only apply firewall checks to payment transactions - if (ctx.tx.getTxnType() != ttPAYMENT) + if (ctx_.tx.getTxnType() == ttFIREWALL_SET) { - JLOG(j.debug()) << "checkFirewall: Not a payment transaction"; + JLOG(j_.debug()) + << "checkFirewall: Ignoring firewall settings transaction"; return tesSUCCESS; } - // Get the account ID of the sender and the destination account - auto const id = ctx.tx.getAccountID(sfAccount); - auto const dest = ctx.tx.getAccountID(sfDestination); - - // Read the firewall settings for the sender account - auto const sleFirewall = ctx.view.read(keylet::firewall(id)); + AccountID const account = ctx_.tx.getAccountID(sfAccount); + auto const sleFirewall = view().peek(keylet::firewall(account)); if (!sleFirewall) { - JLOG(j.debug()) << "checkFirewall: No firewall settings found"; + JLOG(j_.debug()) << "checkFirewall: No firewall settings found"; return tesSUCCESS; } - // Check if the transaction amount exceeds the firewall limit - if (ctx.tx.isFieldPresent(sfAmount) && - sleFirewall->isFieldPresent(sfAmount)) + if (ctx_.tx.isFieldPresent(sfDestination)) { - if (ctx.tx.getFieldAmount(sfAmount) <= - sleFirewall->getFieldAmount(sfAmount)) + AccountID const dest = ctx_.tx.getAccountID(sfDestination); + + // Check if there is a preauthorization for the destination account + if (auto const sleWithdrawPreauth = + view().read(keylet::withdrawPreauth(account, dest)); + sleWithdrawPreauth) { - JLOG(j.debug()) << "checkFirewall: Transaction amount within limit"; + JLOG(j_.debug()) + << "checkFirewall: Preauthorized transactions are not blocked"; return tesSUCCESS; } } - // Check if there is a preauthorization for the destination account - if (auto const sleFirewallPreauth = - ctx.view.read(keylet::firewallPreauth(id, dest)); - !sleFirewallPreauth) + // Reject Pathing Transactions? + // Check self transactions? + + bool const hasOutgoingAmountLimit = sleFirewall->isFieldPresent(sfAmount); + bool const hasOutgoingTimeLimit = + sleFirewall->isFieldPresent(sfTimePeriod) && + sleFirewall->isFieldPresent(sfTimePeriodStart) && + sleFirewall->isFieldPresent(sfTotalOut); + if (hasOutgoingAmountLimit) { - JLOG(j.debug()) << "checkFirewall: No preauthorization for destination"; - return tecFIREWALL_BLOCK; + STAmount outgoingAmountLimit = sleFirewall->getFieldAmount(sfAmount); + STAmount outgoingAmount = STAmount{0}; + ctx_.visit([&outgoingAmount, account]( + uint256 const& index, + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) { + if (before && after && + (before->getType() == ltACCOUNT_ROOT && + before->getAccountID(sfAccount) == account)) + { + STAmount const beforeBalance = + before->getFieldAmount(sfBalance); + STAmount const afterBalance = after->getFieldAmount(sfBalance); + if (afterBalance < beforeBalance) + outgoingAmount = beforeBalance - afterBalance; + } + }); + + if (hasOutgoingTimeLimit) + { + // Firewall with time period and amount limit + std::uint32_t const currentTime = + view().parentCloseTime().time_since_epoch().count(); + std::uint32_t const startTime = + sleFirewall->getFieldU32(sfTimePeriodStart); + std::uint32_t const timePeriod = + sleFirewall->getFieldU32(sfTimePeriod); + STAmount outgoingTotal = sleFirewall->getFieldAmount(sfTotalOut); + + // Check if the monitoring period has expired + if (startTime == 0 || (currentTime - startTime > timePeriod)) + { + // Reset the monitoring period + resetFirewallOutgoingTimer(view(), sleFirewall, currentTime); + outgoingTotal = outgoingAmount; + } + else + { + // Add the transaction amount to the ongoing total + outgoingTotal += outgoingAmount; + } + + // Check if the transaction amount exceeds the firewall limit + if (outgoingTotal <= outgoingAmountLimit) + { + updateFirewallOutgoingTotal(view(), sleFirewall, outgoingTotal); + return tesSUCCESS; + } + } + else + { + // Firewall with amount limit + if (outgoingAmount <= outgoingAmountLimit) + { + JLOG(j_.debug()) + << "checkFirewall: Transaction amount within limit"; + return tesSUCCESS; + } + } } - JLOG(j.debug()) << "checkFirewall: Firewall block due to amount limit"; + JLOG(j_.debug()) << "checkFirewall: Firewall block due to amount limit"; return tecFIREWALL_BLOCK; } @@ -538,21 +645,49 @@ Transactor::apply() return doApply(); } +NotTEC +Transactor::checkFirewallSign(PreclaimContext const& ctx) +{ + auto const account = ctx.tx.getAccountID(sfAccount); + + // Check if the firewall is enabled. + auto const sleFirewall = ctx.view.read(keylet::firewall(account)); + if (!sleFirewall) + return tesSUCCESS; + + // Check if the firewall has signers. + auto const issuer = sleFirewall->getAccountID(sfIssuer); + STArray const& signers(ctx.tx.getFieldArray(sfFirewallSigners)); + auto const sleAccountSigners = ctx.view.read(keylet::signers(issuer)); + if (sleAccountSigners) + return checkMultiSign(ctx, account, signers); + + // Check if the firewall has a single signer. + Blob const pkSigner = signers[0].getFieldVL(sfSigningPubKey); + AccountID const idAccount = signers[0].getAccountID(sfAccount); + return checkSingleSign(ctx, idAccount, pkSigner); +} + NotTEC Transactor::checkSign(PreclaimContext const& ctx) { // If the pk is empty, then we must be multi-signing. if (ctx.tx.getSigningPubKey().empty()) - return checkMultiSign(ctx); + { + auto const account = ctx.tx.getAccountID(sfAccount); + STArray const& signers(ctx.tx.getFieldArray(sfSigners)); + return checkMultiSign(ctx, account, signers); + } - return checkSingleSign(ctx); + Blob const pkSigner = ctx.tx.getSigningPubKey(); + AccountID const idAccount = ctx.tx.getAccountID(sfAccount); + return checkSingleSign(ctx, idAccount, pkSigner); } NotTEC -Transactor::checkSingleSign(PreclaimContext const& ctx) +Transactor::checkSingleSign(PreclaimContext const& ctx, AccountID const& idAccount, Blob const& pkSigner) { // Check that the value in the signing key slot is a public key. - auto const pkSigner = ctx.tx.getSigningPubKey(); if (!publicKeyType(makeSlice(pkSigner))) { JLOG(ctx.j.trace()) @@ -562,7 +697,6 @@ Transactor::checkSingleSign(PreclaimContext const& ctx) // 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; @@ -623,9 +757,11 @@ Transactor::checkSingleSign(PreclaimContext const& ctx) } NotTEC -Transactor::checkMultiSign(PreclaimContext const& ctx) +Transactor::checkMultiSign( + PreclaimContext const& ctx, + AccountID const& id, + STArray const& txSigners) { - auto const id = ctx.tx.getAccountID(sfAccount); // Get mTxnAccountID's SignerList and Quorum. std::shared_ptr sleAccountSigners = ctx.view.read(keylet::signers(id)); @@ -647,9 +783,6 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) 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. @@ -1061,6 +1194,10 @@ Transactor::operator()() applied = isTecClaim(result); } + // Handle Firewall + if (applied) + result = checkFirewall(); + if (applied) { // Check invariants: if `tecINVARIANT_FAILED` is not returned, we can diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index 9da8e60cd64..c203af3b842 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -137,6 +137,9 @@ class Transactor static NotTEC checkSign(PreclaimContext const& ctx); + static NotTEC + checkFirewallSign(PreclaimContext const& ctx); + // Returns the fee in fee units, not scaled for load. static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); @@ -158,9 +161,6 @@ class Transactor uint256 const& ticketIndex, beast::Journal j); - static TER - checkFirewall(PreclaimContext const& ctx, beast::Journal j); - protected: TER apply(); @@ -189,6 +189,9 @@ class Transactor Fees const& fees, ApplyFlags flags); + TER + checkFirewall(); + private: std::pair reset(XRPAmount fee); @@ -198,9 +201,15 @@ class Transactor TER payFee(); static NotTEC - checkSingleSign(PreclaimContext const& ctx); + checkSingleSign( + PreclaimContext const& ctx, + AccountID const& idAccount, + Blob const& pkSigner); static NotTEC - checkMultiSign(PreclaimContext const& ctx); + checkMultiSign( + PreclaimContext const& ctx, + AccountID const& id, + STArray const& txSigners); void trapTransaction(uint256) const; }; diff --git a/src/xrpld/app/tx/detail/FirewallPreauth.cpp b/src/xrpld/app/tx/detail/WithdrawPreauth.cpp similarity index 71% rename from src/xrpld/app/tx/detail/FirewallPreauth.cpp rename to src/xrpld/app/tx/detail/WithdrawPreauth.cpp index 31e6eb678f1..f56ccd57854 100644 --- a/src/xrpld/app/tx/detail/FirewallPreauth.cpp +++ b/src/xrpld/app/tx/detail/WithdrawPreauth.cpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2018 Ripple Labs Inc. + Copyright (c) 2024 Transia, LLC. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -17,11 +17,10 @@ */ //============================================================================== -#include +#include #include #include #include -#include #include #include #include @@ -29,7 +28,7 @@ namespace ripple { NotTEC -FirewallPreauth::preflight(PreflightContext const& ctx) +WithdrawPreauth::preflight(PreflightContext const& ctx) { if (!ctx.rules.enabled(featureFirewall)) return temDISABLED; @@ -71,7 +70,7 @@ FirewallPreauth::preflight(PreflightContext const& ctx) if (optAuth && (target == ctx.tx[sfAccount])) { JLOG(j.trace()) - << "Malformed transaction: Attempting to FirewallPreauth self."; + << "Malformed transaction: Attempting to WithdrawPreauth self."; return temCANNOT_PREAUTH_SELF; } @@ -79,63 +78,64 @@ FirewallPreauth::preflight(PreflightContext const& ctx) } TER -FirewallPreauth::preclaim(PreclaimContext const& ctx) +WithdrawPreauth::preclaim(PreclaimContext const& ctx) { Serializer msg; AccountID const accountID = ctx.tx[sfAccount]; - // Determine which operation we're performing: authorizing or unauthorizing. - if (ctx.tx.isFieldPresent(sfAuthorize)) - { - // Verify that the Authorize account is present in the ledger. - AccountID const auth{ctx.tx[sfAuthorize]}; - if (!ctx.view.exists(keylet::account(auth))) - return tecNO_TARGET; - - // Verify that the Preauth entry they asked to add is not already - // in the ledger. - if (ctx.view.exists(keylet::firewallPreauth(ctx.tx[sfAccount], auth))) - return tecDUPLICATE; + // // Determine which operation we're performing: authorizing or unauthorizing. + // if (ctx.tx.isFieldPresent(sfAuthorize)) + // { + // // Verify that the Authorize account is present in the ledger. + // AccountID const auth{ctx.tx[sfAuthorize]}; + // if (!ctx.view.exists(keylet::account(auth))) + // return tecNO_TARGET; + + // // Verify that the Preauth entry they asked to add is not already + // // in the ledger. + // if (ctx.view.exists(keylet::withdrawPreauth(ctx.tx[sfAccount], auth))) + // return tecDUPLICATE; - serializeFirewallAuthorization(msg, accountID, auth); - } - else - { - // Verify that the Preauth entry they asked to remove is in the ledger. - AccountID const unauth{ctx.tx[sfUnauthorize]}; - if (!ctx.view.exists( - keylet::firewallPreauth(ctx.tx[sfAccount], unauth))) - return tecNO_ENTRY; + // serializeFirewallAuthorization(msg, accountID, auth); + // } + // else + // { + // // Verify that the Preauth entry they asked to remove is in the ledger. + // AccountID const unauth{ctx.tx[sfUnauthorize]}; + // if (!ctx.view.exists( + // keylet::withdrawPreauth(ctx.tx[sfAccount], unauth))) + // return tecNO_ENTRY; - serializeFirewallAuthorization(msg, accountID, unauth); - } + // serializeFirewallAuthorization(msg, accountID, unauth); + // } // Validate Signature ripple::Keylet const firewallKeylet = keylet::firewall(accountID); auto const sleFirewall = ctx.view.read(firewallKeylet); if (!sleFirewall) { - JLOG(ctx.j.debug()) << "FirewallPreauth: Firewall does not exist."; + JLOG(ctx.j.debug()) << "WithdrawPreauth: Firewall does not exist."; return tecNO_TARGET; } - if (!sleFirewall->isFieldPresent(sfPublicKey)) + if (!sleFirewall->isFieldPresent(sfIssuer)) { - JLOG(ctx.j.debug()) << "FirewallPreauth: Missing Firewall Public Key."; + JLOG(ctx.j.debug()) << "WithdrawPreauth: Missing Firewall Issuer."; return tecINTERNAL; } auto const sig = ctx.tx.getFieldVL(sfSignature); - PublicKey const pk(makeSlice(sleFirewall->getFieldVL(sfPublicKey))); + PublicKey const pk(makeSlice(ctx.tx.getFieldVL(sfPublicKey))); + // TODO: Valid PK (AccountID) == sfIssuer if (!verify(pk, msg.slice(), makeSlice(sig), /*canonical*/ true)) { JLOG(ctx.j.debug()) - << "FirewallPreauth: Bad Signature for update."; + << "WithdrawPreauth: Bad Signature for update."; return temBAD_SIGNATURE; } return tesSUCCESS; } TER -FirewallPreauth::doApply() +WithdrawPreauth::doApply() { if (ctx_.tx.isFieldPresent(sfAuthorize)) { @@ -157,7 +157,7 @@ FirewallPreauth::doApply() // Preclaim already verified that the Preauth entry does not yet exist. // Create and populate the Preauth entry. AccountID const auth{ctx_.tx[sfAuthorize]}; - Keylet const preauthKeylet = keylet::firewallPreauth(account_, auth); + Keylet const preauthKeylet = keylet::withdrawPreauth(account_, auth); auto slePreauth = std::make_shared(preauthKeylet); slePreauth->setAccountID(sfAccount, account_); @@ -170,7 +170,7 @@ FirewallPreauth::doApply() preauthKeylet, describeOwnerDir(account_)); - JLOG(j_.trace()) << "Adding FirewallPreauth to owner directory " + JLOG(j_.trace()) << "Adding WithdrawPreauth to owner directory " << to_string(preauthKeylet.key) << ": " << (page ? "success" : "failure"); @@ -185,16 +185,16 @@ FirewallPreauth::doApply() else { auto const preauth = - keylet::firewallPreauth(account_, ctx_.tx[sfUnauthorize]); + keylet::withdrawPreauth(account_, ctx_.tx[sfUnauthorize]); - return FirewallPreauth::removeFromLedger( + return WithdrawPreauth::removeFromLedger( ctx_.app, view(), preauth.key, j_); } return tesSUCCESS; } TER -FirewallPreauth::removeFromLedger( +WithdrawPreauth::removeFromLedger( Application& app, ApplyView& view, uint256 const& preauthIndex, @@ -203,10 +203,10 @@ FirewallPreauth::removeFromLedger( // Verify that the Preauth entry they asked to remove is // in the ledger. std::shared_ptr const slePreauth{ - view.peek(keylet::firewallPreauth(preauthIndex))}; + view.peek(keylet::withdrawPreauth(preauthIndex))}; if (!slePreauth) { - JLOG(j.warn()) << "Selected FirewallPreauth does not exist."; + JLOG(j.warn()) << "Selected WithdrawPreauth does not exist."; return tecNO_ENTRY; } @@ -214,18 +214,18 @@ FirewallPreauth::removeFromLedger( std::uint64_t const page{(*slePreauth)[sfOwnerNode]}; if (!view.dirRemove(keylet::ownerDir(account), page, preauthIndex, false)) { - JLOG(j.fatal()) << "Unable to delete FirewallPreauth from owner."; + JLOG(j.fatal()) << "Unable to delete WithdrawPreauth from owner."; return tefBAD_LEDGER; } - // If we succeeded, update the FirewallPreauth owner's reserve. + // If we succeeded, update the WithdrawPreauth owner's reserve. auto const sleOwner = view.peek(keylet::account(account)); if (!sleOwner) return tefINTERNAL; adjustOwnerCount(view, sleOwner, -1, app.journal("View")); - // Remove FirewallPreauth from ledger. + // Remove WithdrawPreauth from ledger. view.erase(slePreauth); return tesSUCCESS; diff --git a/src/xrpld/app/tx/detail/FirewallPreauth.h b/src/xrpld/app/tx/detail/WithdrawPreauth.h similarity index 86% rename from src/xrpld/app/tx/detail/FirewallPreauth.h rename to src/xrpld/app/tx/detail/WithdrawPreauth.h index 6a7f7cab8fd..8f8ac0a1488 100644 --- a/src/xrpld/app/tx/detail/FirewallPreauth.h +++ b/src/xrpld/app/tx/detail/WithdrawPreauth.h @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012, 2013 Ripple Labs Inc. + Copyright (c) 2024 Transia, LLC. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -17,19 +17,19 @@ */ //============================================================================== -#ifndef RIPPLE_TX_FIREWALL_PREAUTH_H_INCLUDED -#define RIPPLE_TX_FIREWALL_PREAUTH_H_INCLUDED +#ifndef RIPPLE_TX_WITHDRAW_PREAUTH_H_INCLUDED +#define RIPPLE_TX_WITHDRAW_PREAUTH_H_INCLUDED #include namespace ripple { -class FirewallPreauth : public Transactor +class WithdrawPreauth : public Transactor { public: static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; - explicit FirewallPreauth(ApplyContext& ctx) : Transactor(ctx) + explicit WithdrawPreauth(ApplyContext& ctx) : Transactor(ctx) { } diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index e70fd5d5731..7fb49699ba9 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -41,7 +41,7 @@ #include #include #include -#include +#include #include #include #include @@ -180,11 +180,6 @@ invoke_preclaim(PreclaimContext const& ctx) { TER result = T::checkSeqProxy(ctx.view, ctx.tx, ctx.j); - if (result != tesSUCCESS) - return result; - - result = T::checkFirewall(ctx, ctx.j); - if (result != tesSUCCESS) return result; @@ -202,6 +197,14 @@ invoke_preclaim(PreclaimContext const& ctx) if (result != tesSUCCESS) return result; + + if (ctx.tx.getTxnType() == ttFIREWALL_SET || + ctx.tx.getTxnType() == ttWITHDRAW_PREAUTH) + { + result = T::checkFirewallSign(ctx); + if (result != tesSUCCESS) + return result; + } } return T::preclaim(ctx); diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp index 5bd51120253..a142e6758f0 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -931,7 +931,7 @@ chooseLedgerEntryType(Json::Value const& params) std::pair result{RPC::Status::OK, ltANY}; if (params.isMember(jss::type)) { - static constexpr std::array, 25> + static constexpr std::array, 27> types{ {{jss::account, ltACCOUNT_ROOT}, {jss::amendments, ltAMENDMENTS}, @@ -945,7 +945,7 @@ chooseLedgerEntryType(Json::Value const& params) {jss::escrow, ltESCROW}, {jss::fee, ltFEE_SETTINGS}, {jss::firewall, ltFIREWALL}, - {jss::firewall_preauth, ltFIREWALL_PREAUTH}, + {jss::withdraw_preauth, ltWITHDRAW_PREAUTH}, {jss::hashes, ltLEDGER_HASHES}, {jss::nunl, ltNEGATIVE_UNL}, {jss::oracle, ltORACLE}, diff --git a/src/xrpld/rpc/handlers/AccountObjects.cpp b/src/xrpld/rpc/handlers/AccountObjects.cpp index 538b1d79424..898f4b74627 100644 --- a/src/xrpld/rpc/handlers/AccountObjects.cpp +++ b/src/xrpld/rpc/handlers/AccountObjects.cpp @@ -216,6 +216,7 @@ doAccountObjects(RPC::JsonContext& context) } static constexpr deletionBlockers[] = { {jss::check, ltCHECK}, {jss::escrow, ltESCROW}, + {jss::firewall, ltFIREWALL}, {jss::nft_page, ltNFTOKEN_PAGE}, {jss::payment_channel, ltPAYCHAN}, {jss::state, ltRIPPLE_STATE},