Skip to content

Commit efd5f9f

Browse files
authored
prepare Hook API (#413)
1 parent 309e517 commit efd5f9f

File tree

12 files changed

+1167
-91
lines changed

12 files changed

+1167
-91
lines changed

hook/extern.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,5 +329,12 @@ meta_slot(uint32_t slot_no);
329329
extern int64_t
330330
xpop_slot(uint32_t slot_no_tx, uint32_t slot_no_meta);
331331

332+
extern int64_t
333+
prepare(
334+
uint32_t write_ptr,
335+
uint32_t write_len,
336+
uint32_t read_ptr,
337+
uint32_t read_len);
338+
332339
#define HOOK_EXTERN
333340
#endif // HOOK_EXTERN

src/ripple/app/hook/Enum.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
// Override uint256, Feature and Rules for guard checker build
1515
#define uint256 std::string
1616
#define featureHooksUpdate1 "1"
17+
#define featureHooksUpdate2 "1"
1718
#define fix20250131 "1"
1819
namespace hook_api {
1920
struct Rules

src/ripple/app/hook/HookAPI.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ class HookAPI
5858
// sto_erase(): same as sto_emplace with field_object = nullopt
5959

6060
/// etxn APIs
61+
Expected<Bytes, HookReturnCode>
62+
prepare(Slice const& txBlob) const;
63+
6164
Expected<std::shared_ptr<Transaction>, HookReturnCode>
6265
emit(Slice const& txBlob) const;
6366

src/ripple/app/hook/hook_api.macro

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,3 +367,8 @@ HOOK_API_DEFINITION(
367367
HOOK_API_DEFINITION(
368368
int64_t, xpop_slot, (uint32_t, uint32_t),
369369
featureHooksUpdate1)
370+
371+
// int64_t prepare(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
372+
HOOK_API_DEFINITION(
373+
int64_t, prepare, (uint32_t, uint32_t, uint32_t, uint32_t),
374+
featureHooksUpdate2)

src/ripple/app/hook/impl/HookAPI.cpp

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <ripple/app/ledger/OpenLedger.h>
55
#include <ripple/app/ledger/TransactionMaster.h>
66
#include <ripple/app/tx/impl/Import.h>
7+
#include <ripple/protocol/STParsedJSON.h>
78
#include <cfenv>
89

910
namespace hook {
@@ -625,6 +626,122 @@ HookAPI::sto_emplace(
625626
// sto_erase
626627

627628
/// etxn APIs
629+
Expected<Bytes, HookReturnCode>
630+
HookAPI::prepare(Slice const& txBlob) const
631+
{
632+
auto& applyCtx = hookCtx.applyCtx;
633+
auto j = applyCtx.app.journal("View");
634+
635+
if (hookCtx.expected_etxn_count < 0)
636+
return Unexpected(PREREQUISITE_NOT_MET);
637+
638+
Json::Value json;
639+
640+
// std::shared_ptr<STObject const> stpTrans;
641+
try
642+
{
643+
SerialIter sitTrans{txBlob};
644+
json =
645+
STObject(std::ref(sitTrans), sfGeneric).getJson(JsonOptions::none);
646+
}
647+
catch (std::exception& e)
648+
{
649+
JLOG(j.trace()) << "HookInfo[" << HC_ACC() << "]: prepare Failed "
650+
<< e.what() << "\n";
651+
return Unexpected(INVALID_ARGUMENT);
652+
}
653+
654+
// add a dummy fee
655+
json[jss::Fee] = "0";
656+
657+
// force key to empty
658+
json[jss::SigningPubKey] =
659+
"000000000000000000000000000000000000000000000000000000000000000000";
660+
661+
// force sequence to 0
662+
json[jss::Sequence] = Json::Value(0u);
663+
664+
std::string raddr = encodeBase58Token(
665+
TokenType::AccountID, hookCtx.result.account.data(), 20);
666+
667+
json[jss::Account] = raddr;
668+
669+
uint32_t seq = applyCtx.view().info().seq;
670+
if (!json.isMember(jss::FirstLedgerSequence))
671+
json[jss::FirstLedgerSequence] = Json::Value(seq + 1);
672+
673+
if (!json.isMember(jss::LastLedgerSequence))
674+
json[jss::LastLedgerSequence] = Json::Value(seq + 5);
675+
676+
uint8_t details[512];
677+
if (!json.isMember(jss::EmitDetails))
678+
{
679+
auto ret = etxn_details(details);
680+
if (!ret || ret.value() < 2)
681+
return Unexpected(INTERNAL_ERROR);
682+
683+
// truncate the head and tail (emit details object markers)
684+
Slice s(
685+
reinterpret_cast<void const*>(details + 1),
686+
(size_t)(ret.value() - 2));
687+
688+
// std::cout << "emitdets: " << strHex(s) << "\n";
689+
try
690+
{
691+
SerialIter sit{s};
692+
STObject st{sit, sfEmitDetails};
693+
json[jss::EmitDetails] = st.getJson(JsonOptions::none);
694+
}
695+
catch (std::exception const& ex)
696+
{
697+
JLOG(j.warn()) << "HookInfo[" << HC_ACC() << "]: Exception in "
698+
<< __func__ << ": " << ex.what();
699+
return Unexpected(INTERNAL_ERROR);
700+
}
701+
}
702+
703+
// {
704+
// const std::string flat = Json::FastWriter().write(json);
705+
// std::cout << "intermediate: `" << flat << "`\n";
706+
// }
707+
708+
Blob tx_blob;
709+
{
710+
STParsedJSONObject parsed(std::string(jss::tx_json), json);
711+
if (!parsed.object.has_value())
712+
return Unexpected(INVALID_ARGUMENT);
713+
714+
STObject& obj = *(parsed.object);
715+
716+
// serialize it
717+
Serializer s;
718+
obj.add(s);
719+
tx_blob = s.getData();
720+
}
721+
722+
// run it through the fee estimate, this doubles as a txn sanity check
723+
auto fee = etxn_fee_base(Slice(tx_blob.data(), tx_blob.size()));
724+
if (!fee)
725+
return Unexpected(INVALID_ARGUMENT);
726+
727+
json[jss::Fee] = to_string(fee.value());
728+
729+
{
730+
STParsedJSONObject parsed(std::string(jss::tx_json), json);
731+
if (!parsed.object.has_value())
732+
return Unexpected(INVALID_ARGUMENT);
733+
734+
STObject& obj = *(parsed.object);
735+
736+
// serialize it
737+
Serializer s;
738+
obj.add(s);
739+
tx_blob = s.getData();
740+
}
741+
742+
return tx_blob;
743+
}
744+
628745
Expected<std::shared_ptr<Transaction>, HookReturnCode>
629746
HookAPI::emit(Slice const& txBlob) const
630747
{

src/ripple/app/hook/impl/applyHook.cpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2575,6 +2575,43 @@ DEFINE_HOOK_FUNCTION(
25752575
HOOK_TEARDOWN();
25762576
}
25772577

2578+
DEFINE_HOOK_FUNCTION(
2579+
int64_t,
2580+
prepare,
2581+
uint32_t write_ptr,
2582+
uint32_t write_len,
2583+
uint32_t read_ptr,
2584+
uint32_t read_len)
2585+
{
2586+
HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx,
2587+
// hookCtx on current stack
2588+
2589+
if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length))
2590+
return OUT_OF_BOUNDS;
2591+
2592+
if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length))
2593+
return OUT_OF_BOUNDS;
2594+
2595+
ripple::Slice txBlob{
2596+
reinterpret_cast<const void*>(memory + read_ptr), read_len};
2597+
2598+
auto const res = api.prepare(txBlob);
2599+
if (!res)
2600+
return res.error();
2601+
2602+
auto tx_blob = res.value();
2603+
2604+
WRITE_WASM_MEMORY_AND_RETURN(
2605+
write_ptr,
2606+
tx_blob.size(),
2607+
tx_blob.data(),
2608+
tx_blob.size(),
2609+
memory,
2610+
memory_length);
2611+
2612+
HOOK_TEARDOWN();
2613+
}
2614+
25782615
/* Emit a transaction from this hook. Transaction must be in STObject form,
25792616
* fully formed and valid. XRPLD does not modify transactions it only checks
25802617
* them for validity. */

src/ripple/protocol/Feature.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ namespace detail {
7474
// Feature.cpp. Because it's only used to reserve storage, and determine how
7575
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
7676
// the actual number of amendments. A LogicError on startup will verify this.
77-
static constexpr std::size_t numFeatures = 90;
77+
static constexpr std::size_t numFeatures = 91;
7878

7979
/** Amendments that this server supports and the default voting behavior.
8080
Whether they are enabled depends on the Rules defined in the validated
@@ -378,6 +378,7 @@ extern uint256 const fixInvalidTxFlags;
378378
extern uint256 const featureExtendedHookState;
379379
extern uint256 const fixCronStacking;
380380
extern uint256 const fixHookAPI20251128;
381+
extern uint256 const featureHooksUpdate2;
381382
} // namespace ripple
382383

383384
#endif

src/ripple/protocol/impl/Feature.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,7 @@ REGISTER_FIX (fixInvalidTxFlags, Supported::yes, VoteBehavior::De
484484
REGISTER_FEATURE(ExtendedHookState, Supported::yes, VoteBehavior::DefaultNo);
485485
REGISTER_FIX (fixCronStacking, Supported::yes, VoteBehavior::DefaultYes);
486486
REGISTER_FIX (fixHookAPI20251128, Supported::yes, VoteBehavior::DefaultYes);
487+
REGISTER_FEATURE(HooksUpdate2, Supported::yes, VoteBehavior::DefaultNo);
487488

488489
// The following amendments are obsolete, but must remain supported
489490
// because they could potentially get enabled.

src/ripple/protocol/jss.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ JSS(Invalid); //
101101
JSS(Invoke); // transaction type
102102
JSS(InvoiceID); // field
103103
JSS(LastLedgerSequence); // in: TransactionSign; field
104+
JSS(FirstLedgerSequence); // in: TransactionSign; field
104105
JSS(LedgerHashes); // ledger type.
105106
JSS(LimitAmount); // field.
106107
JSS(NetworkID); // field.
@@ -140,6 +141,7 @@ JSS(HookState); // ledger type.
140141
JSS(HookStateData); // field.
141142
JSS(HookStateKey); // field.
142143
JSS(EmittedTxn); // ledger type.
144+
JSS(EmitDetails); // field.
143145
JSS(SignerList); // ledger type.
144146
JSS(SignerListSet); // transaction type.
145147
JSS(SigningPubKey); // field.

src/test/app/HookAPI_test.cpp

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
//==============================================================================
1919
#include <ripple/app/hook/HookAPI.h>
2020
#include <ripple/basics/StringUtilities.h>
21+
#include <ripple/beast/unit_test/suite.hpp>
2122
#include <ripple/json/json_writer.h>
23+
#include <ripple/protocol/SField.h>
2224
#include <ripple/protocol/STAccount.h>
2325
#include <limits>
2426
#include <test/app/Import_json.h>
@@ -75,6 +77,84 @@ class HookAPI_test : public beast::unit_test::suite
7577
BEAST_EXPECT(true);
7678
}
7779

80+
void
81+
test_prepare(FeatureBitset features)
82+
{
83+
testcase("Test prepare");
84+
using namespace jtx;
85+
86+
auto const alice = Account{"alice"};
87+
auto const bob = Account{"bob"};
88+
89+
using namespace hook_api;
90+
Env env{*this, features};
91+
92+
STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {});
93+
OpenView ov{*env.current()};
94+
ApplyContext applyCtx = createApplyContext(env, ov, invokeTx);
95+
96+
STTx const emitInvokeTx = STTx(ttINVOKE, [&](STObject& obj) {});
97+
98+
{
99+
// PREREQUISITE_NOT_MET
100+
auto hookCtx =
101+
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
102+
auto& api = hookCtx.api();
103+
auto tx = emitInvokeTx;
104+
Serializer s = tx.getSerializer();
105+
s.add8(0); // invalid value
106+
auto const result = api.prepare(s.slice());
107+
BEAST_EXPECT(result.error() == PREREQUISITE_NOT_MET);
108+
}
109+
110+
{
111+
// Invalid txn
112+
auto hookCtx = makeStubHookContext(
113+
applyCtx,
114+
alice.id(),
115+
alice.id(),
116+
{
117+
.expected_etxn_count = 1,
118+
});
119+
auto& api = hookCtx.api();
120+
auto tx = emitInvokeTx;
121+
Serializer s = tx.getSerializer();
122+
s.add8(0); // invalid value
123+
auto const result = api.prepare(s.slice());
124+
BEAST_EXPECT(result.error() == INVALID_ARGUMENT);
125+
}
126+
127+
{
128+
// success
129+
auto hookCtx = makeStubHookContext(
130+
applyCtx,
131+
alice.id(),
132+
alice.id(),
133+
{
134+
.expected_etxn_count = 1,
135+
});
136+
auto& api = hookCtx.api();
137+
auto tx = emitInvokeTx;
138+
Serializer s = tx.getSerializer();
139+
auto const result = api.prepare(s.slice());
140+
BEAST_EXPECT(result.has_value());
141+
142+
SerialIter sit(Slice(result.value().data(), result.value().size()));
143+
STObject st(sit, sfGeneric);
144+
BEAST_EXPECT(st.getFieldAmount(sfFee) > XRPAmount(0));
145+
BEAST_EXPECT(st.getFieldU32(sfSequence) == 0);
146+
BEAST_EXPECT(st.getAccountID(sfAccount) == alice.id());
147+
auto const seq = applyCtx.view().info().seq;
148+
BEAST_EXPECT(st.getFieldU32(sfFirstLedgerSequence) == seq + 1);
149+
BEAST_EXPECT(st.getFieldU32(sfLastLedgerSequence) == seq + 5);
150+
BEAST_EXPECT(st.isFieldPresent(sfEmitDetails));
151+
152+
auto const result2 =
153+
api.emit(Slice(result.value().data(), result.value().size()));
154+
BEAST_EXPECT(result2.has_value());
155+
}
156+
}
157+
78158
void
79159
test_emit(FeatureBitset features)
80160
{
@@ -4441,6 +4521,7 @@ class HookAPI_test : public beast::unit_test::suite
44414521
test_rollback(features);
44424522
testGuards(features);
44434523

4524+
test_prepare(features);
44444525
test_emit(features);
44454526
test_etxn_burden(features);
44464527
test_etxn_generation(features);

0 commit comments

Comments
 (0)