Compare commits

...

3 Commits

Author SHA1 Message Date
tequ
692be6b783 HookAtomicEmitFee 2025-09-04 00:26:58 +09:00
tequ
bd7fc84dbc featureAtomicEmit 2025-09-03 18:04:04 +09:00
Denis Angell
1a35c83048 add open view sandbox 2025-09-01 17:34:13 +09:00
23 changed files with 2074 additions and 322 deletions

View File

@@ -349,6 +349,12 @@ const uint8_t max_emit = 255;
const uint8_t max_params = 16;
const double fee_base_multiplier = 1.1f;
enum GuardRules : uint64_t {
HooksUpdate1 = 0x0001U,
Fix20250131 = 0x0002U,
AtomicEmit = 0x0004U,
};
// RH NOTE: Find descriptions of api functions in ./impl/applyHook.cpp and
// hookapi.h (include for hooks) this is a map of the api name to its return
// code (vec[0] and its parameters vec[>0]) as wasm type codes
@@ -434,5 +440,8 @@ static const std::map<std::string, std::vector<uint8_t>> import_whitelist{
// featureHooks1
static const std::map<std::string, std::vector<uint8_t>> import_whitelist_1{
{"xpop_slot", {0x7EU, 0x7FU, 0x7FU}}};
// featureAtomicEmit
static const std::map<std::string, std::vector<uint8_t>> import_whitelist_2{
{"emit_atomic", {0x7EU, 0x7FU, 0x7FU, 0x7FU, 0x7FU}}};
}; // namespace hook_api
#endif

View File

