featureFirewall

This commit is contained in:
Denis Angell
2026-09-12 06:35:47 -04:00
parent 9403736199
commit 008643d01c
39 changed files with 4325 additions and 105 deletions

View File

@@ -0,0 +1,37 @@
#include <xrpl/protocol/Firewall.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/TxSettings.h>
namespace xrpl {
#pragma push_macro("UNWRAP")
#undef UNWRAP
#pragma push_macro("TRANSACTION")
#undef TRANSACTION
#define UNWRAP(...) __VA_ARGS__
#define TRANSACTION(tag, value, name, settings, ...) \
case tag: \
return (TxSettings UNWRAP settings).firewall;
FirewallAction
firewallAction(TxType txType) noexcept
{
switch (txType)
{
#include <xrpl/protocol/detail/transactions.macro>
// Deprecated types
default:
return FirewallAction::Allow;
}
}
#undef TRANSACTION
#pragma pop_macro("TRANSACTION")
#undef UNWRAP
#pragma pop_macro("UNWRAP")
} // namespace xrpl

View File

@@ -104,6 +104,8 @@ enum class LedgerNameSpace : std::uint16_t {
LoanBroker = 'l', // lower-case L
Loan = 'L',
Sponsorship = '>',
Firewall = 'F',
WithdrawPreauth = 'G',
// No longer used or supported. Left here to reserve the space to avoid accidental reuse.
Contract [[deprecated]] = 'c',
@@ -610,6 +612,20 @@ permissionedDomain(uint256 const& domainID) noexcept
return {ltPERMISSIONED_DOMAIN, domainID};
}
Keylet
firewall(AccountID const& account) noexcept
{
return {ltFIREWALL, indexHash(LedgerNameSpace::Firewall, account)};
}
Keylet
withdrawPreauth(AccountID const& owner, AccountID const& preauthorized, std::uint32_t dtag) noexcept
{
return {
ltWITHDRAW_PREAUTH,
indexHash(LedgerNameSpace::WithdrawPreauth, owner, preauthorized, dtag)};
}
} // namespace keylet
} // namespace xrpl

View File

@@ -137,6 +137,7 @@ transResults()
MAKE_ERROR(tefBAD_PATH_COUNT, "Malformed: Too many paths."),
MAKE_ERROR(tefNO_BYTECODE, "There is no WASM code to run, but a WASM-specific field was included."),
MAKE_ERROR(tefBYTECODE_NOT_INCLUDED, "WASM code requires a field that was not included."),
MAKE_ERROR(tefFIREWALL_BLOCK, "Transaction was blocked by the account's firewall."),
MAKE_ERROR(telLOCAL_ERROR, "Local failure."),
MAKE_ERROR(telBAD_DOMAIN, "Domain too long."),

View File

@@ -20,6 +20,7 @@
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Firewall.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Permissions.h>
@@ -1015,6 +1016,66 @@ Transactor::checkSign(PreclaimContext const& ctx)
return checkSign(ctx.view, ctx.flags, ctx.parentBatchId, idAccount, ctx.tx, ctx.j);
}
NotTEC
Transactor::checkFirewall(PreclaimContext const& ctx)
{
if (!ctx.view.rules().enabled(featureFirewall))
return tesSUCCESS;
auto const account = ctx.tx.getAccountID(sfAccount);
auto const sleFirewall = ctx.view.read(keylet::firewall(account));
if (!sleFirewall)
return tesSUCCESS;
if (sleFirewall->isFieldPresent(sfMaxFee) &&
ctx.tx.getFieldAmount(sfFee) > sleFirewall->getFieldAmount(sfMaxFee))
{
JLOG(ctx.j.trace()) << "Firewall: the fee exceeds the firewall's MaxFee";
return tefFIREWALL_BLOCK;
}
switch (firewallAction(ctx.tx.getTxnType()))
{
case FirewallAction::Allow:
return tesSUCCESS;
case FirewallAction::Block:
JLOG(ctx.j.trace()) << "Firewall: transaction type " << ctx.tx.getTxnType()
<< " is blocked while a firewall is set";
return tefFIREWALL_BLOCK;
case FirewallAction::Check:
break;
}
// A payment to itself, or one carrying paths, can deliver to an account the
// preauthorization check below would not see.
if (ctx.tx.getTxnType() == ttPAYMENT &&
(ctx.tx.getAccountID(sfDestination) == account || ctx.tx.isFieldPresent(sfPaths)))
{
JLOG(ctx.j.trace()) << "Firewall: a self payment or a payment with paths is blocked";
return tefFIREWALL_BLOCK;
}
if (!ctx.tx.isFieldPresent(sfDestination))
{
JLOG(ctx.j.trace()) << "Firewall: a checked transaction without a destination is blocked";
return tefFIREWALL_BLOCK;
}
if (!ctx.view.exists(
keylet::withdrawPreauth(
account,
ctx.tx.getAccountID(sfDestination),
ctx.tx[~sfDestinationTag].value_or(0))))
{
JLOG(ctx.j.trace()) << "Firewall: the destination is not preauthorized";
return tefFIREWALL_BLOCK;
}
return tesSUCCESS;
}
NotTEC
Transactor::checkSingleSign(
ReadView const& view,

View File

@@ -191,6 +191,9 @@ invokePreclaim(PreclaimContext const& ctx)
if (NotTEC const result = T::checkSign(ctx))
return result;
if (NotTEC const result = Transactor::checkFirewall(ctx))
return result;
return tesSUCCESS;
}())
return preSigResult;

View File

@@ -0,0 +1,53 @@
#include <xrpl/tx/invariants/FirewallInvariant.h>
#include <xrpl/basics/Log.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAccount.h> // IWYU pragma: keep
#include <xrpl/protocol/STLedgerEntry.h>
namespace xrpl {
void
ValidFirewall::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
{
if (after && after->getType() == ltFIREWALL)
firewalls_.emplace_back(before, after);
}
bool
ValidFirewall::finalize(
STTx const& tx,
TER const,
XRPAmount const,
ReadView const&,
beast::Journal const& j)
{
// A firewall cannot exist unless the amendment is enabled, so the amendment
// needs no separate check here.
for (auto const& [before, after] : firewalls_)
{
if (!after->isFieldPresent(sfOwner) || !after->isFieldPresent(sfCounterparty))
{
JLOG(j.fatal()) << "Invariant failed: a firewall is missing its owner "
"or its counterparty";
return false;
}
if (after->getAccountID(sfOwner) == after->getAccountID(sfCounterparty))
{
JLOG(j.fatal()) << "Invariant failed: a firewall's counterparty is its owner";
return false;
}
if (before && before->getAccountID(sfOwner) != after->getAccountID(sfOwner))
{
JLOG(j.fatal()) << "Invariant failed: a firewall's owner changed";
return false;
}
}
return true;
}
} // namespace xrpl

View File

@@ -28,6 +28,7 @@
#include <xrpl/tx/transactors/account/SignerListSet.h>
#include <xrpl/tx/transactors/delegate/DelegateSet.h>
#include <xrpl/tx/transactors/did/DIDDelete.h>
#include <xrpl/tx/transactors/firewall/WithdrawPreauth.h>
#include <xrpl/tx/transactors/oracle/OracleDelete.h>
#include <xrpl/tx/transactors/payment/DepositPreauth.h>
@@ -122,6 +123,18 @@ removeDepositPreauthFromLedger(
return DepositPreauth::removeFromLedger(view, delIndex, j);
}
TER
removeWithdrawPreauthFromLedger(
ServiceRegistry&,
ApplyView& view,
AccountID const&,
uint256 const& delIndex,
SLE::ref,
beast::Journal j)
{
return WithdrawPreauth::removeFromLedger(view, delIndex, j);
}
TER
removeNFTokenOfferFromLedger(
ServiceRegistry&,
@@ -211,6 +224,8 @@ nonObligationDeleter(LedgerEntryType t)
return removeCredentialFromLedger;
case ltDELEGATE:
return removeDelegateFromLedger;
case ltWITHDRAW_PREAUTH:
return removeWithdrawPreauthFromLedger;
default:
return nullptr;
}

View File

