mirror of
https://github.com/twisterarmy/twister-core.git
synced 2025-01-22 20:44:56 +00:00
highly experimental soft checkpoint based on consensus
This commit is contained in:
parent
1df2b24f90
commit
102d172ca3
@ -146,6 +146,7 @@ BITCOIN_TWISTER_SOURCES = \
|
||||
src/rpcrawtransaction.cpp \
|
||||
src/script.cpp \
|
||||
src/scrypt.cpp \
|
||||
src/softcheckpoint.cpp \
|
||||
src/sync.cpp \
|
||||
src/util.cpp \
|
||||
src/wallet.cpp \
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
#include "main.h"
|
||||
#include "uint256.h"
|
||||
#include "softcheckpoint.h"
|
||||
|
||||
namespace Checkpoints
|
||||
{
|
||||
@ -79,7 +80,8 @@ namespace Checkpoints
|
||||
|
||||
MapCheckpoints::const_iterator i = checkpoints.find(nHeight);
|
||||
if (i == checkpoints.end()) return true;
|
||||
return hash == i->second;
|
||||
if (hash != i->second) return false;
|
||||
return SoftCheckpoints::CheckBlock(nHeight, hash);
|
||||
}
|
||||
|
||||
// Guess how far we are in the verification process at the given block index
|
||||
@ -139,4 +141,11 @@ namespace Checkpoints
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int GetHighestCheckpoint()
|
||||
{
|
||||
const MapCheckpoints& checkpoints = *Checkpoints().mapCheckpoints;
|
||||
MapCheckpoints::const_reverse_iterator i = checkpoints.rbegin();
|
||||
return (*i).first;
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,9 @@ namespace Checkpoints
|
||||
// Returns last CBlockIndex* in mapBlockIndex that is a checkpoint
|
||||
CBlockIndex* GetLastCheckpoint(const std::map<uint256, CBlockIndex*>& mapBlockIndex);
|
||||
|
||||
// Returns the height of the highest checkpoint
|
||||
int GetHighestCheckpoint();
|
||||
|
||||
double GuessVerificationProgress(CBlockIndex *pindex);
|
||||
|
||||
extern bool fEnabled;
|
||||
|
@ -8,7 +8,7 @@
|
||||
// These need to be macros, as version.cpp's and bitcoin-qt.rc's voodoo requires it
|
||||
#define CLIENT_VERSION_MAJOR 0
|
||||
#define CLIENT_VERSION_MINOR 9
|
||||
#define CLIENT_VERSION_REVISION 07
|
||||
#define CLIENT_VERSION_REVISION 8
|
||||
#define CLIENT_VERSION_BUILD 0
|
||||
|
||||
// Set to true for release, false for prerelease or test build
|
||||
|
@ -14,6 +14,7 @@
|
||||
#include "util.h"
|
||||
#include "ui_interface.h"
|
||||
#include "checkpoints.h"
|
||||
#include "softcheckpoint.h"
|
||||
#include "twister.h"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
@ -402,6 +403,7 @@ bool AppInit2(boost::thread_group& threadGroup)
|
||||
// ********************************************************* Step 2: parameter interactions
|
||||
|
||||
Checkpoints::fEnabled = GetBoolArg("-checkpoints", true);
|
||||
SoftCheckpoints::fEnabled = GetBoolArg("-softcheckpoints", true);
|
||||
if (!SelectParamsFromCommandLine()) {
|
||||
return InitError("Invalid combination of -testnet and -regtest.");
|
||||
}
|
||||
|
22
src/main.cpp
22
src/main.cpp
@ -5,6 +5,7 @@
|
||||
|
||||
#include "alert.h"
|
||||
#include "checkpoints.h"
|
||||
#include "softcheckpoint.h"
|
||||
#include "db.h"
|
||||
#include "txdb.h"
|
||||
#include "net.h"
|
||||
@ -1731,6 +1732,8 @@ bool AcceptBlock(CBlock& block, CValidationState& state, CDiskBlockPos* dbp)
|
||||
if (nBestHeight > (pnode->nStartingHeight != -1 ? pnode->nStartingHeight - 2000 : nBlockEstimate))
|
||||
pnode->PushInventory(CInv(MSG_BLOCK, hash));
|
||||
}
|
||||
|
||||
SoftCheckpoints::NewBlockAccepted();
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -2753,6 +2756,8 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
|
||||
item.second.RelayTo(pfrom);
|
||||
}
|
||||
|
||||
SoftCheckpoints::RelayLastCPToNode(pfrom);
|
||||
|
||||
pfrom->fSuccessfullyConnected = true;
|
||||
|
||||
printf("receive version message: version %d, blocks=%d, us=%s, them=%s, peer=%s\n", pfrom->nVersion, pfrom->nStartingHeight, addrMe.ToString().c_str(), addrFrom.ToString().c_str(), pfrom->addr.ToString().c_str());
|
||||
@ -3016,6 +3021,23 @@ bool static ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv)
|
||||
}
|
||||
|
||||
|
||||
else if (strCommand == "cp")
|
||||
{
|
||||
CSoftCheckpoint cp;
|
||||
vRecv >> cp;
|
||||
|
||||
const std::string username( cp.vchUsername.data(), cp.vchUsername.size() );
|
||||
const std::string sign( cp.vchSign.data(), cp.vchSign.size() );
|
||||
|
||||
if (SoftCheckpoints::CastVoteSoftCheckpoint(cp.nHeight,cp.blockHash,username,sign)) {
|
||||
SoftCheckpoints::RelayCP(cp, pfrom);
|
||||
} else {
|
||||
//TODO: misbehaving only if invalid signature etc. not because we have it already.
|
||||
//pfrom->Misbehaving(nDoS);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
else if (strCommand == "block" && !fImporting && !fReindex) // Ignore blocks received while importing
|
||||
{
|
||||
CBlock block;
|
||||
|
@ -122,6 +122,7 @@ OBJS= \
|
||||
obj/rpcrawtransaction.o \
|
||||
obj/script.o \
|
||||
obj/scrypt.o \
|
||||
obj/softcheckpoint.o \
|
||||
obj/sync.o \
|
||||
obj/util.o \
|
||||
obj/wallet.o \
|
||||
|
@ -136,6 +136,7 @@ OBJS= \
|
||||
obj/rpcrawtransaction.o \
|
||||
obj/script.o \
|
||||
obj/scrypt.o \
|
||||
obj/softcheckpoint.o \
|
||||
obj/sync.o \
|
||||
obj/util.o \
|
||||
obj/wallet.o \
|
||||
|
@ -138,6 +138,7 @@ OBJS= \
|
||||
obj/rpcrawtransaction.o \
|
||||
obj/script.o \
|
||||
obj/scrypt.o \
|
||||
obj/softcheckpoint.o \
|
||||
obj/sync.o \
|
||||
obj/util.o \
|
||||
obj/wallet.o \
|
||||
|
254
src/softcheckpoint.cpp
Normal file
254
src/softcheckpoint.cpp
Normal file
@ -0,0 +1,254 @@
|
||||
// Copyright (c) 2014 Miguel Freitas
|
||||
// TODO: write description for the soft checkpoint
|
||||
// More info:
|
||||
// https://groups.google.com/forum/#!topic/twister-dev/tH3HlVQ_wmo
|
||||
|
||||
#include <boost/assign/list_of.hpp> // for 'map_list_of()'
|
||||
#include <boost/assign.hpp>
|
||||
#include <boost/foreach.hpp>
|
||||
|
||||
#include "softcheckpoint.h"
|
||||
|
||||
#include "main.h"
|
||||
#include "checkpoints.h"
|
||||
#include "uint256.h"
|
||||
#include "script.h"
|
||||
#include "init.h"
|
||||
#include "twister.h"
|
||||
|
||||
#define dbgprintf OutputDebugStringF
|
||||
//#define dbgprintf(...) // no debug printf
|
||||
|
||||
namespace SoftCheckpoints
|
||||
{
|
||||
bool fEnabled = true;
|
||||
CCriticalSection cs_softCP;
|
||||
|
||||
typedef std::pair<int, uint256> Checkpoint; // height, hash
|
||||
typedef std::map<std::string, std::string> CPSigMap; // user, sign
|
||||
typedef std::pair<std::string, std::string> CPSigPair; // user, sign
|
||||
|
||||
static Checkpoint lastSoftCP;
|
||||
static CPSigMap lastSoftCPSigs;
|
||||
|
||||
static std::map<Checkpoint, CPSigMap> nextCandidates;
|
||||
static std::map<Checkpoint, CPSigMap> uncheckedCandidates;
|
||||
|
||||
static std::set<std::string> uniqueUsersList =
|
||||
boost::assign::list_of
|
||||
("mf1")("mf1a")("mf2")("mf2a")("mf3");
|
||||
|
||||
|
||||
void SetSoftCPBestChain() {
|
||||
// requires cs_main and cs_softCP locked (in this order)
|
||||
if( !fEnabled )
|
||||
return;
|
||||
|
||||
if( lastSoftCP.first && mapBlockIndex.count(lastSoftCP.second) &&
|
||||
!mapBlockIndex[lastSoftCP.second]->IsInMainChain() ) {
|
||||
dbgprintf("SoftCheckpoints::SetSoftCPBestChain: lastSoftCP %d not in main chain\n", lastSoftCP.first);
|
||||
|
||||
// ? use !mapOrphanBlocks.count() ?
|
||||
|
||||
CBlockIndex *pindex = mapBlockIndex[lastSoftCP.second];
|
||||
while( pindex && !pindex->IsInMainChain() ) {
|
||||
pindex = pindex->pprev;
|
||||
}
|
||||
|
||||
if( !pindex ) {
|
||||
dbgprintf("SoftCheckpoints::SetSoftCPBestChain: lastSoftCP %d currently orphaned? strange.\n", lastSoftCP.first);
|
||||
return;
|
||||
}
|
||||
|
||||
dbgprintf("SoftCheckpoints::SetSoftCPBestChain: trying SetBestChain with lastSoftCP %d\n", lastSoftCP.first);
|
||||
CValidationState state;
|
||||
if (!SetBestChain(state, mapBlockIndex[lastSoftCP.second])) {
|
||||
dbgprintf("SoftCheckpoints::lastSoftCPUpdated: SetBestChain failed\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LastSoftCPUpdated() {
|
||||
//LOCK(cs_main); // FIXME: not needed if called from ProcessMessage. check. wrong mutex order is bad.
|
||||
SetSoftCPBestChain();
|
||||
}
|
||||
|
||||
std::string CPtoString(Checkpoint &cp) {
|
||||
CScript cs = CScript() << cp.first << cp.second;
|
||||
return std::string((const char *)cs.data(), cs.size());
|
||||
}
|
||||
|
||||
void NewBlockAccepted() {
|
||||
LOCK(cs_softCP);
|
||||
SetSoftCPBestChain();
|
||||
|
||||
if( (nBestHeight % SOFT_CHECKPOINT_PERIOD) == 0 &&
|
||||
nBestHeight > Checkpoints::GetHighestCheckpoint() &&
|
||||
nBestHeight > lastSoftCP.first + SOFT_CHECKPOINT_PERIOD &&
|
||||
!fImporting && !fReindex) {
|
||||
LOCK(pwalletMain->cs_wallet);
|
||||
BOOST_FOREACH(const PAIRTYPE(CKeyID, CKeyMetadata)& item, pwalletMain->mapKeyMetadata)
|
||||
{
|
||||
const std::string &username = item.second.username;
|
||||
if(uniqueUsersList.count(username)) {
|
||||
int height = nBestHeight - SOFT_CHECKPOINT_PERIOD;
|
||||
dbgprintf("SoftCheckpoints::NewBlockAccepted: user '%s' will vote for %d\n",
|
||||
username.c_str(), height);
|
||||
|
||||
CBlockIndex* pblockindex = FindBlockByHeight(height);
|
||||
assert( pblockindex );
|
||||
|
||||
Checkpoint cpPair = std::make_pair(height, *pblockindex->phashBlock);
|
||||
std::string dataToSign = CPtoString(cpPair);
|
||||
|
||||
std::string sign = createSignature(dataToSign, username);
|
||||
|
||||
if( sign.size() ) {
|
||||
if( CastVoteSoftCheckpoint(height, *pblockindex->phashBlock, username, sign) ) {
|
||||
dbgprintf("SoftCheckpoints::NewBlockAccepted: relaying our own vote\n");
|
||||
CSoftCheckpoint cp;
|
||||
cp.nHeight = height;
|
||||
cp.blockHash = *pblockindex->phashBlock;
|
||||
cp.vchUsername = std::vector<char>(username.begin(), username.end());
|
||||
cp.vchSign = std::vector<char>(sign.begin(), sign.end());
|
||||
SoftCheckpoints::RelayCP(cp, NULL);
|
||||
} else {
|
||||
dbgprintf("SoftCheckpoints::NewBlockAccepted: CastVoteSoftCheckpoint failed for our own vote!\n");
|
||||
}
|
||||
} else {
|
||||
dbgprintf("SoftCheckpoints::NewBlockAccepted: createSignature failed for user '%s'\n",
|
||||
username.c_str());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool CastVerifiedVote(Checkpoint &cp, const std::string &username, const std::string &sign) {
|
||||
if( cp.first == lastSoftCP.first ) {
|
||||
if( lastSoftCPSigs.count(username) ) {
|
||||
dbgprintf("SoftCheckpoints::CastVerifiedVote: '%s' already voted for lastSoftCP %d\n", username.c_str(), cp.first);
|
||||
return false;
|
||||
}
|
||||
if( cp != lastSoftCP ) {
|
||||
dbgprintf("SoftCheckpoints::CastVerifiedVote: '%s' voted for a different hash than lastSoftCP %d\n", username.c_str(), cp.first);
|
||||
return false;
|
||||
}
|
||||
dbgprintf("SoftCheckpoints::CastVerifiedVote: new vote for lastSoftCP %d by '%s'\n", cp.first, username.c_str());
|
||||
lastSoftCPSigs[username] = sign;
|
||||
return true;
|
||||
}
|
||||
|
||||
if( nextCandidates.count(cp) && nextCandidates[cp].count(username) ) {
|
||||
dbgprintf("SoftCheckpoints::CastVerifiedVote: '%s' already voted for candidate %d\n", username.c_str(), cp.first);
|
||||
return false;
|
||||
}
|
||||
|
||||
nextCandidates[cp][username] = sign;
|
||||
if( nextCandidates[cp].size() > uniqueUsersList.size() / 2) {
|
||||
dbgprintf("SoftCheckpoints::CastVerifiedVote: new soft checkpoint %d wins!\n", cp.first);
|
||||
lastSoftCP = cp;
|
||||
lastSoftCPSigs = nextCandidates[cp];
|
||||
nextCandidates.clear();
|
||||
LastSoftCPUpdated();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// returns true if vote is to be restransmitted
|
||||
bool CastVoteSoftCheckpoint(int height, const uint256 &hash, const std::string &username, const std::string &sign) {
|
||||
LOCK(cs_softCP);
|
||||
|
||||
if( (height % SOFT_CHECKPOINT_PERIOD) != 0 ) {
|
||||
dbgprintf("SoftCheckpoints::CastVoteSoftCheckpoint: height %d not multiple of SOFT_CHECKPOINT_PERIOD\n", height);
|
||||
return false;
|
||||
}
|
||||
|
||||
int hardCheckPointHeight = Checkpoints::GetHighestCheckpoint();
|
||||
|
||||
if( height < hardCheckPointHeight ) {
|
||||
dbgprintf("SoftCheckpoints::CastVoteSoftCheckpoint: height %d < hard checkpoint %d\n", height, hardCheckPointHeight);
|
||||
return false;
|
||||
}
|
||||
|
||||
if( height < lastSoftCP.first ) {
|
||||
dbgprintf("SoftCheckpoints::CastVoteSoftCheckpoint: height %d < soft checkpoint %d\n", height, lastSoftCP.first);
|
||||
return false;
|
||||
}
|
||||
|
||||
if( !uniqueUsersList.count(username) ) {
|
||||
dbgprintf("SoftCheckpoints::CastVoteSoftCheckpoint: username '%s' not accepted\n", username.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
Checkpoint cp = std::make_pair(height, hash);
|
||||
|
||||
if( nBestHeight < hardCheckPointHeight ) {
|
||||
// still downloading blocks, we can't check signatures yet
|
||||
dbgprintf("SoftCheckpoints::CastVoteSoftCheckpoint: vote for %d by '%s' added to unchecked\n", height, username.c_str());
|
||||
uncheckedCandidates[cp][username] = sign;
|
||||
return false;
|
||||
}
|
||||
|
||||
if( !verifySignature( CPtoString(cp), username, sign) ) {
|
||||
dbgprintf("SoftCheckpoints::CastVoteSoftCheckpoint: invalid signature by '%s'\n", username.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
dbgprintf("SoftCheckpoints::CastVoteSoftCheckpoint: signature by '%s' verified, casting vote\n", username.c_str());
|
||||
return CastVerifiedVote( cp, username, sign );
|
||||
}
|
||||
|
||||
bool CheckBlock(int nHeight, const uint256& hash) {
|
||||
LOCK(cs_softCP);
|
||||
if (!fEnabled)
|
||||
return true;
|
||||
|
||||
if (!lastSoftCP.first || nHeight != lastSoftCP.first)
|
||||
return true;
|
||||
dbgprintf("SoftCheckpoints::CheckBlock: height %d isOk=%d\n", nHeight, hash == lastSoftCP.second);
|
||||
return hash == lastSoftCP.second;
|
||||
}
|
||||
|
||||
void RelayCP(const CSoftCheckpoint& cp, CNode* pfrom) {
|
||||
LOCK(cs_vNodes);
|
||||
dbgprintf("SoftCheckpoints::RelayCP: relaying softCP height %d\n", cp.nHeight);
|
||||
BOOST_FOREACH(CNode* pnode, vNodes) {
|
||||
if(pnode == pfrom)
|
||||
continue;
|
||||
if (pnode->nVersion >= SOFT_CHECKPOINT_VERSION) {
|
||||
dbgprintf("SoftCheckpoints::RelayCP: pushMessage to %s\n", pnode->addr.ToString().c_str());
|
||||
pnode->PushMessage("cp", cp);
|
||||
}
|
||||
}
|
||||
|
||||
if( pfrom && lastSoftCP.first == cp.nHeight ) {
|
||||
if( !mapBlockIndex.count(lastSoftCP.second) ) {
|
||||
dbgprintf("SoftCheckpoints::RelayCP: requesting block height %d from node\n", cp.nHeight);
|
||||
PushGetBlocks(pfrom, pindexBest, cp.blockHash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RelayLastCPToNode(CNode* pnode) {
|
||||
LOCK(cs_softCP);
|
||||
if (!lastSoftCP.first)
|
||||
return;
|
||||
|
||||
if (pnode->nVersion >= SOFT_CHECKPOINT_VERSION) {
|
||||
dbgprintf("SoftCheckpoints::RelayToLastCP: relaying lastSoftCP height %d (size %zd) to %s\n",
|
||||
lastSoftCP.first, lastSoftCPSigs.size(), pnode->addr.ToString().c_str());
|
||||
|
||||
BOOST_FOREACH(const CPSigMap::value_type& i, lastSoftCPSigs) {
|
||||
CSoftCheckpoint cp;
|
||||
|
||||
cp.nHeight = lastSoftCP.first;
|
||||
cp.blockHash = lastSoftCP.second;
|
||||
cp.vchUsername = std::vector<char>(i.first.begin(), i.first.end());
|
||||
cp.vchSign = std::vector<char>(i.second.begin(), i.second.end());
|
||||
pnode->PushMessage("cp", cp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
72
src/softcheckpoint.h
Normal file
72
src/softcheckpoint.h
Normal file
@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2014 Miguel Freitas
|
||||
|
||||
#ifndef SOFT_CHECKPOINT_H
|
||||
#define SOFT_CHECKPOINT_H
|
||||
|
||||
#define SOFT_CHECKPOINT_PERIOD 6
|
||||
|
||||
#include "serialize.h"
|
||||
#include "net.h"
|
||||
#include "uint256.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
class CSoftCheckpoint;
|
||||
|
||||
/** Block-chain checkpoints are compiled-in sanity checks.
|
||||
* They are updated every release or three.
|
||||
*/
|
||||
namespace SoftCheckpoints
|
||||
{
|
||||
extern bool fEnabled;
|
||||
|
||||
// Returns true if block passes checkpoint checks
|
||||
bool CheckBlock(int nHeight, const uint256& hash);
|
||||
|
||||
void NewBlockAccepted();
|
||||
|
||||
// returns true if vote is to be restransmitted
|
||||
bool CastVoteSoftCheckpoint(int height, const uint256 &hash, const std::string &username, const std::string &sign);
|
||||
|
||||
void RelayCP(const CSoftCheckpoint& cp, CNode* pfrom);
|
||||
|
||||
void RelayLastCPToNode(CNode* pnode);
|
||||
}
|
||||
|
||||
class CSoftCheckpoint
|
||||
{
|
||||
public:
|
||||
int nHeight;
|
||||
uint256 blockHash;
|
||||
std::vector<char> vchUsername;
|
||||
std::vector<char> vchSign;
|
||||
|
||||
CSoftCheckpoint()
|
||||
{
|
||||
SetNull();
|
||||
}
|
||||
|
||||
IMPLEMENT_SERIALIZE
|
||||
(
|
||||
READWRITE(nHeight);
|
||||
READWRITE(blockHash);
|
||||
READWRITE(vchUsername);
|
||||
READWRITE(vchSign);
|
||||
)
|
||||
|
||||
void SetNull()
|
||||
{
|
||||
nHeight = 0;
|
||||
blockHash = uint256();
|
||||
vchUsername.clear();
|
||||
vchSign.clear();
|
||||
}
|
||||
|
||||
bool IsNull() const
|
||||
{
|
||||
return !nHeight;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
#endif
|
@ -25,7 +25,7 @@ extern const std::string CLIENT_DATE;
|
||||
// network protocol versioning
|
||||
//
|
||||
|
||||
static const int PROTOCOL_VERSION = 70001;
|
||||
static const int PROTOCOL_VERSION = 70002;
|
||||
|
||||
// earlier versions not supported as of Feb 2012, and are disconnected
|
||||
static const int MIN_PROTO_VERSION = 209;
|
||||
@ -41,6 +41,9 @@ static const int NOBLKS_VERSION_END = 32400;
|
||||
// BIP 0031, pong message, is enabled for all versions AFTER this one
|
||||
static const int BIP0031_VERSION = 60000;
|
||||
|
||||
// soft checkpoint min version
|
||||
static const int SOFT_CHECKPOINT_VERSION = 70002;
|
||||
|
||||
// "mempool" command, enhanced "getdata" behavior starts with this version:
|
||||
static const int MEMPOOL_GD_VERSION = 60002;
|
||||
|
||||
|
@ -157,6 +157,7 @@ HEADERS += \
|
||||
src/bignum.h \
|
||||
src/chainparams.h \
|
||||
src/checkpoints.h \
|
||||
src/softcheckpoint.h \
|
||||
src/compat.h \
|
||||
src/sync.h \
|
||||
src/util.h \
|
||||
@ -256,6 +257,7 @@ SOURCES += \ #src/qt/bitcoin.cpp \
|
||||
src/net.cpp \
|
||||
src/bloom.cpp \
|
||||
src/checkpoints.cpp \
|
||||
src/softcheckpoint.cpp \
|
||||
src/addrman.cpp \
|
||||
src/db.cpp \
|
||||
src/walletdb.cpp \
|
||||
|
Loading…
x
Reference in New Issue
Block a user