@@ -634,7 +634,7 @@ check_guard(
}
else if (fc_type == 10) // memory.copy
{
if (rulesVersion & 0x02U)
if (rulesVersion & hook_api::GuardRules::Fix20250131)
GUARD_ERROR("Memory.copy instruction is not allowed.");
REQUIRE(2);
@@ -642,7 +642,7 @@ check_guard(
}
else if (fc_type == 11) // memory.fill
{
if (rulesVersion & 0x02U)
if (rulesVersion & hook_api::GuardRules::Fix20250131)
GUARD_ERROR("Memory.fill instruction is not allowed.");
ADVANCE(1);
@@ -1028,12 +1028,19 @@ validateGuards(
hook_api::import_whitelist.find(import_name) ==
hook_api::import_whitelist.end())
{
if (rulesVersion > 0 &&
if (rulesVersion & hook_api::GuardRules::HooksUpdate1 &&
hook_api::import_whitelist_1.find(import_name) !=
hook_api::import_whitelist_1.end())
{
// PASS, this is a version 1 api
}
else if (
rulesVersion & hook_api::GuardRules::AtomicEmit &&
hook_api::import_whitelist_2.find(import_name) !=
hook_api::import_whitelist_2.end())
{
// PASS, this is a version 2 api
}
else
{
GUARDLOG(hook::log::IMPORT_ILLEGAL)
@@ -1258,12 +1265,20 @@ validateGuards(
{
for (auto const& [import_idx, api_name] : usage->second)
{
auto const& api_signature =
hook_api::import_whitelist.find(api_name) !=
hook_api::import_whitelist.end()
? hook_api::import_whitelist.find(api_name)->second
: hook_api::import_whitelist_1.find(api_name)
->second;
auto findInWhitelist = [&](auto const& whitelist) {
auto it = whitelist.find(api_name);
return it != whitelist.end() ? &it->second
: nullptr;
};
auto const* sig =
findInWhitelist(hook_api::import_whitelist);
if (!sig)
sig = findInWhitelist(hook_api::import_whitelist_1);
if (!sig)
sig = findInWhitelist(hook_api::import_whitelist_2);
auto const& api_signature = *sig;
if (!first_signature)
{

View File

@@ -79,7 +79,10 @@ main(int argc, char** argv)
close(fd);
auto result = validateGuards(hook, std::cout, "", 3);
auto const allRules = hook_api::GuardRules::HooksUpdate1 +
hook_api::GuardRules::Fix20250131 + hook_api::GuardRules::AtomicEmit;
auto result = validateGuards(hook, std::cout, "", allRules);
if (!result)
{

View File

@@ -80,7 +80,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 = 106;
static constexpr std::size_t numFeatures = 107;
/** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated

View File

@@ -361,6 +361,7 @@ enum TECcodes : TERUnderlyingType {
tecARRAY_TOO_LARGE = 196,
tecLOCKED = 197,
tecBAD_CREDENTIALS = 198,
tecHOOK_EMIT_FAILED = 199,
tecLAST_POSSIBLE_ENTRY = 255,
};

View File

@@ -29,6 +29,7 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FEATURE(AtomicEmit, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(Batch, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(PermissionedDomains, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(DynamicNFT, Supported::no, VoteBehavior::DefaultNo)

View File

@@ -91,6 +91,7 @@ LEDGER_ENTRY(ltHOOK_DEFINITION, 'D', HookDefinition, hook_definition, ({
{sfHookCallbackFee, soeOPTIONAL},
{sfPreviousTxnID, soeOPTIONAL},
{sfPreviousTxnLgrSeq, soeOPTIONAL},
{sfHookAtomicEmitFee, soeOPTIONAL},
}))
/** A ledger object containing a hook-emitted transaction from a previous hook execution.

View File

@@ -251,6 +251,7 @@ TYPED_SFIELD(sfPrice, AMOUNT, 28)
TYPED_SFIELD(sfSignatureReward, AMOUNT, 29)
TYPED_SFIELD(sfMinAccountCreateAmount, AMOUNT, 30)
TYPED_SFIELD(sfLPTokenBalance, AMOUNT, 31)
TYPED_SFIELD(sfHookAtomicEmitFee, AMOUNT, 32)
// variable length (common)
TYPED_SFIELD(sfPublicKey, VL, 1)

View File

@@ -96,7 +96,8 @@ InnerObjectFormats::InnerObjectFormats()
{sfHookCanEmit, soeOPTIONAL},
{sfHookApiVersion, soeREQUIRED},
{sfFlags, soeREQUIRED},
{sfFee, soeREQUIRED}});
{sfFee, soeREQUIRED},
{sfHookAtomicEmitFee, soeOPTIONAL}});
add(sfHook.jsonName,
sfHook.getCode(),
@@ -108,7 +109,8 @@ InnerObjectFormats::InnerObjectFormats()
{sfHookOn, soeOPTIONAL},
{sfHookCanEmit, soeOPTIONAL},
{sfHookApiVersion, soeOPTIONAL},
{sfFlags, soeOPTIONAL}});
{sfFlags, soeOPTIONAL},
{sfHookAtomicEmitFee, soeOPTIONAL}});
add(sfHookGrant.jsonName,
sfHookGrant.getCode(),

View File

@@ -123,6 +123,7 @@ transResults()
MAKE_ERROR(tecARRAY_TOO_LARGE, "Array is too large."),
MAKE_ERROR(tecLOCKED, "Fund is locked."),
MAKE_ERROR(tecBAD_CREDENTIALS, "Bad credentials."),
MAKE_ERROR(tecHOOK_EMIT_FAILED, "Failed to emit transactions from Hook."),
MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."),
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),

View File

@@ -2933,6 +2933,457 @@ public:
}
}
Json::Value
getLastLedger(jtx::Env& env)
{
Json::Value params;
params[jss::ledger_index] = env.closed()->seq();
params[jss::transactions] = true;
params[jss::expand] = true;
return env.rpc("json", "ledger", to_string(params));
}
void
test_emit_atomic(FeatureBitset features)
{
testcase("Test emit_atomic");
using namespace jtx;
// Env env{*this, features};
Env env{
*this, envconfig(), features, nullptr, beast::severities::kInfo};
auto const alice = Account{"alice"};
auto const bob = Account{"bob"};
auto const charlie = Account{"charlie"};
auto const nonActive = Account{"nonActive"};
env.fund(XRP(10000), alice);
env.fund(XRP(10000), bob);
env.fund(XRP(10000), charlie);
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_atomic (uint32_t, uint32_t, uint32_t, uint32_t);
extern int64_t etxn_reserve(uint32_t);
extern int64_t otxn_param(uint32_t, uint32_t, uint32_t, uint32_t);
extern int64_t hook_account(uint32_t, uint32_t);
extern int64_t otxn_field (
uint32_t write_ptr,
uint32_t write_len,
uint32_t field_id
);
#define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1)
#define OUT_OF_BOUNDS (-1)
#define ttPAYMENT 0
#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 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, 3 ); /* uint32 | size 5 */ \
_02_14_ENCODE_TAG_DST (buf_out, dest_tag ); /* uint32 | size 5 */ \
_02_26_ENCODE_FLS (buf_out, cls + 0 ); /* uint32 | size 6 */ \
_02_27_ENCODE_LLS (buf_out, cls + 0 ); /* 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 1?? */ \
int64_t fee = etxn_fee_base(buf_out_master, PREPARE_PAYMENT_SIMPLE_SIZE); \
_06_08_ENCODE_DROPS_FEE (fee_ptr, fee ); \
}
#define UINT16_FROM_BUF(buf)\
(((uint64_t)((buf)[0]) << 8U) +\
((uint64_t)((buf)[1]) << 0U))
#define BUFFER_EQUAL_32(buf1, buf2)\
(\
*(((uint64_t*)(buf1)) + 0) == *(((uint64_t*)(buf2)) + 0) &&\
*(((uint64_t*)(buf1)) + 1) == *(((uint64_t*)(buf2)) + 1) &&\
*(((uint64_t*)(buf1)) + 2) == *(((uint64_t*)(buf2)) + 2) &&\
*(((uint64_t*)(buf1)) + 3) == *(((uint64_t*)(buf2)) + 3) &&\
*(((uint64_t*)(buf1)) + 4) == *(((uint64_t*)(buf2)) + 4) &&\
*(((uint64_t*)(buf1)) + 5) == *(((uint64_t*)(buf2)) + 5) &&\
*(((uint64_t*)(buf1)) + 6) == *(((uint64_t*)(buf2)) + 6) &&\
*(((uint64_t*)(buf1)) + 7) == *(((uint64_t*)(buf2)) + 7))
#define ASSERT(x)\
if (!(x))\
rollback((uint32_t)#x,sizeof(#x),__LINE__)
#define sfDestination ((8U << 16U) + 3U)
extern int64_t etxn_generation(void);
extern int64_t otxn_generation(void);
extern int64_t otxn_burden(void);
extern int64_t etxn_burden(void);
// int64_t cbak(uint32_t r)
// {
// // on callback we emit 2 more txns
// uint8_t bob[20];
// ASSERT(otxn_field(SBUF(bob), sfDestination) == 20);
// ASSERT(otxn_generation() + 1 == etxn_generation());
// ASSERT(etxn_burden() == PREREQUISITE_NOT_MET);
// ASSERT(etxn_reserve(2) == 2);
// ASSERT(otxn_burden() > 0);
// ASSERT(etxn_burden() == otxn_burden() * 2);
// uint8_t tx[PREPARE_PAYMENT_SIMPLE_SIZE];
// PREPARE_PAYMENT_SIMPLE(tx, 1000, bob, 0, 0);
// uint8_t hash1[32];
// ASSERT(emit_atomic(SBUF(hash1), SBUF(tx)) == 32);
// ASSERT(etxn_details(tx + 132, 138) == 138);
// uint8_t hash2[32];
// ASSERT(emit_atomic(SBUF(hash2), SBUF(tx)) == 32);
// ASSERT(!BUFFER_EQUAL_32(hash1, hash2));
// return accept(0,0,0);
// }
int64_t hook(uint32_t r)
{
_g(1,1);
etxn_reserve(1);
// bounds checks
ASSERT(emit_atomic(1000000, 32, 0, 32) == OUT_OF_BOUNDS);
ASSERT(emit_atomic(0,1000000, 0, 32) == OUT_OF_BOUNDS);
ASSERT(emit_atomic(0,32, 1000000, 32) == OUT_OF_BOUNDS);
ASSERT(emit_atomic(0,32, 0, 1000000) == OUT_OF_BOUNDS);
ASSERT(otxn_generation() == 0);
ASSERT(otxn_burden == 1);
uint8_t bob[20];
ASSERT(otxn_param(SBUF(bob), "bob", 3) == 20);
uint8_t tx[PREPARE_PAYMENT_SIMPLE_SIZE];
PREPARE_PAYMENT_SIMPLE(tx, 1000, bob, 0, 0);
uint8_t hash[32];
ASSERT(emit_atomic(SBUF(hash), SBUF(tx)) == 32);
return accept(0,0,0);
}
)[test.hook]"];
auto hookObj = hso(hook, overrideFlag);
// hookObj[jss::HookOn] =
// "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFFFFFFFFFFFFFBFFF"
// "FF"; // Invoke
hookObj[sfHookAtomicEmitFee.jsonName] = "1000";
env(ripple::test::jtx::hook(charlie, {{hookObj}}, 0),
M("set emit_atomic"),
HSFEE);
env.close();
// Json::Value invoke;
// invoke[jss::TransactionType] = "Invoke";
// invoke[jss::Account] = alice.human();
// invoke[jss::Destination] = charlie.human();
Json::Value invoke;
invoke[jss::TransactionType] = "Payment";
invoke[jss::Account] = alice.human();
invoke[jss::Destination] = charlie.human();
invoke[jss::Amount] = "1234";
Json::Value params{Json::arrayValue};
params[0U][jss::HookParameter][jss::HookParameterName] =
strHex(std::string("bob"));
params[0U][jss::HookParameter][jss::HookParameterValue] =
strHex(nonActive.id());
invoke[jss::HookParameters] = params;
env(invoke, M("test emit_atomic"), fee(XRP(1)), ter(tesSUCCESS));
env.close();
bool const fixV2 = env.current()->rules().enabled(fixXahauV2);
std::optional<uint256> emithash;
{
auto meta = env.meta(); // meta can close
printf(
"txs: %s\n",
getLastLedger(env)[jss::result][jss::ledger][jss::transactions]
.toStyledString()
.c_str());
// ensure hook execution occured
BEAST_REQUIRE(meta);
BEAST_REQUIRE(meta->isFieldPresent(sfHookExecutions));
auto const hookEmissions = meta->getFieldArray(sfHookEmissions);
BEAST_EXPECT(
hookEmissions[0u].isFieldPresent(sfEmitNonce) == fixV2 ? true
: false);
BEAST_EXPECT(
hookEmissions[0u].getAccountID(sfHookAccount) == alice.id());
auto const hookExecutions = meta->getFieldArray(sfHookExecutions);
BEAST_REQUIRE(hookExecutions.size() == 1);
// ensure there was one emitted txn
BEAST_EXPECT(hookExecutions[0].getFieldU16(sfHookEmitCount) == 1);
BEAST_REQUIRE(meta->isFieldPresent(sfAffectedNodes));
BEAST_REQUIRE(meta->getFieldArray(sfAffectedNodes).size() == 3);
for (auto const& node : meta->getFieldArray(sfAffectedNodes))
{
SField const& metaType = node.getFName();
uint16_t nodeType = node.getFieldU16(sfLedgerEntryType);
if (metaType == sfCreatedNode && nodeType == ltEMITTED_TXN)
{
BEAST_REQUIRE(node.isFieldPresent(sfNewFields));
auto const& nf = const_cast<ripple::STObject&>(node)
.getField(sfNewFields)
.downcast<STObject>();
auto const& et = const_cast<ripple::STObject&>(nf)
.getField(sfEmittedTxn)
.downcast<STObject>();
auto const& em = const_cast<ripple::STObject&>(et)
.getField(sfEmitDetails)
.downcast<STObject>();
BEAST_EXPECT(em.getFieldU32(sfEmitGeneration) == 1);
BEAST_EXPECT(em.getFieldU64(sfEmitBurden) == 1);
Blob txBlob = et.getSerializer().getData();
auto const tx = std::make_unique<STTx>(
Slice{txBlob.data(), txBlob.size()});
emithash = tx->getTransactionID();
break;
}
}
BEAST_REQUIRE(emithash);
BEAST_EXPECT(
emithash == hookEmissions[0u].getFieldH256(sfEmittedTxnID));
}
}
void
test_etxn_details(FeatureBitset features)
{
@@ -12734,117 +13185,118 @@ public:
void
testWithFeatures(FeatureBitset features)
{
testHooksOwnerDir(features);
testHooksDisabled(features);
testTxStructure(features);
testInferHookSetOperation();
testParams(features);
testGrants(features);
testHookCanEmit(features);
// testHooksOwnerDir(features);
// testHooksDisabled(features);
// testTxStructure(features);
// testInferHookSetOperation();
// testParams(features);
// testGrants(features);
// testHookCanEmit(features);
testDelete(features);
testInstall(features);
testCreate(features);
testWithTickets(features);
// testDelete(features);
// testInstall(features);
// testCreate(features);
// testWithTickets(features);
testUpdate(features);
// testUpdate(features);
testNSDelete(features);
testNSDeletePartial(features);
testPageCap(features);
// testNSDelete(features);
// testNSDeletePartial(features);
// testPageCap(features);
testFillCopy(features);
// testFillCopy(features);
testWasm(features);
test_accept(features);
test_rollback(features);
// testWasm(features);
// test_accept(features);
// test_rollback(features);
testGuards(features);
// testGuards(features);
test_emit(features); //
// test_etxn_burden(features); // tested above
// test_etxn_generation(features); // tested above
// test_otxn_burden(features); // tested above
// test_otxn_generation(features); // tested above
test_etxn_details(features); //
test_etxn_fee_base(features); //
test_etxn_nonce(features); //
test_etxn_reserve(features); //
test_fee_base(features); //
// test_emit(features); //
test_emit_atomic(features); //
// // test_etxn_burden(features); // tested above
// // test_etxn_generation(features); // tested above
// // test_otxn_burden(features); // tested above
// // test_otxn_generation(features); // tested above
// test_etxn_details(features); //
// test_etxn_fee_base(features); //
// test_etxn_nonce(features); //
// test_etxn_reserve(features); //
// test_fee_base(features); //
test_otxn_field(features); //
// test_otxn_field(features); //
test_ledger_keylet(features); //
// test_ledger_keylet(features); //
test_float_compare(features); //
test_float_divide(features); //
test_float_int(features); //
test_float_invert(features); //
test_float_log(features); //
test_float_mantissa(features); //
test_float_mulratio(features); //
test_float_multiply(features); //
test_float_negate(features); //
test_float_one(features); //
test_float_root(features); //
test_float_set(features); //
test_float_sign(features); //
test_float_sto(features); //
test_float_sto_set(features); //
test_float_sum(features); //
// test_float_compare(features); //
// test_float_divide(features); //
// test_float_int(features); //
// test_float_invert(features); //
// test_float_log(features); //
// test_float_mantissa(features); //
// test_float_mulratio(features); //
// test_float_multiply(features); //
// test_float_negate(features); //
// test_float_one(features); //
// test_float_root(features); //
// test_float_set(features); //
// test_float_sign(features); //
// test_float_sto(features); //
// test_float_sto_set(features); //
// test_float_sum(features); //
test_hook_account(features); //
test_hook_again(features); //
test_hook_hash(features); //
test_hook_param(features); //
test_hook_param_set(features); //
test_hook_pos(features); //
test_hook_skip(features); //
// test_hook_account(features); //
// test_hook_again(features); //
// test_hook_hash(features); //
// test_hook_param(features); //
// test_hook_param_set(features); //
// test_hook_pos(features); //
// test_hook_skip(features); //
test_ledger_last_hash(features); //
test_ledger_last_time(features); //
test_ledger_nonce(features); //
test_ledger_seq(features); //
// test_ledger_last_hash(features); //
// test_ledger_last_time(features); //
// test_ledger_nonce(features); //
// test_ledger_seq(features); //
test_meta_slot(features); //
test_xpop_slot(features); //
// test_meta_slot(features); //
// test_xpop_slot(features); //
test_otxn_id(features); //
test_otxn_slot(features); //
test_otxn_type(features); //
test_otxn_param(features); //
// test_otxn_id(features); //
// test_otxn_slot(features); //
// test_otxn_type(features); //
// test_otxn_param(features); //
test_slot(features); //
test_slot_clear(features); //
test_slot_count(features); //
test_slot_float(features); //
test_slot_set(features); //
test_slot_size(features); //
test_slot_subarray(features); //
test_slot_subfield(features); //
test_slot_type(features); //
// test_slot(features); //
// test_slot_clear(features); //
// test_slot_count(features); //
// test_slot_float(features); //
// test_slot_set(features); //
// test_slot_size(features); //
// test_slot_subarray(features); //
// test_slot_subfield(features); //
// test_slot_type(features); //
test_state(features); //
test_state_foreign(features); //
test_state_foreign_set(features); //
test_state_foreign_set_max(features); //
test_state_set(features); //
// test_state(features); //
// test_state_foreign(features); //
// test_state_foreign_set(features); //
// test_state_foreign_set_max(features); //
// test_state_set(features); //
test_sto_emplace(features); //
test_sto_erase(features); //
test_sto_subarray(features); //
test_sto_subfield(features); //
test_sto_validate(features); //
// test_sto_emplace(features); //
// test_sto_erase(features); //
// test_sto_subarray(features); //
// test_sto_subfield(features); //
// test_sto_validate(features); //
test_trace(features); //
test_trace_float(features); //
test_trace_num(features); //
// test_trace(features); //
// test_trace_float(features); //
// test_trace_num(features); //
test_util_accid(features); //
test_util_keylet(features); //
test_util_raddr(features); //
test_util_sha512h(features); //
test_util_verify(features); //
// test_util_accid(features); //
// test_util_keylet(features); //
// test_util_raddr(features); //
// test_util_sha512h(features); //
// test_util_verify(features); //
}
public:
@@ -13031,11 +13483,11 @@ SETHOOK_TEST(4, false)
SETHOOK_TEST(5, true)
BEAST_DEFINE_TESTSUITE_PRIO(SetHook0, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook1, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook2, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook3, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook4, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook5, app, ripple, 2);
// BEAST_DEFINE_TESTSUITE_PRIO(SetHook1, app, ripple, 2);
// BEAST_DEFINE_TESTSUITE_PRIO(SetHook2, app, ripple, 2);
// BEAST_DEFINE_TESTSUITE_PRIO(SetHook3, app, ripple, 2);
// BEAST_DEFINE_TESTSUITE_PRIO(SetHook4, app, ripple, 2);
// BEAST_DEFINE_TESTSUITE_PRIO(SetHook5, app, ripple, 2);
} // namespace test
} // namespace ripple
#undef M

File diff suppressed because it is too large Load Diff

View File

@@ -177,6 +177,13 @@ DECLARE_HOOK_FUNCTION(
uint32_t write_len,
uint32_t read_ptr,
uint32_t read_len);
DECLARE_HOOK_FUNCTION(
int64_t,
emit_atomic,
uint32_t write_ptr,
uint32_t write_len,
uint32_t read_ptr,
uint32_t read_len);
DECLARE_HOOK_FUNCTION(int64_t, float_set, int32_t exponent, int64_t mantissa);
DECLARE_HOOK_FUNCTION(int64_t, float_multiply, int64_t float1, int64_t float2);
@@ -454,6 +461,7 @@ apply(
std::map<std::vector<uint8_t>, std::vector<uint8_t>>> const&
hookParamOverrides,
HookStateMap& stateMap,
XRPAmount& atomicEmitFeeRemaining,
ripple::ApplyContext& applyCtx,
ripple::AccountID const& account, /* the account the hook is INSTALLED ON
not always the otxn account */
@@ -489,7 +497,10 @@ struct HookResult
std::queue<std::shared_ptr<ripple::Transaction>>
emittedTxn{}; // etx stored here until accept/rollback
std::queue<std::shared_ptr<ripple::Transaction>>
emittedAtomicTxn{}; // etx stored here until accept/rollback
HookStateMap& stateMap;
XRPAmount& atomicEmitFeeRemaining;
uint16_t changedStateCount = 0;
std::map<
ripple::uint256, // hook hash
@@ -581,6 +592,14 @@ finalizeHookResult(
ripple::ApplyContext&,
bool doEmit);
bool
emitAtomicTransactions(
ripple::Application& app,
ripple::OpenView& view,
ripple::uint256 const& parentTxnId,
ripple::STArray const& hookEmissions,
beast::Journal j);
// write state map to ledger
ripple::TER
finalizeHookState(
@@ -813,6 +832,7 @@ public:
ADD_HOOK_FUNCTION(util_keylet, ctx);
ADD_HOOK_FUNCTION(emit, ctx);
ADD_HOOK_FUNCTION(emit_atomic, ctx);
ADD_HOOK_FUNCTION(etxn_burden, ctx);
ADD_HOOK_FUNCTION(etxn_fee_base, ctx);
ADD_HOOK_FUNCTION(etxn_details, ctx);

View File

@@ -5,6 +5,7 @@
#include <xrpld/app/misc/NetworkOPs.h>
#include <xrpld/app/misc/Transaction.h>
#include <xrpld/app/misc/TxQ.h>
#include <xrpld/app/tx/apply.h>
#include <xrpld/app/tx/detail/Import.h>
#include <xrpld/app/tx/detail/NFTokenUtils.h>
#include <xrpl/basics/Log.h>
@@ -1300,6 +1301,7 @@ hook::apply(
std::map<std::vector<uint8_t>, std::vector<uint8_t>>> const&
hookParamOverrides,
HookStateMap& stateMap,
XRPAmount& atomicEmitFeeRemaining,
ApplyContext& applyCtx,
ripple::AccountID const& account, /* the account the hook is INSTALLED ON
not always the otxn account */
@@ -1324,6 +1326,7 @@ hook::apply(
.otxnAccount = applyCtx.tx.getAccountID(sfAccount),
.hookNamespace = hookNamespace,
.stateMap = stateMap,
.atomicEmitFeeRemaining = atomicEmitFeeRemaining,
.changedStateCount = 0,
.hookParamOverrides = hookParamOverrides,
.hookParams = hookParams,
@@ -2018,10 +2021,9 @@ hook::finalizeHookResult(
if (doEmit)
{
DBG_PRINTF("emitted txn count: %d\n", hookResult.emittedTxn.size());
for (; hookResult.emittedTxn.size() > 0; hookResult.emittedTxn.pop())
{
auto& tpTrans = hookResult.emittedTxn.front();
auto const insertEmittedTxn =
[&](std::shared_ptr<ripple::Transaction> tpTrans,
bool atomic) -> TER {
auto& id = tpTrans->getID();
JLOG(j.trace()) << "HookEmit[" << HR_ACC() << "]: " << id;
@@ -2049,7 +2051,7 @@ hook::finalizeHookResult(
ptr->add(s);
SerialIter sit(s.slice());
sleEmitted->emplace_back(ripple::STObject(sit, sfEmittedTxn));
sleEmitted->set(ripple::STObject(sit, sfEmittedTxn));
auto page = applyCtx.view().dirInsert(
keylet::emittedDir(), emittedId, [&](SLE::ref sle) {
(*sle)[sfFlags] = lsfEmittedDir;
@@ -2058,6 +2060,7 @@ hook::finalizeHookResult(
if (page)
{
(*sleEmitted)[sfOwnerNode] = *page;
(*sleEmitted)[sfFlags] = atomic ? 1 : 0;
applyCtx.view().insert(sleEmitted);
}
else
@@ -2069,6 +2072,24 @@ hook::finalizeHookResult(
return tecDIR_FULL;
}
}
return tesSUCCESS;
};
DBG_PRINTF("emitted txn count: %d\n", hookResult.emittedTxn.size());
for (; hookResult.emittedTxn.size() > 0; hookResult.emittedTxn.pop())
{
auto& tpTrans = hookResult.emittedTxn.front();
insertEmittedTxn(tpTrans, false);
}
DBG_PRINTF(
"emitted atomic txn count: %d\n",
hookResult.emittedAtomicTxn.size());
for (; hookResult.emittedAtomicTxn.size() > 0;
hookResult.emittedAtomicTxn.pop())
{
auto& tpTrans = hookResult.emittedAtomicTxn.front();
insertEmittedTxn(tpTrans, true);
}
}
@@ -2134,6 +2155,57 @@ hook::finalizeHookResult(
return tesSUCCESS;
}
bool
hook::emitAtomicTransactions(
ripple::Application& app,
ripple::OpenView& view,
ripple::uint256 const& parentTxnId,
ripple::STArray const& hookEmissions,
beast::Journal j_)
{
for (auto const& emission : hookEmissions)
{
auto const& etxnId = emission.getFieldH256(sfEmittedTxnID);
auto const& keylet = keylet::emittedTxn(etxnId);
auto sleItem = view.read(keylet);
if (!sleItem)
{
continue;
}
LedgerEntryType const nodeType{
safe_cast<LedgerEntryType>((*sleItem)[sfLedgerEntryType])};
if (nodeType != ltEMITTED_TXN)
{
JLOG(j_.warn())
<< "EmittedTxn processing: emitted directory contained "
"non ltEMITTED_TXN type";
// RH TODO: if this ever happens the entry should be
// gracefully removed (somehow)
continue;
}
auto const& emitted = const_cast<ripple::STLedgerEntry&>(*sleItem)
.getField(sfEmittedTxn)
.downcast<STObject>();
auto s = std::make_shared<ripple::Serializer>();
emitted.add(*s);
SerialIter sitTrans(s->slice());
auto const& stpTrans = std::make_shared<STTx const>(std::ref(sitTrans));
auto const result = ripple::apply(
app, view, parentTxnId, *stpTrans, tapATOMIC_EMIT, j_);
if (!result.applied || !isTesSuccess(result.ter))
return false;
}
return true;
}
/* Retrieve the state into write_ptr identified by the key in kread_ptr */
DEFINE_HOOK_FUNCTION(
int64_t,
@@ -3354,7 +3426,9 @@ DEFINE_HOOK_FUNCTION(
if (hookCtx.expected_etxn_count < 0)
return PREREQUISITE_NOT_MET;
if (hookCtx.result.emittedTxn.size() >= hookCtx.expected_etxn_count)
if (hookCtx.result.emittedTxn.size() +
hookCtx.result.emittedAtomicTxn.size() >=
hookCtx.expected_etxn_count)
return TOO_MANY_EMITTED_TXN;
ripple::Blob blob{memory + read_ptr, memory + read_ptr + read_len};
@@ -3687,6 +3761,386 @@ DEFINE_HOOK_FUNCTION(
HOOK_TEARDOWN();
}
/* Emit a transaction from this hook. Transaction must be in STObject form,
* fully formed and valid. XRPLD does not modify transactions it only checks
* them for validity. */
DEFINE_HOOK_FUNCTION(
int64_t,
emit_atomic,
uint32_t write_ptr,
uint32_t write_len,
uint32_t read_ptr,
uint32_t read_len)
{
HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx,
// hookCtx on current stack
if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length))
return OUT_OF_BOUNDS;
if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length))
return OUT_OF_BOUNDS;
if (write_len < 32)
return TOO_SMALL;
auto& app = hookCtx.applyCtx.app;
// Doesn't allow to call Callbackable Hook
if (hookCtx.result.hasCallback || hookCtx.result.isCallback)
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: emit_atomic: Not allowed in Callbackable Hook.";
return EMISSION_FAILURE;
}
// Doesn't allow to call in Weak Execution
if (!hookCtx.result.isStrong)
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: emit_atomic: Not allowed in Weak Execution.";
return EMISSION_FAILURE;
}
if (hookCtx.expected_etxn_count < 0)
return PREREQUISITE_NOT_MET;
if (hookCtx.result.emittedTxn.size() +
hookCtx.result.emittedAtomicTxn.size() >=
hookCtx.expected_etxn_count)
return TOO_MANY_EMITTED_TXN;
ripple::Blob blob{memory + read_ptr, memory + read_ptr + read_len};
std::shared_ptr<STTx const> stpTrans;
try
{
stpTrans = std::make_shared<STTx const>(
SerialIter{memory + read_ptr, read_len});
}
catch (std::exception& e)
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC() << "]: Failed " << e.what()
<< "\n";
return EMISSION_FAILURE;
}
if (isPseudoTx(*stpTrans))
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: Attempted to emit pseudo txn.";
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
* 1. Sequence: 0
* 2. PubSigningKey: 000000000000000
* 3. sfEmitDetails present and valid
* 4. No sfTxnSignature
* 5. LastLedgerSeq == current ledger
* 6. FirstLedgerSeq == current ledger
* 7. Fee must be correctly high
* 8. The generation cannot be higher than 10
*/
// rule 0: account must match the hook account
if (!stpTrans->isFieldPresent(sfAccount) ||
stpTrans->getAccountID(sfAccount) != hookCtx.result.account)
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: sfAccount does not match hook account";
return EMISSION_FAILURE;
}
// rule 1: sfSequence must be present and 0
// if (!stpTrans->isFieldPresent(sfSequence) ||
// stpTrans->getFieldU32(sfSequence) != 0)
// {
// JLOG(j.trace()) << "HookEmit[" << HC_ACC()
// << "]: sfSequence missing or non-zero";
// return EMISSION_FAILURE;
// }
// rule 2: sfSigningPubKey must be present and 00...00
if (!stpTrans->isFieldPresent(sfSigningPubKey))
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: sfSigningPubKey missing";
return EMISSION_FAILURE;
}
auto const pk = stpTrans->getSigningPubKey();
if (pk.size() != 33 && pk.size() != 0)
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: sfSigningPubKey present but wrong size"
<< " expecting 33 bytes";
return EMISSION_FAILURE;
}
for (int i = 0; i < pk.size(); ++i)
if (pk[i] != 0)
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: sfSigningPubKey present but non-zero.";
return EMISSION_FAILURE;
}
// rule 2.a: no signers
if (stpTrans->isFieldPresent(sfSigners))
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: sfSigners not allowed in emitted txns.";
return EMISSION_FAILURE;
}
// rule 2.b: ticketseq cannot be used
if (stpTrans->isFieldPresent(sfTicketSequence))
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: sfTicketSequence not allowed in emitted txns.";
return EMISSION_FAILURE;
}
// rule 2.c sfAccountTxnID not allowed
if (stpTrans->isFieldPresent(sfAccountTxnID))
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: sfAccountTxnID not allowed in emitted txns.";
return EMISSION_FAILURE;
}
// rule 3: sfEmitDetails must be present and valid
if (!stpTrans->isFieldPresent(sfEmitDetails))
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: sfEmitDetails missing.";
return EMISSION_FAILURE;
}
auto const& emitDetails = const_cast<ripple::STTx&>(*stpTrans)
.getField(sfEmitDetails)
.downcast<STObject>();
if (!emitDetails.isFieldPresent(sfEmitGeneration) ||
!emitDetails.isFieldPresent(sfEmitBurden) ||
!emitDetails.isFieldPresent(sfEmitParentTxnID) ||
!emitDetails.isFieldPresent(sfEmitNonce) ||
!emitDetails.isFieldPresent(sfEmitHookHash))
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: sfEmitDetails malformed.";
return EMISSION_FAILURE;
}
// rule 8: emit generation cannot exceed 10
if (emitDetails.getFieldU32(sfEmitGeneration) >= 10)
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: sfEmitGeneration was 10 or more.";
return EMISSION_FAILURE;
}
uint32_t gen = emitDetails.getFieldU32(sfEmitGeneration);
uint64_t bur = emitDetails.getFieldU64(sfEmitBurden);
ripple::uint256 const& pTxnID = emitDetails.getFieldH256(sfEmitParentTxnID);
ripple::uint256 const& nonce = emitDetails.getFieldH256(sfEmitNonce);
if (emitDetails.isFieldPresent(sfEmitCallback))
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: callback not supported yet in emit_atomic.";
return EMISSION_FAILURE;
}
auto const& hash = emitDetails.getFieldH256(sfEmitHookHash);
uint32_t gen_proper = etxn_generation(hookCtx, frameCtx);
if (gen != gen_proper)
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: sfEmitGeneration provided in EmitDetails "
<< "not correct (" << gen << ") "
<< "should be " << gen_proper;
return EMISSION_FAILURE;
}
uint64_t bur_proper = etxn_burden(hookCtx, frameCtx);
if (bur != bur_proper)
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: sfEmitBurden provided in EmitDetails "
<< "was not correct (" << bur << ") "
<< "should be " << bur_proper;
return EMISSION_FAILURE;
}
if (pTxnID != applyCtx.tx.getTransactionID())
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: sfEmitParentTxnID provided in EmitDetails "
<< "was not correct";
return EMISSION_FAILURE;
}
if (hookCtx.nonce_used.find(nonce) == hookCtx.nonce_used.end())
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: sfEmitNonce provided in EmitDetails "
<< "was not generated by nonce api";
return EMISSION_FAILURE;
}
if (hash != hookCtx.result.hookHash)
{
JLOG(j.trace())
<< "HookEmit[" << HC_ACC()
<< "]: sfEmitHookHash must be the hash of the emitting hook";
return EMISSION_FAILURE;
}
// rule 4: sfTxnSignature must be absent
if (stpTrans->isFieldPresent(sfTxnSignature))
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: sfTxnSignature is present but should not be";
return EMISSION_FAILURE;
}
// rule 5: LastLedgerSeq must be present and after current ledger
if (!stpTrans->isFieldPresent(sfLastLedgerSequence))
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: sfLastLedgerSequence missing";
return EMISSION_FAILURE;
}
uint32_t tx_lls = stpTrans->getFieldU32(sfLastLedgerSequence);
uint32_t ledgerSeq = view.info().seq;
if (tx_lls != ledgerSeq)
{
JLOG(j.trace())
<< "HookEmit[" << HC_ACC()
<< "]: sfLastLedgerSequence cannot be equal to current seq";
return EMISSION_FAILURE;
}
// rule 6
if (!stpTrans->isFieldPresent(sfFirstLedgerSequence) ||
stpTrans->getFieldU32(sfFirstLedgerSequence) != ledgerSeq)
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: sfFirstLedgerSequence must be present and "
<< "= LastLedgerSequence";
return EMISSION_FAILURE;
}
// rule 7 check the emitted txn pays the appropriate fee
int64_t minfee = etxn_fee_base(hookCtx, frameCtx, read_ptr, read_len);
if (minfee < 0)
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: Fee could not be calculated";
return EMISSION_FAILURE;
}
if (!stpTrans->isFieldPresent(sfFee))
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: Fee missing from emitted tx";
return EMISSION_FAILURE;
}
int64_t fee = stpTrans->getFieldAmount(sfFee).xrp().drops();
if (fee < minfee)
{
JLOG(j.trace())
<< "HookEmit[" << HC_ACC()
<< "]: Fee on emitted txn is less than the minimum required fee";
return EMISSION_FAILURE;
}
if (hookCtx.result.atomicEmitFeeRemaining < XRPAmount(fee))
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: Fee on emitted txn is greater than the atomic "
"emit fee remaining";
return EMISSION_FAILURE;
}
std::string reason;
auto tpTrans = std::make_shared<Transaction>(stpTrans, reason, app);
if (tpTrans->getStatus() != NEW)
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: tpTrans->getStatus() != NEW";
return EMISSION_FAILURE;
}
// preflight the transaction
auto preflightResult = ripple::preflight(
applyCtx.app,
applyCtx.view().rules(),
*stpTrans,
ripple::ApplyFlags::tapPREFLIGHT_EMIT,
j);
if (!isTesSuccess(preflightResult.ter))
{
JLOG(j.trace()) << "HookEmit[" << HC_ACC()
<< "]: Transaction preflight failure: "
<< preflightResult.ter;
return EMISSION_FAILURE;
}
auto const& txID = tpTrans->getID();
if (txID.size() > write_len)
return TOO_SMALL;
if (NOT_IN_BOUNDS(write_ptr, txID.size(), memory_length))
return OUT_OF_BOUNDS;
auto const write_txid = [&]() -> int64_t {
WRITE_WASM_MEMORY_AND_RETURN(
write_ptr,
txID.size(),
txID.data(),
txID.size(),
memory,
memory_length);
};
int64_t result = write_txid();
if (result == 32)
{
hookCtx.result.atomicEmitFeeRemaining -= XRPAmount(fee);
XRPL_ASSERT(
hookCtx.result.atomicEmitFeeRemaining >= XRPAmount(0),
"atomicEmitFeeRemaining is negative");
hookCtx.result.emittedAtomicTxn.push(tpTrans);
}
return result;
HOOK_TEARDOWN();
}
// When implemented will return the hash of the current hook
DEFINE_HOOK_FUNCTION(
int64_t,

View File

@@ -129,6 +129,15 @@ apply(
ApplyFlags flags,
beast::Journal journal);
ApplyResult
apply(
Application& app,
OpenView& view,
uint256 const& parentBatchId,
STTx const& tx,
ApplyFlags flags,
beast::Journal journal);
/** Enum class for return value from `applyTransaction`
@see applyTransaction

View File

@@ -47,22 +47,31 @@ ApplyContext::ApplyContext(
, parentBatchId_(parentBatchId)
{
XRPL_ASSERT(
parentBatchId.has_value() == ((flags_ & tapBATCH) == tapBATCH),
"Parent Batch ID should be set if batch apply flag is set");
view_.emplace(&base_, flags_);
parentBatchId.has_value() ==
((flags_ & (tapBATCH | tapATOMIC_EMIT)) > 0),
"Parent Batch ID should be set if batch or AtomicEmit flag is set");
view_.emplace(&base_.view(), flags_);
}
void
ApplyContext::discard()
{
view_.emplace(&base_, flags_);
base_.discard();
view_.emplace(&base_.view(), flags_);
}
void
ApplyContext::finalize()
{
base_.commit();
view_.emplace(&base_.view(), flags_);
}
std::optional<TxMeta>
ApplyContext::apply(TER ter)
{
return view_->apply(
base_, tx, ter, parentBatchId_, flags_ & tapDRY_RUN, journal);
base_.view(), tx, ter, parentBatchId_, flags_ & tapDRY_RUN, journal);
}
std::size_t
@@ -78,7 +87,7 @@ ApplyContext::visit(std::function<void(
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&)> const& func)
{
view_->visit(base_, func);
view_->visit(base_.view(), func);
}
TER

View File

@@ -23,6 +23,8 @@
#include <xrpld/app/main/Application.h>
#include <xrpld/core/Config.h>
#include <xrpld/ledger/ApplyViewImpl.h>
#include <xrpld/ledger/OpenViewSandbox.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/XRPAmount.h>
@@ -64,7 +66,8 @@ public:
journal)
{
XRPL_ASSERT(
(flags & tapBATCH) == 0, "Batch apply flag should not be set");
(flags & (tapBATCH | tapATOMIC_EMIT)) == 0,
"Batch or AtomicEmit flag should not be set");
}
Application& app;
@@ -73,6 +76,12 @@ public:
XRPAmount const baseFee;
beast::Journal const journal;
OpenView&
openView()
{
return base_.view();
}
ApplyView&
view()
{
@@ -109,6 +118,10 @@ public:
void
discard();
/** Finalize changes. */
void
finalize();
/** Apply the transaction result to the base. */
std::optional<TxMeta> apply(TER);
@@ -134,7 +147,7 @@ public:
generateProvisionalMeta()
{
return view_->generateProvisionalMeta(
base_, tx, parentBatchId_, journal);
base_.view(), tx, parentBatchId_, journal);
}
/** Applies all invariant checkers one by one.
@@ -169,7 +182,7 @@ private:
XRPAmount const fee,
std::index_sequence<Is...>);
OpenView& base_;
OpenViewSandbox base_;
ApplyFlags flags_;
std::optional<ApplyViewImpl> view_;

View File

@@ -585,12 +585,21 @@ Change::activateXahauGenesis()
for (auto const& [hookOn, wasmBytes, params] : genesis_hooks)
{
std::ostringstream loggerStream;
auto result = validateGuards(
auto rulesVersion =
(ctx_.view().rules().enabled(featureHooksUpdate1)
? hook_api::GuardRules::HooksUpdate1
: 0U) +
(ctx_.view().rules().enabled(fix20250131)
? hook_api::GuardRules::Fix20250131
: 0U) +
(ctx_.view().rules().enabled(featureAtomicEmit)
? hook_api::GuardRules::AtomicEmit
: 0U);
auto const result = validateGuards(
wasmBytes, // wasm to verify
loggerStream,
"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
(ctx_.view().rules().enabled(featureHooksUpdate1) ? 1 : 0) +
(ctx_.view().rules().enabled(fix20250131) ? 2 : 0));
rulesVersion);
if (!result)
{

View File

@@ -227,7 +227,8 @@ SetHook::inferOperation(STObject const& hookSetObj)
!hookSetObj.isFieldPresent(sfHookOn) &&
!hookSetObj.isFieldPresent(sfHookCanEmit) &&
!hookSetObj.isFieldPresent(sfHookApiVersion) &&
!hookSetObj.isFieldPresent(sfFlags))
!hookSetObj.isFieldPresent(sfFlags) &&
!hookSetObj.isFieldPresent(sfHookAtomicEmitFee))
return hsoNOOP;
uint32_t flags = hookSetObj.isFieldPresent(sfFlags)
@@ -263,7 +264,8 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
hookSetObj.isFieldPresent(sfHookCanEmit) ||
hookSetObj.isFieldPresent(sfHookApiVersion) ||
!hookSetObj.isFieldPresent(sfFlags) ||
!hookSetObj.isFieldPresent(sfHookNamespace))
!hookSetObj.isFieldPresent(sfHookNamespace) ||
!hookSetObj.isFieldPresent(sfHookAtomicEmitFee))
{
JLOG(ctx.j.trace()) << "HookSet(" << hook::log::NSDELETE_FIELD
<< ")[" << HS_ACC()
@@ -293,7 +295,8 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
hookSetObj.isFieldPresent(sfHookCanEmit) ||
hookSetObj.isFieldPresent(sfHookApiVersion) ||
hookSetObj.isFieldPresent(sfHookNamespace) ||
!hookSetObj.isFieldPresent(sfFlags))
!hookSetObj.isFieldPresent(sfFlags) ||
!hookSetObj.isFieldPresent(sfHookAtomicEmitFee))
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::DELETE_FIELD << ")[" << HS_ACC()
@@ -392,6 +395,18 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
return false;
}
if (hookSetObj.isFieldPresent(sfHookAtomicEmitFee))
{
auto const fee = hookSetObj.getFieldAmount(sfHookAtomicEmitFee);
if (!isXRP(fee))
return false;
// TODO: Update maxFee
XRPAmount maxFee{1'000'000};
if (fee < beast::zero || fee > maxFee)
return false;
}
// namespace may be valid, if the user so chooses
// hookon may be present if the user so chooses
// flags may be present if the user so chooses
@@ -462,6 +477,19 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
// pass
}
// validate sfHookAtomicEmitFee
if (hookSetObj.isFieldPresent(sfHookAtomicEmitFee))
{
auto const fee = hookSetObj.getFieldAmount(sfHookAtomicEmitFee);
if (!isXRP(fee))
return false;
// TODO: Update maxFee
XRPAmount maxFee{1'000'000};
if (fee < beast::zero || fee > maxFee)
return false;
}
// finally validate web assembly byte code
{
if (!hookSetObj.isFieldPresent(sfCreateCode))
@@ -485,12 +513,21 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
hsacc = ss.str();
}
auto rulesVersion = (ctx.rules.enabled(featureHooksUpdate1)
? hook_api::GuardRules::HooksUpdate1
: 0U) +
(ctx.rules.enabled(fix20250131)
? hook_api::GuardRules::Fix20250131
: 0U) +
(ctx.rules.enabled(featureAtomicEmit)
? hook_api::GuardRules::AtomicEmit
: 0U);
auto result = validateGuards(
hook, // wasm to verify
logger,
hsacc,
(ctx.rules.enabled(featureHooksUpdate1) ? 1 : 0) +
(ctx.rules.enabled(fix20250131) ? 2 : 0));
rulesVersion);
if (ctx.j.trace())
{
@@ -728,6 +765,10 @@ SetHook::preflight(PreflightContext const& ctx)
hookSetObj.isFieldPresent(sfHookCanEmit))
return temDISABLED;
if (!ctx.rules.enabled(featureAtomicEmit) &&
hookSetObj.isFieldPresent(sfHookAtomicEmitFee))
return temDISABLED;
for (auto const& hookSetElement : hookSetObj)
{
auto const& name = hookSetElement.getFName();
@@ -736,7 +777,7 @@ SetHook::preflight(PreflightContext const& ctx)
name != sfHookNamespace && name != sfHookParameters &&
name != sfHookOn && name != sfHookGrants &&
name != sfHookApiVersion && name != sfFlags &&
name != sfHookCanEmit)
name != sfHookCanEmit && name != sfHookAtomicEmitFee)
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOK_INVALID_FIELD << ")["
@@ -1242,6 +1283,10 @@ SetHook::setHook()
std::optional<uint256> newHookCanEmit;
std::optional<uint256> defHookCanEmit;
std::optional<STAmount> oldHookAtomicEmitFee;
std::optional<STAmount> newHookAtomicEmitFee;
std::optional<STAmount> defHookAtomicEmitFee;
// 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
@@ -1310,6 +1355,16 @@ SetHook::setHook()
oldHookCanEmit = oldHook->get().getFieldH256(sfHookCanEmit);
else if (defHookCanEmit)
oldHookCanEmit = *defHookCanEmit;
if (oldDefSLE && oldDefSLE->isFieldPresent(sfHookAtomicEmitFee))
defHookAtomicEmitFee =
oldDefSLE->getFieldAmount(sfHookAtomicEmitFee);
if (oldHook && oldHook->get().isFieldPresent(sfHookAtomicEmitFee))
oldHookAtomicEmitFee =
oldHook->get().getFieldAmount(sfHookAtomicEmitFee);
else if (defHookAtomicEmitFee)
oldHookAtomicEmitFee = *defHookAtomicEmitFee;
}
// in preparation for three way merge populate fields if they are
@@ -1329,6 +1384,10 @@ SetHook::setHook()
if (hookSetObj->get().isFieldPresent(sfHookCanEmit))
newHookCanEmit = hookSetObj->get().getFieldH256(sfHookCanEmit);
if (hookSetObj->get().isFieldPresent(sfHookAtomicEmitFee))
newHookAtomicEmitFee =
hookSetObj->get().getFieldAmount(sfHookAtomicEmitFee);
if (hookSetObj->get().isFieldPresent(sfHookNamespace))
{
newNamespace = hookSetObj->get().getFieldH256(sfHookNamespace);
@@ -1485,6 +1544,17 @@ SetHook::setHook()
newHook.setFieldH256(sfHookCanEmit, *newHookCanEmit);
}
// set the hookAtomeicFee field if it differs from definition
if (newHookAtomicEmitFee)
{
if (defHookAtomicEmitFee.has_value() &&
*defHookAtomicEmitFee == *newHookAtomicEmitFee)
{
if (newHook.isFieldPresent(sfHookAtomicEmitFee))
newHook.makeFieldAbsent(sfHookAtomicEmitFee);
}
}
// parameters
if (hookSetObj->get().isFieldPresent(sfHookParameters) &&
hookSetObj->get().getFieldArray(sfHookParameters).empty())
@@ -1660,6 +1730,11 @@ SetHook::setHook()
sfHookCallbackFee,
XRPAmount{
hook::computeExecutionFee(maxInstrCountCbak)});
if (hookSetObj->get().isFieldPresent(sfHookAtomicEmitFee))
newHookDef->setFieldAmount(
sfHookAtomicEmitFee,
hookSetObj->get().getFieldAmount(
sfHookAtomicEmitFee));
if (flags)
newHookDef->setFieldU32(sfFlags, newFlags);

View File

@@ -289,15 +289,23 @@ Transactor::calculateHookChainFee(
if (hook::canHook(tx.getTxnType(), hookOn) &&
(!collectCallsOnly || (flags & hook::hsfCOLLECT)))
{
XRPAmount const toAdd{hookDef->getFieldAmount(sfFee).xrp().drops()};
XRPAmount const toAddFee{
hookDef->getFieldAmount(sfFee).xrp().drops()};
XRPAmount const toAddAtomicEmitFee{
hookObj.isFieldPresent(sfHookAtomicEmitFee)
? hookObj.getFieldAmount(sfHookAtomicEmitFee).xrp().drops()
: hookDef->isFieldPresent(sfHookAtomicEmitFee)
? hookDef->getFieldAmount(sfHookAtomicEmitFee).xrp().drops()
: 0};
// this overflow should never happen, if somehow it does
// fee is set to the largest possible valid xrp value to force
// fail the transaction
if (fee + toAdd < fee)
if (fee + toAddFee + toAddAtomicEmitFee < fee)
fee = XRPAmount{INITIAL_XRP.drops()};
else
fee += toAdd;
fee += toAddFee + toAddAtomicEmitFee;
}
}
@@ -493,7 +501,7 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
if (feePaid < feeDue)
{
JLOG(ctx.j.trace())
JLOG(ctx.j.fatal())
<< "Insufficient fee paid: " << to_string(feePaid) << "/"
<< to_string(feeDue);
return telINSUF_FEE_P;
@@ -1368,6 +1376,111 @@ Transactor::reset(XRPAmount fee)
return {ter, fee};
}
std::pair<TER, XRPAmount>
Transactor::checkInvariants(TER result, XRPAmount fee)
{
// Check invariants: if `tecINVARIANT_FAILED` is not returned, we can
// proceed to apply the tx
result = ctx_.checkInvariants(result, fee);
if (result == tecINVARIANT_FAILED)
{
// if invariants checking failed again, reset the context and
// attempt to only claim a fee.
auto const resetResult = reset(fee);
if (!isTesSuccess(resetResult.first))
result = resetResult.first;
fee = resetResult.second;
// Check invariants again to ensure the fee claiming doesn't
// violate invariants.
if (isTesSuccess(result) || isTecClaim(result))
result = ctx_.checkInvariants(result, fee);
}
return {result, fee};
}
void
Transactor::balanceRewards(TER result)
{
TxMeta metaRaw = ctx_.generateProvisionalMeta();
metaRaw.setResult(result, 0);
STObject const meta = metaRaw.getAsObject();
uint32_t lgrCur = view().seq();
bool const has240819 = view().rules().enabled(fix240819);
bool const has240911 = view().rules().enabled(fix240911);
auto const& sfRewardFields =
*(ripple::SField::knownCodeToField.at(917511 - has240819));
// iterate all affected balances
for (auto const& node : meta.getFieldArray(sfAffectedNodes))
{
SField const& metaType = node.getFName();
uint16_t nodeType = node.getFieldU16(sfLedgerEntryType);
// we only care about ltACCOUNT_ROOT objects being modified or
// created
if (nodeType != ltACCOUNT_ROOT || metaType == sfDeletedNode)
continue;
if (!node.isFieldPresent(sfRewardFields) ||
!node.isFieldPresent(sfLedgerIndex))
continue;
auto sle = view().peek(
Keylet{ltACCOUNT_ROOT, node.getFieldH256(sfLedgerIndex)});
if (!sle)
continue;
if (!sle->isFieldPresent(sfRewardLgrFirst) ||
!sle->isFieldPresent(sfRewardLgrLast) ||
!sle->isFieldPresent(sfRewardAccumulator))
continue;
STObject& finalFields = (const_cast<STObject&>(node))
.getField(sfRewardFields)
.downcast<STObject>();
if (!finalFields.isFieldPresent(sfBalance))
continue;
uint64_t bal =
finalFields.getFieldAmount(sfBalance).xrp().drops() / 1'000'000;
if (bal == 0)
continue;
uint32_t lgrLast = sle->getFieldU32(sfRewardLgrLast);
uint32_t lgrElapsed = lgrCur - lgrLast;
// overflow safety
if (!has240911 &&
(lgrElapsed > lgrCur || lgrElapsed > lgrLast || lgrElapsed == 0))
continue;
if (has240911 && (lgrElapsed > lgrCur || lgrElapsed == 0))
continue;
uint64_t accum = sle->getFieldU64(sfRewardAccumulator);
uint64_t accumNew = accum + bal * ((uint64_t)lgrElapsed);
// check for overflow
if (accumNew < accum)
continue;
sle->setFieldU64(sfRewardAccumulator, accumNew);
sle->setFieldU32(sfRewardLgrLast, lgrCur);
view().update(sle);
}
}
TER
Transactor::executeHookChain(
std::shared_ptr<ripple::STLedgerEntry const> const& hookSLE,
@@ -1419,6 +1532,13 @@ Transactor::executeHookChain(
uint256 hookCanEmit = hook::getHookCanEmit(hookObj, hookDef);
XRPAmount atomicEmitFeeRemaining =
hookObj.isFieldPresent(sfHookAtomicEmitFee)
? hookObj.getFieldAmount(sfHookAtomicEmitFee).xrp()
: hookDef->isFieldPresent(sfHookAtomicEmitFee)
? hookDef->getFieldAmount(sfHookAtomicEmitFee).xrp()
: XRPAmount(0);
uint32_t flags =
(hookObj.isFieldPresent(sfFlags) ? hookObj.getFieldU32(sfFlags)
: hookDef->getFieldU32(sfFlags));
@@ -1460,6 +1580,7 @@ Transactor::executeHookChain(
parameters,
hookParamOverrides,
stateMap,
atomicEmitFeeRemaining,
ctx_,
account,
hasCallback,
@@ -1587,6 +1708,8 @@ Transactor::doHookCallback(
uint256 hookCanEmit = hook::getHookCanEmit(hookObj, hookDef);
XRPAmount atomicEmitFeeRemaining = XRPAmount(0);
// fetch the namespace either from the hook object of, if absent, the
// hook def
uint256 const& ns =
@@ -1618,6 +1741,7 @@ Transactor::doHookCallback(
parameters,
{},
stateMap,
atomicEmitFeeRemaining,
ctx_,
callbackAccountID,
true,
@@ -1867,6 +1991,8 @@ Transactor::doAgainAsWeak(
uint256 hookCanEmit = hook::getHookCanEmit(hookObj, hookDef);
XRPAmount atomicEmitFeeRemaining{0};
// fetch the namespace either from the hook object of, if absent, the
// hook def
uint256 const& ns =
@@ -1893,6 +2019,7 @@ Transactor::doAgainAsWeak(
parameters,
{},
stateMap,
atomicEmitFeeRemaining,
ctx_,
hookAccountID,
hookDef->isFieldPresent(sfHookCallbackFee),
@@ -2164,25 +2291,10 @@ Transactor::operator()()
if (applied)
{
// Check invariants: if `tecINVARIANT_FAILED` is not returned, we can
// proceed to apply the tx
result = ctx_.checkInvariants(result, fee);
auto const invariantsResult = checkInvariants(result, fee);
if (result == tecINVARIANT_FAILED)
{
// if invariants checking failed again, reset the context and
// attempt to only claim a fee.
auto const resetResult = reset(fee);
if (!isTesSuccess(resetResult.first))
result = resetResult.first;
fee = resetResult.second;
// Check invariants again to ensure the fee claiming doesn't
// violate invariants.
if (isTesSuccess(result) || isTecClaim(result))
result = ctx_.checkInvariants(result, fee);
}
result = invariantsResult.first;
fee = invariantsResult.second;
// We ran through the invariant checker, which can, in some cases,
// return a tef error code. Don't apply the transaction in that case.
@@ -2192,81 +2304,7 @@ Transactor::operator()()
if (applied && view().rules().enabled(featureBalanceRewards))
{
TxMeta metaRaw = ctx_.generateProvisionalMeta();
metaRaw.setResult(result, 0);
STObject const meta = metaRaw.getAsObject();
uint32_t lgrCur = view().seq();
bool const has240819 = view().rules().enabled(fix240819);
bool const has240911 = view().rules().enabled(fix240911);
auto const& sfRewardFields =
*(ripple::SField::knownCodeToField.at(917511 - has240819));
// iterate all affected balances
for (auto const& node : meta.getFieldArray(sfAffectedNodes))
{
SField const& metaType = node.getFName();
uint16_t nodeType = node.getFieldU16(sfLedgerEntryType);
// we only care about ltACCOUNT_ROOT objects being modified or
// created
if (nodeType != ltACCOUNT_ROOT || metaType == sfDeletedNode)
continue;
if (!node.isFieldPresent(sfRewardFields) ||
!node.isFieldPresent(sfLedgerIndex))
continue;
auto sle = view().peek(
Keylet{ltACCOUNT_ROOT, node.getFieldH256(sfLedgerIndex)});
if (!sle)
continue;
if (!sle->isFieldPresent(sfRewardLgrFirst) ||
!sle->isFieldPresent(sfRewardLgrLast) ||
!sle->isFieldPresent(sfRewardAccumulator))
continue;
STObject& finalFields = (const_cast<STObject&>(node))
.getField(sfRewardFields)
.downcast<STObject>();
if (!finalFields.isFieldPresent(sfBalance))
continue;
uint64_t bal =
finalFields.getFieldAmount(sfBalance).xrp().drops() / 1'000'000;
if (bal == 0)
continue;
uint32_t lgrLast = sle->getFieldU32(sfRewardLgrLast);
uint32_t lgrElapsed = lgrCur - lgrLast;
// overflow safety
if (!has240911 &&
(lgrElapsed > lgrCur || lgrElapsed > lgrLast ||
lgrElapsed == 0))
continue;
if (has240911 && (lgrElapsed > lgrCur || lgrElapsed == 0))
continue;
uint64_t accum = sle->getFieldU64(sfRewardAccumulator);
uint64_t accumNew = accum + bal * ((uint64_t)lgrElapsed);
// check for overflow
if (accumNew < accum)
continue;
sle->setFieldU64(sfRewardAccumulator, accumNew);
sle->setFieldU32(sfRewardLgrLast, lgrCur);
view().update(sle);
}
balanceRewards(result);
}
// Post-application (Weak TSH/AAW) Hooks are executed here.
@@ -2352,6 +2390,53 @@ Transactor::operator()()
applied = false;
}
if (metadata && metadata->hasHookEmissions())
{
OpenView emittedTxnsView(batch_view, ctx_.openView());
bool const emitResult = hook::emitAtomicTransactions(
ctx_.app,
emittedTxnsView,
metadata->getTxID(),
metadata->getHookEmissions(),
j_);
printf("emitResult: %d\n", emitResult);
if (emitResult)
{
emittedTxnsView.apply(ctx_.openView());
}
else
{
// reset context
result = tecHOOK_EMIT_FAILED;
auto const resetResult = reset(fee);
if (!isTesSuccess(resetResult.first))
result = resetResult.first;
fee = resetResult.second;
// InvariantCheck
auto const invariantsResult = checkInvariants(result, fee);
result = invariantsResult.first;
fee = invariantsResult.second;
// BalanceRewards
balanceRewards(result);
// Add Hooks metadata (use metadata)
// apply
metadata = ctx_.apply(result);
}
}
ctx_.finalize();
if (ctx_.flags() & tapATOMIC_EMIT && !isTesSuccess(result))
{
JLOG(j_.trace()) << "HookEmit[]: Atomic emit failed: "
<< transToken(result);
JLOG(j_.trace()) << "HookEmit[]: Atomic emit failed: "
<< ctx_.tx.getFullText();
}
JLOG(j_.trace()) << (applied ? "applied " : "not applied ")
<< transToken(result);

View File

@@ -57,7 +57,8 @@ public:
, j(j_)
{
XRPL_ASSERT(
(flags_ & tapBATCH) == tapBATCH, "Batch apply flag should be set");
(flags_ & (tapBATCH | tapATOMIC_EMIT)) > 0,
"Batch or AtomicEmit flag should be set");
}
PreflightContext(
@@ -69,7 +70,8 @@ public:
: app(app_), tx(tx_), rules(rules_), flags(flags_), j(j_)
{
XRPL_ASSERT(
(flags_ & tapBATCH) == 0, "Batch apply flag should not be set");
(flags_ & (tapBATCH | tapATOMIC_EMIT)) == 0,
"Batch or AtomicEmit flag should not be set");
}
PreflightContext&
@@ -105,8 +107,9 @@ public:
, j(j_)
{
XRPL_ASSERT(
parentBatchId.has_value() == ((flags_ & tapBATCH) == tapBATCH),
"Parent Batch ID should be set if batch apply flag is set");
parentBatchId.has_value() ==
((flags_ & (tapBATCH | tapATOMIC_EMIT)) > 0),
"Parent Batch ID should be set if batch or AtomicEmit flag is set");
}
PreclaimContext(
@@ -126,7 +129,8 @@ public:
j_)
{
XRPL_ASSERT(
(flags_ & tapBATCH) == 0, "Batch apply flag should not be set");
(flags_ & (tapBATCH | tapATOMIC_EMIT)) == 0,
"Batch or AtomicEmit flag should not be set");
}
PreclaimContext&
@@ -312,6 +316,12 @@ private:
std::pair<TER, XRPAmount>
reset(XRPAmount fee);
std::pair<TER, XRPAmount>
checkInvariants(TER result, XRPAmount fee);
void
balanceRewards(TER result);
TER
consumeSeqProxy(SLE::pointer const& sleAccount);
TER

View File

@@ -49,7 +49,10 @@ enum ApplyFlags : std::uint32_t {
// Transaction shouldn't be applied
// Signatures shouldn't be checked
tapDRY_RUN = 0x1000
tapDRY_RUN = 0x1000,
// Transaction is executing as part of a atomic emit
tapATOMIC_EMIT = 0x2000,
};
constexpr ApplyFlags

View File

@@ -0,0 +1,105 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_LEDGER_OPENVIEWSANDBOX_H_INCLUDED
#define RIPPLE_LEDGER_OPENVIEWSANDBOX_H_INCLUDED
#include <xrpld/ledger/OpenView.h>
#include <memory>
namespace ripple {
class OpenViewSandbox
{
private:
OpenView& parent_;
std::unique_ptr<OpenView> sandbox_;
public:
using key_type = ReadView::key_type;
OpenViewSandbox(OpenView& parent)
: parent_(parent)
, sandbox_(std::make_unique<OpenView>(batch_view, parent))
{
}
void
rawErase(std::shared_ptr<SLE> const& sle)
{
sandbox_->rawErase(sle);
}
void
rawInsert(std::shared_ptr<SLE> const& sle)
{
sandbox_->rawInsert(sle);
}
void
rawReplace(std::shared_ptr<SLE> const& sle)
{
sandbox_->rawReplace(sle);
}
void
rawDestroyXRP(XRPAmount const& fee)
{
sandbox_->rawDestroyXRP(fee);
}
void
rawTxInsert(
key_type const& key,
std::shared_ptr<Serializer const> const& txn,
std::shared_ptr<Serializer const> const& metaData)
{
sandbox_->rawTxInsert(key, txn, metaData);
}
void
commit()
{
sandbox_->apply(parent_);
sandbox_ = std::make_unique<OpenView>(batch_view, parent_);
}
void
discard()
{
sandbox_ = std::make_unique<OpenView>(batch_view, parent_);
}
OpenView const&
view() const
{
return *sandbox_;
}
OpenView&
view()
{
return *sandbox_;
}
};
} // namespace ripple
#endif