Files
rippled/src/libxrpl/protocol/STTx.cpp
2026-02-19 23:30:00 +00:00

804 lines
23 KiB
C++

#include <xrpl/basics/Blob.h>
#include <xrpl/basics/Expected.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
#include <xrpl/basics/safe_cast.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Batch.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/SOTemplate.h>
#include <xrpl/protocol/STAccount.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STBase.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/STVector256.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/Sign.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/jss.h>
#include <boost/container/flat_set.hpp>
#include <boost/format/format_fwd.hpp>
#include <boost/format/free_funcs.hpp>
#include <array>
#include <cstddef>
#include <cstdint>
#include <exception>
#include <functional>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
namespace xrpl {
static auto
getTxFormat(TxType type)
{
auto format = TxFormats::getInstance().findByType(type);
if (format == nullptr)
{
Throw<std::runtime_error>(
"Invalid transaction type " +
std::to_string(safe_cast<std::underlying_type_t<TxType>>(type)));
}
return format;
}
STTx::STTx(STObject&& object) : STObject(std::move(object))
{
tx_type_ = safe_cast<TxType>(getFieldU16(sfTransactionType));
applyTemplate(getTxFormat(tx_type_)->getSOTemplate()); // may throw
tid_ = getHash(HashPrefix::transactionID);
}
STTx::STTx(SerialIter& sit) : STObject(sfTransaction)
{
int length = sit.getBytesLeft();
if ((length < txMinSizeBytes) || (length > txMaxSizeBytes))
Throw<std::runtime_error>("Transaction length invalid");
if (set(sit))
Throw<std::runtime_error>("Transaction contains an object terminator");
tx_type_ = safe_cast<TxType>(getFieldU16(sfTransactionType));
applyTemplate(getTxFormat(tx_type_)->getSOTemplate()); // May throw
tid_ = getHash(HashPrefix::transactionID);
}
STTx::STTx(TxType type, std::function<void(STObject&)> assembler) : STObject(sfTransaction)
{
auto format = getTxFormat(type);
set(format->getSOTemplate());
setFieldU16(sfTransactionType, format->getType());
assembler(*this);
tx_type_ = safe_cast<TxType>(getFieldU16(sfTransactionType));
if (tx_type_ != type)
LogicError("Transaction type was mutated during assembly");
tid_ = getHash(HashPrefix::transactionID);
}
STBase*
STTx::copy(std::size_t n, void* buf) const
{
return emplace(n, buf, *this);
}
STBase*
STTx::move(std::size_t n, void* buf)
{
return emplace(n, buf, std::move(*this));
}
// STObject functions.
SerializedTypeID
STTx::getSType() const
{
return STI_TRANSACTION;
}
std::string
STTx::getFullText() const
{
std::string ret = "\"";
ret += to_string(getTransactionID());
ret += "\" = {";
ret += STObject::getFullText();
ret += "}";
return ret;
}
boost::container::flat_set<AccountID>
STTx::getMentionedAccounts() const
{
boost::container::flat_set<AccountID> list;
for (auto const& it : *this)
{
if (auto sacc = dynamic_cast<STAccount const*>(&it))
{
XRPL_ASSERT(!sacc->isDefault(), "xrpl::STTx::getMentionedAccounts : account is set");
if (!sacc->isDefault())
list.insert(sacc->value());
}
else if (auto samt = dynamic_cast<STAmount const*>(&it))
{
auto const& issuer = samt->getIssuer();
if (!isXRP(issuer))
list.insert(issuer);
}
}
return list;
}
static Blob
getSigningData(STTx const& that)
{
Serializer s;
s.add32(HashPrefix::txSign);
that.addWithoutSigningFields(s);
return s.getData();
}
uint256
STTx::getSigningHash() const
{
return STObject::getSigningHash(HashPrefix::txSign);
}
Blob
STTx::getSignature(STObject const& sigObject)
{
try
{
return sigObject.getFieldVL(sfTxnSignature);
}
catch (std::exception const&)
{
return Blob();
}
}
SeqProxy
STTx::getSeqProxy() const
{
std::uint32_t const seq{getFieldU32(sfSequence)};
if (seq != 0)
return SeqProxy::sequence(seq);
std::optional<std::uint32_t> const ticketSeq{operator[](~sfTicketSequence)};
if (!ticketSeq)
// No TicketSequence specified. Return the Sequence, whatever it is.
return SeqProxy::sequence(seq);
return SeqProxy{SeqProxy::ticket, *ticketSeq};
}
std::uint32_t
STTx::getSeqValue() const
{
return getSeqProxy().value();
}
void
STTx::sign(
PublicKey const& publicKey,
SecretKey const& secretKey,
std::optional<std::reference_wrapper<SField const>> signatureTarget)
{
auto const data = getSigningData(*this);
auto const sig = xrpl::sign(publicKey, secretKey, makeSlice(data));
if (signatureTarget)
{
auto& target = peekFieldObject(*signatureTarget);
target.setFieldVL(sfTxnSignature, sig);
}
else
{
setFieldVL(sfTxnSignature, sig);
}
tid_ = getHash(HashPrefix::transactionID);
}
Expected<void, std::string>
STTx::checkSign(Rules const& rules, STObject const& sigObject) const
{
try
{
// Determine whether we're single- or multi-signing by looking
// at the SigningPubKey. If it's empty we must be
// multi-signing. Otherwise we're single-signing.
Blob const& signingPubKey = sigObject.getFieldVL(sfSigningPubKey);
return signingPubKey.empty() ? checkMultiSign(rules, sigObject)
: checkSingleSign(sigObject);
}
catch (std::exception const&)
{
}
return Unexpected("Internal signature check failure.");
}
Expected<void, std::string>
STTx::checkSign(Rules const& rules) const
{
if (auto const ret = checkSign(rules, *this); !ret)
return ret;
if (isFieldPresent(sfCounterpartySignature))
{
auto const counterSig = getFieldObject(sfCounterpartySignature);
if (auto const ret = checkSign(rules, counterSig); !ret)
return Unexpected("Counterparty: " + ret.error());
}
return {};
}
Expected<void, std::string>
STTx::checkBatchSign(Rules const& rules) const
{
try
{
XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::checkBatchSign : not a batch transaction");
if (getTxnType() != ttBATCH)
{
JLOG(debugLog().fatal()) << "not a batch transaction";
return Unexpected("Not a batch transaction.");
}
STArray const& signers{getFieldArray(sfBatchSigners)};
for (auto const& signer : signers)
{
Blob const& signingPubKey = signer.getFieldVL(sfSigningPubKey);
auto const result = signingPubKey.empty() ? checkBatchMultiSign(signer, rules)
: checkBatchSingleSign(signer);
if (!result)
return result;
}
return {};
}
catch (std::exception const& e)
{
JLOG(debugLog().error()) << "Batch signature check failed: " << e.what();
}
return Unexpected("Internal batch signature check failure.");
}
Json::Value
STTx::getJson(JsonOptions options) const
{
Json::Value ret = STObject::getJson(JsonOptions::none);
if (!(options & JsonOptions::disable_API_prior_V2))
ret[jss::hash] = to_string(getTransactionID());
return ret;
}
Json::Value
STTx::getJson(JsonOptions options, bool binary) const
{
bool const V1 = !(options & JsonOptions::disable_API_prior_V2);
if (binary)
{
Serializer s = STObject::getSerializer();
std::string const dataBin = strHex(s.peekData());
if (V1)
{
Json::Value ret(Json::objectValue);
ret[jss::tx] = dataBin;
ret[jss::hash] = to_string(getTransactionID());
return ret;
}
else
return Json::Value{dataBin};
}
Json::Value ret = STObject::getJson(JsonOptions::none);
if (V1)
ret[jss::hash] = to_string(getTransactionID());
return ret;
}
std::string const&
STTx::getMetaSQLInsertReplaceHeader()
{
static std::string const sql =
"INSERT OR REPLACE INTO Transactions "
"(TransID, TransType, FromAcct, FromSeq, LedgerSeq, Status, RawTxn, "
"TxnMeta)"
" VALUES ";
return sql;
}
std::string
STTx::getMetaSQL(std::uint32_t inLedger, std::string const& escapedMetaData) const
{
Serializer s;
add(s);
return getMetaSQL(s, inLedger, txnSqlValidated, escapedMetaData);
}
// VFALCO This could be a free function elsewhere
std::string
STTx::getMetaSQL(
Serializer rawTxn,
std::uint32_t inLedger,
char status,
std::string const& escapedMetaData) const
{
static boost::format bfTrans("('%s', '%s', '%s', '%d', '%d', '%c', %s, %s)");
std::string rTxn = sqlBlobLiteral(rawTxn.peekData());
auto format = TxFormats::getInstance().findByType(tx_type_);
XRPL_ASSERT(format, "xrpl::STTx::getMetaSQL : non-null type format");
return str(
boost::format(bfTrans) % to_string(getTransactionID()) % format->getName() %
toBase58(getAccountID(sfAccount)) % getFieldU32(sfSequence) % inLedger % status % rTxn %
escapedMetaData);
}
static Expected<void, std::string>
singleSignHelper(STObject const& sigObject, Slice const& data)
{
// We don't allow both a non-empty sfSigningPubKey and an sfSigners.
// That would allow the transaction to be signed two ways. So if both
// fields are present the signature is invalid.
if (sigObject.isFieldPresent(sfSigners))
return Unexpected("Cannot both single- and multi-sign.");
bool validSig = false;
try
{
auto const spk = sigObject.getFieldVL(sfSigningPubKey);
if (publicKeyType(makeSlice(spk)))
{
Blob const signature = sigObject.getFieldVL(sfTxnSignature);
validSig = verify(PublicKey(makeSlice(spk)), data, makeSlice(signature));
}
}
catch (std::exception const&)
{
validSig = false;
}
if (!validSig)
return Unexpected("Invalid signature.");
return {};
}
Expected<void, std::string>
STTx::checkSingleSign(STObject const& sigObject) const
{
auto const data = getSigningData(*this);
return singleSignHelper(sigObject, makeSlice(data));
}
Expected<void, std::string>
STTx::checkBatchSingleSign(STObject const& batchSigner) const
{
Serializer msg;
serializeBatch(msg, getFlags(), getBatchTransactionIDs());
return singleSignHelper(batchSigner, msg.slice());
}
Expected<void, std::string>
multiSignHelper(
STObject const& sigObject,
std::optional<AccountID> txnAccountID,
std::function<Serializer(AccountID const&)> makeMsg,
Rules const& rules)
{
// Make sure the MultiSigners are present. Otherwise they are not
// attempting multi-signing and we just have a bad SigningPubKey.
if (!sigObject.isFieldPresent(sfSigners))
return Unexpected("Empty SigningPubKey.");
// We don't allow both an sfSigners and an sfTxnSignature. Both fields
// being present would indicate that the transaction is signed both ways.
if (sigObject.isFieldPresent(sfTxnSignature))
return Unexpected("Cannot both single- and multi-sign.");
STArray const& signers{sigObject.getFieldArray(sfSigners)};
// There are well known bounds that the number of signers must be within.
if (signers.size() < STTx::minMultiSigners || signers.size() > STTx::maxMultiSigners)
return Unexpected("Invalid Signers array size.");
// Signers must be in sorted order by AccountID.
AccountID lastAccountID(beast::zero);
for (auto const& signer : signers)
{
auto const accountID = signer.getAccountID(sfAccount);
// The account owner may not usually multisign for themselves.
// If they can, txnAccountID will be unseated, which is not equal to any
// value.
if (txnAccountID == accountID)
return Unexpected("Invalid multisigner.");
// No duplicate signers allowed.
if (lastAccountID == accountID)
return Unexpected("Duplicate Signers not allowed.");
// Accounts must be in order by account ID. No duplicates allowed.
if (lastAccountID > accountID)
return Unexpected("Unsorted Signers array.");
// The next signature must be greater than this one.
lastAccountID = accountID;
// Verify the signature.
bool validSig = false;
std::optional<std::string> errorWhat;
try
{
auto spk = signer.getFieldVL(sfSigningPubKey);
if (publicKeyType(makeSlice(spk)))
{
Blob const signature = signer.getFieldVL(sfTxnSignature);
validSig = verify(
PublicKey(makeSlice(spk)), makeMsg(accountID).slice(), makeSlice(signature));
}
}
catch (std::exception const& e)
{
// We assume any problem lies with the signature.
validSig = false;
errorWhat = e.what();
}
if (!validSig)
return Unexpected(
std::string("Invalid signature on account ") + toBase58(accountID) +
errorWhat.value_or("") + ".");
}
// All signatures verified.
return {};
}
Expected<void, std::string>
STTx::checkBatchMultiSign(STObject const& batchSigner, Rules const& rules) const
{
// We can ease the computational load inside the loop a bit by
// pre-constructing part of the data that we hash. Fill a Serializer
// with the stuff that stays constant from signature to signature.
Serializer dataStart;
serializeBatch(dataStart, getFlags(), getBatchTransactionIDs());
return multiSignHelper(
batchSigner,
std::nullopt,
[&dataStart](AccountID const& accountID) -> Serializer {
Serializer s = dataStart;
finishMultiSigningData(accountID, s);
return s;
},
rules);
}
Expected<void, std::string>
STTx::checkMultiSign(Rules const& rules, STObject const& sigObject) const
{
// Used inside the loop in multiSignHelper to enforce that
// the account owner may not multisign for themselves.
auto const txnAccountID =
&sigObject != this ? std::nullopt : std::optional<AccountID>(getAccountID(sfAccount));
// We can ease the computational load inside the loop a bit by
// pre-constructing part of the data that we hash. Fill a Serializer
// with the stuff that stays constant from signature to signature.
Serializer dataStart = startMultiSigningData(*this);
return multiSignHelper(
sigObject,
txnAccountID,
[&dataStart](AccountID const& accountID) -> Serializer {
Serializer s = dataStart;
finishMultiSigningData(accountID, s);
return s;
},
rules);
}
/**
* @brief Retrieves a batch of transaction IDs from the STTx.
*
* This function returns a vector of transaction IDs by extracting them from
* the field array `sfRawTransactions` within the STTx. If the batch
* transaction IDs have already been computed and cached in `batchTxnIds_`,
* it returns the cached vector. Otherwise, it computes the transaction IDs,
* caches them, and then returns the vector.
*
* @return A vector of `uint256` containing the batch transaction IDs.
*
* @note The function asserts that the `sfRawTransactions` field array is not
* empty and that the size of the computed batch transaction IDs matches the
* size of the `sfRawTransactions` field array.
*/
std::vector<uint256> const&
STTx::getBatchTransactionIDs() const
{
XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::getBatchTransactionIDs : not a batch transaction");
XRPL_ASSERT(
getFieldArray(sfRawTransactions).size() != 0,
"STTx::getBatchTransactionIDs : empty raw transactions");
// The list of inner ids is built once, then reused on subsequent calls.
// After the list is built, it must always have the same size as the array
// `sfRawTransactions`. The assert below verifies that.
if (batchTxnIds_.size() == 0)
{
for (STObject const& rb : getFieldArray(sfRawTransactions))
batchTxnIds_.push_back(rb.getHash(HashPrefix::transactionID));
}
XRPL_ASSERT(
batchTxnIds_.size() == getFieldArray(sfRawTransactions).size(),
"STTx::getBatchTransactionIDs : batch transaction IDs size mismatch");
return batchTxnIds_;
}
//------------------------------------------------------------------------------
static bool
isMemoOkay(STObject const& st, std::string& reason)
{
if (!st.isFieldPresent(sfMemos))
return true;
auto const& memos = st.getFieldArray(sfMemos);
// The number 2048 is a preallocation hint, not a hard limit
// to avoid allocate/copy/free's
Serializer s(2048);
memos.add(s);
// FIXME move the memo limit into a config tunable
if (s.getDataLength() > 1024)
{
reason = "The memo exceeds the maximum allowed size.";
return false;
}
for (auto const& memo : memos)
{
auto memoObj = dynamic_cast<STObject const*>(&memo);
if (!memoObj || (memoObj->getFName() != sfMemo))
{
reason = "A memo array may contain only Memo objects.";
return false;
}
for (auto const& memoElement : *memoObj)
{
auto const& name = memoElement.getFName();
if (name != sfMemoType && name != sfMemoData && name != sfMemoFormat)
{
reason =
"A memo may contain only MemoType, MemoData or "
"MemoFormat fields.";
return false;
}
// The raw data is stored as hex-octets, which we want to decode.
auto optData = strUnHex(memoElement.getText());
if (!optData)
{
reason =
"The MemoType, MemoData and MemoFormat fields may "
"only contain hex-encoded data.";
return false;
}
if (name == sfMemoData)
continue;
// The only allowed characters for MemoType and MemoFormat are the
// characters allowed in URLs per RFC 3986: alphanumerics and the
// following symbols: -._~:/?#[]@!$&'()*+,;=%
static constexpr std::array<char, 256> const allowedSymbols = []() {
std::array<char, 256> a{};
std::string_view symbols(
"0123456789"
"-._~:/?#[]@!$&'()*+,;=%"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz");
for (unsigned char c : symbols)
a[c] = 1;
return a;
}();
for (unsigned char c : *optData)
{
if (!allowedSymbols[c])
{
reason =
"The MemoType and MemoFormat fields may only "
"contain characters that are allowed in URLs "
"under RFC 3986.";
return false;
}
}
}
}
return true;
}
// Ensure all account fields are 160-bits
static bool
isAccountFieldOkay(STObject const& st)
{
for (int i = 0; i < st.getCount(); ++i)
{
auto t = dynamic_cast<STAccount const*>(st.peekAtPIndex(i));
if (t && t->isDefault())
return false;
}
return true;
}
static bool
invalidMPTAmountInTx(STObject const& tx)
{
auto const txType = tx[~sfTransactionType];
if (!txType)
return false;
if (auto const* item = TxFormats::getInstance().findByType(safe_cast<TxType>(*txType)))
{
for (auto const& e : item->getSOTemplate())
{
if (tx.isFieldPresent(e.sField()) && e.supportMPT() != soeMPTNone)
{
if (auto const& field = tx.peekAtField(e.sField());
(field.getSType() == STI_AMOUNT &&
static_cast<STAmount const&>(field).holds<MPTIssue>()) ||
(field.getSType() == STI_ISSUE &&
static_cast<STIssue const&>(field).holds<MPTIssue>()))
{
if (e.supportMPT() != soeMPTSupported)
return true;
}
}
}
}
return false;
}
static bool
isRawTransactionOkay(STObject const& st, std::string& reason)
{
if (!st.isFieldPresent(sfRawTransactions))
return true;
if (st.isFieldPresent(sfBatchSigners) &&
st.getFieldArray(sfBatchSigners).size() > maxBatchTxCount)
{
reason = "Batch Signers array exceeds max entries.";
return false;
}
auto const& rawTxns = st.getFieldArray(sfRawTransactions);
if (rawTxns.size() > maxBatchTxCount)
{
reason = "Raw Transactions array exceeds max entries.";
return false;
}
for (STObject raw : rawTxns)
{
try
{
TxType const tt = safe_cast<TxType>(raw.getFieldU16(sfTransactionType));
if (tt == ttBATCH)
{
reason = "Raw Transactions may not contain batch transactions.";
return false;
}
raw.applyTemplate(getTxFormat(tt)->getSOTemplate());
}
catch (std::exception const& e)
{
reason = e.what();
return false;
}
}
return true;
}
bool
passesLocalChecks(STObject const& st, std::string& reason)
{
if (!isMemoOkay(st, reason))
return false;
if (!isAccountFieldOkay(st))
{
reason = "An account field is invalid.";
return false;
}
if (isPseudoTx(st))
{
reason = "Cannot submit pseudo transactions.";
return false;
}
if (invalidMPTAmountInTx(st))
{
reason = "Amount can not be MPT.";
return false;
}
if (!isRawTransactionOkay(st, reason))
return false;
return true;
}
std::shared_ptr<STTx const>
sterilize(STTx const& stx)
{
Serializer s;
stx.add(s);
SerialIter sit(s.slice());
return std::make_shared<STTx const>(std::ref(sit));
}
bool
isPseudoTx(STObject const& tx)
{
auto const t = tx[~sfTransactionType];
if (!t)
return false;
auto const tt = safe_cast<TxType>(*t);
return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY;
}
} // namespace xrpl