@@ -0,0 +1,145 @@
#include <xrpl/tx/transactors/firewall/FirewallDelete.h>
#include <xrpl/basics/Log.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
#include <xrpl/tx/transactors/firewall/WithdrawPreauth.h>
#include <cstdint>
#include <memory>
#include <utility>
namespace xrpl {
std::uint32_t
FirewallDelete::getFlagsMask(PreflightContext const& ctx)
{
return tfUniversalMask;
}
XRPAmount
FirewallDelete::calculateBaseFee(ReadView const& view, STTx const& tx)
{
auto const normalCost = Transactor::calculateBaseFee(view, tx);
auto const counterSig = tx.getFieldObject(sfCounterpartySignature);
std::size_t const signerCount = [&counterSig]() -> std::size_t {
if (counterSig.isFieldPresent(sfSigners))
return counterSig.getFieldArray(sfSigners).size();
return counterSig.isFieldPresent(sfTxnSignature) ? 1 : 0;
}();
return normalCost + (view.fees().base * signerCount);
}
NotTEC
FirewallDelete::preflight(PreflightContext const& ctx)
{
auto const counterSig = ctx.tx.getFieldObject(sfCounterpartySignature);
if (auto const ret = detail::preflightCheckSigningKey(counterSig, ctx.j))
return ret;
return tesSUCCESS;
}
TER
FirewallDelete::preclaim(PreclaimContext const& ctx)
{
auto const sleFirewall = ctx.view.read(keylet::firewall(ctx.tx.getFieldH256(sfFirewallID)));
if (!sleFirewall)
{
JLOG(ctx.j.trace()) << "FirewallDelete: the firewall was not found";
return tecNO_TARGET;
}
if (sleFirewall->getAccountID(sfOwner) != ctx.tx.getAccountID(sfAccount))
{
JLOG(ctx.j.trace()) << "FirewallDelete: the account does not own the firewall";
return tecNO_PERMISSION;
}
return Transactor::checkSign(
ctx.view,
ctx.flags,
ctx.parentBatchId,
sleFirewall->getAccountID(sfCounterparty),
ctx.tx.getFieldObject(sfCounterpartySignature),
ctx.j);
}
TER
FirewallDelete::doApply()
{
auto applyViewContext = ctx_.getApplyViewContext();
auto const sleOwner = view().peek(keylet::account(accountID_));
if (!sleOwner)
return tefINTERNAL; // LCOV_EXCL_LINE
uint256 const firewallID = ctx_.tx.getFieldH256(sfFirewallID);
auto const sleFirewall = view().peek(keylet::firewall(firewallID));
if (!sleFirewall)
return tefINTERNAL; // LCOV_EXCL_LINE
// The preauthorizations exist only to let value past the firewall, so they
// go with it.
auto const ter = cleanupOnAccountDelete(
view(),
keylet::ownerDir(accountID_),
[&](LedgerEntryType nodeType,
uint256 const& dirEntry,
std::shared_ptr<SLE>& sleItem) -> std::pair<TER, SkipEntry> {
if (nodeType == ltWITHDRAW_PREAUTH)
return {WithdrawPreauth::removeFromLedger(view(), dirEntry, j_), SkipEntry::No};
return {tesSUCCESS, SkipEntry::Yes};
},
j_);
if (ter != tesSUCCESS)
return ter; // LCOV_EXCL_LINE
std::uint64_t const page{(*sleFirewall)[sfOwnerNode]};
if (!view().dirRemove(keylet::ownerDir(accountID_), page, firewallID, false))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "FirewallDelete: could not remove the firewall from the owner dir";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
decreaseOwnerCountForObject(view(), sleOwner, sleFirewall, 1, j_);
view().erase(sleFirewall);
return tesSUCCESS;
}
void
FirewallDelete::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref)
{
}
bool
FirewallDelete::finalizeInvariants(
STTx const&,
TER,
XRPAmount,
ReadView const&,
beast::Journal const&)
{
return true;
}
} // namespace xrpl

View File

@@ -0,0 +1,303 @@
#include <xrpl/tx/transactors/firewall/FirewallSet.h>
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
#include <cstdint>
#include <memory>
namespace xrpl {
std::uint32_t
FirewallSet::getFlagsMask(PreflightContext const& ctx)
{
return tfUniversalMask;
}
XRPAmount
FirewallSet::calculateBaseFee(ReadView const& view, STTx const& tx)
{
auto const normalCost = Transactor::calculateBaseFee(view, tx);
// Each signature in the counterparty's signature, single or multi, adds one
// base fee. getFieldObject returns an empty object when the field is
// absent, which is the create case.
auto const counterSig = tx.getFieldObject(sfCounterpartySignature);
std::size_t const signerCount = [&counterSig]() -> std::size_t {
if (counterSig.isFieldPresent(sfSigners))
return counterSig.getFieldArray(sfSigners).size();
return counterSig.isFieldPresent(sfTxnSignature) ? 1 : 0;
}();
return normalCost + (view.fees().base * signerCount);
}
NotTEC
FirewallSet::preflight(PreflightContext const& ctx)
{
AccountID const account = ctx.tx.getAccountID(sfAccount);
bool const isCreate = !ctx.tx.isFieldPresent(sfFirewallID);
if (isCreate)
{
if (!ctx.tx.isFieldPresent(sfCounterparty))
{
JLOG(ctx.j.trace()) << "FirewallSet: sfCounterparty is required for creation";
return temMALFORMED;
}
if (account == ctx.tx.getAccountID(sfCounterparty))
{
JLOG(ctx.j.trace()) << "FirewallSet: sfCounterparty must not be the account";
return temMALFORMED;
}
if (!ctx.tx.isFieldPresent(sfBackup))
{
JLOG(ctx.j.trace()) << "FirewallSet: sfBackup is required for creation";
return temMALFORMED;
}
if (account == ctx.tx.getAccountID(sfBackup))
{
JLOG(ctx.j.trace()) << "FirewallSet: sfBackup must not be the account";
return temMALFORMED;
}
if (ctx.tx.isFieldPresent(sfCounterpartySignature))
{
JLOG(ctx.j.trace())
<< "FirewallSet: sfCounterpartySignature is not allowed for creation";
return temMALFORMED;
}
}
else
{
if (ctx.tx.isFieldPresent(sfBackup))
{
JLOG(ctx.j.trace()) << "FirewallSet: sfBackup is not allowed for an update";
return temMALFORMED;
}
if (ctx.tx.isFieldPresent(sfCounterparty) && account == ctx.tx.getAccountID(sfCounterparty))
{
JLOG(ctx.j.trace()) << "FirewallSet: sfCounterparty must not be the account";
return temMALFORMED;
}
if (!ctx.tx.isFieldPresent(sfCounterpartySignature))
{
JLOG(ctx.j.trace()) << "FirewallSet: sfCounterpartySignature is required for an update";
return temBAD_SIGNER;
}
auto const counterSig = ctx.tx.getFieldObject(sfCounterpartySignature);
if (auto const ret = detail::preflightCheckSigningKey(counterSig, ctx.j))
return ret;
}
if (ctx.tx.isFieldPresent(sfMaxFee))
{
auto const maxFee = ctx.tx.getFieldAmount(sfMaxFee);
if (!maxFee.native() || maxFee.negative() || !isLegalNet(maxFee))
{
JLOG(ctx.j.trace()) << "FirewallSet: sfMaxFee is invalid";
return temBAD_AMOUNT;
}
}
return tesSUCCESS;
}
TER
FirewallSet::preclaim(PreclaimContext const& ctx)
{
AccountID const account = ctx.tx.getAccountID(sfAccount);
if (!ctx.tx.isFieldPresent(sfFirewallID))
{
if (ctx.view.exists(keylet::firewall(account)))
{
JLOG(ctx.j.trace()) << "FirewallSet: a firewall already exists for the account";
return tecDUPLICATE;
}
if (!ctx.view.exists(keylet::account(ctx.tx.getAccountID(sfCounterparty))))
{
JLOG(ctx.j.trace()) << "FirewallSet: the counterparty account does not exist";
return tecNO_DST;
}
if (!ctx.view.exists(keylet::account(ctx.tx.getAccountID(sfBackup))))
{
JLOG(ctx.j.trace()) << "FirewallSet: the backup account does not exist";
return tecNO_DST;
}
return tesSUCCESS;
}
auto const sleFirewall = ctx.view.read(keylet::firewall(ctx.tx.getFieldH256(sfFirewallID)));
if (!sleFirewall)
{
JLOG(ctx.j.trace()) << "FirewallSet: the firewall was not found";
return tecNO_TARGET;
}
if (sleFirewall->getAccountID(sfOwner) != account)
{
JLOG(ctx.j.trace()) << "FirewallSet: the account does not own the firewall";
return tecNO_PERMISSION;
}
if (ctx.tx.isFieldPresent(sfCounterparty))
{
AccountID const newCounterparty = ctx.tx.getAccountID(sfCounterparty);
if (sleFirewall->getAccountID(sfCounterparty) == newCounterparty)
{
JLOG(ctx.j.trace()) << "FirewallSet: sfCounterparty matches the recorded counterparty";
return tecDUPLICATE;
}
if (!ctx.view.exists(keylet::account(newCounterparty)))
{
JLOG(ctx.j.trace()) << "FirewallSet: the new counterparty account does not exist";
return tecNO_DST;
}
}
// The counterparty recorded on the firewall authorizes the update, not
// whoever the transaction names.
return Transactor::checkSign(
ctx.view,
ctx.flags,
ctx.parentBatchId,
sleFirewall->getAccountID(sfCounterparty),
ctx.tx.getFieldObject(sfCounterpartySignature),
ctx.j);
}
TER
FirewallSet::createFirewall(std::shared_ptr<SLE> const& sleOwner)
{
auto applyViewContext = ctx_.getApplyViewContext();
// A create inserts the firewall and the backup preauthorization.
if (auto const ret =
checkReserve(applyViewContext, sleOwner, preFeeBalance_, {.ownerCountDelta = 2}, j_);
!isTesSuccess(ret))
return ret;
auto const sleFirewall = std::make_shared<SLE>(keylet::firewall(accountID_));
sleFirewall->setAccountID(sfOwner, accountID_);
sleFirewall->setAccountID(sfCounterparty, ctx_.tx.getAccountID(sfCounterparty));
if (ctx_.tx.isFieldPresent(sfMaxFee))
sleFirewall->setFieldAmount(sfMaxFee, ctx_.tx.getFieldAmount(sfMaxFee));
view().insert(sleFirewall);
if (auto const page = view().dirInsert(
keylet::ownerDir(accountID_), sleFirewall->key(), describeOwnerDir(accountID_)))
{
sleFirewall->setFieldU64(sfOwnerNode, *page);
}
else
{
return tecDIR_FULL; // LCOV_EXCL_LINE
}
increaseOwnerCount(applyViewContext, sleOwner, 1, j_);
addSponsorToLedgerEntry(applyViewContext, sleFirewall);
// The backup destination is preauthorized as the firewall is created, so
// the account always has one destination it can still reach.
AccountID const backup = ctx_.tx.getAccountID(sfBackup);
std::uint32_t const dtag = ctx_.tx[~sfDestinationTag].value_or(0);
Keylet const preauthKeylet = keylet::withdrawPreauth(accountID_, backup, dtag);
auto const slePreauth = std::make_shared<SLE>(preauthKeylet);
slePreauth->setAccountID(sfAccount, accountID_);
slePreauth->setAccountID(sfAuthorize, backup);
slePreauth->setFieldU32(sfDestinationTag, dtag);
view().insert(slePreauth);
if (auto const page = view().dirInsert(
keylet::ownerDir(accountID_), preauthKeylet, describeOwnerDir(accountID_)))
{
slePreauth->setFieldU64(sfOwnerNode, *page);
}
else
{
return tecDIR_FULL; // LCOV_EXCL_LINE
}
increaseOwnerCount(applyViewContext, sleOwner, 1, j_);
addSponsorToLedgerEntry(applyViewContext, slePreauth);
return tesSUCCESS;
}
TER
FirewallSet::updateFirewall()
{
auto const sleFirewall = view().peek(keylet::firewall(ctx_.tx.getFieldH256(sfFirewallID)));
if (!sleFirewall)
return tefINTERNAL; // LCOV_EXCL_LINE
if (ctx_.tx.isFieldPresent(sfCounterparty))
sleFirewall->setAccountID(sfCounterparty, ctx_.tx.getAccountID(sfCounterparty));
// A zero MaxFee removes the cap.
if (ctx_.tx.isFieldPresent(sfMaxFee))
{
if (ctx_.tx.getFieldAmount(sfMaxFee) == beast::kZero)
sleFirewall->makeFieldAbsent(sfMaxFee);
else
sleFirewall->setFieldAmount(sfMaxFee, ctx_.tx.getFieldAmount(sfMaxFee));
}
view().update(sleFirewall);
return tesSUCCESS;
}
TER
FirewallSet::doApply()
{
auto const sleOwner = view().peek(keylet::account(accountID_));
if (!sleOwner)
return tefINTERNAL; // LCOV_EXCL_LINE
if (!ctx_.tx.isFieldPresent(sfFirewallID))
return createFirewall(sleOwner);
return updateFirewall();
}
void
FirewallSet::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref)
{
}
bool
FirewallSet::finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&)
{
return true;
}
} // namespace xrpl

