fix signerlist import crashbug

This commit is contained in:
Richard Holland
2023-07-09 08:02:09 +00:00
parent fc6f8dde94
commit a3e940844d
4 changed files with 228 additions and 112 deletions

View File

@@ -33,6 +33,7 @@
#include <ripple/protocol/STTx.h>
#include <ripple/basics/base64.h>
#include <ripple/app/misc/Manifest.h>
#include <ripple/app/tx/impl/SetSignerList.h>
namespace ripple {
@@ -191,6 +192,13 @@ Import::preflight(PreflightContext const& ctx)
if (!stpTrans)
return temMALFORMED;
if (stpTrans->isFieldPresent(sfTicketSequence))
{
JLOG(ctx.j.warn())
<< "Import: cannot use TicketSequence XPOP.";
return temMALFORMED;
}
// check if txn is emitted or a psuedo
if (isPseudoTx(*stpTrans) || stpTrans->isFieldPresent(sfEmitDetails))
{
@@ -952,16 +960,24 @@ Import::doApply()
// why aren't transactors stateful, why does this need to be recomputed each time...
STAmount burn = stpTrans->getFieldAmount(sfFee);
if (!isXRP(burn) || burn < beast::zero)
{
JLOG(ctx_.journal.warn())
<< "Import: inner fee was not XRP value.";
return tefINTERNAL;
}
uint32_t importSequence = stpTrans->getFieldU32(sfSequence);
auto const id = ctx_.tx[sfAccount];
auto const k = keylet::account(id);
auto const ksl = keylet::signers(id);
auto sle = view().peek(k);
std::optional<STArray> setSignerEntries;
std::optional<
std::vector<ripple::SignerEntries::SignerEntry>> setSignerEntries;
uint32_t setSignerQuorum { 0 };
std::optional<AccountID> setRegularKey;
auto const signingKey = ctx_.tx.getSigningPubKey();
bool const signedWithMaster = !signingKey.empty() && calcAccountID(PublicKey(makeSlice(signingKey))) == id;
@@ -973,9 +989,50 @@ Import::doApply()
{
if (tt == ttSIGNER_LIST_SET)
{
JLOG(ctx_.journal.warn()) << "SingerListSet";
// key import: signer list
setSignerEntries = stpTrans->getFieldArray(sfSignerEntries);
if (stpTrans->isFieldPresent(sfSignerQuorum) &&
stpTrans->isFieldPresent(sfSignerEntries))
{
auto const entries = stpTrans->getFieldArray(sfSignerEntries);
if (entries.empty())
{
JLOG(ctx_.journal.warn()) << "Import: SignerListSet entires empty, skipping.";
}
else
{
JLOG(ctx_.journal.trace()) << "Import: SingerListSet";
// key import: signer list
setSignerEntries.emplace();
setSignerEntries->reserve(entries.size());
for (auto const& e: entries)
{
if (!e.isFieldPresent(sfAccount) || !e.isFieldPresent(sfSignerWeight))
{
JLOG(ctx_.journal.warn())
<< "Import: SignerListSet entry lacked a required field (Account/SignerWeight). "
<< "Skipping SignerListSet.";
setSignerEntries = std::nullopt;
break;
}
std::optional<uint256> tag;
if (e.isFieldPresent(sfWalletLocator))
tag = e.getFieldH256(sfWalletLocator);
setSignerEntries->emplace_back(
e.getAccountID(sfAccount),
e.getFieldU16(sfSignerWeight),
tag);
}
setSignerQuorum = stpTrans->getFieldU32(sfSignerQuorum);
}
}
else
{
JLOG(ctx_.journal.warn())
<< "Import: SingerListSet lacked either populated SignerEntries or SignerQuorum, ignoring.";
}
}
else if (tt == ttREGULAR_KEY_SET)
{
@@ -987,6 +1044,18 @@ Import::doApply()
bool const create = !sle;
// compute the amount they receive first because the amount is maybe needed for computing setsignerlist later
STAmount startBal = create ? STAmount(INITIAL_IMPORT_XRP) : sle->getFieldAmount(sfBalance);
STAmount finalBal = startBal + burn;
// this should never happen
if (finalBal < startBal)
{
JLOG(ctx_.journal.warn())
<< "Import: logic error finalBal < startBal.";
return tefINTERNAL;
}
if (create)
{
// Create the account.
@@ -999,20 +1068,9 @@ Import::doApply()
sle->setFieldU32(sfImportSequence, importSequence);
sle->setFieldU32(sfSequence, seqno);
sle->setFieldU32(sfOwnerCount, 0);
STAmount initBal = STAmount(INITIAL_IMPORT_XRP) + burn;
if (initBal <= beast::zero)
{
JLOG(ctx_.journal.warn())
<< "Import: inital balance <= 0";
return tefINTERNAL;
}
JLOG(ctx_.journal.warn()) << "Import: inital balance" << initBal;
sle->setFieldAmount(sfBalance, initBal);
sle->setFieldAmount(sfBalance, finalBal);
}
else if (sle->getFieldU32(sfImportSequence) >= importSequence)
@@ -1023,25 +1081,19 @@ Import::doApply()
return tefINTERNAL;
}
if (setSignerEntries) {
JLOG(ctx_.journal.warn()) << "signer list set";
auto signerList = std::make_shared<SLE>(ksl);
signerList->setFieldArray(sfSignerEntries, *setSignerEntries);
view().insert(signerList);
}
else if (setRegularKey)
if (setRegularKey)
{
JLOG(ctx_.journal.warn()) << "set regular key";
JLOG(ctx_.journal.warn()) << "Import: actioning SetRegularKey " << *setRegularKey << " acc: " << id;
sle->setAccountID(sfRegularKey, *setRegularKey);
}
if (create)
{
JLOG(ctx_.journal.warn()) << "create";
JLOG(ctx_.journal.trace()) << "Import: creating account " << id;
if (!signedWithMaster)
{
// disable master if the account is created using non-master key
JLOG(ctx_.journal.warn()) << "create - disable master";
JLOG(ctx_.journal.warn()) << "Import: keying of " << id << " is unclear - disable master";
sle->setAccountID(sfRegularKey, noAccount());
sle->setFieldU32(sfFlags, lsfDisableMaster);
}
@@ -1050,23 +1102,13 @@ Import::doApply()
else
{
// account already exists
JLOG(ctx_.journal.warn()) << "update - import sequence";
JLOG(ctx_.journal.trace()) << "Import: updating existing account " << id;
sle->setFieldU32(sfImportSequence, importSequence);
// credit the PoB
if (burn > beast::zero)
{
JLOG(ctx_.journal.warn()) << "update - credit burn";
STAmount startBal = sle->getFieldAmount(sfBalance);
STAmount finalBal = startBal + burn;
if (finalBal > startBal)
sle->setFieldAmount(sfBalance, finalBal);
}
sle->setFieldAmount(sfBalance, finalBal);
view().update(sle);
}
// todo: manifest sequence needs to be recorded to prevent certain types of replay attack
auto const infoVL = getVLInfo(*xpop, ctx_.journal);
@@ -1079,7 +1121,7 @@ Import::doApply()
if (!sleVL)
{
// create VL import seq counter
JLOG(ctx_.journal.warn()) << "create vl seq - insert import sequence + public key";
JLOG(ctx_.journal.trace()) << "Import: create vl seq - insert import sequence + public key";
sleVL = std::make_shared<SLE>(keyletVL);
sleVL->setFieldU32(sfImportSequence, infoVL->first);
sleVL->setFieldVL(sfPublicKey, infoVL->second.slice());
@@ -1087,7 +1129,7 @@ Import::doApply()
}
else
{
JLOG(ctx_.journal.warn()) << "update vl - insert import sequence";
JLOG(ctx_.journal.trace()) << "Import: update vl";
uint32_t current = sleVL->getFieldU32(sfImportSequence);
if (current > infoVL->first)
@@ -1107,6 +1149,40 @@ Import::doApply()
}
}
/// this logic is executed last after the account might have already been created
if (setSignerEntries)
{
JLOG(ctx_.journal.trace())
<< "Import: actioning SignerListSet "
<< "quorum: " << setSignerQuorum << " "
<< "size: " << setSignerEntries->size();
Sandbox sb(&view());
TER result =
SetSignerList::replaceSignersFromLedger(
ctx_.app,
sb,
ctx_.journal,
id,
setSignerQuorum,
*setSignerEntries,
finalBal.xrp());
if (result == tesSUCCESS)
{
JLOG(ctx_.journal.trace())
<< "Import: successful SignerListSet";
sb.apply(ctx_.rawView());
}
else
{
JLOG(ctx_.journal.warn())
<< "Import: SetSignerList failed with code "
<< result << " acc: " << id;
}
}
return tesSUCCESS;
}

View File

@@ -312,70 +312,14 @@ SetSignerList::validateQuorumAndSignerEntries(
TER
SetSignerList::replaceSignerList()
{
auto const accountKeylet = keylet::account(account_);
auto const ownerDirKeylet = keylet::ownerDir(account_);
auto const signerListKeylet = keylet::signers(account_);
// This may be either a create or a replace. Preemptively remove any
// old signer list. May reduce the reserve, so this is done before
// checking the reserve.
if (TER const ter = removeSignersFromLedger(
return replaceSignersFromLedger(
ctx_.app,
view(),
accountKeylet,
ownerDirKeylet,
signerListKeylet,
j_))
return ter;
auto const sle = view().peek(accountKeylet);
if (!sle)
return tefINTERNAL;
// Compute new reserve. Verify the account has funds to meet the reserve.
std::uint32_t const oldOwnerCount{(*sle)[sfOwnerCount]};
// The required reserve changes based on featureMultiSignReserve...
int addedOwnerCount{1};
std::uint32_t flags{lsfOneOwnerCount};
if (!ctx_.view().rules().enabled(featureMultiSignReserve))
{
addedOwnerCount = signerCountBasedOwnerCountDelta(
signers_.size(), ctx_.view().rules());
flags = 0;
}
XRPAmount const newReserve{
view().fees().accountReserve(oldOwnerCount + addedOwnerCount)};
// We check the reserve against the starting balance because we want to
// allow dipping into the reserve to pay fees. This behavior is consistent
// with CreateTicket.
if (mPriorBalance < newReserve)
return tecINSUFFICIENT_RESERVE;
// Everything's ducky. Add the ltSIGNER_LIST to the ledger.
auto signerList = std::make_shared<SLE>(signerListKeylet);
view().insert(signerList);
writeSignersToSLE(signerList, flags);
auto viewJ = ctx_.app.journal("View");
// Add the signer list to the account's directory.
auto const page = ctx_.view().dirInsert(
ownerDirKeylet, signerListKeylet, describeOwnerDir(account_));
JLOG(j_.trace()) << "Create signer list for account " << toBase58(account_)
<< ": " << (page ? "success" : "failure");
if (!page)
return tecDIR_FULL;
signerList->setFieldU64(sfOwnerNode, *page);
// If we succeeded, the new entry counts against the
// creator's reserve.
adjustOwnerCount(view(), sle, addedOwnerCount, viewJ);
return tesSUCCESS;
ctx_.view(),
j_,
account_,
quorum_,
signers_,
mPriorBalance);
}
TER
@@ -400,21 +344,24 @@ SetSignerList::destroySignerList()
void
SetSignerList::writeSignersToSLE(
ApplyView& view,
SLE::pointer const& ledgerEntry,
std::uint32_t flags) const
std::uint32_t flags,
std::uint32_t quorum,
std::vector<SignerEntries::SignerEntry> const& signers)
{
// Assign the quorum, default SignerListID, and flags.
ledgerEntry->setFieldU32(sfSignerQuorum, quorum_);
ledgerEntry->setFieldU32(sfSignerQuorum, quorum);
ledgerEntry->setFieldU32(sfSignerListID, defaultSignerListID_);
if (flags) // Only set flags if they are non-default (default is zero).
ledgerEntry->setFieldU32(sfFlags, flags);
bool const expandedSignerList =
ctx_.view().rules().enabled(featureExpandedSignerList);
view.rules().enabled(featureExpandedSignerList);
// Create the SignerListArray one SignerEntry at a time.
STArray toLedger(signers_.size());
for (auto const& entry : signers_)
STArray toLedger(signers.size());
for (auto const& entry : signers)
{
toLedger.emplace_back(sfSignerEntry);
STObject& obj = toLedger.back();

View File

@@ -73,6 +73,15 @@ public:
beast::Journal j);
private:
static
void
writeSignersToSLE(
ApplyView& view,
SLE::pointer const& ledgerEntry,
std::uint32_t flags,
std::uint32_t quorum,
std::vector<SignerEntries::SignerEntry> const& signers);
static std::tuple<
NotTEC,
std::uint32_t,
@@ -96,6 +105,84 @@ private:
void
writeSignersToSLE(SLE::pointer const& ledgerEntry, std::uint32_t flags)
const;
public:
template <typename V>
static TER
replaceSignersFromLedger(
Application& app,
V& view,
beast::Journal j,
AccountID const& acc,
std::uint32_t quorum,
std::vector<SignerEntries::SignerEntry> const& signers,
XRPAmount const mPriorBalance)
{
auto const accountKeylet = keylet::account(acc);
auto const ownerDirKeylet = keylet::ownerDir(acc);
auto const signerListKeylet = keylet::signers(acc);
// This may be either a create or a replace. Preemptively remove any
// old signer list. May reduce the reserve, so this is done before
// checking the reserve.
if (TER const ter = removeSignersFromLedger(
app,
view,
accountKeylet,
ownerDirKeylet,
signerListKeylet,
j))
return ter;
auto const sle = view.peek(accountKeylet);
if (!sle)
return tefINTERNAL;
// Compute new reserve. Verify the account has funds to meet the reserve.
std::uint32_t const oldOwnerCount{(*sle)[sfOwnerCount]};
// The required reserve changes based on featureMultiSignReserve...
int addedOwnerCount{1};
std::uint32_t flags{lsfOneOwnerCount};
if (!view.rules().enabled(featureMultiSignReserve))
{
addedOwnerCount = signerCountBasedOwnerCountDelta(
signers.size(), view.rules());
flags = 0;
}
XRPAmount const newReserve{
view.fees().accountReserve(oldOwnerCount + addedOwnerCount)};
// We check the reserve against the starting balance because we want to
// allow dipping into the reserve to pay fees. This behavior is consistent
// with CreateTicket.
if (mPriorBalance < newReserve)
return tecINSUFFICIENT_RESERVE;
// Everything's ducky. Add the ltSIGNER_LIST to the ledger.
auto signerList = std::make_shared<SLE>(signerListKeylet);
view.insert(signerList);
writeSignersToSLE(view, signerList, flags, quorum, signers);
// Add the signer list to the account's directory.
auto const page = view.dirInsert(
ownerDirKeylet, signerListKeylet, describeOwnerDir(acc));
JLOG(j.trace()) << "Create signer list for account " << toBase58(acc)
<< ": " << (page ? "success" : "failure");
if (!page)
return tecDIR_FULL;
signerList->setFieldU64(sfOwnerNode, *page);
// If we succeeded, the new entry counts against the
// creator's reserve.
adjustOwnerCount(view, sle, addedOwnerCount, j);
return tesSUCCESS;
}
};
} // namespace ripple

View File

@@ -33,7 +33,12 @@ getRaw) outer txid:
#include <ripple/protocol/Import.h>
#include <ripple/protocol/jss.h>
#include <test/jtx.h>
#define BEAST_REQUIRE(x) \
{ \
BEAST_EXPECT(!!(x)); \
if (!(x)) \
return; \
}
namespace ripple {
namespace test {
@@ -2541,6 +2546,7 @@ class Import_test : public beast::unit_test::suite
postAlice == preAlice + feeDrops + XRP(2) + XRP(0.000002));
auto const [signers, signersSle] =
signersKeyAndSle(*env.current(), alice);
BEAST_REQUIRE(!!signersSle);
auto const signerEntries =
signersSle->getFieldArray(sfSignerEntries);
BEAST_EXPECT(signerEntries.size() == 1);