Add Submit tests to verify transactions with V1/V2 STIssue serialization.

This commit is contained in:
Gregory Tsipenyuk
2026-05-13 10:00:54 -04:00
parent a87cff1c1b
commit 17ee7e784c

View File

@@ -2,15 +2,30 @@
#include <test/jtx/Env.h>
#include <test/jtx/JTx.h>
#include <test/jtx/amount.h>
#include <test/jtx/mpt.h>
#include <test/jtx/pay.h>
#include <test/jtx/utility.h>
#include <test/jtx/vault.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/STIssue.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
#include <algorithm>
#include <string>
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<MPTIssue>();
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();
}
};