mirror of
https://github.com/Xahau/xahaud.git
synced 2026-01-17 05:05:15 +00:00
Compare commits
12 Commits
HookAPISer
...
featRNG
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a8383c6a9 | ||
|
|
b6518f2b65 | ||
|
|
41779dfd23 | ||
|
|
889cda2a56 | ||
|
|
f738b8fd7c | ||
|
|
b6a23d6d3b | ||
|
|
55438ffc45 | ||
|
|
d33a57f566 | ||
|
|
1690c045f2 | ||
|
|
9efcd45212 | ||
|
|
e0fe289b52 | ||
|
|
64cfdae9e9 |
@@ -461,6 +461,7 @@ target_sources (rippled PRIVATE
|
||||
src/ripple/app/tx/impl/CronSet.cpp
|
||||
src/ripple/app/tx/impl/DeleteAccount.cpp
|
||||
src/ripple/app/tx/impl/DepositPreauth.cpp
|
||||
src/ripple/app/tx/impl/Entropy.cpp
|
||||
src/ripple/app/tx/impl/Escrow.cpp
|
||||
src/ripple/app/tx/impl/GenesisMint.cpp
|
||||
src/ripple/app/tx/impl/Import.cpp
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
#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/Entropy.h>
|
||||
#include <ripple/basics/random.h>
|
||||
#include <ripple/beast/core/LexicalCast.h>
|
||||
#include <ripple/consensus/LedgerTiming.h>
|
||||
@@ -225,6 +227,8 @@ RCLConsensus::Adaptor::propose(RCLCxPeerPos::Proposal const& proposal)
|
||||
|
||||
prop.set_signature(sig.data(), sig.size());
|
||||
|
||||
injectShuffleTxn(app_, sig);
|
||||
|
||||
auto const suppression = proposalUniqueId(
|
||||
proposal.position(),
|
||||
proposal.prevLedger(),
|
||||
@@ -652,6 +656,12 @@ RCLConsensus::Adaptor::doAccept(
|
||||
tapNONE,
|
||||
"consensus",
|
||||
[&](OpenView& view, beast::Journal j) {
|
||||
if (rules->enabled(featureRNG))
|
||||
{
|
||||
auto tx = makeEntropyTxn(view, app_, j_);
|
||||
if (tx)
|
||||
app_.getOPs().submitTransaction(tx);
|
||||
}
|
||||
return app_.getTxQ().accept(app_, view);
|
||||
});
|
||||
|
||||
|
||||
@@ -350,7 +350,8 @@ enum hook_return_code : int64_t {
|
||||
MEM_OVERLAP = -43, // one or more specified buffers are the same memory
|
||||
TOO_MANY_STATE_MODIFICATIONS = -44, // more than 5000 modified state
|
||||
// entires in the combined hook chains
|
||||
TOO_MANY_NAMESPACES = -45
|
||||
TOO_MANY_NAMESPACES = -45,
|
||||
TOO_LITTLE_ENTROPY = -46,
|
||||
};
|
||||
|
||||
enum ExitType : uint8_t {
|
||||
@@ -459,6 +460,8 @@ static const APIWhitelist import_whitelist{
|
||||
HOOK_API_DEFINITION(I64, otxn_slot, (I32)),
|
||||
HOOK_API_DEFINITION(I64, otxn_param, (I32, I32, I32, I32)),
|
||||
HOOK_API_DEFINITION(I64, meta_slot, (I32)),
|
||||
HOOK_API_DEFINITION(I64, dice, (I32)),
|
||||
HOOK_API_DEFINITION(I64, random, (I32, I32)),
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
|
||||
@@ -406,6 +406,9 @@ DECLARE_HOOK_FUNCTION(
|
||||
uint32_t slot_no_tx,
|
||||
uint32_t slot_no_meta);
|
||||
|
||||
DECLARE_HOOK_FUNCTION(int64_t, dice, uint32_t sides);
|
||||
DECLARE_HOOK_FUNCTION(int64_t, random, uint32_t write_ptr, uint32_t write_len);
|
||||
|
||||
/*
|
||||
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,
|
||||
@@ -513,6 +516,8 @@ struct HookResult
|
||||
false; // hook_again allows strong pre-apply to nominate
|
||||
// additional weak post-apply execution
|
||||
std::shared_ptr<STObject const> provisionalMeta;
|
||||
uint64_t rngCallCounter{
|
||||
0}; // used to ensure conseq. rng calls don't return same data
|
||||
};
|
||||
|
||||
class HookExecutor;
|
||||
@@ -877,6 +882,9 @@ public:
|
||||
ADD_HOOK_FUNCTION(meta_slot, ctx);
|
||||
ADD_HOOK_FUNCTION(xpop_slot, ctx);
|
||||
|
||||
ADD_HOOK_FUNCTION(dice, ctx);
|
||||
ADD_HOOK_FUNCTION(random, ctx);
|
||||
|
||||
/*
|
||||
ADD_HOOK_FUNCTION(str_find, ctx);
|
||||
ADD_HOOK_FUNCTION(str_replace, ctx);
|
||||
|
||||
@@ -6156,6 +6156,117 @@ DEFINE_HOOK_FUNCTION(
|
||||
|
||||
HOOK_TEARDOWN();
|
||||
}
|
||||
|
||||
// byteCount must be a multiple of 32
|
||||
inline std::vector<uint8_t>
|
||||
fairRng(ApplyContext& applyCtx, hook::HookResult& hr, uint32_t byteCount)
|
||||
{
|
||||
if (byteCount > 512)
|
||||
byteCount = 512;
|
||||
|
||||
// force the byte count to be a multiple of 32
|
||||
byteCount &= ~0b11111;
|
||||
|
||||
if (byteCount == 0)
|
||||
return {};
|
||||
|
||||
auto& view = applyCtx.view();
|
||||
|
||||
auto const sleRandom = view.peek(ripple::keylet::random());
|
||||
auto const seq = view.info().seq;
|
||||
|
||||
if (!sleRandom || sleRandom->getFieldU32(sfLedgerSequence) != seq ||
|
||||
sleRandom->getFieldU16(sfEntropyCount) < 5)
|
||||
return {};
|
||||
|
||||
// we'll generate bytes in lots of 32
|
||||
|
||||
uint256 rndData = sha512Half(
|
||||
view.info().seq,
|
||||
applyCtx.tx.getTransactionID(),
|
||||
hr.otxnAccount,
|
||||
hr.hookHash,
|
||||
hr.account,
|
||||
hr.hookChainPosition,
|
||||
hr.executeAgainAsWeak ? std::string("weak") : std::string("strong"),
|
||||
sleRandom->getFieldH256(sfRandomData),
|
||||
hr.rngCallCounter++);
|
||||
|
||||
std::vector<uint8_t> bytesOut;
|
||||
bytesOut.resize(byteCount);
|
||||
|
||||
uint8_t* ptr = bytesOut.data();
|
||||
while (1)
|
||||
{
|
||||
std::memcpy(ptr, rndData.data(), 32);
|
||||
ptr += 32;
|
||||
|
||||
if (ptr - bytesOut.data() >= byteCount)
|
||||
break;
|
||||
|
||||
rndData = sha512Half(rndData);
|
||||
}
|
||||
|
||||
return bytesOut;
|
||||
}
|
||||
|
||||
DEFINE_HOOK_FUNCTION(int64_t, dice, uint32_t sides)
|
||||
{
|
||||
HOOK_SETUP();
|
||||
|
||||
auto vec = fairRng(applyCtx, hookCtx.result, 32);
|
||||
|
||||
if (vec.empty())
|
||||
return TOO_LITTLE_ENTROPY;
|
||||
|
||||
if (vec.size() != 32)
|
||||
return INTERNAL_ERROR;
|
||||
|
||||
uint32_t value;
|
||||
std::memcpy(&value, vec.data(), sizeof(uint32_t));
|
||||
|
||||
return value % sides;
|
||||
|
||||
HOOK_TEARDOWN();
|
||||
}
|
||||
|
||||
DEFINE_HOOK_FUNCTION(int64_t, random, uint32_t write_ptr, uint32_t write_len)
|
||||
{
|
||||
HOOK_SETUP();
|
||||
|
||||
if (write_len == 0)
|
||||
return TOO_SMALL;
|
||||
|
||||
if (write_len > 512)
|
||||
return TOO_BIG;
|
||||
|
||||
uint32_t required = write_len;
|
||||
|
||||
if (required & ~0b11111 == required)
|
||||
{
|
||||
// already a multiple of 32 bytes
|
||||
}
|
||||
else
|
||||
{
|
||||
// round up
|
||||
required &= ~0b11111;
|
||||
required += 32;
|
||||
}
|
||||
|
||||
if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length))
|
||||
return OUT_OF_BOUNDS;
|
||||
|
||||
auto vec = fairRng(applyCtx, hookCtx.result, required);
|
||||
|
||||
if (vec.empty())
|
||||
return TOO_LITTLE_ENTROPY;
|
||||
|
||||
WRITE_WASM_MEMORY_AND_RETURN(
|
||||
write_ptr, write_len, vec.data(), vec.size(), memory, memory_length);
|
||||
|
||||
HOOK_TEARDOWN();
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
DEFINE_HOOK_FUNCTION(
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <ripple/app/misc/CanonicalTXSet.h>
|
||||
#include <ripple/app/tx/apply.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/digest.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
@@ -103,6 +104,50 @@ applyTransactions(
|
||||
bool certainRetry = true;
|
||||
std::size_t count = 0;
|
||||
|
||||
// apply the ttSHUFFLE txns first in the ledger to
|
||||
// ensure no one can predict the outcome
|
||||
// then apply ttENTROPY transactions
|
||||
if (view.rules().enabled(featureRNG))
|
||||
for (auto tt : {ttSHUFFLE, ttENTROPY})
|
||||
{
|
||||
for (auto it = txns.begin(); it != txns.end();)
|
||||
{
|
||||
if (tt != it->second->getFieldU16(sfTransactionType))
|
||||
{
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto const txid = it->first.getTXID();
|
||||
try
|
||||
{
|
||||
switch (applyTransaction(
|
||||
app, view, *it->second, certainRetry, tapNONE, j))
|
||||
{
|
||||
case ApplyResult::Success:
|
||||
it = txns.erase(it);
|
||||
++count;
|
||||
break;
|
||||
|
||||
case ApplyResult::Fail:
|
||||
failed.insert(txid);
|
||||
it = txns.erase(it);
|
||||
break;
|
||||
|
||||
case ApplyResult::Retry:
|
||||
++it;
|
||||
}
|
||||
}
|
||||
catch (std::exception const& ex)
|
||||
{
|
||||
JLOG(j.warn())
|
||||
<< "Transaction " << txid << " throws: " << ex.what();
|
||||
failed.insert(txid);
|
||||
it = txns.erase(it);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to apply all of the retriable transactions
|
||||
for (int pass = 0; pass < LEDGER_TOTAL_PASSES; ++pass)
|
||||
{
|
||||
|
||||
@@ -599,6 +599,12 @@ public:
|
||||
return validatorKeys_.publicKey;
|
||||
}
|
||||
|
||||
SecretKey const&
|
||||
getValidationSecretKey() const override
|
||||
{
|
||||
return validatorKeys_.secretKey;
|
||||
}
|
||||
|
||||
NetworkOPs&
|
||||
getOPs() override
|
||||
{
|
||||
|
||||
@@ -241,6 +241,9 @@ public:
|
||||
virtual PublicKey const&
|
||||
getValidationPublicKey() const = 0;
|
||||
|
||||
virtual SecretKey const&
|
||||
getValidationSecretKey() const = 0;
|
||||
|
||||
virtual Resource::Manager&
|
||||
getResourceManager() = 0;
|
||||
virtual PathRequests&
|
||||
|
||||
@@ -228,7 +228,7 @@ public:
|
||||
doTransactionSync(
|
||||
std::shared_ptr<Transaction> transaction,
|
||||
bool bUnlimited,
|
||||
FailHard failType);
|
||||
FailHard failType) override;
|
||||
|
||||
/**
|
||||
* For transactions not submitted by a locally connected client, fire and
|
||||
@@ -1078,6 +1078,12 @@ NetworkOPsImp::submitTransaction(std::shared_ptr<STTx const> const& iTrans)
|
||||
return;
|
||||
}
|
||||
|
||||
if (view->rules().enabled(featureRNG) && iTrans->getTxnType() == ttSHUFFLE)
|
||||
{
|
||||
// as above
|
||||
return;
|
||||
}
|
||||
|
||||
// this is an asynchronous interface
|
||||
auto const trans = sterilize(*iTrans);
|
||||
|
||||
|
||||
@@ -112,6 +112,13 @@ public:
|
||||
bool bLocal,
|
||||
FailHard failType) = 0;
|
||||
|
||||
// directly inject transaction, skipping checks
|
||||
virtual void
|
||||
doTransactionSync(
|
||||
std::shared_ptr<Transaction> transaction,
|
||||
bool bUnlimited,
|
||||
FailHard failType) = 0;
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
//
|
||||
// Owner functions
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <ripple/app/tx/apply.h>
|
||||
#include <ripple/basics/mulDiv.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/digest.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <ripple/protocol/st.h>
|
||||
#include <algorithm>
|
||||
@@ -1930,13 +1931,15 @@ TxQ::tryDirectApply(
|
||||
const bool isFirstImport = !sleAccount &&
|
||||
view.rules().enabled(featureImport) && tx->getTxnType() == ttIMPORT;
|
||||
|
||||
const bool accRequired = !(isFirstImport || isUVTx(*tx));
|
||||
|
||||
// Don't attempt to direct apply if the account is not in the ledger.
|
||||
if (!sleAccount && !isFirstImport)
|
||||
if (!sleAccount && accRequired)
|
||||
return {};
|
||||
|
||||
std::optional<SeqProxy> txSeqProx;
|
||||
|
||||
if (!isFirstImport)
|
||||
if (accRequired)
|
||||
{
|
||||
SeqProxy const acctSeqProx =
|
||||
SeqProxy::sequence((*sleAccount)[sfSequence]);
|
||||
@@ -1949,7 +1952,7 @@ TxQ::tryDirectApply(
|
||||
}
|
||||
|
||||
FeeLevel64 const requiredFeeLevel =
|
||||
isFirstImport ? FeeLevel64{0} : [this, &view, flags]() {
|
||||
!accRequired ? FeeLevel64{0} : [this, &view, flags]() {
|
||||
std::lock_guard lock(mutex_);
|
||||
return getRequiredFeeLevel(
|
||||
view, flags, feeMetrics_.getSnapshot(), lock);
|
||||
|
||||
@@ -319,7 +319,7 @@ saveValidatedLedger(
|
||||
*db << sql;
|
||||
}
|
||||
else if (auto const& sleTxn = acceptedLedgerTx->getTxn();
|
||||
!isPseudoTx(*sleTxn))
|
||||
!isPseudoTx(*sleTxn) && !isUVTx(*sleTxn))
|
||||
{
|
||||
// It's okay for pseudo transactions to not affect any
|
||||
// accounts. But otherwise...
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <ripple/app/hook/Guard.h>
|
||||
#include <ripple/app/hook/applyHook.h>
|
||||
#include <ripple/app/ledger/Ledger.h>
|
||||
#include <ripple/app/ledger/LedgerMaster.h>
|
||||
#include <ripple/app/main/Application.h>
|
||||
#include <ripple/app/misc/AmendmentTable.h>
|
||||
#include <ripple/app/misc/NetworkOPs.h>
|
||||
@@ -96,6 +97,12 @@ Change::preflight(PreflightContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.tx.getTxnType() == ttSHUFFLE && !ctx.rules.enabled(featureRNG))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Change: FeatureRNG is not enabled.";
|
||||
return temDISABLED;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -104,7 +111,7 @@ Change::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
// If tapOPEN_LEDGER is resurrected into ApplyFlags,
|
||||
// this block can be moved to preflight.
|
||||
if (ctx.view.open())
|
||||
if (ctx.view.open() && ctx.tx.getTxnType() != ttSHUFFLE)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Change transaction against open ledger";
|
||||
return temINVALID;
|
||||
@@ -154,6 +161,7 @@ Change::preclaim(PreclaimContext const& ctx)
|
||||
case ttAMENDMENT:
|
||||
case ttUNL_MODIFY:
|
||||
case ttEMIT_FAILURE:
|
||||
case ttSHUFFLE:
|
||||
return tesSUCCESS;
|
||||
case ttUNL_REPORT: {
|
||||
if (!ctx.tx.isFieldPresent(sfImportVLKey) ||
|
||||
@@ -209,12 +217,72 @@ Change::doApply()
|
||||
return applyEmitFailure();
|
||||
case ttUNL_REPORT:
|
||||
return applyUNLReport();
|
||||
case ttSHUFFLE:
|
||||
return applyShuffle();
|
||||
default:
|
||||
assert(0);
|
||||
return tefFAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
TER
|
||||
Change::applyShuffle()
|
||||
{
|
||||
auto const seq = view().info().seq;
|
||||
auto const txSeq = ctx_.tx.getFieldU32(sfLedgerSequence);
|
||||
|
||||
if (seq != txSeq)
|
||||
{
|
||||
JLOG(j_.warn()) << "Change: ttSHUFFLE, wrong ledger seq. lgr=" << seq
|
||||
<< " tx=" << txSeq;
|
||||
return tefFAILURE;
|
||||
}
|
||||
|
||||
auto sle = view().peek(keylet::random());
|
||||
|
||||
bool const created = !sle;
|
||||
|
||||
if (created)
|
||||
{
|
||||
sle = std::make_shared<SLE>(keylet::random());
|
||||
}
|
||||
|
||||
auto lastSeq = created ? 0 : sle->getFieldU32(sfLedgerSequence);
|
||||
|
||||
if (lastSeq < seq)
|
||||
{
|
||||
// reset entropy count to zero... this will probably be
|
||||
// one after the below executes but its possible the digest
|
||||
// doesn't match and the entropy count isn't incremented
|
||||
sle->setFieldU16(sfEntropyCount, 0);
|
||||
|
||||
// swap the random data out ready for this round of entropy collection
|
||||
sle->setFieldH256(sfLastRandomData, sle->getFieldH256(sfRandomData));
|
||||
|
||||
// update the ledger sequence of the object
|
||||
sle->setFieldU32(sfLedgerSequence, seq);
|
||||
}
|
||||
|
||||
// increment entropy count
|
||||
sle->setFieldU16(sfEntropyCount, sle->getFieldU16(sfEntropyCount) + 1);
|
||||
|
||||
// contribute the new entropy to the random data field
|
||||
sle->setFieldH256(
|
||||
sfRandomData,
|
||||
sha512Half(
|
||||
seq,
|
||||
sle->getFieldU16(sfEntropyCount),
|
||||
sle->getFieldH256(sfRandomData),
|
||||
ctx_.tx.getFieldH256(sfRandomData)));
|
||||
|
||||
if (!created)
|
||||
view().update(sle);
|
||||
else
|
||||
view().insert(sle);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
Change::applyUNLReport()
|
||||
{
|
||||
@@ -1199,4 +1267,47 @@ Change::applyUNLModify()
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
void
|
||||
injectShuffleTxn(Application& app, Slice const& sig)
|
||||
{
|
||||
// in featureRNG we use trusted proposal signatures as shuffling entropy
|
||||
// so inject a psuedo to do that here
|
||||
auto ol = app.openLedger().current();
|
||||
if (ol && ol->rules().enabled(featureRNG))
|
||||
{
|
||||
uint256 rnd = sha512Half(std::string("shuffler"), sig);
|
||||
// create txn
|
||||
STTx shuffleTx(ttSHUFFLE, [&](auto& obj) {
|
||||
obj.setFieldU32(sfLedgerSequence, ol->info().seq + 1);
|
||||
obj.setFieldH256(sfRandomData, rnd);
|
||||
obj.setAccountID(sfAccount, AccountID());
|
||||
});
|
||||
|
||||
// inject it into the propose set and into the open ledger
|
||||
uint256 txID = shuffleTx.getTransactionID();
|
||||
|
||||
JLOG(app.journal("Transaction").debug())
|
||||
<< "SHUFFLE processing: Submitting pseudo: "
|
||||
<< shuffleTx.getFullText() << " txid: " << txID;
|
||||
app.getHashRouter().setFlags(txID, SF_PRIVATE2);
|
||||
app.getHashRouter().setFlags(txID, SF_EMITTED);
|
||||
|
||||
{
|
||||
auto s = std::make_shared<ripple::Serializer>();
|
||||
shuffleTx.add(*s);
|
||||
|
||||
std::unique_lock masterLock{app.getMasterMutex(), std::defer_lock};
|
||||
|
||||
std::unique_lock ledgerLock{
|
||||
app.getLedgerMaster().peekMutex(), std::defer_lock};
|
||||
std::lock(masterLock, ledgerLock);
|
||||
|
||||
app.openLedger().modify([&](OpenView& view, beast::Journal j) {
|
||||
view.rawTxInsert(txID, std::move(s), nullptr);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -76,8 +76,14 @@ private:
|
||||
|
||||
TER
|
||||
applyUNLReport();
|
||||
|
||||
TER
|
||||
applyShuffle();
|
||||
};
|
||||
|
||||
void
|
||||
injectShuffleTxn(Application& app, Slice const& sig);
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
|
||||
256
src/ripple/app/tx/impl/Entropy.cpp
Normal file
256
src/ripple/app/tx/impl/Entropy.cpp
Normal file
@@ -0,0 +1,256 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/tx/impl/Entropy.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/ledger/View.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
#include <ripple/protocol/TxFlags.h>
|
||||
#include <ripple/protocol/st.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
TxConsequences
|
||||
Entropy::makeTxConsequences(PreflightContext const& ctx)
|
||||
{
|
||||
return TxConsequences{ctx.tx, TxConsequences::normal};
|
||||
}
|
||||
|
||||
NotTEC
|
||||
Entropy::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
if (!ctx.rules.enabled(featureRNG))
|
||||
return temDISABLED;
|
||||
|
||||
return preflight2(ctx);
|
||||
}
|
||||
|
||||
TER
|
||||
Entropy::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
if (!ctx.view.rules().enabled(featureRNG))
|
||||
return temDISABLED;
|
||||
|
||||
// account must be a valid UV
|
||||
if (!inUNLReport(ctx.view, ctx.tx.getAccountID(sfAccount), ctx.j))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Entropy: Txn Account isn't in the UNLReport.";
|
||||
return tefFAILURE;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
Entropy::doApply()
|
||||
{
|
||||
auto const seq = view().info().seq;
|
||||
|
||||
auto sle = view().peek(keylet::random());
|
||||
|
||||
bool const created = !sle;
|
||||
|
||||
if (created)
|
||||
sle = std::make_shared<SLE>(keylet::random());
|
||||
|
||||
auto lastSeq = created ? 0 : sle->getFieldU32(sfLedgerSequence);
|
||||
|
||||
if (lastSeq < seq)
|
||||
{
|
||||
// update the ledger sequence of the object
|
||||
sle->setFieldU32(sfLedgerSequence, seq);
|
||||
|
||||
// reset entropy count to zero... this will probably be
|
||||
// one after the below executes but its possible the digest
|
||||
// doesn't match and the entropy count isn't incremented
|
||||
sle->setFieldU16(sfEntropyCount, 0);
|
||||
|
||||
// swap the random data out ready for this round of entropy collection
|
||||
sle->setFieldH256(sfLastRandomData, sle->getFieldH256(sfRandomData));
|
||||
sle->setFieldH256(sfRandomData, beast::zero);
|
||||
}
|
||||
|
||||
uint256 nextDigest = ctx_.tx.getFieldH256(sfNextRandomDigest);
|
||||
uint256 currentEntropy = ctx_.tx.getFieldH256(sfRandomData);
|
||||
uint256 currentDigest = sha512Half(currentEntropy);
|
||||
|
||||
AccountID const validator = ctx_.tx.getAccountID(sfAccount);
|
||||
|
||||
// iterate the digest array to find the entry if it exists
|
||||
STArray digestEntries = sle->getFieldArray(sfRandomDigests);
|
||||
std::map<AccountID, STObject> entries;
|
||||
|
||||
for (auto& entry : digestEntries)
|
||||
{
|
||||
// we'll automatically clean up really old entries by just omitting them
|
||||
// from the map here
|
||||
if (entry.getFieldU32(sfLedgerSequence) < seq - 5)
|
||||
continue;
|
||||
|
||||
entries.emplace(entry.getAccountID(sfValidator), std::move(entry));
|
||||
}
|
||||
|
||||
if (auto it = entries.find(validator); it != entries.end())
|
||||
{
|
||||
auto& entry = it->second;
|
||||
|
||||
// ensure the precommitted digest matches the provided entropy
|
||||
if (entry.getFieldH256(sfNextRandomDigest) != currentDigest)
|
||||
{
|
||||
if (entry.getFieldU32(sfLedgerSequence) != seq - 1)
|
||||
{
|
||||
// this is a skip-ahead or missed last txn somehow, so ignore,
|
||||
// but no warning.
|
||||
}
|
||||
else
|
||||
{
|
||||
// this is a clear violation so warn (and ignore the entropy)
|
||||
JLOG(j_.warn()) << "!!! Validator " << validator
|
||||
<< " supplied entropy that "
|
||||
<< "does not match precommitment value !!!";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// contribute the new entropy to the random data field
|
||||
sle->setFieldH256(
|
||||
sfRandomData,
|
||||
sha512Half(
|
||||
validator,
|
||||
sle->getFieldH256(sfRandomData),
|
||||
currentEntropy));
|
||||
|
||||
// increment entropy count
|
||||
sle->setFieldU16(
|
||||
sfEntropyCount, sle->getFieldU16(sfEntropyCount) + 1);
|
||||
}
|
||||
|
||||
// update the digest entry
|
||||
entry.setFieldH256(sfNextRandomDigest, nextDigest);
|
||||
entry.setFieldU32(sfLedgerSequence, seq);
|
||||
}
|
||||
else
|
||||
{
|
||||
// this validator doesn't have an entry so create one
|
||||
STObject entry{sfRandomDigestEntry};
|
||||
entry.setAccountID(sfValidator, validator);
|
||||
entry.setFieldH256(sfNextRandomDigest, nextDigest);
|
||||
entry.setFieldU32(sfLedgerSequence, seq);
|
||||
entries.emplace(validator, std::move(entry));
|
||||
}
|
||||
|
||||
// update the array
|
||||
STArray newEntries(sfRandomDigests);
|
||||
newEntries.reserve(entries.size());
|
||||
for (auto& [_, entry] : entries)
|
||||
newEntries.push_back(std::move(entry));
|
||||
|
||||
sle->setFieldArray(sfRandomDigests, std::move(newEntries));
|
||||
|
||||
// send it off to the ledger
|
||||
if (!created)
|
||||
view().update(sle);
|
||||
else
|
||||
view().insert(sle);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
// if this validator is on the UNLReport then return a signed ttENTROPY
|
||||
// transaction to be added to the txq.
|
||||
std::shared_ptr<STTx const>
|
||||
makeEntropyTxn(OpenView& view, Application& app, beast::Journal const& j_)
|
||||
{
|
||||
if (!view.rules().enabled(featureRNG))
|
||||
return {};
|
||||
|
||||
JLOG(j_.debug()) << "ENTROPY processing: started";
|
||||
|
||||
auto const seq = view.info().seq;
|
||||
|
||||
static uint32_t lastSeq = 0;
|
||||
|
||||
// we only generate once per ledger
|
||||
if (lastSeq == seq)
|
||||
return {};
|
||||
|
||||
lastSeq = seq;
|
||||
|
||||
// if we're not a validator we do nothing here
|
||||
if (app.getValidationPublicKey().empty())
|
||||
return {};
|
||||
|
||||
PublicKey pkSigning = app.getValidationPublicKey();
|
||||
|
||||
auto const pk = app.validatorManifests().getMasterKey(pkSigning);
|
||||
|
||||
// Only continue if we're on the UNL
|
||||
if (!inUNLReport(view, app, pk, j_))
|
||||
return {};
|
||||
|
||||
// build and sign a txn
|
||||
|
||||
AccountID acc = calcAccountID(pk);
|
||||
|
||||
static auto getRnd = []() -> uint256 {
|
||||
static std::ifstream rng("/dev/urandom", std::ios::binary);
|
||||
uint256 out;
|
||||
if (rng && rng.read(reinterpret_cast<char*>(out.data()), 32))
|
||||
return out;
|
||||
std::random_device rd;
|
||||
for (auto& word : out)
|
||||
word = rd();
|
||||
return out;
|
||||
};
|
||||
|
||||
static std::optional<uint256> prevRnd;
|
||||
|
||||
uint256 nextRnd = getRnd();
|
||||
|
||||
// create txn
|
||||
auto rngTx = std::make_shared<STTx>(ttENTROPY, [&](auto& obj) {
|
||||
obj.setFieldU32(sfLastLedgerSequence, seq);
|
||||
obj.setAccountID(sfAccount, acc);
|
||||
obj.setFieldU32(sfSequence, 0);
|
||||
if (prevRnd.has_value())
|
||||
obj.setFieldH256(sfRandomData, *prevRnd);
|
||||
obj.setFieldH256(sfNextRandomDigest, sha512Half(nextRnd));
|
||||
obj.setFieldVL(sfSigningPubKey, pkSigning.slice());
|
||||
obj.setFieldH256(sfParentHash, view.info().parentHash);
|
||||
obj.setFieldU32(sfFlags, tfFullyCanonicalSig);
|
||||
|
||||
if (app.config().NETWORK_ID > 1024)
|
||||
obj.setFieldU32(sfNetworkID, app.config().NETWORK_ID);
|
||||
});
|
||||
|
||||
prevRnd = nextRnd;
|
||||
|
||||
// sign the txn using our ephemeral key
|
||||
rngTx->sign(pkSigning, app.getValidationSecretKey());
|
||||
|
||||
JLOG(j_.debug()) << "ENTROPY txn: " << rngTx->getFullText();
|
||||
|
||||
return rngTx;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
67
src/ripple/app/tx/impl/Entropy.h
Normal file
67
src/ripple/app/tx/impl/Entropy.h
Normal file
@@ -0,0 +1,67 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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_ENTROPY_H_INCLUDED
|
||||
#define RIPPLE_TX_ENTROPY_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 Entropy : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
|
||||
|
||||
explicit Entropy(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 then return a signed ttENTROPY
|
||||
// transaction to be added to the txq.
|
||||
std::shared_ptr<STTx const>
|
||||
makeEntropyTxn(OpenView& view, Application& app, beast::Journal const& j_);
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -494,6 +494,7 @@ LedgerEntryTypesMatch::visitEntry(
|
||||
case ltCRON:
|
||||
case ltIMPORT_VLSEQ:
|
||||
case ltUNL_REPORT:
|
||||
case ltRANDOM:
|
||||
break;
|
||||
default:
|
||||
invalidTypeAdded_ = true;
|
||||
|
||||
@@ -273,6 +273,9 @@ Transactor::calculateHookChainFee(
|
||||
XRPAmount
|
||||
Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
if (isUVTx(tx))
|
||||
return XRPAmount{0};
|
||||
|
||||
// Returns the fee in fee units.
|
||||
|
||||
// The computation has two parts:
|
||||
@@ -280,7 +283,9 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
// * The additional cost of each multisignature on the transaction.
|
||||
XRPAmount baseFee = view.fees().base;
|
||||
|
||||
if (tx.getFieldU16(sfTransactionType) == ttIMPORT)
|
||||
auto const tt = tx.getFieldU16(sfTransactionType);
|
||||
|
||||
if (tt == ttIMPORT)
|
||||
{
|
||||
XRPAmount const importFee = baseFee * 10;
|
||||
if (importFee > baseFee)
|
||||
@@ -442,8 +447,21 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
|
||||
// Only check fee is sufficient when the ledger is open.
|
||||
if (ctx.view.open())
|
||||
{
|
||||
auto const feeDue =
|
||||
minimumFee(ctx.app, baseFee, ctx.view.fees(), ctx.flags);
|
||||
auto feeDue = minimumFee(ctx.app, baseFee, ctx.view.fees(), ctx.flags);
|
||||
|
||||
if (ctx.view.rules().enabled(featureRNG))
|
||||
{
|
||||
auto const pkSignerField = ctx.tx.getSigningPubKey();
|
||||
if (publicKeyType(makeSlice(pkSignerField)))
|
||||
{
|
||||
PublicKey pkSigner{makeSlice(pkSignerField)};
|
||||
if (inUNLReport(ctx.view, ctx.app, pkSigner, ctx.j))
|
||||
{
|
||||
// UVTxns don't have to pay a fee
|
||||
feeDue = beast::zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (feePaid < feeDue)
|
||||
{
|
||||
@@ -461,6 +479,9 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
|
||||
auto const sle = ctx.view.read(keylet::account(id));
|
||||
if (!sle)
|
||||
{
|
||||
if (isUVTx(ctx.tx))
|
||||
return tesSUCCESS;
|
||||
|
||||
if (ctx.tx.getTxnType() == ttIMPORT)
|
||||
{
|
||||
if (!ctx.tx.isFieldPresent(sfIssuer))
|
||||
@@ -541,6 +562,13 @@ Transactor::checkSeqProxy(
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
if (isUVTx(tx) && t_seqProx.isSeq() && tx[sfSequence] == 0)
|
||||
{
|
||||
JLOG(j.trace()) << "applyTransaction: allowing UVTx with seq=0 "
|
||||
<< toBase58(id);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
JLOG(j.trace())
|
||||
<< "applyTransaction: delay: source account does not exist "
|
||||
<< toBase58(id);
|
||||
@@ -625,7 +653,9 @@ Transactor::checkPriorTxAndLastLedger(PreclaimContext const& ctx)
|
||||
ctx.view.rules().enabled(featureImport) &&
|
||||
ctx.tx.getTxnType() == ttIMPORT && !ctx.tx.isFieldPresent(sfIssuer);
|
||||
|
||||
if (!sle && !isFirstImport)
|
||||
bool const accRequired = !(isFirstImport || isUVTx(ctx.tx));
|
||||
|
||||
if (!sle && accRequired)
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "applyTransaction: delay: source account does not exist "
|
||||
@@ -781,12 +811,13 @@ Transactor::apply()
|
||||
auto const sle = view().peek(keylet::account(account_));
|
||||
|
||||
// sle must exist except for transactions
|
||||
// that allow zero account. (and ttIMPORT)
|
||||
// that allow zero account. (and ttIMPORT and UV)
|
||||
assert(
|
||||
sle != nullptr || account_ == beast::zero ||
|
||||
view().rules().enabled(featureImport) &&
|
||||
ctx_.tx.getTxnType() == ttIMPORT &&
|
||||
!ctx_.tx.isFieldPresent(sfIssuer));
|
||||
!ctx_.tx.isFieldPresent(sfIssuer) ||
|
||||
isUVTx(ctx_.tx));
|
||||
|
||||
if (sle)
|
||||
{
|
||||
@@ -848,19 +879,26 @@ NotTEC
|
||||
Transactor::checkSingleSign(PreclaimContext const& ctx)
|
||||
{
|
||||
// Check that the value in the signing key slot is a public key.
|
||||
auto const pkSigner = ctx.tx.getSigningPubKey();
|
||||
if (!publicKeyType(makeSlice(pkSigner)))
|
||||
auto const pkSignerField = ctx.tx.getSigningPubKey();
|
||||
if (!publicKeyType(makeSlice(pkSignerField)))
|
||||
{
|
||||
JLOG(ctx.j.trace())
|
||||
<< "checkSingleSign: signing public key type is unknown";
|
||||
return tefBAD_AUTH; // FIXME: should be better error!
|
||||
}
|
||||
|
||||
PublicKey pkSigner{makeSlice(pkSignerField)};
|
||||
|
||||
// Look up the account.
|
||||
auto const idSigner = calcAccountID(PublicKey(makeSlice(pkSigner)));
|
||||
auto const idSigner = calcAccountID(pkSigner);
|
||||
auto const idAccount = ctx.tx.getAccountID(sfAccount);
|
||||
auto const sleAccount = ctx.view.read(keylet::account(idAccount));
|
||||
|
||||
// UVTxns of the approved type don't need an underlying account
|
||||
// and can be signed with the manifest ephemeral key
|
||||
if (isUVTx(ctx.tx) && inUNLReport(ctx.view, ctx.app, pkSigner, ctx.j))
|
||||
return tesSUCCESS;
|
||||
|
||||
if (!sleAccount)
|
||||
return terNO_ACCOUNT;
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include <ripple/app/tx/impl/CronSet.h>
|
||||
#include <ripple/app/tx/impl/DeleteAccount.h>
|
||||
#include <ripple/app/tx/impl/DepositPreauth.h>
|
||||
#include <ripple/app/tx/impl/Entropy.h>
|
||||
#include <ripple/app/tx/impl/Escrow.h>
|
||||
#include <ripple/app/tx/impl/GenesisMint.h>
|
||||
#include <ripple/app/tx/impl/Import.h>
|
||||
@@ -152,6 +153,7 @@ invoke_preflight(PreflightContext const& ctx)
|
||||
case ttUNL_MODIFY:
|
||||
case ttUNL_REPORT:
|
||||
case ttEMIT_FAILURE:
|
||||
case ttSHUFFLE:
|
||||
return invoke_preflight_helper<Change>(ctx);
|
||||
case ttHOOK_SET:
|
||||
return invoke_preflight_helper<SetHook>(ctx);
|
||||
@@ -187,6 +189,8 @@ invoke_preflight(PreflightContext const& ctx)
|
||||
return invoke_preflight_helper<CronSet>(ctx);
|
||||
case ttCRON:
|
||||
return invoke_preflight_helper<Cron>(ctx);
|
||||
case ttENTROPY:
|
||||
return invoke_preflight_helper<Entropy>(ctx);
|
||||
default:
|
||||
assert(false);
|
||||
return {temUNKNOWN, TxConsequences{temUNKNOWN}};
|
||||
@@ -283,6 +287,7 @@ invoke_preclaim(PreclaimContext const& ctx)
|
||||
case ttUNL_MODIFY:
|
||||
case ttUNL_REPORT:
|
||||
case ttEMIT_FAILURE:
|
||||
case ttSHUFFLE:
|
||||
return invoke_preclaim<Change>(ctx);
|
||||
case ttNFTOKEN_MINT:
|
||||
return invoke_preclaim<NFTokenMint>(ctx);
|
||||
@@ -316,6 +321,8 @@ invoke_preclaim(PreclaimContext const& ctx)
|
||||
return invoke_preclaim<CronSet>(ctx);
|
||||
case ttCRON:
|
||||
return invoke_preclaim<Cron>(ctx);
|
||||
case ttENTROPY:
|
||||
return invoke_preclaim<Entropy>(ctx);
|
||||
default:
|
||||
assert(false);
|
||||
return temUNKNOWN;
|
||||
@@ -374,6 +381,7 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
case ttUNL_MODIFY:
|
||||
case ttUNL_REPORT:
|
||||
case ttEMIT_FAILURE:
|
||||
case ttSHUFFLE:
|
||||
return Change::calculateBaseFee(view, tx);
|
||||
case ttNFTOKEN_MINT:
|
||||
return NFTokenMint::calculateBaseFee(view, tx);
|
||||
@@ -407,6 +415,8 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
return CronSet::calculateBaseFee(view, tx);
|
||||
case ttCRON:
|
||||
return Cron::calculateBaseFee(view, tx);
|
||||
case ttENTROPY:
|
||||
return Entropy::calculateBaseFee(view, tx);
|
||||
default:
|
||||
return XRPAmount{0};
|
||||
}
|
||||
@@ -544,6 +554,7 @@ invoke_apply(ApplyContext& ctx)
|
||||
case ttFEE:
|
||||
case ttUNL_MODIFY:
|
||||
case ttUNL_REPORT:
|
||||
case ttSHUFFLE:
|
||||
case ttEMIT_FAILURE: {
|
||||
Change p(ctx);
|
||||
return p();
|
||||
@@ -608,6 +619,10 @@ invoke_apply(ApplyContext& ctx)
|
||||
Cron p(ctx);
|
||||
return p();
|
||||
}
|
||||
case ttENTROPY: {
|
||||
Entropy p(ctx);
|
||||
return p();
|
||||
}
|
||||
default:
|
||||
assert(false);
|
||||
return {temUNKNOWN, false};
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
#ifndef RIPPLE_CONSENSUS_CONSENSUS_H_INCLUDED
|
||||
#define RIPPLE_CONSENSUS_CONSENSUS_H_INCLUDED
|
||||
|
||||
#include <ripple/app/ledger/OpenLedger.h>
|
||||
#include <ripple/app/main/Application.h>
|
||||
#include <ripple/app/misc/HashRouter.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/basics/chrono.h>
|
||||
#include <ripple/beast/utility/Journal.h>
|
||||
@@ -29,6 +32,7 @@
|
||||
#include <ripple/consensus/DisputedTx.h>
|
||||
#include <ripple/consensus/LedgerTiming.h>
|
||||
#include <ripple/json/json_writer.h>
|
||||
#include <ripple/protocol/digest.h>
|
||||
#include <boost/logic/tribool.hpp>
|
||||
#include <deque>
|
||||
#include <optional>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
#ifndef RIPPLE_LEDGER_VIEW_H_INCLUDED
|
||||
#define RIPPLE_LEDGER_VIEW_H_INCLUDED
|
||||
|
||||
#include <ripple/app/main/Application.h>
|
||||
#include <ripple/app/misc/Manifest.h>
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/beast/utility/Journal.h>
|
||||
#include <ripple/core/Config.h>
|
||||
@@ -1094,6 +1096,66 @@ trustTransferLockedBalance(
|
||||
}
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
template <class V>
|
||||
bool
|
||||
inUNLReport(V const& view, AccountID const& id, beast::Journal const& j)
|
||||
{
|
||||
auto const seq = view.info().seq;
|
||||
static uint32_t lastLgrSeq = 0;
|
||||
static std::map<AccountID, bool> cache;
|
||||
|
||||
// for the first 256 ledgers we're just saying everyone is in the UNLReport
|
||||
// because otherwise testing is very difficult.
|
||||
if (seq < 256)
|
||||
return true;
|
||||
|
||||
if (lastLgrSeq != seq)
|
||||
{
|
||||
cache.clear();
|
||||
lastLgrSeq = seq;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (cache.find(id) != cache.end())
|
||||
return cache[id];
|
||||
}
|
||||
|
||||
// Check if UVAcc is on UNLReport we also do nothing
|
||||
auto const unlRep = view.read(keylet::UNLReport());
|
||||
if (!unlRep || !unlRep->isFieldPresent(sfActiveValidators))
|
||||
{
|
||||
JLOG(j.debug()) << "UNLReport misssing";
|
||||
|
||||
// ensure we keep the cache invalid when in this state
|
||||
lastLgrSeq = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto const& avs = unlRep->getFieldArray(sfActiveValidators);
|
||||
for (auto const& av : avs)
|
||||
{
|
||||
if (av.getAccountID(sfAccount) == id)
|
||||
return cache[id] = true;
|
||||
}
|
||||
|
||||
return cache[id] = false;
|
||||
}
|
||||
|
||||
template <class V>
|
||||
bool
|
||||
inUNLReport(
|
||||
V const& view,
|
||||
Application& app,
|
||||
PublicKey const& pk,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
PublicKey uvPk = app.validatorManifests().getMasterKey(pk);
|
||||
|
||||
return inUNLReport(view, calcAccountID(pk), j) ||
|
||||
(uvPk != pk && inUNLReport(view, calcAccountID(uvPk), j));
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <ripple/app/misc/Transaction.h>
|
||||
#include <ripple/app/misc/ValidatorList.h>
|
||||
#include <ripple/app/tx/apply.h>
|
||||
#include <ripple/app/tx/impl/Change.h>
|
||||
#include <ripple/basics/UptimeClock.h>
|
||||
#include <ripple/basics/base64.h>
|
||||
#include <ripple/basics/random.h>
|
||||
@@ -1955,6 +1956,10 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMProposeSet> const& m)
|
||||
if (!isTrusted && app_.config().RELAY_UNTRUSTED_PROPOSALS == -1)
|
||||
return;
|
||||
|
||||
// ttSHUFFLE is injected as part of featureRNG, based on the proposal
|
||||
// signature
|
||||
injectShuffleTxn(app_, makeSlice(set.signature()));
|
||||
|
||||
uint256 const proposeHash{set.currenttxhash()};
|
||||
uint256 const prevLedger{set.previousledger()};
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace detail {
|
||||
// Feature.cpp. Because it's only used to reserve storage, and determine how
|
||||
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
|
||||
// the actual number of amendments. A LogicError on startup will verify this.
|
||||
static constexpr std::size_t numFeatures = 90;
|
||||
static constexpr std::size_t numFeatures = 91;
|
||||
|
||||
/** Amendments that this server supports and the default voting behavior.
|
||||
Whether they are enabled depends on the Rules defined in the validated
|
||||
@@ -378,6 +378,7 @@ extern uint256 const fixInvalidTxFlags;
|
||||
extern uint256 const featureExtendedHookState;
|
||||
extern uint256 const fixCronStacking;
|
||||
extern uint256 const fixHookAPI20251128;
|
||||
extern uint256 const featureRNG;
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
|
||||
@@ -51,6 +51,9 @@ class SeqProxy;
|
||||
*/
|
||||
namespace keylet {
|
||||
|
||||
Keylet const&
|
||||
random() noexcept;
|
||||
|
||||
/** The (fixed) index of the object containing the emitted txns for the ledger.
|
||||
*/
|
||||
Keylet const&
|
||||
|
||||
@@ -260,6 +260,13 @@ enum LedgerEntryType : std::uint16_t
|
||||
\sa keylet::emitted
|
||||
*/
|
||||
ltEMITTED_TXN = 'E',
|
||||
|
||||
|
||||
/** A ledger object containing a consensus-generated random number, operated on by ttENTROPY
|
||||
|
||||
\sa keylet::rng
|
||||
*/
|
||||
ltRANDOM = 0x526EU, // Rn
|
||||
};
|
||||
// clang-format off
|
||||
|
||||
|
||||
@@ -355,6 +355,7 @@ extern SF_UINT16 const sfHookEmitCount;
|
||||
extern SF_UINT16 const sfHookExecutionIndex;
|
||||
extern SF_UINT16 const sfHookApiVersion;
|
||||
extern SF_UINT16 const sfHookStateScale;
|
||||
extern SF_UINT16 const sfEntropyCount;
|
||||
|
||||
// 32-bit integers (common)
|
||||
extern SF_UINT32 const sfNetworkID;
|
||||
@@ -491,6 +492,9 @@ extern SF_UINT256 const sfGovernanceFlags;
|
||||
extern SF_UINT256 const sfGovernanceMarks;
|
||||
extern SF_UINT256 const sfEmittedTxnID;
|
||||
extern SF_UINT256 const sfCron;
|
||||
extern SF_UINT256 const sfRandomData;
|
||||
extern SF_UINT256 const sfLastRandomData;
|
||||
extern SF_UINT256 const sfNextRandomDigest;
|
||||
|
||||
// currency amount (common)
|
||||
extern SF_AMOUNT const sfAmount;
|
||||
@@ -563,6 +567,7 @@ extern SF_ACCOUNT const sfEmitCallback;
|
||||
extern SF_ACCOUNT const sfHookAccount;
|
||||
extern SF_ACCOUNT const sfNFTokenMinter;
|
||||
extern SF_ACCOUNT const sfInform;
|
||||
extern SF_ACCOUNT const sfValidator;
|
||||
|
||||
// path set
|
||||
extern SField const sfPaths;
|
||||
@@ -605,6 +610,7 @@ extern SField const sfHookEmission;
|
||||
extern SField const sfMintURIToken;
|
||||
extern SField const sfAmountEntry;
|
||||
extern SField const sfRemark;
|
||||
extern SField const sfRandomDigestEntry;
|
||||
|
||||
// array of objects (common)
|
||||
// ARRAY/1 is reserved for end of array
|
||||
@@ -634,6 +640,7 @@ extern SField const sfImportVLKeys;
|
||||
extern SField const sfHookEmissions;
|
||||
extern SField const sfAmounts;
|
||||
extern SField const sfRemarks;
|
||||
extern SField const sfRandomDigests;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -171,6 +171,9 @@ sterilize(STTx const& stx);
|
||||
bool
|
||||
isPseudoTx(STObject const& tx);
|
||||
|
||||
bool
|
||||
isUVTx(STObject const& tx);
|
||||
|
||||
inline STTx::STTx(SerialIter&& sit) : STTx(sit)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -149,6 +149,14 @@ enum TxType : std::uint16_t
|
||||
ttURITOKEN_CREATE_SELL_OFFER = 48,
|
||||
ttURITOKEN_CANCEL_SELL_OFFER = 49,
|
||||
|
||||
/* A pseudo-txn type used by featureRNG to shuffle and randomise the transaction set based on proposal
|
||||
* signatures */
|
||||
ttSHUFFLE = 88,
|
||||
|
||||
/* A UNLReport-validator only txn by featureRNG which allows validators to submit blinded entropy
|
||||
* to a consensus based random number system */
|
||||
ttENTROPY = 89,
|
||||
|
||||
/* A pseudo-txn alarm signal for invoking a hook, emitted by validators after alarm set conditions are met */
|
||||
ttCRON = 92,
|
||||
|
||||
|
||||
@@ -484,6 +484,7 @@ 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);
|
||||
REGISTER_FEATURE(RNG, Supported::yes, VoteBehavior::DefaultNo);
|
||||
|
||||
// The following amendments are obsolete, but must remain supported
|
||||
// because they could potentially get enabled.
|
||||
|
||||
@@ -73,6 +73,7 @@ enum class LedgerNameSpace : std::uint16_t {
|
||||
IMPORT_VLSEQ = 'I',
|
||||
UNL_REPORT = 'R',
|
||||
CRON = 'L',
|
||||
RANDOM = 0x526E, // Rn
|
||||
|
||||
// No longer used or supported. Left here to reserve the space
|
||||
// to avoid accidental reuse.
|
||||
@@ -496,6 +497,13 @@ cron(uint32_t timestamp, std::optional<AccountID> const& id)
|
||||
return {ltCRON, uint256::fromVoid(h)};
|
||||
}
|
||||
|
||||
Keylet const&
|
||||
random() noexcept
|
||||
{
|
||||
static Keylet const ret{ltRANDOM, indexHash(LedgerNameSpace::RANDOM)};
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace keylet
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -167,6 +167,14 @@ InnerObjectFormats::InnerObjectFormats()
|
||||
{sfRemarkValue, soeOPTIONAL},
|
||||
{sfFlags, soeOPTIONAL},
|
||||
});
|
||||
|
||||
add(sfRandomDigestEntry.jsonName.c_str(),
|
||||
sfRandomDigestEntry.getCode(),
|
||||
{
|
||||
{sfValidator, soeREQUIRED},
|
||||
{sfNextRandomDigest, soeREQUIRED},
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
});
|
||||
}
|
||||
|
||||
InnerObjectFormats const&
|
||||
|
||||
@@ -381,6 +381,19 @@ LedgerFormats::LedgerFormats()
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::Random,
|
||||
ltRANDOM,
|
||||
{
|
||||
{sfRandomData, soeREQUIRED},
|
||||
{sfLastRandomData, soeREQUIRED},
|
||||
{sfEntropyCount, soeREQUIRED},
|
||||
{sfRandomDigests, soeREQUIRED},
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
{sfPreviousTxnID, soeREQUIRED},
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ 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);
|
||||
CONSTRUCT_TYPED_SFIELD(sfEntropyCount, "EntropyCount", UINT16, 99);
|
||||
|
||||
// 32-bit integers (common)
|
||||
CONSTRUCT_TYPED_SFIELD(sfNetworkID, "NetworkID", UINT32, 1);
|
||||
@@ -244,6 +245,9 @@ CONSTRUCT_TYPED_SFIELD(sfGovernanceMarks, "GovernanceMarks", UINT256,
|
||||
CONSTRUCT_TYPED_SFIELD(sfEmittedTxnID, "EmittedTxnID", UINT256, 97);
|
||||
CONSTRUCT_TYPED_SFIELD(sfHookCanEmit, "HookCanEmit", UINT256, 96);
|
||||
CONSTRUCT_TYPED_SFIELD(sfCron, "Cron", UINT256, 95);
|
||||
CONSTRUCT_TYPED_SFIELD(sfRandomData, "RandomData", UINT256, 94);
|
||||
CONSTRUCT_TYPED_SFIELD(sfLastRandomData, "LastRandomData", UINT256, 93);
|
||||
CONSTRUCT_TYPED_SFIELD(sfNextRandomDigest, "NextRandomDigest", UINT256, 92);
|
||||
|
||||
// currency amount (common)
|
||||
CONSTRUCT_TYPED_SFIELD(sfAmount, "Amount", AMOUNT, 1);
|
||||
@@ -316,6 +320,7 @@ CONSTRUCT_TYPED_SFIELD(sfEmitCallback, "EmitCallback", ACCOUNT,
|
||||
// account (uncommon)
|
||||
CONSTRUCT_TYPED_SFIELD(sfHookAccount, "HookAccount", ACCOUNT, 16);
|
||||
CONSTRUCT_TYPED_SFIELD(sfInform, "Inform", ACCOUNT, 99);
|
||||
CONSTRUCT_TYPED_SFIELD(sfValidator, "Validator", ACCOUNT, 98);
|
||||
|
||||
// vector of 256-bit
|
||||
CONSTRUCT_TYPED_SFIELD(sfIndexes, "Indexes", VECTOR256, 1, SField::sMD_Never);
|
||||
@@ -361,6 +366,7 @@ 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);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfRandomDigestEntry, "RandomDigestEntry", OBJECT, 89);
|
||||
|
||||
// array of objects
|
||||
// ARRAY/1 is reserved for end of array
|
||||
@@ -387,6 +393,7 @@ CONSTRUCT_UNTYPED_SFIELD(sfActiveValidators, "ActiveValidators", ARRAY,
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfImportVLKeys, "ImportVLKeys", ARRAY, 94);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfHookEmissions, "HookEmissions", ARRAY, 93);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfAmounts, "Amounts", ARRAY, 92);
|
||||
CONSTRUCT_UNTYPED_SFIELD(sfRandomDigests, "RandomDigests", ARRAY, 91);
|
||||
|
||||
// clang-format on
|
||||
|
||||
|
||||
@@ -324,13 +324,18 @@ STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const
|
||||
fullyCanonical);
|
||||
}
|
||||
}
|
||||
catch (std::exception const&)
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
// Assume it was a signature failure.
|
||||
validSig = false;
|
||||
|
||||
std::cout << "Invalid cause: " << e.what() << "\n";
|
||||
}
|
||||
if (validSig == false)
|
||||
{
|
||||
std::cout << "Invalid signature on tx: " << this->getFullText() << "\n";
|
||||
return Unexpected("Invalid signature.");
|
||||
}
|
||||
// Signature was verified.
|
||||
return {};
|
||||
}
|
||||
@@ -615,7 +620,19 @@ isPseudoTx(STObject const& tx)
|
||||
|
||||
auto tt = safe_cast<TxType>(*t);
|
||||
return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY ||
|
||||
tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON;
|
||||
tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON ||
|
||||
tt == ttSHUFFLE;
|
||||
}
|
||||
|
||||
bool
|
||||
isUVTx(STObject const& tx)
|
||||
{
|
||||
auto t = tx[~sfTransactionType];
|
||||
if (!t)
|
||||
return false;
|
||||
|
||||
auto tt = safe_cast<TxType>(*t);
|
||||
return tt == ttENTROPY;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -490,6 +490,23 @@ TxFormats::TxFormats()
|
||||
{sfStartTime, soeOPTIONAL},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::Entropy,
|
||||
ttENTROPY,
|
||||
{
|
||||
{sfRandomData, soeREQUIRED},
|
||||
{sfNextRandomDigest, soeREQUIRED},
|
||||
{sfParentHash, soeREQUIRED},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::Shuffle,
|
||||
ttSHUFFLE,
|
||||
{
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
{sfRandomData, soeREQUIRED},
|
||||
},
|
||||
commonFields);
|
||||
}
|
||||
|
||||
TxFormats const&
|
||||
|
||||
@@ -97,49 +97,53 @@ JSS(isSigningField); // out: RPC server_definitions
|
||||
JSS(isVLEncoded); // out: RPC server_definitions
|
||||
JSS(Import);
|
||||
JSS(ImportVLSequence);
|
||||
JSS(Invalid); //
|
||||
JSS(Invoke); // transaction type
|
||||
JSS(InvoiceID); // field
|
||||
JSS(LastLedgerSequence); // in: TransactionSign; field
|
||||
JSS(LedgerHashes); // ledger type.
|
||||
JSS(LimitAmount); // field.
|
||||
JSS(NetworkID); // field.
|
||||
JSS(NFTokenBurn); // transaction type.
|
||||
JSS(NFTokenMint); // transaction type.
|
||||
JSS(NFTokenOffer); // ledger type.
|
||||
JSS(NFTokenAcceptOffer); // transaction type.
|
||||
JSS(NFTokenCancelOffer); // transaction type.
|
||||
JSS(NFTokenCreateOffer); // transaction type.
|
||||
JSS(NFTokenPage); // ledger type.
|
||||
JSS(Offer); // ledger type.
|
||||
JSS(OfferCancel); // transaction type.
|
||||
JSS(OfferCreate); // transaction type.
|
||||
JSS(OfferSequence); // field.
|
||||
JSS(Paths); // in/out: TransactionSign
|
||||
JSS(PayChannel); // ledger type.
|
||||
JSS(Payment); // transaction type.
|
||||
JSS(PaymentChannelClaim); // transaction type.
|
||||
JSS(PaymentChannelCreate); // transaction type.
|
||||
JSS(PaymentChannelFund); // transaction type.
|
||||
JSS(Remit); // transaction type.
|
||||
JSS(RippleState); // ledger type.
|
||||
JSS(SLE_hit_rate); // out: GetCounts.
|
||||
JSS(SetFee); // transaction type.
|
||||
JSS(SetRemarks); // transaction type
|
||||
JSS(UNLModify); // transaction type.
|
||||
JSS(UNLReport); // transaction type.
|
||||
JSS(SettleDelay); // in: TransactionSign
|
||||
JSS(SendMax); // in: TransactionSign
|
||||
JSS(Sequence); // in/out: TransactionSign; field.
|
||||
JSS(SetFlag); // field.
|
||||
JSS(SetRegularKey); // transaction type.
|
||||
JSS(SetHook); // transaction type.
|
||||
JSS(Hook); // ledger type.
|
||||
JSS(HookDefinition); // ledger type.
|
||||
JSS(HookState); // ledger type.
|
||||
JSS(HookStateData); // field.
|
||||
JSS(HookStateKey); // field.
|
||||
JSS(EmittedTxn); // ledger type.
|
||||
JSS(Invalid); //
|
||||
JSS(Invoke); // transaction type
|
||||
JSS(InvoiceID); // field
|
||||
JSS(LastLedgerSequence); // in: TransactionSign; field
|
||||
JSS(LedgerHashes); // ledger type.
|
||||
JSS(LimitAmount); // field.
|
||||
JSS(NetworkID); // field.
|
||||
JSS(NFTokenBurn); // transaction type.
|
||||
JSS(NFTokenMint); // transaction type.
|
||||
JSS(NFTokenOffer); // ledger type.
|
||||
JSS(NFTokenAcceptOffer); // transaction type.
|
||||
JSS(NFTokenCancelOffer); // transaction type.
|
||||
JSS(NFTokenCreateOffer); // transaction type.
|
||||
JSS(NFTokenPage); // ledger type.
|
||||
JSS(Offer); // ledger type.
|
||||
JSS(OfferCancel); // transaction type.
|
||||
JSS(OfferCreate); // transaction type.
|
||||
JSS(OfferSequence); // field.
|
||||
JSS(Paths); // in/out: TransactionSign
|
||||
JSS(PayChannel); // ledger type.
|
||||
JSS(Payment); // transaction type.
|
||||
JSS(PaymentChannelClaim); // transaction type.
|
||||
JSS(PaymentChannelCreate); // transaction type.
|
||||
JSS(PaymentChannelFund); // transaction type.
|
||||
JSS(Remit); // transaction type.
|
||||
JSS(RippleState); // ledger type.
|
||||
JSS(Rng);
|
||||
JSS(Random);
|
||||
JSS(SLE_hit_rate); // out: GetCounts.
|
||||
JSS(SetFee); // transaction type.
|
||||
JSS(SetRemarks); // transaction type
|
||||
JSS(UNLModify); // transaction type.
|
||||
JSS(UNLReport); // transaction type.
|
||||
JSS(SettleDelay); // in: TransactionSign
|
||||
JSS(SendMax); // in: TransactionSign
|
||||
JSS(Sequence); // in/out: TransactionSign; field.
|
||||
JSS(SetFlag); // field.
|
||||
JSS(SetRegularKey); // transaction type.
|
||||
JSS(SetHook); // transaction type.
|
||||
JSS(Hook); // ledger type.
|
||||
JSS(HookDefinition); // ledger type.
|
||||
JSS(HookState); // ledger type.
|
||||
JSS(HookStateData); // field.
|
||||
JSS(HookStateKey); // field.
|
||||
JSS(EmittedTxn); // ledger type.
|
||||
JSS(Entropy);
|
||||
JSS(Shuffle);
|
||||
JSS(SignerList); // ledger type.
|
||||
JSS(SignerListSet); // transaction type.
|
||||
JSS(SigningPubKey); // field.
|
||||
|
||||
Reference in New Issue
Block a user