Add Litecoin (LTC) support. (#114)

Fulcrum will now have support for Litecoin, in addition to Bitcoin Cash and Bitcoin BTC.

Includes support for deserializing/reserializing mweb data, so Fulcrum should work with 
latest `litecoind` `v0.21.2`.  Note that no known Electrum clients on LTC know how to 
*deal* with mweb data, but Fulcrum at least won't die on it when receiving txns or blocks
containing it from `litcoind` RPC. 

See PR #144 for more details.
See also discussion here: https://github.com/spesmilo/electrumx/issues/179
This commit is contained in:
Calin Culianu 2022-05-25 19:20:42 -05:00 committed by GitHub
parent 916b43495f
commit 5ac7d076f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 639 additions and 111 deletions

View file

@ -418,6 +418,7 @@ HEADERS += \
bitcoin/cashaddr.h \
bitcoin/cashaddrenc.h \
bitcoin/compat.h \
bitcoin/copyable_ptr.h \
bitcoin/crypto/byteswap.h \
bitcoin/crypto/endian.h \
bitcoin/crypto/aes.h \
@ -433,6 +434,7 @@ HEADERS += \
bitcoin/feerate.h \
bitcoin/hash.h \
bitcoin/interpreter.h \
bitcoin/litecoin_bits.h \
bitcoin/prevector.h \
bitcoin/pubkey.h \
bitcoin/reverse_iterator.h \

View file

@ -4,7 +4,7 @@
[![Docker Build](https://github.com/cculianu/Fulcrum/actions/workflows/publish.yml/badge.svg)](https://github.com/cculianu/Fulcrum/actions/workflows/publish.yml)
[![Copr build status](https://copr.fedorainfracloud.org/coprs/jonny/BitcoinCash/package/fulcrum/status_image/last_build.png)](https://copr.fedorainfracloud.org/coprs/jonny/BitcoinCash/package/fulcrum/)
A fast & nimble SPV server for Bitcoin Cash & Bitcoin BTC.
A fast & nimble SPV server for Bitcoin Cash, Bitcoin BTC, and Litecoin.
#### Copyright
(C) 2019-2022 Calin Culianu <calin.culianu@gmail.com>
@ -19,7 +19,7 @@ GPLv3. See the included `LICENSE.txt` file or [visit gnu.org and read the licens
- *Fast:* Written in 100% modern `C++17` using multi-threaded and asynchronous programming techniques.
- *A drop-in replacement for ElectronX/ElectrumX:* Fulcrum is 100% protocol-level compatible with the [Electrum Cash 1.4.5 protocol](https://electrum-cash-protocol.readthedocs.io/en/latest/). Existing server admins should feel right at home with this software since installation and management of it is nearly identical to ElectronX/ElectrumX server.
- *Cross-platform:* While this codebase was mainly developed and tested on MacOS, Windows and Linux, it should theoretically work on any modern OS (such as *BSD) that has Qt5 Core and Qt5 Networking available.
- ***NEW!*** *Dual-coin:* Supports both BCH and BTC.
- ***NEW!*** *Triple-coin:* Supports BCH, BTC and LTC.
### Requirements
@ -27,9 +27,11 @@ GPLv3. See the included `LICENSE.txt` file or [visit gnu.org and read the licens
- A supported bitcoin full node with its JSON-RPC service enabled, preferably running on the same machine.
- *For **BCH***: Bitcoin Cash Node, Bitcoin Unlimited Cash, Flowee, and bchd have all been tested extensively and are known to work well with this software.
- *For **BTC***: Bitcoin Core v0.17.0 or later. No other full nodes are supported by this software for BTC.
- *For **LTC***: Litecoin Core v0.17.0 or later. No other full nodes are supported by this software for LTC.
- If using Litcoin Core v0.21.2 or above, your daemon is serializing data using mweb extensions. While Fulcrum understands this serialization format, your Electrum-LTC clients may not. You can run `litecoind` with `-rpcserialversion=1` to have your daemon return transactions in pre-mweb format which is understood by most Electrum-LTC clients.
- The node must have txindex enabled e.g. `txindex=1`.
- The node must not be a pruning node.
- *Optional*: For best results, enable zmq for the "hasblock" topic using e.g. `zmqpubhashblock=tcp://0.0.0.0:8433` in your `bitcoin.conf` file (zmq is only available on: Core, BCHN, or BU 1.9.1+).
- *Optional*: For best results, enable zmq for the "hasblock" topic using e.g. `zmqpubhashblock=tcp://0.0.0.0:8433` in your `bitcoin.conf` file (zmq is only available on: Core, BCHN, BU 1.9.1+, or Litecoin Core).
- *Recommended hardware*: Minimum 1GB RAM, 64-bit CPU, ~40GB disk space for mainnet BCH (slightly more for BTC). For best results, use an SSD rather than an HDD.
- *For compiling*:
- `Qt Core` & `Qt Networking` libraries `5.12.5` or above (I use `5.15.2` myself). Qt `5.12.4` (or earlier) is not supported.

View file

@ -6,5 +6,7 @@
<file>resources/bch/servers_scalenet.json</file>
<file>resources/btc/servers.json</file>
<file>resources/btc/servers_testnet.json</file>
<file>resources/ltc/servers.json</file>
<file>resources/ltc/servers_testnet.json</file>
</qresource>
</RCC>

View file

@ -0,0 +1,22 @@
{
"backup.electrum-ltc.org": {
"t": "50001",
"s": "443"
},
"electrum.ltc.xurious.com": {
"t": "50001",
"s": "50002"
},
"electrum1.cipig.net": {
"t": "10063",
"s": "20063"
},
"electrum2.cipig.net": {
"t": "10063",
"s": "20063"
},
"electrum3.cipig.net": {
"t": "10063",
"s": "20063"
}
}

View file

@ -0,0 +1,10 @@
{
"electrum.ltc.xurious.com": {
"s": "51002",
"t": "51001"
},
"electrum-ltc.bysh.me": {
"s": "51002",
"t": "51001"
}
}

View file

@ -72,11 +72,17 @@ namespace BTC
/// specialization for CTransaction which works differently
template <> bitcoin::CTransaction Deserialize(const QByteArray &bytes, int pos, bool allowSegWit)
template <> bitcoin::CTransaction Deserialize(const QByteArray &bytes, int pos, bool allowSegWit, bool allowMW,
bool noJunkAtEnd)
{
const int version = bitcoin::PROTOCOL_VERSION | (allowSegWit ? bitcoin::SERIALIZE_TRANSACTION_USE_WITNESS : 0);
int version = bitcoin::PROTOCOL_VERSION;
if (allowSegWit) version |= bitcoin::SERIALIZE_TRANSACTION_USE_WITNESS;
if (allowMW) version |= bitcoin::SERIALIZE_TRANSACTION_USE_MWEB;
bitcoin::GenericVectorReader<QByteArray> vr(bitcoin::SER_NETWORK, version, bytes, pos);
return bitcoin::CTransaction(bitcoin::deserialize, vr);
auto ret = bitcoin::CTransaction(bitcoin::deserialize, vr);
if (noJunkAtEnd && !vr.empty())
throw std::ios_base::failure("Got unprocessed bytes at the end when deserializeing a bitcoin object");
return ret;
}
QByteArray Hash(const QByteArray &b, bool once)
@ -167,14 +173,14 @@ namespace BTC
{ RegTestNet, "regtest"},
}};
const QMap<QString, Net> nameNetMap = {{
{"main", MainNet}, // BCHN, BU, ABC, Core
{"main", MainNet}, // BCHN, BU, ABC, Core, LitecoinCore
{"mainnet", MainNet}, // bchd
{"test", TestNet}, // BCHN, BU, ABC, Core
{"test", TestNet}, // BCHN, BU, ABC, Core, LitecoinCore
{"test4", TestNet4}, // BCHN, BU
{"scale", ScaleNet}, // BCHN, BU
{"testnet3", TestNet}, // bchd
{"testnet4", TestNet4}, // possible future bchd
{"regtest", RegTestNet}, // BCHN, BU, ABC, bchd
{"regtest", RegTestNet}, // BCHN, BU, ABC, bchd, Core, LitecoinCore
{"signet", TestNet}, // Core only
}};
const QString invalidNetName = "invalid";
@ -188,12 +194,13 @@ namespace BTC
return nameNetMap.value(name, Net::Invalid /* default if not found */);
}
namespace { const QString coinNameBCH{"BCH"}, coinNameBTC{"BTC"}; }
namespace { const QString coinNameBCH{"BCH"}, coinNameBTC{"BTC"}, coinNameLTC{"LTC"}; }
QString coinToName(Coin c) {
QString ret; // for NRVO
switch (c) {
case Coin::BCH: ret = coinNameBCH; break;
case Coin::BTC: ret = coinNameBTC; break;
case Coin::LTC: ret = coinNameLTC; break;
case Coin::Unknown: break;
}
return ret;
@ -201,6 +208,7 @@ namespace BTC
Coin coinFromName(const QString &s) {
if (s == coinNameBCH) return Coin::BCH;
if (s == coinNameBTC) return Coin::BTC;
if (s == coinNameLTC) return Coin::LTC;
return Coin::Unknown;
}

View file

@ -28,10 +28,12 @@
#include <QByteArray>
#include <QHash>
#include <QMetaType>
#include <QString>
#include <cstddef> // for std::byte, etc
#include <cstring> // for memcpy
#include <ios>
#include <type_traits>
#include <utility> // for pair, etc
@ -39,8 +41,8 @@
/// this namespace is not specific to Bitcoin (Core), but applies to BCH as well.
namespace BTC
{
/// Used by the Storage and Controller subsystem to figure out what coin we are on (BCH vs BTC)
enum class Coin { Unknown = 0, BCH, BTC };
/// Used by the Storage and Controller subsystem to figure out what coin we are on (BCH vs BTC vs LTC)
enum class Coin { Unknown = 0, BCH, BTC, LTC };
QString coinToName(Coin);
Coin coinFromName(const QString &);
@ -62,38 +64,46 @@ namespace BTC
/// Specify from_pos=-1 for appending at the end. Returns a reference to the passed-in buffer. This is very fast
/// and done in-place.
template <typename BitcoinObject>
QByteArray & Serialize(QByteArray &buf, const BitcoinObject &thing, int from_pos = -1, bool allowSegWit = false)
QByteArray & Serialize(QByteArray &buf, const BitcoinObject &thing, int from_pos = -1, bool allowSegWit = false, bool allowMW = false)
{
if (from_pos < 0) from_pos = buf.size();
const int version = bitcoin::PROTOCOL_VERSION | (allowSegWit ? bitcoin::SERIALIZE_TRANSACTION_USE_WITNESS : 0);
int version = bitcoin::PROTOCOL_VERSION;
if (allowSegWit) version |= bitcoin::SERIALIZE_TRANSACTION_USE_WITNESS;
if (allowMW) version |= bitcoin::SERIALIZE_TRANSACTION_USE_MWEB;
bitcoin::GenericVectorWriter<QByteArray> vw(bitcoin::SER_NETWORK, version, buf, from_pos);
thing.Serialize(vw);
return buf;
}
/// Convenience for above -- serialize to a new QByteArray directly
template <typename BitcoinObject>
QByteArray Serialize(const BitcoinObject &thing, bool allowSegWit = false)
QByteArray Serialize(const BitcoinObject &thing, bool allowSegWit = false, bool allowMW = false)
{
QByteArray ret;
Serialize(ret, thing, -1, allowSegWit);
Serialize(ret, thing, -1, allowSegWit, allowMW);
return ret;
}
/// Deserialize to a pre-allocated bitcoin object such as bitcoin::CBlock, bitcoin::CBlockHeader, bitcoin::CMutableTransaction, etc
template <typename BitcoinObject,
/// NB: This in-place Deserialization does *NOT* work with CTransaction because if has const-fields. (use the non-in-place specialization instead)
std::enable_if_t<!std::is_same_v<BitcoinObject, bitcoin::CTransaction>, int> = 0 >
void Deserialize(BitcoinObject &thing, const QByteArray &bytes, int pos = 0, bool allowSegWit = false)
void Deserialize(BitcoinObject &thing, const QByteArray &bytes, int pos = 0, bool allowSegWit = false, bool allowMW = false,
bool throwIfJunkAtEnd = false)
{
const int version = bitcoin::PROTOCOL_VERSION | (allowSegWit ? bitcoin::SERIALIZE_TRANSACTION_USE_WITNESS : 0);
int version = bitcoin::PROTOCOL_VERSION;
if (allowSegWit) version |= bitcoin::SERIALIZE_TRANSACTION_USE_WITNESS;
if (allowMW) version |= bitcoin::SERIALIZE_TRANSACTION_USE_MWEB;
bitcoin::GenericVectorReader<QByteArray> vr(bitcoin::SER_NETWORK, version, bytes, pos);
thing.Unserialize(vr);
if (throwIfJunkAtEnd && !vr.empty())
throw std::ios_base::failure("Got unprocessed bytes at the end when deserializeing a bitcoin object");
}
/// Convenience for above. Create an instance of object and deserialize to it
template <typename BitcoinObject>
BitcoinObject Deserialize(const QByteArray &bytes, int pos = 0, bool allowSegWit = false)
BitcoinObject Deserialize(const QByteArray &bytes, int pos = 0, bool allowSegWit = false, bool allowMW = false,
bool noJunkAtEnd = false)
{
BitcoinObject ret;
Deserialize(ret, bytes, pos, allowSegWit);
Deserialize(ret, bytes, pos, allowSegWit, allowMW, noJunkAtEnd);
return ret;
}
@ -108,20 +118,20 @@ namespace BTC
inline constexpr bool is_block_or_tx_v = is_block_or_tx<BitcoinObject>::value;
/// Template specialization for CTransaction which has const fields and works a little differently (impl. in BTC.cpp)
template <> bitcoin::CTransaction Deserialize(const QByteArray &, int pos, bool allowSegWit);
template <> bitcoin::CTransaction Deserialize(const QByteArray &, int pos, bool allowSegWit, bool allowMW, bool noJunkAtEnd);
/// Convenience to deserialize segwit object (block or tx) (Core only)
template <typename BitcoinObject>
std::enable_if_t<is_block_or_tx_v<BitcoinObject>, BitcoinObject>
/* BitcoinObject */ DeserializeSegWit(const QByteArray &ba, int pos = 0) {
return Deserialize<BitcoinObject>(ba, pos, true);
return Deserialize<BitcoinObject>(ba, pos, true, false);
}
/// Convenience to serialize segwit object (block or tx) (Core only)
template <typename BitcoinObject>
std::enable_if_t<is_block_or_tx_v<BitcoinObject>, QByteArray>
/* QByteArray */ SerializeSegWit(const BitcoinObject &bo, int pos = -1) {
return Serialize<BitcoinObject>(bo, pos, true);
return Serialize<BitcoinObject>(bo, pos, true, false);
}
/// Helper -- returns the size of a block header. Should always be 80. Update this if that changes.
@ -253,3 +263,5 @@ inline bool operator==(const bitcoin::uint256 &hash, const QByteArray &ba) noexc
inline bool operator==(const QByteArray &ba, const bitcoin::uint256 &hash) noexcept { return hash == ba; }
inline bool operator!=(const bitcoin::uint256 &hash, const QByteArray &ba) noexcept { return !(hash == ba); }
inline bool operator!=(const QByteArray &ba, const bitcoin::uint256 &hash) noexcept { return !(hash == ba); }
Q_DECLARE_METATYPE(BTC::Coin);

View file

@ -238,13 +238,13 @@ namespace BTC
/// if isValid: Returns the legacy address string, base58 encoded if legacy==true,
/// otherwise returns the cash address with prefix
/// if !isValid: Returns the empty string.
QString Address::toString(bool legacy) const {
QString Address::toString(bool legacy, std::optional<Byte> verByteOverride) const {
QString ret;
if (isValid()) {
if (legacy) {
std::vector<Byte> vch;
vch.reserve(1 + size_t(h160.size()));
vch.push_back(verByte);
vch.push_back(verByteOverride.value_or(verByte));
vch.insert(vch.end(), h160.begin(), h160.end());
ret = QString::fromStdString(bitcoin::EncodeBase58Check(vch));
} else {
@ -267,6 +267,15 @@ namespace BTC
return ret;
}
QString Address::toLitecoinString() const
{
std::optional<Byte> verByteOverride;
if (_net == Net::MainNet && _kind == Kind::P2PKH)
verByteOverride = Byte{48}; // p2psh on mainnet is the only one that differs for litecoin
return toString(true, verByteOverride);
}
QString Address::toShortString() const
{
QString s = toString(false);

View file

@ -31,6 +31,7 @@
#include <algorithm>
#include <array>
#include <cstring>
#include <optional>
#include <vector>
namespace BTC {
@ -85,7 +86,7 @@ namespace BTC {
/// If isValid, returns the address as a string (either cash address w/ prefix, or legacy address string).
/// Returns an empty QString if !isValid.
QString toString(bool legacy=false) const;
QString toString(bool legacy=false) const { return toString(legacy, std::nullopt); }
/// If isValid, returns the address as a cash address string, without the bitcoincash: or bchtest:, etc, prefix.
QString toShortString() const;
@ -93,6 +94,9 @@ namespace BTC {
/// Alias for toString(true)
QString toLegacyString() const { return toString(true); }
/// Hack to support converting any address to LTC TODO: Proper support for LTC addresses
QString toLitecoinString() const;
Address & operator=(const QString &legacyOrCash) { return (*this = Address::fromString(legacyOrCash)); }
Address & operator=(const char *legacyOrCash) { return (*this = QString(legacyOrCash)); }
Address & operator=(const QByteArray &legacyOrCash) { return (*this = QString(legacyOrCash)); }
@ -124,6 +128,9 @@ namespace BTC {
QByteArray h160;
Kind _kind = Kind::Invalid;
bool autosetKind();
QString toString(bool legacy, std::optional<Byte> verByteOverride) const;
#ifdef ENABLE_TESTS
public:
static bool test();

View file

@ -189,7 +189,7 @@ BitcoinD *BitcoinDMgr::getBitcoinD()
namespace {
struct BitcoinDVersionParseResult {
bool isBchd{}, isCore{}, isBU{}, isBCHN{};
bool isBchd{}, isCore{}, isBU{}, isBCHN{}, isLTC{};
Version version;
constexpr BitcoinDVersionParseResult() noexcept = default;
@ -220,6 +220,7 @@ namespace {
isCore = subversion.startsWith("/Satoshi:");
isBU = subversion.startsWith("/BCH Unlimited:");
isBCHN = subversion.startsWith("/Bitcoin Cash Node:");
isLTC = subversion.startsWith("/LitecoinCore:");
// regular bitcoind, "version" is reliable and always the same format
version = Version::BitcoinDCompact(val);
}
@ -235,7 +236,7 @@ namespace {
// at the time of this writing, released BU is 1.9.0 and it definitely lacks the dsproof RPC
if (isBU && version < Version{1, 9, 1})
return true;
if (isCore) // core will definitely never add this feature
if (isCore || isLTC) // core and/or ltc will definitely never add this feature
return true;
// for all other remote daemons, return false so that calling code will probe.
return false;
@ -270,8 +271,8 @@ void BitcoinDMgr::refreshBitcoinDNetworkInfo()
}
}(bitcoinDInfo.subversion, networkInfo);
// assign to shared object now from stack object BitcoinDVersionParseResult
std::tie(bitcoinDInfo.isBchd, bitcoinDInfo.isCore, bitcoinDInfo.isBU, bitcoinDInfo.version)
= std::tie(res.isBchd, res.isCore, res.isBU, res.version);
std::tie(bitcoinDInfo.isBchd, bitcoinDInfo.isCore, bitcoinDInfo.isBU, bitcoinDInfo.isLTC, bitcoinDInfo.version)
= std::tie(res.isBchd, res.isCore, res.isBU, res.isLTC, res.version);
bitcoinDInfo.relayFee = networkInfo.value("relayfee", 0.0).toDouble();
bitcoinDInfo.warnings = networkInfo.value("warnings", "").toString();
// set quirk flags: requires 0 arg `estimatefee`?
@ -285,7 +286,7 @@ void BitcoinDMgr::refreshBitcoinDNetworkInfo()
return true;
return false;
};
bitcoinDInfo.isZeroArgEstimateFee = !res.isCore && isZeroArgEstimateFee(bitcoinDInfo.version, bitcoinDInfo.subversion);
bitcoinDInfo.isZeroArgEstimateFee = !res.isCore && !res.isLTC && isZeroArgEstimateFee(bitcoinDInfo.version, bitcoinDInfo.subversion);
// Implementations known to lack `getzmqnotifications`:
// - bchd (all versions)
// - BU before version 1.9.1.0
@ -296,7 +297,10 @@ void BitcoinDMgr::refreshBitcoinDNetworkInfo()
bitcoinDInfo.hasDSProofRPC = false;
} // end lock scope
// be sure to announce whether remote bitcoind is bitcoin core (this determines whether we use segwit or not)
emit bitcoinCoreDetection(res.isCore);
BTC::Coin coin = BTC::Coin::BCH; // default BCH if unknown (not segwit)
if (res.isCore) coin = BTC::Coin::BTC; // segwit
else if (res.isLTC) coin = BTC::Coin::LTC; // segwit
emit coinDetected(coin);
// next, be sure to set up the ping time appropriately for bchd vs bitcoind
resetPingTimers(int(res.isBchd ? PingTimes::BCHD : PingTimes::Normal));
// next up, do this query
@ -482,10 +486,10 @@ bool BitcoinDMgr::isZeroArgEstimateFee() const
return bitcoinDInfo.isZeroArgEstimateFee;
}
bool BitcoinDMgr::isBitcoinCore() const
bool BitcoinDMgr::isCoreLike() const
{
std::shared_lock g(bitcoinDInfoLock);
return bitcoinDInfo.isCore;
return bitcoinDInfo.isCore || bitcoinDInfo.isLTC;
}
Version BitcoinDMgr::getBitcoinDVersion() const

View file

@ -50,6 +50,7 @@ struct BitcoinDInfo {
bool isBchd = false; ///< true if remote bitcoind subversion is: /bchd:...
bool isZeroArgEstimateFee = false; ///< true if remote bitcoind expects 0 argument "estimatefee" RPC.
bool isCore = false; ///< true if we are actually connected to /Satoshi.. node (Bitcoin Core)
bool isLTC = false; ///< true if we are actually connected to /LitecoinCore.. node (Litecoin)
bool isBU = false; ///< true if subversion string starts with "/BCH Unlimited:"
bool lacksGetZmqNotifications = false; ///< true if bchd or BU < 1.9.1.0, or if we got an RPC error the last time we queried
bool hasDSProofRPC = false; ///< true if the RPC query to `getdsprooflist` didn't return an error.
@ -119,8 +120,8 @@ public:
/// Thread-safe. Convenient method to avoid an extra copy. Returns getBitcoinDInfo().isZeroArgEstimateFee
bool isZeroArgEstimateFee() const;
/// Thread-safe. Convenient method to avoid an extra copy. Returns getBitcoinDInfo().isCore
bool isBitcoinCore() const;
/// Thread-safe. Convenient method to avoid an extra copy. Returns true iff getBitcoinDInfo().isCore || getBitcoinDInfo().isLTC.
bool isCoreLike() const;
/// Thread-safe. Convenient method to avoid an extra copy. Returns getBitcoinDInfo().version
Version getBitcoinDVersion() const;
@ -139,8 +140,9 @@ signals:
void inWarmUp(const QString &);
/// Emitted as soon as we read the bitcoind subversion. If it starts with /Satoshi:.., we emit this
/// with true, Otherwise, we emit it with false.
void bitcoinCoreDetection(bool);
/// with Coin::BTC, if subversion is /LitecoinCore... we emit Coin::LTC, otherwise, we emit it with
/// Coin::BCH.
void coinDetected(BTC::Coin);
/// Emitted whenever the BitcoinDZmqNotifications change (this is also emitted the first time we retrieve them
/// via getzmqnotifications). Note: this is never emitted if we are compiled without zmq support.

View file

@ -109,7 +109,7 @@ void Controller::startup()
callOnTimerSoon(msgPeriod, waitTimer, []{ Log("Waiting for bitcoind..."); return true; }, false, Qt::TimerType::VeryCoarseTimer);
};
waitForBitcoinD();
conns += connect(bitcoindmgr.get(), &BitcoinDMgr::bitcoinCoreDetection, this, &Controller::on_bitcoinCoreDetection,
conns += connect(bitcoindmgr.get(), &BitcoinDMgr::coinDetected, this, &Controller::on_coinDetected,
/* NOTE --> */ Qt::DirectConnection);
conns += connect(bitcoindmgr.get(), &BitcoinDMgr::allConnectionsLost, this, waitForBitcoinD);
conns += connect(bitcoindmgr.get(), &BitcoinDMgr::allConnectionsLost, this, &Controller::zmqHashBlockStop); // stop zmq unconditionally
@ -209,7 +209,7 @@ void Controller::startup()
bitcoin::CMutableTransaction tx;
constexpr auto kErrLine2 = "Something is wrong with either BitcoinD or our ability to understand its RPC responses.";
try {
BTC::Deserialize(tx, Util::ParseHexFast(resp.result().toByteArray()), 0, isCoinBTC());
BTC::Deserialize(tx, Util::ParseHexFast(resp.result().toByteArray()), 0, isSegWitCoin(), isMimbleWimbleCoin());
} catch (const std::exception &e) {
Error() << "Failed to deserialize tx #1 with txid " << hash.toHex() << ": " << e.what();
Error() << kErrLine2;
@ -268,13 +268,28 @@ void Controller::startup()
conns += connect(this, &Controller::synchronizing, this, [this]{ stopTimer(feeHistogramTimer); });
}
{
// Small utility function to add "ignore" txhashes coming from SynchMempoolTask to our somewhat-persistent
// mempoolIgnoreTxns set (this set is cleared each time the tip changes, but persists across mempool synchs
// for a given tip).
conns += connect(this, &Controller::ignoreMempoolTxn, this, [this] (const QByteArray & txHash) {
if (mempoolIgnoreTxns.insert(txHash).second)
DebugM("Added txHash: ", txHash.toHex(), " to mempool ignore set, set size now: ", mempoolIgnoreTxns.size());
});
// Another small utility function: clears the mempoolIgnoreTxns set whenever we see a new tip
conns += connect(this, &Controller::newHeader, this, [this] {
// we just got to a new tip, clear this (will be repopulated in SynchMempoolTask if need be)
if (!mempoolIgnoreTxns.empty()) DebugM("mempoolIgnoreTxns: ", mempoolIgnoreTxns.size(), Util::Pluralize(" txHash", mempoolIgnoreTxns.size()), " cleared");
mempoolIgnoreTxns.clear();
});
}
start(); // start our thread
}
void Controller::on_bitcoinCoreDetection(bool iscore)
void Controller::on_coinDetected(const BTC::Coin detectedtype)
{
const auto ourtype = coinType.load(std::memory_order_relaxed);
const auto detectedtype = !iscore ? BTC::Coin::BCH : BTC::Coin::BTC;
if (ourtype == BTC::Coin::Unknown) {
// We had no coin set in DB, but we just detected the coin, set it now and return.
// This is the common case with no misconfiguration.
@ -305,10 +320,10 @@ void Controller::on_bitcoinCoreDetection(bool iscore)
"Please either connect to the appropriate bitcoind for this database, or delete\n"
"the datadir and resynch.\n";
} else {
// defensive programming -- should never be reached. This is here in case we
// add new coin types yet we forget to update this code.
Fatal() << "INTERNAL ERROR: Unexpected coin combination: ourtype=" << BTC::coinToName(ourtype)
<< " detectedtype=" << BTC::coinToName(detectedtype) << " -- FIXME!";
// Generic message for LTC/BCH/BTC mismatch.
Fatal() << "Unexpected coin combination: ourtype=" << BTC::coinToName(ourtype)
<< ", rpc daemon's type=" << BTC::coinToName(detectedtype) << ". Are you connected to the"
<< " right node for this " << APPNAME << " database?";
}
}
}
@ -456,6 +471,7 @@ struct DownloadBlocksTask : public CtlTask
std::atomic<size_t> nTx = 0, nIns = 0, nOuts = 0;
const bool allowSegWit; ///< initted in c'tor. If true, deserialize blocks using the optional segwit extensons to the tx format.
const bool allowMimble; ///< like above, but if true we allow mimblewimble (litecoin)
void do_get(unsigned height);
@ -474,7 +490,8 @@ struct DownloadBlocksTask : public CtlTask
DownloadBlocksTask::DownloadBlocksTask(unsigned from, unsigned to, unsigned stride, unsigned nClients, Controller *ctl_)
: CtlTask(ctl_, QStringLiteral("Task.DL %1 -> %2").arg(from).arg(to)), from(from), to(to), stride(stride),
expectedCt(unsigned(nToDL(from, to, stride))), max_q(int(nClients)+1), allowSegWit(ctl_->isCoinBTC())
expectedCt(unsigned(nToDL(from, to, stride))), max_q(int(nClients)+1),
allowSegWit(ctl_->isSegWitCoin()), allowMimble(ctl_->isMimbleWimbleCoin())
{
FatalAssert( (to >= from) && (ctl_) && (stride > 0), "Invalid params to DonloadBlocksTask c'tor, FIXME!");
@ -523,19 +540,47 @@ void DownloadBlocksTask::do_get(unsigned int bnum)
if (bool sizeOk = header.length() == HEADER_SIZE; sizeOk && (chkHash = BTC::HashRev(header)) == hash) {
PreProcessedBlockPtr ppb;
try {
ppb = PreProcessedBlock::makeShared(bnum, size_t(rawblock.size()),
BTC::Deserialize<bitcoin::CBlock>(rawblock, 0, allowSegWit));
const auto cblock = BTC::Deserialize<bitcoin::CBlock>(rawblock, 0, allowSegWit, allowMimble, allowMimble /* thorw if junk at end if Litecoin (catch deser. bugs) */);
ppb = PreProcessedBlock::makeShared(bnum, size_t(rawblock.size()), cblock);
if (allowMimble && Debug::isEnabled()) {
// Litecoin only
bool doSerChk{};
if (cblock.mw_blob) {
const auto n = std::min(cblock.mw_blob->size(), size_t(60));
TraceM("MimbleBlock: ", bnum, ", data_size: ", cblock.mw_blob->size(),
", first ", n, " bytes: ",
Util::ToHexFast(QByteArray::fromRawData(reinterpret_cast<const char *>(cblock.mw_blob->data()), n)));
doSerChk = true;
}
if (cblock.vtx.size() >= 2 && cblock.vtx.back()->mw_blob && cblock.vtx.back()->mw_blob->size() > 1) {
const auto & tx = *cblock.vtx.back();
const auto n = std::min(tx.mw_blob->size(), size_t(60));
// We debug out in Green here to catch this very rare thing which I have never seen before
// to see if it's possible. Someday can demote this to Trace.
Debug(Log::Green) << "MimbleTxn in block: " << bnum << ", hash: " << QString::fromStdString(tx.GetId().ToString())
<< ", data_size: " << tx.mw_blob->size() << ", first " << n << " bytes: "
<< Util::ToHexFast(QByteArray::fromRawData(reinterpret_cast<const char *>(tx.mw_blob->data()), n));
doSerChk = true;
}
// check sanity (debug builds only)
if constexpr (!isReleaseBuild()) {
if (doSerChk && rawblock != BTC::Serialize(cblock, allowSegWit, allowMimble)) {
Fatal() << "Block re-serialized to different data! FIXME!";
return;
}
}
} // /Litecoin only
} catch (const std::ios_base::failure &e) {
// deserialization error -- check if block is segwit and we are not segwit
if (!allowSegWit) {
try {
const auto cblock = BTC::DeserializeSegWit<bitcoin::CBlock>(rawblock);
const auto cblock2 = BTC::DeserializeSegWit<bitcoin::CBlock>(rawblock);
// If we get here the block deserialized ok as segwit but not ok as non-segwit.
// We must assume that there is some misconfiguration e.g. the remote is BTC
// but DB is not expecting BTC. This can happen if user is using non-Satoshi
// bitcoind with BTC. We only support /Satoshi... as uagent for BTC due to the
// way that our auto-detection works.
if (std::any_of(cblock.vtx.begin(), cblock.vtx.end(),
if (std::any_of(cblock2.vtx.begin(), cblock2.vtx.end(),
[](const auto &tx){ return tx->HasWitness(); }))
throw InternalError("SegWit block encountered for non-SegWit coin."
" If you wish to use BTC, please delete the datadir and"
@ -608,8 +653,10 @@ void DownloadBlocksTask::do_get(unsigned int bnum)
/// flag for "has unconfirmed parent tx", and be done with it. Everything else we can calculate.
struct SynchMempoolTask : public CtlTask
{
SynchMempoolTask(Controller *ctl_, std::shared_ptr<Storage> storage, const std::atomic_bool & notifyFlag)
: CtlTask(ctl_, "SynchMempool"), storage(storage), notifyFlag(notifyFlag), isBTC(ctl_->isCoinBTC())
SynchMempoolTask(Controller *ctl_, std::shared_ptr<Storage> storage, const std::atomic_bool & notifyFlag,
const std::unordered_set<TxHash, HashHasher> & ignoreTxns)
: CtlTask(ctl_, "SynchMempool"), storage(storage), notifyFlag(notifyFlag),
txnIgnoreSet(ignoreTxns), isSegWit(ctl_->isSegWitCoin()), isMimble(ctl_->isMimbleWimbleCoin())
{
scriptHashesAffected.reserve(SubsMgr::kRecommendedPendingNotificationsReserveSize);
txidsAffected.reserve(SubsMgr::kRecommendedPendingNotificationsReserveSize);
@ -622,13 +669,16 @@ struct SynchMempoolTask : public CtlTask
bool isdlingtxs = false;
Mempool::TxMap txsNeedingDownload, txsWaitingForResponse;
Mempool::NewTxsMap txsDownloaded;
std::unordered_set<TxHash, HashHasher> txsFailedDownload; ///< set of tx's dropped due to RBF and/or mempool pressure as we were downloading
std::unordered_set<TxHash, HashHasher> txsFailedDownload, ///< set of tx's dropped due to RBF and/or mempool pressure as we were downloading
txsIgnored; ///< Litecoin only -- MWEB-only txns we are completely ignoring.
const std::unordered_set<TxHash, HashHasher> txnIgnoreSet; ///< Litecoin only -- comes from Controller::mempoolIgnoreTxns
unsigned expectedNumTxsDownloaded = 0;
static constexpr int kRedoCtMax = 5; // if we have to retry this many times, error out.
static constexpr unsigned kFailedDownloadMax = 50; // if we have more than this many consecutive failures on getrawtransaction, and no successes, abort with error.
int redoCt = 0;
const bool TRACE = Trace::isEnabled(); // set this to true to print more debug
const bool isBTC; ///< initted in c'tor. If true, deserialize tx's using the optional segwit extensons to the tx format.
const bool isSegWit; ///< initted in c'tor. If true, deserialize tx's using the optional segwit extensons to the tx format.
const bool isMimble; ///< initted in c'tor. If true, deserialize tx's using the optional mimble-wimble extensons to the tx format.
/// The scriptHashes that were affected by this refresh/synch cycle. Used for notifications.
std::unordered_set<HashX, HashHasher> scriptHashesAffected;
@ -640,6 +690,7 @@ struct SynchMempoolTask : public CtlTask
void clear() {
isdlingtxs = false;
txsNeedingDownload.clear(); txsWaitingForResponse.clear(); txsDownloaded.clear(); txsFailedDownload.clear();
txsIgnored.clear();
expectedNumTxsDownloaded = 0;
lastProgress = 0.;
// Note: we don't clear "scriptHashesAffected" intentionally in case we are retrying. We want to accumulate
@ -690,7 +741,7 @@ void SynchMempoolTask::updateLastProgress(std::optional<double> val)
{
double p;
if (val) p = *val;
else p = 0.5 * (txsDownloaded.size() + txsFailedDownload.size()) / std::max(double(expectedNumTxsDownloaded), 1.0);
else p = 0.5 * (txsDownloaded.size() + txsFailedDownload.size() + txsIgnored.size()) / std::max(double(expectedNumTxsDownloaded), 1.0);
lastProgress = std::clamp(p, 0.0, 1.0);
}
@ -768,14 +819,14 @@ void Controller::printMempoolStatusToLog(size_t newSize, size_t numAddresses, do
void SynchMempoolTask::processResults()
{
if (const auto total = txsDownloaded.size() + txsFailedDownload.size(); total != expectedNumTxsDownloaded) {
if (const auto total = txsDownloaded.size() + txsFailedDownload.size() + txsIgnored.size(); total != expectedNumTxsDownloaded) {
Error() << __PRETTY_FUNCTION__ << ": Expected to downlaod " << expectedNumTxsDownloaded << ", instead got "
<< total << ". FIXME!";
emit errored();
return;
} else if (total) {
DebugM("downloaded ", txsDownloaded.size(), " txs (failed: ", txsFailedDownload.size(), "), elapsed so far: ",
elapsed.secsStr(), " secs");
DebugM("downloaded ", txsDownloaded.size(), " txs (failed: ", txsFailedDownload.size(), ", ignored: ",
txsIgnored.size(), "), elapsed so far: ", elapsed.secsStr(), " secs");
}
// precache the confirmed spends with the lock held in shared mode, allowing for concurrency during this slow operation
@ -878,7 +929,32 @@ void SynchMempoolTask::doDLNextTx()
// deserialize tx, catching any deser errors
bitcoin::CMutableTransaction ctx;
try {
ctx = BTC::Deserialize<bitcoin::CMutableTransaction>(txdata, 0, isBTC);
ctx = BTC::Deserialize<bitcoin::CMutableTransaction>(txdata, 0, isSegWit, isMimble, true /* nojunk */);
// Litecoin-only
if (isMimble && ctx.mw_blob && ctx.mw_blob->size() > 1) {
const auto n = std::min(size_t(60), ctx.mw_blob->size());
DebugM("MimbleTxn in mempool: hash: ", tx->hash.toHex(), ", IsWebOnly: ", int(ctx.IsMWEBOnly()),
", vin,vout sizes: [", ctx.vin.size(), ", ", ctx.vout.size(), "]", ", data_size: ",
ctx.mw_blob->size(), ", first ", n, " bytes: ",
Util::ToHexFast(QByteArray::fromRawData(reinterpret_cast<const char *>(ctx.mw_blob->data()), n)),
", nLockTime: ", ctx.nLockTime);
// Litecoin only -- discard MWEB-only txns (they are useless to us for now)
if (ctx.IsMWEBOnly()) {
// Ignore MWEB-only txns completely:
// - their txid is weird and hard to calculate for us (requires blake3 hasher, which we lack)
// - they contain empty vins and vouts, and since Electrum-LTC doesn't grok MWEB, we cannot do anything
// with their spend info anyway.
DebugM("Ignoring MWEB-only txn: ", tx->hash.toHex());
// mark this as "ignored"
emit ctl->ignoreMempoolTxn(tx->hash); // tell Controller in a thread-safe way to remember this across SynchMempoolTask invocations
txsIgnored.insert(tx->hash);
txsWaitingForResponse.erase(tx->hash);
// keep going
updateLastProgress();
AGAIN();
return;
}
}
} catch (const std::exception &e) {
Error() << "Error deserializing tx: " << tx->hash.toHex() << ", exception: " << e.what();
emit errored();
@ -924,7 +1000,7 @@ void SynchMempoolTask::doDLNextTx()
// that there was some RBF action if on BTC, or the tx happened to drop out of mempool due to mempool pressure.
// Since bitcoind doesn't have the tx -- then it and its children will also fail, which is fine. It's as if
// it never existed and as if we never got it in the original list from `getrawmempool`!
const auto *const pre = isBTC ? "Tx dropped out of mempool (possibly due to RBF)" : "Tx dropped out of mempool";
const auto *const pre = isSegWit ? "Tx dropped out of mempool (possibly due to RBF)" : "Tx dropped out of mempool";
Warning() << pre << ": " << QString(hashHex) << " (error response: " << resp.errorMessage()
<< "), ignoring mempool tx ...";
txsFailedDownload.insert(tx->hash);
@ -945,7 +1021,7 @@ void SynchMempoolTask::doDLNextTx()
void SynchMempoolTask::doGetRawMempool()
{
submitRequest("getrawmempool", {false}, [this](const RPC::Message & resp){
std::size_t newCt = 0, droppedCt = 0;
std::size_t newCt = 0, droppedCt = 0, ignoredCt = 0;
const QVariantList txidList = resp.result().toList();
Mempool::TxHashSet droppedTxs;
{
@ -967,13 +1043,20 @@ void SynchMempoolTask::doGetRawMempool()
droppedTxs.erase(it); // mark this tx as "not dropped" since it was in the mempool before and is in the mempool now.
if (TRACE) Debug() << "Existing mempool tx: " << hash.toHex();
} else {
if (TRACE) Debug() << "New mempool tx: " << hash.toHex();
++newCt;
Mempool::TxRef tx = std::make_shared<Mempool::Tx>();
tx->hashXs.max_load_factor(.9); // hopefully this will save some memory by expicitly setting max table size to 90%
tx->hash = hash;
// Note: we end up calculating the fee ourselves since I don't trust doubles here. I wish bitcoind would have returned sats.. :(
txsNeedingDownload[hash] = tx;
if (txnIgnoreSet.find(hash) != txnIgnoreSet.end()) {
// suppressed txn (in ignore set)
if (TRACE) Debug() << "Ignored mempool tx: " << hash.toHex();
++ignoredCt;
} else {
// new txn
if (TRACE) Debug() << "New mempool tx: " << hash.toHex();
++newCt;
Mempool::TxRef tx = std::make_shared<Mempool::Tx>();
tx->hashXs.max_load_factor(.9); // hopefully this will save some memory by expicitly setting max table size to 90%
tx->hash = hash;
// Note: we end up calculating the fee ourselves since I don't trust doubles here. I wish bitcoind would have returned sats.. :(
txsNeedingDownload[hash] = tx;
}
}
}
if (!droppedTxs.empty()) {
@ -1009,7 +1092,7 @@ void SynchMempoolTask::doGetRawMempool()
// . <--- NB: at this point: affected and res.dspsTxsAffected are moved-from
}
if (UNLIKELY(droppedCt != expectedDropCt)) { // This invariant is checked to detect bugs.
Warning() << "Synch mempool expected to drop " << droppedTxs.size() << ", but in fact dropped "
Warning() << "Synch mempool expected to drop " << expectedDropCt << ", but in fact dropped "
<< droppedCt << " -- retrying getrawmempool";
redoFromStart(); // set state such that the next process() call will do getrawmempool again unless redoCt exceeds kRedoCtMax, in which case errors out
return;
@ -1017,7 +1100,8 @@ void SynchMempoolTask::doGetRawMempool()
}
if (newCt || droppedCt)
DebugM(resp.method, ": got reply with ", txidList.size(), " items, ", droppedCt, " dropped, ", newCt, " new");
DebugM(resp.method, ": got reply with ", txidList.size(), " items, ", ignoredCt, " ignored, ",
droppedCt, " dropped, ", newCt, " new");
isdlingtxs = true;
expectedNumTxsDownloaded = unsigned(newCt);
txsDownloaded.reserve(expectedNumTxsDownloaded);
@ -1089,7 +1173,7 @@ unsigned Controller::downloadTaskRecommendedThrottleTimeMsec(unsigned bnum) cons
// mainnet
if (bnum > 150'000) // beyond this height the blocks are starting to be big enough that we want to not eat memory.
maxBackLog = 250;
else if (bnum > 550'000 && !isCoinBTC()) // beyond this height we may start to see 32MB blocks in the future
else if (bnum > 550'000 && !isSegWitCoin()) // beyond this height we may start to see 32MB blocks in the future
maxBackLog = 100;
} else if (sm->net == BTC::ScaleNet) {
if (bnum > 10'000)
@ -1099,7 +1183,7 @@ unsigned Controller::downloadTaskRecommendedThrottleTimeMsec(unsigned bnum) cons
} else {
// testnet
if (bnum > 1'300'000) // beyond this height 32MB blocks may be common, esp. in the future
maxBackLog = isCoinBTC() ? 250 : 100;
maxBackLog = isSegWitCoin() ? 250 : 100;
}
const int diff = int(bnum) - int(sm->ppBlkHtNext.load()); // note: ppBlkHtNext is not guarded by the lock but it is an atomic value, so that's fine.
@ -1386,7 +1470,7 @@ void Controller::process(bool beSilentIfUpToDate)
emit synchFailure();
} else if (sm->state == State::SynchMempool) {
// ...
auto task = newTask<SynchMempoolTask>(true, this, storage, masterNotifySubsFlag);
auto task = newTask<SynchMempoolTask>(true, this, storage, masterNotifySubsFlag, mempoolIgnoreTxns);
task->threadObjectDebugLifecycle = Trace::isEnabled(); // suppress verbose lifecycle prints unless trace mode
connect(task, &CtlTask::success, this, [this, task]{
if (UNLIKELY(!sm || isTaskDeleted(task) || sm->state != State::SynchingMempool))
@ -1908,7 +1992,7 @@ void Controller::zmqHashBlockStop()
}
// --- Debug dump support
void Controller::dumpScriptHashes(const QString &fileName) const
void Controller::dumpScriptHashes(const QString &fileName)
{
if (!storage)
throw InternalError("Dump: Storage is not started");

View file

@ -33,6 +33,7 @@
#include <shared_mutex>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#include <vector>
class CtlTask;
@ -66,8 +67,14 @@ public:
/// for debug printing when it receives new mempool tx's.
static void printMempoolStatusToLog(size_t newSize, size_t numAddresses, double msec, bool useDebugLogger, bool force = false);
/// Thread-safe, lock-free
bool isCoinBTC() const { return coinType.load(std::memory_order_relaxed) == BTC::Coin::BTC; }
/// Thread-safe, lock-free, returns true for BTC and LTC
bool isSegWitCoin() const {
auto const c = coinType.load(std::memory_order_relaxed);
return c == BTC::Coin::BTC || c == BTC::Coin::LTC;
}
/// Thread-safe, lock-free, returns true for LTC
bool isMimbleWimbleCoin() const { return coinType.load(std::memory_order_relaxed) == BTC::Coin::LTC; }
signals:
/// Emitted whenever bitcoind is detected to be up-to-date, and everything is synched up.
@ -92,7 +99,12 @@ signals:
void putBlock(CtlTask *sender, PreProcessedBlockPtr);
/// Emitted only iff the user specified --dump-sh on the CLI. This is emitted once the script hash dump has completed.
void dumpScriptHashesComplete() const;
void dumpScriptHashesComplete();
/// "Private" signal, not intended to be used by outside code. Used internally to mark a txHash as to be "ignored"
/// and not downloaded from the mempool. The private SynchMempoolTask emits this to add hashes to our mempoolIgnoreTxns
/// set in a thread-safe manner
void ignoreMempoolTxn(const QByteArray & txhash);
protected:
Stats stats() const override; // from StatsMixin
@ -111,9 +123,9 @@ protected slots:
/// the supplied block was the next one by height).
void on_putBlock(CtlTask *, PreProcessedBlockPtr);
/// Slot for the BitcoinDMgr::bitcoinCoreDetection. This is compared to coinIsBTC and if there is a mismatch there,
/// we may end up aborting the app and logging an error in this slot.
void on_bitcoinCoreDetection(bool); //< NB: Connected via DirectConnection an may run in the BitcoinDMgr thread!
/// Slot for the BitcoinDMgr::bitcoinCoreDetection. This is compared to this->coinType and if there is a
/// mismatch there, we may end up aborting the app and logging an error in this slot.
void on_coinDetected(BTC::Coin); //< NB: Connected via DirectConnection and may run in the BitcoinDMgr thread!
private:
friend class CtlTask;
@ -173,7 +185,7 @@ private:
void printMempoolStatusToLog() const;
/// If --dump-sh was specified on CLI, this will execute at startup() time right after storage has been loaded. May throw.
void dumpScriptHashes(const QString &fileName) const;
void dumpScriptHashes(const QString &fileName);
/// Will be nullptr if zmq disabled or bitcoind lacks a "hashblock" endpoint
std::unique_ptr<ZmqSubNotifier> zmqHashBlockNotifier;
@ -188,6 +200,12 @@ private:
/// (re)starts listening for notifications from the zmqHashBlockNotifier; called if we received a valid zmq address
/// from BitcoinDMgr, after servers are started.
void zmqHashBlockStart();
/// Litecoin only: Ignore these txhashes from mempool (don't download them). This gets cleared each time
/// before the first SynchMempool after we receive a new block, then is persisted for all the SynchMempools
/// for that block, until a new block arrives, then is cleared again.
std::unordered_set<TxHash, HashHasher> mempoolIgnoreTxns;
private slots:
/// Stops the zmqHashBlockNotifier; called if we received an empty hashblock endpoint address from BitcoinDMgr or
/// when all connections to bitcoind are lost

View file

@ -364,11 +364,9 @@ ServerBase::ServerBase(SrvMgr *sm,
if (!options || !storage || !bitcoindmgr)
// defensive programming
throw BadArgs("ServerBase cannot be constructed with nullptr arguments!");
// setup the isBTC flag -- node that assumption is that storage was aleady setup properly
if (const auto coin = BTC::coinFromName(storage->getCoin()); coin == BTC::Coin::Unknown)
// setup this->coin flag -- note that assumption is that storage was aleady setup properly
if ((coin = BTC::coinFromName(storage->getCoin())) == BTC::Coin::Unknown)
throw InternalError("ServerBase cannot be constructed without a valid \"Coin\" in the database!");
else
isBTC = coin == BTC::Coin::BTC;
}
ServerBase::~ServerBase() { stop(); }
@ -926,15 +924,17 @@ void Server::rpc_server_add_peer(Client *c, const RPC::BatchId batchId, const RP
namespace {
// In case the donation address was default, we transform it correctly to the correct network in the hopes
// that the author of this software (me) might get some BTC and/or BCH appropriately.
QString transformDefaultDonationAddressToBTCOrBCH(const Options &options, bool isBTC)
QString transformDefaultDonationAddressToBTCOrBCHOrLTC(const Options &options, bool isNonBCH, bool isLTC)
{
QString ret = options.donationAddress;
if (!options.isDefaultDonationAddress) return ret; // do nothing if it wasn't the default.
if (!ret.isEmpty()) {
try {
const BTC::Address addr(ret);
if (addr.isValid())
ret = addr.toString(isBTC /* if BTC, then legacy, otherwise cashaddr */);
if (addr.isValid()) {
if (isLTC) ret = addr.toLitecoinString();
else ret = addr.toString(isNonBCH /* if !BCH, then legacy, otherwise cashaddr */);
}
} catch (...) {}
}
return ret;
@ -1001,7 +1001,7 @@ void Server::rpc_server_banner(Client *c, const RPC::BatchId batchId, const RPC:
const auto bitcoinDInfo = bitcoindmgr->getBitcoinDInfo();
generic_do_async(c, batchId, m.id,
[bannerFile,
donationAddress = transformDefaultDonationAddressToBTCOrBCH(*options, isBTC),
donationAddress = transformDefaultDonationAddressToBTCOrBCHOrLTC(*options, isNonBCH(), isLTC()),
daemonVersion = bitcoinDInfo.version,
daemonSubversion = bitcoinDInfo.subversion] {
QVariant ret;
@ -1024,7 +1024,7 @@ void Server::rpc_server_banner(Client *c, const RPC::BatchId batchId, const RPC:
}
void Server::rpc_server_donation_address(Client *c, const RPC::BatchId batchId, const RPC::Message &m)
{
emit c->sendResult(batchId, m.id, transformDefaultDonationAddressToBTCOrBCH(*options, isBTC));
emit c->sendResult(batchId, m.id, transformDefaultDonationAddressToBTCOrBCHOrLTC(*options, isNonBCH(), isLTC()));
}
/* static */
QVariantMap Server::makeFeaturesDictForConnection(AbstractConnection *c, const QByteArray &genesisHash, const Options &opts, bool dsproof)
@ -1287,7 +1287,8 @@ void Server::rpc_blockchain_estimatefee(Client *c, const RPC::BatchId batchId, c
if (!bitcoindmgr->isZeroArgEstimateFee())
params.push_back(unsigned(n));
if (bitcoindmgr->isBitcoinCore() && bitcoindmgr->getBitcoinDVersion() >= Version{0,17,0}) {
if ((bitcoindmgr->isCoreLike())
&& bitcoindmgr->getBitcoinDVersion() >= Version{0,17,0}) {
// Bitcoin Core removed the "estimatefee" RPC method entirely in version 0.17.0, in favor of "estimatesmartfee"
generic_async_to_bitcoind(c, batchId, m.id, "estimatesmartfee", params, [](const RPC::Message &response){
// We don't validate what bitcoind returns. Sometimes if it has not enough information, it may
@ -1339,9 +1340,9 @@ void Server::rpc_blockchain_relayfee(Client *c, const RPC::BatchId batchId, cons
// below for boilerplate checking & parsing.
HashX Server::parseFirstAddrParamToShCommon(const RPC::Message &m, QString *addrStrOut) const
{
if (isBTC)
// unsupported on BTC (for now)
throw RPCError("blockchain.address.* methods are not available on BTC", RPC::ErrorCodes::Code_MethodNotFound);
if (isNonBCH())
// unsupported on non-BCH (for now)
throw RPCError("blockchain.address.* methods are only available on BCH", RPC::ErrorCodes::Code_MethodNotFound);
const auto net = srvmgr->net();
if (UNLIKELY(net == BTC::Net::Invalid))
// This should never happen in practice, but it pays to be paranoid.
@ -1702,9 +1703,12 @@ void Server::rpc_blockchain_transaction_broadcast(Client *c, const RPC::BatchId
// https://github.com/Electron-Cash/electrumx/blob/fbd00416d804c286eb7de856e9399efb07a2ceaf/electrumx/server/session.py#L1526
// https://github.com/Electron-Cash/electrumx/blob/fbd00416d804c286eb7de856e9399efb07a2ceaf/electrumx/lib/coins.py#L397
QString clientName, website;
if (isBTC) {
if (coin == BTC::Coin::BTC) {
clientName = "Electrum";
website = "https://electrum.org/";
} else if (coin == BTC::Coin::LTC) {
clientName = "Electrum-LTC";
website = "https://electrum-ltc.org/";
} else {
clientName = "Electron Cash";
website = "https://electroncash.org/";
@ -1926,7 +1930,7 @@ void Server::rpc_blockchain_transaction_unsubscribe(Client *c, const RPC::BatchI
// DSPROOF
void Server::rpc_blockchain_transaction_dsproof_get(Client *c, const RPC::BatchId batchId, const RPC::Message &m)
{
if (isBTC || !bitcoindmgr->hasDSProofRPC())
if (isNonBCH() || !bitcoindmgr->hasDSProofRPC())
throw RPCError("This server lacks dsproof support", RPC::ErrorCodes::Code_MethodNotFound);
const auto dspid_or_txid = parseFirstHashParamCommon(m, "Invalid dsp hash or tx hash");
generic_do_async(c, batchId, m.id, [this, dspid_or_txid] {
@ -1942,7 +1946,7 @@ void Server::rpc_blockchain_transaction_dsproof_get(Client *c, const RPC::BatchI
}
void Server::rpc_blockchain_transaction_dsproof_list(Client *c, const RPC::BatchId batchId, const RPC::Message &m)
{
if (isBTC || !bitcoindmgr->hasDSProofRPC())
if (isNonBCH() || !bitcoindmgr->hasDSProofRPC())
throw RPCError("This server lacks dsproof support", RPC::ErrorCodes::Code_MethodNotFound);
generic_do_async(c, batchId, m.id, [this] {
DSProof::TxHashSet allDescendants;
@ -1962,14 +1966,14 @@ void Server::rpc_blockchain_transaction_dsproof_list(Client *c, const RPC::Batch
}
void Server::rpc_blockchain_transaction_dsproof_subscribe(Client *c, const RPC::BatchId batchId, const RPC::Message &m)
{
if (isBTC || !bitcoindmgr->hasDSProofRPC())
if (isNonBCH() || !bitcoindmgr->hasDSProofRPC())
throw RPCError("This server lacks dsproof support", RPC::ErrorCodes::Code_MethodNotFound);
const auto txid = parseFirstHashParamCommon(m, "Invalid tx hash");
impl_generic_subscribe(storage->dspSubs(), c, batchId, m, txid);
}
void Server::rpc_blockchain_transaction_dsproof_unsubscribe(Client *c, const RPC::BatchId batchId, const RPC::Message &m)
{
if (isBTC || !bitcoindmgr->hasDSProofRPC())
if (isNonBCH() || !bitcoindmgr->hasDSProofRPC())
throw RPCError("This server lacks dsproof support", RPC::ErrorCodes::Code_MethodNotFound);
const auto txid = parseFirstHashParamCommon(m, "Invalid tx hash");
impl_generic_unsubscribe(storage->dspSubs(), c, batchId, m, txid);

View file

@ -302,9 +302,12 @@ protected:
/// "ws" & "wss" config file options and/or the --ws/--wss (-w/-W) CLI args.
bool usesWS = false;
/// If true we are on the BTC chain. This is set on construction by querying Storage. Subclasses may use this
/// information at runtime to present RPC behavior differences between BTC vs BCH (e.g. in the address_* RPCs).
bool isBTC = false;
/// This is set on construction by querying Storage. Subclasses may use this information at runtime to present
/// RPC behavior differences between BTC vs BCH vs LTC (e.g. in the address_* RPCs).
BTC::Coin coin = BTC::Coin::Unknown;
/// If true we are on the BTC or LTC chains.
bool isNonBCH() const { return coin != BTC::Coin::BCH; }
bool isLTC() const { return coin == BTC::Coin::LTC; }
};
/// Implements the ElectrumX/ElectronX JSON-RPC protocol, version 1.4.4.

View file

@ -10,6 +10,8 @@
#include "serialize.h"
#include "uint256.h"
#include <utility>
namespace bitcoin {
/**
* Nodes collect new transactions into a block, hash them into a hash tree, and
@ -64,6 +66,9 @@ public:
// network and disk
std::vector<CTransactionRef> vtx;
/// Litecoin only
litecoin_bits::MimbleBlobPtr mw_blob;
// memory only
mutable bool fChecked;
@ -80,11 +85,23 @@ public:
inline void SerializationOp(Stream &s, Operation ser_action) {
READWRITEAS(CBlockHeader, *this);
READWRITE(vtx);
// Litecoin only -- Deserialize the mimble-wimble blob at the end under certain conditions (post-activation).
if constexpr (ser_action.ForRead()) mw_blob.reset();
if (s.GetVersion() & SERIALIZE_TRANSACTION_USE_MWEB && vtx.size() >= 2 && vtx.back()->IsHogEx()) {
if constexpr (ser_action.ForRead()) {
mw_blob = litecoin_bits::EatBlockMimbleBlob(s);
} else {
if (mw_blob) {
s.write(reinterpret_cast<const char *>(std::as_const(*mw_blob).data()), mw_blob->size());
}
}
}
}
void SetNull() {
CBlockHeader::SetNull();
vtx.clear();
mw_blob.reset();
fChecked = false;
}

View file

@ -0,0 +1,68 @@
//
// Fulcrum - A fast & nimble SPV Server for Bitcoin Cash
// Copyright (C) 2022 Calin A. Culianu <calin.culianu@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program (see LICENSE.txt). If not, see
// <https://www.gnu.org/licenses/>.
//
#pragma once
#include <memory>
#include <utility>
namespace bitcoin {
/// std::unique_ptr work-alike that is copy-constructible and copy-assignable (does a deep copy)
/// Also can be copy/move-constructed or copy/move-assigned from a T.
template <typename T>
class CopyablePtr {
std::unique_ptr<T> p{};
public:
using element_type = T;
constexpr CopyablePtr() noexcept = default;
explicit CopyablePtr(const T & t) { *this = t; }
explicit CopyablePtr(T && t) { *this = t; }
CopyablePtr(const CopyablePtr & o) { *this = o; }
CopyablePtr(CopyablePtr && o) = default;
CopyablePtr(std::unique_ptr<T> && u) : p{std::move(u)} {}
template <typename ...Args>
static CopyablePtr Make(Args && ...args) { return CopyablePtr(std::make_unique<T>(std::forward<Args>(args)...)); }
CopyablePtr & operator=(const CopyablePtr & o) {
if (o.p) p = std::make_unique<T>(*o.p);
else p.reset();
return *this;
}
CopyablePtr & operator=(CopyablePtr && o) = default;
CopyablePtr & operator=(const T & t) { p = std::make_unique<T>(t); return *this; }
CopyablePtr & operator=(T && t) { p = std::make_unique<T>(std::move(t)); return *this; }
operator bool() const { return static_cast<bool>(p); }
T & operator*() { return *p; }
const T & operator*() const { return *p; }
T * get() { return p.get(); }
const T * get() const { return p.get(); }
T * operator->() { return p.operator->(); }
const T * operator->() const { return p.operator->(); }
void reset(T * t = nullptr) { p.reset(t); }
T * release() { return p.release(); }
};
} // namespace bitcoin

205
src/bitcoin/litecoin_bits.h Normal file
View file

@ -0,0 +1,205 @@
//
// Fulcrum - A fast & nimble SPV Server for Bitcoin Cash
// Copyright (C) 2022 Calin A. Culianu <calin.culianu@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program (see LICENSE.txt). If not, see
// <https://www.gnu.org/licenses/>.
//
#pragma once
// This file contains some helpers used mainly to grok litecoin
// mimble-wimble data in transactions and blocks.
#include "copyable_ptr.h"
#include "serialize.h"
#include "streams.h"
#include <limits>
namespace bitcoin {
namespace litecoin_bits {
using MimbleBlobPtr = CopyablePtr<std::vector<uint8_t>>;
namespace detail {
template <typename Stream>
struct Eater {
Stream & s;
MimbleBlobPtr::element_type & data;
using value_type = MimbleBlobPtr::element_type::value_type;
static_assert(sizeof(value_type) == 1 && std::is_trivial_v<value_type>);
Eater(Stream & s_, MimbleBlobPtr::element_type & data_) : s(s_), data(data_) {}
const value_type * RdWr(size_t nbytes) {
const auto pos = data.size();
if (nbytes) {
data.resize(pos + nbytes);
s.read(reinterpret_cast<char *>(data.data()) + pos, nbytes);
}
return std::as_const(data).data() + pos;
}
uint64_t RdWrSz() {
const uint64_t sz = ReadCompactSize(s);
GenericVectorWriter vw(s.GetType(), s.GetVersion(), data, data.size());
WriteCompactSize(vw, sz); // put it to output buf
return sz;
}
uint64_t RdWrByteVector() {
const auto nbytes = RdWrSz();
RdWr(nbytes);
return nbytes;
}
uint64_t RdWrVarInt() {
const auto vi = ReadVarInt<Stream, uint64_t>(s);
GenericVectorWriter vw(s.GetType(), s.GetVersion(), data, data.size());
WriteVarInt(vw, vi);
return vi;
}
int32_t RdWrHeight() {
const auto nHeight = RdWrVarInt();
if (nHeight > std::numeric_limits<int32_t>::max() || int32_t(nHeight) < 0)
throw std::ios_base::failure("mimble: Height is out of range");
return nHeight;
}
int64_t RdWrAmount() {
const auto nAmount = RdWrVarInt();
if (nAmount > std::numeric_limits<int64_t>::max() || int64_t(nAmount) < 0)
throw std::ios_base::failure("mimble: Amount is out of range");
return nAmount;
}
void EatTxBody() {
uint64_t n;
// read std::vector<Input>
n = RdWrSz(); // compactSize
for (uint64_t i = 0; i < n; ++i) {
// class Input
const auto features = *RdWr(1); // 1 byte features
RdWr(32 + 33 + 33); // 32-byte outputID + 33-byte commitment + 33-byte output public key
if (features & 0x1) {
RdWr(33); // optional input public key
}
if (features & 0x2) { // extraData (variable size vector of uint8_t)
RdWrByteVector();
}
RdWr(64); // 64-byte signature
}
// read std::vector<Output>
n = RdWrSz(); // compactSize
for (uint64_t i = 0; i < n; ++i) {
// class Output
RdWr(33 + 33 + 33); // 33-byte commitment + 33-byte sender public key + 33-byte receiver public key
{ // class OutputMessage
const auto features = *RdWr(1);
if (features & 0x1) { // standard fields feature bit
RdWr(33 + 1 + 8 + 16); // 33-byte key_exchange_pubkey + 1 byte view_tag + 8-byte masked_value + 16-byte masked_nonce
}
if (features & 0x2) { // extended data feature bit
RdWrByteVector();
}
}
RdWr(675 + 64); // 675-byte RangeProof + 64-byte signature
}
// read std::vector<Kernel>
n = RdWrSz();
for (uint64_t i = 0; i < n; ++i) {
// class Kernel
const auto features = *RdWr(1);
if (features & 0x1) RdWrAmount(); // FEE_FEATURE_BIT
if (features & 0x2) RdWrAmount(); // PEGIN_FEATURE_BIT
if (features & 0x4) { // PEGOUT_FEATURE_BIT
// std::vector<PegOutCoin>
const auto n2 = RdWrSz();
for (uint64_t j = 0; j < n2; ++j) {
// class PegOutCoin
RdWrAmount(); // amount
const bool spkSz = RdWrByteVector(); // scriptPubKey
if (!spkSz) throw std::ios_base::failure("mimble: Pegout scriptPubKey must not be empty");
}
}
if (features & 0x8) { // HEIGHT_LOCK_FEATURE_BIT
RdWrHeight();
}
if (features & 0x10) { // STEALTH_EXCESS_FEATURE_BIT
RdWr(33); // 33-byte public key
}
if (features & 0x20) { // EXTRA_DATA_FEATURE_BIT
RdWrByteVector(); // extraData (variable length byte vector)
}
RdWr(33 + 64); // 33-byte commitment + 64-byte signature
}
}
}; // struct Eater
} // namespace detail
/// Eat the mimblewimble data from stream s for a tx, returning a byte blob (as an MwTxBlobPtr)
template <typename TxType, typename Stream>
MimbleBlobPtr EatTxMimbleBlob(const TxType &tx, Stream &s) {
MimbleBlobPtr ret = MimbleBlobPtr::Make();
auto & data = *ret;
detail::Eater eater(s, data);
// read the opt byte, if 0, return the 1-byte buffer
if (*eater.RdWr(1) == 0) {
if (tx.vout.empty()) {
/* It's illegal to include a HogEx with no outputs. */
throw std::ios_base::failure("mimble: Missing HogEx output");
}
return ret;
}
eater.RdWr(32 + 32); // 32-bytes for kernelOffset BlindingFactor + 32-bytes for stealthOffset BlindingFactor
eater.EatTxBody();
return ret;
}
template <typename Stream>
MimbleBlobPtr EatBlockMimbleBlob(Stream &s) {
MimbleBlobPtr ret = MimbleBlobPtr::Make();
auto & data = *ret;
detail::Eater eater(s, data);
// read the opt byte, if 0, return the 1-byte buffer
if (*eater.RdWr(1) == 0) {
return ret;
}
// class Header
eater.RdWrHeight(); // m_height
eater.RdWr(32 * 5); // 32-bytes each for: outputRoot, kernelRoot, leafsetRoot, kernelOffset, stealthOffset
eater.RdWrVarInt(); // m_outputMMRSize
eater.RdWrVarInt(); // m_kernelMMRSize
// TxBody
eater.EatTxBody();
return ret;
}
} // namespace litecoin_bits
} // namespace bitcoin

View file

@ -72,7 +72,7 @@ CMutableTransaction::CMutableTransaction()
: nVersion(CTransaction::CURRENT_VERSION), nLockTime(0) {}
CMutableTransaction::CMutableTransaction(const CTransaction &tx)
: nVersion(tx.nVersion), vin(tx.vin), vout(tx.vout),
nLockTime(tx.nLockTime) {}
nLockTime(tx.nLockTime), mw_blob(tx.mw_blob) {}
static uint256 ComputeCMutableTransactionHash(const CMutableTransaction &tx) {
return SerializeHash(tx, SER_GETHASH, 0);
@ -108,13 +108,13 @@ uint256 CTransaction::ComputeWitnessHash() const {
*/
CTransaction::CTransaction()
: nVersion(CTransaction::CURRENT_VERSION), vin(), vout(), nLockTime(0),
hash() {}
mw_blob{}, hash() {}
CTransaction::CTransaction(const CMutableTransaction &tx)
: nVersion(tx.nVersion), vin(tx.vin), vout(tx.vout),
nLockTime(tx.nLockTime), hash(ComputeHash()) {}
nLockTime(tx.nLockTime), mw_blob(tx.mw_blob), hash(ComputeHash()) {}
CTransaction::CTransaction(CMutableTransaction &&tx)
: nVersion(tx.nVersion), vin(std::move(tx.vin)), vout(std::move(tx.vout)),
nLockTime(tx.nLockTime), hash(ComputeHash()) {}
nLockTime(tx.nLockTime), mw_blob(std::move(tx.mw_blob)), hash(ComputeHash()) {}
Amount CTransaction::GetValueOut() const {
Amount nValueOut = Amount::zero();

View file

@ -9,15 +9,17 @@
#include "amount.h"
#include "feerate.h"
#include "txid.h"
#include "litecoin_bits.h"
#include "script.h"
#include "serialize.h"
#include "txid.h"
#ifdef USE_QT_IN_BITCOIN
#include <QString>
#endif
#include <algorithm>
#include <utility>
#ifdef __clang__
#pragma clang diagnostic push
@ -39,6 +41,10 @@ inline constexpr int SERIALIZE_TRANSACTION = 0x00;
// Added by Calin, imported from Core. Note in Core this flag has the *opposite* meaning
// (there it is called SERIALIZE_TRANSACTION_NO_WITNESS
inline constexpr int SERIALIZE_TRANSACTION_USE_WITNESS = 0x40000000;
// Added by Calin, imported from Litecoin. Note in Litecoin this flag has the *opposite* meaning
// (there it is called SERIALIZE_NO_MWEB
inline constexpr int SERIALIZE_TRANSACTION_USE_MWEB = 0x20000000;
static_assert (sizeof(int) >= 4);
/**
@ -257,16 +263,20 @@ class CMutableTransaction;
* - std::vector<CTxOut> vout
* - if (flags & 1):
* - CTxWitness wit;
* - if (flags & 8):
* - MWEB::Tx (Litecoin only)
* - uint32_t nLockTime
*/
template <typename Stream, typename TxType>
inline void UnserializeTransaction(TxType &tx, Stream &s) {
// MODIFIED BY CALIN -- To allow for deserializing SegWit blocks
// MODIFIED BY CALIN -- To allow for deserializing SegWit blocks as well as LTC MimbleWimble data
const bool fAllowWitness = s.GetVersion() & SERIALIZE_TRANSACTION_USE_WITNESS;
const bool fAllowMimble = s.GetVersion() & SERIALIZE_TRANSACTION_USE_MWEB;
unsigned char flags = 0;
s >> tx.nVersion;
tx.vin.clear();
tx.vout.clear();
tx.mw_blob.reset();
/* Try to read the vin. In case the dummy is there, this will be read as an
* empty vector. */
s >> tx.vin;
@ -294,6 +304,13 @@ inline void UnserializeTransaction(TxType &tx, Stream &s) {
throw std::ios_base::failure("Superfluous witness record");
}
}
if (flags & 0x8 && fAllowMimble) {
/* mweb data, Litecoin only. We don't really process it, we just slurp up the binarry data for eventual
serialization later. */
flags ^= 0x8;
tx.mw_blob = litecoin_bits::EatTxMimbleBlob(tx, s);
}
if (flags) {
/* Unknown flag in the serialization */
throw std::ios_base::failure("Unknown transaction optional data");
@ -308,8 +325,9 @@ inline void SerializeTransaction(const TxType &tx, Stream &s) {
s << tx.vin;
s << tx.vout;
s << tx.nLockTime;*/
// MODIFIED BY CALIN: Allow for SegWit support to be able to sync to BTC
// MODIFIED BY CALIN: Allow for SegWit support to be able to sync to BTC, as well as MimbleWimble for LTC
const bool fAllowWitness = s.GetVersion() & SERIALIZE_TRANSACTION_USE_WITNESS;
const bool fAllowMimble = s.GetVersion() & SERIALIZE_TRANSACTION_USE_MWEB;
s << tx.nVersion;
unsigned char flags = 0;
@ -320,6 +338,12 @@ inline void SerializeTransaction(const TxType &tx, Stream &s) {
flags |= 1;
}
}
if (fAllowMimble) {
// Litecoin only
if (tx.mw_blob && !tx.mw_blob->empty()) {
flags |= 0x8;
}
}
if (flags) {
/* Use extended format in case witnesses are to be serialized. */
const std::vector<CTxIn> vinDummy;
@ -332,6 +356,10 @@ inline void SerializeTransaction(const TxType &tx, Stream &s) {
for (const auto &txin : tx.vin)
s << txin.scriptWitness.stack;
}
if (flags & 8) {
// Litecoin only: write mimble-wimble data blob back out
s.write(reinterpret_cast<const char *>(tx.mw_blob->data()), tx.mw_blob->size());
}
s << tx.nLockTime;
}
@ -360,6 +388,8 @@ public:
const std::vector<CTxOut> vout;
const uint32_t nLockTime;
const litecoin_bits::MimbleBlobPtr mw_blob; //! Litecoin only
private:
/** Memory only. */
const uint256 hash;
@ -438,6 +468,13 @@ public:
bool HasWitness() const {
return std::any_of(vin.begin(), vin.end(), [](const CTxIn & txin){ return !txin.scriptWitness.IsNull(); });
}
/// Added by Calin to support Litecoin
bool HasMimble() const { return bool(mw_blob); }
/// Litecoin only: "IsHogEx" is defined as an "IsNull" but present mimble blob. This tells the block
/// deserializer later to deserialize mimble block extention data at the end of the CBlock stream.
bool IsHogEx() const { return HasMimble() && mw_blob->size() == 1 && *mw_blob->data() == 0; }
bool IsMWEBOnly() const { return HasMimble() && mw_blob->size() > 1 && vin.empty() && vout.empty(); }
};
/**
@ -450,6 +487,8 @@ public:
std::vector<CTxOut> vout;
uint32_t nLockTime;
litecoin_bits::MimbleBlobPtr mw_blob; //! Litecoin only
CMutableTransaction();
CMutableTransaction(const CTransaction &tx);
@ -484,6 +523,15 @@ public:
bool HasWitness() const {
return std::any_of(vin.begin(), vin.end(), [](const CTxIn & txin){ return !txin.scriptWitness.IsNull(); });
}
/// Added by Calin to support Litecoin
bool HasMimble() const { return bool(mw_blob); }
/// Litecoin only: "IsHogEx" is defined as an "IsNull" but present mimble blob. This tells the block
/// deserializer later to deserialize mimble block extention data at the end of the CBlock stream.
bool IsHogEx() const { return HasMimble() && mw_blob->size() == 1 && *mw_blob->data() == 0; }
bool IsMWEBOnly() const { return HasMimble() && mw_blob->size() > 1 && vin.empty() && vout.empty(); }
std::string ToString() const { return CTransaction(*this).ToString(); }
};
typedef std::shared_ptr<const CTransaction> CTransactionRef;

View file

@ -52,7 +52,8 @@ void App::register_MetaTypes()
qRegisterMetaType<PeerInfo>("PeerInfo");
qRegisterMetaType<PeerInfoList>("PeerInfoList");
qRegisterMetaType<BTC::Address>();
qRegisterMetaType<BTC::Address>("BTC::Address");
qRegisterMetaType<BTC::Coin>("BTC::Coin");
qRegisterMetaType<BitcoinDZmqNotifications>("BitcoinDZmqNotifications");