mirror of
https://github.com/dashpay/dash.git
synced 2024-12-28 21:42:47 +01:00
593ff7e929
## Issue being fixed or feature implemented ## What was done? When verifying signature of `CGovernanceVote`/`CGovernanceObject` we need to use the active scheme. ## How Has This Been Tested? ## Breaking Changes ## Checklist: - [x] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation **For repository code-owners and collaborators only** - [x] I have assigned this pull request to a milestone
603 lines
18 KiB
C++
603 lines
18 KiB
C++
// Copyright (c) 2018-2022 The Dash Core developers
|
|
// Distributed under the MIT software license, see the accompanying
|
|
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
|
|
#ifndef DASH_CRYPTO_BLS_H
|
|
#define DASH_CRYPTO_BLS_H
|
|
|
|
#include <hash.h>
|
|
#include <serialize.h>
|
|
#include <uint256.h>
|
|
#include <util/strencodings.h>
|
|
#include <util/ranges.h>
|
|
|
|
// bls-dash uses relic, which may define DEBUG and ERROR, which leads to many warnings in some build setups
|
|
#undef ERROR
|
|
#undef DEBUG
|
|
#include <dashbls/bls.hpp>
|
|
#include <dashbls/privatekey.hpp>
|
|
#include <dashbls/elements.hpp>
|
|
#include <dashbls/schemes.hpp>
|
|
#include <dashbls/threshold.hpp>
|
|
#undef DOUBLE
|
|
#undef SEED
|
|
|
|
#include <array>
|
|
#include <mutex>
|
|
#include <unistd.h>
|
|
|
|
#include <atomic>
|
|
|
|
namespace bls {
|
|
extern std::atomic<bool> bls_legacy_scheme;
|
|
}
|
|
|
|
// reversed BLS12-381
|
|
constexpr int BLS_CURVE_ID_SIZE{32};
|
|
constexpr int BLS_CURVE_SECKEY_SIZE{32};
|
|
constexpr int BLS_CURVE_PUBKEY_SIZE{48};
|
|
constexpr int BLS_CURVE_SIG_SIZE{96};
|
|
|
|
class CBLSSignature;
|
|
class CBLSPublicKey;
|
|
|
|
template <typename ImplType, size_t _SerSize, typename C>
|
|
class CBLSWrapper
|
|
{
|
|
friend class CBLSSecretKey;
|
|
friend class CBLSPublicKey;
|
|
friend class CBLSSignature;
|
|
|
|
protected:
|
|
ImplType impl;
|
|
bool fValid{false};
|
|
mutable uint256 cachedHash;
|
|
|
|
public:
|
|
static constexpr size_t SerSize = _SerSize;
|
|
|
|
explicit CBLSWrapper() = default;
|
|
explicit CBLSWrapper(const std::vector<unsigned char>& vecBytes) : CBLSWrapper<ImplType, _SerSize, C>()
|
|
{
|
|
SetByteVector(vecBytes);
|
|
}
|
|
|
|
CBLSWrapper(const CBLSWrapper& ref) = default;
|
|
CBLSWrapper& operator=(const CBLSWrapper& ref) = default;
|
|
CBLSWrapper(CBLSWrapper&& ref) noexcept
|
|
{
|
|
std::swap(impl, ref.impl);
|
|
std::swap(fValid, ref.fValid);
|
|
std::swap(cachedHash, ref.cachedHash);
|
|
}
|
|
CBLSWrapper& operator=(CBLSWrapper&& ref) noexcept
|
|
{
|
|
std::swap(impl, ref.impl);
|
|
std::swap(fValid, ref.fValid);
|
|
std::swap(cachedHash, ref.cachedHash);
|
|
return *this;
|
|
}
|
|
|
|
virtual ~CBLSWrapper() = default;
|
|
|
|
bool operator==(const C& r) const
|
|
{
|
|
return fValid == r.fValid && impl == r.impl;
|
|
}
|
|
bool operator!=(const C& r) const
|
|
{
|
|
return !((*this) == r);
|
|
}
|
|
|
|
bool IsValid() const
|
|
{
|
|
return fValid;
|
|
}
|
|
|
|
void Reset()
|
|
{
|
|
*(static_cast<C*>(this)) = C();
|
|
}
|
|
|
|
void SetByteVector(const std::vector<uint8_t>& vecBytes, const bool specificLegacyScheme)
|
|
{
|
|
if (vecBytes.size() != SerSize) {
|
|
Reset();
|
|
return;
|
|
}
|
|
|
|
if (ranges::all_of(vecBytes, [](uint8_t c) { return c == 0; })) {
|
|
Reset();
|
|
} else {
|
|
try {
|
|
impl = ImplType::FromBytes(bls::Bytes(vecBytes), specificLegacyScheme);
|
|
fValid = true;
|
|
} catch (...) {
|
|
Reset();
|
|
}
|
|
}
|
|
cachedHash.SetNull();
|
|
}
|
|
|
|
void SetByteVector(const std::vector<uint8_t>& vecBytes)
|
|
{
|
|
SetByteVector(vecBytes, bls::bls_legacy_scheme.load());
|
|
}
|
|
|
|
std::vector<uint8_t> ToByteVector(const bool specificLegacyScheme) const
|
|
{
|
|
if (!fValid) {
|
|
return std::vector<uint8_t>(SerSize, 0);
|
|
}
|
|
return impl.Serialize(specificLegacyScheme);
|
|
}
|
|
|
|
std::vector<uint8_t> ToByteVector() const
|
|
{
|
|
return ToByteVector(bls::bls_legacy_scheme.load());
|
|
}
|
|
|
|
const uint256& GetHash() const
|
|
{
|
|
if (cachedHash.IsNull()) {
|
|
cachedHash = ::SerializeHash(*this);
|
|
}
|
|
return cachedHash;
|
|
}
|
|
|
|
bool SetHexStr(const std::string& str, const bool specificLegacyScheme)
|
|
{
|
|
if (!IsHex(str)) {
|
|
Reset();
|
|
return false;
|
|
}
|
|
auto b = ParseHex(str);
|
|
if (b.size() != SerSize) {
|
|
Reset();
|
|
return false;
|
|
}
|
|
SetByteVector(b, specificLegacyScheme);
|
|
return IsValid();
|
|
}
|
|
|
|
bool SetHexStr(const std::string& str)
|
|
{
|
|
return SetHexStr(str, bls::bls_legacy_scheme.load());
|
|
}
|
|
|
|
inline void Serialize(CSizeComputer& s) const
|
|
{
|
|
s.seek(SerSize);
|
|
}
|
|
|
|
template <typename Stream>
|
|
inline void Serialize(Stream& s, const bool specificLegacyScheme) const
|
|
{
|
|
s.write(reinterpret_cast<const char*>(ToByteVector(specificLegacyScheme).data()), SerSize);
|
|
}
|
|
|
|
template <typename Stream>
|
|
inline void Serialize(Stream& s) const
|
|
{
|
|
Serialize(s, bls::bls_legacy_scheme.load());
|
|
}
|
|
|
|
template <typename Stream>
|
|
inline void Unserialize(Stream& s, const bool specificLegacyScheme, bool checkMalleable = true)
|
|
{
|
|
std::vector<uint8_t> vecBytes(SerSize, 0);
|
|
s.read(reinterpret_cast<char*>(vecBytes.data()), SerSize);
|
|
SetByteVector(vecBytes, specificLegacyScheme);
|
|
|
|
if (checkMalleable && !CheckMalleable(vecBytes, specificLegacyScheme)) {
|
|
// If CheckMalleable failed with specificLegacyScheme, we need to try again with the opposite scheme.
|
|
// Probably we received the BLS object sent with legacy scheme, but in the meanwhile the fork activated.
|
|
SetByteVector(vecBytes, !specificLegacyScheme);
|
|
if (!CheckMalleable(vecBytes, !specificLegacyScheme)) {
|
|
// Both attempts failed
|
|
throw std::ios_base::failure("malleable BLS object");
|
|
} else {
|
|
// Indeed the received vecBytes was in opposite scheme. But we can't keep it (mixing with the new scheme will lead to undefined behavior)
|
|
// Therefore, resetting current object (basically marking it as invalid).
|
|
Reset();
|
|
}
|
|
}
|
|
}
|
|
|
|
template <typename Stream>
|
|
inline void Unserialize(Stream& s, bool checkMalleable = true)
|
|
{
|
|
Unserialize(s, bls::bls_legacy_scheme.load(), checkMalleable);
|
|
}
|
|
|
|
inline bool CheckMalleable(const std::vector<uint8_t>& vecBytes, const bool specificLegacyScheme) const
|
|
{
|
|
if (memcmp(vecBytes.data(), ToByteVector(specificLegacyScheme).data(), SerSize)) {
|
|
// TODO not sure if this is actually possible with the BLS libs. I'm assuming here that somewhere deep inside
|
|
// these libs masking might happen, so that 2 different binary representations could result in the same object
|
|
// representation
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
inline bool CheckMalleable(const std::vector<uint8_t>& vecBytes) const
|
|
{
|
|
return CheckMalleable(vecBytes, bls::bls_legacy_scheme.load());
|
|
}
|
|
|
|
inline std::string ToString(const bool specificLegacyScheme) const
|
|
{
|
|
std::vector<uint8_t> buf = ToByteVector(specificLegacyScheme);
|
|
return HexStr(buf);
|
|
}
|
|
|
|
inline std::string ToString() const
|
|
{
|
|
return ToString(bls::bls_legacy_scheme.load());
|
|
}
|
|
};
|
|
|
|
struct CBLSIdImplicit : public uint256
|
|
{
|
|
CBLSIdImplicit() = default;
|
|
CBLSIdImplicit(const uint256& id)
|
|
{
|
|
memcpy(begin(), id.begin(), sizeof(uint256));
|
|
}
|
|
static CBLSIdImplicit FromBytes(const uint8_t* buffer, const bool fLegacy)
|
|
{
|
|
CBLSIdImplicit instance;
|
|
memcpy(instance.begin(), buffer, sizeof(CBLSIdImplicit));
|
|
return instance;
|
|
}
|
|
[[nodiscard]] std::vector<uint8_t> Serialize(const bool fLegacy) const
|
|
{
|
|
return {begin(), end()};
|
|
}
|
|
};
|
|
|
|
class CBLSId : public CBLSWrapper<CBLSIdImplicit, BLS_CURVE_ID_SIZE, CBLSId>
|
|
{
|
|
public:
|
|
using CBLSWrapper::operator=;
|
|
using CBLSWrapper::operator==;
|
|
using CBLSWrapper::operator!=;
|
|
using CBLSWrapper::CBLSWrapper;
|
|
|
|
CBLSId() = default;
|
|
explicit CBLSId(const uint256& nHash);
|
|
};
|
|
|
|
class CBLSSecretKey : public CBLSWrapper<bls::PrivateKey, BLS_CURVE_SECKEY_SIZE, CBLSSecretKey>
|
|
{
|
|
public:
|
|
using CBLSWrapper::operator=;
|
|
using CBLSWrapper::operator==;
|
|
using CBLSWrapper::operator!=;
|
|
using CBLSWrapper::CBLSWrapper;
|
|
|
|
CBLSSecretKey() = default;
|
|
CBLSSecretKey(const CBLSSecretKey&) = default;
|
|
CBLSSecretKey& operator=(const CBLSSecretKey&) = default;
|
|
|
|
void AggregateInsecure(const CBLSSecretKey& o);
|
|
static CBLSSecretKey AggregateInsecure(const std::vector<CBLSSecretKey>& sks);
|
|
|
|
#ifndef BUILD_BITCOIN_INTERNAL
|
|
void MakeNewKey();
|
|
#endif
|
|
bool SecretKeyShare(const std::vector<CBLSSecretKey>& msk, const CBLSId& id);
|
|
|
|
[[nodiscard]] CBLSPublicKey GetPublicKey() const;
|
|
[[nodiscard]] CBLSSignature Sign(const uint256& hash) const;
|
|
};
|
|
|
|
class CBLSPublicKey : public CBLSWrapper<bls::G1Element, BLS_CURVE_PUBKEY_SIZE, CBLSPublicKey>
|
|
{
|
|
friend class CBLSSecretKey;
|
|
friend class CBLSSignature;
|
|
|
|
public:
|
|
using CBLSWrapper::operator=;
|
|
using CBLSWrapper::operator==;
|
|
using CBLSWrapper::operator!=;
|
|
using CBLSWrapper::CBLSWrapper;
|
|
|
|
CBLSPublicKey() = default;
|
|
|
|
void AggregateInsecure(const CBLSPublicKey& o);
|
|
static CBLSPublicKey AggregateInsecure(const std::vector<CBLSPublicKey>& pks);
|
|
|
|
bool PublicKeyShare(const std::vector<CBLSPublicKey>& mpk, const CBLSId& id);
|
|
bool DHKeyExchange(const CBLSSecretKey& sk, const CBLSPublicKey& pk);
|
|
|
|
};
|
|
|
|
class ConstCBLSPublicKeyVersionWrapper {
|
|
private:
|
|
const CBLSPublicKey& obj;
|
|
bool legacy;
|
|
public:
|
|
ConstCBLSPublicKeyVersionWrapper(const CBLSPublicKey& obj, bool legacy)
|
|
: obj(obj)
|
|
, legacy(legacy)
|
|
{}
|
|
template <typename Stream>
|
|
inline void Serialize(Stream& s) const {
|
|
obj.Serialize(s, legacy);
|
|
}
|
|
};
|
|
|
|
class CBLSPublicKeyVersionWrapper {
|
|
private:
|
|
CBLSPublicKey& obj;
|
|
bool legacy;
|
|
bool checkMalleable;
|
|
public:
|
|
CBLSPublicKeyVersionWrapper(CBLSPublicKey& obj, bool legacy, bool checkMalleable = true)
|
|
: obj(obj)
|
|
, legacy(legacy)
|
|
, checkMalleable(checkMalleable)
|
|
{}
|
|
template <typename Stream>
|
|
inline void Serialize(Stream& s) const {
|
|
obj.Serialize(s, legacy);
|
|
}
|
|
template <typename Stream>
|
|
inline void Unserialize(Stream& s) {
|
|
obj.Unserialize(s, legacy, checkMalleable);
|
|
}
|
|
};
|
|
|
|
class CBLSSignature : public CBLSWrapper<bls::G2Element, BLS_CURVE_SIG_SIZE, CBLSSignature>
|
|
{
|
|
friend class CBLSSecretKey;
|
|
|
|
public:
|
|
using CBLSWrapper::operator==;
|
|
using CBLSWrapper::operator!=;
|
|
using CBLSWrapper::CBLSWrapper;
|
|
|
|
CBLSSignature() = default;
|
|
CBLSSignature(const CBLSSignature&) = default;
|
|
CBLSSignature& operator=(const CBLSSignature&) = default;
|
|
|
|
void AggregateInsecure(const CBLSSignature& o);
|
|
static CBLSSignature AggregateInsecure(const std::vector<CBLSSignature>& sigs);
|
|
static CBLSSignature AggregateSecure(const std::vector<CBLSSignature>& sigs, const std::vector<CBLSPublicKey>& pks, const uint256& hash);
|
|
|
|
void SubInsecure(const CBLSSignature& o);
|
|
[[nodiscard]] bool VerifyInsecure(const CBLSPublicKey& pubKey, const uint256& hash, const bool specificLegacyScheme) const;
|
|
[[nodiscard]] bool VerifyInsecure(const CBLSPublicKey& pubKey, const uint256& hash) const;
|
|
[[nodiscard]] bool VerifyInsecureAggregated(const std::vector<CBLSPublicKey>& pubKeys, const std::vector<uint256>& hashes) const;
|
|
|
|
[[nodiscard]] bool VerifySecureAggregated(const std::vector<CBLSPublicKey>& pks, const uint256& hash) const;
|
|
|
|
bool Recover(const std::vector<CBLSSignature>& sigs, const std::vector<CBLSId>& ids);
|
|
};
|
|
|
|
class CBLSSignatureVersionWrapper {
|
|
private:
|
|
CBLSSignature& obj;
|
|
bool legacy;
|
|
bool checkMalleable;
|
|
public:
|
|
CBLSSignatureVersionWrapper(CBLSSignature& obj, bool legacy, bool checkMalleable = true)
|
|
: obj(obj)
|
|
, legacy(legacy)
|
|
, checkMalleable(checkMalleable)
|
|
{}
|
|
template <typename Stream>
|
|
inline void Serialize(Stream& s) const {
|
|
obj.Serialize(s, legacy);
|
|
}
|
|
template <typename Stream>
|
|
inline void Unserialize(Stream& s) {
|
|
obj.Unserialize(s, legacy, checkMalleable);
|
|
}
|
|
};
|
|
|
|
#ifndef BUILD_BITCOIN_INTERNAL
|
|
template<typename BLSObject>
|
|
class CBLSLazyWrapper
|
|
{
|
|
private:
|
|
mutable std::mutex mutex;
|
|
|
|
mutable std::vector<uint8_t> vecBytes;
|
|
mutable bool bufValid{false};
|
|
mutable bool bufLegacyScheme{true};
|
|
|
|
mutable BLSObject obj;
|
|
mutable bool objInitialized{false};
|
|
|
|
mutable uint256 hash;
|
|
|
|
public:
|
|
CBLSLazyWrapper() :
|
|
vecBytes(BLSObject::SerSize, 0),
|
|
bufLegacyScheme(bls::bls_legacy_scheme.load())
|
|
{
|
|
// the all-zero buf is considered a valid buf, but the resulting object will return false for IsValid
|
|
bufValid = true;
|
|
}
|
|
|
|
explicit CBLSLazyWrapper(const CBLSLazyWrapper& r)
|
|
{
|
|
*this = r;
|
|
}
|
|
virtual ~CBLSLazyWrapper() = default;
|
|
|
|
CBLSLazyWrapper& operator=(const CBLSLazyWrapper& r)
|
|
{
|
|
std::unique_lock<std::mutex> l(r.mutex);
|
|
bufValid = r.bufValid;
|
|
bufLegacyScheme = r.bufLegacyScheme;
|
|
if (r.bufValid) {
|
|
vecBytes = r.vecBytes;
|
|
} else {
|
|
std::fill(vecBytes.begin(), vecBytes.end(), 0);
|
|
}
|
|
objInitialized = r.objInitialized;
|
|
if (r.objInitialized) {
|
|
obj = r.obj;
|
|
} else {
|
|
obj.Reset();
|
|
}
|
|
hash = r.hash;
|
|
return *this;
|
|
}
|
|
|
|
inline void Serialize(CSizeComputer& s) const
|
|
{
|
|
s.seek(BLSObject::SerSize);
|
|
}
|
|
|
|
template<typename Stream>
|
|
inline void Serialize(Stream& s, const bool specificLegacyScheme) const
|
|
{
|
|
std::unique_lock<std::mutex> l(mutex);
|
|
if (!objInitialized && !bufValid) {
|
|
// the all-zero buf is considered a valid buf
|
|
std::fill(vecBytes.begin(), vecBytes.end(), 0);
|
|
bufLegacyScheme = specificLegacyScheme;
|
|
bufValid = true;
|
|
}
|
|
if (!bufValid || (bufLegacyScheme != specificLegacyScheme)) {
|
|
vecBytes = obj.ToByteVector(specificLegacyScheme);
|
|
bufValid = true;
|
|
bufLegacyScheme = specificLegacyScheme;
|
|
hash.SetNull();
|
|
}
|
|
s.write(reinterpret_cast<const char*>(vecBytes.data()), vecBytes.size());
|
|
}
|
|
|
|
template<typename Stream>
|
|
inline void Serialize(Stream& s) const
|
|
{
|
|
Serialize(s, bls::bls_legacy_scheme.load());
|
|
}
|
|
|
|
template<typename Stream>
|
|
inline void Unserialize(Stream& s, const bool specificLegacyScheme) const
|
|
{
|
|
std::unique_lock<std::mutex> l(mutex);
|
|
s.read(reinterpret_cast<char*>(vecBytes.data()), BLSObject::SerSize);
|
|
bufValid = true;
|
|
bufLegacyScheme = specificLegacyScheme;
|
|
objInitialized = false;
|
|
hash.SetNull();
|
|
}
|
|
|
|
template<typename Stream>
|
|
inline void Unserialize(Stream& s) const
|
|
{
|
|
Unserialize(s, bls::bls_legacy_scheme.load());
|
|
}
|
|
|
|
void Set(const BLSObject& _obj)
|
|
{
|
|
std::unique_lock<std::mutex> l(mutex);
|
|
bufValid = false;
|
|
objInitialized = true;
|
|
obj = _obj;
|
|
hash.SetNull();
|
|
}
|
|
const BLSObject& Get() const
|
|
{
|
|
std::unique_lock<std::mutex> l(mutex);
|
|
static BLSObject invalidObj;
|
|
if (!bufValid && !objInitialized) {
|
|
return invalidObj;
|
|
}
|
|
if (!objInitialized) {
|
|
obj.SetByteVector(vecBytes, bufLegacyScheme);
|
|
if (!obj.IsValid()) {
|
|
// If setting of BLS object using one scheme failed, then we need to attempt again with the opposite scheme.
|
|
// This is due to the fact that LazyBLSWrapper receives a serialised buffer but attempts to create actual BLS object when needed.
|
|
// That could happen when the fork has been activated and the enforced scheme has switched.
|
|
obj.SetByteVector(vecBytes, !bufLegacyScheme);
|
|
if (obj.IsValid()) {
|
|
bufLegacyScheme = !bufLegacyScheme;
|
|
}
|
|
}
|
|
if (!obj.CheckMalleable(vecBytes, bufLegacyScheme)) {
|
|
bufValid = false;
|
|
objInitialized = false;
|
|
obj = invalidObj;
|
|
} else {
|
|
objInitialized = true;
|
|
}
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
bool operator==(const CBLSLazyWrapper& r) const
|
|
{
|
|
if (bufValid && r.bufValid && bufLegacyScheme == r.bufLegacyScheme) {
|
|
return vecBytes == r.vecBytes;
|
|
}
|
|
if (objInitialized && r.objInitialized) {
|
|
return obj == r.obj;
|
|
}
|
|
return Get() == r.Get();
|
|
}
|
|
|
|
bool operator!=(const CBLSLazyWrapper& r) const
|
|
{
|
|
return !(*this == r);
|
|
}
|
|
|
|
uint256 GetHash(const bool specificLegacyScheme = bls::bls_legacy_scheme.load()) const
|
|
{
|
|
std::unique_lock<std::mutex> l(mutex);
|
|
if (!bufValid || bufLegacyScheme != specificLegacyScheme) {
|
|
vecBytes = obj.ToByteVector(specificLegacyScheme);
|
|
bufValid = true;
|
|
bufLegacyScheme = specificLegacyScheme;
|
|
hash.SetNull();
|
|
}
|
|
if (hash.IsNull()) {
|
|
CHashWriter ss(SER_GETHASH, PROTOCOL_VERSION);
|
|
ss.write(reinterpret_cast<const char*>(vecBytes.data()), vecBytes.size());
|
|
hash = ss.GetHash();
|
|
}
|
|
return hash;
|
|
}
|
|
};
|
|
using CBLSLazySignature = CBLSLazyWrapper<CBLSSignature>;
|
|
using CBLSLazyPublicKey = CBLSLazyWrapper<CBLSPublicKey>;
|
|
|
|
class CBLSLazyPublicKeyVersionWrapper {
|
|
private:
|
|
CBLSLazyPublicKey& obj;
|
|
bool legacy;
|
|
public:
|
|
CBLSLazyPublicKeyVersionWrapper(CBLSLazyPublicKey& obj, bool legacy)
|
|
: obj(obj)
|
|
, legacy(legacy)
|
|
{}
|
|
template <typename Stream>
|
|
inline void Serialize(Stream& s) const {
|
|
obj.Serialize(s, legacy);
|
|
}
|
|
template <typename Stream>
|
|
inline void Unserialize(Stream& s) {
|
|
obj.Unserialize(s, legacy);
|
|
}
|
|
};
|
|
#endif
|
|
|
|
using BLSIdVector = std::vector<CBLSId>;
|
|
using BLSVerificationVector = std::vector<CBLSPublicKey>;
|
|
using BLSPublicKeyVector = std::vector<CBLSPublicKey>;
|
|
using BLSSecretKeyVector = std::vector<CBLSSecretKey>;
|
|
using BLSSignatureVector = std::vector<CBLSSignature>;
|
|
|
|
using BLSVerificationVectorPtr = std::shared_ptr<BLSVerificationVector>;
|
|
|
|
bool BLSInit();
|
|
|
|
#endif // DASH_CRYPTO_BLS_H
|