Compare commits

..
Author SHA1 Message Date
tequ 68387927e0 Merge branch 'dev' into fix/normal-tx-consequences-factory 2026-09-15 12:48:40 +09:00
tequ c208a40ce8 remove WASM: $COUNTER from SetHook_wasm.h (#804) 2026-09-09 15:31:11 +10:00
tequ 401a35957d retire fixXahauV1, fixXahauV2 Amendments (#722) 2026-09-09 13:00:24 +10:00
Niq Dudfield 65ff61d518 fix: fail fast when amendment blocked instead of degraded state (#707) 2026-09-08 13:39:45 +10:00
Richard Holland 6191c1e09d Onchain manifests (#779) 2026-09-07 13:19:25 +10:00
tequ f1a3a775a1 Add optional Hooks data to account_info response (#789) 2026-09-02 17:22:53 +10:00
tequ c1dabe5576 Fix comments (#768) 2026-09-01 14:18:28 +10:00
Niq Dudfield c6d5b345eb fix: subscription webhook delivery stalling on HTTP errors (#677) 2026-08-27 12:13:21 +10:00
tequ aab273c575 Use normal consequences factory for standard transactions
Replace redundant custom consequence factories that returned normal consequences with the built-in Normal factory.
2026-08-03 21:57:18 +09:00
77 changed files with 5709 additions and 1823 deletions

No files matched your search

+6
View File
@@ -77,6 +77,11 @@ test.ledger > xrpld.app
test.ledger > xrpld.core
test.ledger > xrpld.ledger
test.ledger > xrpl.protocol
test.net > test.toplevel
test.net > xrpl.basics
test.net > xrpld.core
test.net > xrpld.net
test.net > xrpl.json
test.nodestore > test.jtx
test.nodestore > test.toplevel
test.nodestore > test.unit_test
@@ -200,6 +205,7 @@ xrpld.rpc > xrpld.core
xrpld.rpc > xrpld.ledger
xrpld.rpc > xrpld.nodestore
xrpld.rpc > xrpld.shamap
xrpld.rpc > xrpl.hook
xrpld.rpc > xrpl.json
xrpld.rpc > xrpl.protocol
xrpld.rpc > xrpl.resource
+2 -2
View File
@@ -494,7 +494,7 @@
#
# Configure the maximum number of transactions to have in the job queue
#
# Must be a number between 100 and 1000, defaults to 250
# Must be a number between 100 and 1000, defaults to 1000
#
#
# [overlay]
@@ -593,7 +593,7 @@
# reaches or exceeds this number. After that the limit may still
# change, but will stay above the target. If consensus is not
# healthy, the limit will be clamped to this value or lower.
# Default: 50.
# Default: 1000.
#
# maximum_txn_in_ledger = <number>
#
-2
View File
@@ -146,8 +146,6 @@ D686F2538F410C9D0D856788E98E3579595DAF7B38D38887F81ECAC934B06040 HooksUpdate1
3C43D9A973AA4443EF3FC38E42DD306160FBFFDAB901CD8BAA15D09F2597EB87 NonFungibleTokensV1
0285B7E5E08E1A8E4C15636F0591D87F73CB6A7B6452A932AD72BBC8E5D1CBE3 fixNFTokenDirV1
36799EA497B1369B170805C078AEFE6188345F9B3E324C21E9CA3FF574E3C3D6 fixNFTokenNegOffer
4C499D17719BB365B69010A436B64FD1A82AAB199FC1CEB06962EBD01059FB09 fixXahauV1
215181D23BF5C173314B5FDB9C872C92DE6CC918483727DE037C0C13E7E6EE9D fixXahauV2
0D8BF22FF7570D58598D1EF19EBB6E142AD46E59A223FD3816262FBB69345BEA Remit
7CA0426E7F411D39BB014E57CD9E08F61DE1750F0D41FCD428D9FB80BB7596B0 ZeroB2M
4B8466415FAB32FFA89D9DCBE166A42340115771DF611A7160F8D7439C87ECD8 fixNSDelete
+2
View File
@@ -152,6 +152,7 @@
#define sfEscrowID ((5U << 16U) + 35U)
#define sfURITokenID ((5U << 16U) + 36U)
#define sfDomainID ((5U << 16U) + 37U)
#define sfManifestID ((5U << 16U) + 91U)
#define sfHookOnOutgoing ((5U << 16U) + 93U)
#define sfHookOnIncoming ((5U << 16U) + 94U)
#define sfCron ((5U << 16U) + 95U)
@@ -288,6 +289,7 @@
#define sfXChainCreateAccountAttestationCollectionElement ((14U << 16U) + 31U)
#define sfPriceData ((14U << 16U) + 32U)
#define sfCredential ((14U << 16U) + 33U)
#define sfManifest ((14U << 16U) + 90U)
#define sfAmountEntry ((14U << 16U) + 91U)
#define sfMintURIToken ((14U << 16U) + 92U)
#define sfHookEmission ((14U << 16U) + 93U)
+1
View File
@@ -61,6 +61,7 @@
#define ttNFTOKEN_MODIFY 70
#define ttPERMISSIONED_DOMAIN_SET 71
#define ttPERMISSIONED_DOMAIN_DELETE 72
#define ttMANIFEST_SET 91
#define ttCRON 92
#define ttCRON_SET 93
#define ttREMARKS_SET 94
+1 -1
View File
@@ -382,7 +382,7 @@ enum class hook_return_code : int64_t {
INVALID_KEY = -41, // user supplied key was not valid
NOT_A_STRING = -42, // nul terminator missing from a string argument
MEM_OVERLAP = -43, // one or more specified buffers are the same memory
TOO_MANY_STATE_MODIFICATIONS = -44, // more than 5000 modified state
TOO_MANY_STATE_MODIFICATIONS = -44, // more than 256 modified state
// entires in the combined hook chains
TOO_MANY_NAMESPACES = -45
};
+4
View File
@@ -372,6 +372,10 @@ permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept;
Keylet
permissionedDomain(uint256 const& domainID) noexcept;
Keylet
manifest(PublicKey const& pk) noexcept;
} // namespace keylet
// Everything below is deprecated and should be removed in favor of keylets:
+2
View File
@@ -194,6 +194,8 @@ enum TEFcodes : TERUnderlyingType {
tefNONDIR_EMIT,
tefIMPORT_BLACKHOLED,
tefINVALID_LEDGER_FIX_TYPE,
tefPAST_MANIFEST_SEQ,
tefREVOKED_MANIFEST,
};
//------------------------------------------------------------------------------
+3 -3
View File
@@ -34,7 +34,7 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FEATURE(SecureUI, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(OnChainManifests, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (HookMap, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (GuardDepth32, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(NamedHooks, Supported::yes, VoteBehavior::DefaultNo)
@@ -89,8 +89,6 @@ XRPL_FIX (240819, Supported::yes, VoteBehavior::DefaultYe
XRPL_FIX (NSDelete, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(ZeroB2M, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(Remit, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (XahauV2, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (XahauV1, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(HooksUpdate1, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FEATURE(XahauGenesis, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FEATURE(Import, Supported::yes, VoteBehavior::DefaultYes)
@@ -161,6 +159,8 @@ XRPL_FEATURE(CryptoConditionsSuite, Supported::yes, VoteBehavior::Obsolete)
// pre-amendment code has been removed and the identifiers are deprecated.
// All known amendments and amendments that may appear in a validated
// ledger must be registered either here or above with the "active" amendments
XRPL_RETIRE(fixXahauV2)
XRPL_RETIRE(fixXahauV1)
XRPL_RETIRE(MultiSign)
XRPL_RETIRE(TrustSetAuth)
XRPL_RETIRE(FeeEscalation)
@@ -262,7 +262,7 @@ LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({
{sfHookStateScale, soeOPTIONAL},
{sfCron, soeOPTIONAL},
{sfAMMID, soeOPTIONAL},
{sfSecureUI, soeOPTIONAL},
{sfManifestID, soeOPTIONAL},
}))
/** A ledger object which contains a list of object identifiers.
@@ -593,6 +593,26 @@ LEDGER_ENTRY(ltDID, 0x008D, DID, did, ({
{sfPreviousTxnLgrSeq, soeREQUIRED},
}))
LEDGER_ENTRY(ltMANIFEST, 0x004D, Manifest, manifest_entry, ({
{sfAccount, soeREQUIRED},
{sfPublicKey, soeREQUIRED},
{sfSigningPubKey, soeOPTIONAL}, /* may be absent if the master is revoked */
{sfSequence, soeREQUIRED},
/* sfVersion defaults to 0 and is omitted from the signed payload when
absent. Storing it unconditionally would change the bytes and break
signature verification when the manifest is reconstructed. */
{sfVersion, soeOPTIONAL},
{sfDomain, soeOPTIONAL},
/* The signatures are mirrored so the object is a lossless copy of the
manifest and can be independently verified, and re-served to peers,
by any node reading it (see ManifestCache::applyLedger). */
{sfMasterSignature, soeREQUIRED},
{sfSignature, soeOPTIONAL}, /* absent if the master is revoked */
{sfManifestID, soeOPTIONAL}, /* pointer to the identical other copy on the other key */
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
}))
#undef EXPAND
#undef LEDGER_ENTRY_DUPLICATE
+2 -1
View File
@@ -210,6 +210,7 @@ TYPED_SFIELD(sfOfferID, UINT256, 34)
TYPED_SFIELD(sfEscrowID, UINT256, 35)
TYPED_SFIELD(sfURITokenID, UINT256, 36)
TYPED_SFIELD(sfDomainID, UINT256, 37)
TYPED_SFIELD(sfManifestID, UINT256, 91)
TYPED_SFIELD(sfHookOnOutgoing, UINT256, 93)
TYPED_SFIELD(sfHookOnIncoming, UINT256, 94)
TYPED_SFIELD(sfCron, UINT256, 95)
@@ -293,7 +294,6 @@ TYPED_SFIELD(sfAssetClass, VL, 29)
TYPED_SFIELD(sfProvider, VL, 30)
TYPED_SFIELD(sfMPTokenMetadata, VL, 31)
TYPED_SFIELD(sfCredentialType, VL, 32)
TYPED_SFIELD(sfSecureUI, VL, 96)
TYPED_SFIELD(sfHookName, VL, 97)
TYPED_SFIELD(sfRemarkValue, VL, 98)
TYPED_SFIELD(sfRemarkName, VL, 99)
@@ -383,6 +383,7 @@ UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement, OBJECT, 30)
UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, OBJECT, 31)
UNTYPED_SFIELD(sfPriceData, OBJECT, 32)
UNTYPED_SFIELD(sfCredential, OBJECT, 33)
UNTYPED_SFIELD(sfManifest, OBJECT, 90)
UNTYPED_SFIELD(sfAmountEntry, OBJECT, 91)
UNTYPED_SFIELD(sfMintURIToken, OBJECT, 92)
UNTYPED_SFIELD(sfHookEmission, OBJECT, 93)
@@ -74,7 +74,6 @@ TRANSACTION(ttACCOUNT_SET, 3, AccountSet, ({
{sfTickSize, soeOPTIONAL},
{sfNFTokenMinter, soeOPTIONAL},
{sfHookStateScale, soeOPTIONAL},
{sfSecureUI, soeOPTIONAL},
}))
/** This transaction type cancels an existing escrow. */
@@ -501,6 +500,12 @@ TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 72, PermissionedDomainDelete, ({
{sfDomainID, soeREQUIRED},
}))
/* sfAccount is supplied by TxFormats::commonFields; listing it here as well
makes the SOTemplate reject the format at construction. */
TRANSACTION(ttMANIFEST_SET, 91, SetManifest, ({
{sfManifest, soeREQUIRED},
}))
/* A pseudo-txn alarm signal for invoking a hook, emitted by validators after alarm set conditions are met */
TRANSACTION(ttCRON, 92, Cron, ({
{sfOwner, soeREQUIRED},
+1
View File
@@ -350,6 +350,7 @@ JSS(hash_mismatches); // out: catalogue
JSS(have_header); // out: InboundLedger
JSS(have_state); // out: InboundLedger
JSS(have_transactions); // out: InboundLedger
JSS(hooks); // in/out: AccountInfo
JSS(high); // out: BookChanges
JSS(highest_sequence); // out: AccountInfo
JSS(highest_ticket); // out: AccountInfo
+7
View File
@@ -89,6 +89,7 @@ enum class LedgerNameSpace : std::uint16_t {
ORACLE = LEDGER_NAMESPACE2(0x01, 'R'),
MPTOKEN_ISSUANCE = '~',
MPTOKEN = 't',
MANIFEST = 'M',
CREDENTIAL = LEDGER_NAMESPACE2(0x01, 'D'),
PERMISSIONED_DOMAIN = 'm',
@@ -670,6 +671,12 @@ permissionedDomain(uint256 const& domainID) noexcept
return {ltPERMISSIONED_DOMAIN, domainID};
}
Keylet
manifest(PublicKey const& pk) noexcept
{
return {ltMANIFEST, indexHash(LedgerNameSpace::MANIFEST, pk.slice())};
}
} // namespace keylet
} // namespace ripple
+2
View File
@@ -151,6 +151,8 @@ transResults()
MAKE_ERROR(tefNONDIR_EMIT, "An emitted txn was injected into the ledger without a corresponding directory entry."),
MAKE_ERROR(tefIMPORT_BLACKHOLED, "Cannot import keying because target account is blackholed."),
MAKE_ERROR(tefINVALID_LEDGER_FIX_TYPE, "The LedgerFixType field has an invalid value."),
MAKE_ERROR(tefPAST_MANIFEST_SEQ, "The submitted manifest's sequence is not newer than the current."),
MAKE_ERROR(tefREVOKED_MANIFEST, "The submitted manifest is for a revoked master key."),
MAKE_ERROR(telLOCAL_ERROR, "Local failure."),
MAKE_ERROR(telBAD_DOMAIN, "Domain too long."),
+28 -83
View File
@@ -88,9 +88,7 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = fset(account, asfTshCollect);
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "16");
}
void
@@ -112,9 +110,7 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = acctdelete(account, bene);
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "200000" : "200000";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "200000");
}
static uint256
@@ -142,9 +138,7 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = check::cancel(account, checkId);
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "16");
}
void
@@ -167,9 +161,7 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = check::cash(dest, checkId, XRP(100));
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "16");
}
void
@@ -191,9 +183,7 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = check::create(account, dest, XRP(100));
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "16");
}
void
@@ -214,9 +204,7 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = reward::claim(account);
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "16");
}
void
@@ -238,9 +226,7 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = deposit::auth(account, authed);
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "16");
}
void
@@ -262,9 +248,7 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = cancel(account, account, seq1);
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "16");
}
void
@@ -286,8 +270,7 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = escrow(account, dest, XRP(10));
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
std::string const feeResult = "16";
testRPCCall(env, tx, feeResult);
}
@@ -310,9 +293,7 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = finish(account, account, seq1);
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "16");
}
void
@@ -334,9 +315,7 @@ class BaseFee_test : public beast::unit_test::suite
account, import::loadXpop(ImportTCAccountSet::w_seed));
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "106" : "100";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "106");
}
void
@@ -357,9 +336,7 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = invoke::invoke(account);
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "16";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "16");
}
void
@@ -381,9 +358,7 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = offer_cancel(account, offerSeq);
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "16");
}
void
@@ -406,9 +381,7 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = offer(account, USD(1000), XRP(1000));
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "16");
}
void
@@ -430,9 +403,7 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = pay(account, dest, XRP(1));
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "16");
}
static uint256
@@ -468,9 +439,7 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = paychan::claim(account, chan, reqBal, authAmt);
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "16");
}
void
@@ -494,9 +463,7 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = paychan::create(account, dest, XRP(10), settleDelay, pk);
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "16");
}
void
@@ -519,9 +486,7 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = paychan::fund(account, chan, XRP(1));
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "16");
}
void
@@ -552,9 +517,7 @@ class BaseFee_test : public beast::unit_test::suite
hookParams[jss::HookParameter][jss::HookParameterValue] = "DEADBEEF";
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "73022" : "73016";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "73022");
}
void
@@ -576,9 +539,7 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = regkey(account, dest);
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "0" : "0";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "0");
}
void
@@ -601,9 +562,7 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = signers(account, 2, {{signer1, 1}, {signer2, 1}});
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "16");
}
void
@@ -624,9 +583,7 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = ticket::create(account, 2);
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "16");
}
void
@@ -649,9 +606,7 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = trust(account, USD(1000));
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "16");
}
void
@@ -675,9 +630,7 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = uritoken::burn(issuer, hexid);
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "16");
}
void
@@ -702,9 +655,7 @@ class BaseFee_test : public beast::unit_test::suite
tx[jss::Amount] = "1000000";
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "16");
}
void
@@ -728,9 +679,7 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = uritoken::cancel(issuer, hexid);
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "16");
}
void
@@ -757,8 +706,7 @@ class BaseFee_test : public beast::unit_test::suite
tx[jss::Amount] = "1000000";
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
std::string const feeResult = "16";
testRPCCall(env, tx, feeResult);
}
@@ -781,9 +729,7 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = uritoken::mint(account, uri);
// verify hooks fee
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
testRPCCall(env, tx, "16");
}
void
@@ -827,7 +773,6 @@ public:
using namespace test::jtx;
auto const sa = supported_amendments();
testWithFeats(sa);
testWithFeats(sa - fixXahauV1);
}
};
+131 -187
View File
@@ -4422,216 +4422,160 @@ struct Escrow_test : public beast::unit_test::suite
auto const gw = Account{"gateway"};
auto const USD = gw["USD"];
for (bool const withXahauV1 : {true, false})
Env env{*this, features};
env.fund(XRP(10000), alice, bob, gw);
env.close();
env.trust(USD(1000000), alice);
env.trust(USD(1000000), bob);
env.close();
env(pay(gw, alice, USD(10000)));
env(pay(gw, bob, USD(10000)));
env.close();
// EscrowCancel - EscrowID
{
auto const amend = withXahauV1 ? features : features - fixXahauV1;
Env env{*this, amend};
env.fund(XRP(10000), alice, bob, gw);
env.close();
env.trust(USD(1000000), alice);
env.trust(USD(1000000), bob);
env.close();
env(pay(gw, alice, USD(10000)));
env(pay(gw, bob, USD(10000)));
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
cancel_time(env.now() + 2s),
fee(1500));
env.close();
// EscrowCancel - EscrowID
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
cancel_time(env.now() + 2s),
fee(1500));
env.close();
// withXahauV1 - no OfferSequence
env(cancel(bob, alice),
escrow_id(escrowId),
fee(1500),
ter(tesSUCCESS));
env.close();
if (withXahauV1)
{
// withXahauV1 - no OfferSequence
env(cancel(bob, alice),
escrow_id(escrowId),
fee(1500),
ter(tesSUCCESS));
env.close();
}
else
{
// !withXahauV1 - OfferSequence == 0
env(cancel(bob, alice, 0),
escrow_id(escrowId),
fee(1500),
ter(tesSUCCESS));
env.close();
}
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(!escrowLE);
}
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(!escrowLE);
}
// EscrowCancel - no EscrowID or OfferSequence
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
cancel_time(env.now() + 2s),
fee(1500));
env.close();
// EscrowCancel - no EscrowID or OfferSequence
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
cancel_time(env.now() + 2s),
fee(1500));
env.close();
env(cancel(bob, alice), fee(1500), ter(temMALFORMED));
env.close();
env(cancel(bob, alice), fee(1500), ter(temMALFORMED));
env.close();
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(escrowLE);
}
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(escrowLE);
}
// EscrowCancel - EscrowID & OfferSequence
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
auto const seq = env.seq(alice);
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
cancel_time(env.now() + 2s),
fee(1500));
env.close();
// EscrowCancel - EscrowID & OfferSequence
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
auto const seq = env.seq(alice);
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
cancel_time(env.now() + 2s),
fee(1500));
env.close();
env(cancel(bob, alice, seq),
escrow_id(escrowId),
fee(1500),
ter(temMALFORMED));
env.close();
env(cancel(bob, alice, seq),
escrow_id(escrowId),
fee(1500),
ter(temMALFORMED));
env.close();
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(escrowLE);
}
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(escrowLE);
}
// EscrowCancel - EscrowID & OfferSequence 0
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
cancel_time(env.now() + 2s),
fee(1500));
env.close();
// EscrowCancel - EscrowID & OfferSequence 0
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
cancel_time(env.now() + 2s),
fee(1500));
env.close();
// withXahauV1 - OfferSequence 0 == temMALFORMED
env(cancel(bob, alice, 0),
escrow_id(escrowId),
fee(1500),
ter(temMALFORMED));
env.close();
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(escrowLE);
}
if (withXahauV1)
{
// withXahauV1 - OfferSequence 0 == temMALFORMED
env(cancel(bob, alice, 0),
escrow_id(escrowId),
fee(1500),
ter(temMALFORMED));
env.close();
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(escrowLE);
}
else
{
// withXahauV1 - OfferSequence 0 == tesSUCCESS
env(cancel(bob, alice, 0),
escrow_id(escrowId),
fee(1500),
ter(tesSUCCESS));
env.close();
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(!escrowLE);
}
}
// EscrowFinish - EscrowID
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
fee(1500));
env.close(5s);
// EscrowFinish - EscrowID
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
fee(1500));
env.close(5s);
// withXahauV1 - no OfferSequence
env(finish(bob, alice),
escrow_id(escrowId),
fee(1500),
ter(tesSUCCESS));
env.close();
if (withXahauV1)
{
// withXahauV1 - no OfferSequence
env(finish(bob, alice),
escrow_id(escrowId),
fee(1500),
ter(tesSUCCESS));
env.close();
}
else
{
// !withXahauV1 - OfferSequence == 0
env(finish(bob, alice, 0),
escrow_id(escrowId),
fee(1500),
ter(tesSUCCESS));
env.close();
}
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(!escrowLE);
}
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(!escrowLE);
}
// EscrowFinish - no EscrowID or OfferSequence
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
fee(1500));
env.close();
// EscrowFinish - no EscrowID or OfferSequence
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
fee(1500));
env.close();
env(finish(bob, alice), fee(1500), ter(temMALFORMED));
env.close();
env(finish(bob, alice), fee(1500), ter(temMALFORMED));
env.close();
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(escrowLE);
}
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(escrowLE);
}
// EscrowFinish- EscrowID & OfferSequence
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
auto const seq = env.seq(alice);
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
fee(1500));
env.close(5s);
// EscrowFinish- EscrowID & OfferSequence
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
auto const seq = env.seq(alice);
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
fee(1500));
env.close(5s);
env(finish(bob, alice, seq),
escrow_id(escrowId),
fee(1500),
ter(temMALFORMED));
env.close();
env(finish(bob, alice, seq),
escrow_id(escrowId),
fee(1500),
ter(temMALFORMED));
env.close();
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(escrowLE);
}
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(escrowLE);
}
// EscrowFinish- EscrowID & OfferSequence 0
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
fee(1500));
env.close(5s);
// EscrowFinish- EscrowID & OfferSequence 0
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
fee(1500));
env.close(5s);
if (withXahauV1)
{
// withXahauV1 - OfferSequence 0 == temMALFORMED
env(finish(bob, alice, 0),
escrow_id(escrowId),
fee(1500),
ter(temMALFORMED));
env.close();
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(escrowLE);
}
else
{
// !withXahauV1 - OfferSequence 0 == tesSUCCESS
env(finish(bob, alice, 0),
escrow_id(escrowId),
fee(1500),
ter(tesSUCCESS));
env.close();
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(!escrowLE);
}
}
// withXahauV1 - OfferSequence 0 == temMALFORMED
env(finish(bob, alice, 0),
escrow_id(escrowId),
fee(1500),
ter(temMALFORMED));
env.close();
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(escrowLE);
}
}
+14 -32
View File
@@ -336,10 +336,7 @@ struct GenesisMint_test : public beast::unit_test::suite
env.close();
// validate emitted txn
auto const txResult = env.current()->rules().enabled(fixXahauV1)
? "tesSUCCESS"
: "tecINTERNAL";
validateEmittedTxn(env, txResult, __LINE__);
validateEmittedTxn(env, "tesSUCCESS", __LINE__);
}
// missing an amount
@@ -569,39 +566,25 @@ struct GenesisMint_test : public beast::unit_test::suite
validateEmittedTxn(env, "tesSUCCESS", __LINE__);
}
auto const amtResult = env.current()->rules().enabled(fixXahauV1)
? 30000000ULL
: 10000000ULL;
// try to include the same destination twice
{
auto const txResult = env.current()->rules().enabled(fixXahauV1)
? ter(tesSUCCESS)
: ter(tecHOOK_REJECTED);
env(invoke::invoke(
invoker,
env.master,
genesis::makeBlob({
{greg.id(),
XRP(10).value(),
std::nullopt,
std::nullopt},
{greg.id(),
XRP(10).value(),
std::nullopt,
std::nullopt},
})),
fee(XRP(1)),
txResult);
env.close();
env.close();
}
env(invoke::invoke(
invoker,
env.master,
genesis::makeBlob({
{greg.id(), XRP(10).value(), std::nullopt, std::nullopt},
{greg.id(), XRP(10).value(), std::nullopt, std::nullopt},
})),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
env.close();
// check
{
auto const le = env.le(keylet::account(greg.id()));
BEAST_EXPECT(
!!le &&
le->getFieldAmount(sfBalance).xrp().drops() == amtResult);
le->getFieldAmount(sfBalance).xrp().drops() == 30000000ULL);
}
// trip the supply cap invariant
@@ -626,7 +609,7 @@ struct GenesisMint_test : public beast::unit_test::suite
auto const le = env.le(keylet::account(greg.id()));
BEAST_EXPECT(
!!le &&
le->getFieldAmount(sfBalance).xrp().drops() == amtResult);
le->getFieldAmount(sfBalance).xrp().drops() == 30000000ULL);
auto const postCoins = env.current()->info().drops;
auto const txnFee =
@@ -698,7 +681,6 @@ public:
using namespace test::jtx;
auto const sa = supported_amendments();
testWithFeats(sa);
testWithFeats(sa - fixXahauV1);
testWithFeats(sa - fixHookAPI20251128);
}
};
+8 -22
View File
@@ -1663,11 +1663,9 @@ class Import_test : public beast::unit_test::suite
// temMALFORMED - Import: xpop did not contain an 80% quorum for the txn
// it purports to prove.
for (bool const withXahauV1 : {true, false})
{
auto const amend = withXahauV1 ? features : features - fixXahauV1;
test::jtx::Env env{
*this, network::makeNetworkVLConfig(21337, keys), amend};
*this, network::makeNetworkVLConfig(21337, keys), features};
auto const alice = Account("alice");
env.fund(XRP(1000), alice);
@@ -1679,16 +1677,10 @@ class Import_test : public beast::unit_test::suite
"";
valData["n9MTTavZe6EPqqyQ27pJbNWpfHw8ZNspVgznrwx5HWm7cdTjKQie"] =
"";
if (withXahauV1)
{
valData
["n94J5LCRu9bBWrJiwRWujvuVECVPWbXFcQ2VN38qLD378F5pDSDM"] =
"";
}
valData["n94J5LCRu9bBWrJiwRWujvuVECVPWbXFcQ2VN38qLD378F5pDSDM"] =
"";
tmpXpop[jss::validation][jss::data] = valData;
auto const txResult =
withXahauV1 ? ter(temMALFORMED) : ter(temMALFORMED);
env(import::import(alice, tmpXpop), txResult);
env(import::import(alice, tmpXpop), ter(temMALFORMED));
}
test::jtx::Env env{*this, network::makeNetworkVLConfig(21337, keys)};
@@ -5238,7 +5230,6 @@ class Import_test : public beast::unit_test::suite
*this, network::makeNetworkVLConfig(21337, keys)};
auto const feeDrops = env.current()->fees().base;
bool const fixV2 = env.current()->rules().enabled(fixXahauV2);
// confirm total coins header
auto const initCoins = env.current()->info().drops;
@@ -5288,18 +5279,17 @@ class Import_test : public beast::unit_test::suite
auto const preAlice = env.balance(alice);
auto const preCoins = env.current()->info().drops;
auto const result = fixV2 ? ter(tesSUCCESS) : ter(tecHOOK_REJECTED);
env(import::import(
alice, import::loadXpop(ImportTCAccountSet::w_seed)),
import::issuer(issuer),
fee(1'000'000),
result);
ter(tesSUCCESS));
env.close();
// fixXahauV2
bool const zeroBurn =
env.current()->rules().enabled(featureZeroB2M);
auto const mintXAH = fixV2 ? XRP(zeroBurn ? 0 : 1000) : XRP(0);
auto const mintXAH = XRP(zeroBurn ? 0 : 1000);
// confirm fee was burned mint / no mint
auto const postAlice = env.balance(alice);
BEAST_EXPECT(postAlice == preAlice - XRP(1) + mintXAH);
@@ -5309,17 +5299,14 @@ class Import_test : public beast::unit_test::suite
BEAST_EXPECT(postCoins == preCoins - XRP(1) + mintXAH);
// resubmit import without issuer
auto const result2 =
fixV2 ? ter(tefPAST_IMPORT_SEQ) : ter(tesSUCCESS);
env(import::import(
alice, import::loadXpop(ImportTCAccountSet::w_seed)),
fee(feeDrops * 10),
result2);
ter(tefPAST_IMPORT_SEQ));
env.close();
// total burn
auto const totalBurn =
fixV2 ? XRP(0) : XRP(zeroBurn ? 0 : 1000) - (feeDrops * 10);
auto const totalBurn = XRP(0);
// confirm fee was minted / not minted
auto const postAlice2 = env.balance(alice);
@@ -6276,7 +6263,6 @@ public:
{
using namespace test::jtx;
FeatureBitset const all{supported_amendments()};
testWithFeats(all - fixXahauV2);
testWithFeats(all - featureZeroB2M);
testWithFeats(all);
}
+12 -42
View File
@@ -5105,10 +5105,8 @@ public:
using namespace jtx;
// OfferCreate
for (bool const withXahauV1 : {true, false})
{
auto const amend = withXahauV1 ? features : features - fixXahauV1;
Env env{*this, amend};
Env env{*this, features};
auto const gw = Account{"gateway"};
auto const alice = Account{"alice"};
auto const USD = gw["USD"];
@@ -5192,10 +5190,8 @@ public:
}
// OfferCancel
for (bool const withXahauV1 : {true, false})
{
auto const amend = withXahauV1 ? features : features - fixXahauV1;
Env env{*this, amend};
Env env{*this, features};
auto const gw = Account{"gateway"};
auto const alice = Account{"alice"};
auto const USD = gw["USD"];
@@ -5227,24 +5223,10 @@ public:
env(offer(alice, XRP(50), USD(50)));
env.close();
if (withXahauV1)
{
env(offer_cancel(alice),
offer_id(offerId),
ter(tesSUCCESS));
env.close();
auto const offerLE = env.le(keylet::unchecked(offerId));
BEAST_EXPECT(!offerLE);
}
else
{
env(offer_cancel(alice),
offer_id(offerId),
ter(temBAD_SEQUENCE));
env.close();
auto const offerLE = env.le(keylet::unchecked(offerId));
BEAST_EXPECT(offerLE);
}
env(offer_cancel(alice), offer_id(offerId), ter(tesSUCCESS));
env.close();
auto const offerLE = env.le(keylet::unchecked(offerId));
BEAST_EXPECT(!offerLE);
}
// no offer id or offer sequence
@@ -5266,24 +5248,12 @@ public:
env(offer(alice, XRP(50), USD(50)));
env.close();
if (withXahauV1)
{
env(offer_cancel(alice, offerSeqId),
offer_id(offerId),
ter(temBAD_SEQUENCE));
env.close();
auto const offerLE = env.le(keylet::unchecked(offerId));
BEAST_EXPECT(offerLE);
}
else
{
env(offer_cancel(alice, offerSeqId),
offer_id(offerId),
ter(tesSUCCESS));
env.close();
auto const offerLE = env.le(keylet::unchecked(offerId));
BEAST_EXPECT(!offerLE);
}
env(offer_cancel(alice, offerSeqId),
offer_id(offerId),
ter(temBAD_SEQUENCE));
env.close();
auto const offerLE = env.le(keylet::unchecked(offerId));
BEAST_EXPECT(offerLE);
}
// both offer id and offer sequence 0
+209 -175
View File
@@ -30,7 +30,10 @@
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/hook/Enum.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/PayChan.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Sign.h>
#include <xrpl/protocol/jss.h>
#include <vector>
@@ -737,15 +740,7 @@ private:
{
auto const executions = meta[sfHookExecutions.jsonName];
auto const execution = executions[0u][sfHookExecution.jsonName];
bool const fixV2 = env.current()->rules().enabled(fixXahauV2);
if (fixV2)
{
BEAST_EXPECT(execution[sfFlags.jsonName] == expected);
}
else
{
BEAST_REQUIRE(!execution[sfFlags.jsonName]);
}
BEAST_EXPECT(execution[sfFlags.jsonName] == expected);
}
void
@@ -2266,16 +2261,10 @@ private:
setTSHHook(env, account, testStrong);
// cancel escrow
Json::Value tx;
if (!env.current()->rules().enabled(fixXahauV1))
{
tx = cancel(account, account, 0);
}
else
{
tx = cancel(account, account);
}
env(tx, escrow_id(escrowId), fee(XRP(1)), ter(tesSUCCESS));
env(cancel(account, account),
escrow_id(escrowId),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
// verify tsh hook triggered
@@ -2320,16 +2309,10 @@ private:
setTSHHook(env, dest, testStrong);
// cancel escrow
Json::Value tx;
if (!env.current()->rules().enabled(fixXahauV1))
{
tx = cancel(account, account, 0);
}
else
{
tx = cancel(account, account);
}
env(tx, escrow_id(escrowId), fee(XRP(1)), ter(tesSUCCESS));
env(cancel(account, account),
escrow_id(escrowId),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
// verify tsh hook triggered
@@ -2378,16 +2361,10 @@ private:
setTSHHook(env, dest, testStrong);
// cancel escrow
Json::Value tx;
if (!env.current()->rules().enabled(fixXahauV1))
{
tx = cancel(dest, account, 0);
}
else
{
tx = cancel(dest, account);
}
env(tx, escrow_id(escrowId), fee(XRP(1)), ter(tesSUCCESS));
env(cancel(dest, account),
escrow_id(escrowId),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
// verify tsh hook triggered
@@ -2429,23 +2406,14 @@ private:
setTSHHook(env, account, testStrong);
// cancel escrow
bool const fixV1 = env.current()->rules().enabled(fixXahauV1);
Json::Value tx;
if (!fixV1)
{
tx = cancel(dest, account, 0);
}
else
{
tx = cancel(dest, account);
}
env(tx, escrow_id(escrowId), fee(XRP(1)), ter(tesSUCCESS));
env(cancel(dest, account),
escrow_id(escrowId),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
// verify tsh hook triggered
auto const expected =
(fixV1 ? (testStrong ? tshSTRONG : tshSTRONG)
: (testStrong ? tshNONE : tshNONE));
auto const expected = testStrong ? tshSTRONG : tshSTRONG;
testTSHStrongWeak(env, expected, __LINE__);
}
@@ -2494,16 +2462,10 @@ private:
setTSHHook(env, gw, testStrong);
// cancel escrow
Json::Value tx;
if (!env.current()->rules().enabled(fixXahauV1))
{
tx = cancel(account, account, 0);
}
else
{
tx = cancel(account, account);
}
env(tx, escrow_id(escrowId), fee(XRP(1)), ter(tesSUCCESS));
env(cancel(account, account),
escrow_id(escrowId),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
// verify tsh hook triggered
@@ -2919,16 +2881,10 @@ private:
setTSHHook(env, account, testStrong);
// finish escrow
Json::Value tx;
if (!env.current()->rules().enabled(fixXahauV1))
{
tx = finish(account, account, 0);
}
else
{
tx = finish(account, account);
}
env(tx, escrow_id(escrowId), fee(XRP(1)), ter(tesSUCCESS));
env(finish(account, account),
escrow_id(escrowId),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
// verify tsh hook triggered
@@ -2967,23 +2923,14 @@ private:
setTSHHook(env, dest, testStrong);
// finish escrow
bool const fixV1 = env.current()->rules().enabled(fixXahauV1);
Json::Value tx;
if (!fixV1)
{
tx = finish(account, account, 0);
}
else
{
tx = finish(account, account);
}
env(tx, escrow_id(escrowId), fee(XRP(1)), ter(tesSUCCESS));
env(finish(account, account),
escrow_id(escrowId),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
// verify tsh hook triggered
auto const expected =
(fixV1 ? (testStrong ? tshSTRONG : tshSTRONG)
: (testStrong ? tshNONE : tshNONE));
auto const expected = testStrong ? tshSTRONG : tshSTRONG;
testTSHStrongWeak(env, expected, __LINE__);
}
@@ -3019,16 +2966,10 @@ private:
setTSHHook(env, dest, testStrong);
// finish escrow
Json::Value tx;
if (!env.current()->rules().enabled(fixXahauV1))
{
tx = finish(dest, account, 0);
}
else
{
tx = finish(dest, account);
}
env(tx, escrow_id(escrowId), fee(XRP(1)), ter(tesSUCCESS));
env(finish(dest, account),
escrow_id(escrowId),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
// verify tsh hook triggered
@@ -3067,23 +3008,14 @@ private:
setTSHHook(env, account, testStrong);
// finish escrow
bool const fixV1 = env.current()->rules().enabled(fixXahauV1);
Json::Value tx;
if (!fixV1)
{
tx = finish(dest, account, 0);
}
else
{
tx = finish(dest, account);
}
env(tx, escrow_id(escrowId), fee(XRP(1)), ter(tesSUCCESS));
env(finish(dest, account),
escrow_id(escrowId),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
// verify tsh hook triggered
auto const expected =
(fixV1 ? (testStrong ? tshSTRONG : tshSTRONG)
: (testStrong ? tshNONE : tshNONE));
auto const expected = testStrong ? tshSTRONG : tshSTRONG;
testTSHStrongWeak(env, expected, __LINE__);
}
@@ -3129,17 +3061,10 @@ private:
setTSHHook(env, gw, testStrong);
// finish escrow
bool const fixV1 = env.current()->rules().enabled(fixXahauV1);
Json::Value tx;
if (!fixV1)
{
tx = finish(dest, account, 0);
}
else
{
tx = finish(dest, account);
}
env(tx, escrow_id(escrowId), fee(XRP(1)), ter(tesSUCCESS));
env(finish(dest, account),
escrow_id(escrowId),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
// verify tsh hook triggered
@@ -3445,10 +3370,7 @@ private:
env.close();
// verify tsh hook triggered
bool const fixV2 = env.current()->rules().enabled(fixXahauV2);
auto const expected =
(fixV2 ? (testStrong ? tshNONE : tshWEAK)
: (testStrong ? tshSTRONG : tshSTRONG));
auto const expected = testStrong ? tshNONE : tshWEAK;
testTSHStrongWeak(env, expected, __LINE__);
}
}
@@ -6307,10 +6229,7 @@ private:
env.close();
// verify tsh hook triggered
bool const fixV2 = env.current()->rules().enabled(fixXahauV2);
auto const expected =
(fixV2 ? (testStrong ? tshSTRONG : tshSTRONG)
: (testStrong ? tshNONE : tshNONE));
auto const expected = testStrong ? tshSTRONG : tshSTRONG;
testTSHStrongWeak(env, expected, __LINE__);
}
@@ -6391,10 +6310,7 @@ private:
env.close();
// verify tsh hook triggered
bool const fixV2 = env.current()->rules().enabled(fixXahauV2);
auto const expected =
(fixV2 ? (testStrong ? tshSTRONG : tshSTRONG)
: (testStrong ? tshNONE : tshNONE));
auto const expected = testStrong ? tshSTRONG : tshSTRONG;
testTSHStrongWeak(env, expected, __LINE__);
}
}
@@ -6500,15 +6416,12 @@ private:
env.close();
// verify tsh hook triggered
bool const fixV1 = env.current()->rules().enabled(fixXahauV1);
bool const withIOUIssuerWeakTSH =
env.current()->rules().enabled(featureIOUIssuerWeakTSH);
auto const expected =
(fixV1
? (testStrong ? tshNONE
: (withIOUIssuerWeakTSH ? tshWEAK : tshNONE))
: (testStrong ? tshSTRONG : tshSTRONG));
auto const expected = testStrong
? tshNONE
: (withIOUIssuerWeakTSH ? tshWEAK : tshNONE);
testTSHStrongWeak(env, expected, __LINE__);
}
@@ -6607,14 +6520,11 @@ private:
env.close();
// verify tsh hook triggered
bool const fixV1 = env.current()->rules().enabled(fixXahauV1);
bool const withIOUIssuerWeakTSH =
env.current()->rules().enabled(featureIOUIssuerWeakTSH);
auto const expected =
(fixV1
? (testStrong ? tshNONE
: (withIOUIssuerWeakTSH ? tshWEAK : tshNONE))
: (testStrong ? tshSTRONG : tshSTRONG));
auto const expected = testStrong
? tshNONE
: (withIOUIssuerWeakTSH ? tshWEAK : tshNONE);
testTSHStrongWeak(env, expected, __LINE__);
}
@@ -6664,15 +6574,12 @@ private:
env.close();
// verify tsh hook triggered
bool const fixV1 = env.current()->rules().enabled(fixXahauV1);
bool const withIOUIssuerWeakTSH =
env.current()->rules().enabled(featureIOUIssuerWeakTSH);
auto const expected =
(fixV1
? (testStrong ? tshNONE
: (withIOUIssuerWeakTSH ? tshWEAK : tshNONE))
: (testStrong ? tshSTRONG : tshSTRONG));
auto const expected = testStrong
? tshNONE
: (withIOUIssuerWeakTSH ? tshWEAK : tshNONE);
testTSHStrongWeak(env, expected, __LINE__);
}
@@ -7732,7 +7639,6 @@ private:
env.close();
auto const preDest = env.balance(dest);
bool const withFix = env.current()->rules().enabled(fixXahauV2);
env.app().openLedger().modify([&](OpenView& view, beast::Journal j) {
auto const tx =
@@ -7740,24 +7646,15 @@ private:
auto result =
ripple::apply(env.app(), view, *tx, tapNONE, env.journal);
bool const applyResult = withFix ? false : true;
if (withFix)
{
BEAST_EXPECT(result.ter == tefNONDIR_EMIT);
}
else
{
BEAST_EXPECT(result.ter == tesSUCCESS);
}
BEAST_EXPECT(result.applied == applyResult);
BEAST_EXPECT(result.ter == tefNONDIR_EMIT);
BEAST_EXPECT(!result.applied);
return result.applied;
});
env.close();
auto const postDest = env.balance(dest);
auto const postValue = withFix ? XRP(0) : XRP(1);
BEAST_EXPECT(postDest == preDest + postValue);
BEAST_EXPECT(postDest == preDest);
for (size_t i = 0; i < 4; i++)
{
@@ -7768,8 +7665,7 @@ private:
}
auto const postDest1 = env.balance(dest);
auto const postValue1 = withFix ? XRP(0) : XRP(2);
BEAST_EXPECT(postDest1 == postDest + postValue1);
BEAST_EXPECT(postDest1 == postDest);
}
void
@@ -8445,6 +8341,150 @@ private:
}
}
// Builds a manifest signed by `master`, nominating `ephemeral` as the
// signing key. A sequence of UINT32_MAX makes it a revocation, which by
// definition carries no signing key.
static std::string
makeManifestString(
jtx::Account const& master,
jtx::Account const& ephemeral,
std::uint32_t seq)
{
STObject st(sfGeneric);
st[sfSequence] = seq;
st[sfPublicKey] = master.pk();
if (seq != std::numeric_limits<std::uint32_t>::max())
{
st[sfSigningPubKey] = ephemeral.pk();
sign(
st,
HashPrefix::manifest,
*publicKeyType(ephemeral.pk()),
ephemeral.sk());
}
sign(
st,
HashPrefix::manifest,
*publicKeyType(master.pk()),
master.sk(),
sfMasterSignature);
Serializer s;
st.add(s);
return std::string(static_cast<char const*>(s.data()), s.size());
}
// A manifest transaction carries no account signature, so it cannot be
// submitted through env() the way a signed transaction can. Returns the
// resulting transaction id so the caller can inspect its metadata.
uint256
submitManifest(jtx::Env& env, std::string const& manifest)
{
Json::Value params;
params[jss::manifest] = strHex(manifest);
auto const jrr = env.rpc("json", "submit", to_string(params));
auto const& result = jrr[jss::result];
if (!BEAST_EXPECT(
result[jss::engine_result].asString() == "tesSUCCESS"))
{
log << "submitManifest: " << to_string(jrr) << std::endl;
return beast::zero;
}
// An error response carries no tx_json, and strUnHex("") yields an
// engaged but empty Blob, so fromVoid() would memcpy from nullptr.
auto const blob = strUnHex(result[jss::tx_json][jss::hash].asString());
auto const hash =
blob ? uint256::fromVoidChecked(*blob) : std::optional<uint256>{};
if (!BEAST_EXPECT(hash.has_value()))
{
log << "submitManifest: " << to_string(jrr) << std::endl;
return beast::zero;
}
return *hash;
}
// SetManifest
// | otxn | tsh | manifest |
// | M | M | N/A |
// | M | E | W | ephemeral key's logical account
void
testSetManifestTSH(FeatureBitset features)
{
using namespace test::jtx;
using namespace std::literals;
testcase("set manifest TSH");
if (!features[featureOnChainManifests])
return;
// otxn: master
// tsh: ephemeral
// w/s: weak
//
// The ephemeral account is only named by the manifest, so it may
// observe the transaction but not rollback it. It therefore fires only
// when it has asked to collect.
for (bool const testStrong : {true, false})
{
test::jtx::Env env{
*this,
network::makeNetworkConfig(21337, "10", "1000000", "200000"),
features};
auto const master = Account("master", KeyType::ed25519);
auto const ephemeral = Account("ephemeral", KeyType::ed25519);
env.fund(XRP(1000), master, ephemeral);
env.close();
if (!testStrong)
addWeakTSH(env, ephemeral);
setTSHHook(env, ephemeral, testStrong);
auto const txHash =
submitManifest(env, makeManifestString(master, ephemeral, 1));
env.close();
// A strong hook on a weak stake holder is never reached.
auto const expected = testStrong ? tshNONE : tshWEAK;
testTSHStrongWeak(env, txHash, expected, __LINE__);
}
// A revocation names no signing key, so there is no ephemeral stake
// holder to notify at all.
{
test::jtx::Env env{
*this,
network::makeNetworkConfig(21337, "10", "1000000", "200000"),
features};
auto const master = Account("master", KeyType::ed25519);
auto const ephemeral = Account("ephemeral", KeyType::ed25519);
env.fund(XRP(1000), master, ephemeral);
env.close();
addWeakTSH(env, ephemeral);
setTSHHook(env, ephemeral, false);
auto const txHash = submitManifest(
env,
makeManifestString(
master,
ephemeral,
std::numeric_limits<std::uint32_t>::max()));
env.close();
testTSHStrongWeak(env, txHash, tshNONE, __LINE__);
}
}
void
testTSH(FeatureBitset features)
{
@@ -8474,10 +8514,8 @@ public:
using namespace test::jtx;
static FeatureBitset const all{supported_amendments()};
static std::array<FeatureBitset, 4> const feats{
static std::array<FeatureBitset, 2> const feats{
all,
all - fixXahauV1 - fixXahauV2 - featureIOUIssuerWeakTSH,
all - fixXahauV2 - featureIOUIssuerWeakTSH,
all - featureIOUIssuerWeakTSH,
};
@@ -8507,14 +8545,10 @@ public:
} \
};
SETHOOKTSH_TEST(1, false)
SETHOOKTSH_TEST(2, false)
SETHOOKTSH_TEST(3, true)
SETHOOKTSH_TEST(1, true)
BEAST_DEFINE_TESTSUITE_PRIO(SetHookTSH0, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHookTSH1, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHookTSH2, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHookTSH3, app, ripple, 2);
} // namespace test
} // namespace ripple
+19 -49
View File
@@ -294,9 +294,7 @@ public:
iv[jss::CreateCode] = "";
jv[jss::Hooks][0U][jss::Hook] = iv;
bool const fixV1 = env.current()->rules().enabled(fixXahauV1);
auto const txResult = fixV1 ? ter(tesSUCCESS) : ter(tefBAD_LEDGER);
env(jv, HSFEE, txResult);
env(jv, HSFEE, ter(tesSUCCESS));
env.close();
}
@@ -3703,8 +3701,6 @@ public:
env(invoke, M("test emit"), fee(XRP(1)));
bool const fixV2 = env.current()->rules().enabled(fixXahauV2);
std::optional<uint256> emithash;
{
auto meta = env.meta(); // meta can close
@@ -3714,9 +3710,7 @@ public:
BEAST_REQUIRE(meta->isFieldPresent(sfHookExecutions));
auto const hookEmissions = meta->getFieldArray(sfHookEmissions);
BEAST_EXPECT(
hookEmissions[0u].isFieldPresent(sfEmitNonce) == fixV2 ? true
: false);
BEAST_EXPECT(hookEmissions[0u].isFieldPresent(sfEmitNonce));
BEAST_EXPECT(
hookEmissions[0u].getAccountID(sfHookAccount) == alice.id());
@@ -3811,8 +3805,7 @@ public:
BEAST_EXPECT(hookExecutions[0].getFieldU8(sfHookResult) == 3);
BEAST_EXPECT(
hookExecutions[0].getFieldU16(sfHookEmitCount) == 2);
if (fixV2)
BEAST_EXPECT(hookExecutions[0].getFieldU32(sfFlags) == 2);
BEAST_EXPECT(hookExecutions[0].getFieldU32(sfFlags) == 2);
}
env.close();
burden_expected *= 2U;
@@ -3836,8 +3829,7 @@ public:
BEAST_EXPECT(
hookExecutions[0].getFieldU64(sfHookReturnCode) ==
283); // emission failure on first emit
if (fixV2)
BEAST_EXPECT(hookExecutions[0].getFieldU32(sfFlags) == 2);
BEAST_EXPECT(hookExecutions[0].getFieldU32(sfFlags) == 2);
}
BEAST_EXPECT(txcount == 256);
}
@@ -4094,8 +4086,6 @@ public:
env(invoke, M("test emit"), fee(XRP(1)));
bool const fixV2 = env.current()->rules().enabled(fixXahauV2);
std::optional<uint256> emithash;
{
auto meta = env.meta(); // meta can close
@@ -4105,9 +4095,7 @@ public:
BEAST_REQUIRE(meta->isFieldPresent(sfHookExecutions));
auto const hookEmissions = meta->getFieldArray(sfHookEmissions);
BEAST_EXPECT(
hookEmissions[0u].isFieldPresent(sfEmitNonce) == fixV2 ? true
: false);
BEAST_EXPECT(hookEmissions[0u].isFieldPresent(sfEmitNonce));
BEAST_EXPECT(
hookEmissions[0u].getAccountID(sfHookAccount) == alice.id());
@@ -4202,8 +4190,7 @@ public:
BEAST_EXPECT(hookExecutions[0].getFieldU8(sfHookResult) == 3);
BEAST_EXPECT(
hookExecutions[0].getFieldU16(sfHookEmitCount) == 2);
if (fixV2)
BEAST_EXPECT(hookExecutions[0].getFieldU32(sfFlags) == 2);
BEAST_EXPECT(hookExecutions[0].getFieldU32(sfFlags) == 2);
}
env.close();
burden_expected *= 2U;
@@ -4227,8 +4214,7 @@ public:
BEAST_EXPECT(
hookExecutions[0].getFieldU64(sfHookReturnCode) ==
172); // emission failure on first emit
if (fixV2)
BEAST_EXPECT(hookExecutions[0].getFieldU32(sfFlags) == 2);
BEAST_EXPECT(hookExecutions[0].getFieldU32(sfFlags) == 2);
}
BEAST_EXPECT(txcount == 256);
}
@@ -6965,12 +6951,8 @@ public:
BEAST_REQUIRE(hookExecutions.size() == 2);
// get the data in the return code of the execution
bool const fixV2 = env.current()->rules().enabled(fixXahauV2);
if (fixV2)
{
BEAST_EXPECT(hookExecutions[0].getFieldU32(sfFlags) == 5);
BEAST_EXPECT(hookExecutions[1].getFieldU32(sfFlags) == 0);
}
BEAST_EXPECT(hookExecutions[0].getFieldU32(sfFlags) == 5);
BEAST_EXPECT(hookExecutions[1].getFieldU32(sfFlags) == 0);
BEAST_EXPECT(hookExecutions[0].getFieldU64(sfHookReturnCode) == 0);
BEAST_EXPECT(hookExecutions[1].getFieldU64(sfHookReturnCode) == 1);
@@ -10043,19 +10025,13 @@ public:
fee(XRP(1)));
}
// fixXahauV1
bool const fixV1 = env.current()->rules().enabled(fixXahauV1);
auto const txResult = fixV1 ? ter(tecHOOK_REJECTED) : ter(tesSUCCESS);
env(pay(bob, alice, XRP(1)),
M("test state_foreign_set_max"),
fee(XRP(1)),
ter(txResult));
ter(tecHOOK_REJECTED));
env.close();
// verify hook result
// TOO_MANY_NAMESPACES / -45
std::string const hookResult = fixV1 ? "800000000000002d" : "9";
Json::Value params;
params[jss::transaction] =
env.tx()->getJson(JsonOptions::none)[jss::hash];
@@ -10063,7 +10039,9 @@ public:
auto const meta = jrr[jss::result][jss::meta];
auto const executions = meta[sfHookExecutions.jsonName];
auto const execution = executions[0u][sfHookExecution.jsonName];
BEAST_EXPECT(execution[sfHookReturnCode.jsonName] == hookResult);
// TOO_MANY_NAMESPACES / -45
BEAST_EXPECT(
execution[sfHookReturnCode.jsonName] == "800000000000002d");
}
void
@@ -15202,16 +15180,12 @@ public:
using namespace test::jtx;
static FeatureBitset const all{supported_amendments()};
static std::array<FeatureBitset, 8> const feats{
static std::array<FeatureBitset, 6> const feats{
all,
all - fixXahauV2,
all - fixXahauV1 - fixXahauV2,
all - fixXahauV1 - fixXahauV2 - fixNSDelete,
all - fixXahauV1 - fixXahauV2 - fixNSDelete - fixPageCap,
all - fixXahauV1 - fixXahauV2 - fixNSDelete - fixPageCap -
featureHookCanEmit,
all - fixXahauV1 - fixXahauV2 - fixNSDelete - fixPageCap -
featureExtendedHookState,
all - fixNSDelete,
all - fixNSDelete - fixPageCap,
all - fixNSDelete - fixPageCap - featureHookCanEmit,
all - fixNSDelete - fixPageCap - featureExtendedHookState,
all - featureNamedHooks,
};
@@ -15472,9 +15446,7 @@ SETHOOK_TEST(1, false)
SETHOOK_TEST(2, false)
SETHOOK_TEST(3, false)
SETHOOK_TEST(4, false)
SETHOOK_TEST(5, false)
SETHOOK_TEST(6, false)
SETHOOK_TEST(7, true)
SETHOOK_TEST(5, true)
BEAST_DEFINE_TESTSUITE_PRIO(SetHook0, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook1, app, ripple, 2);
@@ -15482,8 +15454,6 @@ BEAST_DEFINE_TESTSUITE_PRIO(SetHook2, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook3, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook4, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook5, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook6, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook7, app, ripple, 2);
} // namespace test
} // namespace ripple
#undef M
-101
View File
@@ -9,7 +9,6 @@
namespace ripple {
namespace test {
std::map<std::string, std::vector<uint8_t>> wasm = {
/* ==== WASM: 0 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -190,7 +189,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x3DU, 0x20U, 0x54U, 0x4FU, 0x4FU, 0x5FU, 0x42U, 0x49U, 0x47U, 0x00U,
}},
/* ==== WASM: 1 ==== */
{R"[test.hook](
(module
(type (;0;) (func (param i32 i32) (result i64)))
@@ -240,7 +238,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x0BU,
}},
/* ==== WASM: 2 ==== */
{R"[test.hook](
(module
(type (;0;) (func (param i32 i32) (result i64)))
@@ -294,7 +291,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x14U, 0x10U, 0x00U, 0x1AU, 0x0CU, 0x00U, 0x0BU, 0x0BU,
}},
/* ==== WASM: 3 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -361,7 +357,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x80U, 0x80U, 0x00U, 0x20U, 0x02U, 0x0BU,
}},
/* ==== WASM: 4 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -467,7 +462,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x6AU, 0x24U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x20U, 0x03U, 0x0BU,
}},
/* ==== WASM: 5 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -542,7 +536,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x6AU, 0x24U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x20U, 0x05U, 0x0BU,
}},
/* ==== WASM: 6 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t, uint32_t);
@@ -1159,7 +1152,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x78U, 0x29U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x33U, 0x32U, 0x00U,
}},
/* ==== WASM: 7 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t, uint32_t);
@@ -1588,7 +1580,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U,
}},
/* ==== WASM: 8 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -1794,7 +1785,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x3DU, 0x20U, 0x30U, 0x78U, 0x45U, 0x31U, 0x00U,
}},
/* ==== WASM: 9 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -1916,7 +1906,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x58U, 0x4EU, 0x00U,
}},
/* ==== WASM: 10 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -2049,7 +2038,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x4FU, 0x4EU, 0x43U, 0x45U, 0x53U, 0x00U,
}},
/* ==== WASM: 11 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -2129,7 +2117,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x54U, 0x00U,
}},
/* ==== WASM: 12 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -2174,7 +2161,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x30U, 0x00U,
}},
/* ==== WASM: 13 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -2493,7 +2479,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x80U, 0x80U, 0x80U, 0x00U, 0x0BU,
}},
/* ==== WASM: 14 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -3279,7 +3264,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x37U, 0x36U, 0x33U, 0x4CU, 0x4CU, 0x20U, 0x29U, 0x00U,
}},
/* ==== WASM: 15 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -3669,7 +3653,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x80U, 0x80U, 0x80U, 0x00U, 0x0BU,
}},
/* ==== WASM: 16 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -3850,7 +3833,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x38U, 0x4CU, 0x4CU, 0x29U, 0x00U,
}},
/* ==== WASM: 17 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -4039,7 +4021,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x29U, 0x00U,
}},
/* ==== WASM: 18 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -4278,7 +4259,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U, 0x42U, 0x00U, 0x10U, 0x85U, 0x80U, 0x80U, 0x80U, 0x00U, 0x0BU,
}},
/* ==== WASM: 19 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -4980,7 +4960,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x38U, 0x35U, 0x35U, 0x32U, 0x55U, 0x29U, 0x00U,
}},
/* ==== WASM: 20 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -6325,7 +6304,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x20U, 0x29U, 0x00U,
}},
/* ==== WASM: 21 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -6427,7 +6405,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x84U, 0x80U, 0x80U, 0x80U, 0x00U, 0x0BU,
}},
/* ==== WASM: 22 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -6470,7 +6447,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U, 0x0BU,
}},
/* ==== WASM: 23 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -6618,7 +6594,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x34U, 0x34U, 0x4CU, 0x4CU, 0x2CU, 0x20U, 0x33U, 0x29U, 0x00U,
}},
/* ==== WASM: 24 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -6946,7 +6921,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x38U, 0x34U, 0x39U, 0x30U, 0x4CU, 0x4CU, 0x00U,
}},
/* ==== WASM: 25 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -7151,7 +7125,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x10U, 0x85U, 0x80U, 0x80U, 0x80U, 0x00U, 0x0BU,
}},
/* ==== WASM: 26 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -7835,7 +7808,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x20U, 0x3DU, 0x3DU, 0x20U, 0x30U, 0x00U,
}},
/* ==== WASM: 27 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -8130,7 +8102,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x32U, 0x34U, 0x31U, 0x36U, 0x55U, 0x4CU, 0x4CU, 0x00U,
}},
/* ==== WASM: 28 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -8843,7 +8814,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x31U, 0x33U, 0x33U, 0x38U, 0x20U, 0x29U, 0x00U,
}},
/* ==== WASM: 29 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -8928,7 +8898,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x30U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x32U, 0x30U, 0x00U,
}},
/* ==== WASM: 30 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -8990,7 +8959,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x80U, 0x80U, 0x00U, 0x0BU,
}},
/* ==== WASM: 31 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -9067,7 +9035,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x31U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x33U, 0x32U, 0x00U,
}},
/* ==== WASM: 32 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -9144,7 +9111,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x31U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x33U, 0x32U, 0x00U,
}},
/* ==== WASM: 33 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -9401,7 +9367,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U, 0x00U,
}},
/* ==== WASM: 34 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -9559,7 +9524,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x04U, 0x00U, 0x00U,
}},
/* ==== WASM: 35 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -9912,7 +9876,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U, 0x2AU, 0x04U, 0x00U, 0x00U, 0x31U, 0x04U, 0x00U, 0x00U,
}},
/* ==== WASM: 36 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -9945,7 +9908,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x82U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x20U, 0x01U, 0x0BU,
}},
/* ==== WASM: 37 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -10172,7 +10134,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U,
}},
/* ==== WASM: 38 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -10203,7 +10164,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x80U, 0x80U, 0x00U, 0x1AU, 0x20U, 0x01U, 0x0BU,
}},
/* ==== WASM: 39 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -10514,7 +10474,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U,
}},
/* ==== WASM: 40 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -10591,7 +10550,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x33U, 0x32U, 0x00U,
}},
/* ==== WASM: 41 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -10625,7 +10583,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x80U, 0x80U, 0x00U, 0x1AU, 0x20U, 0x01U, 0x0BU,
}},
/* ==== WASM: 42 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -10712,7 +10669,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x32U, 0x00U,
}},
/* ==== WASM: 43 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -10746,7 +10702,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x0BU,
}},
/* ==== WASM: 44 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -10886,7 +10841,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x54U, 0x5FU, 0x4DU, 0x45U, 0x54U, 0x00U,
}},
/* ==== WASM: 45 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -11076,7 +11030,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x31U, 0x34U, 0x00U,
}},
/* ==== WASM: 46 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -11230,7 +11183,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U,
}},
/* ==== WASM: 47 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -11391,7 +11343,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U,
}},
/* ==== WASM: 48 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -11548,7 +11499,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x53U, 0x4CU, 0x4FU, 0x54U, 0x53U, 0x00U,
}},
/* ==== WASM: 49 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -11633,7 +11583,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x74U, 0x79U, 0x70U, 0x65U, 0x28U, 0x29U, 0x00U,
}},
/* ==== WASM: 50 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -11890,7 +11839,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U, 0x00U,
}},
/* ==== WASM: 51 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -12128,7 +12076,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x3EU, 0x20U, 0x30U, 0x00U,
}},
/* ==== WASM: 52 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -12224,7 +12171,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x53U, 0x54U, 0x00U,
}},
/* ==== WASM: 53 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -12334,7 +12280,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x20U, 0x31U, 0x00U,
}},
/* ==== WASM: 54 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -12464,7 +12409,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x30U, 0x30U, 0x30U, 0x4CU, 0x4CU, 0x00U,
}},
/* ==== WASM: 55 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -12738,7 +12682,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x30U, 0x00U,
}},
/* ==== WASM: 56 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -12875,7 +12818,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x2CU, 0x20U, 0x73U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x31U, 0x00U,
}},
/* ==== WASM: 57 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -13169,7 +13111,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x5FU, 0x53U, 0x4CU, 0x4FU, 0x54U, 0x53U, 0x00U,
}},
/* ==== WASM: 58 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -13403,7 +13344,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x5FU, 0x53U, 0x4CU, 0x4FU, 0x54U, 0x53U, 0x00U,
}},
/* ==== WASM: 59 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -13699,7 +13639,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x2CU, 0x20U, 0x31U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x30U, 0x00U,
}},
/* ==== WASM: 60 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -13903,7 +13842,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x6EU, 0x74U, 0x32U, 0x22U, 0x20U, 0x2BU, 0x20U, 0x69U, 0x29U, 0x00U,
}},
/* ==== WASM: 61 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -14020,7 +13958,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x69U, 0x29U, 0x00U,
}},
/* ==== WASM: 62 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -14129,7 +14066,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x6EU, 0x74U, 0x65U, 0x6EU, 0x74U, 0x32U, 0x22U, 0x29U, 0x00U,
}},
/* ==== WASM: 63 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -14402,7 +14338,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U,
}},
/* ==== WASM: 64 ==== */
{R"[test.hook](
#include <stdint.h>
#define sfInvoiceID ((5U << 16U) + 17U)
@@ -14621,7 +14556,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x30U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x33U, 0x32U, 0x00U,
}},
/* ==== WASM: 65 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -14733,7 +14667,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x58U, 0x49U, 0x53U, 0x54U, 0x00U,
}},
/* ==== WASM: 66 ==== */
{R"[test.hook](
#include <stdint.h>
#define sfInvoiceID ((5U << 16U) + 17U)
@@ -14869,7 +14802,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x3DU, 0x20U, 0x33U, 0x32U, 0x00U,
}},
/* ==== WASM: 67 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -15005,7 +14937,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x49U, 0x47U, 0x00U,
}},
/* ==== WASM: 68 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -15144,7 +15075,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x66U, 0x28U, 0x64U, 0x61U, 0x74U, 0x61U, 0x32U, 0x29U, 0x00U,
}},
/* ==== WASM: 69 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -15256,7 +15186,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x30U, 0x00U,
}},
/* ==== WASM: 70 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -15350,7 +15279,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x61U, 0x64U, 0x5BU, 0x69U, 0x5DU, 0x00U,
}},
/* ==== WASM: 71 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -15483,7 +15411,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x64U, 0x5BU, 0x69U, 0x5DU, 0x00U,
}},
/* ==== WASM: 72 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -15571,7 +15498,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x61U, 0x74U, 0x61U, 0x29U, 0x00U,
}},
/* ==== WASM: 73 ==== */
{R"[test.hook](
#include <stdint.h>
#define sfInvoiceID ((5U << 16U) + 17U)
@@ -15682,7 +15608,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x20U, 0x33U, 0x32U, 0x00U,
}},
/* ==== WASM: 74 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -15893,7 +15818,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U,
}},
/* ==== WASM: 75 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -16001,7 +15925,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x20U, 0x22U, 0x32U, 0x22U, 0x2CU, 0x20U, 0x31U, 0x29U, 0x00U,
}},
/* ==== WASM: 76 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -16613,7 +16536,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U,
}},
/* ==== WASM: 77 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -16800,7 +16722,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x63U, 0x65U, 0x29U, 0x20U, 0x3EU, 0x20U, 0x30U, 0x00U,
}},
/* ==== WASM: 78 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -17150,7 +17071,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x20U, 0x30U, 0x00U,
}},
/* ==== WASM: 79 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -17286,7 +17206,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U,
}},
/* ==== WASM: 80 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -17345,7 +17264,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x64U, 0xE1U, 0xF1U,
}},
/* ==== WASM: 81 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -17518,7 +17436,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x54U, 0x5FU, 0x45U, 0x58U, 0x49U, 0x53U, 0x54U, 0x00U,
}},
/* ==== WASM: 82 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -17666,7 +17583,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x30U, 0x00U, 0x22U, 0x00U, 0x00U, 0x00U, 0x00U,
}},
/* ==== WASM: 83 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -17820,7 +17736,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x0FU, 0x0BU, 0x02U, 0x56U, 0x00U,
}},
/* ==== WASM: 84 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -17917,7 +17832,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x3DU, 0x20U, 0x30U, 0x00U,
}},
/* ==== WASM: 85 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -17976,7 +17890,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x4FU, 0x46U, 0x5FU, 0x42U, 0x4FU, 0x55U, 0x4EU, 0x44U, 0x53U, 0x00U,
}},
/* ==== WASM: 86 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -18035,7 +17948,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x4EU, 0x44U, 0x53U, 0x00U,
}},
/* ==== WASM: 87 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -19864,7 +19776,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x53U, 0x4DU, 0x41U, 0x4CU, 0x4CU, 0x00U,
}},
/* ==== WASM: 88 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -21483,7 +21394,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x29U, 0x29U, 0x00U,
}},
/* ==== WASM: 89 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -24416,7 +24326,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x4FU, 0x4FU, 0x5FU, 0x53U, 0x4DU, 0x41U, 0x4CU, 0x4CU, 0x00U,
}},
/* ==== WASM: 90 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -26381,7 +26290,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x54U, 0x4FU, 0x4FU, 0x5FU, 0x53U, 0x4DU, 0x41U, 0x4CU, 0x4CU, 0x00U,
}},
/* ==== WASM: 91 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -26666,7 +26574,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x30U, 0x00U,
}},
/* ==== WASM: 92 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t, uint32_t);
@@ -27253,7 +27160,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x4EU, 0x5FU, 0x46U, 0x41U, 0x49U, 0x4CU, 0x55U, 0x52U, 0x45U, 0x00U,
}},
/* ==== WASM: 93 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -27282,7 +27188,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x0BU,
}},
/* ==== WASM: 94 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -27314,7 +27219,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x20U, 0x52U, 0x65U, 0x6AU, 0x65U, 0x63U, 0x74U, 0x65U, 0x64U, 0x00U,
}},
/* ==== WASM: 95 ==== */
{R"[test.hook](
(module
(type (;0;) (func (param i32 i32 i64) (result i64)))
@@ -27341,7 +27245,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x41U, 0x00U, 0x41U, 0x00U, 0x42U, 0x00U, 0x10U, 0x00U, 0x0BU,
}},
/* ==== WASM: 96 ==== */
{R"[test.hook](
(module
(type (;0;) (func (param i32 i32) (result i32)))
@@ -27394,7 +27297,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U, 0x1AU, 0x0BU,
}},
/* ==== WASM: 97 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -34037,7 +33939,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x39U, 0x30U, 0x31U, 0x32U, 0x33U, 0x00U,
}},
/* ==== WASM: 98 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -34083,7 +33984,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x0BU, 0x06U, 0x76U, 0x61U, 0x6CU, 0x75U, 0x65U, 0x00U,
}},
/* ==== WASM: 99 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -34112,7 +34012,6 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x0BU,
}},
/* ==== WASM: 100 ==== */
{R"[test.hook](
#include <stdint.h>
extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
+819
View File
@@ -0,0 +1,819 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx.h>
#include <test/jtx/network.h>
#include <xrpld/app/ledger/OpenLedger.h>
#include <xrpld/app/misc/Manifest.h>
#include <xrpld/app/tx/apply.h>
#include <xrpld/app/tx/detail/SetManifest.h>
#include <xrpld/core/Config.h>
#include <xrpld/ledger/OpenView.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Sign.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol/st.h>
#include <functional>
#include <limits>
#include <utility>
#include <vector>
namespace ripple {
namespace test {
/** Tests the OnChainManifests amendment: the SetManifest transactor, the
submit RPC path that builds its transaction, and reading manifests back out
of the ledger into the manifest cache.
*/
struct SetManifest_test : public beast::unit_test::suite
{
// A manifest transaction is unsigned, so the network id is mandatory and
// the network must be one that requires it (id > 1024).
static std::unique_ptr<Config>
makeConfig()
{
return jtx::network::makeNetworkConfig(
21337, "10", "1000000", "200000");
}
/** Builds a manifest signed by `master` nominating `ephemeral`.
A sequence of UINT32_MAX makes it a revocation, which by definition
names no signing key and carries no ephemeral signature.
*/
static std::string
makeManifest(
jtx::Account const& master,
jtx::Account const& ephemeral,
std::uint32_t seq)
{
STObject st(sfGeneric);
st[sfSequence] = seq;
st[sfPublicKey] = master.pk();
if (seq != std::numeric_limits<std::uint32_t>::max())
{
st[sfSigningPubKey] = ephemeral.pk();
sign(
st,
HashPrefix::manifest,
*publicKeyType(ephemeral.pk()),
ephemeral.sk());
}
sign(
st,
HashPrefix::manifest,
*publicKeyType(master.pk()),
master.sk(),
sfMasterSignature);
Serializer s;
st.add(s);
return std::string(static_cast<char const*>(s.data()), s.size());
}
/** Submits a manifest through the submit RPC.
The transaction is unsigned -- authority comes from the manifest's own
master and ephemeral signatures -- so it cannot be submitted through
env() the way a signed transaction can.
*/
static Json::Value
submit(jtx::Env& env, std::string const& manifest)
{
Json::Value params;
params[jss::manifest] = strHex(manifest);
return env.rpc("json", "submit", to_string(params))[jss::result];
}
static std::string
engineResult(Json::Value const& result)
{
return result[jss::engine_result].asString();
}
/** A well formed STObject that is not a well formed manifest.
The manifest format requires sfPublicKey and sfMasterSignature, so this
parses as an object and then fails applyTemplate.
*/
static std::string
makeUnparseableManifest()
{
STObject st(sfGeneric);
st[sfSequence] = 1;
Serializer s;
st.add(s);
return std::string(static_cast<char const*>(s.data()), s.size());
}
/** The envelope Submit would build for `manifest`, optionally tweaked.
makeSetManifestTx() rejects any manifest the transactor would reject,
and checkValidity() rejects any envelope whose signatures do not check
out, so several of the transactor's own rejections cannot be provoked
through the submit RPC at all. Building the envelope here and applying
it straight to the open view reaches them.
*/
static std::shared_ptr<STTx const>
envelope(
jtx::Env& env,
std::string const& manifest,
AccountID const& account,
std::function<void(STObject&)> const& tweak = {})
{
auto const build = [&](XRPAmount fee, bool tweaked) {
return std::make_shared<STTx const>(
ttMANIFEST_SET, [&](STObject& obj) {
obj.setAccountID(sfAccount, account);
obj.setFieldU32(sfSequence, 0);
obj.setFieldU32(sfNetworkID, env.app().config().NETWORK_ID);
obj.setFieldAmount(sfFee, fee);
obj.setFieldVL(sfSigningPubKey, Blob{});
obj.setFieldVL(sfTxnSignature, Blob{});
SerialIter mit{makeSlice(manifest)};
obj.peekFieldObject(sfManifest).set(mit);
if (tweaked && tweak)
tweak(obj);
});
};
// The same two-pass pricing makeSetManifestTx() does: encode once with
// a placeholder purely to have something to measure, then encode at the
// ceiling checkFee() will accept.
auto const base = SetManifest::calculateBaseFee(
*env.current(), *build(XRPAmount{0}, false));
return build(mulRatio(base, 12, 10, /*roundUp*/ true), true);
}
/** A mutable copy of a ledger object, suitable for the RawView interface.
Round-tripped through the wire format rather than copy constructed.
applyTemplate() rejects an object carrying a materialised soeDEFAULT
field that holds its default value, and that is exactly what reading one
out of a view hands back: an AccountRoot arrives with sfMintedNFTokens
present and zero. Serializing omits those fields and deserializing
re-materialises them.
*/
static std::shared_ptr<SLE>
rawCopy(std::shared_ptr<SLE const> const& sle)
{
Serializer s;
sle->add(s);
SerialIter sit{s.slice()};
return std::make_shared<SLE>(sit, sle->key());
}
/** Removes a ledger object from a view, if present.
OpenView exposes only the RawView interface, so the entry has to be
copied off the read side before it can be handed back to rawErase.
*/
static void
rawErase(OpenView& view, Keylet const& kl)
{
if (auto const sle = view.read(kl))
view.rawErase(rawCopy(sle));
}
/** Applies `tx` to a throwaway copy of the open ledger and reports the TER.
`prepare` runs against the same view first, so a test can corrupt state
that no ordinary transaction could produce. Nothing is retained.
*/
TER
applyDirect(
jtx::Env& env,
std::shared_ptr<STTx const> const& tx,
std::function<void(OpenView&)> const& prepare = {})
{
TER ret = tesSUCCESS;
env.app().openLedger().modify([&](OpenView& view, beast::Journal j) {
if (prepare)
prepare(view);
ret = ripple::apply(env.app(), view, *tx, tapNONE, j).ter;
return false; // discard, corrupt or otherwise
});
return ret;
}
void
testSubmission(FeatureBitset features)
{
testcase("submission");
using namespace jtx;
Env env{*this, makeConfig(), features};
auto const master = Account("master", KeyType::ed25519);
auto const ephemeral = Account("ephemeral", KeyType::ed25519);
env.fund(XRP(1000), master);
env.close();
BEAST_EXPECT(
engineResult(submit(env, makeManifest(master, ephemeral, 1))) ==
"tesSUCCESS");
env.close();
// A manifest is written twice so it can be found from either key, and
// each copy points at the other.
auto const byMaster = env.le(keylet::manifest(master.pk()));
auto const byEphemeral = env.le(keylet::manifest(ephemeral.pk()));
if (!BEAST_EXPECT(byMaster))
return;
if (!BEAST_EXPECT(byEphemeral))
return;
BEAST_EXPECT(byMaster->getAccountID(sfAccount) == master.id());
BEAST_EXPECT(byEphemeral->getAccountID(sfAccount) == master.id());
BEAST_EXPECT(byMaster->getFieldU32(sfSequence) == 1);
BEAST_EXPECT(
byMaster->getFieldH256(sfManifestID) ==
keylet::manifest(ephemeral.pk()).key);
BEAST_EXPECT(
byEphemeral->getFieldH256(sfManifestID) ==
keylet::manifest(master.pk()).key);
// Both signatures are mirrored so the object can be verified, and
// re-served, by any node that reads it.
BEAST_EXPECT(byMaster->isFieldPresent(sfMasterSignature));
BEAST_EXPECT(byMaster->isFieldPresent(sfSignature));
// The account root points at the master key's copy.
auto const sleAcct = env.le(master);
if (!BEAST_EXPECT(sleAcct))
return;
BEAST_EXPECT(
sleAcct->getFieldH256(sfManifestID) ==
keylet::manifest(master.pk()).key);
// The account sequence must be untouched: the transaction is unsigned
// and pinned to sequence 0, so consuming a sequence would let a third
// party burn the validator's sequence numbers -- and writing seq + 1
// would reset the account to 1.
BEAST_EXPECT(sleAcct->getFieldU32(sfSequence) == env.seq(master));
}
void
testUpdate(FeatureBitset features)
{
testcase("update and stale rejection");
using namespace jtx;
Env env{*this, makeConfig(), features};
auto const master = Account("master", KeyType::ed25519);
auto const eph1 = Account("eph1", KeyType::ed25519);
auto const eph2 = Account("eph2", KeyType::ed25519);
env.fund(XRP(1000), master);
env.close();
submit(env, makeManifest(master, eph1, 1));
env.close();
// Rotating the ephemeral key erases both old copies and writes two
// new ones, so the two can never drift apart.
BEAST_EXPECT(
engineResult(submit(env, makeManifest(master, eph2, 2))) ==
"tesSUCCESS");
env.close();
BEAST_EXPECT(!env.le(keylet::manifest(eph1.pk())));
if (!BEAST_EXPECT(env.le(keylet::manifest(eph2.pk()))))
return;
BEAST_EXPECT(
env.le(keylet::manifest(master.pk()))->getFieldU32(sfSequence) ==
2);
// Replaying the manifest we just applied, and anything older, is
// rejected on sequence. This is what prevents replay: the transaction
// is unsigned, so nothing else would.
BEAST_EXPECT(
engineResult(submit(env, makeManifest(master, eph2, 2))) ==
"tefPAST_MANIFEST_SEQ");
BEAST_EXPECT(
engineResult(submit(env, makeManifest(master, eph1, 1))) ==
"tefPAST_MANIFEST_SEQ");
}
void
testRevocation(FeatureBitset features)
{
testcase("revocation");
using namespace jtx;
Env env{*this, makeConfig(), features};
auto const master = Account("master", KeyType::ed25519);
auto const ephemeral = Account("ephemeral", KeyType::ed25519);
env.fund(XRP(1000), master);
env.close();
submit(env, makeManifest(master, ephemeral, 1));
env.close();
BEAST_EXPECT(
engineResult(submit(
env,
makeManifest(
master,
ephemeral,
std::numeric_limits<std::uint32_t>::max()))) ==
"tesSUCCESS");
env.close();
// A revocation has no signing key, so only the master key's copy
// exists and it points at nothing.
auto const byMaster = env.le(keylet::manifest(master.pk()));
if (!BEAST_EXPECT(byMaster))
return;
BEAST_EXPECT(!env.le(keylet::manifest(ephemeral.pk())));
BEAST_EXPECT(!byMaster->isFieldPresent(sfManifestID));
BEAST_EXPECT(!byMaster->isFieldPresent(sfSigningPubKey));
BEAST_EXPECT(
byMaster->getFieldU32(sfSequence) ==
std::numeric_limits<std::uint32_t>::max());
// Nothing supersedes a revocation.
BEAST_EXPECT(
engineResult(submit(env, makeManifest(master, ephemeral, 2))) ==
"tefREVOKED_MANIFEST");
}
void
testRetrieval(FeatureBitset features)
{
testcase("retrieval into the manifest cache");
using namespace jtx;
Env env{*this, makeConfig(), features};
auto const master = Account("master", KeyType::ed25519);
auto const eph1 = Account("eph1", KeyType::ed25519);
auto const eph2 = Account("eph2", KeyType::ed25519);
env.fund(XRP(1000), master);
env.close();
submit(env, makeManifest(master, eph1, 1));
env.close();
auto& cache = env.app().validatorManifests();
// Nothing has fed the cache yet, so an unknown key maps to itself.
BEAST_EXPECT(cache.getMasterKey(eph1.pk()) == eph1.pk());
// Reading the ledger resolves both directions of the mapping. The
// manifest is reconstructed from the ledger object and verified, so a
// lossy round trip would fail here rather than be accepted.
BEAST_EXPECT(cache.applyLedger(*env.closed(), {master.pk()}) == 1);
BEAST_EXPECT(cache.getMasterKey(eph1.pk()) == master.pk());
BEAST_EXPECT(cache.getSigningKey(master.pk()) == eph1.pk());
BEAST_EXPECT(cache.getSequence(master.pk()) == 1);
// Applying the same ledger again is a no-op: the cache is already at
// that sequence.
BEAST_EXPECT(cache.applyLedger(*env.closed(), {master.pk()}) == 0);
// A rotation on-ledger is picked up, and the superseded ephemeral key
// stops resolving.
submit(env, makeManifest(master, eph2, 2));
env.close();
BEAST_EXPECT(cache.applyLedger(*env.closed(), {master.pk()}) == 1);
BEAST_EXPECT(cache.getSigningKey(master.pk()) == eph2.pk());
BEAST_EXPECT(cache.getMasterKey(eph1.pk()) == eph1.pk());
// A key we never ask about is never read.
BEAST_EXPECT(cache.applyLedger(*env.closed(), {eph2.pk()}) == 0);
// A revocation reaches the cache too, and is the one thing that must
// never be lost: forgetting it would mean trusting the old key again.
submit(
env,
makeManifest(
master, eph2, std::numeric_limits<std::uint32_t>::max()));
env.close();
BEAST_EXPECT(cache.applyLedger(*env.closed(), {master.pk()}) == 1);
BEAST_EXPECT(cache.revoked(master.pk()));
}
void
testSigningKeyRetrieval(FeatureBitset features)
{
testcase("retrieval by ephemeral key");
using namespace jtx;
Env env{*this, makeConfig(), features};
auto const master = Account("master", KeyType::ed25519);
auto const eph1 = Account("eph1", KeyType::ed25519);
auto const eph2 = Account("eph2", KeyType::ed25519);
auto const stranger = Account("stranger", KeyType::ed25519);
env.fund(XRP(1000), master);
env.close();
submit(env, makeManifest(master, eph1, 1));
env.close();
auto& cache = env.app().validatorManifests();
// The situation applyLedger() cannot serve: a validation arrives
// signed by eph1 and the node holds no manifest naming it, so the
// master key to probe for is exactly what is missing.
BEAST_EXPECT(cache.getMasterKey(eph1.pk()) == eph1.pk());
BEAST_EXPECT(
cache.applyLedgerSigningKey(*env.closed(), eph1.pk()) ==
master.pk());
BEAST_EXPECT(cache.getMasterKey(eph1.pk()) == master.pk());
BEAST_EXPECT(cache.getSigningKey(master.pk()) == eph1.pk());
BEAST_EXPECT(cache.getSequence(master.pk()) == 1);
// A key with no manifest on-ledger resolves to nothing and leaves the
// cache untouched.
BEAST_EXPECT(
!cache.applyLedgerSigningKey(*env.closed(), stranger.pk()));
BEAST_EXPECT(cache.getMasterKey(stranger.pk()) == stranger.pk());
// A rotation is recovered from the new ephemeral key alone, and the
// superseded key stops resolving because its object is gone.
submit(env, makeManifest(master, eph2, 2));
env.close();
BEAST_EXPECT(
cache.applyLedgerSigningKey(*env.closed(), eph2.pk()) ==
master.pk());
BEAST_EXPECT(cache.getSigningKey(master.pk()) == eph2.pk());
BEAST_EXPECT(cache.getMasterKey(eph1.pk()) == eph1.pk());
BEAST_EXPECT(!env.le(keylet::manifest(eph1.pk())));
BEAST_EXPECT(!cache.applyLedgerSigningKey(*env.closed(), eph1.pk()));
// Asking again is answered from the cache, ahead of the per-ledger
// probe bookkeeping.
BEAST_EXPECT(
cache.applyLedgerSigningKey(*env.closed(), eph2.pk()) ==
master.pk());
// A master key is not a signing key. The object at its keylet is a
// perfectly good manifest and is ingested, but it binds eph2, not the
// master key, so nothing is reported for the key asked about.
BEAST_EXPECT(!cache.applyLedgerSigningKey(*env.closed(), master.pk()));
// A revoked master publishes no ephemeral object at all, so this
// direction goes quiet. The revocation still reaches the cache by
// master key, which is what applyLedger() is for.
submit(
env,
makeManifest(
master, eph2, std::numeric_limits<std::uint32_t>::max()));
env.close();
BEAST_EXPECT(!env.le(keylet::manifest(eph2.pk())));
BEAST_EXPECT(cache.applyLedger(*env.closed(), {master.pk()}) == 1);
BEAST_EXPECT(cache.revoked(master.pk()));
}
void
testMalformed(FeatureBitset features)
{
testcase("malformed submissions");
using namespace jtx;
Env env{*this, makeConfig(), features};
auto const master = Account("master", KeyType::ed25519);
auto const other = Account("other", KeyType::ed25519);
auto const ephemeral = Account("ephemeral", KeyType::ed25519);
env.fund(XRP(1000), master, other);
env.close();
// Not a manifest at all.
BEAST_EXPECT(
submit(env, "not a manifest")[jss::error].asString() ==
"invalidManifest");
// A manifest whose ephemeral signature does not check out.
{
auto blob = makeManifest(master, ephemeral, 1);
blob[blob.size() - 1] ^= 0xFF;
BEAST_EXPECT(
submit(env, blob)[jss::error].asString() == "invalidManifest");
}
// The master key's account must exist: it pays the fee and holds the
// pointer to the manifest.
auto const unfunded = Account("unfunded", KeyType::ed25519);
BEAST_EXPECT(
engineResult(submit(env, makeManifest(unfunded, ephemeral, 1))) ==
"terNO_ACCOUNT");
// An ephemeral key already claimed by a different account would
// collide with -- and clobber -- that account's manifest object.
submit(env, makeManifest(master, ephemeral, 1));
env.close();
BEAST_EXPECT(
engineResult(submit(env, makeManifest(other, ephemeral, 1))) ==
"tecDUPLICATE");
}
void
testEnvelopeRejections(FeatureBitset features)
{
testcase("envelope rejections");
using namespace jtx;
Env env{*this, makeConfig(), features};
auto const master = Account("master", KeyType::ed25519);
auto const other = Account("other", KeyType::ed25519);
auto const ephemeral = Account("ephemeral", KeyType::ed25519);
env.fund(XRP(1000), master, other);
env.close();
auto const good = makeManifest(master, ephemeral, 1);
// Sanity check: the unmodified envelope is the one Submit builds, so
// every rejection below is attributable to the tweak and nothing else.
BEAST_EXPECT(
applyDirect(env, envelope(env, good, master.id())) == tesSUCCESS);
// Any bit but tfFullyCanonicalSig (0x80000000), which is tfUniversal
// and so permitted on every transaction.
BEAST_EXPECT(
applyDirect(
env, envelope(env, good, master.id(), [](STObject& obj) {
obj.setFieldU32(sfFlags, 0x00000001);
})) == temINVALID_FLAG);
// sfManifest parses as an object but not as a manifest.
BEAST_EXPECT(
applyDirect(
env, envelope(env, makeUnparseableManifest(), master.id())) ==
temMALFORMED);
// A manifest whose signatures do not check out. checkValidity() would
// stop this before preflight, so only a direct apply reaches the
// transactor's own verify() call.
{
auto bad = good;
bad[bad.size() - 1] ^= 0xFF;
BEAST_EXPECT(
applyDirect(env, envelope(env, bad, master.id())) ==
temMALFORMED);
}
// The envelope's account must be the manifest's master key.
BEAST_EXPECT(
applyDirect(env, envelope(env, good, other.id())) == temMALFORMED);
// Every envelope field a relayer could otherwise choose is pinned.
for (auto const& [name, tweak] : std::vector<
std::pair<char const*, std::function<void(STObject&)>>>{
{"Sequence",
[](STObject& obj) { obj.setFieldU32(sfSequence, 1); }},
{"AccountTxnID",
[](STObject& obj) {
obj.setFieldH256(sfAccountTxnID, uint256{1});
}},
{"TicketSequence",
[](STObject& obj) { obj.setFieldU32(sfTicketSequence, 1); }},
{"SigningPubKey", [&](STObject& obj) {
obj.setFieldVL(sfSigningPubKey, master.pk().slice());
}}})
{
BEAST_EXPECTS(
applyDirect(env, envelope(env, good, master.id(), tweak)) ==
temMALFORMED,
name);
}
// sfFee is the one envelope field preflight cannot bound, because the
// base fee is not in scope until preclaim. checkFee() caps it instead.
auto const priced = envelope(env, good, master.id());
auto const ceiling = priced->getFieldAmount(sfFee).xrp();
BEAST_EXPECT(
applyDirect(
env, envelope(env, good, master.id(), [&](STObject& obj) {
obj.setFieldAmount(sfFee, ceiling + XRPAmount{1});
})) == temBAD_FEE);
// At the ceiling exactly, which is what Submit sends.
BEAST_EXPECT(
applyDirect(
env, envelope(env, good, master.id(), [&](STObject& obj) {
obj.setFieldAmount(sfFee, ceiling);
})) == tesSUCCESS);
}
void
testCorruptLedger(FeatureBitset features)
{
testcase("corrupt manifest state");
using namespace jtx;
Env env{*this, makeConfig(), features};
auto const master = Account("master", KeyType::ed25519);
auto const eph1 = Account("eph1", KeyType::ed25519);
auto const eph2 = Account("eph2", KeyType::ed25519);
env.fund(XRP(1000), master);
env.close();
BEAST_EXPECT(
engineResult(submit(env, makeManifest(master, eph1, 1))) ==
"tesSUCCESS");
env.close();
auto const update =
envelope(env, makeManifest(master, eph2, 2), master.id());
// sfManifestID on the account root points at nothing.
BEAST_EXPECT(applyDirect(env, update, [&](OpenView& view) {
rawErase(view, keylet::manifest(master.pk()));
}) == tefBAD_LEDGER);
// The master copy survives but the ephemeral copy it points at is
// gone.
BEAST_EXPECT(applyDirect(env, update, [&](OpenView& view) {
rawErase(view, keylet::manifest(eph1.pk()));
}) == tefBAD_LEDGER);
// The reverse: the account root has forgotten its manifest, so the
// erase pass is skipped and the keylet is found occupied.
BEAST_EXPECT(applyDirect(env, update, [&](OpenView& view) {
auto replacement =
rawCopy(view.read(keylet::account(master.id())));
replacement->makeFieldAbsent(sfManifestID);
view.rawReplace(replacement);
}) == tefBAD_LEDGER);
// None of the above was retained, so the ledger is still intact and an
// ordinary update still works.
BEAST_EXPECT(applyDirect(env, update) == tesSUCCESS);
}
void
testGossipSelection(FeatureBitset features)
{
testcase("gossip selection");
using namespace jtx;
Env env{*this, makeConfig(), features};
std::vector<Account> masters;
for (int i = 0; i < 4; ++i)
masters.emplace_back("gm" + std::to_string(i), KeyType::ed25519);
auto const ephemeral = Account("gossipeph", KeyType::ed25519);
for (auto const& m : masters)
env.fund(XRP(1000), m);
env.close();
// Each master needs its own ephemeral key: preclaim rejects a key
// already claimed by another account.
std::vector<Account> ephs;
for (int i = 0; i < 4; ++i)
ephs.emplace_back("ge" + std::to_string(i), KeyType::ed25519);
for (int i = 0; i < 4; ++i)
{
BEAST_EXPECT(
engineResult(
submit(env, makeManifest(masters[i], ephs[i], 1))) ==
"tesSUCCESS");
env.close();
}
ManifestCache cache;
hash_set<PublicKey> all;
for (auto const& m : masters)
all.insert(m.pk());
BEAST_EXPECT(cache.applyLedger(*env.closed(), all) == 4);
// The raw form is what a republishing node needs: the exact bytes the
// master key signed, plus the sequence to compare against the ledger.
auto const raw = cache.getRawManifest(masters[0].pk());
if (BEAST_EXPECT(raw))
{
BEAST_EXPECT(raw->first == 1);
BEAST_EXPECT(!raw->second.empty());
}
BEAST_EXPECT(!cache.getRawManifest(ephemeral.pk()));
// Pinning bumps the sequence so a cached gossip message is rebuilt,
// and pinning the same set again does not.
auto const seq = cache.sequence();
cache.pin({masters[0].pk(), masters[1].pk()});
BEAST_EXPECT(cache.sequence() > seq);
auto const seq2 = cache.sequence();
cache.pin({masters[0].pk(), masters[1].pk()});
BEAST_EXPECT(cache.sequence() == seq2);
// Everything is offered: two pinned plus two under the gossip limit.
std::size_t reserved = 0;
std::vector<PublicKey> offered;
cache.for_each_gossip_manifest(
[&](std::size_t n) { reserved = n; },
[&](Manifest const& m) { offered.push_back(m.masterKey); });
BEAST_EXPECT(reserved == 4);
BEAST_EXPECT(offered.size() == 4);
BEAST_EXPECT(
hash_set<PublicKey>(offered.begin(), offered.end()) == all);
// A pinned key with no manifest is counted in the reservation but not
// offered, since the reservation is only an upper bound.
cache.pin({masters[0].pk(), ephemeral.pk()});
offered.clear();
cache.for_each_gossip_manifest(
[&](std::size_t n) { reserved = n; },
[&](Manifest const& m) { offered.push_back(m.masterKey); });
BEAST_EXPECT(reserved == 5);
BEAST_EXPECT(offered.size() == 4);
}
void
testDisabled(FeatureBitset features)
{
testcase("amendment gate");
using namespace jtx;
Env env{*this, makeConfig(), features - featureOnChainManifests};
auto const master = Account("master", KeyType::ed25519);
auto const ephemeral = Account("ephemeral", KeyType::ed25519);
env.fund(XRP(1000), master);
env.close();
// The submitted transaction carries no account signature, so with the
// amendment off it would otherwise be rejected as unsigned and never
// reach preflight's temDISABLED. The RPC therefore refuses it up
// front, and says why.
auto const result = submit(env, makeManifest(master, ephemeral, 1));
BEAST_EXPECT(result[jss::error].asString() == "notEnabled");
BEAST_EXPECT(!result.isMember(jss::engine_result));
env.close();
BEAST_EXPECT(!env.le(keylet::manifest(master.pk())));
}
public:
void
run() override
{
using namespace test::jtx;
auto const sa = supported_amendments();
testSubmission(sa);
testUpdate(sa);
testRevocation(sa);
testRetrieval(sa);
testSigningKeyRetrieval(sa);
testMalformed(sa);
testEnvelopeRejections(sa);
testCorruptLedger(sa);
testGossipSelection(sa);
testDisabled(sa);
}
};
BEAST_DEFINE_TESTSUITE(SetManifest, app, ripple);
} // namespace test
} // namespace ripple
+14 -38
View File
@@ -191,7 +191,6 @@ struct URIToken_test : public beast::unit_test::suite
using namespace jtx;
using namespace std::literals::chrono_literals;
// fixXahauV1
{
Env env{*this, features};
auto const alice = Account("alice");
@@ -204,11 +203,9 @@ struct URIToken_test : public beast::unit_test::suite
std::string const hexid{strHex(tid)};
// temMALFORMED - cannot include sfDestination without sfAmount
bool const withFixXahauV1 =
env.current()->rules().enabled(fixXahauV1);
auto const txResult =
withFixXahauV1 ? ter(temMALFORMED) : ter(tefINTERNAL);
env(uritoken::mint(alice, uri), uritoken::dest(bob), txResult);
env(uritoken::mint(alice, uri),
uritoken::dest(bob),
ter(temMALFORMED));
env.close();
}
@@ -517,12 +514,10 @@ struct URIToken_test : public beast::unit_test::suite
env.close();
// tecINSUFFICIENT_FUNDS - insufficient xrp - fees
// fixXahauV1 - fix checking wrong account for insufficient xrp
env(pay(env.master, alice, XRP(10000)));
auto const txResult = env.current()->rules().enabled(fixXahauV1)
? ter(tecINSUFFICIENT_FUNDS)
: ter(tecINTERNAL);
env(uritoken::buy(bob, hexid), uritoken::amt(XRP(10000)), txResult);
env(uritoken::buy(bob, hexid),
uritoken::amt(XRP(10000)),
ter(tecINSUFFICIENT_FUNDS));
env.close();
// clear sell and reset new sell
@@ -579,11 +574,9 @@ struct URIToken_test : public beast::unit_test::suite
env.close();
// tecINSUFFICIENT_FUNDS - insufficient xrp - fees
// fixXahauV1 - fix checking wrong account for insufficient xrp
auto const txResult1 = env.current()->rules().enabled(fixXahauV1)
? ter(tecINSUFFICIENT_FUNDS)
: ter(tecINTERNAL);
env(uritoken::buy(bob, hexid), uritoken::amt(XRP(1000)), txResult1);
env(uritoken::buy(bob, hexid),
uritoken::amt(XRP(1000)),
ter(tecINSUFFICIENT_FUNDS));
env.close();
// clear sell and set usd sell
@@ -623,12 +616,10 @@ struct URIToken_test : public beast::unit_test::suite
env.close();
// tecNO_LINE_INSUF_RESERVE - insufficient xrp to create line
auto const txResult = env.current()->rules().enabled(fixXahauV1)
? ter(tecINSUF_RESERVE_SELLER)
: ter(tecNO_LINE_INSUF_RESERVE);
env(noop(echo), fee(XRP(50)), ter(tesSUCCESS));
env(uritoken::buy(dave, hexid), uritoken::amt(USD(1)), txResult);
env(uritoken::buy(dave, hexid),
uritoken::amt(USD(1)),
ter(tecINSUF_RESERVE_SELLER));
env.close();
}
@@ -2121,14 +2112,7 @@ struct URIToken_test : public beast::unit_test::suite
env(uritoken::buy(bob, id), uritoken::amt(delta));
env.close();
auto const postAlice = env.balance(alice, USD.issue());
if (!env.current()->rules().enabled(fixXahauV1))
{
BEAST_EXPECT(to_string(postAlice.value()) == tc.multiply);
}
else
{
BEAST_EXPECT(to_string(postAlice.value()) == tc.divide);
}
BEAST_EXPECT(to_string(postAlice.value()) == tc.divide);
BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob - delta);
}
@@ -2167,14 +2151,7 @@ struct URIToken_test : public beast::unit_test::suite
env.close();
auto const postAlice = env.balance(alice, USD.issue());
if (!env.current()->rules().enabled(fixXahauV1))
{
BEAST_EXPECT(postAlice.value() == preAlice);
}
else
{
BEAST_EXPECT(postAlice.value() == preAlice);
}
BEAST_EXPECT(postAlice.value() == preAlice);
BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob - USD(0));
}
@@ -2682,7 +2659,6 @@ public:
using namespace test::jtx;
auto const sa = supported_amendments();
testWithFeats(sa);
testWithFeats(sa - fixXahauV1);
}
};
+13 -18
View File
@@ -4354,11 +4354,11 @@ struct XahauGenesis_test : public beast::unit_test::suite
using namespace std::chrono_literals;
testcase("test claim reward valid for L1 with unl report");
for (bool const withXahauV1 : {true, false})
{
FeatureBitset _features = features - featureXahauGenesis;
auto const amend = withXahauV1 ? _features : _features - fixXahauV1;
Env env{*this, makeNetworkConfig(21337), amend};
Env env{
*this,
makeNetworkConfig(21337),
features - featureXahauGenesis};
double const rateDrops = 0.00333333333 * 1'000'000;
STAmount const feesXRP = XRP(1);
@@ -4453,9 +4453,7 @@ struct XahauGenesis_test : public beast::unit_test::suite
accountKeyAndSle(*env.current(), alice);
// claim reward
auto const txResult =
withXahauV1 ? ter(tesSUCCESS) : ter(tecHOOK_REJECTED);
env(claimReward(alice, env.master), fee(feesXRP), txResult);
env(claimReward(alice, env.master), fee(feesXRP), ter(tesSUCCESS));
env.close();
// trigger emitted txn
@@ -4464,8 +4462,7 @@ struct XahauGenesis_test : public beast::unit_test::suite
// calculate rewards
STAmount const netReward =
rewardUserAmount(*acctSle, preLedger, rateDrops);
STAmount const l1Reward =
withXahauV1 ? rewardL1Amount(netReward, 20) : STAmount(0);
STAmount const l1Reward = rewardL1Amount(netReward, 20);
// validate govern rewards
BEAST_EXPECT(env.balance(bob) == preBob + l1Reward);
@@ -4475,16 +4472,14 @@ struct XahauGenesis_test : public beast::unit_test::suite
// validate account fields
STAmount const postAlice = preAlice + netReward + l1Reward;
bool const boolResult = withXahauV1 ? true : false;
bool const has240819 = env.current()->rules().enabled(fix240819);
BEAST_EXPECT(
expectAccountFields(
env,
alice,
preLedger,
preLedger + 1,
has240819 ? (preAlice - feesXRP) : postAlice,
preTime) == boolResult);
BEAST_EXPECT(expectAccountFields(
env,
alice,
preLedger,
preLedger + 1,
has240819 ? (preAlice - feesXRP) : postAlice,
preTime));
}
}
-1
View File
@@ -48,7 +48,6 @@ cat $INPUT_FILE | tr '\n' '\f' |
sed -E 's/\)\[test\.hook\]"[\f \t]*/\/*end*\//g' |
while read -r line
do
echo "/* ==== WASM: $COUNTER ==== */" >> $OUTPUT_FILE
echo -n '{ R"[test.hook](' >> $OUTPUT_FILE
cat <<< "$line" | sed -E 's/.{7}$//g' | tr -d '\n' | tr '\f' '\n' >> $OUTPUT_FILE
echo ')[test.hook]",' >> $OUTPUT_FILE
File diff suppressed because it is too large. Load diff
+351
View File
@@ -0,0 +1,351 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx.h>
#include <xrpld/core/Job.h>
#include <xrpld/core/JobQueue.h>
#include <xrpld/net/RPCSub.h>
#include <xrpl/json/json_value.h>
#include <boost/asio.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <atomic>
#include <chrono>
#include <memory>
#include <string>
#include <thread>
namespace ripple {
namespace test {
// Minimal HTTP endpoint that counts received webhook POSTs and replies
// with a configurable status. Responses are EOF-delimited (no
// Content-Length) and the socket is closed right after writing — the
// exact shape that triggered the original handleData EOF-completion
// leak. So these tests exercise RPCSub flow control AND the HTTPClient
// EOF fix end to end: if either regressed, delivery would stall and the
// expected count would never be reached within the timeout.
class MockWebhookEndpoint
{
boost::asio::io_service ios_;
std::unique_ptr<boost::asio::io_service::work> work_;
boost::asio::ip::tcp::acceptor acceptor_;
std::thread thread_;
unsigned short port_;
std::atomic<int> received_{0};
std::atomic<int> status_{200};
std::atomic<int> delayMs_{0};
public:
MockWebhookEndpoint()
: work_(std::make_unique<boost::asio::io_service::work>(ios_))
, acceptor_(
ios_,
boost::asio::ip::tcp::endpoint(
boost::asio::ip::address::from_string("127.0.0.1"),
0))
{
port_ = acceptor_.local_endpoint().port();
accept();
thread_ = std::thread([this] { ios_.run(); });
}
~MockWebhookEndpoint()
{
work_.reset();
boost::system::error_code ec;
acceptor_.close(ec);
ios_.stop();
if (thread_.joinable())
thread_.join();
}
unsigned short
port() const
{
return port_;
}
int
received() const
{
return received_;
}
void
setStatus(int s)
{
status_ = s;
}
// Delay each reply so delivery is deterministically slower than the
// microsecond-fast enqueue loop — keeps the deque full for the
// queue-cap drop test regardless of scheduling.
void
setResponseDelay(int ms)
{
delayMs_ = ms;
}
private:
void
accept()
{
auto sock = std::make_shared<boost::asio::ip::tcp::socket>(ios_);
acceptor_.async_accept(*sock, [this, sock](auto ec) {
if (ec)
return;
handle(sock);
accept();
});
}
void
handle(std::shared_ptr<boost::asio::ip::tcp::socket> sock)
{
auto buf = std::make_shared<boost::asio::streambuf>();
boost::asio::async_read_until(
*sock, *buf, "\r\n\r\n", [this, sock, buf](auto ec, std::size_t) {
if (ec)
return;
++received_;
auto const delay = delayMs_.load();
if (delay > 0)
{
auto timer =
std::make_shared<boost::asio::steady_timer>(ios_);
timer->expires_from_now(std::chrono::milliseconds(delay));
timer->async_wait(
[this, sock, timer](auto) { reply(sock); });
}
else
{
reply(sock);
}
});
}
void
reply(std::shared_ptr<boost::asio::ip::tcp::socket> sock)
{
// EOF-delimited reply: no Content-Length, close after writing.
// This is the realistic failing-webhook shape.
auto resp = std::make_shared<std::string>(
"HTTP/1.0 " + std::to_string(status_.load()) +
" Reply\r\n\r\n{\"result\":{}}");
boost::asio::async_write(
*sock, boost::asio::buffer(*resp), [sock, resp](auto, std::size_t) {
boost::system::error_code ig;
sock->shutdown(boost::asio::ip::tcp::socket::shutdown_both, ig);
sock->close(ig);
});
}
};
//------------------------------------------------------------------------------
class RPCSub_test : public beast::unit_test::suite
{
// Generous ceiling: the instrumented Debug (coverage) build is much
// slower than Release, so timeouts are sized for that, not Release.
template <class Cond>
bool
waitFor(Cond cond, std::chrono::seconds timeout = std::chrono::seconds{30})
{
auto const deadline = std::chrono::steady_clock::now() + timeout;
while (!cond() && std::chrono::steady_clock::now() < deadline)
std::this_thread::sleep_for(std::chrono::milliseconds(10));
return cond();
}
std::shared_ptr<RPCSub>
makeSub(
jtx::Env& env,
MockWebhookEndpoint& ep,
std::size_t maxQueueSize = 16384)
{
return make_RPCSub(
env.app().getOPs(),
env.app().getJobQueue(),
"http://127.0.0.1:" + std::to_string(ep.port()) + "/",
"",
"",
env.app().logs(),
maxQueueSize);
}
// True once no RPCSub sending job is queued or running. sendThread
// captures a raw `this`, so the RPCSub must not be destroyed while a
// job is still in flight — wait on this before letting the sub die.
bool
sendingIdle(jtx::Env& env)
{
return env.app().getJobQueue().getJobCountTotal(jtCLIENT_SUBSCRIBE) ==
0;
}
// Wait for all events to reach the endpoint AND the sending job to
// finish, so the sub can be torn down without racing sendThread.
void
drainAndSettle(jtx::Env& env, MockWebhookEndpoint& ep, int expected)
{
bool const delivered =
waitFor([&] { return ep.received() >= expected; });
bool const idle = waitFor([&] { return sendingIdle(env); });
log << " drainAndSettle: received=" << ep.received() << "/" << expected
<< " idle=" << idle << std::endl;
BEAST_EXPECT(delivered);
BEAST_EXPECT(idle);
}
void
send(std::shared_ptr<RPCSub> const& sub, int n)
{
Json::Value ev(Json::objectValue);
ev["n"] = n;
sub->send(ev, false);
}
void
testDelivery()
{
testcase("Webhook events are delivered");
using namespace jtx;
Env env{*this};
MockWebhookEndpoint ep;
static constexpr int N = 10;
{
auto sub = makeSub(env, ep);
for (int i = 0; i < N; ++i)
send(sub, i);
drainAndSettle(env, ep, N);
}
BEAST_EXPECT(ep.received() == N);
}
void
testErrorsDoNotStall()
{
testcase("Delivery continues when endpoint returns HTTP 500");
// The original bug (xrpld #6341): an endpoint returning errors
// without Content-Length never completed, stalling delivery to
// ALL subscribers. Here every response is a 500 with no
// Content-Length (EOF-delimited) — all N must still arrive.
using namespace jtx;
Env env{*this};
MockWebhookEndpoint ep;
ep.setStatus(500);
static constexpr int N = 10;
{
auto sub = makeSub(env, ep);
for (int i = 0; i < N; ++i)
send(sub, i);
drainAndSettle(env, ep, N);
}
BEAST_EXPECT(ep.received() == N);
}
void
testRestartAfterDrain()
{
testcase("Sending restarts after the queue drains");
// After a batch drains, sendThread clears mSending and returns.
// A later send() must start a fresh sending job; if mSending were
// left set (the #6341 failure mode) the second burst would never
// be delivered.
using namespace jtx;
Env env{*this};
MockWebhookEndpoint ep;
{
auto sub = makeSub(env, ep);
// First burst, then wait for the sending job to fully drain
// and exit (mSending cleared) — deterministically, not via a
// sleep.
for (int i = 0; i < 5; ++i)
send(sub, i);
drainAndSettle(env, ep, 5);
// Second burst must start a fresh sending job.
for (int i = 5; i < 10; ++i)
send(sub, i);
drainAndSettle(env, ep, 10);
}
BEAST_EXPECT(ep.received() == 10);
}
void
testQueueCapDrops()
{
testcase("Events past the queue cap are dropped");
// With a tiny cap, pushing far more events than delivery can keep
// up with forces send() down the drop path: enqueue is microsecond
// -fast while each (delayed) HTTP delivery is a full round-trip, so
// the deque sits at the cap and excess events are dropped. The
// delay makes "delivery slower than enqueue" hold regardless of
// scheduling, so this isn't timing-dependent. We just need some
// delivered (cap works) and some dropped (drop path exercised).
using namespace jtx;
Env env{*this};
MockWebhookEndpoint ep;
ep.setResponseDelay(50);
static constexpr int pushed = 50;
{
auto sub = makeSub(env, ep, /*maxQueueSize*/ 2);
for (int i = 0; i < pushed; ++i)
send(sub, i);
BEAST_EXPECT(waitFor([&] { return sendingIdle(env); }));
}
log << " queue cap: received " << ep.received() << "/" << pushed
<< std::endl;
BEAST_EXPECT(ep.received() > 0);
BEAST_EXPECT(ep.received() < pushed);
}
public:
void
run() override
{
testDelivery();
testErrorsDoNotStall();
testRestartAfterDrain();
testQueueCapDrops();
}
};
BEAST_DEFINE_TESTSUITE(RPCSub, net, ripple);
} // namespace test
} // namespace ripple
+250
View File
@@ -24,6 +24,8 @@
#include <test/jtx/WSClient.h>
#include <test/rpc/GRPCTestClientBase.h>
#include <xrpld/rpc/GRPCHandlers.h>
#include <xrpl/hook/Misc.h>
#include <xrpl/protocol/InnerObjectFormats.h>
#include <xrpl/resource/Charge.h>
#include <xrpl/resource/Fees.h>
@@ -121,6 +123,252 @@ public:
}
}
void
testHooksRequest()
{
testcase("Hooks request");
using namespace jtx;
Env env(*this);
Account const alice{"alice"};
env.fund(XRP(1000), alice);
auto request = [&](Json::Value const& hooks, unsigned apiVersion) {
Json::Value params;
params[jss::account] = alice.human();
params[jss::hooks] = hooks;
if (apiVersion == 1)
return env.rpc("json", "account_info", to_string(params));
params[jss::api_version] = 2;
return env.rpc("json", "account_info", to_string(params));
};
for (Json::Value const& invalid :
{Json::Value(1),
Json::Value(1.1),
Json::Value("true"),
Json::Value(Json::nullValue),
Json::Value(Json::objectValue),
Json::Value(Json::arrayValue)})
{
for (unsigned const apiVersion : {1u, 2u})
{
auto const info = request(invalid, apiVersion);
BEAST_EXPECT(info[jss::result][jss::error] == "invalidParams");
BEAST_EXPECT(
info[jss::result][jss::error_message] ==
"Invalid field 'hooks'.");
}
}
auto const withoutHooks = env.rpc(
"json",
"account_info",
R"({"account": ")" + alice.human() + R"("})");
BEAST_EXPECT(!withoutHooks[jss::result].isMember(jss::hooks));
auto const explicitlyFalse = env.rpc(
"json",
"account_info",
R"({"account": ")" + alice.human() + R"(","hooks":false})");
BEAST_EXPECT(!explicitlyFalse[jss::result].isMember(jss::hooks));
auto const explicitlyTrue = env.rpc(
"json",
"account_info",
std::string{"{ "} + "\"account\": \"" + alice.human() +
"\", \"hooks\": true }");
BEAST_EXPECT(explicitlyTrue[jss::result][jss::hooks].isArray());
BEAST_EXPECT(explicitlyTrue[jss::result][jss::hooks].size() == 0);
}
void
testHooksEffectiveValues()
{
testcase("Effective hook values");
auto const* hookTemplate =
InnerObjectFormats::getInstance().findSOTemplateBySField(sfHook);
if (!BEAST_EXPECTS(hookTemplate != nullptr, "sfHook template"))
return;
// If this count changes, add or remove tests for the corresponding Hook
// fields.
BEAST_EXPECT(hookTemplate->size() == 12);
using namespace jtx;
Env env(*this);
Account const alice{"alice"};
env.fund(XRP(1000), alice);
uint256 const definitionNamespace{1};
uint256 const accountNamespace{2};
auto const makeParameter = [](std::string const& name,
std::string const& value) {
Json::Value parameter;
parameter[jss::HookParameter][jss::HookParameterName] = name;
parameter[jss::HookParameter][jss::HookParameterValue] = value;
return parameter;
};
auto create = hso(genesis::AcceptHook);
create[jss::Flags] = hsfCOLLECT;
create[jss::HookName] = strHex(std::string{"ACCT"});
create[jss::HookNamespace] = to_string(definitionNamespace);
create[jss::HookParameters] = Json::arrayValue;
create[jss::HookParameters].append(makeParameter("AA", "BB"));
create[jss::HookParameters].append(makeParameter("CC", "DD"));
env(hook(alice, {{create, Json::Value{}}}, 0),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
auto const hookSLE = env.le(keylet::hook(alice.id()));
BEAST_EXPECT(hookSLE && hookSLE->isFieldPresent(sfHooks));
if (!hookSLE || !hookSLE->isFieldPresent(sfHooks))
return;
auto const& installed = hookSLE->getFieldArray(sfHooks)[0];
BEAST_EXPECT(installed.isFieldPresent(sfHookHash));
if (!installed.isFieldPresent(sfHookHash))
return;
auto const hash = installed.getFieldH256(sfHookHash);
auto const hookDefinition = env.le(keylet::hookDefinition(hash));
BEAST_EXPECT(hookDefinition);
if (hookDefinition)
{
BEAST_EXPECT(!hookDefinition->isFieldPresent(sfHookName));
BEAST_EXPECT(!hookDefinition->isFieldPresent(sfHookGrants));
BEAST_EXPECT(hookDefinition->isFieldPresent(sfHookApiVersion));
BEAST_EXPECT(hookDefinition->getFieldU16(sfHookApiVersion) == 0);
}
auto accountInfo = env.rpc(
"json",
"account_info",
R"({"account": ")" + alice.human() + R"(","hooks":true})");
auto const& hookJson =
accountInfo[jss::result][jss::hooks][0U][sfHook.jsonName];
BEAST_EXPECT(hookJson[sfHookHash.jsonName] == to_string(hash));
BEAST_EXPECT(!hookJson.isMember(sfCreateCode.jsonName));
BEAST_EXPECT(
hookJson[sfHookNamespace.jsonName] ==
to_string(definitionNamespace));
BEAST_EXPECT(
hookJson[sfHookName.jsonName].isString() &&
hookJson[sfHookName.jsonName] == strHex(std::string{"ACCT"}));
BEAST_EXPECT(
hookJson[sfHookApiVersion.jsonName].isInt() &&
hookJson[sfHookApiVersion.jsonName].asUInt() == 0);
BEAST_EXPECT(hookJson[sfHookOn.jsonName] == to_string(uint256{0}));
BEAST_EXPECT(!hookJson.isMember(sfHookOnIncoming.jsonName));
BEAST_EXPECT(!hookJson.isMember(sfHookOnOutgoing.jsonName));
BEAST_EXPECT(
hookJson[sfHookCanEmit.jsonName] ==
to_string(UINT256_BIT[ttHOOK_SET]));
BEAST_EXPECT(hookJson[sfFlags.jsonName] == hsfCOLLECT);
BEAST_EXPECT(hookJson[sfHookParameters.jsonName].size() == 2);
auto const& hooksJson = accountInfo[jss::result][jss::hooks];
BEAST_EXPECT(hooksJson.isArray());
BEAST_EXPECT(hooksJson.size() == 2);
if (!hooksJson.isArray() || hooksJson.size() < 2)
return;
BEAST_EXPECT(hooksJson[1U].isObject());
BEAST_EXPECT(hooksJson[1U].isMember(sfHook.jsonName));
if (!hooksJson[1U].isObject() ||
!hooksJson[1U].isMember(sfHook.jsonName))
return;
auto const& blankHook = hooksJson[1U][sfHook.jsonName];
BEAST_EXPECT(blankHook.isObject());
BEAST_EXPECT(blankHook.size() == 0);
Json::Value update;
update[jss::HookHash] = to_string(hash);
update[jss::Flags] = hsfOVERRIDE;
update[jss::HookName] = strHex(std::string{"ACCT"});
update[jss::HookGrants] = Json::arrayValue;
update[jss::HookGrants][0U][jss::HookGrant][jss::HookHash] =
to_string(hash);
update[jss::HookNamespace] = to_string(accountNamespace);
update[jss::HookOnIncoming] = to_string(uint256{5});
update[jss::HookOnOutgoing] = to_string(uint256{6});
update[jss::HookCanEmit] = to_string(uint256{4});
update[jss::HookParameters] = Json::arrayValue;
update[jss::HookParameters].append(makeParameter("AA", ""));
update[jss::HookParameters].append(makeParameter("EE", "FF"));
env(hook(alice, {{update}}, 0), fee(XRP(1)), ter(tesSUCCESS));
env.close();
accountInfo = env.rpc(
"json",
"account_info",
R"({"account": ")" + alice.human() + R"(","hooks":true})");
auto const& overridden =
accountInfo[jss::result][jss::hooks][0U][sfHook.jsonName];
BEAST_EXPECT(
overridden[sfHookNamespace.jsonName] ==
to_string(accountNamespace));
BEAST_EXPECT(
overridden[sfHookName.jsonName].isString() &&
overridden[sfHookName.jsonName] == strHex(std::string{"ACCT"}));
BEAST_EXPECT(
overridden[sfHookApiVersion.jsonName].isInt() &&
overridden[sfHookApiVersion.jsonName].asUInt() == 0);
BEAST_EXPECT(overridden[sfHookGrants.jsonName].isArray());
BEAST_EXPECT(overridden[sfHookGrants.jsonName].size() == 1);
if (overridden[sfHookGrants.jsonName].isArray() &&
overridden[sfHookGrants.jsonName].size() == 1)
{
auto const& grant = overridden[sfHookGrants.jsonName][0U];
BEAST_EXPECT(grant.isObject());
BEAST_EXPECT(grant.size() == 1);
BEAST_EXPECT(grant.isMember(sfHookGrant.jsonName));
if (grant.isObject() && grant.size() == 1 &&
grant.isMember(sfHookGrant.jsonName))
{
auto const& grantObject = grant[sfHookGrant.jsonName];
BEAST_EXPECT(grantObject.isObject());
BEAST_EXPECT(grantObject.size() == 1);
BEAST_EXPECT(grantObject.isMember(sfHookHash.jsonName));
if (grantObject.isObject() && grantObject.size() == 1 &&
grantObject.isMember(sfHookHash.jsonName))
BEAST_EXPECT(
grantObject[sfHookHash.jsonName] == to_string(hash));
}
}
BEAST_EXPECT(overridden[sfFlags.jsonName] == 0);
BEAST_EXPECT(!overridden.isMember(sfHookOn.jsonName));
BEAST_EXPECT(
overridden[sfHookOnIncoming.jsonName] == to_string(uint256{5}));
BEAST_EXPECT(
overridden[sfHookOnOutgoing.jsonName] == to_string(uint256{6}));
BEAST_EXPECT(
overridden[sfHookCanEmit.jsonName] == to_string(uint256{4}));
BEAST_EXPECT(overridden[sfHookParameters.jsonName].size() == 3);
if (overridden[sfHookParameters.jsonName].size() != 3)
return;
auto expectParameter = [&](std::string const& name,
std::string const& value) {
bool found = false;
for (auto const& parameter : overridden[sfHookParameters.jsonName])
{
if (!parameter.isObject() ||
!parameter.isMember(sfHookParameter.jsonName))
continue;
auto const& object = parameter[sfHookParameter.jsonName];
if (!object.isObject() ||
object[sfHookParameterName.jsonName] != name)
continue;
found = true;
BEAST_EXPECT(object[sfHookParameterValue.jsonName] == value);
break;
}
BEAST_EXPECT(found);
};
expectParameter("AA", "");
expectParameter("CC", "DD");
expectParameter("EE", "FF");
}
// Test the "signer_lists" argument in account_info.
void
testSignerLists()
@@ -686,6 +934,8 @@ public:
run() override
{
testErrors();
testHooksRequest();
testHooksEffectiveValues();
testSignerLists();
testSignerListsApiVersion2();
testSignerListsV2();
+543
View File
@@ -19,15 +19,548 @@
#include <test/jtx.h>
#include <test/jtx/WSClient.h>
#include <test/jtx/envconfig.h>
#include <xrpld/app/consensus/RCLValidations.h>
#include <xrpld/app/ledger/Ledger.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/main/Application.h>
#include <xrpld/app/misc/AmendmentTable.h>
#include <xrpld/app/misc/NetworkOPs.h>
#include <xrpld/core/Config.h>
#include <xrpld/core/ConfigSections.h>
#include <xrpl/basics/FileUtilities.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/temp_dir.h>
#include <xrpl/protocol/BuildInfo.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/digest.h>
#include <xrpl/protocol/jss.h>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/path.hpp>
#include <chrono>
#include <cstring>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <string>
namespace ripple {
class AmendmentBlocked_test : public beast::unit_test::suite
{
// An amendment id this binary will never support.
static uint256
unsupportedAmendmentId()
{
std::string const in = "AmendmentBlocked_test.unsupported";
sha256_hasher h;
using beast::hash_append;
hash_append(h, in);
auto const d = static_cast<sha256_hasher::result_type>(h);
uint256 result;
std::memcpy(result.data(), d.data(), d.size());
return result;
}
// The majority period only sets how far out activation is expected, so
// shorten it from two weeks. That keeps the ledger close-time jumps in
// these tests down to minutes, and makes the relationship to the
// five-minute shutdown lead time in LedgerMaster obvious.
static std::unique_ptr<Config>
shortMajorityConfig()
{
using namespace std::chrono_literals;
auto cfg = test::jtx::envconfig();
cfg->AMENDMENT_MAJORITY_TIME = 15min;
return cfg;
}
// Give the amendment table a majority, as of now, for an amendment we do
// not support; activation is then expected one majority period out.
// Bypasses the ReadView overload (and therefore needValidatedLedger) on
// purpose: because lastUpdateSeq_ is set to the current sequence, later
// closes within the same 256-ledger block will not recompute -- and so
// will not clear -- what we inject here.
void
injectUnsupportedMajority(test::jtx::Env& env)
{
auto const seq = env.closed()->info().seq;
majorityAmendments_t majority;
majority[unsupportedAmendmentId()] = env.now();
env.app().getAmendmentTable().doValidatedLedger(seq, {}, majority);
auto const first =
env.app().getAmendmentTable().firstUnsupportedExpected();
BEAST_EXPECT(
first &&
*first == env.now() + env.app().config().AMENDMENT_MAJORITY_TIME);
}
void
testReceiptFileHelpers()
{
testcase("amendment blocked receipt helpers");
auto const id = unsupportedAmendmentId();
{
beast::temp_dir td;
Config cfg;
cfg.CONFIG_DIR = td.path();
auto const path = amendmentBlockedFilePath(cfg);
BEAST_EXPECT(path.filename() == "README_AMENDMENT_BLOCKED");
BEAST_EXPECT(
path.parent_path() == boost::filesystem::path{td.path()});
BEAST_EXPECT(!boost::filesystem::exists(path));
// Nothing to remove yet, and that is not an error.
boost::system::error_code ec;
BEAST_EXPECT(!removeAmendmentBlockedFile(cfg, ec));
BEAST_EXPECT(!ec);
BEAST_EXPECT(!writeAmendmentBlockedFile(
cfg, {to_string(id) + " (already active)"}));
BEAST_EXPECT(boost::filesystem::exists(path));
auto const contents = getFileContents(ec, path);
BEAST_EXPECT(!ec);
BEAST_EXPECT(
contents.find("XAHAUD STOPPED: UPGRADE REQUIRED") == 0);
// When it stopped, what was running, and what it choked on.
BEAST_EXPECT(contents.find("Stopped at:") != std::string::npos);
BEAST_EXPECT(
contents.find(BuildInfo::getVersionString()) !=
std::string::npos);
BEAST_EXPECT(contents.find(to_string(id)) != std::string::npos);
BEAST_EXPECT(contents.find("already active") != std::string::npos);
BEAST_EXPECT(contents.find("Upgrade xahaud") != std::string::npos);
// The receipt is self-clearing, so it must not tell the operator
// to delete anything.
BEAST_EXPECT(
contents.find("removed automatically") != std::string::npos);
// The timestamp is rendered, not a placeholder: to_string_iso
// gives YYYY-MM-DDTHH:MM:SSZ.
auto const stampAt = contents.find("Stopped at:");
if (BEAST_EXPECT(stampAt != std::string::npos))
{
auto const eol = contents.find('\n', stampAt);
auto const line = contents.substr(stampAt, eol - stampAt);
BEAST_EXPECT(line.find("20") != std::string::npos);
BEAST_EXPECT(line.find('T') != std::string::npos);
BEAST_EXPECT(line.find('Z') != std::string::npos);
}
// Now it can be removed, and removal is reported.
ec.clear();
BEAST_EXPECT(removeAmendmentBlockedFile(cfg, ec));
BEAST_EXPECT(!ec);
BEAST_EXPECT(!boost::filesystem::exists(path));
// With no amendments to name the receipt still says something
// useful.
BEAST_EXPECT(!writeAmendmentBlockedFile(cfg, {}));
ec.clear();
auto const bare = getFileContents(ec, path);
BEAST_EXPECT(!ec);
BEAST_EXPECT(
bare.find("not supported by this build") != std::string::npos);
}
// A directory we cannot write to must surface an error rather than
// throw. The shutdown continues either way; only the receipt is lost,
// which is what happens on installs where CONFIG_DIR is read-only for
// the account xahaud runs as.
{
Config cfg;
cfg.CONFIG_DIR =
boost::filesystem::path{"/"} / "no" / "such" / "directory";
BEAST_EXPECT(!!writeAmendmentBlockedFile(cfg, {}));
boost::system::error_code ec;
BEAST_EXPECT(!removeAmendmentBlockedFile(cfg, ec));
}
}
void
testStandaloneDoesNotStop()
{
testcase("standalone does not stop or leave a receipt");
using namespace test::jtx;
beast::temp_dir td;
Env env{*this, envconfig([&](std::unique_ptr<Config> cfg) {
cfg->CONFIG_DIR = td.path();
return cfg;
})};
BEAST_EXPECT(env.app().config().standalone());
auto const path = amendmentBlockedFilePath(env.app().config());
env.app().getOPs().setAmendmentBlocked();
BEAST_EXPECT(env.app().getOPs().isAmendmentBlocked());
BEAST_EXPECT(!env.app().getOPs().isAmendmentWarned());
BEAST_EXPECT(!env.app().isStopping());
BEAST_EXPECT(!boost::filesystem::exists(path));
}
void
testShutdownOnBlock()
{
testcase("amendment blocked stops the server");
using namespace test::jtx;
// A non-standalone Env is needed to exercise shutdown rather than
// only setting the blocked flag. Consequences when editing:
// setup() arms the state timer, run() arms the deadlock detector, and
// signalStop() below releases run() to tear the application down
// concurrently with the rest of this function. Do all the assertions
// straight away and let the Env go out of scope promptly.
beast::temp_dir td;
Env env{*this, envconfig([&](std::unique_ptr<Config> config) {
config->NODE_SIZE = 0;
config->setupControl(true, true, false);
// setupControl picks a production node size for
// non-standalone; put it back to "tiny" for the test.
config->NODE_SIZE = 0;
config->CONFIG_DIR = td.path();
config->legacy("database_path", td.path());
return config;
})};
BEAST_EXPECT(!env.app().config().standalone());
auto const path = amendmentBlockedFilePath(env.app().config());
BEAST_EXPECT(path.filename() == "README_AMENDMENT_BLOCKED");
BEAST_EXPECT(!boost::filesystem::exists(path));
BEAST_EXPECT(!env.app().isStopping());
// Make the table report an unsupported amendment so the receipt has
// something concrete to name.
auto const id = unsupportedAmendmentId();
env.app().getAmendmentTable().enable(id);
BEAST_EXPECT(env.app().getAmendmentTable().hasUnsupportedEnabled());
env.app().getOPs().setAmendmentBlocked();
BEAST_EXPECT(env.app().getOPs().isAmendmentBlocked());
BEAST_EXPECT(env.app().isStopping());
BEAST_EXPECT(boost::filesystem::exists(path));
boost::system::error_code readError;
auto const contents = getFileContents(readError, path);
BEAST_EXPECT(!readError);
BEAST_EXPECT(
contents.find("XAHAUD STOPPED: UPGRADE REQUIRED") !=
std::string::npos);
BEAST_EXPECT(contents.find("Upgrade xahaud") != std::string::npos);
BEAST_EXPECT(contents.find(to_string(id)) != std::string::npos);
BEAST_EXPECT(contents.find("already active") != std::string::npos);
// Repeat calls are a no-op: Change::applyAmendment reaches this from
// the transaction apply path without an isBlocked() guard, so it must
// not rewrite the receipt or re-log once per ledger.
BEAST_EXPECT(boost::filesystem::remove(path));
env.app().getOPs().setAmendmentBlocked();
BEAST_EXPECT(!boost::filesystem::exists(path));
}
void
checkJump(bool unsupported)
{
using namespace test::jtx;
beast::temp_dir td;
Env env{*this, envconfig([&](std::unique_ptr<Config> cfg) {
cfg->setupControl(true, true, false);
cfg->NODE_SIZE = 0;
cfg->CONFIG_DIR = td.path();
cfg->legacy("database_path", td.path());
return cfg;
})};
auto& app = env.app();
auto& ops = app.getOPs();
auto& master = app.getLedgerMaster();
std::lock_guard lock(app.getMasterMutex());
auto const before = master.getClosedLedger();
// A direct child is not preferred over our LCL: consensus may be
// about to build it. Two ledgers ahead forces the JUMP path.
auto parent =
std::make_shared<Ledger>(*before, app.timeKeeper().closeTime());
parent->updateSkipList();
parent->setImmutable();
auto candidate =
std::make_shared<Ledger>(*parent, app.timeKeeper().closeTime());
candidate->updateSkipList();
if (unsupported)
{
auto const key = keylet::amendments();
auto const existing = candidate->read(key);
auto sle = existing ? std::make_shared<SLE>(*existing)
: std::make_shared<SLE>(key);
STVector256 amendments;
if (sle->isFieldPresent(sfAmendments))
amendments = sle->getFieldV256(sfAmendments);
amendments.push_back(unsupportedAmendmentId());
sle->setFieldV256(sfAmendments, amendments);
if (existing)
candidate->rawReplace(sle);
else
candidate->rawInsert(sle);
}
// A serialized uint32 with an unknown field number. Insert raw bytes
// so the real transaction parser, reached through TxQ, must throw.
BEAST_EXPECT(SField::getField(STI_UINT32, 255).isInvalid());
auto tx = std::make_shared<Serializer>();
tx->add8(0x20);
tx->add8(255);
while (tx->getDataLength() < txMinSizeBytes)
tx->add8(0);
auto meta = std::make_shared<Serializer>();
meta->add8(0xE1);
auto const txID = sha512Half(tx->slice());
candidate->rawTxInsert(txID, tx, meta);
candidate->setImmutable();
bool unknownField = false;
try
{
candidate->txRead(txID);
}
catch (std::runtime_error const& e)
{
unknownField = std::string(e.what()).starts_with("Unknown field");
}
if (!BEAST_EXPECT(unknownField))
return;
master.storeLedger(candidate);
auto const keys = randomKeyPair(KeyType::secp256k1);
auto const nodeID = calcNodeID(keys.first);
auto validation = std::make_shared<STValidation>(
app.timeKeeper().closeTime(),
keys.first,
keys.second,
nodeID,
[&](STValidation& v) {
v.setFieldH256(sfLedgerHash, candidate->info().hash);
v.setFieldU32(sfLedgerSequence, candidate->seq());
});
// Add directly so ledger acceptance does not discover the amendment
// before JUMP has a chance to inspect the candidate itself.
BEAST_EXPECT(
app.getValidations().add(nodeID, RCLValidation{validation}) ==
ValStatus::current);
if (!BEAST_EXPECT(
app.getValidations().getPreferredLCL(
RCLValidatedLedger{before, env.journal},
master.getValidLedgerIndex(),
{}) == candidate->info().hash))
return;
BEAST_EXPECT(!app.getAmendmentTable().hasUnsupportedEnabled());
BEAST_EXPECT(!app.getAmendmentTable().firstUnsupportedExpected());
BEAST_EXPECT(!ops.isAmendmentBlocked());
BEAST_EXPECT(!app.isStopping());
auto const receipt = amendmentBlockedFilePath(app.config());
BEAST_EXPECT(!boost::filesystem::exists(receipt));
bool threw = false;
try
{
ops.endConsensus({});
}
catch (std::runtime_error const& e)
{
threw = true;
BEAST_EXPECT(std::string(e.what()).starts_with("Unknown field"));
}
BEAST_EXPECT(threw == !unsupported);
BEAST_EXPECT(ops.isAmendmentBlocked() == unsupported);
BEAST_EXPECT(app.isStopping() == unsupported);
BEAST_EXPECT(boost::filesystem::exists(receipt) == unsupported);
BEAST_EXPECT(
master.getClosedLedger()->info().hash == before->info().hash);
}
void
testJumpStopsForUnsupportedAmendment()
{
testcase("JUMP to an unsupported ledger stops the server");
checkJump(/*unsupported=*/true);
}
void
testJumpRethrowsParsingError()
{
testcase("JUMP parsing errors without unsupported amendments escape");
checkJump(/*unsupported=*/false);
}
void
testWarnsOutsideShutdownWindow()
{
testcase("unsupported majority warns while activation is distant");
using namespace test::jtx;
Env env{*this, shortMajorityConfig()};
BEAST_EXPECT(!env.app().getOPs().isBlocked());
BEAST_EXPECT(env.close());
injectUnsupportedMajority(env);
BEAST_EXPECT(env.close());
// A majority period out: warn, keep running.
BEAST_EXPECT(env.app().getOPs().isAmendmentWarned());
BEAST_EXPECT(!env.app().getOPs().isAmendmentBlocked());
// The table can name the amendment, and reports it as pending rather
// than active. This is what ends up in the receipt.
auto const unsupported =
env.app().getAmendmentTable().unsupportedAmendments();
BEAST_EXPECT(unsupported.size() == 1);
if (unsupported.size() == 1)
{
BEAST_EXPECT(unsupported[0].id == unsupportedAmendmentId());
BEAST_EXPECT(
unsupported[0].expected ==
env.app().getAmendmentTable().firstUnsupportedExpected());
}
// Because firstUnsupportedExpected() is set, the warning carries the
// expected activation date.
auto const si = env.rpc("server_info")[jss::result];
BEAST_EXPECT(si.isMember(jss::info));
auto const& warnings = si[jss::info][jss::warnings];
BEAST_EXPECT(warnings.isArray() && warnings.size() == 1);
if (warnings.isArray() && warnings.size() == 1)
{
BEAST_EXPECT(
warnings[0u][jss::id].asInt() == warnRPC_UNSUPPORTED_MAJORITY);
auto const& details = warnings[0u][jss::details];
BEAST_EXPECT(details.isMember(jss::expected_date));
BEAST_EXPECT(details.isMember(jss::expected_date_UTC));
}
}
void
testUnsupportedAmendmentReporting()
{
testcase("unsupported amendments are reported for the receipt");
using namespace test::jtx;
Env env{*this, shortMajorityConfig()};
auto& table = env.app().getAmendmentTable();
BEAST_EXPECT(table.unsupportedAmendments().empty());
// Majority, not yet active -> reported with an expected time.
BEAST_EXPECT(env.close());
injectUnsupportedMajority(env);
auto pending = table.unsupportedAmendments();
BEAST_EXPECT(pending.size() == 1);
if (pending.size() == 1)
BEAST_EXPECT(pending[0].expected.has_value());
// Majority lost -> nothing to report. The list is recomputed from the
// ledger each time, so it must not accumulate.
auto const seq = env.closed()->info().seq;
table.doValidatedLedger(seq, {}, {});
BEAST_EXPECT(table.unsupportedAmendments().empty());
BEAST_EXPECT(!table.firstUnsupportedExpected());
// Active -> reported with no expected time.
BEAST_EXPECT(table.enable(unsupportedAmendmentId()));
BEAST_EXPECT(table.hasUnsupportedEnabled());
auto const active = table.unsupportedAmendments();
BEAST_EXPECT(active.size() == 1);
if (active.size() == 1)
{
BEAST_EXPECT(active[0].id == unsupportedAmendmentId());
BEAST_EXPECT(!active[0].expected);
}
}
void
testWarningVisibleWithoutAdmin()
{
testcase("unsupported majority warning is not admin only");
using namespace test::jtx;
// No closes here: ledger_accept is Role::ADMIN, server_info is
// Role::USER, which is the whole point of the test.
Env env{*this, envconfig(no_admin)};
env.app().getOPs().setAmendmentWarned();
BEAST_EXPECT(env.app().getOPs().isAmendmentWarned());
auto const si = env.rpc("server_info")[jss::result];
BEAST_EXPECT(si.isMember(jss::info));
auto const& warnings = si[jss::info][jss::warnings];
BEAST_EXPECT(warnings.isArray() && warnings.size() == 1);
if (warnings.isArray() && warnings.size() == 1)
{
BEAST_EXPECT(
warnings[0u][jss::id].asInt() == warnRPC_UNSUPPORTED_MAJORITY);
}
}
void
testShutsDownInsideShutdownWindow()
{
testcase("unsupported majority stops the server before activation");
using namespace test::jtx;
Env env{*this, shortMajorityConfig()};
BEAST_EXPECT(!env.app().getOPs().isBlocked());
BEAST_EXPECT(env.close());
injectUnsupportedMajority(env);
// First close only warns: activation is still a majority period away.
BEAST_EXPECT(env.close());
BEAST_EXPECT(env.app().getOPs().isAmendmentWarned());
BEAST_EXPECT(!env.app().getOPs().isAmendmentBlocked());
auto const expected =
*env.app().getAmendmentTable().firstUnsupportedExpected();
// Close two minutes short of the expected activation time. This is not
// a flag ledger and the warning has already been issued, so the check
// has to run on every validated ledger to see this at all.
BEAST_EXPECT(env.close(expected - NetClock::duration{120}));
BEAST_EXPECT(env.app().getOPs().isAmendmentBlocked());
BEAST_EXPECT(!env.app().getOPs().isAmendmentWarned());
// Standalone, so the flag is set but the server keeps running.
BEAST_EXPECT(!env.app().isStopping());
}
void
testShutsDownWhenActivationOverdue()
{
testcase("unsupported majority stops the server when overdue");
using namespace test::jtx;
Env env{*this, shortMajorityConfig()};
BEAST_EXPECT(!env.app().getOPs().isBlocked());
BEAST_EXPECT(env.close());
injectUnsupportedMajority(env);
auto const expected =
*env.app().getAmendmentTable().firstUnsupportedExpected();
// firstUnsupportedExpected() is a lower bound: the amendment actually
// activates at the first flag ledger at or after it, so the server can
// legitimately still be running an hour past it. That is the most
// dangerous state, not the safest, and must stop the server.
BEAST_EXPECT(env.close(expected + NetClock::duration{3600}));
BEAST_EXPECT(env.app().getOPs().isAmendmentBlocked());
BEAST_EXPECT(!env.app().getOPs().isAmendmentWarned());
}
void
testBlockedMethods()
{
@@ -250,6 +783,16 @@ public:
void
run() override
{
testReceiptFileHelpers();
testStandaloneDoesNotStop();
testShutdownOnBlock();
testJumpRethrowsParsingError();
testJumpStopsForUnsupportedAmendment();
testWarnsOutsideShutdownWindow();
testUnsupportedAmendmentReporting();
testWarningVisibleWithoutAdmin();
testShutsDownInsideShutdownWindow();
testShutsDownWhenActivationOverdue();
testBlockedMethods();
}
};
@@ -21,7 +21,9 @@
#include <xrpld/app/ledger/InboundLedger.h>
#include <xrpld/app/ledger/InboundLedgers.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/ledger/OpenLedger.h>
#include <xrpld/app/main/Application.h>
#include <xrpld/app/misc/Manifest.h>
#include <xrpld/app/misc/NetworkOPs.h>
#include <xrpld/app/misc/ValidatorList.h>
#include <xrpld/consensus/LedgerTiming.h>
@@ -31,6 +33,7 @@
#include <xrpl/basics/Log.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/protocol/Feature.h>
#include <memory>
#include <mutex>
#include <thread>
@@ -176,6 +179,25 @@ handleNewValidation(
auto const& hash = val->getLedgerHash();
auto const seq = val->getFieldU32(sfLedgerSequence);
// A validator that rotated its ephemeral key while this node was not
// listening signs with a key no held manifest mentions, which is
// indistinguishable from a validator this node has never heard of. Every
// manifest published on-chain is written at its ephemeral keylet as well
// as its master one, so that binding is recoverable in a single read.
//
// Done here rather than left to the next consensus round, because
// ManifestCache::applyLedger() runs there against the master keys already
// trusted and so cannot resolve a key whose master is what is missing.
// Resolving now also means this validation is classified in the round it
// arrived in rather than written off along with every other one until the
// trusted set is next recomputed.
if (app.validatorManifests().getMasterKey(signingKey) == signingKey)
{
if (auto const view = app.openLedger().current();
view && view->rules().enabled(featureOnChainManifests))
app.validatorManifests().applyLedgerSigningKey(*view, signingKey);
}
// Ensure validation is marked as trusted if signer currently trusted
auto masterKey = app.validators().getTrustedKey(signingKey);
+1 -2
View File
@@ -2743,8 +2743,7 @@ HookAPI::set_state_cache(
if (modified && stateMap.modified_entry_count >= max_state_modifications)
return Unexpected(TOO_MANY_STATE_MODIFICATIONS);
bool const createNamespace = view.rules().enabled(fixXahauV1) &&
!view.exists(keylet::hookStateDir(acc, ns));
bool const createNamespace = !view.exists(keylet::hookStateDir(acc, ns));
if (stateMap.find(acc) == stateMap.end())
{
+66 -104
View File
@@ -9,7 +9,9 @@
#include <xrpld/app/tx/detail/NFTokenUtils.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/st.h>
#include <xrpl/protocol/tokens.h>
@@ -68,9 +70,6 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
return rv.read(keylet::nftoffer(*id));
};
bool const fixV1 = rv.rules().enabled(fixXahauV1);
bool const fixV2 = rv.rules().enabled(fixXahauV2);
switch (tt)
{
case ttCRON: {
@@ -119,7 +118,7 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
case ttIMPORT: {
if (tx.isFieldPresent(sfIssuer))
ADD_TSH(tx.getAccountID(sfIssuer), fixV2 ? tshWEAK : tshSTRONG);
ADD_TSH(tx.getAccountID(sfIssuer), tshWEAK);
break;
}
@@ -144,34 +143,13 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
break;
// pass, already a TSH
// new logic
if (fixV1)
{
// the owner burns their token, and the issuer is a weak TSH
if (*otxnAcc == owner && rv.exists(keylet::account(issuer)))
ADD_TSH(issuer, tshWEAK);
// the issuer burns the owner's token, and the owner is a weak
// TSH
else if (rv.exists(keylet::account(owner)))
ADD_TSH(owner, tshWEAK);
break;
}
// old logic
{
if (*otxnAcc == owner)
{
// the owner burns their token, and the issuer is a weak TSH
ADD_TSH(issuer, tshSTRONG);
}
else
{
// the issuer burns the owner's token, and the owner is a
// weak TSH
ADD_TSH(owner, tshSTRONG);
}
}
// the owner burns their token, and the issuer is a weak TSH
if (*otxnAcc == owner && rv.exists(keylet::account(issuer)))
ADD_TSH(issuer, tshWEAK);
// the issuer burns the owner's token, and the owner is a weak
// TSH
else if (rv.exists(keylet::account(owner)))
ADD_TSH(owner, tshWEAK);
break;
}
@@ -205,15 +183,12 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
case ttURITOKEN_MINT: {
// destination is a strong tsh
if (fixV2 && tx.isFieldPresent(sfDestination))
if (tx.isFieldPresent(sfDestination))
ADD_TSH(tx.getAccountID(sfDestination), tshSTRONG);
break;
}
case ttURITOKEN_CANCEL_SELL_OFFER: {
if (!fixV2)
break;
Keylet const id{ltURI_TOKEN, tx.getFieldH256(sfURITokenID)};
if (!rv.exists(id))
return {};
@@ -384,61 +359,38 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
case ttESCROW_CANCEL:
case ttESCROW_FINISH: {
// new logic
if (fixV1)
{
if (!tx.isFieldPresent(sfOwner))
return {};
if (!tx.isFieldPresent(sfOwner))
return {};
AccountID const owner = tx.getAccountID(sfOwner);
AccountID const owner = tx.getAccountID(sfOwner);
bool const hasSeq = tx.isFieldPresent(sfOfferSequence);
bool const hasID = tx.isFieldPresent(sfEscrowID);
if (!hasSeq && !hasID)
return {};
bool const hasSeq = tx.isFieldPresent(sfOfferSequence);
bool const hasID = tx.isFieldPresent(sfEscrowID);
if (!hasSeq && !hasID)
return {};
Keylet kl = hasSeq
? keylet::escrow(owner, tx.getFieldU32(sfOfferSequence))
: Keylet(ltESCROW, tx.getFieldH256(sfEscrowID));
Keylet kl = hasSeq
? keylet::escrow(owner, tx.getFieldU32(sfOfferSequence))
: Keylet(ltESCROW, tx.getFieldH256(sfEscrowID));
auto escrow = rv.read(kl);
auto escrow = rv.read(kl);
if (!escrow ||
escrow->getFieldU16(sfLedgerEntryType) != ltESCROW)
return {};
if (!escrow || escrow->getFieldU16(sfLedgerEntryType) != ltESCROW)
return {};
// this should always be the same as owner, but defensively...
AccountID const src = escrow->getAccountID(sfAccount);
AccountID const dst = escrow->getAccountID(sfDestination);
// this should always be the same as owner, but defensively...
AccountID const src = escrow->getAccountID(sfAccount);
AccountID const dst = escrow->getAccountID(sfDestination);
// the source account is a strong transacitonal stakeholder for
// fin and can
ADD_TSH(src, tshSTRONG);
// the source account is a strong transacitonal stakeholder for
// fin and can
ADD_TSH(src, tshSTRONG);
// the dest acc is a strong tsh for fin and weak for can
if (src != dst)
ADD_TSH(dst, tt == ttESCROW_FINISH ? tshSTRONG : tshWEAK);
// the dest acc is a strong tsh for fin and weak for can
if (src != dst)
ADD_TSH(dst, tt == ttESCROW_FINISH ? tshSTRONG : tshWEAK);
break;
}
// old logic
{
if (!tx.isFieldPresent(sfOwner) ||
!tx.isFieldPresent(sfOfferSequence))
return {};
auto escrow = rv.read(keylet::escrow(
tx.getAccountID(sfOwner), tx.getFieldU32(sfOfferSequence)));
if (!escrow)
return {};
ADD_TSH(escrow->getAccountID(sfAccount), tshSTRONG);
ADD_TSH(
escrow->getAccountID(sfDestination),
tt == ttESCROW_FINISH ? tshSTRONG : tshWEAK);
break;
}
break;
}
case ttPAYCHAN_FUND:
@@ -513,6 +465,26 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
case ttCRON_SET: {
break;
}
case ttMANIFEST_SET: {
// The ephemeral key's logical account, meaning the r-address its
// public key hashes to, is a weak stake holder: the manifest names
// that key but nothing is done to the account, so it may observe
// but not rollback. Usually no such account exists, in which case
// nothing executes.
if (!tx.isFieldPresent(sfManifest))
break;
STObject const& man =
const_cast<STTx&>(tx).getField(sfManifest).downcast<STObject>();
if (!man.isFieldPresent(sfSigningPubKey))
break;
auto const spk = man.getFieldVL(sfSigningPubKey);
if (publicKeyType(makeSlice(spk)))
ADD_TSH(calcAccountID(PublicKey(makeSlice(spk))), tshWEAK);
break;
}
case ttAMM_CREATE:
case ttAMM_DEPOSIT:
case ttAMM_WITHDRAW:
@@ -1344,13 +1316,9 @@ DEFINE_HOOK_FUNCTION(
auto const key = make_state_key(
std::string_view{(const char*)(memory + kread_ptr), (size_t)kread_len});
if (view.rules().enabled(fixXahauV1))
{
auto const sleAccount = view.peek(hookCtx.result.accountKeylet);
if (!sleAccount)
// should return hook_api::hook_return_code
return static_cast<hook_api::hook_return_code>(tefINTERNAL);
}
if (!view.exists(hookCtx.result.accountKeylet))
// should return hook_api::hook_return_code
return static_cast<hook_api::hook_return_code>(tefINTERNAL);
if (!key)
return INTERNAL_ERROR;
@@ -1560,7 +1528,6 @@ hook::finalizeHookResult(
}
}
bool const fixV2 = applyCtx.view().rules().enabled(fixXahauV2);
// add a metadata entry for this hook execution result
{
STObject meta{sfHookExecution};
@@ -1589,18 +1556,14 @@ hook::finalizeHookResult(
meta.setFieldU16(sfHookStateChangeCount, hookResult.changedStateCount);
meta.setFieldH256(sfHookHash, hookResult.hookHash);
// add informational flags in fix2
if (fixV2)
{
uint32_t flags = 0;
if (hookResult.isStrong)
flags |= hefSTRONG;
if (hookResult.isCallback)
flags |= hefCALLBACK;
if (hookResult.executeAgainAsWeak)
flags |= hefDOAAW;
meta.setFieldU32(sfFlags, flags);
}
uint32_t flags = 0;
if (hookResult.isStrong)
flags |= hefSTRONG;
if (hookResult.isCallback)
flags |= hefCALLBACK;
if (hookResult.executeAgainAsWeak)
flags |= hefDOAAW;
meta.setFieldU32(sfFlags, flags);
avi.addHookExecutionMetaData(std::move(meta));
}
@@ -1614,8 +1577,7 @@ hook::finalizeHookResult(
meta.setFieldH256(sfHookHash, hookResult.hookHash);
meta.setAccountID(sfHookAccount, hookResult.account);
meta.setFieldH256(sfEmittedTxnID, etxnid);
if (fixV2)
meta.setFieldH256(sfEmitNonce, enonce);
meta.setFieldH256(sfEmitNonce, enonce);
avi.addHookEmissionMetaData(std::move(meta));
}
}
+37 -3
View File
@@ -289,14 +289,49 @@ LedgerMaster::setValidLedger(std::shared_ptr<Ledger const> const& l)
app_.getSHAMapStore().onLedgerClosed(getValidatedLedger());
mLedgerHistory.validatedLedger(l, consensusHash);
app_.getAmendmentTable().doValidatedLedger(l);
using namespace std::chrono_literals;
// How far ahead of the expected activation time to stop the server.
// firstUnsupportedExpected() is only a lower bound on activation: the
// amendment goes live at the first flag ledger at or after that time, so
// stopping early is harmless while stopping late risks being handed a
// ledger we cannot deserialize.
constexpr auto amendmentShutdownLeadTime = 5min;
if (!app_.getOPs().isBlocked())
{
auto const firstUnsupported =
app_.getAmendmentTable().firstUnsupportedExpected();
if (app_.getAmendmentTable().hasUnsupportedEnabled())
{
JLOG(m_journal.error()) << "One or more unsupported amendments "
"activated: server blocked.";
app_.getOPs().setAmendmentBlocked();
}
else if (
firstUnsupported &&
app_.timeKeeper().closeTime() + amendmentShutdownLeadTime >=
*firstUnsupported)
{
// Activation is imminent, or the expected time has already passed
// and we are only waiting on the next flag ledger. Shut down now,
// while we can still deserialize the ledgers we are handed.
//
// This is deliberately checked on every validated ledger rather
// than only on flag ledgers: the lead time above is much shorter
// than the flag ledger interval, so a flag-ledger-only check would
// usually skip straight over the window.
//
// The comparison must not be written as (*first - now), because
// NetClock::rep is unsigned and wraps once the expected time is in
// the past -- which is the most dangerous case, not the safest.
JLOG(m_journal.error())
<< "Unsupported amendment expected to activate at "
<< to_string(*firstUnsupported) << ". Shutting down.";
app_.getOPs().setAmendmentBlocked();
}
else if (!app_.getOPs().isAmendmentWarned() || l->isFlagLedger())
{
// Amendments can lose majority, so re-check periodically (every
@@ -308,12 +343,11 @@ LedgerMaster::setValidLedger(std::shared_ptr<Ledger const> const& l)
// this message may be logged more than once per session, because
// the node will otherwise function normally, and this gives
// operators an opportunity to see and resolve the warning.
if (auto const first =
app_.getAmendmentTable().firstUnsupportedExpected())
if (firstUnsupported)
{
JLOG(m_journal.error()) << "One or more unsupported amendments "
"reached majority. Upgrade before "
<< to_string(*first)
<< to_string(*firstUnsupported)
<< " to prevent your server from "
"becoming amendment blocked.";
app_.getOPs().setAmendmentWarned();
+60
View File
@@ -59,6 +59,7 @@
#include <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpld/shamap/NodeFamily.h>
#include <xrpl/basics/ByteUtilities.h>
#include <xrpl/basics/FileUtilities.h>
#include <xrpl/basics/ResolverAsio.h>
#include <xrpl/basics/random.h>
#include <xrpl/basics/safe_cast.h>
@@ -2372,6 +2373,65 @@ Application::Application() : beast::PropertyStream::Source("app")
//------------------------------------------------------------------------------
boost::filesystem::path
amendmentBlockedFilePath(Config const& config)
{
return config.CONFIG_DIR / "README_AMENDMENT_BLOCKED";
}
boost::system::error_code
writeAmendmentBlockedFile(
Config const& config,
std::vector<std::string> const& amendments)
{
using namespace std::chrono;
std::ostringstream ss;
ss << "XAHAUD STOPPED: UPGRADE REQUIRED\n"
<< "\n"
<< "Stopped at: "
<< to_string_iso(time_point_cast<seconds>(system_clock::now())) << "\n"
<< "This build: " << BuildInfo::getVersionString() << "\n"
<< "\n";
if (amendments.empty())
{
ss << "One or more network amendments are not supported by this "
"build.\n";
}
else
{
ss << "Amendments this build does not support:\n";
for (auto const& amendment : amendments)
ss << " " << amendment << "\n";
}
ss << "\n"
<< "The network has moved to rules this build does not implement, so "
"the\n"
<< "server stopped rather than keep serving ledgers it cannot read.\n"
<< "\n"
<< "To get back in sync:\n"
<< "1. Upgrade xahaud to a version that supports the amendments "
"above.\n"
<< "2. Start xahaud again. This file is removed automatically on "
"start.\n"
<< "\n"
<< "Starting this build again without upgrading will stop the server "
"again\n"
<< "and rewrite this file. Nothing needs to be deleted by hand.\n";
boost::system::error_code ec;
writeFileContents(ec, amendmentBlockedFilePath(config), ss.str());
return ec;
}
bool
removeAmendmentBlockedFile(Config const& config, boost::system::error_code& ec)
{
return boost::filesystem::remove(amendmentBlockedFilePath(config), ec);
}
std::unique_ptr<Application>
make_Application(
std::unique_ptr<Config> config,
+26
View File
@@ -27,9 +27,14 @@
#include <xrpl/beast/utility/PropertyStream.h>
#include <xrpl/protocol/Protocol.h>
#include <boost/asio.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/program_options.hpp>
#include <boost/system/error_code.hpp>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <vector>
namespace ripple {
@@ -279,6 +284,27 @@ make_Application(
std::unique_ptr<Logs> logs,
std::unique_ptr<TimeKeeper> timeKeeper);
/** Location of the receipt left behind when the server stops because it does
not support a network amendment. */
boost::filesystem::path
amendmentBlockedFilePath(Config const& config);
/** Write the amendment-blocked receipt: a record for the operator of when the
server stopped and which amendments it could not support. `amendments`
holds one already-rendered line per unsupported amendment, and may be
empty. Best effort -- any error is returned rather than thrown, and the
caller is expected to continue shutting down either way. */
boost::system::error_code
writeAmendmentBlockedFile(
Config const& config,
std::vector<std::string> const& amendments);
/** Remove any amendment-blocked receipt left behind by a previous run.
Returns true if a receipt was present and has been removed; sets `ec` if
removal was attempted and failed. */
bool
removeAmendmentBlockedFile(Config const& config, boost::system::error_code& ec);
} // namespace ripple
#endif
+25
View File
@@ -809,6 +809,31 @@ run(int argc, char** argv)
// No arguments. Run server.
if (!vm.count("parameters"))
{
// Clear any receipt left by a previous amendment-blocked shutdown. It
// records why the server stopped; it is not a lock. If this build
// still does not support the amendment it will stop again and write a
// fresh one, so there is nothing for the operator to delete by hand
// and no way for a stale receipt to keep a working build down.
// Standalone does not write receipts, so it does not clear them
// either, leaving the file readable for diagnosis.
if (!config->standalone())
{
boost::system::error_code ec;
auto const blockedFile = amendmentBlockedFilePath(*config);
if (removeAmendmentBlockedFile(*config, ec))
{
JLOG(logs->journal("Application").warn())
<< "Removed amendment-blocked receipt " << blockedFile
<< " left by a previous run.";
}
else if (ec)
{
JLOG(logs->journal("Application").warn())
<< "Could not remove amendment-blocked receipt "
<< blockedFile << ": " << ec.message();
}
}
// TODO: this comment can be removed in a future release -
// say 1.7 or higher
if (config->had_trailing_comments())
+18
View File
@@ -27,6 +27,7 @@
#include <xrpl/protocol/STValidation.h>
#include <optional>
#include <vector>
namespace ripple {
@@ -50,6 +51,17 @@ public:
VoteBehavior const vote;
};
/** An amendment seen on the network that this server has no code
support for. */
struct UnsupportedAmendment
{
uint256 id;
/** The time the amendment is expected to activate. Unset if it is
already active. */
std::optional<NetClock::time_point> expected;
};
virtual ~AmendmentTable() = default;
virtual uint256
@@ -80,6 +92,12 @@ public:
virtual std::optional<NetClock::time_point>
firstUnsupportedExpected() const = 0;
/** Amendments this server does not support that are already enabled, or
that have reached majority and are expected to activate. Ordered by
amendment id. Intended for operator-facing diagnostics. */
virtual std::vector<UnsupportedAmendment>
unsupportedAmendments() const = 0;
virtual Json::Value
getJson(bool isAdmin) const = 0;
+215
View File
@@ -25,12 +25,18 @@
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SecretKey.h>
#include <algorithm>
#include <atomic>
#include <mutex>
#include <optional>
#include <shared_mutex>
#include <string>
#include <vector>
namespace ripple {
class ReadView;
/*
Validator key manifests
-----------------------
@@ -172,6 +178,16 @@ deserializeManifest(
return deserializeManifest(makeSlice(s), journal);
}
inline std::optional<Manifest>
deserializeManifest(
STObject const& st,
beast::Journal journal = beast::Journal(beast::Journal::getNullSink()))
{
Serializer s;
st.add(s);
return deserializeManifest(makeSlice(s.peekData()), journal);
}
template <
class T,
class = std::enable_if_t<
@@ -264,9 +280,60 @@ private:
/** Master public keys stored by current ephemeral public key. */
hash_map<PublicKey, PublicKey> signingToMasterKeys_;
/** Master keys always offered to a peer, whatever their recency.
Set by pin(); in practice the master keys on the configured validator
lists, which are the manifests consensus actually depends on.
*/
hash_set<PublicKey> pinned_;
/** Recency of use, keyed by master public key.
The structure of this map is guarded by mutex_ exactly as map_ is:
entries are created next to it in applyManifest() and are never
removed. The counters themselves are atomic, so recording a hit is a
write to an atomic rather than a structural modification and is legal
while only a shared lock is held.
This is deliberately not a second mutex. A second mutex would have to
be ordered against mutex_, and that ordering would be an unenforced
invariant that any future caller could invert.
*/
hash_map<PublicKey, std::atomic<std::uint64_t>> mutable lastUsed_;
std::atomic<std::uint64_t> mutable tick_{0};
/** Ephemeral keys already probed against the ledger, and where.
Bounds the reads driven by incoming validations to one per key per
ledger. A key that resolves does not come back here: the mapping then
lives in signingToMasterKeys_ and applyLedgerSigningKey() answers from
it before reaching this map.
Unlike lastUsed_ this is written structurally, so it is guarded by
mutex_ in exclusive mode.
*/
hash_map<PublicKey, std::uint32_t> probed_;
/** Record that a manifest was looked up.
@pre The caller holds mutex_, shared or exclusive.
*/
void
touch(PublicKey const& masterKey) const;
std::atomic<std::uint32_t> seq_{0};
public:
/** Ceiling on the unpinned manifests offered to a newly connected peer. */
static constexpr std::size_t gossipLimit = 64;
/** Ceiling on remembered ephemeral key probes before they are dropped.
A cache of negatives, so dropping it costs at most one extra ledger
read per key.
*/
static constexpr std::size_t probeLimit = 4096;
explicit ManifestCache(
beast::Journal j = beast::Journal(beast::Journal::getNullSink()))
: j_(j)
@@ -355,6 +422,90 @@ public:
ManifestDisposition
applyManifest(Manifest m);
/** Set the master keys that are always offered to peers.
Replaces any previous set. Bumps sequence() when the set actually
changes, so a cached gossip message built from it is rebuilt.
@param keys Master public keys to pin
@par Thread Safety
May be called concurrently
*/
void
pin(hash_set<PublicKey> keys);
/** Returns the sequence and serialized form of a held manifest.
Unlike getManifest() and getSequence(), a revoked master key is
reported rather than skipped. Those two answer "what should I trust",
for which a revocation is correctly nothing; a caller republishing what
it holds needs the revocation most of all.
@param pk Master public key
@par Thread Safety
May be called concurrently
*/
std::optional<std::pair<std::uint32_t, std::string>>
getRawManifest(PublicKey const& pk) const;
/** Ingest manifests published on-ledger.
Reads keylet::manifest() for each supplied master key, reconstructs any
manifest found and feeds it through applyManifest(), so an on-chain
manifest is subject to exactly the same staleness, revocation and
key-reuse rules -- and the same signature check -- as one arriving by
peer gossip or in a published list. It is a third source of manifests,
not a more trusted one.
Probes a known key set rather than scanning the ledger's transactions:
this costs one SHAMap read per key, and picks up manifests published in
ledgers this node never saw.
@param view Ledger to read from
@param masterKeys Master public keys to probe for
@return the number of manifests newly accepted
@par Thread Safety
May be called concurrently
*/
std::size_t
applyLedger(ReadView const& view, hash_set<PublicKey> const& masterKeys);
/** Resolve an ephemeral signing key against a manifest published on-ledger.
applyLedger() probes a known master key set, which cannot help a key
this node has no manifest for: the master key is exactly what is
missing. SetManifest writes a second copy of every manifest keyed by
its ephemeral key, so that case is one read rather than a search.
Anything found is fed through applyManifest(), so an on-chain manifest
faces the same signature check and the same staleness, revocation and
key-reuse rules as one arriving by gossip. The ledger is a transport
here, not an authority: the answer is read back out of the cache rather
than taken from the ledger object, because applyManifest() may decline
it.
A key is probed at most once per ledger, and a key that resolves is
answered from the cache thereafter without any ledger read.
@param view Ledger to read from
@param signingKey Ephemeral public key to resolve
@return the master key now associated with signingKey, if any
@par Thread Safety
May be called concurrently
*/
std::optional<PublicKey>
applyLedgerSigningKey(ReadView const& view, PublicKey const& signingKey);
/** Populate manifest cache with manifests in database and config.
@param dbCon Database connection with dbTable
@@ -460,6 +611,70 @@ public:
f(manifest);
}
}
/** Invokes the callback for the manifests worth offering a new peer.
Offering the whole cache means offering everything the node has ever
seen, which grows without bound and is mostly of no use to the peer.
This offers the pinned set plus up to gossipLimit further manifests,
most recently used first. A manifest left out still reaches the peer by
ordinary relay if it turns out to be needed.
@note Do not call ManifestCache member functions from within the
callback. This can re-lock the mutex from the same thread, which is UB.
@note Do not write ManifestCache member variables from
within the callback. This can lead to data races.
@param pf Pre-function called with the maximum number of times f will be
called (useful for memory allocations)
@param f Function called for each manifest
@par Thread Safety
May be called concurrently
*/
template <class PreFun, class EachFun>
void
for_each_gossip_manifest(PreFun&& pf, EachFun&& f) const
{
std::shared_lock lock{mutex_};
// Rank the unpinned entries by recency and keep the head. Pointers
// into map_ stay valid: it is node based and the shared lock is held
// throughout.
std::vector<std::pair<std::uint64_t, PublicKey const*>> ranked;
ranked.reserve(map_.size());
for (auto const& [key, manifest] : map_)
{
(void)manifest;
if (pinned_.count(key))
continue;
auto const used = lastUsed_.find(key);
ranked.emplace_back(
used == lastUsed_.end()
? 0
: used->second.load(std::memory_order_relaxed),
&key);
}
auto const keep = std::min(gossipLimit, ranked.size());
std::partial_sort(
ranked.begin(),
ranked.begin() + keep,
ranked.end(),
[](auto const& a, auto const& b) { return a.first > b.first; });
// An upper bound: a pinned key need not have a manifest yet.
pf(pinned_.size() + keep);
for (auto const& key : pinned_)
if (auto const iter = map_.find(key); iter != map_.end())
f(iter->second);
for (std::size_t i = 0; i < keep; ++i)
f(map_.find(*ranked[i].second)->second);
}
};
} // namespace ripple
+200 -4
View File
@@ -28,11 +28,13 @@
#include <xrpld/app/ledger/OpenLedger.h>
#include <xrpld/app/ledger/OrderBookDB.h>
#include <xrpld/app/ledger/TransactionMaster.h>
#include <xrpld/app/main/Application.h>
#include <xrpld/app/main/LoadManager.h>
#include <xrpld/app/misc/AmendmentTable.h>
#include <xrpld/app/misc/DeliverMax.h>
#include <xrpld/app/misc/HashRouter.h>
#include <xrpld/app/misc/LoadFeeTrack.h>
#include <xrpld/app/misc/Manifest.h>
#include <xrpld/app/misc/NetworkOPs.h>
#include <xrpld/app/misc/StateAccounting.h>
#include <xrpld/app/misc/Transaction.h>
@@ -42,6 +44,7 @@
#include <xrpld/app/misc/detail/AccountTxPaging.h>
#include <xrpld/app/rdb/backend/SQLiteDatabase.h>
#include <xrpld/app/tx/apply.h>
#include <xrpld/app/tx/detail/SetManifest.h> // makeSetManifestTx
#include <xrpld/consensus/Consensus.h>
#include <xrpld/consensus/ConsensusParms.h>
#include <xrpld/overlay/Cluster.h>
@@ -54,6 +57,7 @@
#include <xrpld/rpc/MPTokenIssuanceID.h>
#include <xrpld/rpc/ServerHandler.h>
#include <xrpld/rpc/detail/UDPInfoSub.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/UptimeClock.h>
#include <xrpl/basics/mulDiv.h>
#include <xrpl/basics/safe_cast.h>
@@ -68,6 +72,7 @@
#include <xrpl/protocol/MultiApiJson.h>
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/resource/Fees.h>
#include <xrpl/resource/ResourceManager.h>
@@ -311,6 +316,8 @@ private:
switchLastClosedLedger(std::shared_ptr<Ledger const> const& newLCL);
bool
checkLastClosedLedger(const Overlay::PeerSequence&, uint256& networkClosed);
void
publishNewerManifests(ReadView const& ledger);
public:
bool
@@ -1156,6 +1163,72 @@ NetworkOPsImp::submitTransaction(std::shared_ptr<STTx const> const& iTrans)
});
}
void
NetworkOPsImp::publishNewerManifests(ReadView const& ledger)
{
for (auto const& pk : app_.validators().getTrustedMasterKeys())
{
// Only for validators that have opted in by publishing on-ledger
// already. Submitting spends the master key account's balance, so an
// account that has never used the feature is left alone.
auto const sleMan = ledger.read(keylet::manifest(pk));
if (!sleMan)
continue;
auto const held = app_.validatorManifests().getRawManifest(pk);
if (!held || held->first <= sleMan->getFieldU32(sfSequence))
continue;
auto const hex = makeSetManifestTx(
makeSlice(held->second),
app_.config().NETWORK_ID,
*app_.openLedger().current(),
app_.journal("Manifest"));
auto const blob = hex ? strUnHex(*hex) : std::nullopt;
if (!blob || blob->empty())
continue;
std::shared_ptr<STTx const> stTx;
std::string reason;
std::shared_ptr<Transaction> tx;
try
{
SerialIter sit{makeSlice(*blob)};
stTx = std::make_shared<STTx const>(std::ref(sit));
tx = std::make_shared<Transaction>(stTx, reason, app_);
}
catch (std::exception const& e)
{
JLOG(m_journal.warn())
<< "publishNewerManifests: " << e.what() << " " << reason;
continue;
}
if (tx->getStatus() != NEW)
continue;
// The master key account pays. Skip rather than submit something that
// can only fail: this runs every ledger, so an unfunded validator
// would otherwise be retried forever.
auto const sleAcct = ledger.read(keylet::account(calcAccountID(pk)));
if (!sleAcct ||
sleAcct->getFieldAmount(sfBalance).xrp() < (*stTx)[sfFee].xrp())
continue;
JLOG(m_journal.info())
<< "publishNewerManifests: publishing manifest seq " << held->first
<< " for " << toBase58(TokenType::NodePublic, pk);
// Submitted from the job queue because this runs on the consensus
// thread, which must not block on transaction processing.
m_job_queue.addJob(jtTRANSACTION, "publishManifest", [this, tx]() {
auto t = tx;
processTransaction(t, false, false, FailHard::no);
});
}
}
void
NetworkOPsImp::processTransaction(
std::shared_ptr<Transaction>& transaction,
@@ -1629,11 +1702,68 @@ NetworkOPsImp::isAmendmentBlocked()
return amendmentBlocked_;
}
// Render the unsupported amendments for the operator-facing receipt. This is
// the only identification available: an amendment this build does not support
// has no name here, so the id is what the operator matches against the
// release notes.
static std::vector<std::string>
describeUnsupportedAmendments(AmendmentTable const& table)
{
std::vector<std::string> lines;
for (auto const& amendment : table.unsupportedAmendments())
{
std::ostringstream ss;
ss << to_string(amendment.id);
if (amendment.expected)
ss << " (expected to activate "
<< to_string_iso(*amendment.expected) << ")";
else
ss << " (already active)";
lines.push_back(ss.str());
}
return lines;
}
void
NetworkOPsImp::setAmendmentBlocked()
{
amendmentBlocked_ = true;
// Idempotent: this is reached from Change::applyAmendment (i.e. from the
// transaction application path, which is not guarded by isBlocked()) as
// well as from LedgerMaster::setValidLedger. Writing the receipt and
// logging once per process is enough, and it keeps the synchronous file
// write out of any subsequent ledger apply.
if (amendmentBlocked_.exchange(true))
return;
setMode(OperatingMode::CONNECTED);
if (!app_.config().standalone())
{
auto const blockedFile = amendmentBlockedFilePath(app_.config());
if (auto const ec = writeAmendmentBlockedFile(
app_.config(),
describeUnsupportedAmendments(app_.getAmendmentTable())))
{
JLOG(m_journal.fatal())
<< "Could not write amendment-blocked receipt " << blockedFile
<< ": " << ec.message();
}
else
{
JLOG(m_journal.fatal())
<< "Amendment-blocked receipt written to " << blockedFile;
}
JLOG(m_journal.fatal())
<< "This version of xahaud does not support a network amendment. "
"The amendment will activate soon or is already active. "
"The server will stop. Upgrade xahaud before you restart the "
"server.";
app_.signalStop(
"Unsupported network amendment. Upgrade xahaud before you restart "
"the server. Details: " +
blockedFile.string());
}
}
inline bool
@@ -1779,6 +1909,27 @@ NetworkOPsImp::checkLastClosedLedger(
return true;
}
// True if `view` enables an amendment this binary does not implement. Reads
// only the amendments ledger entry, so it is safe to call once transaction
// deserialization is already known to be failing.
static bool
ledgerHasUnsupportedAmendments(
AmendmentTable const& table,
ReadView const& view)
{
auto const sle = view.read(keylet::amendments());
if (!sle || !sle->isFieldPresent(sfAmendments))
return false;
for (auto const& amendment : sle->getFieldV256(sfAmendments))
{
if (!table.isSupported(amendment))
return true;
}
return false;
}
void
NetworkOPsImp::switchLastClosedLedger(
std::shared_ptr<Ledger const> const& newLCL)
@@ -1789,8 +1940,32 @@ NetworkOPsImp::switchLastClosedLedger(
clearNeedNetworkLedger();
// Update fee computations.
app_.getTxQ().processClosedLedger(app_, *newLCL, true);
// Update fee computations. May throw if the ledger contains
// transactions with fields unknown to this binary (e.g. after an
// unsupported amendment activates). Catch to allow graceful shutdown.
try
{
app_.getTxQ().processClosedLedger(app_, *newLCL, true);
}
catch (std::runtime_error const& e)
{
// Do not decide this on amendmentBlocked_ alone. A JUMP can happen
// before any validated ledger has been processed -- e.g. immediately
// after a restart, which is precisely the case that crashed -- so the
// flag may still be clear here. Ask the ledger itself instead.
// Anything else is a real bug and must propagate.
if (!amendmentBlocked_ &&
!ledgerHasUnsupportedAmendments(app_.getAmendmentTable(), *newLCL))
throw;
JLOG(m_journal.error()) << "Failed to process closed ledger "
<< newLCL->info().seq << ": " << e.what();
// No-op if we are already blocked; otherwise this starts the
// shutdown that should have been started before activation.
setAmendmentBlocked();
return;
}
// Caller must own master lock
{
@@ -1874,6 +2049,23 @@ NetworkOPsImp::beginConsensus(
if (prevLedger->rules().enabled(featureNegativeUNL))
app_.validators().setNegativeUNL(prevLedger->negativeUNL());
// Pull in any manifests published on-ledger before the trusted set is
// recomputed, so a validator that rotated its ephemeral key on-chain is
// resolved to the new signing key in this same round. The master keys come
// from the published lists, so this needs no bootstrap: only the ephemeral
// half of the mapping ever comes from a manifest.
if (prevLedger->rules().enabled(featureOnChainManifests))
{
app_.validatorManifests().applyLedger(
*prevLedger, app_.validators().getTrustedMasterKeys());
// The reverse of applyLedger above. Manifests reach us by peer gossip
// and in published validator lists, both of which can arrive before
// the validator gets around to publishing on-chain, so the cache may
// hold something newer than the ledger does.
publishNewerManifests(*prevLedger);
}
TrustChanges const changes = app_.validators().updateTrusted(
app_.getValidations().getCurrentNodeIDs(),
closingInfo.parentCloseTime,
@@ -1881,6 +2073,10 @@ NetworkOPsImp::beginConsensus(
app_.overlay(),
app_.getHashRouter());
// Pin the trusted master keys so they are always offered to a new peer and
// cannot be crowded out of the gossip set by more recently used manifests.
app_.validatorManifests().pin(app_.validators().getTrustedMasterKeys());
if (!changes.added.empty() || !changes.removed.empty())
{
app_.getValidations().trustChanged(changes.added, changes.removed);
@@ -2449,7 +2645,7 @@ NetworkOPsImp::getServerInfo(bool human, bool admin, bool counters)
"may be incorrectly configured or some [validator_list_sites] "
"may be unreachable.";
}
if (admin && isAmendmentWarned())
if (isAmendmentWarned())
{
Json::Value& w = warnings.append(Json::objectValue);
w[jss::id] = warnRPC_UNSUPPORTED_MAJORITY;
@@ -432,6 +432,11 @@ private:
// will be enabled.
std::optional<NetClock::time_point> firstUnsupportedExpected_;
// Unsupported amendments that have reached majority, and the time each
// is expected to activate. Recomputed alongside
// firstUnsupportedExpected_, so it clears when majority is lost.
std::vector<std::pair<uint256, NetClock::time_point>> unsupportedMajority_;
beast::Journal const j_;
// Database which persists veto/unveto vote
@@ -495,6 +500,9 @@ public:
std::optional<NetClock::time_point>
firstUnsupportedExpected() const override;
std::vector<UnsupportedAmendment>
unsupportedAmendments() const override;
Json::Value
getJson(bool isAdmin) const override;
Json::Value
@@ -807,6 +815,36 @@ AmendmentTableImpl::firstUnsupportedExpected() const
return firstUnsupportedExpected_;
}
std::vector<AmendmentTable::UnsupportedAmendment>
AmendmentTableImpl::unsupportedAmendments() const
{
std::lock_guard lock(mutex_);
std::vector<UnsupportedAmendment> result;
// Already active.
for (auto const& [id, state] : amendmentMap_)
{
if (state.enabled && !state.supported)
result.push_back({id, std::nullopt});
}
// Reached majority, not yet active.
for (auto const& entry : unsupportedMajority_)
{
if (std::none_of(result.begin(), result.end(), [&entry](auto const& u) {
return u.id == entry.first;
}))
result.push_back({entry.first, entry.second});
}
std::sort(result.begin(), result.end(), [](auto const& a, auto const& b) {
return a.id < b.id;
});
return result;
}
std::vector<uint256>
AmendmentTableImpl::doValidation(std::set<uint256> const& enabled) const
{
@@ -967,6 +1005,7 @@ AmendmentTableImpl::doValidatedLedger(
// if it's currently set. If it's not set when the loop is done, then any
// prior unknown amendments have lost majority.
firstUnsupportedExpected_.reset();
unsupportedMajority_.clear();
for (auto const& [hash, time] : majority)
{
AmendmentState& s = add(hash, lock);
@@ -978,6 +1017,7 @@ AmendmentTableImpl::doValidatedLedger(
{
JLOG(j_.info()) << "Unsupported amendment " << hash
<< " reached majority at " << to_string(time);
unsupportedMajority_.emplace_back(hash, time + majorityTime_);
if (!firstUnsupportedExpected_ || firstUnsupportedExpected_ > time)
firstUnsupportedExpected_ = time;
}
+173 -1
View File
@@ -20,10 +20,12 @@
#include <xrpld/app/misc/Manifest.h>
#include <xrpld/app/rdb/Wallet.h>
#include <xrpld/core/DatabaseCon.h>
#include <xrpld/ledger/ReadView.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base64.h>
#include <xrpl/json/json_reader.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Sign.h>
@@ -54,7 +56,6 @@ deserializeManifest(Slice s, beast::Journal journal)
{
if (s.empty())
return std::nullopt;
static SOTemplate const manifestFormat{
// A manifest must include:
// - the master public key
@@ -301,7 +302,10 @@ ManifestCache::getSigningKey(PublicKey const& pk) const
auto const iter = map_.find(pk);
if (iter != map_.end() && !iter->second.revoked())
{
touch(pk);
return iter->second.signingKey;
}
return pk;
}
@@ -313,7 +317,10 @@ ManifestCache::getMasterKey(PublicKey const& pk) const
if (auto const iter = signingToMasterKeys_.find(pk);
iter != signingToMasterKeys_.end())
{
touch(iter->second);
return iter->second;
}
return pk;
}
@@ -349,7 +356,10 @@ ManifestCache::getManifest(PublicKey const& pk) const
auto const iter = map_.find(pk);
if (iter != map_.end() && !iter->second.revoked())
{
touch(pk);
return iter->second.serialized;
}
return std::nullopt;
}
@@ -361,11 +371,169 @@ ManifestCache::revoked(PublicKey const& pk) const
auto const iter = map_.find(pk);
if (iter != map_.end())
{
touch(pk);
return iter->second.revoked();
}
return false;
}
std::optional<std::pair<std::uint32_t, std::string>>
ManifestCache::getRawManifest(PublicKey const& pk) const
{
std::shared_lock lock{mutex_};
if (auto const iter = map_.find(pk); iter != map_.end())
{
touch(pk);
return std::make_pair(iter->second.sequence, iter->second.serialized);
}
return std::nullopt;
}
void
ManifestCache::touch(PublicKey const& masterKey) const
{
// find() rather than operator[]: inserting here would be a structural
// modification, and callers hold mutex_ only in shared mode. The entry is
// created in applyManifest() alongside the manifest itself, so a lookup
// that hit map_ always finds one here too.
if (auto const iter = lastUsed_.find(masterKey); iter != lastUsed_.end())
iter->second.store(++tick_, std::memory_order_relaxed);
}
void
ManifestCache::pin(hash_set<PublicKey> keys)
{
std::lock_guard lock{mutex_};
if (keys == pinned_)
return;
pinned_ = std::move(keys);
// The pinned set is part of what a gossip message contains, so a change to
// it has to invalidate any message cached against this sequence.
++seq_;
}
namespace {
/** Rebuild the manifest a ltMANIFEST object was written from.
The object is a lossless mirror written by SetManifest::doApply, so this
round-trip is byte-identical to the blob the master key signed and verify()
succeeds, or the manifest is discarded. Presence matters: sfVersion is
soeDEFAULT in the manifest format and must not be materialised.
*/
std::optional<Manifest>
manifestFromSLE(SLE const& sle, beast::Journal j)
{
STObject st{sfGeneric};
st.setFieldU32(sfSequence, sle.getFieldU32(sfSequence));
st.setFieldVL(sfPublicKey, sle.getFieldVL(sfPublicKey));
st.setFieldVL(sfMasterSignature, sle.getFieldVL(sfMasterSignature));
for (auto const& sf :
{std::cref(sfSigningPubKey),
std::cref(sfSignature),
std::cref(sfDomain)})
if (sle.isFieldPresent(sf.get()))
st.setFieldVL(sf.get(), sle.getFieldVL(sf.get()));
if (sle.isFieldPresent(sfVersion))
st.setFieldU16(sfVersion, sle.getFieldU16(sfVersion));
return deserializeManifest(st, j);
}
} // namespace
std::size_t
ManifestCache::applyLedger(
ReadView const& view,
hash_set<PublicKey> const& masterKeys)
{
std::size_t accepted = 0;
for (auto const& pk : masterKeys)
{
auto const sle = view.read(keylet::manifest(pk));
if (!sle)
continue;
// Cheap reject before rebuilding: applyManifest() would call this
// stale anyway, and the signature check is the expensive part.
if (auto const seq = getSequence(pk);
seq && *seq >= sle->getFieldU32(sfSequence))
continue;
if (auto mo = manifestFromSLE(*sle, j_); mo &&
applyManifest(std::move(*mo)) == ManifestDisposition::accepted)
++accepted;
}
return accepted;
}
std::optional<PublicKey>
ManifestCache::applyLedgerSigningKey(
ReadView const& view,
PublicKey const& signingKey)
{
auto const held = [this, &signingKey]() -> std::optional<PublicKey> {
std::shared_lock lock{mutex_};
if (auto const iter = signingToMasterKeys_.find(signingKey);
iter != signingToMasterKeys_.end())
{
touch(iter->second);
return iter->second;
}
return std::nullopt;
};
// A manifest for this key may have arrived by gossip or in a published
// list while the caller was deciding to ask.
if (auto const known = held())
return known;
{
std::lock_guard lock{mutex_};
if (probed_.size() >= probeLimit)
probed_.clear();
// One read per key per ledger. Reaching here already cost the sender a
// signature and this node a verification, so the read is not the
// cheapest thing an unknown key can ask for; the cap is to stop
// repeating it, not to stop anyone.
auto const seq = view.info().seq;
auto const [iter, inserted] = probed_.try_emplace(signingKey, seq);
if (!inserted && iter->second == seq)
return std::nullopt;
iter->second = seq;
}
auto const sle = view.read(keylet::manifest(signingKey));
if (!sle)
return std::nullopt;
// Every manifest is written at both its master and its ephemeral keylet,
// so an object here is either the manifest naming signingKey as its
// ephemeral key -- the case worth having -- or the manifest of a master
// key that is what was asked about. Ingesting either is correct, and
// applyManifest() verifies both signatures, so nothing found here can
// assert a binding its key holder did not sign for.
if (auto mo = manifestFromSLE(*sle, j_))
applyManifest(std::move(*mo));
// Only a signing key resolves: a master key is its own master.
return held();
}
ManifestDisposition
ManifestCache::applyManifest(Manifest m)
{
@@ -499,6 +667,10 @@ ManifestCache::applyManifest(Manifest m)
if (!revoked)
signingToMasterKeys_.emplace(*m.signingKey, m.masterKey);
// Kept in step with map_ so touch() never has to insert; see
// lastUsed_.
lastUsed_.try_emplace(m.masterKey, 0);
auto masterKey = m.masterKey;
map_.emplace(std::move(masterKey), std::move(m));
+12 -2
View File
@@ -1953,13 +1953,23 @@ TxQ::tryDirectApply(
const bool isFirstImport = !sleAccount &&
view.rules().enabled(featureImport) && tx->getTxnType() == ttIMPORT;
// A manifest txn is pinned to sfSequence 0 (Transactor::checkSeqProxy), so
// it can never match the account sequence. Direct-apply it like a first
// Import: letting it fall through to the queue would reject it outright on
// sequence rather than hold it. Manifests are therefore exempt from fee
// escalation, since requiredFeeLevel is not consulted for them.
const bool isManifest = view.rules().enabled(featureOnChainManifests) &&
tx->getTxnType() == ttMANIFEST_SET;
const bool bypassQueue = isFirstImport || isManifest;
// Don't attempt to direct apply if the account is not in the ledger.
if (!sleAccount && !isFirstImport)
return {};
std::optional<SeqProxy> txSeqProx;
if (!isFirstImport)
if (!bypassQueue)
{
SeqProxy const acctSeqProx =
SeqProxy::sequence((*sleAccount)[sfSequence]);
@@ -1972,7 +1982,7 @@ TxQ::tryDirectApply(
}
FeeLevel64 const requiredFeeLevel =
isFirstImport ? FeeLevel64{0} : [this, &view, flags]() {
bypassQueue ? FeeLevel64{0} : [this, &view, flags]() {
std::lock_guard lock(mutex_);
return getRequiredFeeLevel(
view, flags, feeMetrics_.getSnapshot(), lock);
+5 -3
View File
@@ -2033,9 +2033,11 @@ ValidatorList::updateTrusted(
trustChanges.added.insert(calcNodeID(val.first));
}
// If there were any changes, we need to update the ephemeral signing
// keys:
if (!trustChanges.added.empty() || !trustChanges.removed.empty())
// Rebuilt on every call rather than only when membership changed: a
// manifest that rotates a validator's ephemeral key changes no membership
// at all, so a membership-gated rebuild would leave trustedSigningKeys_
// stale and the rotated validator's validations would stop counting. This
// is a handful of map lookups over the UNL.
{
trustedSigningKeys_.clear();
+8 -21
View File
@@ -39,26 +39,14 @@ CancelOffer::preflight(PreflightContext const& ctx)
return temINVALID_FLAG;
}
if (ctx.rules.enabled(fixXahauV1))
if ((!ctx.tx.isFieldPresent(sfOfferSequence) &&
!ctx.tx.isFieldPresent(sfOfferID)) ||
(ctx.tx.isFieldPresent(sfOfferSequence) &&
ctx.tx.isFieldPresent(sfOfferID)))
{
if ((!ctx.tx.isFieldPresent(sfOfferSequence) &&
!ctx.tx.isFieldPresent(sfOfferID)) ||
(ctx.tx.isFieldPresent(sfOfferSequence) &&
ctx.tx.isFieldPresent(sfOfferID)))
{
JLOG(ctx.j.trace())
<< "CancelOffer::preflight: invalid sequence or offer id";
return temBAD_SEQUENCE;
}
}
else
{
if (!ctx.tx.isFieldPresent(sfOfferSequence) ||
ctx.tx[sfOfferSequence] == 0)
{
JLOG(ctx.j.trace()) << "CancelOffer::preflight: missing sequence";
return temBAD_SEQUENCE;
}
JLOG(ctx.j.trace())
<< "CancelOffer::preflight: invalid sequence or offer id";
return temBAD_SEQUENCE;
}
return preflight2(ctx);
@@ -112,8 +100,7 @@ CancelOffer::doApply()
else
JLOG(j_.debug()) << "Trying to cancel offer #" << *offerSequence;
bool const fixV1 = view().rules().enabled(fixXahauV1);
if (fixV1 && sleOffer->getFieldU16(sfLedgerEntryType) != ltOFFER)
if (sleOffer->getFieldU16(sfLedgerEntryType) != ltOFFER)
{
JLOG(j_.debug()) << "OfferCancel specified non-offer ledger object";
return tecINTERNAL;
-6
View File
@@ -30,12 +30,6 @@
namespace ripple {
TxConsequences
ClaimReward::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
ClaimReward::preflight(PreflightContext const& ctx)
{
+1 -4
View File
@@ -32,15 +32,12 @@ namespace ripple {
class ClaimReward : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit ClaimReward(ApplyContext& ctx) : Transactor(ctx)
{
}
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
-6
View File
@@ -30,12 +30,6 @@
namespace ripple {
TxConsequences
Cron::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
Cron::preflight(PreflightContext const& ctx)
{
+1 -4
View File
@@ -30,7 +30,7 @@ namespace ripple {
class Cron : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit Cron(ApplyContext& ctx) : Transactor(ctx)
{
@@ -39,9 +39,6 @@ public:
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
-6
View File
@@ -27,12 +27,6 @@
namespace ripple {
TxConsequences
CronSet::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
CronSet::preflight(PreflightContext const& ctx)
{
+1 -4
View File
@@ -29,7 +29,7 @@ namespace ripple {
class CronSet : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit CronSet(ApplyContext& ctx) : Transactor(ctx)
{
@@ -38,9 +38,6 @@ public:
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
+12 -40
View File
@@ -454,23 +454,11 @@ EscrowFinish::preflight(PreflightContext const& ctx)
// sfOfferSequence was changed to optional, so ensure the behaviour is the
// same until amendment passes
if (!ctx.rules.enabled(fixXahauV1))
{
if (!ctx.tx.isFieldPresent(sfOfferSequence))
return temMALFORMED;
if (ctx.tx.isFieldPresent(sfEscrowID) &&
ctx.tx.getFieldU32(sfOfferSequence) != 0)
return temMALFORMED;
}
else
{
if ((!ctx.tx.isFieldPresent(sfEscrowID) &&
!ctx.tx.isFieldPresent(sfOfferSequence)) ||
(ctx.tx.isFieldPresent(sfEscrowID) &&
ctx.tx.isFieldPresent(sfOfferSequence)))
return temMALFORMED;
}
if ((!ctx.tx.isFieldPresent(sfEscrowID) &&
!ctx.tx.isFieldPresent(sfOfferSequence)) ||
(ctx.tx.isFieldPresent(sfEscrowID) &&
ctx.tx.isFieldPresent(sfOfferSequence)))
return temMALFORMED;
return tesSUCCESS;
}
@@ -512,8 +500,6 @@ EscrowFinish::doApply()
std::optional<uint256> escrowID = ctx_.tx[~sfEscrowID];
std::optional<std::uint32_t> offerSequence = ctx_.tx[~sfOfferSequence];
bool const fixV1 = view().rules().enabled(fixXahauV1);
Keylet k = escrowID ? Keylet(ltESCROW, *escrowID)
: keylet::escrow(ctx_.tx[sfOwner], *offerSequence);
@@ -521,7 +507,7 @@ EscrowFinish::doApply()
if (!slep)
return tecNO_TARGET;
if (fixV1 && slep->getFieldU16(sfLedgerEntryType) != ltESCROW)
if (slep->getFieldU16(sfLedgerEntryType) != ltESCROW)
return tecINTERNAL;
AccountID const account = (*slep)[sfAccount];
@@ -740,23 +726,11 @@ EscrowCancel::preflight(PreflightContext const& ctx)
// sfOfferSequence was changed to optional, so ensure the behaviour is the
// same until amendment passes
if (!ctx.rules.enabled(fixXahauV1))
{
if (!ctx.tx.isFieldPresent(sfOfferSequence))
return temMALFORMED;
if (ctx.tx.isFieldPresent(sfEscrowID) &&
ctx.tx.getFieldU32(sfOfferSequence) != 0)
return temMALFORMED;
}
else
{
if ((!ctx.tx.isFieldPresent(sfEscrowID) &&
!ctx.tx.isFieldPresent(sfOfferSequence)) ||
(ctx.tx.isFieldPresent(sfEscrowID) &&
ctx.tx.isFieldPresent(sfOfferSequence)))
return temMALFORMED;
}
if ((!ctx.tx.isFieldPresent(sfEscrowID) &&
!ctx.tx.isFieldPresent(sfOfferSequence)) ||
(ctx.tx.isFieldPresent(sfEscrowID) &&
ctx.tx.isFieldPresent(sfOfferSequence)))
return temMALFORMED;
return preflight2(ctx);
}
@@ -772,8 +746,6 @@ EscrowCancel::doApply()
std::optional<uint256> escrowID = ctx_.tx[~sfEscrowID];
std::optional<std::uint32_t> offerSequence = ctx_.tx[~sfOfferSequence];
bool const fixV1 = view().rules().enabled(fixXahauV1);
Keylet k = escrowID ? Keylet(ltESCROW, *escrowID)
: keylet::escrow(ctx_.tx[sfOwner], *offerSequence);
@@ -781,7 +753,7 @@ EscrowCancel::doApply()
if (!slep)
return tecNO_TARGET;
if (fixV1 && slep->getFieldU16(sfLedgerEntryType) != ltESCROW)
if (slep->getFieldU16(sfLedgerEntryType) != ltESCROW)
return tecINTERNAL;
if (ctx_.view().rules().enabled(fix1571))
+1 -105
View File
@@ -75,8 +75,6 @@ GenesisMint::preflight(PreflightContext const& ctx)
return temMALFORMED;
}
bool const allowDuplicates = ctx.rules.enabled(fixXahauV1);
std::unordered_set<AccountID> alreadySeen;
for (auto const& dest : dests)
{
@@ -139,17 +137,6 @@ GenesisMint::preflight(PreflightContext const& ctx)
"disallowed account zero or one.";
return temMALFORMED;
}
if (allowDuplicates)
continue;
if (alreadySeen.find(accid) != alreadySeen.end())
{
JLOG(ctx.j.warn()) << "GenesisMint: duplicate in destinations.";
return temMALFORMED;
}
alreadySeen.emplace(accid);
}
return preflight2(ctx);
@@ -177,99 +164,8 @@ GenesisMint::doApply()
{
auto const& dests = ctx_.tx.getFieldArray(sfGenesisMints);
if (!view().rules().enabled(fixXahauV1))
{
STAmount dropsAdded{0};
for (auto const& dest : dests)
{
auto const amt = dest[~sfAmount];
if (amt && !isXRP(*amt))
{
JLOG(ctx_.journal.warn()) << "GenesisMint: Non-xrp amount.";
return tecINTERNAL;
}
auto const flags = dest[~sfGovernanceFlags];
auto const marks = dest[~sfGovernanceMarks];
auto const id = dest.getAccountID(sfDestination);
auto const k = keylet::account(id);
auto sle = view().peek(k);
bool const created = !sle;
if (created)
{
// Create the account.
std::uint32_t const seqno{
view().info().parentCloseTime.time_since_epoch().count()};
sle = std::make_shared<SLE>(k);
sle->setAccountID(sfAccount, id);
sle->setFieldU32(sfSequence, seqno);
if (amt)
{
sle->setFieldAmount(sfBalance, *amt);
dropsAdded += *amt;
}
else // give them 2 XRP if the account didn't exist, same as
// ttIMPORT
{
XRPAmount const initialXrp =
Import::computeStartingBonus(ctx_.view());
sle->setFieldAmount(sfBalance, initialXrp);
dropsAdded += initialXrp;
}
}
else if (amt)
{
// Credit the account
STAmount startBal = sle->getFieldAmount(sfBalance);
STAmount finalBal = startBal + *amt;
if (finalBal <= startBal)
{
JLOG(ctx_.journal.warn())
<< "GenesisMint: cannot credit " << dest
<< " due to balance overflow";
return tecINTERNAL;
}
sle->setFieldAmount(sfBalance, finalBal);
dropsAdded += *amt;
}
// set flags and marks as applicable
if (flags)
sle->setFieldH256(sfGovernanceFlags, *flags);
if (marks)
sle->setFieldH256(sfGovernanceMarks, *marks);
if (created)
view().insert(sle);
else
view().update(sle);
}
// update ledger header
if (dropsAdded < beast::zero ||
dropsAdded.xrp() + view().info().drops < view().info().drops)
{
JLOG(ctx_.journal.warn()) << "GenesisMint: dropsAdded overflowed\n";
return tecINTERNAL;
}
if (dropsAdded > beast::zero)
ctx_.rawView().rawDestroyXRP(-dropsAdded.xrp());
return tesSUCCESS;
}
// RH NOTE:
// As of fixXahauV1, duplicate accounts are allowed
// duplicate accounts are allowed
// so we first do a summation loop then an actioning loop
// if amendment is not active then there's exactly one entry per
+3 -11
View File
@@ -821,17 +821,9 @@ Import::preflight(PreflightContext const& ctx)
<< " validation count: " << validationCount;
// check if the validation count is adequate
auto hasInsufficientQuorum =
[&ctx](uint64_t quorum, uint64_t validationCount) {
if (ctx.rules.enabled(fixXahauV1))
{
return quorum > validationCount;
}
else
{
return quorum >= validationCount;
}
};
auto hasInsufficientQuorum = [](uint64_t quorum, uint64_t validationCount) {
return quorum > validationCount;
};
if (hasInsufficientQuorum(quorum, validationCount))
{
JLOG(ctx.j.warn()) << "Import: xpop did not contain an 80% quorum for "
@@ -612,6 +612,7 @@ LedgerEntryTypesMatch::visitEntry(
case ltMPTOKEN:
case ltCREDENTIAL:
case ltPERMISSIONED_DOMAIN:
case ltMANIFEST:
break;
default:
invalidTypeAdded_ = true;
-27
View File
@@ -26,12 +26,6 @@
namespace ripple {
TxConsequences
Invoke::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
Invoke::preflight(PreflightContext const& ctx)
{
@@ -92,27 +86,6 @@ Invoke::calculateBaseFee(ReadView const& view, STTx const& tx)
extraFee +=
XRPAmount{static_cast<XRPAmount>(tx.getFieldVL(sfBlob).size())};
// old code (prior to fixXahauV1)
if (!view.rules().enabled(fixXahauV1))
{
if (tx.isFieldPresent(sfHookParameters))
{
uint64_t paramBytes = 0;
auto const& params = tx.getFieldArray(sfHookParameters);
for (auto const& param : params)
{
paramBytes +=
(param.isFieldPresent(sfHookParameterName)
? param.getFieldVL(sfHookParameterName).size()
: 0) +
(param.isFieldPresent(sfHookParameterValue)
? param.getFieldVL(sfHookParameterValue).size()
: 0);
}
extraFee += XRPAmount{static_cast<XRPAmount>(paramBytes)};
}
}
return Transactor::calculateBaseFee(view, tx) + extraFee;
}
+1 -4
View File
@@ -30,7 +30,7 @@ namespace ripple {
class Invoke : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit Invoke(ApplyContext& ctx) : Transactor(ctx)
{
@@ -39,9 +39,6 @@ public:
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
-42
View File
@@ -197,31 +197,6 @@ SetAccount::preflight(PreflightContext const& ctx)
return temMALFORMED;
}
if (tx.isFieldPresent(sfSecureUI))
{
if (!ctx.rules.enabled(featureSecureUI))
return temMALFORMED;
Blob ui = tx.getFieldVL(sfSecureUI);
if (ui.size() == 0)
{
// this is an unset operation, pass
}
else if (ui.size() > 4096)
{
JLOG(j.trace()) << "SecureUI: Too long > 4096 bytes";
return temMALFORMED;
}
else if (!URIToken::validateUTF8(ui))
{
JLOG(j.trace()) << "SecureUI: Not UTF-8";
return temMALFORMED;
}
// valid
}
return preflight2(ctx);
}
@@ -724,23 +699,6 @@ SetAccount::doApply()
sle->setFieldU16(sfHookStateScale, newScale);
}
}
if (tx.isFieldPresent(sfSecureUI))
{
Blob ui = tx.getFieldVL(sfSecureUI);
if (ui.size() == 0)
{
// unset operation
if (sle->isFieldPresent(sfSecureUI))
sle->makeFieldAbsent(sfSecureUI);
}
else
{
// set operation
sle->setFieldVL(sfSecureUI, std::move(ui));
}
}
ctx_.view().update(sle);
return tesSUCCESS;
+1 -4
View File
@@ -2173,10 +2173,7 @@ SetHook::setHook()
else if (oldHookSLE && !newHooksEmpty)
{
// UPDATE ltHOOK
if (view().rules().enabled(fixXahauV1))
{
(*newHookSLE)[sfOwnerNode] = (*oldHookSLE)[sfOwnerNode];
}
(*newHookSLE)[sfOwnerNode] = (*oldHookSLE)[sfOwnerNode];
view().erase(oldHookSLE);
view().insert(newHookSLE);
}
+404
View File
@@ -0,0 +1,404 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 XRPL-Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpld/app/misc/Manifest.h>
#include <xrpld/app/tx/detail/SetManifest.h>
#include <xrpld/core/Config.h>
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/StringUtilities.h> // strUnHex
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h> // mulRatio
#include <xrpl/protocol/serialize.h>
#include <xrpl/protocol/st.h>
namespace ripple {
TxConsequences
SetManifest::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
SetManifest::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureOnChainManifests))
return temDISABLED;
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
return ret;
auto& tx = ctx.tx;
auto& j = ctx.j;
if (tx.getFlags() & tfUniversalMask)
{
JLOG(j.warn()) << "SetManifest: Invalid flags set.";
return temINVALID_FLAG;
}
// rules:
// 1. sfManifest must match the manifest template and be validly signed
// 2. the signingpubkey must match the master r-address
// 3. manifest must not be already revoked
STObject const& obj =
const_cast<ripple::STTx&>(tx).getField(sfManifest).downcast<STObject>();
// 1. sfManifest must match the manifest template and be validly signed
auto manifest = deserializeManifest(obj, j);
if (!manifest.has_value())
{
JLOG(j.warn())
<< "SetManifest: invalid manifest passed (parseManifest failed).";
return temMALFORMED;
}
if (!manifest->verify())
{
JLOG(j.warn())
<< "SetManifest: invalid manifest passed (manifest.verify failed).";
return temMALFORMED;
}
// 2. master must match r-address
auto wantID = calcAccountID(manifest->masterKey);
if (ctx.tx.getAccountID(sfAccount) != wantID)
{
JLOG(j.warn())
<< "SetManifest: master key must match sfAccount (r-address).";
return temMALFORMED;
}
// 3. not already revoked will be checked in preclaim because it depends on
// lgr state
// 4. the envelope carries no account signature: authority comes solely
// from the manifest's own master/ephemeral signatures, which do not cover
// the envelope. Pin every envelope field a relayer could otherwise choose.
// The shape below must match the one checkValidity() recognises, or the
// txn falls through to the ordinary signature path and is rejected there.
// sfFee cannot be bounded here because the computed base fee is not in
// scope until preclaim; checkFee() bounds it instead.
if (!tx.isFieldPresent(sfSigningPubKey) || !tx.getSigningPubKey().empty() ||
!tx.isFieldPresent(sfTxnSignature) || !tx.getSignature().empty() ||
tx.isFieldPresent(sfSigners) || tx.isFieldPresent(sfAccountTxnID) ||
tx.isFieldPresent(sfTicketSequence) || tx.getFieldU32(sfSequence) != 0)
{
JLOG(j.warn())
<< "SetManifest: envelope must be unsigned with Sequence 0.";
return temMALFORMED;
}
return preflight2(ctx);
}
TER
SetManifest::preclaim(PreclaimContext const& ctx)
{
if (!ctx.view.rules().enabled(featureOnChainManifests))
return temDISABLED;
auto const id = ctx.tx[sfAccount];
// The account must exist: it pays the fee and anchors sfManifestID.
auto const sle = ctx.view.read(keylet::account(id));
if (!sle)
return terNO_ACCOUNT;
STObject const& newObj = const_cast<ripple::STTx&>(ctx.tx)
.getField(sfManifest)
.downcast<STObject>();
auto const newManifest = deserializeManifest(newObj, ctx.j);
if (!newManifest)
return tefINTERNAL; // preflight already parsed this successfully
// Replay protection. A byte-identical resubmission is rejected as
// tefALREADY by checkPriorTxAndLastLedger, but sfFee may vary within the
// band checkFee() allows, so the same manifest can also arrive under a
// different txid. The strictly-increasing sequence test below is what
// covers that, both within this ledger and in every later one. Either
// result is tef, so a replay is never included and never claims a fee.
if (sle->isFieldPresent(sfManifestID))
{
// A dangling sfManifestID is a corrupt ledger; doApply reports it.
if (auto const sleOld = ctx.view.read(
Keylet{ltMANIFEST, sle->getFieldH256(sfManifestID)}))
{
if (sleOld->getFieldU32(sfSequence) ==
std::numeric_limits<std::uint32_t>::max())
{
JLOG(ctx.j.warn()) << "SetManifest: New manifest submitted for "
"revoked master. "
<< id;
return tefREVOKED_MANIFEST;
}
if (newManifest->sequence <= sleOld->getFieldU32(sfSequence))
{
JLOG(ctx.j.warn())
<< "SetManifest: Manifest sequence already passed. " << id;
return tefPAST_MANIFEST_SEQ;
}
}
}
// On-chain equivalent of the badMasterKey/badEphemeralKey sanity checks in
// ManifestCache::applyManifest. keylet::manifest(masterKey) is 1:1 with the
// account, but keylet::manifest(signingKey) is not: without this, a
// manifest naming another validator's key as its ephemeral key would
// collide with -- and clobber -- that validator's object.
if (newManifest->signingKey)
{
auto const sleEph =
ctx.view.read(keylet::manifest(*newManifest->signingKey));
if (sleEph && sleEph->getAccountID(sfAccount) != id)
{
JLOG(ctx.j.warn())
<< "SetManifest: Ephemeral key already claimed by another "
"account. "
<< id;
return tecDUPLICATE;
}
}
return tesSUCCESS;
}
TER
SetManifest::doApply()
{
auto sle = view().peek(keylet::account(account_));
if (!sle)
return tefINTERNAL;
STObject const& obj = const_cast<ripple::STTx&>(ctx_.tx)
.getField(sfManifest)
.downcast<STObject>();
auto const manifest = deserializeManifest(obj, j_);
// Both of these were established in preflight.
if (!manifest || calcAccountID(manifest->masterKey) != account_)
return tefINTERNAL;
// A manifest is stored twice so it can be found from either key:
// keylet::manifest(masterKey) -> obj1, sfManifestID -> obj2
// keylet::manifest(signingKey) -> obj2, sfManifestID -> obj1
// A revoked manifest has no signing key, so it exists only as obj1 with no
// sfManifestID. Both copies are erased and rewritten on every update so
// they can never drift apart.
if (sle->isFieldPresent(sfManifestID))
{
uint256 const firstID = sle->getFieldH256(sfManifestID);
auto const sleMan1 = view().peek(Keylet{ltMANIFEST, firstID});
if (!sleMan1 || sleMan1->getAccountID(sfAccount) != account_)
{
JLOG(j_.error()) << "SetManifest: Old manifest object missing or "
"misowned (ID1) !! "
<< strHex(firstID);
return tefBAD_LEDGER;
}
// Absent when the previous manifest was a revocation.
if (sleMan1->isFieldPresent(sfManifestID))
{
uint256 const secondID = sleMan1->getFieldH256(sfManifestID);
auto const sleMan2 = view().peek(Keylet{ltMANIFEST, secondID});
if (secondID == firstID || !sleMan2 ||
sleMan2->getAccountID(sfAccount) != account_)
{
JLOG(j_.error())
<< "SetManifest: Old manifest object missing, misowned or "
"self-referential (ID2) !! "
<< strHex(secondID);
return tefBAD_LEDGER;
}
view().erase(sleMan2);
}
view().erase(sleMan1);
}
Keylet const klMan1 = keylet::manifest(manifest->masterKey);
std::optional<Keylet> klMan2;
if (!manifest->revoked() && manifest->signingKey)
klMan2 = keylet::manifest(*manifest->signingKey);
// Neither key may still be occupied: preclaim rejects an ephemeral key held
// by another account, and the block above cleared this account's own
// copies.
if (view().exists(klMan1) || (klMan2 && view().exists(*klMan2)))
{
JLOG(j_.error()) << "SetManifest: Manifest keylet already occupied !! "
<< strHex(klMan1.key);
return tefBAD_LEDGER;
}
// Mirror the manifest losslessly, signatures included, so any node can
// reconstruct and independently verify it (ManifestCache::applyLedger).
// Field *presence* is copied faithfully: sfVersion is soeDEFAULT in the
// manifest format, so materialising an absent one would alter the signed
// payload and break verification.
auto const write = [&](Keylet const& kl,
std::optional<uint256> const& other) {
auto sleMan = std::make_shared<SLE>(kl);
sleMan->setAccountID(sfAccount, account_);
sleMan->setFieldU32(sfSequence, obj.getFieldU32(sfSequence));
sleMan->setFieldVL(sfPublicKey, obj.getFieldVL(sfPublicKey));
sleMan->setFieldVL(
sfMasterSignature, obj.getFieldVL(sfMasterSignature));
if (obj.isFieldPresent(sfVersion))
sleMan->setFieldU16(sfVersion, obj.getFieldU16(sfVersion));
if (obj.isFieldPresent(sfSigningPubKey))
sleMan->setFieldVL(
sfSigningPubKey, obj.getFieldVL(sfSigningPubKey));
if (obj.isFieldPresent(sfSignature))
sleMan->setFieldVL(sfSignature, obj.getFieldVL(sfSignature));
if (obj.isFieldPresent(sfDomain))
sleMan->setFieldVL(sfDomain, obj.getFieldVL(sfDomain));
if (other)
sleMan->setFieldH256(sfManifestID, *other);
view().insert(sleMan);
};
write(klMan1, klMan2 ? std::optional<uint256>{klMan2->key} : std::nullopt);
if (klMan2)
write(*klMan2, klMan1.key);
sle->setFieldH256(sfManifestID, klMan1.key);
view().update(sle);
return tesSUCCESS;
}
XRPAmount
SetManifest::calculateBaseFee(ReadView const& view, STTx const& tx)
{
XRPAmount manifestFee{0};
if (tx.isFieldPresent(sfManifest))
{
STObject const& obj = const_cast<ripple::STTx&>(tx)
.getField(sfManifest)
.downcast<STObject>();
// one drop per byte
manifestFee = XRPAmount{obj.getSerializer().getDataLength()};
}
return Transactor::calculateBaseFee(view, tx) + manifestFee;
}
/** The most sfFee may be: the same 1.2x headroom Submit applies.
Kept in one place so the value Submit writes and the value preclaim will
accept cannot drift apart.
*/
static XRPAmount
manifestFeeCeiling(XRPAmount baseFee)
{
return mulRatio(baseFee, 12, 10, /*roundUp*/ true);
}
TER
SetManifest::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
{
// A ceiling is required because the envelope carries no account signature,
// so sfFee is chosen by whoever relays the txn -- and manifests are public:
// they are gossiped over the peer protocol and embedded in published UNLs,
// so the relayer need not be the master key holder. Uncapped, any observer
// of a not-yet-recorded manifest could wrap it with sfFee set to that
// validator's entire balance. The 20% band is headroom against a fee floor
// that has risen since the txn was built, and bounds what an attacker can
// burn to the same 20%.
if (ctx.tx[sfFee].xrp() > manifestFeeCeiling(baseFee))
{
JLOG(ctx.j.trace()) << "SetManifest: fee above ceiling: "
<< to_string(ctx.tx[sfFee].xrp());
return temBAD_FEE;
}
// Floor and balance are the ordinary rules.
return Transactor::checkFee(ctx, baseFee);
}
std::optional<std::string>
makeSetManifestTx(
Slice const& manifest,
std::uint32_t networkID,
ReadView const& openView,
beast::Journal j)
{
try
{
auto const man = deserializeManifest(manifest, j);
if (!man || !man->verify())
return std::nullopt;
auto const encode = [&](XRPAmount fee) {
return serializeHex(STTx(ttMANIFEST_SET, [&](STObject& obj) {
obj.setAccountID(sfAccount, calcAccountID(man->masterKey));
obj.setFieldU32(sfSequence, 0);
obj.setFieldU32(sfNetworkID, networkID);
obj.setFieldAmount(sfFee, fee);
obj.setFieldVL(sfSigningPubKey, std::vector<std::uint8_t>{});
obj.setFieldVL(sfTxnSignature, std::vector<std::uint8_t>{});
// sfManifest is soeREQUIRED, so STObject::set(SOTemplate) has
// already materialised it as a present, empty object. Fill
// that one in: emitting a second is a duplicate field, which
// STObject::set(SerialIter&) rejects on the way back in.
SerialIter mit{manifest};
obj.peekFieldObject(sfManifest).set(mit);
}));
};
// calculateBaseFee() takes a parsed transaction, so encode once with a
// placeholder fee purely to have something to price. The resulting fee
// does not depend on the placeholder: it is derived from the length of
// the manifest object and the ledger's base fee.
auto const priced = strUnHex(encode(XRPAmount{0}));
if (!priced || priced->empty())
return std::nullopt;
SerialIter sit{makeSlice(*priced)};
STTx const probe{std::ref(sit)};
// Submit the ceiling exactly. preclaim rejects anything above it, and
// the floor rises with network load, so the ceiling is both always
// acceptable and the value most likely to still clear the floor by the
// time the transaction is applied.
return encode(
manifestFeeCeiling(SetManifest::calculateBaseFee(openView, probe)));
}
catch (std::exception const& e)
{
JLOG(j.warn()) << "makeSetManifestTx: " << e.what();
return std::nullopt;
}
}
} // namespace ripple
+90
View File
@@ -0,0 +1,90 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 XRPL-Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_SETMANIFEST_H_INCLUDED
#define RIPPLE_TX_SETMANIFEST_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
#include <xrpld/core/Config.h>
#include <xrpl/basics/Log.h>
#include <xrpl/protocol/Indexes.h>
namespace ripple {
/** Encode the transaction that publishes `manifest` on-ledger.
A manifest transaction carries no account signature, so the protocol pins
the whole envelope: Sequence must be 0, SigningPubKey and TxnSignature must
be empty, and Fee must fall between the computed base fee and a ceiling
above it. SetManifest::preflight and SetManifest::checkFee reject anything
else. Every caller that submits a manifest builds it here so those rules
cannot drift apart from the ones the transactor enforces.
Returns hex rather than an STTx because the manifest is appended to the
encoded transaction verbatim, behind its object marker, instead of being
parsed and re-emitted: the bytes the master key signed survive untouched,
and a future change to the manifest format needs no change here.
@param manifest Serialized manifest
@param networkID Network the transaction is for
@param openView Ledger the fee is priced against
@param j Journal
@return the hex-encoded transaction, or nullopt if the manifest does not
parse or does not verify
*/
std::optional<std::string>
makeSetManifestTx(
Slice const& manifest,
std::uint32_t networkID,
ReadView const& openView,
beast::Journal j);
class SetManifest : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
explicit SetManifest(ApplyContext& ctx) : Transactor(ctx)
{
}
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
// Hides Transactor::checkFee; applySteps dispatches this as T::checkFee.
static TER
checkFee(PreclaimContext const& ctx, XRPAmount baseFee);
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const&);
TER
doApply() override;
};
} // namespace ripple
#endif
-6
View File
@@ -30,12 +30,6 @@
namespace ripple {
TxConsequences
SetRemarks::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
SetRemarks::validateRemarks(STArray const& remarks, beast::Journal const& j)
{
+1 -4
View File
@@ -30,7 +30,7 @@ namespace ripple {
class SetRemarks : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit SetRemarks(ApplyContext& ctx) : Transactor(ctx)
{
@@ -39,9 +39,6 @@ public:
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
+26 -3
View File
@@ -408,7 +408,7 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
XRPAmount accumulator = baseFee;
if (view.rules().enabled(featureHooks) &&
view.rules().enabled(fixXahauV1) && tx.isFieldPresent(sfHookParameters))
tx.isFieldPresent(sfHookParameters))
{
uint64_t paramBytes = 0;
auto const& params = tx.getFieldArray(sfHookParameters);
@@ -604,6 +604,13 @@ Transactor::checkSeqProxy(
return terNO_ACCOUNT;
}
// A manifest txn is derived deterministically from the manifest alone, so
// it cannot depend on account state: preflight pins sfSequence to 0 and the
// account sequence is neither checked here nor consumed below.
if (view.rules().enabled(featureOnChainManifests) &&
tx.getTxnType() == ttMANIFEST_SET)
return tesSUCCESS;
SeqProxy const a_seq = SeqProxy::sequence((*sle)[sfSequence]);
// pass all emitted tx provided their seq is 0
@@ -705,8 +712,7 @@ Transactor::checkPriorTxAndLastLedger(PreclaimContext const& ctx)
if (ctx.view.txExists(ctx.tx.getTransactionID()))
return tefALREADY;
if (hook::isEmittedTxn(ctx.tx) && ctx.view.rules().enabled(featureHooks) &&
ctx.view.rules().enabled(fixXahauV2))
if (hook::isEmittedTxn(ctx.tx) && ctx.view.rules().enabled(featureHooks))
{
// check if the emitted txn exists on ledger and is in the emission
// directory if not that's a re-apply so discard
@@ -755,6 +761,17 @@ Transactor::consumeSeqProxy(SLE::pointer const& sleAccount)
if (ctx_.isEmittedTxn())
return tesSUCCESS;
// Manifest txns get the same treatment: pinned to sfSequence 0 and not
// signed by the account, so they neither consume nor reset its sequence.
// Doing so would be actively harmful -- the write below is
// seqProx.value() + 1, which for a seq-0 txn sets the account sequence to
// 1 and makes every previously used sequence replayable. Handling it here
// rather than in apply() also covers reset(), which re-consumes on the
// tec / failed-invariant path.
if (view().rules().enabled(featureOnChainManifests) &&
ctx_.tx.getTxnType() == ttMANIFEST_SET)
return tesSUCCESS;
SeqProxy const seqProx = ctx_.tx.getSeqProxy();
if (seqProx.isSeq())
{
@@ -898,6 +915,12 @@ Transactor::checkSign(PreclaimContext const& ctx)
ctx.tx.getTxnType() == ttIMPORT)
return tesSUCCESS;
// pass ttMANIFEST_SETs, their signatures are checked in preflight against
// the manifest's internal key logic
if (ctx.view.rules().enabled(featureOnChainManifests) &&
ctx.tx.getTxnType() == ttMANIFEST_SET)
return tesSUCCESS;
if (ctx.flags & tapDRY_RUN)
{
// This code must be different for `simulate`
+145 -558
View File
@@ -78,16 +78,11 @@ URIToken::preflight(PreflightContext const& ctx)
}
}
// fix amendment to return temMALFORMED if sfDestination field is present
// return temMALFORMED if sfDestination field is present
// and sfAmount field is not present
if (ctx.rules.enabled(fixXahauV1))
{
if (ctx.tx.isFieldPresent(sfDestination) &&
!ctx.tx.isFieldPresent(sfAmount))
{
return temMALFORMED;
}
}
if (ctx.tx.isFieldPresent(sfDestination) &&
!ctx.tx.isFieldPresent(sfAmount))
return temMALFORMED;
// the validation for the URI field is also the same regardless of the txn
// type
@@ -144,8 +139,6 @@ URIToken::preflight(PreflightContext const& ctx)
TER
URIToken::preclaim(PreclaimContext const& ctx)
{
bool const fixV1 = ctx.view.rules().enabled(fixXahauV1);
std::shared_ptr<SLE const> sleU;
uint32_t leFlags;
std::optional<AccountID> issuer;
@@ -235,74 +228,42 @@ URIToken::preclaim(PreclaimContext const& ctx)
if (purchaseAmount < saleAmount)
return tecINSUFFICIENT_PAYMENT;
if (fixV1)
if (purchaseAmount.native() && saleAmount->native())
{
if (purchaseAmount.native() && saleAmount->native())
{
// native transfer
// native transfer
STAmount needed{ctx.view.fees().accountReserve(
sle->getFieldU32(sfOwnerCount) + 1)};
STAmount needed{ctx.view.fees().accountReserve(
sle->getFieldU32(sfOwnerCount) + 1)};
STAmount const fee = ctx.tx.getFieldAmount(sfFee).xrp();
STAmount const fee = ctx.tx.getFieldAmount(sfFee).xrp();
if (needed + fee < needed)
return tecINTERNAL;
needed += fee;
if (needed + purchaseAmount < needed)
return tecINTERNAL;
needed += purchaseAmount;
if (needed > sle->getFieldAmount(sfBalance))
return tecINSUFFICIENT_FUNDS;
}
else if (purchaseAmount.native() || saleAmount->native())
{
// should not be able to happen
if (needed + fee < needed)
return tecINTERNAL;
}
else
{
// iou transfer
STAmount availableFunds{accountFunds(
ctx.view,
acc,
purchaseAmount,
fhZERO_IF_FROZEN,
ctx.j)};
needed += fee;
if (purchaseAmount > availableFunds)
return tecINSUFFICIENT_FUNDS;
}
if (needed + purchaseAmount < needed)
return tecINTERNAL;
needed += purchaseAmount;
if (needed > sle->getFieldAmount(sfBalance))
return tecINSUFFICIENT_FUNDS;
}
else if (purchaseAmount.native() || saleAmount->native())
{
// should not be able to happen
return tecINTERNAL;
}
else
{
// old logic
// iou transfer
if (purchaseAmount.native() && saleAmount->native())
{
// if it's an xrp sale/purchase then no trustline needed
if (purchaseAmount >
(sleOwner->getFieldAmount(sfBalance) - ctx.tx[sfFee]))
return tecINSUFFICIENT_FUNDS;
}
else
{
// iou
STAmount availableFunds{accountFunds(
ctx.view,
acc,
purchaseAmount,
fhZERO_IF_FROZEN,
ctx.j)};
STAmount availableFunds{accountFunds(
ctx.view, acc, purchaseAmount, fhZERO_IF_FROZEN, ctx.j)};
if (purchaseAmount > availableFunds)
return tecINSUFFICIENT_FUNDS;
}
if (purchaseAmount > availableFunds)
return tecINSUFFICIENT_FUNDS;
}
return tesSUCCESS;
}
@@ -343,8 +304,6 @@ URIToken::doApply()
Sandbox sb(&ctx_.view());
bool const fixV1 = sb.rules().enabled(fixXahauV1);
auto const sle = sb.peek(keylet::account(account_));
if (!sle)
return tefINTERNAL;
@@ -483,504 +442,134 @@ URIToken::doApply()
if (purchaseAmount.issue() != saleAmount->issue())
return temBAD_CURRENCY;
if (fixV1)
if (purchaseAmount < saleAmount)
return tecINSUFFICIENT_PAYMENT;
// if it's an xrp sale/purchase then no trustline needed
if (purchaseAmount.native())
{
// this is the reworked version of the buy routine
STAmount needed{sb.fees().accountReserve(
sle->getFieldU32(sfOwnerCount) + 1)};
if (purchaseAmount < saleAmount)
return tecINSUFFICIENT_PAYMENT;
STAmount const fee = ctx_.tx.getFieldAmount(sfFee).xrp();
// if it's an xrp sale/purchase then no trustline needed
if (purchaseAmount.native())
{
STAmount needed{sb.fees().accountReserve(
sle->getFieldU32(sfOwnerCount) + 1)};
if (needed + fee < needed)
return tecINTERNAL;
STAmount const fee = ctx_.tx.getFieldAmount(sfFee).xrp();
needed += fee;
if (needed + fee < needed)
return tecINTERNAL;
if (needed + purchaseAmount < needed)
return tecINTERNAL;
needed += fee;
needed += purchaseAmount;
if (needed + purchaseAmount < needed)
return tecINTERNAL;
needed += purchaseAmount;
if (needed > mPriorBalance)
return tecINSUFFICIENT_FUNDS;
}
else
{
// IOU sale
if (TER result = trustTransferAllowed(
sb, {account_, *owner}, purchaseAmount.issue(), j);
!isTesSuccess(result))
{
JLOG(j.trace())
<< "URIToken::doApply trustTransferAllowed result="
<< result;
return result;
}
if (STAmount availableFunds{accountFunds(
sb, account_, purchaseAmount, fhZERO_IF_FROZEN, j)};
purchaseAmount > availableFunds)
return tecINSUFFICIENT_FUNDS;
}
// execute the funds transfer, we'll check reserves last
if (TER result = accountSend(
sb,
account_,
*owner,
purchaseAmount,
j,
WaiveTransferFee::No,
false);
if (needed > mPriorBalance)
return tecINSUFFICIENT_FUNDS;
}
else
{
// IOU sale
if (TER result = trustTransferAllowed(
sb, {account_, *owner}, purchaseAmount.issue(), j);
!isTesSuccess(result))
{
JLOG(j.trace())
<< "URIToken::doApply trustTransferAllowed result="
<< result;
return result;
// add token to new owner dir
auto const newPage = sb.dirInsert(
keylet::ownerDir(account_),
*kl,
describeOwnerDir(account_));
JLOG(j_.trace()) << "Adding URIToken to owner directory "
<< to_string(kl->key) << ": "
<< (newPage ? "success" : "failure");
if (!newPage)
return tecDIR_FULL;
// remove from current owner directory
if (!sb.dirRemove(
keylet::ownerDir(*owner),
sleU->getFieldU64(sfOwnerNode),
kl->key,
true))
{
JLOG(j.fatal())
<< "Could not remove URIToken from owner directory";
return tefBAD_LEDGER;
}
// adjust owner counts
adjustOwnerCount(sb, sleOwner, -1, j);
adjustOwnerCount(sb, sle, 1, j);
// clean the offer off the object
sleU->makeFieldAbsent(sfAmount);
if (sleU->isFieldPresent(sfDestination))
sleU->makeFieldAbsent(sfDestination);
// set the new owner of the object
sleU->setAccountID(sfOwner, account_);
// tell the ledger where to find it
sleU->setFieldU64(sfOwnerNode, *newPage);
// check each side has sufficient balance remaining to cover the
// updated ownercounts
auto hasSufficientReserve =
[&](std::shared_ptr<SLE> const& sle) -> bool {
std::uint32_t const uOwnerCount =
sle->getFieldU32(sfOwnerCount);
return sle->getFieldAmount(sfBalance) >=
sb.fees().accountReserve(uOwnerCount);
};
if (!hasSufficientReserve(sle))
{
JLOG(j.trace()) << "URIToken: buyer " << account_
<< " has insufficient reserve to buy";
return tecINSUFFICIENT_RESERVE;
}
// This should only happen if the owner burned their reserves
// below the needed amount via another transactor. If this
// happens they should top up their account before selling!
if (!hasSufficientReserve(sleOwner))
{
JLOG(j.warn())
<< "URIToken: seller " << *owner
<< " has insufficient reserve to allow purchase!";
return tecINSUF_RESERVE_SELLER;
}
sb.update(sle);
sb.update(sleU);
sb.update(sleOwner);
sb.apply(ctx_.rawView());
return tesSUCCESS;
}
// old logic
{
STAmount const purchaseAmount =
ctx_.tx.getFieldAmount(sfAmount);
bool const sellerLow = purchaseAmount.getIssuer() > *owner;
bool const buyerLow = purchaseAmount.getIssuer() > account_;
bool sellerIssuer = purchaseAmount.getIssuer() == *owner;
bool buyerIssuer = purchaseAmount.getIssuer() == account_;
// check if the seller has listed it at all
if (!saleAmount)
return tecNO_PERMISSION;
// check if the seller has listed it for sale to a specific
// account
if (dest && *dest != account_)
return tecNO_PERMISSION;
if (purchaseAmount.issue() != saleAmount->issue())
return temBAD_CURRENCY;
std::optional<STAmount> initBuyerBal;
std::optional<STAmount> initSellerBal;
std::optional<STAmount> finBuyerBal;
std::optional<STAmount> finSellerBal;
std::optional<STAmount> dstAmt;
std::optional<Keylet> tlSeller;
std::shared_ptr<SLE> sleDstLine;
std::shared_ptr<SLE> sleSrcLine;
// if it's an xrp sale/purchase then no trustline needed
if (purchaseAmount.native())
{
if (purchaseAmount < saleAmount)
return tecINSUFFICIENT_PAYMENT;
if (purchaseAmount >
((*sleOwner)[sfBalance] - ctx_.tx[sfFee]))
return tecINSUFFICIENT_FUNDS;
dstAmt = purchaseAmount;
initSellerBal = (*sleOwner)[sfBalance];
initBuyerBal = (*sle)[sfBalance];
finSellerBal = *initSellerBal + purchaseAmount;
finBuyerBal = *initBuyerBal - purchaseAmount;
}
else
{
// IOU sale
STAmount availableFunds{accountFunds(
if (STAmount availableFunds{accountFunds(
sb, account_, purchaseAmount, fhZERO_IF_FROZEN, j)};
// check for any possible bars to a buy transaction
// between these accounts for this asset
if (buyerIssuer)
{
// pass: issuer does not create own trustline
}
else
{
TER result = trustTransferAllowed(
sb, {account_, *owner}, purchaseAmount.issue(), j);
JLOG(j.trace())
<< "URIToken::doApply trustTransferAllowed result="
<< result;
if (!isTesSuccess(result))
return result;
}
if (purchaseAmount > availableFunds)
return tecINSUFFICIENT_FUNDS;
// check if the seller has a line
tlSeller = keylet::line(
*owner,
purchaseAmount.getIssuer(),
purchaseAmount.getCurrency());
Keylet tlBuyer = keylet::line(
account_,
purchaseAmount.getIssuer(),
purchaseAmount.getCurrency());
sleDstLine = sb.peek(*tlSeller);
sleSrcLine = sb.peek(tlBuyer);
if (sellerIssuer)
{
// pass: issuer does not create own trustline
}
else if (!sleDstLine)
{
// they do not, so we can create one if they have
// sufficient reserve
if (std::uint32_t const ownerCount = {sleOwner->at(
sfOwnerCount)};
(*sleOwner)[sfBalance] <
sb.fees().accountReserve(ownerCount + 1))
{
JLOG(j_.trace())
<< "Trust line does not exist. "
"Insufficent reserve to create line.";
return tecNO_LINE_INSUF_RESERVE;
}
}
if (buyerIssuer)
{
// pass: issuer does not adjust own trustline
initBuyerBal = purchaseAmount.zeroed();
finBuyerBal = purchaseAmount.zeroed();
}
else
{
// remove from buyer
initBuyerBal = buyerLow ? ((*sleSrcLine)[sfBalance])
: -((*sleSrcLine)[sfBalance]);
finBuyerBal = *initBuyerBal - purchaseAmount;
}
dstAmt = purchaseAmount;
static Rate const parityRate(QUALITY_ONE);
auto xferRate = transferRate(sb, saleAmount->getIssuer());
if (!sellerIssuer && !buyerIssuer && xferRate != parityRate)
{
dstAmt = multiplyRound(
purchaseAmount,
xferRate,
purchaseAmount.issue(),
true);
}
initSellerBal = !sleDstLine ? purchaseAmount.zeroed()
: sellerLow ? ((*sleDstLine)[sfBalance])
: -((*sleDstLine)[sfBalance]);
finSellerBal = *initSellerBal + *dstAmt;
}
// sanity check balance mutations (xrp or iou, both are checked
// the same way now)
if (*finSellerBal < *initSellerBal)
{
JLOG(j.warn())
<< "URIToken txid=" << ctx_.tx.getTransactionID() << " "
<< "finSellerBal < initSellerBal";
return tecINTERNAL;
}
if (*finBuyerBal > *initBuyerBal)
{
JLOG(j.warn())
<< "URIToken txid=" << ctx_.tx.getTransactionID() << " "
<< "finBuyerBal > initBuyerBal";
return tecINTERNAL;
}
if (*finBuyerBal < beast::zero)
{
JLOG(j.warn())
<< "URIToken txid=" << ctx_.tx.getTransactionID() << " "
<< "finBuyerBal < 0";
return tecINTERNAL;
}
if (*finSellerBal < beast::zero)
{
JLOG(j.warn())
<< "URIToken txid=" << ctx_.tx.getTransactionID() << " "
<< "finSellerBal < 0";
return tecINTERNAL;
}
// to this point no ledger changes have been made
// make them in a sensible order such that failure doesn't
// require cleanup
// add to new owner's directory first, this can fail if they
// have too many objects
auto const newPage = sb.dirInsert(
keylet::ownerDir(account_),
*kl,
describeOwnerDir(account_));
JLOG(j_.trace()) << "Adding URIToken to owner directory "
<< to_string(kl->key) << ": "
<< (newPage ? "success" : "failure");
if (!newPage)
{
// nothing has happened at all and there is nothing to clean
// up we can just leave with DIR_FULL
return tecDIR_FULL;
}
// Next create destination trustline where applicable. This
// could fail for a variety of reasons. If it does fail we need
// to remove the dir entry we just added to the buyer before we
// leave.
bool lineCreated = false;
if (!isXRP(purchaseAmount) && !sleDstLine && !sellerIssuer)
{
// clang-format off
if (TER const ter = trustCreate(
sb, // payment sandbox
sellerLow, // is dest low?
purchaseAmount.getIssuer(), // source
*owner, // destination
tlSeller->key, // ledger index
sleOwner, // Account to add to
false, // authorize account
(sleOwner->getFlags() & lsfDefaultRipple) == 0,
false, // freeze trust line
false, // deepfreeze trust line
*dstAmt, // initial balance zero
Issue(
purchaseAmount.getCurrency(),
*owner), // limit of zero
0, // quality in
0, // quality out
j); // journal
!isTesSuccess(ter))
{
// remove the newly inserted directory entry before we leave
//
if (!sb.dirRemove(keylet::ownerDir(account_), *newPage, kl->key, true))
{
JLOG(j.fatal())
<< "Could not remove URIToken from owner directory";
return tefBAD_LEDGER;
}
// leave
return ter;
}
// clang-format on
// add their trustline to their ownercount
lineCreated = true;
}
// execution to here means we added the URIToken to the buyer's
// directory and we definitely have a way to send the funds to
// the seller.
// remove from current owner directory
if (!sb.dirRemove(
keylet::ownerDir(*owner),
sleU->getFieldU64(sfOwnerNode),
kl->key,
true))
{
JLOG(j.fatal())
<< "Could not remove URIToken from owner directory";
// remove the newly inserted directory entry before we leave
if (!sb.dirRemove(
keylet::ownerDir(account_),
*newPage,
kl->key,
true))
{
JLOG(j.fatal()) << "Could not remove URIToken from "
"owner directory (2)";
}
// clean up any trustline we might have made
if (lineCreated)
{
auto line = sb.peek(*tlSeller);
if (line)
sb.erase(line);
}
return tefBAD_LEDGER;
}
// above is all the things that could fail. we now have swapped
// the ownership as far as the ownerdirs are concerned, and we
// have a place to pay to and from.
// if a trustline was created then the ownercount stays the same
// on the seller +1 TL -1 URIToken
if (!lineCreated && !isXRP(purchaseAmount))
adjustOwnerCount(sb, sleOwner, -1, j);
// the buyer gets a new object
adjustOwnerCount(sb, sle, 1, j);
// clean the offer off the object
sleU->makeFieldAbsent(sfAmount);
if (sleU->isFieldPresent(sfDestination))
sleU->makeFieldAbsent(sfDestination);
// set the new owner of the object
sleU->setAccountID(sfOwner, account_);
// tell the ledger where to find it
sleU->setFieldU64(sfOwnerNode, *newPage);
// update the buyer's balance
if (isXRP(purchaseAmount))
{
// the sale is for xrp, so set the balance
sle->setFieldAmount(sfBalance, *finBuyerBal);
}
else if (sleSrcLine)
{
// update the buyer's line to reflect the reduction of the
// purchase price
sleSrcLine->setFieldAmount(
sfBalance, buyerLow ? *finBuyerBal : -(*finBuyerBal));
}
else if (buyerIssuer)
{
// pass: buyer is issuer, no update required.
}
else
return tecINTERNAL;
// update the seller's balance
if (isXRP(purchaseAmount))
{
// the sale is for xrp, so set the balance
sleOwner->setFieldAmount(sfBalance, *finSellerBal);
}
else if (sleDstLine)
{
// the line already existed on the seller side so update it
sleDstLine->setFieldAmount(
sfBalance,
sellerLow ? *finSellerBal : -(*finSellerBal));
}
else if (lineCreated)
{
// pass, the TL already has this balance set on it at
// creation
}
else if (sellerIssuer)
{
// pass: seller is issuer, no update required.
}
else
return tecINTERNAL;
if (sleSrcLine)
sb.update(sleSrcLine);
if (sleDstLine)
sb.update(sleDstLine);
sb.update(sle);
sb.update(sleU);
sb.update(sleOwner);
sb.apply(ctx_.rawView());
return tesSUCCESS;
purchaseAmount > availableFunds)
return tecINSUFFICIENT_FUNDS;
}
// execute the funds transfer, we'll check reserves last
if (TER result = accountSend(
sb,
account_,
*owner,
purchaseAmount,
j,
WaiveTransferFee::No,
false);
!isTesSuccess(result))
return result;
// add token to new owner dir
auto const newPage = sb.dirInsert(
keylet::ownerDir(account_), *kl, describeOwnerDir(account_));
JLOG(j_.trace())
<< "Adding URIToken to owner directory " << to_string(kl->key)
<< ": " << (newPage ? "success" : "failure");
if (!newPage)
return tecDIR_FULL;
// remove from current owner directory
if (!sb.dirRemove(
keylet::ownerDir(*owner),
sleU->getFieldU64(sfOwnerNode),
kl->key,
true))
{
JLOG(j.fatal())
<< "Could not remove URIToken from owner directory";
return tefBAD_LEDGER;
}
// adjust owner counts
adjustOwnerCount(sb, sleOwner, -1, j);
adjustOwnerCount(sb, sle, 1, j);
// clean the offer off the object
sleU->makeFieldAbsent(sfAmount);
if (sleU->isFieldPresent(sfDestination))
sleU->makeFieldAbsent(sfDestination);
// set the new owner of the object
sleU->setAccountID(sfOwner, account_);
// tell the ledger where to find it
sleU->setFieldU64(sfOwnerNode, *newPage);
// check each side has sufficient balance remaining to cover the
// updated ownercounts
auto hasSufficientReserve =
[&](std::shared_ptr<SLE> const& sle) -> bool {
std::uint32_t const uOwnerCount =
sle->getFieldU32(sfOwnerCount);
return sle->getFieldAmount(sfBalance) >=
sb.fees().accountReserve(uOwnerCount);
};
if (!hasSufficientReserve(sle))
{
JLOG(j.trace()) << "URIToken: buyer " << account_
<< " has insufficient reserve to buy";
return tecINSUFFICIENT_RESERVE;
}
// This should only happen if the owner burned their reserves
// below the needed amount via another transactor. If this
// happens they should top up their account before selling!
if (!hasSufficientReserve(sleOwner))
{
JLOG(j.warn())
<< "URIToken: seller " << *owner
<< " has insufficient reserve to allow purchase!";
return tecINSUF_RESERVE_SELLER;
}
sb.update(sle);
sb.update(sleU);
sb.update(sleOwner);
sb.apply(ctx_.rawView());
return tesSUCCESS;
}
case ttURITOKEN_BURN: {
@@ -1010,10 +599,8 @@ URIToken::doApply()
sb.erase(sleU);
auto& sleAcc = fixV1 ? sleOwner : sle;
adjustOwnerCount(sb, sleAcc, -1, j);
sb.update(sleAcc);
adjustOwnerCount(sb, sleOwner, -1, j);
sb.update(sleOwner);
sb.apply(ctx_.rawView());
return tesSUCCESS;
}
+24
View File
@@ -18,6 +18,7 @@
//==============================================================================
#include <xrpld/app/misc/HashRouter.h>
#include <xrpld/app/misc/Manifest.h>
#include <xrpld/app/tx/apply.h>
#include <xrpld/app/tx/applySteps.h>
#include <xrpl/basics/Log.h>
@@ -73,6 +74,29 @@ checkValidity(
return {Validity::Valid, ""};
}
if (rules.enabled(featureOnChainManifests) &&
tx.getTxnType() == ttMANIFEST_SET &&
tx.isFieldPresent(sfTxnSignature) &&
tx.getFieldVL(sfTxnSignature).empty() &&
tx.isFieldPresent(sfSigningPubKey) &&
tx.getFieldVL(sfSigningPubKey).empty() && tx.isFieldPresent(sfManifest))
{
// perform alternative signature check over manifest
STObject const& manObj = const_cast<ripple::STTx&>(tx)
.getField(sfManifest)
.downcast<STObject>();
auto man = deserializeManifest(manObj);
if (!man.has_value() || !man->verify())
return {Validity::SigBad, "Manifest signature is bad"};
std::string reason;
if (!passesLocalChecks(tx, reason))
return {Validity::SigGoodOnly, reason};
return {Validity::Valid, ""};
}
if (flags & SF_SIGBAD)
// Signature is known bad
return {Validity::SigBad, "Transaction has bad signature."};
+1
View File
@@ -64,6 +64,7 @@
#include <xrpld/app/tx/detail/Remit.h>
#include <xrpld/app/tx/detail/SetAccount.h>
#include <xrpld/app/tx/detail/SetHook.h>
#include <xrpld/app/tx/detail/SetManifest.h>
#include <xrpld/app/tx/detail/SetOracle.h>
#include <xrpld/app/tx/detail/SetRegularKey.h>
#include <xrpld/app/tx/detail/SetRemarks.h>
+1 -4
View File
@@ -131,8 +131,6 @@ ApplyStateTable::generateTxMeta(
if (!hookEmission.empty())
meta.setHookEmissions(STArray{hookEmission, sfHookEmissions});
bool const recordDefaultAmounts = to.rules().enabled(fixXahauV1);
Mods newMod;
for (auto& item : items_)
{
@@ -245,8 +243,7 @@ ApplyStateTable::generateTxMeta(
for (auto const& obj : *curNode)
{
bool const shouldRecord =
(obj.getSType() == STI_AMOUNT && recordDefaultAmounts) ||
!obj.isDefault();
(obj.getSType() == STI_AMOUNT) || !obj.isDefault();
// save non-default values
if (shouldRecord &&
+4 -4
View File
@@ -22,7 +22,6 @@
#include <xrpld/core/JobQueue.h>
#include <xrpld/net/InfoSub.h>
#include <boost/asio/io_service.hpp>
namespace ripple {
@@ -39,16 +38,17 @@ protected:
explicit RPCSub(InfoSub::Source& source);
};
// VFALCO Why is the io_service needed?
std::shared_ptr<RPCSub>
make_RPCSub(
InfoSub::Source& source,
boost::asio::io_service& io_service,
JobQueue& jobQueue,
std::string const& strUrl,
std::string const& strUsername,
std::string const& strPassword,
Logs& logs);
Logs& logs,
// Max events buffered before new ones are dropped. Configurable so
// tests can exercise the drop path without queueing the full default.
std::size_t maxQueueSize = 16384);
} // namespace ripple
+32 -17
View File
@@ -122,12 +122,20 @@ public:
mComplete = complete;
mTimeout = timeout;
// Bind a non-owning `this` (not shared_from_this()) into mBuild.
// mBuild is a member, so capturing a shared_ptr to self here would
// form a reference cycle (this -> mBuild -> shared_ptr<this>) that
// never breaks, leaking the object and its socket FD after the
// request completes. mBuild is only ever invoked from
// handleRequest(), which always runs inside an async handler that
// already holds a shared_from_this(), so the object is guaranteed
// alive whenever mBuild fires — a raw `this` is safe.
request(
bSSL,
deqSites,
std::bind(
&HTTPClientImp::makeGet,
shared_from_this(),
this,
strPath,
std::placeholders::_1,
std::placeholders::_2),
@@ -393,8 +401,12 @@ public:
if (boost::regex_match(strHeader, smMatch, reBody)) // we got some body
mBody = smMatch[1];
bool const hasContentLength =
boost::regex_match(strHeader, smMatch, reSize);
mReceivedContentLength = hasContentLength;
std::size_t const responseSize = [&] {
if (boost::regex_match(strHeader, smMatch, reSize))
if (hasContentLength)
return beast::lexicalCast<std::size_t>(
std::string(smMatch[1]), maxResponseSize_);
return maxResponseSize_;
@@ -445,22 +457,24 @@ public:
JLOG(j_.trace()) << "Read error: " << mShutdown.message();
invokeComplete(mShutdown);
return;
}
else
{
if (mShutdown)
{
JLOG(j_.trace()) << "Complete.";
}
else
{
mResponse.commit(bytes_transferred);
std::string strBody{
{std::istreambuf_iterator<char>(&mResponse)},
std::istreambuf_iterator<char>()};
invokeComplete(ecResult, mStatus, mBody + strBody);
}
}
// Either the read completed normally or it ended at EOF. EOF is a
// successful completion for EOF-delimited responses, but it is an
// error when the server promised a Content-Length and closed early.
JLOG(j_.trace()) << "Complete.";
mResponse.commit(bytes_transferred);
std::string strBody{
{std::istreambuf_iterator<char>(&mResponse)},
std::istreambuf_iterator<char>()};
auto completeEc = ecResult;
if (completeEc == boost::asio::error::eof && !mReceivedContentLength)
completeEc.clear();
invokeComplete(completeEc, mStatus, mBody + strBody);
}
// Call cancel the deadline timer and invoke the completion routine.
@@ -516,6 +530,7 @@ private:
boost::asio::streambuf mHeader;
boost::asio::streambuf mResponse;
std::string mBody;
bool mReceivedContentLength = false;
const unsigned short mPort;
std::size_t const maxResponseSize_;
int mStatus;
+16 -3
View File
@@ -1585,6 +1585,10 @@ struct RPCCallImp
// callbackFuncP.
// Receive reply
if (ecResult)
Throw<std::runtime_error>(
"RPC transport error: " + ecResult.message());
if (strData.empty())
Throw<std::runtime_error>(
"no response from server. Please "
@@ -1748,6 +1752,7 @@ rpcClient(
}
{
//@@start blocking-request
boost::asio::io_service isService;
RPCCall::fromNetwork(
isService,
@@ -1771,6 +1776,7 @@ rpcClient(
headers);
isService.run(); // This blocks until there are no more
// outstanding async calls.
//@@end blocking-request
}
if (jvOutput.isMember("result"))
{
@@ -1881,15 +1887,21 @@ fromNetwork(
// Send request
// Number of bytes to try to receive if no
// Content-Length header received
constexpr auto RPC_REPLY_MAX_BYTES = megabytes(256);
// Number of bytes to try to receive if no Content-Length header is
// received. Webhook event deliveries ("event") ignore the response
// body, so a missing Content-Length must not pre-allocate the full
// 256MB RPC reply budget per in-flight delivery (maxInFlight can be
// 32 -> 8GB). Cap those small; genuine RPC replies (CLI) keep the
// large budget.
auto const RPC_REPLY_MAX_BYTES =
(strMethod == "event") ? megabytes(1) : megabytes(256);
using namespace std::chrono_literals;
// auto constexpr RPC_NOTIFY = 10min; // Wietse: lolwut 10 minutes for one
// HTTP call?
auto constexpr RPC_NOTIFY = 30s;
//@@start async-request
HTTPClient::request(
bSSL,
io_service,
@@ -1914,6 +1926,7 @@ fromNetwork(
std::placeholders::_3,
j),
j);
//@@end async-request
}
} // namespace RPCCall
+122 -57
View File
@@ -24,29 +24,30 @@
#include <xrpl/basics/contract.h>
#include <xrpl/json/to_string.h>
#include <deque>
#include <memory>
namespace ripple {
// Subscription object for JSON-RPC
class RPCSubImp : public RPCSub
class RPCSubImp : public RPCSub, public std::enable_shared_from_this<RPCSubImp>
{
public:
RPCSubImp(
InfoSub::Source& source,
boost::asio::io_service& io_service,
JobQueue& jobQueue,
std::string const& strUrl,
std::string const& strUsername,
std::string const& strPassword,
Logs& logs)
Logs& logs,
std::size_t maxQueueSize)
: RPCSub(source)
, m_io_service(io_service)
, m_jobQueue(jobQueue)
, mUrl(strUrl)
, mSSL(false)
, mUsername(strUsername)
, mPassword(strPassword)
, mSending(false)
, maxQueueSize_(maxQueueSize)
, j_(logs.journal("RPCSub"))
, logs_(logs)
{
@@ -78,14 +79,26 @@ public:
{
std::lock_guard sl(mLock);
// Wietse: we're not going to limit this, this is admin-port only, scale
// accordingly Dropping events just like this results in inconsistent
// data on the receiving end if (mDeque.size() >= eventQueueMax)
// {
// // Drop the previous event.
// JLOG(j_.warn()) << "RPCCall::fromNetwork drop";
// mDeque.pop_back();
// }
if (mDeque.size() >= maxQueueSize_)
{
// Always advance mSeq so consumers can detect the gap, but
// rate-limit the log: a hopelessly behind endpoint drops on
// every send() and would otherwise flood the log. Warn on
// the first drop of a run and then once per dropLogInterval.
if (mDropped++ % dropLogInterval == 0)
{
JLOG(j_.warn())
<< "RPCCall::fromNetwork drop: queue full ("
<< mDeque.size() << "), seq=" << mSeq
<< ", endpoint=" << mIp << ", dropped=" << mDropped;
}
++mSeq;
return;
}
// Endpoint caught up enough to accept again; reset so the next
// overflow burst logs its first drop immediately.
mDropped = 0;
auto jm = broadcast ? j_.debug() : j_.info();
JLOG(jm) << "RPCCall::fromNetwork push: " << jvObj;
@@ -97,10 +110,7 @@ public:
// Start a sending thread.
JLOG(j_.info()) << "RPCCall::fromNetwork start";
mSending = m_jobQueue.addJob(
jtCLIENT_SUBSCRIBE, "RPCSub::sendThread", [this]() {
sendThread();
});
startSendingJob();
}
}
@@ -121,48 +131,66 @@ public:
}
private:
// XXX Could probably create a bunch of send jobs in a single get of the
// lock.
// Maximum concurrent HTTP deliveries per batch. Bounds file
// descriptor usage while still allowing parallel delivery to
// capable endpoints. With a 1024 FD process limit shared across
// peers, clients, and the node store, 32 per subscriber is a
// meaningful but survivable chunk even with multiple subscribers.
static constexpr int maxInFlight = 32;
// Log one drop warning per this many drops while the queue stays
// full, to avoid flooding the log on a persistently behind endpoint.
static constexpr std::size_t dropLogInterval = 1000;
// Schedule a sending job. Must be called under mLock. The job holds a
// weak_ptr and re-locks it on entry, so the RPCSub is kept alive for
// the duration of the batch even if it is unsubscribed (and would
// otherwise be destroyed) concurrently — sendThread dereferences this
// only via that strong ref. mDeque events are delivered until the sub
// is gone, after which weak.lock() fails and the job is a no-op.
void
startSendingJob()
{
std::weak_ptr<RPCSubImp> weak = weak_from_this();
mSending = m_jobQueue.addJob(
jtCLIENT_SUBSCRIBE, "RPCSub::sendThread", [weak]() {
if (auto self = weak.lock())
self->sendThread();
});
}
void
sendThread()
{
Json::Value jvEvent;
bool bSend;
// Process exactly ONE batch per job, then re-queue if more events
// remain, rather than draining the whole backlog in a single job.
// A local io_service's .run() blocks this worker thread for the
// batch (up to the per-request timeout), so re-queueing between
// batches keeps one slow/hung subscriber from monopolising a
// job-queue worker and starving consensus/ledger/RPC work.
//
// mSending must be cleared under the lock on every non-requeue
// exit path; if it ever stays set without a job in flight, send()
// sees mSending == true and never restarts us, stalling the queue
// forever — the original bug (xrpld issue #6341).
boost::asio::io_service io_service;
int dispatched = 0;
do
try
{
{
// Obtain the lock to manipulate the queue and change sending.
std::lock_guard sl(mLock);
if (mDeque.empty())
{
mSending = false;
bSend = false;
}
else
while (!mDeque.empty() && dispatched < maxInFlight)
{
auto const [seq, env] = mDeque.front();
mDeque.pop_front();
jvEvent = env;
Json::Value jvEvent = env;
jvEvent["seq"] = seq;
bSend = true;
}
}
// Send outside of the lock.
if (bSend)
{
// XXX Might not need this in a try.
try
{
JLOG(j_.info()) << "RPCCall::fromNetwork: " << mIp;
RPCCall::fromNetwork(
m_io_service,
io_service,
mIp,
mPort,
mUsername,
@@ -173,21 +201,51 @@ private:
mSSL,
true,
logs_);
}
catch (const std::exception& e)
{
JLOG(j_.info())
<< "RPCCall::fromNetwork exception: " << e.what();
++dispatched;
}
}
} while (bSend);
// dispatched is always > 0 here (send() only starts a job
// after enqueuing, and the re-queue below only fires with a
// non-empty deque), but guard anyway so an empty batch can't
// log/spin — it falls straight through to clear mSending.
if (dispatched > 0)
{
JLOG(j_.info()) << "RPCCall::fromNetwork: " << mIp
<< " dispatching " << dispatched << " events";
io_service.run();
}
}
catch (std::exception const& e)
{
// Bail rather than re-queue: a persistently failing endpoint
// would otherwise spin the job queue. mSending is reset so the
// next send() restarts delivery.
JLOG(j_.warn()) << "RPCSub::sendThread exception: " << e.what();
std::lock_guard sl(mLock);
mSending = false;
return;
}
catch (...)
{
JLOG(j_.warn()) << "RPCSub::sendThread unknown exception";
std::lock_guard sl(mLock);
mSending = false;
return;
}
// Batch complete: re-queue for the next one (mSending stays set)
// or clear mSending if the queue drained — both under the lock to
// avoid a lost-wakeup race with send().
std::lock_guard sl(mLock);
if (mDeque.empty())
mSending = false;
else
startSendingJob();
}
private:
// Wietse: we're not going to limit this, this is admin-port only, scale
// accordingly enum { eventQueueMax = 32 };
boost::asio::io_service& m_io_service;
JobQueue& m_jobQueue;
std::string mUrl;
@@ -200,8 +258,15 @@ private:
int mSeq; // Next id to allocate.
std::size_t mDropped = 0; // Consecutive drops while queue is full.
bool mSending; // Sending threead is active.
// Maximum queued events before dropping. The default (16384) is a
// ~10-minute buffer at 100+ events/ledger; a hopelessly behind
// endpoint trips it and consumers detect the gap via the seq field.
std::size_t const maxQueueSize_;
std::deque<std::pair<int, Json::Value>> mDeque;
beast::Journal const j_;
@@ -217,21 +282,21 @@ RPCSub::RPCSub(InfoSub::Source& source) : InfoSub(source, Consumer())
std::shared_ptr<RPCSub>
make_RPCSub(
InfoSub::Source& source,
boost::asio::io_service& io_service,
JobQueue& jobQueue,
std::string const& strUrl,
std::string const& strUsername,
std::string const& strPassword,
Logs& logs)
Logs& logs,
std::size_t maxQueueSize)
{
return std::make_shared<RPCSubImp>(
std::ref(source),
std::ref(io_service),
std::ref(jobQueue),
strUrl,
strUsername,
strPassword,
logs);
logs,
maxQueueSize);
}
} // namespace ripple
+7 -1
View File
@@ -1190,7 +1190,13 @@ OverlayImpl::getManifestsMessage()
{
protocol::TMManifests tm;
app_.validatorManifests().for_each_manifest(
// A bounded subset of the cache rather than all of it; see
// ManifestCache::for_each_gossip_manifest for what is selected.
// This message is only rebuilt when the cache sequence changes, so a
// shift in which manifests are the most recently used does not by
// itself refresh it. That is acceptable: the pinned manifests are the
// ones a peer needs, and they are always included.
app_.validatorManifests().for_each_gossip_manifest(
[&tm](std::size_t s) { tm.mutable_list()->Reserve(s); },
[&tm, &hr = app_.getHashRouter()](Manifest const& manifest) {
tm.add_list()->set_stobject(
+189
View File
@@ -23,16 +23,197 @@
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/GRPCHandlers.h>
#include <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpl/hook/Misc.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/jss.h>
#include <algorithm>
#include <grpc/status.h>
#include <map>
#include <optional>
#include <vector>
namespace ripple {
namespace {
void
setEffectiveParameters(
STObject& result,
STObject const& entry,
std::shared_ptr<SLE const> const& definition)
{
bool const hasDefinition =
definition && definition->isFieldPresent(sfHookParameters);
bool const hasEntry = entry.isFieldPresent(sfHookParameters);
if (!hasDefinition && !hasEntry)
return; // LCOV_EXCL_LINE
STArray parameters{sfHookParameters};
auto merge = [&parameters](STArray const& source) {
for (auto const& parameter : source)
{
Blob const name = parameter.getFieldVL(sfHookParameterName);
Blob const value = parameter.getFieldVL(sfHookParameterValue);
auto existing = std::find_if(
parameters.begin(),
parameters.end(),
[&name](STObject const& current) {
return current.getFieldVL(sfHookParameterName) == name;
});
if (existing == parameters.end())
{
STObject merged = STObject::makeInnerObject(sfHookParameter);
merged.setFieldVL(sfHookParameterName, name);
// gatherHookParameters treats an absent value as an empty
// value. Keep it explicit in the effective response.
merged.setFieldVL(sfHookParameterValue, value);
parameters.push_back(std::move(merged));
}
else
{
existing->setFieldVL(sfHookParameterValue, value);
}
}
};
if (hasDefinition)
merge(definition->getFieldArray(sfHookParameters));
if (hasEntry)
merge(entry.getFieldArray(sfHookParameters));
result.setFieldArray(sfHookParameters, std::move(parameters));
}
uint256
effectiveHookOn(
STObject const& entry,
std::shared_ptr<SLE const> const& definition,
SField const& direction)
{
if (entry.isFieldPresent(direction))
return entry.getFieldH256(direction);
if (entry.isFieldPresent(sfHookOn))
return entry.getFieldH256(sfHookOn);
if (!definition)
return uint256{0}; // LCOV_EXCL_LINE
if (definition->isFieldPresent(direction))
return definition->getFieldH256(direction);
if (definition->isFieldPresent(sfHookOn))
return definition->getFieldH256(sfHookOn);
return uint256{0}; // LCOV_EXCL_LINE
}
STObject
effectiveHook(
STObject const& entry,
std::shared_ptr<SLE const> const& definition)
{
STObject result = STObject::makeInnerObject(sfHook);
// If there is no definition, return the entry as is
if (!definition)
return result; // LCOV_EXCL_LINE
// A blank sfHooks slot is meaningful: preserve it as an empty Hook
// object instead of applying the defaults used for an installed hook.
if (!entry.isFieldPresent(sfHookHash))
return result; // LCOV_EXCL_LINE
// These fields are deliberately selected rather than copying the raw
// objects: HookDefinition bookkeeping must not leak from account_info.
if (entry.isFieldPresent(sfHookHash))
result.setFieldH256(sfHookHash, entry.getFieldH256(sfHookHash));
// CreateCode is not included in AccountRoot Hook field
// if (definition->isFieldPresent(sfCreateCode))
// result.setFieldVL(sfCreateCode,
// definition->getFieldVL(sfCreateCode));
if (entry.isFieldPresent(sfHookGrants))
result.setFieldArray(sfHookGrants, entry.getFieldArray(sfHookGrants));
if (entry.isFieldPresent(sfHookNamespace))
result.setFieldH256(
sfHookNamespace, entry.getFieldH256(sfHookNamespace));
else if (definition->isFieldPresent(sfHookNamespace))
result.setFieldH256(
sfHookNamespace, definition->getFieldH256(sfHookNamespace));
if (definition->isFieldPresent(sfHookApiVersion))
result.setFieldU16(
sfHookApiVersion, definition->getFieldU16(sfHookApiVersion));
if (entry.isFieldPresent(sfHookCanEmit))
result.setFieldH256(sfHookCanEmit, entry.getFieldH256(sfHookCanEmit));
else if (definition->isFieldPresent(sfHookCanEmit))
result.setFieldH256(
sfHookCanEmit, definition->getFieldH256(sfHookCanEmit));
else
result.setFieldH256(sfHookCanEmit, UINT256_BIT[ttHOOK_SET]);
if (entry.isFieldPresent(sfHookName))
result.setFieldVL(sfHookName, entry.getFieldVL(sfHookName));
if (entry.isFieldPresent(sfFlags))
result.setFieldU32(sfFlags, entry.getFieldU32(sfFlags));
else if (definition->isFieldPresent(sfFlags))
result.setFieldU32(sfFlags, definition->getFieldU32(sfFlags));
auto const incoming = effectiveHookOn(entry, definition, sfHookOnIncoming);
auto const outgoing = effectiveHookOn(entry, definition, sfHookOnOutgoing);
if (incoming == outgoing)
result.setFieldH256(sfHookOn, incoming);
else
{
result.setFieldH256(sfHookOnIncoming, incoming);
result.setFieldH256(sfHookOnOutgoing, outgoing);
}
setEffectiveParameters(result, entry, definition);
return result;
}
Json::Value
accountHooks(ReadView const& ledger, AccountID const& account)
{
Json::Value hooks(Json::arrayValue);
auto const hookSLE = ledger.read(keylet::hook(account));
if (!hookSLE || !hookSLE->isFieldPresent(sfHooks))
return hooks;
std::map<uint256, std::shared_ptr<SLE const>> definitions;
for (auto const& hookElement : hookSLE->getFieldArray(sfHooks))
{
auto const& entry = hookElement.downcast<STObject const>();
std::shared_ptr<SLE const> definition;
if (entry.isFieldPresent(sfHookHash))
{
auto const hash = entry.getFieldH256(sfHookHash);
auto [it, inserted] = definitions.emplace(hash, nullptr);
if (inserted)
it->second = ledger.read(keylet::hookDefinition(hash));
definition = it->second;
}
Json::Value wrapped(Json::objectValue);
wrapped[sfHook.jsonName] =
effectiveHook(entry, definition).getJson(JsonOptions::none);
hooks.append(std::move(wrapped));
}
return hooks;
}
} // namespace
// {
// account: <ident>,
// ledger_hash : <ledger>
@@ -52,6 +233,11 @@ doAccountInfo(RPC::JsonContext& context)
{
auto& params = context.params;
if (params.isMember(jss::hooks) && !params[jss::hooks].isBool())
return RPC::invalid_field_error(jss::hooks);
bool const includeHooks =
params.isMember(jss::hooks) && params[jss::hooks].asBool();
std::string strIdent;
if (params.isMember(jss::account))
{
@@ -146,6 +332,9 @@ doAccountInfo(RPC::JsonContext& context)
result[jss::account_flags] = std::move(acctFlags);
if (includeHooks)
result[jss::hooks] = accountHooks(*ledger, accountID);
// The document[https://xrpl.org/account_info.html#account_info] states
// that signer_lists is a bool, however assigning any string value
// works. Do not allow this. This check is for api Version 2 onwards
+59 -3
View File
@@ -18,15 +18,20 @@
//==============================================================================
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/ledger/OpenLedger.h>
#include <xrpld/app/misc/HashRouter.h>
#include <xrpld/app/misc/Manifest.h>
#include <xrpld/app/misc/Transaction.h>
#include <xrpld/app/misc/TxQ.h>
#include <xrpld/app/tx/apply.h>
#include <xrpld/app/tx/detail/SetManifest.h> // makeSetManifestTx
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/GRPCHandlers.h>
#include <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpld/rpc/detail/TransactionSign.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/resource/Fees.h>
@@ -89,9 +94,20 @@ doInject(RPC::JsonContext& context)
Json::Value
doSubmit(RPC::JsonContext& context)
{
Json::Value jvResult;
context.loadType = Resource::feeMediumBurdenRPC;
if (!context.params.isMember(jss::tx_blob))
bool const hasManifest = context.params.isMember(jss::manifest);
bool const hasTxBlob = context.params.isMember(jss::tx_blob);
if (hasManifest && hasTxBlob)
{
return RPC::make_error(
rpcINVALID_PARAMS,
"Specify exactly one of either `tx_blob` or `manifest`");
}
else if (!hasTxBlob && !hasManifest)
{
auto const failType = getFailHard(context);
@@ -117,9 +133,49 @@ doSubmit(RPC::JsonContext& context)
return ret;
}
Json::Value jvResult;
std::string txBlob =
hasTxBlob ? context.params[jss::tx_blob].asString() : "";
auto ret = strUnHex(context.params[jss::tx_blob].asString());
if (hasManifest)
{
// OnChainManifests amendment accepts a manifest submission here; turn
// it into the transaction that carries it and drop through to normal
// tx_blob processing below.
auto const view = context.app.openLedger().current();
// The transaction built below carries no account signature; its
// authority is the manifest's own master and ephemeral signatures,
// which checkValidity() only honours once the amendment is active.
// Without this the submitter is told their transaction is unsigned,
// which reads as their mistake. It isn't -- the feature is not live
// yet -- so say so before touching the manifest at all.
if (!view->rules().enabled(featureOnChainManifests))
return RPC::make_error(
rpcNOT_ENABLED,
"The OnChainManifests amendment is not enabled on this "
"network. Manifest submission will work once it activates; "
"nothing is wrong with this request.");
auto const raw = strUnHex(context.params[jss::manifest].asString());
if (!raw || raw->empty())
return rpcError(rpcINVALID_PARAMS);
auto const hex = makeSetManifestTx(
makeSlice(*raw),
context.app.config().NETWORK_ID,
*view,
context.app.journal("Submit"));
if (!hex)
{
jvResult[jss::error] = "invalidManifest";
return jvResult;
}
txBlob = *hex;
}
auto ret = strUnHex(txBlob);
if (!ret || !ret->size())
return rpcError(rpcINVALID_PARAMS);
-1
View File
@@ -76,7 +76,6 @@ doSubscribe(RPC::JsonContext& context)
{
auto rspSub = make_RPCSub(
context.app.getOPs(),
context.app.getIOService(),
context.app.getJobQueue(),
strUrl,
strUsername,