View File

@@ -0,0 +1,223 @@
#include <xrpl/tx/transactors/firewall/WithdrawPreauth.h>
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
#include <cstdint>
#include <memory>
namespace xrpl {
std::uint32_t
WithdrawPreauth::getFlagsMask(PreflightContext const& ctx)
{
return tfUniversalMask;
}
XRPAmount
WithdrawPreauth::calculateBaseFee(ReadView const& view, STTx const& tx)
{
auto const normalCost = Transactor::calculateBaseFee(view, tx);
auto const counterSig = tx.getFieldObject(sfCounterpartySignature);
std::size_t const signerCount = [&counterSig]() -> std::size_t {
if (counterSig.isFieldPresent(sfSigners))
return counterSig.getFieldArray(sfSigners).size();
return counterSig.isFieldPresent(sfTxnSignature) ? 1 : 0;
}();
return normalCost + (view.fees().base * signerCount);
}
NotTEC
WithdrawPreauth::preflight(PreflightContext const& ctx)
{
auto const& tx = ctx.tx;
auto const& j = ctx.j;
auto const optAuth = tx[~sfAuthorize];
auto const optUnauth = tx[~sfUnauthorize];
if (static_cast<bool>(optAuth) == static_cast<bool>(optUnauth))
{
JLOG(j.trace()) << "WithdrawPreauth: exactly one of sfAuthorize and "
"sfUnauthorize is required";
return temMALFORMED;
}
AccountID const target{optAuth ? *optAuth : *optUnauth};
if (target == beast::kZero)
{
JLOG(j.trace()) << "WithdrawPreauth: the authorized account is zeroed";
return temINVALID_ACCOUNT_ID;
}
if (optAuth && (target == tx[sfAccount]))
{
JLOG(j.trace()) << "WithdrawPreauth: an account may not preauthorize itself";
return temCANNOT_PREAUTH_SELF;
}
auto const counterSig = tx.getFieldObject(sfCounterpartySignature);
if (auto const ret = detail::preflightCheckSigningKey(counterSig, j))
return ret;
return tesSUCCESS;
}
TER
WithdrawPreauth::preclaim(PreclaimContext const& ctx)
{
AccountID const accountID = ctx.tx[sfAccount];
std::uint32_t const dtag = ctx.tx[~sfDestinationTag].value_or(0);
if (ctx.tx.isFieldPresent(sfAuthorize))
{
AccountID const auth{ctx.tx[sfAuthorize]};
if (!ctx.view.exists(keylet::account(auth)))
return tecNO_TARGET;
if (ctx.view.exists(keylet::withdrawPreauth(accountID, auth, dtag)))
return tecDUPLICATE;
}
else
{
AccountID const unauth{ctx.tx[sfUnauthorize]};
if (!ctx.view.exists(keylet::withdrawPreauth(accountID, unauth, dtag)))
return tecNO_ENTRY;
}
auto const sleFirewall = ctx.view.read(keylet::firewall(accountID));
if (!sleFirewall)
{
JLOG(ctx.j.trace()) << "WithdrawPreauth: the account has no firewall";
return tecNO_TARGET;
}
if (sleFirewall->key() != ctx.tx.getFieldH256(sfFirewallID))
{
JLOG(ctx.j.trace()) << "WithdrawPreauth: sfFirewallID is not the account's firewall";
return tecNO_PERMISSION;
}
return Transactor::checkSign(
ctx.view,
ctx.flags,
ctx.parentBatchId,
sleFirewall->getAccountID(sfCounterparty),
ctx.tx.getFieldObject(sfCounterpartySignature),
ctx.j);
}
TER
WithdrawPreauth::doApply()
{
std::uint32_t const dtag = ctx_.tx[~sfDestinationTag].value_or(0);
if (!ctx_.tx.isFieldPresent(sfAuthorize))
{
auto const preauth = keylet::withdrawPreauth(accountID_, ctx_.tx[sfUnauthorize], dtag);
return WithdrawPreauth::removeFromLedger(view(), preauth.key, j_);
}
auto applyViewContext = ctx_.getApplyViewContext();
auto const sleOwner = view().peek(keylet::account(accountID_));
if (!sleOwner)
return tefINTERNAL; // LCOV_EXCL_LINE
// The preauthorization counts against the owner's reserve, checked against
// the starting balance so the fee may still dip into it.
if (auto const ret =
checkReserve(applyViewContext, sleOwner, preFeeBalance_, {.ownerCountDelta = 1}, j_);
!isTesSuccess(ret))
return ret;
AccountID const auth{ctx_.tx[sfAuthorize]};
Keylet const preauthKeylet = keylet::withdrawPreauth(accountID_, auth, dtag);
auto const slePreauth = std::make_shared<SLE>(preauthKeylet);
slePreauth->setAccountID(sfAccount, accountID_);
slePreauth->setAccountID(sfAuthorize, auth);
slePreauth->setFieldU32(sfDestinationTag, dtag);
view().insert(slePreauth);
auto const page =
view().dirInsert(keylet::ownerDir(accountID_), preauthKeylet, describeOwnerDir(accountID_));
JLOG(j_.trace()) << "WithdrawPreauth: adding " << to_string(preauthKeylet.key)
<< " to the owner directory: " << (page ? "success" : "failure");
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
slePreauth->setFieldU64(sfOwnerNode, *page);
increaseOwnerCount(applyViewContext, sleOwner, 1, j_);
addSponsorToLedgerEntry(applyViewContext, slePreauth);
return tesSUCCESS;
}
TER
WithdrawPreauth::removeFromLedger(ApplyView& view, uint256 const& preauthIndex, beast::Journal j)
{
auto const slePreauth = view.peek(keylet::withdrawPreauth(preauthIndex));
if (!slePreauth)
{
JLOG(j.warn()) << "WithdrawPreauth: the preauthorization 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))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "WithdrawPreauth: could not remove it from the owner directory";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
auto const sleOwner = view.peek(keylet::account(account));
if (!sleOwner)
return tefINTERNAL; // LCOV_EXCL_LINE
decreaseOwnerCountForObject(view, sleOwner, slePreauth, 1, j);
view.erase(slePreauth);
return tesSUCCESS;
}
void
WithdrawPreauth::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref)
{
}
bool
WithdrawPreauth::finalizeInvariants(
STTx const&,
TER,
XRPAmount,
ReadView const&,
beast::Journal const&)
{
return true;
}
} // namespace xrpl

View File

