diff --git a/include/xrpl/protocol/Feature.h b/include/xrpl/protocol/Feature.h index 90a81c55ef4..df071c69864 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 = 83; +static constexpr std::size_t numFeatures = 84; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 72cf0b527b1..82b1b19a161 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -329,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: @@ -374,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 4f3eef4919d..710370a68f2 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -62,7 +62,6 @@ enum LedgerEntryType : std::uint16_t #undef LEDGER_ENTRY #pragma pop_macro("LEDGER_ENTRY") - //--------------------------------------------------------------------------- /** 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/TER.h b/include/xrpl/protocol/TER.h index 317e9c2c978..61146685548 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -344,6 +344,7 @@ enum TECcodes : TERUnderlyingType { tecARRAY_TOO_LARGE = 191, tecLOCKED = 192, tecBAD_CREDENTIALS = 193, + tecFIREWALL_BLOCK = 194, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/TxFormats.h b/include/xrpl/protocol/TxFormats.h index 2f9121cecb4..e486f8707f8 100644 --- a/include/xrpl/protocol/TxFormats.h +++ b/include/xrpl/protocol/TxFormats.h @@ -97,4 +97,4 @@ class TxFormats : public KnownFormats } // namespace ripple -#endif +#endif \ No newline at end of file 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 f9e0db24949..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 @@ -752,4 +755,4 @@ JSS(NegativeUNL); // out: ValidatorList; ledger type } // namespace jss } // namespace ripple -#endif +#endif \ No newline at end of file diff --git a/src/libxrpl/protocol/Feature.cpp b/src/libxrpl/protocol/Feature.cpp index 3f6e760577a..b6f3cc5e183 100644 --- a/src/libxrpl/protocol/Feature.cpp +++ b/src/libxrpl/protocol/Feature.cpp @@ -463,4 +463,4 @@ uint256 const [[maybe_unused]] static const bool readOnlySet = featureCollections.registrationIsDone(); -} // namespace ripple +} // namespace ripple \ No newline at end of file diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 12142879ad5..c600b9a6372 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -77,6 +77,8 @@ enum class LedgerNameSpace : std::uint16_t { 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. @@ -519,6 +521,20 @@ credential( 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/LedgerFormats.cpp b/src/libxrpl/protocol/LedgerFormats.cpp index d66b085e0d0..42581d77667 100644 --- a/src/libxrpl/protocol/LedgerFormats.cpp +++ b/src/libxrpl/protocol/LedgerFormats.cpp @@ -56,4 +56,4 @@ LedgerFormats::getInstance() return instance; } -} // namespace ripple +} // namespace ripple \ No newline at end of file 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/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 815b27c0018..e2acb56e165 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -117,6 +117,7 @@ transResults() MAKE_ERROR(tecARRAY_TOO_LARGE, "Array is too large."), MAKE_ERROR(tecLOCKED, "Fund is locked."), MAKE_ERROR(tecBAD_CREDENTIALS, "Bad credentials."), + 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 76b1ae8ad4f..9b0dea26570 100644 --- a/src/libxrpl/protocol/TxFormats.cpp +++ b/src/libxrpl/protocol/TxFormats.cpp @@ -71,4 +71,4 @@ TxFormats::getInstance() return instance; } -} // 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 new file mode 100644 index 00000000000..4d617cb76ec --- /dev/null +++ b/src/test/app/Firewall_test.cpp @@ -0,0 +1,518 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + 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 + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include + +namespace ripple { +namespace test { +struct Firewall_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 std::pair> + firewallKeyAndSle(ReadView const& view, jtx::Account const& account) + { + auto const k = keylet::firewall(account); + return {k.key, view.read(k)}; + } + + void + verifyFirewallSle( + ReadView const& view, + jtx::Account const& account, + 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)[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 + testEnabled(FeatureBitset features) + { + testcase("enabled"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + Account const alice = Account("alice"); + Account const bob = Account("bob"); + Account const carol = Account("carol"); + + 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, bob); + env.close(); + + auto const txResult = + withFirewall ? ter(tesSUCCESS) : ter(temDISABLED); + auto const dirCount = withFirewall ? 2 : 0; + + 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), + txResult); + env.close(); + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == dirCount); + } + } + + void + testPreflight(FeatureBitset features) + { + testcase("preflight"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + Account const alice = Account("alice"); + Account const bob = Account("bob"); + Account const carol = Account("carol"); + + // preflight + // --------------------------------------------------------- + + // temINVALID_ACCOUNT_ID + // temCANNOT_PREAUTH_SELF + } + + void + testPreclaim(FeatureBitset features) + { + testcase("preclaim"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + Account const alice = Account("alice"); + Account const bob = Account("bob"); + Account const carol = Account("carol"); + + // preclaim + // --------------------------------------------------------- + + // Set - Create + // temMALFORMED: Firewall: Set must not contain a sfSignature + // temMALFORMED: Firewall: Set must contain a sfAuthorize + // temMALFORMED: Firewall: Set must contain a sfPublicKey + + // 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 + } + + void + testDoApply(FeatureBitset features) + { + testcase("doApply"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + Account const alice = Account("alice"); + Account const bob = Account("bob"); + Account const carol = Account("carol"); + + // 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 + + } + + void + testFirewallSet(FeatureBitset features) + { + 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"); + + // No Amount + { + Env env{*this, features}; + env.fund(XRP(1000), alice, bob, carol); + env.close(); + + 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); + } + + // Amount w/out Time Period + { + Env env{*this, features}; + env.fund(XRP(1000), alice, bob, carol); + env.close(); + + 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(); + + verifyFirewallSle(*env.current(), alice, carol, XRP(10)); + BEAST_EXPECT(ownerDirCount(*env.current(), alice) == 2); + } + + // Amount w/ Time Period + { + Env env{*this, features}; + env.fund(XRP(1000), alice, bob, carol); + env.close(); + + 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 + testFirewallBlock(FeatureBitset features) + { + testcase("firewall block"); + 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(); + + 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(); + + { + 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(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 + testFirewallSetUpdate(FeatureBitset features) + { + testcase("set update"); + 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"); + 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(); + + verifyFirewallSle(*env.current(), alice, carol, XRP(101)); + env(pay(alice, dave, XRP(100)), ter(tesSUCCESS)); + env.close(); + } + + // Update Amount w/ 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::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); + // testPreflight(features); + // testPreclaim(features); + // testDoApply(features); + // testFirewallSet(features); + // testFirewallBlock(features); + // testFirewallDelete(features); + testFirewallSetUpdate(features); + // testUpdatePK(features); + // testMasterDisable(features); + // testTransactionTypes(features); + + // // Bad Amount + // { + // env(pay(alice, bob, XRP(100)), ter(tecFIREWALL_BLOCK)); + // env.close(); + // } + } + +public: + void + run() override + { + using namespace test::jtx; + FeatureBitset const all{supported_amendments()}; + testWithFeats(all); + } +}; + +BEAST_DEFINE_TESTSUITE(Firewall, app, ripple); +} // namespace test +} // namespace ripple diff --git a/src/test/jtx.h b/src/test/jtx.h index b7b9a9fa05c..53ce558b579 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -37,6 +37,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..5ee1fadd03a --- /dev/null +++ b/src/test/jtx/firewall.h @@ -0,0 +1,212 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + 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 + 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, 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 +{ +private: + STAmount amt_; + +public: + explicit amt(STAmount const& amt) : amt_(amt) + { + } + + void + 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 +{ +private: + jtx::Account auth_; + +public: + explicit auth(jtx::Account const& auth) : auth_(auth) + { + } + + void + operator()(Env&, JTx& jtx) const; +}; + +/** Set a firewall signature on a JTx. */ +class 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; + } + }; + + std::vector signers; + +public: + 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& jt) const; +}; + +/** Set a firewall multi signature on a JTx. */ +class msig +{ +public: + struct Reg + { + Account acct; + Account sig; + + Reg(Account const& masterSig) : acct(masterSig), sig(masterSig) + { + } + + Reg(Account const& acct_, Account const& regularSig) + : acct(acct_), sig(regularSig) + { + } + + Reg(char const* masterSig) : acct(masterSig), sig(masterSig) + { + } + + Reg(char const* acct_, char const* regularSig) + : acct(acct_), sig(regularSig) + { + } + + bool + operator<(Reg const& rhs) const + { + return acct < rhs.acct; + } + }; + + Account master; // Add a member to hold the master account + std::vector signers; + +public: + 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& jt) 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..87d44639a52 --- /dev/null +++ b/src/test/jtx/impl/firewall.cpp @@ -0,0 +1,162 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + 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 + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +// #include +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { +namespace jtx { +namespace firewall { + +Json::Value +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(); +} + +sig::sig(std::vector signers_) : signers(std::move(signers_)) +{ + // 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 +{ + 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 +} // namespace jtx +} // namespace test +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/Firewall.cpp b/src/xrpld/app/tx/detail/Firewall.cpp new file mode 100644 index 00000000000..281e572346d --- /dev/null +++ b/src/xrpld/app/tx/detail/Firewall.cpp @@ -0,0 +1,276 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + 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 + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +XRPAmount +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) +{ + 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()) + // << "FirewallSet: tokens can not have the same currency/issuer."; + // return temBAD_AMM_TOKENS; + // } + + // if (auto const err = invalidAmount(amount)) + // { + // JLOG(ctx.j.debug()) << "FirewallSet: invalid asset1 amount."; + // return err; + // } + + // Validate Authorize + if (ctx.tx.isFieldPresent(sfAuthorize)) + { + auto const backupID = ctx.tx.getAccountID(sfAuthorize); + // Make sure that the passed account is valid. + if (backupID == beast::zero) + { + JLOG(ctx.j.debug()) + << "Malformed transaction: Authorized or Unauthorized " + "field zeroed."; + return temINVALID_ACCOUNT_ID; + } + + // An account may not preauthorize itself. + if (backupID == ctx.tx[sfAccount]) + { + JLOG(ctx.j.debug()) + << "Malformed transaction: Attempting to WithdrawPreauth self."; + return temCANNOT_PREAUTH_SELF; + } + } + + return preflight2(ctx); +} + +TER +FirewallSet::preclaim(PreclaimContext const& ctx) +{ + AccountID const accountID = ctx.tx[sfAccount]; + ripple::Keylet const firewallKeylet = keylet::firewall(accountID); + auto const sleFirewall = ctx.view.read(firewallKeylet); + + if (!sleFirewall) + { + if (ctx.tx.isFieldPresent(sfFirewallSigners)) + { + JLOG(ctx.j.debug()) + << "FirewallSet: Set must not contain a sfFirewallSigners"; + return temMALFORMED; + } + if (!ctx.tx.isFieldPresent(sfAuthorize)) + { + JLOG(ctx.j.debug()) << "FirewallSet: Set must contain a sfAuthorize"; + return temMALFORMED; + } + if (!ctx.tx.isFieldPresent(sfIssuer)) + { + JLOG(ctx.j.debug()) << "FirewallSet: Set must contain a sfIssuer"; + return temMALFORMED; + } + } + else + { + if (ctx.tx.isFieldPresent(sfAuthorize)) + { + JLOG(ctx.j.debug()) + << "FirewallSet: Update cannot contain a sfAuthorize"; + return temMALFORMED; + } + + if (!ctx.tx.isFieldPresent(sfFirewallSigners)) + { + JLOG(ctx.j.debug()) << "FirewallSet: Update must contain sfFirewallSigners"; + return temMALFORMED; + } + + std::set firewallSignersSet; + if (ctx.tx.isFieldPresent(sfFirewallSigners)) + { + STArray const signers = ctx.tx.getFieldArray(sfFirewallSigners); + + // Check that the firewall signers array is not too large. + if (signers.size() > 8) + { + JLOG(ctx.j.trace()) << "FirewallSet: signers array exceeds 8 entries."; + return temARRAY_TOO_LARGE; + } + + // Add the batch signers to the set. + for (auto const& signer : signers) + { + AccountID const innerAccount = signer.getAccountID(sfAccount); + if (!firewallSignersSet.insert(innerAccount).second) + { + JLOG(ctx.j.trace()) + << "FirewallSet: Duplicate signer found: " << innerAccount; + return temBAD_SIGNER; + } + } + + // 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()); + + if (!sigResult) + { + JLOG(ctx.j.trace()) << "FirewallSet: invalid batch txn signature."; + return temBAD_SIGNATURE; + } + } + + 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; +} + +TER +FirewallSet::doApply() +{ + Sandbox sb(&ctx_.view()); + + 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); + if (!sleFirewall) + { + auto const sleFirewall = std::make_shared(firewallKeylet); + (*sleFirewall)[sfOwner] = account_; + 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(), + describeOwnerDir(account_))) + { + sleFirewall->setFieldU64(sfOwnerNode, *page); + } + else + { + JLOG(j_.debug()) << "FirewallSet: failed to insert owner dir"; + return tecDIR_FULL; + } + sb.insert(sleFirewall); + adjustOwnerCount(sb, sleOwner, 1, j_); + + { + STAmount const reserve{view().fees().accountReserve( + sleOwner->getFieldU32(sfOwnerCount) + 1)}; + + if (mPriorBalance < reserve) + return tecINSUFFICIENT_RESERVE; + } + + AccountID const auth = ctx_.tx.getAccountID(sfAuthorize); + Keylet const preauthKeylet = keylet::withdrawPreauth(account_, auth); + auto slePreauth = std::make_shared(preauthKeylet); + + slePreauth->setAccountID(sfAccount, account_); + slePreauth->setAccountID(sfAuthorize, auth); + + if (auto const page = sb.dirInsert( + keylet::ownerDir(account_), + slePreauth->key(), + describeOwnerDir(account_))) + { + sleFirewall->setFieldU64(sfOwnerNode, *page); + } + else + { + JLOG(j_.debug()) << "FirewallSet: failed to insert owner dir"; + return tecDIR_FULL; + } + sb.insert(slePreauth); + adjustOwnerCount(sb, sleOwner, 1, j_); + } + else + { + if (ctx_.tx.isFieldPresent(sfIssuer)) + sleFirewall->setAccountID( + sfIssuer, ctx_.tx.getAccountID(sfIssuer)); + 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/Firewall.h b/src/xrpld/app/tx/detail/Firewall.h new file mode 100644 index 00000000000..aef58b1d2dc --- /dev/null +++ b/src/xrpld/app/tx/detail/Firewall.h @@ -0,0 +1,54 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + 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 + 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_FIREWALLSET_H_INCLUDED +#define RIPPLE_TX_FIREWALLSET_H_INCLUDED + +#include +#include +#include +#include + +namespace ripple { + +class FirewallSet : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit FirewallSet(ApplyContext& ctx) : Transactor(ctx) + { + } + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + 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/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 90fc399b344..f49a47827b4 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -482,6 +482,8 @@ LedgerEntryTypesMatch::visitEntry( case ltMPTOKEN_ISSUANCE: case ltMPTOKEN: case ltCREDENTIAL: + case ltFIREWALL: + case ltWITHDRAW_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/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index 052a735a2fd..3243c47e1a7 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -436,6 +436,173 @@ Transactor::ticketDelete( return tesSUCCESS; } +/** + * @brief Updates the total outgoing amount in the firewall ledger entry. + * + * This function sets the total outgoing amount in the specified firewall + * ledger entry and updates the view with the modified entry. + * + * @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() +{ + if (ctx_.tx.getTxnType() == ttFIREWALL_SET) + { + JLOG(j_.debug()) + << "checkFirewall: Ignoring firewall settings transaction"; + return tesSUCCESS; + } + + 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"; + return tesSUCCESS; + } + + if (ctx_.tx.isFieldPresent(sfDestination)) + { + 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: Preauthorized transactions are not blocked"; + return tesSUCCESS; + } + } + + // 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) + { + 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"; + return tecFIREWALL_BLOCK; +} + // check stuff before you bother to lock the ledger void Transactor::preCompute() @@ -478,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()) @@ -502,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; @@ -563,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)); @@ -587,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. @@ -1001,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 c587e5e1994..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); @@ -186,6 +189,9 @@ class Transactor Fees const& fees, ApplyFlags flags); + TER + checkFirewall(); + private: std::pair reset(XRPAmount fee); @@ -195,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/WithdrawPreauth.cpp b/src/xrpld/app/tx/detail/WithdrawPreauth.cpp new file mode 100644 index 00000000000..f56ccd57854 --- /dev/null +++ b/src/xrpld/app/tx/detail/WithdrawPreauth.cpp @@ -0,0 +1,234 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + 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 + 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 +WithdrawPreauth::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 WithdrawPreauth self."; + return temCANNOT_PREAUTH_SELF; + } + + return preflight2(ctx); +} + +TER +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::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::withdrawPreauth(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()) << "WithdrawPreauth: Firewall does not exist."; + return tecNO_TARGET; + } + if (!sleFirewall->isFieldPresent(sfIssuer)) + { + JLOG(ctx.j.debug()) << "WithdrawPreauth: Missing Firewall Issuer."; + return tecINTERNAL; + } + auto const sig = ctx.tx.getFieldVL(sfSignature); + 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()) + << "WithdrawPreauth: Bad Signature for update."; + return temBAD_SIGNATURE; + } + return tesSUCCESS; +} + +TER +WithdrawPreauth::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::withdrawPreauth(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 WithdrawPreauth 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::withdrawPreauth(account_, ctx_.tx[sfUnauthorize]); + + return WithdrawPreauth::removeFromLedger( + ctx_.app, view(), preauth.key, j_); + } + return tesSUCCESS; +} + +TER +WithdrawPreauth::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::withdrawPreauth(preauthIndex))}; + if (!slePreauth) + { + JLOG(j.warn()) << "Selected WithdrawPreauth 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 WithdrawPreauth from owner."; + return tefBAD_LEDGER; + } + + // 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 WithdrawPreauth from ledger. + view.erase(slePreauth); + + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/WithdrawPreauth.h b/src/xrpld/app/tx/detail/WithdrawPreauth.h new file mode 100644 index 00000000000..8f8ac0a1488 --- /dev/null +++ b/src/xrpld/app/tx/detail/WithdrawPreauth.h @@ -0,0 +1,56 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + 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 + 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_WITHDRAW_PREAUTH_H_INCLUDED +#define RIPPLE_TX_WITHDRAW_PREAUTH_H_INCLUDED + +#include + +namespace ripple { + +class WithdrawPreauth : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit WithdrawPreauth(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/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index b3c711084dc..7fb49699ba9 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -40,6 +40,8 @@ #include #include #include +#include +#include #include #include #include @@ -93,7 +95,6 @@ with_txn_type(TxType txnType, F&& f) #undef TRANSACTION #pragma pop_macro("TRANSACTION") - default: throw UnknownTxnType(txnType); } @@ -196,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 af204eaedf7..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}, @@ -944,6 +944,8 @@ chooseLedgerEntryType(Json::Value const& params) {jss::directory, ltDIR_NODE}, {jss::escrow, ltESCROW}, {jss::fee, ltFEE_SETTINGS}, + {jss::firewall, ltFIREWALL}, + {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},