diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 74ff58f60..a9adf2563 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -462,6 +462,7 @@ target_sources (rippled PRIVATE src/ripple/app/tx/impl/DeleteAccount.cpp src/ripple/app/tx/impl/DepositPreauth.cpp src/ripple/app/tx/impl/Escrow.cpp + src/ripple/app/tx/impl/ExportSign.cpp src/ripple/app/tx/impl/GenesisMint.cpp src/ripple/app/tx/impl/Import.cpp src/ripple/app/tx/impl/InvariantCheck.cpp @@ -744,6 +745,7 @@ if (tests) src/test/app/Discrepancy_test.cpp src/test/app/DNS_test.cpp src/test/app/Escrow_test.cpp + src/test/app/Export_test.cpp src/test/app/FeeVote_test.cpp src/test/app/Flow_test.cpp src/test/app/Freeze_test.cpp diff --git a/src/ripple/app/consensus/RCLConsensus.cpp b/src/ripple/app/consensus/RCLConsensus.cpp index e39334d23..571e5df26 100644 --- a/src/ripple/app/consensus/RCLConsensus.cpp +++ b/src/ripple/app/consensus/RCLConsensus.cpp @@ -35,7 +35,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/src/ripple/app/hook/Enum.h b/src/ripple/app/hook/Enum.h index 7a9992f9c..70b29f269 100644 --- a/src/ripple/app/hook/Enum.h +++ b/src/ripple/app/hook/Enum.h @@ -351,8 +351,10 @@ enum hook_return_code : int64_t { TOO_MANY_STATE_MODIFICATIONS = -44, // more than 5000 modified state // entires in the combined hook chains TOO_MANY_NAMESPACES = -45, + //@@start export-error-codes EXPORT_FAILURE = -46, TOO_MANY_EXPORTED_TXN = -47, + //@@end export-error-codes }; @@ -367,7 +369,9 @@ const uint16_t max_state_modifications = 256; const uint8_t max_slots = 255; const uint8_t max_nonce = 255; const uint8_t max_emit = 255; +//@@start max-export const uint8_t max_export = 4; +//@@end max-export const uint8_t max_params = 16; const double fee_base_multiplier = 1.1f; @@ -473,12 +477,14 @@ static const APIWhitelist import_whitelist_1{ // clang-format on }; +//@@start import-whitelist-2 static const APIWhitelist import_whitelist_2{ // clang-format off HOOK_API_DEFINITION(I64, xport, (I32, I32)), HOOK_API_DEFINITION(I64, xport_reserve, (I32)), // clang-format on }; +//@@end import-whitelist-2 #undef HOOK_API_DEFINITION #undef I32 diff --git a/src/ripple/app/hook/Guard.h b/src/ripple/app/hook/Guard.h index 0e4c13151..d62ea6b57 100644 --- a/src/ripple/app/hook/Guard.h +++ b/src/ripple/app/hook/Guard.h @@ -1034,6 +1034,7 @@ validateGuards( { // PASS, this is a version 1 api } + //@@start guard-export-whitelist else if ( rulesVersion & 0x04U && hook_api::import_whitelist_2.find(import_name) != @@ -1041,6 +1042,7 @@ validateGuards( { // PASS, this is an export api } + //@@end guard-export-whitelist else { GUARDLOG(hook::log::IMPORT_ILLEGAL) diff --git a/src/ripple/app/hook/applyHook.h b/src/ripple/app/hook/applyHook.h index 5f2513470..f402ce63a 100644 --- a/src/ripple/app/hook/applyHook.h +++ b/src/ripple/app/hook/applyHook.h @@ -406,6 +406,7 @@ DECLARE_HOOK_FUNCTION( uint32_t slot_no_tx, uint32_t slot_no_meta); +//@@start declare-xport DECLARE_HOOK_FUNCTION( int64_t, xport, @@ -414,6 +415,7 @@ DECLARE_HOOK_FUNCTION( uint32_t read_ptr, uint32_t read_len); DECLARE_HOOK_FUNCTION(int64_t, xport_reserve, uint32_t count); +//@@end declare-xport /* DECLARE_HOOK_FUNCTION(int64_t, str_find, uint32_t hread_ptr, uint32_t hread_len, uint32_t nread_ptr, uint32_t nread_len, uint32_t mode, @@ -493,7 +495,9 @@ struct HookResult std::queue> emittedTxn{}; // etx stored here until accept/rollback + //@@start hook-result-exported-txn std::queue> exportedTxn{}; + //@@end hook-result-exported-txn HookStateMap& stateMap; uint16_t changedStateCount = 0; std::map< @@ -550,7 +554,9 @@ struct HookContext uint16_t ledger_nonce_counter{0}; int64_t expected_etxn_count{-1}; // make this a 64bit int so the uint32 // from the hookapi cant overflow it + //@@start hook-context-export-count int64_t expected_export_count{-1}; + //@@end hook-context-export-count std::map nonce_used{}; uint32_t generation = 0; // used for caching, only generated when txn_generation is called @@ -887,8 +893,10 @@ public: ADD_HOOK_FUNCTION(meta_slot, ctx); ADD_HOOK_FUNCTION(xpop_slot, ctx); + //@@start add-hook-xport ADD_HOOK_FUNCTION(xport, ctx); ADD_HOOK_FUNCTION(xport_reserve, ctx); + //@@end add-hook-xport /* ADD_HOOK_FUNCTION(str_find, ctx); diff --git a/src/ripple/app/hook/impl/applyHook.cpp b/src/ripple/app/hook/impl/applyHook.cpp index ed4671667..31b857aa3 100644 --- a/src/ripple/app/hook/impl/applyHook.cpp +++ b/src/ripple/app/hook/impl/applyHook.cpp @@ -2028,6 +2028,7 @@ hook::finalizeHookResult( } } + //@@start finalize-export DBG_PRINTF("exported txn count: %d\n", hookResult.exportedTxn.size()); for (; hookResult.exportedTxn.size() > 0; hookResult.exportedTxn.pop()) { @@ -2077,6 +2078,7 @@ hook::finalizeHookResult( } } } + //@@end finalize-export } bool const fixV2 = applyCtx.view().rules().enabled(fixXahauV2); @@ -3943,6 +3945,7 @@ DEFINE_HOOK_FUNCTION(int64_t, etxn_reserve, uint32_t count) HOOK_TEARDOWN(); } +//@@start xport-reserve DEFINE_HOOK_FUNCTION(int64_t, xport_reserve, uint32_t count) { HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx, @@ -3963,6 +3966,7 @@ DEFINE_HOOK_FUNCTION(int64_t, xport_reserve, uint32_t count) HOOK_TEARDOWN(); } +//@@end xport-reserve // Compute the burden of an emitted transaction based on a number of factors DEFINE_HOOK_FUNCNARG(int64_t, etxn_burden) @@ -6233,6 +6237,7 @@ DEFINE_HOOK_FUNCTION( HOOK_TEARDOWN(); } +//@@start xport DEFINE_HOOK_FUNCTION( int64_t, xport, @@ -6319,6 +6324,7 @@ DEFINE_HOOK_FUNCTION( return result; HOOK_TEARDOWN(); } +//@@end xport /* DEFINE_HOOK_FUNCTION( diff --git a/src/ripple/app/main/Application.cpp b/src/ripple/app/main/Application.cpp index a118d9066..bc1d5332d 100644 --- a/src/ripple/app/main/Application.cpp +++ b/src/ripple/app/main/Application.cpp @@ -599,6 +599,7 @@ public: return validatorKeys_.publicKey; } + //@@start app-impl-validator-keys SecretKey const& getValidationSecretKey() const override { @@ -610,6 +611,7 @@ public: { return validatorKeys_; } + //@@end app-impl-validator-keys NetworkOPs& getOPs() override diff --git a/src/ripple/app/main/Application.h b/src/ripple/app/main/Application.h index ba6ebf924..58e6a899b 100644 --- a/src/ripple/app/main/Application.h +++ b/src/ripple/app/main/Application.h @@ -241,11 +241,13 @@ public: virtual PublicKey const& getValidationPublicKey() const = 0; + //@@start app-validator-keys virtual SecretKey const& getValidationSecretKey() const = 0; virtual ValidatorKeys const& getValidatorKeys() const = 0; + //@@end app-validator-keys virtual Resource::Manager& getResourceManager() = 0; virtual PathRequests& diff --git a/src/ripple/app/misc/impl/TxQ.cpp b/src/ripple/app/misc/impl/TxQ.cpp index 317a072f5..929d6f164 100644 --- a/src/ripple/app/misc/impl/TxQ.cpp +++ b/src/ripple/app/misc/impl/TxQ.cpp @@ -1541,6 +1541,7 @@ TxQ::accept(Application& app, OpenView& view) } } + //@@start txq-inject-export // Inject exported transactions/signatures, if any if (view.rules().enabled(featureExport)) { @@ -1723,6 +1724,7 @@ TxQ::accept(Application& app, OpenView& view) } while (0); } + //@@end txq-inject-export // Inject emitted transactions if any if (view.rules().enabled(featureHooks)) @@ -2115,8 +2117,10 @@ TxQ::tryDirectApply( const bool isFirstImport = !sleAccount && view.rules().enabled(featureImport) && tx->getTxnType() == ttIMPORT; + //@@start txq-uvtx-handling // UVTxns don't require an account const bool accRequired = !(isFirstImport || isUVTx(*tx)); + //@@end txq-uvtx-handling // Don't attempt to direct apply if the account is not in the ledger. if (!sleAccount && accRequired) diff --git a/src/ripple/app/tx/impl/Change.cpp b/src/ripple/app/tx/impl/Change.cpp index 454788be0..07f82700b 100644 --- a/src/ripple/app/tx/impl/Change.cpp +++ b/src/ripple/app/tx/impl/Change.cpp @@ -23,17 +23,14 @@ #include #include #include -#include #include #include #include #include -#include #include #include #include #include -#include #include #include @@ -46,57 +43,34 @@ Change::preflight(PreflightContext const& ctx) if (!isTesSuccess(ret)) return ret; - // ttEXPORT_SIGN is a UVTx (UNL Validator Transaction), not a pseudo-tx. - // It has a real account, signature, and goes through normal validation. - bool const isUVTx = ctx.tx.getTxnType() == ttEXPORT_SIGN; - - if (!isUVTx) + auto account = ctx.tx.getAccountID(sfAccount); + if (account != beast::zero) { - auto account = ctx.tx.getAccountID(sfAccount); - if (account != beast::zero) - { - JLOG(ctx.j.warn()) << "Change: Bad source id"; - return temBAD_SRC_ACCOUNT; - } - - // No point in going any further if the transaction fee is malformed. - auto const fee = ctx.tx.getFieldAmount(sfFee); - if (!fee.native() || fee != beast::zero) - { - JLOG(ctx.j.warn()) << "Change: invalid fee"; - return temBAD_FEE; - } - - if (!ctx.tx.getSigningPubKey().empty() || - !ctx.tx.getSignature().empty() || ctx.tx.isFieldPresent(sfSigners)) - { - JLOG(ctx.j.warn()) << "Change: Bad signature"; - return temBAD_SIGNATURE; - } - - if (ctx.tx.getFieldU32(sfSequence) != 0 || - ctx.tx.isFieldPresent(sfPreviousTxnID)) - { - JLOG(ctx.j.warn()) << "Change: Bad sequence"; - return temBAD_SEQUENCE; - } + JLOG(ctx.j.warn()) << "Change: Bad source id"; + return temBAD_SRC_ACCOUNT; } - //@@start uvtx-preflight - else + + // No point in going any further if the transaction fee is malformed. + auto const fee = ctx.tx.getFieldAmount(sfFee); + if (!fee.native() || fee != beast::zero) { - // UVTx preflight checks - if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) - return ret; - - if (!ctx.rules.enabled(featureExport)) - return temDISABLED; - - if (auto const ret = preflight2(ctx); !isTesSuccess(ret)) - return ret; - - return tesSUCCESS; + JLOG(ctx.j.warn()) << "Change: invalid fee"; + return temBAD_FEE; + } + + if (!ctx.tx.getSigningPubKey().empty() || !ctx.tx.getSignature().empty() || + ctx.tx.isFieldPresent(sfSigners)) + { + JLOG(ctx.j.warn()) << "Change: Bad signature"; + return temBAD_SIGNATURE; + } + + if (ctx.tx.getFieldU32(sfSequence) != 0 || + ctx.tx.isFieldPresent(sfPreviousTxnID)) + { + JLOG(ctx.j.warn()) << "Change: Bad sequence"; + return temBAD_SEQUENCE; } - //@@end uvtx-preflight if (ctx.tx.getTxnType() == ttUNL_MODIFY && !ctx.rules.enabled(featureNegativeUNL)) @@ -122,13 +96,13 @@ Change::preflight(PreflightContext const& ctx) } } - if ((ctx.tx.getTxnType() == ttEXPORT_SIGN || - ctx.tx.getTxnType() == ttEXPORT) && - !ctx.rules.enabled(featureExport)) + //@@start export-preflight-check + if (ctx.tx.getTxnType() == ttEXPORT && !ctx.rules.enabled(featureExport)) { JLOG(ctx.j.warn()) << "Change: Export not enabled"; return temDISABLED; } + //@@end export-preflight-check return tesSUCCESS; } @@ -138,38 +112,12 @@ Change::preclaim(PreclaimContext const& ctx) { // If tapOPEN_LEDGER is resurrected into ApplyFlags, // this block can be moved to preflight. - // UVTxns like ttEXPORT_SIGN can be applied in the open ledger. - bool const isUVTx = ctx.tx.getTxnType() == ttEXPORT_SIGN; - if (ctx.view.open() && !isUVTx) + if (ctx.view.open()) { JLOG(ctx.j.warn()) << "Change transaction against open ledger"; return temINVALID; } - //@@start uvtx-preclaim - // UVTx (ttEXPORT_SIGN) validation: signer must be in UNLReport - if (isUVTx) - { - if (!ctx.view.rules().enabled(featureExport)) - return temDISABLED; - - auto const& pkSignerField = ctx.tx.getSigningPubKey(); - if (!publicKeyType(makeSlice(pkSignerField))) - return tefBAD_AUTH; - - PublicKey pkSigner{makeSlice(pkSignerField)}; - - if (!inUNLReport(ctx.view, ctx.app, pkSigner, ctx.j)) - { - JLOG(ctx.j.warn()) - << "ExportSign: Txn Account isn't in the UNLReport."; - return tefFAILURE; - } - - return tesSUCCESS; - } - //@@end uvtx-preclaim - switch (ctx.tx.getTxnType()) { case ttFEE: @@ -214,9 +162,10 @@ Change::preclaim(PreclaimContext const& ctx) case ttAMENDMENT: case ttUNL_MODIFY: case ttEMIT_FAILURE: + //@@start export-preclaim-case case ttEXPORT: - case ttEXPORT_SIGN: return tesSUCCESS; + //@@end export-preclaim-case case ttUNL_REPORT: { if (!ctx.tx.isFieldPresent(sfImportVLKey) || ctx.app.config().IMPORT_VL_KEYS.empty()) @@ -271,12 +220,10 @@ Change::doApply() return applyEmitFailure(); case ttUNL_REPORT: return applyUNLReport(); + //@@start export-doapply-case case ttEXPORT: return applyExport(); - //@@start export-sign-route - case ttEXPORT_SIGN: - return applyExportSign(); - //@@end export-sign-route + //@@end export-doapply-case default: assert(0); @@ -670,6 +617,7 @@ Change::activateXahauGenesis() for (auto const& [hookOn, wasmBytes, params] : genesis_hooks) { std::ostringstream loggerStream; + //@@start export-validate-guards auto result = validateGuards( wasmBytes, // wasm to verify loggerStream, @@ -677,6 +625,7 @@ Change::activateXahauGenesis() (ctx_.view().rules().enabled(featureHooksUpdate1) ? 1 : 0) + (ctx_.view().rules().enabled(fix20250131) ? 2 : 0) + (ctx_.view().rules().enabled(featureExport) ? 4 : 0)); + //@@end export-validate-guards if (!result) { @@ -1142,6 +1091,7 @@ Change::applyEmitFailure() return tesSUCCESS; } +//@@start apply-export TER Change::applyExport() { @@ -1180,45 +1130,7 @@ Change::applyExport() } while (0); return tesSUCCESS; } - -TER -Change::applyExportSign() -{ - uint256 txnID(ctx_.tx.getFieldH256(sfTransactionHash)); - do - { - JLOG(j_.info()) << "HookExport[" << txnID - << "]: ttExportSign adding signature to transaction"; - - auto key = keylet::exportedTxn(txnID); - - auto const& sle = view().peek(key); - - if (!sle) - { - // most likely explanation is that this was somehow a double-up, so - // just ignore - JLOG(j_.warn()) - << "HookError[" << txnID - << "]: ttExportSign could not find exported txn in ledger"; - break; - } - - // grab the signer object off the txn - STObject signerObj = const_cast(ctx_.tx) - .getField(sfSigner) - .downcast(); - - // append it to the signers field in the ledger object - STArray signers = sle->getFieldArray(sfSigners); - signers.push_back(signerObj); - sle->setFieldArray(sfSigners, signers); - - // done - view().update(sle); - } while (0); - return tesSUCCESS; -} +//@@end apply-export TER Change::applyUNLModify() @@ -1347,143 +1259,4 @@ Change::applyUNLModify() return tesSUCCESS; } -std::vector> -makeExportSignTxns(OpenView& view, Application& app, beast::Journal const& j) -{ - std::vector> result; - - if (!view.rules().enabled(featureExport)) - return result; - - JLOG(j.debug()) << "EXPORT_SIGN processing: started"; - - auto const seq = view.info().seq; - - // if we're not a validator we do nothing here - if (app.getValidationPublicKey().empty()) - return result; - - auto const& keys = app.getValidatorKeys(); - - if (keys.configInvalid()) - return result; - - PublicKey pkSigning = app.getValidationPublicKey(); - auto const pk = app.validatorManifests().getMasterKey(pkSigning); - - // Only continue if we're on the UNLReport - if (!inUNLReport(view, app, pk, j)) - return result; - - AccountID signingAcc = calcAccountID(pkSigning); - - Keylet const exportedDirKeylet{keylet::exportedDir()}; - if (dirIsEmpty(view, exportedDirKeylet)) - return result; - - std::shared_ptr sleDirNode{}; - unsigned int uDirEntry{0}; - uint256 dirEntry{beast::zero}; - - if (!cdirFirst( - view, exportedDirKeylet.key, sleDirNode, uDirEntry, dirEntry)) - return result; - - do - { - Keylet const itemKeylet{ltCHILD, dirEntry}; - auto sleItem = view.read(itemKeylet); - if (!sleItem) - { - JLOG(j.warn()) - << "ExportedTxn processing: directory node in ledger " << seq - << " has index to object that is missing: " - << to_string(dirEntry); - continue; - } - - LedgerEntryType const nodeType{ - safe_cast((*sleItem)[sfLedgerEntryType])}; - - if (nodeType != ltEXPORTED_TXN) - { - JLOG(j.warn()) << "ExportedTxn processing: exported directory " - "contained non ltEXPORTED_TXN type"; - continue; - } - - auto const& exported = const_cast(*sleItem) - .getField(sfExportedTxn) - .downcast(); - - auto exportedLgrSeq = exported.getFieldU32(sfLedgerSequence); - - // Only sign transactions that were added in the previous ledger - if (exportedLgrSeq != seq - 1) - continue; - - auto s = std::make_shared(); - exported.add(*s); - SerialIter sitTrans(s->slice()); - try - { - auto const& stpTrans = - std::make_shared(std::ref(sitTrans)); - - if (!stpTrans->isFieldPresent(sfAccount) || - stpTrans->getAccountID(sfAccount) == beast::zero) - { - JLOG(j.warn()) - << "Hook: Export failure: sfAccount missing or zero."; - continue; - } - - auto txnHash = stpTrans->getTransactionID(); - - // Build the multisig for the inner exported transaction - Serializer sigData = buildMultiSigningData(*stpTrans, signingAcc); - auto multisig = - ripple::sign(keys.publicKey, keys.secretKey, sigData.slice()); - - // Create the ttEXPORT_SIGN transaction - auto exportSignTx = - std::make_shared(ttEXPORT_SIGN, [&](auto& obj) { - obj.set(([&]() { - auto inner = std::make_unique(sfSigner); - inner->setFieldVL(sfSigningPubKey, keys.publicKey); - inner->setAccountID(sfAccount, signingAcc); - inner->setFieldVL(sfTxnSignature, multisig); - return inner; - })()); - obj.setFieldU32(sfLedgerSequence, seq); - obj.setFieldH256(sfTransactionHash, txnHash); - obj.setAccountID(sfAccount, calcAccountID(pk)); - obj.setFieldU32(sfSequence, 0); - obj.setFieldVL(sfSigningPubKey, pkSigning.slice()); - obj.setFieldU32(sfFlags, tfFullyCanonicalSig); - - if (app.config().NETWORK_ID > 1024) - obj.setFieldU32(sfNetworkID, app.config().NETWORK_ID); - }); - - // Sign the outer transaction using our ephemeral key - exportSignTx->sign(pkSigning, app.getValidationSecretKey()); - - JLOG(j.debug()) - << "EXPORT_SIGN txn: " << exportSignTx->getFullText(); - - result.push_back(exportSignTx); - } - catch (std::exception& e) - { - JLOG(j.warn()) << "ExportedTxn Processing: Failure: " << e.what() - << "\n"; - } - - } while ( - cdirNext(view, exportedDirKeylet.key, sleDirNode, uDirEntry, dirEntry)); - - return result; -} - } // namespace ripple diff --git a/src/ripple/app/tx/impl/Change.h b/src/ripple/app/tx/impl/Change.h index 4562a396d..ac0c6e090 100644 --- a/src/ripple/app/tx/impl/Change.h +++ b/src/ripple/app/tx/impl/Change.h @@ -25,7 +25,6 @@ #include #include #include -#include #include namespace ripple { @@ -75,23 +74,15 @@ private: TER applyEmitFailure(); + //@@start apply-export-decl TER applyExport(); - - TER - applyExportSign(); + //@@end apply-export-decl TER applyUNLReport(); }; -/** - * If this validator is on the UNLReport, generate signed ttEXPORT_SIGN - * transactions for any exported transactions that need signing. - */ -std::vector> -makeExportSignTxns(OpenView& view, Application& app, beast::Journal const& j); - } // namespace ripple #endif diff --git a/src/ripple/app/tx/impl/ExportSign.cpp b/src/ripple/app/tx/impl/ExportSign.cpp new file mode 100644 index 000000000..69bc04cf3 --- /dev/null +++ b/src/ripple/app/tx/impl/ExportSign.cpp @@ -0,0 +1,255 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 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 + +namespace ripple { + +//@@start exportsign-transactor +TxConsequences +ExportSign::makeTxConsequences(PreflightContext const& ctx) +{ + return TxConsequences{ctx.tx, TxConsequences::normal}; +} + +NotTEC +ExportSign::preflight(PreflightContext const& ctx) +{ + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (!ctx.rules.enabled(featureExport)) + return temDISABLED; + + return preflight2(ctx); +} + +TER +ExportSign::preclaim(PreclaimContext const& ctx) +{ + if (!ctx.view.rules().enabled(featureExport)) + return temDISABLED; + + // Signer must be in the UNLReport + auto const& pkSignerField = ctx.tx.getSigningPubKey(); + if (!publicKeyType(makeSlice(pkSignerField))) + { + JLOG(ctx.j.warn()) << "ExportSign: Invalid signing public key type."; + return tefBAD_AUTH; + } + + PublicKey pkSigner{makeSlice(pkSignerField)}; + + if (!inUNLReport(ctx.view, ctx.app, pkSigner, ctx.j)) + { + JLOG(ctx.j.warn()) << "ExportSign: Signer isn't in the UNLReport."; + return tefFAILURE; + } + + return tesSUCCESS; +} + +TER +ExportSign::doApply() +{ + uint256 txnID(ctx_.tx.getFieldH256(sfTransactionHash)); + + JLOG(j_.info()) << "HookExport[" << txnID + << "]: ttExportSign adding signature to transaction"; + + auto key = keylet::exportedTxn(txnID); + auto const& sle = view().peek(key); + + if (!sle) + { + // Most likely this was a double-up, so just ignore + JLOG(j_.warn()) << "HookError[" << txnID + << "]: ttExportSign could not find exported txn in " + "ledger"; + return tesSUCCESS; + } + + // Grab the signer object off the txn + STObject signerObj = const_cast(ctx_.tx) + .getField(sfSigner) + .downcast(); + + // Append it to the signers field in the ledger object + STArray signers = sle->getFieldArray(sfSigners); + signers.push_back(signerObj); + sle->setFieldArray(sfSigners, signers); + + view().update(sle); + + return tesSUCCESS; +} +//@@end exportsign-transactor + +//@@start make-export-sign-txns +std::vector> +makeExportSignTxns(OpenView& view, Application& app, beast::Journal const& j) +{ + std::vector> result; + + if (!view.rules().enabled(featureExport)) + return result; + + JLOG(j.debug()) << "EXPORT_SIGN processing: started"; + + auto const seq = view.info().seq; + + // If we're not a validator we do nothing here + if (app.getValidationPublicKey().empty()) + return result; + + auto const& keys = app.getValidatorKeys(); + + if (keys.configInvalid()) + return result; + + PublicKey pkSigning = app.getValidationPublicKey(); + auto const pk = app.validatorManifests().getMasterKey(pkSigning); + + // Only continue if we're on the UNLReport + if (!inUNLReport(view, app, pk, j)) + return result; + + AccountID signingAcc = calcAccountID(pkSigning); + + Keylet const exportedDirKeylet{keylet::exportedDir()}; + if (dirIsEmpty(view, exportedDirKeylet)) + return result; + + std::shared_ptr sleDirNode{}; + unsigned int uDirEntry{0}; + uint256 dirEntry{beast::zero}; + + if (!cdirFirst( + view, exportedDirKeylet.key, sleDirNode, uDirEntry, dirEntry)) + return result; + + do + { + Keylet const itemKeylet{ltCHILD, dirEntry}; + auto sleItem = view.read(itemKeylet); + if (!sleItem) + { + JLOG(j.warn()) + << "ExportedTxn processing: directory node in ledger " << seq + << " has index to object that is missing: " + << to_string(dirEntry); + continue; + } + + LedgerEntryType const nodeType{ + safe_cast((*sleItem)[sfLedgerEntryType])}; + + if (nodeType != ltEXPORTED_TXN) + { + JLOG(j.warn()) << "ExportedTxn processing: exported directory " + "contained non ltEXPORTED_TXN type"; + continue; + } + + auto const& exported = const_cast(*sleItem) + .getField(sfExportedTxn) + .downcast(); + + auto exportedLgrSeq = exported.getFieldU32(sfLedgerSequence); + + // Only sign transactions that were added in the previous ledger + if (exportedLgrSeq != seq - 1) + continue; + + auto s = std::make_shared(); + exported.add(*s); + SerialIter sitTrans(s->slice()); + try + { + auto const& stpTrans = + std::make_shared(std::ref(sitTrans)); + + if (!stpTrans->isFieldPresent(sfAccount) || + stpTrans->getAccountID(sfAccount) == beast::zero) + { + JLOG(j.warn()) + << "Hook: Export failure: sfAccount missing or zero."; + continue; + } + + auto txnHash = stpTrans->getTransactionID(); + + // Build the multisig for the inner exported transaction + Serializer sigData = buildMultiSigningData(*stpTrans, signingAcc); + auto multisig = + ripple::sign(keys.publicKey, keys.secretKey, sigData.slice()); + + // Create the ttEXPORT_SIGN transaction + auto exportSignTx = + std::make_shared(ttEXPORT_SIGN, [&](auto& obj) { + obj.set(([&]() { + auto inner = std::make_unique(sfSigner); + inner->setFieldVL(sfSigningPubKey, keys.publicKey); + inner->setAccountID(sfAccount, signingAcc); + inner->setFieldVL(sfTxnSignature, multisig); + return inner; + })()); + obj.setFieldU32(sfLedgerSequence, seq); + obj.setFieldH256(sfTransactionHash, txnHash); + obj.setAccountID(sfAccount, calcAccountID(pk)); + obj.setFieldU32(sfSequence, 0); + obj.setFieldVL(sfSigningPubKey, pkSigning.slice()); + obj.setFieldU32(sfFlags, tfFullyCanonicalSig); + + if (app.config().NETWORK_ID > 1024) + obj.setFieldU32(sfNetworkID, app.config().NETWORK_ID); + }); + + // Sign the outer transaction using our ephemeral key + exportSignTx->sign(pkSigning, app.getValidationSecretKey()); + + JLOG(j.debug()) + << "EXPORT_SIGN txn: " << exportSignTx->getFullText(); + + result.push_back(exportSignTx); + } + catch (std::exception& e) + { + JLOG(j.warn()) << "ExportedTxn Processing: Failure: " << e.what() + << "\n"; + } + + } while ( + cdirNext(view, exportedDirKeylet.key, sleDirNode, uDirEntry, dirEntry)); + + return result; +} +//@@end make-export-sign-txns + +} // namespace ripple diff --git a/src/ripple/app/tx/impl/ExportSign.h b/src/ripple/app/tx/impl/ExportSign.h new file mode 100644 index 000000000..6f201bb2b --- /dev/null +++ b/src/ripple/app/tx/impl/ExportSign.h @@ -0,0 +1,78 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 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_EXPORTSIGN_H_INCLUDED +#define RIPPLE_TX_EXPORTSIGN_H_INCLUDED + +#include +#include +#include +#include +#include +#include + +namespace ripple { + +class OpenView; + +//@@start exportsign-class +/** + * ExportSign is a UVTxn (UNL Validator Transaction) that allows validators + * on the UNLReport to submit their signatures for exported cross-chain + * transactions without requiring a funded account. + */ +class ExportSign : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Custom}; + + explicit ExportSign(ApplyContext& ctx) : Transactor(ctx) + { + } + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx) + { + return XRPAmount{0}; + } + + static TxConsequences + makeTxConsequences(PreflightContext const& ctx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +/** + * If this validator is on the UNLReport, generate signed ttEXPORT_SIGN + * transactions for any exported transactions that need signing. + */ +std::vector> +makeExportSignTxns(OpenView& view, Application& app, beast::Journal const& j); +//@@end exportsign-class + +} // namespace ripple + +#endif diff --git a/src/ripple/app/tx/impl/Import.cpp b/src/ripple/app/tx/impl/Import.cpp index 364e73b5c..e4b2d691c 100644 --- a/src/ripple/app/tx/impl/Import.cpp +++ b/src/ripple/app/tx/impl/Import.cpp @@ -41,8 +41,10 @@ namespace ripple { +//@@start shadow-ticket-namespace static const uint256 shadowTicketNamespace = uint256::fromVoid("RESERVED NAMESPACE SHADOW TICKET"); +//@@end shadow-ticket-namespace TxConsequences Import::makeTxConsequences(PreflightContext const& ctx) @@ -201,12 +203,14 @@ Import::preflight(PreflightContext const& ctx) if (!stpTrans || !meta) return temMALFORMED; + //@@start import-ticket-preflight if (stpTrans->isFieldPresent(sfTicketSequence) && !ctx.rules.enabled(featureExport)) { JLOG(ctx.j.warn()) << "Import: cannot use TicketSequence XPOP."; return temMALFORMED; } + //@@end import-ticket-preflight // check if txn is emitted or a psuedo if (isPseudoTx(*stpTrans) || stpTrans->isFieldPresent(sfEmitDetails)) @@ -893,6 +897,7 @@ Import::preclaim(PreclaimContext const& ctx) return tefINTERNAL; } + //@@start import-shadow-ticket-preclaim bool const hasTicket = stpTrans->isFieldPresent(sfTicketSequence); if (hasTicket) @@ -916,6 +921,7 @@ Import::preclaim(PreclaimContext const& ctx) // SF_BAD } } + //@@end import-shadow-ticket-preclaim auto const& sle = ctx.view.read(keylet::account(ctx.tx[sfAccount])); @@ -957,6 +963,7 @@ Import::preclaim(PreclaimContext const& ctx) } while (0); } + //@@start import-ticket-replay-check if (!hasTicket) { if (sle && sle->isFieldPresent(sfImportSequence)) @@ -968,6 +975,7 @@ Import::preclaim(PreclaimContext const& ctx) return tefPAST_IMPORT_SEQ; } } + //@@end import-ticket-replay-check // when importing for the first time the fee must be zero if (!sle && ctx.tx.getFieldAmount(sfFee) != beast::zero) @@ -1274,6 +1282,7 @@ Import::doApply() auto const id = ctx_.tx[sfAccount]; auto sle = view().peek(keylet::account(id)); + //@@start import-ticket-apply-check std::optional ticket; if (stpTrans->isFieldPresent(sfTicketSequence)) ticket = uint256(stpTrans->getFieldU32(sfTicketSequence)); @@ -1281,6 +1290,7 @@ Import::doApply() if (sle && !ticket.has_value() && sle->getFieldU32(sfImportSequence) >= importSequence) { + //@@end import-ticket-apply-check // make double sure import seq hasn't passed JLOG(ctx_.journal.warn()) << "Import: ImportSequence passed"; return tefINTERNAL; @@ -1372,6 +1382,7 @@ Import::doApply() } } + //@@start import-ticket-apply-finalize if (!ticket.has_value()) sle->setFieldU32(sfImportSequence, importSequence); @@ -1391,6 +1402,7 @@ Import::doApply() // RHUPTO: ticketseq billing? } + //@@end import-ticket-apply-finalize if (create) { diff --git a/src/ripple/app/tx/impl/SetHook.cpp b/src/ripple/app/tx/impl/SetHook.cpp index a3dddf359..38db64bc9 100644 --- a/src/ripple/app/tx/impl/SetHook.cpp +++ b/src/ripple/app/tx/impl/SetHook.cpp @@ -486,6 +486,7 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj) hsacc = ss.str(); } + //@@start sethook-rules-version auto result = validateGuards( hook, // wasm to verify logger, @@ -493,6 +494,7 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj) (ctx.rules.enabled(featureHooksUpdate1) ? 1 : 0) + (ctx.rules.enabled(fix20250131) ? 2 : 0) + (ctx.rules.enabled(featureExport) ? 4 : 0)); + //@@end sethook-rules-version if (ctx.j.trace()) { diff --git a/src/ripple/app/tx/impl/Transactor.cpp b/src/ripple/app/tx/impl/Transactor.cpp index 1f16f12b3..1ffe0fca5 100644 --- a/src/ripple/app/tx/impl/Transactor.cpp +++ b/src/ripple/app/tx/impl/Transactor.cpp @@ -450,6 +450,7 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee) { auto feeDue = minimumFee(ctx.app, baseFee, ctx.view.fees(), ctx.flags); + //@@start uvtx-fee-waiver // UVTxns from validators in UNLReport don't have to pay a fee if (ctx.view.rules().enabled(featureExport) && isUVTx(ctx.tx)) { @@ -461,6 +462,7 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee) feeDue = beast::zero; } } + //@@end uvtx-fee-waiver if (feePaid < feeDue) { @@ -478,9 +480,11 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee) auto const sle = ctx.view.read(keylet::account(id)); if (!sle) { + //@@start uvtx-account-check // UVTxns don't need an underlying account if (isUVTx(ctx.tx)) return tesSUCCESS; + //@@end uvtx-account-check if (ctx.tx.getTxnType() == ttIMPORT) { @@ -656,8 +660,10 @@ Transactor::checkPriorTxAndLastLedger(PreclaimContext const& ctx) ctx.view.rules().enabled(featureImport) && ctx.tx.getTxnType() == ttIMPORT && !ctx.tx.isFieldPresent(sfIssuer); + //@@start uvtx-preclaim-account // UVTxns don't require an underlying account bool const accRequired = !(isFirstImport || isUVTx(ctx.tx)); + //@@end uvtx-preclaim-account if (!sle && accRequired) { @@ -814,6 +820,7 @@ Transactor::apply() // list one, preflight will have already a flagged a failure. auto const sle = view().peek(keylet::account(account_)); + //@@start uvtx-apply-assert // sle must exist except for transactions // that allow zero account. (ttIMPORT and UVTxns) assert( @@ -822,6 +829,7 @@ Transactor::apply() ctx_.tx.getTxnType() == ttIMPORT && !ctx_.tx.isFieldPresent(sfIssuer) || isUVTx(ctx_.tx)); + //@@end uvtx-apply-assert if (sle) { diff --git a/src/ripple/app/tx/impl/applySteps.cpp b/src/ripple/app/tx/impl/applySteps.cpp index 043e7dfc1..2694aab53 100644 --- a/src/ripple/app/tx/impl/applySteps.cpp +++ b/src/ripple/app/tx/impl/applySteps.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -147,16 +148,17 @@ invoke_preflight(PreflightContext const& ctx) return invoke_preflight_helper(ctx); case ttTRUST_SET: return invoke_preflight_helper(ctx); - //@@start export-sign-preflight case ttAMENDMENT: case ttFEE: case ttUNL_MODIFY: case ttUNL_REPORT: case ttEMIT_FAILURE: + //@@start export-preflight case ttEXPORT: - case ttEXPORT_SIGN: return invoke_preflight_helper(ctx); - //@@end export-sign-preflight + case ttEXPORT_SIGN: + return invoke_preflight_helper(ctx); + //@@end export-preflight case ttHOOK_SET: return invoke_preflight_helper(ctx); case ttNFTOKEN_MINT: @@ -282,16 +284,17 @@ invoke_preclaim(PreclaimContext const& ctx) return invoke_preclaim(ctx); case ttHOOK_SET: return invoke_preclaim(ctx); - //@@start export-sign-preclaim case ttAMENDMENT: case ttFEE: case ttUNL_MODIFY: case ttUNL_REPORT: case ttEMIT_FAILURE: + //@@start export-preclaim case ttEXPORT: - case ttEXPORT_SIGN: return invoke_preclaim(ctx); - //@@end export-sign-preclaim + case ttEXPORT_SIGN: + return invoke_preclaim(ctx); + //@@end export-preclaim case ttNFTOKEN_MINT: return invoke_preclaim(ctx); case ttNFTOKEN_BURN: @@ -377,16 +380,17 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx) return SetTrust::calculateBaseFee(view, tx); case ttHOOK_SET: return SetHook::calculateBaseFee(view, tx); - //@@start export-sign-fee case ttAMENDMENT: case ttFEE: case ttUNL_MODIFY: case ttUNL_REPORT: case ttEMIT_FAILURE: - case ttEXPORT_SIGN: + //@@start export-basefee case ttEXPORT: return Change::calculateBaseFee(view, tx); - //@@end export-sign-fee + case ttEXPORT_SIGN: + return ExportSign::calculateBaseFee(view, tx); + //@@end export-basefee case ttNFTOKEN_MINT: return NFTokenMint::calculateBaseFee(view, tx); case ttNFTOKEN_BURN: @@ -552,18 +556,21 @@ invoke_apply(ApplyContext& ctx) SetHook p(ctx); return p(); } - //@@start export-sign-apply case ttAMENDMENT: case ttFEE: case ttUNL_MODIFY: case ttUNL_REPORT: + //@@start export-apply case ttEXPORT: - case ttEXPORT_SIGN: case ttEMIT_FAILURE: { Change p(ctx); return p(); } - //@@end export-sign-apply + case ttEXPORT_SIGN: { + ExportSign p(ctx); + return p(); + } + //@@end export-apply case ttNFTOKEN_MINT: { NFTokenMint p(ctx); return p(); diff --git a/src/ripple/ledger/View.h b/src/ripple/ledger/View.h index 254f19021..59c651725 100644 --- a/src/ripple/ledger/View.h +++ b/src/ripple/ledger/View.h @@ -1097,6 +1097,7 @@ trustTransferLockedBalance( return tesSUCCESS; } +//@@start inUNLReport-account /** * Check if an account (derived from a validator's master public key) is * in the UNLReport's ActiveValidators list. @@ -1145,7 +1146,9 @@ inUNLReport(V const& view, AccountID const& id, beast::Journal const& j) return cache[id] = false; } +//@@end inUNLReport-account +//@@start inUNLReport-pubkey /** * Check if a public key (or its master key via manifest lookup) is * in the UNLReport's ActiveValidators list. @@ -1163,6 +1166,7 @@ inUNLReport( return inUNLReport(view, calcAccountID(pk), j) || (uvPk != pk && inUNLReport(view, calcAccountID(uvPk), j)); } +//@@end inUNLReport-pubkey } // namespace ripple diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 180cd4cc2..b897c340f 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -378,7 +378,9 @@ extern uint256 const fixInvalidTxFlags; extern uint256 const featureExtendedHookState; extern uint256 const fixCronStacking; extern uint256 const fixHookAPI20251128; +//@@start feature-export extern uint256 const featureExport; +//@@end feature-export } // namespace ripple #endif diff --git a/src/ripple/protocol/Indexes.h b/src/ripple/protocol/Indexes.h index b21f8adf4..35a67e4f4 100644 --- a/src/ripple/protocol/Indexes.h +++ b/src/ripple/protocol/Indexes.h @@ -56,14 +56,18 @@ namespace keylet { Keylet const& emittedDir() noexcept; +//@@start keylet-exported-dir-decl Keylet const& exportedDir() noexcept; +//@@end keylet-exported-dir-decl Keylet emittedTxn(uint256 const& id) noexcept; +//@@start keylet-exported-txn-decl Keylet exportedTxn(uint256 const& id) noexcept; +//@@end keylet-exported-txn-decl Keylet hookDefinition(uint256 const& hash) noexcept; diff --git a/src/ripple/protocol/LedgerFormats.h b/src/ripple/protocol/LedgerFormats.h index de5be4357..32d932878 100644 --- a/src/ripple/protocol/LedgerFormats.h +++ b/src/ripple/protocol/LedgerFormats.h @@ -261,7 +261,9 @@ enum LedgerEntryType : std::uint16_t */ ltEMITTED_TXN = 'E', + //@@start lt-exported-txn ltEXPORTED_TXN = 0x4578, // Ex (exported transaction) + //@@end lt-exported-txn }; // clang-format off diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index 9cafd8623..2ec4758bd 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -355,7 +355,9 @@ extern SF_UINT16 const sfHookEmitCount; extern SF_UINT16 const sfHookExecutionIndex; extern SF_UINT16 const sfHookApiVersion; extern SF_UINT16 const sfHookStateScale; +//@@start sf-hook-export-count extern SF_UINT16 const sfHookExportCount; +//@@end sf-hook-export-count // 32-bit integers (common) extern SF_UINT32 const sfNetworkID; @@ -596,7 +598,9 @@ extern SField const sfSigner; extern SField const sfMajority; extern SField const sfDisabledValidator; extern SField const sfEmittedTxn; +//@@start sf-exported-txn extern SField const sfExportedTxn; +//@@end sf-exported-txn extern SField const sfHookExecution; extern SField const sfHookDefinition; extern SField const sfHookParameter; diff --git a/src/ripple/protocol/STTx.h b/src/ripple/protocol/STTx.h index 59ef546b7..5b86aa15d 100644 --- a/src/ripple/protocol/STTx.h +++ b/src/ripple/protocol/STTx.h @@ -171,9 +171,12 @@ sterilize(STTx const& stx); bool isPseudoTx(STObject const& tx); +/** Check whether a transaction is a UNL Validator Transaction (UVTx) */ +//@@start is-uvtx-decl /** Check whether a transaction is a UNL Validator Transaction (UVTx) */ bool isUVTx(STObject const& tx); +//@@end is-uvtx-decl inline STTx::STTx(SerialIter&& sit) : STTx(sit) { diff --git a/src/ripple/protocol/TER.h b/src/ripple/protocol/TER.h index 5256e8d75..b1e15fffe 100644 --- a/src/ripple/protocol/TER.h +++ b/src/ripple/protocol/TER.h @@ -67,7 +67,9 @@ enum TELcodes : TERUnderlyingType { telNON_LOCAL_EMITTED_TXN, telIMPORT_VL_KEY_NOT_RECOGNISED, telCAN_NOT_QUEUE_IMPORT, + //@@start tel-shadow-ticket telSHADOW_TICKET_REQUIRED, + //@@end tel-shadow-ticket }; //------------------------------------------------------------------------------ diff --git a/src/ripple/protocol/TxFormats.h b/src/ripple/protocol/TxFormats.h index f3fcc7358..7f2fdd75b 100644 --- a/src/ripple/protocol/TxFormats.h +++ b/src/ripple/protocol/TxFormats.h @@ -149,11 +149,13 @@ enum TxType : std::uint16_t ttURITOKEN_CREATE_SELL_OFFER = 48, ttURITOKEN_CANCEL_SELL_OFFER = 49, + //@@start tt-export /* A pseudo-txn containing an exported transaction plus signatures from the validators */ ttEXPORT = 90, /* A pseudo-txn containing a validator's signature for an export transaction */ ttEXPORT_SIGN = 91, + //@@end tt-export /* A pseudo-txn alarm signal for invoking a hook, emitted by validators after alarm set conditions are met */ ttCRON = 92, diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index de47c7406..3df6b4831 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -484,7 +484,9 @@ REGISTER_FIX (fixInvalidTxFlags, Supported::yes, VoteBehavior::De REGISTER_FEATURE(ExtendedHookState, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FIX (fixCronStacking, Supported::yes, VoteBehavior::DefaultYes); REGISTER_FIX (fixHookAPI20251128, Supported::yes, VoteBehavior::DefaultYes); +//@@start register-export REGISTER_FEATURE(Export, Supported::yes, VoteBehavior::DefaultNo); +//@@end register-export // The following amendments are obsolete, but must remain supported // because they could potentially get enabled. diff --git a/src/ripple/protocol/impl/Indexes.cpp b/src/ripple/protocol/impl/Indexes.cpp index 4b9f1229c..c50090c0c 100644 --- a/src/ripple/protocol/impl/Indexes.cpp +++ b/src/ripple/protocol/impl/Indexes.cpp @@ -66,8 +66,10 @@ enum class LedgerNameSpace : std::uint16_t { HOOK_DEFINITION = 'D', EMITTED_TXN = 'E', EMITTED_DIR = 'F', + //@@start ledger-namespace-export EXPORTED_TXN = 0x4578, // Ex EXPORTED_DIR = 0x4564, // Ed + //@@end ledger-namespace-export NFTOKEN_OFFER = 'q', NFTOKEN_BUY_OFFERS = 'h', NFTOKEN_SELL_OFFERS = 'i', diff --git a/src/ripple/protocol/impl/InnerObjectFormats.cpp b/src/ripple/protocol/impl/InnerObjectFormats.cpp index 4c2500ff2..18b74b2c4 100644 --- a/src/ripple/protocol/impl/InnerObjectFormats.cpp +++ b/src/ripple/protocol/impl/InnerObjectFormats.cpp @@ -73,6 +73,7 @@ InnerObjectFormats::InnerObjectFormats() {sfHookExecutionIndex, soeREQUIRED}, {sfHookStateChangeCount, soeREQUIRED}, {sfHookEmitCount, soeREQUIRED}, + {sfHookExportCount, soeREQUIRED}, {sfFlags, soeOPTIONAL}}); add(sfHookEmission.jsonName.c_str(), diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index 8342e1eab..9e2445fa1 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -381,6 +381,7 @@ LedgerFormats::LedgerFormats() }, commonFields); + //@@start exported-txn-format add(jss::ExportedTxn, ltEXPORTED_TXN, { @@ -389,6 +390,7 @@ LedgerFormats::LedgerFormats() {sfLedgerSequence, soeREQUIRED}, }, commonFields); + //@@end exported-txn-format // clang-format on } diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index 9d266770e..1097b0c8b 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -103,7 +103,9 @@ CONSTRUCT_TYPED_SFIELD(sfHookEmitCount, "HookEmitCount", UINT16, CONSTRUCT_TYPED_SFIELD(sfHookExecutionIndex, "HookExecutionIndex", UINT16, 19); CONSTRUCT_TYPED_SFIELD(sfHookApiVersion, "HookApiVersion", UINT16, 20); CONSTRUCT_TYPED_SFIELD(sfHookStateScale, "HookStateScale", UINT16, 21); +//@@start construct-sf-hook-export-count CONSTRUCT_TYPED_SFIELD(sfHookExportCount, "HookExportCount", UINT16, 22); +//@@end construct-sf-hook-export-count // 32-bit integers (common) CONSTRUCT_TYPED_SFIELD(sfNetworkID, "NetworkID", UINT32, 1); @@ -362,7 +364,9 @@ CONSTRUCT_UNTYPED_SFIELD(sfImportVLKey, "ImportVLKey", OBJECT, CONSTRUCT_UNTYPED_SFIELD(sfHookEmission, "HookEmission", OBJECT, 93); CONSTRUCT_UNTYPED_SFIELD(sfMintURIToken, "MintURIToken", OBJECT, 92); CONSTRUCT_UNTYPED_SFIELD(sfAmountEntry, "AmountEntry", OBJECT, 91); +//@@start construct-sf-exported-txn CONSTRUCT_UNTYPED_SFIELD(sfExportedTxn, "ExportedTxn", OBJECT, 90); +//@@end construct-sf-exported-txn // array of objects // ARRAY/1 is reserved for end of array diff --git a/src/ripple/protocol/impl/TER.cpp b/src/ripple/protocol/impl/TER.cpp index f31105782..ad7085e1e 100644 --- a/src/ripple/protocol/impl/TER.cpp +++ b/src/ripple/protocol/impl/TER.cpp @@ -141,7 +141,9 @@ transResults() MAKE_ERROR(telNON_LOCAL_EMITTED_TXN, "Emitted transaction cannot be applied because it was not generated locally."), MAKE_ERROR(telIMPORT_VL_KEY_NOT_RECOGNISED, "Import vl key was not recognized."), MAKE_ERROR(telCAN_NOT_QUEUE_IMPORT, "Import transaction was not able to be directly applied and cannot be queued."), + //@@start ter-shadow-ticket MAKE_ERROR(telSHADOW_TICKET_REQUIRED, "The imported transaction uses a TicketSequence but no shadow ticket exists."), + //@@end ter-shadow-ticket MAKE_ERROR(temMALFORMED, "Malformed transaction."), MAKE_ERROR(temBAD_AMOUNT, "Can only send positive amounts."), MAKE_ERROR(temBAD_CURRENCY, "Malformed: Bad currency."), diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index cb2a313ac..928d5a56a 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -491,6 +491,7 @@ TxFormats::TxFormats() }, commonFields); + //@@start export-formats add(jss::ExportSign, ttEXPORT_SIGN, { @@ -505,10 +506,13 @@ TxFormats::TxFormats() { {sfTransactionHash, soeREQUIRED}, {sfExportedTxn, soeREQUIRED}, - {sfSigners, soeREQUIRED}, + // We can't actually do this because it's already optional so it + // causes parse failures Enforce this in the transactor. {sfSigners, + // soeREQUIRED}, {sfLedgerSequence, soeREQUIRED}, }, commonFields); + //@@end export-formats } TxFormats const& diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index 869ca9174..1d4cd4a08 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -140,9 +140,11 @@ JSS(HookState); // ledger type. JSS(HookStateData); // field. JSS(HookStateKey); // field. JSS(EmittedTxn); // ledger type. +//@@start jss-export JSS(ExportedTxn); JSS(Export); JSS(ExportSign); +//@@end jss-export JSS(SignerList); // ledger type. JSS(SignerListSet); // transaction type. JSS(SigningPubKey); // field. diff --git a/src/test/app/Export_test.cpp b/src/test/app/Export_test.cpp new file mode 100644 index 000000000..2de61a2a5 --- /dev/null +++ b/src/test/app/Export_test.cpp @@ -0,0 +1,101 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 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 + +namespace ripple { +namespace test { + +using TestHook = std::vector const&; + +// Large fee for hook operations +#define HSFEE fee(100'000'000) + +struct Export_test : public beast::unit_test::suite +{ + // Bare bones hook that just accepts + TestHook accept_wasm = export_test_wasm[R"[test.hook]( + #include + extern int32_t _g(uint32_t id, uint32_t maxiter); + extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code); + + int64_t hook(uint32_t reserved) { + _g(1, 1); + accept(0, 0, 0); + return 0; + } + )[test.hook]"]; + + void + testBasicSetup(FeatureBitset features) + { + testcase("Basic Setup"); + + using namespace jtx; + + auto severity = beast::severities::kNone; + // Minimal setup: validator keys + amendments + Env env{*this, envconfig(), features, nullptr, severity}; + + Account const alice{"alice"}; // Hook owner + Account const bob{"bob"}; // Sender + + // Fund accounts + env.fund(XRP(10000), alice, bob); + env.close(); + + // Verify setup + BEAST_EXPECT(env.current()->seq() < 256); // Grace period + // Note: Validator key check removed - not needed for basic hook testing + + // Deploy hook on alice + env(ripple::test::jtx::hook(alice, {{hso(accept_wasm)}}, 0), + HSFEE, + ter(tesSUCCESS)); + env.close(); + + // Bob sends payment to alice (triggers hook) + auto const alicePreBal = env.balance(alice); + auto const bobPreBal = env.balance(bob); + + env(pay(bob, alice, XRP(100)), fee(XRP(1)), ter(tesSUCCESS)); + env.close(); + + // Verify payment went through + BEAST_EXPECT(env.balance(alice) == alicePreBal + XRP(100)); + } + + void + run() override + { + using namespace test::jtx; + FeatureBitset const all{supported_amendments()}; + testBasicSetup(all); + } +}; + +BEAST_DEFINE_TESTSUITE(Export, app, ripple); + +} // namespace test +} // namespace ripple diff --git a/src/test/app/Export_test_hooks.h b/src/test/app/Export_test_hooks.h new file mode 100644 index 000000000..718ee2f4f --- /dev/null +++ b/src/test/app/Export_test_hooks.h @@ -0,0 +1,45 @@ + +// This file is generated by build_test_hooks.py +#ifndef EXPORT_TEST_WASM_INCLUDED +#define EXPORT_TEST_WASM_INCLUDED +#include +#include +#include +#include +namespace ripple { +namespace test { +std::map> export_test_wasm = { + /* ==== WASM: 0 ==== */ + {R"[test.hook]( + #include + extern int32_t _g(uint32_t id, uint32_t maxiter); + extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code); + + int64_t hook(uint32_t reserved) { + _g(1, 1); + accept(0, 0, 0); + return 0; + } + )[test.hook]", + { + 0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U, 0x13U, + 0x03U, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7FU, 0x60U, 0x03U, 0x7FU, + 0x7FU, 0x7EU, 0x01U, 0x7EU, 0x60U, 0x01U, 0x7FU, 0x01U, 0x7EU, 0x02U, + 0x17U, 0x02U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x02U, 0x5FU, 0x67U, 0x00U, + 0x00U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x06U, 0x61U, 0x63U, 0x63U, 0x65U, + 0x70U, 0x74U, 0x00U, 0x01U, 0x03U, 0x02U, 0x01U, 0x02U, 0x05U, 0x03U, + 0x01U, 0x00U, 0x02U, 0x06U, 0x21U, 0x05U, 0x7FU, 0x01U, 0x41U, 0x80U, + 0x88U, 0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x7FU, + 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x88U, + 0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x07U, 0x08U, + 0x01U, 0x04U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x00U, 0x02U, 0x0AU, 0xA0U, + 0x80U, 0x00U, 0x01U, 0x9CU, 0x80U, 0x00U, 0x00U, 0x41U, 0x01U, 0x41U, + 0x01U, 0x10U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x41U, 0x00U, + 0x41U, 0x00U, 0x42U, 0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U, + 0x1AU, 0x42U, 0x00U, 0x0BU, + }}, + +}; +} +} // namespace ripple +#endif