From 17ee7e784c25c18763d1d06dd39f855c719c1ee0 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Wed, 13 May 2026 10:00:54 -0400 Subject: [PATCH] Add Submit tests to verify transactions with V1/V2 STIssue serialization. --- src/test/rpc/Submit_test.cpp | 141 +++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/src/test/rpc/Submit_test.cpp b/src/test/rpc/Submit_test.cpp index fe86a6c550..a9f52eca36 100644 --- a/src/test/rpc/Submit_test.cpp +++ b/src/test/rpc/Submit_test.cpp @@ -2,15 +2,30 @@ #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::test { class Submit_test : public beast::unit_test::Suite @@ -86,10 +101,136 @@ public: } } + void + testSTIssueV1SignedVaultSubmit() + { + testcase("V1 STIssue signed Vault tx succeeds submit signature verification"); + using namespace jtx; + + Env env{*this}; + Account const issuer{"issuer"}; + Account const owner{"owner"}; + env.fund(XRP(100'000), issuer, owner); + env.close(); + BEAST_EXPECT(env.current()->rules().enabled(fixCleanup3_2_0)); + + MPTTester mpt{env, issuer, kMPT_INIT_NO_FUND}; + mpt.create({.flags = tfMPTCanTransfer | tfMPTRequireAuth}); + mpt.authorize({.account = owner}); + mpt.authorize({.account = issuer, .holder = owner}); + + PrettyAsset const asset = mpt.issuanceID(); + env(pay(issuer, owner, asset(100))); + env.close(); + + Vault const vault{env}; + auto [jv, keylet] = vault.create({.owner = owner, .asset = asset}); + (void)keylet; + jv[jss::Fee] = to_string(env.current()->fees().base); + jv[jss::Sequence] = env.seq(owner); + jv[jss::SigningPubKey] = strHex(owner.pk().slice()); + + STTx tx{parse(jv)}; + tx.sign(owner.pk(), owner.sk()); + BEAST_EXPECT(tx.checkSign(env.current()->rules())); + + auto const jrr = env.rpc("submit", strHex(tx.getSerializer().slice()))[jss::result]; + BEAST_EXPECT(jrr[jss::engine_result] == "tesSUCCESS"); + } + + void + testSTIssueV2SignedVaultSubmit() + { + testcase("V2 STIssue signed Vault tx fails submit signature verification"); + using namespace jtx; + + Env env{*this}; + Account const issuer{"issuer"}; + Account const owner{"owner"}; + env.fund(XRP(100'000), issuer, owner); + env.close(); + BEAST_EXPECT(env.current()->rules().enabled(fixCleanup3_2_0)); + + MPTTester mpt{env, issuer, kMPT_INIT_NO_FUND}; + mpt.create({.flags = tfMPTCanTransfer | tfMPTRequireAuth}); + mpt.authorize({.account = owner}); + mpt.authorize({.account = issuer, .holder = owner}); + + PrettyAsset const asset = mpt.issuanceID(); + env(pay(issuer, owner, asset(100))); + env.close(); + + Vault const vault{env}; + auto [jv, keylet] = vault.create({.owner = owner, .asset = asset}); + (void)keylet; + jv[jss::Fee] = to_string(env.current()->fees().base); + jv[jss::Sequence] = env.seq(owner); + jv[jss::SigningPubKey] = strHex(owner.pk().slice()); + + // Model an external client that already writes the V2 STIssue wire + // format. The test must not rely on CurrentTransactionRulesGuard to + // produce the bytes that are signed. + MPTIssue const mptIssue = asset.raw().get(); + auto const serializeV1Asset = [&mptIssue]() { + Serializer s; + STIssue const st{sfAsset, Asset{mptIssue}}; + st.addFieldID(s); + st.add(s); + return s.getData(); + }; + auto const serializeV2Asset = [&mptIssue]() { + Serializer s; + STIssue const st{sfAsset, Asset{mptIssue}}; + st.addFieldID(s); + s.addBitString(mptIssue.getIssuer()); + s.addBitString(xrpAccount()); + s.addRaw(mptIssue.getMptID().data(), sizeof(std::uint32_t)); + return s.getData(); + }; + auto const replaceAsset = [this](Blob data, Blob const& from, Blob const& to) { + BEAST_EXPECT(from.size() == to.size()); + auto found = std::ranges::search(data, from); + BEAST_EXPECT(!found.empty()); + if (!found.empty()) + std::copy(to.begin(), to.end(), found.begin()); + return data; + }; + + Blob const v1Asset = serializeV1Asset(); + Blob const v2Asset = serializeV2Asset(); + BEAST_EXPECT(v1Asset != v2Asset); + + STTx tx{parse(jv)}; + Serializer signingData; + signingData.add32(HashPrefix::TxSign); + tx.addWithoutSigningFields(signingData); + Blob const clientSigningData = replaceAsset(signingData.getData(), v1Asset, v2Asset); + auto const sig = sign(owner.pk(), owner.sk(), makeSlice(clientSigningData)); + Slice const sigSlice{sig.data(), sig.size()}; + BEAST_EXPECT(verify(owner.pk(), makeSlice(clientSigningData), sigSlice)); + tx.setFieldVL(sfTxnSignature, sigSlice); + + Serializer txSerializer; + tx.add(txSerializer); + Blob const clientTx = replaceAsset(txSerializer.getData(), v1Asset, v2Asset); + + SerialIter sit{makeSlice(clientTx)}; + STTx const submittedTx{sit}; + BEAST_EXPECT(submittedTx[sfAsset] == asset.raw()); + BEAST_EXPECT(!submittedTx.checkSign(env.current()->rules())); + + auto const jrr = env.rpc("submit", strHex(clientTx))[jss::result]; + BEAST_EXPECT(jrr[jss::error] == "invalidTransaction"); + BEAST_EXPECT( + jrr[jss::error_exception].asString().find("Invalid signature") != std::string::npos); + } + void run() override { testFailHardValidation(); + testSTIssueV1SignedVaultSubmit(); + testSTIssueV2SignedVaultSubmit(); } };