Skip to content

Commit 9150dcf

Browse files
Merge pull request #443 from stellar/updateBannedAccountsLogic
Update banned accounts logic
2 parents a486e6d + ab3424f commit 9150dcf

11 files changed

Lines changed: 364 additions & 1 deletion

File tree

docs/software/commands.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,18 @@ Most commands return their results in JSON format.
252252
* **bans**
253253
List current active bans
254254

255+
* **banaccounts**
256+
* `banaccounts`<br>
257+
Lists the currently filtered account addresses as a JSON array.<br>
258+
* `banaccounts?accountids=G_ADDRESS1,G_ADDRESS2,...`<br>
259+
Overrides the set of filtered G-addresses at runtime (the
260+
`FILTERED_G_ADDRESSES` configuration). Any transaction whose source account,
261+
operation source account, fee-bump fee source, or (for Soroban transactions)
262+
write footprint account entry matches an address in this list will be
263+
rejected from the transaction queue.<br>
264+
* `banaccounts?accountids=`<br>
265+
Clears all filtered accounts.<br>
266+
255267
* **checkdb**
256268
Triggers the instance to perform a background check of the database's state.
257269

src/herder/Herder.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,14 @@ class Herder
213213
// gets the upgrades that are scheduled by this node
214214
virtual std::string getUpgradesJson() = 0;
215215

216+
// Override the filtered G-addresses at runtime, updating both classic
217+
// and soroban transaction queues.
218+
virtual void
219+
setFilteredAccounts(std::vector<std::string> const& addresses) = 0;
220+
221+
// Return the currently active filtered G-addresses.
222+
virtual std::vector<std::string> getFilteredAccounts() const = 0;
223+
216224
virtual void forceSCPStateIntoSyncWithLastClosedLedger() = 0;
217225

218226
// helper function to craft an SCPValue

src/herder/HerderImpl.cpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1608,6 +1608,32 @@ HerderImpl::getUpgradesJson()
16081608
return mUpgrades.getParameters().toDebugJson(ls);
16091609
}
16101610

1611+
void
1612+
HerderImpl::setFilteredAccounts(std::vector<std::string> const& addresses)
1613+
{
1614+
UnorderedSet<AccountID> accounts;
1615+
for (auto const& addr : addresses)
1616+
{
1617+
accounts.emplace(KeyUtils::fromStrKey<PublicKey>(addr));
1618+
}
1619+
mTransactionQueue.setFilteredAccounts(accounts);
1620+
if (mSorobanTransactionQueue)
1621+
{
1622+
mSorobanTransactionQueue->setFilteredAccounts(accounts);
1623+
}
1624+
}
1625+
1626+
std::vector<std::string>
1627+
HerderImpl::getFilteredAccounts() const
1628+
{
1629+
std::vector<std::string> result;
1630+
for (auto const& acc : mTransactionQueue.getFilteredAccounts())
1631+
{
1632+
result.push_back(KeyUtils::toStrKey(acc));
1633+
}
1634+
return result;
1635+
}
1636+
16111637
void
16121638
HerderImpl::forceSCPStateIntoSyncWithLastClosedLedger()
16131639
{

src/herder/HerderImpl.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,11 @@ class HerderImpl : public Herder
191191
void setUpgrades(Upgrades::UpgradeParameters const& upgrades) override;
192192
std::string getUpgradesJson() override;
193193

194+
void
195+
setFilteredAccounts(std::vector<std::string> const& addresses) override;
196+
197+
std::vector<std::string> getFilteredAccounts() const override;
198+
194199
void forceSCPStateIntoSyncWithLastClosedLedger() override;
195200

196201
bool resolveNodeID(std::string const& s, PublicKey& retKey) override;

src/herder/TransactionQueue.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,18 @@ TransactionQueue::TransactionQueue(Application& app, uint32 pendingDepth,
118118
rand_uniform<uint64>(0, std::numeric_limits<uint64>::max());
119119
}
120120

121+
void
122+
TransactionQueue::setFilteredAccounts(UnorderedSet<AccountID> const& accounts)
123+
{
124+
mFilteredAccounts = accounts;
125+
}
126+
127+
UnorderedSet<AccountID> const&
128+
TransactionQueue::getFilteredAccounts() const
129+
{
130+
return mFilteredAccounts;
131+
}
132+
121133
ClassicTransactionQueue::ClassicTransactionQueue(Application& app,
122134
uint32 pendingDepth,
123135
uint32 banDepth,

src/herder/TransactionQueue.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ class TransactionQueue
162162
TxFrameList getTransactions(LedgerHeader const& lcl) const;
163163
bool sourceAccountPending(AccountID const& accountID) const;
164164

165+
void setFilteredAccounts(UnorderedSet<AccountID> const& accounts);
166+
UnorderedSet<AccountID> const& getFilteredAccounts() const;
167+
165168
virtual size_t getMaxQueueSizeOps() const = 0;
166169

167170
#ifdef BUILD_TESTS

src/herder/test/HerderTests.cpp

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6030,6 +6030,163 @@ TEST_CASE("filter transactions by G address", "[herder]")
60306030
}
60316031
}
60326032

