Compare commits

..

54 Commits

Author SHA1 Message Date
Richard Holland
6b49032436 feature count 2025-04-15 20:19:44 +10:00
RichardAH
7a62559da9 Merge branch 'dev' into remarks 2025-04-15 20:09:33 +10:00
tequ
59e334c099 fixRewardClaimFlags (#487) 2025-04-15 20:08:19 +10:00
Denis Angell
d7dd6196e8 fix test 2025-04-15 11:06:56 +02:00
Richard Holland
d3cfd46af3 add 1 to feature count 2025-04-15 17:15:40 +10:00
Richard Holland
94fab7d58b tx flags 2025-04-15 14:36:07 +10:00
Richard Holland
53b3b543a7 cleanup 2025-04-15 14:14:07 +10:00
Richard Holland
69e72ecb91 ensure numerically 0 value blob isnt a deletion 2025-04-15 13:59:42 +10:00
Richard Holland
98a33d11e0 change tem code 2025-04-15 13:54:59 +10:00
Richard Holland
c908018647 re-order check 2025-04-15 13:51:45 +10:00
RichardAH
c6ddd6d2c4 Merge branch 'dev' into remarks 2025-04-15 13:42:28 +10:00
tequ
9018596532 HookCanEmit (#392) 2025-04-15 13:32:35 +10:00
Niq Dudfield
b827f0170d feat(catalogue): add cli commands and fix file_size (#486)
* feat(catalogue): add cli commands and fix file_size

* feat(catalogue): add cli commands and fix file_size

* feat(catalogue): fix tests

* feat(catalogue): fix tests

* feat(catalogue): use formatBytesIEC

* feat: add file_size_estimated

* feat: add file_size_estimated

* feat: add file_size_estimated
2025-04-15 08:50:15 +10:00
tequ
e4b7e8f0f2 Update sfcodes script (#479) 2025-04-10 09:44:31 +10:00
tequ
1485078d91 Update CHooks build script (#465) 2025-04-09 20:22:34 +10:00
tequ
6625d2be92 Add xpop_slot test (#470) 2025-04-09 20:20:23 +10:00
RichardAH
78906ee086 Merge branch 'dev' into remarks 2025-04-09 17:14:04 +10:00
tequ
2fb5c92140 feat: Run unittests in parallel with Github Actions (#483)
Implement parallel execution for unit tests using Github Actions to improve CI pipeline efficiency and reduce build times.
2025-04-04 19:32:47 +02:00
Niq Dudfield
c4b5ae3787 Fix missing includes in Catalogue.cpp for non-unity builds (#485) 2025-04-04 12:53:45 +10:00
Niq Dudfield
d546d761ce Fix using using Status with rpcError (#484) 2025-04-01 21:00:13 +10:00
RichardAH
e84a36867b Catalogue (#443) 2025-04-01 16:47:48 +10:00
RichardAH
987247ddc1 Merge branch 'dev' into remarks 2024-11-20 12:15:32 +10:00
RichardAH
a5e2fd0699 Merge branch 'dev' into remarks 2024-11-09 15:27:55 +10:00
RichardAH
d92403ce35 Merge branch 'dev' into remarks 2024-11-09 13:45:31 +10:00
Denis Angell
6fb8fef883 clang-format 2024-09-19 16:29:18 +02:00
Denis Angell
a8a4774232 add tests 2024-09-19 16:27:33 +02:00
Denis Angell
eaec08471b Merge branch 'dev' into remarks 2024-09-19 14:41:53 +02:00
Denis Angell
caffeea6fc Merge branch 'dev' into remarks 2024-07-08 09:59:26 +02:00
Denis Angell
23d49d0548 Merge branch 'dev' into remarks 2024-05-23 08:23:54 +02:00
Denis Angell
519ab34e4f add more tests 2024-04-03 15:28:23 +02:00
Denis Angell
bdc59ac4ec apply sandbox and fixup 2024-04-03 15:28:07 +02:00
Denis Angell
96bb67bfe5 clang-format 2024-04-02 17:12:59 +02:00
Denis Angell
798212f87c add tests 2024-04-02 17:08:46 +02:00
Denis Angell
a3d61c0fbf make sfRemarkValue Optional 2024-04-02 16:55:31 +02:00
Denis Angell
3e926c9946 add remark fee 2024-04-02 16:55:15 +02:00
Denis Angell
4392342c99 update error warning 2024-04-02 16:54:47 +02:00
Denis Angell
f4fe7b7d9a add jtx helper 2024-04-02 16:53:33 +02:00
Richard Holland
d268638a39 whoops 2024-03-27 03:36:01 +00:00
Richard Holland
b1447afcc0 refactor, feature enable check 2024-03-27 02:22:02 +00:00
Denis Angell
f40621c662 Update mulDiv.cpp 2024-03-25 22:18:01 +01:00
Denis Angell
36ff48474a Revert "fix muldiv"
This reverts commit 63b0245d06.
2024-03-25 22:05:09 +01:00
Denis Angell
2adc234bf1 Update SetRemarks_test.cpp 2024-03-25 17:39:43 +01:00
Denis Angell
89bcacca5b use sandbox and peak 2024-03-25 17:30:26 +01:00
Denis Angell
6d496cc16f create set remarks test 2024-03-25 17:06:51 +01:00
Denis Angell
63b0245d06 fix muldiv 2024-03-25 17:06:39 +01:00
Denis Angell
fdf02a3853 Update SetRemarks.cpp 2024-03-25 17:06:32 +01:00
Denis Angell
9edf7ae67a create SetRemarks header 2024-03-25 17:06:29 +01:00
Denis Angell
533ba7ab75 nit: remit headerfile 2024-03-25 17:05:37 +01:00
Denis Angell
4e10d7d61f fix applySteps headers 2024-03-25 17:05:25 +01:00
Denis Angell
01e7caa0d6 fix applyHook headers 2024-03-25 17:05:15 +01:00
Denis Angell
349f4d2d68 reorder cmake 2024-03-25 17:05:04 +01:00
Richard Holland
24ac5d5f51 bug fixes, but levelisation issues 2024-03-25 06:58:31 +00:00
Richard Holland
8522c6684b transactor 2024-03-25 00:55:42 +00:00
Richard Holland
7efc26a8b1 initial version of remarks 2024-03-25 00:54:08 +00:00
38 changed files with 3358 additions and 136 deletions

2
.gitignore vendored
View File

@@ -114,3 +114,5 @@ pkg_out
pkg
CMakeUserPresets.json
bld.rippled/
generated

View File

@@ -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
View 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'

View File

@@ -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)")}'

View File

@@ -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)

View File

@@ -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;

View File

@@ -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

View File

@@ -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) &&

View File

@@ -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>

View File

@@ -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:

View 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

View 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

View File

@@ -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,

View File

@@ -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:

View File

@@ -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},

View File

@@ -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

View File

@@ -315,6 +315,9 @@ enum LedgerSpecificFlags {
// ltURI_TOKEN
lsfBurnable = 0x00000001, // True, issuer can burn the token
// remarks
lsfImmutable = 1,
};
//------------------------------------------------------------------------------

View File

@@ -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;
//------------------------------------------------------------------------------

View File

@@ -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,
};

View File

@@ -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

View File

@@ -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,

View File

@@ -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.

View File

@@ -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&

View File

@@ -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},

View File

@@ -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);

View File

@@ -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."),

View File

@@ -456,6 +456,14 @@ TxFormats::TxFormats()
{sfTicketSequence, soeOPTIONAL},
},
commonFields);
add(jss::SetRemarks,
ttREMARKS_SET,
{
{sfObjectID, soeREQUIRED},
{sfRemarks, soeREQUIRED},
},
commonFields);
}
TxFormats const&

View File

@@ -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

View File

@@ -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;

View File

@@ -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.

View File

@@ -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

View 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

View File

@@ -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

View 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>

View 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
View 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

View File

@@ -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