mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
Merge branch 'master' of github.com:jedmccaleb/NewCoin
Conflicts: src/LedgerEntrySet.h
This commit is contained in:
106
src/LedgerEntrySet.cpp
Normal file
106
src/LedgerEntrySet.cpp
Normal file
@@ -0,0 +1,106 @@
|
||||
#include "LedgerEntrySet.h"
|
||||
|
||||
#include <boost/make_shared.hpp>
|
||||
|
||||
LedgerEntrySet LedgerEntrySet::duplicate()
|
||||
{
|
||||
return LedgerEntrySet(mEntries, mSeq + 1);
|
||||
}
|
||||
|
||||
void LedgerEntrySet::setTo(LedgerEntrySet& e)
|
||||
{
|
||||
mEntries = e.mEntries;
|
||||
mSeq = e.mSeq;
|
||||
}
|
||||
|
||||
void LedgerEntrySet::swapWith(LedgerEntrySet& e)
|
||||
{
|
||||
std::swap(mSeq, e.mSeq);
|
||||
mEntries.swap(e.mEntries);
|
||||
}
|
||||
|
||||
SLE::pointer LedgerEntrySet::getEntry(const uint256& index, LedgerEntryAction& action)
|
||||
{
|
||||
boost::unordered_map<uint256, LedgerEntrySetEntry>::iterator it = mEntries.find(index);
|
||||
if (it == mEntries.end())
|
||||
{
|
||||
action = taaNONE;
|
||||
return SLE::pointer();
|
||||
}
|
||||
if (it->second.mSeq != mSeq)
|
||||
{
|
||||
it->second.mEntry = boost::make_shared<SerializedLedgerEntry>(*it->second.mEntry);
|
||||
it->second.mSeq = mSeq;
|
||||
}
|
||||
action = it->second.mAction;
|
||||
return it->second.mEntry;
|
||||
}
|
||||
|
||||
LedgerEntryAction LedgerEntrySet::hasEntry(const uint256& index) const
|
||||
{
|
||||
boost::unordered_map<uint256, LedgerEntrySetEntry>::const_iterator it = mEntries.find(index);
|
||||
if (it == mEntries.end())
|
||||
return taaNONE;
|
||||
return it->second.mAction;
|
||||
}
|
||||
|
||||
void LedgerEntrySet::entryCache(SLE::pointer sle)
|
||||
{
|
||||
boost::unordered_map<uint256, LedgerEntrySetEntry>::iterator it = mEntries.find(sle->getIndex());
|
||||
if (it == mEntries.end())
|
||||
mEntries.insert(std::make_pair(sle->getIndex(), LedgerEntrySetEntry(sle, taaCACHED, mSeq)));
|
||||
else if (it->second.mAction == taaCACHED)
|
||||
{
|
||||
it->second.mSeq = mSeq;
|
||||
it->second.mEntry = sle;
|
||||
}
|
||||
else
|
||||
throw std::runtime_error("Cache after modify/delete");
|
||||
}
|
||||
|
||||
void LedgerEntrySet::entryCreate(SLE::pointer sle)
|
||||
{
|
||||
boost::unordered_map<uint256, LedgerEntrySetEntry>::iterator it = mEntries.find(sle->getIndex());
|
||||
if (it == mEntries.end())
|
||||
mEntries.insert(std::make_pair(sle->getIndex(), LedgerEntrySetEntry(sle, taaDELETE, mSeq)));
|
||||
else if (it->second.mAction == taaDELETE)
|
||||
throw std::runtime_error("Create after delete"); // We could make this a modify
|
||||
else if (it->second.mAction == taaMODIFY)
|
||||
throw std::runtime_error("Create after modify");
|
||||
else
|
||||
{
|
||||
it->second.mSeq = mSeq;
|
||||
it->second.mEntry = sle;
|
||||
it->second.mAction = taaDELETE;
|
||||
}
|
||||
}
|
||||
|
||||
void LedgerEntrySet::entryModify(SLE::pointer sle)
|
||||
{
|
||||
boost::unordered_map<uint256, LedgerEntrySetEntry>::iterator it = mEntries.find(sle->getIndex());
|
||||
if (it == mEntries.end())
|
||||
mEntries.insert(std::make_pair(sle->getIndex(), LedgerEntrySetEntry(sle, taaMODIFY, mSeq)));
|
||||
else if (it->second.mAction == taaDELETE)
|
||||
throw std::runtime_error("Modify after delete");
|
||||
else
|
||||
{
|
||||
it->second.mSeq = mSeq;
|
||||
it->second.mEntry = sle;
|
||||
it->second.mAction = (it->second.mAction == taaCREATE) ? taaCREATE : taaDELETE;
|
||||
}
|
||||
}
|
||||
|
||||
void LedgerEntrySet::entryDelete(SLE::pointer sle)
|
||||
{
|
||||
boost::unordered_map<uint256, LedgerEntrySetEntry>::iterator it = mEntries.find(sle->getIndex());
|
||||
if (it == mEntries.end())
|
||||
mEntries.insert(std::make_pair(sle->getIndex(), LedgerEntrySetEntry(sle, taaDELETE, mSeq)));
|
||||
else if (it->second.mAction == taaCREATE) // We support delete after create
|
||||
mEntries.erase(it);
|
||||
else
|
||||
{
|
||||
it->second.mSeq = mSeq;
|
||||
it->second.mEntry = sle;
|
||||
it->second.mAction = taaDELETE;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
#ifndef __LEDGERENTRYSET__
|
||||
#define __LEDGERENTRYSET__
|
||||
|
||||
#include <boost/unordered_map.hpp>
|
||||
|
||||
#include "SerializedLedger.h"
|
||||
|
||||
enum LedgerEntryAction
|
||||
@@ -10,7 +12,8 @@ enum LedgerEntryAction
|
||||
taaMODIFY, // Modifed, must have previously been taaCACHED.
|
||||
taaDELETE, // Delete, must have previously been taaDELETE or taaMODIFY.
|
||||
taaCREATE, // Newly created.
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
class LedgerEntrySetEntry
|
||||
{
|
||||
@@ -19,14 +22,18 @@ public:
|
||||
LedgerEntryAction mAction;
|
||||
int mSeq;
|
||||
|
||||
LedgerEntrySetEntry(SLE::pointer e, LedgerEntryAction a, int s) : mEntry(e), mAction(a), mSeq(s) { ; }
|
||||
};
|
||||
|
||||
|
||||
class LedgerEntrySet
|
||||
{
|
||||
protected:
|
||||
boost::unordered_map<uint256, LedgerEntrySetEntry> mEntries;
|
||||
int mSeq;
|
||||
|
||||
LedgerEntrySet(const boost::unordered_map<uint256, LedgerEntrySetEntry> &e, int m) : mEntries(e), mSeq(m) { ; }
|
||||
|
||||
public:
|
||||
LedgerEntrySet() : mSeq(0) { ; }
|
||||
|
||||
@@ -35,6 +42,10 @@ public:
|
||||
void setTo(LedgerEntrySet&); // Set this set to have the same contents as another
|
||||
void swapWith(LedgerEntrySet&); // Swap the contents of two sets
|
||||
|
||||
int getSeq() const { return mSeq; }
|
||||
void bumpSeq() { ++mSeq; }
|
||||
void clear() { mEntries.empty(); mSeq = 0; }
|
||||
|
||||
// basic entry functions
|
||||
SLE::pointer getEntry(const uint256& index, LedgerEntryAction&);
|
||||
LedgerEntryAction hasEntry(const uint256& index) const;
|
||||
@@ -44,11 +55,11 @@ public:
|
||||
void entryModify(SLE::pointer); // This entry will be modified
|
||||
|
||||
// iterator functions
|
||||
bool isEmpty() const;
|
||||
boost::unordered_map<uint256, LedgerEntrySetEntry>::const_iterator begin() const;
|
||||
boost::unordered_map<uint256, LedgerEntrySetEntry>::const_iterator end() const;
|
||||
boost::unordered_map<uint256, LedgerEntrySetEntry>::iterator begin();
|
||||
boost::unordered_map<uint256, LedgerEntrySetEntry>::iterator end();
|
||||
bool isEmpty() const { return mEntries.empty(); }
|
||||
boost::unordered_map<uint256, LedgerEntrySetEntry>::const_iterator begin() const { return mEntries.begin(); }
|
||||
boost::unordered_map<uint256, LedgerEntrySetEntry>::const_iterator end() const { return mEntries.end(); }
|
||||
boost::unordered_map<uint256, LedgerEntrySetEntry>::iterator begin() { return mEntries.begin(); }
|
||||
boost::unordered_map<uint256, LedgerEntrySetEntry>::iterator end() { return mEntries.end(); }
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -77,8 +77,6 @@ public:
|
||||
};
|
||||
|
||||
extern std::size_t hash_value(const SHAMapNode& mn);
|
||||
extern std::size_t hash_value(const uint256& u);
|
||||
extern std::size_t hash_value(const uint160& u);
|
||||
|
||||
class SHAMapItem
|
||||
{ // an item stored in a SHAMap
|
||||
|
||||
@@ -699,26 +699,16 @@ SLE::pointer TransactionEngine::entryCache(LedgerEntryType letType, const uint25
|
||||
|
||||
if (!uIndex.isZero())
|
||||
{
|
||||
entryMap::const_iterator it = mEntries.find(uIndex);
|
||||
|
||||
switch (it == mEntries.end() ? taaNONE : it->second.second)
|
||||
LedgerEntryAction action;
|
||||
sleEntry = mNodes.getEntry(uIndex, action);
|
||||
if (!sleEntry)
|
||||
{
|
||||
case taaNONE:
|
||||
sleEntry = mLedger->getSLE(uIndex);
|
||||
if (sleEntry)
|
||||
mEntries[uIndex] = std::make_pair(sleEntry, taaCACHED); // Add to cache.
|
||||
break;
|
||||
|
||||
case taaCREATE:
|
||||
case taaCACHED:
|
||||
case taaMODIFY:
|
||||
sleEntry = it->second.first; // Get from cache.
|
||||
break;
|
||||
|
||||
case taaDELETE:
|
||||
assert(false); // Unexpected case.
|
||||
break;
|
||||
sleEntry = mLedger->getSLE(uIndex);
|
||||
if (sleEntry)
|
||||
mNodes.entryCache(sleEntry);
|
||||
}
|
||||
else if(action == taaDELETE)
|
||||
assert(false);
|
||||
}
|
||||
|
||||
return sleEntry;
|
||||
@@ -729,10 +719,8 @@ SLE::pointer TransactionEngine::entryCreate(LedgerEntryType letType, const uint2
|
||||
assert(!uIndex.isZero());
|
||||
|
||||
SLE::pointer sleNew = boost::make_shared<SerializedLedgerEntry>(letType);
|
||||
|
||||
sleNew->setIndex(uIndex);
|
||||
|
||||
mEntries[uIndex] = std::make_pair(sleNew, taaCREATE);
|
||||
mNodes.entryCreate(sleNew);
|
||||
|
||||
return sleNew;
|
||||
}
|
||||
@@ -740,60 +728,23 @@ SLE::pointer TransactionEngine::entryCreate(LedgerEntryType letType, const uint2
|
||||
|
||||
void TransactionEngine::entryDelete(SLE::pointer sleEntry)
|
||||
{
|
||||
assert(sleEntry);
|
||||
const uint256& uIndex = sleEntry->getIndex();
|
||||
entryMap::const_iterator it = mEntries.find(uIndex);
|
||||
|
||||
switch (it == mEntries.end() ? taaNONE : it->second.second)
|
||||
{
|
||||
case taaCREATE:
|
||||
assert(false); // Unexpected case.
|
||||
break;
|
||||
|
||||
case taaCACHED:
|
||||
case taaMODIFY:
|
||||
case taaNONE:
|
||||
mEntries[uIndex] = std::make_pair(sleEntry, taaDELETE); // Upgrade.
|
||||
break;
|
||||
|
||||
case taaDELETE:
|
||||
nothing(); // No change.
|
||||
break;
|
||||
}
|
||||
mNodes.entryDelete(sleEntry);
|
||||
}
|
||||
|
||||
void TransactionEngine::entryModify(SLE::pointer sleEntry)
|
||||
{
|
||||
assert(sleEntry);
|
||||
const uint256& uIndex = sleEntry->getIndex();
|
||||
entryMap::const_iterator it = mEntries.find(uIndex);
|
||||
|
||||
switch (it == mEntries.end() ? taaNONE : it->second.second)
|
||||
{
|
||||
case taaDELETE:
|
||||
assert(false); // Unexpected case.
|
||||
break;
|
||||
|
||||
case taaCACHED:
|
||||
case taaNONE:
|
||||
mEntries[uIndex] = std::make_pair(sleEntry, taaMODIFY); // Upgrade.
|
||||
break;
|
||||
|
||||
case taaCREATE:
|
||||
case taaMODIFY:
|
||||
nothing(); // No change.
|
||||
break;
|
||||
}
|
||||
mNodes.entryModify(sleEntry);
|
||||
}
|
||||
|
||||
void TransactionEngine::txnWrite()
|
||||
{
|
||||
// Write back the account states and add the transaction to the ledger
|
||||
BOOST_FOREACH(entryMap_value_type it, mEntries)
|
||||
for (boost::unordered_map<uint256, LedgerEntrySetEntry>::iterator it = mNodes.begin(), end = mNodes.end();
|
||||
it != end; ++it)
|
||||
{
|
||||
const SLE::pointer& sleEntry = it.second.first;
|
||||
const SLE::pointer& sleEntry = it->second.mEntry;
|
||||
|
||||
switch (it.second.second)
|
||||
switch (it->second.mAction)
|
||||
{
|
||||
case taaNONE:
|
||||
assert(false);
|
||||
@@ -824,7 +775,7 @@ void TransactionEngine::txnWrite()
|
||||
{
|
||||
Log(lsINFO) << "applyTransaction: taaDELETE: " << sleEntry->getText();
|
||||
|
||||
if (!mLedger->peekAccountStateMap()->delItem(it.first))
|
||||
if (!mLedger->peekAccountStateMap()->delItem(it->first))
|
||||
assert(false);
|
||||
}
|
||||
break;
|
||||
@@ -834,19 +785,9 @@ void TransactionEngine::txnWrite()
|
||||
|
||||
// This is for when a transaction fails from the issuer's point of view and the current changes need to be cleared so other
|
||||
// actions can be applied to the ledger.
|
||||
void TransactionEngine::entryReset(const SerializedTransaction& txn)
|
||||
void TransactionEngine::entryReset()
|
||||
{
|
||||
mEntries.clear(); // Lose old SLE modifications.
|
||||
mTxnAccount = entryCache(ltACCOUNT_ROOT, Ledger::getAccountRootIndex(mTxnAccountID)); // Get new SLE.
|
||||
|
||||
entryModify(mTxnAccount);
|
||||
|
||||
STAmount saPaid = txn.getTransactionFee();
|
||||
STAmount saSrcBalance = mTxnAccount->getIValueFieldAmount(sfBalance);
|
||||
|
||||
mTxnAccount->setIFieldAmount(sfBalance, saSrcBalance - saPaid);
|
||||
|
||||
// XXX Bump sequence too.
|
||||
mNodes.setTo(mOrigNodes);
|
||||
}
|
||||
|
||||
TransactionEngineResult TransactionEngine::applyTransaction(const SerializedTransaction& txn,
|
||||
@@ -855,6 +796,7 @@ TransactionEngineResult TransactionEngine::applyTransaction(const SerializedTran
|
||||
Log(lsTRACE) << "applyTransaction>";
|
||||
assert(mLedger);
|
||||
mLedgerParentCloseTime = mLedger->getParentCloseTimeNC();
|
||||
mNodes.clear();
|
||||
|
||||
#ifdef DEBUG
|
||||
if (1)
|
||||
@@ -1017,7 +959,7 @@ TransactionEngineResult TransactionEngine::applyTransaction(const SerializedTran
|
||||
bHaveAuthKey = mTxnAccount->getIFieldPresent(sfAuthorizedKey);
|
||||
}
|
||||
|
||||
// Check if account cliamed.
|
||||
// Check if account claimed.
|
||||
if (terSUCCESS == terResult)
|
||||
{
|
||||
switch (txn.getTxnType())
|
||||
@@ -1168,6 +1110,7 @@ TransactionEngineResult TransactionEngine::applyTransaction(const SerializedTran
|
||||
if (terSUCCESS == terResult)
|
||||
{
|
||||
entryModify(mTxnAccount);
|
||||
mOrigNodes = mNodes.duplicate();
|
||||
|
||||
switch (txn.getTxnType())
|
||||
{
|
||||
@@ -1251,7 +1194,8 @@ TransactionEngineResult TransactionEngine::applyTransaction(const SerializedTran
|
||||
}
|
||||
|
||||
mTxnAccount = SLE::pointer();
|
||||
mEntries.clear();
|
||||
mNodes.clear();
|
||||
mOrigNodes.clear();
|
||||
mUnfunded.clear();
|
||||
|
||||
return terResult;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "Ledger.h"
|
||||
#include "SerializedTransaction.h"
|
||||
#include "SerializedLedger.h"
|
||||
#include "LedgerEntrySet.h"
|
||||
|
||||
// A TransactionEngine applies serialized transactions to a ledger
|
||||
// It can also, verify signatures, verify fees, and give rejection reasons
|
||||
@@ -97,17 +98,6 @@ enum TransactionEngineParams
|
||||
tepMETADATA = 5, // put metadata in tree, not transaction
|
||||
};
|
||||
|
||||
enum TransactionAccountAction
|
||||
{
|
||||
taaNONE,
|
||||
taaCACHED, // Unmodified.
|
||||
taaMODIFY, // Modifed, must have previously been taaCACHED.
|
||||
taaDELETE, // Delete, must have previously been taaDELETE or taaMODIFY.
|
||||
taaCREATE, // Newly created.
|
||||
};
|
||||
|
||||
typedef std::pair<TransactionAccountAction, SerializedLedgerEntry::pointer> AffectedAccount;
|
||||
|
||||
// Hold a path state under incremental application.
|
||||
class PathState
|
||||
{
|
||||
@@ -129,10 +119,7 @@ public:
|
||||
class TransactionEngine
|
||||
{
|
||||
private:
|
||||
typedef boost::unordered_map<uint256, std::pair<SLE::pointer, TransactionAccountAction> > entryMap;
|
||||
typedef entryMap::iterator entryMap_iterator;
|
||||
typedef entryMap::const_iterator entryMap_const_iterator;
|
||||
typedef entryMap::iterator::value_type entryMap_value_type;
|
||||
LedgerEntrySet mNodes, mOrigNodes;
|
||||
|
||||
TransactionEngineResult dirAdd(
|
||||
uint64& uNodeDir, // Node of entry.
|
||||
@@ -194,7 +181,6 @@ protected:
|
||||
uint160 mTxnAccountID;
|
||||
SLE::pointer mTxnAccount;
|
||||
|
||||
entryMap mEntries;
|
||||
boost::unordered_set<uint256> mUnfunded; // Indexes that were found unfunded.
|
||||
|
||||
SLE::pointer entryCreate(LedgerEntryType letType, const uint256& uIndex);
|
||||
@@ -202,7 +188,7 @@ protected:
|
||||
void entryDelete(SLE::pointer sleEntry);
|
||||
void entryModify(SLE::pointer sleEntry);
|
||||
|
||||
void entryReset(const SerializedTransaction& txn);
|
||||
void entryReset();
|
||||
|
||||
STAmount rippleHolds(const uint160& uAccountID, const uint160& uCurrency, const uint160& uIssuerID);
|
||||
STAmount rippleTransit(const uint160& uSenderID, const uint160& uReceiverID, const uint160& uIssuerID, const STAmount& saAmount);
|
||||
|
||||
@@ -89,6 +89,22 @@ Json::Value TMNEBalance::getJson(int p) const
|
||||
return ret;
|
||||
}
|
||||
|
||||
void TMNEUnfunded::addRaw(Serializer& sit) const
|
||||
{
|
||||
sit.add8(mType);
|
||||
}
|
||||
|
||||
Json::Value TMNEUnfunded::getJson(int) const
|
||||
{
|
||||
return Json::Value("delete_unfunded");
|
||||
}
|
||||
|
||||
int TMNEUnfunded::compare(const TransactionMetaNodeEntry&) const
|
||||
{
|
||||
assert(false); // Can't be two deletes for same node
|
||||
return 0;
|
||||
}
|
||||
|
||||
TransactionMetaNode::TransactionMetaNode(const uint256& node, SerializerIterator& sit) : mNode(node)
|
||||
{
|
||||
mNode = sit.get256();
|
||||
@@ -100,6 +116,8 @@ TransactionMetaNode::TransactionMetaNode(const uint256& node, SerializerIterator
|
||||
type = sit.get8();
|
||||
if (type == TransactionMetaNodeEntry::TMNChangedBalance)
|
||||
mEntries.insert(boost::shared_ptr<TransactionMetaNodeEntry>(new TMNEBalance(sit)));
|
||||
if (type == TransactionMetaNodeEntry::TMNDeleteUnfunded)
|
||||
mEntries.insert(boost::shared_ptr<TransactionMetaNodeEntry>(new TMNEUnfunded()));
|
||||
else if (type != TransactionMetaNodeEntry::TMNEndOfMetadata)
|
||||
throw std::runtime_error("Unparseable metadata");
|
||||
} while (type != TransactionMetaNodeEntry::TMNEndOfMetadata);
|
||||
|
||||
@@ -21,6 +21,7 @@ public:
|
||||
|
||||
static const int TMNEndOfMetadata = 0;
|
||||
static const int TMNChangedBalance = 1;
|
||||
static const int TMNDeleteUnfunded = 2;
|
||||
|
||||
int mType;
|
||||
TransactionMetaNodeEntry(int type) : mType(type) { ; }
|
||||
@@ -68,6 +69,15 @@ public:
|
||||
virtual int compare(const TransactionMetaNodeEntry&) const;
|
||||
};
|
||||
|
||||
class TMNEUnfunded : public TransactionMetaNodeEntry
|
||||
{ // node was deleted because it was unfunded
|
||||
public:
|
||||
TMNEUnfunded() : TransactionMetaNodeEntry(TMNDeleteUnfunded) { ; }
|
||||
virtual void addRaw(Serializer&) const;
|
||||
virtual Json::Value getJson(int) const;
|
||||
virtual int compare(const TransactionMetaNodeEntry&) const;
|
||||
};
|
||||
|
||||
class TransactionMetaNode
|
||||
{ // a node that has been affected by a transaction
|
||||
public:
|
||||
@@ -117,10 +127,10 @@ public:
|
||||
void addRaw(Serializer&) const;
|
||||
|
||||
void threadNode(const uint256& node, const uint256& previousTransaction, uint32 previousLedger);
|
||||
bool signedBy(const uint256& node);
|
||||
bool signedBy(const uint256& node, const STAmount& fee);
|
||||
bool deleteUnfunded(const uint256& node);
|
||||
bool adjustBalance(const uint256& node, unsigned flags, const STAmount &amount);
|
||||
bool adjustBalances(const uint256& node, unsigned flags, const STAmount &firstAmt, const STAmount &secondAmt);
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -469,6 +469,8 @@ inline const uint160 operator^(const uint160& a, const uint160& b) { return (b
|
||||
inline const uint160 operator&(const uint160& a, const uint160& b) { return (base_uint160)a & (base_uint160)b; }
|
||||
inline const uint160 operator|(const uint160& a, const uint160& b) { return (base_uint160)a | (base_uint160)b; }
|
||||
|
||||
extern std::size_t hash_value(const uint160&);
|
||||
|
||||
inline const std::string strHex(const uint160& ui)
|
||||
{
|
||||
return strHex(ui.begin(), ui.size());
|
||||
@@ -555,6 +557,7 @@ inline bool operator!=(const uint256& a, const uint256& b) { return (base_ui
|
||||
inline const uint256 operator^(const uint256& a, const uint256& b) { return (base_uint256)a ^ (base_uint256)b; }
|
||||
inline const uint256 operator&(const uint256& a, const uint256& b) { return (base_uint256)a & (base_uint256)b; }
|
||||
inline const uint256 operator|(const uint256& a, const uint256& b) { return (base_uint256)a | (base_uint256)b; }
|
||||
extern std::size_t hash_value(const uint256&);
|
||||
|
||||
inline int Testuint256AdHoc(std::vector<std::string> vArg)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user