#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace xrpl { // This file owns HashRouterFlags::PRIVATE1-4 and PRIVATE7-8 in HashRouter.h. // These are the first four; the other two are below. constexpr HashRouterFlags kSfSigbad = HashRouterFlags::PRIVATE1; // Signature is bad constexpr HashRouterFlags kSfSiggood = HashRouterFlags::PRIVATE2; // Signature is good constexpr HashRouterFlags kSfLocalbad = HashRouterFlags::PRIVATE3; // Local checks failed constexpr HashRouterFlags kSfLocalgood = HashRouterFlags::PRIVATE4; // Local checks passed // Before fixCleanup3_4_0, a signature in an alternate role field, such as // sfSponsorSignature, covered the same bytes as the top level signature. Which // bytes a role signature must cover therefore depends on whether the fix is // enabled, but the four flags above record only the verdict, not the rules that // produced it. A verdict reached under one prefix would otherwise be reused // under the other. // // The two flags below hold the verdict for the pre-fix prefixes, so the pre-fix // and post-fix verdicts occupy separate slots and neither is ever read in the // other's era. Nothing is cleared when the amendment activates: setFlags only // sets bits, so a stale pre-fix verdict simply stops being read and ages out // with the rest of the routing table. // // This is not one switchover at a single instant. The era is chosen per call // from the rules passed in, and callers do not agree on the rules: relay and // submit verify against the validated rules, which lag the open ledger rules // that preflight2 verifies against. At the amendment's flag ledger the same // transaction can therefore be checked under both prefixes, on the same node, // at the same time. // // Remove these two flags, and oldPrefixSig below, when Cleanup3_4_0 is retired // in features.macro. constexpr HashRouterFlags kSfSigbadOldPrefix = HashRouterFlags::PRIVATE7; constexpr HashRouterFlags kSfSiggoodOldPrefix = HashRouterFlags::PRIVATE8; //------------------------------------------------------------------------------ std::pair checkValidity(HashRouter& router, STTx const& tx, Rules const& rules) { auto const id = tx.getTransactionID(); auto const flags = router.getFlags(id); // Batch inner transactions are never independently valid: they are applied // within their batch, not through checkValidity. Reaching here means one was // relayed or submitted on its own, so mark it bad regardless of the // amendment (like PeerImp and NetworkOPs). if (tx.isFlag(tfInnerBatchTxn)) { router.setFlags(id, kSfSigbad); return {Validity::SigBad, "Batch inner transactions are never considered validly signed."}; } // Pick the cache slot for this call's era; see kSfSiggoodOldPrefix above. // Only a transaction that carries a role signature, and only while the fix // is disabled, uses the separate slot. Every other transaction, and every // transaction once the fix is enabled, uses the ordinary flags and verifies // exactly once, so there is no steady state cost. // // Both directions matter. A good verdict from before the fix must not let a // signature moved between roles survive the amendment, and a bad verdict // from before the fix must not condemn a transaction that the new prefixes // accept. // // Whether a transaction carries a role signature is fixed for its ID: the // fields are kNotSigning, so they are excluded from the signed bytes, but // they are still covered by the transaction ID. Repeat calls for one ID // therefore always agree on which slot pair to use. bool const oldPrefixSig = !rules.enabled(fixCleanup3_4_0) && (tx.isFieldPresent(sfSponsorSignature) || tx.isFieldPresent(sfCounterpartySignature)); auto const sigbadFlag = oldPrefixSig ? kSfSigbadOldPrefix : kSfSigbad; auto const siggoodFlag = oldPrefixSig ? kSfSiggoodOldPrefix : kSfSiggood; if (any(flags & sigbadFlag)) { // Signature is known bad return {Validity::SigBad, "Transaction has bad signature."}; } if (!any(flags & siggoodFlag)) { auto const sigVerify = tx.checkSign(rules); if (!sigVerify) { router.setFlags(id, sigbadFlag); return {Validity::SigBad, sigVerify.error()}; } router.setFlags(id, siggoodFlag); } // Signature is now known good if (any(flags & kSfLocalbad)) { // ...but the local checks // are known bad. return {Validity::SigGoodOnly, "Local checks failed."}; } if (any(flags & kSfLocalgood)) { // ...and the local checks // are known good. return {Validity::Valid, ""}; } // Do the local checks std::string reason; if (!passesLocalChecks(tx, reason)) { router.setFlags(id, kSfLocalbad); return {Validity::SigGoodOnly, reason}; } router.setFlags(id, kSfLocalgood); return {Validity::Valid, ""}; } void forceValidity(HashRouter& router, uint256 const& txid, Validity validity) { // Callers reach here when they deliberately skip signature verification, // such as a cluster peer that trusts its neighbor's checks, or a // configuration that turns signature checks off. Nothing was verified, so // there is no prefix era to record. Mark both of checkValidity's signature // slots good: otherwise the forced verdict is ignored for a role-signature // transaction until fixCleanup3_4_0 is enabled, and the signature the // caller meant to skip gets verified after all. Marking both cannot leak a // verdict across eras, because no verdict was reached, and this is the only // place the distinction can be recorded: kSfSiggood alone does not say // whether checkValidity verified a post-fix signature or a caller forced // the result. An already cached bad verdict still wins, since checkValidity // tests its bad flag first. Drop kSfSiggoodOldPrefix when Cleanup3_4_0 is // retired. HashRouterFlags flags = HashRouterFlags::UNDEFINED; switch (validity) { case Validity::Valid: flags |= kSfLocalgood; [[fallthrough]]; case Validity::SigGoodOnly: flags |= kSfSiggood | kSfSiggoodOldPrefix; [[fallthrough]]; case Validity::SigBad: // would be silly to call directly break; } if (any(flags)) router.setFlags(txid, flags); } template ApplyResult apply(ServiceRegistry& registry, OpenView& view, PreflightChecks&& preflightChecks) { return doApply(preclaim(preflightChecks(), registry, view), registry, view); } ApplyResult apply(ServiceRegistry& registry, OpenView& view, STTx const& tx, ApplyFlags flags, beast::Journal j) { return apply( registry, view, [&]() mutable { return preflight(registry, view.rules(), tx, flags, j); }); } ApplyResult apply( ServiceRegistry& registry, OpenView& view, uint256 const& parentBatchId, STTx const& tx, ApplyFlags flags, beast::Journal j) { return apply(registry, view, [&]() mutable { return preflight(registry, view.rules(), parentBatchId, tx, flags, j); }); } static bool applyBatchTransactions( ServiceRegistry& registry, OpenView& batchView, STTx const& batchTxn, beast::Journal j) { XRPL_ASSERT( batchTxn.getTxnType() == ttBATCH && !batchTxn.getFieldArray(sfRawTransactions).empty(), "Batch transaction missing sfRawTransactions"); auto const parentBatchId = batchTxn.getTransactionID(); auto const mode = batchTxn.getFlags(); auto applyOneTransaction = [®istry, &j, &parentBatchId, &batchView](STTx const& tx) { OpenView perTxBatchView(kBatchView, batchView); auto const ret = apply(registry, perTxBatchView, parentBatchId, tx, TapBatch, j); XRPL_ASSERT( ret.applied == (isTesSuccess(ret.ter) || isTecClaim(ret.ter)), "Inner transaction should not be applied"); JLOG(j.debug()) << "BatchTrace[" << parentBatchId << "]: " << tx.getTransactionID() << " " << (ret.applied ? "applied" : "failure") << ": " << transToken(ret.ter); // If the transaction should be applied push its changes to the // whole-batch view. // NOTE: each inner tx is individually capped at kOversizeMetaDataCap; // there is no aggregate cap here. Bounded by kMaxBatchTxCount * cap, // which standalone txns can already produce in one ledger. if (ret.applied && (isTesSuccess(ret.ter) || isTecClaim(ret.ter))) perTxBatchView.apply(batchView); return ret; }; int applied = 0; for (auto const& stx : batchTxn.getBatchTransactions()) { auto const result = applyOneTransaction(*stx); XRPL_ASSERT( result.applied == (isTesSuccess(result.ter) || isTecClaim(result.ter)), "Outer Batch failure, inner transaction should not be applied"); if (result.applied) ++applied; if (!isTesSuccess(result.ter)) { if ((mode & tfAllOrNothing) != 0u) return false; if ((mode & tfUntilFailure) != 0u) break; } else if ((mode & tfOnlyOne) != 0u) { break; } } return applied != 0; } ApplyTransactionResult applyTransaction( ServiceRegistry& registry, OpenView& view, STTx const& txn, bool retryAssured, ApplyFlags flags, beast::Journal j) { // Returns false if the transaction has need not be retried. if (retryAssured) flags = flags | TapRetry; JLOG(j.debug()) << "TXN " << txn.getTransactionID() << (retryAssured ? "/retry" : "/final"); try { auto const result = apply(registry, view, txn, flags, j); if (result.applied) { JLOG(j.debug()) << "Transaction applied: " << transToken(result.ter); // The batch transaction was just applied; now we need to apply // its inner transactions as necessary. if (isTesSuccess(result.ter) && txn.getTxnType() == ttBATCH) { OpenView wholeBatchView(kBatchView, view); if (applyBatchTransactions(registry, wholeBatchView, txn, j)) wholeBatchView.apply(view); } return ApplyTransactionResult::Success; } if (isTefFailure(result.ter) || isTemMalformed(result.ter) || isTelLocal(result.ter)) { // failure JLOG(j.debug()) << "Transaction failure: " << transHuman(result.ter); return ApplyTransactionResult::Fail; } JLOG(j.debug()) << "Transaction retry: " << transHuman(result.ter); return ApplyTransactionResult::Retry; } catch (std::exception const& ex) { JLOG(j.warn()) << "Throws: " << ex.what(); return ApplyTransactionResult::Fail; } } } // namespace xrpl