{
  "amendment_name": "Batch / XLS-56",
  "code_excerpts": {
    "rippled_2_5_0_STTx_batch_signature_window": "  269  STTx::checkBatchSign(\n  270      RequireFullyCanonicalSig requireCanonicalSig,\n  271      Rules const& rules) const\n  272  {\n  273      try\n  274      {\n  275          XRPL_ASSERT(\n  276              getTxnType() == ttBATCH,\n  277              \"STTx::checkBatchSign : not a batch transaction\");\n  278          if (getTxnType() != ttBATCH)\n  279          {\n  280              JLOG(debugLog().fatal()) << \"not a batch transaction\";\n  281              return Unexpected(\"Not a batch transaction.\");\n  282          }\n  283          STArray const& signers{getFieldArray(sfBatchSigners)};\n  284          for (auto const& signer : signers)\n  285          {\n  286              Blob const& signingPubKey = signer.getFieldVL(sfSigningPubKey);\n  287              auto const result = signingPubKey.empty()\n  288                  ? checkBatchMultiSign(signer, requireCanonicalSig, rules)\n  289                  : checkBatchSingleSign(signer, requireCanonicalSig);\n  290  \n  291              if (!result)\n  292                  return result;\n  293          }\n  294          return {};\n  295      }\n  296      catch (std::exception const& e)\n  297      {\n  298          JLOG(debugLog().error())\n  299              << \"Batch signature check failed: \" << e.what();\n  300      }\n  301      return Unexpected(\"Internal batch signature check failure.\");\n  302  }\n  303  \n  304  Json::Value\n  305  STTx::getJson(JsonOptions options) const\n  306  {\n  307      Json::Value ret = STObject::getJson(JsonOptions::none);\n  308      if (!(options & JsonOptions::disable_API_prior_V2))\n  309          ret[jss::hash] = to_string(getTransactionID());\n  310      return ret;\n  311  }\n  312  \n  313  Json::Value\n  314  STTx::getJson(JsonOptions options, bool binary) const\n  315  {\n  316      bool const V1 = !(options & JsonOptions::disable_API_prior_V2);\n  317  \n  318      if (binary)\n  319      {\n  320          Serializer s = STObject::getSerializer();\n  321          std::string const dataBin = strHex(s.peekData());\n  322  \n  323          if (V1)\n  324          {\n  325              Json::Value ret(Json::objectValue);\n  326              ret[jss::tx] = dataBin;\n  327              ret[jss::hash] = to_string(getTransactionID());\n  328              return ret;\n  329          }\n  330          else\n  331              return Json::Value{dataBin};\n  332      }\n  333  \n  334      Json::Value ret = STObject::getJson(JsonOptions::none);\n  335      if (V1)\n  336          ret[jss::hash] = to_string(getTransactionID());\n  337  \n  338      return ret;\n  339  }\n  340  \n  341  std::string const&\n  342  STTx::getMetaSQLInsertReplaceHeader()\n  343  {\n  344      static std::string const sql =\n  345          \"INSERT OR REPLACE INTO Transactions \"\n  346          \"(TransID, TransType, FromAcct, FromSeq, LedgerSeq, Status, RawTxn, \"\n  347          \"TxnMeta)\"\n  348          \" VALUES \";\n  349  \n  350      return sql;\n  351  }\n  352  \n  353  std::string\n  354  STTx::getMetaSQL(std::uint32_t inLedger, std::string const& escapedMetaData)\n  355      const\n  356  {\n  357      Serializer s;\n  358      add(s);\n  359      return getMetaSQL(s, inLedger, txnSqlValidated, escapedMetaData);\n  360  }\n  361  \n  362  // VFALCO This could be a free function elsewhere\n  363  std::string\n  364  STTx::getMetaSQL(\n  365      Serializer rawTxn,\n  366      std::uint32_t inLedger,\n  367      char status,\n  368      std::string const& escapedMetaData) const\n  369  {\n  370      static boost::format bfTrans(\n  371          \"('%s', '%s', '%s', '%d', '%d', '%c', %s, %s)\");\n  372      std::string rTxn = sqlBlobLiteral(rawTxn.peekData());\n  373  \n  374      auto format = TxFormats::getInstance().findByType(tx_type_);\n  375      XRPL_ASSERT(format, \"ripple::STTx::getMetaSQL : non-null type format\");\n  376  \n  377      return str(\n  378          boost::format(bfTrans) % to_string(getTransactionID()) %\n  379          format->getName() % toBase58(getAccountID(sfAccount)) %\n  380          getFieldU32(sfSequence) % inLedger % status % rTxn % escapedMetaData);\n  381  }\n  382  \n  383  static Expected<void, std::string>\n  384  singleSignHelper(\n  385      STObject const& signer,\n  386      Slice const& data,\n  387      bool const fullyCanonical)\n  388  {\n  389      // We don't allow both a non-empty sfSigningPubKey and an sfSigners.\n  390      // That would allow the transaction to be signed two ways.  So if both\n  391      // fields are present the signature is invalid.\n  392      if (signer.isFieldPresent(sfSigners))\n  393          return Unexpected(\"Cannot both single- and multi-sign.\");\n  394  \n  395      bool validSig = false;\n  396      try\n  397      {\n  398          auto const spk = signer.getFieldVL(sfSigningPubKey);\n  399          if (publicKeyType(makeSlice(spk)))\n  400          {\n  401              Blob const signature = signer.getFieldVL(sfTxnSignature);\n  402              validSig = verify(\n  403                  PublicKey(makeSlice(spk)),\n  404                  data,\n  405                  makeSlice(signature),\n  406                  fullyCanonical);\n  407          }\n  408      }\n  409      catch (std::exception const&)\n  410      {\n  411          validSig = false;\n  412      }\n  413  \n  414      if (!validSig)\n  415          return Unexpected(\"Invalid signature.\");\n  416  \n  417      return {};\n  418  }\n  419  \n  420  Expected<void, std::string>\n  421  STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const\n  422  {\n  423      auto const data = getSigningData(*this);\n  424      bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) ||\n  425          (requireCanonicalSig == STTx::RequireFullyCanonicalSig::yes);\n  426      return singleSignHelper(*this, makeSlice(data), fullyCanonical);\n  427  }\n  428  \n  429  Expected<void, std::string>\n  430  STTx::checkBatchSingleSign(\n  431      STObject const& batchSigner,\n  432      RequireFullyCanonicalSig requireCanonicalSig) const\n  433  {\n  434      Serializer msg;\n  435      serializeBatch(msg, getFlags(), getBatchTransactionIDs());\n  436      bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) ||\n  437          (requireCanonicalSig == STTx::RequireFullyCanonicalSig::yes);\n  438      return singleSignHelper(batchSigner, msg.slice(), fullyCanonical);\n  439  }\n  440  \n  441  Expected<void, std::string>\n  442  multiSignHelper(\n  443      STObject const& signerObj,\n  444      bool const fullyCanonical,\n  445      std::function<Serializer(AccountID const&)> makeMsg,\n  446      Rules const& rules)\n  447  {\n  448      // Make sure the MultiSigners are present.  Otherwise they are not\n  449      // attempting multi-signing and we just have a bad SigningPubKey.\n  450      if (!signerObj.isFieldPresent(sfSigners))\n  451          return Unexpected(\"Empty SigningPubKey.\");\n  452  \n  453      // We don't allow both an sfSigners and an sfTxnSignature.  Both fields\n  454      // being present would indicate that the transaction is signed both ways.\n  455      if (signerObj.isFieldPresent(sfTxnSignature))\n  456          return Unexpected(\"Cannot both single- and multi-sign.\");\n  457  \n  458      STArray const& signers{signerObj.getFieldArray(sfSigners)};\n  459  \n  460      // There are well known bounds that the number of signers must be within.\n  461      if (signers.size() < STTx::minMultiSigners ||\n  462          signers.size() > STTx::maxMultiSigners(&rules))\n  463          return Unexpected(\"Invalid Signers array size.\");\n  464  \n  465      // We also use the sfAccount field inside the loop.  Get it once.\n  466      auto const txnAccountID = signerObj.getAccountID(sfAccount);\n  467  \n  468      // Signers must be in sorted order by AccountID.\n  469      AccountID lastAccountID(beast::zero);\n  470  \n  471      for (auto const& signer : signers)\n  472      {\n  473          auto const accountID = signer.getAccountID(sfAccount);\n  474  \n  475          // The account owner may not multisign for themselves.\n  476          if (accountID == txnAccountID)\n  477              return Unexpected(\"Invalid multisigner.\");\n  478  \n  479          // No duplicate signers allowed.\n  480          if (lastAccountID == accountID)\n  481              return Unexpected(\"Duplicate Signers not allowed.\");\n  482  \n  483          // Accounts must be in order by account ID.  No duplicates allowed.\n  484          if (lastAccountID > accountID)\n  485              return Unexpected(\"Unsorted Signers array.\");\n  486  \n  487          // The next signature must be greater than this one.\n  488          lastAccountID = accountID;\n  489  \n  490          // Verify the signature.\n  491          bool validSig = false;\n  492          try\n  493          {\n  494              auto spk = signer.getFieldVL(sfSigningPubKey);\n  495              if (publicKeyType(makeSlice(spk)))\n  496              {\n  497                  Blob const signature = signer.getFieldVL(sfTxnSignature);\n  498                  validSig = verify(\n  499                      PublicKey(makeSlice(spk)),\n  500                      makeMsg(accountID).slice(),\n  501                      makeSlice(signature),\n  502                      fullyCanonical);\n  503              }\n  504          }\n  505          catch (std::exception const&)\n  506          {\n  507              // We assume any problem lies with the signature.\n  508              validSig = false;\n  509          }\n  510          if (!validSig)\n  511              return Unexpected(\n  512                  std::string(\"Invalid signature on account \") +\n  513                  toBase58(accountID) + \".\");\n  514      }\n  515      // All signatures verified.\n  516      return {};\n  517  }\n  518  \n  519  Expected<void, std::string>\n  520  STTx::checkBatchMultiSign(\n  521      STObject const& batchSigner,\n  522      RequireFullyCanonicalSig requireCanonicalSig,\n  523      Rules const& rules) const\n  524  {\n  525      bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) ||\n  526          (requireCanonicalSig == RequireFullyCanonicalSig::yes);\n  527  \n  528      // We can ease the computational load inside the loop a bit by\n  529      // pre-constructing part of the data that we hash.  Fill a Serializer\n  530      // with the stuff that stays constant from signature to signature.\n  531      Serializer dataStart;\n  532      serializeBatch(dataStart, getFlags(), getBatchTransactionIDs());\n  533      return multiSignHelper(\n  534          batchSigner,\n  535          fullyCanonical,\n  536          [&dataStart](AccountID const& accountID) mutable -> Serializer {\n  537              Serializer s = dataStart;\n  538              finishMultiSigningData(accountID, s);\n  539              return s;\n  540          },\n  541          rules);\n",
    "rippled_3_1_3_STTx_batch_signature_window": "  300  STTx::checkBatchSign(\n  301      RequireFullyCanonicalSig requireCanonicalSig,\n  302      Rules const& rules) const\n  303  {\n  304      try\n  305      {\n  306          XRPL_ASSERT(\n  307              getTxnType() == ttBATCH,\n  308              \"STTx::checkBatchSign : not a batch transaction\");\n  309          if (getTxnType() != ttBATCH)\n  310          {\n  311              JLOG(debugLog().fatal()) << \"not a batch transaction\";\n  312              return Unexpected(\"Not a batch transaction.\");\n  313          }\n  314          STArray const& signers{getFieldArray(sfBatchSigners)};\n  315          for (auto const& signer : signers)\n  316          {\n  317              Blob const& signingPubKey = signer.getFieldVL(sfSigningPubKey);\n  318              auto const result = signingPubKey.empty()\n  319                  ? checkBatchMultiSign(signer, requireCanonicalSig, rules)\n  320                  : checkBatchSingleSign(signer, requireCanonicalSig);\n  321  \n  322              if (!result)\n  323                  return result;\n  324          }\n  325          return {};\n  326      }\n  327      catch (std::exception const& e)\n  328      {\n  329          JLOG(debugLog().error())\n  330              << \"Batch signature check failed: \" << e.what();\n  331      }\n  332      return Unexpected(\"Internal batch signature check failure.\");\n  333  }\n  334  \n  335  Json::Value\n  336  STTx::getJson(JsonOptions options) const\n  337  {\n  338      Json::Value ret = STObject::getJson(JsonOptions::none);\n  339      if (!(options & JsonOptions::disable_API_prior_V2))\n  340          ret[jss::hash] = to_string(getTransactionID());\n  341      return ret;\n  342  }\n  343  \n  344  Json::Value\n  345  STTx::getJson(JsonOptions options, bool binary) const\n  346  {\n  347      bool const V1 = !(options & JsonOptions::disable_API_prior_V2);\n  348  \n  349      if (binary)\n  350      {\n  351          Serializer s = STObject::getSerializer();\n  352          std::string const dataBin = strHex(s.peekData());\n  353  \n  354          if (V1)\n  355          {\n  356              Json::Value ret(Json::objectValue);\n  357              ret[jss::tx] = dataBin;\n  358              ret[jss::hash] = to_string(getTransactionID());\n  359              return ret;\n  360          }\n  361          else\n  362              return Json::Value{dataBin};\n  363      }\n  364  \n  365      Json::Value ret = STObject::getJson(JsonOptions::none);\n  366      if (V1)\n  367          ret[jss::hash] = to_string(getTransactionID());\n  368  \n  369      return ret;\n  370  }\n  371  \n  372  std::string const&\n  373  STTx::getMetaSQLInsertReplaceHeader()\n  374  {\n  375      static std::string const sql =\n  376          \"INSERT OR REPLACE INTO Transactions \"\n  377          \"(TransID, TransType, FromAcct, FromSeq, LedgerSeq, Status, RawTxn, \"\n  378          \"TxnMeta)\"\n  379          \" VALUES \";\n  380  \n  381      return sql;\n  382  }\n  383  \n  384  std::string\n  385  STTx::getMetaSQL(std::uint32_t inLedger, std::string const& escapedMetaData)\n  386      const\n  387  {\n  388      Serializer s;\n  389      add(s);\n  390      return getMetaSQL(s, inLedger, txnSqlValidated, escapedMetaData);\n  391  }\n  392  \n  393  // VFALCO This could be a free function elsewhere\n  394  std::string\n  395  STTx::getMetaSQL(\n  396      Serializer rawTxn,\n  397      std::uint32_t inLedger,\n  398      char status,\n  399      std::string const& escapedMetaData) const\n  400  {\n  401      static boost::format bfTrans(\n  402          \"('%s', '%s', '%s', '%d', '%d', '%c', %s, %s)\");\n  403      std::string rTxn = sqlBlobLiteral(rawTxn.peekData());\n  404  \n  405      auto format = TxFormats::getInstance().findByType(tx_type_);\n  406      XRPL_ASSERT(format, \"ripple::STTx::getMetaSQL : non-null type format\");\n  407  \n  408      return str(\n  409          boost::format(bfTrans) % to_string(getTransactionID()) %\n  410          format->getName() % toBase58(getAccountID(sfAccount)) %\n  411          getFieldU32(sfSequence) % inLedger % status % rTxn % escapedMetaData);\n  412  }\n  413  \n  414  static Expected<void, std::string>\n  415  singleSignHelper(\n  416      STObject const& sigObject,\n  417      Slice const& data,\n  418      bool const fullyCanonical)\n  419  {\n  420      // We don't allow both a non-empty sfSigningPubKey and an sfSigners.\n  421      // That would allow the transaction to be signed two ways.  So if both\n  422      // fields are present the signature is invalid.\n  423      if (sigObject.isFieldPresent(sfSigners))\n  424          return Unexpected(\"Cannot both single- and multi-sign.\");\n  425  \n  426      bool validSig = false;\n  427      try\n  428      {\n  429          auto const spk = sigObject.getFieldVL(sfSigningPubKey);\n  430          if (publicKeyType(makeSlice(spk)))\n  431          {\n  432              Blob const signature = sigObject.getFieldVL(sfTxnSignature);\n  433              validSig = verify(\n  434                  PublicKey(makeSlice(spk)),\n  435                  data,\n  436                  makeSlice(signature),\n  437                  fullyCanonical);\n  438          }\n  439      }\n  440      catch (std::exception const&)\n  441      {\n  442          validSig = false;\n  443      }\n  444  \n  445      if (!validSig)\n  446          return Unexpected(\"Invalid signature.\");\n  447  \n  448      return {};\n  449  }\n  450  \n  451  Expected<void, std::string>\n  452  STTx::checkSingleSign(\n  453      RequireFullyCanonicalSig requireCanonicalSig,\n  454      STObject const& sigObject) const\n  455  {\n  456      auto const data = getSigningData(*this);\n  457      bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) ||\n  458          (requireCanonicalSig == STTx::RequireFullyCanonicalSig::yes);\n  459      return singleSignHelper(sigObject, makeSlice(data), fullyCanonical);\n  460  }\n  461  \n  462  Expected<void, std::string>\n  463  STTx::checkBatchSingleSign(\n  464      STObject const& batchSigner,\n  465      RequireFullyCanonicalSig requireCanonicalSig) const\n  466  {\n  467      Serializer msg;\n  468      serializeBatch(msg, getFlags(), getBatchTransactionIDs());\n  469      bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) ||\n  470          (requireCanonicalSig == STTx::RequireFullyCanonicalSig::yes);\n  471      return singleSignHelper(batchSigner, msg.slice(), fullyCanonical);\n  472  }\n  473  \n  474  Expected<void, std::string>\n  475  multiSignHelper(\n  476      STObject const& sigObject,\n  477      std::optional<AccountID> txnAccountID,\n  478      bool const fullyCanonical,\n  479      std::function<Serializer(AccountID const&)> makeMsg,\n  480      Rules const& rules)\n  481  {\n  482      // Make sure the MultiSigners are present.  Otherwise they are not\n  483      // attempting multi-signing and we just have a bad SigningPubKey.\n  484      if (!sigObject.isFieldPresent(sfSigners))\n  485          return Unexpected(\"Empty SigningPubKey.\");\n  486  \n  487      // We don't allow both an sfSigners and an sfTxnSignature.  Both fields\n  488      // being present would indicate that the transaction is signed both ways.\n  489      if (sigObject.isFieldPresent(sfTxnSignature))\n  490          return Unexpected(\"Cannot both single- and multi-sign.\");\n  491  \n  492      STArray const& signers{sigObject.getFieldArray(sfSigners)};\n  493  \n  494      // There are well known bounds that the number of signers must be within.\n  495      if (signers.size() < STTx::minMultiSigners ||\n  496          signers.size() > STTx::maxMultiSigners(&rules))\n  497          return Unexpected(\"Invalid Signers array size.\");\n  498  \n  499      // Signers must be in sorted order by AccountID.\n  500      AccountID lastAccountID(beast::zero);\n  501  \n  502      for (auto const& signer : signers)\n  503      {\n  504          auto const accountID = signer.getAccountID(sfAccount);\n  505  \n  506          // The account owner may not usually multisign for themselves.\n  507          // If they can, txnAccountID will be unseated, which is not equal to any\n  508          // value.\n  509          if (txnAccountID == accountID)\n  510              return Unexpected(\"Invalid multisigner.\");\n  511  \n  512          // No duplicate signers allowed.\n  513          if (lastAccountID == accountID)\n  514              return Unexpected(\"Duplicate Signers not allowed.\");\n  515  \n  516          // Accounts must be in order by account ID.  No duplicates allowed.\n  517          if (lastAccountID > accountID)\n  518              return Unexpected(\"Unsorted Signers array.\");\n  519  \n  520          // The next signature must be greater than this one.\n  521          lastAccountID = accountID;\n  522  \n  523          // Verify the signature.\n  524          bool validSig = false;\n  525          std::optional<std::string> errorWhat;\n  526          try\n  527          {\n  528              auto spk = signer.getFieldVL(sfSigningPubKey);\n  529              if (publicKeyType(makeSlice(spk)))\n  530              {\n  531                  Blob const signature = signer.getFieldVL(sfTxnSignature);\n  532                  validSig = verify(\n  533                      PublicKey(makeSlice(spk)),\n  534                      makeMsg(accountID).slice(),\n  535                      makeSlice(signature),\n  536                      fullyCanonical);\n  537              }\n  538          }\n  539          catch (std::exception const& e)\n  540          {\n  541              // We assume any problem lies with the signature.\n  542              validSig = false;\n  543              errorWhat = e.what();\n  544          }\n  545          if (!validSig)\n  546              return Unexpected(\n  547                  std::string(\"Invalid signature on account \") +\n  548                  toBase58(accountID) + errorWhat.value_or(\"\") + \".\");\n  549      }\n  550      // All signatures verified.\n  551      return {};\n  552  }\n  553  \n  554  Expected<void, std::string>\n  555  STTx::checkBatchMultiSign(\n  556      STObject const& batchSigner,\n  557      RequireFullyCanonicalSig requireCanonicalSig,\n  558      Rules const& rules) const\n  559  {\n  560      bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) ||\n  561          (requireCanonicalSig == RequireFullyCanonicalSig::yes);\n  562  \n  563      // We can ease the computational load inside the loop a bit by\n  564      // pre-constructing part of the data that we hash.  Fill a Serializer\n  565      // with the stuff that stays constant from signature to signature.\n  566      Serializer dataStart;\n  567      serializeBatch(dataStart, getFlags(), getBatchTransactionIDs());\n  568      return multiSignHelper(\n  569          batchSigner,\n  570          std::nullopt,\n  571          fullyCanonical,\n  572          [&dataStart](AccountID const& accountID) -> Serializer {\n  573              Serializer s = dataStart;\n  574              finishMultiSigningData(accountID, s);\n  575              return s;\n  576          },\n  577          rules);\n  578  }\n"
  },
  "code_receipts": [
    {
      "excerpt_file": "source/rippled-2.5.0-STTx-batch-signature-window.txt",
      "excerpt_sha256": "06bb570aa3e25eb9da54a2792282f7edde0507d5e500b6c100f3e641e2542c22",
      "git_head": "1e01cd34f7a216092ed779f291b43324c167167a",
      "line_window": "269-541",
      "path": "src/libxrpl/protocol/STTx.cpp",
      "repo": "/home/postfiat/repos/rippled-2.5.0"
    },
    {
      "excerpt_file": "source/rippled-3.1.3-STTx-batch-signature-window.txt",
      "excerpt_sha256": "e7d52292f28a85b092b4721d13b72df4fdc19a2584b3f0a980616ae67f22eccc",
      "git_head": "46b241ace8b30d9c9775d60ffba7d24b21903896",
      "line_window": "300-578",
      "path": "src/libxrpl/protocol/STTx.cpp",
      "repo": "/home/postfiat/repos/rippled-3.1.3-old"
    }
  ],
  "constructed_at": "2026-06-01T23:54:09+00:00",
  "disclosure_cutoff": "2026-02-18T23:59:59Z",
  "facts": [
    {
      "claim": "Batch packages multiple inner transactions into one atomic outer transaction.",
      "fact_id": "F1",
      "source": "XLS-56 / XRPL Batch documentation; user-supplied research packet"
    },
    {
      "claim": "Inner Batch transactions are intentionally unsigned; authorization is delegated through outer Batch signer data.",
      "fact_id": "F2",
      "source": "Batch design summary and local rippled signer-validation code surface"
    },
    {
      "claim": "Batch signer validation is implemented through STTx::checkBatchSign, checkBatchSingleSign, checkBatchMultiSign, and multiSignHelper.",
      "fact_id": "F3",
      "source": "local rippled source excerpts"
    },
    {
      "claim": "Batch was in the validator-voting/amendment process, so a HOLD/NO route could still prevent activation without mainnet fund loss.",
      "fact_id": "F4",
      "source": "XRPL amendment process"
    }
  ],
  "historical_truth_withheld_from_model": {
    "later_bug_class": "critical signer-validation loop/early-success flaw in Batch authorization that could skip remaining signer checks",
    "later_disclosure_date": "2026-02-19",
    "later_outcome": "validators advised to vote No; rippled 3.1.1 disabled Batch and fixBatchInnerSigs"
  },
  "packet_id": "xrpl-batch-predisclosure-2026",
  "purpose": "Pre-disclosure simulation packet: contains amendment/process/code-surface facts but excludes the February 19, 2026 vulnerability disclosure, exploit description, and emergency response."
}
