Files
rippled/src/test/rpc/AccountSet_test.cpp
Jingchen ff18cfef96 refactor: Retire DepositPreAuth and DepositAuth amendments (#5978)
Amendments activated for more than 2 years can be retired. This change retires the fDepositPreAuth and DepositAuth amendments.
2025-11-11 15:21:07 +00:00

599 lines
19 KiB
C++

#include <test/jtx.h>
#include <xrpld/app/tx/apply.h>
#include <xrpl/protocol/AmountConversions.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/jss.h>
namespace ripple {
class AccountSet_test : public beast::unit_test::suite
{
public:
void
testNullAccountSet()
{
testcase("No AccountSet");
using namespace test::jtx;
Env env(*this);
Account const alice("alice");
env.fund(XRP(10000), noripple(alice));
// ask for the ledger entry - account root, to check its flags
auto const jrr = env.le(alice);
BEAST_EXPECT(jrr && jrr->at(sfFlags) == 0u);
}
void
testMostFlags()
{
testcase("Most Flags");
using namespace test::jtx;
Account const alice("alice");
Env env(*this, testable_amendments());
env.fund(XRP(10000), noripple(alice));
// Give alice a regular key so she can legally set and clear
// her asfDisableMaster flag.
Account const alie{"alie", KeyType::secp256k1};
env(regkey(alice, alie));
env.close();
auto testFlags = [this, &alice, &alie, &env](
std::initializer_list<std::uint32_t> goodFlags) {
std::uint32_t const orig_flags = (*env.le(alice))[sfFlags];
for (std::uint32_t flag{1u};
flag < std::numeric_limits<std::uint32_t>::digits;
++flag)
{
if (flag == asfNoFreeze)
{
// The asfNoFreeze flag can't be cleared. It is tested
// elsewhere.
continue;
}
if (flag == asfAuthorizedNFTokenMinter)
{
// The asfAuthorizedNFTokenMinter flag requires the
// presence or absence of the sfNFTokenMinter field in
// the transaction. It is tested elsewhere.
continue;
}
if (flag == asfDisallowIncomingCheck ||
flag == asfDisallowIncomingPayChan ||
flag == asfDisallowIncomingNFTokenOffer ||
flag == asfDisallowIncomingTrustline)
{
// These flags are part of the DisallowIncoming amendment
// and are tested elsewhere
continue;
}
if (flag == asfAllowTrustLineClawback)
{
// The asfAllowTrustLineClawback flag can't be cleared. It
// is tested elsewhere.
continue;
}
if (flag == asfAllowTrustLineLocking)
{
// These flags are part of the AllowTokenLocking amendment
// and are tested elsewhere
continue;
}
if (std::find(goodFlags.begin(), goodFlags.end(), flag) !=
goodFlags.end())
{
// Good flag
env.require(nflags(alice, flag));
env(fset(alice, flag), sig(alice));
env.close();
env.require(flags(alice, flag));
env(fclear(alice, flag), sig(alie));
env.close();
env.require(nflags(alice, flag));
std::uint32_t const now_flags = (*env.le(alice))[sfFlags];
BEAST_EXPECT(now_flags == orig_flags);
}
else
{
// Bad flag
BEAST_EXPECT((*env.le(alice))[sfFlags] == orig_flags);
env(fset(alice, flag), sig(alice));
env.close();
BEAST_EXPECT((*env.le(alice))[sfFlags] == orig_flags);
env(fclear(alice, flag), sig(alie));
env.close();
BEAST_EXPECT((*env.le(alice))[sfFlags] == orig_flags);
}
}
};
testFlags(
{asfRequireDest,
asfRequireAuth,
asfDisallowXRP,
asfGlobalFreeze,
asfDisableMaster,
asfDefaultRipple,
asfDepositAuth});
}
void
testSetAndResetAccountTxnID()
{
testcase("Set and reset AccountTxnID");
using namespace test::jtx;
Env env(*this);
Account const alice("alice");
env.fund(XRP(10000), noripple(alice));
std::uint32_t const orig_flags = (*env.le(alice))[sfFlags];
// asfAccountTxnID is special and not actually set as a flag,
// so we check the field presence instead
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfAccountTxnID));
env(fset(alice, asfAccountTxnID), sig(alice));
BEAST_EXPECT(env.le(alice)->isFieldPresent(sfAccountTxnID));
env(fclear(alice, asfAccountTxnID));
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfAccountTxnID));
std::uint32_t const now_flags = (*env.le(alice))[sfFlags];
BEAST_EXPECT(now_flags == orig_flags);
}
void
testSetNoFreeze()
{
testcase("Set NoFreeze");
using namespace test::jtx;
Env env(*this);
Account const alice("alice");
env.fund(XRP(10000), noripple(alice));
env.memoize("eric");
env(regkey(alice, "eric"));
env.require(nflags(alice, asfNoFreeze));
env(fset(alice, asfNoFreeze), sig("eric"), ter(tecNEED_MASTER_KEY));
env(fset(alice, asfNoFreeze), sig(alice));
env.require(flags(alice, asfNoFreeze));
env(fclear(alice, asfNoFreeze), sig(alice));
// verify flag is still set (clear does not clear in this case)
env.require(flags(alice, asfNoFreeze));
}
void
testDomain()
{
testcase("Domain");
using namespace test::jtx;
Env env(*this);
Account const alice("alice");
env.fund(XRP(10000), alice);
auto jt = noop(alice);
// The Domain field is represented as the hex string of the lowercase
// ASCII of the domain. For example, the domain example.com would be
// represented as "6578616d706c652e636f6d".
//
// To remove the Domain field from an account, send an AccountSet with
// the Domain set to an empty string.
std::string const domain = "example.com";
jt[sfDomain.fieldName] = strHex(domain);
env(jt);
BEAST_EXPECT((*env.le(alice))[sfDomain] == makeSlice(domain));
jt[sfDomain.fieldName] = "";
env(jt);
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfDomain));
// The upper limit on the length is 256 bytes
// (defined as DOMAIN_BYTES_MAX in SetAccount)
// test the edge cases: 255, 256, 257.
std::size_t const maxLength = 256;
for (std::size_t len = maxLength - 1; len <= maxLength + 1; ++len)
{
std::string domain2 =
std::string(len - domain.length() - 1, 'a') + "." + domain;
BEAST_EXPECT(domain2.length() == len);
jt[sfDomain.fieldName] = strHex(domain2);
if (len <= maxLength)
{
env(jt);
BEAST_EXPECT((*env.le(alice))[sfDomain] == makeSlice(domain2));
}
else
{
env(jt, ter(telBAD_DOMAIN));
}
}
}
void
testMessageKey()
{
testcase("MessageKey");
using namespace test::jtx;
Env env(*this);
Account const alice("alice");
env.fund(XRP(10000), alice);
auto jt = noop(alice);
auto const rkp = randomKeyPair(KeyType::ed25519);
jt[sfMessageKey.fieldName] = strHex(rkp.first.slice());
env(jt);
BEAST_EXPECT(
strHex((*env.le(alice))[sfMessageKey]) ==
strHex(rkp.first.slice()));
jt[sfMessageKey.fieldName] = "";
env(jt);
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfMessageKey));
using namespace std::string_literals;
jt[sfMessageKey.fieldName] = strHex("NOT_REALLY_A_PUBKEY"s);
env(jt, ter(telBAD_PUBLIC_KEY));
}
void
testWalletID()
{
testcase("WalletID");
using namespace test::jtx;
Env env(*this);
Account const alice("alice");
env.fund(XRP(10000), alice);
auto jt = noop(alice);
std::string const locator =
"9633EC8AF54F16B5286DB1D7B519EF49EEFC050C0C8AC4384F1D88ACD1BFDF05";
jt[sfWalletLocator.fieldName] = locator;
env(jt);
BEAST_EXPECT(to_string((*env.le(alice))[sfWalletLocator]) == locator);
jt[sfWalletLocator.fieldName] = "";
env(jt);
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfWalletLocator));
}
void
testEmailHash()
{
testcase("EmailHash");
using namespace test::jtx;
Env env(*this);
Account const alice("alice");
env.fund(XRP(10000), alice);
auto jt = noop(alice);
std::string const mh("5F31A79367DC3137FADA860C05742EE6");
jt[sfEmailHash.fieldName] = mh;
env(jt);
BEAST_EXPECT(to_string((*env.le(alice))[sfEmailHash]) == mh);
jt[sfEmailHash.fieldName] = "";
env(jt);
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfEmailHash));
}
void
testTransferRate()
{
struct test_results
{
double set;
TER code;
double get;
};
testcase("TransferRate");
using namespace test::jtx;
auto doTests = [this](
FeatureBitset const& features,
std::initializer_list<test_results> testData) {
Env env(*this, features);
Account const alice("alice");
env.fund(XRP(10000), alice);
for (auto const& r : testData)
{
env(rate(alice, r.set), ter(r.code));
env.close();
// If the field is not present expect the default value
if (!(*env.le(alice))[~sfTransferRate])
BEAST_EXPECT(r.get == 1.0);
else
BEAST_EXPECT(
*(*env.le(alice))[~sfTransferRate] ==
r.get * QUALITY_ONE);
}
};
doTests(
testable_amendments(),
{{1.0, tesSUCCESS, 1.0},
{1.1, tesSUCCESS, 1.1},
{2.0, tesSUCCESS, 2.0},
{2.1, temBAD_TRANSFER_RATE, 2.0},
{0.0, tesSUCCESS, 1.0},
{2.0, tesSUCCESS, 2.0},
{0.9, temBAD_TRANSFER_RATE, 2.0}});
}
void
testGateway()
{
testcase("Gateway");
using namespace test::jtx;
Account const alice("alice");
Account const bob("bob");
Account const gw("gateway");
auto const USD = gw["USD"];
// Test gateway with a variety of allowed transfer rates
for (double transferRate = 1.0; transferRate <= 2.0;
transferRate += 0.03125)
{
Env env(*this);
env.fund(XRP(10000), gw, alice, bob);
env.close();
env.trust(USD(10), alice, bob);
env.close();
env(rate(gw, transferRate));
env.close();
auto const amount = USD(1);
Rate const rate(transferRate * QUALITY_ONE);
auto const amountWithRate =
toAmount<STAmount>(multiply(amount.value(), rate));
env(pay(gw, alice, USD(10)));
env.close();
env(pay(alice, bob, USD(1)), sendmax(USD(10)));
env.close();
env.require(balance(alice, USD(10) - amountWithRate));
env.require(balance(bob, USD(1)));
}
// Since fix1201 was enabled on Nov 14 2017 a rate in excess of
// 2.0 has been blocked by the transactor. But there are a few
// accounts on the MainNet that have larger-than-currently-allowed
// TransferRates. We'll bypass the transactor so we can check
// operation of these legacy TransferRates.
//
// Two out-of-bound values are currently in the ledger (March 2020)
// They are 4.0 and 4.294967295. So those are the values we test.
for (double transferRate : {4.0, 4.294967295})
{
Env env(*this);
env.fund(XRP(10000), gw, alice, bob);
env.close();
env.trust(USD(10), alice, bob);
env.close();
// We'd like to use transferRate here, but the transactor
// blocks transfer rates that large. So we use an acceptable
// transfer rate here and later hack the ledger to replace
// the acceptable value with an out-of-bounds value.
env(rate(gw, 2.0));
env.close();
// Because we're hacking the ledger we need the account to have
// non-zero sfMintedNFTokens and sfBurnedNFTokens fields. This
// prevents an exception when the AccountRoot template is applied.
{
uint256 const nftId0{token::getNextID(env, gw, 0u)};
env(token::mint(gw, 0u));
env.close();
env(token::burn(gw, nftId0));
env.close();
}
// Note that we're bypassing almost all of the ledger's safety
// checks with this modify() call. If you call close() between
// here and the end of the test all the effort will be lost.
env.app().openLedger().modify(
[&gw, transferRate](OpenView& view, beast::Journal j) {
// Get the account root we want to hijack.
auto const sle = view.read(keylet::account(gw.id()));
if (!sle)
return false; // This would be really surprising!
// We'll insert a replacement for the account root
// with the higher (currently invalid) transfer rate.
auto replacement = std::make_shared<SLE>(*sle, sle->key());
(*replacement)[sfTransferRate] =
static_cast<std::uint32_t>(transferRate * QUALITY_ONE);
view.rawReplace(replacement);
return true;
});
auto const amount = USD(1);
auto const amountWithRate = toAmount<STAmount>(
multiply(amount.value(), Rate(transferRate * QUALITY_ONE)));
env(pay(gw, alice, USD(10)));
env(pay(alice, bob, amount), sendmax(USD(10)));
env.require(balance(alice, USD(10) - amountWithRate));
env.require(balance(bob, amount));
}
}
void
testBadInputs()
{
testcase("Bad inputs");
using namespace test::jtx;
Env env(*this);
Account const alice("alice");
env.fund(XRP(10000), alice);
auto jt = fset(alice, asfDisallowXRP);
jt[jss::ClearFlag] = asfDisallowXRP;
env(jt, ter(temINVALID_FLAG));
jt = fset(alice, asfRequireAuth);
jt[jss::ClearFlag] = asfRequireAuth;
env(jt, ter(temINVALID_FLAG));
jt = fset(alice, asfRequireDest);
jt[jss::ClearFlag] = asfRequireDest;
env(jt, ter(temINVALID_FLAG));
jt = fset(alice, asfDisallowXRP);
jt[sfFlags.fieldName] = tfAllowXRP;
env(jt, ter(temINVALID_FLAG));
jt = fset(alice, asfRequireAuth);
jt[sfFlags.fieldName] = tfOptionalAuth;
env(jt, ter(temINVALID_FLAG));
jt = fset(alice, asfRequireDest);
jt[sfFlags.fieldName] = tfOptionalDestTag;
env(jt, ter(temINVALID_FLAG));
jt = fset(alice, asfRequireDest);
jt[sfFlags.fieldName] = tfAccountSetMask;
env(jt, ter(temINVALID_FLAG));
env(fset(alice, asfDisableMaster),
sig(alice),
ter(tecNO_ALTERNATIVE_KEY));
}
void
testRequireAuthWithDir()
{
testcase("Require auth");
using namespace test::jtx;
Env env(*this);
Account const alice("alice");
Account const bob("bob");
env.fund(XRP(10000), alice);
env.close();
// alice should have an empty directory.
BEAST_EXPECT(dirIsEmpty(*env.closed(), keylet::ownerDir(alice)));
// Give alice a signer list, then there will be stuff in the directory.
env(signers(alice, 1, {{bob, 1}}));
env.close();
BEAST_EXPECT(!dirIsEmpty(*env.closed(), keylet::ownerDir(alice)));
env(fset(alice, asfRequireAuth), ter(tecOWNERS));
// Remove the signer list. After that asfRequireAuth should succeed.
env(signers(alice, test::jtx::none));
env.close();
BEAST_EXPECT(dirIsEmpty(*env.closed(), keylet::ownerDir(alice)));
env(fset(alice, asfRequireAuth));
}
void
testTicket()
{
using namespace test::jtx;
Env env(*this);
Account const alice("alice");
env.fund(XRP(10000), alice);
env.close();
std::uint32_t const ticketSeq{env.seq(alice) + 1};
env(ticket::create(alice, 1));
env.close();
env.require(owners(alice, 1), tickets(alice, 1));
// Try using a ticket that alice doesn't have.
env(noop(alice), ticket::use(ticketSeq + 1), ter(terPRE_TICKET));
env.close();
env.require(owners(alice, 1), tickets(alice, 1));
// Actually use alice's ticket. Note that if a transaction consumes
// a ticket then the account's sequence number does not advance.
std::uint32_t const aliceSeq{env.seq(alice)};
env(noop(alice), ticket::use(ticketSeq));
env.close();
env.require(owners(alice, 0), tickets(alice, 0));
BEAST_EXPECT(aliceSeq == env.seq(alice));
// Try re-using a ticket that alice already used.
env(noop(alice), ticket::use(ticketSeq), ter(tefNO_TICKET));
env.close();
}
void
testBadSigningKey()
{
using namespace test::jtx;
testcase("Bad signing key");
Env env(*this);
Account const alice("alice");
env.fund(XRP(10000), alice);
env.close();
auto jtx = env.jt(noop("alice"), ter(temBAD_SIGNATURE));
if (!BEAST_EXPECT(jtx.stx))
return;
auto stx = std::make_shared<STTx>(*jtx.stx);
stx->at(sfSigningPubKey) = makeSlice(std::string("badkey"));
env.app().openLedger().modify([&](OpenView& view, beast::Journal j) {
auto const result =
ripple::apply(env.app(), view, *stx, tapNONE, j);
BEAST_EXPECT(result.ter == temBAD_SIGNATURE);
BEAST_EXPECT(!result.applied);
return result.applied;
});
}
void
run() override
{
testNullAccountSet();
testMostFlags();
testSetAndResetAccountTxnID();
testSetNoFreeze();
testDomain();
testGateway();
testMessageKey();
testWalletID();
testEmailHash();
testBadInputs();
testRequireAuthWithDir();
testTransferRate();
testTicket();
testBadSigningKey();
}
};
BEAST_DEFINE_TESTSUITE_PRIO(AccountSet, rpc, ripple, 1);
} // namespace ripple