refactor: separate ExportSign transactor from Change

Move ttEXPORT_SIGN handling to dedicated ExportSign transactor class,
following the same pattern as ttENTROPY/Entropy from the RNG feature.
UVTxns (signed validator transactions) should not be mixed with
pseudo-transactions in the Change transactor.

- Create ExportSign.h/cpp with preflight, preclaim, doApply
- Route ttEXPORT_SIGN through ExportSign in applySteps.cpp
- Remove UVTx branches from Change transactor
- Add documentation markers to View.h for inUNLReport functions
This commit is contained in:
Nicholas Dudfield
2026-01-20 14:14:15 +07:00
parent c01b9a657b
commit dc6a2dc6ff
35 changed files with 634 additions and 288 deletions

View File

@@ -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

View File

@@ -35,7 +35,7 @@
#include <ripple/app/misc/TxQ.h>
#include <ripple/app/misc/ValidatorKeys.h>
#include <ripple/app/misc/ValidatorList.h>
#include <ripple/app/tx/impl/Change.h>
#include <ripple/app/tx/impl/ExportSign.h>
#include <ripple/basics/random.h>
#include <ripple/beast/core/LexicalCast.h>
#include <ripple/consensus/LedgerTiming.h>

View File

@@ -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

View File

@@ -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)

View File

@@ -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<std::shared_ptr<ripple::Transaction>>
emittedTxn{}; // etx stored here until accept/rollback
//@@start hook-result-exported-txn
std::queue<std::shared_ptr<ripple::Transaction>> 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<ripple::uint256, bool> 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);

View File

@@ -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(

View File

@@ -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

View File

@@ -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&

View File

@@ -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)

View File

