mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2024-11-19 09:52:09 +01:00
Implement BIP 155 addrv2 messages (#2321)
* Implement BIP 155 addrv2 messages * Add unit tests
This commit is contained in:
parent
a334247457
commit
8cdddfecde
@ -0,0 +1,203 @@
|
||||
package org.bitcoins.core.serializers.p2p.messages
|
||||
|
||||
import org.bitcoins.core.number.{UInt16, UInt32, UInt64}
|
||||
import org.bitcoins.core.p2p._
|
||||
import org.bitcoins.core.protocol.CompactSizeUInt
|
||||
import org.bitcoins.testkit.core.gen.NumberGenerator
|
||||
import org.bitcoins.testkit.util.BitcoinSUnitTest
|
||||
import org.scalacheck.Gen
|
||||
import scodec.bits.HexStringSyntax
|
||||
|
||||
import java.net.InetAddress
|
||||
|
||||
class RawAddrV2MessageSerializerTest extends BitcoinSUnitTest {
|
||||
|
||||
def ipv4AddrV2MessageGen: Gen[IPv4AddrV2Message] = {
|
||||
for {
|
||||
time <- NumberGenerator.uInt32s
|
||||
services <- NumberGenerator.compactSizeUInts
|
||||
addrBytes <- NumberGenerator.bytevector(AddrV2Message.IPV4_ADDR_LENGTH)
|
||||
port <- NumberGenerator.uInt16
|
||||
} yield IPv4AddrV2Message(time,
|
||||
services,
|
||||
InetAddress.getByAddress(addrBytes.toArray),
|
||||
port)
|
||||
}
|
||||
|
||||
def ipv6AddrV2MessageGen: Gen[IPv6AddrV2Message] = {
|
||||
for {
|
||||
time <- NumberGenerator.uInt32s
|
||||
services <- NumberGenerator.compactSizeUInts
|
||||
addrBytes <- NumberGenerator.bytevector(AddrV2Message.IPV6_ADDR_LENGTH)
|
||||
port <- NumberGenerator.uInt16
|
||||
} yield IPv6AddrV2Message(time,
|
||||
services,
|
||||
InetAddress.getByAddress(addrBytes.toArray),
|
||||
port)
|
||||
}
|
||||
|
||||
def torV2AddrV2MessageGen: Gen[TorV2AddrV2Message] = {
|
||||
for {
|
||||
time <- NumberGenerator.uInt32s
|
||||
services <- NumberGenerator.compactSizeUInts
|
||||
addrBytes <- NumberGenerator.bytevector(AddrV2Message.TOR_V2_ADDR_LENGTH)
|
||||
port <- NumberGenerator.uInt16
|
||||
} yield TorV2AddrV2Message(time, services, addrBytes, port)
|
||||
}
|
||||
|
||||
def torV3AddrV2MessageGen: Gen[TorV3AddrV2Message] = {
|
||||
for {
|
||||
time <- NumberGenerator.uInt32s
|
||||
services <- NumberGenerator.compactSizeUInts
|
||||
addrBytes <- NumberGenerator.bytevector(AddrV2Message.TOR_V3_ADDR_LENGTH)
|
||||
port <- NumberGenerator.uInt16
|
||||
} yield TorV3AddrV2Message(time, services, addrBytes, port)
|
||||
}
|
||||
|
||||
def i2pAddrV2MessageGen: Gen[I2PAddrV2Message] = {
|
||||
for {
|
||||
time <- NumberGenerator.uInt32s
|
||||
services <- NumberGenerator.compactSizeUInts
|
||||
addrBytes <- NumberGenerator.bytevector(AddrV2Message.I2P_ADDR_LENGTH)
|
||||
port <- NumberGenerator.uInt16
|
||||
} yield I2PAddrV2Message(time, services, addrBytes, port)
|
||||
}
|
||||
|
||||
def cjdnsAddrV2MessageGen: Gen[CJDNSAddrV2Message] = {
|
||||
for {
|
||||
time <- NumberGenerator.uInt32s
|
||||
services <- NumberGenerator.compactSizeUInts
|
||||
addrBytes <- NumberGenerator.bytevector(AddrV2Message.CJDNS_ADDR_LENGTH)
|
||||
port <- NumberGenerator.uInt16
|
||||
} yield CJDNSAddrV2Message(time, services, addrBytes, port)
|
||||
}
|
||||
|
||||
def unknownAddrV2MessageGen: Gen[UnknownNetworkAddrV2Message] = {
|
||||
for {
|
||||
time <- NumberGenerator.uInt32s
|
||||
services <- NumberGenerator.compactSizeUInts
|
||||
networkId <- NumberGenerator.byte.suchThat(byte =>
|
||||
!AddrV2Message.knownNetworkBytes.contains(byte))
|
||||
addrBytes <- NumberGenerator.bytevector
|
||||
port <- NumberGenerator.uInt16
|
||||
} yield UnknownNetworkAddrV2Message(time,
|
||||
services,
|
||||
networkId,
|
||||
addrBytes,
|
||||
port)
|
||||
}
|
||||
|
||||
"IPv4AddrV2Message" must "have serialization symmetry" in {
|
||||
forAll(ipv4AddrV2MessageGen) { msg =>
|
||||
assert(msg.bytes == AddrV2Message(msg.bytes).bytes)
|
||||
}
|
||||
}
|
||||
|
||||
it must "parse an IPv4AddrV2Message" in {
|
||||
val msg = IPv4AddrV2Message(UInt32(4523),
|
||||
CompactSizeUInt(UInt64(53453453L)),
|
||||
InetAddress.getByAddress(hex"00000000".toArray),
|
||||
UInt16(8333))
|
||||
|
||||
assert("000011abfe8da22f030100000000208d" == msg.hex)
|
||||
}
|
||||
|
||||
"IPv6AddrV2Message" must "have serialization symmetry" in {
|
||||
forAll(ipv6AddrV2MessageGen) { msg =>
|
||||
assert(msg.bytes == AddrV2Message(msg.bytes).bytes)
|
||||
}
|
||||
}
|
||||
|
||||
it must "parse an IPv6AddrV2Message" in {
|
||||
val msg = IPv6AddrV2Message(
|
||||
UInt32(4523),
|
||||
CompactSizeUInt(UInt64(53453453L)),
|
||||
InetAddress.getByAddress(hex"00000000000000000000000000000000".toArray),
|
||||
UInt16(8333))
|
||||
|
||||
assert(
|
||||
"000011abfe8da22f030200000000000000000000000000000000208d" == msg.hex)
|
||||
}
|
||||
|
||||
"TorV2AddrV2Message" must "have serialization symmetry" in {
|
||||
forAll(torV2AddrV2MessageGen) { msg =>
|
||||
assert(msg.bytes == AddrV2Message(msg.bytes).bytes)
|
||||
}
|
||||
}
|
||||
|
||||
it must "parse a TorV2AddrV2Message" in {
|
||||
val msg = TorV2AddrV2Message(UInt32(4523),
|
||||
CompactSizeUInt(UInt64(53453453L)),
|
||||
hex"00000000000000000000",
|
||||
UInt16(8333))
|
||||
|
||||
assert("000011abfe8da22f030300000000000000000000208d" == msg.hex)
|
||||
}
|
||||
|
||||
"TorV3AddrV2Message" must "have serialization symmetry" in {
|
||||
forAll(torV3AddrV2MessageGen) { msg =>
|
||||
assert(msg.bytes == AddrV2Message(msg.bytes).bytes)
|
||||
}
|
||||
}
|
||||
|
||||
it must "parse a TorV3AddrV2Message" in {
|
||||
val msg = TorV3AddrV2Message(
|
||||
UInt32(4523),
|
||||
CompactSizeUInt(UInt64(53453453L)),
|
||||
hex"0000000000000000000000000000000000000000000000000000000000000000",
|
||||
UInt16(8333))
|
||||
|
||||
assert(
|
||||
"000011abfe8da22f03040000000000000000000000000000000000000000000000000000000000000000208d" == msg.hex)
|
||||
}
|
||||
|
||||
"I2PAddrV2Message" must "have serialization symmetry" in {
|
||||
forAll(i2pAddrV2MessageGen) { msg =>
|
||||
assert(msg.bytes == AddrV2Message(msg.bytes).bytes)
|
||||
}
|
||||
}
|
||||
|
||||
it must "parse an I2PAddrV2Message" in {
|
||||
val msg = I2PAddrV2Message(
|
||||
UInt32(4523),
|
||||
CompactSizeUInt(UInt64(53453453L)),
|
||||
hex"0000000000000000000000000000000000000000000000000000000000000000",
|
||||
UInt16(8333))
|
||||
|
||||
assert(
|
||||
"000011abfe8da22f03050000000000000000000000000000000000000000000000000000000000000000208d" == msg.hex)
|
||||
}
|
||||
|
||||
"CJDNSAddrV2Message" must "have serialization symmetry" in {
|
||||
forAll(cjdnsAddrV2MessageGen) { msg =>
|
||||
assert(msg.bytes == AddrV2Message(msg.bytes).bytes)
|
||||
}
|
||||
}
|
||||
|
||||
it must "parse a CJDNSAddrV2Message" in {
|
||||
val msg = CJDNSAddrV2Message(UInt32(4523),
|
||||
CompactSizeUInt(UInt64(53453453L)),
|
||||
hex"00000000000000000000000000000000",
|
||||
UInt16(8333))
|
||||
|
||||
assert(
|
||||
"000011abfe8da22f030600000000000000000000000000000000208d" == msg.hex)
|
||||
}
|
||||
|
||||
"UnknownNetworkAddrV2Message" must "have serialization symmetry" in {
|
||||
forAll(unknownAddrV2MessageGen) { msg =>
|
||||
assert(msg.bytes == AddrV2Message(msg.bytes).bytes)
|
||||
}
|
||||
}
|
||||
|
||||
it must "parse a UnknownNetworkAddrV2Message" in {
|
||||
val msg = UnknownNetworkAddrV2Message(UInt32(4523),
|
||||
CompactSizeUInt(UInt64(53453453L)),
|
||||
0x07,
|
||||
hex"00000000000000000000000000000000",
|
||||
UInt16(8333))
|
||||
|
||||
assert(
|
||||
"000011abfe8da22f030700000000000000000000000000000000208d" == msg.hex)
|
||||
}
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
package org.bitcoins.core.p2p
|
||||
|
||||
import java.net.{InetAddress, InetSocketAddress}
|
||||
|
||||
import org.bitcoins.core.bloom.{BloomFilter, BloomFlag}
|
||||
import org.bitcoins.core.config.NetworkParameters
|
||||
import org.bitcoins.core.gcs.{FilterHeader, FilterType, GolombFilter}
|
||||
import org.bitcoins.core.number.{Int32, Int64, UInt32, UInt64}
|
||||
import org.bitcoins.core.number.{Int32, Int64, UInt16, UInt32, UInt64}
|
||||
import org.bitcoins.core.protocol.CompactSizeUInt
|
||||
import org.bitcoins.core.protocol.blockchain.{Block, BlockHeader, MerkleBlock}
|
||||
import org.bitcoins.core.protocol.transaction.Transaction
|
||||
@ -528,6 +527,212 @@ object AddrMessage extends Factory[AddrMessage] {
|
||||
|
||||
}
|
||||
|
||||
/** addrV2 relays information about a peer. It supports many different
|
||||
* address types and networks.
|
||||
*
|
||||
* @see https://github.com/bitcoin/bips/blob/master/bip-0155.mediawiki
|
||||
*/
|
||||
sealed trait AddrV2Message extends ControlPayload {
|
||||
def time: UInt32
|
||||
def services: CompactSizeUInt
|
||||
def networkId: Byte
|
||||
def addrBytes: ByteVector
|
||||
def port: UInt16
|
||||
|
||||
override def commandName: String = NetworkPayload.addrV2CommandName
|
||||
|
||||
override def bytes: ByteVector =
|
||||
time.bytes ++ services.bytes ++ ByteVector.fromByte(
|
||||
networkId) ++ addrBytes ++ port.bytes
|
||||
}
|
||||
|
||||
/** addrv2 message that contains an IPv4 address */
|
||||
case class IPv4AddrV2Message(
|
||||
time: UInt32,
|
||||
services: CompactSizeUInt,
|
||||
addr: InetAddress,
|
||||
port: UInt16)
|
||||
extends AddrV2Message {
|
||||
override val networkId: Byte = AddrV2Message.IPV4_NETWORK_BYTE
|
||||
|
||||
override val addrBytes: ByteVector = ByteVector(addr.getAddress)
|
||||
|
||||
require(addrBytes.size == AddrV2Message.IPV4_ADDR_LENGTH,
|
||||
"Incorrect size of IPv4 message, consider using IPv6AddrV2Message")
|
||||
}
|
||||
|
||||
/** addrv2 message that contains an IPv6 address */
|
||||
case class IPv6AddrV2Message(
|
||||
time: UInt32,
|
||||
services: CompactSizeUInt,
|
||||
addr: InetAddress,
|
||||
port: UInt16)
|
||||
extends AddrV2Message {
|
||||
override val networkId: Byte = AddrV2Message.IPV6_NETWORK_BYTE
|
||||
|
||||
override val addrBytes: ByteVector = ByteVector(addr.getAddress)
|
||||
|
||||
require(addrBytes.size == AddrV2Message.IPV6_ADDR_LENGTH,
|
||||
"Incorrect size of IPv6 message, consider using IPv4AddrV2Message")
|
||||
}
|
||||
|
||||
/** addrv2 message that contains a TorV2 address */
|
||||
case class TorV2AddrV2Message(
|
||||
time: UInt32,
|
||||
services: CompactSizeUInt,
|
||||
addrBytes: ByteVector,
|
||||
port: UInt16)
|
||||
extends AddrV2Message {
|
||||
require(
|
||||
addrBytes.size == AddrV2Message.TOR_V2_ADDR_LENGTH,
|
||||
s"TorV2 addresses are ${AddrV2Message.TOR_V2_ADDR_LENGTH} bytes, got ${addrBytes.size}")
|
||||
override val networkId: Byte = AddrV2Message.TOR_V2_NETWORK_BYTE
|
||||
}
|
||||
|
||||
/** addrv2 message that contains a TorV3 address */
|
||||
case class TorV3AddrV2Message(
|
||||
time: UInt32,
|
||||
services: CompactSizeUInt,
|
||||
addrBytes: ByteVector,
|
||||
port: UInt16)
|
||||
extends AddrV2Message {
|
||||
require(
|
||||
addrBytes.size == AddrV2Message.TOR_V3_ADDR_LENGTH,
|
||||
s"TorV3 addresses are ${AddrV2Message.TOR_V3_ADDR_LENGTH} bytes, got ${addrBytes.size}")
|
||||
override val networkId: Byte = AddrV2Message.TOR_V3_NETWORK_BYTE
|
||||
}
|
||||
|
||||
/** addrv2 message that contains an I2P address */
|
||||
case class I2PAddrV2Message(
|
||||
time: UInt32,
|
||||
services: CompactSizeUInt,
|
||||
addrBytes: ByteVector,
|
||||
port: UInt16)
|
||||
extends AddrV2Message {
|
||||
require(
|
||||
addrBytes.size == AddrV2Message.I2P_ADDR_LENGTH,
|
||||
s"I2P addresses are ${AddrV2Message.I2P_ADDR_LENGTH} bytes, got ${addrBytes.size}")
|
||||
override val networkId: Byte = AddrV2Message.I2P_NETWORK_BYTE
|
||||
}
|
||||
|
||||
/** addrv2 message that contains an CJDNS address */
|
||||
case class CJDNSAddrV2Message(
|
||||
time: UInt32,
|
||||
services: CompactSizeUInt,
|
||||
addrBytes: ByteVector,
|
||||
port: UInt16)
|
||||
extends AddrV2Message {
|
||||
require(
|
||||
addrBytes.size == AddrV2Message.CJDNS_ADDR_LENGTH,
|
||||
s"CJDNS addresses are ${AddrV2Message.CJDNS_ADDR_LENGTH} bytes, got ${addrBytes.size}")
|
||||
override val networkId: Byte = AddrV2Message.CJDNS_NETWORK_BYTE
|
||||
}
|
||||
|
||||
/** addrv2 message that contains an address from a network we do not understand */
|
||||
case class UnknownNetworkAddrV2Message(
|
||||
time: UInt32,
|
||||
services: CompactSizeUInt,
|
||||
networkId: Byte,
|
||||
addrBytes: ByteVector,
|
||||
port: UInt16)
|
||||
extends AddrV2Message {
|
||||
require(!AddrV2Message.knownNetworkBytes.contains(networkId),
|
||||
s"$networkId is a known network byte")
|
||||
}
|
||||
|
||||
/**
|
||||
* The companion object for an AddrV2Message
|
||||
* @see https://developer.bitcoin.org/reference/p2p_networking.html#addrv2
|
||||
* @see https://github.com/bitcoin/bips/blob/master/bip-0155.mediawiki
|
||||
*/
|
||||
object AddrV2Message extends Factory[AddrV2Message] {
|
||||
|
||||
final val IPV4_NETWORK_BYTE: Byte = 0x01
|
||||
final val IPV4_ADDR_LENGTH: Int = 4
|
||||
|
||||
final val IPV6_NETWORK_BYTE: Byte = 0x02
|
||||
final val IPV6_ADDR_LENGTH: Int = 16
|
||||
|
||||
final val TOR_V2_NETWORK_BYTE: Byte = 0x03
|
||||
final val TOR_V2_ADDR_LENGTH: Int = 10
|
||||
|
||||
final val TOR_V3_NETWORK_BYTE: Byte = 0x04
|
||||
final val TOR_V3_ADDR_LENGTH: Int = 32
|
||||
|
||||
final val I2P_NETWORK_BYTE: Byte = 0x05
|
||||
final val I2P_ADDR_LENGTH: Int = 32
|
||||
|
||||
final val CJDNS_NETWORK_BYTE: Byte = 0x06
|
||||
final val CJDNS_ADDR_LENGTH: Int = 16
|
||||
|
||||
val knownNetworkBytes: Vector[Byte] = Vector(IPV4_NETWORK_BYTE,
|
||||
IPV6_NETWORK_BYTE,
|
||||
TOR_V2_NETWORK_BYTE,
|
||||
TOR_V3_NETWORK_BYTE,
|
||||
I2P_NETWORK_BYTE,
|
||||
CJDNS_NETWORK_BYTE)
|
||||
|
||||
override def fromBytes(bytes: ByteVector): AddrV2Message = {
|
||||
val timeBytes = bytes.take(4)
|
||||
val time = UInt32(timeBytes)
|
||||
|
||||
val services = CompactSizeUInt(bytes.drop(4))
|
||||
|
||||
val remainingBytes = bytes.drop(4 + services.bytes.size)
|
||||
|
||||
val networkId = remainingBytes.head
|
||||
|
||||
val addrPortBytes = remainingBytes.tail
|
||||
|
||||
networkId match {
|
||||
case IPV4_NETWORK_BYTE =>
|
||||
val addrBytes = addrPortBytes.take(IPV4_ADDR_LENGTH)
|
||||
val address = InetAddress.getByAddress(addrBytes.toArray)
|
||||
val port = UInt16(addrPortBytes.drop(IPV4_ADDR_LENGTH).take(2))
|
||||
IPv4AddrV2Message(time, services, address, port)
|
||||
case IPV6_NETWORK_BYTE =>
|
||||
val addrBytes = addrPortBytes.take(IPV6_ADDR_LENGTH)
|
||||
val address = InetAddress.getByAddress(addrBytes.toArray)
|
||||
val port = UInt16(addrPortBytes.drop(IPV6_ADDR_LENGTH).take(2))
|
||||
IPv6AddrV2Message(time, services, address, port)
|
||||
case TOR_V2_NETWORK_BYTE =>
|
||||
val addrBytes = addrPortBytes.take(TOR_V2_ADDR_LENGTH)
|
||||
val port = UInt16(addrPortBytes.drop(TOR_V2_ADDR_LENGTH).take(2))
|
||||
TorV2AddrV2Message(time, services, addrBytes, port)
|
||||
case TOR_V3_NETWORK_BYTE =>
|
||||
val addrBytes = addrPortBytes.take(TOR_V3_ADDR_LENGTH)
|
||||
val port = UInt16(addrPortBytes.drop(TOR_V3_ADDR_LENGTH).take(2))
|
||||
TorV3AddrV2Message(time, services, addrBytes, port)
|
||||
case I2P_NETWORK_BYTE =>
|
||||
val addrBytes = addrPortBytes.take(I2P_ADDR_LENGTH)
|
||||
val port = UInt16(addrPortBytes.drop(I2P_ADDR_LENGTH).take(2))
|
||||
I2PAddrV2Message(time, services, addrBytes, port)
|
||||
case CJDNS_NETWORK_BYTE =>
|
||||
val addrBytes = addrPortBytes.take(CJDNS_ADDR_LENGTH)
|
||||
val port = UInt16(addrPortBytes.drop(CJDNS_ADDR_LENGTH).take(2))
|
||||
CJDNSAddrV2Message(time, services, addrBytes, port)
|
||||
case unknown: Byte =>
|
||||
val addrBytes = addrPortBytes.dropRight(2)
|
||||
val port = UInt16(addrPortBytes.drop(addrBytes.size))
|
||||
UnknownNetworkAddrV2Message(time, services, unknown, addrBytes, port)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sending such a message indicates that a node can understand and
|
||||
* prefers to receive addrv2 messages instead of addr messages.
|
||||
* I.e. "Send me addrv2".sendaddrv2 SHOULD be sent after receiving the verack message from the peer.
|
||||
* For older peers, that did not emit sendaddrv2, keep sending the legacy
|
||||
* addr message, ignoring addresses with the newly introduced address types.
|
||||
* @see [[https://github.com/bitcoin/bips/blob/master/bip-0155.mediawiki#signaling-support-and-compatibility]]
|
||||
*/
|
||||
case object SendAddrV2Message extends ControlPayload {
|
||||
override val commandName: String = NetworkPayload.sendAddrV2CommandName
|
||||
override val bytes: ByteVector = ByteVector.empty
|
||||
}
|
||||
|
||||
/**
|
||||
* The feefilter message is a request to the receiving peer to not relay any transaction inv messages
|
||||
* to the sending peer where the fee rate for the transaction is below the fee rate specified in the
|
||||
@ -1329,6 +1534,8 @@ object NetworkPayload {
|
||||
private[core] val notFoundCommandName = "notfound"
|
||||
private[core] val transactionCommandName = "tx"
|
||||
private[core] val addrCommandName = "addr"
|
||||
private[core] val addrV2CommandName = "addrv2"
|
||||
private[core] val sendAddrV2CommandName = "sendaddrv2"
|
||||
private[core] val feeFilterCommandName = "feefilter"
|
||||
private[core] val filterAddCommandName = "filteradd"
|
||||
private[core] val filterClearCommandName = "filterclear"
|
||||
@ -1368,6 +1575,10 @@ object NetworkPayload {
|
||||
notFoundCommandName -> RawNotFoundMessageSerializer.read,
|
||||
transactionCommandName -> RawTransactionMessageSerializer.read,
|
||||
addrCommandName -> RawAddrMessageSerializer.read,
|
||||
addrV2CommandName -> AddrV2Message.fromBytes,
|
||||
sendAddrV2CommandName -> { _: ByteVector =>
|
||||
SendAddrV2Message
|
||||
},
|
||||
feeFilterCommandName -> RawFeeFilterMessageSerializer.read,
|
||||
filterAddCommandName -> RawFilterAddMessageSerializer.read,
|
||||
filterClearCommandName -> { _: ByteVector =>
|
||||
|
@ -219,6 +219,11 @@ class PeerMessageReceiver(
|
||||
Future.successful(this)
|
||||
case _: AddrMessage =>
|
||||
Future.successful(this)
|
||||
case _: AddrV2Message =>
|
||||
sender.sendSendAddrV2Message()
|
||||
Future.successful(this)
|
||||
case SendAddrV2Message =>
|
||||
Future.successful(this)
|
||||
case _ @(_: FilterAddMessage | _: FilterLoadMessage |
|
||||
FilterClearMessage) =>
|
||||
Future.successful(this)
|
||||
|
@ -90,6 +90,10 @@ case class PeerMessageSender(client: P2PClient)(implicit conf: NodeAppConfig)
|
||||
sendMsg(verackMsg)
|
||||
}
|
||||
|
||||
def sendSendAddrV2Message(): Future[Unit] = {
|
||||
sendMsg(SendAddrV2Message)
|
||||
}
|
||||
|
||||
/** Responds to a ping message */
|
||||
def sendPong(ping: PingMessage): Future[Unit] = {
|
||||
val pong = PongMessage(ping.nonce)
|
||||
|
Loading…
Reference in New Issue
Block a user