diff --git a/.gitignore b/.gitignore index 23d896b8e..2e13904cc 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,8 @@ bin/project-cache.jam # Ignore backup/temps *~ +build/docker + # Ignore object files. *.o build diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 731ea8bf6..922688484 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -429,12 +429,14 @@ target_sources (rippled PRIVATE src/ripple/app/tx/impl/Payment.cpp src/ripple/app/tx/impl/SetAccount.cpp src/ripple/app/tx/impl/SetRegularKey.cpp + src/ripple/app/tx/impl/SetHook.cpp src/ripple/app/tx/impl/SetSignerList.cpp src/ripple/app/tx/impl/SetTrust.cpp src/ripple/app/tx/impl/SignerEntries.cpp src/ripple/app/tx/impl/Taker.cpp src/ripple/app/tx/impl/Transactor.cpp src/ripple/app/tx/impl/apply.cpp + src/ripple/app/tx/impl/applyHook.cpp src/ripple/app/tx/impl/applySteps.cpp #[===============================[ main sources: @@ -981,6 +983,8 @@ target_link_libraries (rippled Ripple::opts Ripple::libs Ripple::xrpl_core + libssvm + # /usr/lib/libwasmer.a ) exclude_if_included (rippled) # define a macro for tests that might need to diff --git a/Builds/CMake/RippledSettings.cmake b/Builds/CMake/RippledSettings.cmake index 5fcc9441a..c4b760384 100644 --- a/Builds/CMake/RippledSettings.cmake +++ b/Builds/CMake/RippledSettings.cmake @@ -2,7 +2,7 @@ declare user options/settings #]===================================================================] -option (assert "Enables asserts, even in release builds" OFF) +option (assert "Enables asserts, even in release builds" ON) option (reporting "Build rippled with reporting mode enabled" OFF) @@ -15,7 +15,7 @@ if (unity) set (unity OFF CACHE BOOL "unity only available for cmake 3.16+" FORCE) else () if (NOT is_ci) - set (CMAKE_UNITY_BUILD_BATCH_SIZE 15 CACHE STRING "") + set (CMAKE_UNITY_BUILD_BATCH_SIZE 5 CACHE STRING "") endif () endif () endif () diff --git a/Builds/CMake/deps/libssvm.cmake b/Builds/CMake/deps/libssvm.cmake new file mode 100644 index 000000000..0094903b9 --- /dev/null +++ b/Builds/CMake/deps/libssvm.cmake @@ -0,0 +1,17 @@ +find_package (libssvm_src QUIET) +if (NOT TARGET libssvm_src) + FetchContent_Declare( + libssvm_src + GIT_REPOSITORY git@github.com:RichardAH/libssvm.git + GIT_TAG 382bd11497439d334b37c585f38cc003d7aef080 + ) + FetchContent_MakeAvailable(libssvm_src) + # FetchContent_GetProperties(libssvm_src) + #if(NOT libssvm_src_POPULATED) + # FetchContent_Populate(libssvm_src) + # add_subdirectory(${libssvm_src_SOURCE_DIR} ${libssvm_src_BINARY_DIR}) + #endif() + #Message("libssvm: srcdir:") + #Message(${libssvm_src_SOURCE_DIR}) + target_include_directories (libssvm SYSTEM INTERFACE ${libssvm_src_SOURCE_DIR}/include) +endif() diff --git a/CMakeLists.txt b/CMakeLists.txt index 9a3c9b4e0..de41201d2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,7 @@ include(RippledInterface) ### +include(deps/libssvm) include(deps/Boost) include(deps/OpenSSL) include(deps/Secp256k1) @@ -65,5 +66,4 @@ include(RippledCore) include(RippledInstall) include(RippledCov) include(RippledMultiConfig) -include(RippledDocs) include(RippledValidatorKeys) diff --git a/README.md b/README.md index 902c05be0..c35ccffc3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,137 @@ +# Hooks Public Testnet +This is a fork of the rippled codebase incorporating the work-in-progress "Hooks" amendment. This amendment will allow web assembly smart contracts to run directly on the XRP ledger when completed and adopted. + +## Docker Container +Building `rippled` can be non-trivial, especially in this case since modified libraries are used. We have provided a testnet docker container for your convenience. This container contains an instance of `rippled` configured as a "Hooks stock node" running on the Public Testnet. You can interact with it using the steps below: + +### Updating an existing container +If you already have the docker image and need to update then use this instruction to pull and run the new version + ```bash + docker rmi -f xrpllabsofficial/xrpld-hooks-testnet + ``` +### Starting the container +1. Download and install docker: https://docs.docker.com/get-docker/ +2. Then to run the container interactively use: +```bash +docker run -d --name xrpld-hooks xrpllabsofficial/xrpld-hooks-testnet +docker exec -it xrpld-hooks bash +``` +3. Set up a second terminal to view the log: + +Open a new terminal window on your system and run. +```bash +docker exec -it xrpld-hooks tail -f log +``` + This will show you the trace log of xrpld as it runs, which will be important for knowing if your transactions fail or succeed and what actions the hooks take. + Since there is rather a lot of log output you might find it useful to run this with `grep -a ` after you obtain an account you are interested in. + E.g. `docker exec -it xrpld-hooks tail -f log | grep -a rEy6oGFEeKNiMUTTEzTDnMVfe7SvcBsHZK` + +4. If you need to kill and destroy the container and restart it (if you are still attached to the container, type `exit` there to quit the container terminal): +```bash +docker rm -f xrpld-hooks +``` + Then repeat step 2. If you need to fetch a newly published image, check the `Update an existing container` step above. + +### Interacting with the container +After following the above steps you will be inside a shell inside the container. Rippled will already be running with the correct settings. Read the README.md in the container for further instructions on installing and interacting with the example hooks. + +## What's in this docker? +1. A `rippled` instance connected to the Hooks Public Testnet. +2. A compiler toolchain for building hooks (wasmcc, wasm2wat, etc...) +3. Example hooks and support scripts to install them on your account. + +## Get testnet XRP +The Faucet is on the [main page](https://hooks-testnet.xrpl-labs.com/). Make a note of your secret (family seed) because you will need it for all the examples. + +## Testnet explorer +Use the [Testnet Explorer](https://hooks-testnet-explorer.xrpl-labs.com/) to view transactions, accounts and hook state objects as you go. + +## File types +1. The example hooks are written in C. Any file ending in `.c` is a hook. Any file ending in `.h` is a header file used by +all hooks. +2. Hooks compile to `.wasm` files. These are hook binaries ready to be installed onto an account on the ledger using +the `SetHook` transaction. +3. Javascript files `.js` are runnable with nodejs and provide a way to installing hooks and interacting with the ledger through ripple-lib. + +## Building +To build you can run `make` from any hook's directory. The example `makefile` in each directory shows you how to build a hook. + +## Interacting +To interact with the example hooks change (`cd`) into one of these directories: +- liteacc +- firewall +- carbon +- accept +- notary +- peggy + +All example hooks are installed by running `node .js`. The usage information will be provided at the commandline. + +You can check a reported `SetHook` transaction ID with the [Hooks Testnet Explorer](https://hooks-testnet-explorer.xrpl-labs.com/). + +Finally run the additional `.js` files to interact with the Hook or write your own interaction. + +## Hook API +- Documentation for the Hook API can be found at the [Hooks Testnet Site](https://hooks-testnet.xrpl-labs.com/). +- For further details check: +1. `hook-api-examples/hookapi.h` +2. `src/ripple/app/tx/applyHook.h` +3. `src/ripple/app/tx/impl/applyHook.cpp` + +## Viewing state +You can view the current Hook State for your Hook by locating the account it is installed on with the [Hooks Testnet Explorer](https://hooks-testnet-explorer.xrpl-labs.com/). + +You can also run `./rippled account_objects ` to inspect the Hook's State Data. + +## Minimum example hook +Please have a look at the accept hook in `./accept/` + +## Tracing output +Output is written to a file in the current working directory called `log`. + +Rippled produces a large amount of output in `trace` mode (which is the default mode used in this example). You may +find that you need to scroll back considerably after running a hook or interacting with a hook to find out what +precisely happened. + +Greping for the relevant account will help, as all Hooks prepend the otxn account and hook account to the trace line. + +## SetHook Transaction +Set a Hook on an activated account using a SetHook Transaction (ttHOOK_SET = 22). This must contain the following fields: +- sfAccount +- sfCreateCode: Containing the binary of the web assembly +- sfHookOn: An unsigned 64bit integer (explained bellow) + +### sfHookOn +Each bit in this unsigned int64 indicates whether the Hook should execute on a particular transaction type. All bits are *active low* **except** bit 22 which is *active high*. Since 22 is ttHOOK_SET this means the default value of all 0's will not fire on a SetHook transaction but will fire on every other transaction type. This is a deliberate design choice to help people avoid bricking their XRPL account with a misbehaving hook. + +Bits are numbered from right to left from 0 to 63). + +Examples: + +1. If we want to completely disable the hook: +```C +~(1ULL << 22) /* every bit is 1 except bit 22 which is 0 */ +``` + +2. If we want to disable the hook on everything except ttPAYMENT: +```C +~(1ULL << 22) & ~(1ULL) +``` + +3. If we want to enable the hook on everything except ttHOOK_SET +```C +0 +``` + +4. If we want to enable hook firing on ttHOOK_SET (dangerous) and every other transaction type: +```C +(1ULL << 22) +``` + + + +------- + # The XRP Ledger The [XRP Ledger](https://xrpl.org/) is a decentralized cryptographic ledger powered by a network of peer-to-peer servers. The XRP Ledger uses a novel Byzantine Fault Tolerant consensus algorithm to settle and record transactions in a secure distributed database without a central operator. diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 000000000..dfa99ba6a --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,43 @@ +# Use the official image as a parent image. +FROM centos + +# Set the working directory. +WORKDIR /opt/xrpld-hooks/ + +# Copy the file from your host to your current location. +COPY docker/screenrc /root/.screenrc +COPY docker/wasm2wat /usr/bin/ +COPY rippled . +COPY testnet.cfg . +COPY testnetvalidators.txt . +COPY docker/libboost/libboost_coroutine.so.1.70.0 /usr/lib/ +COPY docker/libboost/libboost_context.so.1.70.0 /usr/lib +COPY docker/libboost/libboost_filesystem.so.1.70.0 /usr/lib +COPY docker/libboost/libboost_program_options.so.1.70.0 /usr/lib +COPY docker/libboost/libboost_regex.so.1.70.0 /usr/lib +COPY docker/libboost/libboost_system.so.1.70.0 /usr/lib +COPY docker/libboost/libboost_thread.so.1.70.0 /usr/lib +COPY docker/libboost/libboost_chrono.so.1.70.0 /usr/lib +COPY docker/libboost/libboost_date_time.so.1.70.0 /usr/lib +COPY docker/libboost/libboost_atomic.so.1.70.0 /usr/lib +COPY docker/js/ ./ +# Run the command inside your image filesystem. +RUN dnf install epel-release -y +RUN yum install -y vim screen python3-setuptools-wheel python3-pip-wheel python3 python3-pip curl make nodejs +RUN curl https://cmake.org/files/v3.17/cmake-3.17.1-Linux-x86_64.sh --output cmake-3.17.1-Linux-x86_64.sh \ + && mkdir /opt/cmake \ + && printf "y\nn\n" | sh cmake-3.17.1-Linux-x86_64.sh --prefix=/opt/cmake > /dev/null \ + && ln -s /opt/cmake/bin/cmake /usr/local/bin/cmake +RUN curl https://raw.githubusercontent.com/wasienv/wasienv/master/install.sh | sh +RUN echo 'PATH=$PATH:/root/.wasienv/bin/' >> /root/.bash_rc +RUN rm -f cmake-3.17.1-Linux-x86_64.sh +RUN mkdir /etc/opt/ripple +RUN ln -s /opt/xrpld-hooks/testnet.cfg /etc/opt/ripple/rippled.cfg +RUN ln -s /opt/xrpld-hooks/testnetvalidators.txt /etc/opt/ripple/testnetvalidators.txt + +# Add metadata to the image to describe which port the container is listening on at runtime. +EXPOSE 6005 +EXPOSE 5005 + +# Run the specified command within the container. +CMD ./rippled --conf testnet.cfg --net >> log 2>> log diff --git a/docker/build_docker_xrpllabs.sh b/docker/build_docker_xrpllabs.sh new file mode 100755 index 000000000..2caa7b342 --- /dev/null +++ b/docker/build_docker_xrpllabs.sh @@ -0,0 +1,6 @@ +#!/bin/bash +cp -r ../hook-api-examples docker/js #docker doesnt like symlinks? +/usr/bin/cp /root/wabt/bin/wasm2wat docker/ +docker build --tag xrpllabsofficial/xrpld-hooks-testnet:latest . && docker create xrpllabsofficial/xrpld-hooks-testnet +rm -rf docker/js +docker push xrpllabsofficial/xrpld-hooks-testnet:latest diff --git a/src/ripple/app/consensus/RCLConsensus.cpp b/src/ripple/app/consensus/RCLConsensus.cpp index 859c66d31..0fc67fca0 100644 --- a/src/ripple/app/consensus/RCLConsensus.cpp +++ b/src/ripple/app/consensus/RCLConsensus.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include #include @@ -177,6 +178,11 @@ RCLConsensus::Adaptor::share(RCLCxPeerPos const& peerPos) void RCLConsensus::Adaptor::share(RCLCxTx const& tx) { + //RH TODO: never broadcast emitted transactions + //fix below: + //if (tx.isFieldPresent(sfEmitDetails)) + // return; + // If we didn't relay this transaction recently, relay it to all peers if (app_.getHashRouter().shouldRelay(tx.id())) { @@ -649,7 +655,6 @@ RCLConsensus::Adaptor::doAccept( tapNONE, "consensus", [&](OpenView& view, beast::Journal j) { - // Stuff the ledger with transactions from the queue. return app_.getTxQ().accept(app_, view); }); @@ -774,6 +779,7 @@ RCLConsensus::Adaptor::buildLCL( j_); }(); + // Update fee computations based on accepted txs using namespace std::chrono_literals; app_.getTxQ().processClosedLedger(app_, *built, roundTime > 5s); diff --git a/src/ripple/app/ledger/OpenLedger.h b/src/ripple/app/ledger/OpenLedger.h index 42120cf63..c3471ba49 100644 --- a/src/ripple/app/ledger/OpenLedger.h +++ b/src/ripple/app/ledger/OpenLedger.h @@ -112,6 +112,10 @@ public: std::shared_ptr current() const; + // not mutex guarded, only use if you are sure + std::shared_ptr + current_unsafe() const; + /** Modify the open ledger Thread safety: diff --git a/src/ripple/app/ledger/impl/LedgerMaster.cpp b/src/ripple/app/ledger/impl/LedgerMaster.cpp index 7bc1bf243..8290b4150 100644 --- a/src/ripple/app/ledger/impl/LedgerMaster.cpp +++ b/src/ripple/app/ledger/impl/LedgerMaster.cpp @@ -1033,6 +1033,8 @@ LedgerMaster::checkAccept(std::shared_ptr const& ledger) { // Can we accept this ledger as our new last fully-validated ledger + JLOG(m_journal.info()) << "=========> checkAccept (" << ledger->info().seq << ")\n"; + if (!canBeCurrent(ledger)) return; @@ -1061,6 +1063,8 @@ LedgerMaster::checkAccept(std::shared_ptr const& ledger) ledger->setValidated(); ledger->setFull(); setValidLedger(ledger); + + JLOG(m_journal.info()) << "=========> checkAccept (" << ledger->info().seq << ") = validated\n"; if (!mPubLedger) { pendSaveValidated(app_, ledger, true, true); diff --git a/src/ripple/app/ledger/impl/OpenLedger.cpp b/src/ripple/app/ledger/impl/OpenLedger.cpp index 113c46951..4fd9d7324 100644 --- a/src/ripple/app/ledger/impl/OpenLedger.cpp +++ b/src/ripple/app/ledger/impl/OpenLedger.cpp @@ -53,6 +53,12 @@ OpenLedger::current() const return current_; } +std::shared_ptr +OpenLedger::current_unsafe() const +{ + return current_; +} + bool OpenLedger::modify(modify_type const& f) { diff --git a/src/ripple/app/main/Main.cpp b/src/ripple/app/main/Main.cpp index 214233953..dcd5ebd95 100644 --- a/src/ripple/app/main/Main.cpp +++ b/src/ripple/app/main/Main.cpp @@ -15,8 +15,7 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ -//============================================================================== - + //============================================================================== #include #include #include @@ -346,6 +345,7 @@ run(int argc, char** argv) { using namespace std; + beast::setCurrentThreadName( "rippled: main " + BuildInfo::getVersionString()); @@ -778,9 +778,13 @@ run(int argc, char** argv) } // namespace ripple +// Must be outside the namespace for obvious reasons +// + int main(int argc, char** argv) { + #if BOOST_OS_WINDOWS { // Work around for https://svn.boost.org/trac/boost/ticket/10657 diff --git a/src/ripple/app/misc/NetworkOPs.cpp b/src/ripple/app/misc/NetworkOPs.cpp index 1b3fcd1de..1c5ff409f 100644 --- a/src/ripple/app/misc/NetworkOPs.cpp +++ b/src/ripple/app/misc/NetworkOPs.cpp @@ -42,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -1106,6 +1107,7 @@ NetworkOPsImp::strOperatingMode(OperatingMode const mode, bool const admin) return states_[static_cast(mode)]; } + void NetworkOPsImp::submitTransaction(std::shared_ptr const& iTrans) { @@ -1115,6 +1117,17 @@ NetworkOPsImp::submitTransaction(std::shared_ptr const& iTrans) return; } + auto const view = m_ledgerMaster.getCurrentLedger(); + + // Enforce Network bar for emitted txn + if (view->rules().enabled(featureHooks) && + hook::isEmittedTxn(*iTrans)) + { + JLOG(m_journal.warn()) + << "Submitted transaction invalid: EmitDetails present."; + return; + } + // this is an asynchronous interface auto const trans = sterilize(*iTrans); @@ -1167,6 +1180,21 @@ NetworkOPsImp::processTransaction( FailHard failType) { auto ev = m_job_queue.makeLoadEvent(jtTXN_PROC, "ProcessTXN"); + + auto const view = m_ledgerMaster.getCurrentLedger(); + + // Enforce Network bar for emitted txn + if (view->rules().enabled(featureHooks) && + hook::isEmittedTxn(*transaction->getSTransaction())) + { + JLOG(m_journal.warn()) + << "Submitted transaction invalid: EmitDetails present."; + //RH NOTE: cannot set SF_BAD because if the tx will be generated by a hook we are about to execute + //then this would poison consensus for that emitted tx + //RH TODO: (severely) charge peer who sent + return; + } + auto const newFlags = app_.getHashRouter().getFlags(transaction->getID()); if ((newFlags & SF_BAD) != 0) @@ -1180,7 +1208,6 @@ NetworkOPsImp::processTransaction( // NOTE eahennis - I think this check is redundant, // but I'm not 100% sure yet. // If so, only cost is looking up HashRouter flags. - auto const view = m_ledgerMaster.getCurrentLedger(); auto const [validity, reason] = checkValidity( app_.getHashRouter(), *transaction->getSTransaction(), @@ -1201,9 +1228,10 @@ NetworkOPsImp::processTransaction( // canonicalize can change our pointer app_.getMasterTransaction().canonicalize(&transaction); + if (bLocal) doTransactionSync(transaction, bUnlimited, failType); - else + else doTransactionAsync(transaction, bUnlimited, failType); } @@ -1214,6 +1242,20 @@ NetworkOPsImp::doTransactionAsync( FailHard failType) { std::lock_guard lock(mMutex); + + auto const view = m_ledgerMaster.getCurrentLedger(); + + // Enforce Network bar for emitted txn + if (view->rules().enabled(featureHooks) && + hook::isEmittedTxn(*transaction->getSTransaction())) + { + JLOG(m_journal.info()) + << "Transaction received over network has EmitDetails, discarding."; + //RH NOTE: cannot set SF_BAD because if the tx will be generated by a hook we are about to execute + //then this would poison consensus for that emitted tx + //RH TODO: (severely) charge peer who sent + return; + } if (transaction->getApplying()) return; @@ -1241,6 +1283,20 @@ NetworkOPsImp::doTransactionSync( { std::unique_lock lock(mMutex); + auto const view = m_ledgerMaster.getCurrentLedger(); + + // Enforce Network bar for emitted txn + if (view->rules().enabled(featureHooks) && + hook::isEmittedTxn(*transaction->getSTransaction())) + { + JLOG(m_journal.info()) + << "Transaction received over network has EmitDetails, discarding."; + //RH NOTE: cannot set SF_BAD because if the tx will be generated by a hook we are about to execute + //then this would poison consensus for that emitted tx + //RH TODO: (severely) charge peer who sent + return; + } + if (!transaction->getApplying()) { mTransactions.push_back( diff --git a/src/ripple/app/misc/TxQ.h b/src/ripple/app/misc/TxQ.h index 5f1d41c01..a8283f20d 100644 --- a/src/ripple/app/misc/TxQ.h +++ b/src/ripple/app/misc/TxQ.h @@ -216,6 +216,8 @@ public: FeeLevel64 feeLevel; /// LastValidLedger field of the queued transaction, if any std::optional lastValid; + /// FirstValidLedger field of the queued transaction, if any + std::optional firstValid; /** Potential @ref TxConsequences of applying the queued transaction to the open ledger. */ @@ -525,6 +527,7 @@ private: AccountID const account; /// Expiration ledger for the transaction /// (`sfLastLedgerSequence` field). + std::optional const firstValid; std::optional const lastValid; /// Transaction SeqProxy number /// (`sfSequence` or `sfTicketSequence` field). diff --git a/src/ripple/app/misc/impl/TxQ.cpp b/src/ripple/app/misc/impl/TxQ.cpp index 40d6aced3..595429e45 100644 --- a/src/ripple/app/misc/impl/TxQ.cpp +++ b/src/ripple/app/misc/impl/TxQ.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -70,6 +71,14 @@ getLastLedgerSequence(STTx const& tx) return tx.getFieldU32(sfLastLedgerSequence); } +static boost::optional +getFirstLedgerSequence(STTx const& tx) +{ + if (!tx.isFieldPresent(sfFirstLedgerSequence)) + return boost::none; + return tx.getFieldU32(sfFirstLedgerSequence); +} + static FeeLevel64 increase(FeeLevel64 level, std::uint32_t increasePercent) { @@ -282,6 +291,11 @@ TxQ::MaybeTx::MaybeTx( , flags(flags_) , pfresult(pfresult_) { + firstValid = getFirstLedgerSequence(*txn); + lastValid = getLastLedgerSequence(*txn); + + if (txn->isFieldPresent(sfAccountTxnID)) + priorTxID = txn->getFieldH256(sfAccountTxnID); } std::pair @@ -1181,6 +1195,7 @@ TxQ::apply( txIter->first->second.retriesRemaining == MaybeTx::retriesAllowed && feeLevelPaid > requiredFeeLevel && requiredFeeLevel > baseLevel) { + OpenView sandbox(open_ledger, &view, view.rules()); auto result = tryClearAccountQueueUpThruTx( @@ -1419,6 +1434,134 @@ TxQ::accept(Application& app, OpenView& view) std::lock_guard lock(mutex_); auto const metricsSnapshot = feeMetrics_.getSnapshot(); + // inject emitted transactions if any + if (view.rules().enabled(featureHooks)) + do { + Keylet const emittedDirKeylet { keylet::emittedDir() }; + if (dirIsEmpty(view, emittedDirKeylet)) + break; + + std::shared_ptr sleDirNode{}; + unsigned int uDirEntry{0}; + uint256 dirEntry{beast::zero}; + + if (!cdirFirst( + view, + emittedDirKeylet.key, + sleDirNode, + uDirEntry, + dirEntry, + j_)) + break; + + do + { + Keylet const itemKeylet{ltCHILD, dirEntry}; + auto sleItem = view.read(itemKeylet); + if (!sleItem) + { + // Directory node has an invalid index. Bail out. + JLOG(j_.fatal()) + << "EmittedTxn processing: directory node in ledger " << view.seq() + << " has index to object that is missing: " + << to_string(dirEntry); + break; + } + + LedgerEntryType const nodeType{ + safe_cast((*sleItem)[sfLedgerEntryType])}; + + if (nodeType != ltEMITTED) + { + JLOG(j_.fatal()) + << "EmittedTxn processing: emitted directory contained non ltEMITTED type"; + break; + } + + JLOG(j_.info()) << "Processing emitted txn: " << *sleItem; + + auto const& emitted = + const_cast(*sleItem).getField(sfEmittedTxn).downcast(); + + auto s = std::make_shared(); + emitted.add(*s); + SerialIter sitTrans(s->slice()); // do we need slice? + try + { + auto const& stpTrans = std::make_shared(std::ref(sitTrans)); + + if (!stpTrans->isFieldPresent(sfEmitDetails) || + !stpTrans->isFieldPresent(sfFirstLedgerSequence) || + !stpTrans->isFieldPresent(sfLastLedgerSequence)) + { + JLOG(j_.warn()) + << "Hook: Emission failure: " + << "sfEmitDetails or sfFirst/LastLedgerSeq missing."; + continue; + } + + auto seq = view.info().seq; + auto txnHash = stpTrans->getTransactionID(); + + if (stpTrans->getFieldU32(sfLastLedgerSequence) < seq) + { + JLOG(j_.trace()) + << "Hook: Emission failure, adding cleanup pseudotxn to ledger " << seq; + + auto const& emitDetails = + const_cast(*stpTrans).getField(sfEmitDetails).downcast(); + + STTx efTx ( + ttEMIT_FAILURE, + [seq, txnHash, emitDetails](auto& obj) { + obj[sfLedgerSequence] = seq; + obj[sfTransactionHash] = txnHash; + obj.emplace_back(emitDetails); + /*std::unique_ptr ed = + std::make_unique(emitDetails); + ed->setFName(sfEmitDetails); + obj.set(std::move(ed));*/ + + }); + + uint256 txID = efTx.getTransactionID(); + + auto s = std::make_shared(); + efTx.add(*s); + + // RH TODO: should this txn be added in a different way to prevent any chance of failure + app.getHashRouter().setFlags(txID, SF_PRIVATE2); + view.rawTxInsert(txID, std::move(s), nullptr); + ledgerChanged = true; + + continue; + } + + auto fls = stpTrans->getFieldU32(sfFirstLedgerSequence); + if (fls > view.info().seq) + { + JLOG(j_.info()) << + "Holding TX " << stpTrans->getTransactionID() << " for future ledger."; + continue; + } + + // execution to here means we are adding the tx to the local set + if (fls >= view.info().seq) + { + app.getHashRouter().setFlags(txnHash, SF_PRIVATE2); + view.rawTxInsert(stpTrans->getTransactionID(), std::move(s), nullptr); + ledgerChanged = true; + } + + } + catch (std::exception& e) + { + JLOG(j_.fatal()) << "EmittedTxn Processing: Failure: " << e.what() << "\n"; + } + + } while (cdirNext(view, emittedDirKeylet.key, sleDirNode, uDirEntry, dirEntry, j_)); + + } while(0); for (auto candidateIter = byFee_.begin(); candidateIter != byFee_.end();) { diff --git a/src/ripple/app/tx/applyHook.h b/src/ripple/app/tx/applyHook.h new file mode 100644 index 000000000..64e14bd82 --- /dev/null +++ b/src/ripple/app/tx/applyHook.h @@ -0,0 +1,555 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "common/value.h" +#include "vm/configure.h" +#include "vm/vm.h" +#include "common/errcode.h" +#include "runtime/hostfunc.h" +#include "runtime/importobj.h" + +namespace hook { + struct HookContext; + struct HookResult; + bool isEmittedTxn(ripple::STTx const& tx); +} + +namespace hook_api { + +#define TER_TO_HOOK_RETURN_CODE(x)\ + (((TERtoInt(x)) << 16)*-1) + + +// for debugging if you want a lot of output change these to if (1) +#define DBG_PRINTF if (0) printf +#define DBG_FPRINTF if (0) fprintf + + namespace keylet_code { + enum keylet_code : uint32_t { + HOOK = 1, + HOOK_STATE = 2, + ACCOUNT = 3, + AMENDMENTS = 4, + CHILD = 5, + SKIP = 6, + FEES = 7, + NEGATIVE_UNL = 8, + LINE = 9, + OFFER = 10, + QUALITY = 11, + EMITTED_DIR = 12, + TICKET = 13, + SIGNERS = 14, + CHECK = 15, + DEPOSIT_PREAUTH = 16, + UNCHECKED = 17, + OWNER_DIR = 18, + PAGE = 19, + ESCROW = 20, + PAYCHAN = 21, + EMITTED = 22 + }; + } + + namespace compare_mode { + enum compare_mode : uint32_t { + EQUAL = 1, + LESS = 2, + GREATER = 4 + }; + } + + enum hook_return_code : int64_t { + SUCCESS = 0, // return codes > 0 are reserved for hook apis to return "success" + OUT_OF_BOUNDS = -1, // could not read or write to a pointer to provided by hook + INTERNAL_ERROR = -2, // eg directory is corrupt + TOO_BIG = -3, // something you tried to store was too big + TOO_SMALL = -4, // something you tried to store or provide was too small + DOESNT_EXIST = -5, // something you requested wasn't found + NO_FREE_SLOTS = -6, // when trying to load an object there is a maximum of 255 slots + INVALID_ARGUMENT = -7, // self explanatory + ALREADY_SET = -8, // returned when a one-time parameter was already set by the hook + PREREQUISITE_NOT_MET = -9, // returned if a required param wasn't set, before calling + FEE_TOO_LARGE = -10, // returned if the attempted operation would result in an absurd fee + EMISSION_FAILURE = -11, // returned if an emitted tx was not accepted by rippled + TOO_MANY_NONCES = -12, // a hook has a maximum of 256 nonces + TOO_MANY_EMITTED_TXN = -13, // a hook has emitted more than its stated number of emitted txn + NOT_IMPLEMENTED = -14, // an api was called that is reserved for a future version + INVALID_ACCOUNT = -15, // an api expected an account id but got something else + GUARD_VIOLATION = -16, // a guarded loop or function iterated over its maximum + INVALID_FIELD = -17, // the field requested is returning sfInvalid + PARSE_ERROR = -18, // hook asked hookapi to parse something the contents of which was invalid + RC_ROLLBACK = -19, // hook should terminate due to a rollback() call + RC_ACCEPT = -20, // hook should temrinate due to an accept() call + NO_SUCH_KEYLET = -21, // invalid keylet or keylet type + NOT_AN_ARRAY = -22, // if a count of an sle is requested but its not STI_ARRAY + NOT_AN_OBJECT = -23, // if a subfield is requested from something that isn't an object + INVALID_FLOAT = -10024, // specially selected value that will never be a valid exponent + DIVISION_BY_ZERO = -25, + MANTISSA_OVERSIZED = -26, + MANTISSA_UNDERSIZED = -27, + EXPONENT_OVERSIZED = -28, + EXPONENT_UNDERSIZED = -29, + OVERFLOW = -30, // if an operation with a float results in an overflow + NOT_IOU_AMOUNT = -31, + NOT_AN_AMOUNT = -32, + CANT_RETURN_NEGATIVE = -33, + NOT_AUTHORIZED = -34, + PREVIOUS_FAILURE_PREVENTS_RETRY = -35, + TOO_MANY_PARAMS = -36 + }; + + enum ExitType : uint8_t { + UNSET = 0, + WASM_ERROR = 1, + ROLLBACK = 2, + ACCEPT = 3, + }; + + const int etxn_details_size = 105; + const int max_slots = 255; + const int max_nonce = 255; + const int max_emit = 255; + const int max_params = 16; + const int drops_per_byte = 31250; //RH TODO make these votable config option + const double fee_base_multiplier = 1.1f; + + // RH TODO: consider replacing macros with templates, if SSVM compatible templates even exist + + #define LPAREN ( + #define RPAREN ) + #define EXPAND(...) __VA_ARGS__ + #define CAT(a, ...) PRIMITIVE_CAT(a, __VA_ARGS__) + #define PRIMITIVE_CAT(a, ...) a ## __VA_ARGS__ + #define SPLIT_1(D) EXPAND(CAT(SPLIT_, D)) + #define SPLIT_uint32_t + #define SPLIT_int32_t + #define SPLIT_uint64_t + #define SPLIT_int64_t + #define SPLIT_2(a, b) SPLIT_1(a), SPLIT_1(b) + #define SPLIT_3(a, ...) SPLIT_1(a), SPLIT_2(__VA_ARGS__) + #define SPLIT_4(a, ...) SPLIT_1(a), SPLIT_3(__VA_ARGS__) + #define SPLIT_5(a, ...) SPLIT_1(a), SPLIT_4(__VA_ARGS__) + #define SPLIT_6(a, ...) SPLIT_1(a), SPLIT_5(__VA_ARGS__) + #define SPLIT_7(a, ...) SPLIT_1(a), SPLIT_6(__VA_ARGS__) + #define SPLIT_8(a, ...) SPLIT_1(a), SPLIT_7(__VA_ARGS__) + #define SPLIT_9(a, ...) SPLIT_1(a), SPLIT_8(__VA_ARGS__) + #define SPLIT_10(a, ...) SPLIT_1(a), SPLIT_9(__VA_ARGS__) + #define EMPTY() + #define DEFER(id) id EMPTY() + #define OBSTRUCT(...) __VA_ARGS__ DEFER(EMPTY)() + #define VA_NARGS_IMPL(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, N, ...) N + #define VA_NARGS(__drop, ...) VA_NARGS_IMPL(__VA_ARGS__, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1) + #define STRIP_TYPES(...) DEFER(CAT(SPLIT_,VA_NARGS(NULL, __VA_ARGS__))CAT(LPAREN OBSTRUCT(__VA_ARGS__) RPAREN)) + + // The above macros allow types to be stripped off a pramater list, used below for proxying call through class + + #define DECLARE_HOOK_FUNCTION(R, F, ...)\ + R F(hook::HookContext& hookCtx, SSVM::Runtime::Instance::MemoryInstance& memoryCtx, __VA_ARGS__);\ + class WasmFunction_##F : public SSVM::Runtime::HostFunction\ + {\ + public:\ + hook::HookContext& hookCtx;\ + WasmFunction_##F(hook::HookContext& ctx) : hookCtx(ctx) {};\ + SSVM::Expect body(SSVM::Runtime::Instance::MemoryInstance* memoryCtx, __VA_ARGS__)\ + {\ + R return_code = hook_api::F(hookCtx, *memoryCtx, STRIP_TYPES(__VA_ARGS__));\ + if (return_code == RC_ROLLBACK || return_code == RC_ACCEPT)\ + return SSVM::Unexpect(SSVM::ErrCode::Terminated);\ + return return_code;\ + }\ + }; + + #define DECLARE_HOOK_FUNCNARG(R, F)\ + R F(hook::HookContext& hookCtx, SSVM::Runtime::Instance::MemoryInstance& memoryCtx);\ + class WasmFunction_##F : public SSVM::Runtime::HostFunction\ + {\ + public:\ + hook::HookContext& hookCtx;\ + WasmFunction_##F(hook::HookContext& ctx) : hookCtx(ctx) {};\ + SSVM::Expect body(SSVM::Runtime::Instance::MemoryInstance* memoryCtx)\ + {\ + R return_code = hook_api::F(hookCtx, *memoryCtx);\ + if (return_code == RC_ROLLBACK || return_code == RC_ACCEPT)\ + return SSVM::Unexpect(SSVM::ErrCode::Terminated);\ + return return_code;\ + }\ + }; + + #define DEFINE_HOOK_FUNCTION(R, F, ...)\ + R hook_api::F(hook::HookContext& hookCtx, SSVM::Runtime::Instance::MemoryInstance& memoryCtx, __VA_ARGS__) + + #define DEFINE_HOOK_FUNCNARG(R, F)\ + R hook_api::F(hook::HookContext& hookCtx, SSVM::Runtime::Instance::MemoryInstance& memoryCtx) + + + // RH NOTE: Find descriptions of api functions in ./impl/applyHook.cpp and hookapi.h (include for hooks) + + + DECLARE_HOOK_FUNCTION(int32_t, _g, uint32_t guard_id, uint32_t maxiter ); + + DECLARE_HOOK_FUNCTION(int64_t, accept, uint32_t read_ptr, uint32_t read_len, int64_t error_code ); + DECLARE_HOOK_FUNCTION(int64_t, rollback, uint32_t read_ptr, uint32_t read_len, int64_t error_code ); + DECLARE_HOOK_FUNCTION(int64_t, util_raddr, uint32_t write_ptr, uint32_t write_len, + uint32_t read_ptr, uint32_t read_len ); + DECLARE_HOOK_FUNCTION(int64_t, util_accid, uint32_t write_ptr, uint32_t write_len, + uint32_t read_ptr, uint32_t read_len ); + DECLARE_HOOK_FUNCTION(int64_t, util_verify, uint32_t dread_ptr, uint32_t dread_len, + uint32_t sread_ptr, uint32_t sread_len, + uint32_t kread_ptr, uint32_t kread_len ); + DECLARE_HOOK_FUNCTION(int64_t, sto_validate, uint32_t tread_ptr, uint32_t tread_len ); + DECLARE_HOOK_FUNCTION(int64_t, sto_subfield, uint32_t read_ptr, uint32_t read_len, uint32_t field_id ); + DECLARE_HOOK_FUNCTION(int64_t, sto_subarray, uint32_t read_ptr, uint32_t read_len, uint32_t array_id ); + DECLARE_HOOK_FUNCTION(int64_t, sto_emplace, uint32_t write_ptr, uint32_t write_len, + uint32_t sread_ptr, uint32_t sread_len, + uint32_t fread_ptr, uint32_t fread_len, uint32_t field_id ); + DECLARE_HOOK_FUNCTION(int64_t, sto_erase, uint32_t write_ptr, uint32_t write_len, + uint32_t read_ptr, uint32_t read_len, uint32_t field_id ); + + DECLARE_HOOK_FUNCTION(int64_t, util_sha512h, uint32_t write_ptr, uint32_t write_len, + uint32_t read_ptr, uint32_t read_len ); + DECLARE_HOOK_FUNCTION(int64_t, util_keylet, uint32_t write_ptr, uint32_t write_len, uint32_t keylet_type, + uint32_t a, uint32_t b, uint32_t c, + uint32_t d, uint32_t e, uint32_t f ); + DECLARE_HOOK_FUNCNARG(int64_t, etxn_burden ); + DECLARE_HOOK_FUNCTION(int64_t, etxn_details, uint32_t write_ptr, uint32_t write_len ); + DECLARE_HOOK_FUNCTION(int64_t, etxn_fee_base, uint32_t tx_byte_count); + DECLARE_HOOK_FUNCTION(int64_t, etxn_reserve, uint32_t count ); + DECLARE_HOOK_FUNCNARG(int64_t, etxn_generation ); + DECLARE_HOOK_FUNCTION(int64_t, emit, 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 ); + DECLARE_HOOK_FUNCTION(int64_t, float_mulratio, int64_t float1, uint32_t round_up, + uint32_t numerator, uint32_t denominator ); + DECLARE_HOOK_FUNCTION(int64_t, float_negate, int64_t float1 ); + DECLARE_HOOK_FUNCTION(int64_t, float_compare, int64_t float1, int64_t float2, uint32_t mode ); + DECLARE_HOOK_FUNCTION(int64_t, float_sum, int64_t float1, int64_t float2 ); + DECLARE_HOOK_FUNCTION(int64_t, float_sto, uint32_t write_ptr, uint32_t write_len, + uint32_t cread_ptr, uint32_t cread_len, + uint32_t iread_ptr, uint32_t iread_len, + int64_t float1, uint32_t field_code); + DECLARE_HOOK_FUNCTION(int64_t, float_sto_set, uint32_t read_ptr, uint32_t read_len ); + DECLARE_HOOK_FUNCTION(int64_t, float_invert, int64_t float1 ); + DECLARE_HOOK_FUNCTION(int64_t, float_divide, int64_t float1, int64_t float2 ); + DECLARE_HOOK_FUNCNARG(int64_t, float_one ); + + DECLARE_HOOK_FUNCTION(int64_t, float_exponent, int64_t float1 ); + DECLARE_HOOK_FUNCTION(int64_t, float_exponent_set, int64_t float1, int32_t exponent ); + DECLARE_HOOK_FUNCTION(int64_t, float_mantissa, int64_t float1 ); + DECLARE_HOOK_FUNCTION(int64_t, float_mantissa_set, int64_t float1, int64_t mantissa ); + DECLARE_HOOK_FUNCTION(int64_t, float_sign, int64_t float1 ); + DECLARE_HOOK_FUNCTION(int64_t, float_sign_set, int64_t float1, uint32_t negative ); + DECLARE_HOOK_FUNCTION(int64_t, float_int, int64_t float1, uint32_t decimal_places, uint32_t abs ); + + DECLARE_HOOK_FUNCTION(int64_t, hook_account, uint32_t write_ptr, uint32_t write_len ); + DECLARE_HOOK_FUNCTION(int64_t, hook_hash, uint32_t write_ptr, uint32_t write_len, int32_t hook_no ); + DECLARE_HOOK_FUNCNARG(int64_t, fee_base ); + DECLARE_HOOK_FUNCNARG(int64_t, ledger_seq ); + DECLARE_HOOK_FUNCTION(int64_t, ledger_last_hash, uint32_t write_ptr, uint32_t write_len ); + DECLARE_HOOK_FUNCTION(int64_t, nonce, uint32_t write_ptr, uint32_t write_len ); + + + DECLARE_HOOK_FUNCTION(int64_t, hook_param_set, uint32_t read_ptr, uint32_t read_len, + uint32_t kread_ptr, uint32_t kread_len, + uint32_t hread_ptr, uint32_t hread_len); + + DECLARE_HOOK_FUNCTION(int64_t, hook_param, uint32_t write_ptr, uint32_t write_len, + uint32_t read_ptr, uint32_t read_len); + + DECLARE_HOOK_FUNCTION(int64_t, hook_skip, uint32_t read_ptr, uint32_t read_len, uint32_t flags); + DECLARE_HOOK_FUNCNARG(int64_t, hook_pos); + + DECLARE_HOOK_FUNCTION(int64_t, slot, uint32_t write_ptr, uint32_t write_len, uint32_t slot ); + DECLARE_HOOK_FUNCTION(int64_t, slot_clear, uint32_t slot ); + DECLARE_HOOK_FUNCTION(int64_t, slot_count, uint32_t slot ); + DECLARE_HOOK_FUNCTION(int64_t, slot_id, uint32_t write_ptr, uint32_t write_len, uint32_t slot ); + DECLARE_HOOK_FUNCTION(int64_t, slot_set, uint32_t read_ptr, uint32_t read_len, int32_t slot ); + DECLARE_HOOK_FUNCTION(int64_t, slot_size, uint32_t slot ); + DECLARE_HOOK_FUNCTION(int64_t, slot_subarray, uint32_t parent_slot, uint32_t array_id, uint32_t new_slot ); + DECLARE_HOOK_FUNCTION(int64_t, slot_subfield, uint32_t parent_slot, uint32_t field_id, uint32_t new_slot ); + DECLARE_HOOK_FUNCTION(int64_t, slot_type, uint32_t slot_no, uint32_t flags ); + DECLARE_HOOK_FUNCTION(int64_t, slot_float, uint32_t slot_no ); + + DECLARE_HOOK_FUNCTION(int64_t, state_set, uint32_t read_ptr, uint32_t read_len, + uint32_t kread_ptr, uint32_t kread_len ); + DECLARE_HOOK_FUNCTION(int64_t, state_foreign_set, uint32_t read_ptr, uint32_t read_len, + uint32_t kread_ptr, uint32_t kread_len, + uint32_t nread_ptr, uint32_t nread_len, + uint32_t aread_ptr, uint32_t aread_len ); + DECLARE_HOOK_FUNCTION(int64_t, state, uint32_t write_ptr, uint32_t write_len, + uint32_t kread_ptr, uint32_t kread_len ); + DECLARE_HOOK_FUNCTION(int64_t, state_foreign, uint32_t write_ptr, uint32_t write_len, + uint32_t kread_ptr, uint32_t kread_len, + uint32_t nread_ptr, uint32_t nread_len, + uint32_t aread_ptr, uint32_t aread_len ); + DECLARE_HOOK_FUNCTION(int64_t, trace_slot, uint32_t read_ptr, uint32_t read_len, uint32_t slot ); + DECLARE_HOOK_FUNCTION(int64_t, trace, uint32_t mread_ptr, uint32_t mread_len, + uint32_t dread_ptr, uint32_t dread_len, uint32_t as_hex ); + DECLARE_HOOK_FUNCTION(int64_t, trace_num, uint32_t read_ptr, uint32_t read_len, int64_t number ); + DECLARE_HOOK_FUNCTION(int64_t, trace_float, uint32_t read_ptr, uint32_t read_len, int64_t float1 ); + + DECLARE_HOOK_FUNCNARG(int64_t, otxn_burden ); + DECLARE_HOOK_FUNCTION(int64_t, otxn_field, uint32_t write_ptr, uint32_t write_len, uint32_t field_id ); + DECLARE_HOOK_FUNCTION(int64_t, otxn_field_txt, uint32_t write_ptr, uint32_t write_len, uint32_t field_id ); + DECLARE_HOOK_FUNCNARG(int64_t, otxn_generation ); + DECLARE_HOOK_FUNCTION(int64_t, otxn_id, uint32_t write_ptr, uint32_t write_len, uint32_t flags ); + DECLARE_HOOK_FUNCNARG(int64_t, otxn_type ); + DECLARE_HOOK_FUNCTION(int64_t, otxn_slot, uint32_t slot_no ); + + +} /* end namespace hook_api */ + +namespace hook { + + bool canHook(ripple::TxType txType, uint64_t hookOn); + + struct HookResult; + + HookResult apply( + ripple::uint256 const&, + ripple::uint256 const&, + ripple::uint256 const&, + ripple::Blob const&, + std::map< + std::vector, /* param name */ + std::vector /* param value */ + > const& parameters, + std::map< + ripple::uint256, + std::map< + std::vector, /* param name */ + std::vector /* param value */ + >> const& parameterOverrides, + ripple::ApplyContext&, + ripple::AccountID const&, + bool callback, + uint32_t wasmParam = 0, + int32_t hookChainPosition = -1); + + struct HookContext; + + uint32_t maxHookStateDataSize(void); + uint32_t maxHookWasmSize(void); + uint32_t maxHookParameterSize(void); + uint32_t maxHookChainLength(void); + + uint32_t computeExecutionFee(uint64_t instructionCount); + uint32_t computeCreationFee(uint64_t byteCount); + + struct HookResult + { + ripple::uint256 const& hookSetTxnID; + ripple::uint256 const& hookHash; + ripple::Keylet const& accountKeylet; + ripple::Keylet const& ownerDirKeylet; + ripple::Keylet const& hookKeylet; + ripple::AccountID const& account; + ripple::AccountID const& otxnAccount; + ripple::uint256 const& hookNamespace; + + std::queue> emittedTxn {}; // etx stored here until accept/rollback + std::shared_ptr< + std::map>>>> // actual state data + changedState; + std::map< + ripple::uint256, // hook hash + std::map< + std::vector, // hook param name + std::vector // hook param value + >> hookParamOverrides; + + std::map< + std::vector, + std::vector> + const& hookParams; + std::set hookSkips; + hook_api::ExitType exitType = hook_api::ExitType::ROLLBACK; + std::string exitReason {""}; + int64_t exitCode {-1}; + uint64_t instructionCount {0}; + bool callback = false; + uint32_t wasmParam = 0; + uint32_t overrideCount = 0; + int32_t hookChainPosition = -1; + bool foreignStateSetDisabled = false; + }; + + class HookModule; + + struct SlotEntry + { + std::vector id; + std::shared_ptr storage; + const ripple::STBase* entry; // raw pointer into the storage, that can be freely pointed around inside + }; + + struct HookContext { + ripple::ApplyContext& applyCtx; + // slots are used up by requesting objects from inside the hook + // the map stores pairs consisting of a memory view and whatever shared or unique ptr is required to + // keep the underlying object alive for the duration of the hook's execution + // slot number -> { keylet or hash, { pointer to current object, storage for that object } } + std::map slot {}; + int slot_counter { 1 }; + std::queue slot_free {}; + int64_t expected_etxn_count { -1 }; // make this a 64bit int so the uint32 from the hookapi cant overflow it + int nonce_counter { 0 }; // incremented whenever nonce is called to ensure unique nonces + std::map nonce_used {}; + uint32_t generation = 0; // used for caching, only generated when txn_generation is called + int64_t burden = 0; // used for caching, only generated when txn_burden is called + int64_t fee_base = 0; + std::map guard_map {}; // iteration guard map upto_iteration> + HookResult result; + std::optional emitFailure; // if this is a callback from a failed + // emitted txn then this optional becomes + // populated with the SLE + const HookModule* module = 0; + }; + + + ripple::TER + setHookState( + HookResult& hookResult, + ripple::ApplyContext& applyCtx, + ripple::AccountID const& acc, + ripple::uint256 const& ns, + 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 ); + + #define ADD_HOOK_FUNCTION(F, ctx)\ + addHostFunc(#F, std::make_unique(ctx)) + + class HookModule : public SSVM::Runtime::ImportObject + { + //RH TODO UPTO put the hook-api functions here! + //then wrap/proxy them with the appropriate DECLARE_HOOK classes + //and add those below. HookModule::otxn_id() ... + public: + HookContext hookCtx; + + HookModule(HookContext& ctx) : SSVM::Runtime::ImportObject("env"), hookCtx(ctx) + { + ctx.module = this; + + ADD_HOOK_FUNCTION(_g, ctx); + ADD_HOOK_FUNCTION(accept, ctx); + ADD_HOOK_FUNCTION(rollback, ctx); + ADD_HOOK_FUNCTION(util_raddr, ctx); + ADD_HOOK_FUNCTION(util_accid, ctx); + ADD_HOOK_FUNCTION(util_verify, ctx); + ADD_HOOK_FUNCTION(util_sha512h, ctx); + ADD_HOOK_FUNCTION(sto_validate, ctx); + ADD_HOOK_FUNCTION(sto_subfield, ctx); + ADD_HOOK_FUNCTION(sto_subarray, ctx); + ADD_HOOK_FUNCTION(sto_emplace, ctx); + ADD_HOOK_FUNCTION(sto_erase, ctx); + ADD_HOOK_FUNCTION(util_keylet, ctx); + + ADD_HOOK_FUNCTION(emit, ctx); + ADD_HOOK_FUNCTION(etxn_burden, ctx); + ADD_HOOK_FUNCTION(etxn_fee_base, ctx); + ADD_HOOK_FUNCTION(etxn_details, ctx); + ADD_HOOK_FUNCTION(etxn_reserve, ctx); + ADD_HOOK_FUNCTION(etxn_generation, ctx); + + ADD_HOOK_FUNCTION(float_set, ctx); + ADD_HOOK_FUNCTION(float_multiply, ctx); + ADD_HOOK_FUNCTION(float_mulratio, ctx); + ADD_HOOK_FUNCTION(float_negate, ctx); + ADD_HOOK_FUNCTION(float_compare, ctx); + ADD_HOOK_FUNCTION(float_sum, ctx); + ADD_HOOK_FUNCTION(float_sto, ctx); + ADD_HOOK_FUNCTION(float_sto_set, ctx); + ADD_HOOK_FUNCTION(float_invert, ctx); + ADD_HOOK_FUNCTION(float_mantissa, ctx); + ADD_HOOK_FUNCTION(float_exponent, ctx); + + ADD_HOOK_FUNCTION(float_divide, ctx); + ADD_HOOK_FUNCTION(float_one, ctx); + ADD_HOOK_FUNCTION(float_mantissa, ctx); + ADD_HOOK_FUNCTION(float_mantissa_set, ctx); + ADD_HOOK_FUNCTION(float_exponent, ctx); + ADD_HOOK_FUNCTION(float_exponent_set, ctx); + ADD_HOOK_FUNCTION(float_sign, ctx); + ADD_HOOK_FUNCTION(float_sign_set, ctx); + ADD_HOOK_FUNCTION(float_int, ctx); + + + + ADD_HOOK_FUNCTION(otxn_burden, ctx); + ADD_HOOK_FUNCTION(otxn_generation, ctx); + ADD_HOOK_FUNCTION(otxn_field_txt, ctx); + ADD_HOOK_FUNCTION(otxn_field, ctx); + ADD_HOOK_FUNCTION(otxn_id, ctx); + ADD_HOOK_FUNCTION(otxn_type, ctx); + ADD_HOOK_FUNCTION(otxn_slot, ctx); + ADD_HOOK_FUNCTION(hook_account, ctx); + ADD_HOOK_FUNCTION(hook_hash, ctx); + ADD_HOOK_FUNCTION(fee_base, ctx); + ADD_HOOK_FUNCTION(ledger_seq, ctx); + ADD_HOOK_FUNCTION(ledger_last_hash, ctx); + ADD_HOOK_FUNCTION(nonce, ctx); + + ADD_HOOK_FUNCTION(hook_param, ctx); + ADD_HOOK_FUNCTION(hook_param_set, ctx); + ADD_HOOK_FUNCTION(hook_skip, ctx); + ADD_HOOK_FUNCTION(hook_pos, ctx); + + ADD_HOOK_FUNCTION(state, ctx); + ADD_HOOK_FUNCTION(state_foreign, ctx); + ADD_HOOK_FUNCTION(state_set, ctx); + ADD_HOOK_FUNCTION(state_foreign_set, ctx); + + ADD_HOOK_FUNCTION(slot, ctx); + ADD_HOOK_FUNCTION(slot_clear, ctx); + ADD_HOOK_FUNCTION(slot_count, ctx); + ADD_HOOK_FUNCTION(slot_id, ctx); + ADD_HOOK_FUNCTION(slot_set, ctx); + ADD_HOOK_FUNCTION(slot_size, ctx); + ADD_HOOK_FUNCTION(slot_subarray, ctx); + ADD_HOOK_FUNCTION(slot_subfield, ctx); + ADD_HOOK_FUNCTION(slot_type, ctx); + ADD_HOOK_FUNCTION(slot_float, ctx); + + ADD_HOOK_FUNCTION(trace, ctx); + ADD_HOOK_FUNCTION(trace_slot, ctx); + ADD_HOOK_FUNCTION(trace_num, ctx); + ADD_HOOK_FUNCTION(trace_float, ctx); + + SSVM::AST::Limit TabLimit(10, 20); + addHostTable("table", std::make_unique( + SSVM::ElemType::FuncRef, TabLimit)); + SSVM::AST::Limit MemLimit(1, 1); + addHostMemory("memory", std::make_unique(MemLimit)); + } + virtual ~HookModule() = default; + }; + +} + + diff --git a/src/ripple/app/tx/impl/ApplyContext.h b/src/ripple/app/tx/impl/ApplyContext.h index 1a4782661..7fdc887de 100644 --- a/src/ripple/app/tx/impl/ApplyContext.h +++ b/src/ripple/app/tx/impl/ApplyContext.h @@ -110,6 +110,11 @@ public: TER checkInvariants(TER const result, XRPAmount const fee); + bool emitted() + { + return tx.isFieldPresent(sfEmitDetails); + } + private: TER failInvariantCheck(TER const result); diff --git a/src/ripple/app/tx/impl/Change.cpp b/src/ripple/app/tx/impl/Change.cpp index 8c34f532d..602c45cdd 100644 --- a/src/ripple/app/tx/impl/Change.cpp +++ b/src/ripple/app/tx/impl/Change.cpp @@ -91,6 +91,7 @@ Change::preclaim(PreclaimContext const& ctx) case ttAMENDMENT: case ttFEE: case ttUNL_MODIFY: + case ttEMIT_FAILURE: return tesSUCCESS; default: return temUNKNOWN; @@ -108,6 +109,8 @@ Change::doApply() return applyFee(); case ttUNL_MODIFY: return applyUNLModify(); + case ttEMIT_FAILURE: + return applyEmitFailure(); default: assert(0); return tefFAILURE; @@ -242,6 +245,44 @@ Change::applyFee() return tesSUCCESS; } +TER +Change::applyEmitFailure() +{ + uint256 txnID(ctx_.tx.getFieldH256(sfTransactionHash)); + do + { + JLOG(j_.warn()) + << "HookEmit[" << txnID << "]: ttEmitFailure removing emitted txn"; + + auto key = keylet::emitted(txnID); + + auto const& sle = view().peek(key); + + if (!sle) + { + // RH NOTE: This will now be the normal execution path, the alternative will only occur if something + // went really wrong with the hook callback +// JLOG(j_.warn()) +// << "HookError[" << txnID << "]: ttEmitFailure (Change) tried to remove already removed emittedtxn"; + break; + } + + if (!view().dirRemove( + keylet::emittedDir(), + sle->getFieldU64(sfOwnerNode), + key, + false)) + { + JLOG(j_.fatal()) + << "HookError[" << txnID << "]: ttEmitFailure (Change) tefBAD_LEDGER"; + return tefBAD_LEDGER; + } + + view().erase(sle); + } while (0); + return tesSUCCESS; +} + TER Change::applyUNLModify() { diff --git a/src/ripple/app/tx/impl/Change.h b/src/ripple/app/tx/impl/Change.h index acd21837e..5d35aeab1 100644 --- a/src/ripple/app/tx/impl/Change.h +++ b/src/ripple/app/tx/impl/Change.h @@ -64,6 +64,9 @@ private: TER applyUNLModify(); + + TER + applyEmitFailure(); }; } // namespace ripple diff --git a/src/ripple/app/tx/impl/InvariantCheck.cpp b/src/ripple/app/tx/impl/InvariantCheck.cpp index 73b20a0f1..b75130619 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.cpp +++ b/src/ripple/app/tx/impl/InvariantCheck.cpp @@ -366,6 +366,10 @@ LedgerEntryTypesMatch::visitEntry( case ltCHECK: case ltDEPOSIT_PREAUTH: case ltNEGATIVE_UNL: + case ltHOOK: + case ltHOOK_DEFINITION: + case ltHOOK_STATE: + case ltEMITTED: break; default: invalidTypeAdded_ = true; diff --git a/src/ripple/app/tx/impl/SetHook.cpp b/src/ripple/app/tx/impl/SetHook.cpp new file mode 100644 index 000000000..256e847e1 --- /dev/null +++ b/src/ripple/app/tx/impl/SetHook.cpp @@ -0,0 +1,1909 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2014 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. +*/ +//============================================================================== + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "common/value.h" +#include "vm/configure.h" +#include "vm/vm.h" +#include "common/errcode.h" +#include "runtime/hostfunc.h" +#include "runtime/importobj.h" + +#define HS_ACC() ctx.tx.getAccountID(sfAccount) << "-" << ctx.tx.getTransactionID() +namespace ripple { + +// RH TODO deal with overflow on leb128 +// web assembly contains a lot of run length encoding in LEB128 format +inline uint64_t +parseLeb128(std::vector& buf, int start_offset, int* end_offset) +{ + uint64_t val = 0, shift = 0, i = start_offset; + while (i < buf.size()) + { + int b = (int)(buf[i]); + val += (b & 0x7F) << shift; + ++i; + if (b & 0x80) + { + shift += 7; + continue; + } + *end_offset = i; + return val; + } + return 0; +} + +// this macro will return temMALFORMED if i ever exceeds the end of the hook +#define CHECK_SHORT_HOOK()\ +{\ + if (i >= hook.size())\ + {\ + JLOG(ctx.j.trace())\ + << "HookSet[" << HS_ACC() << "]: Malformed transaction: Hook truncated or otherwise invalid\n";\ + return {false, 0};\ + }\ +} + +// RH TODO find a better home for this or a better solution? +const std::set import_whitelist +{ + "accept", + "emit", + "etxn_burden", + "etxn_details", + "etxn_fee_base", + "etxn_generation", + "etxn_reserve", + "float_compare", + "float_divide", + "float_exponent", + "float_exponent_set", + "float_invert", + "float_mantissa", + "float_mantissa_set", + "float_mulratio", + "float_multiply", + "float_int", + "float_negate", + "float_one", + "float_set", + "float_sign", + "float_sign_set", + "float_sto", + "float_sto_set", + "float_sum", + "fee_base", + "_g", + "hook_account", + "hook_hash", + "ledger_seq", + "ledger_last_hash", + "nonce", + "otxn_burden", + "otxn_field", + "otxn_slot", + "otxn_generation", + "otxn_id", + "otxn_type", + "rollback", + "slot", + "slot_clear", + "slot_count", + "slot_id", + "slot_set", + "slot_size", + "slot_subarray", + "slot_subfield", + "slot_type", + "slot_float", + "state", + "state_foreign", + "state_set", + "state_foreign_set", + "trace", + "trace_num", + "trace_float", + "trace_slot", + "util_accid", + "util_raddr", + "util_sha512h", + "util_verify", + "sto_subarray", + "sto_subfield", + "sto_validate", + "sto_emplace", + "sto_erase", + "util_keylet", + "hook_pos", + "hook_param", + "hook_param_set", + "hook_skip" +}; + + +#define DEBUG_GUARD_CHECK 0 + + +// checks the WASM binary for the appropriate required _g guard calls and rejects it if they are not found +// start_offset is where the codesection or expr under analysis begins and end_offset is where it ends +// returns {valid, worst case instruction count} +std::pair +check_guard( + SetHookCtx& ctx, + ripple::Blob& hook, int codesec, + int start_offset, int end_offset, int guard_func_idx, int last_import_idx) +{ + + if (end_offset <= 0) end_offset = hook.size(); + int block_depth = 0; + int mode = 1; // controls the state machine for searching for guards + // 0 = looking for guard from a trigger point (loop or function start) + // 1 = looking for a new trigger point (loop); + // currently always starts at 1 no-top-of-func check, see above block comment + + std::stack stack; // we track the stack in mode 0 to work out if constants end up in the guard function + std::map local_map; // map of local variables since the trigger point + std::map global_map; // map of global variables since the trigger point + + // block depth level -> { largest guard, rolling instruction count } + std::map> instruction_count; + + // largest guard // instr ccount + instruction_count[0] = {1, 0}; + + if (DEBUG_GUARD_CHECK) + printf("\n\n\nstart of guard analysis for codesec %d\n", codesec); + + for (int i = start_offset; i < end_offset; ) + { + + if (DEBUG_GUARD_CHECK) + { + printf("->"); + for (int z = i; z < 16 + i && z < end_offset; ++z) + printf("%02X", hook[z]); + printf("\n"); + } + + int instr = hook[i++]; CHECK_SHORT_HOOK(); + instruction_count[block_depth].second++; + + if (instr == 0x10) // call instr + { + int callee_idx = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK(); + if (DEBUG_GUARD_CHECK) + printf("%d - call instruction at %d -- call funcid: %d\n", mode, i, callee_idx); + + // disallow calling of user defined functions inside a hook + if (callee_idx > last_import_idx) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() << "]: GuardCheck " + << "Hook calls a function outside of the whitelisted imports " + << "codesec: " << codesec << " hook byte offset: " << i; + return {false, 0}; + } + + if (callee_idx == guard_func_idx) + { + // found! + if (mode == 0) + { + + if (stack.size() < 2) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() << "]: GuardCheck " + << "_g() called but could not detect constant parameters " + << "codesec: " << codesec << " hook byte offset: " << i << "\n"; + return {false, 0}; + } + + uint64_t a = stack.top(); + stack.pop(); + uint64_t b = stack.top(); + stack.pop(); + if (DEBUG_GUARD_CHECK) + printf("FOUND: GUARD(%llu, %llu), codesec: %d offset %d\n", a, b, codesec, i); + + if (b <= 0) + { + // 0 has a special meaning, generally it's not a constant value + // < 0 is a constant but negative, either way this is a reject condition + JLOG(ctx.j.trace()) << "HookSet[" << HS_ACC() << "]: GuardCheck " + << "_g() called but could not detect constant parameters " + << "codesec: " << codesec << " hook byte offset: " << i << "\n"; + return {false, 0}; + } + + // update the instruction count for this block depth to the largest possible guard + if (instruction_count[block_depth].first < a) + { + instruction_count[block_depth].first = a; + if (DEBUG_GUARD_CHECK) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() << "]: GuardCheck " + << "Depth " << block_depth << " guard: " << a; + } + } + + // clear stack and maps + while (stack.size() > 0) + stack.pop(); + local_map.clear(); + global_map.clear(); + mode = 1; + } + } + continue; + } + + if (instr == 0x11) // call indirect [ we don't allow guard to be called this way ] + { + JLOG(ctx.j.trace()) << "HookSet[" << HS_ACC() << "]: GuardCheck " + << "Call indirect detected and is disallowed in hooks " + << "codesec: " << codesec << " hook byte offset: " << i; + return {false, 0}; + /* + if (DEBUG_GUARD_CHECK) + printf("%d - call_indirect instruction at %d\n", mode, i); + parseLeb128(hook, i, &i); CHECK_SHORT_HOOK(); + ++i; CHECK_SHORT_HOOK(); //absorb 0x00 trailing + continue; + */ + } + + // unreachable and nop instructions + if (instr == 0x00 || instr == 0x01) + { + if (DEBUG_GUARD_CHECK) + printf("%d - unreachable/nop instruction at %d\n", mode, i); + continue; + } + + // branch loop block instructions + if ((instr >= 0x02 && instr <= 0x0F) || instr == 0x11) + { + if (mode == 0) + { + JLOG(ctx.j.trace()) << "HookSet[" << HS_ACC() << "]: GuardCheck " + << "_g() did not occur at start of function or loop statement " + << "codesec: " << codesec << " hook byte offset: " << i; + return {false, 0}; + } + + // execution to here means we are in 'search mode' for loop instructions + + // block instruction + if (instr == 0x02) + { + if (DEBUG_GUARD_CHECK) + printf("%d - block instruction at %d\n", mode, i); + + ++i; CHECK_SHORT_HOOK(); + block_depth++; + instruction_count[block_depth] = {1, 0}; + continue; + } + + // loop instruction + if (instr == 0x03) + { + if (DEBUG_GUARD_CHECK) + printf("%d - loop instruction at %d\n", mode, i); + + ++i; CHECK_SHORT_HOOK(); + mode = 0; // we now search for a guard() + block_depth++; + instruction_count[block_depth] = {1, 0}; + continue; + } + + // if instr + if (instr == 0x04) + { + if (DEBUG_GUARD_CHECK) + printf("%d - if instruction at %d\n", mode, i); + ++i; CHECK_SHORT_HOOK(); + block_depth++; + instruction_count[block_depth] = {1, 0}; + continue; + } + + // else instr + if (instr == 0x05) + { + if (DEBUG_GUARD_CHECK) + printf("%d - else instruction at %d\n", mode, i); + continue; + } + + // branch instruction + if (instr == 0x0C || instr == 0x0D) + { + if (DEBUG_GUARD_CHECK) + printf("%d - br instruction at %d\n", mode, i); + parseLeb128(hook, i, &i); CHECK_SHORT_HOOK(); + continue; + } + + // branch table instr + if (instr == 0x0E) + { + if (DEBUG_GUARD_CHECK) + printf("%d - br_table instruction at %d\n", mode, i); + int vec_count = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK(); + for (int v = 0; v < vec_count; ++v) + { + parseLeb128(hook, i, &i); + CHECK_SHORT_HOOK(); + } + parseLeb128(hook, i, &i); CHECK_SHORT_HOOK(); + continue; + } + } + + + // parametric instructions | no operands + if (instr == 0x1A || instr == 0x1B) + { + if (DEBUG_GUARD_CHECK) + printf("%d - parametric instruction at %d\n", mode, i); + continue; + } + + // variable instructions + if (instr >= 0x20 && instr <= 0x24) + { + if (DEBUG_GUARD_CHECK) + printf("%d - variable local/global instruction at %d\n", mode, i); + + int idx = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK(); + + if (mode == 1) + continue; + + // we need to do stack and map manipualtion to track any possible constants before guard call + if (instr == 0x20 || instr == 0x23) // local.get idx or global.get idx + { + auto& map = ( instr == 0x20 ? local_map : global_map ); + if (map.find(idx) == map.end()) + stack.push(0); // we always put a 0 in place of a local or global we don't know the value of + else + stack.push(map[idx]); + continue; + } + + if (instr == 0x21 || instr == 0x22 || instr == 0x24) // local.set idx or global.set idx + { + auto& map = ( instr == 0x21 || instr == 0x22 ? local_map : global_map ); + + uint64_t to_store = (stack.size() == 0 ? 0 : stack.top()); + map[idx] = to_store; + if (instr != 0x22) + stack.pop(); + + continue; + } + } + + // RH TODO support guard consts being passed through memory functions (maybe) + + //memory instructions + if (instr >= 0x28 && instr <= 0x3E) + { + if (DEBUG_GUARD_CHECK) + printf("%d - variable memory instruction at %d\n", mode, i); + + parseLeb128(hook, i, &i); CHECK_SHORT_HOOK(); + parseLeb128(hook, i, &i); CHECK_SHORT_HOOK(); + continue; + } + + // more memory instructions + if (instr == 0x3F || instr == 0x40) + { + if (DEBUG_GUARD_CHECK) + printf("%d - memory instruction at %d\n", mode, i); + + ++i; CHECK_SHORT_HOOK(); + if (instr == 0x40) // disallow memory.grow + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() << "]: GuardCheck " + << "Memory.grow instruction not allowed at " + << "codesec: " << codesec << " hook byte offset: " << i << "\n"; + return {false, 0}; + } + continue; + } + + // int const instrs + // numeric instructions with immediates + if (instr == 0x41 || instr == 0x42) + { + + if (DEBUG_GUARD_CHECK) + printf("%d - const instruction at %d\n", mode, i); + + uint64_t immediate = parseLeb128(hook, i, &i); + CHECK_SHORT_HOOK(); // RH TODO enforce i32 i64 size limit + + // in mode 0 we should be stacking our constants and tracking their movement in + // and out of locals and globals + stack.push(immediate); + continue; + } + + // const instr + // more numerics with immediates + if (instr == 0x43 || instr == 0x44) + { + + if (DEBUG_GUARD_CHECK) + printf("%d - const float instruction at %d\n", mode, i); + + i += ( instr == 0x43 ? 4 : 8 ); + CHECK_SHORT_HOOK(); + continue; + } + + // numerics no immediates + if (instr >= 0x45 && instr <= 0xC4) + { + if (DEBUG_GUARD_CHECK) + printf("%d - numeric instruction at %d\n", mode, i); + continue; + } + + // truncation instructions + if (instr == 0xFC) + { + if (DEBUG_GUARD_CHECK) + printf("%d - truncation instruction at %d\n", mode, i); + i++; CHECK_SHORT_HOOK(); + parseLeb128(hook, i, &i); CHECK_SHORT_HOOK(); + continue; + } + + if (instr == 0x0B) + { + if (DEBUG_GUARD_CHECK) + printf("%d - block end instruction at %d\n", mode, i); + + // end of expression + if (block_depth == 0) + break; + + block_depth--; + + if (block_depth < 0) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() << "]: GuardCheck " + << "Unexpected 0x0B instruction, malformed" + << "codesec: " << codesec << " hook byte offset: " << i; + return {false, 0}; + } + + // perform the instruction count * guard accounting + instruction_count[block_depth].second += + instruction_count[block_depth+1].second * instruction_count[block_depth+1].first; + instruction_count.erase(block_depth+1); + } + } + + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() << "]: GuardCheck " + << "Total worse-case execution count: " << instruction_count[0].second; + + // RH TODO: don't hardcode this + if (instruction_count[0].second > 0xFFFFF) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() << "]: GuardCheck " + << "Maximum possible instructions exceed 1048575, please make your hook smaller " + << "or check your guards!"; + return {false, 0}; + } + + // if we reach the end of the code looking for another trigger the guards are installed correctly + if (mode == 1) + return {true, instruction_count[0].second}; + + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() << "]: GuardCheck " + << "Guard did not occur before end of loop / function. " + << "Codesec: " << codesec; + return {false, 0}; + +} + +bool +validateHookParams(SetHookCtx& ctx, STArray const& hookParams) +{ + for (auto const& hookParam : hookParams) + { + auto const& hookParamObj = dynamic_cast(&hookParam); + + if (!hookParamObj || (hookParamObj->getFName() != sfHookParameter)) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: " + << "SetHook sfHookParameters contains obj other than sfHookParameter."; + return false; + } + + bool nameFound = false; + for (auto const& paramElement : *hookParamObj) + { + auto const& name = paramElement.getFName(); + + if (name != sfHookParameterName && name != sfHookParameterValue) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: " + << "SetHook sfHookParameter contains object other than sfHookParameterName/Value."; + return false; + } + + if (name == sfHookParameterName) + nameFound = true; + } + + if (!nameFound) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: " + << "SetHook sfHookParameter must contain at least sfHookParameterName"; + return false; + } + } + + return true; +} + +// returns < valid, instruction count > +std::pair +validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj) +{ + uint64_t maxInstrCount = 0; + uint64_t byteCount = 0; + + bool hasHash = hookSetObj.isFieldPresent(sfHookHash); + bool hasCode = hookSetObj.isFieldPresent(sfCreateCode); + + // mutex options: either link an existing hook or create a new one + if (hasHash && hasCode) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: SetHook must provide only one of sfCreateCode or sfHookHash."; + return {false, 0}; + } + + // validate hook params structure + if (hookSetObj.isFieldPresent(sfHookParameters) && + !validateHookParams(ctx, hookSetObj.getFieldArray(sfHookParameters))) + return {false, 0}; + + // validate hook grants structure + if (hookSetObj.isFieldPresent(sfHookGrants)) + { + auto const& hookGrants = hookSetObj.getFieldArray(sfHookGrants); + + if (hookGrants.size() < 1) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: SetHook sfHookGrants empty."; + return {false, 0}; + } + + if (hookGrants.size() > 8) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: SetHook sfHookGrants contains more than 8 entries."; + return {false, 0}; + } + + for (auto const& hookGrant : hookGrants) + { + auto const& hookGrantObj = dynamic_cast(&hookGrant); + if (!hookGrantObj || (hookGrantObj->getFName() != sfHookGrant)) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: SetHook sfHookGrants did not contain sfHookGrant object."; + return {false, 0}; + } + } + } + + // link existing hook + if (hasHash) + { + // ensure no hookapiversion field was provided + if (hookSetObj.isFieldPresent(sfHookApiVersion)) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: HookApiVersion can only be provided when creating a new hook."; + return {false, 0}; + } + + return {true, 0}; + } + + // execution to here means this is an sfCreateCode (hook creation) entry + + // ensure hooknamespace is present + if (!hookSetObj.isFieldPresent(sfHookNamespace)) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: SetHook sfHookDefinition must contain sfHookNamespace."; + return {false, 0}; + } + + // validate api version, if provided + if (!hookSetObj.isFieldPresent(sfHookApiVersion)) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: SetHook sfHookApiVersion must be included."; + return {false, 0}; + } + else + { + auto version = hookSetObj.getFieldU16(sfHookApiVersion); + if (version != 0) + { + // we currently only accept api version 0 + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: SetHook sfHookDefinition->sfHookApiVersion invalid. (Try 0)."; + return {false, 0}; + } + } + + // validate sfHookOn + if (!hookSetObj.isFieldPresent(sfHookOn)) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: SetHook must include sfHookOn when creating a new hook."; + return {false, 0}; + } + + + // validate createcode + Blob hook = hookSetObj.getFieldVL(sfCreateCode); + if (hook.empty()) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: SetHook sfHookDefinition must contain non-blank sfCreateCode."; + return {false, 0}; + } + + byteCount = hook.size(); + + // RH TODO compute actual smallest possible hook and update this value + if (byteCount < 10) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() << "]: " + << "Malformed transaction: Hook was not valid webassembly binary. Too small."; + return {false, 0}; + } + + // check header, magic number + unsigned char header[8] = { 0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U }; + for (int i = 0; i < 8; ++i) + { + if (hook[i] != header[i]) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() << "]: " + << "Malformed transaction: Hook was not valid webassembly binary. " + << "Missing magic number or version."; + return {false, 0}; + } + } + + // now we check for guards... first check if _g is imported + int guard_import_number = -1; + int last_import_number = -1; + for (int i = 8, j = 0; i < hook.size();) + { + + if (j == i) + { + // if the loop iterates twice with the same value for i then + // it's an infinite loop edge case + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() << "]: Malformed transaction: Hook is invalid WASM binary."; + return {false, 0}; + } + + j = i; + + // each web assembly section begins with a single byte section type followed by an leb128 length + int section_type = hook[i++]; + int section_length = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK(); + //int section_start = i; + + if (DEBUG_GUARD_CHECK) + printf("WASM binary analysis -- upto %d: section %d with length %d\n", + i, section_type, section_length); + + int next_section = i + section_length; + + // we are interested in the import section... we need to know if _g is imported and which import# it is + if (section_type == 2) // import section + { + int import_count = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK(); + if (import_count <= 0) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() << "]: Malformed transaction. " + << "Hook did not import any functions... " + << "required at least guard(uint32_t, uint32_t) and accept, reject or rollback"; + return {false, 0}; + } + + // process each import one by one + int func_upto = 0; // not all imports are functions so we need an indep counter for these + for (int j = 0; j < import_count; ++j) + { + // first check module name + int mod_length = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK(); + if (mod_length < 1 || mod_length > (hook.size() - i)) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() << "]: Malformed transaction. " + << "Hook attempted to specify nil or invalid import module"; + return {false, 0}; + } + + if (std::string_view( (const char*)(hook.data() + i), (size_t)mod_length ) != "env") + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() << "]: Malformed transaction. " + << "Hook attempted to specify import module other than 'env'"; + return {false, 0}; + } + + i += mod_length; CHECK_SHORT_HOOK(); + + // next get import name + int name_length = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK(); + if (name_length < 1 || name_length > (hook.size() - i)) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() << "]: Malformed transaction. " + << "Hook attempted to specify nil or invalid import name"; + return {false, 0}; + } + + std::string import_name { (const char*)(hook.data() + i), (size_t)name_length }; + + i += name_length; CHECK_SHORT_HOOK(); + + // next get import type + if (hook[i] > 0x00) + { + // not a function import + // RH TODO check these other imports for weird stuff + i++; CHECK_SHORT_HOOK(); + parseLeb128(hook, i, &i); CHECK_SHORT_HOOK(); + continue; + } + + // execution to here means it's a function import + i++; CHECK_SHORT_HOOK(); + /*int type_idx = */ + parseLeb128(hook, i, &i); CHECK_SHORT_HOOK(); + + // RH TODO: validate that the parameters of the imported functions are correct + if (import_name == "_g") + { + guard_import_number = func_upto; + } else if (import_whitelist.find(import_name) == import_whitelist.end()) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() << "]: Malformed transaction. " + << "Hook attempted to import a function that does not " + << "appear in the hook_api function set: `" << import_name << "`"; + return {false, 0}; + } + func_upto++; + } + + if (guard_import_number == -1) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() << "]: Malformed transaction. " + << "Hook did not import _g (guard) function"; + return {false, 0}; + } + + last_import_number = func_upto - 1; + + // we have an imported guard function, so now we need to enforce the guard rules + // which are: + // 1. all functions must start with a guard call before any branching [ RH TODO ] + // 2. all loops must start with a guard call before any branching + // to enforce these rules we must do a second pass of the wasm in case the function + // section was placed in this wasm binary before the import section + + } else + if (section_type == 7) // export section + { + int export_count = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK(); + if (export_count <= 0) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() << "]: Malformed transaction. " + << "Hook did not export any functions... " + << "required hook(int64_t), callback(int64_t)."; + return {false, 0}; + } + + bool found_hook_export = false; + bool found_cbak_export = false; + for (int j = 0; j < export_count && !(found_hook_export && found_cbak_export); ++j) + { + int name_len = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK(); + if (name_len == 4) + { + + if (hook[i] == 'h' && hook[i+1] == 'o' && hook[i+2] == 'o' && hook[i+3] == 'k') + found_hook_export = true; + else + if (hook[i] == 'c' && hook[i+1] == 'b' && hook[i+2] == 'a' && hook[i+3] == 'k') + found_cbak_export = true; + } + + i += name_len + 1; + parseLeb128(hook, i, &i); CHECK_SHORT_HOOK(); + } + + // execution to here means export section was parsed + if (!(found_hook_export && found_cbak_export)) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() << "]: Malformed transaction. " + << "Hook did not export: " << + ( !found_hook_export ? "hook(int64_t); " : "" ) << + ( !found_cbak_export ? "cbak(int64_t);" : "" ); + return {false, 0}; + } + } + + i = next_section; + continue; + } + + + // second pass... where we check all the guard function calls follow the guard rules + // minimal other validation in this pass because first pass caught most of it + for (int i = 8; i < hook.size();) + { + + int section_type = hook[i++]; + int section_length = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK(); + //int section_start = i; + int next_section = i + section_length; + + // RH TODO: parse anywhere else an expr is allowed in wasm and enforce rules there too + if (section_type == 10) // code section + { + // these are the functions + int func_count = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK(); + + for (int j = 0; j < func_count; ++j) + { + // parse locals + int code_size = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK(); + int code_end = i + code_size; + int local_count = parseLeb128(hook, i, &i); CHECK_SHORT_HOOK(); + for (int k = 0; k < local_count; ++k) + { + /*int array_size = */ + parseLeb128(hook, i, &i); CHECK_SHORT_HOOK(); + if (!(hook[i] >= 0x7C && hook[i] <= 0x7F)) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() << "]: Invalid local type. " + << "Codesec: " << j << " " + << "Local: " << k << " " + << "Offset: " << i; + return {false, 0}; + } + i++; CHECK_SHORT_HOOK(); + } + + if (i == code_end) + continue; // allow empty functions + + // execution to here means we are up to the actual expr for the codesec/function + + auto [valid, instruction_count] = + check_guard(ctx, hook, j, i, code_end, guard_import_number, last_import_number); + + if (!valid) + return {false, 0}; + + // the worst case execution is the fee, this includes the worst case between cbak and hook + if (instruction_count > maxInstrCount) + maxInstrCount = instruction_count; + + i = code_end; + + } + } + i = next_section; + } + + // execution to here means guards are installed correctly + + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() << "]: Trying to wasm instantiate proposed hook " + << "size = " << hook.size(); + + // check if wasm can be run + SSVM::VM::Configure cfg; + SSVM::VM::VM vm(cfg); + if (auto res = vm.loadWasm(SSVM::Span(hook.data(), hook.size()))) + { + // do nothing + } else + { + uint32_t ssvm_error = static_cast(res.error()); + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() << "]: " + << "Tried to set a hook with invalid code. SSVM error: " << ssvm_error; + return {false, 0}; + } + + return {true, maxInstrCount}; +} + +FeeUnit64 +SetHook::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + FeeUnit64 extraFee{0}; + + auto const& hookSets = tx.getFieldArray(sfHooks); + + for (auto const& hookSet : hookSets) + { + auto const& hookSetObj = dynamic_cast(&hookSet); + + if (!hookSetObj->isFieldPresent(sfCreateCode)) + continue; + + extraFee += FeeUnit64{ + hook::computeCreationFee( + hookSetObj->getFieldVL(sfCreateCode).size())}; + } + + return Transactor::calculateBaseFee(view, tx) + extraFee; +} + +TER +SetHook::preclaim(ripple::PreclaimContext const& ctx) +{ + + auto const& hookSets = ctx.tx.getFieldArray(sfHooks); + + for (auto const& hookSet : hookSets) + { + + auto const& hookSetObj = dynamic_cast(&hookSet); + + if (!hookSetObj->isFieldPresent(sfHookHash)) + continue; + + auto const& hash = hookSetObj->getFieldH256(sfHookHash); + { + if (!ctx.view.exists(keylet::hookDefinition(hash))) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: No hook exists with the specified hash."; + return terNO_HOOK; + } + } + } + + return tesSUCCESS; +} + +NotTEC +SetHook::preflight(PreflightContext const& ctx) +{ + + if (!ctx.rules.enabled(featureHooks)) + { + JLOG(ctx.j.warn()) << "HookSet[" << HS_ACC() << "]: Hooks Amendment not enabled!"; + return temDISABLED; + } + + auto const ret = preflight1(ctx); + if (!isTesSuccess(ret)) + return ret; + + if (!ctx.tx.isFieldPresent(sfHooks)) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() << "]: Malformed transaction: SetHook lacked sfHooks array."; + return temMALFORMED; + } + + auto const& hookSets = ctx.tx.getFieldArray(sfHooks); + + if (hookSets.size() < 1) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: SetHook sfHooks empty."; + return temMALFORMED; + } + + if (hookSets.size() > hook::maxHookChainLength()) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: SetHook sfHooks contains more than " << hook::maxHookChainLength() + << " entries."; + return temMALFORMED; + } + + SetHookCtx shCtx + { + .j = ctx.j, + .tx = ctx.tx, + .app = ctx.app + }; + + bool allBlank = true; + + for (auto const& hookSet : hookSets) + { + + auto const& hookSetObj = dynamic_cast(&hookSet); + + if (!hookSetObj || (hookSetObj->getFName() != sfHook)) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: SetHook sfHooks contains obj other than sfHook."; + return temMALFORMED; + } + + if (hookSetObj->getCount() == 0) // skip blanks + continue; + + allBlank = false; + + for (auto const& hookSetElement : *hookSetObj) + { + auto const& name = hookSetElement.getFName(); + + if (name != sfCreateCode && + name != sfHookHash && + name != sfHookNamespace && + name != sfHookParameters && + name != sfHookOn && + name != sfHookGrants && + name != sfHookApiVersion && + name != sfFlags) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: SetHook sfHook contains invalid field."; + return temMALFORMED; + } + } + + // validate the "create code" part if it's present + [[maybe_unused]] + auto [valid, _] = + validateHookSetEntry(shCtx, *hookSetObj); + + if (!valid) + return temMALFORMED; + } + + if (allBlank) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: SetHook sfHooks must contain at least one non-blank sfHook."; + return temMALFORMED; + } + + + return preflight2(ctx); +} + + + +TER +SetHook::doApply() +{ + preCompute(); + return setHook(); +} + +void +SetHook::preCompute() +{ + return Transactor::preCompute(); +} + +TER +SetHook::destroyNamespace( + SetHookCtx& ctx, + ApplyView& view, + const AccountID& account, + const Keylet & dirKeylet // the keylet of the namespace directory +) { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() << "]: DeleteState " + << "Destroying Hook Namespace for " << account << " namespace keylet " << dirKeylet.key; + + std::shared_ptr sleDirNode{}; + unsigned int uDirEntry{0}; + uint256 dirEntry{beast::zero}; + + if (dirIsEmpty(view, dirKeylet)) + return tesSUCCESS; + + if (!cdirFirst( + view, + dirKeylet.key, + sleDirNode, + uDirEntry, + dirEntry, + ctx.j)) { + JLOG(ctx.j.fatal()) + << "HookSet[" << HS_ACC() << "]: DeleteState " + << "account directory missing " << account; + return tefINTERNAL; + } + + do + { + // Make sure any directory node types that we find are the kind + // we can delete. + Keylet const itemKeylet{ltCHILD, dirEntry}; + auto sleItem = view.peek(itemKeylet); + if (!sleItem) + { + // Directory node has an invalid index. Bail out. + JLOG(ctx.j.fatal()) + << "HookSet[" << HS_ACC() << "]: DeleteState " + << "directory node in ledger " << view.seq() << " " + << "has index to object that is missing: " + << to_string(dirEntry); + return tefBAD_LEDGER; + } + + auto nodeType = sleItem->getFieldU16(sfLedgerEntryType); + + if (nodeType == ltHOOK_STATE) { + // delete it! + auto const hint = (*sleItem)[sfOwnerNode]; + if (!view.dirRemove(dirKeylet, hint, itemKeylet.key, false)) + { + JLOG(ctx.j.fatal()) + << "HookSet[" << HS_ACC() << "]: DeleteState " + << "directory node in ledger " << view.seq() << " " + << "has undeletable ltHOOK_STATE"; + return tefBAD_LEDGER; + } + view.erase(sleItem); + } + + + } while (cdirNext( + view, dirKeylet.key, sleDirNode, uDirEntry, dirEntry, ctx.j)); + + return tesSUCCESS; +} + + +#define DIRECTORY_DEC()\ +{\ + if (!oldDirSLE)\ + {\ + JLOG(ctx.j.warn())\ + << "HookSet could not find old hook state dir "\ + << HS_ACC() << "!!!";\ + return tecINTERNAL;\ + }\ + uint64_t refCount = oldDirSLE->getFieldU64(sfReferenceCount);\ + printf("refcount1: %d\n", refCount);\ + if (refCount == 0)\ + {\ + JLOG(ctx.j.warn())\ + << "HookSet dir reference count below 0 "\ + << HS_ACC() << "!!!";\ + return tecINTERNAL;\ + }\ + --refCount;\ + printf("refcount2: %d\n", refCount);\ + oldDirSLE->setFieldU64(sfReferenceCount, refCount);\ + view().update(oldDirSLE);\ + if (refCount <= 0)\ + dirsToDestroy[oldDirKeylet->key] = flags & FLAG_NSDELETE;\ +} + +#define DIRECTORY_INC()\ +{\ + if (!newDirSLE)\ + {\ + newDirSLE = std::make_shared(*newDirKeylet);\ + auto const page = dirAdd(\ + view(),\ + ownerDirKeylet,\ + newDirKeylet->key,\ + false,\ + describeOwnerDir(account_),\ + ctx.j);\ + JLOG(ctx.j.trace()) << "Create state dir for account " << toBase58(account_)\ + << ": " << (page ? "success" : "failure");\ + if (!page)\ + return tecDIR_FULL;\ + newDirSLE->setFieldU64(sfOwnerNode, *page);\ + newDirSLE->setFieldU64(sfReferenceCount, 1);\ + view().insert(newDirSLE);\ + }\ + else\ + {\ + newDirSLE->setFieldU64(sfReferenceCount, newDirSLE->getFieldU64(sfReferenceCount) + 1);\ + view().update(newDirSLE);\ + }\ +} + +#define DEFINITION_DEC()\ +{\ + if (!oldDefSLE)\ + {\ + JLOG(ctx.j.warn())\ + << "HookSet could not find old hook "\ + << HS_ACC() << "!!!";\ + return tecINTERNAL;\ + }\ + uint64_t refCount = oldDefSLE->getFieldU64(sfReferenceCount);\ + if (refCount == 0)\ + {\ + JLOG(ctx.j.warn())\ + << "HookSet def reference count below 0 "\ + << HS_ACC() << "!!!";\ + return tecINTERNAL;\ + }\ + oldDefSLE->setFieldU64(sfReferenceCount, refCount-1);\ + view().update(oldDefSLE);\ + if (refCount <= 0)\ + defsToDestroy[oldDefKeylet->key] = flags & FLAG_OVERRIDE;\ +} + +#define DEFINITION_INC()\ +{\ + newDefSLE->setFieldU64(sfReferenceCount, newDefSLE->getFieldU64(sfReferenceCount) + 1);\ + view().update(newDefSLE);\ +} + +TER +SetHook::setHook() +{ + + /** + * Each account has optionally an ltHOOK object + * Which contains an array (sfHooks) of sfHook objects + * The set hook transaction also contains an array (sfHooks) of sfHook objects + * These two arrays are mapped 1-1 when updating, inserting or deleting hooks + * When the user submits a new hook that does not yet exist on the ledger an ltHOOK_DEFINITION object is created + * Further users setting the same hook code will reference this object using sfHookHash. + */ + + SetHookCtx ctx + { + .j = ctx_.app.journal("View"), + .tx = ctx_.tx, + .app = ctx_.app + }; + + const int blobMax = hook::maxHookWasmSize(); + const int paramMax = hook::maxHookParameterSize(); + auto const accountKeylet = keylet::account(account_); + auto const ownerDirKeylet = keylet::ownerDir(account_); + auto const hookKeylet = keylet::hook(account_); + + ripple::STArray newHooks{sfHooks, 8}; + auto newHookSLE = std::make_shared(hookKeylet); + + int oldHookCount = 0; + std::optional> oldHooks; + auto const& oldHookSLE = view().peek(hookKeylet); + + if (oldHookSLE) + { + oldHooks = oldHookSLE->getFieldArray(sfHooks); + oldHookCount = (oldHooks->get()).size(); + } + + std::map defsToDestroy {}; // keylet => override was in flags + std::map dirsToDestroy {}; // keylet => nsdelete was in flags + + int hookSetNumber = -1; + auto const& hookSets = ctx.tx.getFieldArray(sfHooks); + for (auto const& hookSet : hookSets) + { + hookSetNumber++; + + ripple::STObject newHook { sfHook }; + std::optional> oldHook; + // an existing hook would only be present if the array slot also exists on the ltHOOK object + if (hookSetNumber < oldHookCount) + oldHook = std::cref((oldHooks->get()[hookSetNumber]).downcast()); + + STObject const* hookSetObj = dynamic_cast(&hookSet); + // blank hookSet entries are allowed and means semantically: "do nothing to this hook entry in the chain" + if (hookSetObj->getCount() == 0) + { + // if a hook already exists here then migrate it to the new array + // if it doesn't exist just place a blank object here + newHooks.push_back( oldHook ? oldHook->get() : ripple::STObject{sfHook} ); + continue; + } + + // execution to here means the hookset entry is not blank + + std::optional oldNamespace; + std::optional defNamespace; + std::optional oldDirKeylet; + std::optional oldDefKeylet; + std::optional newDefKeylet; + std::shared_ptr oldDefSLE; + std::shared_ptr newDefSLE; + std::shared_ptr oldDirSLE; + std::shared_ptr newDirSLE; + + std::optional newNamespace; + std::optional newDirKeylet; + + std::optional oldHookOn; + std::optional newHookOn; + std::optional defHookOn; + + /** + * This is the primary HookSet loop. We iterate the sfHooks array inside the txn + * each entry of this array is available as hookSetObj. + * Depending on whether or not an existing hook is present in the array slot we are currently up to + * this hook and its various attributes are available in the optionals prefixed with old. + * Even if an existing hook is being modified by the sethook obj, we create a newHook obj + * so a degree of copying is required. + */ + + + bool hasHookHash = hookSetObj->isFieldPresent(sfHookHash); + bool hasCreateCode = hookSetObj->isFieldPresent(sfCreateCode); + bool hasParameters = hookSetObj->isFieldPresent(sfHookParameters); + bool hasGrants = hookSetObj->isFieldPresent(sfHookGrants); + + uint32_t flags = hookSetObj->isFieldPresent(sfFlags) ? hookSetObj->getFieldU32(sfFlags) : 0; + + bool isDeleteOperation = + hasCreateCode && hookSetObj->getFieldVL(sfCreateCode).size() == 0; + + printf("PATH X\n"); + bool isUpdateOperation = + oldHook && hasHookHash && + (hookSetObj->getFieldH256(sfHookHash) == oldHook->get().getFieldH256(sfHookHash)); + + bool isCreateOperation = + !isUpdateOperation && !isDeleteOperation && hasCreateCode; + + bool isInstallOperation = + !isUpdateOperation && !isDeleteOperation && hasHookHash; + + /** + * Variables for logic: + * hasHookHash <=> the current HookSet operation contains a sfHookHash (not an sfCreateCode) + * hasCreateCode <=> the current HookSet operation contains a sfCreateCode (not sfHookHash) + * isDeleteOperation <=> the current HookSet operation contains a blank sfCreateCode + * isUpdateOperation <=> old hook exists and the current operation updates it + * isCreateOperation <=> old hook does not exist and is not a delete operaiton and code is present + * isInstallOperation <=> old hook does not exist, and we're installing from a hash + */ + + + printf("PATH Y\n"); + // if an existing hook exists at this position in the chain then extract the relevant fields + if (oldHook) + { + // certain actions require explicit flagging to prevent user error + if (!(flags & FLAG_OVERRIDE) && !isUpdateOperation) + { + // deletes (and creates that override an existing hook) require a flag + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: sethook entry must be flagged for override."; + return tecREQUIRES_FLAG; + } + printf("PATH Z\n"); + oldDefKeylet = keylet::hookDefinition(oldHook->get().getFieldH256(sfHookHash)); + oldDefSLE = view().peek(*oldDefKeylet); + defNamespace = oldDefSLE->getFieldH256(sfHookNamespace); + oldNamespace = oldHook->get().isFieldPresent(sfHookNamespace) + ? oldHook->get().getFieldH256(sfHookNamespace) + : *defNamespace; + oldDirKeylet = keylet::hookStateDir(account_, *oldNamespace); + oldDirSLE = view().peek(*oldDirKeylet); + defHookOn = oldDefSLE->getFieldU64(sfHookOn); + oldHookOn = oldHook->get().isFieldPresent(sfHookOn) + ? oldHook->get().getFieldU64(sfHookOn) + : *defHookOn; + } + + + if (hasHookHash) + { + newDefKeylet = keylet::hookDefinition(hookSetObj->getFieldH256(sfHookHash)); + newDefSLE = view().peek(*newDefKeylet); + } + + if (hookSetObj->isFieldPresent(sfHookOn)) + newHookOn = hookSetObj->getFieldU64(sfHookOn); + + // if the sethook txn specifies a new namespace then extract those fields + if (hookSetObj->isFieldPresent(sfHookNamespace)) + { + newNamespace = hookSetObj->getFieldH256(sfHookNamespace); + newDirKeylet = keylet::hookStateDir(account_, *newNamespace); + newDirSLE = view().peek(*newDirKeylet); + } + + if (oldDirSLE) + { + printf("PATH A\n"); + DIRECTORY_DEC(); + } + else + printf("PATH B\n"); + + if (oldDefSLE) + { + printf("PATH C\n"); + DEFINITION_DEC(); + } + else + printf("PATH D\n"); + + // handle delete operation + if (isDeleteOperation) + { + newHooks.push_back(ripple::STObject{sfHook}); + continue; + } + + // if we're not performing a delete operation then we must have a newDirKeylet and newDirSLE + // otherwise we will not be able to create/update a state directory + if (!newDirKeylet) + { + if (newDefSLE) + newDirKeylet = keylet::hookStateDir(account_, newDefSLE->getFieldH256(sfHookNamespace)); + else if (oldDirKeylet) + newDirKeylet = oldDirKeylet; + else + { + JLOG(ctx.j.warn()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: sethook could not find a namespace to place hook state into."; + return tecINTERNAL; + } + newDirSLE = view().peek(*newDirKeylet); + } + + DIRECTORY_INC(); + + // handle create operation + if (isCreateOperation) + { + ripple::Blob wasmBytes = hookSetObj->getFieldVL(sfCreateCode); + + if (wasmBytes.size() > blobMax) + { + JLOG(ctx.j.warn()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: SetHook operation would create blob larger than max"; + return tecINTERNAL; + } + + + auto hash = ripple::sha512Half_s( + ripple::Slice(wasmBytes.data(), wasmBytes.size()) + ); + + // update hook hash + newHook.setFieldH256(sfHookHash, hash); + + auto keylet = ripple::keylet::hookDefinition(hash); + + if (view().exists(keylet)) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: SetHook operation would create a duplicate wasm blob, using hash only"; + + // update reference count + newDefSLE = view().peek(keylet); + newDefKeylet = keylet; + isInstallOperation = true; + isCreateOperation = false; + + // this falls through to install + } + else + { + // create hook definition SLE + auto [valid, maxInstrCount] = + validateHookSetEntry(ctx, *hookSetObj); + + if (!valid) + { + JLOG(ctx.j.warn()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: SetHook operation would create invalid hook wasm"; + return tecINTERNAL; + } + + auto newHookDef = std::make_shared( keylet ); + newHookDef->setFieldH256(sfHookHash, hash); + newHookDef->setFieldU64( sfHookOn, *newHookOn); + newHookDef->setFieldH256( sfHookNamespace, *newNamespace); + newHookDef->setFieldArray( sfHookParameters, + hookSetObj->isFieldPresent(sfHookParameters) + ? hookSetObj->getFieldArray(sfHookParameters) + : STArray {} ); + newHookDef->setFieldU16( sfHookApiVersion, hookSetObj->getFieldU16(sfHookApiVersion)); + newHookDef->setFieldVL( sfCreateCode, wasmBytes); + newHookDef->setFieldH256( sfHookSetTxnID, ctx.tx.getTransactionID()); + newHookDef->setFieldU64( sfReferenceCount, 1); + newHookDef->setFieldAmount(sfFee, XRPAmount { hook::computeExecutionFee(maxInstrCount) } ); + view().insert(newHookDef); + newHooks.push_back(std::move(newHook)); + continue; + } + } + else if (isInstallOperation) // this needs to be here to allow duplicate wasm blob fallthrough case above + newHook.setFieldH256(sfHookHash, hookSetObj->getFieldH256(sfHookHash)); + + + + // install operations are half way between create and update operations + // here we install an existing hook definition into an unused hook slot + // but care must be taken to ensure parameters, namespaces etc are as the user intends + // without needlessly duplicating these from the hook definition + if (isInstallOperation) + { + DEFINITION_INC(); + + if (newNamespace && *defNamespace != *newNamespace) + newHook.setFieldH256(sfHookNamespace, *newNamespace); + + if (newHookOn && defHookOn != *newHookOn) + newHook.setFieldU64(sfHookOn, *newHookOn); + + std::map parameters; + + // first pull the parameters into a map + auto const& hookParameters = hookSetObj->getFieldArray(sfHookParameters); + for (auto const& hookParameter : hookParameters) + { + auto const& hookParameterObj = dynamic_cast(&hookParameter); + parameters[hookParameterObj->getFieldVL(sfHookParameterName)] = + hookParameterObj->getFieldVL(sfHookParameterValue); + + } + + // then erase anything that is the same as the definition's default parameters + if (parameters.size() > 0) + { + auto const& defParameters = oldDefSLE->getFieldArray(sfHookParameters); + for (auto const& hookParameter : defParameters) + { + auto const& hookParameterObj = dynamic_cast(&hookParameter); + ripple::Blob n = hookParameterObj->getFieldVL(sfHookParameterName); + ripple::Blob v = hookParameterObj->getFieldVL(sfHookParameterValue); + + if (parameters.find(n) != parameters.end() && parameters[n] == v) + parameters.erase(n); + } + } + + int parameterCount = (int)(parameters.size()); + if (parameterCount > 16) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: Txn would result in too many parameters on hook"; + return tecINTERNAL; + } + + STArray newParameters {sfHookParameters, parameterCount}; + for (const auto& [parameterName, parameterValue] : parameters) + { + STObject param { sfHookParameter }; + param.setFieldVL(sfHookParameterName, parameterName); + param.setFieldVL(sfHookParameterValue, parameterValue); + newParameters.push_back(std::move(param)); + } + + newHook.setFieldArray(sfHookParameters, std::move(newParameters)); + + if (hasGrants) + newHook.setFieldArray( sfHookGrants, hookSetObj->getFieldArray(sfHookGrants)); + + newHooks.push_back(std::move(newHook)); + continue; + } + + + // handle update operation + if (isUpdateOperation) + { + DEFINITION_INC(); + + newHook.setFieldH256(sfHookHash, hookSetObj->getFieldH256(sfHookHash)); + + // handle HookOn update logic + if (hookSetObj->isFieldPresent(sfHookOn)) + { + uint64_t newHookOn = hookSetObj->getFieldU64(sfHookOn); + if (newHookOn != defHookOn) + newHook.setFieldU64(sfHookOn, hookSetObj->getFieldU64(sfHookOn)); + } + else if (*oldHookOn != *defHookOn) + newHook.setFieldU64(sfHookOn, *oldHookOn); + + + // handle namespace update logic + if (newNamespace && *newNamespace != *defNamespace) + newHook.setFieldH256(sfHookNamespace, *newNamespace); + else if (oldNamespace && *oldNamespace != *defNamespace) + newHook.setFieldH256(sfHookNamespace, *oldNamespace); + + // process the parameters + if (hasParameters) + { + fprintf(stderr, "PATH L\n"); + std::map parameters; + + // gather up existing parameters, but only if this is an update + if (oldHook->get().isFieldPresent(sfHookParameters)) + { + fprintf(stderr, "PATH M\n"); + auto const& oldParameters = oldHook->get().getFieldArray(sfHookParameters); + for (auto const& hookParameter : oldParameters) + { + fprintf(stderr, "PATH N\n"); + auto const& hookParameterObj = dynamic_cast(&hookParameter); + parameters[hookParameterObj->getFieldVL(sfHookParameterName)] = + hookParameterObj->getFieldVL(sfHookParameterValue); + } + } + + fprintf(stderr, "PATH O\n"); + // process hookset parameters + auto const& hookParameters = hookSetObj->getFieldArray(sfHookParameters); + for (auto const& hookParameter : hookParameters) + { + fprintf(stderr, "PATH P\n"); + auto const& hookParameterObj = dynamic_cast(&hookParameter); + if (!hookParameterObj->isFieldPresent(sfHookParameterName)) + { + fprintf(stderr, "PATH Q\n"); + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: Parameter without ParameterName"; + return tecINTERNAL; + } + + ripple::Blob paramName = hookParameterObj->getFieldVL(sfHookParameterName); + fprintf(stderr, "PATH R\n"); + if (paramName.size() > paramMax) + { + fprintf(stderr, "PATH S\n"); + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: ParameterName too large"; + return tecINTERNAL; + } + + fprintf(stderr, "PATH T\n"); + if (hookParameterObj->isFieldPresent(sfHookParameterValue)) + { + fprintf(stderr, "PATH U\n"); + // parameter update or set operation + ripple::Blob newValue = hookParameterObj->getFieldVL(sfHookParameterValue); + fprintf(stderr, "PATH V\n"); + if (newValue.size() > paramMax) + { + fprintf(stderr, "PATH W\n"); + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: ParameterValue too large"; + return tecINTERNAL; + } + parameters[hookParameterObj->getFieldVL(sfHookParameterName)] = newValue; + } + else + { + + fprintf(stderr, "PATH @\n"); + // parameter delete operation + parameters.erase(hookParameterObj->getFieldVL(sfHookParameterName)); + } + } + + fprintf(stderr, "PATH #\n"); + // remove any duplicate entries that exist in the sle + auto const& defParameters = newDefSLE->getFieldArray(sfHookParameters); + for (auto const& hookParameter : defParameters) + { + fprintf(stderr, "PATH $\n"); + auto const& hookParameterObj = dynamic_cast(&hookParameter); + ripple::Blob n = hookParameterObj->getFieldVL(sfHookParameterName); + ripple::Blob v = hookParameterObj->getFieldVL(sfHookParameterValue); + + if (parameters.find(n) != parameters.end() && parameters[n] == v) + parameters.erase(n); + } + + fprintf(stderr, "PATH %\n"); + int parameterCount = (int)(parameters.size()); + if (parameterCount > 16) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: Txn would result in too many parameters on hook"; + return tecINTERNAL; + } + + fprintf(stderr, "PATH ^\n"); + STArray newParameters {sfHookParameters, parameterCount}; + for (const auto& [parameterName, parameterValue] : parameters) + { + fprintf(stderr, "PATH &\n"); + STObject param { sfHookParameter }; + param.setFieldVL(sfHookParameterName, parameterName); + param.setFieldVL(sfHookParameterValue, parameterValue); + newParameters.push_back(std::move(param)); + } + + fprintf(stderr, "PATH *\n"); + newHook.setFieldArray(sfHookParameters, std::move(newParameters)); + } + + // process the grants + if (hasGrants) + newHook.setFieldArray(sfHookGrants, hookSetObj->getFieldArray(sfHookGrants)); + + fprintf(stderr, "PATH (\n"); + newHooks.push_back(std::move(newHook)); + fprintf(stderr, "PATH )\n"); + continue; + } + + + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: Ambiguous sethook entry"; + return tecINTERNAL; + + } + + // clean up any zero reference dirs and defs + + // dirs + for (auto const& p : dirsToDestroy) + { + auto const& sle = view().peek(ripple::Keylet { ltDIR_NODE, p.first }); + uint64_t refCount = sle->getFieldU64(sfReferenceCount); + if (refCount <= 0) + { + if (!p.second) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: SetHook operation would delete a namespace of hook states" + << " but to do this you must set the NSDELETE flag"; + return tecREQUIRES_FLAG; + } + + destroyNamespace(ctx, view(), account_, { ltDIR_NODE, p.first } ); + view().erase(sle); + } + } + + + // defs + for (auto const& p : defsToDestroy) + { + auto const& sle = view().peek(ripple::Keylet { ltHOOK_DEFINITION, p.first }); + uint64_t refCount = sle->getFieldU64(sfReferenceCount); + if (refCount <= 0) + { + if (!p.second) + { + JLOG(ctx.j.trace()) + << "HookSet[" << HS_ACC() + << "]: Malformed transaction: SetHook operation would delete a hook" + << " but to do this you must set the OVERRIDE flag"; + return tecREQUIRES_FLAG; + } + view().erase(sle); + } + } + + newHookSLE->setFieldArray(sfHooks, newHooks); + newHookSLE->setAccountID(sfAccount, account_); + + if (oldHookSLE) + view().erase(oldHookSLE); + view().insert(newHookSLE); + + return tesSUCCESS; +} + + +} // namespace ripple diff --git a/src/ripple/app/tx/impl/SetHook.h b/src/ripple/app/tx/impl/SetHook.h new file mode 100644 index 000000000..32ef7c5d7 --- /dev/null +++ b/src/ripple/app/tx/impl/SetHook.h @@ -0,0 +1,108 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2014 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_TX_SETHOOK_H_INCLUDED +#define RIPPLE_TX_SETHOOK_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + + +enum SetHookFlags : uint8_t { + FLAG_OVERRIDE = 1U, + FLAG_NSDELETE = 2U +}; + +struct SetHookCtx +{ + beast::Journal j; + STTx const& tx; + Application& app; +}; + +class SetHook : public Transactor +{ + +public: + explicit SetHook(ApplyContext& ctx) : Transactor(ctx) + { + } + + static bool + affectsSubsequentTransactionAuth(STTx const& tx) + { + return true; + } + + static NotTEC + preflight(PreflightContext const& ctx); + + TER + doApply() override; + + void + preCompute() override; + + static TER + preclaim(PreclaimContext const&); + + // RH TODO: compute fee in transactor on chain execution + static FeeUnit64 + calculateBaseFee(ReadView const& view, STTx const& tx); + +private: + + TER + setHook(); + + TER + destroyNamespace( + SetHookCtx& ctx, + ApplyView& view, + const AccountID& account, + const Keylet & dirKeylet // the keylet of the namespace directory + ); + + TER + removeHookFromLedger( + Application& app, + ApplyView& view, + Keylet const& accountKeylet, + Keylet const& ownerDirKeylet, + Keylet const& hookKeylet + ); + +}; + +} // namespace ripple + +#endif diff --git a/src/ripple/app/tx/impl/Transactor.cpp b/src/ripple/app/tx/impl/Transactor.cpp index 8b74463de..cb248ac8c 100644 --- a/src/ripple/app/tx/impl/Transactor.cpp +++ b/src/ripple/app/tx/impl/Transactor.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include #include @@ -32,6 +33,19 @@ #include #include #include +#include + + +//RH TODO: remove after debugging +#include +#include +#include +//-------- + +#define PRINTFTHREAD(x)\ +{\ + printf("%s - %d\n", x, syscall(__NR_gettid));\ +} namespace ripple { @@ -82,6 +96,13 @@ preflight1(PreflightContext const& ctx) return temBAD_FEE; } + // if a hook emitted this transaction we bypass signature checks + // there is a bar to circularing emitted transactions on the network + // in their prevalidated form so this is safe + if (ctx.rules.enabled(featureHooks) && + hook::isEmittedTxn(ctx.tx)) + return tesSUCCESS; + auto const spk = ctx.tx.getSigningPubKey(); if (!spk.empty() && !publicKeyType(makeSlice(spk))) @@ -136,6 +157,64 @@ Transactor::Transactor(ApplyContext& ctx) { } +inline +std::optional +getDestinationAccount(STTx const& tx) +{ + if (tx.isFieldPresent(sfOwner)) + return tx.getAccountID(sfOwner); + + if (tx.isFieldPresent(sfDestination)) + return tx.getAccountID(sfDestination); + + return std::nullopt; +} + +FeeUnit64 +Transactor::calculateHookChainFee(ReadView const& view, STTx const& tx, Keylet const& hookKeylet) +{ + + PRINTFTHREAD("PATH X1"); + std::shared_ptr hookSLE = view.read(hookKeylet); + PRINTFTHREAD("PATH X2"); + if (!hookSLE) + return FeeUnit64{0}; + + PRINTFTHREAD("PATH X3"); + FeeUnit64 fee{0}; + PRINTFTHREAD("PATH X4"); + + auto const& hooks = hookSLE->getFieldArray(sfHooks); + PRINTFTHREAD("PATH X5"); + for (auto const& hook : hooks) + { + PRINTFTHREAD("PATH X6"); + ripple::STObject const* hookObj = dynamic_cast(&hook); + + PRINTFTHREAD("PATH X7"); + + if (hookObj->getCount() == 0) // skip blanks + continue; + + PRINTFTHREAD("PATH X8"); + + uint256 const& hash = hookObj->getFieldH256(sfHookHash); + + PRINTFTHREAD("PATH X9"); + + std::shared_ptr hookDef = view.read(keylet::hookDefinition(hash)); + + fee += FeeUnit64{ + (uint32_t)(hookDef->getFieldAmount(sfFee).xrp().drops()) + }; + PRINTFTHREAD("PATH X10"); + } + + PRINTFTHREAD("PATH X11"); + return fee; + +} + FeeUnit64 Transactor::calculateBaseFee(ReadView const& view, STTx const& tx) { @@ -151,7 +230,61 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx) std::size_t const signerCount = tx.isFieldPresent(sfSigners) ? tx.getFieldArray(sfSigners).size() : 0; - return baseFee + (signerCount * baseFee); + FeeUnit64 hookExecutionFee{0}; + if (view.rules().enabled(featureHooks)) + { + PRINTFTHREAD("PATH Z1"); + // RH UPTO: reserve callback fee on emit, and unreserve and burn on callback + // if this is a "cleanup" txn we regard it as already paid up + if (tx.getFieldU16(sfTransactionType) == ttEMIT_FAILURE) + return FeeUnit64{0}; + + PRINTFTHREAD("PATH Z2"); + // if the txn is an emitted txn then we add the callback fee + // if the txn is NOT an emitted txn then we process the sending account's hook chain + if (tx.isFieldPresent(sfEmitDetails)) + { + + PRINTFTHREAD("PATH Z3"); + STObject const& emitDetails = + const_cast(tx).getField(sfEmitDetails).downcast(); + + PRINTFTHREAD("PATH Z4"); + + uint256 const& callbackHookHash = emitDetails.getFieldH256(sfEmitHookHash); + + PRINTFTHREAD("PATH Z5"); + + std::shared_ptr hookDef = view.read(keylet::hookDefinition(callbackHookHash)); + + PRINTFTHREAD("PATH Z6"); + + if (hookDef) + hookExecutionFee += FeeUnit64{(uint32_t)(hookDef->getFieldAmount(sfFee).xrp().drops())}; + + PRINTFTHREAD("PATH Z7"); + } + else + hookExecutionFee += + calculateHookChainFee(view, tx, keylet::hook(tx.getAccountID(sfAccount))); + + PRINTFTHREAD("PATH Z8"); + + std::optional + destAccountID = getDestinationAccount(tx); + + PRINTFTHREAD("PATH Z9"); + + // if there is a receiving account then we also compute the fee for its hook chain + if (destAccountID) + hookExecutionFee += + calculateHookChainFee(view, tx, keylet::hook(*destAccountID)); + + PRINTFTHREAD("PATH Z10"); + } + + return baseFee + (signerCount * baseFee) + hookExecutionFee; // RH NOTE: hookExecutionFee = 0 + // unless featureHooks enabled } XRPAmount @@ -254,6 +387,21 @@ Transactor::checkSeqProxy( SeqProxy const t_seqProx = tx.getSeqProxy(); SeqProxy const a_seq = SeqProxy::sequence((*sle)[sfSequence]); + // pass all emitted tx provided their seq is 0 + if (ctx.view.rules().enabled(featureHooks) && + hook::isEmittedTxn(ctx.tx)) + { + // this is more strictly enforced in the emit() hook api + // here this is only acting as a sanity check in case of bugs + if (!ctx.tx.isFieldPresent(sfFirstLedgerSequence)) + return tefINTERNAL; + return tesSUCCESS; + } + + // reserved for emitted tx only at this time + if (ctx.tx.isFieldPresent(sfFirstLedgerSequence)) + return tefINTERNAL; + if (t_seqProx.isSeq()) { if (tx.isFieldPresent(sfTicketSequence) && @@ -339,6 +487,12 @@ TER Transactor::consumeSeqProxy(SLE::pointer const& sleAccount) { assert(sleAccount); + + // RH TODO: determine what interactions between hooks and tickets might cause issues + // do not update sequence of sfAccountTxnID for emitted tx + if (ctx_.emitted()) + return; + SeqProxy const seqProx = ctx_.tx.getSeqProxy(); if (seqProx.isSeq()) { @@ -451,6 +605,11 @@ Transactor::apply() NotTEC Transactor::checkSign(PreclaimContext const& ctx) { + // hook emitted transactions do not have signatures + if (ctx.view.rules().enabled(featureHooks) && + hook::isEmittedTxn(ctx.tx)) + return tesSUCCESS; + // If the pk is empty, then we must be multi-signing. if (ctx.tx.getSigningPubKey().empty()) return checkMultiSign(ctx); @@ -716,7 +875,12 @@ removeUnfundedOffers( std::pair Transactor::reset(XRPAmount fee) { + ApplyViewImpl& avi = dynamic_cast(ctx_.view()); + std::vector hookMeta; + avi.copyHookMetaData(hookMeta); ctx_.discard(); + ApplyViewImpl& avi2 = dynamic_cast(ctx_.view()); + avi2.setHookMetaData(std::move(hookMeta)); auto const txnAcct = view().peek(keylet::account(ctx_.tx.getAccountID(sfAccount))); @@ -751,6 +915,163 @@ Transactor::reset(XRPAmount fee) return {ter, fee}; } +static __inline__ unsigned long long rdtsc(void) +{ + unsigned hi, lo; + __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi)); + return ( (unsigned long long)lo)|( ((unsigned long long)hi)<<32 ); +} + +bool /* error = true */ +gatherHookParameters( + std::shared_ptr const& hookDef, + ripple::STObject const* hookObj, + std::map, std::vector>& parameters, + beast::Journal const& j_) +{ + if (!hookDef->isFieldPresent(sfHookParameters)) + { + JLOG(j_.fatal()) + << "HookError[]: Failure: hook def missing parameters (send)"; + return true; + } + + // first defaults + auto const& defaultParameters = hookDef->getFieldArray(sfHookParameters); + for (auto const& hookParameter : defaultParameters) + { + auto const& hookParameterObj = dynamic_cast(&hookParameter); + parameters[hookParameterObj->getFieldVL(sfHookParameterName)] = + hookParameterObj->getFieldVL(sfHookParameterValue); + } + + // and then custom + if (hookObj->isFieldPresent(sfHookParameters)) + { + auto const& hookParameters = hookObj->getFieldArray(sfHookParameters); + for (auto const& hookParameter : hookParameters) + { + auto const& hookParameterObj = dynamic_cast(&hookParameter); + parameters[hookParameterObj->getFieldVL(sfHookParameterName)] = + hookParameterObj->getFieldVL(sfHookParameterValue); + } + } + return false; +} + +bool /* error = true */ +executeHookChain( + std::shared_ptr const& hookSLE, + std::vector& results, + int& executedHookCount, + ripple::AccountID const& account, + ripple::ApplyContext& ctx, + beast::Journal const& j_, + TER& result) +{ + std::set hookSkips; + std::map< + uint256, + std::map< + std::vector, + std::vector + >> hookParamOverrides {}; + + auto const& hooks = hookSLE->getFieldArray(sfHooks); + int hook_no = 0; + for (auto const& hook : hooks) + { + ripple::STObject const* hookObj = dynamic_cast(&hook); + + if (hookObj->getCount() == 0) // skip blanks + continue; + + // lookup hook definition + uint256 const& hookHash = hookObj->getFieldH256(sfHookHash); + + if (hookSkips.find(hookHash) != hookSkips.end()) + { + JLOG(j_.trace()) + << "HookInfo: Skipping " << hookHash; + continue; + } + + auto const& hookDef = ctx.view().peek(keylet::hookDefinition(hookHash)); + if (!hookDef) + { + JLOG(j_.fatal()) + << "HookError[]: Failure: hook def missing (send)"; + return true; + } + + // check if the hook can fire + uint64_t hookOn = (hookObj->isFieldPresent(sfHookOn) + ? hookObj->getFieldU64(sfHookOn) + : hookDef->getFieldU64(sfHookOn)); + + if (!hook::canHook(ctx.tx.getTxnType(), hookOn)) + continue; // skip if it can't + + // fetch the namespace either from the hook object of, if absent, the hook def + uint256 const& ns = + (hookObj->isFieldPresent(sfHookNamespace) + ? hookObj->getFieldH256(sfHookNamespace) + : hookDef->getFieldH256(sfHookNamespace)); + + // gather parameters + std::map, std::vector> parameters; + if (gatherHookParameters(hookDef, hookObj, parameters, j_)) + return true; + + results.push_back( + hook::apply( + hookDef->getFieldH256(sfHookSetTxnID), + hookHash, + ns, + hookDef->getFieldVL(sfCreateCode), + parameters, + hookParamOverrides, + ctx, + account, + false, + 0, + hook_no)); + + executedHookCount++; + + hook::HookResult& hookResult = results.back(); + + if (hookResult.exitType != hook_api::ExitType::ACCEPT) + { + if (results.back().exitType == hook_api::ExitType::WASM_ERROR) + result = temMALFORMED; + return true; + } + + // gather skips + for (uint256 const& hash : hookResult.hookSkips) + if (hookSkips.find(hash) == hookSkips.end()) + hookSkips.emplace(hash); + + // gather overrides + auto const& resultOverrides = hookResult.hookParamOverrides; + for (auto const& [hash, params] : resultOverrides) + { + if (hookParamOverrides.find(hash) == hookParamOverrides.end()) + hookParamOverrides[hash] = {}; + + auto& overrides = hookParamOverrides[hash]; + for (auto const& [k, v] : params) + overrides[k] = v; + } + + hook_no++; + } + return false; +} + + + //------------------------------------------------------------------------------ std::pair Transactor::operator()() @@ -776,7 +1097,154 @@ Transactor::operator()() } #endif + auto result = ctx_.preclaimResult; + + int executedHookCount = 0; + bool rollback = false; + uint64_t prehook_cycles = rdtsc(); + + + if (ctx_.view().rules().enabled(featureHooks) && + (result == tesSUCCESS || result == tecHOOK_REJECTED)) + { + + auto const& ledger = ctx_.view(); + auto const& accountID = ctx_.tx.getAccountID(sfAccount); + std::vector sendResults; + std::vector recvResults; + + auto const& hooksSending = ledger.read(keylet::hook(accountID)); + + // 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); + + // Next check if the Receiving account has as a hook that can be fired... + std::optional + destAccountID = getDestinationAccount(ctx_.tx); + + if (!rollback && destAccountID) + { + auto const& hooksReceiving = ledger.read(keylet::hook(*destAccountID)); + if (hooksReceiving && hooksReceiving->isFieldPresent(sfHooks)) + rollback = + executeHookChain(hooksReceiving, recvResults, executedHookCount, *destAccountID, ctx_, j_, result); + } + + if (rollback && result != temMALFORMED) + result = tecHOOK_REJECTED; + + // Finally check if there is a callback + do + { + if (ctx_.tx.isFieldPresent(sfEmitDetails) && result == tesSUCCESS) + { + auto const& emitDetails = + const_cast(ctx_.tx).getField(sfEmitDetails).downcast(); + + if (!emitDetails.isFieldPresent(sfEmitCallback)) + { + JLOG(j_.fatal()) + << "HookError[]: Callback Processing: Failure: sfEmitCallback missing"; + break; + } + + AccountID const& callbackAccountID = emitDetails.getAccountID(sfEmitCallback); + uint256 const& callbackHookHash = emitDetails.getFieldH256(sfEmitHookHash); + + + auto const& hooksCallback = ledger.read(keylet::hook(callbackAccountID)); + auto const& hookDef = view().peek(keylet::hookDefinition(callbackHookHash)); + if (!hookDef) + { + JLOG(j_.warn()) + << "HookError[]: Hook def missing on callback"; + rollback = true; + break; + } + + bool found = false; + auto const& hooks = hooksCallback->getFieldArray(sfHooks); + int hook_no = 0; + for (auto const& hook : hooks) + { + hook_no++; + + STObject const* hookObj = dynamic_cast(&hook); + + if (hookObj->getCount() == 0) // skip blanks + continue; + + if (hookObj->getFieldH256(sfHookHash) != callbackHookHash) + continue; + + // fetch the namespace either from the hook object of, if absent, the hook def + uint256 const& ns = + (hookObj->isFieldPresent(sfHookNamespace) + ? hookObj->getFieldH256(sfHookNamespace) + : hookDef->getFieldH256(sfHookNamespace)); + + executedHookCount++; + + std::map, std::vector> parameters; + if (gatherHookParameters(hookDef, hookObj, parameters, j_)) + { + rollback = true; + 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( + ctx_.tx.getFieldU16(sfTransactionType)) == ttEMIT_FAILURE ? 1UL : 0UL, hook_no - 1); + } + catch (std::exception& e) + { + JLOG(j_.fatal()) << "HookError[" << callbackAccountID << "]: Callback failure " << e.what(); + } + + break; + } + + if (!found) + { + 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 ); + } + + uint64_t posthook_cycles = rdtsc(); + JLOG(j_.trace()) + << "HookStats[]: " << executedHookCount << " hooks executed. Execution took " + << (posthook_cycles - prehook_cycles) << " cycles."; + + // fall through allows normal apply if (result == tesSUCCESS) result = apply(); @@ -793,11 +1261,10 @@ Transactor::operator()() if (ctx_.size() > oversizeMetaDataCap) result = tecOVERSIZE; - if (isTecClaim(result) && (view().flags() & tapFAIL_HARD)) + if ((isTecClaim(result) && (view().flags() & tapFAIL_HARD))) { // If the tapFAIL_HARD flag is set, a tec result // must not do anything - ctx_.discard(); applied = false; } diff --git a/src/ripple/app/tx/impl/Transactor.h b/src/ripple/app/tx/impl/Transactor.h index c54f5a1a3..5449ac0ea 100644 --- a/src/ripple/app/tx/impl/Transactor.h +++ b/src/ripple/app/tx/impl/Transactor.h @@ -25,6 +25,13 @@ #include #include +namespace hook { + // RH TODO: fix applyHook.h so this prototype isn't needed + struct HookContext; + struct HookResult; + bool isEmittedTxn(ripple::STTx const& tx); +} + namespace ripple { /** State information when preflighting a tx. */ @@ -48,6 +55,7 @@ public: operator=(PreflightContext const&) = delete; }; + /** State information when determining if a tx is likely to claim a fee. */ struct PreclaimContext { @@ -99,6 +107,7 @@ protected: public: enum ConsequencesFactoryType { Normal, Blocker, Custom }; + /** Process the transaction. */ std::pair operator()(); @@ -137,9 +146,13 @@ public: static NotTEC checkSign(PreclaimContext const& ctx); + // Returns the fee in fee units, not scaled for load. static FeeUnit64 calculateBaseFee(ReadView const& view, STTx const& tx); + + static FeeUnit64 + calculateHookChainFee(ReadView const& view, STTx const& tx, Keylet const& hookKeylet); static TER preclaim(PreclaimContext const& ctx) diff --git a/src/ripple/app/tx/impl/apply.cpp b/src/ripple/app/tx/impl/apply.cpp index 332364863..5918cc365 100644 --- a/src/ripple/app/tx/impl/apply.cpp +++ b/src/ripple/app/tx/impl/apply.cpp @@ -42,6 +42,7 @@ checkValidity( { auto const id = tx.getTransactionID(); auto const flags = router.getFlags(id); + if (flags & SF_SIGBAD) // Signature is known bad return {Validity::SigBad, "Transaction has bad signature."}; diff --git a/src/ripple/app/tx/impl/applyHook.cpp b/src/ripple/app/tx/impl/applyHook.cpp new file mode 100644 index 000000000..4cc0a7b9d --- /dev/null +++ b/src/ripple/app/tx/impl/applyHook.cpp @@ -0,0 +1,4269 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "support/span.h" + +using namespace ripple; + +#define HR_ACC() hookResult.account << "-" << hookResult.otxnAccount +#define HC_ACC() hookCtx.result.account << "-" << hookCtx.result.otxnAccount + +#define COMPUTE_HOOK_DATA_OWNER_COUNT(state_count)\ + (std::ceil( (double)state_count/(double)5.0 )) + +#define HOOK_SETUP()\ + [[maybe_unused]] ApplyContext& applyCtx = hookCtx.applyCtx;\ + [[maybe_unused]] auto& view = applyCtx.view();\ + [[maybe_unused]] auto j = applyCtx.app.journal("View");\ + [[maybe_unused]] unsigned char* memory = memoryCtx.getPointer(0);\ + [[maybe_unused]] const uint64_t memory_length = memoryCtx.getDataPageSize() * memoryCtx.kPageSize; + +#define WRITE_WASM_MEMORY(bytes_written, guest_dst_ptr, guest_dst_len,\ + host_src_ptr, host_src_len, host_memory_ptr, guest_memory_length)\ +{\ + int64_t bytes_to_write = std::min(static_cast(host_src_len), static_cast(guest_dst_len));\ + if (guest_dst_ptr + bytes_to_write > guest_memory_length)\ + {\ + JLOG(j.warn())\ + << "HookError[" << HC_ACC() << "]: "\ + << __func__ << " tried to retreive blob of " << host_src_len\ + << " bytes past end of wasm memory";\ + return OUT_OF_BOUNDS;\ + }\ + memoryCtx.setBytes(SSVM::Span((const uint8_t*)host_src_ptr, host_src_len), \ + guest_dst_ptr, 0, bytes_to_write);\ + bytes_written += bytes_to_write;\ +} + +#define WRITE_WASM_MEMORY_AND_RETURN(guest_dst_ptr, guest_dst_len,\ + host_src_ptr, host_src_len, host_memory_ptr, guest_memory_length)\ +{\ + int64_t bytes_written = 0;\ + WRITE_WASM_MEMORY(bytes_written, guest_dst_ptr, guest_dst_len, host_src_ptr,\ + host_src_len, host_memory_ptr, guest_memory_length);\ + return bytes_written;\ +} + +#define RETURN_HOOK_TRACE(read_ptr, read_len, t)\ +{\ + int rl = read_len;\ + if (rl > 1024)\ + rl = 1024;\ + if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length))\ + {\ + return OUT_OF_BOUNDS;\ + }\ + else if (read_ptr == 0 && read_len == 0)\ + {\ + JLOG(j.trace()) \ + << "HookTrace[" << HC_ACC() << "]: " << t;\ + }\ + else if (is_UTF16LE(memory + read_ptr, rl))\ + {\ + uint8_t output[1024];\ + int len = rl / 2;\ + for (int i = 0; i < len && i < 512; ++i)\ + output[i] = memory[read_ptr + i * 2];\ + JLOG(j.trace()) \ + << "HookTrace[" << HC_ACC() << "]: "\ + << std::string_view((const char*)output, (size_t)(len)) << " "\ + << t;\ + }\ + else\ + {\ + JLOG(j.trace()) \ + << "HookTrace[" << HC_ACC() << "]: "\ + << std::string_view((const char*)(memory + read_ptr), (size_t)rl) << " "\ + << t;\ + }\ + return 0;\ +} +// ptr = pointer inside the wasm memory space +#define NOT_IN_BOUNDS(ptr, len, memory_length)\ + (ptr > memory_length || \ + static_cast(ptr) + static_cast(len) > static_cast(memory_length)) + +#define HOOK_EXIT(read_ptr, read_len, error_code, exit_type)\ +{\ + if (read_len > 256) read_len = 256;\ + if (read_ptr) {\ + if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length)) {\ + JLOG(j.warn())\ + << "HookError[" << HC_ACC() << "]: "\ + << "Tried to accept/rollback but specified memory outside of the wasm instance " <<\ + "limit when specifying a reason string";\ + return OUT_OF_BOUNDS;\ + }\ + /* assembly script and some other languages use utf16 for strings */\ + if (is_UTF16LE(read_ptr + memory, read_len))\ + {\ + uint8_t output[128];\ + int len = read_len / 2; /* is_UTF16LE will only return true if read_len is even */\ + for (int i = 0; i < len; ++i)\ + output[i] = memory[read_ptr + i * 2];\ + hookCtx.result.exitReason = std::string((const char*)(output), (size_t)len);\ + } else\ + hookCtx.result.exitReason = std::string((const char*)(memory + read_ptr), (size_t)read_len);\ + }\ + hookCtx.result.exitType = exit_type;\ + hookCtx.result.exitCode = error_code;\ + return (exit_type == hook_api::ExitType::ACCEPT ? RC_ACCEPT : RC_ROLLBACK);\ +} +namespace hook_float +{ + using namespace hook_api; + static int64_t const minMantissa = 1000000000000000ull; + static int64_t const maxMantissa = 9999999999999999ull; + static int32_t const minExponent = -96; + static int32_t const maxExponent = 80; + inline int32_t get_exponent(int64_t float1) + { + if (float1 < 0) + return INVALID_FLOAT; + if (float1 == 0) + return 0; + if (float1 < 0) return INVALID_FLOAT; + uint64_t float_in = (uint64_t)float1; + float_in >>= 54U; + float_in &= 0xFFU; + return ((int32_t)float_in) - 97; + } + + inline uint64_t get_mantissa(int64_t float1) + { + if (float1 < 0) + return INVALID_FLOAT; + if (float1 == 0) + return 0; + if (float1 < 0) return INVALID_FLOAT; + float1 -= ((((uint64_t)float1) >> 54U) << 54U); + return float1; + } + + inline bool is_negative(int64_t float1) + { + return ((float1 >> 62U) & 1ULL) == 0; + } + + inline int64_t invert_sign(int64_t float1) + { + int64_t r = (int64_t)(((uint64_t)float1) ^ (1ULL<<62U)); + return r; + } + + inline int64_t set_sign(int64_t float1, bool set_negative) + { + bool neg = is_negative(float1); + if ((neg && set_negative) || (!neg && !set_negative)) + return float1; + + return invert_sign(float1); + } + + inline int64_t set_mantissa(int64_t float1, uint64_t mantissa) + { + if (mantissa > maxMantissa) + return MANTISSA_OVERSIZED; + if (mantissa < minMantissa) + return MANTISSA_UNDERSIZED; + return float1 - get_mantissa(float1) + mantissa; + } + + inline int64_t set_exponent(int64_t float1, int32_t exponent) + { + if (exponent > maxExponent) + return EXPONENT_OVERSIZED; + if (exponent < minExponent) + return EXPONENT_UNDERSIZED; + + uint64_t exp = (exponent + 97); + exp <<= 54U; + float1 &= ~(0xFFLL<<54); + float1 += (int64_t)exp; + return float1; + } + + inline int64_t make_float(ripple::IOUAmount& amt) + { + int64_t man_out = amt.mantissa(); + int64_t float_out = 0; + bool neg = man_out < 0; + if (neg) + man_out *= -1; + + float_out = set_sign(float_out, neg); + float_out = set_mantissa(float_out, man_out); + float_out = set_exponent(float_out, amt.exponent()); + return float_out; + } + + inline int64_t make_float(int64_t mantissa, int32_t exponent) + { + if (mantissa == 0) + return 0; + if (mantissa > maxMantissa) + return MANTISSA_OVERSIZED; + if (exponent > maxExponent) + return EXPONENT_OVERSIZED; + if (exponent < minExponent) + return EXPONENT_UNDERSIZED; + bool neg = mantissa < 0; + if (neg) + mantissa *= -1LL; + int64_t out = 0; + out = set_mantissa(out, mantissa); + out = set_exponent(out, exponent); + out = set_sign(out, neg); + return out; + } + + inline int64_t float_set(int32_t exp, int64_t mantissa) + { + if (mantissa == 0) + return 0; + + bool neg = mantissa < 0; + if (neg) + mantissa *= -1LL; + + // normalize + while (mantissa < minMantissa) + { + mantissa *= 10; + exp--; + if (exp < minExponent) + return INVALID_FLOAT; //underflow + } + while (mantissa > maxMantissa) + { + mantissa /= 10; + exp++; + if (exp > maxExponent) + return INVALID_FLOAT; //overflow + } + + return make_float( ( neg ? -1LL : 1LL ) * mantissa, exp); + + } + +} +using namespace hook_float; +inline +int32_t +no_free_slots(hook::HookContext& hookCtx) +{ + return (hookCtx.slot_counter > hook_api::max_slots && hookCtx.slot_free.size() == 0); +} + + +inline +int32_t +get_free_slot(hook::HookContext& hookCtx) +{ + int32_t slot_into = 0; + + // allocate a slot + if (hookCtx.slot_free.size() > 0) + { + slot_into = hookCtx.slot_free.front(); + hookCtx.slot_free.pop(); + } + + // no slots were available in the queue so increment slot counter + if (slot_into == 0) + slot_into = hookCtx.slot_counter++; + + return slot_into; +} + +inline int64_t +serialize_keylet( + ripple::Keylet& kl, + uint8_t* memory, uint32_t write_ptr, uint32_t write_len) +{ + if (write_len < 34) + return hook_api::TOO_SMALL; + + memory[write_ptr + 0] = (kl.type >> 8) & 0xFFU; + memory[write_ptr + 1] = (kl.type >> 0) & 0xFFU; + + for (int i = 0; i < 32; ++i) + memory[write_ptr + 2 + i] = kl.key.data()[i]; + + return 34; +} + +std::optional +unserialize_keylet(uint8_t* ptr, uint32_t len) +{ + if (len != 34) + return std::nullopt; + + uint16_t ktype = + ((uint16_t)ptr[0] << 8) + + ((uint16_t)ptr[1]); + + ripple::Keylet reconstructed { (ripple::LedgerEntryType)ktype, ripple::uint256::fromVoid(ptr + 2) }; + return reconstructed; +} + + +// RH TODO: this is used by sethook to determine the value stored in ltHOOK +// replace this with votable value +uint32_t hook::maxHookStateDataSize(void) { + return 128; +} + +uint32_t hook::maxHookWasmSize(void) +{ + return 0xFFFFU; +} + +uint32_t hook::maxHookParameterSize(void) +{ + return 128; +} + +bool hook::isEmittedTxn(ripple::STTx const& tx) +{ + return tx.isFieldPresent(ripple::sfEmitDetails); +} + + +#define U32MAX ((uint32_t)(-1)) +uint32_t hook::computeExecutionFee(uint64_t instructionCount) +{ + // RH TODO: fee multiplier, validator votable + if (instructionCount > U32MAX) + return U32MAX; + return instructionCount; +} + +uint32_t hook::computeCreationFee(uint64_t byteCount) +{ + // RH TODO: fee multiplier, validator votable + if (byteCount > U32MAX) + return U32MAX; + return byteCount; +} + +uint32_t hook::maxHookChainLength(void) +{ + return 4; +} + +// many datatypes can be encoded into an int64_t +inline int64_t data_as_int64( + void const* ptr_raw, + uint32_t len) +{ + uint8_t const* ptr = reinterpret_cast(ptr_raw); + if (len > 8) + return hook_api::hook_return_code::TOO_BIG; + uint64_t output = 0; + for (int i = 0, j = (len-1)*8; i < len; ++i, j-=8) + output += (((uint64_t)ptr[i]) << j); + if ((1ULL<<63) & output) + return hook_api::hook_return_code::TOO_BIG; + return output; +} + +/* returns true iff every even char is ascii and every odd char is 00 + * only a hueristic, may be inaccurate in edgecases */ +inline bool is_UTF16LE(const uint8_t* buffer, size_t len) +{ + if (len % 2 != 0 || len == 0) + return false; + + for (int i = 0; i < len; i+=2) + if (buffer[i + 0] == 0 || buffer[i + 1] != 0) + return false; + + return true; +} + + +// Called by Transactor.cpp to determine if a transaction type can trigger a given hook... +// The HookOn field in the SetHook transaction determines which transaction types (tt's) trigger the hook. +// Every bit except ttHookSet is active low, so for example ttESCROW_FINISH = 2, so if the 2nd bit (counting from 0) +// from the right is 0 then the hook will trigger on ESCROW_FINISH. If it is 1 then ESCROW_FINISH will not trigger +// the hook. However ttHOOK_SET = 22 is active high, so by default (HookOn == 0) ttHOOK_SET is not triggered by +// transactions. If you wish to set a hook that has control over ttHOOK_SET then set bit 1U<<22. +bool hook::canHook(ripple::TxType txType, uint64_t hookOn) { + // invert ttHOOK_SET bit + hookOn ^= (1ULL << ttHOOK_SET); + // invert entire field + hookOn ^= 0xFFFFFFFFFFFFFFFFULL; + return (hookOn >> txType) & 1; +} + + +// Update HookState ledger objects for the hook... only called after accept() or reject() +// assumes the specified acc has already been checked for authoriation (hook grants) +TER +hook::setHookState( + HookResult& hookResult, + ripple::ApplyContext& applyCtx, + ripple::AccountID const& acc, + ripple::uint256 const& ns, + ripple::uint256 const& key, + ripple::Slice const& data +){ + + auto& view = applyCtx.view(); + auto j = applyCtx.app.journal("View"); + auto const sle = + ( acc == hookResult.account ? + view.peek(hookResult.accountKeylet) : + view.peek(ripple::keylet::account(acc))); + + if (!sle) + return tefINTERNAL; + + // if the blob is too large don't set it + if (data.size() > hook::maxHookStateDataSize()) + return temHOOK_DATA_TOO_LARGE; + + auto hookStateKeylet = ripple::keylet::hookState(acc, key, ns); + auto hookStateDirKeylet = ripple::keylet::hookStateDir(acc, ns); + + uint32_t stateCount = sle->getFieldU32(sfHookStateCount); + uint32_t oldStateReserve = COMPUTE_HOOK_DATA_OWNER_COUNT(stateCount); + + auto const oldHookState = view.peek(hookStateKeylet); + + // if the blob is nil then delete the entry if it exists + if (data.size() == 0) + { + + if (!view.peek(hookStateKeylet)) + return tesSUCCESS; // a request to remove a non-existent entry is defined as success + + auto const hint = (*oldHookState)[sfOwnerNode]; + + // Remove the node from the namespace directory + if (!view.dirRemove(hookStateDirKeylet, hint, hookStateKeylet.key, false)) + return tefBAD_LEDGER; + + // remove the actual hook state obj + view.erase(oldHookState); + + // adjust state object count + if (stateCount > 0) + --stateCount; // guard this because in the "impossible" event it is already 0 we'll wrap back to int_max + + // if removing this state entry would destroy the allotment then reduce the owner count + if (COMPUTE_HOOK_DATA_OWNER_COUNT(stateCount) < oldStateReserve) + adjustOwnerCount(view, sle, -1, j); + + sle->setFieldU32(sfHookStateCount, stateCount); + view.update(sle); + return tesSUCCESS; + } + + + std::uint32_t ownerCount{(*sle)[sfOwnerCount]}; + + if (oldHookState) { + view.erase(oldHookState); + } else { + + ++stateCount; + + if (COMPUTE_HOOK_DATA_OWNER_COUNT(stateCount) > oldStateReserve) { + // the hook used its allocated allotment of state entries for its previous ownercount + // increment ownercount and give it another allotment + + ++ownerCount; + XRPAmount const newReserve{ + view.fees().accountReserve(ownerCount)}; + + if (STAmount((*sle)[sfBalance]).xrp() < newReserve) + return tecINSUFFICIENT_RESERVE; + + + adjustOwnerCount(view, sle, 1, j); + + } + + // update state count + sle->setFieldU32(sfHookStateCount, stateCount); + view.update(sle); + } + + // add new data to ledger + auto newHookState = std::make_shared(hookStateKeylet); + view.insert(newHookState); + newHookState->setFieldVL(sfHookStateData, data); + newHookState->setFieldH256(sfHookStateKey, key); + newHookState->setAccountID(sfAccount, acc); // RH TODO remove these + newHookState->setFieldH256(sfHookNamespace, ns); // in prod + + if (!oldHookState) { + // Add the hook to the account's directory if it wasn't there already + auto const page = dirAdd( + view, + hookStateDirKeylet, + hookStateKeylet.key, + false, + describeOwnerDir(acc), + j); + + JLOG(j.trace()) << "HookInfo[" << HR_ACC() << "]: " + << "Create/update hook state: " + << (page ? "success" : "failure"); + + if (!page) + return tecDIR_FULL; + + newHookState->setFieldU64(sfOwnerNode, *page); + } + + return tesSUCCESS; +} + +hook::HookResult +hook::apply( + ripple::uint256 const& hookSetTxnID, /* this is the txid of the sethook, used for caching (one day) */ + ripple::uint256 const& hookHash, /* hash of the actual hook byte code, used for metadata */ + ripple::uint256 const& hookNamespace, + ripple::Blob const& wasm, + std::map< + std::vector, /* param name */ + std::vector /* param value */ + > const& hookParams, + std::map< + ripple::uint256, /* hook hash */ + std::map< + std::vector, + std::vector + >> const& hookParamOverrides, + ApplyContext& applyCtx, + ripple::AccountID const& account, /* the account the hook is INSTALLED ON not always the otxn account */ + bool callback = false, + uint32_t wasmParam, + int32_t hookChainPosition) +{ + + + HookContext hookCtx = + { + .applyCtx = applyCtx, + // we will return this context object (RVO / move constructed) + .result = { + .hookSetTxnID = hookSetTxnID, + .hookHash = hookHash, + .accountKeylet = keylet::account(account), + .ownerDirKeylet = keylet::ownerDir(account), + .hookKeylet = keylet::hook(account), + .account = account, + .otxnAccount = applyCtx.tx.getAccountID(sfAccount), + .hookNamespace = hookNamespace, + .changedState = + std::make_shared< + std::map>>>>(), + .hookParamOverrides = hookParamOverrides, + .hookParams = hookParams, + .hookSkips = {}, + .exitType = hook_api::ExitType::ROLLBACK, // default is to rollback unless hook calls accept() + .exitReason = std::string(""), + .exitCode = -1, + .callback = callback, + .wasmParam = wasmParam, + .hookChainPosition = hookChainPosition, + .foreignStateSetDisabled = false + }, + .emitFailure = + callback && wasmParam & 1 + ? std::optional( + (*(applyCtx.view().peek( + keylet::emitted(applyCtx.tx.getFieldH256(sfTransactionHash))) + )).downcast() + ) + : std::optional() + }; + + + auto const& j = applyCtx.app.journal("View"); + + SSVM::VM::Configure cfg; + SSVM::VM::VM vm(cfg); + HookModule env(hookCtx); + vm.registerModule(env); + + std::vector params, results; + params.push_back(wasmParam); + + JLOG(j.trace()) + << "HookInfo[" << HC_ACC() << "]: creating wasm instance"; + if (auto result = + vm.runWasmFile( + SSVM::Span(wasm.data(), wasm.size()), (callback ? "cbak" : "hook"), params)) + hookCtx.result.instructionCount = vm.getStatistics().getInstrCount(); + else + { + uint32_t ssvm_error = static_cast(result.error()); + if (ssvm_error > 1) + { + JLOG(j.warn()) + << "HookError[" << HC_ACC() << "]: SSVM error " << ssvm_error; + hookCtx.result.exitType = hook_api::ExitType::WASM_ERROR; + return hookCtx.result; + } + } + + JLOG(j.trace()) << + "HookInfo[" << HC_ACC() << "]: " << + ( 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; +} + +/* If XRPLD is running with trace log level hooks may produce debugging output to the trace log + * specifying both a string and an integer to output */ +DEFINE_HOOK_FUNCTION( + int64_t, + trace_num, + uint32_t read_ptr, uint32_t read_len, int64_t number) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx on current stack + if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length)) + return OUT_OF_BOUNDS; + + RETURN_HOOK_TRACE(read_ptr, read_len, number); +} + + +DEFINE_HOOK_FUNCTION( + int64_t, + trace, + uint32_t mread_ptr, uint32_t mread_len, + uint32_t dread_ptr, uint32_t dread_len, uint32_t as_hex ) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx on current stack + if (NOT_IN_BOUNDS(mread_ptr, mread_len, memory_length) || + NOT_IN_BOUNDS(dread_ptr, dread_len, memory_length)) + return OUT_OF_BOUNDS; + + if (!j.trace()) + return 0; + + if (mread_len > 128) + mread_len = 128; + + if (dread_len > 1024) + dread_len = 1024; + + uint8_t output[2048]; + size_t out_len = 0; + if (as_hex) + { + out_len = dread_len * 2; + for (int i = 0; i < dread_len && i < memory_length; ++i) + { + unsigned char high = (memory[dread_ptr + i] >> 4) & 0xF; + unsigned char low = (memory[dread_ptr + i] & 0xF); + high += ( high < 10U ? '0' : 'A' - 10 ); + low += ( low < 10U ? '0' : 'A' - 10 ); + output[i*2 + 0] = high; + output[i*2 + 1] = low; + } + } + else if (is_UTF16LE(memory + dread_ptr, dread_len)) + { + out_len = dread_len / 2; //is_UTF16LE will only return true if read_len is even + for (int i = 0; i < out_len; ++i) + output[i] = memory[dread_ptr + i * 2]; + } + + RETURN_HOOK_TRACE(mread_ptr, mread_len, + std::string_view((const char*)output, out_len)); +} + + +// zero pad on the left a key to bring it up to 32 bytes +std::optional +inline +make_state_key( + std::string_view source) +{ + + size_t source_len = source.size(); + + if (source_len > 32 || source_len < 1) + return std::nullopt; + + unsigned char key_buffer[32]; + int i = 0; + int pad = 32 - source_len; + + // zero pad on the left + for (; i < pad; ++i) + key_buffer[i] = 0; + + const char* data = source.data(); + + for (; i < 32; ++i) + key_buffer[i] = data[i - pad]; + + return ripple::uint256::fromVoid(key_buffer); +} + +// check the state cache +inline +std::optional const>> +lookup_state_cache( + hook::HookContext& hookCtx, + ripple::AccountID const& acc, + ripple::uint256 const& ns, + ripple::uint256 const& key) +{ + auto& changedState = *(hookCtx.result.changedState); + if (changedState.find(acc) == changedState.end()) + return std::nullopt; + + auto& changedStateAcc = changedState[acc]; + if (changedStateAcc.find(ns) == changedStateAcc.end()) + return std::nullopt; + + auto& changedStateNs = changedStateAcc[ns]; + + auto const& ret = changedStateNs.find(key); + + if (ret == changedStateNs.end()) + return std::nullopt; + + return std::cref(ret->second); +} + + +// update the state cache +inline +void +set_state_cache( + hook::HookContext& hookCtx, + ripple::AccountID const& acc, + ripple::uint256 const& ns, + ripple::uint256 const& key, + ripple::Blob& data, + bool modified) +{ + + auto& changedState = *(hookCtx.result.changedState); + if (changedState.find(acc) == changedState.end()) + { + changedState[acc] = + { + { ns, + { + { key, + { modified, data } + } + } + } + }; + return; + } + + auto& changedStateAcc = changedState[acc]; + if (changedStateAcc.find(ns) == changedStateAcc.end()) + { + changedStateAcc[ns] = + { + { key, + { modified, data } + } + }; + return; + } + + auto& changedStateNs = changedStateAcc[ns]; + if (changedStateNs.find(key) == changedStateNs.end()) + { + changedStateNs[key] = { modified, data }; + return; + } + + if (modified) + changedStateNs[key].first = true; + + changedStateNs[key].second = data; + return; +} + +DEFINE_HOOK_FUNCTION( + int64_t, + state_set, + uint32_t read_ptr, uint32_t read_len, + uint32_t kread_ptr, uint32_t kread_len ) +{ + return + state_foreign_set( + hookCtx, memoryCtx, + read_ptr, read_len, + kread_ptr, kread_len, + 0, 0, + 0, 0); +} +// update or create a hook state object +// read_ptr = data to set, kread_ptr = key +// RH NOTE passing 0 size causes a delete operation which is as-intended +// RH TODO: check reserve +/* + uint32_t write_ptr, uint32_t write_len, + uint32_t kread_ptr, uint32_t kread_len, // key + uint32_t nread_ptr, uint32_t nread_len, // namespace + uint32_t aread_ptr, uint32_t aread_len ) // account + */ +DEFINE_HOOK_FUNCTION( + int64_t, + state_foreign_set, + uint32_t read_ptr, uint32_t read_len, + uint32_t kread_ptr, uint32_t kread_len, + uint32_t nread_ptr, uint32_t nread_len, + uint32_t aread_ptr, uint32_t aread_len) +{ + + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + + if (NOT_IN_BOUNDS(kread_ptr, 32, memory_length)) + return OUT_OF_BOUNDS; + + if (read_ptr == 0 && read_len == 0) + { + // valid, this is a delete operation + } + else if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length)) + return OUT_OF_BOUNDS; + + if (kread_len > 32) + return TOO_BIG; + + if (kread_len < 1) + return TOO_SMALL; + + // ns can be null if and only if this is a local set + if (nread_ptr == 0 && nread_len == 0 && !(aread_ptr == 0 && aread_len == 0)) + return INVALID_ARGUMENT; + + if ((nread_len && !NOT_IN_BOUNDS(nread_ptr, nread_len, memory_length)) || + (kread_len && !NOT_IN_BOUNDS(kread_ptr, kread_len, memory_length)) || + (aread_len && !NOT_IN_BOUNDS(aread_ptr, aread_len, memory_length))) + return OUT_OF_BOUNDS; + + uint32_t maxSize = hook::maxHookStateDataSize(); + if (read_len > maxSize) + return TOO_BIG; + + uint256 ns = + nread_len == 0 + ? hookCtx.result.hookNamespace + : ripple::base_uint<256>::fromVoid(memory + nread_ptr); + + ripple::AccountID acc = + aread_len == 0 + ? AccountID::fromVoid(memory + aread_ptr) + : hookCtx.result.account; + + auto const key = + make_state_key( std::string_view { (const char*)(memory + kread_ptr), (size_t)kread_len } ); + + ripple::Blob data {memory + read_ptr, memory + read_ptr + read_len}; + + // local modifications are always allowed + if (aread_len == 0 || acc == hookCtx.result.account) + { + set_state_cache(hookCtx, acc, ns, *key, data, true); + return read_len; + } + + // execution to here means it's actually a foreign set + if (hookCtx.result.foreignStateSetDisabled) + return PREVIOUS_FAILURE_PREVENTS_RETRY; + + // first check if we've already modified this state + auto cacheEntry = lookup_state_cache(hookCtx, acc, ns, *key); + if (cacheEntry && cacheEntry->get().first) + { + // if a cache entry already exists and it has already been modified don't check grants again + set_state_cache(hookCtx, acc, ns, *key, data, true); + return read_len; + } + + // cache miss or cache was present but entry was not marked as previously modified + // therefore before continuing we need to check grants + auto const sle = view.peek(ripple::keylet::hook(acc)); + if (!sle) + return INTERNAL_ERROR; + + bool found_auth = false; + + // we do this by iterating the hooks installed on the foreign account and in turn their grants and namespaces + auto const& hooks = sle->getFieldArray(sfHooks); + for (auto const& hook : hooks) + { + STObject const* hookObj = dynamic_cast(&hook); + + // skip blank entries + if (!hookObj->isFieldPresent(sfHookGrants)) + continue; + + auto const& hookGrants = hookObj->getFieldArray(sfHookGrants); + + if (hookGrants.size() < 1) + continue; + + // the grant allows the hook to modify the granter's namespace only + if (hookObj->getFieldH256(sfHookNamespace) != ns) + continue; + + // this is expensive search so we'll disallow after one failed attempt + for (auto const& hookGrant : hookGrants) + { + STObject const* hookGrantObj = dynamic_cast(&hookGrant); + bool hasAuthorizedField = hookGrantObj->isFieldPresent(sfAuthorize); + + if (hookGrantObj->getFieldH256(sfHookHash) == hookCtx.result.hookHash && + (!hasAuthorizedField || + (hasAuthorizedField && + hookGrantObj->getAccountID(sfAuthorize) == hookCtx.result.account))) + { + found_auth = true; + break; + } + } + + if (found_auth) + break; + } + + if (!found_auth) + { + // hook only gets one attempt + hookCtx.result.foreignStateSetDisabled = true; + return NOT_AUTHORIZED; + } + + set_state_cache( + hookCtx, + acc, + ns, + *key, + data, + true); + + 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 + * ------------------------ + */ +{ + + 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 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()); + + setHookState(hookResult, applyCtx, acc, ns, key, slice); + // ^ should not fail... checks were done before map insert + } + } + } + } + + } + + // open views do not modify add/remove ledger entries + if (applyCtx.view().open()) + return; + + //RH TODO: this seems hacky... and also maybe there's a way this cast might fail? + ApplyViewImpl& avi = dynamic_cast(applyCtx.view()); + + 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) + { + DBG_PRINTF("emitted txn count: %d\n", hookResult.emittedTxn.size()); + for (; hookResult.emittedTxn.size() > 0; hookResult.emittedTxn.pop()) + { + auto& tpTrans = hookResult.emittedTxn.front(); + auto& id = tpTrans->getID(); + JLOG(j.trace()) + << "HookEmit[" << HR_ACC() << "]: " << id; + + std::shared_ptr ptr = tpTrans->getSTransaction(); + + ripple::Serializer s; + ptr->add(s); + SerialIter sit(s.slice()); + + auto emittedId = keylet::emitted(id); + + auto sleEmitted = applyCtx.view().peek(keylet::emitted(id)); + if (!sleEmitted) + { + ++emission_count; + sleEmitted = std::make_shared(emittedId); + sleEmitted->emplace_back( + ripple::STObject(sit, sfEmittedTxn) + ); + auto page = applyCtx.view().dirAppend( + keylet::emittedDir(), + emittedId, + [&](SLE::ref sle) { + // RH TODO: should something be here? + }); + + if (page) + { + (*sleEmitted)[sfOwnerNode] = *page; + applyCtx.view().insert(sleEmitted); + } + else + { + JLOG(j.warn()) << "HookError[" << HR_ACC() << "]: " << + "Emission Directory full when trying to insert " << id; + break; + } + } + } + } + + // 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(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 ); + meta.setAccountID(sfHookAccount, hookResult.account); + + // RH NOTE: this is probably not necessary, a direct cast should always put the (negative) 1 bit at the MSB + // however to ensure this is consistent across different arch/compilers it's done explicitly here. + uint64_t unsigned_exit_code = + ( hookResult.exitCode >= 0 ? hookResult.exitCode : + 0x8000000000000000ULL + (-1 * hookResult.exitCode )); + + meta.setFieldU64(sfHookReturnCode, unsigned_exit_code); + meta.setFieldVL(sfHookReturnString, ripple::Slice{hookResult.exitReason.data(), hookResult.exitReason.size()}); + 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.setFieldH256(sfHookHash, hookResult.hookHash); + avi.addHookMetaData(std::move(meta)); +} + +/* Retrieve the state into write_ptr identified by the key in kread_ptr */ +DEFINE_HOOK_FUNCTION( + int64_t, + state, + uint32_t write_ptr, uint32_t write_len, + uint32_t kread_ptr, uint32_t kread_len ) +{ + return + state_foreign( + hookCtx, memoryCtx, + write_ptr, write_len, + 0, 0, + kread_ptr, kread_len, + 0, 0); +} + +/* This api actually serves both local and foreign state requests + * feeding aread_ptr = 0 and aread_len = 0 will cause it to read local + * feeding nread_len = 0 will cause hook's native namespace to be used */ +DEFINE_HOOK_FUNCTION( + int64_t, + state_foreign, + uint32_t write_ptr, uint32_t write_len, + uint32_t kread_ptr, uint32_t kread_len, // key + uint32_t nread_ptr, uint32_t nread_len, // namespace + uint32_t aread_ptr, uint32_t aread_len ) // account +{ + + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + + bool is_foreign = false; + if (aread_ptr == 0) + { + // valid arguments, local state + } else if (aread_ptr > 0) + { + // valid arguments, foreign state + is_foreign = true; + } else return INVALID_ARGUMENT; + + if (NOT_IN_BOUNDS(kread_ptr, kread_len, memory_length) || + NOT_IN_BOUNDS(nread_ptr, nread_len, memory_length) || + NOT_IN_BOUNDS(aread_ptr, aread_len, memory_length) || + NOT_IN_BOUNDS(write_ptr, write_len, memory_length)) + return OUT_OF_BOUNDS; + + if (kread_len > 32) + return TOO_BIG; + + + if (!is_foreign && nread_len == 0) + { + // local account will be populated with local hook namespace unless otherwise specified + } + else if (nread_len != 32) + return INVALID_ARGUMENT; + + if (is_foreign && aread_len != 20) + return INVALID_ACCOUNT; + + uint256 ns = + nread_len == 0 + ? hookCtx.result.hookNamespace + : ripple::base_uint<256>::fromVoid(memory + nread_ptr); + + ripple::AccountID acc = + is_foreign + ? AccountID::fromVoid(memory + aread_ptr) + : hookCtx.result.account; + + auto const key = + make_state_key( std::string_view { (const char*)(memory + kread_ptr), (size_t)kread_len } ); + + if (!key) + return INVALID_ARGUMENT; + + // first check if the requested state was previously cached this session + auto cacheEntryLookup = lookup_state_cache(hookCtx, acc, ns, *key); + if (cacheEntryLookup) + { + auto const& cacheEntry = cacheEntryLookup->get(); + if (write_ptr == 0) + return data_as_int64(cacheEntry.second.data(), cacheEntry.second.size()); + + if (cacheEntry.second.size() > write_len) + return TOO_SMALL; + + WRITE_WASM_MEMORY_AND_RETURN( + write_ptr, write_len, + cacheEntry.second.data(), cacheEntry.second.size(), + memory, memory_length); + } + + auto hsSLE = + view.peek(keylet::hookState(acc, *key, ns)); + + if (!hsSLE) + return DOESNT_EXIST; + + Blob b = hsSLE->getFieldVL(sfHookStateData); + + // it exists add it to cache and return it + set_state_cache(hookCtx, acc, ns, *key, b, false); + + if (write_ptr == 0) + return data_as_int64(b.data(), b.size()); + + if (b.size() > write_len) + return TOO_SMALL; + + WRITE_WASM_MEMORY_AND_RETURN( + write_ptr, write_len, + b.data(), b.size(), + memory, memory_length); +} + + +// Cause the originating transaction to go through, save state changes and emit emitted tx, exit hook +DEFINE_HOOK_FUNCTION( + int64_t, + accept, + uint32_t read_ptr, uint32_t read_len, int64_t error_code ) +{ + HOOK_SETUP(); + HOOK_EXIT(read_ptr, read_len, error_code, hook_api::ExitType::ACCEPT); +} + +// Cause the originating transaction to be rejected, discard state changes and discard emitted tx, exit hook +DEFINE_HOOK_FUNCTION( + int64_t, + rollback, + uint32_t read_ptr, uint32_t read_len, int64_t error_code ) +{ + HOOK_SETUP(); + HOOK_EXIT(read_ptr, read_len, error_code, hook_api::ExitType::ROLLBACK); +} + + +// Write the TxnID of the originating transaction into the write_ptr +DEFINE_HOOK_FUNCTION( + int64_t, + otxn_id, + uint32_t write_ptr, uint32_t write_len, uint32_t flags ) +{ + + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + + auto const& txID = + (hookCtx.emitFailure && !flags + ? applyCtx.tx.getFieldH256(sfTransactionHash) + : applyCtx.tx.getTransactionID()); + + if (txID.size() > write_len) + return TOO_SMALL; + + if (NOT_IN_BOUNDS(write_ptr, txID.size(), memory_length)) + return OUT_OF_BOUNDS; + + WRITE_WASM_MEMORY_AND_RETURN( + write_ptr, txID.size(), + txID.data(), txID.size(), + memory, memory_length); +} + +// Return the tt (Transaction Type) numeric code of the originating transaction +DEFINE_HOOK_FUNCNARG( + int64_t, + otxn_type ) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + + if (hookCtx.emitFailure) + return safe_cast(hookCtx.emitFailure->getFieldU16(sfTransactionType)); + + return applyCtx.tx.getTxnType(); +} + +DEFINE_HOOK_FUNCTION( + int64_t, + otxn_slot, + uint32_t slot_into ) +{ + + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + + if (slot_into > hook_api::max_slots) + return INVALID_ARGUMENT; + + // check if we can emplace the object to a slot + if (slot_into == 0 && no_free_slots(hookCtx)) + return NO_FREE_SLOTS; + + if (slot_into == 0) + slot_into = get_free_slot(hookCtx); + + + auto const& st_tx = + std::make_shared( + hookCtx.emitFailure + ? *(hookCtx.emitFailure) + : const_cast(applyCtx.tx).downcast() + ); + + auto const& txID = + hookCtx.emitFailure + ? applyCtx.tx.getFieldH256(sfTransactionHash) + : applyCtx.tx.getTransactionID(); + + hookCtx.slot.emplace( std::pair { slot_into, hook::SlotEntry { + .id = std::vector{ txID.data(), txID.data() + txID.size() }, + .storage = st_tx, + .entry = 0 + }}); + hookCtx.slot[slot_into].entry = &(*hookCtx.slot[slot_into].storage); + + return slot_into; + +} +// Return the burden of the originating transaction... this will be 1 unless the originating transaction +// was itself an emitted transaction from a previous hook invocation +DEFINE_HOOK_FUNCNARG( + int64_t, + otxn_burden) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + if (hookCtx.burden) + return hookCtx.burden; + + auto const& tx = applyCtx.tx; + if (!tx.isFieldPresent(sfEmitDetails)) + return 1; // burden is always 1 if the tx wasn't a emit + + auto const& pd = const_cast(tx).getField(sfEmitDetails).downcast(); + + if (!pd.isFieldPresent(sfEmitBurden)) { + JLOG(j.warn()) + << "HookError[" << HC_ACC() << "]: found sfEmitDetails but sfEmitBurden was not present"; + return 1; + } + + uint64_t burden = pd.getFieldU64(sfEmitBurden); + burden &= ((1ULL << 63)-1); // wipe out the two high bits just in case somehow they are set + hookCtx.burden = burden; + return (int64_t)(burden); +} + +// Return the generation of the originating transaction... this will be 1 unless the originating transaction +// was itself an emitted transaction from a previous hook invocation +DEFINE_HOOK_FUNCNARG( + int64_t, + otxn_generation) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + + // cache the result as it will not change for this hook execution + if (hookCtx.generation) + return hookCtx.generation; + + auto const& tx = applyCtx.tx; + if (!tx.isFieldPresent(sfEmitDetails)) + return 1; // generation is always 1 if the tx wasn't a emit + + auto const& pd = const_cast(tx).getField(sfEmitDetails).downcast(); + + if (!pd.isFieldPresent(sfEmitGeneration)) { + JLOG(j.warn()) + << "HookError[" << HC_ACC() << "]: found sfEmitDetails but sfEmitGeneration was not present"; + return 1; + } + + hookCtx.generation = pd.getFieldU32(sfEmitGeneration); + // this overflow will never happen in the life of the ledger but deal with it anyway + if (hookCtx.generation + 1 > hookCtx.generation) + hookCtx.generation++; + + return hookCtx.generation; +} + +// Return the generation of a hypothetically emitted transaction from this hook +DEFINE_HOOK_FUNCNARG( + int64_t, + etxn_generation) +{ + return otxn_generation(hookCtx, memoryCtx) + 1; +} + + +// Return the current ledger sequence number +DEFINE_HOOK_FUNCNARG( + int64_t, + ledger_seq) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + return applyCtx.app.getLedgerMaster().getValidLedgerIndex() + 1; +} + + +DEFINE_HOOK_FUNCTION( + int64_t, + ledger_last_hash, + uint32_t write_ptr, uint32_t write_len) +{ + HOOK_SETUP(); + if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length)) + return OUT_OF_BOUNDS; + if (write_len < 32) + return TOO_SMALL; + + uint256 hash = applyCtx.app.getLedgerMaster().getValidatedLedger()->info().hash; + + WRITE_WASM_MEMORY_AND_RETURN( + write_ptr, write_len, + hash.data(), 32, + memory, memory_length); + +} + +// Dump a field in 'full text' form into the hook's memory +DEFINE_HOOK_FUNCTION( + int64_t, + otxn_field_txt, + uint32_t write_ptr, uint32_t write_len, + uint32_t field_id ) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length)) + return OUT_OF_BOUNDS; + + + SField const& fieldType = ripple::SField::getField( field_id ); + + if (fieldType == sfInvalid) + return INVALID_FIELD; + + if (!applyCtx.tx.isFieldPresent(fieldType)) + return DOESNT_EXIST; + + auto const& field = + hookCtx.emitFailure + ? hookCtx.emitFailure->getField(fieldType) + : const_cast(applyCtx.tx).getField(fieldType); + + std::string out = field.getText(); + + if (out.size() > write_len) + return TOO_SMALL; + + WRITE_WASM_MEMORY_AND_RETURN( + write_ptr, write_len, + out.data(), out.size(), + memory, memory_length); + +} + +// Dump a field from the originating transaction into the hook's memory +DEFINE_HOOK_FUNCTION( + int64_t, + otxn_field, + uint32_t write_ptr, uint32_t write_len, + uint32_t field_id ) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + + if (write_ptr != 0 && NOT_IN_BOUNDS(write_ptr, write_len, memory_length)) + return OUT_OF_BOUNDS; + + + SField const& fieldType = ripple::SField::getField( field_id ); + + if (fieldType == sfInvalid) + return INVALID_FIELD; + + if (!applyCtx.tx.isFieldPresent(fieldType)) + return DOESNT_EXIST; + + auto const& field = + hookCtx.emitFailure + ? hookCtx.emitFailure->getField(fieldType) + : const_cast(applyCtx.tx).getField(fieldType); + + bool is_account = field.getSType() == STI_ACCOUNT; //RH TODO improve this hack + + Serializer s; + field.add(s); + + if (write_ptr == 0) + return data_as_int64(s.getDataPtr(), s.getDataLength()); + + if (s.getDataLength() - (is_account ? 1 : 0) > write_len) + return TOO_SMALL; + + WRITE_WASM_MEMORY_AND_RETURN( + write_ptr, write_len, + (unsigned char*)(s.getDataPtr()) + (is_account ? 1 : 0), s.getDataLength() - (is_account ? 1 : 0), + memory, memory_length); +} + +DEFINE_HOOK_FUNCTION( + int64_t, + slot, + uint32_t write_ptr, uint32_t write_len, + uint32_t slot_no ) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + + if (!(write_ptr == 0 && write_len == 0) && + NOT_IN_BOUNDS(write_ptr, write_len, memory_length)) + return OUT_OF_BOUNDS; + + if (write_ptr != 0 && write_len == 0) + return TOO_SMALL; + + if (hookCtx.slot.find(slot_no) == hookCtx.slot.end()) + return DOESNT_EXIST; + + if (hookCtx.slot[slot_no].entry == 0) + return INTERNAL_ERROR; + + Serializer s; + hookCtx.slot[slot_no].entry->add(s); + + if (write_ptr == 0) + return data_as_int64(s.getDataPtr(), s.getDataLength()); + + bool is_account = hookCtx.slot[slot_no].entry->getSType() == STI_ACCOUNT; //RH TODO improve this hack + + if (s.getDataLength() - (is_account ? 1 : 0) > write_len) + return TOO_SMALL; + + WRITE_WASM_MEMORY_AND_RETURN( + write_ptr, write_len, + (unsigned char*)(s.getDataPtr()) + (is_account ? 1 : 0), s.getDataLength() - (is_account ? 1 : 0), + memory, memory_length); + +} + +DEFINE_HOOK_FUNCTION( + int64_t, + slot_clear, + uint32_t slot_no ) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + + if (hookCtx.slot.find(slot_no) == hookCtx.slot.end()) + return DOESNT_EXIST; + + hookCtx.slot.erase(slot_no); + hookCtx.slot_free.push(slot_no); + + return 1; +} + +DEFINE_HOOK_FUNCTION( + int64_t, + slot_count, + uint32_t slot_no ) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + if (hookCtx.slot.find(slot_no) == hookCtx.slot.end()) + return DOESNT_EXIST; + + if (hookCtx.slot[slot_no].entry->getSType() != STI_ARRAY) + return NOT_AN_ARRAY; + + if (hookCtx.slot[slot_no].entry == 0) + return INTERNAL_ERROR; + + return hookCtx.slot[slot_no].entry->downcast().size(); +} + + + +DEFINE_HOOK_FUNCTION( + int64_t, + slot_id, + uint32_t write_ptr, uint32_t write_len, + uint32_t slot_no ) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + + if (hookCtx.slot.find(slot_no) == hookCtx.slot.end()) + return DOESNT_EXIST; + + auto& e = hookCtx.slot[slot_no].id; + + if (write_len < e.size()) + return TOO_SMALL; + + if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length)) + return OUT_OF_BOUNDS; + + WRITE_WASM_MEMORY_AND_RETURN( + write_ptr, write_len, + e.data(), e.size(), + memory, memory_length); +} + +DEFINE_HOOK_FUNCTION( + int64_t, + slot_set, + uint32_t read_ptr, uint32_t read_len, // readptr is a keylet + int32_t slot_into /* providing 0 allocates a slot to you */ ) +{ + 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 ((read_len != 32 && read_len != 34) || slot_into < 0 || slot_into > hook_api::max_slots) + return INVALID_ARGUMENT; + + // check if we can emplace the object to a slot + if (slot_into == 0 && no_free_slots(hookCtx)) + return NO_FREE_SLOTS; + + std::vector slot_key { memory + read_ptr, memory + read_ptr + read_len }; + std::optional> slot_value = std::nullopt; + + if (read_len == 34) + { + std::optional kl = unserialize_keylet(memory + read_ptr, read_len); + if (!kl) + return DOESNT_EXIST; + + auto sle = applyCtx.view().peek(*kl); + if (!sle) + return DOESNT_EXIST; + + slot_value = sle; + } + else if (read_len == 32) + { + + uint256 hash; + if (!hash.SetHexExact((const char*)(memory + read_ptr))) + return INVALID_ARGUMENT; + + ripple::error_code_i ec { ripple::error_code_i::rpcUNKNOWN }; + std::shared_ptr hTx = applyCtx.app.getMasterTransaction().fetch(hash, ec); + if (!hTx) + return DOESNT_EXIST; + + slot_value = hTx->getSTransaction(); + } + else + return DOESNT_EXIST; + + if (!slot_value.has_value()) + return DOESNT_EXIST; + + if (slot_into == 0) + slot_into = get_free_slot(hookCtx); + + + hookCtx.slot.emplace( std::pair { slot_into, hook::SlotEntry { + .id = slot_key, + .storage = *slot_value, + .entry = 0 + }}); + hookCtx.slot[slot_into].entry = &(*hookCtx.slot[slot_into].storage); + + return slot_into; +} + +DEFINE_HOOK_FUNCTION( + int64_t, + slot_size, + uint32_t slot_no ) +{ + if (hookCtx.slot.find(slot_no) == hookCtx.slot.end()) + return DOESNT_EXIST; + + //RH TODO: this is a very expensive way of computing size, fix it + Serializer s; + hookCtx.slot[slot_no].entry->add(s); + return s.getDataLength(); +} + +DEFINE_HOOK_FUNCTION( + int64_t, + slot_subarray, + uint32_t parent_slot, uint32_t array_id, uint32_t new_slot ) +{ + if (hookCtx.slot.find(parent_slot) == hookCtx.slot.end()) + return DOESNT_EXIST; + + if (hookCtx.slot[parent_slot].entry->getSType() != STI_ARRAY) + return NOT_AN_ARRAY; + + if (hookCtx.slot[parent_slot].entry == 0) + return INTERNAL_ERROR; + + if (new_slot == 0 && no_free_slots(hookCtx)) + return NO_FREE_SLOTS; + + bool copied = false; + try + { + ripple::STArray& parent_obj = + const_cast(*hookCtx.slot[parent_slot].entry).downcast(); + + if (parent_obj.size() <= array_id) + return DOESNT_EXIST; + new_slot = ( new_slot == 0 ? get_free_slot(hookCtx) : new_slot ); + + // copy + if (new_slot != parent_slot) + { + copied = true; + hookCtx.slot[new_slot] = hookCtx.slot[parent_slot]; + } + hookCtx.slot[new_slot].entry = &(parent_obj[array_id]); + return new_slot; + } + catch (const std::bad_cast& e) + { + if (copied) + { + hookCtx.slot.erase(new_slot); + hookCtx.slot_free.push(new_slot); + } + return NOT_AN_ARRAY; + } +} + + +DEFINE_HOOK_FUNCTION( + int64_t, + slot_subfield, + uint32_t parent_slot, uint32_t field_id, uint32_t new_slot) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + + if (hookCtx.slot.find(parent_slot) == hookCtx.slot.end()) + return DOESNT_EXIST; + + if (new_slot == 0 && no_free_slots(hookCtx)) + return NO_FREE_SLOTS; + + SField const& fieldCode = ripple::SField::getField( field_id ); + + if (fieldCode == sfInvalid) + return INVALID_FIELD; + + bool copied = false; + + try + { + ripple::STObject& parent_obj = + const_cast(*hookCtx.slot[parent_slot].entry).downcast(); + + if (!parent_obj.isFieldPresent(fieldCode)) + return DOESNT_EXIST; + + new_slot = ( new_slot == 0 ? get_free_slot(hookCtx) : new_slot ); + + // copy + if (new_slot != parent_slot) + { + copied = true; + hookCtx.slot[new_slot] = hookCtx.slot[parent_slot]; + } + + hookCtx.slot[new_slot].entry = &(parent_obj.getField(fieldCode)); + return new_slot; + } + catch (const std::bad_cast& e) + { + if (copied) + { + hookCtx.slot.erase(new_slot); + hookCtx.slot_free.push(new_slot); + } + return NOT_AN_OBJECT; + } + +} + +DEFINE_HOOK_FUNCTION( + int64_t, + slot_type, + uint32_t slot_no, uint32_t flags ) +{ + + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + + if (hookCtx.slot.find(slot_no) == hookCtx.slot.end()) + return DOESNT_EXIST; + + if (hookCtx.slot[slot_no].entry == 0) + return INTERNAL_ERROR; + try + { + ripple::STBase& obj = + const_cast(*hookCtx.slot[slot_no].entry);//.downcast(); + if (flags == 0) + return obj.getFName().fieldCode; + + // this flag is for use with an amount field to determine if the amount is native (xrp) + if (flags == 1) + { + if (obj.getSType() != STI_AMOUNT) + return NOT_AN_AMOUNT; + return + const_cast(*hookCtx.slot[slot_no].entry).downcast().native(); + } + + return INVALID_ARGUMENT; + } + catch (const std::bad_cast& e) + { + return INTERNAL_ERROR; + } +} + +DEFINE_HOOK_FUNCTION( + int64_t, + slot_float, + uint32_t slot_no ) +{ + if (hookCtx.slot.find(slot_no) == hookCtx.slot.end()) + return DOESNT_EXIST; + + try + { + ripple::STAmount& st_amt = + const_cast(*hookCtx.slot[slot_no].entry).downcast(); + if (st_amt.native()) + { + ripple::XRPAmount amt = st_amt.xrp(); + int64_t drops = amt.drops(); + int32_t exp = -6; + // normalize + return hook_float::float_set(exp, drops); + } + else + { + ripple::IOUAmount amt = st_amt.iou(); + return make_float(amt); + } + } + catch (const std::bad_cast& e) + { + return NOT_AN_AMOUNT; + } + +} + +DEFINE_HOOK_FUNCTION( + int64_t, + trace_slot, + uint32_t read_ptr, uint32_t read_len, + uint32_t slot_no ) +{ + + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + + if (hookCtx.slot.find(slot_no) == hookCtx.slot.end()) + return DOESNT_EXIST; + + if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length)) + return OUT_OF_BOUNDS; + + uint8_t* id = hookCtx.slot[slot_no].id.data(); + size_t id_size = hookCtx.slot[slot_no].id.size(); + uint8_t output[64]; + if (id_size > 32) id_size = 32; + for (int i = 0; i < id_size; ++i) + { + unsigned char high = (id[i] >> 4) & 0xF; + unsigned char low = (id[i] & 0xF); + high += ( high < 10U ? '0' : 'A' - 10 ); + low += ( low < 10U ? '0' : 'A' - 10 ); + output[i*2 + 0] = high; + output[i*2 + 1] = low; + } + + RETURN_HOOK_TRACE(read_ptr, read_len, + "Slot " << slot_no << " - " + << std::string_view((const char*)output, (size_t)(id_size*2))); +} + + +DEFINE_HOOK_FUNCTION( + int64_t, + util_keylet, + uint32_t write_ptr, uint32_t write_len, uint32_t keylet_type, + uint32_t a, uint32_t b, uint32_t c, + uint32_t d, uint32_t e, uint32_t f ) +{ + HOOK_SETUP(); + + if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length)) + return OUT_OF_BOUNDS; + + if (write_len < 34) + return TOO_SMALL; + + if (keylet_type < 1 || keylet_type > 21) + return INVALID_ARGUMENT; + + try + { + switch (keylet_type) + { + + // keylets that take a keylet and an 8 byte uint + case keylet_code::QUALITY: + { + if (a == 0 || b == 0 || c == 0 || d == 0) + return INVALID_ARGUMENT; + if (e != 0 || f != 0) + return INVALID_ARGUMENT; + + uint32_t read_ptr = a, read_len = b; + + if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length)) + return OUT_OF_BOUNDS; + + if (read_len != 34) + return INVALID_ARGUMENT; + + std::optional kl = unserialize_keylet(memory + read_ptr, read_len); + if (!kl) + return NO_SUCH_KEYLET; + + uint64_t arg = (((uint64_t)c)<<32U)+((uint64_t)d); + + ripple::Keylet kl_out = + ripple::keylet::quality(*kl, arg); + + return serialize_keylet(kl_out, memory, write_ptr, write_len); + } + + // keylets that take a 32 byte uint + case keylet_code::CHILD: + case keylet_code::EMITTED: + case keylet_code::UNCHECKED: + { + if (a == 0 || b == 0) + return INVALID_ARGUMENT; + + if (c != 0 || d != 0 || e != 0 || f != 0) + return INVALID_ARGUMENT; + + uint32_t read_ptr = a, read_len = b; + + if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length)) + return OUT_OF_BOUNDS; + + if (read_len != 32) + return INVALID_ARGUMENT; + + base_uint<256> id = ripple::base_uint<256>::fromVoid(memory + read_ptr); + + ripple::Keylet kl = + keylet_type == keylet_code::CHILD ? ripple::keylet::child(id) : + keylet_type == keylet_code::EMITTED ? ripple::keylet::emitted(id) : + ripple::keylet::unchecked(id); + + return serialize_keylet(kl, memory, write_ptr, write_len); + } + + // keylets that take a 20 byte account id + case keylet_code::OWNER_DIR: + case keylet_code::SIGNERS: + case keylet_code::ACCOUNT: + case keylet_code::HOOK: + { + if (a == 0 || b == 0) + return INVALID_ARGUMENT; + + if (c != 0 || d != 0 || e != 0 || f != 0) + return INVALID_ARGUMENT; + + uint32_t read_ptr = a, read_len = b; + + if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length)) + return OUT_OF_BOUNDS; + + if (read_len != 20) + return INVALID_ARGUMENT; + + ripple::AccountID id = + ripple::base_uint<160, ripple::detail::AccountIDTag>::fromVoid(memory + read_ptr); + + ripple::Keylet kl = + keylet_type == keylet_code::HOOK ? ripple::keylet::hook(id) : + keylet_type == keylet_code::SIGNERS ? ripple::keylet::signers(id) : + keylet_type == keylet_code::OWNER_DIR ? ripple::keylet::ownerDir(id) : + ripple::keylet::account(id); + + return serialize_keylet(kl, memory, write_ptr, write_len); + } + + // keylets that take 20 byte account id, and 4 byte uint + case keylet_code::OFFER: + case keylet_code::CHECK: + case keylet_code::ESCROW: + { + if (a == 0 || b == 0 || c == 0) + return INVALID_ARGUMENT; + if (d != 0 || e != 0 || f != 0) + return INVALID_ARGUMENT; + + uint32_t read_ptr = a, read_len = b; + + if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length)) + return OUT_OF_BOUNDS; + + if (read_len != 20) + return INVALID_ARGUMENT; + + ripple::AccountID id = + ripple::base_uint<160, ripple::detail::AccountIDTag>::fromVoid(memory + read_ptr); + + ripple::Keylet kl = + keylet_type == keylet_code::CHECK ? ripple::keylet::check(id, c) : + keylet_type == keylet_code::ESCROW ? ripple::keylet::escrow(id, c) : + ripple::keylet::offer(id, c); + + return serialize_keylet(kl, memory, write_ptr, write_len); + } + + // keylets that take a 32 byte uint and an 8byte uint64 + case keylet_code::PAGE: + { + if (a == 0 || b == 0 || c == 0 || d == 0) + return INVALID_ARGUMENT; + + if (e != 0 || f != 0) + return INVALID_ARGUMENT; + + uint32_t kread_ptr = a, kread_len = b; + + if (NOT_IN_BOUNDS(kread_ptr, kread_len, memory_length)) + return OUT_OF_BOUNDS; + + if (b != 32) + return INVALID_ARGUMENT; + + uint64_t index = (((uint64_t)c)<<32U) + ((uint64_t)d); + ripple::Keylet kl = ripple::keylet::page(ripple::base_uint<256>::fromVoid(memory + a), index); + return serialize_keylet(kl, memory, write_ptr, write_len); + } + + // keylets that take both a 20 byte account id and a 32 byte uint + case keylet_code::HOOK_STATE: + { + if (a == 0 || b == 0 || c == 0 || d == 0) + return INVALID_ARGUMENT; + + + uint32_t aread_ptr = a, aread_len = b, kread_ptr = c, kread_len = d, nread_ptr = e, nread_len = f; + + if (NOT_IN_BOUNDS(aread_ptr, aread_len, memory_length) || + NOT_IN_BOUNDS(kread_ptr, kread_len, memory_length) || + NOT_IN_BOUNDS(nread_ptr, nread_len, memory_length)) + return OUT_OF_BOUNDS; + + if (aread_len != 20 || kread_len != 32 || nread_len != 32) + return INVALID_ARGUMENT; + + ripple::Keylet kl = + ripple::keylet::hookState( + ripple::base_uint<160, ripple::detail::AccountIDTag>::fromVoid(memory + aread_ptr), + ripple::base_uint<256>::fromVoid(memory + kread_ptr), + ripple::base_uint<256>::fromVoid(memory + nread_ptr)); + + return serialize_keylet(kl, memory, write_ptr, write_len); + } + + + // skip is overloaded, has a single, optional 4 byte argument + case keylet_code::SKIP: + { + if (c != 0 || d != 0 || e != 0 || f != 0) + return INVALID_ARGUMENT; + + ripple::Keylet kl = + (b == 0 ? ripple::keylet::skip() : + ripple::keylet::skip(a)); + + return serialize_keylet(kl, memory, write_ptr, write_len); + } + + // no arguments + case keylet_code::AMENDMENTS: + case keylet_code::FEES: + case keylet_code::NEGATIVE_UNL: + case keylet_code::EMITTED_DIR: + { + if (a != 0 || b != 0 || c != 0 || d != 0 || e != 0 || f != 0) + return INVALID_ARGUMENT; + + ripple::Keylet kl = + keylet_type == keylet_code::AMENDMENTS ? ripple::keylet::amendments() : + keylet_type == keylet_code::FEES ? ripple::keylet::fees() : + keylet_type == keylet_code::NEGATIVE_UNL ? ripple::keylet::negativeUNL() : + ripple::keylet::emittedDir(); + + return serialize_keylet(kl, memory, write_ptr, write_len); + } + + case keylet_code::LINE: + { + if (a == 0 || b == 0 || c == 0 || d == 0 || e == 0 || f == 0) + return INVALID_ARGUMENT; + + uint32_t hi_ptr = a, hi_len = b, lo_ptr = c, lo_len = d, cu_ptr = e, cu_len = f; + + if (NOT_IN_BOUNDS(hi_ptr, hi_len, memory_length) || + NOT_IN_BOUNDS(lo_ptr, lo_len, memory_length) || + NOT_IN_BOUNDS(cu_ptr, cu_len, memory_length)) + return OUT_OF_BOUNDS; + + if (hi_len != 20 || lo_len != 20 || cu_len != 20) + return INVALID_ARGUMENT; + + ripple::AccountID a0 = ripple::base_uint<160, ripple::detail::AccountIDTag>::fromVoid(memory + hi_ptr); + ripple::AccountID a1 = ripple::base_uint<160, ripple::detail::AccountIDTag>::fromVoid(memory + lo_ptr); + ripple::Currency cu = ripple::base_uint<160, ripple::detail::CurrencyTag>::fromVoid(memory + cu_ptr); + + ripple::Keylet kl = + ripple::keylet::line(a0, a1, cu); + return serialize_keylet(kl, memory, write_ptr, write_len); + } + + // keylets that take two 20 byte account ids + case keylet_code::DEPOSIT_PREAUTH: + { + if (a == 0 || b == 0 || c == 0 || d == 0) + return INVALID_ARGUMENT; + + if (e != 0 || f != 0) + return INVALID_ARGUMENT; + + uint32_t aread_ptr = a, aread_len = b; + uint32_t bread_ptr = c, bread_len = d; + + if (NOT_IN_BOUNDS(aread_ptr, aread_len, memory_length) || + NOT_IN_BOUNDS(bread_ptr, bread_len, memory_length)) + return OUT_OF_BOUNDS; + + if (aread_len != 20 || bread_len != 20) + return INVALID_ARGUMENT; + + ripple::AccountID aid = + ripple::base_uint<160, ripple::detail::AccountIDTag>::fromVoid(memory + aread_ptr); + ripple::AccountID bid = + ripple::base_uint<160, ripple::detail::AccountIDTag>::fromVoid(memory + bread_ptr); + + ripple::Keylet kl = + ripple::keylet::depositPreauth(aid, bid); + + return serialize_keylet(kl, memory, write_ptr, write_len); + } + + // keylets that take two 20 byte account ids and a 4 byte uint + case keylet_code::PAYCHAN: + { + if (a == 0 || b == 0 || c == 0 || d == 0 || e == 0) + return INVALID_ARGUMENT; + + if (f != 0) + return INVALID_ARGUMENT; + + uint32_t aread_ptr = a, aread_len = b; + uint32_t bread_ptr = c, bread_len = d; + + if (NOT_IN_BOUNDS(aread_ptr, aread_len, memory_length) || + NOT_IN_BOUNDS(bread_ptr, bread_len, memory_length)) + return OUT_OF_BOUNDS; + + if (aread_len != 20 || bread_len != 20) + return INVALID_ARGUMENT; + + ripple::AccountID aid = + ripple::base_uint<160, ripple::detail::AccountIDTag>::fromVoid(memory + aread_ptr); + ripple::AccountID bid = + ripple::base_uint<160, ripple::detail::AccountIDTag>::fromVoid(memory + bread_ptr); + + ripple::Keylet kl = + ripple::keylet::payChan(aid, bid, e); + + return serialize_keylet(kl, memory, write_ptr, write_len); + } + + } + } + catch (std::exception& e) + { + JLOG(j.warn()) + << "HookError[" << HC_ACC() << "]: Keylet exception " << e.what(); + return INTERNAL_ERROR; + } + + return NO_SUCH_KEYLET; +} + + +/* 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, + 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, 32, memory_length)) + return OUT_OF_BOUNDS; + + auto& app = hookCtx.applyCtx.app; + + if (hookCtx.expected_etxn_count < 0) + return PREREQUISITE_NOT_MET; + + if (hookCtx.result.emittedTxn.size() >= hookCtx.expected_etxn_count) + return TOO_MANY_EMITTED_TXN; + + ripple::Blob blob{memory + read_ptr, memory + read_ptr + read_len}; + + DBG_PRINTF("hook is emitting tx:-----\n"); + for (unsigned char c: blob) + DBG_PRINTF("%02X", c); + DBG_PRINTF("\n--------\n"); + + std::shared_ptr stpTrans; + try + { + stpTrans = std::make_shared(SerialIter { memory + read_ptr, read_len }); + } + catch (std::exception& e) + { + JLOG(j.trace()) + << "HookEmit[" << HC_ACC() << "]: Failed " << e.what() << "\n"; + return EMISSION_FAILURE; + } + + // check the emitted txn is valid + /* Emitted TXN rules + * 1. Sequence: 0 + * 2. PubSigningKey: 000000000000000 + * 3. sfEmitDetails present and valid + * 4. No sfSignature + * 5. LastLedgerSeq > current ledger, > firstledgerseq & LastLedgerSeq < seq + 5 + * 6. FirstLedgerSeq > current ledger + * 7. Fee must be correctly high + * 8. The generation cannot be higher than 10 + */ + + + // 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 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(*stpTrans).getField(sfEmitDetails).downcast(); + + if (!emitDetails.isFieldPresent(sfEmitGeneration) || + !emitDetails.isFieldPresent(sfEmitBurden) || + !emitDetails.isFieldPresent(sfEmitParentTxnID) || + !emitDetails.isFieldPresent(sfEmitNonce) || + !emitDetails.isFieldPresent(sfEmitCallback) || + !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); + auto const& callback = emitDetails.getAccountID(sfEmitCallback); + auto const& hash = emitDetails.getFieldH256(sfEmitHookHash); + + uint32_t gen_proper = etxn_generation(hookCtx, memoryCtx); + + 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, memoryCtx); + 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 (callback != hookCtx.result.account) + { + JLOG(j.trace()) + << "HookEmit[" << HC_ACC() << "]: sfEmitCallback account must be the account " + << "of the emitting hook"; + 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: sfSignature must be absent + if (stpTrans->isFieldPresent(sfSignature)) + { + JLOG(j.trace()) << + "HookEmit[" << HC_ACC() << "]: sfSignature is present but should not be"; + return EMISSION_FAILURE; + } + + // rule 5: LastLedgerSeq must be present and after current ledger + + uint32_t tx_lls = stpTrans->getFieldU32(sfLastLedgerSequence); + uint32_t ledgerSeq = applyCtx.app.getLedgerMaster().getValidLedgerIndex() + 1; + if (!stpTrans->isFieldPresent(sfLastLedgerSequence) || tx_lls < ledgerSeq + 1) + { + JLOG(j.trace()) + << "HookEmit[" << HC_ACC() << "]: sfLastLedgerSequence missing or invalid"; + return EMISSION_FAILURE; + } + + if (tx_lls > ledgerSeq + 5) + { + JLOG(j.trace()) + << "HookEmit[" << HC_ACC() << "]: sfLastLedgerSequence cannot be greater than current seq + 5"; + return EMISSION_FAILURE; + } + + // rule 6 + if (!stpTrans->isFieldPresent(sfFirstLedgerSequence) || + stpTrans->getFieldU32(sfFirstLedgerSequence) > tx_lls) + { + 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 + if (hookCtx.fee_base == 0) + hookCtx.fee_base = etxn_fee_base(hookCtx, memoryCtx, read_len); + + int64_t minfee = hookCtx.fee_base * hook_api::drops_per_byte * read_len; + if (minfee < 0 || hookCtx.fee_base < 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; + } + + std::string reason; + auto tpTrans = std::make_shared(stpTrans, reason, app); + if (tpTrans->getStatus() != NEW) + { + JLOG(j.trace()) + << "HookEmit[" << HC_ACC() << "]: tpTrans->getStatus() != NEW"; + return EMISSION_FAILURE; + } + + hookCtx.result.emittedTxn.push(tpTrans); + + 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; + + WRITE_WASM_MEMORY_AND_RETURN( + write_ptr, txID.size(), + txID.data(), txID.size(), + memory, memory_length); +} + +// When implemented will return the hash of the current hook +DEFINE_HOOK_FUNCTION( + int64_t, + hook_hash, + uint32_t write_ptr, uint32_t write_len, int32_t hook_no ) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + + if (write_len < 32) + return TOO_SMALL; + + if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length)) + return OUT_OF_BOUNDS; + + if (hook_no == -1) + { + + WRITE_WASM_MEMORY_AND_RETURN( + write_ptr, write_len, + hookCtx.result.hookHash.data(), 32, + memory, memory_length); + } + + std::shared_ptr hookSLE = applyCtx.view().peek(hookCtx.result.hookKeylet); + if (!hookSLE || !hookSLE->isFieldPresent(sfHooks)) + return INTERNAL_ERROR; + + ripple::STArray const& hooks = hookSLE->getFieldArray(sfHooks); + if (hook_no >= hooks.size()) + return DOESNT_EXIST; + + auto const& hook = hooks[hook_no]; + if (!hook.isFieldPresent(sfHookHash)) + return DOESNT_EXIST; + + ripple::uint256 const& hash = hook.getFieldH256(sfHookHash); + + WRITE_WASM_MEMORY_AND_RETURN( + write_ptr, write_len, + hash.data(), hash.size(), + memory, memory_length); +} + +// Write the account id that the running hook is installed on into write_ptr +DEFINE_HOOK_FUNCTION( + int64_t, + hook_account, + uint32_t write_ptr, uint32_t ptr_len ) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + if (NOT_IN_BOUNDS(write_ptr, 20, memory_length)) + return OUT_OF_BOUNDS; + + WRITE_WASM_MEMORY_AND_RETURN( + write_ptr, 20, + hookCtx.result.account.data(), 20, + memory, memory_length); +} + +// Deterministic nonces (can be called multiple times) +// Writes nonce into the write_ptr +DEFINE_HOOK_FUNCTION( + int64_t, + nonce, + uint32_t write_ptr, uint32_t write_len ) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx, view on current stack + + if (write_len < 32) + return TOO_SMALL; + + if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length)) + return OUT_OF_BOUNDS; + + if (hookCtx.nonce_counter > hook_api::max_nonce) + return TOO_MANY_NONCES; + + auto hash = ripple::sha512Half( + ripple::HashPrefix::emitTxnNonce, +// view.info().seq, + applyCtx.tx.getTransactionID(), + hookCtx.nonce_counter++, + hookCtx.result.account + ); + + hookCtx.nonce_used[hash] = true; + + WRITE_WASM_MEMORY_AND_RETURN( + write_ptr, 32, + hash.data(), 32, + memory, memory_length); + + return 32; +} + +// Reserve one or more transactions for emission from the running hook +DEFINE_HOOK_FUNCTION( + int64_t, + etxn_reserve, + uint32_t count ) +{ + + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + if (hookCtx.expected_etxn_count > -1) + return ALREADY_SET; + + if (count > hook_api::max_emit) + return TOO_BIG; + + hookCtx.expected_etxn_count = count; + + return count; +} + +// Compute the burden of an emitted transaction based on a number of factors +DEFINE_HOOK_FUNCNARG( + int64_t, + etxn_burden) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + + if (hookCtx.expected_etxn_count <= -1) + return PREREQUISITE_NOT_MET; + + uint64_t last_burden = (uint64_t)otxn_burden(hookCtx, memoryCtx); // always non-negative so cast is safe + + uint64_t burden = last_burden * hookCtx.expected_etxn_count; + if (burden < last_burden) // this overflow will never happen but handle it anyway + return FEE_TOO_LARGE; + + return burden; +} + +DEFINE_HOOK_FUNCTION( + int64_t, + util_sha512h, + 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, view on current stack + + if (write_len < 32) + return TOO_SMALL; + + if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length)) + return OUT_OF_BOUNDS; + + auto hash = ripple::sha512Half( + ripple::Slice { memory + read_ptr, read_len } + ); + + WRITE_WASM_MEMORY_AND_RETURN( + write_ptr, 32, + hash.data(), 32, + memory, memory_length); +} + + +// RH NOTE this is a light-weight stobject parsing function for drilling into a provided serialzied object +// however it could probably be replaced by an existing class or routine or set of routines in XRPLD +// Returns object length including header bytes (and footer bytes in the event of array or object) +// negative indicates error +// -1 = unexpected end of bytes +// -2 = unknown type (detected early) +// -3 = unknown type (end of function) +// -4 = excessive stobject nesting +// -5 = excessively large array or object +inline int32_t get_stobject_length ( + unsigned char* start, // in - begin iterator + unsigned char* maxptr, // in - end iterator + int& type, // out - populated by serialized type code + int& field, // out - populated by serialized field code + int& payload_start, // out - the start of actual payload data for this type + int& payload_length, // out - the length of actual payload data for this type + int recursion_depth = 0) // used internally +{ + if (recursion_depth > 10) + return -4; + + unsigned char* end = maxptr; + unsigned char* upto = start; + int high = *upto >> 4; + int low = *upto & 0xF; + + upto++; if (upto >= end) return -1; + if (high > 0 && low > 0) + { + // common type common field + type = high; + field = low; + } else if (high > 0) { + // common type, uncommon field + type = high; + field = *upto++; + } else if (low > 0) { + // common field, uncommon type + field = low; + type = *upto++; + } else { + // uncommon type and field + type = *upto++; + if (upto >= end) return -1; + field = *upto++; + } + + DBG_PRINTF("%d get_st_object found field %d type %d\n", recursion_depth, field, type); + + if (upto >= end) return -1; + + // RH TODO: link this to rippled's internal STObject constants + // E.g.: + /* + int field_code = (safe_cast(type) << 16) | field; + auto const& fieldObj = ripple::SField::getField; + */ + + if (type < 1 || type > 19 || ( type >= 9 && type <= 13)) + return -2; + + bool is_vl = (type == 8 /*ACCID*/ || type == 7 || type == 18 || type == 19); + + + int length = -1; + if (is_vl) + { + length = *upto++; + if (upto >= end) + return -1; + + if (length < 193) + { + // do nothing + } else if (length > 192 && length < 241) + { + length -= 193; + length *= 256; + length += *upto++ + 193; if (upto > end) return -1; + } else { + int b2 = *upto++; if (upto >= end) return -1; + length -= 241; + length *= 65536; + length += 12481 + (b2 * 256) + *upto++; if (upto >= end) return -1; + } + } else if ((type >= 1 && type <= 5) || type == 16 || type == 17 ) + { + length = (type == 1 ? 2 : + (type == 2 ? 4 : + (type == 3 ? 8 : + (type == 4 ? 16 : + (type == 5 ? 32 : + (type == 16 ? 1 : + (type == 17 ? 20 : -1 ))))))); + + } else if (type == 6) /* AMOUNT */ + { + length = (*upto >> 6 == 1) ? 8 : 48; + if (upto >= end) return -1; + } + + if (length > -1) + { + payload_start = upto - start; + payload_length = length; + DBG_PRINTF("%d get_stobject_length field: %d Type: %d VL: %s Len: %d Payload_Start: %d Payload_Len: %d\n", + recursion_depth, field, type, (is_vl ? "yes": "no"), length, payload_start, payload_length); + return length + (upto - start); + } + + if (type == 15 || type == 14) /* Object / Array */ + { + payload_start = upto - start; + + for(int i = 0; i < 1024; ++i) + { + int subfield = -1, subtype = -1, payload_start_ = -1, payload_length_ = -1; + int32_t sublength = get_stobject_length( + upto, end, subtype, subfield, payload_start_, payload_length_, recursion_depth + 1); + DBG_PRINTF("%d get_stobject_length i %d %d-%d, upto %d sublength %d\n", recursion_depth, i, + subtype, subfield, upto - start, sublength); + if (sublength < 0) + return -1; + upto += sublength; + if (upto >= end) + return -1; + + if ((*upto == 0xE1U && type == 0xEU) || + (*upto == 0xF1U && type == 0xFU)) + { + payload_length = upto - start - payload_start; + upto++; + return (upto - start); + } + } + return -5; + } + + return -3; + +} + +// Given an serialized object in memory locate and return the offset and length of the payload of a subfield of that +// object. Arrays are returned fully formed. If successful returns offset and length joined as int64_t. +// Use SUB_OFFSET and SUB_LENGTH to extract. +DEFINE_HOOK_FUNCTION( + int64_t, + sto_subfield, + uint32_t read_ptr, uint32_t read_len, uint32_t field_id ) +{ + + 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 (read_len < 1) + return TOO_SMALL; + + unsigned char* start = (unsigned char*)(memory + read_ptr); + unsigned char* upto = start; + unsigned char* end = start + read_len; + + DBG_PRINTF("sto_subfield called, looking for field %u type %u\n", field_id & 0xFFFF, (field_id >> 16)); + for (int j = -5; j < 5; ++j) + DBG_PRINTF(( j == 0 ? " >%02X< " : " %02X "), *(start + j)); + DBG_PRINTF("\n"); + +// if ((*upto & 0xF0) == 0xE0) +// upto++; + + for (int i = 0; i < 1024 && upto < end; ++i) + { + int type = -1, field = -1, payload_start = -1, payload_length = -1; + int32_t length = get_stobject_length(upto, end, type, field, payload_start, payload_length, 0); + if (length < 0) + return PARSE_ERROR; + if ((type << 16) + field == field_id) + { + DBG_PRINTF("sto_subfield returned for field %u type %u\n", field_id & 0xFFFF, (field_id >> 16)); + for (int j = -5; j < 5; ++j) + DBG_PRINTF(( j == 0 ? " [%02X] " : " %02X "), *(upto + j)); + DBG_PRINTF("\n"); + if (type == 0xF) // we return arrays fully formed + return (((int64_t)(upto - start)) << 32) /* start of the object */ + + (uint32_t)(length); + // return pointers to all other objects as payloads + return (((int64_t)(upto - start + payload_start)) << 32) /* start of the object */ + + (uint32_t)(payload_length); + } + upto += length; + } + + return DOESNT_EXIST; +} + +// Same as subfield but indexes into a serialized array +DEFINE_HOOK_FUNCTION( + int64_t, + sto_subarray, + uint32_t read_ptr, uint32_t read_len, uint32_t index_id ) +{ + + 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 (read_len < 1) + return TOO_SMALL; + + unsigned char* start = (unsigned char*)(memory + read_ptr); + unsigned char* upto = start; + unsigned char* end = start + read_len; + + if ((*upto & 0xF0) == 0xF0) + upto++; + + /* + DBG_PRINTF("sto_subarray called, looking for index %u\n", index_id); + for (int j = -5; j < 5; ++j) + printf(( j == 0 ? " >%02X< " : " %02X "), *(start + j)); + DBG_PRINTF("\n"); + */ + for (int i = 0; i < 1024 && upto < end; ++i) + { + int type = -1, field = -1, payload_start = -1, payload_length = -1; + int32_t length = get_stobject_length(upto, end, type, field, payload_start, payload_length, 0); + if (length < 0) + return PARSE_ERROR; + if (i == index_id) + { + DBG_PRINTF("sto_subarray returned for index %u\n", index_id); + for (int j = -5; j < 5; ++j) + DBG_PRINTF(( j == 0 ? " [%02X] " : " %02X "), *(upto + j + length)); + DBG_PRINTF("\n"); + + return (((int64_t)(upto - start)) << 32) /* start of the object */ + + (uint32_t)(length); + } + upto += length; + } + + return DOESNT_EXIST; +} + +// Convert an account ID into a base58-check encoded r-address +DEFINE_HOOK_FUNCTION( + int64_t, + util_raddr, + 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(write_ptr, write_len, memory_length)) + return OUT_OF_BOUNDS; + + if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length)) + return OUT_OF_BOUNDS; + + if (read_len != 20) + return INVALID_ARGUMENT; + + std::string raddr = base58EncodeToken(TokenType::AccountID, memory + read_ptr, read_len); + + if (write_len < raddr.size()) + return TOO_SMALL; + + WRITE_WASM_MEMORY_AND_RETURN( + write_ptr, write_len, + raddr.c_str(), raddr.size(), + memory, memory_length); +} + +// Convert a base58-check encoded r-address into a 20 byte account id +DEFINE_HOOK_FUNCTION( + int64_t, + util_accid, + 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(write_ptr, write_len, memory_length)) + return OUT_OF_BOUNDS; + + if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length)) + return OUT_OF_BOUNDS; + + if (write_len < 20) + return TOO_SMALL; + + if (read_len > 49) + return TOO_BIG; + + // RH TODO we shouldn't need to slice this input but the base58 routine fails if we dont... maybe + // some encoding or padding that shouldnt be there or maybe something that should be there + char buffer[50]; + for (int i = 0; i < read_len; ++i) + buffer[i] = *(memory + read_ptr + i); + buffer[read_len] = 0; + + std::string raddr{buffer}; + auto const result = decodeBase58Token(raddr, TokenType::AccountID); + if (result.empty()) + return INVALID_ARGUMENT; + + + WRITE_WASM_MEMORY_AND_RETURN( + write_ptr, write_len, + result.data(), 20, + memory, memory_length); +} + + +/** + * Inject a field into an sto if there is sufficient space + * Field must be fully formed and wrapped (NOT JUST PAYLOAD) + * sread - source object + * fread - field to inject + */ +DEFINE_HOOK_FUNCTION( + int64_t, + sto_emplace, + uint32_t write_ptr, uint32_t write_len, + uint32_t sread_ptr, uint32_t sread_len, + uint32_t fread_ptr, uint32_t fread_len, uint32_t field_id ) +{ + HOOK_SETUP(); + + if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length)) + return OUT_OF_BOUNDS; + + if (NOT_IN_BOUNDS(sread_ptr, sread_len, memory_length)) + return OUT_OF_BOUNDS; + + if (NOT_IN_BOUNDS(fread_ptr, fread_len, memory_length)) + return OUT_OF_BOUNDS; + + if (write_len < sread_len + fread_len) + return TOO_SMALL; + + // RH TODO: put these constants somewhere (votable?) + if (sread_len > 1024*16) + return TOO_BIG; + + if (fread_len > 4096) + return TOO_BIG; + + // we must inject the field at the canonical location.... + // so find that location + unsigned char* start = (unsigned char*)(memory + sread_ptr); + unsigned char* upto = start; + unsigned char* end = start + sread_len; + unsigned char* inject_start = end; + unsigned char* inject_end = end; + + DBG_PRINTF("sto_emplace called, looking for field %u type %u\n", field_id & 0xFFFF, (field_id >> 16)); + for (int j = -5; j < 5; ++j) + DBG_PRINTF(( j == 0 ? " >%02X< " : " %02X "), *(start + j)); + DBG_PRINTF("\n"); + + + for (int i = 0; i < 1024 && upto < end; ++i) + { + int type = -1, field = -1, payload_start = -1, payload_length = -1; + int32_t length = get_stobject_length(upto, end, type, field, payload_start, payload_length, 0); + if (length < 0) + return PARSE_ERROR; + if ((type << 16) + field == field_id) + { + inject_start = upto; + inject_end = upto + length; + break; + } + else if ((type << 16) + field > field_id) + { + inject_start = upto; + inject_end = upto; + break; + } + upto += length; + } + + // upto is injection point + int64_t bytes_written = 0; + + // part 1 + if (inject_start - start > 0) + { + WRITE_WASM_MEMORY( + bytes_written, + write_ptr, write_len, + start, (inject_start - start), + memory, memory_length); + } + + // write the field + WRITE_WASM_MEMORY( + bytes_written, + (write_ptr + bytes_written), (write_len - bytes_written), + memory + fread_ptr, fread_len, + memory, memory_length); + + + + // part 2 + if (end - inject_end > 0) + { + WRITE_WASM_MEMORY( + bytes_written, + (write_ptr + bytes_written), (write_len - bytes_written), + inject_end, (end - inject_end), + memory, memory_length); + } + return bytes_written; +} + +/** + * Remove a field from an sto if the field is present + */ +DEFINE_HOOK_FUNCTION( + int64_t, + sto_erase, + uint32_t write_ptr, uint32_t write_len, + uint32_t read_ptr, uint32_t read_len, uint32_t field_id ) +{ + HOOK_SETUP(); + + if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length)) + return OUT_OF_BOUNDS; + + if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length)) + return OUT_OF_BOUNDS; + + // RH TODO: constants + if (read_len > 16*1024) + return TOO_BIG; + + if (write_len < read_len) + return TOO_SMALL; + + unsigned char* start = (unsigned char*)(memory + read_ptr); + unsigned char* upto = start; + unsigned char* end = start + read_len; + unsigned char* erase_start = 0; + unsigned char* erase_end = 0; + + DBG_PRINTF("sto_erase called, looking for field %u type %u\n", field_id & 0xFFFF, (field_id >> 16)); + for (int j = -5; j < 5; ++j) + DBG_PRINTF(( j == 0 ? " >%02X< " : " %02X "), *(start + j)); + DBG_PRINTF("\n"); + + + for (int i = 0; i < 1024 && upto < end; ++i) + { + int type = -1, field = -1, payload_start = -1, payload_length = -1; + int32_t length = get_stobject_length(upto, end, type, field, payload_start, payload_length, 0); + if (length < 0) + return PARSE_ERROR; + if ((type << 16) + field == field_id) + { + erase_start = upto; + erase_end = upto + length; + } + upto += length; + } + + + if (erase_start >= start && erase_end >= start && erase_start <= end && erase_end <= end) + { + // do erasure via selective copy + int64_t bytes_written = 0; + + // part 1 + if (erase_start - start > 0) + WRITE_WASM_MEMORY( + bytes_written, + write_ptr, write_len, + start, (erase_start - start), + memory, memory_length); + + // skip the field we're erasing + + // part 2 + if (end - erase_end > 0) + WRITE_WASM_MEMORY( + bytes_written, + (write_ptr + bytes_written), (write_len - bytes_written), + erase_end, (end - erase_end), + memory, memory_length); + return bytes_written; + } + return DOESNT_EXIST; + +} + + + + +DEFINE_HOOK_FUNCTION( + int64_t, + sto_validate, + uint32_t read_ptr, uint32_t read_len ) +{ + + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + + // RH TODO: see if an internal ripple function/class would do this better + + if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length)) + return OUT_OF_BOUNDS; + + if (read_len < 1) + return TOO_SMALL; + + unsigned char* start = (unsigned char*)(memory + read_ptr); + unsigned char* upto = start; + unsigned char* end = start + read_len; + + + for (int i = 0; i < 1024 && upto < end; ++i) + { + int type = -1, field = -1, payload_start = -1, payload_length = -1; + int32_t length = get_stobject_length(upto, end, type, field, payload_start, payload_length, 0); + if (length < 0) + return 0; + upto += length; + } + + return 1; +} + + + +// Validate either an secp256k1 signature or an ed25519 signature, using the XRPLD convention for identifying +// the key type. Pointer prefixes: d = data, s = signature, k = public key. +DEFINE_HOOK_FUNCTION( + int64_t, + util_verify, + uint32_t dread_ptr, uint32_t dread_len, + uint32_t sread_ptr, uint32_t sread_len, + uint32_t kread_ptr, uint32_t kread_len ) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + + if (NOT_IN_BOUNDS(dread_ptr, dread_len, memory_length) || + NOT_IN_BOUNDS(sread_ptr, sread_len, memory_length) || + NOT_IN_BOUNDS(kread_ptr, kread_len, memory_length)) + return OUT_OF_BOUNDS; + + ripple::Slice keyslice {reinterpret_cast(kread_ptr + memory), kread_len}; + ripple::Slice data {reinterpret_cast(dread_ptr + memory), dread_len}; + ripple::Slice sig {reinterpret_cast(sread_ptr + memory), sread_len}; + ripple::PublicKey key { keyslice }; + return verify(key, data, sig, false) ? 1 : 0; +} + +// Return the current fee base of the current ledger (multiplied by a margin) +DEFINE_HOOK_FUNCNARG( + int64_t, + fee_base) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + return (int64_t)((double)(view.fees().base.drops()) * hook_api::fee_base_multiplier); +} + +// Return the fee base for a hypothetically emitted transaction from the current hook based on byte count +DEFINE_HOOK_FUNCTION( + int64_t, + etxn_fee_base, + uint32_t tx_byte_count ) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + + if (hookCtx.expected_etxn_count <= -1) + return PREREQUISITE_NOT_MET; + + uint64_t base_fee = (uint64_t)fee_base(hookCtx, memoryCtx); // will always return non-negative + + int64_t burden = etxn_burden(hookCtx, memoryCtx); + if (burden < 1) + return FEE_TOO_LARGE; + + uint64_t fee = base_fee * burden; + if (fee < burden || fee & (3ULL << 62)) // a second under flow to handle + return FEE_TOO_LARGE; + + hookCtx.fee_base = fee; + + return fee * hook_api::drops_per_byte * tx_byte_count; +} + +// Populate an sfEmitDetails field in a soon-to-be emitted transaction +DEFINE_HOOK_FUNCTION( + int64_t, + etxn_details, + uint32_t write_ptr, uint32_t write_len ) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length)) + return OUT_OF_BOUNDS; + + if (write_len < hook_api::etxn_details_size) + return TOO_SMALL; + + if (hookCtx.expected_etxn_count <= -1) + return PREREQUISITE_NOT_MET; + + uint32_t generation = (uint32_t)(etxn_generation(hookCtx, memoryCtx)); // always non-negative so cast is safe + + int64_t burden = etxn_burden(hookCtx, memoryCtx); + if (burden < 1) + return FEE_TOO_LARGE; + + unsigned char* out = memory + write_ptr; + + *out++ = 0xECU; // begin sfEmitDetails /* upto = 0 | size = 1 */ + *out++ = 0x20U; // sfEmitGeneration preamble /* upto = 1 | size = 6 */ + *out++ = 0x2BU; // preamble cont + *out++ = ( generation >> 24 ) & 0xFFU; + *out++ = ( generation >> 16 ) & 0xFFU; + *out++ = ( generation >> 8 ) & 0xFFU; + *out++ = ( generation >> 0 ) & 0xFFU; + *out++ = 0x3C; // sfEmitBurden preamble /* upto = 7 | size = 9 */ + *out++ = ( burden >> 56 ) & 0xFFU; + *out++ = ( burden >> 48 ) & 0xFFU; + *out++ = ( burden >> 40 ) & 0xFFU; + *out++ = ( burden >> 32 ) & 0xFFU; + *out++ = ( burden >> 24 ) & 0xFFU; + *out++ = ( burden >> 16 ) & 0xFFU; + *out++ = ( burden >> 8 ) & 0xFFU; + *out++ = ( burden >> 0 ) & 0xFFU; + *out++ = 0x5A; // sfEmitParentTxnID preamble /* upto = 16 | size = 33 */ + if (otxn_id(hookCtx, memoryCtx, out - memory, 32, 1) != 32) + return INTERNAL_ERROR; + out += 32; + *out++ = 0x5B; // sfEmitNonce /* upto = 49 | size = 33 */ + if (nonce(hookCtx, memoryCtx, out - memory, 32) != 32) + return INTERNAL_ERROR; + out += 32; + *out++= 0x5C; // sfEmitHookHash preamble /* upto = 82 | size = 33 */ + for (int i = 0; i < 32; ++i) + *out++ = hookCtx.result.hookHash.data()[i]; + *out++ = 0x89; // sfEmitCallback preamble /* upto = 115 | size = 22 */ + *out++ = 0x14; // preamble cont + if (hook_account(hookCtx, memoryCtx, out - memory, 20) != 20) + return INTERNAL_ERROR; + out += 20; + *out++ = 0xE1U; // end object (sfEmitDetails) /* upto = 137 | size = 1 */ + /* upto = 138 | --------- */ + DBG_PRINTF("emitdetails size = %d\n", (out - memory - write_ptr)); + return 138; +} + + +// RH TODO: bill based on guard counts +// Guard function... very important. Enforced on SetHook transaction, keeps track of how many times a +// runtime loop iterates and terminates the hook if the iteration count rises above a preset number of iterations +// as determined by the hook developer +DEFINE_HOOK_FUNCTION( + int32_t, + _g, + uint32_t id, uint32_t maxitr ) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + if (hookCtx.guard_map.find(id) == hookCtx.guard_map.end()) + hookCtx.guard_map[id] = 1; + else + hookCtx.guard_map[id]++; + + if (hookCtx.guard_map[id] > maxitr) + { + if (id > 0xFFFFU) + { + JLOG(j.trace()) + << "HookInfo[" << HC_ACC() << "]: Macro guard violation. " + << "Src line: " << (id & 0xFFFFU) << " " + << "Macro line: " << (id >> 16) << " " + << "Iterations: " << hookCtx.guard_map[id]; + } + else + { + JLOG(j.trace()) + << "HookInfo[" << HC_ACC() << "]: Guard violation. " + << "Src line: " << id + << "Iterations: " << hookCtx.guard_map[id]; + } + hookCtx.result.exitType = hook_api::ExitType::ROLLBACK; + hookCtx.result.exitCode = GUARD_VIOLATION; + return RC_ROLLBACK; + } + return 1; +} + +#define RETURN_IF_INVALID_FLOAT(float1)\ +{\ + if (float1 < 0) return hook_api::INVALID_FLOAT;\ + if (float1 != 0)\ + {\ + int64_t mantissa = get_mantissa(float1);\ + int32_t exponent = get_exponent(float1);\ + if (mantissa < minMantissa ||\ + mantissa > maxMantissa ||\ + exponent > maxExponent ||\ + exponent < minExponent)\ + return INVALID_FLOAT;\ + }\ +} + + +DEFINE_HOOK_FUNCTION( + int64_t, + trace_float, + uint32_t read_ptr, uint32_t read_len, + int64_t float1) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx on current stack + if (!j.trace()) + return 0; + + if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length)) + return OUT_OF_BOUNDS; + + if (float1 == 0) + RETURN_HOOK_TRACE(read_ptr, read_len, "Float 0*10^(0) "); + + int64_t man = get_mantissa(float1); + int32_t exp = get_exponent(float1); + bool neg = is_negative(float1); + if (man < minMantissa || man > maxMantissa || exp < minExponent || exp > maxExponent) + RETURN_HOOK_TRACE(read_ptr, read_len, "Float "); + + man *= (neg ? -1 : 1); + + RETURN_HOOK_TRACE(read_ptr, read_len, "Float " << man << "*10^(" << exp << ")"); +} + +DEFINE_HOOK_FUNCTION( + int64_t, + float_set, + int32_t exp, int64_t mantissa ) +{ + if (mantissa == 0) + return 0; + + // normalize + while (mantissa < minMantissa) + { + mantissa *= 10; + exp--; + if (exp < minExponent) + return INVALID_FLOAT; //underflow + } + while (mantissa > maxMantissa) + { + mantissa /= 10; + exp++; + if (exp > maxExponent) + return INVALID_FLOAT; //overflow + } + + return make_float(mantissa, exp); +} + +// https://stackoverflow.com/questions/31652875/fastest-way-to-multiply-two-64-bit-ints-to-128-bit-then-to-64-bit +inline void umul64wide (uint64_t a, uint64_t b, uint64_t *hi, uint64_t *lo) +{ + uint64_t a_lo = (uint64_t)(uint32_t)a; + uint64_t a_hi = a >> 32; + uint64_t b_lo = (uint64_t)(uint32_t)b; + uint64_t b_hi = b >> 32; + + uint64_t p0 = a_lo * b_lo; + uint64_t p1 = a_lo * b_hi; + uint64_t p2 = a_hi * b_lo; + uint64_t p3 = a_hi * b_hi; + + uint32_t cy = (uint32_t)(((p0 >> 32) + (uint32_t)p1 + (uint32_t)p2) >> 32); + + *lo = p0 + (p1 << 32) + (p2 << 32); + *hi = p3 + (p1 >> 32) + (p2 >> 32) + cy; +} + +inline int64_t mulratio_internal + (int64_t& man1, int32_t& exp1, bool round_up, uint32_t numerator, uint32_t denominator) +{ + try + { + ripple::IOUAmount amt {man1, exp1}; + ripple::IOUAmount out = ripple::mulRatio(amt, numerator, denominator, round_up != 0); // already normalized + man1 = out.mantissa(); + exp1 = out.exponent(); + return 1; + } + catch (std::overflow_error& e) + { + return OVERFLOW; + } +} + +inline int64_t float_multiply_internal_parts( + uint64_t man1, + int32_t exp1, + bool neg1, + uint64_t man2, + int32_t exp2, + bool neg2) +{ + int32_t exp_out = exp1 + exp2; + + // multiply the mantissas, this could result in upto a 128 bit number, represented as high and low here + uint64_t man_hi = 0, man_lo = 0; + umul64wide(man1, man2, &man_hi, &man_lo); + + // normalize our double wide mantissa by shifting bits under man_hi is 0 + uint8_t man_shifted = 0; + while (man_hi > 0) + { + bool set = (man_hi & 1) != 0; + man_hi >>= 1; + man_lo >>= 1; + man_lo += (set ? (1ULL<<63U) : 0); + man_shifted++; + } + + // we shifted the mantissa by man_shifted bits, which equates to a division by 2^man_shifted + // now shift into the normalized range + while (man_lo > maxMantissa) + { + if (exp_out > maxExponent) + return OVERFLOW; + man_lo /= 10; + exp_out++; + } + + // we can adjust for the bitshifting by doing upto two smaller multiplications now + neg1 = (neg1 && !neg2) || (!neg1 && neg2); + int64_t man_out = (neg1 ? -1 : 1) * ((int64_t)(man_lo)); + if (man_shifted > 32) + { + man_shifted -=32; + if (mulratio_internal(man_out, exp_out, false, 0xFFFFFFFFU, 1) < 0) + return OVERFLOW; + } + + if (mulratio_internal(man_out, exp_out, false, 1U << man_shifted, 1) < 0) + return OVERFLOW; + + // now we have our product + return make_float(man_out, exp_out); +} + +DEFINE_HOOK_FUNCTION( + int64_t, + float_int, + int64_t float1, + uint32_t decimal_places, + uint32_t absolute) +{ + RETURN_IF_INVALID_FLOAT(float1); + if (float1 == 0) return 0; + uint64_t man1 = get_mantissa(float1); + int32_t exp1 = get_exponent(float1); + bool neg1 = is_negative(float1); + + if (decimal_places > 15) + return INVALID_ARGUMENT; + + if (neg1 && !absolute) + return CANT_RETURN_NEGATIVE; + + + while (exp1 > -decimal_places) + { + man1 *= 10; + exp1--; + } + + while (exp1 < -decimal_places) + { + man1 /= 10; + exp1++; + } + if (((int64_t)(man1)) < man1) + return INVALID_FLOAT; + + return man1; + +} + + +DEFINE_HOOK_FUNCTION( + int64_t, + float_multiply, + int64_t float1, int64_t float2 ) +{ + RETURN_IF_INVALID_FLOAT(float1); + RETURN_IF_INVALID_FLOAT(float2); + + if (float1 == 0 || float2 == 0) return 0; + + uint64_t man1 = get_mantissa(float1); + int32_t exp1 = get_exponent(float1); + bool neg1 = is_negative(float1); + uint64_t man2 = get_mantissa(float2); + int32_t exp2 = get_exponent(float2); + bool neg2 = is_negative(float2); + + + return float_multiply_internal_parts(man1, exp1, neg1, man2, exp2, neg2); +} + + +DEFINE_HOOK_FUNCTION( + int64_t, + float_mulratio, + int64_t float1, uint32_t round_up, + uint32_t numerator, uint32_t denominator ) +{ + RETURN_IF_INVALID_FLOAT(float1); + if (float1 == 0) + return 0; + if (denominator == 0) + return DIVISION_BY_ZERO; + + int64_t man1 = (int64_t)(get_mantissa(float1)) * (is_negative(float1) ? -1 : 1); + int32_t exp1 = get_exponent(float1); + + if (mulratio_internal(man1, exp1, round_up > 0, numerator, denominator) < 0) + return OVERFLOW; + + return make_float(man1, exp1); +} + +DEFINE_HOOK_FUNCTION( + int64_t, + float_negate, + int64_t float1 ) +{ + if (float1 == 0) return 0; + RETURN_IF_INVALID_FLOAT(float1); + return hook_float::invert_sign(float1); +} + +DEFINE_HOOK_FUNCTION( + int64_t, + float_compare, + int64_t float1, int64_t float2, uint32_t mode) +{ + RETURN_IF_INVALID_FLOAT(float1); + RETURN_IF_INVALID_FLOAT(float2); + + bool equal_flag = mode & compare_mode::EQUAL; + bool less_flag = mode & compare_mode::LESS; + bool greater_flag = mode & compare_mode::GREATER; + bool not_equal = less_flag && greater_flag; + + if ((equal_flag && less_flag && greater_flag) || mode == 0) + return INVALID_ARGUMENT; + + try + { + int64_t man1 = (int64_t)(get_mantissa(float1)) * (is_negative(float1) ? -1 : 1); + int32_t exp1 = get_exponent(float1); + ripple::IOUAmount amt1 {man1, exp1}; + int64_t man2 = (int64_t)(get_mantissa(float2)) * (is_negative(float2) ? -1 : 1); + int32_t exp2 = get_exponent(float2); + ripple::IOUAmount amt2 {man2, exp2}; + + if (not_equal && amt1 != amt2) + return 1; + + if (equal_flag && amt1 == amt2) + return 1; + + if (greater_flag && amt1 > amt2) + return 1; + + if (less_flag && amt1 < amt2) + return 1; + + return 0; + } + catch (std::overflow_error& e) + { + return OVERFLOW; + } + +} + +DEFINE_HOOK_FUNCTION( + int64_t, + float_sum, + int64_t float1, int64_t float2) +{ + RETURN_IF_INVALID_FLOAT(float1); + RETURN_IF_INVALID_FLOAT(float2); + + if (float1 == 0) return float2; + if (float2 == 0) return float1; + + int64_t man1 = (int64_t)(get_mantissa(float1)) * (is_negative(float1) ? -1 : 1); + int32_t exp1 = get_exponent(float1); + int64_t man2 = (int64_t)(get_mantissa(float2)) * (is_negative(float2) ? -1 : 1); + int32_t exp2 = get_exponent(float2); + + try + { + ripple::IOUAmount amt1 {man1, exp1}; + ripple::IOUAmount amt2 {man2, exp2}; + amt1 += amt2; + return make_float(amt1); + } + catch (std::overflow_error& e) + { + return OVERFLOW; + } +} + +DEFINE_HOOK_FUNCTION( + int64_t, + float_sto, + uint32_t write_ptr, uint32_t write_len, + uint32_t cread_ptr, uint32_t cread_len, + uint32_t iread_ptr, uint32_t iread_len, + int64_t float1, uint32_t field_code) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + RETURN_IF_INVALID_FLOAT(float1); + + uint16_t field = field_code & 0xFFFFU; + uint16_t type = field_code >> 16U; + + bool is_xrp = field_code == 0; + bool is_short = field_code == 0xFFFFFFFFU; // non-xrp value but do not output header or tail, just amount + + int bytes_needed = 8 + + ( field == 0 && type == 0 ? 0 : + ( field == 0xFFFFU && type == 0xFFFFU ? 0 : + ( field < 16 && type < 16 ? 1 : + ( field >= 16 && type < 16 ? 2 : + ( field < 16 && type >= 16 ? 2 : 3 ))))); + + int64_t bytes_written = 0; + + if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length)) + return OUT_OF_BOUNDS; + + if (!is_xrp && !is_short && (cread_ptr == 0 && cread_len == 0 && iread_ptr == 0 && iread_len == 0)) + return INVALID_ARGUMENT; + + if (!is_xrp && !is_short) + { + if (NOT_IN_BOUNDS(cread_ptr, cread_len, memory_length) || + NOT_IN_BOUNDS(iread_ptr, iread_len, memory_length)) + return OUT_OF_BOUNDS; + + if (cread_len != 20 || iread_len != 20) + return INVALID_ARGUMENT; + + bytes_needed += 40; + + } + + if (bytes_needed > write_len) + return TOO_SMALL; + + if (is_xrp || is_short) + { + // do nothing + } + else if (field < 16 && type < 16) + { + *(memory + write_ptr) = (((uint8_t)type) << 4U) + ((uint8_t)field); + bytes_written++; + } + else if (field >= 16 && type < 16) + { + *(memory + write_ptr) = (((uint8_t)type) << 4U); + *(memory + write_ptr + 1) = ((uint8_t)field); + bytes_written += 2; + } + else if (field < 16 && type >= 16) + { + *(memory + write_ptr) = (((uint8_t)field) << 4U); + *(memory + write_ptr + 1) = ((uint8_t)type); + bytes_written += 2; + } + else + { + *(memory + write_ptr) = 0; + *(memory + write_ptr + 1) = ((uint8_t)type); + *(memory + write_ptr + 2) = ((uint8_t)field); + bytes_written += 3; + } + + uint64_t man = get_mantissa(float1); + int32_t exp = get_exponent(float1); + bool neg = is_negative(float1); + uint8_t out[8]; + if (is_xrp) + { + // we need to normalize to exp -6 + while (exp < -6) + { + man /= 10; + exp++; + } + + while (exp > -6) + { + man *= 10; + exp--; + } + + out[0] = (neg ? 0b00000000U : 0b01000000U); + out[0] += (uint8_t)((man >> 56U) & 0b111111U); + out[1] = (uint8_t)((man >> 48U) & 0xFF); + out[2] = (uint8_t)((man >> 40U) & 0xFF); + out[3] = (uint8_t)((man >> 32U) & 0xFF); + out[4] = (uint8_t)((man >> 24U) & 0xFF); + out[5] = (uint8_t)((man >> 16U) & 0xFF); + out[6] = (uint8_t)((man >> 8U) & 0xFF); + out[7] = (uint8_t)((man >> 0U) & 0xFF); + + } + else if (man == 0) + { + out[0] = 0b11000000U; + for (int i = 1; i < 8; ++i) + out[i] = 0; + } + else + { + + exp += 97; + + /// encode the rippled floating point sto format + + out[0] = (neg ? 0b10000000U : 0b11000000U); + out[0] += (uint8_t)(exp >> 2U); + out[1] = ((uint8_t)(exp & 0b11U)) << 6U; + out[1] += (((uint8_t)(man >> 48U)) & 0b111111U); + out[2] = (uint8_t)((man >> 40U) & 0xFFU); + out[3] = (uint8_t)((man >> 32U) & 0xFFU); + out[4] = (uint8_t)((man >> 24U) & 0xFFU); + out[5] = (uint8_t)((man >> 16U) & 0xFFU); + out[6] = (uint8_t)((man >> 8U) & 0xFFU); + out[7] = (uint8_t)((man >> 0U) & 0xFFU); + } + + WRITE_WASM_MEMORY( + bytes_written, + write_ptr + bytes_written, write_len - bytes_written, + out, 8, + memory, memory_length); + + if (!is_xrp && !is_short) + { + WRITE_WASM_MEMORY( + bytes_written, + write_ptr + bytes_written, write_len - bytes_written, + memory + cread_ptr, 20, + memory, memory_length); + + WRITE_WASM_MEMORY( + bytes_written, + write_ptr + bytes_written, write_len - bytes_written, + memory + iread_ptr, 20, + memory, memory_length); + } + + return bytes_written; + +} + +DEFINE_HOOK_FUNCTION( + int64_t, + float_sto_set, + uint32_t read_ptr, uint32_t read_len ) +{ + + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + + if (read_len < 8) + return NOT_AN_OBJECT; + + if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length)) + return OUT_OF_BOUNDS; + + uint8_t* upto = memory + read_ptr; + + if (read_len > 8) + { + uint8_t hi = memory[read_ptr] >> 4U; + uint8_t lo = memory[read_ptr] & 0xFU; + + if (hi == 0 && lo == 0) + { + // typecode >= 16 && fieldcode >= 16 + if (read_len < 11) + return NOT_AN_OBJECT; + upto += 3; + } + else if (hi == 0 || lo == 0) + { + // typecode >= 16 && fieldcode < 16 + if (read_len < 10) + return NOT_AN_OBJECT; + upto += 2; + } + else + { + // typecode < 16 && fieldcode < 16 + upto++; + } + } + + + bool is_negative = (((*upto) & 0b01000000U) == 0); + int32_t exponent = (((*upto++) & 0b00111111U)) << 2U; + exponent += ((*upto)>>6U); + exponent -= 97; + uint64_t mantissa = (((uint64_t)(*upto++)) & 0b00111111U) << 48U; + mantissa += ((uint64_t)*upto++) << 40U; + mantissa += ((uint64_t)*upto++) << 32U; + mantissa += ((uint64_t)*upto++) << 24U; + mantissa += ((uint64_t)*upto++) << 16U; + mantissa += ((uint64_t)*upto++) << 8U; + mantissa += ((uint64_t)*upto++); + + if (mantissa == 0) + return 0; + + return hook_float::float_set(exponent, (is_negative ? -1 : 1) * ((int64_t)(mantissa))); +} +inline int64_t float_divide_internal(int64_t float1, int64_t float2) +{ + RETURN_IF_INVALID_FLOAT(float1); + RETURN_IF_INVALID_FLOAT(float2); + if (float2 == 0) + return DIVISION_BY_ZERO; + if (float1 == 0) + return 0; + + uint64_t man1 = get_mantissa(float1); + int32_t exp1 = get_exponent(float1); + bool neg1 = is_negative(float1); + uint64_t man2 = get_mantissa(float2); + int32_t exp2 = get_exponent(float2); + bool neg2 = is_negative(float2); + + while (man1 > maxMantissa) + { + man1 /= 10; + exp1++; + if (exp1 > maxExponent) + return INVALID_FLOAT; + } + + while (man1 < minMantissa) + { + man1 *= 10; + exp1--; + if (exp1 < minExponent) + return 0; + } + + + while (man2 > man1) + { + man2 /= 10; + exp2++; + } + + if (man2 == 0) + return DIVISION_BY_ZERO; + + while (man2 < man1) + { + if (man2*10 > man1) + break; + man2 *= 10; + exp2--; + } + + uint64_t man3 = 0; + int32_t exp3 = exp1 - exp2; + while (man2 > 0) + { + int i = 0; + for (; man1 > man2; man1 -= man2, ++i); + + man3 *= 10; + man3 += i; + man2 /= 10; + if (man2 == 0) + break; + exp3--; + } + + // normalize + while (man3 < minMantissa) + { + man3 *= 10; + exp3--; + if (exp3 < minExponent) + return 0; + } + + while (man3 > maxMantissa) + { + man3 /= 10; + exp3++; + if (exp3 > maxExponent) + return INVALID_FLOAT; + } + + bool neg3 = !((neg1 && neg2) || (!neg1 && !neg2)); + int64_t float_out = set_sign(0, neg3); + float_out = set_exponent(float_out, exp3); + float_out = set_mantissa(float_out, man3); + return float_out; + +} +DEFINE_HOOK_FUNCTION( + int64_t, + float_divide, + int64_t float1, int64_t float2 ) +{ + return float_divide_internal(float1, float2); +} +const int64_t float_one_internal = make_float(1000000000000000ull, -15); + + +DEFINE_HOOK_FUNCTION( + int64_t, + float_sign_set, + int64_t float1, uint32_t negative) +{ + RETURN_IF_INVALID_FLOAT(float1); + if (float1 == 0) + return 0; + return set_sign(float1, negative != 0); +} +DEFINE_HOOK_FUNCNARG( + int64_t, + float_one) +{ + return float_one_internal; +} + +DEFINE_HOOK_FUNCTION( + int64_t, + float_invert, + int64_t float1 ) +{ + if (float1 == 0) + return DIVISION_BY_ZERO; + return float_divide_internal(float_one_internal, float1); +} +DEFINE_HOOK_FUNCTION( + int64_t, + float_exponent, + int64_t float1 ) +{ + RETURN_IF_INVALID_FLOAT(float1); + if (float1 == 0) + return 0; + return get_exponent(float1); +} + +DEFINE_HOOK_FUNCTION( + int64_t, + float_mantissa, + int64_t float1 ) +{ + RETURN_IF_INVALID_FLOAT(float1); + if (float1 == 0) + return 0; + return get_mantissa(float1); +} + +DEFINE_HOOK_FUNCTION( + int64_t, + float_sign, + int64_t float1 ) +{ + RETURN_IF_INVALID_FLOAT(float1); + if (float1 == 0) + return 0; + return is_negative(float1); +} + +DEFINE_HOOK_FUNCTION( + int64_t, + float_exponent_set, + int64_t float1, int32_t exponent ) +{ + RETURN_IF_INVALID_FLOAT(float1); + if (float1 == 0) + return 0; + return set_exponent(float1, exponent); +} + +DEFINE_HOOK_FUNCTION( + int64_t, + float_mantissa_set, + int64_t float1, int64_t mantissa ) +{ + RETURN_IF_INVALID_FLOAT(float1); + if (mantissa == 0) + return 0; + return set_mantissa(float1, mantissa); +} + +DEFINE_HOOK_FUNCTION( + int64_t, + hook_param, + 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 (read_len < 1) + return TOO_SMALL; + + if (read_len > 32) + return TOO_BIG; + + std::vector paramName { read_ptr + memory, read_ptr + read_len + memory }; + + // first check for overrides set by prior hooks in the chain + auto const& overrides = hookCtx.result.hookParamOverrides; + if (overrides.find(hookCtx.result.hookHash) != overrides.end()) + { + auto const& params = overrides.at(hookCtx.result.hookHash); + if (params.find(paramName) != params.end()) + { + auto const& param = params.at(paramName); + if (param.size() == 0) + return DOESNT_EXIST; // allow overrides to "delete" parameters + + WRITE_WASM_MEMORY_AND_RETURN( + write_ptr, write_len, + param.data(), param.size(), + memory, memory_length); + } + } + + // next check if there's a param set on this hook + auto const& params = hookCtx.result.hookParams; + if (params.find(paramName) != params.end()) + { + auto const& param = params.at(paramName); + if (param.size() == 0) + return DOESNT_EXIST; + + WRITE_WASM_MEMORY_AND_RETURN( + write_ptr, write_len, + param.data(), param.size(), + memory, memory_length); + } + + return DOESNT_EXIST; +} + + +DEFINE_HOOK_FUNCTION( + int64_t, + hook_param_set, + uint32_t read_ptr, uint32_t read_len, + uint32_t kread_ptr, uint32_t kread_len, + uint32_t hread_ptr, uint32_t hread_len) +{ + HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, hookCtx on current stack + + if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length) || + NOT_IN_BOUNDS(kread_ptr, kread_len, memory_length)) + return OUT_OF_BOUNDS; + + if (kread_len < 1) + return TOO_SMALL; + + if (kread_len > 32) + return TOO_BIG; + + if (hread_len != 32) + return INVALID_ARGUMENT; + + if (read_len > hook::maxHookParameterSize()) + return TOO_BIG; + + std::vector paramName { kread_ptr + memory, kread_ptr + kread_len + memory }; + std::vector paramValue { read_ptr + memory, read_ptr + read_len + memory }; + + ripple::uint256 hash = ripple::uint256::fromVoid(memory + hread_ptr); + + if (hookCtx.result.overrideCount >= hook_api::max_params) + return TOO_MANY_PARAMS; + + hookCtx.result.overrideCount++; + + auto& overrides = hookCtx.result.hookParamOverrides; + if (overrides.find(hash) == overrides.end()) + { + overrides[hash] = + std::map, std::vector> + { + { + std::move(paramName), + std::move(paramValue) + } + }; + } + else + overrides[hash][std::move(paramName)] = std::move(paramValue); + + return read_len; +} + +DEFINE_HOOK_FUNCTION( + int64_t, + hook_skip, + uint32_t read_ptr, uint32_t read_len, uint32_t flags) +{ + 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 (read_len != 32) + return INVALID_ARGUMENT; + + auto& skips = hookCtx.result.hookSkips; + ripple::uint256 hash = ripple::uint256::fromVoid(memory + read_ptr); + + if (flags == 1) + { + // delete flag + if (skips.find(hash) == skips.end()) + return DOESNT_EXIST; + skips.erase(hash); + return 1; + } + + // first check if it's already in the skips set + if (skips.find(hash) != skips.end()) + return 1; + + // next check if it's even in this chain + std::shared_ptr hookSLE = applyCtx.view().peek(hookCtx.result.hookKeylet); + + if (!hookSLE || !hookSLE->isFieldPresent(sfHooks)) + return INTERNAL_ERROR; + + ripple::STArray const& hooks = hookSLE->getFieldArray(sfHooks); + bool found = false; + for (auto const& hook : hooks) + { + auto const& hookObj = dynamic_cast(&hook); + if (hookObj->isFieldPresent(sfHookHash)) + { + if (hookObj->getFieldH256(sfHookHash) == hash) + { + found = true; + break; + } + } + } + + if (!found) + return DOESNT_EXIST; + + // finally add it to the skips list + hookCtx.result.hookSkips.emplace(hash); + return 1; + +} + +DEFINE_HOOK_FUNCNARG( + int64_t, + hook_pos) +{ + return hookCtx.result.hookChainPosition; +} + diff --git a/src/ripple/app/tx/impl/applySteps.cpp b/src/ripple/app/tx/impl/applySteps.cpp index c70ac96d7..bae4485f8 100644 --- a/src/ripple/app/tx/impl/applySteps.cpp +++ b/src/ripple/app/tx/impl/applySteps.cpp @@ -35,6 +35,7 @@ #include #include #include +#include namespace ripple { @@ -131,7 +132,10 @@ invoke_preflight(PreflightContext const& ctx) case ttAMENDMENT: case ttFEE: case ttUNL_MODIFY: + case ttEMIT_FAILURE: return invoke_preflight_helper(ctx); + case ttHOOK_SET: + return invoke_preflight_helper(ctx); default: assert(false); return {temUNKNOWN, TxConsequences{temUNKNOWN}}; @@ -146,6 +150,7 @@ template static TER invoke_preclaim(PreclaimContext const& ctx) { + // If the transactor requires a valid account and the transaction doesn't // list one, preflight will have already a flagged a failure. auto const id = ctx.tx.getAccountID(sfAccount); @@ -219,9 +224,12 @@ invoke_preclaim(PreclaimContext const& ctx) return invoke_preclaim(ctx); case ttTRUST_SET: return invoke_preclaim(ctx); + case ttHOOK_SET: + return invoke_preclaim(ctx); case ttAMENDMENT: case ttFEE: case ttUNL_MODIFY: + case ttEMIT_FAILURE: return invoke_preclaim(ctx); default: assert(false); @@ -272,9 +280,12 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx) return CreateTicket::calculateBaseFee(view, tx); case ttTRUST_SET: return SetTrust::calculateBaseFee(view, tx); + case ttHOOK_SET: + return SetHook::calculateBaseFee(view, tx); case ttAMENDMENT: case ttFEE: case ttUNL_MODIFY: + case ttEMIT_FAILURE: return Change::calculateBaseFee(view, tx); default: assert(false); @@ -324,6 +335,7 @@ TxConsequences::TxConsequences(STTx const& tx, std::uint32_t sequencesConsumed) static std::pair invoke_apply(ApplyContext& ctx) { + switch (ctx.tx.getTxnType()) { case ttACCOUNT_DELETE: { @@ -402,9 +414,14 @@ invoke_apply(ApplyContext& ctx) SetTrust p(ctx); return p(); } + case ttHOOK_SET: { + SetHook p(ctx); + return p(); + } case ttAMENDMENT: case ttFEE: - case ttUNL_MODIFY: { + case ttUNL_MODIFY: + case ttEMIT_FAILURE: { Change p(ctx); return p(); } @@ -505,6 +522,7 @@ doApply(PreclaimResult const& preclaimResult, Application& app, OpenView& view) { if (!preclaimResult.likelyToClaimFee) return {preclaimResult.ter, false}; + ApplyContext ctx( app, view, diff --git a/src/ripple/ledger/ApplyView.h b/src/ripple/ledger/ApplyView.h index b99a22ae1..fc394844c 100644 --- a/src/ripple/ledger/ApplyView.h +++ b/src/ripple/ledger/ApplyView.h @@ -43,7 +43,7 @@ enum ApplyFlags : std::uint32_t { tapPREFER_QUEUE = 0x40, // Transaction came from a privileged source - tapUNLIMITED = 0x400, + tapUNLIMITED = 0x400 }; constexpr ApplyFlags diff --git a/src/ripple/ledger/ApplyViewImpl.h b/src/ripple/ledger/ApplyViewImpl.h index a7fb1bd42..d0e0aec75 100644 --- a/src/ripple/ledger/ApplyViewImpl.h +++ b/src/ripple/ledger/ApplyViewImpl.h @@ -24,7 +24,11 @@ #include #include #include - +#include +#include +#include +#include + namespace ripple { /** Editable, discardable view that can build metadata for one tx. @@ -68,6 +72,34 @@ public: deliver_ = amount; } + + /* Set hook metadata for a hook execution + * Takes ownership / use std::move + */ + void + addHookMetaData(STObject&& hookExecution) + { + hookExecution_.push_back(std::move(hookExecution)); + } + + void + setHookMetaData(std::vector&& vec) + { + hookExecution_ = std::move(vec); + } + + void + copyHookMetaData(std::vector& into) + { + std::copy(hookExecution_.begin(), hookExecution_.end(), std::back_inserter(into)); + } + + uint16_t + nextHookExecutionIndex() + { + return hookExecution_.size(); + } + /** Get the number of modified entries */ std::size_t @@ -86,6 +118,7 @@ public: private: std::optional deliver_; + std::vector hookExecution_; }; } // namespace ripple diff --git a/src/ripple/ledger/OpenView.h b/src/ripple/ledger/OpenView.h index 8467e4abc..748d28b7f 100644 --- a/src/ripple/ledger/OpenView.h +++ b/src/ripple/ledger/OpenView.h @@ -99,6 +99,7 @@ private: bool open_ = true; public: + OpenView() = delete; OpenView& operator=(OpenView&&) = delete; diff --git a/src/ripple/ledger/detail/ApplyStateTable.h b/src/ripple/ledger/detail/ApplyStateTable.h index 3fbc49609..9ab3dc14d 100644 --- a/src/ripple/ledger/detail/ApplyStateTable.h +++ b/src/ripple/ledger/detail/ApplyStateTable.h @@ -70,6 +70,7 @@ public: STTx const& tx, TER ter, std::optional const& deliver, + std::vector const& hookExecution, beast::Journal j); bool diff --git a/src/ripple/ledger/impl/ApplyStateTable.cpp b/src/ripple/ledger/impl/ApplyStateTable.cpp index ae70c7191..8c592789f 100644 --- a/src/ripple/ledger/impl/ApplyStateTable.cpp +++ b/src/ripple/ledger/impl/ApplyStateTable.cpp @@ -115,6 +115,7 @@ ApplyStateTable::apply( STTx const& tx, TER ter, std::optional const& deliver, + std::vector const& hookExecution, beast::Journal j) { // Build metadata and insert @@ -126,6 +127,10 @@ ApplyStateTable::apply( TxMeta meta(tx.getTransactionID(), to.seq()); if (deliver) meta.setDeliveredAmount(*deliver); + + if (!hookExecution.empty()) + meta.setHookExecutions(STArray{hookExecution, sfHookExecutions}); + Mods newMod; for (auto& item : items_) { diff --git a/src/ripple/ledger/impl/ApplyViewImpl.cpp b/src/ripple/ledger/impl/ApplyViewImpl.cpp index 155ff4df9..11e0b7fe0 100644 --- a/src/ripple/ledger/impl/ApplyViewImpl.cpp +++ b/src/ripple/ledger/impl/ApplyViewImpl.cpp @@ -31,7 +31,7 @@ ApplyViewImpl::ApplyViewImpl(ReadView const* base, ApplyFlags flags) void ApplyViewImpl::apply(OpenView& to, STTx const& tx, TER ter, beast::Journal j) { - items_.apply(to, tx, ter, deliver_, j); + items_.apply(to, tx, ter, deliver_, hookExecution_, j); } std::size_t diff --git a/src/ripple/ledger/impl/OpenView.cpp b/src/ripple/ledger/impl/OpenView.cpp index 67858ad56..fd03d7665 100644 --- a/src/ripple/ledger/impl/OpenView.cpp +++ b/src/ripple/ledger/impl/OpenView.cpp @@ -267,6 +267,7 @@ OpenView::rawTxInsert( std::piecewise_construct, std::forward_as_tuple(key), std::forward_as_tuple(txn, metaData)); + if (!result.second) LogicError("rawTxInsert: duplicate TX id" + to_string(key)); } diff --git a/src/ripple/overlay/impl/PeerImp.cpp b/src/ripple/overlay/impl/PeerImp.cpp index ad8ad88bb..a0b48d734 100644 --- a/src/ripple/overlay/impl/PeerImp.cpp +++ b/src/ripple/overlay/impl/PeerImp.cpp @@ -1539,6 +1539,12 @@ PeerImp::handleTransaction( auto stx = std::make_shared(sit); uint256 txID = stx->getTransactionID(); + if (stx->isFieldPresent(sfEmitDetails)) + { + JLOG(p_journal_.warn()) << "Ignoring Network relayed Tx containing sfEmitDetails."; + return; + } + int flags; constexpr std::chrono::seconds tx_interval = 10s; diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index d65e8f8f0..624c2862c 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -333,6 +333,7 @@ extern uint256 const featureFlowSortStrands; extern uint256 const fixSTAmountCanonicalize; extern uint256 const fixRmSmallIncreasedQOffers; extern uint256 const featureCheckCashMakesTrustLine; +extern uint256 const featureHooks; } // namespace ripple diff --git a/src/ripple/protocol/HashPrefix.h b/src/ripple/protocol/HashPrefix.h index 409f9de9b..1a7c50600 100644 --- a/src/ripple/protocol/HashPrefix.h +++ b/src/ripple/protocol/HashPrefix.h @@ -87,6 +87,12 @@ enum class HashPrefix : std::uint32_t { /** shard info for signing */ shardInfo = detail::make_hash_prefix('S', 'H', 'D'), + + /** Emit Transaction Nonce */ + emitTxnNonce = detail::make_hash_prefix('E', 'T', 'X'), + + /* Hash of a Hook's actual code */ + hookDefinition = detail::make_hash_prefix('W', 'S', 'M') }; template diff --git a/src/ripple/protocol/Indexes.h b/src/ripple/protocol/Indexes.h index 411f4db24..890f64263 100644 --- a/src/ripple/protocol/Indexes.h +++ b/src/ripple/protocol/Indexes.h @@ -49,6 +49,25 @@ class SeqProxy; */ namespace keylet { +/** The (fixed) index of the object containing the emitted txns for the ledger. */ +Keylet const& +emittedDir() noexcept; + +Keylet +emitted(uint256 const& id) noexcept; + +Keylet +hookDefinition(uint256 const& hash) noexcept; + +Keylet +hook(AccountID const& id) noexcept; + +Keylet +hookState(AccountID const& id, uint256 const& key, uint256 const& ns) noexcept; + +Keylet +hookStateDir(AccountID const& id, uint256 const& ns) noexcept; + /** AccountID root */ Keylet account(AccountID const& id) noexcept; diff --git a/src/ripple/protocol/LedgerFormats.h b/src/ripple/protocol/LedgerFormats.h index 1f6079838..1f2012ab0 100644 --- a/src/ripple/protocol/LedgerFormats.h +++ b/src/ripple/protocol/LedgerFormats.h @@ -193,13 +193,37 @@ enum LedgerEntryType : std::uint16_t */ ltCONTRACT [[deprecated("This object type is not supported and should not be used.")]] = 0x0063, - /** A legacy, deprecated type. + /** A legacy, deprecated type. \deprecated **This object type is not supported and should not be used.** Support for this type of object was never implemented. No objects of this type were ever created. */ ltGENERATOR_MAP [[deprecated("This object type is not supported and should not be used.")]] = 0x0067, + + /** A ledger object which describes an installed hook on an account. + + \sa keylet::hook + */ + ltHOOK ='H', + + /** A ledger object which describes a stored value (from a k-v pair) for an installed hook. + + \sa keylet::hookState + */ + ltHOOK_STATE ='v', + + /** A reference-counted ledger object which stores the web assembly bytecode of a hook. + + \sa keylet::hookDefinition + */ + ltHOOK_DEFINITION = 'D', + + /** A ledger object containing a hook-emitted transaction from a previous hook execution. + + \sa keylet::emitted + */ + ltEMITTED = 'E', }; // clang-format off @@ -221,6 +245,8 @@ enum LedgerSpecificFlags { 0x00800000, // True, trust lines allow rippling by default lsfDepositAuth = 0x01000000, // True, all deposits require authorization + lsfHookEnabled = 0x02000000, // True, all in and out tx will fire hook code + // ltOFFER lsfPassive = 0x00010000, lsfSell = 0x00020000, // True, offer was placed as a sell. diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index f9278ea73..8c519aff2 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -341,6 +341,7 @@ extern SF_UINT8 const sfMethod; extern SF_UINT8 const sfTransactionResult; extern SF_UINT8 const sfTickSize; extern SF_UINT8 const sfUNLModifyDisabling; +extern SF_UINT8 const sfHookResult; // code indicating what happened accept/rollback/error, NOT return code. // 16-bit integers extern SF_UINT16 const sfLedgerEntryType; @@ -349,6 +350,10 @@ extern SF_UINT16 const sfSignerWeight; // 16-bit integers (uncommon) extern SF_UINT16 const sfVersion; +extern SF_UINT16 const sfHookStateChangeCount; +extern SF_UINT16 const sfHookEmitCount; +extern SF_UINT16 const sfHookExecutionIndex; +extern SF_UINT16 const sfHookApiVersion; // 32-bit integers (common) extern SF_UINT32 const sfFlags; @@ -392,6 +397,8 @@ extern SF_UINT32 const sfSignerListID; extern SF_UINT32 const sfSettleDelay; extern SF_UINT32 const sfTicketCount; extern SF_UINT32 const sfTicketSequence; +extern SF_UINT32 const sfHookStateCount; +extern SF_UINT32 const sfEmitGeneration; // 64-bit integers extern SF_UINT64 const sfIndexNext; @@ -405,6 +412,11 @@ extern SF_UINT64 const sfHighNode; extern SF_UINT64 const sfDestinationNode; extern SF_UINT64 const sfCookie; extern SF_UINT64 const sfServerVersion; +extern SF_UINT64 const sfHookOn; +extern SF_UINT64 const sfHookInstructionCount; +extern SF_UINT64 const sfEmitBurden; +extern SF_UINT64 const sfHookReturnCode; // the code returned by hook dev (rollback or accept) NOT exe. result +extern SF_UINT64 const sfReferenceCount; // 128-bit extern SF_HASH128 const sfEmailHash; @@ -425,6 +437,9 @@ extern SF_HASH256 const sfLedgerIndex; extern SF_HASH256 const sfWalletLocator; extern SF_HASH256 const sfRootIndex; extern SF_HASH256 const sfAccountTxnID; +extern SF_HASH256 const sfEmitParentTxnID; +extern SF_HASH256 const sfEmitNonce; +extern SF_HASH256 const sfEmitHookHash; // 256-bit (uncommon) extern SF_HASH256 const sfBookDirectory; @@ -436,6 +451,10 @@ extern SF_HASH256 const sfChannel; extern SF_HASH256 const sfConsensusHash; extern SF_HASH256 const sfCheckID; extern SF_HASH256 const sfValidatedHash; +extern SF_HASH256 const sfHookStateKey; +extern SF_HASH256 const sfHookHash; +extern SF_HASH256 const sfHookNamespace; +extern SF_HASH256 const sfHookSetTxnID; // currency amount (common) extern SF_AMOUNT const sfAmount; @@ -476,6 +495,10 @@ extern SF_VL const sfMasterSignature; extern SF_VL const sfUNLModifyValidator; extern SF_VL const sfValidatorToDisable; extern SF_VL const sfValidatorToReEnable; +extern SF_VL const sfHookStateData; +extern SF_VL const sfHookReturnString; +extern SF_VL const sfHookParameterName; +extern SF_VL const sfHookParameterValue; // account extern SF_ACCOUNT const sfAccount; @@ -486,6 +509,10 @@ extern SF_ACCOUNT const sfAuthorize; extern SF_ACCOUNT const sfUnauthorize; extern SF_ACCOUNT const sfTarget; extern SF_ACCOUNT const sfRegularKey; +extern SF_ACCOUNT const sfEmitCallback; + +// account (uncommon) +extern SF_ACCOUNT const sfHookAccount; // path set extern SField const sfPaths; @@ -510,6 +537,11 @@ extern SField const sfSignerEntry; extern SField const sfSigner; extern SField const sfMajority; extern SField const sfDisabledValidator; +extern SField const sfEmittedTxn; +extern SField const sfHook; +extern SField const sfHookDefinition; +extern SField const sfHookParameter; +extern SField const sfHookGrant; // array of objects // ARRAY/1 is reserved for end of array @@ -523,6 +555,12 @@ extern SField const sfAffectedNodes; extern SField const sfMemos; extern SField const sfMajorities; extern SField const sfDisabledValidators; +extern SField const sfEmitDetails; +extern SField const sfHookExecutions; // array of executions +extern SField const sfHookExecution; // actual execution result +extern SField const sfHookParameters; +extern SField const sfHooks; +extern SField const sfHookGrants; //------------------------------------------------------------------------------ } // namespace ripple diff --git a/src/ripple/protocol/STArray.h b/src/ripple/protocol/STArray.h index 12c698649..fbcaa3317 100644 --- a/src/ripple/protocol/STArray.h +++ b/src/ripple/protocol/STArray.h @@ -41,6 +41,7 @@ public: STArray(STArray&&); STArray(STArray const&) = default; STArray(SField const& f, int n); + STArray(std::vector const& v, SField const& f); // copy assignment constructor from std::vector STArray(SerialIter& sit, SField const& f, int depth = 0); explicit STArray(int n); explicit STArray(SField const& f); diff --git a/src/ripple/protocol/STObject.h b/src/ripple/protocol/STObject.h index 3d2a72b02..f7003fc3e 100644 --- a/src/ripple/protocol/STObject.h +++ b/src/ripple/protocol/STObject.h @@ -619,7 +619,7 @@ public: if (auto cf = dynamic_cast(rf)) cf->setValue(v); else - Throw("Wrong field type"); + Throw("Wrong field type " + field.fieldName); } STObject& @@ -691,7 +691,7 @@ private: const T* cf = dynamic_cast(rf); if (!cf) - Throw("Wrong field type"); + Throw("Wrong field type " + field.fieldName); return cf->value(); } @@ -718,7 +718,7 @@ private: const T* cf = dynamic_cast(rf); if (!cf) - Throw("Wrong field type"); + Throw("Wrong field type " + field.fieldName); return *cf; } @@ -741,7 +741,7 @@ private: T* cf = dynamic_cast(rf); if (!cf) - Throw("Wrong field type"); + Throw("Wrong field type " + field.fieldName); cf->setValue(std::move(value)); } @@ -762,7 +762,7 @@ private: T* cf = dynamic_cast(rf); if (!cf) - Throw("Wrong field type"); + Throw("Wrong field type " + field.fieldName); (*cf) = value; } @@ -783,7 +783,7 @@ private: T* cf = dynamic_cast(rf); if (!cf) - Throw("Wrong field type"); + Throw("Wrong field type " + field.fieldName); return *cf; } diff --git a/src/ripple/protocol/TER.h b/src/ripple/protocol/TER.h index 3a1351058..495021e8e 100644 --- a/src/ripple/protocol/TER.h +++ b/src/ripple/protocol/TER.h @@ -114,6 +114,8 @@ enum TEMcodes : TERUnderlyingType { temINVALID_ACCOUNT_ID, temCANNOT_PREAUTH_SELF, temINVALID_COUNT, + temHOOK_DATA_TOO_LARGE, + temHOOK_REJECTED, temUNCERTAIN, // An internal intermediate result; should never be returned. temUNKNOWN, // An internal intermediate result; should never be returned. @@ -200,6 +202,7 @@ enum TERcodes : TERUnderlyingType { terNO_RIPPLE, // Rippling not allowed terQUEUED, // Transaction is being held in TxQ until fee drops terPRE_TICKET, // Ticket is not yet in ledger but might be on its way + terNO_HOOK // Transaction requires a non-existent hook definition (referenced by sfHookHash) }; //------------------------------------------------------------------------------ @@ -278,7 +281,8 @@ enum TECcodes : TERUnderlyingType { tecKILLED = 150, tecHAS_OBLIGATIONS = 151, tecTOO_SOON = 152, - tecHOOK_ERROR [[maybe_unused]] = 153 + tecHOOK_REJECTED = 153, + tecREQUIRES_FLAG = 154, }; //------------------------------------------------------------------------------ diff --git a/src/ripple/protocol/TxFlags.h b/src/ripple/protocol/TxFlags.h index 9b75b692a..27d1ff3b3 100644 --- a/src/ripple/protocol/TxFlags.h +++ b/src/ripple/protocol/TxFlags.h @@ -84,8 +84,9 @@ const std::uint32_t tfOfferCreateMask = const std::uint32_t tfNoRippleDirect = 0x00010000; const std::uint32_t tfPartialPayment = 0x00020000; const std::uint32_t tfLimitQuality = 0x00040000; +const std::uint32_t tfAutoTrustSet = 0x00080000; const std::uint32_t tfPaymentMask = - ~(tfUniversal | tfPartialPayment | tfLimitQuality | tfNoRippleDirect); + ~(tfUniversal | tfPartialPayment | tfLimitQuality | tfNoRippleDirect | tfAutoTrustSet); // TrustSet flags: const std::uint32_t tfSetfAuth = 0x00010000; diff --git a/src/ripple/protocol/TxFormats.h b/src/ripple/protocol/TxFormats.h index 44f17fde2..f6032582d 100644 --- a/src/ripple/protocol/TxFormats.h +++ b/src/ripple/protocol/TxFormats.h @@ -122,7 +122,7 @@ enum TxType : std::uint16_t ttACCOUNT_DELETE = 21, /** This transaction type installs a hook. */ - ttHOOK_SET [[maybe_unused]] = 22, + ttHOOK_SET = 22, /** This system-generated transaction type is used to update the status of the various amendments. @@ -141,6 +141,7 @@ enum TxType : std::uint16_t For details, see: https://xrpl.org/negative-unl.html */ ttUNL_MODIFY = 102, + ttEMIT_FAILURE = 103, }; // clang-format on diff --git a/src/ripple/protocol/TxMeta.h b/src/ripple/protocol/TxMeta.h index 6d61a27e8..445aaab7d 100644 --- a/src/ripple/protocol/TxMeta.h +++ b/src/ripple/protocol/TxMeta.h @@ -113,6 +113,24 @@ public: mDelivered = delivered; } + STArray const& + getHookExecutions() const + { + return *mHookExecutions; + } + + void + setHookExecutions(const STArray& hookExecutions) + { + mHookExecutions = hookExecutions; + } + + bool + hasHookExecutions() const + { + return static_cast(mHookExecutions); + } + STAmount getDeliveredAmount() const { @@ -133,6 +151,7 @@ private: int mResult; std::optional mDelivered; + std::optional mHookExecutions; STArray mNodes; }; diff --git a/src/ripple/protocol/impl/AccountID.cpp b/src/ripple/protocol/impl/AccountID.cpp index 8ca8d1d15..b5c6cc4a8 100644 --- a/src/ripple/protocol/impl/AccountID.cpp +++ b/src/ripple/protocol/impl/AccountID.cpp @@ -16,7 +16,6 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ //============================================================================== - #include #include #include diff --git a/src/ripple/protocol/impl/BuildInfo.cpp b/src/ripple/protocol/impl/BuildInfo.cpp index 622701934..246ff61bb 100644 --- a/src/ripple/protocol/impl/BuildInfo.cpp +++ b/src/ripple/protocol/impl/BuildInfo.cpp @@ -33,7 +33,7 @@ namespace BuildInfo { // and follow the format described at http://semver.org/ //------------------------------------------------------------------------------ // clang-format off -char const* const versionString = "1.8.0-b7" +char const* const versionString = "1.8.0-b7-hooks" // clang-format on #if defined(DEBUG) || defined(SANITIZER) diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index e1d82cb1b..198a752f2 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -437,6 +437,7 @@ REGISTER_FEATURE(FlowSortStrands, Supported::yes, DefaultVote::yes REGISTER_FIX (fixSTAmountCanonicalize, Supported::yes, DefaultVote::yes); REGISTER_FIX (fixRmSmallIncreasedQOffers, Supported::yes, DefaultVote::yes); REGISTER_FEATURE(CheckCashMakesTrustLine, Supported::yes, DefaultVote::no); +REGISTER_FEATURE(Hooks, Supported::yes, DefaultVote::no); // The following amendments have been active for at least two years. Their // pre-amendment code has been removed and the identifiers are deprecated. diff --git a/src/ripple/protocol/impl/Indexes.cpp b/src/ripple/protocol/impl/Indexes.cpp index 6d7b7cc22..1bd500f70 100644 --- a/src/ripple/protocol/impl/Indexes.cpp +++ b/src/ripple/protocol/impl/Indexes.cpp @@ -60,6 +60,12 @@ enum class LedgerNameSpace : std::uint16_t { CHECK = 'C', DEPOSIT_PREAUTH = 'p', NEGATIVE_UNL = 'N', + HOOK = 'H', + HOOK_STATE_DIR = 'J', + HOOK_STATE = 'v', + HOOK_DEFINITION = 'D', + EMITTED = 'E', + EMITTED_DIR = 'F', // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -126,6 +132,44 @@ getTicketIndex(AccountID const& account, SeqProxy ticketSeq) namespace keylet { +Keylet const& +emittedDir() noexcept +{ + static Keylet const ret{ + ltDIR_NODE, indexHash(LedgerNameSpace::EMITTED_DIR)}; + return ret; +} + +Keylet +hookStateDir(AccountID const& id, uint256 const& ns) noexcept +{ + return {ltDIR_NODE, indexHash(LedgerNameSpace::HOOK_STATE_DIR, id, ns)}; +} + +Keylet +emitted(uint256 const& id) noexcept +{ + return {ltEMITTED, indexHash(LedgerNameSpace::EMITTED, id)}; +} + +Keylet +hook(AccountID const& id) noexcept +{ + return {ltHOOK, indexHash(LedgerNameSpace::HOOK, id)}; +} + +Keylet +hookDefinition(uint256 const& hash) noexcept +{ + return {ltHOOK_DEFINITION, indexHash(LedgerNameSpace::HOOK_DEFINITION, hash)}; +} + +Keylet +hookState(AccountID const& id, uint256 const& key, uint256 const& ns) noexcept +{ + return {ltHOOK_STATE, indexHash(LedgerNameSpace::HOOK_STATE, id, key, ns)}; +} + Keylet account(AccountID const& id) noexcept { diff --git a/src/ripple/protocol/impl/InnerObjectFormats.cpp b/src/ripple/protocol/impl/InnerObjectFormats.cpp index 32d712b95..88779e4f8 100644 --- a/src/ripple/protocol/impl/InnerObjectFormats.cpp +++ b/src/ripple/protocol/impl/InnerObjectFormats.cpp @@ -23,6 +23,17 @@ namespace ripple { InnerObjectFormats::InnerObjectFormats() { + add(sfEmitDetails.jsonName.c_str(), + sfEmitDetails.getCode(), + { + {sfEmitGeneration, soeREQUIRED}, + {sfEmitBurden, soeREQUIRED}, + {sfEmitParentTxnID, soeREQUIRED}, + {sfEmitNonce, soeREQUIRED}, + {sfEmitCallback, soeREQUIRED}, + {sfEmitHookHash, soeREQUIRED} + }); + add(sfSignerEntry.jsonName.c_str(), sfSignerEntry.getCode(), { @@ -51,6 +62,60 @@ InnerObjectFormats::InnerObjectFormats() {sfPublicKey, soeREQUIRED}, {sfFirstLedgerSequence, soeREQUIRED}, }); + + add(sfHookExecution.jsonName.c_str(), + sfHookExecution.getCode(), + { + {sfHookResult, soeREQUIRED}, + {sfHookHash, soeREQUIRED}, + {sfHookAccount, soeREQUIRED}, + {sfHookReturnCode, soeREQUIRED}, + {sfHookReturnString, soeREQUIRED}, + {sfHookInstructionCount, soeREQUIRED}, + {sfHookExecutionIndex, soeREQUIRED}, + {sfHookStateChangeCount, soeREQUIRED}, + {sfHookEmitCount, soeREQUIRED} + }); + + add(sfHookDefinition.jsonName.c_str(), + sfHook.getCode(), + { + {sfCreateCode, soeREQUIRED}, + {sfHookNamespace, soeREQUIRED}, + {sfHookParameters, soeREQUIRED}, + {sfHookOn, soeREQUIRED}, + {sfHookApiVersion, soeREQUIRED}, + {sfFlags, soeREQUIRED} + }); + + add(sfHook.jsonName.c_str(), + sfHook.getCode(), + { + {sfHookHash, soeOPTIONAL}, + {sfCreateCode, soeOPTIONAL}, + {sfHookGrants, soeOPTIONAL}, + {sfHookNamespace, soeOPTIONAL}, + {sfHookParameters, soeOPTIONAL}, + {sfHookOn, soeOPTIONAL}, + {sfHookApiVersion, soeOPTIONAL}, + {sfFlags, soeOPTIONAL} + }); + + add(sfHookGrant.jsonName.c_str(), + sfHookGrant.getCode(), + { + {sfHookHash, soeREQUIRED}, + {sfAuthorize, soeOPTIONAL}, + {sfFlags, soeOPTIONAL} + }); + + add(sfHookParameter.jsonName.c_str(), + sfHookParameter.getCode(), + { + {sfHookParameterName, soeREQUIRED}, + {sfHookParameterValue, soeREQUIRED} + }); + } InnerObjectFormats const& diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index b6feb3823..45f4ab1d3 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -54,6 +54,7 @@ LedgerFormats::LedgerFormats() {sfDomain, soeOPTIONAL}, {sfTickSize, soeOPTIONAL}, {sfTicketCount, soeOPTIONAL}, + {sfHookStateCount, soeOPTIONAL} }, commonFields); @@ -66,6 +67,9 @@ LedgerFormats::LedgerFormats() {sfTakerGetsCurrency, soeOPTIONAL}, // for order book directories {sfTakerGetsIssuer, soeOPTIONAL}, // for order book directories {sfExchangeRate, soeOPTIONAL}, // for order book directories + {sfHookNamespace, soeOPTIONAL}, // for hook state directories + {sfOwnerNode, soeOPTIONAL}, // for hook state directories + {sfReferenceCount, soeOPTIONAL}, // for hook state directories {sfIndexes, soeREQUIRED}, {sfRootIndex, soeREQUIRED}, {sfIndexNext, soeOPTIONAL}, @@ -177,6 +181,43 @@ LedgerFormats::LedgerFormats() }, commonFields); + add(jss::Hook, + ltHOOK, + { + {sfAccount, soeOPTIONAL}, + {sfOwnerNode, soeREQUIRED}, + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED}, + {sfHooks, soeREQUIRED} + }, + commonFields); + + add(jss::HookDefinition, + ltHOOK_DEFINITION, + { + {sfHookHash, soeREQUIRED}, + {sfHookOn, soeREQUIRED}, + {sfHookNamespace, soeREQUIRED}, + {sfHookParameters, soeREQUIRED}, + {sfHookApiVersion, soeREQUIRED}, + {sfCreateCode, soeREQUIRED}, + {sfHookSetTxnID, soeREQUIRED}, + {sfReferenceCount, soeREQUIRED}, + {sfFee, soeREQUIRED} + }, + commonFields); + + add(jss::HookState, + ltHOOK_STATE, + { + {sfAccount, soeOPTIONAL}, //RH TODO: this can be removed for production + {sfOwnerNode, soeREQUIRED}, + {sfHookStateKey, soeREQUIRED}, + {sfHookStateData, soeREQUIRED}, + {sfHookNamespace, soeOPTIONAL} // as can this + }, + commonFields); + add(jss::PayChannel, ltPAYCHAN, { @@ -234,6 +275,15 @@ LedgerFormats::LedgerFormats() {sfValidatorToReEnable, soeOPTIONAL}, }, commonFields); + + add(jss::Emitted, + ltEMITTED, + { + {sfEmittedTxn, soeOPTIONAL}, + {sfOwnerNode, soeREQUIRED}, + }, + commonFields); + } LedgerFormats const& diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index 590ffeb65..e2d10fba2 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -59,12 +59,15 @@ static SField::private_access_tag_t access; #pragma push_macro("CONSTRUCT_TYPED_SFIELD") #undef CONSTRUCT_TYPED_SFIELD +#define XSTR(x) STR(x) +#define STR(x) #x + #define CONSTRUCT_TYPED_SFIELD(sfName, txtName, stiSuffix, fieldValue, ...) \ SF_##stiSuffix const sfName( \ access, STI_##stiSuffix, fieldValue, txtName, ##__VA_ARGS__); \ static_assert( \ std::string_view(#sfName) == "sf" txtName, \ - "Declaration of SField does not match its text name") + "Declaration of SField does not match its text name: " txtName) // clang-format off @@ -88,6 +91,7 @@ CONSTRUCT_TYPED_SFIELD(sfTransactionResult, "TransactionResult", UINT8, // 8-bit integers (uncommon) CONSTRUCT_TYPED_SFIELD(sfTickSize, "TickSize", UINT8, 16); CONSTRUCT_TYPED_SFIELD(sfUNLModifyDisabling, "UNLModifyDisabling", UINT8, 17); +CONSTRUCT_TYPED_SFIELD(sfHookResult, "HookResult", UINT8, 18); // 16-bit integers CONSTRUCT_TYPED_SFIELD(sfLedgerEntryType, "LedgerEntryType", UINT16, 1, SField::sMD_Never); @@ -95,7 +99,11 @@ CONSTRUCT_TYPED_SFIELD(sfTransactionType, "TransactionType", UINT16, CONSTRUCT_TYPED_SFIELD(sfSignerWeight, "SignerWeight", UINT16, 3); // 16-bit integers (uncommon) -CONSTRUCT_TYPED_SFIELD(sfVersion, "Version", UINT16, 16); +CONSTRUCT_TYPED_SFIELD(sfVersion, "Version", UINT16, 16); +CONSTRUCT_TYPED_SFIELD(sfHookStateChangeCount, "HookStateChangeCount", UINT16, 17); +CONSTRUCT_TYPED_SFIELD(sfHookEmitCount, "HookEmitCount", UINT16, 18); +CONSTRUCT_TYPED_SFIELD(sfHookExecutionIndex, "HookExecutionIndex", UINT16, 19); +CONSTRUCT_TYPED_SFIELD(sfHookApiVersion, "HookApiVersion", UINT16, 20); // 32-bit integers (common) CONSTRUCT_TYPED_SFIELD(sfFlags, "Flags", UINT32, 2); @@ -139,6 +147,8 @@ CONSTRUCT_TYPED_SFIELD(sfSignerListID, "SignerListID", UINT32, CONSTRUCT_TYPED_SFIELD(sfSettleDelay, "SettleDelay", UINT32, 39); CONSTRUCT_TYPED_SFIELD(sfTicketCount, "TicketCount", UINT32, 40); CONSTRUCT_TYPED_SFIELD(sfTicketSequence, "TicketSequence", UINT32, 41); +CONSTRUCT_TYPED_SFIELD(sfHookStateCount, "HookStateCount", UINT32, 42); +CONSTRUCT_TYPED_SFIELD(sfEmitGeneration, "EmitGeneration", UINT32, 43); // 64-bit integers CONSTRUCT_TYPED_SFIELD(sfIndexNext, "IndexNext", UINT64, 1); @@ -152,6 +162,13 @@ CONSTRUCT_TYPED_SFIELD(sfHighNode, "HighNode", UINT64, CONSTRUCT_TYPED_SFIELD(sfDestinationNode, "DestinationNode", UINT64, 9); CONSTRUCT_TYPED_SFIELD(sfCookie, "Cookie", UINT64, 10); CONSTRUCT_TYPED_SFIELD(sfServerVersion, "ServerVersion", UINT64, 11); +CONSTRUCT_TYPED_SFIELD(sfEmitBurden, "EmitBurden", UINT64, 12); + +// 64-bit integers (uncommon) +CONSTRUCT_TYPED_SFIELD(sfHookOn, "HookOn", UINT64, 16); +CONSTRUCT_TYPED_SFIELD(sfHookInstructionCount, "HookInstructionCount", UINT64, 17); +CONSTRUCT_TYPED_SFIELD(sfHookReturnCode, "HookReturnCode", UINT64, 18); +CONSTRUCT_TYPED_SFIELD(sfReferenceCount, "ReferenceCount", UINT64, 19); // 128-bit CONSTRUCT_TYPED_SFIELD(sfEmailHash, "EmailHash", HASH128, 1); @@ -172,6 +189,9 @@ CONSTRUCT_TYPED_SFIELD(sfLedgerIndex, "LedgerIndex", HASH256, CONSTRUCT_TYPED_SFIELD(sfWalletLocator, "WalletLocator", HASH256, 7); CONSTRUCT_TYPED_SFIELD(sfRootIndex, "RootIndex", HASH256, 8, SField::sMD_Always); CONSTRUCT_TYPED_SFIELD(sfAccountTxnID, "AccountTxnID", HASH256, 9); +CONSTRUCT_TYPED_SFIELD(sfEmitParentTxnID, "EmitParentTxnID", HASH256, 10); +CONSTRUCT_TYPED_SFIELD(sfEmitNonce, "EmitNonce", HASH256, 11); +CONSTRUCT_TYPED_SFIELD(sfEmitHookHash, "EmitHookHash", HASH256, 12); // 256-bit (uncommon) CONSTRUCT_TYPED_SFIELD(sfBookDirectory, "BookDirectory", HASH256, 16); @@ -184,6 +204,10 @@ CONSTRUCT_TYPED_SFIELD(sfChannel, "Channel", HASH256, CONSTRUCT_TYPED_SFIELD(sfConsensusHash, "ConsensusHash", HASH256, 23); CONSTRUCT_TYPED_SFIELD(sfCheckID, "CheckID", HASH256, 24); CONSTRUCT_TYPED_SFIELD(sfValidatedHash, "ValidatedHash", HASH256, 25); +CONSTRUCT_TYPED_SFIELD(sfHookStateKey, "HookStateKey", HASH256, 26); +CONSTRUCT_TYPED_SFIELD(sfHookHash, "HookHash", HASH256, 27); +CONSTRUCT_TYPED_SFIELD(sfHookNamespace, "HookNamespace", HASH256, 28); +CONSTRUCT_TYPED_SFIELD(sfHookSetTxnID, "HookSetTxnID", HASH256, 29); // currency amount (common) CONSTRUCT_TYPED_SFIELD(sfAmount, "Amount", AMOUNT, 1); @@ -225,6 +249,10 @@ CONSTRUCT_TYPED_SFIELD(sfMasterSignature, "MasterSignature", VL, CONSTRUCT_TYPED_SFIELD(sfUNLModifyValidator, "UNLModifyValidator", VL, 19); CONSTRUCT_TYPED_SFIELD(sfValidatorToDisable, "ValidatorToDisable", VL, 20); CONSTRUCT_TYPED_SFIELD(sfValidatorToReEnable, "ValidatorToReEnable", VL, 21); +CONSTRUCT_TYPED_SFIELD(sfHookStateData, "HookStateData", VL, 22); +CONSTRUCT_TYPED_SFIELD(sfHookReturnString, "HookReturnString", VL, 23); +CONSTRUCT_TYPED_SFIELD(sfHookParameterName, "HookParameterName", VL, 24); +CONSTRUCT_TYPED_SFIELD(sfHookParameterValue, "HookParameterValue", VL, 25); // account CONSTRUCT_TYPED_SFIELD(sfAccount, "Account", ACCOUNT, 1); @@ -235,6 +263,10 @@ CONSTRUCT_TYPED_SFIELD(sfAuthorize, "Authorize", ACCOUNT, CONSTRUCT_TYPED_SFIELD(sfUnauthorize, "Unauthorize", ACCOUNT, 6); // 7 is currently unused CONSTRUCT_TYPED_SFIELD(sfRegularKey, "RegularKey", ACCOUNT, 8); +CONSTRUCT_TYPED_SFIELD(sfEmitCallback, "EmitCallback", ACCOUNT, 9); + +// account (uncommon) +CONSTRUCT_TYPED_SFIELD(sfHookAccount, "HookAccount", ACCOUNT, 16); // vector of 256-bit CONSTRUCT_TYPED_SFIELD(sfIndexes, "Indexes", VECTOR256, 1, SField::sMD_Never); @@ -256,12 +288,19 @@ CONSTRUCT_UNTYPED_SFIELD(sfNewFields, "NewFields", OBJECT, CONSTRUCT_UNTYPED_SFIELD(sfTemplateEntry, "TemplateEntry", OBJECT, 9); CONSTRUCT_UNTYPED_SFIELD(sfMemo, "Memo", OBJECT, 10); CONSTRUCT_UNTYPED_SFIELD(sfSignerEntry, "SignerEntry", OBJECT, 11); +CONSTRUCT_UNTYPED_SFIELD(sfEmitDetails, "EmitDetails", OBJECT, 12); +CONSTRUCT_UNTYPED_SFIELD(sfHook, "Hook", OBJECT, 13); // inner object (uncommon) CONSTRUCT_UNTYPED_SFIELD(sfSigner, "Signer", OBJECT, 16); // 17 has not been used yet CONSTRUCT_UNTYPED_SFIELD(sfMajority, "Majority", OBJECT, 18); CONSTRUCT_UNTYPED_SFIELD(sfDisabledValidator, "DisabledValidator", OBJECT, 19); +CONSTRUCT_UNTYPED_SFIELD(sfEmittedTxn, "EmittedTxn", OBJECT, 20); +CONSTRUCT_UNTYPED_SFIELD(sfHookExecution, "HookExecution", OBJECT, 21); +CONSTRUCT_UNTYPED_SFIELD(sfHookDefinition, "HookDefinition", OBJECT, 22); +CONSTRUCT_UNTYPED_SFIELD(sfHookParameter, "HookParameter", OBJECT, 23); +CONSTRUCT_UNTYPED_SFIELD(sfHookGrant, "HookGrant", OBJECT, 24); // array of objects // ARRAY/1 is reserved for end of array @@ -273,10 +312,14 @@ CONSTRUCT_UNTYPED_SFIELD(sfNecessary, "Necessary", ARRAY, CONSTRUCT_UNTYPED_SFIELD(sfSufficient, "Sufficient", ARRAY, 7); CONSTRUCT_UNTYPED_SFIELD(sfAffectedNodes, "AffectedNodes", ARRAY, 8); CONSTRUCT_UNTYPED_SFIELD(sfMemos, "Memos", ARRAY, 9); +CONSTRUCT_UNTYPED_SFIELD(sfHooks, "Hooks", ARRAY, 10); // array of objects (uncommon) CONSTRUCT_UNTYPED_SFIELD(sfMajorities, "Majorities", ARRAY, 16); CONSTRUCT_UNTYPED_SFIELD(sfDisabledValidators, "DisabledValidators", ARRAY, 17); +CONSTRUCT_UNTYPED_SFIELD(sfHookExecutions, "HookExecutions", ARRAY, 18); +CONSTRUCT_UNTYPED_SFIELD(sfHookParameters, "HookParameters", ARRAY, 19); +CONSTRUCT_UNTYPED_SFIELD(sfHookGrants, "HookGrants", ARRAY, 20); // clang-format on diff --git a/src/ripple/protocol/impl/SOTemplate.cpp b/src/ripple/protocol/impl/SOTemplate.cpp index 8457b7591..101ac5d03 100644 --- a/src/ripple/protocol/impl/SOTemplate.cpp +++ b/src/ripple/protocol/impl/SOTemplate.cpp @@ -44,7 +44,7 @@ SOTemplate::SOTemplate( // Make sure that this field hasn't already been assigned // if (getIndex(sField) != -1) - Throw("Duplicate field index for SOTemplate."); + Throw(std::string("Duplicate field index for SOTemplate. ") + sField.fieldName); // Add the field to the index mapping table // diff --git a/src/ripple/protocol/impl/STArray.cpp b/src/ripple/protocol/impl/STArray.cpp index 229b3700a..97b5800ea 100644 --- a/src/ripple/protocol/impl/STArray.cpp +++ b/src/ripple/protocol/impl/STArray.cpp @@ -42,6 +42,11 @@ STArray::STArray(int n) v_.reserve(n); } +STArray::STArray(std::vector const& v, SField const& f) : STBase(f) +{ + v_ = v; +} + STArray::STArray(SField const& f) : STBase(f) { } @@ -74,7 +79,7 @@ STArray::STArray(SerialIter& sit, SField const& f, int depth) : STBase(f) { JLOG(debugLog().error()) << "Unknown field: " << type << "/" << field; - Throw("Unknown field"); + Throw("Unknown field - In Array"); } if (fn.fieldType != STI_OBJECT) diff --git a/src/ripple/protocol/impl/STObject.cpp b/src/ripple/protocol/impl/STObject.cpp index e05b1cb9a..3bc4161f6 100644 --- a/src/ripple/protocol/impl/STObject.cpp +++ b/src/ripple/protocol/impl/STObject.cpp @@ -186,7 +186,10 @@ STObject::set(SerialIter& sit, int depth) noexcept(false) { JLOG(debugLog().error()) << "Unknown field: field_type=" << type << ", field_name=" << field; - Throw("Unknown field"); + std::stringstream ss; + ss << "Unknown field in Object t=" << type << " f=" << field; + + Throw(ss.str().c_str()); } // Unflatten the field diff --git a/src/ripple/protocol/impl/STTx.cpp b/src/ripple/protocol/impl/STTx.cpp index a7a45912e..b71ca5800 100644 --- a/src/ripple/protocol/impl/STTx.cpp +++ b/src/ripple/protocol/impl/STTx.cpp @@ -537,8 +537,9 @@ isPseudoTx(STObject const& tx) auto t = tx[~sfTransactionType]; if (!t) return false; + auto tt = safe_cast(*t); - return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY; + return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY || tt == ttEMIT_FAILURE; } } // namespace ripple diff --git a/src/ripple/protocol/impl/TER.cpp b/src/ripple/protocol/impl/TER.cpp index cb8cd7e89..b9f14e1e7 100644 --- a/src/ripple/protocol/impl/TER.cpp +++ b/src/ripple/protocol/impl/TER.cpp @@ -80,6 +80,7 @@ transResults() MAKE_ERROR(tecKILLED, "FillOrKill offer killed."), MAKE_ERROR(tecHAS_OBLIGATIONS, "The account cannot be deleted since it has obligations."), MAKE_ERROR(tecTOO_SOON, "It is too early to attempt the requested operation. Please wait."), + MAKE_ERROR(tecHOOK_REJECTED, "Rejected by hook on sending or receiving account."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index ff3f7f507..04038c648 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -40,6 +40,8 @@ TxFormats::TxFormats() {sfSigningPubKey, soeREQUIRED}, {sfTxnSignature, soeOPTIONAL}, {sfSigners, soeOPTIONAL}, // submit_multisigned + {sfEmitDetails, soeOPTIONAL}, + {sfFirstLedgerSequence, soeOPTIONAL}, }; add(jss::AccountSet, @@ -150,6 +152,14 @@ TxFormats::TxFormats() }, commonFields); + add(jss::EmitFailure, + ttEMIT_FAILURE, + { + {sfLedgerSequence, soeREQUIRED}, + {sfTransactionHash, soeREQUIRED}, + }, + commonFields); + add(jss::SetFee, ttFEE, { @@ -271,6 +281,13 @@ TxFormats::TxFormats() {sfTicketSequence, soeOPTIONAL}, }, commonFields); + + add(jss::SetHook, + ttHOOK_SET, + { + {sfHooks, soeREQUIRED}, + }, + commonFields); } TxFormats const& diff --git a/src/ripple/protocol/impl/TxMeta.cpp b/src/ripple/protocol/impl/TxMeta.cpp index 6030ff89c..96d4d3c87 100644 --- a/src/ripple/protocol/impl/TxMeta.cpp +++ b/src/ripple/protocol/impl/TxMeta.cpp @@ -43,6 +43,9 @@ TxMeta::TxMeta( if (obj.isFieldPresent(sfDeliveredAmount)) setDeliveredAmount(obj.getFieldAmount(sfDeliveredAmount)); + + if (obj.isFieldPresent(sfHookExecutions)) + setHookExecutions(obj.getFieldArray(sfHookExecutions)); } TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, STObject const& obj) @@ -61,6 +64,9 @@ TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, STObject const& obj) if (obj.isFieldPresent(sfDeliveredAmount)) setDeliveredAmount(obj.getFieldAmount(sfDeliveredAmount)); + + if (obj.isFieldPresent(sfHookExecutions)) + setHookExecutions(obj.getFieldArray(sfHookExecutions)); } TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, Blob const& vec) @@ -211,6 +217,10 @@ TxMeta::getAsObject() const metaData.emplace_back(mNodes); if (hasDeliveredAmount()) metaData.setFieldAmount(sfDeliveredAmount, getDeliveredAmount()); + + if (hasHookExecutions()) + metaData.setFieldArray(sfHookExecutions, getHookExecutions()); + return metaData; } diff --git a/src/ripple/protocol/impl/tokens.cpp b/src/ripple/protocol/impl/tokens.cpp index 816d49e40..a337a3af8 100644 --- a/src/ripple/protocol/impl/tokens.cpp +++ b/src/ripple/protocol/impl/tokens.cpp @@ -161,7 +161,9 @@ decodeBase58(std::string const& s) } if (remain > 64) + { return {}; + } // Allocate enough space in big-endian base256 representation. // log(58) / log(256), rounded up. @@ -170,7 +172,9 @@ decodeBase58(std::string const& s) { auto carry = alphabetReverse[*psz]; if (carry == -1) + { return {}; + } // Apply "b256 = b256 * 58 + carry". for (auto iter = b256.rbegin(); iter != b256.rend(); ++iter) { @@ -190,6 +194,7 @@ decodeBase58(std::string const& s) result.assign(zeroes, 0x00); while (iter != b256.end()) result.push_back(*(iter++)); + return result; } diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index 361de64ad..50df60a8e 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -58,6 +58,7 @@ JSS(DepositPreauth); // transaction and ledger type. JSS(Destination); // in: TransactionSign; field. JSS(DirectoryNode); // ledger type. JSS(EnableAmendment); // transaction type. +JSS(EmitFailure); // transaction type. (cleanup emit) JSS(Escrow); // ledger type. JSS(EscrowCancel); // transaction type. JSS(EscrowCreate); // transaction type. @@ -89,6 +90,11 @@ JSS(SendMax); // in: TransactionSign JSS(Sequence); // in/out: TransactionSign; field. JSS(SetFlag); // field. JSS(SetRegularKey); // transaction type. +JSS(SetHook); // transaction type. +JSS(Hook); // ledger type. +JSS(HookState); // ledger type. +JSS(HookDefinition); +JSS(Emitted); // ledger type. JSS(SignerList); // ledger type. JSS(SignerListSet); // transaction type. JSS(SigningPubKey); // field. diff --git a/src/ripple/rpc/handlers/LedgerHandler.cpp b/src/ripple/rpc/handlers/LedgerHandler.cpp index c358f2761..656ae002b 100644 --- a/src/ripple/rpc/handlers/LedgerHandler.cpp +++ b/src/ripple/rpc/handlers/LedgerHandler.cpp @@ -74,8 +74,9 @@ LedgerHandler::check() { // Until some sane way to get full ledgers has been implemented, // disallow retrieving all state nodes. - if (!isUnlimited(context_.role)) - return rpcNO_PERMISSION; +// RH TODO: recomment this for prod +// if (!isUnlimited(context_.role)) +// return rpcNO_PERMISSION; if (context_.app.getFeeTrack().isLoadedLocal() && !isUnlimited(context_.role)) diff --git a/src/ripple/rpc/handlers/Submit.cpp b/src/ripple/rpc/handlers/Submit.cpp index 220760172..d0c16f5a7 100644 --- a/src/ripple/rpc/handlers/Submit.cpp +++ b/src/ripple/rpc/handlers/Submit.cpp @@ -236,7 +236,7 @@ doSubmitGrpc( context.app.getHashRouter(), *stpTrans, context.ledgerMaster.getCurrentLedger()->rules(), - context.app.config()); + context.app.config()); if (validity != Validity::Valid) { grpc::Status errorStatus{ diff --git a/src/test/rpc/JSONRPC_test.cpp b/src/test/rpc/JSONRPC_test.cpp index a0970bbd7..f4ab0d5b1 100644 --- a/src/test/rpc/JSONRPC_test.cpp +++ b/src/test/rpc/JSONRPC_test.cpp @@ -2385,6 +2385,7 @@ public: std::shared_ptr&, bool, bool, + bool, NetworkOPs::FailHard) { ;