6033+
TEST_CASE("banaccounts HTTP command", "[herder]")
6034+
{
6035+
SECTION("ban accounts via command")
6036+
{
6037+
VirtualClock clock;
6038+
auto cfg = getTestConfig();
6039+
cfg.FILTERED_G_ADDRESSES = {};
6040+
Application::pointer app = createTestApplication(clock, cfg);
6041+
6042+
auto root = app->getRoot();
6043+
auto srcKey = SecretKey::pseudoRandomForTesting();
6044+
auto src = root->create(srcKey, 1000000000);
6045+
6046+
// Initially, no filter — transaction should be accepted
6047+
auto acc = getAccount("acc");
6048+
auto tx = src.tx({createAccount(acc.getPublicKey(), 1)});
6049+
REQUIRE(app->getHerder().recvTransaction(tx, false).code ==
6050+
TransactionQueue::AddResultCode::ADD_STATUS_PENDING);
6051+
6052+
// Now set the filter via the HTTP command
6053+
auto addr = KeyUtils::toStrKey(srcKey.getPublicKey());
6054+
auto result = app->getCommandHandler().manualCmd(
6055+
"banaccounts?accountids=" + addr);
6056+
REQUIRE(result.find("filtered accounts updated") != std::string::npos);
6057+
REQUIRE(result.find("\"count\": 1") != std::string::npos);
6058+
6059+
// New transaction from the same source should now be filtered
6060+
auto acc2 = getAccount("acc2");
6061+
auto tx2 = src.tx({createAccount(acc2.getPublicKey(), 1)});
6062+
REQUIRE(app->getHerder().recvTransaction(tx2, false).code ==
6063+
TransactionQueue::AddResultCode::ADD_STATUS_FILTERED);
6064+
}
6065+
6066+
SECTION("clear banned accounts via command")
6067+
{
6068+
VirtualClock clock;
6069+
auto cfg = getTestConfig();
6070+
auto srcKey = SecretKey::pseudoRandomForTesting();
6071+
cfg.FILTERED_G_ADDRESSES = {KeyUtils::toStrKey(srcKey.getPublicKey())};
6072+
Application::pointer app = createTestApplication(clock, cfg);
6073+
6074+
auto root = app->getRoot();
6075+
auto src = root->create(srcKey, 1000000000);
6076+
6077+
// Source is initially filtered
6078+
auto acc = getAccount("acc");
6079+
auto tx = src.tx({createAccount(acc.getPublicKey(), 1)});
6080+
REQUIRE(app->getHerder().recvTransaction(tx, false).code ==
6081+
TransactionQueue::AddResultCode::ADD_STATUS_FILTERED);
6082+
6083+
// Clear the filter via empty accountids
6084+
auto result = app->getCommandHandler().manualCmd(
6085+
"banaccounts?accountids=");
6086+
REQUIRE(result.find("filtered accounts cleared") != std::string::npos);
6087+
6088+
// Resubmit the same transaction — it should now be accepted
6089+
REQUIRE(app->getHerder().recvTransaction(tx, false).code ==
6090+
TransactionQueue::AddResultCode::ADD_STATUS_PENDING);
6091+
}
6092+
6093+
SECTION("multiple accounts")
6094+
{
6095+
VirtualClock clock;
6096+
auto cfg = getTestConfig();
6097+
cfg.FILTERED_G_ADDRESSES = {};
6098+
Application::pointer app = createTestApplication(clock, cfg);
6099+
6100+
auto root = app->getRoot();
6101+
auto key1 = SecretKey::pseudoRandomForTesting();
6102+
auto key2 = SecretKey::pseudoRandomForTesting();
6103+
auto src1 = root->create(key1, 1000000000);
6104+
auto src2 = root->create(key2, 1000000000);
6105+
6106+
auto addr1 = KeyUtils::toStrKey(key1.getPublicKey());
6107+
auto addr2 = KeyUtils::toStrKey(key2.getPublicKey());
6108+
6109+
auto result = app->getCommandHandler().manualCmd(
6110+
"banaccounts?accountids=" + addr1 + "," + addr2);
6111+
REQUIRE(result.find("\"count\": 2") != std::string::npos);
6112+
6113+
auto acc = getAccount("acc");
6114+
auto tx1 = src1.tx({createAccount(acc.getPublicKey(), 1)});
6115+
REQUIRE(app->getHerder().recvTransaction(tx1, false).code ==
6116+
TransactionQueue::AddResultCode::ADD_STATUS_FILTERED);
6117+
6118+
auto acc2 = getAccount("acc2");
6119+
auto tx2 = src2.tx({createAccount(acc2.getPublicKey(), 1)});
6120+
REQUIRE(app->getHerder().recvTransaction(tx2, false).code ==
6121+
TransactionQueue::AddResultCode::ADD_STATUS_FILTERED);
6122+
}
6123+
6124+
SECTION("invalid address returns error")
6125+
{
6126+
VirtualClock clock;
6127+
auto cfg = getTestConfig();
6128+
cfg.FILTERED_G_ADDRESSES = {};
6129+
Application::pointer app = createTestApplication(clock, cfg);
6130+
6131+
auto result = app->getCommandHandler().manualCmd(
6132+
"banaccounts?accountids=NOT_A_VALID_ADDRESS");
6133+
REQUIRE(result.find("invalid address") != std::string::npos);
6134+
}
6135+
6136+
SECTION("no accountids lists current filter")
6137+
{
6138+
VirtualClock clock;
6139+
auto cfg = getTestConfig();
6140+
auto srcKey = SecretKey::pseudoRandomForTesting();
6141+
auto addr = KeyUtils::toStrKey(srcKey.getPublicKey());
6142+
cfg.FILTERED_G_ADDRESSES = {addr};
6143+
Application::pointer app = createTestApplication(clock, cfg);
6144+
6145+
// With no accountids param, should list the current filter
6146+
auto result = app->getCommandHandler().manualCmd("banaccounts");
6147+
REQUIRE(result.find("filteredAccounts") != std::string::npos);
6148+
REQUIRE(result.find(addr) != std::string::npos);
6149+
6150+
// Set two accounts, then list again
6151+
auto key2 = SecretKey::pseudoRandomForTesting();
6152+
auto addr2 = KeyUtils::toStrKey(key2.getPublicKey());
6153+
app->getCommandHandler().manualCmd(
6154+
"banaccounts?accountids=" + addr + "," + addr2);
6155+
6156+
result = app->getCommandHandler().manualCmd("banaccounts");
6157+
REQUIRE(result.find(addr) != std::string::npos);
6158+
REQUIRE(result.find(addr2) != std::string::npos);
6159+
6160+
// Clear and verify empty list
6161+
app->getCommandHandler().manualCmd("banaccounts?accountids=");
6162+
result = app->getCommandHandler().manualCmd("banaccounts");
6163+
REQUIRE(result.find("filteredAccounts") != std::string::npos);
6164+
REQUIRE(result.find(addr) == std::string::npos);
6165+
}
6166+
6167+
SECTION("fee-bump with banned fee source is rejected")
6168+
{
6169+
VirtualClock clock;
6170+
auto cfg = getTestConfig();
6171+
cfg.FILTERED_G_ADDRESSES = {};
6172+
Application::pointer app = createTestApplication(clock, cfg);
6173+
6174+
auto root = app->getRoot();
6175+
auto filteredKey = SecretKey::pseudoRandomForTesting();
6176+
auto feeSource = root->create(filteredKey, 1000000000);
6177+
auto feeSourceAcct = TestAccount{*app, filteredKey};
6178+
6179+
auto addr = KeyUtils::toStrKey(filteredKey.getPublicKey());
6180+
app->getCommandHandler().manualCmd("banaccounts?accountids=" + addr);
6181+
6182+
auto innerTx = root->tx({payment(root->getPublicKey(), 1)});
6183+
auto fb = feeBump(*app, feeSourceAcct, innerTx, 200);
6184+
6185+
REQUIRE(app->getHerder().recvTransaction(fb, false).code ==
6186+
TransactionQueue::AddResultCode::ADD_STATUS_FILTERED);
6187+
}
6188+
}
6189+
60336190
// Test that Herder updates the scphistory table with additional messages from
60346191
// ledger `n-1` when closing ledger `n`
60356192
TEST_CASE("SCP message capture from previous ledger", "[herder]")

