fix: Merge upstream branch

This commit is contained in:
TimothyBanks
2026-08-11 14:51:40 -04:00
245 changed files with 18996 additions and 13733 deletions

View File

@@ -43,6 +43,7 @@ set(test_modules
shamap
tx
protocol_autogen
server
)
if(NOT WIN32)
list(APPEND test_modules net)

View File

@@ -32,7 +32,7 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip)
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
auto const domainIDValue = canonical_UINT256();
auto const mutableFlagsValue = canonical_UINT32();
auto const immutableFlagsValue = canonical_UINT32();
auto const referenceHoldingValue = canonical_UINT256();
auto const issuerEncryptionKeyValue = canonical_VL();
auto const auditorEncryptionKeyValue = canonical_VL();
@@ -53,7 +53,7 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip)
builder.setLockedAmount(lockedAmountValue);
builder.setMPTokenMetadata(mPTokenMetadataValue);
builder.setDomainID(domainIDValue);
builder.setMutableFlags(mutableFlagsValue);
builder.setImmutableFlags(immutableFlagsValue);
builder.setReferenceHolding(referenceHoldingValue);
builder.setIssuerEncryptionKey(issuerEncryptionKeyValue);
builder.setAuditorEncryptionKey(auditorEncryptionKeyValue);
@@ -153,11 +153,11 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip)
}
{
auto const& expected = mutableFlagsValue;
auto const actualOpt = entry.getMutableFlags();
auto const& expected = immutableFlagsValue;
auto const actualOpt = entry.getImmutableFlags();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfMutableFlags");
EXPECT_TRUE(entry.hasMutableFlags());
expectEqualField(expected, *actualOpt, "sfImmutableFlags");
EXPECT_TRUE(entry.hasImmutableFlags());
}
{
@@ -217,7 +217,7 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip)
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
auto const domainIDValue = canonical_UINT256();
auto const mutableFlagsValue = canonical_UINT32();
auto const immutableFlagsValue = canonical_UINT32();
auto const referenceHoldingValue = canonical_UINT256();
auto const issuerEncryptionKeyValue = canonical_VL();
auto const auditorEncryptionKeyValue = canonical_VL();
@@ -237,7 +237,7 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip)
sle->at(sfPreviousTxnID) = previousTxnIDValue;
sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue;
sle->at(sfDomainID) = domainIDValue;
sle->at(sfMutableFlags) = mutableFlagsValue;
sle->at(sfImmutableFlags) = immutableFlagsValue;
sle->at(sfReferenceHolding) = referenceHoldingValue;
sle->at(sfIssuerEncryptionKey) = issuerEncryptionKeyValue;
sle->at(sfAuditorEncryptionKey) = auditorEncryptionKeyValue;
@@ -391,16 +391,16 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip)
}
{
auto const& expected = mutableFlagsValue;
auto const& expected = immutableFlagsValue;
auto const fromSleOpt = entryFromSle.getMutableFlags();
auto const fromBuilderOpt = entryFromBuilder.getMutableFlags();
auto const fromSleOpt = entryFromSle.getImmutableFlags();
auto const fromBuilderOpt = entryFromBuilder.getImmutableFlags();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfMutableFlags");
expectEqualField(expected, *fromBuilderOpt, "sfMutableFlags");
expectEqualField(expected, *fromSleOpt, "sfImmutableFlags");
expectEqualField(expected, *fromBuilderOpt, "sfImmutableFlags");
}
{
@@ -531,8 +531,8 @@ TEST(MPTokenIssuanceTests, OptionalFieldsReturnNullopt)
EXPECT_FALSE(entry.getMPTokenMetadata().has_value());
EXPECT_FALSE(entry.hasDomainID());
EXPECT_FALSE(entry.getDomainID().has_value());
EXPECT_FALSE(entry.hasMutableFlags());
EXPECT_FALSE(entry.getMutableFlags().has_value());
EXPECT_FALSE(entry.hasImmutableFlags());
EXPECT_FALSE(entry.getImmutableFlags().has_value());
EXPECT_FALSE(entry.hasReferenceHolding());
EXPECT_FALSE(entry.getReferenceHolding().has_value());
EXPECT_FALSE(entry.hasIssuerEncryptionKey());

View File

@@ -34,7 +34,7 @@ TEST(TransactionsMPTokenIssuanceCreateTests, BuilderSettersRoundTrip)
auto const maximumAmountValue = canonical_UINT64();
auto const mPTokenMetadataValue = canonical_VL();
auto const domainIDValue = canonical_UINT256();
auto const mutableFlagsValue = canonical_UINT32();
auto const immutableFlagsValue = canonical_UINT32();
MPTokenIssuanceCreateBuilder builder{
accountValue,
@@ -48,7 +48,7 @@ TEST(TransactionsMPTokenIssuanceCreateTests, BuilderSettersRoundTrip)
builder.setMaximumAmount(maximumAmountValue);
builder.setMPTokenMetadata(mPTokenMetadataValue);
builder.setDomainID(domainIDValue);
builder.setMutableFlags(mutableFlagsValue);
builder.setImmutableFlags(immutableFlagsValue);
auto tx = builder.build(publicKey, secretKey);
@@ -107,11 +107,11 @@ TEST(TransactionsMPTokenIssuanceCreateTests, BuilderSettersRoundTrip)
}
{
auto const& expected = mutableFlagsValue;
auto const actualOpt = tx.getMutableFlags();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMutableFlags should be present";
expectEqualField(expected, *actualOpt, "sfMutableFlags");
EXPECT_TRUE(tx.hasMutableFlags());
auto const& expected = immutableFlagsValue;
auto const actualOpt = tx.getImmutableFlags();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfImmutableFlags should be present";
expectEqualField(expected, *actualOpt, "sfImmutableFlags");
EXPECT_TRUE(tx.hasImmutableFlags());
}
}
@@ -135,7 +135,7 @@ TEST(TransactionsMPTokenIssuanceCreateTests, BuilderFromStTxRoundTrip)
auto const maximumAmountValue = canonical_UINT64();
auto const mPTokenMetadataValue = canonical_VL();
auto const domainIDValue = canonical_UINT256();
auto const mutableFlagsValue = canonical_UINT32();
auto const immutableFlagsValue = canonical_UINT32();
// Build an initial transaction
MPTokenIssuanceCreateBuilder initialBuilder{
@@ -149,7 +149,7 @@ TEST(TransactionsMPTokenIssuanceCreateTests, BuilderFromStTxRoundTrip)
initialBuilder.setMaximumAmount(maximumAmountValue);
initialBuilder.setMPTokenMetadata(mPTokenMetadataValue);
initialBuilder.setDomainID(domainIDValue);
initialBuilder.setMutableFlags(mutableFlagsValue);
initialBuilder.setImmutableFlags(immutableFlagsValue);
auto initialTx = initialBuilder.build(publicKey, secretKey);
@@ -204,10 +204,10 @@ TEST(TransactionsMPTokenIssuanceCreateTests, BuilderFromStTxRoundTrip)
}
{
auto const& expected = mutableFlagsValue;
auto const actualOpt = rebuiltTx.getMutableFlags();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMutableFlags should be present";
expectEqualField(expected, *actualOpt, "sfMutableFlags");
auto const& expected = immutableFlagsValue;
auto const actualOpt = rebuiltTx.getImmutableFlags();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfImmutableFlags should be present";
expectEqualField(expected, *actualOpt, "sfImmutableFlags");
}
}
@@ -275,8 +275,8 @@ TEST(TransactionsMPTokenIssuanceCreateTests, OptionalFieldsReturnNullopt)
EXPECT_FALSE(tx.getMPTokenMetadata().has_value());
EXPECT_FALSE(tx.hasDomainID());
EXPECT_FALSE(tx.getDomainID().has_value());
EXPECT_FALSE(tx.hasMutableFlags());
EXPECT_FALSE(tx.getMutableFlags().has_value());
EXPECT_FALSE(tx.hasImmutableFlags());
EXPECT_FALSE(tx.getImmutableFlags().has_value());
}
}

