Compare commits

...

13 Commits

Author SHA1 Message Date
Nicholas Dudfield
7245a161f8 Merge remote-tracking branch 'origin/dev' into multi-sig-nested-squash
# Conflicts:
#	include/xrpl/protocol/Feature.h
#	include/xrpl/protocol/detail/features.macro
2026-05-06 12:52:58 +07:00
tequ
5e8d26f67a refactor: Calculate numFeatures automatically (#5324) (#739)
Co-authored-by: Ed Hennis <ed@ripple.com>
2026-04-30 18:17:50 +10:00
tequ
a6186d7855 IOURewardClaim (#500) 2026-04-30 15:27:51 +10:00
Niq Dudfield
3cd7a30754 Merge branch 'dev' into multi-sig-nested-squash 2026-04-30 09:46:55 +07:00
Nicholas Dudfield
e27eb4f12d fix: bump numFeatures to 116 for nestedmultisign + ioulockedbalanceinvariant 2026-04-29 15:25:23 +07:00
Nicholas Dudfield
37bfa0af49 Merge remote-tracking branch 'origin/multi-sig-nested-squash' into multisign-xahaud-pr-squashed 2026-04-29 15:14:51 +07:00
Nicholas Dudfield
69b82beafa Merge remote-tracking branch 'origin/dev' into multisign-xahaud-pr-squashed
# Conflicts:
#	include/xrpl/protocol/detail/features.macro
2026-04-29 15:10:20 +07:00
Niq Dudfield
198ff08ee5 Merge branch 'dev' into multi-sig-nested-squash 2026-04-29 12:16:17 +07:00
Nicholas Dudfield
d6aad6744e chore: align nested multisign quorum terminology 2026-04-29 11:33:54 +07:00
Nicholas Dudfield
339f1b7f6f test: port nested multisign review coverage 2026-04-29 11:25:01 +07:00
Nicholas Dudfield
bb6553193e fix: port nested multisign review fixes after dev merge 2026-04-29 11:01:41 +07:00
Nicholas Dudfield
ffcfd94327 Merge remote-tracking branch 'origin/dev' into multisign-xahaud-pr-squashed
# Conflicts:
#	include/xrpl/protocol/Feature.h
#	src/libxrpl/protocol/Feature.cpp
#	src/test/jtx/multisign.h
#	src/xrpld/app/tx/detail/Transactor.cpp
2026-04-29 11:00:14 +07:00
RichardAH
1baeb9fb33 Multi sig nested (#572) (squash) 2026-02-04 11:56:16 +10:00
24 changed files with 3363 additions and 346 deletions

View File

@@ -189,6 +189,7 @@
#define sfSignatureReward ((6U << 16U) + 29U)
#define sfMinAccountCreateAmount ((6U << 16U) + 30U)
#define sfLPTokenBalance ((6U << 16U) + 31U)
#define sfTrustLineRewardAccumulator ((6U << 16U) + 99U)
#define sfPublicKey ((7U << 16U) + 1U)
#define sfMessageKey ((7U << 16U) + 2U)
#define sfSigningPubKey ((7U << 16U) + 3U)
@@ -255,6 +256,7 @@
#define sfIssuingChainIssue ((24U << 16U) + 2U)
#define sfAsset ((24U << 16U) + 3U)
#define sfAsset2 ((24U << 16U) + 4U)
#define sfClaimCurrency ((24U << 16U) + 5U)
#define sfXChainBridge ((25U << 16U) + 1U)
#define sfTransactionMetaData ((14U << 16U) + 2U)
#define sfCreatedNode ((14U << 16U) + 3U)
@@ -292,6 +294,8 @@
#define sfActiveValidator ((14U << 16U) + 95U)
#define sfGenesisMint ((14U << 16U) + 96U)
#define sfRemark ((14U << 16U) + 97U)
#define sfHighReward ((14U << 16U) + 98U)
#define sfLowReward ((14U << 16U) + 99U)
#define sfSigners ((15U << 16U) + 3U)
#define sfSignerEntries ((15U << 16U) + 4U)
#define sfTemplate ((15U << 16U) + 5U)

View File

@@ -33,35 +33,39 @@
*
* Steps required to add new features to the code:
*
* 1) In this file, increment `numFeatures` and add a uint256 declaration
* for the feature at the bottom
* 2) Add a uint256 definition for the feature to the corresponding source
* file (Feature.cpp). Use `registerFeature` to create the feature with
* the feature's name, `Supported::no`, and `VoteBehavior::DefaultNo`. This
* should be the only place the feature's name appears in code as a string.
* 3) Use the uint256 as the parameter to `view.rules.enabled()` to
* control flow into new code that this feature limits.
* 4) If the feature development is COMPLETE, and the feature is ready to be
* SUPPORTED, change the `registerFeature` parameter to Supported::yes.
* 5) When the feature is ready to be ENABLED, change the `registerFeature`
* parameter to `VoteBehavior::DefaultYes`.
* In general, any newly supported amendments (`Supported::yes`) should have
* a `VoteBehavior::DefaultNo` for at least one full release cycle. High
* priority bug fixes can be an exception to this rule of thumb.
* 1) Add the appropriate XRPL_FEATURE or XRPL_FIX macro definition for the
* feature to features.macro with the feature's name, `Supported::no`, and
* `VoteBehavior::DefaultNo`.
*
* 2) Use the generated variable name as the parameter to `view.rules.enabled()`
* to control flow into new code that this feature limits. (featureName or
* fixName)
*
* 3) If the feature development is COMPLETE, and the feature is ready to be
* SUPPORTED, change the macro parameter in features.macro to Supported::yes.
*
* 4) In general, any newly supported amendments (`Supported::yes`) should have
* a `VoteBehavior::DefaultNo` indefinitely so that external governance can
* make the decision on when to activate it. High priority bug fixes can be
* an exception to this rule. In such cases, ensure the fix has been
* clearly communicated to the community using appropriate channels,
* then change the macro parameter in features.macro to
* `VoteBehavior::DefaultYes`. The communication process is beyond
* the scope of these instructions.
*
*
* When a feature has been enabled for several years, the conditional code
* may be removed, and the feature "retired". To retire a feature:
* 1) Remove the uint256 declaration from this file.
* 2) MOVE the uint256 definition in Feature.cpp to the "retired features"
* section at the end of the file.
* 3) CHANGE the name of the variable to start with "retired".
* 4) CHANGE the parameters of the `registerFeature` call to `Supported::yes`
* and `VoteBehavior::DefaultNo`.
*
* 1) MOVE the macro definition in features.macro to the "retired features"
* section at the end of the file, and change the macro to XRPL_RETIRE.
*
* The feature must remain registered and supported indefinitely because it
* still exists in the ledger, but there is no need to vote for it because
* there's nothing to vote for. If it is removed completely from the code, any
* instances running that code will get amendment blocked. Removing the
* feature from the ledger is beyond the scope of these instructions.
* may exist in the Amendments object on ledger. There is no need to vote
* for it because there's nothing to vote for. If the feature definition is
* removed completely from the code, any instances running that code will get
* amendment blocked. Removing the feature from the ledger is beyond the scope
* of these instructions.
*
*/
@@ -76,11 +80,32 @@ allAmendments();
namespace detail {
#pragma push_macro("XRPL_FEATURE")
#undef XRPL_FEATURE
#pragma push_macro("XRPL_FIX")
#undef XRPL_FIX
#pragma push_macro("XRPL_RETIRE")
#undef XRPL_RETIRE
#define XRPL_FEATURE(name, supported, vote) +1
#define XRPL_FIX(name, supported, vote) +1
#define XRPL_RETIRE(name) +1
// This value SHOULD be equal to the number of amendments registered in
// Feature.cpp. Because it's only used to reserve storage, and determine how
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
// the actual number of amendments. A LogicError on startup will verify this.
static constexpr std::size_t numFeatures = 115;
static constexpr std::size_t numFeatures =
(0 +
#include <xrpl/protocol/detail/features.macro>
);
#undef XRPL_RETIRE
#pragma pop_macro("XRPL_RETIRE")
#undef XRPL_FIX
#pragma pop_macro("XRPL_FIX")
#undef XRPL_FEATURE
#pragma pop_macro("XRPL_FEATURE")
/** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated
@@ -320,12 +345,17 @@ foreachFeature(FeatureBitset bs, F&& f)
#undef XRPL_FEATURE
#pragma push_macro("XRPL_FIX")
#undef XRPL_FIX
#pragma push_macro("XRPL_RETIRE")
#undef XRPL_RETIRE
#define XRPL_FEATURE(name, supported, vote) extern uint256 const feature##name;
#define XRPL_FIX(name, supported, vote) extern uint256 const fix##name;
#define XRPL_RETIRE(name)
#include <xrpl/protocol/detail/features.macro>
#undef XRPL_RETIRE
#pragma pop_macro("XRPL_RETIRE")
#undef XRPL_FIX
#pragma pop_macro("XRPL_FIX")
#undef XRPL_FEATURE

View File

@@ -195,6 +195,71 @@ STTx::getTransactionID() const
return tid_;
}
//------------------------------------------------------------------------------
// Multi-sign depth and leaf limits
//------------------------------------------------------------------------------
/** Maximum nesting depth for nested multi-signing (featureNestedMultiSign). */
constexpr int nestedMultiSignMaxDepth = 4;
/** Maximum nesting depth when nested multi-signing is disabled (flat only). */
constexpr int legacyMultiSignMaxDepth = 1;
/** Maximum total leaf signers across the entire nested tree.
Bounds worst-case signature verification cost. Only enforced when
featureNestedMultiSign is enabled; flat signing is already capped by the
per-array multisign limit.
*/
constexpr std::size_t nestedMultiSignMaxLeafSigners = 64;
//------------------------------------------------------------------------------
// Multi-sign signer entry helpers
//------------------------------------------------------------------------------
/** Count populated fields in an STObject.
STObject::getCount() includes template slots for optional fields that are
not present. Signer shape validation needs only populated fields.
*/
inline std::size_t
countPresentFields(STObject const& obj)
{
std::size_t count = 0;
for (auto const& field : obj)
{
if (field.getSType() != STI_NOTPRESENT)
++count;
}
return count;
}
/** A leaf signer has Account + SigningPubKey + TxnSignature only. */
inline bool
isLeafSigner(STObject const& signer)
{
return signer.isFieldPresent(sfAccount) &&
signer.isFieldPresent(sfSigningPubKey) &&
signer.isFieldPresent(sfTxnSignature) &&
!signer.isFieldPresent(sfSigners) && countPresentFields(signer) == 3;
}
/** A nested signer has Account + Signers only. */
inline bool
isNestedSigner(STObject const& signer)
{
return signer.isFieldPresent(sfAccount) &&
signer.isFieldPresent(sfSigners) &&
!signer.isFieldPresent(sfSigningPubKey) &&
!signer.isFieldPresent(sfTxnSignature) &&
countPresentFields(signer) == 2;
}
/** True when a signer entry is either a valid leaf or a valid nested signer. */
inline bool
isValidSignerEntry(STObject const& signer)
{
return isLeafSigner(signer) || isNestedSigner(signer);
}
} // namespace ripple
#endif

View File

