diff --git a/src/util/XDRCereal.cpp b/src/util/XDRCereal.cpp index 596831f71a..2b7437838b 100644 --- a/src/util/XDRCereal.cpp +++ b/src/util/XDRCereal.cpp @@ -29,42 +29,8 @@ cereal_override(cereal::JSONOutputArchive& ar, stellar::SCAddress const& addr, case stellar::SC_ADDRESS_TYPE_ACCOUNT: xdr::archive(ar, stellar::KeyUtils::toStrKey(addr.accountId()), field); return; - case stellar::SC_ADDRESS_TYPE_MUXED_ACCOUNT: - ar.setNextName(field); - ar.startNode(); - xdr::archive(ar, addr.muxedAccount().id, "id"); - xdr::archive(ar, - stellar::binToHex(stellar::ByteSlice( - addr.muxedAccount().ed25519.data(), - addr.muxedAccount().ed25519.size())), - "ed25519"); - ar.finishNode(); - return; - case stellar::SC_ADDRESS_TYPE_CLAIMABLE_BALANCE: - { - auto const& cbID = addr.claimableBalanceId(); - if (cbID.type() == stellar::CLAIMABLE_BALANCE_ID_TYPE_V0) - { - xdr::archive(ar, - stellar::binToHex(stellar::ByteSlice( - cbID.v0().data(), cbID.v0().size())), - field); - } - return; - } - case stellar::SC_ADDRESS_TYPE_LIQUIDITY_POOL: - xdr::archive( - ar, - stellar::binToHex(stellar::ByteSlice( - addr.liquidityPoolId().data(), addr.liquidityPoolId().size())), - field); - return; default: - // Unknown address type - serialize as "Unknown(type_id)" - xdr::archive(ar, - std::string("Unknown(") + - std::to_string(static_cast(addr.type())) + ")", - field); + ar(cereal::make_nvp(field, addr)); return; } } @@ -111,13 +77,6 @@ cereal_override(cereal::JSONOutputArchive& ar, } } -void -cerealPoolAsset(cereal::JSONOutputArchive& ar, stellar::Asset const& asset, - char const* field) -{ - xdr::archive(ar, std::string("INVALID"), field); -} - void cerealPoolAsset(cereal::JSONOutputArchive& ar, stellar::TrustLineAsset const& asset, char const* field) @@ -140,3 +99,109 @@ cerealPoolAsset(cereal::JSONOutputArchive& ar, xdr::archive(ar, cp.fee, "fee"); ar.finishNode(); } + +#ifdef BUILD_TESTS +void +cereal_override(cereal::JSONInputArchive& ar, stellar::PublicKey& s, + const char* field) +{ + std::string strkey; + xdr::archive(ar, strkey, field); + s = stellar::KeyUtils::fromStrKey(strkey); +} + +void +cereal_override(cereal::JSONInputArchive& ar, stellar::SCAddress& addr, + char const* field) +{ + try + { + std::string strkey; + xdr::archive(ar, strkey, field); + uint8_t version; + std::vector decoded; + if (stellar::strKey::fromStrKey(strkey, version, decoded)) + { + switch (version) + { + case stellar::strKey::STRKEY_CONTRACT: + addr.type(stellar::SC_ADDRESS_TYPE_CONTRACT); + if (addr.contractId().size() != decoded.size()) + { + break; + } + std::copy(decoded.begin(), decoded.end(), + addr.contractId().begin()); + return; + case stellar::strKey::STRKEY_PUBKEY_ED25519: + addr.type(stellar::SC_ADDRESS_TYPE_ACCOUNT); + addr.accountId().type(stellar::PUBLIC_KEY_TYPE_ED25519); + if (addr.accountId().ed25519().size() != decoded.size()) + { + break; + } + std::copy(decoded.begin(), decoded.end(), + addr.accountId().ed25519().begin()); + return; + } + } + } + catch (...) + { + } + ar(cereal::make_nvp(field, addr)); +} + +void +cereal_override(cereal::JSONInputArchive& ar, stellar::ConfigUpgradeSetKey& key, + char const* field) +{ + ar.setNextName(field); + ar.startNode(); + + std::string id; + xdr::archive(ar, id, "contractID"); + uint8_t version; + std::vector decoded; + releaseAssertOrThrow(stellar::strKey::fromStrKey(id, version, decoded)); + releaseAssertOrThrow(version == stellar::strKey::STRKEY_CONTRACT); + releaseAssertOrThrow(decoded.size() == key.contractID.size()); + std::copy(decoded.begin(), decoded.end(), key.contractID.begin()); + + xdr::archive(ar, key.contentHash, "contentHash"); + + ar.finishNode(); +} + +void +cereal_override(cereal::JSONInputArchive& ar, + stellar::MuxedAccount& muxedAccount, char const* field) +{ + try + { + std::string key; + xdr::archive(ar, key, field); + uint8_t version; + std::vector decoded; + releaseAssertOrThrow( + stellar::strKey::fromStrKey(key, version, decoded)); + releaseAssertOrThrow(version == stellar::strKey::STRKEY_PUBKEY_ED25519); + muxedAccount.type(stellar::KEY_TYPE_ED25519); + releaseAssertOrThrow(decoded.size() == muxedAccount.ed25519().size()); + std::copy(decoded.begin(), decoded.end(), + muxedAccount.ed25519().begin()); + } + catch (const cereal::RapidJSONException&) + { + std::string key; + muxedAccount.type(stellar::KEY_TYPE_MUXED_ED25519); + xdr::archive( + ar, + std::make_tuple(cereal::make_nvp("id", muxedAccount.med25519().id), + cereal::make_nvp("accountID", key)), + field); + muxedAccount.med25519().ed25519 = + stellar::KeyUtils::fromStrKey(key).ed25519(); + } +} +#endif // BUILD_TESTS diff --git a/src/util/XDRCereal.h b/src/util/XDRCereal.h index 5115079873..134f75bbdf 100644 --- a/src/util/XDRCereal.h +++ b/src/util/XDRCereal.h @@ -14,6 +14,7 @@ #include "transactions/TransactionUtils.h" #include "util/types.h" #include +#include using namespace std::placeholders; @@ -22,7 +23,29 @@ void cereal_override(cereal::JSONOutputArchive& ar, xdr::xstring const& s, char const* field) { - xdr::archive(ar, static_cast(s), field); + // If we have any '\0's, we serialize as an object with a "raw" key so that + // we can roundtrip it (cereal JSONInputArchive doesn't properly read in + // '\0's + bool hasZero = false; + for (char ch : s) + { + if (ch == '\0') + { + hasZero = true; + break; + } + } + if (hasZero) + { + ar.setNextName(field); + ar.startNode(); + ar(cereal::make_nvp("raw", stellar::binToHex(s))); + ar.finishNode(); + } + else + { + xdr::archive(ar, static_cast(s), field); + } } template @@ -89,14 +112,11 @@ void cereal_override(cereal::JSONOutputArchive& ar, stellar::MuxedAccount const& muxedAccount, char const* field); -void cerealPoolAsset(cereal::JSONOutputArchive& ar, stellar::Asset const& asset, - char const* field); - void cerealPoolAsset(cereal::JSONOutputArchive& ar, - stellar::TrustLineAsset const& asset, char const* field); + stellar::ChangeTrustAsset const& asset, char const* field); void cerealPoolAsset(cereal::JSONOutputArchive& ar, - stellar::ChangeTrustAsset const& asset, char const* field); + stellar::TrustLineAsset const& asset, char const* field); template typename std::enable_if::value || @@ -111,7 +131,14 @@ cereal_override(cereal::JSONOutputArchive& ar, T const& asset, xdr::archive(ar, std::string("NATIVE"), field); break; case stellar::ASSET_TYPE_POOL_SHARE: - cerealPoolAsset(ar, asset, field); + if constexpr (std::is_same_v) + { + xdr::archive(ar, std::string("UNKNOWN"), field); + } + else + { + cerealPoolAsset(ar, asset, field); + } break; case stellar::ASSET_TYPE_CREDIT_ALPHANUM4: case stellar::ASSET_TYPE_CREDIT_ALPHANUM12: @@ -119,19 +146,57 @@ cereal_override(cereal::JSONOutputArchive& ar, T const& asset, ar.setNextName(field); ar.startNode(); - // asset is templated, so we just pull the assetCode string directly so - // we don't have to templatize assetToString + // Cereal JSON archives can't handle roundtripping `\0`s, so if we have + // an invalid assetCode, we serialize it as raw bytes + auto archive = [&ar](auto& assetCode) { + static_assert(std::is_same_v, + stellar::AssetCode12> || + std::is_same_v, + stellar::AssetCode4>); + int first0; + for (first0 = 0; first0 < assetCode.size(); first0++) + { + if (!assetCode[first0]) + { + break; + } + } + bool valid = true; + for (int i = first0; i < assetCode.size(); i++) + { + if (assetCode[i]) + { + valid = false; + break; + } + } + if (std::is_same_v, + stellar::AssetCode12> && + first0 <= 4) + { + valid = false; + } + if (valid) + { + std::string code; + std::copy(assetCode.begin(), assetCode.begin() + first0, + std::back_inserter(code)); + xdr::archive(ar, code, "assetCode"); + } + else + { + xdr::archive(ar, assetCode, "assetCodeRaw"); + } + }; std::string code; if (asset.type() == stellar::ASSET_TYPE_CREDIT_ALPHANUM4) { - stellar::assetCodeToStr(asset.alphaNum4().assetCode, code); + archive(asset.alphaNum4().assetCode); } else { - stellar::assetCodeToStr(asset.alphaNum12().assetCode, code); + archive(asset.alphaNum12().assetCode); } - - xdr::archive(ar, code, "assetCode"); xdr::archive(ar, stellar::getIssuer(asset), "issuer"); ar.finishNode(); break; @@ -158,14 +223,21 @@ cereal_override(cereal::JSONOutputArchive& ar, T const& t, char const* field) xdr::archive(ar, name, field); } +template constexpr bool isXdrPointer = false; +template constexpr bool isXdrPointer> = true; + template -void +std::enable_if_t> cereal_override(cereal::JSONOutputArchive& ar, xdr::pointer const& t, char const* field) { - // We tolerate a little information-loss here collapsing *T into T for - // the non-null case, and use JSON 'null' for the null case. This reads - // much better than the thing JSONOutputArchive does with PtrWrapper. + // We collapse *T into T for the non-null case, and use JSON 'null' for the + // null case. This reads much better than the thing JSONOutputArchive does + // with PtrWrapper and is clearer than the empty/1-element array that the + // container override does. **T doesn't get collapsed so that we can tell + // the difference between the outer and inner optionals being null (note + // that, at the time of writing, none of the stellar xdr types are an + // optional optional) if (t) { xdr::archive(ar, *t, field); @@ -178,6 +250,222 @@ cereal_override(cereal::JSONOutputArchive& ar, xdr::pointer const& t, } } +#ifdef BUILD_TESTS +// We shouldn't use the JSONInputArchive overrides in production code. + +template +void +cereal_override(cereal::JSONInputArchive& ar, xdr::xstring& s, + const char* field) +{ + try + { + xdr::archive(ar, static_cast(s), field); + } + catch (cereal::RapidJSONException const&) + { + ar.setNextName(field); + ar.startNode(); + std::string hex; + ar(cereal::make_nvp("raw", hex)); + auto bin = stellar::hexToBin(hex); + s.resize(bin.size()); + std::copy(bin.begin(), bin.end(), s.begin()); + ar.finishNode(); + } +} + +template +void +cereal_override(cereal::JSONInputArchive& ar, xdr::opaque_array& s, + const char* field) +{ + std::string hex; + xdr::archive(ar, hex, field); + auto bin = stellar::hexToBin(hex); + releaseAssertOrThrow(bin.size() == N); + std::copy(bin.begin(), bin.end(), s.begin()); +} + +template +std::enable_if_t::is_container> +cereal_override(cereal::JSONInputArchive& ar, T& t, const char* field) +{ + cereal::size_type size; + ar.setNextName(field); + cereal::prologue(ar, t); + ar(cereal::make_size_tag(size)); + t.resize(static_cast(size)); + for (auto&& element : t) + { + xdr::archive(ar, element); + } + cereal::epilogue(ar, t); +} + +template +void +cereal_override(cereal::JSONInputArchive& ar, xdr::opaque_vec& s, + const char* field) +{ + std::string hex; + xdr::archive(ar, hex, field); + auto bin = stellar::hexToBin(hex); + s.resize(bin.size()); + std::copy(bin.begin(), bin.end(), s.begin()); +} + +void cereal_override(cereal::JSONInputArchive& ar, stellar::PublicKey& s, + const char* field); + +void cereal_override(cereal::JSONInputArchive& ar, stellar::SCAddress& addr, + const char* field); + +void cereal_override(cereal::JSONInputArchive& ar, + stellar::ConfigUpgradeSetKey& key, const char* field); + +void cereal_override(cereal::JSONInputArchive& ar, + stellar::MuxedAccount& muxedAccount, const char* field); + +template +typename std::enable_if::value || + std::is_same::value || + std::is_same::value>::type +cereal_override(cereal::JSONInputArchive& ar, T& asset, const char* field) +{ + try + { + std::string name; + xdr::archive(ar, name, field); + if (name == "NATIVE") + { + asset.type(stellar::ASSET_TYPE_NATIVE); + return; + } + if constexpr (std::is_same_v) + { + asset.type(stellar::ASSET_TYPE_POOL_SHARE); + auto bin = stellar::hexToBin(name); + if (bin.size() == asset.liquidityPoolID().size()) + { + std::copy(bin.begin(), bin.end(), + asset.liquidityPoolID().begin()); + return; + } + } + } + catch (const cereal::RapidJSONException&) + { + } + + ar.setNextName(field); + ar.startNode(); + if constexpr (std::is_same_v) + { + asset.type(stellar::ASSET_TYPE_POOL_SHARE); + asset.liquidityPool().type(stellar::LIQUIDITY_POOL_CONSTANT_PRODUCT); + auto& cp = asset.liquidityPool().constantProduct(); + + try + { + + xdr::archive(ar, cp.assetA, "assetA"); + xdr::archive(ar, cp.assetB, "assetB"); + xdr::archive(ar, cp.fee, "fee"); + ar.finishNode(); + return; + } + catch (cereal::Exception const&) + { + } + } + + stellar::AccountID issuer; + std::vector assetCode; + try + { + std::string code; + xdr::archive(ar, code, "assetCode"); + std::copy(code.begin(), code.end(), std::back_inserter(assetCode)); + } + catch (cereal::Exception const&) + { + std::string hex; + xdr::archive(ar, hex, "assetCodeRaw"); + assetCode = stellar::hexToBin(hex); + releaseAssertOrThrow(assetCode.size() == 12 || assetCode.size() == 4); + } + xdr::archive(ar, issuer, "issuer"); + ar.finishNode(); + + auto setAlpha = [&assetCode, &issuer](auto& alpha) { + alpha.assetCode.fill(0); + std::copy(assetCode.begin(), assetCode.end(), alpha.assetCode.begin()); + alpha.issuer = issuer; + }; + + if (assetCode.size() <= 4) + { + asset.type(stellar::ASSET_TYPE_CREDIT_ALPHANUM4); + setAlpha(asset.alphaNum4()); + } + else + { + asset.type(stellar::ASSET_TYPE_CREDIT_ALPHANUM12); + setAlpha(asset.alphaNum12()); + } +} + +template +typename std::enable_if::is_enum>::type +cereal_override(cereal::JSONInputArchive& ar, T& t, const char* field) +{ + using traits = xdr::xdr_traits; + using case_type = typename traits::case_type; + thread_local std::unordered_map map; + + if (map.empty()) + { + for (auto& value : traits::enum_values()) + { + case_type enum_value{value}; + map[traits::enum_name(static_cast(value))] = enum_value; + } + } + + std::string name; + xdr::archive(ar, name, field); + auto val = map.find(name); + if (val == map.end()) + { + t = static_cast(std::stoll(name)); + } + else + { + t = static_cast(val->second); + } +} + +template +std::enable_if_t> +cereal_override(cereal::JSONInputArchive& ar, xdr::pointer& t, + const char* field) +{ + try + { + std::nullptr_t p; + xdr::archive(ar, p, field); + t.reset(nullptr); + } + catch (const cereal::RapidJSONException&) + { + T base; + xdr::archive(ar, base, field); + t = xdr::pointer(new T{base}); + } +} +#endif // BUILD_TESTS + // NOTE: Nothing else should include xdrpp/cereal.h directly. // cereal_override's have to be defined before xdrpp/cereal.h, // otherwise some interplay of name lookup and visibility diff --git a/src/util/test/XDRCerealTests.cpp b/src/util/test/XDRCerealTests.cpp new file mode 100644 index 0000000000..5627b76577 --- /dev/null +++ b/src/util/test/XDRCerealTests.cpp @@ -0,0 +1,684 @@ +#include "crypto/StrKey.h" +#include "test/Catch2.h" +#include "util/Decoder.h" +#include +#include + +namespace +{ + +template +bool +roundtrip(const T& a) +{ + std::stringstream ss; + T b; + { + cereal::JSONOutputArchive ar{ss}; + xdr::archive(ar, a); + } + cereal::JSONInputArchive ar{ss}; + xdr::archive(ar, b); + return a == b; +} + +// We make our own generator for XDR types instead of using the overload for +// autocheck::generator defined in so that we can set the +// `levels` value to 1. +template struct XDRGenerator +{ + using result_type = T; + + result_type + operator()(size_t size) const + { + xdr::generator_t g(size, 1); + T t; + xdr::archive(g, t); + return t; + } +}; + +template struct Generator +{ + using type = autocheck::generator; +}; + +template +struct Generator::valid && + !xdr::xdr_traits::is_numeric>> +{ + using type = XDRGenerator; +}; + +template +bool +roundtripRandoms() +{ + autocheck::arbitrary::type> gen; + autocheck::value> arg; + gen.resize([](size_t s) { return std::min(s, static_cast(10)); }); + + for (int i = 0; i < 1000; i++) + { + gen(arg); + auto& actual = std::get<0>(arg.ref()); + if (!roundtrip(actual)) + { + std::cerr << "Failed for XDR #" << i << ": " + << stellar::decoder::encode_b64( + xdr::xdr_to_opaque(actual)) + << "\n"; + return false; + } + } + return true; +} + +#define ROUNDTRIP(name) \ + TEST_CASE("XDRCereal overrides can roundtrip " #name, \ + "[xdr cereal roundtrip][acceptance]") \ + { \ + using namespace stellar; \ + REQUIRE(roundtripRandoms()); \ + } + +ROUNDTRIP(bool) +ROUNDTRIP(xdr::pointer>) + +#ifdef TEST_XDR_ROUNDTRIPS +ROUNDTRIP(AccountEntry) +ROUNDTRIP(AccountEntryExtensionV1) +ROUNDTRIP(AccountEntryExtensionV2) +ROUNDTRIP(AccountEntryExtensionV3) +ROUNDTRIP(AccountFlags) +ROUNDTRIP(AccountID) +ROUNDTRIP(AccountMergeResult) +ROUNDTRIP(AccountMergeResultCode) +ROUNDTRIP(AllowTrustOp) +ROUNDTRIP(AllowTrustResult) +ROUNDTRIP(AllowTrustResultCode) +ROUNDTRIP(AlphaNum12) +ROUNDTRIP(AlphaNum4) +ROUNDTRIP(Asset) +ROUNDTRIP(AssetCode) +ROUNDTRIP(AssetCode12) +ROUNDTRIP(AssetCode4) +ROUNDTRIP(AssetType) +ROUNDTRIP(Auth) +ROUNDTRIP(AuthCert) +ROUNDTRIP(AuthenticatedMessage) +ROUNDTRIP(BeginSponsoringFutureReservesOp) +ROUNDTRIP(BeginSponsoringFutureReservesResult) +ROUNDTRIP(BeginSponsoringFutureReservesResultCode) +ROUNDTRIP(BinaryFuseFilterType) +ROUNDTRIP(BucketEntry) +ROUNDTRIP(BucketEntryType) +ROUNDTRIP(BucketListType) +ROUNDTRIP(BucketMetadata) +ROUNDTRIP(BumpSequenceOp) +ROUNDTRIP(BumpSequenceResult) +ROUNDTRIP(BumpSequenceResultCode) +ROUNDTRIP(ChangeTrustAsset) +ROUNDTRIP(ChangeTrustOp) +ROUNDTRIP(ChangeTrustResult) +ROUNDTRIP(ChangeTrustResultCode) +ROUNDTRIP(ClaimAtom) +ROUNDTRIP(ClaimAtomType) +ROUNDTRIP(ClaimClaimableBalanceOp) +ROUNDTRIP(ClaimClaimableBalanceResult) +ROUNDTRIP(ClaimClaimableBalanceResultCode) +ROUNDTRIP(ClaimLiquidityAtom) +ROUNDTRIP(ClaimOfferAtom) +ROUNDTRIP(ClaimOfferAtomV0) +ROUNDTRIP(ClaimPredicate) +ROUNDTRIP(ClaimPredicateType) +ROUNDTRIP(ClaimableBalanceEntry) +ROUNDTRIP(ClaimableBalanceEntryExtensionV1) +ROUNDTRIP(ClaimableBalanceFlags) +ROUNDTRIP(ClaimableBalanceID) +ROUNDTRIP(ClaimableBalanceIDType) +ROUNDTRIP(Claimant) +ROUNDTRIP(ClaimantType) +ROUNDTRIP(ClawbackClaimableBalanceOp) +ROUNDTRIP(ClawbackClaimableBalanceResult) +ROUNDTRIP(ClawbackClaimableBalanceResultCode) +ROUNDTRIP(ClawbackOp) +ROUNDTRIP(ClawbackResult) +ROUNDTRIP(ClawbackResultCode) +ROUNDTRIP(ConfigSettingContractBandwidthV0) +ROUNDTRIP(ConfigSettingContractComputeV0) +ROUNDTRIP(ConfigSettingContractEventsV0) +ROUNDTRIP(ConfigSettingContractExecutionLanesV0) +ROUNDTRIP(ConfigSettingContractHistoricalDataV0) +ROUNDTRIP(ConfigSettingContractLedgerCostExtV0) +ROUNDTRIP(ConfigSettingContractLedgerCostV0) +ROUNDTRIP(ConfigSettingContractParallelComputeV0) +ROUNDTRIP(ConfigSettingEntry) +ROUNDTRIP(ConfigSettingID) +ROUNDTRIP(ConfigSettingSCPTiming) +ROUNDTRIP(ConfigUpgradeSet) +ROUNDTRIP(ConfigUpgradeSetKey) +ROUNDTRIP(ContractCodeCostInputs) +ROUNDTRIP(ContractCodeEntry) +ROUNDTRIP(ContractCostParamEntry) +ROUNDTRIP(ContractCostParams) +ROUNDTRIP(ContractCostType) +ROUNDTRIP(ContractDataDurability) +ROUNDTRIP(ContractDataEntry) +ROUNDTRIP(ContractEvent) +ROUNDTRIP(ContractEventType) +ROUNDTRIP(ContractExecutable) +ROUNDTRIP(ContractExecutableType) +ROUNDTRIP(ContractIDPreimage) +ROUNDTRIP(ContractIDPreimageType) +ROUNDTRIP(CreateAccountOp) +ROUNDTRIP(CreateAccountResult) +ROUNDTRIP(CreateAccountResultCode) +ROUNDTRIP(CreateClaimableBalanceOp) +ROUNDTRIP(CreateClaimableBalanceResult) +ROUNDTRIP(CreateClaimableBalanceResultCode) +ROUNDTRIP(CreateContractArgs) +ROUNDTRIP(CreateContractArgsV2) +ROUNDTRIP(CreatePassiveSellOfferOp) +ROUNDTRIP(CryptoKeyType) +ROUNDTRIP(Curve25519Public) +ROUNDTRIP(Curve25519Secret) +ROUNDTRIP(DataEntry) +ROUNDTRIP(DataValue) +ROUNDTRIP(DecoratedSignature) +ROUNDTRIP(DependentTxCluster) +ROUNDTRIP(DiagnosticEvent) +ROUNDTRIP(DontHave) +ROUNDTRIP(Duration) +ROUNDTRIP(EncryptedBody) +ROUNDTRIP(EndSponsoringFutureReservesResult) +ROUNDTRIP(EndSponsoringFutureReservesResultCode) +ROUNDTRIP(EnvelopeType) +ROUNDTRIP(Error) +ROUNDTRIP(ErrorCode) +ROUNDTRIP(EvictionIterator) +ROUNDTRIP(ExtendFootprintTTLOp) +ROUNDTRIP(ExtendFootprintTTLResult) +ROUNDTRIP(ExtendFootprintTTLResultCode) +ROUNDTRIP(ExtensionPoint) +ROUNDTRIP(FeeBumpTransaction) +ROUNDTRIP(FeeBumpTransactionEnvelope) +ROUNDTRIP(FloodAdvert) +ROUNDTRIP(FloodDemand) +ROUNDTRIP(GeneralizedTransactionSet) +ROUNDTRIP(Hash) +ROUNDTRIP(HashIDPreimage) +ROUNDTRIP(Hello) +ROUNDTRIP(HmacSha256Key) +ROUNDTRIP(HmacSha256Mac) +ROUNDTRIP(HostFunction) +ROUNDTRIP(HostFunctionType) +ROUNDTRIP(HotArchiveBucketEntry) +ROUNDTRIP(HotArchiveBucketEntryType) +ROUNDTRIP(IPAddrType) +ROUNDTRIP(InflationPayout) +ROUNDTRIP(InflationResult) +ROUNDTRIP(InflationResultCode) +ROUNDTRIP(InnerTransactionResult) +ROUNDTRIP(InnerTransactionResultPair) +ROUNDTRIP(Int128Parts) +ROUNDTRIP(Int256Parts) +ROUNDTRIP(InvokeContractArgs) +ROUNDTRIP(InvokeHostFunctionOp) +ROUNDTRIP(InvokeHostFunctionResult) +ROUNDTRIP(InvokeHostFunctionResultCode) +ROUNDTRIP(InvokeHostFunctionSuccessPreImage) +ROUNDTRIP(LedgerBounds) +ROUNDTRIP(LedgerCloseMeta) +ROUNDTRIP(LedgerCloseMetaExt) +ROUNDTRIP(LedgerCloseMetaExtV1) +ROUNDTRIP(LedgerCloseMetaV0) +ROUNDTRIP(LedgerCloseMetaV1) +ROUNDTRIP(LedgerCloseMetaV2) +ROUNDTRIP(LedgerCloseValueSignature) +ROUNDTRIP(LedgerEntry) +ROUNDTRIP(LedgerEntryChange) +ROUNDTRIP(LedgerEntryChangeType) +ROUNDTRIP(LedgerEntryChanges) +ROUNDTRIP(LedgerEntryExtensionV1) +ROUNDTRIP(LedgerEntryType) +ROUNDTRIP(LedgerFootprint) +ROUNDTRIP(LedgerHeader) +ROUNDTRIP(LedgerHeaderExtensionV1) +ROUNDTRIP(LedgerHeaderFlags) +ROUNDTRIP(LedgerHeaderHistoryEntry) +ROUNDTRIP(LedgerKey) +ROUNDTRIP(LedgerSCPMessages) +ROUNDTRIP(LedgerUpgrade) +ROUNDTRIP(LedgerUpgradeType) +ROUNDTRIP(Liabilities) +ROUNDTRIP(LiquidityPoolConstantProductParameters) +ROUNDTRIP(LiquidityPoolDepositOp) +ROUNDTRIP(LiquidityPoolDepositResult) +ROUNDTRIP(LiquidityPoolDepositResultCode) +ROUNDTRIP(LiquidityPoolEntry) +ROUNDTRIP(LiquidityPoolParameters) +ROUNDTRIP(LiquidityPoolType) +ROUNDTRIP(LiquidityPoolWithdrawOp) +ROUNDTRIP(LiquidityPoolWithdrawResult) +ROUNDTRIP(LiquidityPoolWithdrawResultCode) +ROUNDTRIP(ManageBuyOfferOp) +ROUNDTRIP(ManageBuyOfferResult) +ROUNDTRIP(ManageBuyOfferResultCode) +ROUNDTRIP(ManageDataOp) +ROUNDTRIP(ManageDataResult) +ROUNDTRIP(ManageDataResultCode) +ROUNDTRIP(ManageOfferEffect) +ROUNDTRIP(ManageOfferSuccessResult) +ROUNDTRIP(ManageSellOfferOp) +ROUNDTRIP(ManageSellOfferResult) +ROUNDTRIP(ManageSellOfferResultCode) +ROUNDTRIP(Memo) +ROUNDTRIP(MemoType) +ROUNDTRIP(MessageType) +ROUNDTRIP(MuxedAccount) +ROUNDTRIP(MuxedEd25519Account) +ROUNDTRIP(OfferEntry) +ROUNDTRIP(OfferEntryFlags) +ROUNDTRIP(Operation) +ROUNDTRIP(OperationMeta) +ROUNDTRIP(OperationMetaV2) +ROUNDTRIP(OperationResult) +ROUNDTRIP(OperationResultCode) +ROUNDTRIP(OperationType) +ROUNDTRIP(ParallelTxExecutionStage) +ROUNDTRIP(ParallelTxsComponent) +ROUNDTRIP(PathPaymentStrictReceiveOp) +ROUNDTRIP(PathPaymentStrictReceiveResult) +ROUNDTRIP(PathPaymentStrictReceiveResultCode) +ROUNDTRIP(PathPaymentStrictSendOp) +ROUNDTRIP(PathPaymentStrictSendResult) +ROUNDTRIP(PathPaymentStrictSendResultCode) +ROUNDTRIP(PaymentOp) +ROUNDTRIP(PaymentResult) +ROUNDTRIP(PaymentResultCode) +ROUNDTRIP(PeerAddress) +ROUNDTRIP(PeerStats) +ROUNDTRIP(PreconditionType) +ROUNDTRIP(Preconditions) +ROUNDTRIP(PreconditionsV2) +ROUNDTRIP(Price) +ROUNDTRIP(PublicKeyType) +ROUNDTRIP(RestoreFootprintOp) +ROUNDTRIP(RestoreFootprintResult) +ROUNDTRIP(RestoreFootprintResultCode) +ROUNDTRIP(RevokeSponsorshipOp) +ROUNDTRIP(RevokeSponsorshipResult) +ROUNDTRIP(RevokeSponsorshipResultCode) +ROUNDTRIP(RevokeSponsorshipType) +ROUNDTRIP(SCAddress) +ROUNDTRIP(SCAddressType) +ROUNDTRIP(SCBytes) +ROUNDTRIP(SCContractInstance) +ROUNDTRIP(SCError) +ROUNDTRIP(SCErrorCode) +ROUNDTRIP(SCErrorType) +ROUNDTRIP(SCMap) +ROUNDTRIP(SCMapEntry) +ROUNDTRIP(SCNonceKey) +ROUNDTRIP(SCPBallot) +ROUNDTRIP(SCPEnvelope) +ROUNDTRIP(SCPHistoryEntry) +ROUNDTRIP(SCPHistoryEntryV0) +ROUNDTRIP(SCPNomination) +ROUNDTRIP(SCPQuorumSet) +ROUNDTRIP(SCPStatement) +ROUNDTRIP(SCPStatementType) +// Note SCString and SCSymbol aren't listed because their overloads are +// ambiguous +ROUNDTRIP(SCVal) +ROUNDTRIP(SCValType) +ROUNDTRIP(SCVec) +ROUNDTRIP(SendMore) +ROUNDTRIP(SendMoreExtended) +ROUNDTRIP(SequenceNumber) +ROUNDTRIP(SerializedBinaryFuseFilter) +ROUNDTRIP(SetOptionsOp) +ROUNDTRIP(SetOptionsResult) +ROUNDTRIP(SetOptionsResultCode) +ROUNDTRIP(SetTrustLineFlagsOp) +ROUNDTRIP(SetTrustLineFlagsResult) +ROUNDTRIP(SetTrustLineFlagsResultCode) +ROUNDTRIP(ShortHashSeed) +ROUNDTRIP(SignedTimeSlicedSurveyRequestMessage) +ROUNDTRIP(SignedTimeSlicedSurveyResponseMessage) +ROUNDTRIP(SignedTimeSlicedSurveyStartCollectingMessage) +ROUNDTRIP(SignedTimeSlicedSurveyStopCollectingMessage) +ROUNDTRIP(Signer) +ROUNDTRIP(SignerKey) +ROUNDTRIP(SignerKeyType) +ROUNDTRIP(SimplePaymentResult) +ROUNDTRIP(SorobanAddressCredentials) +ROUNDTRIP(SorobanAuthorizationEntries) +ROUNDTRIP(SorobanAuthorizationEntry) +ROUNDTRIP(SorobanAuthorizedFunction) +ROUNDTRIP(SorobanAuthorizedFunctionType) +ROUNDTRIP(SorobanAuthorizedInvocation) +ROUNDTRIP(SorobanCredentials) +ROUNDTRIP(SorobanCredentialsType) +ROUNDTRIP(SorobanResources) +ROUNDTRIP(SorobanResourcesExtV0) +ROUNDTRIP(SorobanTransactionData) +ROUNDTRIP(SorobanTransactionMeta) +ROUNDTRIP(SorobanTransactionMetaExt) +ROUNDTRIP(SorobanTransactionMetaExtV1) +ROUNDTRIP(SorobanTransactionMetaV2) +ROUNDTRIP(SponsorshipDescriptor) +ROUNDTRIP(StateArchivalSettings) +ROUNDTRIP(StellarMessage) +ROUNDTRIP(StellarValue) +ROUNDTRIP(StellarValueType) +ROUNDTRIP(SurveyMessageCommandType) +ROUNDTRIP(SurveyMessageResponseType) +ROUNDTRIP(SurveyRequestMessage) +ROUNDTRIP(SurveyResponseBody) +ROUNDTRIP(SurveyResponseMessage) +ROUNDTRIP(TTLEntry) +ROUNDTRIP(ThresholdIndexes) +ROUNDTRIP(TimeBounds) +ROUNDTRIP(TimeSlicedNodeData) +ROUNDTRIP(TimeSlicedPeerData) +ROUNDTRIP(TimeSlicedPeerDataList) +ROUNDTRIP(TimeSlicedSurveyRequestMessage) +ROUNDTRIP(TimeSlicedSurveyResponseMessage) +ROUNDTRIP(TimeSlicedSurveyStartCollectingMessage) +ROUNDTRIP(TimeSlicedSurveyStopCollectingMessage) +ROUNDTRIP(TopologyResponseBodyV2) +ROUNDTRIP(Transaction) +ROUNDTRIP(TransactionEnvelope) +ROUNDTRIP(TransactionEvent) +ROUNDTRIP(TransactionEventStage) +ROUNDTRIP(TransactionHistoryEntry) +ROUNDTRIP(TransactionHistoryResultEntry) +ROUNDTRIP(TransactionMeta) +ROUNDTRIP(TransactionMetaV1) +ROUNDTRIP(TransactionMetaV2) +ROUNDTRIP(TransactionMetaV3) +ROUNDTRIP(TransactionMetaV4) +ROUNDTRIP(TransactionPhase) +ROUNDTRIP(TransactionResult) +ROUNDTRIP(TransactionResultCode) +ROUNDTRIP(TransactionResultMeta) +ROUNDTRIP(TransactionResultMetaV1) +ROUNDTRIP(TransactionResultPair) +ROUNDTRIP(TransactionResultSet) +ROUNDTRIP(TransactionSet) +ROUNDTRIP(TransactionSetV1) +ROUNDTRIP(TransactionSignaturePayload) +ROUNDTRIP(TransactionV0) +ROUNDTRIP(TransactionV0Envelope) +ROUNDTRIP(TransactionV1Envelope) +ROUNDTRIP(TrustLineAsset) +ROUNDTRIP(TrustLineEntry) +ROUNDTRIP(TrustLineEntryExtensionV2) +ROUNDTRIP(TrustLineFlags) +ROUNDTRIP(TxAdvertVector) +ROUNDTRIP(TxSetComponent) +ROUNDTRIP(TxSetComponentType) +ROUNDTRIP(UInt128Parts) +ROUNDTRIP(UInt256Parts) +ROUNDTRIP(UpgradeEntryMeta) +ROUNDTRIP(UpgradeType) +#endif + +template +std::string +toCerealCompact(T const& t, std::string const& name) +{ + using stellar::xdrToCerealString; + using namespace rapidjson; + std::string result = xdrToCerealString(t, name); + Document d; + d.Parse(result.c_str()); + + StringBuffer sb; + Writer w(sb); + d.Accept(w); + + return sb.GetString(); +} + +template +void +copyHexToArray(xdr::opaque_array& arr, char const* hex) +{ + auto bin = stellar::hexToBin(hex); + releaseAssert(bin.size() <= N); + std::copy(bin.begin(), bin.end(), arr.begin()); +} + +template +void +copyToArray(xdr::opaque_array& arr, std::string const& s) +{ + releaseAssert(s.size() <= N); + std::copy(s.begin(), s.end(), arr.begin()); +} + +TEST_CASE("XDRCereal overrides") +{ + using namespace stellar; + char const accountIdHex[] = + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + static_assert(sizeof(accountIdHex) == + uint256::container_fixed_nelem * 2 + 1); + static_assert(std::is_same_v); + + // Check xstring + { + REQUIRE(toCerealCompact(xdr::xstring<5>{"abc"}, "s") == + R"({"s":"abc"})"); + xdr::xstring<5> s{std::string{'a', '\0', 'b', 'c'}}; + REQUIRE(toCerealCompact(s, "s") == R"({"s":{"raw":"61006263"}})"); + } + + // Check opaque array + { + xdr::opaque_array<8> o; + copyHexToArray(o, "0123456789abcdef"); + REQUIRE(toCerealCompact(o, "o") == R"({"o":"0123456789abcdef"})"); + } + + // Check container (xdr::xvector) + { + xdr::xvector v{1, 2, 3}; + CHECK(toCerealCompact(v, "v") == R"({"v":[1,2,3]})"); + } + + // Check container with nested override (xdr::xvector of enums) + { + xdr::xvector v; + Asset a; + a.type(ASSET_TYPE_NATIVE); + v.push_back(a); + a.type(ASSET_TYPE_CREDIT_ALPHANUM4); + copyToArray(a.alphaNum4().assetCode, "USD"); + copyHexToArray(a.alphaNum4().issuer.ed25519(), accountIdHex); + v.push_back(a); + CHECK( + toCerealCompact(v, "v") == + R"({"v":["NATIVE",{"assetCode":"USD","issuer":"GAASGRLHRGV433YBENCWPCNLZXXQCI2FM6E2XTPPAERUKZ4JVPG66OUL"}]})"); + } + + // Check opaque_vec + { + xdr::opaque_vec<> v{0x01, 0x23, 0x45}; + CHECK(toCerealCompact(v, "v") == R"({"v":"012345"})"); + } + + // Check PublicKey + { + PublicKey pk; + pk.type(PUBLIC_KEY_TYPE_ED25519); + copyHexToArray(pk.ed25519(), accountIdHex); + CHECK( + toCerealCompact(pk, "pk") == + R"({"pk":"GAASGRLHRGV433YBENCWPCNLZXXQCI2FM6E2XTPPAERUKZ4JVPG66OUL"})"); + } + + // Check SCAddress (account) + { + SCAddress addr; + addr.type(SC_ADDRESS_TYPE_ACCOUNT); + addr.accountId().type(PUBLIC_KEY_TYPE_ED25519); + copyHexToArray(addr.accountId().ed25519(), accountIdHex); + CHECK( + toCerealCompact(addr, "a") == + R"({"a":"GAASGRLHRGV433YBENCWPCNLZXXQCI2FM6E2XTPPAERUKZ4JVPG66OUL"})"); + } + + // Check SCAddress (contract) + { + SCAddress addr; + addr.type(SC_ADDRESS_TYPE_CONTRACT); + copyHexToArray(addr.contractId(), accountIdHex); + CHECK( + toCerealCompact(addr, "a") == + R"({"a":"CAASGRLHRGV433YBENCWPCNLZXXQCI2FM6E2XTPPAERUKZ4JVPG67KRS"})"); + } + + // Check ConfigUpgradeSetKey + { + ConfigUpgradeSetKey key; + copyHexToArray(key.contentHash, accountIdHex); + copyHexToArray(key.contractID, accountIdHex); + CHECK( + toCerealCompact(key, "k") == + R"({"k":{"contractID":"CAASGRLHRGV433YBENCWPCNLZXXQCI2FM6E2XTPPAERUKZ4JVPG67KRS","contentHash":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}})"); + } + + // Check MuxedAccount (ED25519) + { + MuxedAccount ma; + ma.type(KEY_TYPE_ED25519); + copyHexToArray(ma.ed25519(), accountIdHex); + CHECK( + toCerealCompact(ma, "m") == + R"({"m":"GAASGRLHRGV433YBENCWPCNLZXXQCI2FM6E2XTPPAERUKZ4JVPG66OUL"})"); + } + + // Check MuxedAccount (MUXED_ED25519) + { + MuxedAccount ma; + ma.type(KEY_TYPE_MUXED_ED25519); + ma.med25519().id = 12345; + copyHexToArray(ma.med25519().ed25519, accountIdHex); + CHECK( + toCerealCompact(ma, "m") == + R"({"m":{"id":12345,"accountID":"GAASGRLHRGV433YBENCWPCNLZXXQCI2FM6E2XTPPAERUKZ4JVPG66OUL"}})"); + } + + // Check Asset (NATIVE) + { + Asset a; + a.type(ASSET_TYPE_NATIVE); + CHECK(toCerealCompact(a, "a") == R"({"a":"NATIVE"})"); + } + + // Check Asset (CREDIT_ALPHANUM4, valid code) + { + Asset a; + a.type(ASSET_TYPE_CREDIT_ALPHANUM4); + copyToArray(a.alphaNum4().assetCode, "USD"); + a.alphaNum4().issuer.type(PUBLIC_KEY_TYPE_ED25519); + copyHexToArray(a.alphaNum4().issuer.ed25519(), accountIdHex); + CHECK( + toCerealCompact(a, "a") == + R"({"a":{"assetCode":"USD","issuer":"GAASGRLHRGV433YBENCWPCNLZXXQCI2FM6E2XTPPAERUKZ4JVPG66OUL"}})"); + ; + } + + // Check Asset (CREDIT_ALPHANUM12, valid code) + { + Asset a; + a.type(ASSET_TYPE_CREDIT_ALPHANUM12); + copyToArray(a.alphaNum12().assetCode, "USDC12"); + copyHexToArray(a.alphaNum12().issuer.ed25519(), accountIdHex); + CHECK( + toCerealCompact(a, "a") == + R"({"a":{"assetCode":"USDC12","issuer":"GAASGRLHRGV433YBENCWPCNLZXXQCI2FM6E2XTPPAERUKZ4JVPG66OUL"}})"); + } + + // Check Asset (CREDIT_ALPHANUM4, invalid code with embedded NUL) + { + Asset a; + a.type(ASSET_TYPE_CREDIT_ALPHANUM4); + copyToArray(a.alphaNum4().assetCode, std::string{'U', '\0', 'D', '\0'}); + copyHexToArray(a.alphaNum4().issuer.ed25519(), accountIdHex); + CHECK( + toCerealCompact(a, "a") == + R"({"a":{"assetCodeRaw":"55004400","issuer":"GAASGRLHRGV433YBENCWPCNLZXXQCI2FM6E2XTPPAERUKZ4JVPG66OUL"}})"); + } + + // Check TrustLineAsset (POOL_SHARE) + { + TrustLineAsset tla; + tla.type(ASSET_TYPE_POOL_SHARE); + copyHexToArray(tla.liquidityPoolID(), accountIdHex); + CHECK( + toCerealCompact(tla, "a") == + R"({"a":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"})"); + } + + // Check ChangeTrustAsset (POOL_SHARE) + { + ChangeTrustAsset cta; + cta.type(ASSET_TYPE_POOL_SHARE); + cta.liquidityPool().type(LIQUIDITY_POOL_CONSTANT_PRODUCT); + auto& cp = cta.liquidityPool().constantProduct(); + cp.assetA.type(ASSET_TYPE_NATIVE); + cp.assetB.type(ASSET_TYPE_NATIVE); + cp.fee = 30; + CHECK(toCerealCompact(cta, "a") == + R"({"a":{"assetA":"NATIVE","assetB":"NATIVE","fee":30}})"); + } + + // Check enum + { + AssetType e = ASSET_TYPE_NATIVE; + CHECK(toCerealCompact(e, "e") == R"({"e":"ASSET_TYPE_NATIVE"})"); + } + + // Check pointer (null) + { + xdr::pointer p; + CHECK(toCerealCompact(p, "p") == R"({"p":null})"); + } + + // Check pointer (non-null) + { + Asset a; + a.type(ASSET_TYPE_NATIVE); + xdr::pointer p(new Asset{a}); + CHECK(toCerealCompact(p, "p") == R"({"p":"NATIVE"})"); + } + + // Check nested pointer (null) + { + xdr::pointer> p; + CHECK(toCerealCompact(p, "p") == R"({"p":[]})"); + } + + // Check nested pointer (outer non-null, inner null) + { + xdr::pointer> p; + p.reset(new xdr::pointer{}); + CHECK(toCerealCompact(p, "p") == R"({"p":[null]})"); + } + + // Check nested pointer (outer non-null, inner non-null) + { + Asset a; + a.type(ASSET_TYPE_NATIVE); + xdr::pointer> p; + p.reset(new xdr::pointer{new Asset{a}}); + CHECK(toCerealCompact(p, "p") == R"({"p":["NATIVE"]})"); + } +} +} // namespace