View File

@@ -34,7 +34,7 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderSettersRoundTrip)
auto const domainIDValue = canonical_UINT256();
auto const mPTokenMetadataValue = canonical_VL();
auto const transferFeeValue = canonical_UINT16();
auto const mutableFlagsValue = canonical_UINT32();
auto const immutableFlagsValue = canonical_UINT32();
auto const issuerEncryptionKeyValue = canonical_VL();
auto const auditorEncryptionKeyValue = canonical_VL();
@@ -50,7 +50,7 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderSettersRoundTrip)
builder.setDomainID(domainIDValue);
builder.setMPTokenMetadata(mPTokenMetadataValue);
builder.setTransferFee(transferFeeValue);
builder.setMutableFlags(mutableFlagsValue);
builder.setImmutableFlags(immutableFlagsValue);
builder.setIssuerEncryptionKey(issuerEncryptionKeyValue);
builder.setAuditorEncryptionKey(auditorEncryptionKeyValue);
@@ -109,11 +109,11 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderSettersRoundTrip)
}
{
auto const& expected = mutableFlagsValue;
auto const actualOpt = tx.getMutableFlags();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMutableFlags should be present";
expectEqualField(expected, *actualOpt, "sfMutableFlags");
EXPECT_TRUE(tx.hasMutableFlags());
auto const& expected = immutableFlagsValue;
auto const actualOpt = tx.getImmutableFlags();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfImmutableFlags should be present";
expectEqualField(expected, *actualOpt, "sfImmutableFlags");
EXPECT_TRUE(tx.hasImmutableFlags());
}
{
@@ -153,7 +153,7 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderFromStTxRoundTrip)
auto const domainIDValue = canonical_UINT256();
auto const mPTokenMetadataValue = canonical_VL();
auto const transferFeeValue = canonical_UINT16();
auto const mutableFlagsValue = canonical_UINT32();
auto const immutableFlagsValue = canonical_UINT32();
auto const issuerEncryptionKeyValue = canonical_VL();
auto const auditorEncryptionKeyValue = canonical_VL();
@@ -169,7 +169,7 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderFromStTxRoundTrip)
initialBuilder.setDomainID(domainIDValue);
initialBuilder.setMPTokenMetadata(mPTokenMetadataValue);
initialBuilder.setTransferFee(transferFeeValue);
initialBuilder.setMutableFlags(mutableFlagsValue);
initialBuilder.setImmutableFlags(immutableFlagsValue);
initialBuilder.setIssuerEncryptionKey(issuerEncryptionKeyValue);
initialBuilder.setAuditorEncryptionKey(auditorEncryptionKeyValue);
@@ -225,10 +225,10 @@ TEST(TransactionsMPTokenIssuanceSetTests, BuilderFromStTxRoundTrip)
}
{
auto const& expected = mutableFlagsValue;
auto const actualOpt = rebuiltTx.getMutableFlags();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMutableFlags should be present";
expectEqualField(expected, *actualOpt, "sfMutableFlags");
auto const& expected = immutableFlagsValue;
auto const actualOpt = rebuiltTx.getImmutableFlags();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfImmutableFlags should be present";
expectEqualField(expected, *actualOpt, "sfImmutableFlags");
}
{
@@ -310,8 +310,8 @@ TEST(TransactionsMPTokenIssuanceSetTests, OptionalFieldsReturnNullopt)
EXPECT_FALSE(tx.getMPTokenMetadata().has_value());
EXPECT_FALSE(tx.hasTransferFee());
EXPECT_FALSE(tx.getTransferFee().has_value());
EXPECT_FALSE(tx.hasMutableFlags());
EXPECT_FALSE(tx.getMutableFlags().has_value());
EXPECT_FALSE(tx.hasImmutableFlags());
EXPECT_FALSE(tx.getImmutableFlags().has_value());
EXPECT_FALSE(tx.hasIssuerEncryptionKey());
EXPECT_FALSE(tx.getIssuerEncryptionKey().has_value());
EXPECT_FALSE(tx.hasAuditorEncryptionKey());

View File

@@ -31,9 +31,9 @@ TEST(TransactionsSponsorshipSetTests, BuilderSettersRoundTrip)
// Transaction-specific field values
auto const counterpartySponsorValue = canonical_ACCOUNT();
auto const sponseeValue = canonical_ACCOUNT();
auto const feeAmountValue = canonical_AMOUNT();
auto const feeAmountDeltaValue = canonical_AMOUNT();
auto const maxFeeValue = canonical_AMOUNT();
auto const remainingOwnerCountValue = canonical_UINT32();
auto const remainingOwnerCountDeltaValue = canonical_INT32();
SponsorshipSetBuilder builder{
accountValue,
@@ -44,9 +44,9 @@ TEST(TransactionsSponsorshipSetTests, BuilderSettersRoundTrip)
// Set optional fields
builder.setCounterpartySponsor(counterpartySponsorValue);
builder.setSponsee(sponseeValue);
builder.setFeeAmount(feeAmountValue);
builder.setFeeAmountDelta(feeAmountDeltaValue);
builder.setMaxFee(maxFeeValue);
builder.setRemainingOwnerCount(remainingOwnerCountValue);
builder.setRemainingOwnerCountDelta(remainingOwnerCountDeltaValue);
auto tx = builder.build(publicKey, secretKey);
@@ -81,11 +81,11 @@ TEST(TransactionsSponsorshipSetTests, BuilderSettersRoundTrip)
}
{
auto const& expected = feeAmountValue;
auto const actualOpt = tx.getFeeAmount();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFeeAmount should be present";
expectEqualField(expected, *actualOpt, "sfFeeAmount");
EXPECT_TRUE(tx.hasFeeAmount());
auto const& expected = feeAmountDeltaValue;
auto const actualOpt = tx.getFeeAmountDelta();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFeeAmountDelta should be present";
expectEqualField(expected, *actualOpt, "sfFeeAmountDelta");
EXPECT_TRUE(tx.hasFeeAmountDelta());
}
{
@@ -97,11 +97,11 @@ TEST(TransactionsSponsorshipSetTests, BuilderSettersRoundTrip)
}
{
auto const& expected = remainingOwnerCountValue;
auto const actualOpt = tx.getRemainingOwnerCount();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfRemainingOwnerCount should be present";
expectEqualField(expected, *actualOpt, "sfRemainingOwnerCount");
EXPECT_TRUE(tx.hasRemainingOwnerCount());
auto const& expected = remainingOwnerCountDeltaValue;
auto const actualOpt = tx.getRemainingOwnerCountDelta();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfRemainingOwnerCountDelta should be present";
expectEqualField(expected, *actualOpt, "sfRemainingOwnerCountDelta");
EXPECT_TRUE(tx.hasRemainingOwnerCountDelta());
}
}
@@ -122,9 +122,9 @@ TEST(TransactionsSponsorshipSetTests, BuilderFromStTxRoundTrip)
// Transaction-specific field values
auto const counterpartySponsorValue = canonical_ACCOUNT();
auto const sponseeValue = canonical_ACCOUNT();
auto const feeAmountValue = canonical_AMOUNT();
auto const feeAmountDeltaValue = canonical_AMOUNT();
auto const maxFeeValue = canonical_AMOUNT();
auto const remainingOwnerCountValue = canonical_UINT32();
auto const remainingOwnerCountDeltaValue = canonical_INT32();
// Build an initial transaction
SponsorshipSetBuilder initialBuilder{
@@ -135,9 +135,9 @@ TEST(TransactionsSponsorshipSetTests, BuilderFromStTxRoundTrip)
initialBuilder.setCounterpartySponsor(counterpartySponsorValue);
initialBuilder.setSponsee(sponseeValue);
initialBuilder.setFeeAmount(feeAmountValue);
initialBuilder.setFeeAmountDelta(feeAmountDeltaValue);
initialBuilder.setMaxFee(maxFeeValue);
initialBuilder.setRemainingOwnerCount(remainingOwnerCountValue);
initialBuilder.setRemainingOwnerCountDelta(remainingOwnerCountDeltaValue);
auto initialTx = initialBuilder.build(publicKey, secretKey);
@@ -171,10 +171,10 @@ TEST(TransactionsSponsorshipSetTests, BuilderFromStTxRoundTrip)
}
{
auto const& expected = feeAmountValue;
auto const actualOpt = rebuiltTx.getFeeAmount();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFeeAmount should be present";
expectEqualField(expected, *actualOpt, "sfFeeAmount");
auto const& expected = feeAmountDeltaValue;
auto const actualOpt = rebuiltTx.getFeeAmountDelta();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfFeeAmountDelta should be present";
expectEqualField(expected, *actualOpt, "sfFeeAmountDelta");
}
{
@@ -185,10 +185,10 @@ TEST(TransactionsSponsorshipSetTests, BuilderFromStTxRoundTrip)
}
{
auto const& expected = remainingOwnerCountValue;
auto const actualOpt = rebuiltTx.getRemainingOwnerCount();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfRemainingOwnerCount should be present";
expectEqualField(expected, *actualOpt, "sfRemainingOwnerCount");
auto const& expected = remainingOwnerCountDeltaValue;
auto const actualOpt = rebuiltTx.getRemainingOwnerCountDelta();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfRemainingOwnerCountDelta should be present";
expectEqualField(expected, *actualOpt, "sfRemainingOwnerCountDelta");
}
}
@@ -250,12 +250,12 @@ TEST(TransactionsSponsorshipSetTests, OptionalFieldsReturnNullopt)
EXPECT_FALSE(tx.getCounterpartySponsor().has_value());
EXPECT_FALSE(tx.hasSponsee());
EXPECT_FALSE(tx.getSponsee().has_value());
EXPECT_FALSE(tx.hasFeeAmount());
EXPECT_FALSE(tx.getFeeAmount().has_value());
EXPECT_FALSE(tx.hasFeeAmountDelta());
EXPECT_FALSE(tx.getFeeAmountDelta().has_value());
EXPECT_FALSE(tx.hasMaxFee());
EXPECT_FALSE(tx.getMaxFee().has_value());
EXPECT_FALSE(tx.hasRemainingOwnerCount());
EXPECT_FALSE(tx.getRemainingOwnerCount().has_value());
EXPECT_FALSE(tx.hasRemainingOwnerCountDelta());
EXPECT_FALSE(tx.getRemainingOwnerCountDelta().has_value());
}
}

