ZIP 32 Sapling structs
This commit is contained in:
@@ -108,7 +108,8 @@ LIBZCASH_H = \
|
||||
zcash/prf.h \
|
||||
zcash/Proof.hpp \
|
||||
zcash/util.h \
|
||||
zcash/Zcash.h
|
||||
zcash/Zcash.h \
|
||||
zcash/zip32.h
|
||||
|
||||
.PHONY: FORCE collate-libsnark check-symbols check-security
|
||||
# bitcoin core #
|
||||
@@ -520,6 +521,7 @@ libzcash_a_SOURCES = \
|
||||
zcash/Note.cpp \
|
||||
zcash/prf.cpp \
|
||||
zcash/util.cpp \
|
||||
zcash/zip32.cpp \
|
||||
zcash/circuit/commitment.tcc \
|
||||
zcash/circuit/gadget.tcc \
|
||||
zcash/circuit/merkle.tcc \
|
||||
|
||||
@@ -44,7 +44,8 @@ zcash_gtest_SOURCES += \
|
||||
gtest/test_proofs.cpp \
|
||||
gtest/test_paymentdisclosure.cpp \
|
||||
gtest/test_pedersen_hash.cpp \
|
||||
gtest/test_checkblock.cpp
|
||||
gtest/test_checkblock.cpp \
|
||||
gtest/test_zip32.cpp
|
||||
if ENABLE_WALLET
|
||||
zcash_gtest_SOURCES += \
|
||||
wallet/gtest/test_wallet.cpp
|
||||
|
||||
134
src/gtest/test_zip32.cpp
Normal file
134
src/gtest/test_zip32.cpp
Normal file
@@ -0,0 +1,134 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
#include <zcash/zip32.h>
|
||||
|
||||
// From https://github.com/zcash-hackworks/zcash-test-vectors/blob/master/sapling_zip32.py
|
||||
// Sapling consistently uses little-endian encoding, but uint256S takes its input in
|
||||
// big-endian byte order, so the test vectors below are byte-reversed.
|
||||
TEST(ZIP32, TestVectors) {
|
||||
std::vector<unsigned char, secure_allocator<unsigned char>> rawSeed {
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
|
||||
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31};
|
||||
HDSeed seed(rawSeed);
|
||||
|
||||
auto m = libzcash::SaplingExtendedSpendingKey::Master(seed);
|
||||
EXPECT_EQ(m.depth, 0);
|
||||
EXPECT_EQ(m.parentFVKTag, 0);
|
||||
EXPECT_EQ(m.childIndex, 0);
|
||||
EXPECT_EQ(
|
||||
m.chaincode,
|
||||
uint256S("8e661820750d557e8b34733ebf7ecdfdf31c6d27724fb47aa372bf034b7c94d0"));
|
||||
EXPECT_EQ(
|
||||
m.expsk.ask,
|
||||
uint256S("06257454c907f6510ba1c1830ebf60657760a8869ee968a2b93260d3930cc0b6"));
|
||||
EXPECT_EQ(
|
||||
m.expsk.nsk,
|
||||
uint256S("06ea21888a749fd38eb443d20a030abd2e6e997f5db4f984bd1f2f3be8ed0482"));
|
||||
EXPECT_EQ(
|
||||
m.expsk.ovk,
|
||||
uint256S("21fb4adfa42183848306ffb27719f27d76cf9bb81d023c93d4b9230389845839"));
|
||||
EXPECT_EQ(
|
||||
m.dk,
|
||||
uint256S("72a196f93e8abc0935280ea2a96fa57d6024c9913e0f9fb3af96775bb77cc177"));
|
||||
EXPECT_THAT(
|
||||
m.ToXFVK().DefaultAddress().d,
|
||||
testing::ElementsAreArray({ 0xd8, 0x62, 0x1b, 0x98, 0x1c, 0xf3, 0x00, 0xe9, 0xd4, 0xcc, 0x89 }));
|
||||
|
||||
auto m_1 = m.Derive(1);
|
||||
EXPECT_EQ(m_1.depth, 1);
|
||||
EXPECT_EQ(m_1.parentFVKTag, 0x3a71c214);
|
||||
EXPECT_EQ(m_1.childIndex, 1);
|
||||
EXPECT_EQ(
|
||||
m_1.chaincode,
|
||||
uint256S("e6bcda05678a43fad229334ef0b795a590e7c50590baf0d9b9031a690c114701"));
|
||||
EXPECT_EQ(
|
||||
m_1.expsk.ask,
|
||||
uint256S("0c357a2655b4b8d761794095df5cb402d3ba4a428cf6a88e7c2816a597c12b28"));
|
||||
EXPECT_EQ(
|
||||
m_1.expsk.nsk,
|
||||
uint256S("01ba6bff1018fd4eac04da7e3f2c6be9c229e662c5c4d1d6fc1ecafd8829a3e7"));
|
||||
EXPECT_EQ(
|
||||
m_1.expsk.ovk,
|
||||
uint256S("7474a4c518551bd82f14a7f7365a8ffa403c50cfeffedf026ada8688fc81135f"));
|
||||
EXPECT_EQ(
|
||||
m_1.dk,
|
||||
uint256S("dcb4c170d878510e96c4a74192d7eecde9c9912b00b99a12ec91d7a232e84de0"));
|
||||
EXPECT_THAT(
|
||||
m_1.ToXFVK().DefaultAddress().d,
|
||||
testing::ElementsAreArray({ 0x8b, 0x41, 0x38, 0x32, 0x0d, 0xfa, 0xfd, 0x7b, 0x39, 0x97, 0x81 }));
|
||||
|
||||
auto m_1_2h = m_1.Derive(2 | ZIP32_HARDENED_KEY_LIMIT);
|
||||
EXPECT_EQ(m_1_2h.depth, 2);
|
||||
EXPECT_EQ(m_1_2h.parentFVKTag, 0x079e99db);
|
||||
EXPECT_EQ(m_1_2h.childIndex, 2 | ZIP32_HARDENED_KEY_LIMIT);
|
||||
EXPECT_EQ(
|
||||
m_1_2h.chaincode,
|
||||
uint256S("35d4a883737742ca41a4baa92323bdb3c93dcb3b462a26b039971bedf415ce97"));
|
||||
EXPECT_EQ(
|
||||
m_1_2h.expsk.ask,
|
||||
uint256S("0dc6e4fe846bda925c82e632980434e17b51dac81fc4821fa71334ee3c11e88b"));
|
||||
EXPECT_EQ(
|
||||
m_1_2h.expsk.nsk,
|
||||
uint256S("0c99a63a275c1c66734761cfb9c62fe9bd1b953f579123d3d0e769c59d057837"));
|
||||
EXPECT_EQ(
|
||||
m_1_2h.expsk.ovk,
|
||||
uint256S("bc1328fc5eb693e18875c5149d06953b11d39447ebd6e38c023c22962e1881cf"));
|
||||
EXPECT_EQ(
|
||||
m_1_2h.dk,
|
||||
uint256S("377bb062dce7e0dcd8a0054d0ca4b4d1481b3710bfa1df12ca46ff9e9fa1eda3"));
|
||||
EXPECT_THAT(
|
||||
m_1_2h.ToXFVK().DefaultAddress().d,
|
||||
testing::ElementsAreArray({ 0xe8, 0xd0, 0x37, 0x93, 0xcd, 0xd2, 0xba, 0xcc, 0x9c, 0x70, 0x41 }));
|
||||
|
||||
auto m_1_2hv = m_1_2h.ToXFVK();
|
||||
EXPECT_EQ(m_1_2hv.depth, 2);
|
||||
EXPECT_EQ(m_1_2hv.parentFVKTag, 0x079e99db);
|
||||
EXPECT_EQ(m_1_2hv.childIndex, 2 | ZIP32_HARDENED_KEY_LIMIT);
|
||||
EXPECT_EQ(
|
||||
m_1_2hv.chaincode,
|
||||
uint256S("35d4a883737742ca41a4baa92323bdb3c93dcb3b462a26b039971bedf415ce97"));
|
||||
EXPECT_EQ(
|
||||
m_1_2hv.fvk.ak,
|
||||
uint256S("4138cffdf7200e52d4e9f4384481b4a4c4d070493a5e401e4ffa850f5a92c5a6"));
|
||||
EXPECT_EQ(
|
||||
m_1_2hv.fvk.nk,
|
||||
uint256S("11eee22577304f660cc036bc84b3fc88d1ec50ae8a4d657beb6b211659304e30"));
|
||||
EXPECT_EQ(
|
||||
m_1_2hv.fvk.ovk,
|
||||
uint256S("bc1328fc5eb693e18875c5149d06953b11d39447ebd6e38c023c22962e1881cf"));
|
||||
EXPECT_EQ(
|
||||
m_1_2hv.dk,
|
||||
uint256S("377bb062dce7e0dcd8a0054d0ca4b4d1481b3710bfa1df12ca46ff9e9fa1eda3"));
|
||||
EXPECT_EQ(m_1_2hv.DefaultAddress(), m_1_2h.ToXFVK().DefaultAddress());
|
||||
|
||||
// Hardened derivation from an xfvk fails
|
||||
EXPECT_FALSE(m_1_2hv.Derive(3 | ZIP32_HARDENED_KEY_LIMIT));
|
||||
|
||||
// Non-hardened derivation succeeds
|
||||
auto maybe_m_1_2hv_3 = m_1_2hv.Derive(3);
|
||||
EXPECT_TRUE(maybe_m_1_2hv_3);
|
||||
|
||||
auto m_1_2hv_3 = maybe_m_1_2hv_3.get();
|
||||
EXPECT_EQ(m_1_2hv_3.depth, 3);
|
||||
EXPECT_EQ(m_1_2hv_3.parentFVKTag, 0x7583c148);
|
||||
EXPECT_EQ(m_1_2hv_3.childIndex, 3);
|
||||
EXPECT_EQ(
|
||||
m_1_2hv_3.chaincode,
|
||||
uint256S("e8e7d6a74a5a1c05be41baec7998d91f7b3603a4c0af495b0d43ba81cf7b938d"));
|
||||
EXPECT_EQ(
|
||||
m_1_2hv_3.fvk.ak,
|
||||
uint256S("a3a697bdda9d648d32a97553de4754b2fac866d726d3f2c436259c507bc585b1"));
|
||||
EXPECT_EQ(
|
||||
m_1_2hv_3.fvk.nk,
|
||||
uint256S("4f66c0814b769963f3bf1bc001270b50edabb27de042fc8a5607d2029e0488db"));
|
||||
EXPECT_EQ(
|
||||
m_1_2hv_3.fvk.ovk,
|
||||
uint256S("f61a699934dc78441324ef628b4b4721611571e8ee3bd591eb3d4b1cfae0b969"));
|
||||
EXPECT_EQ(
|
||||
m_1_2hv_3.dk,
|
||||
uint256S("6ee53b1261f2c9c0f7359ab236f87b52a0f1b0ce43305cdad92ebb63c350cbbe"));
|
||||
EXPECT_THAT(
|
||||
m_1_2hv_3.DefaultAddress().d,
|
||||
testing::ElementsAreArray({ 0x03, 0x0f, 0xfb, 0x26, 0x3a, 0x93, 0x9e, 0x23, 0x0e, 0x96, 0xdd }));
|
||||
}
|
||||
@@ -88,6 +88,15 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
/** 88-bit opaque blob.
|
||||
*/
|
||||
class blob88 : public base_blob<88> {
|
||||
public:
|
||||
blob88() {}
|
||||
blob88(const base_blob<88>& b) : base_blob<88>(b) {}
|
||||
explicit blob88(const std::vector<unsigned char>& vch) : base_blob<88>(vch) {}
|
||||
};
|
||||
|
||||
/** 160-bit opaque blob.
|
||||
* @note This type is called uint160 for historical reasons only. It is an opaque
|
||||
* blob of 160 bits and has no integer operations.
|
||||
|
||||
@@ -19,6 +19,9 @@ const size_t SerializedSproutPaymentAddressSize = 64;
|
||||
const size_t SerializedSproutViewingKeySize = 64;
|
||||
const size_t SerializedSproutSpendingKeySize = 32;
|
||||
|
||||
const size_t SerializedSaplingPaymentAddressSize = 43;
|
||||
const size_t SerializedSaplingFullViewingKeySize = 96;
|
||||
const size_t SerializedSaplingExpandedSpendingKeySize = 96;
|
||||
const size_t SerializedSaplingSpendingKeySize = 32;
|
||||
|
||||
typedef std::array<unsigned char, ZC_DIVERSIFIER_SIZE> diversifier_t;
|
||||
|
||||
139
src/zcash/zip32.cpp
Normal file
139
src/zcash/zip32.cpp
Normal file
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) 2018 The Zcash developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include "zip32.h"
|
||||
|
||||
#include "hash.h"
|
||||
#include "random.h"
|
||||
#include "streams.h"
|
||||
#include "version.h"
|
||||
|
||||
#include <librustzcash.h>
|
||||
#include <sodium.h>
|
||||
|
||||
const unsigned char ZCASH_HD_SEED_FP_PERSONAL[crypto_generichash_blake2b_PERSONALBYTES] =
|
||||
{'Z', 'c', 'a', 's', 'h', '_', 'H', 'D', '_', 'S', 'e', 'e', 'd', '_', 'F', 'P'};
|
||||
|
||||
HDSeed HDSeed::Random(size_t len)
|
||||
{
|
||||
assert(len >= 32);
|
||||
RawHDSeed rawSeed(len, 0);
|
||||
GetRandBytes(rawSeed.data(), len);
|
||||
return HDSeed(rawSeed);
|
||||
}
|
||||
|
||||
uint256 HDSeed::Fingerprint() const
|
||||
{
|
||||
CBLAKE2bWriter h(SER_GETHASH, 0, ZCASH_HD_SEED_FP_PERSONAL);
|
||||
h << seed;
|
||||
return h.GetHash();
|
||||
}
|
||||
|
||||
namespace libzcash {
|
||||
|
||||
boost::optional<SaplingExtendedFullViewingKey> SaplingExtendedFullViewingKey::Derive(uint32_t i) const
|
||||
{
|
||||
CDataStream ss_p(SER_NETWORK, PROTOCOL_VERSION);
|
||||
ss_p << *this;
|
||||
CSerializeData p_bytes(ss_p.begin(), ss_p.end());
|
||||
|
||||
CSerializeData i_bytes(ZIP32_XFVK_SIZE);
|
||||
if (librustzcash_zip32_xfvk_derive(
|
||||
reinterpret_cast<unsigned char*>(p_bytes.data()),
|
||||
i,
|
||||
reinterpret_cast<unsigned char*>(i_bytes.data())
|
||||
)) {
|
||||
CDataStream ss_i(i_bytes, SER_NETWORK, PROTOCOL_VERSION);
|
||||
SaplingExtendedFullViewingKey xfvk_i;
|
||||
ss_i >> xfvk_i;
|
||||
return xfvk_i;
|
||||
} else {
|
||||
return boost::none;
|
||||
}
|
||||
}
|
||||
|
||||
boost::optional<std::pair<diversifier_index_t, libzcash::SaplingPaymentAddress>>
|
||||
SaplingExtendedFullViewingKey::Address(diversifier_index_t j) const
|
||||
{
|
||||
CDataStream ss_xfvk(SER_NETWORK, PROTOCOL_VERSION);
|
||||
ss_xfvk << *this;
|
||||
CSerializeData xfvk_bytes(ss_xfvk.begin(), ss_xfvk.end());
|
||||
|
||||
diversifier_index_t j_ret;
|
||||
CSerializeData addr_bytes(libzcash::SerializedSaplingPaymentAddressSize);
|
||||
if (librustzcash_zip32_xfvk_address(
|
||||
reinterpret_cast<unsigned char*>(xfvk_bytes.data()),
|
||||
j.begin(), j_ret.begin(),
|
||||
reinterpret_cast<unsigned char*>(addr_bytes.data()))) {
|
||||
CDataStream ss_addr(addr_bytes, SER_NETWORK, PROTOCOL_VERSION);
|
||||
libzcash::SaplingPaymentAddress addr;
|
||||
ss_addr >> addr;
|
||||
return std::make_pair(j_ret, addr);
|
||||
} else {
|
||||
return boost::none;
|
||||
}
|
||||
}
|
||||
|
||||
libzcash::SaplingPaymentAddress SaplingExtendedFullViewingKey::DefaultAddress() const
|
||||
{
|
||||
diversifier_index_t j0;
|
||||
auto addr = Address(j0);
|
||||
// If we can't obtain a default address, we are *very* unlucky...
|
||||
if (!addr) {
|
||||
throw std::runtime_error("SaplingExtendedFullViewingKey::DefaultAddress(): No valid diversifiers out of 2^88!");
|
||||
}
|
||||
return addr.get().second;
|
||||
}
|
||||
|
||||
SaplingExtendedSpendingKey SaplingExtendedSpendingKey::Master(const HDSeed& seed)
|
||||
{
|
||||
auto rawSeed = seed.RawSeed();
|
||||
CSerializeData m_bytes(ZIP32_XSK_SIZE);
|
||||
librustzcash_zip32_xsk_master(
|
||||
rawSeed.data(),
|
||||
rawSeed.size(),
|
||||
reinterpret_cast<unsigned char*>(m_bytes.data()));
|
||||
|
||||
CDataStream ss(m_bytes, SER_NETWORK, PROTOCOL_VERSION);
|
||||
SaplingExtendedSpendingKey xsk_m;
|
||||
ss >> xsk_m;
|
||||
return xsk_m;
|
||||
}
|
||||
|
||||
SaplingExtendedSpendingKey SaplingExtendedSpendingKey::Derive(uint32_t i) const
|
||||
{
|
||||
CDataStream ss_p(SER_NETWORK, PROTOCOL_VERSION);
|
||||
ss_p << *this;
|
||||
CSerializeData p_bytes(ss_p.begin(), ss_p.end());
|
||||
|
||||
CSerializeData i_bytes(ZIP32_XSK_SIZE);
|
||||
librustzcash_zip32_xsk_derive(
|
||||
reinterpret_cast<unsigned char*>(p_bytes.data()),
|
||||
i,
|
||||
reinterpret_cast<unsigned char*>(i_bytes.data()));
|
||||
|
||||
CDataStream ss_i(i_bytes, SER_NETWORK, PROTOCOL_VERSION);
|
||||
SaplingExtendedSpendingKey xsk_i;
|
||||
ss_i >> xsk_i;
|
||||
return xsk_i;
|
||||
}
|
||||
|
||||
SaplingExtendedFullViewingKey SaplingExtendedSpendingKey::ToXFVK() const
|
||||
{
|
||||
SaplingExtendedFullViewingKey ret;
|
||||
ret.depth = depth;
|
||||
ret.parentFVKTag = parentFVKTag;
|
||||
ret.childIndex = childIndex;
|
||||
ret.chaincode = chaincode;
|
||||
ret.fvk = expsk.full_viewing_key();
|
||||
ret.dk = dk;
|
||||
return ret;
|
||||
}
|
||||
|
||||
libzcash::SaplingPaymentAddress SaplingExtendedSpendingKey::DefaultAddress() const
|
||||
{
|
||||
return ToXFVK().DefaultAddress();
|
||||
}
|
||||
|
||||
}
|
||||
121
src/zcash/zip32.h
Normal file
121
src/zcash/zip32.h
Normal file
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) 2018 The Zcash developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef ZCASH_ZIP32_H
|
||||
#define ZCASH_ZIP32_H
|
||||
|
||||
#include "serialize.h"
|
||||
#include "support/allocators/secure.h"
|
||||
#include "uint256.h"
|
||||
#include "zcash/Address.hpp"
|
||||
|
||||
#include <boost/optional.hpp>
|
||||
|
||||
const uint32_t ZIP32_HARDENED_KEY_LIMIT = 0x80000000;
|
||||
const size_t ZIP32_XFVK_SIZE = 169;
|
||||
const size_t ZIP32_XSK_SIZE = 169;
|
||||
|
||||
typedef std::vector<unsigned char, secure_allocator<unsigned char>> RawHDSeed;
|
||||
|
||||
class HDSeed {
|
||||
private:
|
||||
RawHDSeed seed;
|
||||
|
||||
public:
|
||||
HDSeed() {}
|
||||
HDSeed(RawHDSeed& seedIn) : seed(seedIn) {}
|
||||
|
||||
static HDSeed Random(size_t len = 32);
|
||||
bool IsNull() const { return seed.empty(); };
|
||||
uint256 Fingerprint() const;
|
||||
RawHDSeed RawSeed() const { return seed; }
|
||||
|
||||
friend bool operator==(const HDSeed& a, const HDSeed& b)
|
||||
{
|
||||
return a.seed == b.seed;
|
||||
}
|
||||
|
||||
friend bool operator!=(const HDSeed& a, const HDSeed& b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
};
|
||||
|
||||
namespace libzcash {
|
||||
|
||||
typedef blob88 diversifier_index_t;
|
||||
|
||||
struct SaplingExtendedFullViewingKey {
|
||||
uint8_t depth;
|
||||
uint32_t parentFVKTag;
|
||||
uint32_t childIndex;
|
||||
uint256 chaincode;
|
||||
libzcash::SaplingFullViewingKey fvk;
|
||||
uint256 dk;
|
||||
|
||||
ADD_SERIALIZE_METHODS;
|
||||
|
||||
template <typename Stream, typename Operation>
|
||||
inline void SerializationOp(Stream& s, Operation ser_action) {
|
||||
READWRITE(depth);
|
||||
READWRITE(parentFVKTag);
|
||||
READWRITE(childIndex);
|
||||
READWRITE(chaincode);
|
||||
READWRITE(fvk);
|
||||
READWRITE(dk);
|
||||
}
|
||||
|
||||
boost::optional<SaplingExtendedFullViewingKey> Derive(uint32_t i) const;
|
||||
|
||||
// Returns the first index starting from j that generates a valid
|
||||
// payment address, along with the corresponding address. Returns
|
||||
// an error if the diversifier space is exhausted.
|
||||
boost::optional<std::pair<diversifier_index_t, libzcash::SaplingPaymentAddress>>
|
||||
Address(diversifier_index_t j) const;
|
||||
|
||||
libzcash::SaplingPaymentAddress DefaultAddress() const;
|
||||
};
|
||||
|
||||
struct SaplingExtendedSpendingKey {
|
||||
uint8_t depth;
|
||||
uint32_t parentFVKTag;
|
||||
uint32_t childIndex;
|
||||
uint256 chaincode;
|
||||
libzcash::SaplingExpandedSpendingKey expsk;
|
||||
uint256 dk;
|
||||
|
||||
ADD_SERIALIZE_METHODS;
|
||||
|
||||
template <typename Stream, typename Operation>
|
||||
inline void SerializationOp(Stream& s, Operation ser_action) {
|
||||
READWRITE(depth);
|
||||
READWRITE(parentFVKTag);
|
||||
READWRITE(childIndex);
|
||||
READWRITE(chaincode);
|
||||
READWRITE(expsk);
|
||||
READWRITE(dk);
|
||||
}
|
||||
|
||||
static SaplingExtendedSpendingKey Master(const HDSeed& seed);
|
||||
|
||||
SaplingExtendedSpendingKey Derive(uint32_t i) const;
|
||||
|
||||
SaplingExtendedFullViewingKey ToXFVK() const;
|
||||
|
||||
libzcash::SaplingPaymentAddress DefaultAddress() const;
|
||||
|
||||
friend bool operator==(const SaplingExtendedSpendingKey& a, const SaplingExtendedSpendingKey& b)
|
||||
{
|
||||
return a.depth == b.depth &&
|
||||
a.parentFVKTag == b.parentFVKTag &&
|
||||
a.childIndex == b.childIndex &&
|
||||
a.chaincode == b.chaincode &&
|
||||
a.expsk == b.expsk &&
|
||||
a.dk == b.dk;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // ZCASH_ZIP32_H
|
||||
Reference in New Issue
Block a user