state change update to persist changes between hook executions in the same otxn, not compiling

This commit is contained in:
Richard Holland
2022-02-08 14:00:39 +00:00
parent 7596d8bbd0
commit f185f6418e
3 changed files with 222 additions and 174 deletions

View File

@@ -32,7 +32,6 @@ enum HookSetOperation : int8_t
hsoUPDATE = 5
};
namespace hook
{
struct HookContext;
@@ -40,6 +39,13 @@ namespace hook
bool isEmittedTxn(ripple::STTx const& tx);
// this map type acts as both a read and write cache for hook execution
using HookStateMap = std::map<ripple::AccountID, // account that owns the state
std::map<ripple::uint256, // namespace
std::map<ripple::uint256, // key
std::pair<
bool, // is modified from ledger value
ripple::Blob>>>>; // the value
namespace log
{
@@ -410,6 +416,7 @@ namespace hook
std::vector<uint8_t>,
std::vector<uint8_t>
>> const& hookParamOverrides,
std::shared_ptr<HookStateMap>& stateMap,
ripple::ApplyContext& applyCtx,
ripple::AccountID const& account, /* the account the hook is INSTALLED ON not always the otxn account */
bool callback = false,
@@ -441,14 +448,8 @@ namespace hook
ripple::uint256 const& hookNamespace;
std::queue<std::shared_ptr<ripple::Transaction>> emittedTxn {}; // etx stored here until accept/rollback
std::shared_ptr<
std::map<ripple::AccountID, // account to whom the state belongs
std::map<ripple::uint256, // namespace
std::map<ripple::uint256, // state key
std::pair<
bool, // has been modified
ripple::Blob>>>>> // actual state data
changedState;
std::shared_ptr<HookStateMap> stateMap;
uint16_t changedStateCount = 0;
std::map<
ripple::uint256, // hook hash
std::map<
@@ -514,18 +515,26 @@ namespace hook
ripple::uint256 const & key,
ripple::Slice const& data);
// commit changes to ledger flags
enum cclFlags : uint8_t {
cclREMOVE = 0b10U,
cclAPPLY = 0b01U
};
// finalize the changes the hook made to the ledger
void commitChangesToLedger( hook::HookResult& hookResult, ripple::ApplyContext&, uint8_t );
// write hook execution metadata and remove emitted transaction ledger entries
ripple::TER
finalizeHookResult(
hook::HookResult& hookResult,
ripple::ApplyContext&,
bool doEmit);
// write state map to ledger
ripple::TER
finalizeHookState(
std::shared_ptr<HookStateMap>&,
ripple::ApplyContext&);
// if the txn being executed was an emitted txn then this removes it from the emission directory
ripple::TER
removeEmissionEntry(
ripple::ApplyContext& applyCtx);
// RH TODO: call destruct for these on rippled shutdown
#define ADD_HOOK_FUNCTION(F, ctx)\
{\
WasmEdge_FunctionInstanceContext* hf = WasmEdge_FunctionInstanceCreate(\

View File

@@ -939,6 +939,7 @@ gatherHookParameters(
bool /* error = true */
executeHookChain(
std::shared_ptr<ripple::STLedgerEntry const> const& hookSLE,
std::shared_ptr<hook::HookStateMap>& stateMap,
std::vector<hook::HookResult>& results,
int& executedHookCount,
ripple::AccountID const& account,
@@ -956,6 +957,7 @@ executeHookChain(
auto const& hooks = hookSLE->getFieldArray(sfHooks);
int hook_no = 0;
for (auto const& hook : hooks)
{
ripple::STObject const* hookObj = dynamic_cast<ripple::STObject const*>(&hook);
@@ -999,7 +1001,11 @@ executeHookChain(
// gather parameters
std::map<std::vector<uint8_t>, std::vector<uint8_t>> parameters;
if (gatherHookParameters(hookDef, hookObj, parameters, j_))
{
JLOG(j_.warn())
<< "HookError[]: Failure: gatherHookParameters failed)";
return true;
}
results.push_back(
hook::apply(
@@ -1009,6 +1015,7 @@ executeHookChain(
hookDef->getFieldVL(sfCreateCode),
parameters,
hookParamOverrides,
stateMap,
ctx,
account,
false,
@@ -1087,6 +1094,10 @@ Transactor::operator()()
(result == tesSUCCESS || result == tecHOOK_REJECTED))
{
// this state map will be shared across all hooks in this execution chain
// and any associated chains which are executed during this transaction also
std::shared_ptr<hook::HookStateMap> stateMap = std::make_shared<hook::HookStateMap>();
auto const& ledger = ctx_.view();
auto const& accountID = ctx_.tx.getAccountID(sfAccount);
std::vector<hook::HookResult> sendResults;
@@ -1096,7 +1107,8 @@ Transactor::operator()()
// First check if the Sending account has any hooks that can be fired
if (hooksSending && hooksSending->isFieldPresent(sfHooks) && !ctx_.emitted())
rollback = executeHookChain(hooksSending, sendResults, executedHookCount, accountID, ctx_, j_, result);
rollback = executeHookChain(hooksSending, stateMap,
sendResults, executedHookCount, accountID, ctx_, j_, result);
// Next check if the Receiving account has as a hook that can be fired...
std::optional<AccountID>
@@ -1107,12 +1119,23 @@ Transactor::operator()()
auto const& hooksReceiving = ledger.read(keylet::hook(*destAccountID));
if (hooksReceiving && hooksReceiving->isFieldPresent(sfHooks))
rollback =
executeHookChain(hooksReceiving, recvResults, executedHookCount, *destAccountID, ctx_, j_, result);
executeHookChain(hooksReceiving, stateMap,
recvResults, executedHookCount, *destAccountID, ctx_, j_, result);
}
if (rollback && result != temMALFORMED)
result = tecHOOK_REJECTED;
// write state if all chains executed successfully
if (result == tesSUCCESS)
hook::finalizeHookState(stateMap, ctx_);
// write hook results
for (auto& sendResult: sendResults)
hook::finalizeHookResult(sendResult, ctx_, result == tesSUCCESS);
for (auto& recvResult: recvResults)
hook::finalizeHookResult(recvResult, ctx_, result == tesSUCCESS);
// Finally check if there is a callback
do
{
@@ -1138,7 +1161,6 @@ Transactor::operator()()
{
JLOG(j_.warn())
<< "HookError[]: Hook def missing on callback";
rollback = true;
break;
}
@@ -1168,31 +1190,58 @@ Transactor::operator()()
std::map<std::vector<uint8_t>, std::vector<uint8_t>> parameters;
if (gatherHookParameters(hookDef, hookObj, parameters, j_))
{
rollback = true;
JLOG(j_.warn())
<< "HookError[]: Failure: gatherHookParameters failed)";
break;
}
found = true;
// this call will clean up ltEMITTED_NODE as well
try {
hook::apply(
hookDef->getFieldH256(sfHookSetTxnID),
callbackHookHash,
ns,
hookDef->getFieldVL(sfCreateCode),
// params
parameters,
{},
ctx_,
callbackAccountID,
true,
safe_cast<TxType>(
ctx_.tx.getFieldU16(sfTransactionType)) == ttEMIT_FAILURE ? 1UL : 0UL, hook_no - 1);
try
{
// reset the stateMap
stateMap = std::make_shared<hook::HookStateMap>();
hook::HookResult callbackResult =
hook::apply(
hookDef->getFieldH256(sfHookSetTxnID),
callbackHookHash,
ns,
hookDef->getFieldVL(sfCreateCode),
// params
parameters,
{},
stateMap,
ctx_,
callbackAccountID,
true,
safe_cast<TxType>(ctx_.tx.getFieldU16(sfTransactionType)) == ttEMIT_FAILURE
? 1UL : 0UL,
hook_no - 1);
bool success = callbackResult.exitType == hook_api::ExitType::ACCEPT;
// write any state changes if cbak resulted in accept()
if (result == tesSUCCESS)
hook::finalizeHookState(stateMap, ctx_);
// write the final result
ripple::TER result =
finalizeHookResult(callbackResult, ctx_, success);
JLOG(j_.trace())
<< "HookInfo[" << HC_ACC() << "]: "
<< "Callback finalizeHookResult = "
<< result;
}
catch (std::exception& e)
{
JLOG(j_.fatal()) << "HookError[" << callbackAccountID << "]: Callback failure " << e.what();
JLOG(j_.fatal())
<< "HookError[" << callbackAccountID
<< "]: Callback failure " << e.what();
}
break;
@@ -1203,18 +1252,13 @@ Transactor::operator()()
JLOG(j_.warn())
<< "HookError[]: Hookhash not found on callback account";
}
if (rollback)
break;
}
}
while(0);
for (auto& sendResult: sendResults)
hook::commitChangesToLedger(sendResult, ctx_, result == tesSUCCESS ? hook::cclAPPLY : hook::cclREMOVE);
for (auto& recvResult: recvResults)
hook::commitChangesToLedger(recvResult, ctx_, result == tesSUCCESS ? hook::cclAPPLY : hook::cclREMOVE );
// remove emission entry if this is an emitted transaction
hook::removeEmissionEntry(ctx_);
}
uint64_t posthook_cycles = rdtsc();

View File

@@ -459,6 +459,7 @@ hook::apply(
std::vector<uint8_t>,
std::vector<uint8_t>
>> const& hookParamOverrides,
std::shared_ptr<HookStateMap> stateMap,
ApplyContext& applyCtx,
ripple::AccountID const& account, /* the account the hook is INSTALLED ON not always the otxn account */
bool callback,
@@ -479,14 +480,8 @@ hook::apply(
.account = account,
.otxnAccount = applyCtx.tx.getAccountID(sfAccount),
.hookNamespace = hookNamespace,
.changedState =
std::make_shared<
std::map<ripple::AccountID,
std::map<ripple::uint256,
std::map<ripple::uint256,
std::pair<
bool,
ripple::Blob>>>>>(),
.stateMap = stateMap,
.changedStateCount = 0,
.hookParamOverrides = hookParamOverrides,
.hookParams = hookParams,
.hookSkips = {},
@@ -519,17 +514,6 @@ hook::apply(
( hookCtx.result.exitType == hook_api::ExitType::ROLLBACK ? "ROLLBACK" : "ACCEPT" ) <<
" RS: '" << hookCtx.result.exitReason.c_str() << "' RC: " << hookCtx.result.exitCode;
// callback auto-commits on non-rollback
if (callback)
{
// importantly the callback always removes the entry from the ltEMITTED structure
uint8_t cclMode = hook::cclREMOVE;
// we will only apply changes from the callback if the callback accepted
if (hookCtx.result.exitType == hook_api::ExitType::ACCEPT)
cclMode |= hook::cclAPPLY;
commitChangesToLedger(hookCtx.result, applyCtx, cclMode);
}
return hookCtx.result;
}
@@ -632,20 +616,27 @@ lookup_state_cache(
ripple::uint256 const& ns,
ripple::uint256 const& key)
{
auto& changedState = *(hookCtx.result.changedState);
if (changedState.find(acc) == changedState.end())
std::cout << "Lookup_state_cache: acc: " << acc << " ns: " << ns << " key: " << key << "\n";
auto& stateMap = *(hookCtx.result.stateMap);
if (stateMap.find(acc) == stateMap.end())
return std::nullopt;
auto& changedStateAcc = changedState[acc];
if (changedStateAcc.find(ns) == changedStateAcc.end())
printf("here1\n");
auto& stateMapAcc = stateMap[acc];
if (stateMapAcc.find(ns) == stateMapAcc.end())
return std::nullopt;
printf("here2\n");
auto& changedStateNs = changedStateAcc[ns];
auto& stateMapNs = stateMapAcc[ns];
auto const& ret = changedStateNs.find(key);
auto const& ret = stateMapNs.find(key);
if (ret == changedStateNs.end())
if (ret == stateMapNs.end())
return std::nullopt;
printf("here3\n");
return std::cref(ret->second);
}
@@ -663,10 +654,10 @@ set_state_cache(
bool modified)
{
auto& changedState = *(hookCtx.result.changedState);
if (changedState.find(acc) == changedState.end())
auto& stateMap = *(hookCtx.result.stateMap);
if (stateMap.find(acc) == stateMap.end())
{
changedState[acc] =
stateMap[acc] =
{
{ ns,
{
@@ -679,10 +670,10 @@ set_state_cache(
return;
}
auto& changedStateAcc = changedState[acc];
if (changedStateAcc.find(ns) == changedStateAcc.end())
auto& stateMapAcc = stateMap[acc];
if (stateMapAcc.find(ns) == stateMapAcc.end())
{
changedStateAcc[ns] =
stateMapAcc[ns] =
{
{ key,
{ modified, data }
@@ -691,17 +682,23 @@ set_state_cache(
return;
}
auto& changedStateNs = changedStateAcc[ns];
if (changedStateNs.find(key) == changedStateNs.end())
auto& stateMapNs = stateMapAcc[ns];
if (stateMapNs.find(key) == stateMapNs.end())
{
changedStateNs[key] = { modified, data };
stateMapNs[key] = { modified, data };
hookCtx.changedStateCount++;
return;
}
if (modified)
changedStateNs[key].first = true;
{
if (!stateMapNs[key].first)
hookCtx.changedStateCount++;
changedStateNs[key].second = data;
stateMapNs[key].first = true;
}
stateMapNs[key].second = data;
return;
}
@@ -869,73 +866,103 @@ DEFINE_HOOK_FUNCTION(
return read_len;
}
void hook::commitChangesToLedger(
hook::HookResult& hookResult,
ripple::ApplyContext& applyCtx,
uint8_t cclMode = 0b11U)
/* Mode: (Bits)
* (MSB) (LSB)
* ------------------------
* | cclRemove | cclApply |
* ------------------------
* | 1 | 1 | Remove old ltEMITTED entry (where applicable) and apply state changes
* | 0 | 1 | Apply but don't Remove ltEMITTED entry
* | 1 | 0 | Remove but don't Apply (used when rollback on an emitted txn)
* | 0 | 0 | Invalid option
* ------------------------
*/
ripple::TER
hook::
finalizeHookState(
std::shared_ptr<HookStateMap>& stateMap,
ripple::ApplyContext& applyCtx)
{
auto const& j = applyCtx.app.journal("View");
if (cclMode == 0)
{
JLOG(j.warn()) <<
"HookError[" << HR_ACC() << "]: commitChangesToLedger called with invalid mode (00)";
return;
}
uint16_t changeCount = 0;
uint16_t change_count = 0;
// write hook state changes, if we are allowed to
if (cclMode & cclAPPLY)
{
// write all changes to state, if in "apply" mode
for (const auto& accEntry : *(hookResult.changedState)) {
const auto& acc = accEntry.first;
for (const auto& nsEntry : accEntry.second) {
const auto& ns = nsEntry.first;
for (const auto& cacheEntry : nsEntry.second) {
bool is_modified = cacheEntry.second.first;
const auto& key = cacheEntry.first;
const auto& blob = cacheEntry.second.second;
if (is_modified) {
change_count++;
// this entry isn't just cached, it was actually modified
auto slice = Slice(blob.data(), blob.size());
TER result =
setHookState(hookResult, applyCtx, acc, ns, key, slice);
if (result != tesSUCCESS)
{
JLOG(j.warn())
<< "HookError[" << HR_ACC() << "]: SetHookState failed: " << result
<< " Key: " << key
<< " Value: " << slice;
}
// ^ should not fail... checks were done before map insert
// write all changes to state, if in "apply" mode
for (const auto& accEntry : *(hookResult.stateMap)) {
const auto& acc = accEntry.first;
for (const auto& nsEntry : accEntry.second) {
const auto& ns = nsEntry.first;
for (const auto& cacheEntry : nsEntry.second) {
bool is_modified = cacheEntry.second.first;
const auto& key = cacheEntry.first;
const auto& blob = cacheEntry.second.second;
if (is_modified) {
changeCount++;
if (changeCount == 0) // RH TODO: limit the max number of state changes?
{
// overflow
JLOG(j.warn())
<< "HooKError[" << HR_ACC << "]: SetHooKState failed: Too many state changes";
return tecHOOK_REJECTED;
}
// this entry isn't just cached, it was actually modified
auto slice = Slice(blob.data(), blob.size());
TER result =
setHookState(hookResult, applyCtx, acc, ns, key, slice);
if (result != tesSUCCESS)
{
JLOG(j.warn())
<< "HookError[" << HR_ACC() << "]: SetHookState failed: " << result
<< " Key: " << key
<< " Value: " << slice;
}
// ^ should not fail... checks were done before map insert
}
}
}
}
return changeCount;
}
ripple::TER
hook::
removeEmissionEntry(ripple::ApplyContext& applyCtx)
{
auto const& j = applyCtx.app.journal("View");
auto const& tx = applyCtx.tx;
if (!const_cast<ripple::STTx&>(tx).isFieldPresent(sfEmitDetails))
return tesSUCCESS;
auto key = keylet::emitted(tx.getTransactionID());
auto const& sle = applyCtx.view().peek(key);
if (!sle)
return tesSUCCESS;
if (!applyCtx.view().dirRemove(
keylet::emittedDir(),
sle->getFieldU64(sfOwnerNode),
key,
false))
{
JLOG(j.fatal())
<< "HookError[" << HR_ACC() << "]: ccl tefBAD_LEDGER";
return tefBAD_LEDGER;
}
applyCtx.view().erase(sle);
return tesSUCCESS;
}
TER
hook::
finalizeHookResult(
hook::HookResult& hookResult,
ripple::ApplyContext& applyCtx,
bool doEmit)
{
auto const& j = applyCtx.app.journal("View");
uint16_t change_count = 0;
// open views do not modify add/remove ledger entries
if (applyCtx.view().open())
return;
return tesSUCCESS;
//RH TODO: this seems hacky... and also maybe there's a way this cast might fail?
ApplyViewImpl& avi = dynamic_cast<ApplyViewImpl&>(applyCtx.view());
@@ -943,7 +970,7 @@ void hook::commitChangesToLedger(
uint16_t exec_index = avi.nextHookExecutionIndex();
uint16_t emission_count = 0;
// apply emitted transactions to the ledger (by adding them to the emitted directory) if we are allowed to
if (cclMode & cclAPPLY)
if (doEmit)
{
DBG_PRINTF("emitted txn count: %d\n", hookResult.emittedTxn.size());
for (; hookResult.emittedTxn.size() > 0; hookResult.emittedTxn.pop())
@@ -985,47 +1012,12 @@ void hook::commitChangesToLedger(
{
JLOG(j.warn()) << "HookError[" << HR_ACC() << "]: " <<
"Emission Directory full when trying to insert " << id;
break;
return tecDIR_FULL;
}
}
}
}
// remove this (activating) transaction from the emitted directory if we were instructed to
if (cclMode & cclREMOVE)
{
do
{
auto const& tx = applyCtx.tx;
if (!const_cast<ripple::STTx&>(tx).isFieldPresent(sfEmitDetails))
break;
auto key = keylet::emitted(tx.getTransactionID());
auto const& sle = applyCtx.view().peek(key);
if (!sle)
{
JLOG(j.warn())
<< "HookError[" << HR_ACC() << "]: ccl tried to remove already removed emittedtxn";
break;
}
if (!applyCtx.view().dirRemove(
keylet::emittedDir(),
sle->getFieldU64(sfOwnerNode),
key,
false))
{
JLOG(j.fatal())
<< "HookError[" << HR_ACC() << "]: ccl tefBAD_LEDGER";
break;
}
applyCtx.view().erase(sle);
} while (0);
}
// add a metadata entry for this hook execution result
STObject meta { sfHookExecution };
meta.setFieldU8(sfHookResult, hookResult.exitType );
@@ -1042,11 +1034,14 @@ void hook::commitChangesToLedger(
meta.setFieldU64(sfHookInstructionCount, hookResult.instructionCount);
meta.setFieldU16(sfHookEmitCount, emission_count); // this will never wrap, hard limit
meta.setFieldU16(sfHookExecutionIndex, exec_index );
meta.setFieldU16(sfHookStateChangeCount, change_count );
meta.setFieldU16(sfHookStateChangeCount, hookResult.changedStateCount );
meta.setFieldH256(sfHookHash, hookResult.hookHash);
avi.addHookMetaData(std::move(meta));
return tesSUCCESS;
}
/* Retrieve the state into write_ptr identified by the key in kread_ptr */
DEFINE_HOOK_FUNCTION(
int64_t,
@@ -1058,8 +1053,8 @@ DEFINE_HOOK_FUNCTION(
state_foreign(
hookCtx, memoryCtx,
write_ptr, write_len,
0, 0,
kread_ptr, kread_len,
0, 0,
0, 0);
}