View File

@@ -0,0 +1,60 @@
#include <xrpl/server/InfoSub.h>
#include <gtest/gtest.h>
#include <cstddef>
#include <limits>
using namespace xrpl;
// The per-connection subscription cap is enforced by the pure predicate
// exceedsSubscriptionCap(current, additional). Testing it directly (rather than
// by subscribing the real cap through a WebSocket, which would exceed the frame
// limit and drop the connection before the check runs) lets the boundary be
// asserted exactly.
TEST(InfoSubSubscriptionCap, Boundary)
{
constexpr std::size_t cap = kMaxSubscriptionsPerConnection;
// Empty connection: anything up to the cap is admitted, cap+1 is not.
EXPECT_FALSE(exceedsSubscriptionCap(0, 0));
EXPECT_FALSE(exceedsSubscriptionCap(0, cap));
EXPECT_TRUE(exceedsSubscriptionCap(0, cap + 1));
// Exactly at the cap: zero more is fine, one more is rejected.
EXPECT_FALSE(exceedsSubscriptionCap(cap, 0));
EXPECT_TRUE(exceedsSubscriptionCap(cap, 1));
// One below the cap: exactly one more reaches the cap; two exceed it.
EXPECT_FALSE(exceedsSubscriptionCap(cap - 1, 1));
EXPECT_TRUE(exceedsSubscriptionCap(cap - 1, 2));
}
TEST(InfoSubSubscriptionCap, NoOverflow)
{
constexpr std::size_t cap = kMaxSubscriptionsPerConnection;
constexpr std::size_t max = std::numeric_limits<std::size_t>::max();
// current + additional must not wrap: a huge additional is rejected even
// when current is 0 (the additional > cap term guards the subtraction).
EXPECT_TRUE(exceedsSubscriptionCap(0, max));
EXPECT_TRUE(exceedsSubscriptionCap(cap, max));
}
TEST(InfoSubSubscriptionCap, ExplicitCap)
{
// A configured override is honored: the boundary tracks the passed cap, not
// the built-in default. This is the seam doSubscribe uses to enforce a
// per-connection cap set via [max_subscriptions_per_connection].
constexpr std::size_t cap = 5;
EXPECT_FALSE(exceedsSubscriptionCap(0, cap, cap));
EXPECT_TRUE(exceedsSubscriptionCap(0, cap + 1, cap));
EXPECT_FALSE(exceedsSubscriptionCap(cap, 0, cap));
EXPECT_TRUE(exceedsSubscriptionCap(cap, 1, cap));
EXPECT_FALSE(exceedsSubscriptionCap(cap - 1, 1, cap));
EXPECT_TRUE(exceedsSubscriptionCap(cap - 1, 2, cap));
// The overflow guard still holds with a small explicit cap.
EXPECT_TRUE(exceedsSubscriptionCap(0, std::numeric_limits<std::size_t>::max(), cap));
}

