mirror of
https://github.com/Xahau/xahaud.git
synced 2026-04-15 00:02:22 +00:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b49032436 | ||
|
|
7a62559da9 | ||
|
|
59e334c099 | ||
|
|
d7dd6196e8 | ||
|
|
d3cfd46af3 | ||
|
|
94fab7d58b | ||
|
|
53b3b543a7 | ||
|
|
69e72ecb91 | ||
|
|
98a33d11e0 | ||
|
|
c908018647 | ||
|
|
c6ddd6d2c4 | ||
|
|
9018596532 | ||
|
|
b827f0170d | ||
|
|
e4b7e8f0f2 | ||
|
|
1485078d91 | ||
|
|
6625d2be92 | ||
|
|
78906ee086 | ||
|
|
2fb5c92140 | ||
|
|
c4b5ae3787 | ||
|
|
d546d761ce | ||
|
|
e84a36867b | ||
|
|
987247ddc1 | ||
|
|
a5e2fd0699 | ||
|
|
d92403ce35 | ||
|
|
6fb8fef883 | ||
|
|
a8a4774232 | ||
|
|
eaec08471b | ||
|
|
caffeea6fc | ||
|
|
23d49d0548 | ||
|
|
519ab34e4f | ||
|
|
bdc59ac4ec | ||
|
|
96bb67bfe5 | ||
|
|
798212f87c | ||
|
|
a3d61c0fbf | ||
|
|
3e926c9946 | ||
|
|
4392342c99 | ||
|
|
f4fe7b7d9a | ||
|
|
d268638a39 | ||
|
|
b1447afcc0 | ||
|
|
f40621c662 | ||
|
|
36ff48474a | ||
|
|
2adc234bf1 | ||
|
|
89bcacca5b | ||
|
|
6d496cc16f | ||
|
|
63b0245d06 | ||
|
|
fdf02a3853 | ||
|
|
9edf7ae67a | ||
|
|
533ba7ab75 | ||
|
|
4e10d7d61f | ||
|
|
01e7caa0d6 | ||
|
|
349f4d2d68 | ||
|
|
24ac5d5f51 | ||
|
|
8522c6684b | ||
|
|
7efc26a8b1 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -114,3 +114,5 @@ pkg_out
|
||||
pkg
|
||||
CMakeUserPresets.json
|
||||
bld.rippled/
|
||||
|
||||
generated
|
||||
|
||||
@@ -456,6 +456,7 @@ target_sources (rippled PRIVATE
|
||||
src/ripple/app/tx/impl/Remit.cpp
|
||||
src/ripple/app/tx/impl/SetAccount.cpp
|
||||
src/ripple/app/tx/impl/SetHook.cpp
|
||||
src/ripple/app/tx/impl/SetRemarks.cpp
|
||||
src/ripple/app/tx/impl/SetRegularKey.cpp
|
||||
src/ripple/app/tx/impl/SetSignerList.cpp
|
||||
src/ripple/app/tx/impl/SetTrust.cpp
|
||||
@@ -752,7 +753,10 @@ if (tests)
|
||||
src/test/app/Remit_test.cpp
|
||||
src/test/app/SHAMapStore_test.cpp
|
||||
src/test/app/SetAuth_test.cpp
|
||||
src/test/app/SetHook_test.cpp
|
||||
src/test/app/SetHookTSH_test.cpp
|
||||
src/test/app/SetRegularKey_test.cpp
|
||||
src/test/app/SetRemarks_test.cpp
|
||||
src/test/app/SetTrust_test.cpp
|
||||
src/test/app/Taker_test.cpp
|
||||
src/test/app/TheoreticalQuality_test.cpp
|
||||
@@ -765,8 +769,6 @@ if (tests)
|
||||
src/test/app/ValidatorKeys_test.cpp
|
||||
src/test/app/ValidatorList_test.cpp
|
||||
src/test/app/ValidatorSite_test.cpp
|
||||
src/test/app/SetHook_test.cpp
|
||||
src/test/app/SetHookTSH_test.cpp
|
||||
src/test/app/Wildcard_test.cpp
|
||||
src/test/app/XahauGenesis_test.cpp
|
||||
src/test/app/tx/apply_test.cpp
|
||||
@@ -900,6 +902,7 @@ if (tests)
|
||||
src/test/jtx/impl/rate.cpp
|
||||
src/test/jtx/impl/regkey.cpp
|
||||
src/test/jtx/impl/reward.cpp
|
||||
src/test/jtx/impl/remarks.cpp
|
||||
src/test/jtx/impl/remit.cpp
|
||||
src/test/jtx/impl/sendmax.cpp
|
||||
src/test/jtx/impl/seq.cpp
|
||||
|
||||
11
docker-unit-tests.sh
Normal file → Executable file
11
docker-unit-tests.sh
Normal file → Executable file
@@ -1,4 +1,11 @@
|
||||
#!/bin/bash
|
||||
#!/bin/bash -x
|
||||
|
||||
BUILD_CORES=$(echo "scale=0 ; `nproc` / 1.337" | bc)
|
||||
|
||||
if [[ "$GITHUB_REPOSITORY" == "" ]]; then
|
||||
#Default
|
||||
BUILD_CORES=8
|
||||
fi
|
||||
|
||||
echo "Mounting $(pwd)/io in ubuntu and running unit tests"
|
||||
docker run --rm -i -v $(pwd):/io ubuntu sh -c '/io/release-build/xahaud -u'
|
||||
docker run --rm -i -v $(pwd):/io -e BUILD_CORES=$BUILD_CORES ubuntu sh -c '/io/release-build/xahaud --unittest-jobs $BUILD_CORES -u'
|
||||
|
||||
@@ -3,27 +3,27 @@ RIPPLED_ROOT="../src/ripple"
|
||||
echo '// For documentation please see: https://xrpl-hooks.readme.io/reference/'
|
||||
echo '// Generated using generate_sfcodes.sh'
|
||||
cat $RIPPLED_ROOT/protocol/impl/SField.cpp | grep -E '^CONSTRUCT_' |
|
||||
sed 's/UINT16/1/g' |
|
||||
sed 's/UINT32/2/g' |
|
||||
sed 's/UINT64/3/g' |
|
||||
sed 's/HASH128/4/g' |
|
||||
sed 's/HASH256/5/g' |
|
||||
sed 's/UINT128/4/g' |
|
||||
sed 's/UINT256/5/g' |
|
||||
sed 's/AMOUNT/6/g' |
|
||||
sed 's/VL/7/g' | sed 's/Import7/ImportVL/g' |
|
||||
sed 's/ACCOUNT/8/g' |
|
||||
sed 's/OBJECT/14/g' |
|
||||
sed 's/ARRAY/15/g' |
|
||||
sed 's/UINT8/16/g' |
|
||||
sed 's/HASH160/17/g' |
|
||||
sed 's/UINT160/17/g' |
|
||||
sed 's/PATHSET/18/g' |
|
||||
sed 's/VECTOR256/19/g' |
|
||||
sed 's/UINT96/20/g' |
|
||||
sed 's/UINT192/21/g' |
|
||||
sed 's/UINT384/22/g' |
|
||||
sed 's/UINT512/23/g' |
|
||||
sed 's/UINT16,/1,/g' |
|
||||
sed 's/UINT32,/2,/g' |
|
||||
sed 's/UINT64,/3,/g' |
|
||||
sed 's/HASH128,/4,/g' |
|
||||
sed 's/HASH256,/5,/g' |
|
||||
sed 's/UINT128,/4,/g' |
|
||||
sed 's/UINT256,/5,/g' |
|
||||
sed 's/AMOUNT,/6,/g' |
|
||||
sed 's/VL,/7,/g' |
|
||||
sed 's/ACCOUNT,/8,/g' |
|
||||
sed 's/OBJECT,/14,/g' |
|
||||
sed 's/ARRAY,/15,/g' |
|
||||
sed 's/UINT8,/16,/g' |
|
||||
sed 's/HASH160,/17,/g' |
|
||||
sed 's/UINT160,/17,/g' |
|
||||
sed 's/PATHSET,/18,/g' |
|
||||
sed 's/VECTOR256,/19,/g' |
|
||||
sed 's/UINT96,/20,/g' |
|
||||
sed 's/UINT192,/21,/g' |
|
||||
sed 's/UINT384,/22,/g' |
|
||||
sed 's/UINT512,/23,/g' |
|
||||
grep -Eo '"([^"]+)", *([0-9]+), *([0-9]+)' |
|
||||
sed 's/"//g' | sed 's/ *//g' | sed 's/,/ /g' |
|
||||
awk '{print ("#define sf"$1" (("$2"U << 16U) + "$3"U)")}'
|
||||
|
||||
@@ -83,14 +83,15 @@
|
||||
#define sfHookInstructionCount ((3U << 16U) + 17U)
|
||||
#define sfHookReturnCode ((3U << 16U) + 18U)
|
||||
#define sfReferenceCount ((3U << 16U) + 19U)
|
||||
#define sfTouchCount ((3U << 16U) + 97U)
|
||||
#define sfAccountIndex ((3U << 16U) + 98U)
|
||||
#define sfAccountCount ((3U << 16U) + 99U)
|
||||
#define sfRewardAccumulator ((3U << 16U) + 100U)
|
||||
#define sfEmailHash ((4U << 16U) + 1U)
|
||||
#define sfTakerPaysCurrency ((10U << 16U) + 1U)
|
||||
#define sfTakerPaysIssuer ((10U << 16U) + 2U)
|
||||
#define sfTakerGetsCurrency ((10U << 16U) + 3U)
|
||||
#define sfTakerGetsIssuer ((10U << 16U) + 4U)
|
||||
#define sfTakerPaysCurrency ((17U << 16U) + 1U)
|
||||
#define sfTakerPaysIssuer ((17U << 16U) + 2U)
|
||||
#define sfTakerGetsCurrency ((17U << 16U) + 3U)
|
||||
#define sfTakerGetsIssuer ((17U << 16U) + 4U)
|
||||
#define sfLedgerHash ((5U << 16U) + 1U)
|
||||
#define sfParentHash ((5U << 16U) + 2U)
|
||||
#define sfTransactionHash ((5U << 16U) + 3U)
|
||||
|
||||
@@ -428,6 +428,12 @@ namespace hook {
|
||||
bool
|
||||
canHook(ripple::TxType txType, ripple::uint256 hookOn);
|
||||
|
||||
bool
|
||||
canEmit(ripple::TxType txType, ripple::uint256 hookCanEmit);
|
||||
|
||||
ripple::uint256
|
||||
getHookCanEmit(ripple::STObject const& hookObj, SLE::pointer const& hookDef);
|
||||
|
||||
struct HookResult;
|
||||
|
||||
HookResult
|
||||
@@ -436,6 +442,7 @@ apply(
|
||||
used for caching (one day) */
|
||||
ripple::uint256 const&
|
||||
hookHash, /* hash of the actual hook byte code, used for metadata */
|
||||
ripple::uint256 const& hookCanEmit,
|
||||
ripple::uint256 const& hookNamespace,
|
||||
ripple::Blob const& wasm,
|
||||
std::map<
|
||||
@@ -472,6 +479,7 @@ struct HookResult
|
||||
{
|
||||
ripple::uint256 const hookSetTxnID;
|
||||
ripple::uint256 const hookHash;
|
||||
ripple::uint256 const hookCanEmit;
|
||||
ripple::Keylet const accountKeylet;
|
||||
ripple::Keylet const ownerDirKeylet;
|
||||
ripple::Keylet const hookKeylet;
|
||||
|
||||
@@ -1028,6 +1028,29 @@ hook::canHook(ripple::TxType txType, ripple::uint256 hookOn)
|
||||
return (hookOn & UINT256_BIT[txType]) != beast::zero;
|
||||
}
|
||||
|
||||
bool
|
||||
hook::canEmit(ripple::TxType txType, ripple::uint256 hookCanEmit)
|
||||
{
|
||||
return hook::canHook(txType, hookCanEmit);
|
||||
}
|
||||
|
||||
ripple::uint256
|
||||
hook::getHookCanEmit(
|
||||
ripple::STObject const& hookObj,
|
||||
SLE::pointer const& hookDef)
|
||||
{
|
||||
// default allows all transaction types
|
||||
uint256 defaultHookCanEmit = UINT256_BIT[ttHOOK_SET];
|
||||
|
||||
uint256 hookCanEmit =
|
||||
(hookObj.isFieldPresent(sfHookCanEmit)
|
||||
? hookObj.getFieldH256(sfHookCanEmit)
|
||||
: hookDef->isFieldPresent(sfHookCanEmit)
|
||||
? hookDef->getFieldH256(sfHookCanEmit)
|
||||
: defaultHookCanEmit);
|
||||
return hookCanEmit;
|
||||
}
|
||||
|
||||
// Update HookState ledger objects for the hook... only called after accept()
|
||||
// assumes the specified acc has already been checked for authoriation (hook
|
||||
// grants)
|
||||
@@ -1179,6 +1202,7 @@ hook::apply(
|
||||
used for caching (one day) */
|
||||
ripple::uint256 const&
|
||||
hookHash, /* hash of the actual hook byte code, used for metadata */
|
||||
ripple::uint256 const& hookCanEmit,
|
||||
ripple::uint256 const& hookNamespace,
|
||||
ripple::Blob const& wasm,
|
||||
std::map<
|
||||
@@ -1206,6 +1230,7 @@ hook::apply(
|
||||
.result =
|
||||
{.hookSetTxnID = hookSetTxnID,
|
||||
.hookHash = hookHash,
|
||||
.hookCanEmit = hookCanEmit,
|
||||
.accountKeylet = keylet::account(account),
|
||||
.ownerDirKeylet = keylet::ownerDir(account),
|
||||
.hookKeylet = keylet::hook(account),
|
||||
@@ -3270,6 +3295,16 @@ DEFINE_HOOK_FUNCTION(
|
||||
return EMISSION_FAILURE;
|
||||
}
|
||||
|
||||
ripple::TxType txType = stpTrans->getTxnType();
|
||||
|
||||
ripple::uint256 const& hookCanEmit = hookCtx.result.hookCanEmit;
|
||||
if (!hook::canEmit(txType, hookCanEmit))
|
||||
{
|
||||
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
|
||||
<< "]: Hook cannot emit this txn.";
|
||||
return EMISSION_FAILURE;
|
||||
}
|
||||
|
||||
// check the emitted txn is valid
|
||||
/* Emitted TXN rules
|
||||
* 0. Account must match the hook account
|
||||
|
||||
@@ -45,8 +45,11 @@ ClaimReward::preflight(PreflightContext const& ctx)
|
||||
return ret;
|
||||
|
||||
// can have flag 1 set to opt-out of rewards
|
||||
if (ctx.tx.isFieldPresent(sfFlags) &&
|
||||
ctx.tx.getFieldU32(sfFlags) > tfOptOut)
|
||||
auto const invalidFlags = ctx.rules.enabled(fixRewardClaimFlags)
|
||||
? (ctx.tx.getFlags() & tfClaimRewardMask)
|
||||
: (ctx.tx.isFieldPresent(sfFlags) &&
|
||||
ctx.tx.getFieldU32(sfFlags) > tfOptOut);
|
||||
if (invalidFlags)
|
||||
return temINVALID_FLAG;
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfIssuer) &&
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_TX_SIMPLE_PAYMENT_H_INCLUDED
|
||||
#define RIPPLE_TX_SIMPLE_PAYMENT_H_INCLUDED
|
||||
#ifndef RIPPLE_TX_REMIT_H_INCLUDED
|
||||
#define RIPPLE_TX_REMIT_H_INCLUDED
|
||||
|
||||
#include <ripple/app/tx/impl/Transactor.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
|
||||
@@ -225,6 +225,7 @@ SetHook::inferOperation(STObject const& hookSetObj)
|
||||
!hookSetObj.isFieldPresent(sfHookNamespace) &&
|
||||
!hookSetObj.isFieldPresent(sfHookParameters) &&
|
||||
!hookSetObj.isFieldPresent(sfHookOn) &&
|
||||
!hookSetObj.isFieldPresent(sfHookCanEmit) &&
|
||||
!hookSetObj.isFieldPresent(sfHookApiVersion) &&
|
||||
!hookSetObj.isFieldPresent(sfFlags))
|
||||
return hsoNOOP;
|
||||
@@ -259,6 +260,7 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
|
||||
if (hookSetObj.isFieldPresent(sfHookGrants) ||
|
||||
hookSetObj.isFieldPresent(sfHookParameters) ||
|
||||
hookSetObj.isFieldPresent(sfHookOn) ||
|
||||
hookSetObj.isFieldPresent(sfHookCanEmit) ||
|
||||
hookSetObj.isFieldPresent(sfHookApiVersion) ||
|
||||
!hookSetObj.isFieldPresent(sfFlags) ||
|
||||
!hookSetObj.isFieldPresent(sfHookNamespace))
|
||||
@@ -288,6 +290,7 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
|
||||
if (hookSetObj.isFieldPresent(sfHookGrants) ||
|
||||
hookSetObj.isFieldPresent(sfHookParameters) ||
|
||||
hookSetObj.isFieldPresent(sfHookOn) ||
|
||||
hookSetObj.isFieldPresent(sfHookCanEmit) ||
|
||||
hookSetObj.isFieldPresent(sfHookApiVersion) ||
|
||||
hookSetObj.isFieldPresent(sfHookNamespace) ||
|
||||
!hookSetObj.isFieldPresent(sfFlags))
|
||||
@@ -452,6 +455,13 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
|
||||
return false;
|
||||
}
|
||||
|
||||
// validate sfHookCanEmit
|
||||
// HookCanEmit field is an optional field for backward compatibility
|
||||
if (!hookSetObj.isFieldPresent(sfHookCanEmit))
|
||||
{
|
||||
// pass
|
||||
}
|
||||
|
||||
// finally validate web assembly byte code
|
||||
{
|
||||
if (!hookSetObj.isFieldPresent(sfCreateCode))
|
||||
@@ -714,6 +724,10 @@ SetHook::preflight(PreflightContext const& ctx)
|
||||
|
||||
allBlank = false;
|
||||
|
||||
if (!ctx.rules.enabled(featureHookCanEmit) &&
|
||||
hookSetObj.isFieldPresent(sfHookCanEmit))
|
||||
return temDISABLED;
|
||||
|
||||
for (auto const& hookSetElement : hookSetObj)
|
||||
{
|
||||
auto const& name = hookSetElement.getFName();
|
||||
@@ -721,7 +735,8 @@ SetHook::preflight(PreflightContext const& ctx)
|
||||
if (name != sfCreateCode && name != sfHookHash &&
|
||||
name != sfHookNamespace && name != sfHookParameters &&
|
||||
name != sfHookOn && name != sfHookGrants &&
|
||||
name != sfHookApiVersion && name != sfFlags)
|
||||
name != sfHookApiVersion && name != sfFlags &&
|
||||
name != sfHookCanEmit)
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "HookSet(" << hook::log::HOOK_INVALID_FIELD << ")["
|
||||
@@ -1222,6 +1237,10 @@ SetHook::setHook()
|
||||
std::optional<uint256> newHookOn;
|
||||
std::optional<uint256> defHookOn;
|
||||
|
||||
std::optional<uint256> oldHookCanEmit;
|
||||
std::optional<uint256> newHookCanEmit;
|
||||
std::optional<uint256> defHookCanEmit;
|
||||
|
||||
// when hsoCREATE is invoked it populates this variable in case the hook
|
||||
// definition already exists and the operation falls through into a
|
||||
// hsoINSTALL operation instead
|
||||
@@ -1282,6 +1301,14 @@ SetHook::setHook()
|
||||
oldHookOn = oldHook->get().getFieldH256(sfHookOn);
|
||||
else if (defHookOn)
|
||||
oldHookOn = *defHookOn;
|
||||
|
||||
if (oldDefSLE && oldDefSLE->isFieldPresent(sfHookCanEmit))
|
||||
defHookCanEmit = oldDefSLE->getFieldH256(sfHookCanEmit);
|
||||
|
||||
if (oldHook && oldHook->get().isFieldPresent(sfHookCanEmit))
|
||||
oldHookCanEmit = oldHook->get().getFieldH256(sfHookCanEmit);
|
||||
else if (defHookCanEmit)
|
||||
oldHookCanEmit = *defHookCanEmit;
|
||||
}
|
||||
|
||||
// in preparation for three way merge populate fields if they are
|
||||
@@ -1298,6 +1325,9 @@ SetHook::setHook()
|
||||
if (hookSetObj->get().isFieldPresent(sfHookOn))
|
||||
newHookOn = hookSetObj->get().getFieldH256(sfHookOn);
|
||||
|
||||
if (hookSetObj->get().isFieldPresent(sfHookCanEmit))
|
||||
newHookCanEmit = hookSetObj->get().getFieldH256(sfHookCanEmit);
|
||||
|
||||
if (hookSetObj->get().isFieldPresent(sfHookNamespace))
|
||||
{
|
||||
newNamespace = hookSetObj->get().getFieldH256(sfHookNamespace);
|
||||
@@ -1315,13 +1345,14 @@ SetHook::setHook()
|
||||
}
|
||||
else if (op == hsoNSDELETE && newDirKeylet)
|
||||
{
|
||||
printf("Marking a namespace for destruction.... NSDELETE\n");
|
||||
JLOG(ctx.j.trace())
|
||||
<< "Marking a namespace for destruction.... NSDELETE";
|
||||
namespacesToDestroy.emplace(*newNamespace);
|
||||
}
|
||||
else if (oldDirKeylet)
|
||||
{
|
||||
printf(
|
||||
"Marking a namespace for destruction.... non-NSDELETE\n");
|
||||
JLOG(ctx.j.trace())
|
||||
<< "Marking a namespace for destruction.... non-NSDELETE";
|
||||
namespacesToDestroy.emplace(*oldNamespace);
|
||||
}
|
||||
else
|
||||
@@ -1407,6 +1438,10 @@ SetHook::setHook()
|
||||
if (oldHook->get().isFieldPresent(sfHookOn))
|
||||
newHook.setFieldH256(
|
||||
sfHookOn, oldHook->get().getFieldH256(sfHookOn));
|
||||
if (oldHook->get().isFieldPresent(sfHookCanEmit))
|
||||
newHook.setFieldH256(
|
||||
sfHookCanEmit,
|
||||
oldHook->get().getFieldH256(sfHookCanEmit));
|
||||
if (oldHook->get().isFieldPresent(sfHookNamespace))
|
||||
newHook.setFieldH256(
|
||||
sfHookNamespace,
|
||||
@@ -1436,6 +1471,19 @@ SetHook::setHook()
|
||||
newHook.setFieldH256(sfHookOn, *newHookOn);
|
||||
}
|
||||
|
||||
// set the hookcanemit field if it differs from definition
|
||||
if (newHookCanEmit)
|
||||
{
|
||||
if (defHookCanEmit.has_value() &&
|
||||
*defHookCanEmit == *newHookCanEmit)
|
||||
{
|
||||
if (newHook.isFieldPresent(sfHookCanEmit))
|
||||
newHook.makeFieldAbsent(sfHookCanEmit);
|
||||
}
|
||||
else
|
||||
newHook.setFieldH256(sfHookCanEmit, *newHookCanEmit);
|
||||
}
|
||||
|
||||
// parameters
|
||||
if (hookSetObj->get().isFieldPresent(sfHookParameters) &&
|
||||
hookSetObj->get().getFieldArray(sfHookParameters).empty())
|
||||
@@ -1585,6 +1633,9 @@ SetHook::setHook()
|
||||
auto newHookDef = std::make_shared<SLE>(keylet);
|
||||
newHookDef->setFieldH256(sfHookHash, *createHookHash);
|
||||
newHookDef->setFieldH256(sfHookOn, *newHookOn);
|
||||
if (newHookCanEmit)
|
||||
newHookDef->setFieldH256(
|
||||
sfHookCanEmit, *newHookCanEmit);
|
||||
newHookDef->setFieldH256(sfHookNamespace, *newNamespace);
|
||||
newHookDef->setFieldArray(
|
||||
sfHookParameters,
|
||||
@@ -1678,6 +1729,8 @@ SetHook::setHook()
|
||||
// change which definition we're using to the new target
|
||||
defNamespace = newDefSLE->getFieldH256(sfHookNamespace);
|
||||
defHookOn = newDefSLE->getFieldH256(sfHookOn);
|
||||
if (newDefSLE->isFieldPresent(sfHookCanEmit))
|
||||
defHookCanEmit = newDefSLE->getFieldH256(sfHookCanEmit);
|
||||
|
||||
// set the namespace if it differs from the definition namespace
|
||||
if (newNamespace && *defNamespace != *newNamespace)
|
||||
@@ -1687,6 +1740,12 @@ SetHook::setHook()
|
||||
if (newHookOn && *defHookOn != *newHookOn)
|
||||
newHook.setFieldH256(sfHookOn, *newHookOn);
|
||||
|
||||
// set the hookcanemit field if it differs from definition
|
||||
if (newHookCanEmit &&
|
||||
!(defHookCanEmit.has_value() &&
|
||||
*defHookCanEmit == *newHookCanEmit))
|
||||
newHook.setFieldH256(sfHookCanEmit, *newHookCanEmit);
|
||||
|
||||
// parameters
|
||||
TER result = updateHookParameters(
|
||||
ctx,
|
||||
@@ -1735,8 +1794,8 @@ SetHook::setHook()
|
||||
// sfHook: 1 reserve PER non-blank entry
|
||||
// sfParameters: 1 reserve PER entry
|
||||
// sfGrants are: 1 reserve PER entry
|
||||
// sfHookHash, sfHookNamespace, sfHookOn, sfHookApiVersion, sfFlags:
|
||||
// free
|
||||
// sfHookHash, sfHookNamespace, sfHookOn, sfHookCanEmit,
|
||||
// sfHookApiVersion, sfFlags: free
|
||||
|
||||
// sfHookDefinition is not reserved because it is an unowned object,
|
||||
// rather the uploader is billed via fee according to the following:
|
||||
|
||||
450
src/ripple/app/tx/impl/SetRemarks.cpp
Normal file
450
src/ripple/app/tx/impl/SetRemarks.cpp
Normal file
@@ -0,0 +1,450 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 <ripple/app/tx/impl/SetRemarks.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/core/Config.h>
|
||||
#include <ripple/ledger/View.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
#include <ripple/protocol/PublicKey.h>
|
||||
#include <ripple/protocol/Quality.h>
|
||||
#include <ripple/protocol/st.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
TxConsequences
|
||||
SetRemarks::makeTxConsequences(PreflightContext const& ctx)
|
||||
{
|
||||
return TxConsequences{ctx.tx, TxConsequences::normal};
|
||||
}
|
||||
|
||||
NotTEC
|
||||
SetRemarks::validateRemarks(STArray const& remarks, beast::Journal const& j)
|
||||
{
|
||||
std::set<Blob> already_seen;
|
||||
|
||||
if (remarks.empty() || remarks.size() > 32)
|
||||
{
|
||||
JLOG(j.warn()) << "SetRemarks: Cannot set more than 32 remarks (or "
|
||||
"fewer than 1) in a txn.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
for (auto const& remark : remarks)
|
||||
{
|
||||
if (remark.getFName() != sfRemark)
|
||||
{
|
||||
JLOG(j.warn()) << "SetRemarks: contained non-sfRemark field.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// will be checked by template system, extra check for security
|
||||
if (!remark.isFieldPresent(sfRemarkName))
|
||||
return temMALFORMED;
|
||||
|
||||
Blob const& name = remark.getFieldVL(sfRemarkName);
|
||||
if (name.size() == 0 || name.size() > 256)
|
||||
{
|
||||
JLOG(j.warn()) << "SetRemarks: RemarkName cannot be empty or "
|
||||
"larger than 256 chars.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (already_seen.find(name) != already_seen.end())
|
||||
{
|
||||
JLOG(j.warn()) << "SetRemarks: duplicate RemarkName entry.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
already_seen.emplace(name);
|
||||
|
||||
uint32_t flags =
|
||||
remark.isFieldPresent(sfFlags) ? remark.getFieldU32(sfFlags) : 0;
|
||||
if (flags != 0 && flags != tfImmutable)
|
||||
{
|
||||
JLOG(j.warn())
|
||||
<< "SetRemarks: Flags must be either tfImmutable or 0";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
if (!remark.isFieldPresent(sfRemarkValue))
|
||||
{
|
||||
if (flags & tfImmutable)
|
||||
{
|
||||
JLOG(j.warn()) << "SetRemarks: A remark deletion cannot be "
|
||||
"marked immutable.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
Blob const& val = remark.getFieldVL(sfRemarkValue);
|
||||
if (val.size() == 0 || val.size() > 256)
|
||||
{
|
||||
JLOG(j.warn()) << "SetRemarks: RemarkValue cannot be empty or "
|
||||
"larger than 256 chars.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
SetRemarks::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (!ctx.rules.enabled(featureRemarks))
|
||||
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()) << "SetRemarks: Invalid flags set.";
|
||||
return temINVALID_FLAG;
|
||||
}
|
||||
|
||||
auto const& remarks = tx.getFieldArray(sfRemarks);
|
||||
if (NotTEC result = validateRemarks(remarks, j); !isTesSuccess(result))
|
||||
return result;
|
||||
|
||||
return preflight2(ctx);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline std::optional<AccountID>
|
||||
getRemarksIssuer(T const& sleO)
|
||||
{
|
||||
std::optional<AccountID> issuer;
|
||||
|
||||
// check if it's an allowable object type
|
||||
uint16_t lt = sleO->getFieldU16(sfLedgerEntryType);
|
||||
|
||||
switch (lt)
|
||||
{
|
||||
case ltACCOUNT_ROOT:
|
||||
case ltOFFER:
|
||||
case ltESCROW:
|
||||
case ltTICKET:
|
||||
case ltPAYCHAN:
|
||||
case ltCHECK:
|
||||
case ltDEPOSIT_PREAUTH: {
|
||||
issuer = sleO->getAccountID(sfAccount);
|
||||
break;
|
||||
}
|
||||
|
||||
case ltNFTOKEN_OFFER: {
|
||||
issuer = sleO->getAccountID(sfOwner);
|
||||
break;
|
||||
}
|
||||
|
||||
case ltURI_TOKEN: {
|
||||
issuer = sleO->getAccountID(sfIssuer);
|
||||
break;
|
||||
}
|
||||
|
||||
case ltRIPPLE_STATE: {
|
||||
// remarks can only be attached to a trustline by the issuer
|
||||
AccountID lowAcc = sleO->getFieldAmount(sfLowLimit).getIssuer();
|
||||
AccountID highAcc = sleO->getFieldAmount(sfHighLimit).getIssuer();
|
||||
|
||||
STAmount bal = sleO->getFieldAmount(sfBalance);
|
||||
|
||||
if (bal < beast::zero)
|
||||
{
|
||||
// low account is issuer
|
||||
issuer = lowAcc;
|
||||
break;
|
||||
}
|
||||
|
||||
if (bal > beast::zero)
|
||||
{
|
||||
// high acccount is issuer
|
||||
issuer = highAcc;
|
||||
break;
|
||||
}
|
||||
|
||||
// if the balance is zero we'll look for the side in default state
|
||||
// and assume this is the issuer
|
||||
uint32_t flags = sleO->getFieldU32(sfFlags);
|
||||
bool const highReserve = (flags & lsfHighReserve);
|
||||
bool const lowReserve = (flags & lsfLowReserve);
|
||||
|
||||
if (!highReserve && !lowReserve)
|
||||
{
|
||||
// error state
|
||||
// do nothing, fallthru.
|
||||
}
|
||||
else if (highReserve && lowReserve)
|
||||
{
|
||||
// in this edge case we don't know who is the issuer, because
|
||||
// there isn't a clear issuer. do nothing, fallthru.
|
||||
}
|
||||
else
|
||||
{
|
||||
issuer = (highReserve ? lowAcc : highAcc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return issuer;
|
||||
}
|
||||
|
||||
TER
|
||||
SetRemarks::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
if (!ctx.view.rules().enabled(featureRemarks))
|
||||
return temDISABLED;
|
||||
|
||||
auto const id = ctx.tx[sfAccount];
|
||||
|
||||
auto const sle = ctx.view.read(keylet::account(id));
|
||||
if (!sle)
|
||||
return terNO_ACCOUNT;
|
||||
|
||||
auto const objID = ctx.tx[sfObjectID];
|
||||
auto const sleO = ctx.view.read(keylet::unchecked(objID));
|
||||
if (!sleO)
|
||||
return tecNO_TARGET;
|
||||
|
||||
std::optional<AccountID> issuer = getRemarksIssuer(sleO);
|
||||
|
||||
if (!issuer || *issuer != id)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// sanity check the remarks merge between txn and obj
|
||||
auto const& remarksTxn = ctx.tx.getFieldArray(sfRemarks);
|
||||
|
||||
std::map<Blob, std::pair<Blob, bool>> keys;
|
||||
if (sleO->isFieldPresent(sfRemarks))
|
||||
{
|
||||
auto const& remarksObj = sleO->getFieldArray(sfRemarks);
|
||||
|
||||
// map the remark name to its value and whether it's immutable
|
||||
for (auto const& remark : remarksObj)
|
||||
keys.emplace(std::make_pair(
|
||||
remark.getFieldVL(sfRemarkName),
|
||||
std::make_pair(
|
||||
remark.getFieldVL(sfRemarkValue),
|
||||
remark.isFieldPresent(sfFlags) &&
|
||||
remark.getFieldU32(sfFlags) & tfImmutable)));
|
||||
}
|
||||
|
||||
int64_t count = keys.size();
|
||||
|
||||
for (auto const& remark : remarksTxn)
|
||||
{
|
||||
std::optional<Blob> valTxn;
|
||||
if (remark.isFieldPresent(sfRemarkValue))
|
||||
valTxn = remark.getFieldVL(sfRemarkValue);
|
||||
bool const isDeletion = !valTxn.has_value();
|
||||
|
||||
Blob name = remark.getFieldVL(sfRemarkName);
|
||||
if (keys.find(name) == keys.end())
|
||||
{
|
||||
// new remark
|
||||
if (isDeletion)
|
||||
{
|
||||
// this could have been an error but deleting something
|
||||
// that doesn't exist is traditionally not an error in xrpl
|
||||
continue;
|
||||
}
|
||||
|
||||
++count;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto const& [valObj, immutable] = keys[name];
|
||||
|
||||
// even if it's mutable, if we don't mutate it that's a noop so just
|
||||
// pass it
|
||||
if (valTxn.has_value() && *valTxn == valObj)
|
||||
continue;
|
||||
|
||||
if (immutable)
|
||||
{
|
||||
JLOG(ctx.j.warn())
|
||||
<< "SetRemarks: attempt to mutate an immutable remark.";
|
||||
return tecIMMUTABLE;
|
||||
}
|
||||
|
||||
if (isDeletion)
|
||||
{
|
||||
if (--count < 0)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "SetRemarks: insane remarks accounting.";
|
||||
return tecCLAIM;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 32)
|
||||
{
|
||||
JLOG(ctx.j.warn())
|
||||
<< "SetRemarks: an object may have at most 32 remarks.";
|
||||
return tecTOO_MANY_REMARKS;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
SetRemarks::doApply()
|
||||
{
|
||||
auto j = ctx_.journal;
|
||||
Sandbox sb(&ctx_.view());
|
||||
|
||||
auto const sle = sb.read(keylet::account(account_));
|
||||
if (!sle)
|
||||
return terNO_ACCOUNT;
|
||||
|
||||
auto const objID = ctx_.tx[sfObjectID];
|
||||
auto sleO = sb.peek(keylet::unchecked(objID));
|
||||
if (!sleO)
|
||||
return terNO_ACCOUNT;
|
||||
|
||||
std::optional<AccountID> issuer = getRemarksIssuer(sleO);
|
||||
|
||||
if (!issuer || *issuer != account_)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
auto const& remarksTxn = ctx_.tx.getFieldArray(sfRemarks);
|
||||
|
||||
std::map<Blob, std::pair<Blob, bool>> remarksMap;
|
||||
|
||||
if (sleO->isFieldPresent(sfRemarks))
|
||||
{
|
||||
auto const& remarksObj = sleO->getFieldArray(sfRemarks);
|
||||
for (auto const& remark : remarksObj)
|
||||
{
|
||||
uint32_t flags = remark.isFieldPresent(sfFlags)
|
||||
? remark.getFieldU32(sfFlags)
|
||||
: 0;
|
||||
bool const immutable = (flags & tfImmutable) != 0;
|
||||
remarksMap[remark.getFieldVL(sfRemarkName)] = {
|
||||
remark.getFieldVL(sfRemarkValue),
|
||||
remark.isFieldPresent(sfFlags) && immutable};
|
||||
}
|
||||
}
|
||||
|
||||
for (auto const& remark : remarksTxn)
|
||||
{
|
||||
std::optional<Blob> val;
|
||||
if (remark.isFieldPresent(sfRemarkValue))
|
||||
val = remark.getFieldVL(sfRemarkValue);
|
||||
Blob name = remark.getFieldVL(sfRemarkName);
|
||||
|
||||
bool const isDeletion = !val.has_value();
|
||||
uint32_t flags =
|
||||
remark.isFieldPresent(sfFlags) ? remark.getFieldU32(sfFlags) : 0;
|
||||
bool const setImmutable = (flags & tfImmutable) != 0;
|
||||
|
||||
if (isDeletion)
|
||||
{
|
||||
if (remarksMap.find(name) != remarksMap.end())
|
||||
remarksMap.erase(name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (remarksMap.find(name) == remarksMap.end())
|
||||
{
|
||||
remarksMap[name] = std::make_pair(*val, setImmutable);
|
||||
continue;
|
||||
}
|
||||
|
||||
remarksMap[name].first = *val;
|
||||
if (!remarksMap[name].second)
|
||||
remarksMap[name].second = setImmutable;
|
||||
}
|
||||
|
||||
// canonically order
|
||||
std::vector<Blob> keys;
|
||||
for (auto const& [k, _] : remarksMap)
|
||||
keys.push_back(k);
|
||||
|
||||
std::sort(keys.begin(), keys.end());
|
||||
|
||||
STArray newRemarks{sfRemarks, static_cast<int>(keys.size())};
|
||||
for (auto const& k : keys)
|
||||
{
|
||||
STObject remark{sfRemark};
|
||||
|
||||
remark.setFieldVL(sfRemarkName, k);
|
||||
remark.setFieldVL(sfRemarkValue, remarksMap[k].first);
|
||||
if (remarksMap[k].second)
|
||||
remark.setFieldU32(sfFlags, lsfImmutable);
|
||||
|
||||
newRemarks.push_back(std::move(remark));
|
||||
}
|
||||
|
||||
if (newRemarks.size() > 32)
|
||||
return tecTOO_MANY_REMARKS;
|
||||
|
||||
if (newRemarks.empty() && sleO->isFieldPresent(sfRemarks))
|
||||
sleO->makeFieldAbsent(sfRemarks);
|
||||
else
|
||||
sleO->setFieldArray(sfRemarks, std::move(newRemarks));
|
||||
|
||||
sb.update(sleO);
|
||||
sb.apply(ctx_.rawView());
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
SetRemarks::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
XRPAmount remarkFee{0};
|
||||
if (tx.isFieldPresent(sfRemarks))
|
||||
{
|
||||
int64_t remarkBytes = 0;
|
||||
auto const& remarks = tx.getFieldArray(sfRemarks);
|
||||
for (auto const& remark : remarks)
|
||||
{
|
||||
int64_t entryBytes = 0;
|
||||
if (remark.isFieldPresent(sfRemarkName))
|
||||
{
|
||||
entryBytes += remark.getFieldVL(sfRemarkName).size();
|
||||
}
|
||||
if (remark.isFieldPresent(sfRemarkValue))
|
||||
{
|
||||
entryBytes += remark.getFieldVL(sfRemarkValue).size();
|
||||
}
|
||||
|
||||
// overflow
|
||||
if (remarkBytes + entryBytes < remarkBytes)
|
||||
return INITIAL_XRP;
|
||||
|
||||
remarkBytes += entryBytes;
|
||||
}
|
||||
|
||||
// one drop per byte
|
||||
remarkFee = XRPAmount{remarkBytes};
|
||||
}
|
||||
auto fee = Transactor::calculateBaseFee(view, tx);
|
||||
return fee + remarkFee;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
60
src/ripple/app/tx/impl/SetRemarks.h
Normal file
60
src/ripple/app/tx/impl/SetRemarks.h
Normal file
@@ -0,0 +1,60 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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_SETREMARKS_H_INCLUDED
|
||||
#define RIPPLE_TX_SETREMARKS_H_INCLUDED
|
||||
|
||||
#include <ripple/app/tx/impl/Transactor.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/core/Config.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class SetRemarks : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
|
||||
|
||||
explicit SetRemarks(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static XRPAmount
|
||||
calculateBaseFee(ReadView const& view, STTx const& tx);
|
||||
|
||||
static TxConsequences
|
||||
makeTxConsequences(PreflightContext const& ctx);
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const&);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
|
||||
static NotTEC
|
||||
validateRemarks(STArray const& remarks, beast::Journal const& j);
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -1230,6 +1230,8 @@ Transactor::executeHookChain(
|
||||
if (!hook::canHook(ctx_.tx.getTxnType(), hookOn))
|
||||
continue; // skip if it can't
|
||||
|
||||
uint256 hookCanEmit = hook::getHookCanEmit(hookObj, hookDef);
|
||||
|
||||
uint32_t flags =
|
||||
(hookObj.isFieldPresent(sfFlags) ? hookObj.getFieldU32(sfFlags)
|
||||
: hookDef->getFieldU32(sfFlags));
|
||||
@@ -1265,6 +1267,7 @@ Transactor::executeHookChain(
|
||||
results.push_back(hook::apply(
|
||||
hookDef->getFieldH256(sfHookSetTxnID),
|
||||
hookHash,
|
||||
hookCanEmit,
|
||||
ns,
|
||||
hookDef->getFieldVL(sfCreateCode),
|
||||
parameters,
|
||||
@@ -1395,6 +1398,8 @@ Transactor::doHookCallback(
|
||||
if (hookObj.getFieldH256(sfHookHash) != callbackHookHash)
|
||||
continue;
|
||||
|
||||
uint256 hookCanEmit = hook::getHookCanEmit(hookObj, hookDef);
|
||||
|
||||
// fetch the namespace either from the hook object of, if absent, the
|
||||
// hook def
|
||||
uint256 const& ns =
|
||||
@@ -1420,6 +1425,7 @@ Transactor::doHookCallback(
|
||||
hook::HookResult callbackResult = hook::apply(
|
||||
hookDef->getFieldH256(sfHookSetTxnID),
|
||||
callbackHookHash,
|
||||
hookCanEmit,
|
||||
ns,
|
||||
hookDef->getFieldVL(sfCreateCode),
|
||||
parameters,
|
||||
@@ -1667,6 +1673,8 @@ Transactor::doAgainAsWeak(
|
||||
continue;
|
||||
}
|
||||
|
||||
uint256 hookCanEmit = hook::getHookCanEmit(hookObj, hookDef);
|
||||
|
||||
// fetch the namespace either from the hook object of, if absent, the
|
||||
// hook def
|
||||
uint256 const& ns =
|
||||
@@ -1687,6 +1695,7 @@ Transactor::doAgainAsWeak(
|
||||
hook::HookResult aawResult = hook::apply(
|
||||
hookDef->getFieldH256(sfHookSetTxnID),
|
||||
hookHash,
|
||||
hookCanEmit,
|
||||
ns,
|
||||
hookDef->getFieldVL(sfCreateCode),
|
||||
parameters,
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
#include <ripple/app/tx/impl/SetAccount.h>
|
||||
#include <ripple/app/tx/impl/SetHook.h>
|
||||
#include <ripple/app/tx/impl/SetRegularKey.h>
|
||||
#include <ripple/app/tx/impl/SetRemarks.h>
|
||||
#include <ripple/app/tx/impl/SetSignerList.h>
|
||||
#include <ripple/app/tx/impl/SetTrust.h>
|
||||
#include <ripple/app/tx/impl/URIToken.h>
|
||||
@@ -169,6 +170,8 @@ invoke_preflight(PreflightContext const& ctx)
|
||||
return invoke_preflight_helper<Invoke>(ctx);
|
||||
case ttREMIT:
|
||||
return invoke_preflight_helper<Remit>(ctx);
|
||||
case ttREMARKS_SET:
|
||||
return invoke_preflight_helper<SetRemarks>(ctx);
|
||||
case ttURITOKEN_MINT:
|
||||
case ttURITOKEN_BURN:
|
||||
case ttURITOKEN_BUY:
|
||||
@@ -290,6 +293,8 @@ invoke_preclaim(PreclaimContext const& ctx)
|
||||
return invoke_preclaim<Invoke>(ctx);
|
||||
case ttREMIT:
|
||||
return invoke_preclaim<Remit>(ctx);
|
||||
case ttREMARKS_SET:
|
||||
return invoke_preclaim<SetRemarks>(ctx);
|
||||
case ttURITOKEN_MINT:
|
||||
case ttURITOKEN_BURN:
|
||||
case ttURITOKEN_BUY:
|
||||
@@ -373,6 +378,8 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
return Invoke::calculateBaseFee(view, tx);
|
||||
case ttREMIT:
|
||||
return Remit::calculateBaseFee(view, tx);
|
||||
case ttREMARKS_SET:
|
||||
return SetRemarks::calculateBaseFee(view, tx);
|
||||
case ttURITOKEN_MINT:
|
||||
case ttURITOKEN_BURN:
|
||||
case ttURITOKEN_BUY:
|
||||
@@ -556,6 +563,10 @@ invoke_apply(ApplyContext& ctx)
|
||||
Remit p(ctx);
|
||||
return p();
|
||||
}
|
||||
case ttREMARKS_SET: {
|
||||
SetRemarks p(ctx);
|
||||
return p();
|
||||
}
|
||||
case ttURITOKEN_MINT:
|
||||
case ttURITOKEN_BURN:
|
||||
case ttURITOKEN_BUY:
|
||||
|
||||
@@ -859,6 +859,67 @@ private:
|
||||
return parseAccountRaw2(jvParams, jss::destination_account);
|
||||
}
|
||||
|
||||
// catalogue_create <min_ledger> <max_ledger> <output_file>
|
||||
// [compression_level]
|
||||
Json::Value
|
||||
parseCatalogueCreate(Json::Value const& jvParams)
|
||||
{
|
||||
Json::Value jvRequest(Json::objectValue);
|
||||
|
||||
if (jvParams.size() >= 3)
|
||||
{
|
||||
jvRequest[jss::min_ledger] = jvParams[0u].asUInt();
|
||||
jvRequest[jss::max_ledger] = jvParams[1u].asUInt();
|
||||
jvRequest[jss::output_file] = jvParams[2u].asString();
|
||||
|
||||
if (jvParams.size() >= 4)
|
||||
{
|
||||
// Handle compression level parameter
|
||||
if (jvParams[3u].isString())
|
||||
{
|
||||
// If string parameter, convert to integer
|
||||
jvRequest[jss::compression_level] =
|
||||
beast::lexicalCast<std::uint32_t>(
|
||||
jvParams[3u].asString());
|
||||
}
|
||||
else
|
||||
{
|
||||
jvRequest[jss::compression_level] = jvParams[3u].asUInt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return jvRequest;
|
||||
}
|
||||
|
||||
// catalogue_load <input_file> [ignore_hash]
|
||||
Json::Value
|
||||
parseCatalogueLoad(Json::Value const& jvParams)
|
||||
{
|
||||
Json::Value jvRequest(Json::objectValue);
|
||||
|
||||
if (jvParams.size() >= 1)
|
||||
{
|
||||
jvRequest[jss::input_file] = jvParams[0u].asString();
|
||||
|
||||
if (jvParams.size() >= 2 &&
|
||||
boost::iequals(jvParams[1u].asString(), "ignore_hash"))
|
||||
{
|
||||
jvRequest[jss::ignore_hash] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return jvRequest;
|
||||
}
|
||||
|
||||
// catalogue_status - no parameters required
|
||||
Json::Value
|
||||
parseCatalogueStatus(Json::Value const& jvParams)
|
||||
{
|
||||
// No special parameters needed - just return an empty object
|
||||
return {Json::objectValue};
|
||||
}
|
||||
|
||||
// channel_authorize: <private_key> [<key_type>] <channel_id> <drops |
|
||||
// amount>
|
||||
Json::Value
|
||||
@@ -1400,6 +1461,9 @@ public:
|
||||
{"book_changes", &RPCParser::parseLedgerId, 1, 1},
|
||||
{"book_offers", &RPCParser::parseBookOffers, 2, 7},
|
||||
{"can_delete", &RPCParser::parseCanDelete, 0, 1},
|
||||
{"catalogue_create", &RPCParser::parseCatalogueCreate, 3, 4},
|
||||
{"catalogue_load", &RPCParser::parseCatalogueLoad, 1, 2},
|
||||
{"catalogue_status", &RPCParser::parseCatalogueStatus, 0, 0},
|
||||
{"channel_authorize", &RPCParser::parseChannelAuthorize, 3, 4},
|
||||
{"channel_verify", &RPCParser::parseChannelVerify, 4, 4},
|
||||
{"connect", &RPCParser::parseConnect, 1, 2},
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace detail {
|
||||
// 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 = 78;
|
||||
static constexpr std::size_t numFeatures = 81;
|
||||
|
||||
/** Amendments that this server supports and the default voting behavior.
|
||||
Whether they are enabled depends on the Rules defined in the validated
|
||||
@@ -362,10 +362,13 @@ extern uint256 const fix240819;
|
||||
extern uint256 const fixPageCap;
|
||||
extern uint256 const fix240911;
|
||||
extern uint256 const fixFloatDivide;
|
||||
extern uint256 const featureRemarks;
|
||||
extern uint256 const featureTouch;
|
||||
extern uint256 const fixReduceImport;
|
||||
extern uint256 const fixXahauV3;
|
||||
extern uint256 const fix20250131;
|
||||
extern uint256 const featureHookCanEmit;
|
||||
extern uint256 const fixRewardClaimFlags;
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
|
||||
@@ -315,6 +315,9 @@ enum LedgerSpecificFlags {
|
||||
|
||||
// ltURI_TOKEN
|
||||
lsfBurnable = 0x00000001, // True, issuer can burn the token
|
||||
|
||||
// remarks
|
||||
lsfImmutable = 1,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -450,6 +450,7 @@ extern SF_UINT256 const sfParentHash;
|
||||
extern SF_UINT256 const sfTransactionHash;
|
||||
extern SF_UINT256 const sfAccountHash;
|
||||
extern SF_UINT256 const sfHookOn;
|
||||
extern SF_UINT256 const sfHookCanEmit;
|
||||
extern SF_UINT256 const sfPreviousTxnID;
|
||||
extern SF_UINT256 const sfLedgerIndex;
|
||||
extern SF_UINT256 const sfWalletLocator;
|
||||
@@ -459,6 +460,7 @@ extern SF_UINT256 const sfNFTokenID;
|
||||
extern SF_UINT256 const sfEmitParentTxnID;
|
||||
extern SF_UINT256 const sfEmitNonce;
|
||||
extern SF_UINT256 const sfEmitHookHash;
|
||||
extern SF_UINT256 const sfObjectID;
|
||||
|
||||
// 256-bit (uncommon)
|
||||
extern SF_UINT256 const sfBookDirectory;
|
||||
@@ -538,6 +540,8 @@ extern SF_VL const sfHookReturnString;
|
||||
extern SF_VL const sfHookParameterName;
|
||||
extern SF_VL const sfHookParameterValue;
|
||||
extern SF_VL const sfBlob;
|
||||
extern SF_VL const sfRemarkName;
|
||||
extern SF_VL const sfRemarkValue;
|
||||
|
||||
// account
|
||||
extern SF_ACCOUNT const sfAccount;
|
||||
@@ -595,6 +599,7 @@ extern SField const sfImportVLKey;
|
||||
extern SField const sfHookEmission;
|
||||
extern SField const sfMintURIToken;
|
||||
extern SField const sfAmountEntry;
|
||||
extern SField const sfRemark;
|
||||
|
||||
// array of objects (common)
|
||||
// ARRAY/1 is reserved for end of array
|
||||
@@ -623,6 +628,7 @@ extern SField const sfActiveValidators;
|
||||
extern SField const sfImportVLKeys;
|
||||
extern SField const sfHookEmissions;
|
||||
extern SField const sfAmounts;
|
||||
extern SField const sfRemarks;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -341,6 +341,8 @@ enum TECcodes : TERUnderlyingType {
|
||||
tecXCHAIN_SELF_COMMIT = 185, // RESERVED - XCHAIN
|
||||
tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR = 186, // RESERVED - XCHAIN
|
||||
tecINSUF_RESERVE_SELLER = 187,
|
||||
tecIMMUTABLE = 188,
|
||||
tecTOO_MANY_REMARKS = 189,
|
||||
tecLAST_POSSIBLE_ENTRY = 255,
|
||||
};
|
||||
|
||||
|
||||
@@ -189,6 +189,10 @@ constexpr std::uint32_t const tfURITokenNonMintMask = ~tfUniversal;
|
||||
enum ClaimRewardFlags : uint32_t {
|
||||
tfOptOut = 0x00000001,
|
||||
};
|
||||
constexpr std::uint32_t const tfClaimRewardMask = ~(tfUniversal | tfOptOut);
|
||||
|
||||
// Remarks flags:
|
||||
constexpr std::uint32_t const tfImmutable = 1;
|
||||
|
||||
// clang-format on
|
||||
|
||||
|
||||
@@ -146,6 +146,9 @@ enum TxType : std::uint16_t
|
||||
ttURITOKEN_CREATE_SELL_OFFER = 48,
|
||||
ttURITOKEN_CANCEL_SELL_OFFER = 49,
|
||||
|
||||
/* A note attaching transactor that allows the owner or issuer (on a object by object basis) to attach remarks */
|
||||
ttREMARKS_SET = 94,
|
||||
|
||||
/* A payment transactor that delivers only the exact amounts specified, creating accounts and TLs as needed
|
||||
* that the sender pays for. */
|
||||
ttREMIT = 95,
|
||||
|
||||
@@ -468,10 +468,13 @@ REGISTER_FIX (fix240819, Supported::yes, VoteBehavior::De
|
||||
REGISTER_FIX (fixPageCap, Supported::yes, VoteBehavior::DefaultYes);
|
||||
REGISTER_FIX (fix240911, Supported::yes, VoteBehavior::DefaultYes);
|
||||
REGISTER_FIX (fixFloatDivide, Supported::yes, VoteBehavior::DefaultYes);
|
||||
REGISTER_FEATURE(Remarks, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FEATURE(Touch, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FIX (fixReduceImport, Supported::yes, VoteBehavior::DefaultYes);
|
||||
REGISTER_FIX (fixXahauV3, Supported::yes, VoteBehavior::DefaultYes);
|
||||
REGISTER_FIX (fix20250131, Supported::yes, VoteBehavior::DefaultYes);
|
||||
REGISTER_FEATURE(HookCanEmit, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FIX (fixRewardClaimFlags, Supported::yes, VoteBehavior::DefaultYes);
|
||||
|
||||
// The following amendments are obsolete, but must remain supported
|
||||
// because they could potentially get enabled.
|
||||
|
||||
@@ -88,6 +88,7 @@ InnerObjectFormats::InnerObjectFormats()
|
||||
{sfHookNamespace, soeREQUIRED},
|
||||
{sfHookParameters, soeREQUIRED},
|
||||
{sfHookOn, soeREQUIRED},
|
||||
{sfHookCanEmit, soeOPTIONAL},
|
||||
{sfHookApiVersion, soeREQUIRED},
|
||||
{sfFlags, soeREQUIRED},
|
||||
{sfFee, soeREQUIRED}});
|
||||
@@ -100,6 +101,7 @@ InnerObjectFormats::InnerObjectFormats()
|
||||
{sfHookNamespace, soeOPTIONAL},
|
||||
{sfHookParameters, soeOPTIONAL},
|
||||
{sfHookOn, soeOPTIONAL},
|
||||
{sfHookCanEmit, soeOPTIONAL},
|
||||
{sfHookApiVersion, soeOPTIONAL},
|
||||
{sfFlags, soeOPTIONAL}});
|
||||
|
||||
@@ -157,6 +159,14 @@ InnerObjectFormats::InnerObjectFormats()
|
||||
{sfDigest, soeOPTIONAL},
|
||||
{sfFlags, soeOPTIONAL},
|
||||
});
|
||||
|
||||
add(sfRemark.jsonName.c_str(),
|
||||
sfRemark.getCode(),
|
||||
{
|
||||
{sfRemarkName, soeREQUIRED},
|
||||
{sfRemarkValue, soeOPTIONAL},
|
||||
{sfFlags, soeOPTIONAL},
|
||||
});
|
||||
}
|
||||
|
||||
InnerObjectFormats const&
|
||||
|
||||
@@ -31,6 +31,7 @@ LedgerFormats::LedgerFormats()
|
||||
{sfLedgerIndex, soeOPTIONAL},
|
||||
{sfLedgerEntryType, soeREQUIRED},
|
||||
{sfFlags, soeREQUIRED},
|
||||
{sfRemarks, soeOPTIONAL},
|
||||
};
|
||||
|
||||
add(jss::AccountRoot,
|
||||
@@ -225,7 +226,8 @@ LedgerFormats::LedgerFormats()
|
||||
ltHOOK_DEFINITION,
|
||||
{
|
||||
{sfHookHash, soeREQUIRED},
|
||||
{sfHookOn, soeREQUIRED},
|
||||
{sfHookOn, soeREQUIRED},
|
||||
{sfHookCanEmit, soeOPTIONAL},
|
||||
{sfHookNamespace, soeREQUIRED},
|
||||
{sfHookParameters, soeREQUIRED},
|
||||
{sfHookApiVersion, soeREQUIRED},
|
||||
|
||||
@@ -211,6 +211,7 @@ CONSTRUCT_TYPED_SFIELD(sfNFTokenID, "NFTokenID", UINT256,
|
||||
CONSTRUCT_TYPED_SFIELD(sfEmitParentTxnID, "EmitParentTxnID", UINT256, 11);
|
||||
CONSTRUCT_TYPED_SFIELD(sfEmitNonce, "EmitNonce", UINT256, 12);
|
||||
CONSTRUCT_TYPED_SFIELD(sfEmitHookHash, "EmitHookHash", UINT256, 13);
|
||||
CONSTRUCT_TYPED_SFIELD(sfObjectID, "ObjectID", UINT256, 14);
|
||||
|
||||
// 256-bit (uncommon)
|
||||
CONSTRUCT_TYPED_SFIELD(sfBookDirectory, "BookDirectory", UINT256, 16);
|
||||
@@ -237,6 +238,7 @@ CONSTRUCT_TYPED_SFIELD(sfURITokenID, "URITokenID", UINT256,
|
||||
CONSTRUCT_TYPED_SFIELD(sfGovernanceFlags, "GovernanceFlags", UINT256, 99);
|
||||
CONSTRUCT_TYPED_SFIELD(sfGovernanceMarks, "GovernanceMarks", UINT256, 98);
|
||||
CONSTRUCT_TYPED_SFIELD(sfEmittedTxnID, "EmittedTxnID", UINT256, 97);
|
||||
CONSTRUCT_TYPED_SFIELD(sfHookCanEmit, "HookCanEmit", UINT256, 96);
|
||||
|
||||
// currency amount (common)
|
||||
CONSTRUCT_TYPED_SFIELD(sfAmount, "Amount", AMOUNT, 1);
|
||||
@@ -291,6 +293,8 @@ CONSTRUCT_TYPED_SFIELD(sfHookReturnString, "HookReturnString", VL,
|
||||
CONSTRUCT_TYPED_SFIELD(sfHookParameterName, "HookParameterName", VL, 24);
|
||||
CONSTRUCT_TYPED_SFIELD(sfHookParameterValue, "HookParameterValue", VL, 25);
|
||||
CONSTRUCT_TYPED_SFIELD(sfBlob, "Blob", VL, 26);
|
||||
CONSTRUCT_TYPED_SFIELD(sfRemarkValue, "RemarkValue", VL, 98);
|
||||
CONSTRUCT_TYPED_SFIELD(sfRemarkName, "RemarkName", VL, 99);
|
||||
|
||||
// account
|
||||
CONSTRUCT_TYPED_SFIELD(sfAccount, "Account", ACCOUNT, 1);
|
||||
@@ -345,6 +349,7 @@ CONSTRUCT_UNTYPED_SFIELD(sfHookExecution, "HookExecution", OBJECT,
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfHookDefinition, "HookDefinition", OBJECT, 22);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfHookParameter, "HookParameter", OBJECT, 23);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfHookGrant, "HookGrant", OBJECT, 24);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfRemark, "Remark", OBJECT, 97);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfGenesisMint, "GenesisMint", OBJECT, 96);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfActiveValidator, "ActiveValidator", OBJECT, 95);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfImportVLKey, "ImportVLKey", OBJECT, 94);
|
||||
@@ -371,6 +376,7 @@ CONSTRUCT_UNTYPED_SFIELD(sfDisabledValidators, "DisabledValidators", ARRAY,
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfHookExecutions, "HookExecutions", ARRAY, 18);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfHookParameters, "HookParameters", ARRAY, 19);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfHookGrants, "HookGrants", ARRAY, 20);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfRemarks, "Remarks", ARRAY, 97);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfGenesisMints, "GenesisMints", ARRAY, 96);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfActiveValidators, "ActiveValidators", ARRAY, 95);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfImportVLKeys, "ImportVLKeys", ARRAY, 94);
|
||||
|
||||
@@ -92,6 +92,8 @@ transResults()
|
||||
MAKE_ERROR(tecREQUIRES_FLAG, "The transaction or part-thereof requires a flag that wasn't set."),
|
||||
MAKE_ERROR(tecPRECISION_LOSS, "The amounts used by the transaction cannot interact."),
|
||||
MAKE_ERROR(tecINSUF_RESERVE_SELLER, "The seller of an object has insufficient reserves, and thus cannot complete the sale."),
|
||||
MAKE_ERROR(tecIMMUTABLE, "The remark is marked immutable on the object, and therefore cannot be updated."),
|
||||
MAKE_ERROR(tecTOO_MANY_REMARKS, "The number of remarks on the object would exceed the limit of 32."),
|
||||
MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."),
|
||||
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),
|
||||
MAKE_ERROR(tefBAD_AUTH, "Transaction's public key is not authorized."),
|
||||
|
||||
@@ -456,6 +456,14 @@ TxFormats::TxFormats()
|
||||
{sfTicketSequence, soeOPTIONAL},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::SetRemarks,
|
||||
ttREMARKS_SET,
|
||||
{
|
||||
{sfObjectID, soeREQUIRED},
|
||||
{sfRemarks, soeREQUIRED},
|
||||
},
|
||||
commonFields);
|
||||
}
|
||||
|
||||
TxFormats const&
|
||||
|
||||
@@ -78,6 +78,7 @@ JSS(GenesisMints);
|
||||
JSS(GovernanceMarks);
|
||||
JSS(GovernanceFlags);
|
||||
JSS(HookApiVersion); // field
|
||||
JSS(HookCanEmit); // field
|
||||
JSS(HookHash); // field
|
||||
JSS(HookNamespace); // field
|
||||
JSS(HookOn); // field
|
||||
@@ -121,6 +122,7 @@ JSS(Remit); // transaction type.
|
||||
JSS(RippleState); // ledger type.
|
||||
JSS(SLE_hit_rate); // out: GetCounts.
|
||||
JSS(SetFee); // transaction type.
|
||||
JSS(SetRemarks); // transaction type
|
||||
JSS(UNLModify); // transaction type.
|
||||
JSS(UNLReport); // transaction type.
|
||||
JSS(SettleDelay); // in: TransactionSign
|
||||
@@ -334,6 +336,8 @@ JSS(finished);
|
||||
JSS(fix_txns); // in: LedgerCleaner
|
||||
JSS(file);
|
||||
JSS(file_size);
|
||||
JSS(file_size_estimated_human);
|
||||
JSS(file_size_human);
|
||||
JSS(flags); // out: AccountOffers,
|
||||
// NetworkOPs
|
||||
JSS(force); // in: catalogue
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/app/ledger/Ledger.h>
|
||||
#include <ripple/app/ledger/LedgerMaster.h>
|
||||
#include <ripple/app/ledger/LedgerToJson.h>
|
||||
#include <ripple/app/main/Application.h>
|
||||
#include <ripple/app/tx/apply.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/basics/Slice.h>
|
||||
@@ -70,6 +72,25 @@ static constexpr uint16_t CATALOGUE_COMPRESS_LEVEL_MASK =
|
||||
static constexpr uint16_t CATALOGUE_RESERVED_MASK =
|
||||
0xF000; // Bits 12-15: reserved
|
||||
|
||||
std::string
|
||||
formatBytesIEC(uint64_t bytes, int precision = 2)
|
||||
{
|
||||
static const char* units[] = {"B", "KiB", "MiB", "GiB", "TiB", "PiB"};
|
||||
int unit_index = 0;
|
||||
auto size = static_cast<double>(bytes);
|
||||
|
||||
while (size >= 1024.0 && unit_index < 5)
|
||||
{
|
||||
size /= 1024.0;
|
||||
unit_index++;
|
||||
}
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << std::fixed << std::setprecision(precision) << size << " "
|
||||
<< units[unit_index];
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
// Helper functions for version field manipulation
|
||||
inline uint8_t
|
||||
getCatalogueVersion(uint16_t versionField)
|
||||
@@ -143,8 +164,9 @@ struct CatalogueRunStatus
|
||||
CatalogueJobType jobType;
|
||||
std::string filename;
|
||||
uint8_t compressionLevel = 0;
|
||||
std::string hash; // Hex-encoded hash
|
||||
uint64_t filesize = 0; // File size in bytes
|
||||
std::string hash; // Hex-encoded hash
|
||||
uint64_t filesize = 0; // File size in bytes
|
||||
std::string fileSizeEstimated = "unknown"; // Estimated file size
|
||||
};
|
||||
|
||||
// Global status for catalogue operations
|
||||
@@ -159,6 +181,131 @@ static CatalogueRunStatus catalogueRunStatus; // Always in memory
|
||||
catalogueRunStatus.field = value; \
|
||||
}
|
||||
|
||||
class ByteCounterFilter : public boost::iostreams::output_filter
|
||||
{
|
||||
private:
|
||||
uint64_t bytesWritten_;
|
||||
|
||||
public:
|
||||
ByteCounterFilter() : bytesWritten_(0)
|
||||
{
|
||||
}
|
||||
|
||||
template <typename Sink>
|
||||
bool
|
||||
put(Sink& sink, char c)
|
||||
{
|
||||
bool result = boost::iostreams::put(sink, c);
|
||||
if (result)
|
||||
bytesWritten_++;
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename Sink>
|
||||
std::streamsize
|
||||
write(Sink& sink, const char* data, std::streamsize n)
|
||||
{
|
||||
std::streamsize result = boost::iostreams::write(sink, data, n);
|
||||
if (result > 0)
|
||||
bytesWritten_ += result;
|
||||
return result;
|
||||
}
|
||||
|
||||
uint64_t
|
||||
getBytesWritten() const
|
||||
{
|
||||
return bytesWritten_;
|
||||
}
|
||||
void
|
||||
resetCounter()
|
||||
{
|
||||
bytesWritten_ = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Simple size predictor class
|
||||
class CatalogueSizePredictor
|
||||
{
|
||||
private:
|
||||
uint32_t minLedger_;
|
||||
uint32_t maxLedger_;
|
||||
uint64_t headerSize_;
|
||||
|
||||
// Keep track of actual bytes
|
||||
uint64_t totalBytesWritten_;
|
||||
uint64_t firstLedgerSize_;
|
||||
uint64_t processedLedgers_;
|
||||
std::deque<uint64_t> recentDeltas_;
|
||||
static constexpr size_t MAX_DELTAS = 10;
|
||||
|
||||
public:
|
||||
CatalogueSizePredictor(
|
||||
uint32_t minLedger,
|
||||
uint32_t maxLedger,
|
||||
uint64_t headerSize)
|
||||
: minLedger_(minLedger)
|
||||
, maxLedger_(maxLedger)
|
||||
, headerSize_(headerSize)
|
||||
, processedLedgers_(0)
|
||||
, totalBytesWritten_(headerSize)
|
||||
, firstLedgerSize_(0)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
addLedger(uint32_t seq, uint64_t bytes)
|
||||
{
|
||||
totalBytesWritten_ += bytes;
|
||||
processedLedgers_++;
|
||||
|
||||
if (seq == minLedger_)
|
||||
{
|
||||
firstLedgerSize_ = bytes;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Track recent deltas
|
||||
recentDeltas_.push_back(bytes);
|
||||
if (recentDeltas_.size() > MAX_DELTAS)
|
||||
recentDeltas_.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
// Get current size estimate
|
||||
uint64_t
|
||||
getEstimate() const
|
||||
{
|
||||
if (recentDeltas_.empty())
|
||||
{
|
||||
return totalBytesWritten_;
|
||||
}
|
||||
|
||||
uint64_t totalDeltaSize = 0;
|
||||
for (auto size : recentDeltas_)
|
||||
totalDeltaSize += size;
|
||||
|
||||
uint64_t avgDelta = totalDeltaSize / recentDeltas_.size();
|
||||
|
||||
uint32_t totalLedgers = maxLedger_ - minLedger_ + 1;
|
||||
uint32_t remainingLedgers = (totalLedgers >= processedLedgers_)
|
||||
? (totalLedgers - processedLedgers_)
|
||||
: 0;
|
||||
|
||||
return totalBytesWritten_ + avgDelta * remainingLedgers;
|
||||
}
|
||||
|
||||
std::string
|
||||
getEstimateHuman() const
|
||||
{
|
||||
auto bytes = getEstimate();
|
||||
if (bytes == totalBytesWritten_)
|
||||
return totalBytesWritten_ == 0
|
||||
? "unknown"
|
||||
: formatBytesIEC(totalBytesWritten_) + "+";
|
||||
return formatBytesIEC(bytes);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to generate status JSON
|
||||
// IMPORTANT: Caller must hold at least a shared (read) lock on
|
||||
// catalogueStatusMutex before calling this function
|
||||
@@ -289,9 +436,16 @@ generateStatusJson(bool includeErrorInfo = false)
|
||||
// Add filesize if available
|
||||
if (catalogueRunStatus.filesize > 0)
|
||||
{
|
||||
jvResult[jss::file_size] = Json::UInt(catalogueRunStatus.filesize);
|
||||
jvResult[jss::file_size_human] =
|
||||
formatBytesIEC(catalogueRunStatus.filesize);
|
||||
jvResult[jss::file_size] =
|
||||
std::to_string(catalogueRunStatus.filesize);
|
||||
}
|
||||
|
||||
// Add estimated filesize ("unknown" if not available)
|
||||
jvResult[jss::file_size_estimated_human] =
|
||||
catalogueRunStatus.fileSizeEstimated;
|
||||
|
||||
if (includeErrorInfo)
|
||||
{
|
||||
jvResult[jss::error] = "busy";
|
||||
@@ -470,6 +624,10 @@ doCatalogueCreate(RPC::JsonContext& context)
|
||||
JLOG(context.j.info())
|
||||
<< "No compression (level 0), using direct output";
|
||||
}
|
||||
|
||||
ByteCounterFilter byteCounter;
|
||||
compStream->push(boost::ref(byteCounter));
|
||||
|
||||
compStream->push(boost::ref(outfile));
|
||||
|
||||
// Process ledgers with local processor implementation
|
||||
@@ -484,14 +642,18 @@ doCatalogueCreate(RPC::JsonContext& context)
|
||||
return true;
|
||||
};
|
||||
|
||||
CatalogueSizePredictor predictor(
|
||||
header.min_ledger, header.max_ledger, sizeof(CATLHeader));
|
||||
|
||||
// Modified outputLedger to work with individual ledgers instead of a vector
|
||||
auto outputLedger =
|
||||
[&writeToFile, &context, &compStream](
|
||||
[&writeToFile, &context, &compStream, &predictor, &byteCounter](
|
||||
std::shared_ptr<Ledger const> ledger,
|
||||
std::optional<std::reference_wrapper<const SHAMap>> prevStateMap =
|
||||
std::nullopt) -> bool {
|
||||
try
|
||||
{
|
||||
byteCounter.resetCounter();
|
||||
auto const& info = ledger->info();
|
||||
|
||||
uint64_t closeTime = info.closeTime.time_since_epoch().count();
|
||||
@@ -521,6 +683,8 @@ doCatalogueCreate(RPC::JsonContext& context)
|
||||
size_t txNodesWritten =
|
||||
ledger->txMap().serializeToStream(*compStream);
|
||||
|
||||
predictor.addLedger(info.seq, byteCounter.getBytesWritten());
|
||||
|
||||
JLOG(context.j.info()) << "Ledger " << info.seq << ": Wrote "
|
||||
<< stateNodesWritten << " state nodes, "
|
||||
<< "and " << txNodesWritten << " tx nodes";
|
||||
@@ -549,9 +713,8 @@ doCatalogueCreate(RPC::JsonContext& context)
|
||||
UPDATE_CATALOGUE_STATUS(ledgerUpto, min_ledger);
|
||||
|
||||
// Load the first ledger
|
||||
auto status = RPC::getLedger(currLedger, min_ledger, context);
|
||||
if (status.toErrorCode() != rpcSUCCESS)
|
||||
return rpcError(status);
|
||||
if (auto error = RPC::getLedger(currLedger, min_ledger, context))
|
||||
return rpcError(error.toErrorCode(), error.message());
|
||||
if (!currLedger)
|
||||
return rpcError(rpcLEDGER_MISSING);
|
||||
|
||||
@@ -575,9 +738,8 @@ doCatalogueCreate(RPC::JsonContext& context)
|
||||
|
||||
// Load the next ledger
|
||||
currLedger = nullptr; // Release any previous current ledger
|
||||
auto status = RPC::getLedger(currLedger, ledger_seq, context);
|
||||
if (status.toErrorCode() != rpcSUCCESS)
|
||||
return rpcError(status);
|
||||
if (auto error = RPC::getLedger(currLedger, ledger_seq, context))
|
||||
return rpcError(error.toErrorCode(), error.message());
|
||||
if (!currLedger)
|
||||
return rpcError(rpcLEDGER_MISSING);
|
||||
|
||||
@@ -586,6 +748,9 @@ doCatalogueCreate(RPC::JsonContext& context)
|
||||
return rpcError(
|
||||
rpcINTERNAL, "Error occurred while processing ledgers");
|
||||
|
||||
UPDATE_CATALOGUE_STATUS(
|
||||
fileSizeEstimated, predictor.getEstimateHuman());
|
||||
|
||||
ledgers_written++;
|
||||
|
||||
// Cycle the ledgers: current becomes previous, we'll load a new current
|
||||
@@ -699,7 +864,8 @@ doCatalogueCreate(RPC::JsonContext& context)
|
||||
jvResult[jss::min_ledger] = min_ledger;
|
||||
jvResult[jss::max_ledger] = max_ledger;
|
||||
jvResult[jss::output_file] = filepath;
|
||||
jvResult[jss::file_size] = Json::UInt(file_size);
|
||||
jvResult[jss::file_size_human] = formatBytesIEC(file_size);
|
||||
jvResult[jss::file_size] = std::to_string(file_size);
|
||||
jvResult[jss::ledgers_written] = static_cast<Json::UInt>(ledgers_written);
|
||||
jvResult[jss::status] = jss::success;
|
||||
jvResult[jss::compression_level] = compressionLevel;
|
||||
@@ -1129,7 +1295,8 @@ doCatalogueLoad(RPC::JsonContext& context)
|
||||
jvResult[jss::ledger_count] =
|
||||
static_cast<Json::UInt>(header.max_ledger - header.min_ledger + 1);
|
||||
jvResult[jss::ledgers_loaded] = static_cast<Json::UInt>(ledgersLoaded);
|
||||
jvResult[jss::file_size] = Json::UInt(file_size);
|
||||
jvResult[jss::file_size_human] = formatBytesIEC(file_size);
|
||||
jvResult[jss::file_size] = std::to_string(file_size);
|
||||
jvResult[jss::status] = jss::success;
|
||||
jvResult[jss::compression_level] = compressionLevel;
|
||||
jvResult[jss::hash] = hash_hex;
|
||||
|
||||
@@ -186,6 +186,27 @@ struct ClaimReward_test : public beast::unit_test::suite
|
||||
ter(temINVALID_FLAG));
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
for (bool const withFixFlags : {false, true})
|
||||
{
|
||||
auto const amend =
|
||||
withFixFlags ? features : features - fixRewardClaimFlags;
|
||||
Env env{*this, amend};
|
||||
|
||||
auto const alice = Account("alice");
|
||||
auto const issuer = Account("issuer");
|
||||
|
||||
env.fund(XRP(1000), alice, issuer);
|
||||
env.close();
|
||||
|
||||
auto tx = reward::claim(alice);
|
||||
env(tx,
|
||||
reward::issuer(issuer),
|
||||
txflags(tfFullyCanonicalSig),
|
||||
withFixFlags ? ter(tesSUCCESS) : ter(temINVALID_FLAG));
|
||||
env.close();
|
||||
}
|
||||
}
|
||||
|
||||
// temMALFORMED
|
||||
// Issuer cannot be the source account.
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <ripple/json/json_writer.h>
|
||||
#include <ripple/protocol/TxFlags.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <test/app/Import_json.h>
|
||||
#include <test/app/SetHook_wasm.h>
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/hook.h>
|
||||
@@ -69,10 +70,10 @@ using JSSMap =
|
||||
}
|
||||
|
||||
#define HASH_WASM(x) \
|
||||
uint256 const x##_hash = \
|
||||
[[maybe_unused]] uint256 const x##_hash = \
|
||||
ripple::sha512Half_s(ripple::Slice(x##_wasm.data(), x##_wasm.size())); \
|
||||
std::string const x##_hash_str = to_string(x##_hash); \
|
||||
Keylet const x##_keylet = keylet::hookDefinition(x##_hash);
|
||||
[[maybe_unused]] std::string const x##_hash_str = to_string(x##_hash); \
|
||||
[[maybe_unused]] Keylet const x##_keylet = keylet::hookDefinition(x##_hash);
|
||||
|
||||
class SetHook_test : public beast::unit_test::suite
|
||||
{
|
||||
@@ -643,6 +644,9 @@ public:
|
||||
using namespace jtx;
|
||||
Env env{*this, features};
|
||||
|
||||
bool const hasHookCanEmit =
|
||||
env.current()->rules().enabled(featureHookCanEmit);
|
||||
|
||||
auto const alice = Account{"alice"};
|
||||
env.fund(XRP(10000), alice);
|
||||
|
||||
@@ -677,24 +681,30 @@ public:
|
||||
env.close();
|
||||
}
|
||||
|
||||
// grants, parameters, hookon, hookapiversion, hooknamespace keys must
|
||||
// be absent
|
||||
// grants, parameters, hookon, hookcanemit, hookapiversion,
|
||||
// hooknamespace keys must be absent
|
||||
for (auto const& [key, value] : JSSMap{
|
||||
{jss::HookGrants, Json::arrayValue},
|
||||
{jss::HookParameters, Json::arrayValue},
|
||||
{jss::HookOn,
|
||||
"000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000"},
|
||||
{jss::HookCanEmit,
|
||||
"000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000"},
|
||||
{jss::HookApiVersion, "0"},
|
||||
{jss::HookNamespace, to_string(uint256{beast::zero})}})
|
||||
{
|
||||
if (!hasHookCanEmit && key == jss::HookCanEmit)
|
||||
continue;
|
||||
|
||||
Json::Value iv;
|
||||
iv[jss::CreateCode] = "";
|
||||
iv[key] = value;
|
||||
jv[jss::Hooks][0U][jss::Hook] = iv;
|
||||
env(jv,
|
||||
M("Hook DELETE operation cannot include: grants, params, "
|
||||
"hookon, apiversion, namespace"),
|
||||
"hookon, hookcanemit, apiversion, namespace"),
|
||||
HSFEE,
|
||||
ter(temMALFORMED));
|
||||
env.close();
|
||||
@@ -831,6 +841,8 @@ public:
|
||||
Env env{*this, features};
|
||||
|
||||
bool const fixNS = env.current()->rules().enabled(fixNSDelete);
|
||||
bool const hasHookCanEmit =
|
||||
env.current()->rules().enabled(featureHookCanEmit);
|
||||
|
||||
auto const alice = Account{"alice"};
|
||||
env.fund(XRP(10000), alice);
|
||||
@@ -850,9 +862,15 @@ public:
|
||||
{jss::HookOn,
|
||||
"000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000"},
|
||||
{jss::HookCanEmit,
|
||||
"000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000"},
|
||||
{jss::HookApiVersion, "0"},
|
||||
})
|
||||
{
|
||||
if (!hasHookCanEmit && key == jss::HookCanEmit)
|
||||
continue;
|
||||
|
||||
Json::Value iv;
|
||||
iv[key] = value;
|
||||
iv[jss::Flags] = hsfNSDELETE;
|
||||
@@ -860,7 +878,7 @@ public:
|
||||
jv[jss::Hooks][0U][jss::Hook] = iv;
|
||||
env(jv,
|
||||
M("Hook NSDELETE operation cannot include: grants, params, "
|
||||
"hookon, apiversion"),
|
||||
"hookon, hookcanemit, apiversion"),
|
||||
HSFEE,
|
||||
ter(temMALFORMED));
|
||||
env.close();
|
||||
@@ -1193,6 +1211,9 @@ public:
|
||||
using namespace jtx;
|
||||
Env env{*this, features};
|
||||
|
||||
bool const hasHookCanEmit =
|
||||
env.current()->rules().enabled(featureHookCanEmit);
|
||||
|
||||
auto const bob = Account{"bob"};
|
||||
env.fund(XRP(10000), bob);
|
||||
|
||||
@@ -1236,6 +1257,10 @@ public:
|
||||
iv[jss::HookOn] =
|
||||
"00000000000000000000000000000000000000000000000000000000000000"
|
||||
"00";
|
||||
if (hasHookCanEmit)
|
||||
iv[jss::HookCanEmit] =
|
||||
"0000000000000000000000000000000000000000000000000000000000"
|
||||
"000000";
|
||||
jv[jss::Hooks][0U] = Json::Value{};
|
||||
jv[jss::Hooks][0U][jss::Hook] = iv;
|
||||
|
||||
@@ -1254,6 +1279,10 @@ public:
|
||||
iv[jss::HookOn] =
|
||||
"00000000000000000000000000000000000000000000000000000000000000"
|
||||
"00";
|
||||
if (hasHookCanEmit)
|
||||
iv[jss::HookCanEmit] =
|
||||
"0000000000000000000000000000000000000000000000000000000000"
|
||||
"000000";
|
||||
jv[jss::Hooks][0U] = Json::Value{};
|
||||
jv[jss::Hooks][0U][jss::Hook] = iv;
|
||||
|
||||
@@ -1273,6 +1302,10 @@ public:
|
||||
iv[jss::HookOn] =
|
||||
"00000000000000000000000000000000000000000000000000000000000000"
|
||||
"00";
|
||||
if (hasHookCanEmit)
|
||||
iv[jss::HookCanEmit] =
|
||||
"0000000000000000000000000000000000000000000000000000000000"
|
||||
"000000";
|
||||
jv[jss::Hooks][0U] = Json::Value{};
|
||||
jv[jss::Hooks][0U][jss::Hook] = iv;
|
||||
|
||||
@@ -1289,6 +1322,10 @@ public:
|
||||
iv[jss::CreateCode] = strHex(accept_wasm);
|
||||
iv[jss::HookNamespace] = to_string(uint256{beast::zero});
|
||||
iv[jss::HookApiVersion] = 0U;
|
||||
if (hasHookCanEmit)
|
||||
iv[jss::HookCanEmit] =
|
||||
"0000000000000000000000000000000000000000000000000000000000"
|
||||
"000000";
|
||||
jv[jss::Hooks][0U] = Json::Value{};
|
||||
jv[jss::Hooks][0U][jss::Hook] = iv;
|
||||
|
||||
@@ -1435,6 +1472,9 @@ public:
|
||||
using namespace jtx;
|
||||
Env env{*this, features};
|
||||
|
||||
bool const hasHookCanEmit =
|
||||
env.current()->rules().enabled(featureHookCanEmit);
|
||||
|
||||
auto const alice = Account{"alice"};
|
||||
env.fund(XRP(10000), alice);
|
||||
|
||||
@@ -1456,6 +1496,10 @@ public:
|
||||
iv[jss::HookOn] =
|
||||
"00000000000000000000000000000000000000000000000000000000000000"
|
||||
"00";
|
||||
if (hasHookCanEmit)
|
||||
iv[jss::HookCanEmit] =
|
||||
"0000000000000000000000000000000000000000000000000000000000"
|
||||
"000000";
|
||||
iv[jss::HookParameters] = Json::Value{Json::arrayValue};
|
||||
iv[jss::HookParameters][0U] = Json::Value{};
|
||||
iv[jss::HookParameters][0U][jss::HookParameter] = Json::Value{};
|
||||
@@ -1537,12 +1581,18 @@ public:
|
||||
{jss::HookOn,
|
||||
"00000000000000000000000000000000000000000000000000000000"
|
||||
"00000001"},
|
||||
{jss::HookCanEmit,
|
||||
"00000000000000000000000000000000000000000000000000000000"
|
||||
"00000001"},
|
||||
{jss::HookNamespace,
|
||||
"CAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFE"
|
||||
"CAFECAFE"},
|
||||
{jss::HookParameters, params},
|
||||
{jss::HookGrants, grants}})
|
||||
{
|
||||
if (!hasHookCanEmit && key == jss::HookCanEmit)
|
||||
continue;
|
||||
|
||||
Json::Value iv;
|
||||
iv[key] = value;
|
||||
jv[jss::Hooks][0U] = Json::Value{};
|
||||
@@ -1565,6 +1615,15 @@ public:
|
||||
BEAST_REQUIRE(hooks[0].isFieldPresent(sfHookOn));
|
||||
BEAST_EXPECT(hooks[0].getFieldH256(sfHookOn) == UINT256_BIT[0]);
|
||||
|
||||
if (hasHookCanEmit)
|
||||
{
|
||||
BEAST_REQUIRE(hooks[0].isFieldPresent(sfHookCanEmit));
|
||||
BEAST_EXPECT(
|
||||
hooks[0].getFieldH256(sfHookCanEmit) ==
|
||||
ripple::uint256("000000000000000000000000000000000000000000"
|
||||
"0000000000000000000001"));
|
||||
}
|
||||
|
||||
auto const ns = uint256::fromVoid(
|
||||
(std::array<uint8_t, 32>{
|
||||
0xCAU, 0xFEU, 0xCAU, 0xFEU, 0xCAU, 0xFEU, 0xCAU, 0xFEU,
|
||||
@@ -1596,14 +1655,20 @@ public:
|
||||
BEAST_REQUIRE(g[0].getFieldH256(sfHookHash) == accept_hash);
|
||||
}
|
||||
|
||||
// reset hookon and namespace to defaults
|
||||
// reset hookon, hookcanemit, and namespace to defaults
|
||||
{
|
||||
for (auto const& [key, value] : JSSMap{
|
||||
{jss::HookOn,
|
||||
"00000000000000000000000000000000000000000000000000000000"
|
||||
"00000000"},
|
||||
{jss::HookCanEmit,
|
||||
"00000000000000000000000000000000000000000000000000000000"
|
||||
"00000000"},
|
||||
{jss::HookNamespace, to_string(uint256{beast::zero})}})
|
||||
{
|
||||
if (key == jss::HookCanEmit && !hasHookCanEmit)
|
||||
continue;
|
||||
|
||||
Json::Value iv;
|
||||
iv[key] = value;
|
||||
jv[jss::Hooks][0U] = Json::Value{};
|
||||
@@ -1625,6 +1690,7 @@ public:
|
||||
// ensure the two fields are now absent (because they were reset to
|
||||
// the defaults on the hook def)
|
||||
BEAST_EXPECT(!hooks[0].isFieldPresent(sfHookOn));
|
||||
BEAST_EXPECT(!hooks[0].isFieldPresent(sfHookCanEmit));
|
||||
BEAST_EXPECT(!hooks[0].isFieldPresent(sfHookNamespace));
|
||||
}
|
||||
|
||||
@@ -1844,12 +1910,18 @@ public:
|
||||
{jss::HookOn,
|
||||
"00000000000000000000000000000000000000000000000000000000"
|
||||
"00000001"},
|
||||
{jss::HookCanEmit,
|
||||
"00000000000000000000000000000000000000000000000000000000"
|
||||
"00000001"},
|
||||
{jss::HookNamespace,
|
||||
"CAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFE"
|
||||
"CAFECAFE"},
|
||||
{jss::HookParameters, params},
|
||||
{jss::HookGrants, grants}})
|
||||
{
|
||||
if (key == jss::HookCanEmit && !hasHookCanEmit)
|
||||
continue;
|
||||
|
||||
Json::Value iv;
|
||||
iv[key] = value;
|
||||
jv[jss::Hooks][0U] = Json::Value{};
|
||||
@@ -2365,10 +2437,7 @@ public:
|
||||
{
|
||||
testcase("Test float_emit");
|
||||
using namespace jtx;
|
||||
Env env{
|
||||
*this, envconfig(), features, nullptr, beast::severities::kWarning
|
||||
// beast::severities::kTrace
|
||||
};
|
||||
Env env{*this, features};
|
||||
|
||||
auto const alice = Account{"alice"};
|
||||
auto const bob = Account{"bob"};
|
||||
@@ -6613,6 +6682,124 @@ public:
|
||||
BEAST_EXPECT(hookExecutions[1].getFieldU64(sfHookReturnCode) == 1);
|
||||
}
|
||||
|
||||
void
|
||||
test_xpop_slot(FeatureBitset features)
|
||||
{
|
||||
testcase("Test xpop_slot");
|
||||
using namespace jtx;
|
||||
std::vector<std::string> const keys = {
|
||||
"ED74D4036C6591A4BDF9C54CEFA39B996A5DCE5F86D11FDA1874481CE9D5A1CDC"
|
||||
"1"};
|
||||
Env env{*this, network::makeNetworkVLConfig(21337, keys)};
|
||||
|
||||
auto const master = Account("masterpassphrase");
|
||||
env(noop(master), fee(10'000'000'000), ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
env.fund(XRP(10000), alice);
|
||||
env.fund(XRP(10000), bob);
|
||||
|
||||
TestHook hook = wasm[R"[test.hook](
|
||||
#include <stdint.h>
|
||||
extern int32_t _g (uint32_t id, uint32_t maxiter);
|
||||
#define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1)
|
||||
extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
extern int64_t rollback (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
extern int64_t otxn_type(void);
|
||||
extern int64_t otxn_field(uint32_t, uint32_t, uint32_t);
|
||||
extern int64_t otxn_slot(uint32_t);
|
||||
extern int64_t slot(uint32_t, uint32_t, uint32_t);
|
||||
extern int64_t trace(uint32_t, uint32_t, uint32_t, uint32_t, uint32_t);
|
||||
extern int64_t xpop_slot(uint32_t, uint32_t);
|
||||
extern int64_t slot_subfield (
|
||||
uint32_t parent_slot,
|
||||
uint32_t field_id,
|
||||
uint32_t new_slot
|
||||
);
|
||||
#define ttIMPORT 97
|
||||
#define DOESNT_EXIST -5
|
||||
#define NO_FREE_SLOTS -6
|
||||
#define INVALID_ARGUMENT -7
|
||||
#define ALREADY_SET -8
|
||||
#define PREREQUISITE_NOT_MET -9
|
||||
#define INVALID_TXN -37
|
||||
#define ASSERT(x)\
|
||||
if (!(x))\
|
||||
rollback((uint32_t)#x, sizeof(#x), __LINE__);
|
||||
#define sfBlob ((7U << 16U) + 26U)
|
||||
#define sfAccount ((8U << 16U) + 1U)
|
||||
#define sfTransactionType ((1U << 16U) + 2U)
|
||||
#define sfHookExecutions ((15U << 16U) + 18U)
|
||||
#define sfTransactionResult ((16U << 16U) + 3U)
|
||||
#define sfAffectedNodes ((15U << 16U) + 8U)
|
||||
#define sfTransactionIndex ((2U << 16U) + 28U)
|
||||
int64_t hook(uint32_t r)
|
||||
{
|
||||
_g(1,1);
|
||||
// invalid tt
|
||||
if (otxn_type() != ttIMPORT)
|
||||
{
|
||||
ASSERT(xpop_slot(1, 2) == PREREQUISITE_NOT_MET);
|
||||
return accept(0,0,1);
|
||||
}
|
||||
|
||||
// invalid slotno
|
||||
ASSERT(xpop_slot(256, 1) == INVALID_ARGUMENT);
|
||||
ASSERT(xpop_slot(1, 256) == INVALID_ARGUMENT);
|
||||
ASSERT(xpop_slot(1, 1) == INVALID_ARGUMENT);
|
||||
|
||||
for (int i = 1; GUARD(255), i <= 255; ++i) {
|
||||
otxn_slot(i);
|
||||
}
|
||||
ASSERT(xpop_slot(0, 0) == NO_FREE_SLOTS);
|
||||
|
||||
ASSERT(xpop_slot(1, 11) == ((1 << 16) + 11));
|
||||
|
||||
ASSERT(slot_subfield(1, sfTransactionType, 2) == 2);
|
||||
ASSERT(slot_subfield(1, sfAccount, 3) == 3);
|
||||
|
||||
ASSERT(slot_subfield(11, sfTransactionIndex, 12) == 12);
|
||||
ASSERT(slot_subfield(11, sfAffectedNodes, 13) == 13);
|
||||
ASSERT(slot_subfield(11, sfTransactionResult, 14) == 14);
|
||||
|
||||
return accept(0,0,2);
|
||||
}
|
||||
)[test.hook]"];
|
||||
|
||||
// install the hook on alice
|
||||
env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0),
|
||||
M("set xpop_slot"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
|
||||
auto checkResult =
|
||||
[this](auto const& meta, uint64_t expectedCode) -> void {
|
||||
BEAST_REQUIRE(meta);
|
||||
BEAST_REQUIRE(meta->isFieldPresent(sfHookExecutions));
|
||||
auto const hookExecutions = meta->getFieldArray(sfHookExecutions);
|
||||
BEAST_REQUIRE(hookExecutions.size() == 1);
|
||||
BEAST_EXPECT(
|
||||
hookExecutions[0].getFieldU64(sfHookReturnCode) ==
|
||||
expectedCode);
|
||||
};
|
||||
|
||||
env(pay(bob, alice, XRP(1)), M("test xpop_slot"), fee(XRP(1)));
|
||||
env.close();
|
||||
auto meta = env.meta();
|
||||
checkResult(meta, 1);
|
||||
|
||||
// sfBlob is required and validity check is done in the Import
|
||||
// transaction.
|
||||
|
||||
auto const xpopJson = import::loadXpop(ImportTCAccountSet::w_seed);
|
||||
env(import::import(alice, xpopJson), M("test xpop_slot"), fee(XRP(1)));
|
||||
env.close();
|
||||
meta = env.meta();
|
||||
checkResult(meta, 2);
|
||||
}
|
||||
|
||||
void
|
||||
test_otxn_field(FeatureBitset features)
|
||||
{
|
||||
@@ -12000,6 +12187,551 @@ public:
|
||||
env(pay(bob, alice, XRP(1)), M("test util_verify"), fee(XRP(1)));
|
||||
}
|
||||
|
||||
void
|
||||
testHookCanEmit(FeatureBitset features)
|
||||
{
|
||||
testcase("test HookCanEmit");
|
||||
using namespace jtx;
|
||||
Env env{*this, features};
|
||||
|
||||
auto const caller = Account{"caller"};
|
||||
auto const alice = Account{"alice"};
|
||||
auto const bob = Account{"bob"};
|
||||
auto const charlie = Account{"charlie"};
|
||||
auto const hookacc = Account{"hookacc"};
|
||||
env.fund(XRP(10000), caller);
|
||||
env.fund(XRP(10000), alice);
|
||||
env.fund(XRP(10000), bob);
|
||||
env.fund(XRP(10000), charlie);
|
||||
env.fund(XRP(10000), hookacc);
|
||||
env.close();
|
||||
|
||||
TestHook hook = wasm[R"[test.hook](
|
||||
#include <stdint.h>
|
||||
extern int32_t _g(uint32_t, uint32_t);
|
||||
extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
extern int64_t rollback (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
extern int64_t emit (uint32_t, uint32_t, uint32_t, uint32_t);
|
||||
extern int64_t etxn_reserve(uint32_t);
|
||||
extern int64_t hook_account(uint32_t, uint32_t);
|
||||
extern int64_t otxn_field(uint32_t, uint32_t, uint32_t);
|
||||
extern int64_t hook_pos(void);
|
||||
#define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1)
|
||||
#define OUT_OF_BOUNDS (-1)
|
||||
#define ttPAYMENT 0
|
||||
#define ttACCOUNT_SET 3
|
||||
#define ttHOOK_SET 22
|
||||
#define tfCANONICAL 0x80000000UL
|
||||
#define amAMOUNT 1U
|
||||
#define amFEE 8U
|
||||
#define atACCOUNT 1U
|
||||
#define DOESNT_EXIST (-5)
|
||||
#define atDESTINATION 3U
|
||||
#define SBUF(x) (uint32_t)x,sizeof(x)
|
||||
#define sfAccount ((8U << 16U) + 1U)
|
||||
#define EMISSION_FAILURE -11
|
||||
|
||||
#define ASSERT(x)\
|
||||
if (!(x))\
|
||||
rollback((uint32_t)#x, sizeof(#x), __LINE__);
|
||||
|
||||
#define PREREQUISITE_NOT_MET -9
|
||||
#define ENCODE_DROPS_SIZE 9
|
||||
#define ENCODE_DROPS(buf_out, drops, amount_type ) \
|
||||
{\
|
||||
uint8_t uat = amount_type; \
|
||||
uint64_t udrops = drops; \
|
||||
buf_out[0] = 0x60U +(uat & 0x0FU ); \
|
||||
buf_out[1] = 0b01000000 + (( udrops >> 56 ) & 0b00111111 ); \
|
||||
buf_out[2] = (udrops >> 48) & 0xFFU; \
|
||||
buf_out[3] = (udrops >> 40) & 0xFFU; \
|
||||
buf_out[4] = (udrops >> 32) & 0xFFU; \
|
||||
buf_out[5] = (udrops >> 24) & 0xFFU; \
|
||||
buf_out[6] = (udrops >> 16) & 0xFFU; \
|
||||
buf_out[7] = (udrops >> 8) & 0xFFU; \
|
||||
buf_out[8] = (udrops >> 0) & 0xFFU; \
|
||||
buf_out += ENCODE_DROPS_SIZE; \
|
||||
}
|
||||
|
||||
#define _06_XX_ENCODE_DROPS(buf_out, drops, amount_type )\
|
||||
ENCODE_DROPS(buf_out, drops, amount_type );
|
||||
|
||||
#define ENCODE_DROPS_AMOUNT(buf_out, drops )\
|
||||
ENCODE_DROPS(buf_out, drops, amAMOUNT );
|
||||
#define _06_01_ENCODE_DROPS_AMOUNT(buf_out, drops )\
|
||||
ENCODE_DROPS_AMOUNT(buf_out, drops );
|
||||
|
||||
#define ENCODE_DROPS_FEE(buf_out, drops )\
|
||||
ENCODE_DROPS(buf_out, drops, amFEE );
|
||||
#define _06_08_ENCODE_DROPS_FEE(buf_out, drops )\
|
||||
ENCODE_DROPS_FEE(buf_out, drops );
|
||||
|
||||
#define ENCODE_TT_SIZE 3
|
||||
#define ENCODE_TT(buf_out, tt )\
|
||||
{\
|
||||
uint8_t utt = tt;\
|
||||
buf_out[0] = 0x12U;\
|
||||
buf_out[1] =(utt >> 8 ) & 0xFFU;\
|
||||
buf_out[2] =(utt >> 0 ) & 0xFFU;\
|
||||
buf_out += ENCODE_TT_SIZE; \
|
||||
}
|
||||
#define _01_02_ENCODE_TT(buf_out, tt)\
|
||||
ENCODE_TT(buf_out, tt);
|
||||
|
||||
|
||||
#define ENCODE_ACCOUNT_SIZE 22
|
||||
#define ENCODE_ACCOUNT(buf_out, account_id, account_type)\
|
||||
{\
|
||||
uint8_t uat = account_type;\
|
||||
buf_out[0] = 0x80U + uat;\
|
||||
buf_out[1] = 0x14U;\
|
||||
*(uint64_t*)(buf_out + 2) = *(uint64_t*)(account_id + 0);\
|
||||
*(uint64_t*)(buf_out + 10) = *(uint64_t*)(account_id + 8);\
|
||||
*(uint32_t*)(buf_out + 18) = *(uint32_t*)(account_id + 16);\
|
||||
buf_out += ENCODE_ACCOUNT_SIZE;\
|
||||
}
|
||||
#define _08_XX_ENCODE_ACCOUNT(buf_out, account_id, account_type)\
|
||||
ENCODE_ACCOUNT(buf_out, account_id, account_type);
|
||||
|
||||
#define ENCODE_ACCOUNT_SRC_SIZE 22
|
||||
#define ENCODE_ACCOUNT_SRC(buf_out, account_id)\
|
||||
ENCODE_ACCOUNT(buf_out, account_id, atACCOUNT);
|
||||
#define _08_01_ENCODE_ACCOUNT_SRC(buf_out, account_id)\
|
||||
ENCODE_ACCOUNT_SRC(buf_out, account_id);
|
||||
|
||||
#define ENCODE_ACCOUNT_DST_SIZE 22
|
||||
#define ENCODE_ACCOUNT_DST(buf_out, account_id)\
|
||||
ENCODE_ACCOUNT(buf_out, account_id, atDESTINATION);
|
||||
#define _08_03_ENCODE_ACCOUNT_DST(buf_out, account_id)\
|
||||
ENCODE_ACCOUNT_DST(buf_out, account_id);
|
||||
|
||||
#define ENCODE_ACCOUNT_OWNER_SIZE 22
|
||||
#define ENCODE_ACCOUNT_OWNER(buf_out, account_id) \
|
||||
ENCODE_ACCOUNT(buf_out, account_id, atOWNER);
|
||||
#define _08_02_ENCODE_ACCOUNT_OWNER(buf_out, account_id) \
|
||||
ENCODE_ACCOUNT_OWNER(buf_out, account_id);
|
||||
|
||||
#define ENCODE_UINT32_COMMON_SIZE 5U
|
||||
#define ENCODE_UINT32_COMMON(buf_out, i, field)\
|
||||
{\
|
||||
uint32_t ui = i; \
|
||||
uint8_t uf = field; \
|
||||
buf_out[0] = 0x20U +(uf & 0x0FU); \
|
||||
buf_out[1] =(ui >> 24 ) & 0xFFU; \
|
||||
buf_out[2] =(ui >> 16 ) & 0xFFU; \
|
||||
buf_out[3] =(ui >> 8 ) & 0xFFU; \
|
||||
buf_out[4] =(ui >> 0 ) & 0xFFU; \
|
||||
buf_out += ENCODE_UINT32_COMMON_SIZE; \
|
||||
}
|
||||
#define _02_XX_ENCODE_UINT32_COMMON(buf_out, i, field)\
|
||||
ENCODE_UINT32_COMMON(buf_out, i, field)\
|
||||
|
||||
#define ENCODE_UINT32_UNCOMMON_SIZE 6U
|
||||
#define ENCODE_UINT32_UNCOMMON(buf_out, i, field)\
|
||||
{\
|
||||
uint32_t ui = i; \
|
||||
uint8_t uf = field; \
|
||||
buf_out[0] = 0x20U; \
|
||||
buf_out[1] = uf; \
|
||||
buf_out[2] =(ui >> 24 ) & 0xFFU; \
|
||||
buf_out[3] =(ui >> 16 ) & 0xFFU; \
|
||||
buf_out[4] =(ui >> 8 ) & 0xFFU; \
|
||||
buf_out[5] =(ui >> 0 ) & 0xFFU; \
|
||||
buf_out += ENCODE_UINT32_UNCOMMON_SIZE; \
|
||||
}
|
||||
#define _02_XX_ENCODE_UINT32_UNCOMMON(buf_out, i, field)\
|
||||
ENCODE_UINT32_UNCOMMON(buf_out, i, field)\
|
||||
|
||||
#define ENCODE_LLS_SIZE 6U
|
||||
#define ENCODE_LLS(buf_out, lls )\
|
||||
ENCODE_UINT32_UNCOMMON(buf_out, lls, 0x1B );
|
||||
#define _02_27_ENCODE_LLS(buf_out, lls )\
|
||||
ENCODE_LLS(buf_out, lls );
|
||||
|
||||
#define ENCODE_FLS_SIZE 6U
|
||||
#define ENCODE_FLS(buf_out, fls )\
|
||||
ENCODE_UINT32_UNCOMMON(buf_out, fls, 0x1A );
|
||||
#define _02_26_ENCODE_FLS(buf_out, fls )\
|
||||
ENCODE_FLS(buf_out, fls );
|
||||
|
||||
#define ENCODE_TAG_SRC_SIZE 5
|
||||
#define ENCODE_TAG_SRC(buf_out, tag )\
|
||||
ENCODE_UINT32_COMMON(buf_out, tag, 0x3U );
|
||||
#define _02_03_ENCODE_TAG_SRC(buf_out, tag )\
|
||||
ENCODE_TAG_SRC(buf_out, tag );
|
||||
|
||||
#define ENCODE_TAG_DST_SIZE 5
|
||||
#define ENCODE_TAG_DST(buf_out, tag )\
|
||||
ENCODE_UINT32_COMMON(buf_out, tag, 0xEU );
|
||||
#define _02_14_ENCODE_TAG_DST(buf_out, tag )\
|
||||
ENCODE_TAG_DST(buf_out, tag );
|
||||
|
||||
#define ENCODE_SEQUENCE_SIZE 5
|
||||
#define ENCODE_SEQUENCE(buf_out, sequence )\
|
||||
ENCODE_UINT32_COMMON(buf_out, sequence, 0x4U );
|
||||
#define _02_04_ENCODE_SEQUENCE(buf_out, sequence )\
|
||||
ENCODE_SEQUENCE(buf_out, sequence );
|
||||
|
||||
#define ENCODE_FLAGS_SIZE 5
|
||||
#define ENCODE_FLAGS(buf_out, tag )\
|
||||
ENCODE_UINT32_COMMON(buf_out, tag, 0x2U );
|
||||
#define _02_02_ENCODE_FLAGS(buf_out, tag )\
|
||||
ENCODE_FLAGS(buf_out, tag );
|
||||
|
||||
#define ENCODE_SIGNING_PUBKEY_SIZE 35
|
||||
#define ENCODE_SIGNING_PUBKEY(buf_out, pkey )\
|
||||
{\
|
||||
buf_out[0] = 0x73U;\
|
||||
buf_out[1] = 0x21U;\
|
||||
*(uint64_t*)(buf_out + 2) = *(uint64_t*)(pkey + 0);\
|
||||
*(uint64_t*)(buf_out + 10) = *(uint64_t*)(pkey + 8);\
|
||||
*(uint64_t*)(buf_out + 18) = *(uint64_t*)(pkey + 16);\
|
||||
*(uint64_t*)(buf_out + 26) = *(uint64_t*)(pkey + 24);\
|
||||
buf[34] = pkey[32];\
|
||||
buf_out += ENCODE_SIGNING_PUBKEY_SIZE;\
|
||||
}
|
||||
|
||||
#define _07_03_ENCODE_SIGNING_PUBKEY(buf_out, pkey )\
|
||||
ENCODE_SIGNING_PUBKEY(buf_out, pkey );
|
||||
|
||||
#define ENCODE_SIGNING_PUBKEY_NULL_SIZE 35
|
||||
#define ENCODE_SIGNING_PUBKEY_NULL(buf_out )\
|
||||
{\
|
||||
buf_out[0] = 0x73U;\
|
||||
buf_out[1] = 0x21U;\
|
||||
*(uint64_t*)(buf_out+2) = 0;\
|
||||
*(uint64_t*)(buf_out+10) = 0;\
|
||||
*(uint64_t*)(buf_out+18) = 0;\
|
||||
*(uint64_t*)(buf_out+25) = 0;\
|
||||
buf_out += ENCODE_SIGNING_PUBKEY_NULL_SIZE;\
|
||||
}
|
||||
|
||||
#define _07_03_ENCODE_SIGNING_PUBKEY_NULL(buf_out )\
|
||||
ENCODE_SIGNING_PUBKEY_NULL(buf_out );
|
||||
|
||||
extern int64_t etxn_fee_base (
|
||||
uint32_t read_ptr,
|
||||
uint32_t read_len
|
||||
);
|
||||
extern int64_t etxn_details (
|
||||
uint32_t write_ptr,
|
||||
uint32_t write_len
|
||||
);
|
||||
extern int64_t ledger_seq (void);
|
||||
|
||||
#define PREPARE_PAYMENT_SIMPLE_SIZE 248U
|
||||
#define PREPARE_PAYMENT_SIMPLE(buf_out_master, drops_amount_raw, to_address, dest_tag_raw, src_tag_raw)\
|
||||
{\
|
||||
uint8_t* buf_out = buf_out_master;\
|
||||
uint8_t acc[20];\
|
||||
uint64_t drops_amount = (drops_amount_raw);\
|
||||
uint32_t dest_tag = (dest_tag_raw);\
|
||||
uint32_t src_tag = (src_tag_raw);\
|
||||
uint32_t cls = (uint32_t)ledger_seq();\
|
||||
hook_account(SBUF(acc));\
|
||||
_01_02_ENCODE_TT (buf_out, ttPAYMENT ); /* uint16 | size 3 */ \
|
||||
_02_02_ENCODE_FLAGS (buf_out, tfCANONICAL ); /* uint32 | size 5 */ \
|
||||
_02_03_ENCODE_TAG_SRC (buf_out, src_tag ); /* uint32 | size 5 */ \
|
||||
_02_04_ENCODE_SEQUENCE (buf_out, 0 ); /* uint32 | size 5 */ \
|
||||
_02_14_ENCODE_TAG_DST (buf_out, dest_tag ); /* uint32 | size 5 */ \
|
||||
_02_26_ENCODE_FLS (buf_out, cls + 1 ); /* uint32 | size 6 */ \
|
||||
_02_27_ENCODE_LLS (buf_out, cls + 5 ); /* uint32 | size 6 */ \
|
||||
_06_01_ENCODE_DROPS_AMOUNT (buf_out, drops_amount ); /* amount | size 9 */ \
|
||||
uint8_t* fee_ptr = buf_out;\
|
||||
_06_08_ENCODE_DROPS_FEE (buf_out, 0 ); /* amount | size 9 */ \
|
||||
_07_03_ENCODE_SIGNING_PUBKEY_NULL (buf_out ); /* pk | size 35 */ \
|
||||
_08_01_ENCODE_ACCOUNT_SRC (buf_out, acc ); /* account | size 22 */ \
|
||||
_08_03_ENCODE_ACCOUNT_DST (buf_out, to_address ); /* account | size 22 */ \
|
||||
int64_t edlen = etxn_details((uint32_t)buf_out, PREPARE_PAYMENT_SIMPLE_SIZE); /* emitdet | size 116 */ \
|
||||
int64_t fee = etxn_fee_base(buf_out_master, PREPARE_PAYMENT_SIMPLE_SIZE); \
|
||||
_06_08_ENCODE_DROPS_FEE (fee_ptr, fee ); \
|
||||
}
|
||||
|
||||
#define PREPARE_ACCOUNT_SET_SIZE 207U
|
||||
#define PREPARE_ACCOUNT_SET(buf_out_master)\
|
||||
{\
|
||||
uint8_t* buf_out = buf_out_master;\
|
||||
uint8_t acc[20];\
|
||||
uint32_t cls = (uint32_t)ledger_seq();\
|
||||
hook_account(SBUF(acc));\
|
||||
_01_02_ENCODE_TT (buf_out, ttACCOUNT_SET ); /* uint16 | size 3 */ \
|
||||
_02_02_ENCODE_FLAGS (buf_out, tfCANONICAL ); /* uint32 | size 5 */ \
|
||||
_02_04_ENCODE_SEQUENCE (buf_out, 0 ); /* uint32 | size 5 */ \
|
||||
_02_26_ENCODE_FLS (buf_out, cls + 1 ); /* uint32 | size 6 */ \
|
||||
_02_27_ENCODE_LLS (buf_out, cls + 5 ); /* uint32 | size 6 */ \
|
||||
uint8_t* fee_ptr = buf_out;\
|
||||
_06_08_ENCODE_DROPS_FEE (buf_out, 0 ); /* amount | size 9 */ \
|
||||
_07_03_ENCODE_SIGNING_PUBKEY_NULL (buf_out ); /* pk | size 35 */ \
|
||||
_08_01_ENCODE_ACCOUNT_SRC (buf_out, acc ); /* account | size 22 */ \
|
||||
int64_t edlen = etxn_details((uint32_t)buf_out, PREPARE_ACCOUNT_SET_SIZE); /* emitdet | size 116 */ \
|
||||
int64_t fee = etxn_fee_base(buf_out_master, PREPARE_ACCOUNT_SET_SIZE); \
|
||||
_06_08_ENCODE_DROPS_FEE (fee_ptr, fee ); \
|
||||
}
|
||||
|
||||
#define PREPARE_HOOK_SET_SIZE 211U
|
||||
#define PREPARE_HOOK_SET(buf_out_master)\
|
||||
{\
|
||||
uint8_t* buf_out = buf_out_master;\
|
||||
uint8_t acc[20];\
|
||||
uint32_t cls = (uint32_t)ledger_seq();\
|
||||
hook_account(SBUF(acc));\
|
||||
_01_02_ENCODE_TT (buf_out, ttHOOK_SET ); /* uint16 | size 3 */ \
|
||||
_02_02_ENCODE_FLAGS (buf_out, tfCANONICAL ); /* uint32 | size 5 */ \
|
||||
_02_04_ENCODE_SEQUENCE (buf_out, 0 ); /* uint32 | size 5 */ \
|
||||
_02_26_ENCODE_FLS (buf_out, cls + 1 ); /* uint32 | size 6 */ \
|
||||
_02_27_ENCODE_LLS (buf_out, cls + 5 ); /* uint32 | size 6 */ \
|
||||
buf_out[0] = 0xFBU;\
|
||||
buf_out[1] = 0xEEU;\
|
||||
buf_out[2] = 0xE1U;\
|
||||
buf_out[3] = 0xF1U;\
|
||||
buf_out += 4;\
|
||||
uint8_t* fee_ptr = buf_out;\
|
||||
_06_08_ENCODE_DROPS_FEE (buf_out, 0 ); /* amount | size 9 */ \
|
||||
_07_03_ENCODE_SIGNING_PUBKEY_NULL (buf_out ); /* pk | size 35 */ \
|
||||
_08_01_ENCODE_ACCOUNT_SRC (buf_out, acc ); /* account | size 22 */ \
|
||||
int64_t edlen = etxn_details((uint32_t)buf_out, PREPARE_HOOK_SET_SIZE); /* emitdet | size 116 */ \
|
||||
int64_t fee = etxn_fee_base(buf_out_master, PREPARE_HOOK_SET_SIZE); \
|
||||
_06_08_ENCODE_DROPS_FEE (fee_ptr, fee ); \
|
||||
}
|
||||
|
||||
int64_t hook(uint32_t reserved)
|
||||
{
|
||||
_g(1,1);
|
||||
etxn_reserve(3);
|
||||
|
||||
int8_t otxn_acc[20];
|
||||
ASSERT(otxn_field(SBUF(otxn_acc), sfAccount) == 20);
|
||||
|
||||
uint8_t payment_tx[PREPARE_PAYMENT_SIMPLE_SIZE];
|
||||
PREPARE_PAYMENT_SIMPLE(payment_tx, 1000, otxn_acc, 0, 0);
|
||||
|
||||
uint8_t account_set_tx[PREPARE_ACCOUNT_SET_SIZE];
|
||||
PREPARE_ACCOUNT_SET(account_set_tx);
|
||||
|
||||
uint8_t hook_set_tx[PREPARE_HOOK_SET_SIZE];
|
||||
PREPARE_HOOK_SET(hook_set_tx);
|
||||
|
||||
uint8_t hash[32];
|
||||
if (hook_pos() == 0) {
|
||||
// default (hookcanemit not set)
|
||||
ASSERT(emit(SBUF(hash), SBUF(payment_tx)) == 32);
|
||||
ASSERT(emit(SBUF(hash), SBUF(account_set_tx)) == 32);
|
||||
ASSERT(emit(SBUF(hash), SBUF(hook_set_tx)) == 32);
|
||||
return accept(0, 0, hook_pos());
|
||||
}
|
||||
if (hook_pos() == 1) {
|
||||
// hookcanemit all low
|
||||
ASSERT(emit(SBUF(hash), SBUF(payment_tx)) == 32);
|
||||
ASSERT(emit(SBUF(hash), SBUF(account_set_tx)) == 32);
|
||||
ASSERT(emit(SBUF(hash), SBUF(hook_set_tx)) == EMISSION_FAILURE);
|
||||
return accept(0, 0, hook_pos());
|
||||
}
|
||||
if (hook_pos() == 2) {
|
||||
// hookcanemit all high
|
||||
ASSERT(emit(SBUF(hash), SBUF(payment_tx)) == EMISSION_FAILURE);
|
||||
ASSERT(emit(SBUF(hash), SBUF(account_set_tx)) == EMISSION_FAILURE);
|
||||
ASSERT(emit(SBUF(hash), SBUF(hook_set_tx)) == 32);
|
||||
return accept(0, 0, hook_pos());
|
||||
}
|
||||
}
|
||||
)[test.hook]"];
|
||||
|
||||
bool const hasFeature =
|
||||
env.current()->rules().enabled(featureHookCanEmit);
|
||||
|
||||
Json::Value jv;
|
||||
jv[jss::CreateCode] = "";
|
||||
jv[jss::Flags] = hsfOVERRIDE;
|
||||
|
||||
auto const deleteHook = [&env](Account const& account) {
|
||||
Json::Value jv;
|
||||
jv[jss::Account] = account.human();
|
||||
jv[jss::TransactionType] = jss::SetHook;
|
||||
jv[jss::Flags] = 0;
|
||||
jv[jss::Hooks] = Json::Value{Json::arrayValue};
|
||||
Json::Value iv;
|
||||
iv[jss::CreateCode] = "";
|
||||
iv[jss::Flags] = hsfOVERRIDE;
|
||||
jv[jss::Hooks][0U][jss::Hook] = iv;
|
||||
jv[jss::Hooks][1U][jss::Hook] = iv;
|
||||
jv[jss::Hooks][2U][jss::Hook] = iv;
|
||||
|
||||
env(jv, M("hook DELETE"), HSFEE);
|
||||
env.close();
|
||||
};
|
||||
|
||||
std::array<Account, 3> accounts{{alice, bob, charlie}};
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
auto const acc = accounts[i];
|
||||
// i=0: Create
|
||||
// i=1: Install
|
||||
// i=2: Update
|
||||
|
||||
if (i == 1)
|
||||
{
|
||||
Json::Value h = hso(hook, overrideFlag);
|
||||
env(ripple::test::jtx::hook(hookacc, {{h}}, 0),
|
||||
M("set hookcanemit"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
}
|
||||
else if (i == 2)
|
||||
{
|
||||
Json::Value h = hso(hook, overrideFlag);
|
||||
env(ripple::test::jtx::hook(acc, {{h}}, 0),
|
||||
M("set hookcanemit"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
}
|
||||
|
||||
{
|
||||
Json::Value h = hso(hook, overrideFlag);
|
||||
env(ripple::test::jtx::hook(acc, {{h}}, 0),
|
||||
M("set hookcanemit"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
|
||||
// invoke the hook
|
||||
env(pay(caller, acc, XRP(1)),
|
||||
M("test hookcanemit 1"),
|
||||
fee(XRP(1)));
|
||||
env.close();
|
||||
|
||||
auto meta = env.meta();
|
||||
|
||||
// ensure hook execution occured
|
||||
BEAST_REQUIRE(meta);
|
||||
BEAST_REQUIRE(meta->isFieldPresent(sfHookExecutions));
|
||||
|
||||
// ensure there was four hook executions
|
||||
auto const hookExecutions =
|
||||
meta->getFieldArray(sfHookExecutions);
|
||||
BEAST_REQUIRE(hookExecutions.size() == 1);
|
||||
|
||||
// get the data in the return code of the execution
|
||||
BEAST_EXPECT(
|
||||
hookExecutions[0].getFieldU64(sfHookReturnCode) == 0);
|
||||
if (i == 0 || i == 1)
|
||||
deleteHook(acc);
|
||||
}
|
||||
|
||||
{
|
||||
// same result with no-HookCanEmit
|
||||
Json::Value h = hso(hook, overrideFlag);
|
||||
h[jss::HookCanEmit] =
|
||||
"0000000000000000000000000000000000000000000000000000000000"
|
||||
"400000";
|
||||
env(ripple::test::jtx::hook(acc, {{h}}, 0),
|
||||
M("set hookcanemit"),
|
||||
HSFEE,
|
||||
hasFeature ? ter(tesSUCCESS) : ter(temDISABLED));
|
||||
env.close();
|
||||
|
||||
if (hasFeature)
|
||||
{
|
||||
// invoke the hook
|
||||
env(pay(caller, acc, XRP(1)),
|
||||
M("test hookcanemit 1"),
|
||||
fee(XRP(1)));
|
||||
env.close();
|
||||
|
||||
auto meta = env.meta();
|
||||
|
||||
// ensure hook execution occured
|
||||
BEAST_REQUIRE(meta);
|
||||
BEAST_REQUIRE(meta->isFieldPresent(sfHookExecutions));
|
||||
|
||||
// ensure there was four hook executions
|
||||
auto const hookExecutions =
|
||||
meta->getFieldArray(sfHookExecutions);
|
||||
BEAST_REQUIRE(hookExecutions.size() == 1);
|
||||
|
||||
// get the data in the return code of the execution
|
||||
BEAST_EXPECT(
|
||||
hookExecutions[0].getFieldU64(sfHookReturnCode) == 0);
|
||||
if (i == 0 || i == 1)
|
||||
deleteHook(acc);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// install the hook on acc
|
||||
Json::Value hookCanEmitHook = hso(hook, overrideFlag);
|
||||
hookCanEmitHook[jss::HookCanEmit] =
|
||||
"00000000000000000000000000000000000000000000000000"
|
||||
"00000000000000";
|
||||
env(ripple::test::jtx::hook(acc, {{jv, hookCanEmitHook}}, 0),
|
||||
M("test hookcanemit"),
|
||||
HSFEE,
|
||||
hasFeature ? ter(tesSUCCESS) : ter(temDISABLED));
|
||||
env.close();
|
||||
|
||||
if (!hasFeature)
|
||||
continue;
|
||||
|
||||
// invoke the hook
|
||||
env(pay(caller, acc, XRP(1)),
|
||||
M("test hookcanemit 2"),
|
||||
fee(XRP(1)));
|
||||
env.close();
|
||||
|
||||
auto meta = env.meta();
|
||||
|
||||
// ensure hook execution occured
|
||||
BEAST_REQUIRE(meta);
|
||||
BEAST_REQUIRE(meta->isFieldPresent(sfHookExecutions));
|
||||
|
||||
// ensure there was four hook executions
|
||||
auto const hookExecutions =
|
||||
meta->getFieldArray(sfHookExecutions);
|
||||
BEAST_REQUIRE(hookExecutions.size() == 1);
|
||||
|
||||
// get the data in the return code of the execution
|
||||
BEAST_EXPECT(
|
||||
hookExecutions[0].getFieldU64(sfHookReturnCode) == 1);
|
||||
if (i == 0 || i == 1)
|
||||
deleteHook(acc);
|
||||
}
|
||||
|
||||
{
|
||||
// install the hook on acc
|
||||
Json::Value hookCanEmitHook = hso(hook, overrideFlag);
|
||||
hookCanEmitHook[jss::HookCanEmit] =
|
||||
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
|
||||
"FFFFFFFFFFFFFF";
|
||||
env(ripple::test::jtx::hook(
|
||||
acc, {{jv, jv, hookCanEmitHook}}, 0),
|
||||
M("test hookcanemit 3"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
|
||||
// invoke the hook
|
||||
env(pay(caller, acc, XRP(1)),
|
||||
M("test hookcanemit 3"),
|
||||
fee(XRP(1)));
|
||||
env.close();
|
||||
|
||||
auto meta = env.meta();
|
||||
|
||||
// ensure hook execution occured
|
||||
BEAST_REQUIRE(meta);
|
||||
BEAST_REQUIRE(meta->isFieldPresent(sfHookExecutions));
|
||||
|
||||
// ensure there was four hook executions
|
||||
auto const hookExecutions =
|
||||
meta->getFieldArray(sfHookExecutions);
|
||||
BEAST_REQUIRE(hookExecutions.size() == 1);
|
||||
|
||||
// get the data in the return code of the execution
|
||||
BEAST_EXPECT(
|
||||
hookExecutions[0].getFieldU64(sfHookReturnCode) == 2);
|
||||
if (i == 0 || i == 1)
|
||||
deleteHook(acc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWithFeatures(FeatureBitset features)
|
||||
{
|
||||
@@ -12009,6 +12741,7 @@ public:
|
||||
testInferHookSetOperation();
|
||||
testParams(features);
|
||||
testGrants(features);
|
||||
testHookCanEmit(features);
|
||||
|
||||
testDelete(features);
|
||||
testInstall(features);
|
||||
@@ -12075,6 +12808,7 @@ public:
|
||||
test_ledger_seq(features); //
|
||||
|
||||
test_meta_slot(features); //
|
||||
test_xpop_slot(features); //
|
||||
|
||||
test_otxn_id(features); //
|
||||
test_otxn_slot(features); //
|
||||
@@ -12125,6 +12859,9 @@ public:
|
||||
testWithFeatures(sa - fixXahauV1 - fixXahauV2 - fixNSDelete);
|
||||
testWithFeatures(
|
||||
sa - fixXahauV1 - fixXahauV2 - fixNSDelete - fixPageCap);
|
||||
testWithFeatures(
|
||||
sa - fixXahauV1 - fixXahauV2 - fixNSDelete - fixPageCap -
|
||||
featureHookCanEmit);
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
591
src/test/app/SetRemarks_test.cpp
Normal file
591
src/test/app/SetRemarks_test.cpp
Normal file
@@ -0,0 +1,591 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 <ripple/core/ConfigSections.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
#include <ripple/protocol/TxFlags.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <sstream>
|
||||
#include <test/jtx.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
struct SetRemarks_test : public beast::unit_test::suite
|
||||
{
|
||||
// debugRemarks(env, keylet::account(alice).key);
|
||||
void
|
||||
debugRemarks(jtx::Env& env, uint256 const& id)
|
||||
{
|
||||
Json::Value params;
|
||||
params[jss::index] = strHex(id);
|
||||
auto const info = env.rpc("json", "ledger_entry", to_string(params));
|
||||
std::cout << "INFO: " << info << "\n";
|
||||
}
|
||||
|
||||
void
|
||||
validateRemarks(
|
||||
ReadView const& view,
|
||||
uint256 const& id,
|
||||
std::vector<jtx::remarks::remark> const& marks)
|
||||
{
|
||||
using namespace jtx;
|
||||
auto const slep = view.read(keylet::unchecked(id));
|
||||
if (slep && slep->isFieldPresent(sfRemarks))
|
||||
{
|
||||
auto const& remarksObj = slep->getFieldArray(sfRemarks);
|
||||
BEAST_EXPECT(remarksObj.size() == marks.size());
|
||||
for (int i = 0; i < marks.size(); ++i)
|
||||
{
|
||||
remarks::remark const expectedMark = marks[i];
|
||||
STObject const remark = remarksObj[i];
|
||||
|
||||
Blob name = remark.getFieldVL(sfRemarkName);
|
||||
// BEAST_EXPECT(expectedMark.name == name);
|
||||
|
||||
uint32_t flags = remark.isFieldPresent(sfFlags)
|
||||
? remark.getFieldU32(sfFlags)
|
||||
: 0;
|
||||
BEAST_EXPECT(expectedMark.flags == flags);
|
||||
|
||||
std::optional<Blob> val;
|
||||
if (remark.isFieldPresent(sfRemarkValue))
|
||||
val = remark.getFieldVL(sfRemarkValue);
|
||||
// BEAST_EXPECT(expectedMark.value == val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testEnabled(FeatureBitset features)
|
||||
{
|
||||
testcase("enabled");
|
||||
using namespace jtx;
|
||||
|
||||
// setup env
|
||||
auto const alice = Account("alice");
|
||||
auto const bob = Account("bob");
|
||||
|
||||
for (bool const withRemarks : {false, true})
|
||||
{
|
||||
// If the Remarks amendment is not enabled, you cannot add remarks
|
||||
auto const amend =
|
||||
withRemarks ? features : features - featureRemarks;
|
||||
Env env{*this, amend};
|
||||
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
std::vector<remarks::remark> marks = {
|
||||
{"CAFE", "DEADBEEF", 0},
|
||||
};
|
||||
auto const txResult =
|
||||
withRemarks ? ter(tesSUCCESS) : ter(temDISABLED);
|
||||
env(remarks::setRemarks(alice, keylet::account(alice).key, marks),
|
||||
fee(XRP(1)),
|
||||
txResult);
|
||||
env.close();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testPreflightInvalid(FeatureBitset features)
|
||||
{
|
||||
testcase("preflight invalid");
|
||||
using namespace jtx;
|
||||
|
||||
// setup env
|
||||
auto const alice = Account("alice");
|
||||
auto const bob = Account("bob");
|
||||
|
||||
Env env{*this, features};
|
||||
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// preflight
|
||||
|
||||
// temDISABLED
|
||||
// DA: testEnabled()
|
||||
|
||||
// temINVALID_FLAG: SetRemarks: Invalid flags set.
|
||||
{
|
||||
std::vector<remarks::remark> marks = {
|
||||
{"CAFE", "DEADBEEF", 0},
|
||||
};
|
||||
env(remarks::setRemarks(alice, keylet::account(alice).key, marks),
|
||||
txflags(tfClose),
|
||||
fee(XRP(1)),
|
||||
ter(temINVALID_FLAG));
|
||||
env.close();
|
||||
}
|
||||
// temMALFORMED: SetRemarks: Cannot set more than 32 remarks (or fewer
|
||||
// than 1) in a txn.
|
||||
{
|
||||
std::vector<remarks::remark> marks;
|
||||
for (int i = 0; i < 0; ++i)
|
||||
{
|
||||
marks.push_back({"CAFE", "DEADBEEF", 0});
|
||||
}
|
||||
env(remarks::setRemarks(alice, keylet::account(alice).key, marks),
|
||||
fee(XRP(1)),
|
||||
ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
// temMALFORMED: SetRemarks: Cannot set more than 32 remarks (or fewer
|
||||
// than 1) in a txn.
|
||||
{
|
||||
std::vector<remarks::remark> marks;
|
||||
for (int i = 0; i < 33; ++i)
|
||||
{
|
||||
marks.push_back({"CAFE", "DEADBEEF", 0});
|
||||
}
|
||||
env(remarks::setRemarks(alice, keylet::account(alice).key, marks),
|
||||
fee(XRP(1)),
|
||||
ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
// temMALFORMED: SetRemarks: contained non-sfRemark field.
|
||||
{
|
||||
std::vector<remarks::remark> marks = {
|
||||
{"CAFE", "DEADBEEF", 0},
|
||||
};
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = jss::SetRemarks;
|
||||
jv[jss::Account] = alice.human();
|
||||
jv[sfObjectID.jsonName] = strHex(keylet::account(alice).key);
|
||||
auto& ja = jv[sfRemarks.getJsonName()];
|
||||
for (std::size_t i = 0; i < 1; ++i)
|
||||
{
|
||||
ja[i][sfGenesisMint.jsonName] = Json::Value{};
|
||||
ja[i][sfGenesisMint.jsonName][jss::Amount] =
|
||||
STAmount(1).getJson(JsonOptions::none);
|
||||
ja[i][sfGenesisMint.jsonName][jss::Destination] = bob.human();
|
||||
}
|
||||
jv[sfRemarks.jsonName] = ja;
|
||||
env(jv, fee(XRP(1)), ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
// temMALFORMED: SetRemarks: duplicate RemarkName entry.
|
||||
{
|
||||
std::vector<remarks::remark> marks = {
|
||||
{"CAFE", "DEADBEEF", 0},
|
||||
{"CAFE", "DEADBEEF", 0},
|
||||
};
|
||||
env(remarks::setRemarks(alice, keylet::account(alice).key, marks),
|
||||
fee(XRP(1)),
|
||||
ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
// temMALFORMED: SetRemarks: RemarkName cannot be empty or larger than
|
||||
// 256 chars.
|
||||
{
|
||||
std::vector<remarks::remark> marks = {
|
||||
{"", "DEADBEEF", 0},
|
||||
};
|
||||
env(remarks::setRemarks(alice, keylet::account(alice).key, marks),
|
||||
fee(XRP(1)),
|
||||
ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
// temMALFORMED: SetRemarks: RemarkName cannot be empty or larger than
|
||||
// 256 chars.
|
||||
{
|
||||
std::string const name((256 * 2) + 1, 'A');
|
||||
std::vector<remarks::remark> marks = {
|
||||
{name, "DEADBEEF", 0},
|
||||
};
|
||||
env(remarks::setRemarks(alice, keylet::account(alice).key, marks),
|
||||
fee(XRP(1)),
|
||||
ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
// temMALFORMED: SetRemarks: Flags must be either tfImmutable or 0
|
||||
{
|
||||
std::vector<remarks::remark> marks = {
|
||||
{"CAFE", "DEADBEEF", 2},
|
||||
};
|
||||
env(remarks::setRemarks(alice, keylet::account(alice).key, marks),
|
||||
fee(XRP(1)),
|
||||
ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
// temMALFORMED: SetRemarks: A remark deletion cannot be marked
|
||||
// immutable.
|
||||
{
|
||||
std::vector<remarks::remark> marks = {
|
||||
{"CAFE", std::nullopt, 1},
|
||||
};
|
||||
env(remarks::setRemarks(alice, keylet::account(alice).key, marks),
|
||||
fee(XRP(1)),
|
||||
ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
// temMALFORMED: SetRemarks: RemarkValue cannot be empty or larger than
|
||||
// 256 chars.
|
||||
{
|
||||
std::vector<remarks::remark> marks = {
|
||||
{"CAFE", "", 0},
|
||||
};
|
||||
env(remarks::setRemarks(alice, keylet::account(alice).key, marks),
|
||||
fee(XRP(1)),
|
||||
ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
// temMALFORMED: SetRemarks: RemarkValue cannot be empty or larger than
|
||||
// 256 chars.
|
||||
{
|
||||
std::string const value((256 * 2) + 1, 'A');
|
||||
std::vector<remarks::remark> marks = {
|
||||
{"CAFE", value, 0},
|
||||
};
|
||||
env(remarks::setRemarks(alice, keylet::account(alice).key, marks),
|
||||
fee(XRP(1)),
|
||||
ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testPreclaimInvalid(FeatureBitset features)
|
||||
{
|
||||
testcase("preclaim invalid");
|
||||
using namespace jtx;
|
||||
|
||||
// setup env
|
||||
Env env{*this, features};
|
||||
auto const alice = Account("alice");
|
||||
auto const bob = Account("bob");
|
||||
auto const carol = Account("carol");
|
||||
env.memoize(carol);
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
|
||||
std::vector<remarks::remark> marks = {
|
||||
{"CAFE", "DEADBEEF", 0},
|
||||
};
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// preclaim
|
||||
|
||||
// temDISABLED
|
||||
// DA: testEnabled()
|
||||
|
||||
// terNO_ACCOUNT - account doesnt exist
|
||||
{
|
||||
auto const carol = Account("carol");
|
||||
env.memoize(carol);
|
||||
auto tx =
|
||||
remarks::setRemarks(carol, keylet::account(carol).key, marks);
|
||||
tx[jss::Sequence] = 0;
|
||||
env(tx, carol, fee(XRP(1)), ter(terNO_ACCOUNT));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// tecNO_TARGET - object doesnt exist
|
||||
{
|
||||
env(remarks::setRemarks(alice, keylet::account(carol).key, marks),
|
||||
fee(XRP(1)),
|
||||
ter(tecNO_TARGET));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// tecNO_PERMISSION: !issuer
|
||||
{
|
||||
env(deposit::auth(bob, alice));
|
||||
env(remarks::setRemarks(
|
||||
alice, keylet::depositPreauth(bob, alice).key, marks),
|
||||
fee(XRP(1)),
|
||||
ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
}
|
||||
// tecNO_PERMISSION: issuer != _account
|
||||
{
|
||||
env(remarks::setRemarks(alice, keylet::account(bob).key, marks),
|
||||
fee(XRP(1)),
|
||||
ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
}
|
||||
// tecIMMUTABLE: SetRemarks: attempt to mutate an immutable remark.
|
||||
{
|
||||
// alice creates immutable remark
|
||||
std::vector<remarks::remark> immutableMarks = {
|
||||
{"CAFF", "DEAD", tfImmutable},
|
||||
};
|
||||
env(remarks::setRemarks(
|
||||
alice, keylet::account(alice).key, immutableMarks),
|
||||
fee(XRP(1)),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
// alice cannot update immutable remark
|
||||
std::vector<remarks::remark> badMarks = {
|
||||
{"CAFF", "DEADBEEF", 0},
|
||||
};
|
||||
env(remarks::setRemarks(
|
||||
alice, keylet::account(alice).key, badMarks),
|
||||
fee(XRP(1)),
|
||||
ter(tecIMMUTABLE));
|
||||
env.close();
|
||||
}
|
||||
// tecCLAIM: SetRemarks: insane remarks accounting.
|
||||
{} // tecTOO_MANY_REMARKS: SetRemarks: an object may have at most 32
|
||||
// remarks.
|
||||
{
|
||||
std::vector<remarks::remark> _marks;
|
||||
unsigned int hexValue = 0xEFAC;
|
||||
for (int i = 0; i < 31; ++i)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << std::hex << std::uppercase << hexValue;
|
||||
_marks.push_back({ss.str(), "DEADBEEF", 0});
|
||||
hexValue++;
|
||||
}
|
||||
env(remarks::setRemarks(alice, keylet::account(alice).key, _marks),
|
||||
fee(XRP(1)),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
env(remarks::setRemarks(alice, keylet::account(alice).key, marks),
|
||||
fee(XRP(1)),
|
||||
ter(tecTOO_MANY_REMARKS));
|
||||
env.close();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testDoApplyInvalid(FeatureBitset features)
|
||||
{
|
||||
testcase("doApply invalid");
|
||||
using namespace jtx;
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// doApply
|
||||
|
||||
// terNO_ACCOUNT
|
||||
// tecNO_TARGET
|
||||
// tecNO_PERMISSION
|
||||
// tecTOO_MANY_REMARKS
|
||||
}
|
||||
|
||||
void
|
||||
testDelete(FeatureBitset features)
|
||||
{
|
||||
testcase("delete");
|
||||
using namespace jtx;
|
||||
|
||||
// setup env
|
||||
Env env{*this, features};
|
||||
auto const alice = Account("alice");
|
||||
auto const bob = Account("bob");
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
|
||||
auto const id = keylet::account(alice).key;
|
||||
|
||||
// Set Remarks
|
||||
{
|
||||
std::vector<remarks::remark> marks = {
|
||||
{"CAFE", "DEADBEEF", 0},
|
||||
};
|
||||
env(remarks::setRemarks(alice, id, marks), fee(XRP(1)));
|
||||
env.close();
|
||||
validateRemarks(*env.current(), id, marks);
|
||||
}
|
||||
|
||||
// Delete Remarks
|
||||
{
|
||||
std::vector<remarks::remark> marks = {
|
||||
{"CAFE", std::nullopt, 0},
|
||||
};
|
||||
env(remarks::setRemarks(alice, id, marks), fee(XRP(1)));
|
||||
env.close();
|
||||
validateRemarks(*env.current(), id, {});
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testLedgerObjects(FeatureBitset features)
|
||||
{
|
||||
testcase("ledger objects");
|
||||
using namespace jtx;
|
||||
|
||||
// setup env
|
||||
Env env{*this, features};
|
||||
auto const alice = Account("alice");
|
||||
auto const bob = Account("bob");
|
||||
auto const gw = Account("gw");
|
||||
auto const USD = gw["USD"];
|
||||
env.fund(XRP(10000), alice, bob, gw);
|
||||
env.close();
|
||||
env.trust(USD(10000), alice);
|
||||
env.trust(USD(10000), bob);
|
||||
env.close();
|
||||
env(pay(gw, alice, USD(1000)));
|
||||
env(pay(gw, bob, USD(1000)));
|
||||
env.close();
|
||||
|
||||
std::vector<remarks::remark> marks = {
|
||||
{"CAFE", "DEADBEEF", 0},
|
||||
};
|
||||
|
||||
// ltACCOUNT_ROOT
|
||||
{
|
||||
auto const id = keylet::account(alice).key;
|
||||
env(remarks::setRemarks(alice, id, marks), fee(XRP(1)));
|
||||
env.close();
|
||||
validateRemarks(*env.current(), id, marks);
|
||||
}
|
||||
// ltOFFER
|
||||
{
|
||||
auto const id = keylet::offer(alice, env.seq(alice)).key;
|
||||
env(offer(alice, XRP(10), USD(10)), fee(XRP(1)));
|
||||
env(remarks::setRemarks(alice, id, marks), fee(XRP(1)));
|
||||
env.close();
|
||||
validateRemarks(*env.current(), id, marks);
|
||||
}
|
||||
// ltESCROW
|
||||
{
|
||||
using namespace std::literals::chrono_literals;
|
||||
auto const id = keylet::escrow(alice, env.seq(alice)).key;
|
||||
env(escrow::create(alice, bob, XRP(10)),
|
||||
escrow::finish_time(env.now() + 1s),
|
||||
fee(XRP(1)));
|
||||
env(remarks::setRemarks(alice, id, marks), fee(XRP(1)));
|
||||
env.close();
|
||||
validateRemarks(*env.current(), id, marks);
|
||||
}
|
||||
// ltTICKET
|
||||
{
|
||||
auto const id = keylet::ticket(alice, env.seq(alice) + 1).key;
|
||||
env(ticket::create(alice, 10), fee(XRP(1)));
|
||||
env(remarks::setRemarks(alice, id, marks), fee(XRP(1)));
|
||||
env.close();
|
||||
validateRemarks(*env.current(), id, marks);
|
||||
}
|
||||
// ltPAYCHAN
|
||||
{
|
||||
using namespace std::literals::chrono_literals;
|
||||
auto const id = keylet::payChan(alice, bob, env.seq(alice)).key;
|
||||
auto const pk = alice.pk();
|
||||
auto const settleDelay = 100s;
|
||||
env(paychan::create(alice, bob, XRP(10), settleDelay, pk),
|
||||
fee(XRP(1)));
|
||||
env(remarks::setRemarks(alice, id, marks), fee(XRP(1)));
|
||||
env.close();
|
||||
validateRemarks(*env.current(), id, marks);
|
||||
}
|
||||
// ltCHECK
|
||||
{
|
||||
auto const id = keylet::check(alice, env.seq(alice)).key;
|
||||
env(check::create(alice, bob, XRP(10)), fee(XRP(1)));
|
||||
env(remarks::setRemarks(alice, id, marks), fee(XRP(1)));
|
||||
env.close();
|
||||
validateRemarks(*env.current(), id, marks);
|
||||
}
|
||||
// ltDEPOSIT_PREAUTH
|
||||
{
|
||||
env(fset(bob, asfDepositAuth));
|
||||
auto const id = keylet::depositPreauth(alice, bob).key;
|
||||
env(deposit::auth(alice, bob), fee(XRP(1)));
|
||||
env(remarks::setRemarks(alice, id, marks), fee(XRP(1)));
|
||||
env.close();
|
||||
validateRemarks(*env.current(), id, marks);
|
||||
}
|
||||
// ltURI_TOKEN
|
||||
{
|
||||
std::string const uri(256, 'A');
|
||||
auto const id =
|
||||
keylet::uritoken(alice, Blob(uri.begin(), uri.end())).key;
|
||||
env(uritoken::mint(alice, uri), fee(XRP(1)));
|
||||
env(remarks::setRemarks(alice, id, marks), fee(XRP(1)));
|
||||
env.close();
|
||||
validateRemarks(*env.current(), id, marks);
|
||||
}
|
||||
// ltRIPPLE_STATE: bal < 0
|
||||
{
|
||||
auto const alice2 = Account("alice2");
|
||||
env.fund(XRP(1000), alice2);
|
||||
env.close();
|
||||
env.trust(USD(10000), alice2);
|
||||
auto const id = keylet::line(alice2, USD).key;
|
||||
env(pay(gw, alice2, USD(1000)));
|
||||
env(remarks::setRemarks(gw, id, marks), fee(XRP(1)));
|
||||
env.close();
|
||||
validateRemarks(*env.current(), id, marks);
|
||||
}
|
||||
// ltRIPPLE_STATE: bal > 0
|
||||
{
|
||||
auto const carol0 = Account("carol0");
|
||||
env.fund(XRP(1000), carol0);
|
||||
env.close();
|
||||
env.trust(USD(10000), carol0);
|
||||
auto const id = keylet::line(carol0, USD).key;
|
||||
env(pay(gw, carol0, USD(1000)));
|
||||
env(remarks::setRemarks(gw, id, marks), fee(XRP(1)));
|
||||
env.close();
|
||||
validateRemarks(*env.current(), id, marks);
|
||||
}
|
||||
// ltRIPPLE_STATE: highReserve
|
||||
{
|
||||
auto const dan1 = Account("dan1");
|
||||
env.fund(XRP(1000), dan1);
|
||||
env.close();
|
||||
env.trust(USD(1000), dan1);
|
||||
auto const id = keylet::line(dan1, USD).key;
|
||||
env(remarks::setRemarks(gw, id, marks), fee(XRP(1)));
|
||||
env.close();
|
||||
validateRemarks(*env.current(), id, marks);
|
||||
}
|
||||
// ltRIPPLE_STATE: lowReserve
|
||||
{
|
||||
auto const bob0 = Account("bob0");
|
||||
env.fund(XRP(1000), bob0);
|
||||
env.close();
|
||||
env.trust(USD(1000), bob0);
|
||||
auto const id = keylet::line(bob0, USD).key;
|
||||
env(remarks::setRemarks(gw, id, marks), fee(XRP(1)));
|
||||
env.close();
|
||||
validateRemarks(*env.current(), id, marks);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWithFeats(FeatureBitset features)
|
||||
{
|
||||
testEnabled(features);
|
||||
testPreflightInvalid(features);
|
||||
testPreclaimInvalid(features);
|
||||
testDoApplyInvalid(features);
|
||||
testDelete(features);
|
||||
testLedgerObjects(features);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace test::jtx;
|
||||
auto const sa = supported_amendments();
|
||||
testWithFeats(sa);
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(SetRemarks, app, ripple);
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -1,4 +1,35 @@
|
||||
#!/bin/bash
|
||||
#!/bin/bash -u
|
||||
|
||||
# Generate the SetHook_wasm.h file from the SetHook_test.cpp file.
|
||||
#
|
||||
# Prerequisites:
|
||||
# - wasmcc:
|
||||
# https://github.com/wasienv/wasienv
|
||||
#
|
||||
# - hook-cleaner:
|
||||
# https://github.com/RichardAH/hook-cleaner-c
|
||||
#
|
||||
# - wat2wasm
|
||||
# https://github.com/WebAssembly/wabt
|
||||
#
|
||||
# - clang-format:
|
||||
# Ubuntu: $sudo apt-get install clang-format
|
||||
# macOS: $brew install clang-format
|
||||
#
|
||||
# - (macOS Only) GNU sed, grep:
|
||||
# $brew install gnu-sed grep
|
||||
# add path: PATH="/opt/homebrew/opt/gnu-sed/libexec/gnubin:$PATH"
|
||||
|
||||
set -e
|
||||
# Get the script directory (retrieving the correct path regardless of where it's executed from)
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "${SCRIPT_DIR}"
|
||||
# Set the project root directory
|
||||
WASM_DIR="generated/hook/c"
|
||||
INPUT_FILE="SetHook_test.cpp"
|
||||
OUTPUT_FILE="SetHook_wasm.h"
|
||||
|
||||
mkdir -p $WASM_DIR
|
||||
echo '
|
||||
//This file is generated by build_test_hooks.h
|
||||
#ifndef SETHOOK_WASM_INCLUDED
|
||||
@@ -9,54 +40,55 @@ echo '
|
||||
#include <vector>
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
std::map<std::string, std::vector<uint8_t>> wasm = {' > SetHook_wasm.h
|
||||
std::map<std::string, std::vector<uint8_t>> wasm = {' > $OUTPUT_FILE
|
||||
COUNTER="0"
|
||||
cat SetHook_test.cpp | tr '\n' '\f' |
|
||||
cat $INPUT_FILE | tr '\n' '\f' |
|
||||
grep -Po 'R"\[test\.hook\](.*?)\[test\.hook\]"' |
|
||||
sed -E 's/R"\[test\.hook\]\(//g' |
|
||||
sed -E 's/\)\[test\.hook\]"[\f \t]*/\/*end*\//g' |
|
||||
while read -r line
|
||||
do
|
||||
echo "/* ==== WASM: $COUNTER ==== */" >> SetHook_wasm.h
|
||||
echo -n '{ R"[test.hook](' >> SetHook_wasm.h
|
||||
cat <<< $line | sed -E 's/.{7}$//g' | tr -d '\n' | tr '\f' '\n' >> SetHook_wasm.h
|
||||
echo ')[test.hook]",' >> SetHook_wasm.h
|
||||
echo "{" >> SetHook_wasm.h
|
||||
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
|
||||
echo "{" >> $OUTPUT_FILE
|
||||
WAT=`grep -Eo '\(module' <<< $line | wc -l`
|
||||
if [ "$WAT" -eq "0" ]
|
||||
then
|
||||
echo '#include "api.h"' > /root/xrpld-hooks/hook/tests/hookapi/wasm/test-$COUNTER-gen.c
|
||||
tr '\f' '\n' <<< $line >> /root/xrpld-hooks/hook/tests/hookapi/wasm/test-$COUNTER-gen.c
|
||||
echo '#include "api.h"' > "$WASM_DIR/test-$COUNTER-gen.c"
|
||||
tr '\f' '\n' <<< $line >> "$WASM_DIR/test-$COUNTER-gen.c"
|
||||
DECLARED="`tr '\f' '\n' <<< $line | grep -E '(extern|define) ' | grep -Eo '[a-z\-\_]+ *\(' | grep -v 'sizeof' | sed -E 's/[^a-z\-\_]//g' | sort | uniq`"
|
||||
USED="`tr '\f' '\n' <<< $line | grep -vE '(extern|define) ' | grep -Eo '[a-z\-\_]+\(' | grep -v 'sizeof' | sed -E 's/[^a-z\-\_]//g' | grep -vE '^(hook|cbak)' | sort | uniq`"
|
||||
ONCE="`echo $DECLARED $USED | tr ' ' '\n' | sort | uniq -c | grep '1 ' | sed -E 's/^ *1 //g'`"
|
||||
FILTER="`echo $DECLARED | tr ' ' '|' | sed -E 's/|$//g'`"
|
||||
UNDECL=`echo "$ONCE" | grep -v -E "$FILTER"`
|
||||
FILTER="`echo $DECLARED | tr ' ' '|' | sed -E 's/\|$//g'`"
|
||||
UNDECL="`echo $ONCE | grep -v -E $FILTER 2>/dev/null || echo ''`"
|
||||
if [ ! -z "$UNDECL" ]
|
||||
then
|
||||
echo "Undeclared in $COUNTER: $UNDECL"
|
||||
echo "$line"
|
||||
exit 1
|
||||
fi
|
||||
wasmcc -x c /dev/stdin -o /dev/stdout -O2 -Wl,--allow-undefined <<< `tr '\f' '\n' <<< $line` |
|
||||
wasmcc -x c /dev/stdin -o /dev/stdout -O2 -Wl,--allow-undefined <<< "`tr '\f' '\n' <<< $line`" |
|
||||
hook-cleaner - - 2>/dev/null |
|
||||
xxd -p -u -c 19 |
|
||||
sed -E 's/../0x\0U,/g' | sed -E 's/^/ /g' >> SetHook_wasm.h
|
||||
xxd -p -u -c 10 |
|
||||
sed -E 's/../0x&U,/g' | sed -E 's/^/ /g' >> $OUTPUT_FILE
|
||||
else
|
||||
wat2wasm - -o /dev/stdout <<< `tr '\f' '\n' <<< $(sed -E 's/.{7}$//g' <<< $line)` |
|
||||
xxd -p -u -c 19 |
|
||||
sed -E 's/../0x\0U,/g' | sed -E 's/^/ /g' >> SetHook_wasm.h
|
||||
wat2wasm - -o /dev/stdout <<< "`tr '\f' '\n' <<< $(sed -E 's/.{7}$//g' <<< $line)`" |
|
||||
xxd -p -u -c 10 |
|
||||
sed -E 's/../0x&U,/g' | sed -E 's/^/ /g' >> $OUTPUT_FILE
|
||||
fi
|
||||
if [ "$?" -gt "0" ]
|
||||
then
|
||||
echo "Compilation error ^"
|
||||
exit 1
|
||||
fi
|
||||
echo '}},' >> SetHook_wasm.h
|
||||
echo >> SetHook_wasm.h
|
||||
echo '}},' >> $OUTPUT_FILE
|
||||
echo >> $OUTPUT_FILE
|
||||
COUNTER=`echo $COUNTER + 1 | bc`
|
||||
done
|
||||
echo '};
|
||||
}
|
||||
}
|
||||
#endif' >> SetHook_wasm.h
|
||||
#endif' >> $OUTPUT_FILE
|
||||
clang-format -i $OUTPUT_FILE
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
#include <test/jtx/quality.h>
|
||||
#include <test/jtx/rate.h>
|
||||
#include <test/jtx/regkey.h>
|
||||
#include <test/jtx/remarks.h>
|
||||
#include <test/jtx/remit.h>
|
||||
#include <test/jtx/require.h>
|
||||
#include <test/jtx/requires.h>
|
||||
|
||||
56
src/test/jtx/impl/remarks.cpp
Normal file
56
src/test/jtx/impl/remarks.cpp
Normal file
@@ -0,0 +1,56 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2023 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 <ripple/protocol/jss.h>
|
||||
#include <test/jtx/remarks.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace jtx {
|
||||
namespace remarks {
|
||||
|
||||
Json::Value
|
||||
setRemarks(
|
||||
jtx::Account const& account,
|
||||
uint256 const& id,
|
||||
std::vector<remark> const& marks)
|
||||
{
|
||||
using namespace jtx;
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = jss::SetRemarks;
|
||||
jv[jss::Account] = account.human();
|
||||
jv[sfObjectID.jsonName] = strHex(id);
|
||||
auto& ja = jv[sfRemarks.getJsonName()];
|
||||
for (std::size_t i = 0; i < marks.size(); ++i)
|
||||
{
|
||||
ja[i][sfRemark.jsonName] = Json::Value{};
|
||||
ja[i][sfRemark.jsonName][sfRemarkName.jsonName] = marks[i].name;
|
||||
if (marks[i].value)
|
||||
ja[i][sfRemark.jsonName][sfRemarkValue.jsonName] = *marks[i].value;
|
||||
if (marks[i].flags)
|
||||
ja[i][sfRemark.jsonName][sfFlags.jsonName] = *marks[i].flags;
|
||||
}
|
||||
jv[sfRemarks.jsonName] = ja;
|
||||
return jv;
|
||||
}
|
||||
|
||||
} // namespace remarks
|
||||
} // namespace jtx
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
64
src/test/jtx/remarks.h
Normal file
64
src/test/jtx/remarks.h
Normal file
@@ -0,0 +1,64 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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_TEST_JTX_REMARKS_H_INCLUDED
|
||||
#define RIPPLE_TEST_JTX_REMARKS_H_INCLUDED
|
||||
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace jtx {
|
||||
|
||||
namespace remarks {
|
||||
|
||||
struct remark
|
||||
{
|
||||
std::string name;
|
||||
std::optional<std::string> value;
|
||||
std::optional<std::uint32_t> flags;
|
||||
remark(
|
||||
std::string name_,
|
||||
std::optional<std::string> value_ = std::nullopt,
|
||||
std::optional<std::uint32_t> flags_ = std::nullopt)
|
||||
: name(name_), value(value_), flags(flags_)
|
||||
{
|
||||
if (value_)
|
||||
value = *value_;
|
||||
|
||||
if (flags_)
|
||||
flags = *flags_;
|
||||
}
|
||||
};
|
||||
|
||||
Json::Value
|
||||
setRemarks(
|
||||
jtx::Account const& account,
|
||||
uint256 const& id,
|
||||
std::vector<remark> const& marks);
|
||||
|
||||
} // namespace remarks
|
||||
|
||||
} // namespace jtx
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
|
||||
#endif // RIPPLE_TEST_JTX_REMARKS_H_INCLUDED
|
||||
@@ -201,7 +201,8 @@ class Catalogue_test : public beast::unit_test::suite
|
||||
BEAST_EXPECT(result[jss::min_ledger] == 3);
|
||||
BEAST_EXPECT(result[jss::max_ledger] == 5);
|
||||
BEAST_EXPECT(result[jss::output_file] == cataloguePath);
|
||||
BEAST_EXPECT(result[jss::file_size].asUInt() > 0);
|
||||
BEAST_EXPECT(!result[jss::file_size].asString().empty());
|
||||
BEAST_EXPECT(!result[jss::file_size_human].asString().empty());
|
||||
BEAST_EXPECT(result[jss::ledgers_written].asUInt() == 3);
|
||||
|
||||
// Verify file exists and is not empty
|
||||
@@ -680,9 +681,11 @@ class Catalogue_test : public beast::unit_test::suite
|
||||
auto const result =
|
||||
env.client().invoke("catalogue_create", params)[jss::result];
|
||||
BEAST_EXPECT(result[jss::status] == jss::success);
|
||||
BEAST_EXPECT(result.isMember(jss::file_size_human));
|
||||
BEAST_EXPECT(!result[jss::file_size_human].asString().empty());
|
||||
BEAST_EXPECT(result.isMember(jss::file_size));
|
||||
uint64_t originalSize = result[jss::file_size].asUInt();
|
||||
BEAST_EXPECT(originalSize > 0);
|
||||
auto originalSize = result[jss::file_size].asString();
|
||||
BEAST_EXPECT(!originalSize.empty());
|
||||
}
|
||||
|
||||
// Test 1: Successful file size verification (normal load)
|
||||
@@ -694,6 +697,7 @@ class Catalogue_test : public beast::unit_test::suite
|
||||
env.client().invoke("catalogue_load", params)[jss::result];
|
||||
BEAST_EXPECT(result[jss::status] == jss::success);
|
||||
BEAST_EXPECT(result.isMember(jss::file_size));
|
||||
BEAST_EXPECT(result.isMember(jss::file_size_human));
|
||||
}
|
||||
|
||||
// Test 2: Modify file size in header to cause mismatch
|
||||
@@ -770,7 +774,11 @@ class Catalogue_test : public beast::unit_test::suite
|
||||
|
||||
BEAST_EXPECT(createResult[jss::status] == jss::success);
|
||||
|
||||
uint64_t fileSize = createResult[jss::file_size].asUInt();
|
||||
BEAST_EXPECT(createResult.isMember(jss::file_size_human));
|
||||
BEAST_EXPECT(
|
||||
!createResult[jss::file_size_human].asString().empty());
|
||||
auto fileSize =
|
||||
std::stoull(createResult[jss::file_size].asString());
|
||||
BEAST_EXPECT(fileSize > 0);
|
||||
|
||||
// Load the catalogue to verify it works
|
||||
|
||||
Reference in New Issue
Block a user