src/main/CommandHandler.cpp

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
#include "test/TxTests.h"
4444
#endif
4545
#include <optional>
46+
#include <sstream>
4647

4748
using std::placeholders::_1;
4849
using std::placeholders::_2;
@@ -123,6 +124,7 @@ CommandHandler::CommandHandler(Application& app) : mApp(app)
123124
addRoute("metrics", &CommandHandler::metrics);
124125
addRoute("tx", &CommandHandler::tx);
125126
addRoute("upgrades", &CommandHandler::upgrades);
127+
addRoute("banaccounts", &CommandHandler::banaccounts);
126128
addRoute("dumpproposedsettings", &CommandHandler::dumpProposedSettings);
127129
addRoute("self-check", &CommandHandler::selfCheck);
128130
addRoute("sorobaninfo", &CommandHandler::sorobanInfo);
@@ -667,6 +669,65 @@ CommandHandler::upgrades(std::string const& params, std::string& retStr)
667669
}
668670
}
669671

672+
void
673+
CommandHandler::banaccounts(std::string const& params, std::string& retStr)
674+
{
675+
ZoneScoped;
676+
std::map<std::string, std::string> retMap;
677+
http::server::server::parseParams(params, retMap);
678+
679+
auto it = retMap.find("accountids");
680+
if (it == retMap.end())
681+
{
682+
// parseParams drops empty values, so check the raw params to
683+
// distinguish "not specified" (list) from "empty value" (clear).
684+
if (params.find("accountids") != std::string::npos)
685+
{
686+
mApp.getHerder().setFilteredAccounts({});
687+
retStr = R"({"status": "filtered accounts cleared"})";
688+
return;
689+
}
690+
691+
// No accountids param at all: list current filtered accounts
692+
auto accounts = mApp.getHerder().getFilteredAccounts();
693+
Json::Value root;
694+
root["filteredAccounts"] = Json::arrayValue;
695+
for (auto const& addr : accounts)
696+
{
697+
root["filteredAccounts"].append(addr);
698+
}
699+
retStr = root.toStyledString();
700+
return;
701+
}
702+
703+
std::vector<std::string> addresses;
704+
std::istringstream iss(it->second);
705+
std::string addr;
706+
while (std::getline(iss, addr, ','))
707+
{
708+
if (addr.empty())
709+
{
710+
continue;
711+
}
712+
try
713+
{
714+
KeyUtils::fromStrKey<PublicKey>(addr);
715+
}
716+
catch (std::exception const&)
717+
{
718+
retStr = fmt::format(
719+
FMT_STRING(R"({{"error": "invalid address: '{}'"}})"), addr);
720+
return;
721+
}
722+
addresses.push_back(addr);
723+
}
724+
725+
mApp.getHerder().setFilteredAccounts(addresses);
726+
retStr = fmt::format(
727+
FMT_STRING(R"({{"status": "filtered accounts updated", "count": {}}})"),
728+
addresses.size());
729+
}
730+
670731
void
671732
CommandHandler::dumpProposedSettings(std::string const& params,
672733
std::string& retStr)