View File

@@ -15,6 +15,7 @@
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol_autogen/ledger_entries/AccountRoot.h>
@@ -610,7 +611,7 @@ TEST(AccountSet, Ticket)
// Get alice's current sequence - the ticket will be created at seq + 1
std::uint32_t const aliceSeqBefore = env.getAccountRoot(alice.id()).getSequence();
std::uint32_t const ticketSeq = aliceSeqBefore + 1;
auto const ticketSeq = SeqProxy::rawTicket(aliceSeqBefore + 1);
// Create a ticket
EXPECT_EQ(env.submit(transactions::TicketCreateBuilder{alice, 1}, alice).ter, tesSUCCESS);
@@ -623,7 +624,9 @@ TEST(AccountSet, Ticket)
// Try using a ticket that alice doesn't have
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setTicketSequence(ticketSeq + 1), alice)
env.submit(
transactions::AccountSetBuilder{alice}.setTicketSequence(ticketSeq.value() + 1),
alice)
.ter,
terPRE_TICKET);
env.close();
@@ -636,7 +639,9 @@ TEST(AccountSet, Ticket)
// Actually use alice's ticket (noop AccountSet)
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setTicketSequence(ticketSeq), alice).ter,
env.submit(
transactions::AccountSetBuilder{alice}.setTicketSequence(ticketSeq.value()), alice)
.ter,
tesSUCCESS);
env.close();
@@ -649,7 +654,9 @@ TEST(AccountSet, Ticket)
// Try re-using a ticket that alice already used
EXPECT_EQ(
env.submit(transactions::AccountSetBuilder{alice}.setTicketSequence(ticketSeq), alice).ter,
env.submit(
transactions::AccountSetBuilder{alice}.setTicketSequence(ticketSeq.value()), alice)
.ter,
tefNO_TICKET);
}

