mirror of
https://github.com/cculianu/Fulcrum.git
synced 2026-08-13 12:33:27 +02:00
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:
parent
916b43495f
commit
5ac7d076f1
21 changed files with 639 additions and 111 deletions
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
[](https://github.com/cculianu/Fulcrum/actions/workflows/publish.yml)
|
||||
[](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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
22
resources/ltc/servers.json
Normal file
22
resources/ltc/servers.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
resources/ltc/servers_testnet.json
Normal file
10
resources/ltc/servers_testnet.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"electrum.ltc.xurious.com": {
|
||||
"s": "51002",
|
||||
"t": "51001"
|
||||
},
|
||||
"electrum-ltc.bysh.me": {
|
||||
"s": "51002",
|
||||
"t": "51001"
|
||||
}
|
||||
}
|
||||
22
src/BTC.cpp
22
src/BTC.cpp
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
38
src/BTC.h
38
src/BTC.h
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
68
src/bitcoin/copyable_ptr.h
Normal file
68
src/bitcoin/copyable_ptr.h
Normal 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
205
src/bitcoin/litecoin_bits.h
Normal 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
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue