Compare commits

..

5 Commits

Author SHA1 Message Date
Nicholas Dudfield
1e6cda4d64 fix: add fatal log on amendment-blocked shutdown 2026-06-19 09:35:39 +07:00
Nicholas Dudfield
c146f15247 fix: rethrow runtime_error if not amendment blocked 2026-06-19 09:35:39 +07:00
Nicholas Dudfield
fb5081d1f4 fix: narrow catch to std::runtime_error in switchLastClosedLedger 2026-06-19 09:35:39 +07:00
Nicholas Dudfield
d4bec012a2 fix: skip signalStop in standalone mode for test compatibility 2026-06-19 09:35:39 +07:00
Nicholas Dudfield
f7187ba94f fix: fail fast when amendment blocked instead of zombie state
- signalStop() for graceful shutdown when unsupported amendment activates
- early shutdown ~1 minute before expected activation to avoid race
- try/catch in switchLastClosedLedger to survive unknown field crashes
  during shutdown window
- show amendment warning to all RPC users, not just admin

Fixes: #706
2026-06-19 09:35:39 +07:00
11 changed files with 55 additions and 724 deletions

View File

@@ -95,16 +95,8 @@ if [[ "$4" == "" ]]; then
echo "Non GH, local building, no Action runner magic"
else
# GH Action, runner
if [[ "$(git rev-parse --abbrev-ref HEAD)" == "release" ]]; then
echo "building on the release branch... placing it in builds/candidate"
mkdir /data/builds/candidate
cp /io/release-build/xahaud /data/builds/candidate/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
cp /io/release-build/release.info /data/builds/candidate/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4.releaseinfo
else
echo "building non-release branch, placing it in builds root"
cp /io/release-build/xahaud /data/builds/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
cp /io/release-build/release.info /data/builds/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4.releaseinfo
fi
cp /io/release-build/xahaud /data/builds/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
cp /io/release-build/release.info /data/builds/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4.releaseinfo
echo "Published build to: http://build.xahau.tech/"
echo $(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
fi

View File

@@ -242,7 +242,6 @@
#define sfLockingChainDoor ((8U << 16U) + 22U)
#define sfIssuingChainDoor ((8U << 16U) + 23U)
#define sfSubject ((8U << 16U) + 24U)
#define sfBrokerAccount ((8U << 16U) + 98U)
#define sfInform ((8U << 16U) + 99U)
#define sfIndexes ((19U << 16U) + 1U)
#define sfHashes ((19U << 16U) + 2U)

View File

@@ -34,7 +34,6 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FEATURE(URITokenBroker, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (HookMap, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (GuardDepth32, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(NamedHooks, Supported::yes, VoteBehavior::DefaultNo)

View File

@@ -319,7 +319,6 @@ TYPED_SFIELD(sfAttestationRewardAccount, ACCOUNT, 21)
TYPED_SFIELD(sfLockingChainDoor, ACCOUNT, 22)
TYPED_SFIELD(sfIssuingChainDoor, ACCOUNT, 23)
TYPED_SFIELD(sfSubject, ACCOUNT, 24)
TYPED_SFIELD(sfBrokerAccount, ACCOUNT, 98)
TYPED_SFIELD(sfInform, ACCOUNT, 99)
// vector of 256-bit

View File

@@ -314,7 +314,6 @@ TRANSACTION(ttURITOKEN_BURN, 46, URITokenBurn, ({
TRANSACTION(ttURITOKEN_BUY, 47, URITokenBuy, ({
{sfURITokenID, soeREQUIRED},
{sfAmount, soeREQUIRED},
{sfBrokerAccount, soeOPTIONAL},
}))
TRANSACTION(ttURITOKEN_CREATE_SELL_OFFER, 48, URITokenCreateSellOffer, ({

View File

@@ -7033,130 +7033,6 @@ private:
testTSHStrongWeak(env, tshNONE, __LINE__);
}
}
// A broker is an explicitly discovered weak TSH. This is required for
// XAH sales because native balance changes are not found by the generic
// balance-based weak-TSH discovery.
if (features[featureURITokenBroker] && features[fixXahauV1])
{
for (bool const collectEnabled : {false, true})
{
test::jtx::Env env{
*this,
network::makeNetworkConfig(
21337, "10", "1000000", "200000"),
features};
auto const issuer = Account("alice");
auto const owner = Account("bob");
auto const buyer = Account("carol");
auto const broker = Account("dave");
env.fund(XRP(1000), issuer, owner, buyer, broker);
env.close();
std::string const uri =
collectEnabled ? "broker-collect" : "broker-no-collect";
auto const tid = uritoken::tokenid(issuer, uri);
std::string const hexid{strHex(tid)};
env(uritoken::mint(issuer, uri),
uritoken::dest(owner),
uritoken::amt(XRP(1)),
ter(tesSUCCESS));
env.close();
env(uritoken::buy(owner, hexid),
uritoken::amt(XRP(1)),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
env(uritoken::sell(owner, hexid),
uritoken::dest(buyer),
uritoken::amt(XRP(1)),
ter(tesSUCCESS));
env.close();
if (collectEnabled)
addWeakTSH(env, broker);
setTSHHook(env, broker, false);
auto buy = uritoken::buy(buyer, hexid);
buy[sfAmount.jsonName] =
XRP(2).value().getJson(JsonOptions::none);
buy[sfBrokerAccount.jsonName] = broker.human();
env(buy, fee(XRP(1)), ter(tesSUCCESS));
env.close();
testTSHStrongWeak(
env, collectEnabled ? tshWEAK : tshNONE, __LINE__);
}
}
// If the token issuer is also the broker, TSH strengths are ORed and
// the account executes exactly once: weak for a non-burnable token and
// strong for a burnable token.
if (features[featureURITokenBroker] && features[fixXahauV1])
{
for (bool const burnable : {false, true})
{
test::jtx::Env env{
*this,
network::makeNetworkConfig(
21337, "10", "1000000", "200000"),
features};
auto const issuer = Account("alice");
auto const owner = Account("bob");
auto const buyer = Account("carol");
env.fund(XRP(1000), issuer, owner, buyer);
env.close();
std::string const uri =
burnable ? "broker-issuer-burnable" : "broker-issuer";
auto const tid = uritoken::tokenid(issuer, uri);
std::string const hexid{strHex(tid)};
env(uritoken::mint(issuer, uri),
uritoken::dest(owner),
uritoken::amt(XRP(1)),
txflags(burnable ? tfBurnable : 0),
ter(tesSUCCESS));
env.close();
env(uritoken::buy(owner, hexid),
uritoken::amt(XRP(1)),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
env(uritoken::sell(owner, hexid),
uritoken::dest(buyer),
uritoken::amt(XRP(1)),
ter(tesSUCCESS));
env.close();
if (!burnable)
addWeakTSH(env, issuer);
setTSHHook(env, issuer, burnable);
auto buy = uritoken::buy(buyer, hexid);
buy[sfAmount.jsonName] =
XRP(2).value().getJson(JsonOptions::none);
buy[sfBrokerAccount.jsonName] = issuer.human();
env(buy, fee(XRP(1)), ter(tesSUCCESS));
env.close();
testTSHStrongWeak(
env, burnable ? tshSTRONG : tshWEAK, __LINE__);
Json::Value params;
params[jss::transaction] =
env.tx()->getJson(JsonOptions::none)[jss::hash];
auto const result = env.rpc("json", "tx", to_string(params));
auto const executions =
result[jss::result][jss::meta][sfHookExecutions.jsonName];
BEAST_EXPECT(executions.size() == 1);
}
}
}
void

View File

@@ -1,465 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2026 XRPL-Labs
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 <test/jtx.h>
#include <test/jtx/AMM.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/TxFlags.h>
namespace ripple {
namespace test {
struct URITokenBroker_test : public beast::unit_test::suite
{
static Json::Value
brokeredBuy(
jtx::Account const& buyer,
std::string const& tokenID,
STAmount const& amount,
jtx::Account const& broker)
{
auto tx = jtx::uritoken::buy(buyer, tokenID);
tx[sfAmount.jsonName] = amount.getJson(JsonOptions::none);
tx[sfBrokerAccount.jsonName] = broker.human();
return tx;
}
static std::string
mintAndSell(
jtx::Env& env,
jtx::Account const& seller,
std::string const& uri,
STAmount const& amount,
std::optional<jtx::Account> const& destination = std::nullopt)
{
auto const id = strHex(jtx::uritoken::tokenid(seller, uri));
env(jtx::uritoken::mint(seller, uri));
if (destination)
env(jtx::uritoken::sell(seller, id),
jtx::uritoken::amt(amount),
jtx::uritoken::dest(*destination));
else
env(jtx::uritoken::sell(seller, id), jtx::uritoken::amt(amount));
env.close();
return id;
}
static std::shared_ptr<SLE const>
token(jtx::Env const& env, uint256 const& id)
{
return env.le(Keylet{ltURI_TOKEN, id});
}
static std::shared_ptr<SLE const>
token(jtx::Env const& env, std::string const& id)
{
uint256 tokenID;
if (!tokenID.parseHex(id))
return nullptr;
return token(env, tokenID);
}
void
testAmendment(FeatureBitset const& features)
{
testcase("amendment");
using namespace jtx;
Account const seller{"seller"};
Account const buyer{"buyer"};
Account const broker{"broker"};
// A broker-less buy remains valid when the broker amendment is off.
{
Env env{*this, features - featureURITokenBroker};
env.fund(XRP(1000), seller, buyer, broker);
auto const id = mintAndSell(env, seller, "no-broker", XRP(10));
env(uritoken::buy(buyer, id), uritoken::amt(XRP(10)));
}
// BrokerAccount requires both URITokenBroker and fixXahauV1.
for (auto const disabled : {featureURITokenBroker, fixXahauV1})
{
Env env{*this, features - disabled};
env.fund(XRP(1000), seller, buyer, broker);
auto const id =
mintAndSell(env, seller, to_string(disabled), XRP(10));
env(brokeredBuy(buyer, id, XRP(11), broker), ter(temDISABLED));
}
}
void
testMalformed(FeatureBitset const& features)
{
testcase("preflight");
using namespace jtx;
Env env{*this, features};
Account const seller{"seller"};
Account const buyer{"buyer"};
env.fund(XRP(1000), seller, buyer);
auto const id = mintAndSell(env, seller, "preflight", XRP(10));
env(brokeredBuy(buyer, id, XRP(11), buyer), ter(temREDUNDANT));
auto zero = uritoken::buy(buyer, id);
zero[sfAmount.jsonName] = XRP(11).value().getJson(JsonOptions::none);
zero[sfBrokerAccount.jsonName] = "rrrrrrrrrrrrrrrrrrrrrhoLvTp";
env(zero, ter(temMALFORMED));
// BrokerAccount is only part of the URITokenBuy template.
auto mint = uritoken::mint(seller, "broker-on-mint");
mint[sfBrokerAccount.jsonName] = buyer.human();
env(mint, ter(temMALFORMED));
// BrokerAccount belongs only to URITokenBuy's serialization template.
auto payment = pay(buyer, seller, XRP(1));
payment[sfBrokerAccount.jsonName] = seller.human();
env(payment, ter(temMALFORMED));
}
void
testPreclaim(FeatureBitset const& features)
{
testcase("preclaim");
using namespace jtx;
Account const seller{"seller"};
Account const buyer{"buyer"};
Account const broker{"broker"};
Account const missing{"missing"};
// Missing broker account.
{
Env env{*this, features};
env.fund(XRP(1000), seller, buyer);
env.memoize(missing);
auto const id = mintAndSell(env, seller, "missing", XRP(10));
env(brokeredBuy(buyer, id, XRP(11), missing), ter(tecNO_TARGET));
}
// The seller cannot also receive the broker spread.
{
Env env{*this, features};
env.fund(XRP(1000), seller, buyer);
auto const id = mintAndSell(env, seller, "seller", XRP(10));
env(brokeredBuy(buyer, id, XRP(11), seller), ter(tecNO_PERMISSION));
}
// A brokered sale requires both a positive price and positive spread.
{
Env env{*this, features};
env.fund(XRP(1000), seller, buyer, broker);
auto const zeroID = mintAndSell(env, seller, "zero", XRP(0), buyer);
env(brokeredBuy(buyer, zeroID, XRP(1), broker),
ter(tecNO_PERMISSION));
// The legacy destination-restricted zero-price buy is unchanged.
env(uritoken::buy(buyer, zeroID), uritoken::amt(XRP(0)));
auto const id = mintAndSell(env, seller, "spread", XRP(10));
env(brokeredBuy(buyer, id, XRP(9), broker),
ter(tecINSUFFICIENT_PAYMENT));
env(brokeredBuy(buyer, id, XRP(10), broker), ter(tecNO_PERMISSION));
}
// An IOU broker must already have a trust line.
{
Env env{*this, features};
Account const gw{"gateway"};
auto const USD = gw["USD"];
env.fund(XRP(1000), seller, buyer, broker, gw);
env.trust(USD(1000), seller, buyer);
env.close();
env(pay(gw, buyer, USD(100)));
auto const id = mintAndSell(env, seller, "no-line", USD(10));
env(brokeredBuy(buyer, id, USD(11), broker), ter(tecNO_LINE));
}
// AMM pseudo-accounts cannot be named as brokers.
{
Env env{*this, features | featureAMM};
Account const gw{"gateway"};
auto const USD = gw["USD"];
env.fund(XRP(30'000), seller, buyer, gw);
env.trust(USD(30'000), seller, buyer);
env.close();
env(pay(gw, seller, USD(20'000)));
AMM amm{env, seller, XRP(10'000), USD(10'000)};
auto const id = mintAndSell(env, seller, "amm", XRP(10));
auto tx = uritoken::buy(buyer, id);
tx[sfAmount.jsonName] = XRP(11).value().getJson(JsonOptions::none);
tx[sfBrokerAccount.jsonName] = toBase58(amm.ammAccount());
env(tx, ter(tecNO_PERMISSION));
}
}
void
testXAH(FeatureBitset const& features)
{
testcase("XAH settlement");
using namespace jtx;
Env env{*this, features};
Account const seller{"seller"};
Account const buyer{"buyer"};
Account const broker{"broker"};
env.fund(XRP(1000), seller, buyer, broker);
env(fset(seller, asfDepositAuth));
env(fset(broker, asfDepositAuth));
env.close();
auto const id = mintAndSell(env, seller, "xah", XRP(100));
auto const sellerBefore = env.balance(seller);
auto const buyerBefore = env.balance(buyer);
auto const brokerBefore = env.balance(broker);
auto const buyerOwners = env.ownerCount(buyer);
auto const brokerOwners = env.ownerCount(broker);
auto const fee = env.current()->fees().base;
env(brokeredBuy(buyer, id, XRP(110), broker));
env.close();
BEAST_EXPECT(env.balance(seller) == sellerBefore + XRP(100));
BEAST_EXPECT(env.balance(broker) == brokerBefore + XRP(10));
BEAST_EXPECT(env.balance(buyer) == buyerBefore - XRP(110) - fee);
BEAST_EXPECT(env.ownerCount(buyer) == buyerOwners + 1);
BEAST_EXPECT(env.ownerCount(broker) == brokerOwners);
auto const sle = token(env, id);
if (!BEAST_EXPECT(sle))
return;
BEAST_EXPECT((*sle)[sfOwner] == buyer.id());
BEAST_EXPECT(!sle->isFieldPresent(sfAmount));
BEAST_EXPECT(!sle->isFieldPresent(sfDestination));
}
void
testXAHReserve(FeatureBitset const& features)
{
testcase("XAH reserve boundary");
using namespace jtx;
Env env{*this, features};
Account const seller{"seller"};
Account const buyer{"buyer"};
Account const broker{"broker"};
env.fund(XRP(1000), seller, broker);
env.fund(XRP(300), buyer);
env.close();
auto const id = mintAndSell(env, seller, "reserve", XRP(1));
auto const sellerBefore = env.balance(seller);
auto const brokerBefore = env.balance(broker);
// The buyer needs the next owner reserve plus the full signed amount,
// not merely the seller's one-XAH listing amount.
env(brokeredBuy(buyer, id, XRP(51), broker),
ter(tecINSUFFICIENT_FUNDS));
BEAST_EXPECT(env.balance(seller) == sellerBefore);
BEAST_EXPECT(env.balance(broker) == brokerBefore);
auto const sle = token(env, id);
BEAST_EXPECT(sle && (*sle)[sfOwner] == seller.id());
}
void
testIOU(FeatureBitset const& features)
{
testcase("IOU settlement");
using namespace jtx;
Account const seller{"seller"};
Account const buyer{"buyer"};
Account const broker{"broker"};
Account const gw{"gateway"};
auto const USD = gw["USD"];
// Both recipients bear the transfer rate; buyer debit remains exact.
{
Env env{*this, features};
env.fund(XRP(1000), seller, buyer, broker, gw);
env(rate(gw, 1.25));
env.trust(USD(1000), seller, buyer, broker);
env.close();
env(trust(gw, seller["USD"](1000), tfClearNoRipple));
env(trust(gw, buyer["USD"](1000), tfClearNoRipple));
env(trust(gw, broker["USD"](1000), tfClearNoRipple));
env.close();
env(pay(gw, buyer, USD(1000)));
auto const id = mintAndSell(env, seller, "rate", USD(100));
auto const buyerBefore = env.balance(buyer, USD.issue());
auto const brokerOwners = env.ownerCount(broker);
env(brokeredBuy(buyer, id, USD(110), broker));
env.close();
BEAST_EXPECT(env.balance(seller, USD.issue()) == USD(80));
BEAST_EXPECT(env.balance(broker, USD.issue()) == USD(8));
BEAST_EXPECT(
env.balance(buyer, USD.issue()) == buyerBefore - USD(110));
BEAST_EXPECT(env.ownerCount(broker) == brokerOwners);
}
// The issuer may be the broker and needs no trust line.
{
Env env{*this, features};
env.fund(XRP(1000), seller, buyer, gw);
env(rate(gw, 1.25));
env.trust(USD(1000), seller, buyer);
env.close();
env(trust(gw, seller["USD"](1000), tfClearNoRipple));
env(trust(gw, buyer["USD"](1000), tfClearNoRipple));
env.close();
env(pay(gw, buyer, USD(1000)));
auto const id = mintAndSell(env, seller, "issuer", USD(100));
auto const buyerBefore = env.balance(buyer, USD.issue());
env(brokeredBuy(buyer, id, USD(110), gw));
env.close();
BEAST_EXPECT(env.balance(seller, USD.issue()) == USD(80));
BEAST_EXPECT(
env.balance(buyer, USD.issue()) == buyerBefore - USD(110));
}
// Existing line limits are not consulted and no owner object is added.
{
Env env{*this, features};
env.fund(XRP(1000), seller, buyer, broker, gw);
env.trust(USD(1000), seller, buyer);
env.trust(USD(1), broker);
env.close();
env(trust(gw, seller["USD"](1000), tfClearNoRipple));
env(trust(gw, buyer["USD"](1000), tfClearNoRipple));
env(trust(gw, broker["USD"](1), tfClearNoRipple));
env.close();
env(pay(gw, buyer, USD(100)));
env(pay(gw, broker, USD(1)));
auto const id = mintAndSell(env, seller, "limit", USD(10));
auto const brokerOwners = env.ownerCount(broker);
env(brokeredBuy(buyer, id, USD(20), broker));
env.close();
BEAST_EXPECT(env.balance(broker, USD.issue()) == USD(11));
BEAST_EXPECT(env.ownerCount(broker) == brokerOwners);
}
}
void
testIOUPermissions(FeatureBitset const& features)
{
testcase("IOU permissions");
using namespace jtx;
Account const seller{"seller"};
Account const buyer{"buyer"};
Account const broker{"broker"};
Account const gw{"gateway"};
auto const USD = gw["USD"];
// A frozen broker line rejects the broker leg atomically.
{
Env env{*this, features};
env.fund(XRP(1000), seller, buyer, broker, gw);
env.trust(USD(1000), seller, buyer, broker);
env.close();
env(trust(gw, seller["USD"](1000), tfClearNoRipple));
env(trust(gw, buyer["USD"](1000), tfClearNoRipple));
env(pay(gw, buyer, USD(100)));
env(trust(gw, broker["USD"](1000), tfSetFreeze | tfClearNoRipple));
env.close();
auto const id = mintAndSell(env, seller, "freeze", USD(10));
auto const buyerBefore = env.balance(buyer, USD.issue());
env(brokeredBuy(buyer, id, USD(11), broker), ter(tecFROZEN));
BEAST_EXPECT(env.balance(seller, USD.issue()) == USD(0));
BEAST_EXPECT(env.balance(broker, USD.issue()) == USD(0));
BEAST_EXPECT(env.balance(buyer, USD.issue()) == buyerBefore);
}
// Under RequireAuth, an existing but unauthorized broker line fails.
{
Env env{*this, features};
auto const sellerUSD = seller["USD"];
auto const buyerUSD = buyer["USD"];
env.fund(XRP(1000), seller, buyer, broker, gw);
env(fset(gw, asfRequireAuth));
env.close();
env.trust(USD(1000), seller, buyer, broker);
env(trust(gw, sellerUSD(1000)),
txflags(tfSetfAuth | tfClearNoRipple));
env(trust(gw, buyerUSD(1000)),
txflags(tfSetfAuth | tfClearNoRipple));
env(trust(gw, broker["USD"](1000)), txflags(tfClearNoRipple));
env.close();
env(pay(gw, buyer, USD(100)));
auto const id = mintAndSell(env, seller, "auth", USD(10));
env(brokeredBuy(buyer, id, USD(11), broker), ter(tecNO_AUTH));
}
// NoRipple on the broker leg rejects the whole transaction; the
// already-computed seller leg must also be rolled back.
{
Env env{*this, features};
env.fund(XRP(1000), seller, buyer, broker, gw);
env.trust(USD(1000), seller, buyer, broker);
env.close();
env(trust(gw, seller["USD"](1000), tfClearNoRipple));
env(trust(gw, buyer["USD"](1000), tfClearNoRipple));
env(pay(gw, buyer, USD(100)));
env(trust(gw, broker["USD"](1000), tfSetNoRipple));
env.close();
auto const id = mintAndSell(env, seller, "no-ripple", USD(10));
auto const sellerBefore = env.balance(seller, USD.issue());
auto const buyerBefore = env.balance(buyer, USD.issue());
auto const brokerBefore = env.balance(broker, USD.issue());
env(brokeredBuy(buyer, id, USD(11), broker), ter(tecPATH_DRY));
BEAST_EXPECT(env.balance(seller, USD.issue()) == sellerBefore);
BEAST_EXPECT(env.balance(buyer, USD.issue()) == buyerBefore);
BEAST_EXPECT(env.balance(broker, USD.issue()) == brokerBefore);
auto const sle = token(env, id);
BEAST_EXPECT(sle && (*sle)[sfOwner] == seller.id());
}
}
public:
void
run() override
{
auto const features = jtx::supported_amendments();
testAmendment(features);
testMalformed(features);
testPreclaim(features);
testXAH(features);
testXAHReserve(features);
testIOU(features);
testIOUPermissions(features);
}
};
BEAST_DEFINE_TESTSUITE(URITokenBroker, app, ripple);
} // namespace test
} // namespace ripple

View File

@@ -200,10 +200,6 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
issuer,
(ut->getFlags() & lsfBurnable) ? tshSTRONG : tshWEAK);
if (rv.rules().enabled(featureURITokenBroker) &&
tx.isFieldPresent(sfBrokerAccount))
ADD_TSH(tx.getAccountID(sfBrokerAccount), tshWEAK);
break;
}

View File

@@ -311,12 +311,28 @@ LedgerMaster::setValidLedger(std::shared_ptr<Ledger const> const& l)
if (auto const first =
app_.getAmendmentTable().firstUnsupportedExpected())
{
JLOG(m_journal.error()) << "One or more unsupported amendments "
"reached majority. Upgrade before "
<< to_string(*first)
<< " to prevent your server from "
"becoming amendment blocked.";
app_.getOPs().setAmendmentWarned();
using namespace std::chrono_literals;
auto const now = app_.timeKeeper().closeTime();
if (*first > now && (*first - now) <= 1min)
{
// Shut down just before the amendment activates to
// avoid processing ledgers with unknown fields.
JLOG(m_journal.error())
<< "Unsupported amendment activating imminently "
"at "
<< to_string(*first) << ". Shutting down.";
app_.getOPs().setAmendmentBlocked();
}
else
{
JLOG(m_journal.error())
<< "One or more unsupported amendments "
"reached majority. Upgrade before "
<< to_string(*first)
<< " to prevent your server from "
"becoming amendment blocked.";
app_.getOPs().setAmendmentWarned();
}
}
else
app_.getOPs().clearAmendmentWarned();

View File

@@ -1634,6 +1634,16 @@ NetworkOPsImp::setAmendmentBlocked()
{
amendmentBlocked_ = true;
setMode(OperatingMode::CONNECTED);
if (!app_.config().standalone())
{
JLOG(m_journal.fatal())
<< "One or more unsupported amendments activated. "
"Shutting down. Upgrade the server to remain "
"compatible with the network.";
app_.signalStop(
"One or more unsupported amendments activated. "
"Server must be upgraded to remain compatible with the network.");
}
}
inline bool
@@ -1789,8 +1799,23 @@ NetworkOPsImp::switchLastClosedLedger(
clearNeedNetworkLedger();
// Update fee computations.
app_.getTxQ().processClosedLedger(app_, *newLCL, true);
// Update fee computations. May throw if the ledger contains
// transactions with fields unknown to this binary (e.g. after an
// unsupported amendment activates). Catch to allow graceful shutdown.
//@@start process-closed-ledger-catch
try
{
app_.getTxQ().processClosedLedger(app_, *newLCL, true);
}
catch (std::runtime_error const& e)
{
if (!amendmentBlocked_)
throw;
JLOG(m_journal.error())
<< "Failed to process closed ledger: " << e.what();
return;
}
//@@end process-closed-ledger-catch
// Caller must own master lock
{
@@ -2449,7 +2474,7 @@ NetworkOPsImp::getServerInfo(bool human, bool admin, bool counters)
"may be incorrectly configured or some [validator_list_sites] "
"may be unreachable.";
}
if (admin && isAmendmentWarned())
if (isAmendmentWarned())
{
Json::Value& w = warnings.append(Json::objectValue);
w[jss::id] = warnRPC_UNSUPPORTED_MAJORITY;
@@ -2893,6 +2918,7 @@ NetworkOPsImp::pubLedger(std::shared_ptr<ReadView const> const& lpAccepted)
// Ledgers are published only when they acquire sufficient validations
// Holes are filled across connection loss or other catastrophe
//@@start pubLedger-accepted-ledger-construction
std::shared_ptr<AcceptedLedger> alpAccepted =
app_.getAcceptedLedgerCache().fetch(lpAccepted->info().hash);
if (!alpAccepted)
@@ -2901,6 +2927,7 @@ NetworkOPsImp::pubLedger(std::shared_ptr<ReadView const> const& lpAccepted)
app_.getAcceptedLedgerCache().canonicalize_replace_client(
lpAccepted->info().hash, alpAccepted);
}
//@@end pubLedger-accepted-ledger-construction
XRPL_ASSERT(
alpAccepted->getLedger().get() == lpAccepted.get(),

View File

@@ -29,33 +29,6 @@
namespace ripple {
TER
checkBroker(
ReadView const& view,
AccountID const& broker,
AccountID const& owner,
STAmount const& saleAmount,
STAmount const& purchaseAmount)
{
auto const sleBroker = view.read(keylet::account(broker));
if (!sleBroker)
return tecNO_TARGET;
if (sleBroker->isFieldPresent(sfAMMID))
return tecNO_PERMISSION;
if (broker == owner || saleAmount == beast::zero ||
purchaseAmount == saleAmount)
return tecNO_PERMISSION;
if (!purchaseAmount.native() && broker != purchaseAmount.getIssuer() &&
!view.exists(keylet::line(
broker, purchaseAmount.getIssuer(), purchaseAmount.getCurrency())))
return tecNO_LINE;
return tesSUCCESS;
}
NotTEC
URIToken::preflight(PreflightContext const& ctx)
{
@@ -165,23 +138,6 @@ URIToken::preflight(PreflightContext const& ctx)
ctx.tx.getAccountID(sfAccount) == ctx.tx.getAccountID(sfDestination))
return temREDUNDANT;
if (ctx.tx.isFieldPresent(sfBrokerAccount))
{
if (tt != ttURITOKEN_BUY)
return temMALFORMED;
if (!ctx.rules.enabled(featureURITokenBroker) ||
!ctx.rules.enabled(fixXahauV1))
return temDISABLED;
auto const broker = ctx.tx.getAccountID(sfBrokerAccount);
if (broker == beast::zero)
return temMALFORMED;
if (broker == ctx.tx.getAccountID(sfAccount))
return temREDUNDANT;
}
return preflight2(ctx);
}
@@ -279,21 +235,6 @@ URIToken::preclaim(PreclaimContext const& ctx)
if (purchaseAmount < saleAmount)
return tecINSUFFICIENT_PAYMENT;
if (ctx.tx.isFieldPresent(sfBrokerAccount))
{
if (!ctx.view.rules().enabled(featureURITokenBroker) || !fixV1)
return temDISABLED;
if (TER const result = checkBroker(
ctx.view,
ctx.tx.getAccountID(sfBrokerAccount),
*owner,
*saleAmount,
purchaseAmount);
!isTesSuccess(result))
return result;
}
if (fixV1)
{
if (purchaseAmount.native() && saleAmount->native())
@@ -542,11 +483,6 @@ URIToken::doApply()
if (purchaseAmount.issue() != saleAmount->issue())
return temBAD_CURRENCY;
std::optional<AccountID> const broker = ctx_.tx[~sfBrokerAccount];
if (broker &&
(!sb.rules().enabled(featureURITokenBroker) || !fixV1))
return temDISABLED;
if (fixV1)
{
// this is the reworked version of the buy routine
@@ -554,14 +490,6 @@ URIToken::doApply()
if (purchaseAmount < saleAmount)
return tecINSUFFICIENT_PAYMENT;
if (broker)
{
if (TER const result = checkBroker(
sb, *broker, *owner, *saleAmount, purchaseAmount);
!isTesSuccess(result))
return result;
}
// if it's an xrp sale/purchase then no trustline needed
if (purchaseAmount.native())
{
@@ -597,23 +525,6 @@ URIToken::doApply()
return result;
}
if (broker)
{
if (TER result = trustTransferAllowed(
sb,
{account_, *broker},
purchaseAmount.issue(),
j);
!isTesSuccess(result))
{
JLOG(j.trace()) << "URIToken::doApply broker "
"trustTransferAllowed result="
<< result;
return result;
}
}
if (STAmount availableFunds{accountFunds(
sb, account_, purchaseAmount, fhZERO_IF_FROZEN, j)};
purchaseAmount > availableFunds)
@@ -625,31 +536,13 @@ URIToken::doApply()
sb,
account_,
*owner,
broker ? *saleAmount : purchaseAmount,
purchaseAmount,
j,
WaiveTransferFee::No,
false);
!isTesSuccess(result))
return result;
if (broker)
{
STAmount const spread = purchaseAmount - *saleAmount;
if (spread <= beast::zero)
return tecINTERNAL; // LCOV_EXCL_LINE
if (TER result = accountSend(
sb,
account_,
*broker,
spread,
j,
WaiveTransferFee::No,
false);
!isTesSuccess(result))
return result;
}
// add token to new owner dir
auto const newPage = sb.dirInsert(
keylet::ownerDir(account_),