View File

@@ -46,16 +46,12 @@ struct MockHostFunctions : HostFunctions
(Slice const& data),
(const, override));
// Takes the rendered text, not the guest's buffer: rendering is `HostContext`'s, so what
// a test asserts here is the log line a node would write.
MOCK_METHOD(
(std::expected<std::int32_t, HostFunctionError>),
void,
trace,
(std::string_view const& msg, Slice const& data, bool asHex),
(const, override));
MOCK_METHOD(
(std::expected<std::int32_t, HostFunctionError>),
traceNum,
(std::string_view const& msg, std::int64_t number),
(std::string_view const& msg, std::string_view const& data),
(const, override));
};

View File

@@ -234,14 +234,14 @@ TEST_F(WasmVMTest, DirtyHostIsRefusedBeforeContractRuns)
// A soft host error is the contract's to interpret, so its code has to cross the boundary
// unchanged: the engine must not renumber it, clamp it, or turn it into a failure of its own.
//
// Over the whole of `HostFunctionError` rather than a sample, because the C++ and Rust error
// enums are two hand-maintained lists of the same wire numbers and they have already drifted
// once — C++ spells -11 `OutOfTransferLimit` where the Rust ABI spells it `Decoding`. This is
// the test that notices if either side renumbers.
// Over the whole of `HostFunctionError` rather than a sample, because `HostFunctionError` and
// the Rust ABI's `HostError` are two hand-maintained lists of the same wire numbers: -1
// through -20 have to mean the same thing on both sides, and this is the test that notices if
// either side renumbers.
//
// The two exclusions are the codes the Rust engine treats as host-fatal, which stop the run
// instead of reaching the guest: -1 (its `Internal`, which C++ spells `Unimplemented`) and
// -14 `NoMemExported`.
// The two exclusions are the codes the Rust engine converts into a fault, which stops the run
// instead of reaching the guest: -1 `Unimplemented` and -14 `NoMemExported`. Both say the call
// was not served at all.
TEST_F(WasmVMTest, SoftHostErrorCodesCrossUnchanged)
{
static constexpr HostFunctionError kSoftErrors[] = {
@@ -292,7 +292,10 @@ TEST_F(WasmVMTest, FatalHostErrorStopsRun)
return std::unexpected(refused);
});
for (auto const error : {HostFunctionError::Unimplemented, HostFunctionError::NoMemExported})
for (auto const error :
{HostFunctionError::InternalFatal,
HostFunctionError::Unimplemented,
HostFunctionError::NoMemExported})
{
refused = error;

View File

@@ -1,62 +1,219 @@
#include <xrpl/basics/Number.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/tx/wasm/HostContext.h>
#include <xrpl/tx/wasm/HostFunc.h>
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/MockHostFunctions.h>
#include <tx/wasm/WasmFixture.h>
// For `TraceDataType`, which the bridge declares and this header defines.
#include <expected>
#include <cstdint>
#include <format>
#include <string>
#include <string_view>
#include <utility>
namespace xrpl::test {
using testing::Return;
namespace {
// trace — two byte inputs and a flag, no output.
// Bytes as a WAT data segment's contents. Hex-escaped throughout, so a buffer needs no
// thought about which of its bytes the text format would otherwise read.
std::string
watBytes(Bytes const& bytes)
{
std::string escaped;
escaped.reserve(bytes.size() * 4);
for (auto const byte : bytes)
escaped += std::format("\\{:02x}", byte);
return escaped;
}
Bytes
serialized(STAmount const& amount)
{
Serializer s;
amount.add(s);
return s.getData();
}
} // namespace
// trace — a message, a data type, and a buffer holding what that type says. One import for
// what were five, so what a test varies is the type rather than the function.
//
// The buffer arrives as bytes and leaves as text: `HostContext` renders it, and the host is
// handed the finished line. So a test says which renderer the type selected.
struct TraceCall : HostCallTest
{
static constexpr std::int32_t kDataAt = 64;
// What the guest passes. `typeCode` rather than a `TraceDataType` so a test can send a
// code that names no type, which is the guest's to get wrong.
std::int32_t typeCode{static_cast<std::int32_t>(TraceDataType::AsText)};
Bytes data;
void
traces(TraceDataType type, Bytes bytes)
{
typeCode = static_cast<std::int32_t>(type);
data = std::move(bytes);
}
void
traces(TraceDataType type, std::string_view text)
{
traces(type, Bytes{text.begin(), text.end()});
}
[[nodiscard]] std::string
wat() const override
{
return std::string{R"wat(
// {0} data offset, {1} the data itself, {2} the type under test, {3} its length,
// {4} a type the constant modules can name, {5} the data cap.
return std::format(
R"wat(
(module
(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32) (result i32)))
(import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32)))
(memory (export "memory") 1)
(data (i32.const 0) "note")
(data (i32.const 16) "\07\08")
(data (i32.const {0}) "{1}")
(func (export "escrow_finish") (result i32)
(call $trace (i32.const 0) (i32.const 4) (i32.const 16) (i32.const 2) (i32.const 1)))
(call $trace (i32.const 0) (i32.const 4) (i32.const {2}) (i32.const {0}) (i32.const {3}))
(i32.const 1))
(func (export "not_as_hex") (result i32)
(call $trace (i32.const 0) (i32.const 4) (i32.const 16) (i32.const 2) (i32.const 0))))
)wat"};
(func (export "unnamed_type") (result i32)
(call $trace (i32.const 0) (i32.const 4) (i32.const 0) (i32.const {0}) (i32.const 0))
(i32.const 1))
(func (export "past_memory") (result i32)
(call $trace (i32.const 0) (i32.const 4) (i32.const {4}) (i32.const 65536) (i32.const 1))
(i32.const 1))
(func (export "too_long") (result i32)
(call $trace (i32.const 0) (i32.const 4) (i32.const {4}) (i32.const {0}) (i32.const {5}))
(i32.const 1)))
)wat",
kDataAt,
watBytes(data),
typeCode,
data.size(),
static_cast<std::int32_t>(TraceDataType::AsHex),
kMaxWasmDataLength);
}
// The line the host was handed, for a run that is expected to reach it.
void
expectTraced(std::string_view text)
{
EXPECT_CALL(host, trace(std::string_view("note"), text));
EXPECT_EQ(hostAnswer(), 1) << "the contract runs on past its trace";
}
};
// Two borrowed regions in one call, which is the shape a single-input helper could not
// express — so this pins that both arrive intact, and the flag with them.
TEST_F(TraceCall, MessageDataAndFlagAllArrive)
// The eight-byte types are the pair worth naming: the same bytes, and the type is the whole
// difference between the two readings.
TEST_F(TraceCall, Int64ReadsTheBufferSigned)
{
EXPECT_CALL(host, trace(std::string_view("note"), BytesAre("\x07\x08"), true))
.WillOnce(Return(0));
traces(TraceDataType::Int64, Bytes(8, 0xff));
EXPECT_EQ(hostAnswer(), 0) << "a call with nothing to report answers 0";
expectTraced("-1");
}
TEST_F(TraceCall, HexFlagIsGuestsToChoose)
TEST_F(TraceCall, Uint64ReadsTheSameBufferUnsigned)
{
EXPECT_CALL(host, trace(testing::_, testing::_, false)).WillOnce(Return(0));
traces(TraceDataType::Uint64, Bytes(8, 0xff));
EXPECT_EQ(hostAnswer("not_as_hex"), 0);
expectTraced("18446744073709551615");
}
TEST_F(TraceCall, HostErrorBecomesContractReturnValue)
TEST_F(TraceCall, AsTextTakesTheBufferVerbatim)
{
EXPECT_CALL(host, trace).WillOnce(Return(std::unexpected(HostFunctionError::InvalidParams)));
traces(TraceDataType::AsText, "hello");
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::InvalidParams));
expectTraced("hello");
}
TEST_F(TraceCall, AsHexEncodesTheBuffer)
{
traces(TraceDataType::AsHex, Bytes{0x07, 0x08, 0xff});
expectTraced("0708FF");
}
// The zero account, so the expectation is the well-known base58 rather than a rendering of
// whatever the renderer happened to do.
TEST_F(TraceCall, AccountIsBase58)
{
traces(TraceDataType::Account, Bytes(AccountID::size(), 0));
expectTraced("rrrrrrrrrrrrrrrrrrrrrhoLvTp");
}
TEST_F(TraceCall, AmountCarriesItsAssetIntoTheText)
{
traces(TraceDataType::Amount, serialized(STAmount{XRPAmount{1000}}));
expectTraced("1000/XRP");
}
TEST_F(TraceCall, XfloatIsDecodedToItsValue)
{
auto const encoded = wasm_float::floatFromIntImpl(
42, static_cast<std::int32_t>(Number::RoundingMode::ToNearest));
ASSERT_TRUE(encoded.has_value());
traces(TraceDataType::Xfloat, *encoded);
expectTraced("42");
}
// The width is part of the type, and a buffer that is not it holds no value to print. The
// contract is not told: a trace answers nothing at all.
TEST_F(TraceCall, ABufferOfTheWrongWidthIsDropped)
{
traces(TraceDataType::Int64, Bytes(4, 0xff));
EXPECT_CALL(host, trace).Times(0);
EXPECT_EQ(hostAnswer(), 1);
}
// `STAmount`'s deserializer rejects this by throwing, which must not escape into the run.
TEST_F(TraceCall, AMalformedAmountIsDroppedRatherThanThrown)
{
traces(TraceDataType::Amount, Bytes(3, 0xff));
EXPECT_CALL(host, trace).Times(0);
EXPECT_EQ(hostAnswer(), 1);
}
// Zero is the code a guest sends by omission, which is why no type carries it.
TEST_F(TraceCall, ACodeThatNamesNoTypeIsDropped)
{
EXPECT_CALL(host, trace).Times(0);
EXPECT_EQ(hostAnswer("unnamed_type"), 1);
}
// The memory policy every input region is held to, on the one call that cannot report it.
TEST_F(TraceCall, ARegionPastMemoryIsDropped)
{
EXPECT_CALL(host, trace).Times(0);
EXPECT_EQ(hostAnswer("past_memory"), 1);
}
TEST_F(TraceCall, AMessageAndBufferPastTheDataCapAreDropped)
{
EXPECT_CALL(host, trace).Times(0);
EXPECT_EQ(hostAnswer("too_long"), 1);
}
} // namespace xrpl::test