@@ -23,6 +23,9 @@
#if !defined(XRPL_FIX)
#error "undefined macro: XRPL_FIX"
#endif
#if !defined(XRPL_RETIRE)
#error "undefined macro: XRPL_RETIRE"
#endif
// clang-format off
@@ -31,6 +34,8 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FEATURE(NestedMultiSign, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(IOURewardClaim, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (IOULockedBalanceInvariant, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (ImportIssuer, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FEATURE(HookAPISerializedType240, Supported::yes, VoteBehavior::DefaultNo)
@@ -56,16 +61,16 @@ XRPL_FIX (DisallowIncomingV1, Supported::yes, VoteBehavior::DefaultYe
XRPL_FEATURE(XChainBridge, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(AMM, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (ReducedOffersV1, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FEATURE(HooksUpdate2, Supported::yes, VoteBehavior::DefaultNo);
XRPL_FEATURE(HookOnV2, Supported::yes, VoteBehavior::DefaultNo);
XRPL_FIX (HookAPI20251128, Supported::yes, VoteBehavior::DefaultYes);
XRPL_FIX (CronStacking, Supported::yes, VoteBehavior::DefaultYes);
XRPL_FEATURE(ExtendedHookState, Supported::yes, VoteBehavior::DefaultNo);
XRPL_FIX (InvalidTxFlags, Supported::yes, VoteBehavior::DefaultYes);
XRPL_FEATURE(Cron, Supported::yes, VoteBehavior::DefaultNo);
XRPL_FEATURE(IOUIssuerWeakTSH, Supported::yes, VoteBehavior::DefaultNo);
XRPL_FEATURE(DeepFreeze, Supported::yes, VoteBehavior::DefaultNo);
XRPL_FIX (ProvisionalDoubleThreading, Supported::yes, VoteBehavior::DefaultYes);
XRPL_FEATURE(HooksUpdate2, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(HookOnV2, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (HookAPI20251128, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (CronStacking, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FEATURE(ExtendedHookState, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (InvalidTxFlags, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FEATURE(Cron, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(IOUIssuerWeakTSH, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(DeepFreeze, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (ProvisionalDoubleThreading, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FEATURE(Clawback, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (RewardClaimFlags, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FEATURE(HookCanEmit, Supported::yes, VoteBehavior::DefaultNo)
@@ -149,4 +154,24 @@ XRPL_FIX (NFTokenDirV1, Supported::yes, VoteBehavior::Obsolete)
XRPL_FEATURE(NonFungibleTokensV1, Supported::yes, VoteBehavior::Obsolete)
XRPL_FEATURE(CryptoConditionsSuite, Supported::yes, VoteBehavior::Obsolete)
// The following amendments have been active for at least two years. Their
// 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(MultiSign)
XRPL_RETIRE(TrustSetAuth)
XRPL_RETIRE(FeeEscalation)
XRPL_RETIRE(PayChan)
XRPL_RETIRE(CryptoConditions)
XRPL_RETIRE(TickSize)
XRPL_RETIRE(fix1368)
XRPL_RETIRE(Escrow)
XRPL_RETIRE(fix1373)
XRPL_RETIRE(EnforceInvariants)
XRPL_RETIRE(SortedDirectories)
XRPL_RETIRE(fix1201)
XRPL_RETIRE(fix1512)
XRPL_RETIRE(fix1523)
XRPL_RETIRE(fix1528)
// clang-format on

View File

@@ -396,6 +396,8 @@ LEDGER_ENTRY(ltRIPPLE_STATE, 0x0072, RippleState, state, ({
{sfHighQualityOut, soeOPTIONAL},
{sfLockedBalance, soeOPTIONAL},
{sfLockCount, soeOPTIONAL},
{sfHighReward, soeOPTIONAL},
{sfLowReward, soeOPTIONAL},
}))
/** The ledger object which lists the network's fee settings.

View File

@@ -257,6 +257,7 @@ TYPED_SFIELD(sfPrice, AMOUNT, 28)
TYPED_SFIELD(sfSignatureReward, AMOUNT, 29)
TYPED_SFIELD(sfMinAccountCreateAmount, AMOUNT, 30)
TYPED_SFIELD(sfLPTokenBalance, AMOUNT, 31)
TYPED_SFIELD(sfTrustLineRewardAccumulator,AMOUNT, 99)
// variable length (common)
TYPED_SFIELD(sfPublicKey, VL, 1)
@@ -340,6 +341,7 @@ TYPED_SFIELD(sfLockingChainIssue, ISSUE, 1)
TYPED_SFIELD(sfIssuingChainIssue, ISSUE, 2)
TYPED_SFIELD(sfAsset, ISSUE, 3)
TYPED_SFIELD(sfAsset2, ISSUE, 4)
TYPED_SFIELD(sfClaimCurrency, ISSUE, 5)
// bridge
TYPED_SFIELD(sfXChainBridge, XCHAIN_BRIDGE, 1)
@@ -386,6 +388,8 @@ UNTYPED_SFIELD(sfImportVLKey, OBJECT, 94)
UNTYPED_SFIELD(sfActiveValidator, OBJECT, 95)
UNTYPED_SFIELD(sfGenesisMint, OBJECT, 96)
UNTYPED_SFIELD(sfRemark, OBJECT, 97)
UNTYPED_SFIELD(sfHighReward, OBJECT, 98)
UNTYPED_SFIELD(sfLowReward, OBJECT, 99)
// array of objects (common)
// ARRAY/1 is reserved for end of array

View File

@@ -550,6 +550,7 @@ TRANSACTION(ttIMPORT, 97, Import, ({
* from a specified hook */
TRANSACTION(ttCLAIM_REWARD, 98, ClaimReward, ({
{sfIssuer, soeOPTIONAL},
{sfClaimCurrency, soeOPTIONAL},
}))
/** This transaction invokes a hook, providing arbitrary data. Essentially as a 0 drop payment. **/

View File

@@ -250,12 +250,9 @@ FeatureCollections::registerFeature(
Feature const* i = getByName(name);
if (!i)
{
// If this check fails, and you just added a feature, increase the
// numFeatures value in Feature.h
check(
features.size() < detail::numFeatures,
"More features defined than allocated. Adjust numFeatures in "
"Feature.h.");
"More features defined than allocated.");
auto const f = sha512Half(Slice(name.data(), name.size()));
@@ -424,45 +421,26 @@ featureToName(uint256 const& f)
#undef XRPL_FEATURE
#pragma push_macro("XRPL_FIX")
#undef XRPL_FIX
#pragma push_macro("XRPL_RETIRE")
#undef XRPL_RETIRE
#define XRPL_FEATURE(name, supported, vote) \
uint256 const feature##name = registerFeature(#name, supported, vote);
#define XRPL_FIX(name, supported, vote) \
uint256 const fix##name = registerFeature("fix" #name, supported, vote);
#define XRPL_RETIRE(name) \
[[deprecated("The referenced amendment has been retired"), maybe_unused]] \
uint256 const retired##name = retireFeature(#name);
#include <xrpl/protocol/detail/features.macro>
#undef XRPL_RETIRE
#pragma pop_macro("XRPL_RETIRE")
#undef XRPL_FIX
#pragma pop_macro("XRPL_FIX")
#undef XRPL_FEATURE
#pragma pop_macro("XRPL_FEATURE")
// clang-format off
// The following amendments have been active for at least two years. Their
// 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
[[deprecated("The referenced amendment has been retired"), maybe_unused]]
uint256 const
retiredMultiSign = retireFeature("MultiSign"),
retiredTrustSetAuth = retireFeature("TrustSetAuth"),
retiredFeeEscalation = retireFeature("FeeEscalation"),
retiredPayChan = retireFeature("PayChan"),
retiredCryptoConditions = retireFeature("CryptoConditions"),
retiredTickSize = retireFeature("TickSize"),
retiredFix1368 = retireFeature("fix1368"),
retiredEscrow = retireFeature("Escrow"),
retiredFix1373 = retireFeature("fix1373"),
retiredEnforceInvariants = retireFeature("EnforceInvariants"),
retiredSortedDirectories = retireFeature("SortedDirectories"),
retiredFix1201 = retireFeature("fix1201"),
retiredFix1512 = retireFeature("fix1512"),
retiredFix1523 = retireFeature("fix1523"),
retiredFix1528 = retireFeature("fix1528");
// clang-format on
// All of the features should now be registered, since variables in a cpp file
// are initialized from top to bottom.
//

View File

@@ -49,8 +49,9 @@ InnerObjectFormats::InnerObjectFormats()
sfSigner.getCode(),
{
{sfAccount, soeREQUIRED},
{sfSigningPubKey, soeREQUIRED},
{sfTxnSignature, soeREQUIRED},
{sfSigningPubKey, soeOPTIONAL},
{sfTxnSignature, soeOPTIONAL},
{sfSigners, soeOPTIONAL},
});
add(sfMajority.jsonName,
@@ -253,6 +254,24 @@ InnerObjectFormats::InnerObjectFormats()
{sfIssuer, soeREQUIRED},
{sfCredentialType, soeREQUIRED},
});
add(sfHighReward.jsonName,
sfHighReward.getCode(),
{
{sfRewardLgrFirst, soeREQUIRED},
{sfRewardLgrLast, soeREQUIRED},
{sfRewardTime, soeREQUIRED},
{sfTrustLineRewardAccumulator, soeREQUIRED},
});
add(sfLowReward.jsonName,
sfLowReward.getCode(),
{
{sfRewardLgrFirst, soeREQUIRED},
{sfRewardLgrLast, soeREQUIRED},
{sfRewardTime, soeREQUIRED},
{sfTrustLineRewardAccumulator, soeREQUIRED},
});
}
InnerObjectFormats const&

View File

@@ -370,11 +370,6 @@ STTx::checkMultiSign(
STArray const& signers{getFieldArray(sfSigners)};
// There are well known bounds that the number of signers must be within.
if (signers.size() < minMultiSigners ||
signers.size() > maxMultiSigners(&rules))
return Unexpected("Invalid Signers array size.");
// We can ease the computational load inside the loop a bit by
// pre-constructing part of the data that we hash. Fill a Serializer
// with the stuff that stays constant from signature to signature.
@@ -387,64 +382,117 @@ STTx::checkMultiSign(
bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) ||
(requireCanonicalSig == RequireFullyCanonicalSig::yes);
// Signers must be in sorted order by AccountID.
AccountID lastAccountID(beast::zero);
bool const isWildcardNetwork =
isFieldPresent(sfNetworkID) && getFieldU32(sfNetworkID) == 65535;
for (auto const& signer : signers)
{
auto const accountID = signer.getAccountID(sfAccount);
// Set max depth and leaf cap based on feature flag.
bool const nested = rules.enabled(featureNestedMultiSign);
int const maxDepth =
nested ? nestedMultiSignMaxDepth : legacyMultiSignMaxDepth;
std::size_t const maxLeafSigners =
nested ? nestedMultiSignMaxLeafSigners : maxMultiSigners(&rules);
std::size_t totalLeafSigners = 0;
// The account owner may not multisign for themselves.
if (accountID == txnAccountID)
return Unexpected("Invalid multisigner.");
// Define recursive lambda for checking signatures at any depth.
std::function<Expected<void, std::string>(
STArray const&, AccountID const&, int)>
checkSignersArray;
// No duplicate signers allowed.
if (lastAccountID == accountID)
return Unexpected("Duplicate Signers not allowed.");
checkSignersArray = [&](STArray const& signersArray,
[[maybe_unused]] AccountID const& parentAccountID,
int depth) -> Expected<void, std::string> {
// Check depth limit.
if (depth > maxDepth)
return Unexpected("Multi-signing depth limit exceeded.");
// Accounts must be in order by account ID. No duplicates allowed.
if (lastAccountID > accountID)
return Unexpected("Unsorted Signers array.");
// There are well known bounds that the number of signers must be
// within.
if (signersArray.size() < minMultiSigners ||
signersArray.size() > maxMultiSigners(&rules))
return Unexpected("Invalid Signers array size.");
// The next signature must be greater than this one.
lastAccountID = accountID;
// Signers must be in sorted order by AccountID.
AccountID lastAccountID(beast::zero);
// Verify the signature.
bool validSig = false;
try
for (auto const& signer : signersArray)
{
Serializer s = dataStart;
finishMultiSigningData(accountID, s);
auto const accountID = signer.getAccountID(sfAccount);
auto spk = signer.getFieldVL(sfSigningPubKey);
// The account owner may not multisign for themselves.
if (accountID == txnAccountID)
return Unexpected("Invalid multisigner.");
if (publicKeyType(makeSlice(spk)))
// No duplicate signers allowed.
if (lastAccountID == accountID)
return Unexpected("Duplicate Signers not allowed.");
// Accounts must be in order by account ID. No duplicates allowed.
if (lastAccountID > accountID)
return Unexpected("Unsorted Signers array.");
// The next signature must be greater than this one.
lastAccountID = accountID;
if (isNestedSigner(signer))
{
Blob const signature = signer.getFieldVL(sfTxnSignature);
if (!nested)
return Unexpected("FeatureNestedMultiSign is disabled");
// wildcard network gets a free pass
validSig = isWildcardNetwork ||
verify(PublicKey(makeSlice(spk)),
s.slice(),
makeSlice(signature),
fullyCanonical);
STArray const& nestedSigners = signer.getFieldArray(sfSigners);
auto result =
checkSignersArray(nestedSigners, accountID, depth + 1);
if (!result)
return result;
}
else if (isLeafSigner(signer))
{
if (++totalLeafSigners > maxLeafSigners)
return Unexpected(std::string("Too many leaf signers."));
bool validSig = false;
try
{
Serializer s = dataStart;
finishMultiSigningData(accountID, s);
auto spk = signer.getFieldVL(sfSigningPubKey);
if (publicKeyType(makeSlice(spk)))
{
Blob const signature =
signer.getFieldVL(sfTxnSignature);
// wildcard network gets a free pass
validSig = isWildcardNetwork ||
verify(PublicKey(makeSlice(spk)),
s.slice(),
makeSlice(signature),
fullyCanonical);
}
}
catch (std::exception const&)
{
// We assume any problem lies with the signature.
validSig = false;
}
if (!validSig)
return Unexpected(
std::string("Invalid signature on account ") +
toBase58(accountID) + ".");
}
else
{
return Unexpected(
std::string("Malformed signer entry for account ") +
toBase58(accountID) + ".");
}
}
catch (std::exception const&)
{
// We assume any problem lies with the signature.
validSig = false;
}
if (!validSig)
return Unexpected(
std::string("Invalid signature on account ") +
toBase58(accountID) + ".");
}
// All signatures verified.
return {};
return {};
};
// Start the recursive check at depth 1.
return checkSignersArray(signers, txnAccountID, 1);
}
//------------------------------------------------------------------------------

View File

@@ -27,6 +27,14 @@ namespace ripple {
namespace test {
struct ClaimReward_test : public beast::unit_test::suite
{
private:
// helper
void static overrideFlag(Json::Value& jv)
{
jv[jss::Flags] = hsfOVERRIDE;
}
public:
bool
expectRewards(
jtx::Env const& env,
@@ -60,6 +68,52 @@ struct ClaimReward_test : public beast::unit_test::suite
return true;
}
bool
expectRewardsIOU(
jtx::Env const& env,
jtx::Account const& acct,
jtx::IOU const& iou,
std::uint32_t ledgerFirst,
std::uint32_t ledgerLast,
STAmount accumulator,
std::uint32_t time)
{
auto const sle = env.le(keylet::line(acct, iou.account, iou.currency));
BEAST_EXPECT(!!sle);
auto const& sfRewardField =
std::minmax(acct.id(), iou.account.id()).first == acct.id()
? sfLowReward
: sfHighReward;
if (!sle->isFieldPresent(sfRewardField))
return false;
auto const& reward =
static_cast<STObject const&>(sle->peekAtField(sfRewardField));
if (!reward.isFieldPresent(sfRewardLgrFirst) ||
reward.getFieldU32(sfRewardLgrFirst) != ledgerFirst)
{
return false;
}
if (!reward.isFieldPresent(sfRewardLgrLast) ||
reward.getFieldU32(sfRewardLgrLast) != ledgerLast)
{
return false;
}
if (!reward.isFieldPresent(sfTrustLineRewardAccumulator) ||
reward.getFieldAmount(sfTrustLineRewardAccumulator) != accumulator)
{
return false;
}
if (!reward.isFieldPresent(sfRewardTime) ||
reward.getFieldU32(sfRewardTime) != time)
{
return false;
}
return true;
}
bool
expectNoRewards(jtx::Env const& env, jtx::Account const& acct)
{
@@ -83,6 +137,24 @@ struct ClaimReward_test : public beast::unit_test::suite
return true;
}
bool
expectNoRewardsIOU(
jtx::Env const& env,
jtx::Account const& acct,
jtx::IOU const& iou)
{
auto const sle = env.le(keylet::line(acct, iou.account, iou.currency));
BEAST_EXPECT(!!sle);
auto const& sfRewardField =
std::minmax(acct.id(), iou.account.id()).first == acct.id()
? sfLowReward
: sfHighReward;
if (sle->isFieldPresent(sfRewardField))
return false;
return true;
}
void
testEnabled(FeatureBitset features)
{
@@ -92,7 +164,7 @@ struct ClaimReward_test : public beast::unit_test::suite
// setup env
auto const alice = Account("alice");
auto const issuer = Account("issuer");
auto const issuer = Account::master;
for (bool const withClaimReward : {false, true})
{
@@ -102,7 +174,11 @@ struct ClaimReward_test : public beast::unit_test::suite
withClaimReward ? features : features - featureBalanceRewards;
Env env{*this, amend};
env.fund(XRP(1000), alice, issuer);
env.fund(XRP(1000), alice);
env.close();
env(hook(issuer, {{hso(jtx::genesis::AcceptHook)}}, 0),
fee(XRP(1)));
env.close();
auto const txResult =
@@ -119,7 +195,10 @@ struct ClaimReward_test : public beast::unit_test::suite
.count();
// CLAIM
env(reward::claim(alice), reward::issuer(issuer), txResult);
env(reward::claim(alice),
reward::issuer(issuer),
fee(XRP(1)),
txResult);
env.close();
if (withClaimReward)
@@ -195,14 +274,19 @@ struct ClaimReward_test : public beast::unit_test::suite
Env env{*this, amend};
auto const alice = Account("alice");
auto const issuer = Account("issuer");
auto const issuer = Account::master;
env.fund(XRP(1000), alice, issuer);
env.fund(XRP(1000), alice);
env.close();
env(hook(issuer, {{hso(jtx::genesis::AcceptHook)}}, 0),
fee(XRP(1)));
env.close();
auto tx = reward::claim(alice);
env(tx,
reward::issuer(issuer),
fee(XRP(1)),
txflags(tfFullyCanonicalSig),
withFixFlags ? ter(tesSUCCESS) : ter(temINVALID_FLAG));
env.close();
@@ -221,6 +305,105 @@ struct ClaimReward_test : public beast::unit_test::suite
env(reward::claim(alice), reward::issuer(alice), ter(temMALFORMED));
env.close();
}
// featureIOURewardClaim
// temDISABLED
// featureIOURewardClaim amendment is disabled
{
test::jtx::Env env{
*this,
network::makeNetworkConfig(21337),
features - featureIOURewardClaim};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
env.fund(XRP(1000), alice, bob, gw);
env.close();
jtx::IOU const USD = gw["USD"];
env(reward::claim(alice),
reward::issuer(bob),
reward::claimCurrency(USD),
ter(temDISABLED));
env.close();
}
// temMALFORMED
// ClaimCurrency.account cannot be the source account.
{
test::jtx::Env env{*this, network::makeNetworkConfig(21337)};
auto const alice = Account("alice");
auto const bob = Account("bob");
env.fund(XRP(1000), alice);
env.close();
jtx::IOU const USD = alice["USD"];
env(reward::claim(alice),
reward::issuer(bob),
reward::claimCurrency(USD),
ter(temMALFORMED));
env.close();
}
// temMALFORMED
// Issuer cannot be Genesis account if ClaimCurrency is set.
{
test::jtx::Env env{*this, network::makeNetworkConfig(21337)};
auto const alice = Account("alice");
auto const gw = Account("gw");
env.fund(XRP(1000), alice, gw);
env.close();
jtx::IOU const USD = gw["USD"];
env(reward::claim(alice),
reward::issuer(Account::master),
reward::claimCurrency(USD),
ter(temBAD_ISSUER));
}
// MPT
{
// tested in testMPTInvalidInTx() at MPToken_test.cpp
}
// XAH RewardClaim: Issuer must be the Genesis account if
// featureXahauGenesis and featureIOURewardClaim are enabled.
for (bool const withIOURewardClaim : {false, true})
{
auto const amend = withIOURewardClaim
? features
: features - featureIOURewardClaim;
auto const alice = Account("alice");
auto const badIssuer = Account("gw");
auto const issuer = Account::master;
auto const USD = badIssuer["USD"];
Env env{*this, amend};
env.fund(XRP(1000), alice, badIssuer);
env.close();
env(hook(issuer, {{hso(jtx::genesis::AcceptHook)}}, 0),
fee(XRP(1)));
env.close();
env(reward::claim(alice),
reward::issuer(badIssuer),
fee(XRP(1)),
withIOURewardClaim ? ter(temBAD_ISSUER) : ter(tesSUCCESS));
env(reward::claim(alice),
reward::issuer(issuer),
fee(XRP(1)),
ter(tesSUCCESS));
}
}
void
@@ -244,6 +427,7 @@ struct ClaimReward_test : public beast::unit_test::suite
auto const alice = Account("alice");
auto const issuer = Account("issuer");
env.memoize(alice);
auto USD = issuer["USD"];
env.fund(XRP(1000), issuer);
env.close();
@@ -251,7 +435,10 @@ struct ClaimReward_test : public beast::unit_test::suite
auto tx = reward::claim(alice);
tx[jss::Sequence] = 0;
tx[jss::Fee] = 10;
env(tx, reward::issuer(issuer), ter(terNO_ACCOUNT));
env(tx,
reward::issuer(issuer),
reward::claimCurrency(USD),
ter(terNO_ACCOUNT));
env.close();
}
@@ -261,9 +448,9 @@ struct ClaimReward_test : public beast::unit_test::suite
test::jtx::Env env{*this, network::makeNetworkConfig(21337)};
auto const alice = Account("alice");
auto const issuer = Account("issuer");
auto const issuer = Account::master;
env.fund(XRP(1000), alice, issuer);
env.fund(XRP(1000), alice);
env.close();
env(reward::claim(alice),
@@ -294,11 +481,16 @@ struct ClaimReward_test : public beast::unit_test::suite
auto const issuer = Account("issuer");
env.memoize(issuer);
auto USD = issuer["USD"];
env.fund(XRP(1000), alice);
env.close();
auto tx = reward::claim(alice);
env(tx, reward::issuer(issuer), ter(tecNO_ISSUER));
env(tx,
reward::issuer(issuer),
reward::claimCurrency(USD),
ter(tecNO_ISSUER));
env.close();
}
@@ -320,9 +512,162 @@ struct ClaimReward_test : public beast::unit_test::suite
env(reward::claim(alice),
reward::issuer(amm.ammAccount()),
reward::claimCurrency(USD),
ter(tecNO_PERMISSION));
env.close();
}
// tecNO_TARGET
// no claim reward hook
{
Env env{*this};
auto const alice = Account("alice");
auto const issuer = Account::master;
env.fund(XRP(1000), alice);
env.close();
// Doesn't have hook
{
env(reward::claim(alice),
reward::issuer(issuer),
ter(tecNO_TARGET));
env.close();
}
// Invalid HookOn
{
auto hookObj = hso(jtx::genesis::AcceptHook, overrideFlag);
hookObj[jss::HookOn] = to_string(UINT256_BIT[ttCLAIM_REWARD]);
env(hook(issuer, {{hookObj}}, 0), fee(XRP(1)));
env.close();
env(reward::claim(alice),
reward::issuer(issuer),
ter(tecNO_TARGET));
env.close();
}
// Invalid IncomingHookOn
{
auto hookObj = hso(jtx::genesis::AcceptHook, overrideFlag);
hookObj.removeMember(jss::HookOn);
hookObj[jss::HookOnIncoming] =
to_string(UINT256_BIT[ttCLAIM_REWARD]);
hookObj[jss::HookOnOutgoing] = to_string(uint256{});
env(hook(issuer, {{hookObj}}, 0), fee(XRP(1)));
env.close();
env(reward::claim(alice),
reward::issuer(issuer),
ter(tecNO_TARGET));
}
// Vaild HookOn
{
auto hookObj = hso(jtx::genesis::AcceptHook, overrideFlag);
hookObj[jss::HookOn] = to_string(~UINT256_BIT[ttCLAIM_REWARD]);
env(hook(issuer, {{hookObj}}, 0), fee(XRP(1)));
env.close();
env(reward::claim(alice),
reward::issuer(issuer),
fee(XRP(1)),
ter(tesSUCCESS));
}
// Vaild IncomingHookOn
{
auto hookObj = hso(jtx::genesis::AcceptHook, overrideFlag);
hookObj.removeMember(jss::HookOn);
hookObj[jss::HookOnIncoming] =
to_string(~UINT256_BIT[ttCLAIM_REWARD]);
hookObj[jss::HookOnOutgoing] = to_string(uint256{});
env(hook(issuer, {{hookObj}}, 0), fee(XRP(1)));
env.close();
env(reward::claim(alice),
reward::issuer(issuer),
fee(XRP(1)),
ter(tesSUCCESS));
}
// Invalid Hooks Array
{
auto hookObj = hso(jtx::genesis::AcceptHook, overrideFlag);
hookObj[jss::HookOn] = to_string(UINT256_BIT[ttCLAIM_REWARD]);
env(hook(
issuer,
{{
hookObj,
hookObj,
hookObj,
hookObj,
hookObj,
hookObj,
hookObj,
hookObj,
hookObj,
hookObj,
}},
0),
fee(XRP(1)));
env.close();
env(reward::claim(alice),
reward::issuer(issuer),
fee(XRP(1)),
ter(tecNO_TARGET));
}
// Vaild Hooks Array
{
auto hookObj = hso(jtx::genesis::AcceptHook, overrideFlag);
hookObj[jss::HookOn] = to_string(UINT256_BIT[ttCLAIM_REWARD]);
auto hookObj2 = hso(jtx::genesis::AcceptHook, overrideFlag);
hookObj2[jss::HookOn] = to_string(~UINT256_BIT[ttCLAIM_REWARD]);
env(hook(
issuer,
{{
hookObj,
hookObj,
hookObj,
hookObj,
hookObj,
hookObj,
hookObj,
hookObj,
hookObj,
hookObj2,
}},
0),
fee(XRP(1)));
env.close();
env(reward::claim(alice),
reward::issuer(issuer),
fee(XRP(1)),
ter(tesSUCCESS));
}
}
// tecNO_LINE
// trustline does not exist.
{
test::jtx::Env env{*this, network::makeNetworkConfig(21337)};
auto const alice = Account("alice");
auto const gw = Account("gw");
env.fund(XRP(1000), alice, gw);
env.close();
env(hook(gw, {{hso(jtx::genesis::AcceptHook)}}, 0), fee(XRP(1)));
env.close();
jtx::IOU const USD = gw["USD"];
env(reward::claim(alice),
reward::issuer(gw),
reward::claimCurrency(USD),
fee(XRP(1)),
ter(tecNO_LINE));
}
}
void
@@ -335,9 +680,17 @@ struct ClaimReward_test : public beast::unit_test::suite
test::jtx::Env env{*this, network::makeNetworkConfig(21337)};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account("gw");
auto const issuer = Account("issuer");
env.fund(XRP(1000), alice, issuer);
env.fund(XRP(1000), alice, bob, gw, issuer);
env.close();
env(hook(issuer, {{hso(jtx::genesis::AcceptHook)}}, 0), fee(XRP(1)));
env.close();
env(hook(Account::master, {{hso(jtx::genesis::AcceptHook)}}, 0),
fee(XRP(1)));
env.close();
// test claim rewards - no opt out
@@ -352,7 +705,7 @@ struct ClaimReward_test : public beast::unit_test::suite
.count();
auto tx = reward::claim(alice);
env(tx, reward::issuer(issuer), ter(tesSUCCESS));
env(tx, reward::issuer(Account::master), fee(XRP(1)), ter(tesSUCCESS));
env.close();
BEAST_EXPECT(
@@ -365,6 +718,51 @@ struct ClaimReward_test : public beast::unit_test::suite
env.close();
BEAST_EXPECT(expectNoRewards(env, alice) == true);
// test iou claim rewards
{
// set trustline
env(trust(bob, gw["USD"](10000)));
env.close();
// opt in
auto const currentLedger = env.current()->seq();
auto const currentTime =
std::chrono::duration_cast<std::chrono::seconds>(
env.app()
.getLedgerMaster()
.getValidatedLedger()
->info()
.parentCloseTime.time_since_epoch())
.count();
auto tx = reward::claim(bob);
env(tx,
reward::issuer(issuer),
reward::claimCurrency(gw["USD"]),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
BEAST_EXPECT(
expectRewardsIOU(
env,
bob,
gw["USD"],
currentLedger,
currentLedger,
gw["USD"](0),
currentTime) == true);
// opt out
env(reward::claim(bob),
reward::claimCurrency(gw["USD"]),
txflags(tfOptOut),
ter(tesSUCCESS));
env.close();
BEAST_EXPECT(expectNoRewardsIOU(env, bob, gw["USD"]) == true);
}
}
void
@@ -375,16 +773,20 @@ struct ClaimReward_test : public beast::unit_test::suite
using namespace std::literals::chrono_literals;
Env env{*this, features};
auto const alice = Account("alice");
auto const issuer = Account("issuer");
env.fund(XRP(10000), alice, issuer);
auto const issuer = Account::master;
env.fund(XRP(10000), alice);
std::uint32_t aliceTicketSeq{env.seq(alice) + 1};
env(ticket::create(alice, 10));
std::uint32_t const aliceSeq{env.seq(alice)};
env.require(owners(alice, 10));
env(hook(issuer, {{hso(jtx::genesis::AcceptHook)}}, 0), fee(XRP(1)));
env.close();
env(reward::claim(alice),
reward::issuer(issuer),
ticket::use(aliceTicketSeq++),
fee(XRP(1)),
ter(tesSUCCESS));
env.require(tickets(alice, env.seq(alice) - aliceTicketSeq));
@@ -392,6 +794,317 @@ struct ClaimReward_test : public beast::unit_test::suite
env.require(owners(alice, 9));
}
void
testBalanceChanges(FeatureBitset features)
{
testcase("balance changes");
using namespace jtx;
using namespace std::literals::chrono_literals;
auto const getCurrentTime = [&](Env& env) {
return std::chrono::duration_cast<std::chrono::seconds>(
env.app()
.getLedgerMaster()
.getValidatedLedger()
->info()
.parentCloseTime.time_since_epoch())
.count();
};
// Native Reward Claim
{
Env env{*this, features};
auto const alice = Account("alice");
auto const gw = Account("gw");
auto const issuer = Account::master;
env.fund(XRP(10001), alice, gw);
env.close();
env(hook(issuer, {{hso(jtx::genesis::AcceptHook)}}, 0),
fee(XRP(1)));
env.close();
auto const currentTime = getCurrentTime(env);
auto const currentLedger = env.current()->seq();
env(reward::claim(alice), reward::issuer(issuer), fee(XRP(1)));
env.close();
env(fset(alice, 0));
env.close();
BEAST_EXPECT(
expectRewards(
env,
alice,
currentLedger,
currentLedger + 1,
10000, // 10000 XAH * time 1
currentTime) == true);
}
// IOU Reward Claim
for (bool const fromHighAccount : {true, false})
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const issuer = Account("issuer");
auto const user = fromHighAccount ? alice : bob;
auto const gw = fromHighAccount ? bob : alice;
if (fromHighAccount)
BEAST_EXPECT(user.id() < gw.id());
else
BEAST_EXPECT(user.id() > gw.id());
env.fund(XRP(10000), user, gw, issuer);
env(fset(gw, asfDefaultRipple));
auto hookObj = hso(jtx::genesis::AcceptHook);
hookObj[jss::HookOn] = to_string(~UINT256_BIT[ttCLAIM_REWARD]);
env(hook(gw, {{hookObj}}, 0), fee(XRP(1)));
env.close();
env(trust(user, gw["USD"](1000000)), fee(XRP(1)));
env.close();
env(pay(gw, user, gw["USD"](10000)));
env.close();
auto currentTime = getCurrentTime(env);
auto currentLedger = env.current()->seq();
env(reward::claim(user),
reward::issuer(gw),
reward::claimCurrency(gw["USD"]),
fee(XRP(1)));
env.close();
env(pay(user, gw, gw["USD"](10000)));
env.close();
BEAST_EXPECT(
expectRewardsIOU(
env,
user,
gw["USD"],
currentLedger,
currentLedger + 1,
user["USD"](10000), // 10000 USD * time 1
currentTime) == true);
env(pay(gw, user, gw["USD"](1)));
env.close();
// check Balance == 0
BEAST_EXPECT(
expectRewardsIOU(
env,
user,
gw["USD"],
currentLedger,
currentLedger + 2,
user["USD"](10000), // 10000 USD * time 1 + 0 USD * time 1
currentTime) == true);
}
// Check Balance minus -> plus, plus -> minus
for (bool const fromHighAccount : {true, false})
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const issuer = Account("issuer");
auto const user = fromHighAccount ? alice : bob;
auto const gw = fromHighAccount ? bob : alice;
if (fromHighAccount)
BEAST_EXPECT(user.id() < gw.id());
else
BEAST_EXPECT(user.id() > gw.id());
env.fund(XRP(10000), user, gw, issuer);
env(fset(gw, asfDefaultRipple));
env.close();
auto hookObj = hso(jtx::genesis::AcceptHook);
hookObj[jss::HookOn] = to_string(~UINT256_BIT[ttCLAIM_REWARD]);
env(hook(gw, {{hookObj}}, 0), fee(XRP(1)));
env.close();
env(trust(user, gw["USD"](1000000)));
env.close();
env(trust(gw, user["USD"](1000000)));
env(pay(gw, user, gw["USD"](10000)));
env.close();
auto currentTime = getCurrentTime(env);
auto currentLedger = env.current()->seq();
env(reward::claim(user),
reward::issuer(gw),
reward::claimCurrency(gw["USD"]),
fee(XRP(1)));
env.close();
env(pay(user, gw, gw["USD"](20000)));
env.close();
env(pay(user, gw, gw["USD"](1)));
env.close();
BEAST_EXPECT(
expectRewardsIOU(
env,
user,
gw["USD"],
currentLedger,
currentLedger + 2,
user["USD"](10000), // 10000 USD * time 1 + 0 USD * time 1
currentTime) == true);
}
// test with escrow (locked balance)
for (bool const fromHighAccount : {true, false})
{
for (bool const hasEscrow : {true, false})
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const issuer = Account("issuer");
auto const user = fromHighAccount ? alice : bob;
auto const gw = fromHighAccount ? bob : alice;
if (fromHighAccount)
BEAST_EXPECT(user.id() < gw.id());
else
BEAST_EXPECT(user.id() > gw.id());
env.fund(XRP(10000), user, gw, issuer);
env(fset(gw, asfDefaultRipple));
auto hookObj = hso(jtx::genesis::AcceptHook);
hookObj[jss::HookOn] = to_string(~UINT256_BIT[ttCLAIM_REWARD]);
env(hook(gw, {{hookObj}}, 0), fee(XRP(1)));
env.close();
env(trust(user, gw["USD"](1000000)), fee(XRP(1)));
env.close();
env(pay(gw, user, gw["USD"](10000)));
env.close();
if (hasEscrow)
{
env(escrow(user, user, gw["USD"](2000)),
finish_time(env.now() + 1s),
fee(XRP(1)));
env.close();
}
auto currentTime = getCurrentTime(env);
auto currentLedger = env.current()->seq();
env(reward::claim(user),
reward::issuer(gw),
reward::claimCurrency(gw["USD"]),
fee(XRP(1)));
env.close();
env(pay(user, gw, gw["USD"](5000)));
env.close();
BEAST_EXPECT(
expectRewardsIOU(
env,
user,
gw["USD"],
currentLedger,
currentLedger + 1,
user["USD"](10000), // 10000 USD * time 1
currentTime) == true);
env(pay(gw, user, gw["USD"](1)));
env.close();
// check Balance == 0
BEAST_EXPECT(
expectRewardsIOU(
env,
user,
gw["USD"],
currentLedger,
currentLedger + 2,
user["USD"](
15000), // 10000 USD * time 1 + 5000 USD * time 1
currentTime) == true);
}
}
// STAmount overflow in reward accumulation should not cause
// transaction failure (tefEXCEPTION). The overflow should be
// gracefully skipped via try-catch.
for (bool const fromHighAccount : {true, false})
{
Env env{*this, features};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const issuer = Account("issuer");
auto const user = fromHighAccount ? alice : bob;
auto const gw = fromHighAccount ? bob : alice;
env.fund(XRP(10000), user, gw, issuer);
env(fset(gw, asfDefaultRipple));
env.close();
auto hookObj = hso(jtx::genesis::AcceptHook);
hookObj[jss::HookOn] = to_string(~UINT256_BIT[ttCLAIM_REWARD]);
env(hook(gw, {{hookObj}}, 0), fee(XRP(1)));
env.close();
// Use a near-max IOU balance at exponent 80. When
// multiply(balance, STAmount(lgrElapsed), issue) is called
// with lgrElapsed >= 2, the result exponent exceeds
// cMaxOffset(80), causing IOUAmount::normalize to throw
// std::overflow_error("value overflow").
auto const bigUSD = STAmount{
gw["USD"].issue(), std::uint64_t(5000000000000000ull), 80};
// Payment amount must be large enough to register a
// balance change given STAmount's 16-digit precision.
auto const payBackUSD = STAmount{
gw["USD"].issue(), std::uint64_t(1000000000000000ull), 80};
env(trust(user, bigUSD));
env.close();
env(pay(gw, user, bigUSD));
env.close();
// Claim IOU reward to initialize reward tracking
env(reward::claim(user),
reward::issuer(gw),
reward::claimCurrency(gw["USD"]),
fee(XRP(1)));
env.close();
// Advance ledger so lgrElapsed >= 2. With lgrElapsed=1
// the multiply result is exactly at cMaxOffset boundary
// (no overflow). With lgrElapsed >= 2, the result exponent
// exceeds cMaxOffset and triggers the overflow.
env.close();
// This payment modifies the trustline balance, triggering
// reward accumulation in Transactor. Without the try-catch
// fix, multiply() throws std::overflow_error("value overflow")
// and the transaction fails with tefEXCEPTION.
env(pay(user, gw, payBackUSD), ter(tesSUCCESS));
env.close();
}
}
void
testWithFeats(FeatureBitset features)
{
@@ -400,6 +1113,7 @@ struct ClaimReward_test : public beast::unit_test::suite
testInvalidPreclaim(features);
testValidNoHook(features);
testUsingTickets(features);
testBalanceChanges(features);
}
public:

View File

@@ -1878,6 +1878,13 @@ class MPToken_test : public beast::unit_test::suite
[jss::Amount] = mpt.getJson(JsonOptions::none);
test(jv, sfAmounts.jsonName.c_str());
}
// ClaimReward
{
Json::Value jv = reward::claim(alice);
jv[sfClaimCurrency.jsonName][jss::mpt_issuance_id] =
to_string(issue);
test(jv, sfClaimCurrency.jsonName.c_str());
}
}
for (const auto& str : txWithAmounts)
printf("%s\n", str.c_str());

File diff suppressed because it is too large Load Diff

View File

@@ -1647,8 +1647,12 @@ private:
features};
auto const account = Account("alice");
auto const issuer = Account("issuer");
env.fund(XRP(1000), account, issuer);
auto const issuer = Account::master;
env.fund(XRP(1000), account);
env.close();
env(hook(issuer, {{hso(jtx::genesis::AcceptHook)}}, 0),
fee(XRP(1)));
env.close();
// set tsh collect
@@ -1680,8 +1684,8 @@ private:
features};
auto const account = Account("alice");
auto const issuer = Account("issuer");
env.fund(XRP(1000), account, issuer);
auto const issuer = Account::master;
env.fund(XRP(1000), account);
env.close();
// set tsh collect

View File

@@ -207,19 +207,25 @@ private:
test::jtx::Env env{*this, envconfig(), features};
auto const alice = Account("alice");
auto const issuer = Account("issuer");
env.fund(XRP(1000), alice, issuer);
auto const issuer = Account::master;
env.fund(XRP(1000), alice);
env.close();
env(hook(issuer, {{hso(jtx::genesis::AcceptHook)}}, 0), fee(XRP(1)));
env.close();
// claim reward
env(reward::claim(alice), reward::issuer(issuer), ter(tesSUCCESS));
env(reward::claim(alice),
reward::issuer(issuer),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
// verify touch
validateTouch(env, alice, {"ClaimReward", "tesSUCCESS"});
auto const tt = env.current()->rules().enabled(featureTouch)
? "ClaimReward"
: "AccountSet";
: "SetHook";
validateTouch(env, issuer, {tt, "tesSUCCESS"});
}

View File

@@ -66,15 +66,45 @@ signers(Account const& account, none_t)
//------------------------------------------------------------------------------
msig::msig(std::vector<msig::Reg> signers_) : signers(std::move(signers_))
// Helper function to recursively sort nested signers
void
sortSignersRecursive(std::vector<msig::SignerPtr>& signers)
{
// Signatures must be applied in sorted order.
// Sort current level by account ID
std::sort(
signers.begin(),
signers.end(),
[](msig::Reg const& lhs, msig::Reg const& rhs) {
return lhs.acct.id() < rhs.acct.id();
[](msig::SignerPtr const& lhs, msig::SignerPtr const& rhs) {
return lhs->id() < rhs->id();
});
// Recursively sort nested signers for each signer at this level
for (auto& signer : signers)
{
if (signer->isNested() && !signer->nested.empty())
{
sortSignersRecursive(signer->nested);
}
}
}
msig::msig(std::vector<msig::SignerPtr> signers_) : signers(std::move(signers_))
{
// Recursively sort all signers at all nesting levels
// This ensures account IDs are in strictly ascending order at each level
sortSignersRecursive(signers);
}
msig::msig(std::vector<msig::Reg> signers_)
{
// Convert Reg vector to SignerPtr vector for backward compatibility
signers.reserve(signers_.size());
for (auto const& s : signers_)
signers.push_back(s.toSigner());
// Recursively sort all signers at all nesting levels
// This ensures account IDs are in strictly ascending order at each level
sortSignersRecursive(signers);
}
void
@@ -93,19 +123,47 @@ msig::operator()(Env& env, JTx& jt) const
env.test.log << pretty(jtx.jv) << std::endl;
Rethrow();
}
// Recursive function to build signer JSON
std::function<Json::Value(SignerPtr const&)> buildSignerJson;
buildSignerJson = [&](SignerPtr const& signer) -> Json::Value {
Json::Value jo;
jo[jss::Account] = signer->acct.human();
if (signer->isNested())
{
// For nested signers, we use the already-sorted nested vector
// (sorted during construction via sortSignersRecursive)
// This ensures account IDs are in strictly ascending order
auto& subJs = jo[sfSigners.getJsonName()];
for (std::size_t i = 0; i < signer->nested.size(); ++i)
{
auto& subJo = subJs[i][sfSigner.getJsonName()];
subJo = buildSignerJson(signer->nested[i]);
}
}
else
{
// This is a leaf signer - add signature
jo[jss::SigningPubKey] = strHex(signer->sig.pk().slice());
Serializer ss{buildMultiSigningData(*st, signer->acct.id())};
auto const sig = ripple::sign(
*publicKeyType(signer->sig.pk().slice()),
signer->sig.sk(),
ss.slice());
jo[sfTxnSignature.getJsonName()] =
strHex(Slice{sig.data(), sig.size()});
}
return jo;
};
auto& js = jtx[sfSigners.getJsonName()];
for (std::size_t i = 0; i < mySigners.size(); ++i)
{
auto const& e = mySigners[i];
auto& jo = js[i][sfSigner.getJsonName()];
jo[jss::Account] = e.acct.human();
jo[jss::SigningPubKey] = strHex(e.sig.pk().slice());
Serializer ss{buildMultiSigningData(*st, e.acct.id())};
auto const sig = ripple::sign(
*publicKeyType(e.sig.pk().slice()), e.sig.sk(), ss.slice());
jo[sfTxnSignature.getJsonName()] =
strHex(Slice{sig.data(), sig.size()});
jo = buildSignerJson(mySigners[i]);
}
};
}

View File

@@ -49,6 +49,13 @@ issuer::operator()(Env& env, JTx& jt) const
jt.jv[sfIssuer.jsonName] = to_string(issuer_);
}
void
claimCurrency::operator()(Env& env, JTx& jt) const
{
jt.jv[sfClaimCurrency.jsonName] =
STIssue{sfClaimCurrency, claimCurrency_}.getJson(JsonOptions::none);
}
} // namespace reward
} // namespace jtx

View File

@@ -26,6 +26,7 @@
#include <test/jtx/tags.h>
#include <concepts>
#include <cstdint>
#include <memory>
#include <optional>
namespace ripple {
@@ -66,6 +67,48 @@ signers(Account const& account, none_t);
class msig
{
public:
// Recursive signer structure
struct Signer
{
Account acct;
Account sig; // For leaf signers (same as acct for master key)
std::vector<std::shared_ptr<Signer>> nested; // For nested signers
// Leaf signer constructor (regular signing)
Signer(Account const& masterSig) : acct(masterSig), sig(masterSig)
{
}
// Leaf signer constructor (with different signing key)
Signer(Account const& acct_, Account const& regularSig)
: acct(acct_), sig(regularSig)
{
}
// Nested signer constructor
Signer(
Account const& acct_,
std::vector<std::shared_ptr<Signer>> nested_)
: acct(acct_), sig(acct_), nested(std::move(nested_))
{
}
bool
isNested() const
{
return !nested.empty();
}
AccountID
id() const
{
return acct.id();
}
};
using SignerPtr = std::shared_ptr<Signer>;
// For backward compatibility
struct Reg
{
Account acct;
@@ -74,16 +117,13 @@ public:
Reg(Account const& masterSig) : acct(masterSig), sig(masterSig)
{
}
Reg(Account const& acct_, Account const& regularSig)
: acct(acct_), sig(regularSig)
{
}
Reg(char const* masterSig) : acct(masterSig), sig(masterSig)
{
}
Reg(char const* acct_, char const* regularSig)
: acct(acct_), sig(regularSig)
{
@@ -94,13 +134,32 @@ public:
{
return acct < rhs.acct;
}
// Convert to Signer
SignerPtr
toSigner() const
{
return std::make_shared<Signer>(acct, sig);
}
};
std::vector<Reg> signers;
std::vector<SignerPtr> signers;
public:
// Initializer list constructor - resolves brace-init ambiguity
msig(std::initializer_list<SignerPtr> signers_)
: msig(std::vector<SignerPtr>(signers_))
{
// handled by :
}
// Direct constructor with SignerPtr vector
explicit msig(std::vector<SignerPtr> signers_);
// Backward compatibility constructor
msig(std::vector<Reg> signers_);
// Variadic constructor for backward compatibility
template <class AccountType, class... Accounts>
requires std::convertible_to<AccountType, Reg>
explicit msig(AccountType&& a0, Accounts&&... aN)
@@ -114,6 +173,30 @@ public:
operator()(Env&, JTx& jt) const;
};
// Helper functions to create signers - renamed to avoid conflict with sig()
// transaction modifier
inline msig::SignerPtr
msigner(Account const& acct)
{
return std::make_shared<msig::Signer>(acct);
}
inline msig::SignerPtr
msigner(Account const& acct, Account const& signingKey)
{
return std::make_shared<msig::Signer>(acct, signingKey);
}
// Create nested signer with initializer list
template <typename... Args>
inline msig::SignerPtr
msigner(Account const& acct, Args&&... args)
{
std::vector<msig::SignerPtr> nested;
(nested.push_back(std::forward<Args>(args)), ...);
return std::make_shared<msig::Signer>(acct, std::move(nested));
}
//------------------------------------------------------------------------------
/** The number of signer lists matches. */

View File

@@ -56,6 +56,21 @@ public:
operator()(Env&, JTx& jtx) const;
};
/** Sets the optional ClaimCurrency on a JTx. */
class claimCurrency
{
private:
Issue claimCurrency_;
public:
explicit claimCurrency(Issue const& issue) : claimCurrency_(issue)
{
}
void
operator()(Env&, JTx& jtx) const;
};
} // namespace reward
} // namespace jtx

View File

@@ -1798,30 +1798,32 @@ public:
// This lambda contains the bulk of the test code.
auto testMalformedSigningAccount =
[this, &txn](STObject const& signer, bool expectPass) {
// Create SigningAccounts array.
STArray signers(sfSigners, 1);
signers.push_back(signer);
[this, &txn](
STObject const& signer, bool expectPass) -> bool /* passed */ {
// Create SigningAccounts array.
STArray signers(sfSigners, 1);
signers.push_back(signer);
// Insert signers into transaction.
STTx tempTxn(txn);
tempTxn.setFieldArray(sfSigners, signers);
// Insert signers into transaction.
STTx tempTxn(txn);
tempTxn.setFieldArray(sfSigners, signers);
Serializer rawTxn;
tempTxn.add(rawTxn);
SerialIter sit(rawTxn.slice());
bool serialized = false;
try
{
STTx copy(sit);
serialized = true;
}
catch (std::exception const&)
{
; // If it threw then serialization failed.
}
BEAST_EXPECT(serialized == expectPass);
};
Serializer rawTxn;
tempTxn.add(rawTxn);
SerialIter sit(rawTxn.slice());
bool serialized = false;
try
{
STTx copy(sit);
serialized = true;
}
catch (std::exception const&)
{
; // If it threw then serialization failed.
}
BEAST_EXPECT(serialized == expectPass);
return serialized == expectPass;
};
{
// Test case 1. Make a valid Signer object.
@@ -1831,12 +1833,21 @@ public:
soTest1.setFieldVL(sfTxnSignature, saMultiSignature);
testMalformedSigningAccount(soTest1, true);
}
{
// Test case 2. Omit sfSigningPubKey from SigningAccount.
// Test case 2. Omit sfSigningPubKey from SigningAccount.
// With nested multi-sign, sfSigningPubKey is optional in the
// template, so serialization succeeds. The signer-shape helpers
// still reject {Account + TxnSignature} as neither leaf nor nested.
STObject soTest2(sfSigner);
soTest2.setAccountID(sfAccount, id2);
soTest2.setFieldVL(sfTxnSignature, saMultiSignature);
testMalformedSigningAccount(soTest2, false);
testMalformedSigningAccount(soTest2, true);
soTest2.applyTemplateFromSField(sfSigner);
BEAST_EXPECT(!isLeafSigner(soTest2));
BEAST_EXPECT(!isNestedSigner(soTest2));
BEAST_EXPECT(!isValidSignerEntry(soTest2));
}
{
// Test case 3. Extra sfAmount in SigningAccount.

View File

@@ -332,6 +332,7 @@ multi_runner_child::run_multi(Pred pred)
{
if (!pred(*t))
continue;
try
{
failed = run(*t) || failed;

View File

@@ -17,6 +17,7 @@
*/
//==============================================================================
#include <xrpld/app/hook/applyHook.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/tx/detail/ClaimReward.h>
#include <xrpld/core/Config.h>
@@ -60,6 +61,57 @@ ClaimReward::preflight(PreflightContext const& ctx)
return temMALFORMED;
}
if (ctx.tx.isFieldPresent(sfClaimCurrency))
{
// IOU RewardClaim
if (!ctx.rules.enabled(featureIOURewardClaim))
return temDISABLED;
auto const claimAsset = ctx.tx[sfClaimCurrency];
bool const isMPT = claimAsset.holds<MPTIssue>();
if (isMPT)
{
JLOG(ctx.j.debug()) << "ClaimReward: MPT is not supported yet.";
return temMALFORMED;
}
auto const claimIssue = claimAsset.get<Issue>();
if (claimIssue.account == beast::zero || isXRP(claimIssue.currency))
return temMALFORMED;
if (claimIssue.account == ctx.tx.getAccountID(sfAccount))
return temMALFORMED;
}
if (ctx.rules.enabled(featureXahauGenesis) &&
ctx.rules.enabled(featureIOURewardClaim) &&
ctx.tx.isFieldPresent(sfIssuer))
{
static auto const genesisAccountId = calcAccountID(
generateKeyPair(
KeyType::secp256k1, generateSeed("masterpassphrase"))
.first);
auto const issuer = ctx.tx.getAccountID(sfIssuer);
if (ctx.tx.isFieldPresent(sfClaimCurrency))
{
if (issuer == genesisAccountId)
{
JLOG(ctx.j.debug()) << "ClaimReward (IOU): Issuer cannot "
"be the Genesis account";
return temBAD_ISSUER;
}
}
else
{
if (issuer != genesisAccountId)
{
JLOG(ctx.j.debug()) << "ClaimReward (XAH): Issuer must be "
"the Genesis account";
return temBAD_ISSUER;
}
}
}
return preflight2(ctx);
}
@@ -90,8 +142,53 @@ ClaimReward::preclaim(PreclaimContext const& ctx)
if (sleIssuer->isFieldPresent(sfAMMID))
return tecNO_PERMISSION;
if (ctx.view.rules().enabled(featureIOURewardClaim))
{
auto const& sleHook = ctx.view.read(keylet::hook(*issuer));
if (!sleHook || !sleHook->isFieldPresent(sfHooks) ||
sleHook->getFieldArray(sfHooks).empty())
return tecNO_TARGET;
bool hasClaimRewardHook = false;
auto const& hooks = sleHook->getFieldArray(sfHooks);
for (auto const& hook : hooks)
{
if (!hook.isFieldPresent(sfHookHash))
return tefINTERNAL; // LCOV_EXCL_LINE
auto const& hash = hook.getFieldH256(sfHookHash);
auto const& sleDef =
ctx.view.read(keylet::hookDefinition(hash));
if (!sleDef)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const& hookOn =
hook::getHookOn(hook, sleDef, sfHookOnIncoming);
if (hook::canHook(ttCLAIM_REWARD, hookOn))
{
hasClaimRewardHook = true;
break;
}
}
if (!hasClaimRewardHook)
return tecNO_TARGET;
}
}
if (ctx.tx.isFieldPresent(sfClaimCurrency))
{
auto const claimCurrency = ctx.tx[sfClaimCurrency];
bool const isMPT = claimCurrency.holds<MPTIssue>();
if (isMPT)
return tefINTERNAL;
auto const claimIssue = claimCurrency.get<Issue>();
if (!ctx.view.exists(
keylet::line(id, claimIssue.account, claimIssue.currency)))
return tecNO_LINE;
}
return tesSUCCESS;
}
@@ -105,6 +202,61 @@ ClaimReward::doApply()
std::optional<uint32_t> flags = ctx_.tx[~sfFlags];
bool isOptOut = flags && *flags == tfOptOut;
uint32_t lgrCur = view().seq();
uint32_t lgrFirst = lgrCur;
uint32_t lgrLast = lgrCur;
uint64_t accumulator = 0ULL;
uint32_t rewardTime = std::chrono::duration_cast<std::chrono::seconds>(
ctx_.app.getLedgerMaster()
.getValidatedLedger()
->info()
.parentCloseTime.time_since_epoch())
.count();
if (ctx_.tx.isFieldPresent(sfClaimCurrency))
{
auto const claimCurrency = ctx_.tx[sfClaimCurrency];
bool const isMPT = claimCurrency.holds<MPTIssue>();
if (isMPT)
return tefINTERNAL;
auto const claimIssue = claimCurrency.get<Issue>();
auto lineSle = view().peek(
keylet::line(account_, claimIssue.account, claimIssue.currency));
if (!lineSle)
return tefINTERNAL;
bool const isHigh = account_ > claimIssue.account;
auto const& rewardField = isHigh ? sfHighReward : sfLowReward;
if (isOptOut)
{
if (lineSle->isFieldPresent(rewardField))
lineSle->makeFieldAbsent(rewardField);
}
else
{
auto const& rewardAccumulatorField =
lineSle->getFieldAmount(isHigh ? sfHighLimit : sfLowLimit)
.zeroed();
// all actual rewards are handled by the hook on the sfIssuer
// the tt just resets the counters
auto& reward = lineSle->peekFieldObject(rewardField);
reward.setFieldU32(sfRewardLgrFirst, lgrFirst);
reward.setFieldU32(sfRewardLgrLast, lgrLast);
reward.setFieldAmount(
sfTrustLineRewardAccumulator, rewardAccumulatorField);
reward.setFieldU32(sfRewardTime, rewardTime);
}
view().update(lineSle);
return tesSUCCESS;
}
if (isOptOut)
{
if (sle->isFieldPresent(sfRewardLgrFirst))
@@ -120,18 +272,10 @@ ClaimReward::doApply()
{
// all actual rewards are handled by the hook on the sfIssuer
// the tt just resets the counters
uint32_t lgrCur = view().seq();
sle->setFieldU32(sfRewardLgrFirst, lgrCur);
sle->setFieldU32(sfRewardLgrLast, lgrCur);
sle->setFieldU64(sfRewardAccumulator, 0ULL);
sle->setFieldU32(
sfRewardTime,
std::chrono::duration_cast<std::chrono::seconds>(
ctx_.app.getLedgerMaster()
.getValidatedLedger()
->info()
.parentCloseTime.time_since_epoch())
.count());
sle->setFieldU32(sfRewardLgrFirst, lgrFirst);
sle->setFieldU32(sfRewardLgrLast, lgrLast);
sle->setFieldU64(sfRewardAccumulator, accumulator);
sle->setFieldU32(sfRewardTime, rewardTime);
}
view().update(sle);

View File

@@ -38,8 +38,12 @@
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/STAccount.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/UintTypes.h>
#include <functional>
#include <limits>
#include <map>
#include <optional>
#include <set>
namespace ripple {
@@ -317,8 +321,36 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
// Each signer adds one more baseFee to the minimum required fee
// for the transaction.
std::size_t const signerCount =
tx.isFieldPresent(sfSigners) ? tx.getFieldArray(sfSigners).size() : 0;
std::size_t signerCount = 0;
if (tx.isFieldPresent(sfSigners))
{
// Depth guard to prevent stack overflow from malicious deep nesting.
int const maxDepth = view.rules().enabled(featureNestedMultiSign)
? nestedMultiSignMaxDepth
: legacyMultiSignMaxDepth;
std::function<std::size_t(STArray const&, int)> countSigners;
countSigners = [&](STArray const& signers, int depth) -> std::size_t {
if (depth > maxDepth)
return 0;
std::size_t count = 0;
for (auto const& signer : signers)
{
if (isNestedSigner(signer))
count += countSigners(
signer.getFieldArray(sfSigners), depth + 1);
else if (isLeafSigner(signer))
count += 1;
}
return count;
};
signerCount = countSigners(tx.getFieldArray(sfSigners), 1);
}
XRPAmount hookExecutionFee{0};
uint64_t burden{1};
@@ -972,167 +1004,273 @@ NotTEC
Transactor::checkMultiSign(PreclaimContext const& ctx)
{
auto const id = ctx.tx.getAccountID(sfAccount);
// Get mTxnAccountID's SignerList and Quorum.
std::shared_ptr<STLedgerEntry const> sleAccountSigners =
ctx.view.read(keylet::signers(id));
// If the signer list doesn't exist the account is not multi-signing.
if (!sleAccountSigners)
{
JLOG(ctx.j.trace())
<< "applyTransaction: Invalid: Not a multi-signing account.";
return tefNOT_MULTI_SIGNING;
}
// We have plans to support multiple SignerLists in the future. The
// presence and defaulted value of the SignerListID field will enable that.
XRPL_ASSERT(
sleAccountSigners->isFieldPresent(sfSignerListID),
"ripple::Transactor::checkMultiSign : has signer list ID");
XRPL_ASSERT(
sleAccountSigners->getFieldU32(sfSignerListID) == 0,
"ripple::Transactor::checkMultiSign : signer list ID is 0");
bool const allowNested = ctx.view.rules().enabled(featureNestedMultiSign);
int const maxDepth =
allowNested ? nestedMultiSignMaxDepth : legacyMultiSignMaxDepth;
auto accountSigners =
SignerEntries::deserialize(*sleAccountSigners, ctx.j, "ledger");
if (!accountSigners)
return accountSigners.error();
// Nested multi-sign is dynamic account delegation: a parent SignerList
// authorizes signer accounts, not a frozen set of leaf keys. A nested
// signer contributes when that signer account's current on-ledger
// SignerList is satisfied by the transaction evidence.
//
// ancestors tracks the current proof path. An authorized signer entry that
// points back to an ancestor is unavailable on that path and may cause the
// local quorum to be cycle-adjusted below.
std::function<NotTEC(
AccountID const&, STArray const&, int, std::set<AccountID>)>
validateSigners;
// Get the array of transaction signers.
STArray const& txSigners(ctx.tx.getFieldArray(sfSigners));
validateSigners = [&](AccountID const& acc,
STArray const& signers,
int depth,
std::set<AccountID> ancestors) -> NotTEC {
// Cycle detection is handled per authorized edge in the loop below,
// rather than by failing the delegated account outright.
// Walk the accountSigners performing a variety of checks and see if
// the quorum is met.
// Both the multiSigners and accountSigners are sorted by account. So
// matching multi-signers to account signers should be a simple
// linear walk. *All* signers must be valid or the transaction fails.
std::uint32_t weightSum = 0;
auto iter = accountSigners->begin();
for (auto const& txSigner : txSigners)
{
AccountID const txSignerAcctID = txSigner.getAccountID(sfAccount);
// Attempt to match the SignerEntry with a Signer;
while (iter->account < txSignerAcctID)
if (depth > maxDepth)
{
if (++iter == accountSigners->end())
JLOG(ctx.j.trace())
<< "checkMultiSign: Multi-signing depth limit exceeded at "
<< depth << " (max=" << maxDepth << ")";
return temMALFORMED;
}
ancestors.insert(acc);
// Get the SignerList for the account we're validating signers for.
std::shared_ptr<STLedgerEntry const> sleAllowedSigners =
ctx.view.read(keylet::signers(acc));
if (!sleAllowedSigners)
{
JLOG(ctx.j.trace()) << "checkMultiSign: Account " << acc
<< " not set up for multi-signing.";
return tefNOT_MULTI_SIGNING;
}
// We have plans to support multiple SignerLists in the future. The
// presence and defaulted value of the SignerListID field will enable
// that.
XRPL_ASSERT(
sleAllowedSigners->isFieldPresent(sfSignerListID),
"ripple::Transactor::checkMultiSign : has signer list ID");
XRPL_ASSERT(
sleAllowedSigners->getFieldU32(sfSignerListID) == 0,
"ripple::Transactor::checkMultiSign : signer list ID is 0");
uint32_t const quorum = sleAllowedSigners->getFieldU32(sfSignerQuorum);
uint32_t sum{0};
auto allowedSigners =
SignerEntries::deserialize(*sleAllowedSigners, ctx.j, "ledger");
if (!allowedSigners)
return allowedSigners.error();
// Build lookup map for signer validation and weight retrieval.
std::map<AccountID, uint16_t> signerWeights;
uint32_t totalWeight{0}, cyclicWeight{0};
for (auto const& entry : *allowedSigners)
{
signerWeights[entry.account] = entry.weight;
totalWeight += entry.weight;
if (ancestors.count(entry.account))
cyclicWeight += entry.weight;
}
// Walk the signers array, validating each signer. Signers must be in
// strict ascending order for consensus.
std::optional<AccountID> prevSigner;
for (auto const& signerEntry : signers)
{
AccountID const signer = signerEntry.getAccountID(sfAccount);
if (prevSigner && signer <= *prevSigner)
{
JLOG(ctx.j.trace())
<< "applyTransaction: Invalid SigningAccount.Account.";
<< "checkMultiSign: Signers not in strict ascending order: "
<< signer << " <= " << *prevSigner;
return temMALFORMED;
}
prevSigner = signer;
// Parent-edge authorization is checked before cycle skipping so an
// unauthorized cyclic entry is rejected, not silently ignored.
auto const weightIt = signerWeights.find(signer);
if (weightIt == signerWeights.end())
{
JLOG(ctx.j.trace())
<< "checkMultiSign: Invalid signer " << signer
<< " not in signer list for " << acc;
return tefBAD_SIGNATURE;
}
}
if (iter->account != txSignerAcctID)
{
// The SigningAccount is not in the SignerEntries.
JLOG(ctx.j.trace())
<< "applyTransaction: Invalid SigningAccount.Account.";
return tefBAD_SIGNATURE;
}
// We found the SigningAccount in the list of valid signers. Now we
// need to compute the accountID that is associated with the signer's
// public key.
auto const spk = txSigner.getFieldVL(sfSigningPubKey);
if (!(ctx.flags & tapDRY_RUN) && !publicKeyType(makeSlice(spk)))
{
JLOG(ctx.j.trace())
<< "checkMultiSign: signing public key type is unknown";
return tefBAD_SIGNATURE;
}
// This ternary is only needed to handle `simulate`
XRPL_ASSERT(
(ctx.flags & tapDRY_RUN) || !spk.empty(),
"ripple::Transactor::checkMultiSign : non-empty signer or "
"simulation");
AccountID const signingAcctIDFromPubKey = spk.empty()
? txSignerAcctID
: calcAccountID(PublicKey(makeSlice(spk)));
// Verify that the signingAcctID and the signingAcctIDFromPubKey
// belong together. Here are the rules:
//
// 1. "Phantom account": an account that is not in the ledger
// A. If signingAcctID == signingAcctIDFromPubKey and the
// signingAcctID is not in the ledger then we have a phantom
// account.
// B. Phantom accounts are always allowed as multi-signers.
//
// 2. "Master Key"
// A. signingAcctID == signingAcctIDFromPubKey, and signingAcctID
// is in the ledger.
// B. If the signingAcctID in the ledger does not have the
// asfDisableMaster flag set, then the signature is allowed.
//
// 3. "Regular Key"
// A. signingAcctID != signingAcctIDFromPubKey, and signingAcctID
// is in the ledger.
// B. If signingAcctIDFromPubKey == signingAcctID.RegularKey (from
// ledger) then the signature is allowed.
//
// No other signatures are allowed. (January 2015)
// In any of these cases we need to know whether the account is in
// the ledger. Determine that now.
auto sleTxSignerRoot = ctx.view.read(keylet::account(txSignerAcctID));
if (signingAcctIDFromPubKey == txSignerAcctID)
{
// Either Phantom or Master. Phantoms automatically pass.
if (sleTxSignerRoot)
// The signer is authorized by acc, but is already in the current
// proof path. Treat that cyclic ancestor edge as unavailable for
// this path; do not recurse into it and do not count its weight.
if (ancestors.count(signer))
{
// Master Key. Account may not have asfDisableMaster set.
std::uint32_t const signerAccountFlags =
sleTxSignerRoot->getFieldU32(sfFlags);
JLOG(ctx.j.trace())
<< "checkMultiSign: Skipping cyclic signer: " << signer;
continue;
}
uint16_t const weight = weightIt->second;
if (signerAccountFlags & lsfDisableMaster)
if (isNestedSigner(signerEntry))
{
STArray const& nestedSigners =
signerEntry.getFieldArray(sfSigners);
NotTEC result = validateSigners(
signer, nestedSigners, depth + 1, ancestors);
if (!isTesSuccess(result))
return result;
sum += weight;
JLOG(ctx.j.trace())
<< "checkMultiSign: Nested signer " << signer
<< " validated, weight=" << weight << ", depth=" << depth
<< ", sum=" << sum << "/" << quorum;
}
else if (isLeafSigner(signerEntry))
{
auto const spk = signerEntry.getFieldVL(sfSigningPubKey);
// spk being non-empty in non-simulate is checked in
// STTx::checkMultiSign.
if (!spk.empty() && !publicKeyType(makeSlice(spk)))
{
JLOG(ctx.j.trace())
<< "applyTransaction: Signer:Account lsfDisableMaster.";
return tefMASTER_DISABLED;
<< "checkMultiSign: Unknown public key type for signer "
<< signer;
return tefBAD_SIGNATURE;
}
XRPL_ASSERT(
(ctx.flags & tapDRY_RUN) || !spk.empty(),
"ripple::Transactor::checkMultiSign : non-empty signer or "
"simulation");
AccountID const signingAcctIDFromPubKey = spk.empty()
? signer
: calcAccountID(PublicKey(makeSlice(spk)));
auto sleTxSignerRoot = ctx.view.read(keylet::account(signer));
if (signingAcctIDFromPubKey == signer)
{
// Either Phantom or Master. Phantoms automatically pass.
if (sleTxSignerRoot)
{
// Master Key. Account may not have asfDisableMaster
// set.
std::uint32_t const signerAccountFlags =
sleTxSignerRoot->getFieldU32(sfFlags);
if (signerAccountFlags & lsfDisableMaster)
{
JLOG(ctx.j.trace())
<< "checkMultiSign: Signer " << signer
<< " has lsfDisableMaster set.";
return tefMASTER_DISABLED;
}
}
}
else
{
// May be a Regular Key. Let's find out.
if (!sleTxSignerRoot)
{
JLOG(ctx.j.trace())
<< "checkMultiSign: Non-phantom signer " << signer
<< " lacks account root.";
return tefBAD_SIGNATURE;
}
if (!sleTxSignerRoot->isFieldPresent(sfRegularKey))
{
JLOG(ctx.j.trace()) << "checkMultiSign: Signer "
<< signer << " lacks RegularKey.";
return tefBAD_SIGNATURE;
}
if (signingAcctIDFromPubKey !=
sleTxSignerRoot->getAccountID(sfRegularKey))
{
JLOG(ctx.j.trace())
<< "checkMultiSign: Signer " << signer
<< " pubkey doesn't match RegularKey.";
return tefBAD_SIGNATURE;
}
}
sum += weight;
JLOG(ctx.j.trace())
<< "checkMultiSign: Leaf signer " << signer
<< " validated, weight=" << weight << ", depth=" << depth
<< ", sum=" << sum << "/" << quorum;
}
else
{
JLOG(ctx.j.trace())
<< "checkMultiSign: Malformed signer entry for " << signer;
return tefBAD_SIGNATURE;
}
}
else
if (cyclicWeight > totalWeight)
{
// May be a Regular Key. Let's find out.
// Public key must hash to the account's regular key.
if (!sleTxSignerRoot)
{
JLOG(ctx.j.trace()) << "applyTransaction: Non-phantom signer "
"lacks account root.";
return tefBAD_SIGNATURE;
}
if (!sleTxSignerRoot->isFieldPresent(sfRegularKey))
{
JLOG(ctx.j.trace())
<< "applyTransaction: Account lacks RegularKey.";
return tefBAD_SIGNATURE;
}
if (signingAcctIDFromPubKey !=
sleTxSignerRoot->getAccountID(sfRegularKey))
{
JLOG(ctx.j.trace())
<< "applyTransaction: Account doesn't match RegularKey.";
return tefBAD_SIGNATURE;
}
JLOG(ctx.j.error()) << "checkMultiSign: Invariant violation for "
<< acc << ": cyclicWeight (" << cyclicWeight
<< ") > totalWeight (" << totalWeight << ")";
return tefINTERNAL;
}
// The signer is legitimate. Add their weight toward the quorum.
weightSum += iter->weight;
}
// Cannot perform transaction if quorum is not met.
if (weightSum < sleAccountSigners->getFieldU32(sfSignerQuorum))
// Dynamic delegation still requires the delegated account's own policy
// to be satisfied. The only adjustment is for authorized ancestor edges
// that cannot be used without circular proof. If those cyclic edges
// make the configured quorum unreachable, require all remaining
// non-cyclic weight.
uint32_t cycleAdjustedQuorum = quorum;
uint32_t const maxAchievable = totalWeight - cyclicWeight;
if (cyclicWeight > 0 && maxAchievable < quorum)
{
JLOG(ctx.j.warn()) << "checkMultiSign: Cycle-adjusted quorum for "
<< acc << ": " << quorum << " -> "
<< maxAchievable << " (total=" << totalWeight
<< ", cyclic=" << cyclicWeight << ")";
cycleAdjustedQuorum = maxAchievable;
}
if (cycleAdjustedQuorum == 0)
{
JLOG(ctx.j.warn()) << "checkMultiSign: All signers for " << acc
<< " are cyclic - no valid signing path exists.";
return tefBAD_QUORUM;
}
if (sum < cycleAdjustedQuorum)
{
JLOG(ctx.j.trace()) << "checkMultiSign: Quorum not met for " << acc
<< " at depth " << depth << " (sum=" << sum
<< ", required=" << cycleAdjustedQuorum << ")";
return tefBAD_QUORUM;
}
return tesSUCCESS;
};
STArray const& entries(ctx.tx.getFieldArray(sfSigners));
NotTEC result = validateSigners(id, entries, 1, {});
if (!isTesSuccess(result))
{
JLOG(ctx.j.trace())
<< "applyTransaction: Signers failed to meet quorum.";
return tefBAD_QUORUM;
<< "checkMultiSign: Validation failed with " << transToken(result);
return result;
}
// Met the quorum. Continue.
return tesSUCCESS;
}
@@ -2151,7 +2289,8 @@ Transactor::operator()()
bool const has240819 = view().rules().enabled(fix240819);
bool const has240911 = view().rules().enabled(fix240911);
bool const hasIOURewardClaim =
view().rules().enabled(featureIOURewardClaim);
auto const& sfRewardFields =
*(ripple::SField::knownCodeToField.at(917511 - has240819));
@@ -2161,11 +2300,92 @@ Transactor::operator()()
SField const& metaType = node.getFName();
uint16_t nodeType = node.getFieldU16(sfLedgerEntryType);
// we only care about ltACCOUNT_ROOT objects being modified or
// created
if (nodeType != ltACCOUNT_ROOT || metaType == sfDeletedNode)
// we only care about ltACCOUNT_ROOT and ltRIPPLE_STATE objects
// being modified or created
if ((nodeType != ltACCOUNT_ROOT && nodeType != ltRIPPLE_STATE) ||
metaType == sfDeletedNode)
continue;
// ltRippleState
if (nodeType == ltRIPPLE_STATE)
{
if (!hasIOURewardClaim)
continue;
if (!node.isFieldPresent(sfPreviousFields) ||
!node.isFieldPresent(sfLedgerIndex))
continue;
auto sle = view().peek(
Keylet{ltRIPPLE_STATE, node.getFieldH256(sfLedgerIndex)});
if (!sle)
continue;
STObject& previousFields = (const_cast<STObject&>(node))
.getField(sfPreviousFields)
.downcast<STObject>();
if (!previousFields.isFieldPresent(sfBalance))
continue;
auto balance = previousFields.getFieldAmount(sfBalance);
if (balance.native())
continue;
SField const* sfRewardFields[] = {&sfLowReward, &sfHighReward};
for (auto const* sfRewardFieldPtr : sfRewardFields)
{
auto const& sfRewardField = *sfRewardFieldPtr;
if (!sle->isFieldPresent(sfRewardField))
continue;
auto balance_ = balance;
if (sfRewardField == sfHighReward)
balance_.negate();
if (balance_.negative())
balance_.clear();
auto& reward = sle->peekFieldObject(sfRewardField);
uint32_t lgrLast = reward.getFieldU32(sfRewardLgrLast);
uint32_t lgrElapsed = lgrCur - lgrLast;
// update even in cases such as overflow or underflow.
reward.setFieldU32(sfRewardLgrLast, lgrCur);
// overflow safety
if (lgrElapsed > lgrCur || lgrElapsed == 0)
continue;
auto accum =
reward.getFieldAmount(sfTrustLineRewardAccumulator);
STAmount accumNew;
try
{
accumNew = accum +
multiply(balance_,
STAmount(((uint64_t)lgrElapsed)),
balance_.issue());
}
catch (std::exception const&)
{
// Overflow detected, skip this reward calculation
continue;
}
// check for overflow(<) and underflow(=)
if (accumNew <= accum)
continue;
reward.setFieldAmount(
sfTrustLineRewardAccumulator, accumNew);
}
view().update(sle);
continue;
}
// ltAccountRoot
if (!node.isFieldPresent(sfRewardFields) ||
!node.isFieldPresent(sfLedgerIndex))
continue;

View File

@@ -37,9 +37,11 @@
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/STAccount.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/Sign.h>
#include <xrpl/protocol/TxFlags.h>
#include <algorithm>
#include <functional>
#include <iterator>
namespace ripple {
@@ -1318,19 +1320,31 @@ transactionSubmitMultiSigned(
if (signers.empty())
return RPC::make_param_error("tx_json.Signers array may not be empty.");
// The Signers array may only contain Signer objects.
if (std::find_if_not(
signers.begin(), signers.end(), [](STObject const& obj) {
return (
// A Signer object always contains these fields and no
// others.
obj.isFieldPresent(sfAccount) &&
obj.isFieldPresent(sfSigningPubKey) &&
obj.isFieldPresent(sfTxnSignature) && obj.getCount() == 3);
}) != signers.end())
// Recursively validate signer entry structure (including nested ones).
// Feature enablement is enforced later in preflight; RPC only validates
// shape here.
std::function<bool(STArray const&, int)> validateSignersRecursive;
validateSignersRecursive = [&](STArray const& arr, int depth) -> bool {
if (depth > nestedMultiSignMaxDepth)
return false;
for (auto const& signer : arr)
{
if (!isValidSignerEntry(signer))
return false;
if (isNestedSigner(signer) &&
!validateSignersRecursive(
signer.getFieldArray(sfSigners), depth + 1))
return false;
}
return true;
};
if (!validateSignersRecursive(signers, 1))
{
return RPC::make_param_error(
"Signers array may only contain Signer entries.");
"Signers array may only contain valid Signer entries.");
}
// The array must be sorted and validated.