#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace xrpl { static auto getTxFormat(TxType type) { auto format = TxFormats::getInstance().findByType(type); if (format == nullptr) { Throw( "Invalid transaction type " + std::to_string(safe_cast>(type))); } return format; } STTx::STTx(STObject&& object) : STObject(std::move(object)) { tx_type_ = safe_cast(getFieldU16(sfTransactionType)); applyTemplate(getTxFormat(tx_type_)->getSOTemplate()); // may throw tid_ = getHash(HashPrefix::transactionID); buildBatchTxnIds(); } STTx::STTx(SerialIter& sit) : STObject(sfTransaction) { int const length = sit.getBytesLeft(); if ((length < txMinSizeBytes) || (length > txMaxSizeBytes)) Throw("Transaction length invalid"); if (set(sit)) Throw("Transaction contains an object terminator"); tx_type_ = safe_cast(getFieldU16(sfTransactionType)); applyTemplate(getTxFormat(tx_type_)->getSOTemplate()); // May throw tid_ = getHash(HashPrefix::transactionID); buildBatchTxnIds(); } STTx::STTx(TxType type, std::function assembler) : STObject(sfTransaction) { auto format = getTxFormat(type); set(format->getSOTemplate()); setFieldU16(sfTransactionType, format->getType()); assembler(*this); tx_type_ = safe_cast(getFieldU16(sfTransactionType)); if (tx_type_ != type) LogicError("Transaction type was mutated during assembly"); tid_ = getHash(HashPrefix::transactionID); buildBatchTxnIds(); } 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 STTx::getMentionedAccounts() const { boost::container::flat_set list; for (auto const& it : *this) { if (auto sacc = dynamic_cast(&it)) { XRPL_ASSERT(!sacc->isDefault(), "xrpl::STTx::getMentionedAccounts : account is set"); if (!sacc->isDefault()) list.insert(sacc->value()); } else if (auto samt = dynamic_cast(&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 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(); } AccountID STTx::getFeePayer() const { // If sfDelegate is present, the delegate account is the payer // note: if a delegate is specified, its authorization to act on behalf of the account is // enforced in `Transactor::checkPermission` // cryptographic signature validity is checked separately (e.g., in `Transactor::checkSign`) if (isFieldPresent(sfDelegate)) return getAccountID(sfDelegate); // Default payer return getAccountID(sfAccount); } void STTx::sign( PublicKey const& publicKey, SecretKey const& secretKey, std::optional> 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 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 (...) { return Unexpected("Internal signature check failure."); } } Expected 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 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."); } if (!isFieldPresent(sfBatchSigners)) return Unexpected("Missing BatchSigners field."); 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) { return Unexpected(std::string("Internal batch signature check failure: ") + e.what()); } } 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 const 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; } 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, TxnSql::txnSqlValidated, escapedMetaData); } // VFALCO This could be a free function elsewhere std::string STTx::getMetaSQL( Serializer rawTxn, std::uint32_t inLedger, TxnSql status, std::string const& escapedMetaData) const { static boost::format const 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 % safe_cast(status) % rTxn % escapedMetaData); } static Expected 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 STTx::checkSingleSign(STObject const& sigObject) const { auto const data = getSigningData(*this); return singleSignHelper(sigObject, makeSlice(data)); } Expected STTx::checkBatchSingleSign(STObject const& batchSigner) const { Serializer msg; serializeBatch(msg, getFlags(), getBatchTransactionIDs()); finishMultiSigningData(batchSigner.getAccountID(sfAccount), msg); return singleSignHelper(batchSigner, msg.slice()); } Expected multiSignHelper( STObject const& sigObject, std::optional txnAccountID, std::function 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 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 ? ": " + *errorWhat : "") + "."); } } // All signatures verified. return {}; } Expected 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 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(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); } void STTx::buildBatchTxnIds() { if (tx_type_ != ttBATCH || !isFieldPresent(sfRawTransactions)) return; auto const& raw = getFieldArray(sfRawTransactions); batchTxnIds_.reserve(raw.size()); for (STObject const& rb : raw) batchTxnIds_.push_back(rb.getHash(HashPrefix::transactionID)); } std::vector const& STTx::getBatchTransactionIDs() const { XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::getBatchTransactionIDs : not a batch transaction"); XRPL_ASSERT( !batchTxnIds_.empty(), "STTx::getBatchTransactionIDs : batch transaction IDs not built"); 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(&memo); if ((memoObj == nullptr) || (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 const allowedSymbols = []() { std::array a{}; std::string_view const symbols( "0123456789" "-._~:/?#[]@!$&'()*+,;=%" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz"); for (unsigned char const c : symbols) a[c] = 1; return a; }(); for (unsigned char const c : *optData) { if (allowedSymbols[c] == 0) { 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(st.peekAtPIndex(i)); if ((t != nullptr) && 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))) { 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 && safe_downcast(field).holds()) || (field.getSType() == STI_ISSUE && safe_downcast(field).holds())) { 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(raw.getFieldU16(sfTransactionType)); if (tt == ttBATCH) { reason = "Raw Transactions may not contain batch transactions."; return false; } raw.applyTemplate(getTxFormat(tt)->getSOTemplate()); if (!passesLocalChecks(raw, reason)) return false; } 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 sterilize(STTx const& stx) { Serializer s; stx.add(s); SerialIter sit(s.slice()); return std::make_shared(std::ref(sit)); } bool isPseudoTx(STObject const& tx) { auto const t = tx[~sfTransactionType]; if (!t) return false; auto const tt = safe_cast(*t); return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY; } } // namespace xrpl