@@ -0,0 +1,428 @@
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/acctdelete.h>
#include <test/jtx/amount.h>
#include <test/jtx/fee.h>
#include <test/jtx/noop.h>
#include <test/jtx/offer.h>
#include <test/jtx/owners.h>
#include <test/jtx/paths.h>
#include <test/jtx/pay.h>
#include <test/jtx/sig.h>
#include <test/jtx/ter.h>
#include <test/jtx/trust.h>
#include <test/jtx/utility.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/jss.h>
namespace xrpl::test {
class Firewall_test : public beast::unit_test::Suite
{
// A counterparty signature adds one base fee to the minimum.
static inline jtx::Fee const kSignedFee{jtx::drops(20)};
static uint256
firewallID(jtx::Account const& account)
{
return keylet::firewall(account.id()).key;
}
static bool
hasFirewall(jtx::Env const& env, jtx::Account const& account)
{
return env.le(keylet::firewall(account.id())) != nullptr;
}
void
testEnabled(FeatureBitset features)
{
testcase("enabled");
using namespace jtx;
// Without the amendment the transactions are disabled.
{
Env env{*this, features - featureFirewall};
Account const alice{"alice"};
Account const carol{"carol"};
Account const backup{"backup"};
env.fund(XRP(10000), alice, carol, backup);
env.close();
env(firewall::set(alice.id(), carol.id(), backup.id()), Ter(temDISABLED));
env.close();
BEAST_EXPECT(!hasFirewall(env, alice));
}
// With it, a firewall is created.
{
Env env{*this, features};
Account const alice{"alice"};
Account const carol{"carol"};
Account const backup{"backup"};
env.fund(XRP(10000), alice, carol, backup);
env.close();
auto const ownersBefore = ownerCount(env, alice);
env(firewall::set(alice.id(), carol.id(), backup.id()));
env.close();
BEAST_EXPECT(hasFirewall(env, alice));
// The firewall and the backup preauthorization.
BEAST_EXPECT(ownerCount(env, alice) == ownersBefore + 2);
auto const sle = env.le(keylet::firewall(alice.id()));
BEAST_EXPECT(sle && sle->getAccountID(sfOwner) == alice.id());
BEAST_EXPECT(sle && sle->getAccountID(sfCounterparty) == carol.id());
}
}
void
testCreatePreflight(FeatureBitset features)
{
testcase("create preflight");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice"};
Account const carol{"carol"};
Account const backup{"backup"};
env.fund(XRP(10000), alice, carol, backup);
env.close();
// The counterparty is required.
{
auto jv = firewall::set(alice.id(), carol.id(), backup.id());
jv.removeMember(sfCounterparty.jsonName);
env(jv, Ter(temMALFORMED));
}
// The backup is required.
{
auto jv = firewall::set(alice.id(), carol.id(), backup.id());
jv.removeMember(sfBackup.jsonName);
env(jv, Ter(temMALFORMED));
}
// Neither may be the account itself.
env(firewall::set(alice.id(), alice.id(), backup.id()), Ter(temMALFORMED));
env(firewall::set(alice.id(), carol.id(), alice.id()), Ter(temMALFORMED));
// A creation carries no counterparty signature.
env(firewall::set(alice.id(), carol.id(), backup.id()),
Sig(sfCounterpartySignature, carol),
kSignedFee,
Ter(temMALFORMED));
env.close();
BEAST_EXPECT(!hasFirewall(env, alice));
}
void
testCreatePreclaim(FeatureBitset features)
{
testcase("create preclaim");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice"};
Account const carol{"carol"};
Account const backup{"backup"};
Account const absent{"absent"};
env.fund(XRP(10000), alice, carol, backup);
env.close();
// The counterparty and the backup must exist.
env(firewall::set(alice.id(), absent.id(), backup.id()), Ter(tecNO_DST));
env(firewall::set(alice.id(), carol.id(), absent.id()), Ter(tecNO_DST));
env.close();
env(firewall::set(alice.id(), carol.id(), backup.id()));
env.close();
BEAST_EXPECT(hasFirewall(env, alice));
// Only one firewall per account.
env(firewall::set(alice.id(), carol.id(), backup.id()), Ter(tecDUPLICATE));
env.close();
}
void
testUpdate(FeatureBitset features)
{
testcase("update");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice"};
Account const carol{"carol"};
Account const dave{"dave"};
Account const backup{"backup"};
env.fund(XRP(10000), alice, carol, dave, backup);
env.close();
env(firewall::set(alice.id(), carol.id(), backup.id()));
env.close();
auto const fwID = firewallID(alice);
// An update needs the counterparty's signature.
env(firewall::set(alice.id(), fwID), Ter(temBAD_SIGNER));
// Signed by someone other than the recorded counterparty.
env(firewall::set(alice.id(), fwID),
Sig(sfCounterpartySignature, dave),
kSignedFee,
Ter(tefBAD_AUTH));
env.close();
// Signed by the counterparty, changing the counterparty.
env(firewall::set(alice.id(), fwID),
firewall::kCounterparty(dave),
Sig(sfCounterpartySignature, carol),
kSignedFee);
env.close();
auto const sle = env.le(keylet::firewall(alice.id()));
BEAST_EXPECT(sle && sle->getAccountID(sfCounterparty) == dave.id());
// The old counterparty can no longer authorize.
env(firewall::set(alice.id(), fwID),
Sig(sfCounterpartySignature, carol),
kSignedFee,
Ter(tefBAD_AUTH));
env.close();
}
void
testBlockedAndAllowed(FeatureBitset features)
{
testcase("blocked and allowed");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice"};
Account const carol{"carol"};
Account const backup{"backup"};
Account const stranger{"stranger"};
env.fund(XRP(10000), alice, carol, backup, stranger);
env.close();
env(firewall::set(alice.id(), carol.id(), backup.id()));
env.close();
// The backup was preauthorized when the firewall was created.
env(pay(alice, backup, XRP(10)));
env.close();
// An unauthorized destination is blocked.
env(pay(alice, stranger, XRP(10)), Ter(tefFIREWALL_BLOCK));
// Paths are blocked even to a preauthorized destination, because they
// can deliver somewhere the preauthorization check never sees. A
// cross-currency send is used so the pathfinder actually sets sfPaths.
Account const gw{"gw"};
env.fund(XRP(10000), gw);
env.close();
env.trust(gw["USD"](1000), backup);
env.close();
env(offer(gw, XRP(100), gw["USD"](100)));
env.close();
env(pay(alice, backup, gw["USD"](10)), Paths(XRP), Ter(tefFIREWALL_BLOCK));
// A blocked transaction type is rejected whatever its destination.
env(offer(alice, XRP(10), alice["USD"](10)), Ter(tefFIREWALL_BLOCK));
// A transaction the firewall allows still works.
env(noop(alice));
env.close();
// An account without a firewall is unaffected.
env(pay(stranger, alice, XRP(10)));
env.close();
}
void
testPreauth(FeatureBitset features)
{
testcase("preauth");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice"};
Account const carol{"carol"};
Account const backup{"backup"};
Account const stranger{"stranger"};
env.fund(XRP(10000), alice, carol, backup, stranger);
env.close();
env(firewall::set(alice.id(), carol.id(), backup.id()));
env.close();
auto const fwID = firewallID(alice);
env(pay(alice, stranger, XRP(10)), Ter(tefFIREWALL_BLOCK));
// Preauthorizing needs the counterparty's signature.
env(firewall::authorize(alice.id(), fwID, stranger.id()), Ter(temMALFORMED));
env(firewall::authorize(alice.id(), fwID, stranger.id()),
Sig(sfCounterpartySignature, carol),
kSignedFee);
env.close();
// Now the payment goes through.
env(pay(alice, stranger, XRP(10)));
env.close();
// Authorizing the same destination twice fails.
env(firewall::authorize(alice.id(), fwID, stranger.id()),
Sig(sfCounterpartySignature, carol),
kSignedFee,
Ter(tecDUPLICATE));
// An account may not preauthorize itself.
env(firewall::authorize(alice.id(), fwID, alice.id()),
Sig(sfCounterpartySignature, carol),
kSignedFee,
Ter(temCANNOT_PREAUTH_SELF));
env.close();
// Removing the preauthorization blocks it again.
env(firewall::unauthorize(alice.id(), fwID, stranger.id()),
Sig(sfCounterpartySignature, carol),
kSignedFee);
env.close();
env(pay(alice, stranger, XRP(10)), Ter(tefFIREWALL_BLOCK));
env.close();
// Removing one that does not exist fails.
env(firewall::unauthorize(alice.id(), fwID, stranger.id()),
Sig(sfCounterpartySignature, carol),
kSignedFee,
Ter(tecNO_ENTRY));
env.close();
}
void
testMaxFee(FeatureBitset features)
{
testcase("max fee");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice"};
Account const carol{"carol"};
Account const backup{"backup"};
env.fund(XRP(10000), alice, carol, backup);
env.close();
auto jv = firewall::set(alice.id(), carol.id(), backup.id());
jv[sfMaxFee.jsonName] = to_string(XRP(1).value());
env(jv);
env.close();
// A fee above the cap is blocked, one at or below it is not.
env(noop(alice), Fee(XRP(2)), Ter(tefFIREWALL_BLOCK));
env(noop(alice), Fee(XRP(1)));
env.close();
}
void
testAccountDelete(FeatureBitset features)
{
testcase("account delete");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice"};
Account const carol{"carol"};
Account const backup{"backup"};
env.fund(XRP(10000), alice, carol, backup);
env.close();
env(firewall::set(alice.id(), carol.id(), backup.id()));
env.close();
// The ledger sequence must advance far enough for AccountDelete.
for (int i = 0; i < 256; ++i)
env.close();
// A firewall is an obligation, so the account cannot be deleted while
// one is set.
env(acctdelete(alice, backup),
Fee(drops(env.current()->fees().increment)),
Ter(tecHAS_OBLIGATIONS));
env.close();
}
void
testDelete(FeatureBitset features)
{
testcase("delete");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice"};
Account const carol{"carol"};
Account const backup{"backup"};
Account const stranger{"stranger"};
env.fund(XRP(10000), alice, carol, backup, stranger);
env.close();
auto const ownersBefore = ownerCount(env, alice);
env(firewall::set(alice.id(), carol.id(), backup.id()));
env.close();
auto const fwID = firewallID(alice);
env(firewall::authorize(alice.id(), fwID, stranger.id()),
Sig(sfCounterpartySignature, carol),
kSignedFee);
env.close();
BEAST_EXPECT(ownerCount(env, alice) == ownersBefore + 3);
// A delete needs the counterparty's signature.
env(firewall::del(alice.id(), fwID), Ter(temMALFORMED));
env(firewall::del(alice.id(), fwID),
Sig(sfCounterpartySignature, stranger),
kSignedFee,
Ter(tefBAD_AUTH));
env.close();
env(firewall::del(alice.id(), fwID), Sig(sfCounterpartySignature, carol), kSignedFee);
env.close();
// The firewall and every preauthorization it owned are gone, and the
// reserve comes back.
BEAST_EXPECT(!hasFirewall(env, alice));
BEAST_EXPECT(ownerCount(env, alice) == ownersBefore);
// Value moves freely again.
env(pay(alice, stranger, XRP(10)));
env.close();
}
public:
void
run() override
{
using namespace jtx;
auto const all = jtx::testableAmendments();
testEnabled(all);
testCreatePreflight(all);
testCreatePreclaim(all);
testUpdate(all);
testBlockedAndAllowed(all);
testPreauth(all);
testMaxFee(all);
testAccountDelete(all);
testDelete(all);
}
};
BEAST_DEFINE_TESTSUITE(Firewall, app, xrpl);
} // namespace xrpl::test

