diff --git a/src/test/app/Export_test.cpp b/src/test/app/Export_test.cpp index e11574991..5a2a3d983 100644 --- a/src/test/app/Export_test.cpp +++ b/src/test/app/Export_test.cpp @@ -1248,9 +1248,11 @@ struct Export_test : public beast::unit_test::suite Account const alice{"alice"}; Account const carol{"carol"}; + std::uint32_t const xahauNetworkID = 21337; + std::uint32_t const targetNetworkID = 31337; // ── Xahau env: export the inner tx ───────────────────────────── - Env xahau{*this, xpopCtx.makeEnvConfig(21337), features}; + Env xahau{*this, xpopCtx.makeEnvConfig(xahauNetworkID), features}; xahau.fund(XRP(10000), alice, carol); xahau.close(); @@ -1264,9 +1266,9 @@ struct Export_test : public beast::unit_test::suite // TicketSequence (required for exports — avoids sequence jams // if the tx bounces on XRPL). // - // OperationLimit tells Import::preflight which network this - // tx targets (must match Xahau's NETWORK_ID). - // + // sfNetworkID targets the destination network. Ticket-path imports + // must accept it back because the shadow ticket hash binds this exact + // inner transaction; OperationLimit is only for B2M imports. // We'll create the matching ticket on the XRPL side later. std::uint32_t const ticketSeq = 2; // alice's first ticket on XRPL @@ -1279,6 +1281,7 @@ struct Export_test : public beast::unit_test::suite innerObj.setFieldU32(sfFlags, tfFullyCanonicalSig); innerObj.setFieldU32(sfSequence, 0); innerObj.setFieldU32(sfTicketSequence, ticketSeq); + innerObj.setFieldU32(sfNetworkID, targetNetworkID); innerObj.setFieldU32(sfLastLedgerSequence, 100); innerObj.setFieldAmount(sfAmount, XRPAmount{1000000}); innerObj.setFieldAmount(sfFee, XRPAmount{20}); @@ -1336,7 +1339,7 @@ struct Export_test : public beast::unit_test::suite BEAST_EXPECT(xahau.current()->exists(stKey)); // ── XRPL env: submit the multisigned blob ────────────────────── - Env xrpl{*this}; + Env xrpl{*this, xpopCtx.makeEnvConfig(targetNetworkID)}; xrpl.fund(XRP(10000), alice, carol); xrpl.close(); @@ -1397,6 +1400,7 @@ struct Export_test : public beast::unit_test::suite noop.setFieldU16(sfTransactionType, ttACCOUNT_SET); noop.setFieldU32(sfFlags, tfFullyCanonicalSig); noop.setFieldU32(sfSequence, xrpl.seq(alice)); + noop.setFieldU32(sfNetworkID, targetNetworkID); noop.setFieldAmount(sfFee, XRPAmount{20}); // (1+1)*base noop.setFieldVL(sfSigningPubKey, Blob{}); noop.setAccountID(sfAccount, alice.id()); diff --git a/src/xrpld/app/tx/detail/Import.cpp b/src/xrpld/app/tx/detail/Import.cpp index 31bb7c52d..6d877c3b6 100644 --- a/src/xrpld/app/tx/detail/Import.cpp +++ b/src/xrpld/app/tx/detail/Import.cpp @@ -215,8 +215,9 @@ Import::preflight(PreflightContext const& ctx) << " hasResult=" << meta->isFieldPresent(sfTransactionResult); - if (stpTrans->isFieldPresent(sfTicketSequence) && - !ctx.rules.enabled(featureExport)) + bool const hasTicket = stpTrans->isFieldPresent(sfTicketSequence); + + if (hasTicket && !ctx.rules.enabled(featureExport)) { JLOG(ctx.j.warn()) << "Import: cannot use TicketSequence XPOP."; return temMALFORMED; @@ -269,9 +270,10 @@ Import::preflight(PreflightContext const& ctx) return temMALFORMED; } - // ensure inner txn is for networkid = 0 (network id must therefore be - // missing) - if (stpTrans->isFieldPresent(sfNetworkID)) + // B2M imports use OperationLimit to target this network and therefore + // reject inner NetworkID. Export callbacks may carry a target NetworkID; + // the shadow ticket binds that exact inner transaction hash. + if (!hasTicket && stpTrans->isFieldPresent(sfNetworkID)) { JLOG(ctx.j.warn()) << "Import: attempted to import xpop containing a " "txn with a sfNetworkID field. " @@ -283,7 +285,7 @@ Import::preflight(PreflightContext const& ctx) // tx was destined for this network. For the export callback path // (sfTicketSequence present), the shadow ticket already establishes // the relationship, so OperationLimit is not required. - if (!stpTrans->isFieldPresent(sfTicketSequence)) + if (!hasTicket) { if (!stpTrans->isFieldPresent(sfOperationLimit)) { @@ -311,7 +313,7 @@ Import::preflight(PreflightContext const& ctx) // export callback path and is validator-multisigned (not alice-signed). // The shadow ticket already proves the relationship, so skip the // signing key match check. - if (!stpTrans->isFieldPresent(sfTicketSequence)) + if (!hasTicket) { auto outer = tx.getSigningPubKey(); auto inner = stpTrans->getSigningPubKey();