View File

@@ -1,53 +0,0 @@
#include <xrpl/tx/wasm/WasmCommon.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tx/wasm/WasmFixture.h>
#include <cstdint>
#include <expected>
#include <limits>
#include <string>
#include <string_view>
namespace xrpl::test {
using testing::Return;
// trace_num — a string and an i64, the ABI's only 64-bit parameter.
struct TraceNumCall : HostCallTest
{
[[nodiscard]] std::string
wat() const override
{
return std::string{R"wat(
(module
(import "host_lib" "trace_num" (func $trace_num (param i32 i32 i64) (result i32)))
(memory (export "memory") 1)
(data (i32.const 0) "count")
(func (export "escrow_finish") (result i32)
(call $trace_num (i32.const 0) (i32.const 5) (i64.const -9223372036854775808))))
)wat"};
}
};
// The extreme value on purpose: an `i64` that a truncating or sign-losing conversion anywhere
// on the wire would visibly mangle.
TEST_F(TraceNumCall, I64ArrivesWholeIncludingMostNegativeValue)
{
EXPECT_CALL(host, traceNum(std::string_view("count"), std::numeric_limits<std::int64_t>::min()))
.WillOnce(Return(0));
EXPECT_EQ(hostAnswer(), 0);
}
TEST_F(TraceNumCall, HostErrorBecomesContractReturnValue)
{
EXPECT_CALL(host, traceNum)
.WillOnce(Return(std::unexpected(HostFunctionError::IndexOutOfBounds)));
EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::IndexOutOfBounds));
}
} // namespace xrpl::test