src/main/CommandHandler.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class CommandHandler
6666
void tx(std::string const& params, std::string& retStr);
6767
void unban(std::string const& params, std::string& retStr);
6868
void upgrades(std::string const& params, std::string& retStr);
69+
void banaccounts(std::string const& params, std::string& retStr);
6970
void dumpProposedSettings(std::string const& params, std::string& retStr);
7071
void surveyTopology(std::string const&, std::string& retStr);
7172
void stopSurvey(std::string const&, std::string& retStr);

src/main/Config.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,6 @@ Config::Config() : NODE_SEED(SecretKey::random())
354354
FILTERED_G_ADDRESSES = {
355355
"GBO7VUL2TOKPWFAWKATIW7K3QYA7WQ63VDY5CAE6AFUUX6BHZBOC2WXC",
356356
"GATDQL767ZM2JQTBEG4BQ5WKOQNGAGWZDUN4GYT2UINPEU3RT2UAMVZH",
357-
"GC2XJKN5VZEMM35F5LRSUP5CWVDZJVM37YKR7UYYXGN3TGKZXMP5FZIB",
358357
"GCQCWEQDICASV3R737LPWPDJ3FPBC6XPWXKPJDL22DLQVGOJAUH5DBJI"};
359358

360359
OP_APPLY_SLEEP_TIME_DURATION_FOR_TESTING = {};

0 commit comments

Comments
 (0)