mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-01-18 21:34:39 +01:00
Brought down ecdsa adaptor signatures implemented in scala from the dlc-crypto branch (#2034)
This commit is contained in:
parent
09dfd5eb73
commit
e71b664e1a
@ -3,7 +3,12 @@ package org.bitcoins.core.crypto
|
||||
import org.bitcoins.core.protocol.transaction.Transaction
|
||||
import org.bitcoins.core.script.crypto.HashType
|
||||
import org.bitcoins.core.wallet.utxo.{InputInfo, InputSigningInfo}
|
||||
import org.bitcoins.crypto.{DERSignatureUtil, ECDigitalSignature, ECPrivateKey}
|
||||
import org.bitcoins.crypto.{
|
||||
DERSignatureUtil,
|
||||
ECAdaptorSignature,
|
||||
ECDigitalSignature,
|
||||
ECPrivateKey
|
||||
}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
@ -145,6 +150,15 @@ sealed abstract class TransactionSignatureCreator {
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
def createSig(
|
||||
component: TxSigComponent,
|
||||
adaptorSign: ByteVector => ECAdaptorSignature,
|
||||
hashType: HashType): ECAdaptorSignature = {
|
||||
val hash =
|
||||
TransactionSignatureSerializer.hashForSignature(component, hashType)
|
||||
adaptorSign(hash.bytes)
|
||||
}
|
||||
}
|
||||
|
||||
object TransactionSignatureCreator extends TransactionSignatureCreator
|
||||
|
@ -2,7 +2,11 @@ package org.bitcoins.crypto
|
||||
|
||||
import org.bitcoins.core.config.{MainNet, RegTest, SigNet, TestNet3}
|
||||
import org.bitcoins.core.crypto.ECPrivateKeyUtil
|
||||
import org.bitcoins.testkit.core.gen.{ChainParamsGenerator, CryptoGenerators}
|
||||
import org.bitcoins.testkit.core.gen.{
|
||||
ChainParamsGenerator,
|
||||
CryptoGenerators,
|
||||
NumberGenerator
|
||||
}
|
||||
import org.bitcoins.testkit.util.BitcoinSUnitTest
|
||||
|
||||
class ECPrivateKeyTest extends BitcoinSUnitTest {
|
||||
@ -117,4 +121,20 @@ class ECPrivateKeyTest extends BitcoinSUnitTest {
|
||||
}
|
||||
}
|
||||
|
||||
it must "correctly execute the ecdsa single signer adaptor signature protocol" in {
|
||||
forAll(CryptoGenerators.privateKey,
|
||||
CryptoGenerators.privateKey,
|
||||
NumberGenerator.bytevector(32)) {
|
||||
case (privKey, adaptorSecret, msg) =>
|
||||
val adaptorSig = privKey.adaptorSign(adaptorSecret.publicKey, msg)
|
||||
assert(
|
||||
privKey.publicKey
|
||||
.adaptorVerify(msg, adaptorSecret.publicKey, adaptorSig))
|
||||
val sig = adaptorSecret.completeAdaptorSignature(adaptorSig)
|
||||
val secret =
|
||||
adaptorSecret.publicKey.extractAdaptorSecret(adaptorSig, sig)
|
||||
assert(secret == adaptorSecret)
|
||||
assert(privKey.publicKey.verify(msg, sig))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,11 @@ object BouncyCastleUtil {
|
||||
.getOrElse(false)
|
||||
}
|
||||
|
||||
def pubKeyTweakMul(pubKey: ECPublicKey, tweak: FieldElement): ECPublicKey = {
|
||||
val tweakedPoint = pubKey.toPoint.multiply(tweak.toBigInteger)
|
||||
ECPublicKey.fromPoint(tweakedPoint, pubKey.isCompressed)
|
||||
}
|
||||
|
||||
def decompressPublicKey(publicKey: ECPublicKey): ECPublicKey = {
|
||||
if (publicKey.isCompressed) {
|
||||
val point = decodePoint(publicKey.bytes)
|
||||
@ -215,3 +220,224 @@ object BouncyCastleUtil {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object AdaptorStuff {
|
||||
import ECAdaptorSignature.{deserializePoint, serializePoint}
|
||||
|
||||
// Compute s' = k^-1 * (dataToSign + rx*privateKey)
|
||||
private def adaptorSignHelper(
|
||||
dataToSign: ByteVector,
|
||||
k: FieldElement,
|
||||
r: ECPublicKey,
|
||||
privateKey: ECPrivateKey): FieldElement = {
|
||||
val rx = FieldElement(r.toPoint.getXCoord.toBigInteger)
|
||||
val x = privateKey.fieldElement
|
||||
val m = FieldElement(dataToSign)
|
||||
val kInv = k.inverse
|
||||
|
||||
rx.multiply(x).add(m).multiply(kInv)
|
||||
}
|
||||
|
||||
def adaptorSign(
|
||||
privateKey: ECPrivateKey,
|
||||
adaptorPoint: ECPublicKey,
|
||||
dataToSign: ByteVector): ECAdaptorSignature = {
|
||||
// Include dataToSign and adaptor in nonce derivation
|
||||
val hash = CryptoUtil.sha256(dataToSign ++ serializePoint(adaptorPoint))
|
||||
val k = DLEQStuff.dleqNonceFunc(hash.bytes,
|
||||
privateKey.fieldElement,
|
||||
"ECDSAAdaptorNon")
|
||||
|
||||
if (k.isZero) {
|
||||
throw new RuntimeException("Nonce cannot be zero.")
|
||||
}
|
||||
|
||||
val untweakedNonce = k.getPublicKey // k*G
|
||||
val tweakedNonce = adaptorPoint.tweakMultiply(k) // k*Y
|
||||
|
||||
// DLEQ_prove((G,R'),(Y, R))
|
||||
val (proofS, proofE) =
|
||||
DLEQStuff.dleqProve(k, adaptorPoint, "ECDSAAdaptorSig")
|
||||
|
||||
// s' = k^-1*(m + rx*x)
|
||||
val adaptedSig = adaptorSignHelper(dataToSign, k, tweakedNonce, privateKey)
|
||||
|
||||
ECAdaptorSignature(tweakedNonce, adaptedSig, untweakedNonce, proofS, proofE)
|
||||
}
|
||||
|
||||
// Compute R'x = s^-1 * (msg*G + rx*pubKey) = s^-1 * (msg + rx*privKey) * G
|
||||
private def adaptorVerifyHelper(
|
||||
rx: FieldElement,
|
||||
s: FieldElement,
|
||||
pubKey: ECPublicKey,
|
||||
msg: ByteVector): FieldElement = {
|
||||
val m = FieldElement(msg)
|
||||
val untweakedPoint =
|
||||
m.getPublicKey.add(pubKey.tweakMultiply(rx)).tweakMultiply(s.inverse)
|
||||
|
||||
FieldElement(untweakedPoint.bytes.tail)
|
||||
}
|
||||
|
||||
def adaptorVerify(
|
||||
adaptorSig: ECAdaptorSignature,
|
||||
pubKey: ECPublicKey,
|
||||
data: ByteVector,
|
||||
adaptor: ECPublicKey): Boolean = {
|
||||
val untweakedNonce = deserializePoint(adaptorSig.dleqProof.take(33))
|
||||
val proofS = FieldElement(adaptorSig.dleqProof.drop(33).take(32))
|
||||
val proofR = FieldElement(adaptorSig.dleqProof.drop(65))
|
||||
|
||||
val tweakedNonce = deserializePoint(adaptorSig.adaptedSig.take(33))
|
||||
val adaptedSig = FieldElement(adaptorSig.adaptedSig.drop(33))
|
||||
|
||||
val validProof = DLEQStuff.dleqVerify(
|
||||
"ECDSAAdaptorSig",
|
||||
proofS,
|
||||
proofR,
|
||||
untweakedNonce,
|
||||
adaptor,
|
||||
tweakedNonce
|
||||
)
|
||||
|
||||
if (validProof) {
|
||||
val tweakedNoncex = FieldElement(tweakedNonce.bytes.tail)
|
||||
val untweakedNoncex = FieldElement(untweakedNonce.bytes.tail)
|
||||
|
||||
if (tweakedNoncex.isZero || untweakedNoncex.isZero) {
|
||||
false
|
||||
} else {
|
||||
|
||||
val untweakedRx =
|
||||
adaptorVerifyHelper(tweakedNoncex, adaptedSig, pubKey, data)
|
||||
|
||||
untweakedRx == untweakedNoncex
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
def adaptorComplete(
|
||||
adaptorSecret: ECPrivateKey,
|
||||
adaptedSig: ByteVector): ECDigitalSignature = {
|
||||
val tweakedNonce: ECPublicKey =
|
||||
ECAdaptorSignature.deserializePoint(adaptedSig.take(33))
|
||||
val rx = FieldElement(tweakedNonce.bytes.tail)
|
||||
val adaptedS: FieldElement = FieldElement(adaptedSig.drop(33))
|
||||
val correctedS = adaptedS.multInv(adaptorSecret.fieldElement)
|
||||
|
||||
val sig = ECDigitalSignature.fromRS(BigInt(rx.toBigInteger),
|
||||
BigInt(correctedS.toBigInteger))
|
||||
DERSignatureUtil.lowS(sig)
|
||||
}
|
||||
|
||||
def extractAdaptorSecret(
|
||||
sig: ECDigitalSignature,
|
||||
adaptorSig: ECAdaptorSignature,
|
||||
adaptor: ECPublicKey): ECPrivateKey = {
|
||||
val secretOrNeg = adaptorSig.adaptedS.multInv(FieldElement(sig.s))
|
||||
if (secretOrNeg.getPublicKey == adaptor) {
|
||||
secretOrNeg.toPrivateKey
|
||||
} else {
|
||||
secretOrNeg.negate.toPrivateKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object DLEQStuff {
|
||||
import ECAdaptorSignature.serializePoint
|
||||
|
||||
def dleqPair(
|
||||
fe: FieldElement,
|
||||
adaptorPoint: ECPublicKey): (ECPublicKey, ECPublicKey) = {
|
||||
val point = fe.getPublicKey
|
||||
val tweakedPoint = adaptorPoint.tweakMultiply(fe)
|
||||
|
||||
(point, tweakedPoint)
|
||||
}
|
||||
|
||||
def dleqNonceFunc(
|
||||
hash: ByteVector,
|
||||
fe: FieldElement,
|
||||
algoName: String): FieldElement = {
|
||||
val kBytes =
|
||||
CryptoUtil.taggedSha256(fe.bytes ++ hash, algoName).bytes
|
||||
FieldElement(kBytes)
|
||||
}
|
||||
|
||||
def dleqChallengeHash(
|
||||
algoName: String,
|
||||
adaptorPoint: ECPublicKey,
|
||||
r1: ECPublicKey,
|
||||
r2: ECPublicKey,
|
||||
p1: ECPublicKey,
|
||||
p2: ECPublicKey): ByteVector = {
|
||||
CryptoUtil
|
||||
.taggedSha256(
|
||||
serializePoint(adaptorPoint) ++ serializePoint(r1) ++ serializePoint(
|
||||
r2) ++ serializePoint(p1) ++ serializePoint(p2),
|
||||
algoName)
|
||||
.bytes
|
||||
}
|
||||
|
||||
/** Proves that the DLOG_G(R') = DLOG_Y(R) (= fe)
|
||||
* For a full description, see https://cs.nyu.edu/courses/spring07/G22.3220-001/lec3.pdf
|
||||
*/
|
||||
def dleqProve(
|
||||
fe: FieldElement,
|
||||
adaptorPoint: ECPublicKey,
|
||||
algoName: String): (FieldElement, FieldElement) = {
|
||||
// (fe*G, fe*Y)
|
||||
val (p1, p2) = dleqPair(fe, adaptorPoint)
|
||||
|
||||
// hash(Y || fe*G || fe*Y)
|
||||
val hash =
|
||||
CryptoUtil
|
||||
.sha256(
|
||||
serializePoint(adaptorPoint) ++ serializePoint(p1) ++ serializePoint(
|
||||
p2))
|
||||
.bytes
|
||||
val k = dleqNonceFunc(hash, fe, algoName)
|
||||
|
||||
if (k.isZero) {
|
||||
throw new RuntimeException("Nonce cannot be zero.")
|
||||
}
|
||||
|
||||
val r1 = k.getPublicKey
|
||||
val r2 = adaptorPoint.tweakMultiply(k)
|
||||
|
||||
// Hash all components to get a challenge (this is the trick that turns
|
||||
// interactive ZKPs into non-interactive ZKPs, using hash assumptions)
|
||||
//
|
||||
// In short, rather than having the verifier present challenges, hash
|
||||
// all shared information (so that both parties can compute) and use
|
||||
// this hash as the challenge to the prover as loosely speaking this
|
||||
// should only be game-able if the prover can reverse hash functions.
|
||||
val challengeHash =
|
||||
dleqChallengeHash(algoName, adaptorPoint, r1, r2, p1, p2)
|
||||
val e = FieldElement(challengeHash)
|
||||
|
||||
// s = k + fe*challenge. This proof works because then k = fe*challenge - s
|
||||
// so that R' = k*G =?= p1*challenge - s and R = k*Y =?= p2*challenge - s
|
||||
// can both be verified given s and challenge and will be true if and only
|
||||
// if R = y*R' which is what we are trying to prove.
|
||||
val s = fe.multiply(e).add(k)
|
||||
|
||||
(s, e)
|
||||
}
|
||||
|
||||
/** Verifies a proof that the DLOG_G of P1 equals the DLOG_adaptor of P2 */
|
||||
def dleqVerify(
|
||||
algoName: String,
|
||||
s: FieldElement,
|
||||
e: FieldElement,
|
||||
p1: ECPublicKey,
|
||||
adaptor: ECPublicKey,
|
||||
p2: ECPublicKey): Boolean = {
|
||||
val r1 = p1.tweakMultiply(e.negate).add(s.getPublicKey)
|
||||
val r2 = p2.tweakMultiply(e.negate).add(adaptor.tweakMultiply(s))
|
||||
val challengeHash = dleqChallengeHash(algoName, adaptor, r1, r2, p1, p2)
|
||||
|
||||
challengeHash == e.bytes
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,59 @@
|
||||
package org.bitcoins.crypto
|
||||
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
case class ECAdaptorSignature(bytes: ByteVector) extends NetworkElement {
|
||||
require(
|
||||
bytes.length == 162,
|
||||
s"Adaptor signature must have 65 byte sig and 97 byte dleq proof, got $bytes")
|
||||
|
||||
val (adaptedSig: ByteVector, dleqProof: ByteVector) = bytes.splitAt(65)
|
||||
|
||||
val tweakedNonce: ECPublicKey =
|
||||
ECAdaptorSignature.deserializePoint(adaptedSig.take(33))
|
||||
val adaptedS: FieldElement = FieldElement(adaptedSig.drop(33))
|
||||
|
||||
require(!adaptedS.isZero, "Adapted signature cannot be zero.")
|
||||
|
||||
val untweakedNonce: ECPublicKey =
|
||||
ECAdaptorSignature.deserializePoint(dleqProof.take(33))
|
||||
val dleqProofS: FieldElement = FieldElement(dleqProof.drop(33).take(32))
|
||||
val dleqProofE: FieldElement = FieldElement(dleqProof.drop(65))
|
||||
|
||||
require(ECPublicKey.isFullyValidWithBouncyCastle(tweakedNonce.bytes),
|
||||
s"Tweaked nonce (R) must be a valid public key: $tweakedNonce")
|
||||
require(ECPublicKey.isFullyValidWithBouncyCastle(untweakedNonce.bytes),
|
||||
s"Untweaked nonce (R') must be a valid public key: $untweakedNonce")
|
||||
}
|
||||
|
||||
object ECAdaptorSignature extends Factory[ECAdaptorSignature] {
|
||||
|
||||
def fromBytes(bytes: ByteVector): ECAdaptorSignature = {
|
||||
new ECAdaptorSignature(bytes)
|
||||
}
|
||||
|
||||
def apply(
|
||||
tweakedNonce: ECPublicKey,
|
||||
adaptedS: FieldElement,
|
||||
untweakedNonce: ECPublicKey,
|
||||
dleqProofS: FieldElement,
|
||||
dleqProofE: FieldElement): ECAdaptorSignature = {
|
||||
fromBytes(
|
||||
serializePoint(tweakedNonce) ++ adaptedS.bytes ++ serializePoint(
|
||||
untweakedNonce) ++ dleqProofS.bytes ++ dleqProofE.bytes
|
||||
)
|
||||
}
|
||||
|
||||
def empty(): ECAdaptorSignature =
|
||||
fromBytes(ByteVector.fill(162)(0.toByte))
|
||||
|
||||
def serializePoint(point: ECPublicKey): ByteVector = {
|
||||
val (sign, xCoor) = point.bytes.splitAt(1)
|
||||
sign.map(b => (b & 0x01).toByte) ++ xCoor
|
||||
}
|
||||
|
||||
def deserializePoint(point: ByteVector): ECPublicKey = {
|
||||
val (sign, xCoor) = point.splitAt(1)
|
||||
ECPublicKey(sign.map(b => (b | 0x02).toByte) ++ xCoor)
|
||||
}
|
||||
}
|
@ -167,6 +167,58 @@ sealed abstract class ECPrivateKey
|
||||
BouncyCastleUtil.schnorrSignWithNonce(dataToSign, this, nonce)
|
||||
}
|
||||
|
||||
// TODO: match on CryptoContext once secp version is added
|
||||
def adaptorSign(
|
||||
adaptorPoint: ECPublicKey,
|
||||
msg: ByteVector): ECAdaptorSignature = {
|
||||
adaptorSignWithBouncyCastle(adaptorPoint, msg)
|
||||
}
|
||||
|
||||
/*
|
||||
def adaptorSignWithSecp(
|
||||
adaptorPoint: ECPublicKey,
|
||||
msg: ByteVector): ECAdaptorSignature = {
|
||||
val sigWithProof = NativeSecp256k1.adaptorSign(bytes.toArray,
|
||||
adaptorPoint.bytes.toArray,
|
||||
msg.toArray)
|
||||
ECAdaptorSignature(ByteVector(sigWithProof))
|
||||
}
|
||||
*/
|
||||
|
||||
def adaptorSignWithBouncyCastle(
|
||||
adaptorPoint: ECPublicKey,
|
||||
msg: ByteVector): ECAdaptorSignature = {
|
||||
AdaptorStuff.adaptorSign(this, adaptorPoint, msg)
|
||||
}
|
||||
|
||||
// TODO: match on CryptoContext once secp version is added
|
||||
def completeAdaptorSignature(
|
||||
adaptorSignature: ECAdaptorSignature): ECDigitalSignature = {
|
||||
completeAdaptorSignatureWithBouncyCastle(adaptorSignature)
|
||||
}
|
||||
|
||||
/*
|
||||
def completeAdaptorSignatureWithSecp(
|
||||
adaptorSignature: ECAdaptorSignature): ECDigitalSignature = {
|
||||
val sigBytes = NativeSecp256k1.adaptorAdapt(
|
||||
bytes.toArray,
|
||||
adaptorSignature.adaptedSig.toArray)
|
||||
ECDigitalSignature.fromBytes(ByteVector(sigBytes))
|
||||
}
|
||||
*/
|
||||
|
||||
def completeAdaptorSignatureWithBouncyCastle(
|
||||
adaptorSignature: ECAdaptorSignature): ECDigitalSignature = {
|
||||
AdaptorStuff.adaptorComplete(this, adaptorSignature.adaptedSig)
|
||||
}
|
||||
|
||||
def completeAdaptorSignature(
|
||||
adaptorSignature: ECAdaptorSignature,
|
||||
hashTypeByte: Byte): ECDigitalSignature = {
|
||||
val completedSig = completeAdaptorSignature(adaptorSignature)
|
||||
ECDigitalSignature(completedSig.bytes ++ ByteVector.fromByte(hashTypeByte))
|
||||
}
|
||||
|
||||
def nonceKey: ECPrivateKey = {
|
||||
if (schnorrNonce.publicKey == publicKey) {
|
||||
this
|
||||
@ -394,6 +446,60 @@ sealed abstract class ECPublicKey extends BaseECKey {
|
||||
|
||||
def schnorrNonce: SchnorrNonce = SchnorrNonce(bytes)
|
||||
|
||||
// TODO: match on CryptoContext once secp version is added
|
||||
def adaptorVerify(
|
||||
msg: ByteVector,
|
||||
adaptorPoint: ECPublicKey,
|
||||
adaptorSignature: ECAdaptorSignature): Boolean = {
|
||||
adaptorVerifyWithBouncyCastle(msg, adaptorPoint, adaptorSignature)
|
||||
}
|
||||
|
||||
/*
|
||||
def adaptorVerifyWithSecp(
|
||||
msg: ByteVector,
|
||||
adaptorPoint: ECPublicKey,
|
||||
adaptorSignature: ECAdaptorSignature): Boolean = {
|
||||
NativeSecp256k1.adaptorVerify(adaptorSignature.adaptedSig.toArray,
|
||||
bytes.toArray,
|
||||
msg.toArray,
|
||||
adaptorPoint.bytes.toArray,
|
||||
adaptorSignature.dleqProof.toArray)
|
||||
}
|
||||
*/
|
||||
|
||||
def adaptorVerifyWithBouncyCastle(
|
||||
msg: ByteVector,
|
||||
adaptorPoint: ECPublicKey,
|
||||
adaptorSignature: ECAdaptorSignature): Boolean = {
|
||||
AdaptorStuff.adaptorVerify(adaptorSignature, this, msg, adaptorPoint)
|
||||
}
|
||||
|
||||
// TODO: match on CryptoContext once secp version is added
|
||||
def extractAdaptorSecret(
|
||||
adaptorSignature: ECAdaptorSignature,
|
||||
signature: ECDigitalSignature): ECPrivateKey = {
|
||||
extractAdaptorSecretWithBouncyCastle(adaptorSignature, signature)
|
||||
}
|
||||
|
||||
/*
|
||||
def extractAdaptorSecretWithSecp(
|
||||
adaptorSignature: ECAdaptorSignature,
|
||||
signature: ECDigitalSignature): ECPrivateKey = {
|
||||
val secretBytes = NativeSecp256k1.adaptorExtractSecret(
|
||||
signature.bytes.toArray,
|
||||
adaptorSignature.adaptedSig.toArray,
|
||||
bytes.toArray)
|
||||
|
||||
ECPrivateKey(ByteVector(secretBytes))
|
||||
}
|
||||
*/
|
||||
|
||||
def extractAdaptorSecretWithBouncyCastle(
|
||||
adaptorSignature: ECAdaptorSignature,
|
||||
signature: ECDigitalSignature): ECPrivateKey = {
|
||||
AdaptorStuff.extractAdaptorSecret(signature, adaptorSignature, this)
|
||||
}
|
||||
|
||||
override def toString: String = "ECPublicKey(" + hex + ")"
|
||||
|
||||
/** Checks if the [[org.bitcoins.crypto.ECPublicKey ECPublicKey]] is compressed */
|
||||
|
@ -12,6 +12,7 @@ import org.bitcoins.crypto.{
|
||||
AesPassword,
|
||||
CryptoUtil,
|
||||
DoubleSha256Digest,
|
||||
ECAdaptorSignature,
|
||||
ECDigitalSignature,
|
||||
ECPrivateKey,
|
||||
ECPublicKey,
|
||||
@ -228,6 +229,18 @@ sealed abstract class CryptoGenerators {
|
||||
} yield privKey.schnorrSign(hash.bytes)
|
||||
}
|
||||
|
||||
def adaptorSignature: Gen[ECAdaptorSignature] = {
|
||||
for {
|
||||
tweakedNonce <- publicKey
|
||||
untweakedNonce <- publicKey
|
||||
adaptedS <- fieldElement
|
||||
proofS <- fieldElement
|
||||
proofE <- fieldElement
|
||||
} yield {
|
||||
ECAdaptorSignature(tweakedNonce, adaptedS, untweakedNonce, proofS, proofE)
|
||||
}
|
||||
}
|
||||
|
||||
def sha256Digest: Gen[Sha256Digest] =
|
||||
for {
|
||||
bytes <- NumberGenerator.bytevector
|
||||
|
Loading…
Reference in New Issue
Block a user