@@ -23,17 +23,14 @@
#include <ripple/app/main/Application.h>
#include <ripple/app/misc/AmendmentTable.h>
#include <ripple/app/misc/NetworkOPs.h>
#include <ripple/app/misc/ValidatorKeys.h>
#include <ripple/app/tx/impl/Change.h>
#include <ripple/app/tx/impl/SetSignerList.h>
#include <ripple/app/tx/impl/XahauGenesis.h>
#include <ripple/basics/Log.h>
#include <ripple/ledger/OpenView.h>
#include <ripple/ledger/Sandbox.h>
#include <ripple/protocol/AccountID.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/Sign.h>
#include <ripple/protocol/TxFlags.h>
#include <string_view>
@@ -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<ripple::STTx&>(ctx_.tx)
.getField(sfSigner)
.downcast<STObject>();
// 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<std::shared_ptr<STTx const>>
makeExportSignTxns(OpenView& view, Application& app, beast::Journal const& j)
{
std::vector<std::shared_ptr<STTx const>> 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<SLE const> 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<LedgerEntryType>((*sleItem)[sfLedgerEntryType])};
if (nodeType != ltEXPORTED_TXN)
{
JLOG(j.warn()) << "ExportedTxn processing: exported directory "
"contained non ltEXPORTED_TXN type";
continue;
}
auto const& exported = const_cast<ripple::STLedgerEntry&>(*sleItem)
.getField(sfExportedTxn)
.downcast<STObject>();
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<ripple::Serializer>();
exported.add(*s);
SerialIter sitTrans(s->slice());
try
{
auto const& stpTrans =
std::make_shared<STTx const>(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<STTx>(ttEXPORT_SIGN, [&](auto& obj) {
obj.set(([&]() {
auto inner = std::make_unique<STObject>(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

View File

@@ -25,7 +25,6 @@
#include <ripple/app/misc/NetworkOPs.h>
#include <ripple/app/tx/impl/Transactor.h>
#include <ripple/basics/Log.h>
#include <ripple/ledger/OpenView.h>
#include <ripple/protocol/Indexes.h>
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<std::shared_ptr<STTx const>>
makeExportSignTxns(OpenView& view, Application& app, beast::Journal const& j);
} // namespace ripple
#endif

View File

@@ -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 <ripple/app/main/Application.h>
#include <ripple/app/misc/ValidatorKeys.h>
#include <ripple/app/tx/impl/ExportSign.h>
#include <ripple/basics/Log.h>
#include <ripple/ledger/OpenView.h>
#include <ripple/ledger/View.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/Sign.h>
#include <ripple/protocol/TxFlags.h>
#include <ripple/protocol/st.h>
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<ripple::STTx&>(ctx_.tx)
.getField(sfSigner)
.downcast<STObject>();
// 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<std::shared_ptr<STTx const>>
makeExportSignTxns(OpenView& view, Application& app, beast::Journal const& j)
{
std::vector<std::shared_ptr<STTx const>> 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<SLE const> 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<LedgerEntryType>((*sleItem)[sfLedgerEntryType])};
if (nodeType != ltEXPORTED_TXN)
{
JLOG(j.warn()) << "ExportedTxn processing: exported directory "
"contained non ltEXPORTED_TXN type";
continue;
}
auto const& exported = const_cast<ripple::STLedgerEntry&>(*sleItem)
.getField(sfExportedTxn)
.downcast<STObject>();
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<ripple::Serializer>();
exported.add(*s);
SerialIter sitTrans(s->slice());
try
{
auto const& stpTrans =
std::make_shared<STTx const>(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<STTx>(ttEXPORT_SIGN, [&](auto& obj) {
obj.set(([&]() {
auto inner = std::make_unique<STObject>(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

View File

@@ -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 <ripple/app/misc/Manifest.h>
#include <ripple/app/tx/impl/Transactor.h>
#include <ripple/basics/Log.h>
#include <ripple/core/Config.h>
#include <ripple/ledger/View.h>
#include <ripple/protocol/Indexes.h>
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<std::shared_ptr<STTx const>>
makeExportSignTxns(OpenView& view, Application& app, beast::Journal const& j);
//@@end exportsign-class
} // namespace ripple
#endif

View File

@@ -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<uint256> 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)
{

View File

@@ -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())
{

View File

@@ -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)
{

View File

@@ -33,6 +33,7 @@
#include <ripple/app/tx/impl/DeleteAccount.h>
#include <ripple/app/tx/impl/DepositPreauth.h>
#include <ripple/app/tx/impl/Escrow.h>
#include <ripple/app/tx/impl/ExportSign.h>
#include <ripple/app/tx/impl/GenesisMint.h>
#include <ripple/app/tx/impl/Import.h>
#include <ripple/app/tx/impl/Invoke.h>
@@ -147,16 +148,17 @@ invoke_preflight(PreflightContext const& ctx)
return invoke_preflight_helper<CreateTicket>(ctx);
case ttTRUST_SET:
return invoke_preflight_helper<SetTrust>(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<Change>(ctx);
//@@end export-sign-preflight
case ttEXPORT_SIGN:
return invoke_preflight_helper<ExportSign>(ctx);
//@@end export-preflight
case ttHOOK_SET:
return invoke_preflight_helper<SetHook>(ctx);
case ttNFTOKEN_MINT:
@@ -282,16 +284,17 @@ invoke_preclaim(PreclaimContext const& ctx)
return invoke_preclaim<SetTrust>(ctx);
case ttHOOK_SET:
return invoke_preclaim<SetHook>(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<Change>(ctx);
//@@end export-sign-preclaim
case ttEXPORT_SIGN:
return invoke_preclaim<ExportSign>(ctx);
//@@end export-preclaim
case ttNFTOKEN_MINT:
return invoke_preclaim<NFTokenMint>(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();

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -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)
{

View File

@@ -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
};
//------------------------------------------------------------------------------

View File

@@ -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,

View File

@@ -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.

View File

@@ -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',

View File

@@ -73,6 +73,7 @@ InnerObjectFormats::InnerObjectFormats()
{sfHookExecutionIndex, soeREQUIRED},
{sfHookStateChangeCount, soeREQUIRED},
{sfHookEmitCount, soeREQUIRED},
{sfHookExportCount, soeREQUIRED},
{sfFlags, soeOPTIONAL}});
add(sfHookEmission.jsonName.c_str(),

View File

@@ -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
}

View File

@@ -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

View File

@@ -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."),

View File

@@ -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&

View File

@@ -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.

View File

@@ -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 <ripple/protocol/Feature.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/jss.h>
#include <test/app/Export_test_hooks.h>
#include <test/jtx.h>
#include <test/jtx/hook.h>
namespace ripple {
namespace test {
using TestHook = std::vector<uint8_t> 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 <stdint.h>
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

View File

@@ -0,0 +1,45 @@
// This file is generated by build_test_hooks.py
#ifndef EXPORT_TEST_WASM_INCLUDED
#define EXPORT_TEST_WASM_INCLUDED
#include <map>
#include <stdint.h>
#include <string>
#include <vector>
namespace ripple {
namespace test {
std::map<std::string, std::vector<uint8_t>> export_test_wasm = {
/* ==== WASM: 0 ==== */
{R"[test.hook](
#include <stdint.h>
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