View File

@@ -913,6 +913,48 @@ auto const kDestination = JTxFieldWrapper<AccountIdField>(sfDestination);
} // namespace loan_broker
/* Firewall */
/******************************************************************************/
namespace firewall {
/**
* Create a firewall. The counterparty and backup are required on creation.
*/
json::Value
set(AccountID const& account, AccountID const& counterparty, AccountID const& backup);
/**
* Update the firewall named by firewallID. Needs a counterparty signature.
*/
json::Value
set(AccountID const& account, uint256 const& firewallID);
/**
* Delete a firewall. Needs a counterparty signature.
*/
json::Value
del(AccountID const& account, uint256 const& firewallID);
/**
* Preauthorize a destination. Needs a counterparty signature.
*/
json::Value
authorize(AccountID const& account, uint256 const& firewallID, AccountID const& authorized);
/**
* Remove a preauthorized destination. Needs a counterparty signature.
*/
json::Value
unauthorize(AccountID const& account, uint256 const& firewallID, AccountID const& unauthorized);
auto const kCounterparty = JTxFieldWrapper<AccountIdField>(sfCounterparty);
auto const kBackup = JTxFieldWrapper<AccountIdField>(sfBackup);
// For `CounterpartySignature`, use `Sig(sfCounterpartySignature, ...)`
} // namespace firewall
/* Loan */
/******************************************************************************/
namespace loan {

View File

@@ -815,6 +815,63 @@ coverClawback(AccountID const& account, std::uint32_t flags)
/* Loan */
/******************************************************************************/
namespace firewall {
json::Value
set(AccountID const& account, AccountID const& counterparty, AccountID const& backup)
{
json::Value jv;
jv[sfTransactionType] = jss::FirewallSet;
jv[sfAccount] = to_string(account);
jv[sfCounterparty] = to_string(counterparty);
jv[sfBackup] = to_string(backup);
return jv;
}
json::Value
set(AccountID const& account, uint256 const& firewallID)
{
json::Value jv;
jv[sfTransactionType] = jss::FirewallSet;
jv[sfAccount] = to_string(account);
jv[sfFirewallID] = to_string(firewallID);
return jv;
}
json::Value
del(AccountID const& account, uint256 const& firewallID)
{
json::Value jv;
jv[sfTransactionType] = jss::FirewallDelete;
jv[sfAccount] = to_string(account);
jv[sfFirewallID] = to_string(firewallID);
return jv;
}
json::Value
authorize(AccountID const& account, uint256 const& firewallID, AccountID const& authorized)
{
json::Value jv;
jv[sfTransactionType] = jss::WithdrawPreauth;
jv[sfAccount] = to_string(account);
jv[sfFirewallID] = to_string(firewallID);
jv[sfAuthorize] = to_string(authorized);
return jv;
}
json::Value
unauthorize(AccountID const& account, uint256 const& firewallID, AccountID const& unauthorized)
{
json::Value jv;
jv[sfTransactionType] = jss::WithdrawPreauth;
jv[sfAccount] = to_string(account);
jv[sfFirewallID] = to_string(firewallID);
jv[sfUnauthorize] = to_string(unauthorized);
return jv;
}
} // namespace firewall
namespace loan {
json::Value

View File

@@ -0,0 +1,253 @@
// Auto-generated unit tests for ledger entry Firewall
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol_autogen/ledger_entries/Firewall.h>
#include <xrpl/protocol_autogen/ledger_entries/Ticket.h>
#include <string>
namespace xrpl::ledger_entries {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed for both the
// builder's STObject and the wrapper's SLE.
TEST(FirewallTests, BuilderSettersRoundTrip)
{
uint256 const index{1u};
auto const ownerValue = canonical_ACCOUNT();
auto const counterpartyValue = canonical_ACCOUNT();
auto const maxFeeValue = canonical_AMOUNT();
auto const ownerNodeValue = canonical_UINT64();
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
FirewallBuilder builder{
ownerValue,
counterpartyValue,
ownerNodeValue,
previousTxnIDValue,
previousTxnLgrSeqValue
};
builder.setMaxFee(maxFeeValue);
builder.setLedgerIndex(index);
builder.setFlags(0x1u);
EXPECT_TRUE(builder.validate());
auto const entry = builder.build(index);
EXPECT_TRUE(entry.validate());
{
auto const& expected = ownerValue;
auto const actual = entry.getOwner();
expectEqualField(expected, actual, "sfOwner");
}
{
auto const& expected = counterpartyValue;
auto const actual = entry.getCounterparty();
expectEqualField(expected, actual, "sfCounterparty");
}
{
auto const& expected = ownerNodeValue;
auto const actual = entry.getOwnerNode();
expectEqualField(expected, actual, "sfOwnerNode");
}
{
auto const& expected = previousTxnIDValue;
auto const actual = entry.getPreviousTxnID();
expectEqualField(expected, actual, "sfPreviousTxnID");
}
{
auto const& expected = previousTxnLgrSeqValue;
auto const actual = entry.getPreviousTxnLgrSeq();
expectEqualField(expected, actual, "sfPreviousTxnLgrSeq");
}
{
auto const& expected = maxFeeValue;
auto const actualOpt = entry.getMaxFee();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfMaxFee");
EXPECT_TRUE(entry.hasMaxFee());
}
EXPECT_TRUE(entry.hasLedgerIndex());
auto const ledgerIndex = entry.getLedgerIndex();
ASSERT_TRUE(ledgerIndex.has_value());
EXPECT_EQ(*ledgerIndex, index);
EXPECT_EQ(entry.getKey(), index);
}
// 2 & 4) Start from an SLE, set fields directly on it, construct a builder
// from that SLE, build a new wrapper, and verify all fields (and validate()).
TEST(FirewallTests, BuilderFromSleRoundTrip)
{
uint256 const index{2u};
auto const ownerValue = canonical_ACCOUNT();
auto const counterpartyValue = canonical_ACCOUNT();
auto const maxFeeValue = canonical_AMOUNT();
auto const ownerNodeValue = canonical_UINT64();
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
auto sle = std::make_shared<SLE>(Firewall::entryType, index);
sle->at(sfOwner) = ownerValue;
sle->at(sfCounterparty) = counterpartyValue;
sle->at(sfMaxFee) = maxFeeValue;
sle->at(sfOwnerNode) = ownerNodeValue;
sle->at(sfPreviousTxnID) = previousTxnIDValue;
sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue;
FirewallBuilder builderFromSle{sle};
EXPECT_TRUE(builderFromSle.validate());
auto const entryFromBuilder = builderFromSle.build(index);
Firewall entryFromSle{sle};
EXPECT_TRUE(entryFromBuilder.validate());
EXPECT_TRUE(entryFromSle.validate());
{
auto const& expected = ownerValue;
auto const fromSle = entryFromSle.getOwner();
auto const fromBuilder = entryFromBuilder.getOwner();
expectEqualField(expected, fromSle, "sfOwner");
expectEqualField(expected, fromBuilder, "sfOwner");
}
{
auto const& expected = counterpartyValue;
auto const fromSle = entryFromSle.getCounterparty();
auto const fromBuilder = entryFromBuilder.getCounterparty();
expectEqualField(expected, fromSle, "sfCounterparty");
expectEqualField(expected, fromBuilder, "sfCounterparty");
}
{
auto const& expected = ownerNodeValue;
auto const fromSle = entryFromSle.getOwnerNode();
auto const fromBuilder = entryFromBuilder.getOwnerNode();
expectEqualField(expected, fromSle, "sfOwnerNode");
expectEqualField(expected, fromBuilder, "sfOwnerNode");
}
{
auto const& expected = previousTxnIDValue;
auto const fromSle = entryFromSle.getPreviousTxnID();
auto const fromBuilder = entryFromBuilder.getPreviousTxnID();
expectEqualField(expected, fromSle, "sfPreviousTxnID");
expectEqualField(expected, fromBuilder, "sfPreviousTxnID");
}
{
auto const& expected = previousTxnLgrSeqValue;
auto const fromSle = entryFromSle.getPreviousTxnLgrSeq();
auto const fromBuilder = entryFromBuilder.getPreviousTxnLgrSeq();
expectEqualField(expected, fromSle, "sfPreviousTxnLgrSeq");
expectEqualField(expected, fromBuilder, "sfPreviousTxnLgrSeq");
}
{
auto const& expected = maxFeeValue;
auto const fromSleOpt = entryFromSle.getMaxFee();
auto const fromBuilderOpt = entryFromBuilder.getMaxFee();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfMaxFee");
expectEqualField(expected, *fromBuilderOpt, "sfMaxFee");
}
EXPECT_EQ(entryFromSle.getKey(), index);
EXPECT_EQ(entryFromBuilder.getKey(), index);
}
// 3) Verify wrapper throws when constructed from wrong ledger entry type.
TEST(FirewallTests, WrapperThrowsOnWrongEntryType)
{
uint256 const index{3u};
// Build a valid ledger entry of a different type
// Ticket requires: Account, OwnerNode, TicketSequence, PreviousTxnID, PreviousTxnLgrSeq
// Check requires: Account, Destination, SendMax, Sequence, OwnerNode, DestinationNode, PreviousTxnID, PreviousTxnLgrSeq
TicketBuilder wrongBuilder{
canonical_ACCOUNT(),
canonical_UINT64(),
canonical_UINT32(),
canonical_UINT256(),
canonical_UINT32()};
auto wrongEntry = wrongBuilder.build(index);
EXPECT_THROW(Firewall{wrongEntry.getSle()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong ledger entry type.
TEST(FirewallTests, BuilderThrowsOnWrongEntryType)
{
uint256 const index{4u};
// Build a valid ledger entry of a different type
TicketBuilder wrongBuilder{
canonical_ACCOUNT(),
canonical_UINT64(),
canonical_UINT32(),
canonical_UINT256(),
canonical_UINT32()};
auto wrongEntry = wrongBuilder.build(index);
EXPECT_THROW(FirewallBuilder{wrongEntry.getSle()}, std::runtime_error);
}
// 5) Build with only required fields and verify optional fields return nullopt.
TEST(FirewallTests, OptionalFieldsReturnNullopt)
{
uint256 const index{3u};
auto const ownerValue = canonical_ACCOUNT();
auto const counterpartyValue = canonical_ACCOUNT();
auto const ownerNodeValue = canonical_UINT64();
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
FirewallBuilder builder{
ownerValue,
counterpartyValue,
ownerNodeValue,
previousTxnIDValue,
previousTxnLgrSeqValue
};
auto const entry = builder.build(index);
// Verify optional fields are not present
EXPECT_FALSE(entry.hasMaxFee());
EXPECT_FALSE(entry.getMaxFee().has_value());
}
}

View File

@@ -0,0 +1,253 @@
// Auto-generated unit tests for ledger entry WithdrawPreauth
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol_autogen/ledger_entries/WithdrawPreauth.h>
#include <xrpl/protocol_autogen/ledger_entries/Ticket.h>
#include <string>
namespace xrpl::ledger_entries {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed for both the
// builder's STObject and the wrapper's SLE.
TEST(WithdrawPreauthTests, BuilderSettersRoundTrip)
{
uint256 const index{1u};
auto const accountValue = canonical_ACCOUNT();
auto const authorizeValue = canonical_ACCOUNT();
auto const destinationTagValue = canonical_UINT32();
auto const ownerNodeValue = canonical_UINT64();
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
WithdrawPreauthBuilder builder{
accountValue,
authorizeValue,
ownerNodeValue,
previousTxnIDValue,
previousTxnLgrSeqValue
};
builder.setDestinationTag(destinationTagValue);
builder.setLedgerIndex(index);
builder.setFlags(0x1u);
EXPECT_TRUE(builder.validate());
auto const entry = builder.build(index);
EXPECT_TRUE(entry.validate());
{
auto const& expected = accountValue;
auto const actual = entry.getAccount();
expectEqualField(expected, actual, "sfAccount");
}
{
auto const& expected = authorizeValue;
auto const actual = entry.getAuthorize();
expectEqualField(expected, actual, "sfAuthorize");
}
{
auto const& expected = ownerNodeValue;
auto const actual = entry.getOwnerNode();
expectEqualField(expected, actual, "sfOwnerNode");
}
{
auto const& expected = previousTxnIDValue;
auto const actual = entry.getPreviousTxnID();
expectEqualField(expected, actual, "sfPreviousTxnID");
}
{
auto const& expected = previousTxnLgrSeqValue;
auto const actual = entry.getPreviousTxnLgrSeq();
expectEqualField(expected, actual, "sfPreviousTxnLgrSeq");
}
{
auto const& expected = destinationTagValue;
auto const actualOpt = entry.getDestinationTag();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfDestinationTag");
EXPECT_TRUE(entry.hasDestinationTag());
}
EXPECT_TRUE(entry.hasLedgerIndex());
auto const ledgerIndex = entry.getLedgerIndex();
ASSERT_TRUE(ledgerIndex.has_value());
EXPECT_EQ(*ledgerIndex, index);
EXPECT_EQ(entry.getKey(), index);
}
// 2 & 4) Start from an SLE, set fields directly on it, construct a builder
// from that SLE, build a new wrapper, and verify all fields (and validate()).
TEST(WithdrawPreauthTests, BuilderFromSleRoundTrip)
{
uint256 const index{2u};
auto const accountValue = canonical_ACCOUNT();
auto const authorizeValue = canonical_ACCOUNT();
auto const destinationTagValue = canonical_UINT32();
auto const ownerNodeValue = canonical_UINT64();
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
auto sle = std::make_shared<SLE>(WithdrawPreauth::entryType, index);
sle->at(sfAccount) = accountValue;
sle->at(sfAuthorize) = authorizeValue;
sle->at(sfDestinationTag) = destinationTagValue;
sle->at(sfOwnerNode) = ownerNodeValue;
sle->at(sfPreviousTxnID) = previousTxnIDValue;
sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue;
WithdrawPreauthBuilder builderFromSle{sle};
EXPECT_TRUE(builderFromSle.validate());
auto const entryFromBuilder = builderFromSle.build(index);
WithdrawPreauth entryFromSle{sle};
EXPECT_TRUE(entryFromBuilder.validate());
EXPECT_TRUE(entryFromSle.validate());
{
auto const& expected = accountValue;
auto const fromSle = entryFromSle.getAccount();
auto const fromBuilder = entryFromBuilder.getAccount();
expectEqualField(expected, fromSle, "sfAccount");
expectEqualField(expected, fromBuilder, "sfAccount");
}
{
auto const& expected = authorizeValue;
auto const fromSle = entryFromSle.getAuthorize();
auto const fromBuilder = entryFromBuilder.getAuthorize();
expectEqualField(expected, fromSle, "sfAuthorize");
expectEqualField(expected, fromBuilder, "sfAuthorize");
}
{
auto const& expected = ownerNodeValue;
auto const fromSle = entryFromSle.getOwnerNode();
auto const fromBuilder = entryFromBuilder.getOwnerNode();
expectEqualField(expected, fromSle, "sfOwnerNode");
expectEqualField(expected, fromBuilder, "sfOwnerNode");
}
{
auto const& expected = previousTxnIDValue;
auto const fromSle = entryFromSle.getPreviousTxnID();
auto const fromBuilder = entryFromBuilder.getPreviousTxnID();
expectEqualField(expected, fromSle, "sfPreviousTxnID");
expectEqualField(expected, fromBuilder, "sfPreviousTxnID");
}
{
auto const& expected = previousTxnLgrSeqValue;
auto const fromSle = entryFromSle.getPreviousTxnLgrSeq();
auto const fromBuilder = entryFromBuilder.getPreviousTxnLgrSeq();
expectEqualField(expected, fromSle, "sfPreviousTxnLgrSeq");
expectEqualField(expected, fromBuilder, "sfPreviousTxnLgrSeq");
}
{
auto const& expected = destinationTagValue;
auto const fromSleOpt = entryFromSle.getDestinationTag();
auto const fromBuilderOpt = entryFromBuilder.getDestinationTag();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfDestinationTag");
expectEqualField(expected, *fromBuilderOpt, "sfDestinationTag");
}
EXPECT_EQ(entryFromSle.getKey(), index);
EXPECT_EQ(entryFromBuilder.getKey(), index);
}
// 3) Verify wrapper throws when constructed from wrong ledger entry type.
TEST(WithdrawPreauthTests, WrapperThrowsOnWrongEntryType)
{
uint256 const index{3u};
// Build a valid ledger entry of a different type
// Ticket requires: Account, OwnerNode, TicketSequence, PreviousTxnID, PreviousTxnLgrSeq
// Check requires: Account, Destination, SendMax, Sequence, OwnerNode, DestinationNode, PreviousTxnID, PreviousTxnLgrSeq
TicketBuilder wrongBuilder{
canonical_ACCOUNT(),
canonical_UINT64(),
canonical_UINT32(),
canonical_UINT256(),
canonical_UINT32()};
auto wrongEntry = wrongBuilder.build(index);
EXPECT_THROW(WithdrawPreauth{wrongEntry.getSle()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong ledger entry type.
TEST(WithdrawPreauthTests, BuilderThrowsOnWrongEntryType)
{
uint256 const index{4u};
// Build a valid ledger entry of a different type
TicketBuilder wrongBuilder{
canonical_ACCOUNT(),
canonical_UINT64(),
canonical_UINT32(),
canonical_UINT256(),
canonical_UINT32()};
auto wrongEntry = wrongBuilder.build(index);
EXPECT_THROW(WithdrawPreauthBuilder{wrongEntry.getSle()}, std::runtime_error);
}
// 5) Build with only required fields and verify optional fields return nullopt.
TEST(WithdrawPreauthTests, OptionalFieldsReturnNullopt)
{
uint256 const index{3u};
auto const accountValue = canonical_ACCOUNT();
auto const authorizeValue = canonical_ACCOUNT();
auto const ownerNodeValue = canonical_UINT64();
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
WithdrawPreauthBuilder builder{
accountValue,
authorizeValue,
ownerNodeValue,
previousTxnIDValue,
previousTxnLgrSeqValue
};
auto const entry = builder.build(index);
// Verify optional fields are not present
EXPECT_FALSE(entry.hasDestinationTag());
EXPECT_FALSE(entry.getDestinationTag().has_value());
}
}

View File

@@ -0,0 +1,162 @@
// Auto-generated unit tests for transaction FirewallDelete
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/FirewallDelete.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
namespace xrpl::transactions {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed.
TEST(TransactionsFirewallDeleteTests, BuilderSettersRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testFirewallDelete"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 1;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const counterpartySignatureValue = canonical_OBJECT();
auto const firewallIDValue = canonical_UINT256();
FirewallDeleteBuilder builder{
accountValue,
counterpartySignatureValue,
firewallIDValue,
sequenceValue,
feeValue
};
// Set optional fields
auto tx = builder.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(tx.validate(reason)) << reason;
// Verify signing was applied
EXPECT_FALSE(tx.getSigningPubKey().empty());
EXPECT_TRUE(tx.hasTxnSignature());
// Verify common fields
EXPECT_EQ(tx.getAccount(), accountValue);
EXPECT_EQ(tx.getSequence(), sequenceValue);
EXPECT_EQ(tx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = counterpartySignatureValue;
auto const actual = tx.getCounterpartySignature();
expectEqualField(expected, actual, "sfCounterpartySignature");
}
{
auto const& expected = firewallIDValue;
auto const actual = tx.getFirewallID();
expectEqualField(expected, actual, "sfFirewallID");
}
// Verify optional fields
}
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
// and verify all fields match.
TEST(TransactionsFirewallDeleteTests, BuilderFromStTxRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testFirewallDeleteFromTx"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 2;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const counterpartySignatureValue = canonical_OBJECT();
auto const firewallIDValue = canonical_UINT256();
// Build an initial transaction
FirewallDeleteBuilder initialBuilder{
accountValue,
counterpartySignatureValue,
firewallIDValue,
sequenceValue,
feeValue
};
auto initialTx = initialBuilder.build(publicKey, secretKey);
// Create builder from existing STTx
FirewallDeleteBuilder builderFromTx{initialTx.getSTTx()};
auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
// Verify common fields
EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
EXPECT_EQ(rebuiltTx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = counterpartySignatureValue;
auto const actual = rebuiltTx.getCounterpartySignature();
expectEqualField(expected, actual, "sfCounterpartySignature");
}
{
auto const& expected = firewallIDValue;
auto const actual = rebuiltTx.getFirewallID();
expectEqualField(expected, actual, "sfFirewallID");
}
// Verify optional fields
}
// 3) Verify wrapper throws when constructed from wrong transaction type.
TEST(TransactionsFirewallDeleteTests, WrapperThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(FirewallDelete{wrongTx.getSTTx()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong transaction type.
TEST(TransactionsFirewallDeleteTests, BuilderThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(FirewallDeleteBuilder{wrongTx.getSTTx()}, std::runtime_error);
}
}

View File

@@ -0,0 +1,282 @@
// Auto-generated unit tests for transaction FirewallSet
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/FirewallSet.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
namespace xrpl::transactions {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed.
TEST(TransactionsFirewallSetTests, BuilderSettersRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testFirewallSet"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 1;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const counterpartyValue = canonical_ACCOUNT();
auto const backupValue = canonical_ACCOUNT();
auto const maxFeeValue = canonical_AMOUNT();
auto const destinationTagValue = canonical_UINT32();
auto const counterpartySignatureValue = canonical_OBJECT();
auto const firewallIDValue = canonical_UINT256();
FirewallSetBuilder builder{
accountValue,
sequenceValue,
feeValue
};
// Set optional fields
builder.setCounterparty(counterpartyValue);
builder.setBackup(backupValue);
builder.setMaxFee(maxFeeValue);
builder.setDestinationTag(destinationTagValue);
builder.setCounterpartySignature(counterpartySignatureValue);
builder.setFirewallID(firewallIDValue);
auto tx = builder.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(tx.validate(reason)) << reason;
// Verify signing was applied
EXPECT_FALSE(tx.getSigningPubKey().empty());
EXPECT_TRUE(tx.hasTxnSignature());
// Verify common fields
EXPECT_EQ(tx.getAccount(), accountValue);
EXPECT_EQ(tx.getSequence(), sequenceValue);
EXPECT_EQ(tx.getFee(), feeValue);
// Verify required fields
// Verify optional fields
{
auto const& expected = counterpartyValue;
auto const actualOpt = tx.getCounterparty();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCounterparty should be present";
expectEqualField(expected, *actualOpt, "sfCounterparty");
EXPECT_TRUE(tx.hasCounterparty());
}
{
auto const& expected = backupValue;
auto const actualOpt = tx.getBackup();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfBackup should be present";
expectEqualField(expected, *actualOpt, "sfBackup");
EXPECT_TRUE(tx.hasBackup());
}
{
auto const& expected = maxFeeValue;
auto const actualOpt = tx.getMaxFee();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMaxFee should be present";
expectEqualField(expected, *actualOpt, "sfMaxFee");
EXPECT_TRUE(tx.hasMaxFee());
}
{
auto const& expected = destinationTagValue;
auto const actualOpt = tx.getDestinationTag();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
expectEqualField(expected, *actualOpt, "sfDestinationTag");
EXPECT_TRUE(tx.hasDestinationTag());
}
{
auto const& expected = counterpartySignatureValue;
auto const actualOpt = tx.getCounterpartySignature();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCounterpartySignature should be present";
expectEqualField(expected, *actualOpt, "sfCounterpartySignature");
EXPECT_TRUE(tx.hasCounterpartySignature());
}
{
auto const& expected = firewallIDValue;
auto const actualOpt = tx.getFirewallID();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFirewallID should be present";
expectEqualField(expected, *actualOpt, "sfFirewallID");
EXPECT_TRUE(tx.hasFirewallID());
}
}
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
// and verify all fields match.
TEST(TransactionsFirewallSetTests, BuilderFromStTxRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testFirewallSetFromTx"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 2;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const counterpartyValue = canonical_ACCOUNT();
auto const backupValue = canonical_ACCOUNT();
auto const maxFeeValue = canonical_AMOUNT();
auto const destinationTagValue = canonical_UINT32();
auto const counterpartySignatureValue = canonical_OBJECT();
auto const firewallIDValue = canonical_UINT256();
// Build an initial transaction
FirewallSetBuilder initialBuilder{
accountValue,
sequenceValue,
feeValue
};
initialBuilder.setCounterparty(counterpartyValue);
initialBuilder.setBackup(backupValue);
initialBuilder.setMaxFee(maxFeeValue);
initialBuilder.setDestinationTag(destinationTagValue);
initialBuilder.setCounterpartySignature(counterpartySignatureValue);
initialBuilder.setFirewallID(firewallIDValue);
auto initialTx = initialBuilder.build(publicKey, secretKey);
// Create builder from existing STTx
FirewallSetBuilder builderFromTx{initialTx.getSTTx()};
auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
// Verify common fields
EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
EXPECT_EQ(rebuiltTx.getFee(), feeValue);
// Verify required fields
// Verify optional fields
{
auto const& expected = counterpartyValue;
auto const actualOpt = rebuiltTx.getCounterparty();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCounterparty should be present";
expectEqualField(expected, *actualOpt, "sfCounterparty");
}
{
auto const& expected = backupValue;
auto const actualOpt = rebuiltTx.getBackup();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfBackup should be present";
expectEqualField(expected, *actualOpt, "sfBackup");
}
{
auto const& expected = maxFeeValue;
auto const actualOpt = rebuiltTx.getMaxFee();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMaxFee should be present";
expectEqualField(expected, *actualOpt, "sfMaxFee");
}
{
auto const& expected = destinationTagValue;
auto const actualOpt = rebuiltTx.getDestinationTag();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
expectEqualField(expected, *actualOpt, "sfDestinationTag");
}
{
auto const& expected = counterpartySignatureValue;
auto const actualOpt = rebuiltTx.getCounterpartySignature();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCounterpartySignature should be present";
expectEqualField(expected, *actualOpt, "sfCounterpartySignature");
}
{
auto const& expected = firewallIDValue;
auto const actualOpt = rebuiltTx.getFirewallID();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFirewallID should be present";
expectEqualField(expected, *actualOpt, "sfFirewallID");
}
}
// 3) Verify wrapper throws when constructed from wrong transaction type.
TEST(TransactionsFirewallSetTests, WrapperThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(FirewallSet{wrongTx.getSTTx()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong transaction type.
TEST(TransactionsFirewallSetTests, BuilderThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(FirewallSetBuilder{wrongTx.getSTTx()}, std::runtime_error);
}
// 5) Build with only required fields and verify optional fields return nullopt.
TEST(TransactionsFirewallSetTests, OptionalFieldsReturnNullopt)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testFirewallSetNullopt"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 3;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific required field values
FirewallSetBuilder builder{
accountValue,
sequenceValue,
feeValue
};
// Do NOT set optional fields
auto tx = builder.build(publicKey, secretKey);
// Verify optional fields are not present
EXPECT_FALSE(tx.hasCounterparty());
EXPECT_FALSE(tx.getCounterparty().has_value());
EXPECT_FALSE(tx.hasBackup());
EXPECT_FALSE(tx.getBackup().has_value());
EXPECT_FALSE(tx.hasMaxFee());
EXPECT_FALSE(tx.getMaxFee().has_value());
EXPECT_FALSE(tx.hasDestinationTag());
EXPECT_FALSE(tx.getDestinationTag().has_value());
EXPECT_FALSE(tx.hasCounterpartySignature());
EXPECT_FALSE(tx.getCounterpartySignature().has_value());
EXPECT_FALSE(tx.hasFirewallID());
EXPECT_FALSE(tx.getFirewallID().has_value());
}
}

View File

@@ -0,0 +1,255 @@
// Auto-generated unit tests for transaction WithdrawPreauth
#include <gtest/gtest.h>
#include <protocol_autogen/TestHelpers.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol_autogen/transactions/WithdrawPreauth.h>
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
#include <string>
namespace xrpl::transactions {
// 1 & 4) Set fields via builder setters, build, then read them back via
// wrapper getters. After build(), validate() should succeed.
TEST(TransactionsWithdrawPreauthTests, BuilderSettersRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWithdrawPreauth"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 1;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const authorizeValue = canonical_ACCOUNT();
auto const unauthorizeValue = canonical_ACCOUNT();
auto const destinationTagValue = canonical_UINT32();
auto const counterpartySignatureValue = canonical_OBJECT();
auto const firewallIDValue = canonical_UINT256();
WithdrawPreauthBuilder builder{
accountValue,
counterpartySignatureValue,
firewallIDValue,
sequenceValue,
feeValue
};
// Set optional fields
builder.setAuthorize(authorizeValue);
builder.setUnauthorize(unauthorizeValue);
builder.setDestinationTag(destinationTagValue);
auto tx = builder.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(tx.validate(reason)) << reason;
// Verify signing was applied
EXPECT_FALSE(tx.getSigningPubKey().empty());
EXPECT_TRUE(tx.hasTxnSignature());
// Verify common fields
EXPECT_EQ(tx.getAccount(), accountValue);
EXPECT_EQ(tx.getSequence(), sequenceValue);
EXPECT_EQ(tx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = counterpartySignatureValue;
auto const actual = tx.getCounterpartySignature();
expectEqualField(expected, actual, "sfCounterpartySignature");
}
{
auto const& expected = firewallIDValue;
auto const actual = tx.getFirewallID();
expectEqualField(expected, actual, "sfFirewallID");
}
// Verify optional fields
{
auto const& expected = authorizeValue;
auto const actualOpt = tx.getAuthorize();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuthorize should be present";
expectEqualField(expected, *actualOpt, "sfAuthorize");
EXPECT_TRUE(tx.hasAuthorize());
}
{
auto const& expected = unauthorizeValue;
auto const actualOpt = tx.getUnauthorize();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfUnauthorize should be present";
expectEqualField(expected, *actualOpt, "sfUnauthorize");
EXPECT_TRUE(tx.hasUnauthorize());
}
{
auto const& expected = destinationTagValue;
auto const actualOpt = tx.getDestinationTag();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
expectEqualField(expected, *actualOpt, "sfDestinationTag");
EXPECT_TRUE(tx.hasDestinationTag());
}
}
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
// and verify all fields match.
TEST(TransactionsWithdrawPreauthTests, BuilderFromStTxRoundTrip)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWithdrawPreauthFromTx"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 2;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific field values
auto const authorizeValue = canonical_ACCOUNT();
auto const unauthorizeValue = canonical_ACCOUNT();
auto const destinationTagValue = canonical_UINT32();
auto const counterpartySignatureValue = canonical_OBJECT();
auto const firewallIDValue = canonical_UINT256();
// Build an initial transaction
WithdrawPreauthBuilder initialBuilder{
accountValue,
counterpartySignatureValue,
firewallIDValue,
sequenceValue,
feeValue
};
initialBuilder.setAuthorize(authorizeValue);
initialBuilder.setUnauthorize(unauthorizeValue);
initialBuilder.setDestinationTag(destinationTagValue);
auto initialTx = initialBuilder.build(publicKey, secretKey);
// Create builder from existing STTx
WithdrawPreauthBuilder builderFromTx{initialTx.getSTTx()};
auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
std::string reason;
EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
// Verify common fields
EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
EXPECT_EQ(rebuiltTx.getFee(), feeValue);
// Verify required fields
{
auto const& expected = counterpartySignatureValue;
auto const actual = rebuiltTx.getCounterpartySignature();
expectEqualField(expected, actual, "sfCounterpartySignature");
}
{
auto const& expected = firewallIDValue;
auto const actual = rebuiltTx.getFirewallID();
expectEqualField(expected, actual, "sfFirewallID");
}
// Verify optional fields
{
auto const& expected = authorizeValue;
auto const actualOpt = rebuiltTx.getAuthorize();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuthorize should be present";
expectEqualField(expected, *actualOpt, "sfAuthorize");
}
{
auto const& expected = unauthorizeValue;
auto const actualOpt = rebuiltTx.getUnauthorize();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfUnauthorize should be present";
expectEqualField(expected, *actualOpt, "sfUnauthorize");
}
{
auto const& expected = destinationTagValue;
auto const actualOpt = rebuiltTx.getDestinationTag();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
expectEqualField(expected, *actualOpt, "sfDestinationTag");
}
}
// 3) Verify wrapper throws when constructed from wrong transaction type.
TEST(TransactionsWithdrawPreauthTests, WrapperThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(WithdrawPreauth{wrongTx.getSTTx()}, std::runtime_error);
}
// 4) Verify builder throws when constructed from wrong transaction type.
TEST(TransactionsWithdrawPreauthTests, BuilderThrowsOnWrongTxType)
{
// Build a valid transaction of a different type
auto const [pk, sk] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
auto const account = calcAccountID(pk);
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
auto wrongTx = wrongBuilder.build(pk, sk);
EXPECT_THROW(WithdrawPreauthBuilder{wrongTx.getSTTx()}, std::runtime_error);
}
// 5) Build with only required fields and verify optional fields return nullopt.
TEST(TransactionsWithdrawPreauthTests, OptionalFieldsReturnNullopt)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testWithdrawPreauthNullopt"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 3;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific required field values
auto const counterpartySignatureValue = canonical_OBJECT();
auto const firewallIDValue = canonical_UINT256();
WithdrawPreauthBuilder builder{
accountValue,
counterpartySignatureValue,
firewallIDValue,
sequenceValue,
feeValue
};
// Do NOT set optional fields
auto tx = builder.build(publicKey, secretKey);
// Verify optional fields are not present
EXPECT_FALSE(tx.hasAuthorize());
EXPECT_FALSE(tx.getAuthorize().has_value());
EXPECT_FALSE(tx.hasUnauthorize());
EXPECT_FALSE(tx.getUnauthorize().has_value());
EXPECT_FALSE(tx.hasDestinationTag());
EXPECT_FALSE(tx.getDestinationTag().has_value());
}
}

View File

@@ -212,6 +212,49 @@ parseCredential(
return keylet::credential(*subject, *issuer, Slice(credType->data(), credType->size())).key;
}
static std::expected<uint256, json::Value>
parseFirewall(
json::Value const& params,
json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
// A firewall is keyed by the account it protects, so an account string is
// enough to name one.
if (auto const account = ledger_entry_helpers::parse<AccountID>(params))
return keylet::firewall(*account).key;
return ledger_entry_helpers::invalidFieldError("malformedAddress", fieldName, "AccountID");
}
static std::expected<uint256, json::Value>
parseWithdrawPreauth(
json::Value const& params,
json::StaticString const fieldName,
[[maybe_unused]] unsigned const apiVersion)
{
if (!params.isObject())
{
return parseObjectID(params, fieldName);
}
auto const owner =
ledger_entry_helpers::requiredAccountID(params, jss::owner, "malformedOwner");
if (!owner)
return std::unexpected(owner.error());
auto const authorized =
ledger_entry_helpers::requiredAccountID(params, jss::authorized, "malformedAuthorized");
if (!authorized)
return std::unexpected(authorized.error());
std::uint32_t const dtag =
params.isMember(jss::destination_tag) && params[jss::destination_tag].isIntegral()
? params[jss::destination_tag].asUInt()
: 0;
return keylet::withdrawPreauth(*owner, *authorized, dtag).key;
}
static std::expected<uint256, json::Value>
parseDelegate(
json::Value const& params,