mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-27 07:26:51 +00:00
fix: float_from_stnumber and float_from_stamount deserialization not returning canonicalized floats
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#include <xrpl/tx/wasm/HostContext.h>
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Number.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
@@ -1128,6 +1129,10 @@ HostContext::floatFromSTAmount(
|
||||
{
|
||||
return hfErrorToInt(parsed.error());
|
||||
}
|
||||
if (!isLegalMPT(*parsed) || !isLegalNet(*parsed))
|
||||
{
|
||||
return hfErrorToInt(HostFunctionError::InvalidParams);
|
||||
}
|
||||
return invoke<false>(out, [&] { return hostFunctions_.floatFromSTAmount(*parsed, mode); });
|
||||
});
|
||||
}
|
||||
@@ -1139,6 +1144,14 @@ HostContext::floatFromSTNumber(
|
||||
rust::Slice<std::uint8_t> out) const noexcept
|
||||
{
|
||||
return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
|
||||
// The rounding mode has to be installed *before* the bytes are decoded, not
|
||||
// only around the encode the host does afterwards.
|
||||
auto rounding = std::optional<NumberRoundModeGuard>{};
|
||||
if (auto const rm = Number::checkedRoundingMode(mode))
|
||||
{
|
||||
rounding.emplace(*rm);
|
||||
}
|
||||
|
||||
auto const parsed = parseST<STNumber>(number);
|
||||
if (!parsed)
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/tx/wasm/WasmCommon.h>
|
||||
@@ -99,4 +100,106 @@ TEST_F(FloatFromSTAmountCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
|
||||
EXPECT_FALSE(out.wasWritten());
|
||||
}
|
||||
|
||||
// Bytes that parse are not yet bytes that are safe to use: both branches of
|
||||
// `STAmount(SerialIter&, SField const&)` return before `canonicalize`, where the bounds are.
|
||||
// `STAmount`'s own constructors canonicalize, so these are laid out by hand.
|
||||
struct FloatFromSTAmountBounds : HostContextTest
|
||||
{
|
||||
std::int32_t const mode = 1;
|
||||
|
||||
// The MPT wire form: eight bytes carrying the flags, one more byte of magnitude, then the
|
||||
// 192-bit issuance id. The magnitude is reassembled as `(first << 8) | second`, so the
|
||||
// flag bits shift out and the low 56 bits of the first word hold all but the last byte.
|
||||
static Bytes
|
||||
serializedMpt(std::uint64_t magnitude, bool positive)
|
||||
{
|
||||
auto header = (magnitude >> 8) | STAmount::kMpToken;
|
||||
if (positive)
|
||||
{
|
||||
header |= STAmount::kPositive;
|
||||
}
|
||||
|
||||
auto s = Serializer{};
|
||||
s.add64(header);
|
||||
s.add8(static_cast<unsigned char>(magnitude & 0xffU));
|
||||
s.addBitString(MPTID{42});
|
||||
return s.getData();
|
||||
}
|
||||
|
||||
// The XRP wire form is the eight bytes alone; `kValueMask` clears the two flag bits, so
|
||||
// everything below them is magnitude.
|
||||
static Bytes
|
||||
serializedXrp(std::uint64_t drops)
|
||||
{
|
||||
auto s = Serializer{};
|
||||
s.add64(drops | STAmount::kPositive);
|
||||
return s.getData();
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(FloatFromSTAmountBounds, AnMptMagnitudeAtTheSignBitIsRefused)
|
||||
{
|
||||
EXPECT_CALL(host, floatFromSTAmount).Times(0);
|
||||
|
||||
auto const wire = serializedMpt(std::uint64_t{1} << 63U, /*positive*/ false);
|
||||
auto out = OutRegion{32};
|
||||
EXPECT_EQ(
|
||||
hostContext.floatFromSTAmount(bytesOf(wire), mode, out.slice()),
|
||||
hfErrorToInt(HostFunctionError::InvalidParams));
|
||||
}
|
||||
|
||||
TEST_F(FloatFromSTAmountBounds, AnMptMagnitudePastTheProtocolMaximumIsRefused)
|
||||
{
|
||||
EXPECT_CALL(host, floatFromSTAmount).Times(0);
|
||||
|
||||
auto const wire = serializedMpt(kMaxMpTokenAmount + 1, /*positive*/ true);
|
||||
auto out = OutRegion{32};
|
||||
EXPECT_EQ(
|
||||
hostContext.floatFromSTAmount(bytesOf(wire), mode, out.slice()),
|
||||
hfErrorToInt(HostFunctionError::InvalidParams));
|
||||
}
|
||||
|
||||
// MPT amounts are non-negative by protocol invariant, which is why `isLegalMPT` asks.
|
||||
TEST_F(FloatFromSTAmountBounds, ANegativeMptIsRefused)
|
||||
{
|
||||
EXPECT_CALL(host, floatFromSTAmount).Times(0);
|
||||
|
||||
auto const wire = serializedMpt(1000, /*positive*/ false);
|
||||
auto out = OutRegion{32};
|
||||
EXPECT_EQ(
|
||||
hostContext.floatFromSTAmount(bytesOf(wire), mode, out.slice()),
|
||||
hfErrorToInt(HostFunctionError::InvalidParams));
|
||||
}
|
||||
|
||||
// `isLegalNet`'s half: the XRP branch skips `canonicalize` too, so drops past the network
|
||||
// maximum reach the host unchallenged without this.
|
||||
TEST_F(FloatFromSTAmountBounds, XrpDropsPastTheNetworkMaximumAreRefused)
|
||||
{
|
||||
EXPECT_CALL(host, floatFromSTAmount).Times(0);
|
||||
|
||||
auto const wire = serializedXrp(STAmount::kMaxNativeN + 1);
|
||||
auto out = OutRegion{32};
|
||||
EXPECT_EQ(
|
||||
hostContext.floatFromSTAmount(bytesOf(wire), mode, out.slice()),
|
||||
hfErrorToInt(HostFunctionError::InvalidParams));
|
||||
}
|
||||
|
||||
TEST_F(FloatFromSTAmountBounds, TheMaximaThemselvesStillReachTheHost)
|
||||
{
|
||||
auto const result = Bytes{1, 2, 3};
|
||||
EXPECT_CALL(host, floatFromSTAmount(testing::_, mode))
|
||||
.Times(2)
|
||||
.WillRepeatedly(testing::Return(result));
|
||||
|
||||
for (auto const& wire :
|
||||
{serializedMpt(kMaxMpTokenAmount, /*positive*/ true),
|
||||
serializedXrp(STAmount::kMaxNativeN)})
|
||||
{
|
||||
auto out = OutRegion{32};
|
||||
EXPECT_EQ(
|
||||
hostContext.floatFromSTAmount(bytesOf(wire), mode, out.slice()),
|
||||
static_cast<std::int32_t>(result.size()));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace xrpl::test
|
||||
|
||||
@@ -92,6 +92,71 @@ TEST_F(FloatFromSTNumberCall, MalformedBytesAreRefusedWithoutAskingHost)
|
||||
hfErrorToInt(HostFunctionError::InvalidParams));
|
||||
}
|
||||
|
||||
// Decoding is where the rounding happens, so the guest's mode has to be installed before it.
|
||||
struct FloatFromSTNumberRounding : HostContextTest
|
||||
{
|
||||
// Declared first so the range is in force while the expectations below are built.
|
||||
NumberMantissaScaleGuard const scale{MantissaRange::MantissaScale::Small};
|
||||
|
||||
// Seventeen digits against a sixteen-digit range: normalizing drops the last one and the
|
||||
// mode decides its fate. A dropped `7` rounds up under `ToNearest`, a dropped `3` down, so
|
||||
// each case below disagrees with `ToNearest` and fails if the decode does not honour the
|
||||
// mode it was given.
|
||||
std::int32_t const exponent = 0;
|
||||
Bytes const dropsSeven = serialized(12'345'678'901'234'567, exponent);
|
||||
Bytes const dropsThree = serialized(12'345'678'901'234'563, exponent);
|
||||
|
||||
STNumber const truncated{sfGeneric, Number{1'234'567'890'123'456, 1}};
|
||||
STNumber const raised{sfGeneric, Number{1'234'567'890'123'457, 1}};
|
||||
|
||||
void
|
||||
expectDecodedAs(Bytes const& wire, Number::RoundingMode mode, STNumber const& expected)
|
||||
{
|
||||
auto const asInt = static_cast<std::int32_t>(mode);
|
||||
auto const result = Bytes{1, 2, 3};
|
||||
EXPECT_CALL(host, floatFromSTNumber(testing::Eq(expected), asInt))
|
||||
.WillOnce(testing::Return(result));
|
||||
|
||||
auto out = OutRegion{32};
|
||||
EXPECT_EQ(
|
||||
hostContext.floatFromSTNumber(bytesOf(wire), asInt, out.slice()),
|
||||
static_cast<std::int32_t>(result.size()));
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(FloatFromSTNumberRounding, TowardsZeroTruncatesInsteadOfRoundingToNearest)
|
||||
{
|
||||
expectDecodedAs(dropsSeven, Number::RoundingMode::TowardsZero, truncated);
|
||||
}
|
||||
|
||||
TEST_F(FloatFromSTNumberRounding, UpwardRoundsAwayInsteadOfRoundingToNearest)
|
||||
{
|
||||
expectDecodedAs(dropsThree, Number::RoundingMode::Upward, raised);
|
||||
}
|
||||
|
||||
TEST_F(FloatFromSTNumberRounding, DownwardTruncatesInsteadOfRoundingToNearest)
|
||||
{
|
||||
expectDecodedAs(dropsSeven, Number::RoundingMode::Downward, truncated);
|
||||
}
|
||||
|
||||
TEST_F(FloatFromSTNumberRounding, ToNearestIsUnchanged)
|
||||
{
|
||||
expectDecodedAs(dropsSeven, Number::RoundingMode::ToNearest, raised);
|
||||
expectDecodedAs(dropsThree, Number::RoundingMode::ToNearest, truncated);
|
||||
}
|
||||
|
||||
TEST_F(FloatFromSTNumberRounding, AnInvalidModeStillReachesTheHost)
|
||||
{
|
||||
constexpr auto kNotAMode = std::int32_t{99};
|
||||
EXPECT_CALL(host, floatFromSTNumber(testing::_, kNotAMode))
|
||||
.WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatInputMalformed)));
|
||||
|
||||
auto out = OutRegion{32};
|
||||
EXPECT_EQ(
|
||||
hostContext.floatFromSTNumber(bytesOf(dropsSeven), kNotAMode, out.slice()),
|
||||
hfErrorToInt(HostFunctionError::FloatInputMalformed));
|
||||
}
|
||||
|
||||
// The out-region contract: write only if the whole value fits, and return the true length
|
||||
// either way.
|
||||
TEST_F(FloatFromSTNumberCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
|
||||
|
||||
Reference in New Issue
Block a user