From 19bbfe4ce0099ba68ab6054a7e9cb9da01766218 Mon Sep 17 00:00:00 2001 From: TomZ Date: Mon, 2 Sep 2019 23:28:14 +0200 Subject: [PATCH 1/5] Add the concept of DoubleSpendProofs The DSP is owned by the DoubleSpendProofStorage, which in turn is owned by the mempool. Each mempool-entry can refer to one proof. --- libs/server/CMakeLists.txt | 2 + libs/server/DoubleSpendProof.cpp | 318 ++++++++++++++++++ libs/server/DoubleSpendProof.h | 95 ++++++ libs/server/DoubleSpendProofStorage.cpp | 178 ++++++++++ libs/server/DoubleSpendProofStorage.h | 73 ++++ libs/server/init.cpp | 2 + libs/server/txmempool.cpp | 68 +++- libs/server/txmempool.h | 28 +- libs/server/validation/BlockValidation.cpp | 2 + .../server/validation/ValidationException.cpp | 8 + libs/server/validation/ValidationException.h | 14 + 11 files changed, 780 insertions(+), 8 deletions(-) create mode 100644 libs/server/DoubleSpendProof.cpp create mode 100644 libs/server/DoubleSpendProof.h create mode 100644 libs/server/DoubleSpendProofStorage.cpp create mode 100644 libs/server/DoubleSpendProofStorage.h diff --git a/libs/server/CMakeLists.txt b/libs/server/CMakeLists.txt index 65986b362..0e8778f11 100644 --- a/libs/server/CMakeLists.txt +++ b/libs/server/CMakeLists.txt @@ -31,6 +31,8 @@ set (FLOWEE_SERVER_FILES core_write.cpp CrashCatcher.cpp dbwrapper.cpp + DoubleSpendProof.cpp + DoubleSpendProofStorage.cpp encodings_legacy.cpp httprpc.cpp httpserver.cpp diff --git a/libs/server/DoubleSpendProof.cpp b/libs/server/DoubleSpendProof.cpp new file mode 100644 index 000000000..2e711e6e1 --- /dev/null +++ b/libs/server/DoubleSpendProof.cpp @@ -0,0 +1,318 @@ +/* + * This file is part of the Flowee project + * Copyright (C) 2019 Tom Zander + * + * 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. If not, see . + */ + +#include "DoubleSpendProof.h" +#include "UnspentOutputData.h" +#include "txmempool.h" +#include "script/interpreter.h" + +#include +#include +#include + +namespace { + enum ScriptType { + P2PKH + }; + + void getP2PKHSignature(const CScript &script, std::vector &vchRet) + { + auto scriptIter = script.begin(); + opcodetype type; + script.GetOp(scriptIter, type, vchRet); + } + + void hashTx(DoubleSpendProof::Spender &spender, const CTransaction &tx, int inputIndex) + { + assert(!spender.pushData.empty()); + assert(!spender.pushData.front().empty()); + auto hashType = spender.pushData.front().back(); + if (!(hashType & SIGHASH_ANYONECANPAY)) { + CHashWriter ss(SER_GETHASH, 0); + for (size_t n = 0; n < tx.vin.size(); ++n) { + ss << tx.vin[n].prevout; + } + spender.hashPrevOutputs = ss.GetHash(); + } + if (!(hashType & SIGHASH_ANYONECANPAY) && (hashType & 0x1f) != SIGHASH_SINGLE + && (hashType & 0x1f) != SIGHASH_NONE) { + CHashWriter ss(SER_GETHASH, 0); + for (size_t n = 0; n < tx.vin.size(); ++n) { + ss << tx.vin[n].nSequence; + } + spender.hashSequence = ss.GetHash(); + } + if ((hashType & 0x1f) != SIGHASH_SINGLE && (hashType & 0x1f) != SIGHASH_NONE) { + CHashWriter ss(SER_GETHASH, 0); + for (size_t n = 0; n < tx.vout.size(); ++n) { + ss << tx.vout[n]; + } + spender.hashOutputs = ss.GetHash(); + } else if ((hashType & 0x1f) == SIGHASH_SINGLE && inputIndex < tx.vout.size()) { + CHashWriter ss(SER_GETHASH, 0); + ss << tx.vout[inputIndex]; + spender.hashOutputs = ss.GetHash(); + } + } + + class DSPSignatureChecker : public BaseSignatureChecker { + public: + DSPSignatureChecker(const DoubleSpendProof *proof, const DoubleSpendProof::Spender &spender, uint64_t amount) + : m_proof(proof), + m_spender(spender), + m_amount(amount) + { + } + + bool CheckSig(const std::vector &vchSigIn, const std::vector &vchPubKey, const CScript &scriptCode, uint32_t flags) const { + CPubKey pubkey(vchPubKey); + if (!pubkey.IsValid()) + return false; + + std::vector vchSig(vchSigIn); + if (vchSig.empty()) + return false; + vchSig.pop_back(); // drop the hashtype byte tacked on to the end of the signature + + CHashWriter ss(SER_GETHASH, 0); + ss << m_spender.txVersion << m_spender.hashPrevOutputs << m_spender.hashSequence; + ss << COutPoint(m_proof->prevTxId(), m_proof->prevOutIndex()); + ss << static_cast(scriptCode); + ss << m_amount << m_spender.outSequence << m_spender.hashOutputs; + ss << m_spender.lockTime << (int) m_spender.pushData.front().back(); + const uint256 sighash = ss.GetHash(); + + if ((flags & SCRIPT_ENABLE_SCHNORR) && (vchSig.size() == 64)) + return pubkey.verifySchnorr(sighash, vchSig); + return pubkey.verifyECDSA(sighash, vchSig); + } + bool CheckLockTime(const CScriptNum&) const { + return true; + } + bool CheckSequence(const CScriptNum&) const { + return true; + } + + const DoubleSpendProof *m_proof; + const DoubleSpendProof::Spender &m_spender; + const uint64_t m_amount; + }; +} + +// static +DoubleSpendProof DoubleSpendProof::create(const Tx &tx1, const Tx &tx2) +{ + DoubleSpendProof answer; + Spender &s1 = answer.m_spender1; + Spender &s2 = answer.m_spender2; + + CTransaction t1 = tx1.createOldTransaction(); + CTransaction t2 = tx2.createOldTransaction(); + + size_t inputIndex1 = 0; + size_t inputIndex2 = 0; + for (; inputIndex1 < t1.vin.size(); ++inputIndex1) { + const CTxIn &in1 = t1.vin.at(inputIndex1); + for (;inputIndex2 < t2.vin.size(); ++inputIndex2) { + const CTxIn &in2 = t2.vin.at(inputIndex2); + if (in1.prevout == in2.prevout) { + answer.m_prevOutIndex = in1.prevout.n; + answer.m_prevTxId = in1.prevout.hash; + + s1.outSequence = in1.nSequence; + s2.outSequence = in2.nSequence; + + // TODO pass in the mempool and find the prev-tx we spend + // then we can determine what script type we are dealing with and + // be smarter about finding the signature. + // Assume p2pkh for now. + + s1.pushData.resize(1); + getP2PKHSignature(in1.scriptSig, s1.pushData.front()); + s2.pushData.resize(1); + getP2PKHSignature(in2.scriptSig, s2.pushData.front()); + + auto hashType = s1.pushData.front().back(); + if (!(hashType & SIGHASH_FORKID)) + throw std::runtime_error("Tx1 Not a Bitcoin Cash transaction"); + hashType = s2.pushData.front().back(); + if (!(hashType & SIGHASH_FORKID)) + throw std::runtime_error("Tx2 Not a Bitcoin Cash transaction"); + + break; + } + } + } + + if (answer.m_prevOutIndex == -1) + throw std::runtime_error("Transactions do not double spend each other"); + if (s1.pushData.front().empty() || s2.pushData.front().empty()) + throw std::runtime_error("Transactions not using known payment type. Could not find sig"); + + s1.txVersion = t1.nVersion; + s2.txVersion = t2.nVersion; + s1.lockTime = t1.nLockTime; + s2.lockTime = t2.nLockTime; + + hashTx(s1, t1, inputIndex1); + hashTx(s2, t2, inputIndex2); + + // sort the spenders to have proof be independent of the order of txs coming in + int diff = s1.hashOutputs.Compare(s2.hashOutputs); + if (diff == 0) + diff = s1.hashPrevOutputs.Compare(s2.hashPrevOutputs); + if (diff > 0) + std::swap(s1, s2); + + return answer; +} + +DoubleSpendProof::DoubleSpendProof() +{ +} + +bool DoubleSpendProof::isEmpty() const +{ + return m_prevOutIndex == -1 || m_prevTxId.IsNull(); +} + +DoubleSpendProof::Validity DoubleSpendProof::validate(const CTxMemPool &mempool, uint32_t flags) const +{ + if (m_prevTxId.IsNull() || m_prevOutIndex < 0) + return Invalid; + if (m_spender1.pushData.empty() || m_spender1.pushData.front().empty() + || m_spender2.pushData.empty() || m_spender2.pushData.front().empty()) + return Invalid; + + // check if ordering is proper + int diff = m_spender1.hashOutputs.Compare(m_spender2.hashOutputs); + if (diff == 0) + diff = m_spender1.hashPrevOutputs.Compare(m_spender2.hashPrevOutputs); + if (diff > 0) + return Invalid; + + // Get the previous output we are spending. + uint64_t amount; + CScript prevOutScript; + Tx prevTx; + if (mempool.lookup(m_prevTxId, prevTx)) { + auto output = prevTx.output(m_prevOutIndex); + if (output.outputValue < 0 || output.outputScript.empty()) + return Invalid; + amount = output.outputValue; + prevOutScript = output.outputScript; + } else { + auto prevTxData = mempool.utxo()->find(m_prevTxId, m_prevOutIndex); + if (!prevTxData.isValid()) // probably means a block was just mined with our transaction in it. + return MissingUTXO; + UnspentOutputData data(prevTxData); + amount = data.outputValue(); + prevOutScript = data.outputScript(); + } + + /* + * Find the matching transaction spending this. Possibly identical to one + * of the sides of this DSP. + * We need this because we want the public key that it contains. + */ + Tx tx; + if (!mempool.lookup(COutPoint(m_prevTxId, m_prevOutIndex), tx)) { + // Maybe the DSP is old and one of the transaction is already mined. + auto prevTxData = mempool.utxo()->find(m_prevTxId, m_prevOutIndex); + if (!prevTxData.isValid()) + return MissingUTXO; + return MissingTransction; + } + + /* + * TZ: At this point (2019-07) we only support P2PKH payments. + * + * Since we have an actually spending tx, we could trivially support various other + * types of scripts because all we need to do is replace the signature from our 'tx' + * with the one that comes from the DSP. + */ + ScriptType scriptType = P2PKH; // TODO look at prevTx to find out script-type + + std::vector pubkey; + Tx::Iterator iter(tx); + while (iter.next() != Tx::End) { + if (iter.tag() == Tx::PrevTxHash) { + if (iter.uint256Data() == m_prevTxId) { + iter.next(); + assert(iter.tag() == Tx::PrevTxIndex); + if (iter.intData() == m_prevOutIndex) { + iter.next(); + assert(iter.tag() == Tx::TxInScript); + // Found the input script we need! + + CScript inScript = iter.byteData(); + auto scriptIter = inScript.begin(); + opcodetype type; + inScript.GetOp(scriptIter, type); // P2PKH: first signature + inScript.GetOp(scriptIter, type, pubkey); // then pubkey + break; + } + } + } + else if (iter.tag() == Tx::OutputValue) { // end of inputs + break; + } + } + assert(!pubkey.empty()); + if (pubkey.empty()) + return Invalid; + + CScript inScript; + if (scriptType == P2PKH) { + inScript << m_spender1.pushData.front(); + inScript << pubkey; + } + DSPSignatureChecker checker1(this, m_spender1, amount); + ScriptError_t error; + if (!VerifyScript(inScript, prevOutScript, flags, checker1, &error)) { + logDebug(Log::Bitcoin) << "DoubleSpendProof failed validating first tx due to" << ScriptErrorString(error); + return Invalid; + } + + inScript.clear(); + if (scriptType == P2PKH) { + inScript << m_spender2.pushData.front(); + inScript << pubkey; + } + DSPSignatureChecker checker2(this, m_spender2, amount); + if (!VerifyScript(inScript, prevOutScript, flags, checker2, &error)) { + logDebug(Log::Bitcoin) << "DoubleSpendProof failed validating second tx due to" << ScriptErrorString(error); + return Invalid; + } + return Valid; +} + +uint256 DoubleSpendProof::prevTxId() const +{ + return m_prevTxId; +} + +int DoubleSpendProof::prevOutIndex() const +{ + return m_prevOutIndex; +} + +uint256 DoubleSpendProof::createHash() const +{ + return SerializeHash(*this); +} diff --git a/libs/server/DoubleSpendProof.h b/libs/server/DoubleSpendProof.h new file mode 100644 index 000000000..482365ec3 --- /dev/null +++ b/libs/server/DoubleSpendProof.h @@ -0,0 +1,95 @@ +/* + * This file is part of the Flowee project + * Copyright (C) 2019 Tom Zander + * + * 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. If not, see . + */ +#ifndef DOUBLESPENDPROOF_H +#define DOUBLESPENDPROOF_H + +#include +#include +#include "serialize.h" +#include + +class CTxMemPool; + +class DoubleSpendProof +{ +public: + DoubleSpendProof(); + + static DoubleSpendProof create(const Tx &tx1, const Tx &tx2); + + bool isEmpty() const; + + enum Validity { + Valid, + MissingTransction, + MissingUTXO, + Invalid + }; + + Validity validate(const CTxMemPool &mempool, uint32_t flags) const; + + uint256 prevTxId() const; + int prevOutIndex() const; + + struct Spender { + uint32_t txVersion = 0, outSequence = 0, lockTime = 0; + uint256 hashPrevOutputs, hashSequence, hashOutputs; + std::vector> pushData; + }; + + Spender firstSpender() const { + return m_spender1; + } + Spender doubleSpender() const { + return m_spender2; + } + + // old fashioned serialization. + ADD_SERIALIZE_METHODS + template + inline void SerializationOp(Stream& s, Operation ser_action, int nType, int nVersion) { + READWRITE(m_prevTxId); + READWRITE(m_prevOutIndex); + + READWRITE(m_spender1.txVersion); + READWRITE(m_spender1.outSequence); + READWRITE(m_spender1.lockTime); + READWRITE(m_spender1.hashPrevOutputs); + READWRITE(m_spender1.hashSequence); + READWRITE(m_spender1.hashOutputs); + READWRITE(m_spender1.pushData); + + READWRITE(m_spender2.txVersion); + READWRITE(m_spender2.outSequence); + READWRITE(m_spender2.lockTime); + READWRITE(m_spender2.hashPrevOutputs); + READWRITE(m_spender2.hashSequence); + READWRITE(m_spender2.hashOutputs); + READWRITE(m_spender2.pushData); + } + + uint256 createHash() const; + +private: + uint256 m_prevTxId; + int32_t m_prevOutIndex = -1; + + Spender m_spender1, m_spender2; +}; + +#endif diff --git a/libs/server/DoubleSpendProofStorage.cpp b/libs/server/DoubleSpendProofStorage.cpp new file mode 100644 index 000000000..c797492f0 --- /dev/null +++ b/libs/server/DoubleSpendProofStorage.cpp @@ -0,0 +1,178 @@ +/* + * This file is part of the Flowee project + * Copyright (c) 2019 Tom Zander + * + * 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. If not, see . + */ +#include "DoubleSpendProofStorage.h" +#include +#include + +#define SECONDS_TO_KEEP_ORPHANS 90 + +DoubleSpendProofStorage::DoubleSpendProofStorage() + : m_recentRejects(120000, 0.000001) +{ +} + +DoubleSpendProof DoubleSpendProofStorage::proof(int proof) const +{ + std::lock_guard lock(m_lock); + auto iter = m_proofs.find(proof); + if (iter != m_proofs.end()) + return iter->second; + return DoubleSpendProof(); +} + +int DoubleSpendProofStorage::add(const DoubleSpendProof &proof) +{ + std::lock_guard lock(m_lock); + + uint256 hash = proof.createHash(); + auto lookupIter = m_dspIdLookupTable.find(hash); + if (lookupIter != m_dspIdLookupTable.end()) + return lookupIter->second; + + auto iter = m_proofs.find(m_nextId); + while (iter != m_proofs.end()) { + if (++m_nextId < 0) + m_nextId = 1; + iter = m_proofs.find(m_nextId); + } + m_proofs.insert(std::make_pair(m_nextId, proof)); + m_dspIdLookupTable.insert(std::make_pair(hash, m_nextId)); + + return m_nextId++; +} + +void DoubleSpendProofStorage::addOrphan(const DoubleSpendProof &proof) +{ + std::lock_guard lock(m_lock); + const int next = m_nextId; + const int id = add(proof); + if (id != next) // it was already in the storage + return; + + m_orphans.insert(std::make_pair(id, GetTime())); + m_prevTxIdLookupTable[proof.prevTxId().GetCheapHash()].push_back(id); +} + +int DoubleSpendProofStorage::claimOrphan(const COutPoint &prevOut) +{ + std::lock_guard lock(m_lock); + auto iter = m_prevTxIdLookupTable.find(prevOut.hash.GetCheapHash()); + if (iter == m_prevTxIdLookupTable.end()) + return -1; + + std::deque &q = iter->second; + for (auto proofId = q.begin(); proofId != q.end(); ++proofId) { + auto proofIter = m_proofs.find(*proofId); + assert (proofIter != m_proofs.end()); + if (proofIter->second.prevOutIndex() != prevOut.n) + continue; + if (proofIter->second.prevTxId() == prevOut.hash) { + q.erase(proofId); + m_orphans.erase(*proofId); + return *proofId; + } + } + return -1; +} + +void DoubleSpendProofStorage::remove(int proof) +{ + std::lock_guard lock(m_lock); + auto iter = m_proofs.find(proof); + if (iter == m_proofs.end()) + return; + + auto orphan = m_orphans.find(iter->first); + if (orphan != m_orphans.end()) { + m_orphans.erase(orphan); + auto orphanLookup = m_prevTxIdLookupTable.find(iter->second.prevTxId().GetCheapHash()); + if (orphanLookup != m_prevTxIdLookupTable.end()) { + std::deque &queue = orphanLookup->second; + if (queue.size() == 1) { + assert(queue.front() == proof); + m_prevTxIdLookupTable.erase(orphanLookup); + } + else { +#ifndef NDEBUG + size_t sizeBefore = queue.size(); +#endif + for (auto i = queue.begin(); i != queue.end(); ++i) { + if (*i == proof) { + queue.erase(i); + break; + } + } +assert(queue.size() < sizeBefore); + assert(orphanLookup->second.size() < sizeBefore); + } + } + } + auto hash = iter->second.createHash(); + m_dspIdLookupTable.erase(hash); + m_proofs.erase(iter); +} + +DoubleSpendProof DoubleSpendProofStorage::lookup(const uint256 &proofId) const +{ + std::lock_guard lock(m_lock); + auto lookupIter = m_dspIdLookupTable.find(proofId); + if (lookupIter == m_dspIdLookupTable.end()) + return DoubleSpendProof(); + return m_proofs.at(lookupIter->second); +} + +bool DoubleSpendProofStorage::exists(const uint256 &proofId) const +{ + std::lock_guard lock(m_lock); + return m_dspIdLookupTable.find(proofId) != m_dspIdLookupTable.end(); +} + +void DoubleSpendProofStorage::periodicCleanup() +{ + std::lock_guard lock(m_lock); + auto expire = GetTime() - SECONDS_TO_KEEP_ORPHANS; + auto iter = m_orphans.begin(); + while (iter != m_orphans.end()) { + if (iter->second <= expire) { + const int proofId = iter->first; + iter = m_orphans.erase(iter); + remove(proofId); + } + else { + ++iter; + } + } +} + +bool DoubleSpendProofStorage::isRecentlyRejectedProof(const uint256 &proofHash) const +{ + std::lock_guard lock(m_lock); + return m_recentRejects.contains(proofHash); +} + +void DoubleSpendProofStorage::markProofRejected(const uint256 &proofHash) +{ + std::lock_guard lock(m_lock); + m_recentRejects.insert(proofHash); +} + +void DoubleSpendProofStorage::newBlockFound() +{ + std::lock_guard lock(m_lock); + m_recentRejects.reset(); +} diff --git a/libs/server/DoubleSpendProofStorage.h b/libs/server/DoubleSpendProofStorage.h new file mode 100644 index 000000000..d2b9e5b89 --- /dev/null +++ b/libs/server/DoubleSpendProofStorage.h @@ -0,0 +1,73 @@ +/* + * This file is part of the Flowee project + * Copyright (c) 2019 Tom Zander + * + * 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. If not, see . + */ +#ifndef DOUBLESPENDPROOFSTORAGE_H +#define DOUBLESPENDPROOFSTORAGE_H + +#include "DoubleSpendProof.h" +#include "bloom.h" +#include + +#include +#include +#include + +class COutPoint; + +class DoubleSpendProofStorage +{ +public: + DoubleSpendProofStorage(); + + /// returns a double spend proof based on proof-id + DoubleSpendProof proof(int proof) const; + /// adds a proof, returns an internal proof-id that proof is known under. + /// notice that if the proof (by hash) was known, that proof-id is returned instead. + int add(const DoubleSpendProof &proof); + /// remove by proof-id + void remove(int proof); + + /// this add()s and additionally registers this is an orphan. + /// you can fetch those upto 90s using 'claim()'. + void addOrphan(const DoubleSpendProof &proof); + /// returns -1 if not found, otherwise a proof-id + int claimOrphan(const COutPoint &prevOut); + + DoubleSpendProof lookup(const uint256 &proofId) const; + bool exists(const uint256 &proofId) const; + + // called every minute + void periodicCleanup(); + + bool isRecentlyRejectedProof(const uint256 &proofHash) const; + void markProofRejected(const uint256 &proofHash); + void newBlockFound(); + +private: + std::map m_proofs; + int m_nextId = 1; + std::map m_orphans; + + typedef boost::unordered_map LookupTable; + LookupTable m_dspIdLookupTable; + std::map > m_prevTxIdLookupTable; + mutable std::recursive_mutex m_lock; + + CRollingBloomFilter m_recentRejects; +}; + +#endif diff --git a/libs/server/init.cpp b/libs/server/init.cpp index 45c2c04b2..da8259b43 100644 --- a/libs/server/init.cpp +++ b/libs/server/init.cpp @@ -33,6 +33,7 @@ #include "checkpoints.h" #include "compat/sanity.h" #include "consensus/validation.h" +#include "DoubleSpendProofStorage.h" #include "httpserver.h" #include "httprpc.h" #include @@ -774,6 +775,7 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler) } Application::instance()->validation()->setMempool(&mempool); + scheduler.scheduleEvery(std::bind(&DoubleSpendProofStorage::periodicCleanup, mempool.doubleSpendProofStorage()), 60); // ********************************************************* Step 5: verify wallet database integrity #ifdef ENABLE_WALLET diff --git a/libs/server/txmempool.cpp b/libs/server/txmempool.cpp index 334c7a732..aa4ec7124 100644 --- a/libs/server/txmempool.cpp +++ b/libs/server/txmempool.cpp @@ -2,7 +2,7 @@ * This file is part of the Flowee project * Copyright (C) 2009-2010 Satoshi Nakamoto * Copyright (C) 2009-2015 The Bitcoin Core developers - * Copyright (C) 2017 Tom Zander + * Copyright (C) 2017-2019 Tom Zander * * 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 @@ -18,6 +18,8 @@ * along with this program. If not, see . */ +#include "DoubleSpendProof.h" +#include "DoubleSpendProofStorage.h" #include "txmempool.h" #include "consensus/consensus.h" @@ -356,13 +358,15 @@ void CTxMemPoolEntry::UpdateState(int64_t modifySize, CAmount modifyFee, int64_t CTxMemPool::CTxMemPool() : nTransactionsUpdated(0), - m_utxo(nullptr) + m_utxo(nullptr), + m_dspStorage(new DoubleSpendProofStorage()) { _clear(); //lock free clear } CTxMemPool::~CTxMemPool() { + delete m_dspStorage; } unsigned int CTxMemPool::GetTransactionsUpdated() const @@ -428,19 +432,39 @@ void CTxMemPool::addUnchecked(const uint256& hash, const CTxMemPoolEntry &entry, totalTxSize += entry.GetTxSize(); } -bool CTxMemPool::insertTx(const CTxMemPoolEntry &entry) +bool CTxMemPool::insertTx(CTxMemPoolEntry &entry) { assert(m_utxo); + assert(entry.dsproof == -1); LOCK(cs); if (exists(entry.tx.createHash())) return false; for (const CTxIn &txin : entry.oldTx.vin) { + int proofId = doubleSpendProofStorage()->claimOrphan(txin.prevout); + if (proofId != -1) { + entry.dsproof = proofId; + // if we find this here, AS AN ORPHAN, then nothing has entered the mempool yet + // that claimed it. As such we don't have to check for conflicts. + assert(mapNextTx.find(txin.prevout) != mapNextTx.end()); // Check anyway + continue; + } auto oldTx = mapNextTx.find(txin.prevout); if (oldTx != mapNextTx.end()) { // double spend detected! - ValidationNotifier().DoubleSpendFound(oldTx->second.tx, entry.tx); - throw Validation::Exception("txn-mempool-conflict", Validation::RejectConflict, 0); + auto iter = mapTx.find(oldTx->second.tx.createHash()); + assert(mapTx.end() != iter); + int newProofId = -1; + if (iter->dsproof == -1) { // no DS proof exists, lets make one. + auto item = *iter; + item.dsproof = m_dspStorage->add(DoubleSpendProof::create(oldTx->second.tx, entry.tx)); + logDebug(Log::Mempool) << "Double spend found, creating double spend proof" + << oldTx->second.tx.createHash() + << entry.tx.createHash() << "proof:" << item.dsproof; + mapTx.replace(iter, item); + newProofId = item.dsproof; + } + throw Validation::DoubleSpendException(oldTx->second.tx, newProofId); } auto iter = mapTx.find(txin.prevout.hash); @@ -657,6 +681,17 @@ bool CTxMemPool::lookup(const uint256 &hash, Tx& result) const return true; } +bool CTxMemPool::lookup(const COutPoint &outpoint, Tx& result) const +{ + LOCK(cs); + + auto oldTx = mapNextTx.find(outpoint); + if (oldTx == mapNextTx.end()) + return false; + result = oldTx->second.tx; + return true; +} + void CTxMemPool::PrioritiseTransaction(const uint256 hash, const std::string strHash, double dPriorityDelta, const CAmount& nFeeDelta) { { @@ -787,6 +822,29 @@ void CTxMemPool::setUtxo(UnspentOutputDatabase *utxo) m_utxo = utxo; } +Tx CTxMemPool::addDoubleSpendProof(const DoubleSpendProof &proof) +{ + LOCK(cs); + auto oldTx = mapNextTx.find(COutPoint(proof.prevTxId(), proof.prevOutIndex())); + if (oldTx == mapNextTx.end()) + return Tx(); + + auto iter = mapTx.find(oldTx->second.tx.createHash()); + assert(mapTx.end() != iter); + if (iter->dsproof != -1) // A DSProof already exists for this tx. + return Tx(); // don't propagate new one. + + auto item = *iter; + item.dsproof = m_dspStorage->add(proof); + mapTx.replace(iter, item); + + return oldTx->second.tx; +} + +DoubleSpendProofStorage *CTxMemPool::doubleSpendProofStorage() const +{ + return m_dspStorage; +} CFeeRate CTxMemPool::GetMinFee() const { diff --git a/libs/server/txmempool.h b/libs/server/txmempool.h index 7b0b6b5e6..676182e88 100644 --- a/libs/server/txmempool.h +++ b/libs/server/txmempool.h @@ -2,7 +2,7 @@ * This file is part of the Flowee project * Copyright (C) 2009-2010 Satoshi Nakamoto * Copyright (C) 2009-2015 The Bitcoin Core developers - * Copyright (C) 2017 Tom Zander + * Copyright (C) 2017-2019 Tom Zander * * 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 @@ -35,6 +35,8 @@ class CAutoFile; class CBlockIndex; class UnspentOutputDatabase; +class DoubleSpendProofStorage; +class DoubleSpendProof; inline double AllowFreeThreshold() { @@ -112,6 +114,7 @@ public: uint64_t nCountWithDescendants; //! number of descendant transactions uint64_t nSizeWithDescendants; //! ... and size CAmount nModFeesWithDescendants; //! ... and total fees (all including us) + int dsproof = -1; CTxMemPoolEntry(const Tx &tx); @@ -424,6 +427,14 @@ public: return m_utxo; } + /** + * Add a double spend proof we received elsewhere to an existing mempool-entry. + * Return CTransaction of the mempool entry we added this to. + */ + Tx addDoubleSpendProof(const DoubleSpendProof &proof); + + DoubleSpendProofStorage *doubleSpendProofStorage() const; + private: typedef std::map cacheMap; @@ -454,8 +465,17 @@ public: void addUnchecked(const uint256& hash, const CTxMemPoolEntry &entry, bool fCurrentEstimate = true); void addUnchecked(const uint256& hash, const CTxMemPoolEntry &entry, const setEntries &setAncestors, bool fCurrentEstimate = true); - /// throws if something goes wrong - bool insertTx(const CTxMemPoolEntry &entry); + /** + * Check entry for double spend, and adds if Ok. + * Notice that the entry.dsproof int gets changed if there was a double-spend-proof already. + * Throws if something goes wrong. + */ + bool insertTx(CTxMemPoolEntry &entry); + + inline bool insertTx(Tx &tx) { + CTxMemPoolEntry entry(tx); + return insertTx(entry); + } void remove(const CTransaction &tx, std::list& removed, bool fRecursive = false); void removeForReorg(unsigned int nMemPoolHeight, int flags); @@ -544,6 +564,7 @@ public: bool lookup(const uint256 &hash, CTransaction& result) const; bool lookup(const uint256 &hash, Tx& result) const; + bool lookup(const COutPoint &outpoint, Tx& result) const; size_t DynamicMemoryUsage() const; @@ -591,6 +612,7 @@ private: void removeUnchecked(txiter entry); UnspentOutputDatabase *m_utxo; + DoubleSpendProofStorage *m_dspStorage; }; // We want to sort transactions by coin age priority diff --git a/libs/server/validation/BlockValidation.cpp b/libs/server/validation/BlockValidation.cpp index d282fc462..f813d2c35 100644 --- a/libs/server/validation/BlockValidation.cpp +++ b/libs/server/validation/BlockValidation.cpp @@ -20,6 +20,7 @@ #include #include "BlockValidation_p.h" #include "TxValidation_p.h" +#include "DoubleSpendProofStorage.h" #include "ValidationException.h" #include #include @@ -604,6 +605,7 @@ void ValidationEnginePrivate::processNewBlock(std::shared_ptrSetTip(index); tip.store(index); mempool->AddTransactionsUpdated(1); + mempool->doubleSpendProofStorage()->newBlockFound(); cvBlockChange.notify_all(); #ifdef ENABLE_BENCHMARKS end = GetTimeMicros(); diff --git a/libs/server/validation/ValidationException.cpp b/libs/server/validation/ValidationException.cpp index 9befaed3a..6dead437b 100644 --- a/libs/server/validation/ValidationException.cpp +++ b/libs/server/validation/ValidationException.cpp @@ -38,3 +38,11 @@ Validation::Exception::Exception(const std::string &error, Validation::RejectCod { m_rejectCode = rejectCode; } + + +Validation::DoubleSpendException::DoubleSpendException(const Tx &otherTx, int dspProofId) + : Exception("", 0), + otherTx(otherTx), + id(dspProofId) +{ +} diff --git a/libs/server/validation/ValidationException.h b/libs/server/validation/ValidationException.h index 44cd3b866..7eabc70b5 100644 --- a/libs/server/validation/ValidationException.h +++ b/libs/server/validation/ValidationException.h @@ -22,6 +22,8 @@ #include #include +#include + namespace Validation { enum RejectCodes { @@ -75,6 +77,18 @@ private: RejectCodes m_rejectCode; bool m_corruptionPossible = false; }; + +/** + * A special subclass of the validation exception reserved for transactions being marked + * as double spends. + */ +class DoubleSpendException : public Exception { +public: + DoubleSpendException(const Tx &otherTx, int dspProofId); + + const Tx otherTx; + const int id; +}; } #endif -- GitLab From 6cda85e79f4f03484f3bbe849cc9ddfc78822cb9 Mon Sep 17 00:00:00 2001 From: TomZ Date: Mon, 2 Sep 2019 23:33:33 +0200 Subject: [PATCH 2/5] Catch DSP exception and pass to interface --- libs/interfaces/validationinterface.cpp | 7 +++- libs/interfaces/validationinterface.h | 13 +++++-- libs/server/protocol.h | 2 ++ libs/server/validation/TxValidation.cpp | 46 +++++++++++++++++++++++++ libs/server/validation/TxValidation_p.h | 7 ++++ 5 files changed, 72 insertions(+), 3 deletions(-) diff --git a/libs/interfaces/validationinterface.cpp b/libs/interfaces/validationinterface.cpp index 7cf6ec393..f3ec5195d 100644 --- a/libs/interfaces/validationinterface.cpp +++ b/libs/interfaces/validationinterface.cpp @@ -2,7 +2,7 @@ * This file is part of the Flowee project * Copyright (C) 2009-2010 Satoshi Nakamoto * Copyright (C) 2009-2014 The Bitcoin Core developers - * Copyright (C) 2017-2018 Tom Zander + * Copyright (C) 2017-2019 Tom Zander * * 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 @@ -75,6 +75,11 @@ void ValidationInterfaceBroadcaster::DoubleSpendFound(const Tx &first, const Tx for (auto i : m_listeners) i->DoubleSpendFound(first, duplicate); } +void ValidationInterfaceBroadcaster::DoubleSpendFound(const Tx &txInMempool, const DoubleSpendProof &proof) +{ + for (auto i : m_listeners) i->DoubleSpendFound(txInMempool, proof); +} + void ValidationInterfaceBroadcaster::addListener(ValidationInterface *impl) { m_listeners.push_back(impl); diff --git a/libs/interfaces/validationinterface.h b/libs/interfaces/validationinterface.h index b40374226..5161ce821 100644 --- a/libs/interfaces/validationinterface.h +++ b/libs/interfaces/validationinterface.h @@ -2,7 +2,7 @@ * This file is part of the Flowee project * Copyright (C) 2009-2010 Satoshi Nakamoto * Copyright (C) 2009-2015 The Bitcoin Core developers - * Copyright (C) 2017-2018 Tom Zander + * Copyright (C) 2017-2019 Tom Zander * * 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 @@ -32,6 +32,7 @@ class CTransaction; class uint256; class Tx; class FastBlock; +class DoubleSpendProof; class ValidationInterface { public: @@ -62,10 +63,17 @@ public: /** Notifies listeners that a key for mining is required (coinbase) */ virtual void GetScriptForMining(boost::shared_ptr) {} - /** Notifies listeners that we received a double-spend. + /** + * Notifies listeners that we received a double-spend. * First is the tx that is in our mempool, duplicate is the one we received and reject */ virtual void DoubleSpendFound(const Tx &first, const Tx &duplicate) {} + + /** + * Notifies listeners that we received a double-spend proof. + * First is the tx that is in our mempool, proof is the actual proof. + */ + virtual void DoubleSpendFound(const Tx &txInMempool, const DoubleSpendProof &proof) {} }; class ValidationInterfaceBroadcaster : public ValidationInterface @@ -81,6 +89,7 @@ public: void ResendWalletTransactions(int64_t nBestBlockTime) override; void GetScriptForMining(boost::shared_ptr) override; void DoubleSpendFound(const Tx &first, const Tx &duplicate) override; + void DoubleSpendFound(const Tx &txInMempool, const DoubleSpendProof &proof) override; void addListener(ValidationInterface *impl); void removeListener(ValidationInterface *impl); diff --git a/libs/server/protocol.h b/libs/server/protocol.h index fb877b955..b48a287d8 100644 --- a/libs/server/protocol.h +++ b/libs/server/protocol.h @@ -386,6 +386,8 @@ enum { // BUIP010 Xtreme Thinblocks: an Xtreme thin block contains the first 8 bytes of all the tx hashes // and also provides the missing transactions that are needed at the other end to reconstruct the block MSG_XTHINBLOCK, + + MSG_DOUBLESPENDPROOF = 0x94a0 }; diff --git a/libs/server/validation/TxValidation.cpp b/libs/server/validation/TxValidation.cpp index 3e681814f..d72d1aad0 100644 --- a/libs/server/validation/TxValidation.cpp +++ b/libs/server/validation/TxValidation.cpp @@ -17,6 +17,8 @@ */ #include "Engine.h" +#include "../DoubleSpendProofStorage.h" +#include "../DoubleSpendProof.h" #include #include #include @@ -413,6 +415,17 @@ void TxValidationState::checkTransaction() CTxOrphanCache::instance()->EraseOrphansByTime(); parent->strand.post(std::bind(&TxValidationState::sync, shared_from_this())); + } catch (const Validation::DoubleSpendException &ex) { + raii.result = strprintf("%i: %s", Validation::RejectConflict, "txn-mempool-conflict"); + logWarning(Log::TxValidation) << "Tx-Validation found a double spend"; + + m_doubleSpendTx = ex.otherTx; + m_doubleSpendProofId = ex.id; + + parent->strand.post(std::bind(&TxValidationState::notifyDoubleSpend, shared_from_this())); + + std::lock_guard rejects(parent->recentRejectsLock); + parent->recentTxRejects.insert(txid); } catch (const Exception &ex) { raii.result = strprintf("%i: %s", ex.rejectCode(), ex.what()); if (inputsMissing) {// if missing inputs, add to orphan cache @@ -466,6 +479,39 @@ void TxValidationState::sync() ValidationNotifier().SyncTx(m_tx); } +void TxValidationState::notifyDoubleSpend() +{ + std::shared_ptr parent = m_parent.lock(); + if (parent.get() == nullptr) + return; + assert(parent->strand.running_in_this_thread()); + + // send INV to all peers + if (m_doubleSpendProofId != -1) { + auto dsp = mempool.doubleSpendProofStorage()->proof(m_doubleSpendProofId); + if (!dsp.isEmpty()) { + CInv inv(MSG_DOUBLESPENDPROOF, dsp.createHash()); + const CTransaction oldTx = m_doubleSpendTx.createOldTransaction(); + + LOCK(cs_vNodes); + for (CNode* pnode : vNodes) { + if(!pnode->fRelayTxes) + continue; + LOCK(pnode->cs_filter); + if (pnode->pfilter) { + // For nodes that we sent this Tx before, send a proof. + if (pnode->pfilter->IsRelevantAndUpdate(oldTx)) + pnode->PushInventory(inv); + } else { + pnode->PushInventory(inv); + } + } + } + } + + ValidationNotifier().DoubleSpendFound(m_doubleSpendTx, m_tx); +} + uint32_t Validation::countSigOps(const CTransaction &tx) { diff --git a/libs/server/validation/TxValidation_p.h b/libs/server/validation/TxValidation_p.h index 6073ff4da..d678c202e 100644 --- a/libs/server/validation/TxValidation_p.h +++ b/libs/server/validation/TxValidation_p.h @@ -54,9 +54,16 @@ public: std::int32_t m_originatingNodeId; std::uint64_t m_originalInsertTime; + // Data for double-spend-notifications (done in notifyDoubleSpend()) + Tx m_doubleSpendTx; + int m_doubleSpendProofId = -1; + void checkTransaction(); /// Only called when fully successful, to be called in the strand. void sync(); + + /// to be called in the strand + void notifyDoubleSpend(); }; -- GitLab From 57220bb1245311a2633048c6c90d2acccbfaf1ca Mon Sep 17 00:00:00 2001 From: TomZ Date: Mon, 2 Sep 2019 23:34:35 +0200 Subject: [PATCH 3/5] Handle doublespend proof message in AddressMonitor --- libs/api/AddressMonitorService.cpp | 77 +++++++++++++++++++++--------- libs/api/AddressMonitorService.h | 5 +- 2 files changed, 57 insertions(+), 25 deletions(-) diff --git a/libs/api/AddressMonitorService.cpp b/libs/api/AddressMonitorService.cpp index 02b8106b6..082377520 100644 --- a/libs/api/AddressMonitorService.cpp +++ b/libs/api/AddressMonitorService.cpp @@ -16,20 +16,22 @@ * along with this program. If not, see . */ #include "AddressMonitorService.h" + +// server 'lib' +#include +#include #include +#include +#include + #include #include #include #include -#include - #include #include - -#include "Application.h" -#include "txmempool.h" - +#include #include AddressMonitorService::AddressMonitorService() @@ -48,19 +50,20 @@ void AddressMonitorService::SyncTx(const Tx &tx) const auto rem = remotes(); std::map matches; Tx::Iterator iter(tx); - bool m = match(iter, rem, matches); - if (!m) + if (!match(iter, rem, matches)) return; for (auto i = matches.begin(); i != matches.end(); ++i) { Match &match = i->second; std::lock_guard guard(m_poolMutex); - m_pool.reserve(match.keys.size() * 24 + 50); + m_pool.reserve(match.keys.size() * 24 + match.amounts.size() * 10 + 40); Streaming::MessageBuilder builder(m_pool); for (auto key : match.keys) builder.add(Api::AddressMonitor::BitcoinAddress, key); - builder.add(Api::AddressMonitor::Amount, i->second.amount); + for (auto amount : match.amounts) + builder.add(Api::AddressMonitor::Amount, amount); builder.add(Api::AddressMonitor::TxId, tx.createHash()); + logDebug(Log::MonitorService) << "Remote" << i->first << "gets" << match.keys.size() << "tx notification(s)"; rem[i->first]->connection.send(builder.message(Api::AddressMonitorService, Api::AddressMonitor::TransactionFound)); } } @@ -94,7 +97,7 @@ bool AddressMonitorService::match(Tx::Iterator &iter, const std::deque(remotes.at(i)); if (rwk->keys.find(keyID) != rwk->keys.end()) { Match &m = matchingRemotes[i]; - m.amount += amount; + m.amounts.push_back(amount); m.keys.push_back(keyID); } } @@ -118,13 +121,15 @@ void AddressMonitorService::SyncAllTransactionsInBlock(const FastBlock &block, C for (auto i = matches.begin(); i != matches.end(); ++i) { Match &match = i->second; std::lock_guard guard(m_poolMutex); - m_pool.reserve(match.keys.size() * 24 + 30); + m_pool.reserve(match.keys.size() * 24 + match.amounts.size() * 10 + 20); Streaming::MessageBuilder builder(m_pool); for (auto key : match.keys) builder.add(Api::AddressMonitor::BitcoinAddress, key); - builder.add(Api::AddressMonitor::Amount, i->second.amount); + for (auto amount : match.amounts) + builder.add(Api::AddressMonitor::Amount, amount); builder.add(Api::AddressMonitor::OffsetInBlock, static_cast(iter.prevTx().offsetInBlock(block))); builder.add(Api::AddressMonitor::BlockHeight, index->nHeight); + logDebug(Log::MonitorService) << "Remote" << i->first << "gets" << match.keys.size() << "tx notification(s) from block"; rem[i->first]->connection.send(builder.message(Api::AddressMonitorService, Api::AddressMonitor::TransactionFound)); } } @@ -132,38 +137,64 @@ void AddressMonitorService::SyncAllTransactionsInBlock(const FastBlock &block, C void AddressMonitorService::DoubleSpendFound(const Tx &first, const Tx &duplicate) { - logCritical(Log::MonitorService) << "Double spend found" << first.createHash() << duplicate.createHash(); + logDebug(Log::MonitorService) << "Double spend found" << first.createHash() << duplicate.createHash(); const auto rem = remotes(); std::map matches; Tx::Iterator iter(first); - bool m = match(iter, rem, matches); - if (!m) - return; + if (!match(iter, rem, matches)) + return; // returns false if no listeners Tx::Iterator iter2(duplicate); - m = match(iter2, rem, matches); + bool m = match(iter2, rem, matches); assert(m); // our duplicate tx object should have data for (auto i = matches.begin(); i != matches.end(); ++i) { Match &match = i->second; std::lock_guard guard(m_poolMutex); - m_pool.reserve(match.keys.size() * 24 + 40 + duplicate.size()); + m_pool.reserve(match.keys.size() * 24 + match.amounts.size() * 10 + 30 + duplicate.size()); Streaming::MessageBuilder builder(m_pool); for (auto key : match.keys) builder.add(Api::AddressMonitor::BitcoinAddress, key); - builder.add(Api::AddressMonitor::Amount, i->second.amount); + for (auto amount : match.amounts) + builder.add(Api::AddressMonitor::Amount, amount); builder.add(Api::AddressMonitor::TxId, first.createHash()); builder.add(Api::AddressMonitor::GenericByteData, duplicate.data()); rem[i->first]->connection.send(builder.message(Api::AddressMonitorService, Api::AddressMonitor::DoubleSpendFound)); } } +void AddressMonitorService::DoubleSpendFound(const Tx &txInMempool, const DoubleSpendProof &proof) +{ + logDebug(Log::MonitorService) << "Double spend proof found. TxId:" << txInMempool.createHash(); + const auto rem = remotes(); + std::map matches; + Tx::Iterator iter(txInMempool); + if (!match(iter, rem, matches)) + return; // returns false if no listeners + + CDataStream stream(SER_NETWORK, PROTOCOL_VERSION); + stream << proof; + const std::vector serializedProof(stream.begin(), stream.end()); + + for (auto i = matches.begin(); i != matches.end(); ++i) { + Match &match = i->second; + std::lock_guard guard(m_poolMutex); + m_pool.reserve(match.keys.size() * 24 + match.amounts.size() * 10 + 35 + serializedProof.size()); + Streaming::MessageBuilder builder(m_pool); + for (auto key : match.keys) + builder.add(Api::AddressMonitor::BitcoinAddress, key); + for (auto amount : match.amounts) + builder.add(Api::AddressMonitor::Amount, amount); + builder.add(Api::AddressMonitor::TxId, txInMempool.createHash()); + builder.addByteArray(Api::AddressMonitor::GenericByteData, &serializedProof[0], serializedProof.size()); + rem[i->first]->connection.send(builder.message(Api::AddressMonitorService, Api::AddressMonitor::DoubleSpendFound)); + } +} + void AddressMonitorService::onIncomingMessage(Remote *remote_, const Message &message, const EndPoint &ep) { assert(dynamic_cast(remote_)); RemoteWithKeys *remote = static_cast(remote_); - if (message.messageId() == Api::AddressMonitor::Subscribe) - logInfo(Log::MonitorService) << "Remote" << ep.connectionId << "registered a new address"; if (message.messageId() == Api::AddressMonitor::Subscribe || message.messageId() == Api::AddressMonitor::Unsubscribe) { @@ -196,6 +227,8 @@ void AddressMonitorService::onIncomingMessage(Remote *remote_, const Message &me remote->pool.reserve(10 + error.size()); Streaming::MessageBuilder builder(remote->pool); builder.add(Api::AddressMonitor::Result, done); + if (message.messageId() == Api::AddressMonitor::Subscribe) + logInfo(Log::MonitorService) << "Remote" << ep.connectionId << "registered" << done << "new address(es)"; if (!error.empty()) builder.add(Api::AddressMonitor::ErrorMessage, error); remote->connection.send(builder.reply(message)); diff --git a/libs/api/AddressMonitorService.h b/libs/api/AddressMonitorService.h index 8f4f39d20..0ce7aaf83 100644 --- a/libs/api/AddressMonitorService.h +++ b/libs/api/AddressMonitorService.h @@ -39,8 +39,8 @@ public: // the hub pushed a transaction into its mempool void SyncTx(const Tx &tx) override; void SyncAllTransactionsInBlock(const FastBlock &block, CBlockIndex *index) override; - // void SetBestChain(const CBlockLocator &locator) override; void DoubleSpendFound(const Tx &first, const Tx &duplicate) override; + void DoubleSpendFound(const Tx &txInMempool, const DoubleSpendProof &proof) override; void onIncomingMessage(Remote *con, const Message &message, const EndPoint &ep) override; @@ -61,8 +61,7 @@ protected: private: struct Match { - Match() : amount(0) {} - uint64_t amount; + std::deque amounts; std::deque keys; }; -- GitLab From 37b359cdac01062ae2a8b403f5607aa741e0b874 Mon Sep 17 00:00:00 2001 From: TomZ Date: Mon, 2 Sep 2019 23:35:14 +0200 Subject: [PATCH 4/5] Implement network awareness of double-spend-proof --- libs/server/main.cpp | 97 ++++++++++++++++++++++++++++++++++------ libs/server/protocol.cpp | 36 +++++++-------- libs/server/protocol.h | 13 +++--- 3 files changed, 107 insertions(+), 39 deletions(-) diff --git a/libs/server/main.cpp b/libs/server/main.cpp index f177edb38..3a87bdbc0 100644 --- a/libs/server/main.cpp +++ b/libs/server/main.cpp @@ -29,6 +29,8 @@ #include "consensus/consensus.h" #include "consensus/merkle.h" #include "consensus/validation.h" +#include "DoubleSpendProof.h" +#include "DoubleSpendProofStorage.h" #include "hash.h" #include "init.h" #include "serverutil.h" @@ -1363,6 +1365,9 @@ bool static AlreadyHave(const CInv& inv) EXCLUSIVE_LOCKS_REQUIRED(cs_main) } case MSG_BLOCK: return Blocks::Index::exists(inv.hash); + case MSG_DOUBLESPENDPROOF: + return mempool.doubleSpendProofStorage()->exists(inv.hash) + || mempool.doubleSpendProofStorage()->isRecentlyRejectedProof(inv.hash); } // Don't know what it is, just say we already got one return true; @@ -1484,8 +1489,7 @@ void static ProcessGetData(CNode* pfrom, const Consensus::Params& consensusParam } } } - else if (inv.IsKnownType()) - { + else if (inv.IsKnownType()) { // Send stream from relay memory bool pushed = false; { @@ -1495,7 +1499,7 @@ void static ProcessGetData(CNode* pfrom, const Consensus::Params& consensusParam pfrom->PushMessage(inv.GetCommand(), (*mi).second); } } - if (!pushed && inv.type == MSG_TX) { + if (inv.type == MSG_TX) { CTransaction tx; if (mempool.lookup(inv.hash, tx)) { CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); @@ -1505,9 +1509,18 @@ void static ProcessGetData(CNode* pfrom, const Consensus::Params& consensusParam pushed = true; } } - if (!pushed) { - vNotFound.push_back(inv); + else if (inv.type == MSG_DOUBLESPENDPROOF) { + DoubleSpendProof dsp = mempool.doubleSpendProofStorage()->lookup(inv.hash); + if (!dsp.isEmpty()) { + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss.reserve(600); + ss << dsp; + pfrom->PushMessage(NetMsgType::DSPROOF, ss); + pushed = true; + } } + if (!pushed) + vNotFound.push_back(inv); } // Track requests for our stuff. @@ -1766,8 +1779,7 @@ bool static ProcessMessage(CNode* pfrom, std::string strCommand, CDataStream& vR { std::vector vInv; vRecv >> vInv; - if (vInv.size() > MAX_INV_SZ) - { + if (vInv.size() > MAX_INV_SZ) { Misbehaving(pfrom->GetId(), 20); return error("message inv size() = %u", vInv.size()); } @@ -1785,12 +1797,8 @@ bool static ProcessMessage(CNode* pfrom, std::string strCommand, CDataStream& vR LOCK(cs_main); std::vector vToFetch; - - for (unsigned int nInv = 0; nInv < vInv.size(); nInv++) - { + for (unsigned int nInv = 0; nInv < vInv.size(); nInv++) { const CInv &inv = vInv[nInv]; - - boost::this_thread::interruption_point(); pfrom->AddInventoryKnown(inv); bool fAlreadyHave = AlreadyHave(inv); @@ -1857,7 +1865,8 @@ bool static ProcessMessage(CNode* pfrom, std::string strCommand, CDataStream& vR << " to peer:" << pfrom->id; } } - else if (!fBlocksOnly && !fAlreadyHave && !fReindex) { + else if ((inv.type == MSG_DOUBLESPENDPROOF || inv.type == MSG_TX) + && !fBlocksOnly && !fAlreadyHave && !fReindex) { pfrom->AskFor(inv); } @@ -2551,7 +2560,6 @@ bool static ProcessMessage(CNode* pfrom, std::string strCommand, CDataStream& vR } } - else if (strCommand == NetMsgType::FILTERCLEAR) { if (!GetBoolArg("-peerbloomfilters", true)) { @@ -2565,6 +2573,67 @@ bool static ProcessMessage(CNode* pfrom, std::string strCommand, CDataStream& vR pfrom->fRelayTxes = true; } + else if (strCommand == NetMsgType::DSPROOF) { + uint256 hash; + try { + DoubleSpendProof dsp; + vRecv >> dsp; + if (dsp.isEmpty()) + throw std::runtime_error("DSP empty"); + + hash = dsp.createHash(); + CInv inv(MSG_DOUBLESPENDPROOF, hash); + pfrom->setAskFor.erase(inv.hash); + { + LOCK(cs_main); + mapAlreadyAskedFor.erase(hash); + } + + switch (dsp.validate(mempool, Application::instance()->validation()->tipValidationFlags(fRequireStandard))) { + case DoubleSpendProof::Valid: { + const auto tx = mempool.addDoubleSpendProof(dsp); + if (tx.size() > 0) { // added to mempool correctly, then forward to nodes. + ValidationNotifier().DoubleSpendFound(tx, dsp); + + CTransaction oldTx = tx.createOldTransaction(); + LOCK(cs_vNodes); + for (CNode* pnode : vNodes) { + if(!pnode->fRelayTxes || pnode == pfrom) + continue; + LOCK(pnode->cs_filter); + if (pnode->pfilter) { + // For nodes that we sent this Tx before, send a proof. + if (pnode->pfilter->IsRelevantAndUpdate(oldTx)) + pnode->PushInventory(inv); + } else { + pnode->PushInventory(inv); + } + } + } + break; + } + case DoubleSpendProof::MissingTransction: + logDebug(Log::Net) << "DoubleSpend Proof postponed: Missing Tx"; + mempool.doubleSpendProofStorage()->addOrphan(dsp); + break; + case DoubleSpendProof::MissingUTXO: + logDebug(Log::Net) << "DoubleSpendProof rejected due to missing UTXO (outdated?)"; + return false; + case DoubleSpendProof::Invalid: + throw std::runtime_error("Proof didn't validate"); + default: + assert(false); + return false; + } + } catch (const std::exception &e) { + logInfo(Log::Net) << "Failure handing double spend proof. Peer:" << pfrom->GetId() << "Reason:" << e; + if (!hash.IsNull()) + mempool.doubleSpendProofStorage()->markProofRejected(hash); + LOCK(cs_main); + Misbehaving(pfrom->GetId(), 10); + return false; + } + } else if (strCommand == NetMsgType::REJECT) { diff --git a/libs/server/protocol.cpp b/libs/server/protocol.cpp index cf439db24..237610574 100644 --- a/libs/server/protocol.cpp +++ b/libs/server/protocol.cpp @@ -2,6 +2,7 @@ * This file is part of the Flowee project * Copyright (c) 2009-2010 Satoshi Nakamoto * Copyright (c) 2009-2015 The Bitcoin Core developers + * Copyright (C) 2019 Tom Zander * * 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 @@ -60,6 +61,8 @@ const char *VERACK2="buverack"; const char *XPEDITEDREQUEST="req_xpedited"; const char *XPEDITEDBLK="Xb"; const char *XPEDITEDTxn="Xt"; + +const char *DSPROOF="dsproof-beta"; } static const char* ppszTypeName[] = @@ -199,22 +202,6 @@ CInv::CInv(int typeIn, const uint256& hashIn) hash = hashIn; } -CInv::CInv(const std::string& strType, const uint256& hashIn) -{ - unsigned int i; - for (i = 1; i < ARRAYLEN(ppszTypeName); i++) - { - if (strType == ppszTypeName[i]) - { - type = i; - break; - } - } - if (i == ARRAYLEN(ppszTypeName)) - throw std::out_of_range(strprintf("CInv::CInv(string, uint256): unknown type '%s'", strType)); - hash = hashIn; -} - bool operator<(const CInv& a, const CInv& b) { return (a.type < b.type || (a.type == b.type && a.hash < b.hash)); @@ -222,14 +209,23 @@ bool operator<(const CInv& a, const CInv& b) bool CInv::IsKnownType() const { - return (type >= 1 && type < (int)ARRAYLEN(ppszTypeName)); + return type >= 1 && type < 8 || type == MSG_DOUBLESPENDPROOF; } const char* CInv::GetCommand() const { - if (!IsKnownType()) - throw std::out_of_range(strprintf("CInv::GetCommand(): type=%d unknown type", type)); - return ppszTypeName[type]; + switch (type) { + case MSG_TX: return NetMsgType::TX; + case MSG_BLOCK: return NetMsgType::BLOCK; + case MSG_FILTERED_BLOCK: return "filtered block"; + case MSG_THINBLOCK: return NetMsgType::THINBLOCK; + case MSG_XTHINBLOCK: return NetMsgType::XTHINBLOCK; + case 6: return NetMsgType::XBLOCKTX; + case 7: return NetMsgType::GET_XBLOCKTX; + case MSG_DOUBLESPENDPROOF: return NetMsgType::DSPROOF; + default: + return "unknown type"; + } } std::string CInv::ToString() const diff --git a/libs/server/protocol.h b/libs/server/protocol.h index b48a287d8..dcd388242 100644 --- a/libs/server/protocol.h +++ b/libs/server/protocol.h @@ -2,6 +2,7 @@ * This file is part of the Flowee project * Copyright (C) 2009-2010 Satoshi Nakamoto * Copyright (C) 2009-2015 The Bitcoin Core developers + * Copyright (C) 2019 Tom Zander * * 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 @@ -271,7 +272,11 @@ extern const char *XPEDITEDBLK; * @since protocol version 80000 */ extern const char *XPEDITEDTXN; -}; +/** + * Double spend proof + */ +extern const char *DSPROOF; +} /* Get a vector of all valid message types (see above) */ const std::vector &getAllNetMessageTypes(); @@ -347,13 +352,11 @@ class CInv public: CInv(); CInv(int typeIn, const uint256& hashIn); - CInv(const std::string& strType, const uint256& hashIn); ADD_SERIALIZE_METHODS template - inline void SerializationOp(Stream& s, Operation ser_action, int nType, int nVersion) - { + inline void SerializationOp(Stream& s, Operation ser_action, int nType, int nVersion) { READWRITE(type); READWRITE(hash); } @@ -368,8 +371,8 @@ public: return hash; } - // TODO: make private (improves encapsulation) public: + // TODO: make private (improves encapsulation) int type; uint256 hash; }; -- GitLab From 1dbb23224cd29fcfea12f549766756898c89cc36 Mon Sep 17 00:00:00 2001 From: TomZ Date: Mon, 2 Sep 2019 23:35:43 +0200 Subject: [PATCH 5/5] Add unit tests for double spend proof --- testing/api/BlackBoxTest.cpp | 68 ++++++-- testing/api/BlackBoxTest.h | 7 + testing/api/TestAddressMonitor.cpp | 138 ++++++++++++++++ testing/api/TestAddressMonitor.h | 1 + testing/bitcoin-protocol/CMakeLists.txt | 1 + .../bitcoin-protocol/DoubleSpendProofTest.cpp | 147 ++++++++++++++++++ .../bitcoin-protocol/DoubleSpendProofTest.h | 32 ++++ testing/bitcoin-protocol/main.cpp | 5 + testing/doublespend/double_spend.cpp | 2 +- 9 files changed, 387 insertions(+), 14 deletions(-) create mode 100644 testing/bitcoin-protocol/DoubleSpendProofTest.cpp create mode 100644 testing/bitcoin-protocol/DoubleSpendProofTest.h diff --git a/testing/api/BlackBoxTest.cpp b/testing/api/BlackBoxTest.cpp index 68c492acf..ff77c5166 100644 --- a/testing/api/BlackBoxTest.cpp +++ b/testing/api/BlackBoxTest.cpp @@ -7,6 +7,18 @@ #include #include +namespace { +void writeLogsConf(const QString &nodePath) +{ + QFile logConfFile(nodePath + "logs.conf"); + bool ok = logConfFile.open(QIODevice::WriteOnly); + Q_ASSERT(ok); + QTextStream log(&logConfFile); + log << "channel file\noption timestamp time\nALL debug\n2101 quiet\n#3000 quiet\n#3001 info\n"; + logConfFile.close(); +} +} + QString BlackBoxTest::s_hubPath = QString(); BlackBoxTest::BlackBoxTest(QObject *parent) @@ -28,6 +40,8 @@ void BlackBoxTest::startHubs(int amount, Connect connect) { Q_ASSERT(m_hubs.empty()); Q_ASSERT(amount > 0); + Q_ASSERT(m_onConnectCallbacks.size() <= amount); + m_onConnectCallbacks.resize(amount); m_hubs.reserve(amount + 1); m_currentTest = QString(QTest::currentTestFunction()); m_baseDir = QDir::tempPath() + QString("/flowee-bbtest-%1").arg(rand()); @@ -55,15 +69,9 @@ void BlackBoxTest::startHubs(int amount, Connect connect) "discover=false\n"; if (connect == ConnectHubs && i > 0) - conf << "connect=127.0.0.1:" << hub.p2pPort - 2 << "\n\n"; + conf << "addnode=127.0.0.1:" << hub.p2pPort - 2 << "\n\n"; confFile.close(); - - QFile logConfFile(nodePath + "logs.conf"); - ok = logConfFile.open(QIODevice::WriteOnly); - Q_ASSERT(ok); - QTextStream log(&logConfFile); - log << "channel file\noption timestamp time\nALL debug\n2101 quiet\n#3000 quiet\n#3001 info\n"; - logConfFile.close(); + writeLogsConf(nodePath); hub.proc->setProgram(s_hubPath); hub.proc->setWorkingDirectory(nodePath); @@ -73,9 +81,13 @@ void BlackBoxTest::startHubs(int amount, Connect connect) con.push_back(std::move(m_network.connection( EndPoint(boost::asio::ip::address_v4::loopback(), hub.apiPort)))); con.back().setOnIncomingMessage(std::bind(&BlackBoxTest::Hub::addMessage, &m_hubs.back(), std::placeholders::_1)); + if (m_onConnectCallbacks.at(i)) + con.back().setOnConnected(m_onConnectCallbacks.at(i)); } MilliSleep(500); // Assuming that the hub takes half a second is better than assuming it doesn't and hitting the reconnect-time. - con.back().connect(); + for (int i = 0; i < amount; ++i) { + con.at(i).connect(); + } logDebug() << "Hubs started"; } @@ -88,6 +100,7 @@ void BlackBoxTest::feedDefaultBlocksToHub(int hubIndex) logDebug().nospace() << "Starting new hub with pre-prepared chain: node" << m_hubs.size(); QString nodePath = m_baseDir + QString("/node%1/").arg(m_hubs.size()); QDir(nodePath).mkpath("regtest/blocks"); + writeLogsConf(nodePath + "/regtest/"); QFile blk(":/blk00000.dat"); bool ok = blk.open(QIODevice::ReadOnly); Q_ASSERT(ok); @@ -97,8 +110,10 @@ void BlackBoxTest::feedDefaultBlocksToHub(int hubIndex) { QProcess proc1; proc1.setProgram(s_hubPath); - proc1.setArguments(QStringList() << "-api=false" << "-server=false" << "-regtest" << "-listen=false" - << "-datadir=." << "-reindex" << "-stopafterblockimport"); + auto args = QStringList() << "-api=false" << "-server=false" << "-regtest" << "-listen=false" + << "-datadir=." << "-reindex" << "-stopafterblockimport"; + proc1.setArguments(args); + logFatal() << args; proc1.setWorkingDirectory(nodePath); proc1.start(QProcess::ReadOnly); proc1.waitForFinished(); @@ -107,8 +122,10 @@ void BlackBoxTest::feedDefaultBlocksToHub(int hubIndex) Hub hub; hub.proc = new QProcess(this); hub.proc->setProgram(s_hubPath); - hub.proc->setArguments(QStringList() << "-api=false" << "-server=false" << "-regtest" << "-listen=false" - << "-datadir=." << QString("-connect=127.0.0.1:%1").arg(target.p2pPort)); + auto args = QStringList() << "-api=false" << "-server=false" << "-regtest" + << "-datadir=." << QString("-connect=127.0.0.1:%1").arg(target.p2pPort); + hub.proc->setArguments(args); + logFatal() << args; hub.proc->setWorkingDirectory(nodePath); hub.proc->start(QProcess::ReadOnly); m_hubs.push_back(hub); @@ -179,6 +196,30 @@ Message BlackBoxTest::waitForReply(int hubId, const Message &message, int messag } } +bool BlackBoxTest::waitForHeight(int height) +{ + QSet nodes; + for (int i = 0; i < (int) con.size(); ++i) + nodes.insert(i); + + QTime timer; + timer.start(); + while (!nodes.isEmpty() && timer.elapsed() < 30000) { + MilliSleep(100); + auto copy(nodes); + for (auto i : copy) { + auto m = waitForReply(i, Message(Api::BlockChainService, Api::BlockChain::GetBlockCount), Api::BlockChain::GetBlockCountReply); + if (m.serviceId() == Api::BlockChainService) { // not an error. + Streaming::MessageParser p(m); + p.next(); + if (p.intData() >= height) + nodes.remove(i); + } + } + } + return nodes.isEmpty(); +} + void BlackBoxTest::cleanup() { for (int i = 0; i < con.size(); ++i) { @@ -231,6 +272,7 @@ void BlackBoxTest::cleanup() m_hubs.clear(); m_currentTest.clear(); m_baseDir.clear(); + m_onConnectCallbacks.clear(); } void BlackBoxTest::Hub::addMessage(const Message &message) diff --git a/testing/api/BlackBoxTest.h b/testing/api/BlackBoxTest.h index 099b8a338..618007079 100644 --- a/testing/api/BlackBoxTest.h +++ b/testing/api/BlackBoxTest.h @@ -10,6 +10,8 @@ #include #include +#include + class QProcess; class NetworkManager; @@ -54,6 +56,9 @@ protected: */ Message waitForReply(int hub, const Message &message, int messageId, int timeout = 30000); + /// Checks if all hubs reached at least the designated height + bool waitForHeight(int height); + struct Hub { QProcess *proc; int p2pPort = 0; @@ -73,6 +78,8 @@ protected: QString m_currentTest; QString m_baseDir; static QString s_hubPath; + + std::vector > m_onConnectCallbacks; }; #endif diff --git a/testing/api/TestAddressMonitor.cpp b/testing/api/TestAddressMonitor.cpp index 40cf72db6..a02292d6e 100644 --- a/testing/api/TestAddressMonitor.cpp +++ b/testing/api/TestAddressMonitor.cpp @@ -21,8 +21,11 @@ #include #include +#include #include +#include + void TestAddressMonitor::testBasic() { startHubs(1); @@ -91,3 +94,138 @@ void TestAddressMonitor::testBasic() } QCOMPARE(total, 196); } + +void TestAddressMonitor::testDoubleSpendProof() +{ + /* + * In this test we use the existing testing chain and spend one of the outputs twice. + * Both transactions have 1 in, 1 out. + * The outputs are different and the addresses are the ones hardcoded in the onConnected() + * method so the monitor should always trigger on our sending those tx's to the nodes. + * + * We first send one tx to one node, then (after waiting for propagation) the other to + * the second node. + * Then we wait for the double spend proofs to be send to us from both nodes. + * The last node that actually saw the double spend just parrots it to us, with a transaction + * and not a proof. + * Then the other node (the first) should get a double spend as proof over p2p which we should + * get from the monitor. + */ + + // on connect, always subscribe to the two outputs that + struct MonitorAddressesInit { + explicit MonitorAddressesInit(NetworkManager *nm) : network(nm) { Q_ASSERT(nm); } + void onConnected(const EndPoint &ep) { + Streaming::BufferPool pool; + pool.reserve(50); + Streaming::MessageBuilder builder(pool); + uint160 address1; + address1.SetHex("2a6bfbe42790e346a0990e16d9a97ad56a263884"); + builder.add(Api::AddressMonitor::BitcoinAddress, address1); + uint160 address2; + address2.SetHex("226db9f47b0302a14b3bee10a892808bbfb249a4"); + builder.add(Api::AddressMonitor::BitcoinAddress, address2); + auto subscribeMessage = builder.message(Api::AddressMonitorService, Api::AddressMonitor::Subscribe); + network->connection(ep).send(subscribeMessage); + } + private: + NetworkManager *network; + }; + + MonitorAddressesInit subscriber(&m_network); + m_onConnectCallbacks.push_back(std::bind(&MonitorAddressesInit::onConnected, &subscriber, std::placeholders::_1)); + m_onConnectCallbacks.push_back(std::bind(&MonitorAddressesInit::onConnected, &subscriber, std::placeholders::_1)); + + startHubs(2); + feedDefaultBlocksToHub(0); + waitForHeight(115); // make sure all nodes are at the same tip. + + Streaming::BufferPool pool; + // two transactions that both spend the first output of the first (non-coinbase) tx on block 115 + // The spend TO the amove addresses. + pool.writeHex("0x01000000010b9d14b709aa59bd594edca17db2951c6660ebc8daa31ceae233a5550314f158000000006b483045022100b34a120e69bc933ae16c10db0f565cb2da1b80a9695a51707e8a80c9aa5c22bf02206c390cb328763ab9ab2d45f874d308af2837d6d8cfc618af76744b9eeb69c3934121022708a547a1d14ba6df79ec0f4216eeec65808cf0a32f09ad1cf730b44e8e14a6ffffffff01faa7be00000000001976a9148438266ad57aa9d9160e99a046e39027e4fb6b2a88ac00000000"); + Tx tx1(pool.commit()); + + pool.writeHex("0x01000000010b9d14b709aa59bd594edca17db2951c6660ebc8daa31ceae233a5550314f158000000006b483045022100d9d22406611228d64e6b674de8b16e802f8f789f8338130506c7741cdae9116602202dc63a4f5f9e750eec9dfc1557469bda43d3491b358484e5c25992a381048a494121022708a547a1d14ba6df79ec0f4216eeec65808cf0a32f09ad1cf730b44e8e14a6ffffffff01ea80be00000000001976a914a449b2bf8b8092a810ee3b4ba102037bf4b96d2288ac00000000"); + Tx tx2(pool.commit()); + + logDebug() << "Sending tx1 to hub0" << tx1.createHash(); + + // I sent one tx to peer zero, and wait for it to be synchronized on peer 1 as well. + m_hubs[1].m_waitForMessageId = Api::AddressMonitor::TransactionFound; + m_hubs[1].m_waitForServiceId = Api::AddressMonitorService; + m_hubs[1].m_waitForMessageId2 = -1; + m_hubs[1].m_foundMessage.store(nullptr); + + Streaming::MessageBuilder builder(pool); + builder.add(Api::LiveTransactions::GenericByteData, tx1.data()); + con[0].send(builder.message(Api::LiveTransactionService, Api::LiveTransactions::SendTransaction)); + + QTRY_VERIFY_WITH_TIMEOUT(m_hubs[1].m_foundMessage.load() != nullptr, 15000); + auto m = *m_hubs[1].m_foundMessage.load(); + QCOMPARE(m.serviceId(), (int) Api::AddressMonitorService); + QCOMPARE(m.messageId(), (int) Api::AddressMonitor::TransactionFound); + + + // now we send the second tx and expect a double-spend-notification from both peers. + // This will be propagated by proof between them. + + m_hubs[0].m_waitForMessageId = m_hubs[1].m_waitForMessageId = Api::AddressMonitor::DoubleSpendFound; + m_hubs[0].m_waitForServiceId = Api::AddressMonitorService; + m_hubs[0].m_waitForMessageId2 = -1; + m_hubs[0].m_foundMessage.store(nullptr); + m_hubs[1].m_foundMessage.store(nullptr); + + logDebug() << "Sending tx2 to hub1" << tx2.createHash(); + // double-spend-tx (same input, other output and amount) + builder = Streaming::MessageBuilder(pool); + builder.add(Api::LiveTransactions::GenericByteData, tx2.data()); + // Sent this to HUB 1 + con[1].send(builder.message(Api::LiveTransactionService, Api::LiveTransactions::SendTransaction)); + + + // from hub 1 I should have received an old fashioned double spend. THe one with TX. + QTRY_VERIFY(m_hubs[1].m_foundMessage.load() != nullptr); + m = *m_hubs[1].m_foundMessage.load(); + QCOMPARE(m.serviceId(), (int) Api::AddressMonitorService); + QCOMPARE(m.messageId(), (int) Api::AddressMonitor::DoubleSpendFound); + + Streaming::MessageParser p(m); + p.next(); + QCOMPARE(p.tag(), (uint32_t) Api::AddressMonitor::BitcoinAddress); + QCOMPARE(p.dataLength(), 20); + p.next(); + QCOMPARE(p.tag(), (uint32_t) Api::AddressMonitor::BitcoinAddress); + QCOMPARE(p.dataLength(), 20); + p.next(); + QCOMPARE(p.tag(), (uint32_t) Api::AddressMonitor::Amount); + QCOMPARE(p.longData(), (uint64_t) 12494842); + p.next(); + QCOMPARE(p.tag(), (uint32_t) Api::AddressMonitor::Amount); + QCOMPARE(p.longData(), (uint64_t) 12484842); + p.next(); + QCOMPARE(p.tag(), (uint32_t) Api::AddressMonitor::TxId); + QCOMPARE(p.dataLength(), 32); + p.next(); + QCOMPARE(p.tag(), (uint32_t) Api::AddressMonitor::GenericByteData); // the transaction. + QCOMPARE(p.dataLength(), 192); + + + // From peer 0 I get a double spend notifaction as well, but one based on the proof. + QTRY_VERIFY(m_hubs[0].m_foundMessage.load() != nullptr); + m = *m_hubs[0].m_foundMessage.load(); + + p = Streaming::MessageParser(m); + p.next(); + QCOMPARE(p.tag(), (uint32_t) Api::AddressMonitor::BitcoinAddress); + QCOMPARE(p.dataLength(), 20); + p.next(); + QCOMPARE(p.tag(), (uint32_t) Api::AddressMonitor::Amount); + QCOMPARE(p.longData(), (uint64_t) 12494842); + p.next(); + QCOMPARE(p.tag(), (uint32_t) Api::AddressMonitor::TxId); + QCOMPARE(p.dataLength(), 32); + p.next(); + QCOMPARE(p.tag(), (uint32_t) Api::AddressMonitor::GenericByteData); // the proof + QCOMPARE(p.dataLength(), 400); +} diff --git a/testing/api/TestAddressMonitor.h b/testing/api/TestAddressMonitor.h index 96c22e086..0e7af5ef7 100644 --- a/testing/api/TestAddressMonitor.h +++ b/testing/api/TestAddressMonitor.h @@ -25,6 +25,7 @@ class TestAddressMonitor : public BlackBoxTest Q_OBJECT private slots: void testBasic(); + void testDoubleSpendProof(); }; #endif diff --git a/testing/bitcoin-protocol/CMakeLists.txt b/testing/bitcoin-protocol/CMakeLists.txt index 439a276da..5fcaa62f9 100644 --- a/testing/bitcoin-protocol/CMakeLists.txt +++ b/testing/bitcoin-protocol/CMakeLists.txt @@ -55,6 +55,7 @@ add_executable(test_protocol scriptnum_tests.cpp transaction_tests.cpp transaction_utils.cpp + DoubleSpendProofTest.cpp main.cpp ) target_link_libraries(test_protocol diff --git a/testing/bitcoin-protocol/DoubleSpendProofTest.cpp b/testing/bitcoin-protocol/DoubleSpendProofTest.cpp new file mode 100644 index 000000000..fd283e639 --- /dev/null +++ b/testing/bitcoin-protocol/DoubleSpendProofTest.cpp @@ -0,0 +1,147 @@ +/* + * This file is part of the Flowee project + * Copyright (C) 2019 Tom Zander + * + * 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. If not, see . + */ + +#include "DoubleSpendProofTest.h" + +#include +#include +#include +#include + +#include +#include + +namespace { + void createDoubleSpend(const Tx &in, int outIndex, const CKey &key, Tx &out1, Tx &out2) + { + auto out = in.output(outIndex); + assert(out.outputValue >= 0); + { + TransactionBuilder builder; + builder.appendInput(in.createHash(), 0); + builder.pushInputSignature(key, out.outputScript, out.outputValue); + builder.appendOutput(50 * COIN); + CKey k; + k.MakeNewKey(); + builder.pushOutputPay2Address(k.GetPubKey().GetID()); + out1 = builder.createTransaction(); + } + { + TransactionBuilder builder; + builder.appendInput(in.createHash(), 0); + builder.pushInputSignature(key, out.outputScript, out.outputValue); + builder.appendOutput(50 * COIN); + CKey k; + k.MakeNewKey(); + builder.pushOutputPay2Address(k.GetPubKey().GetID()); + out2 = builder.createTransaction(); + } + } +} + +void DoubleSpendProofTest::basic() +{ + CKey key; + key.MakeNewKey(); + std::vector blocks = bv->appendChain(101, key, MockBlockValidation::FullOutScript); + blocks.front().findTransactions(); + const Tx coinbase = blocks.front().transactions().at(0); + + Tx first, second; + createDoubleSpend(coinbase, 0, key, first, second); + + DoubleSpendProof dsp = DoubleSpendProof::create(first, second); + QVERIFY(!dsp.isEmpty()); + QCOMPARE(dsp.prevTxId(), coinbase.createHash()); + QCOMPARE(dsp.prevOutIndex(), 0); + + auto s1 = dsp.firstSpender(); + QCOMPARE(s1.lockTime, (uint32_t) 0); + QCOMPARE(s1.txVersion, (uint32_t) 1); + QCOMPARE(s1.outSequence, (uint32_t) 0xFFFFFFFF); + QVERIFY(s1.pushData.size() == 1); + QVERIFY(s1.pushData.front().size() > 70); + QCOMPARE(s1.pushData.front().back(), (uint8_t) 65); + QVERIFY(!s1.hashOutputs.IsNull()); + QVERIFY(!s1.hashSequence.IsNull()); + QVERIFY(!s1.hashPrevOutputs.IsNull()); + + auto s2 = dsp.doubleSpender(); + QCOMPARE(s2.lockTime, (uint32_t) 0); + QCOMPARE(s2.txVersion, (uint32_t) 1); + QCOMPARE(s2.outSequence, (uint32_t) 0xFFFFFFFF); + QVERIFY(s2.pushData.size() == 1); + QVERIFY(s2.pushData.front().size() > 70); + QCOMPARE(s2.pushData.front().back(), (uint8_t) 65); + QVERIFY(!s2.hashOutputs.IsNull()); + QVERIFY(!s2.hashSequence.IsNull()); + QVERIFY(!s2.hashPrevOutputs.IsNull()); + + // Will fail on MissingTransaction because we didn't add anything to the mempool yet. + QCOMPARE(dsp.validate(*bv->mempool(), bv->tipValidationFlags()), DoubleSpendProof::MissingTransction); + + // add one to the mempool. + bv->mempool()->insertTx(first); + QCOMPARE(dsp.validate(*bv->mempool(), bv->tipValidationFlags()), DoubleSpendProof::Valid); +} + +void DoubleSpendProofTest::proofOrder() +{ + CKey key; + key.MakeNewKey(); + std::vector blocks = bv->appendChain(101, key, MockBlockValidation::FullOutScript); + blocks.front().findTransactions(); + const Tx coinbase = blocks.front().transactions().at(0); + + Tx first, second; + createDoubleSpend(coinbase, 0, key, first, second); + DoubleSpendProof dsp1 = DoubleSpendProof::create(first, second); + DoubleSpendProof dsp2 = DoubleSpendProof::create(second, first); + + // now, however we process them, the result is the same. + QCOMPARE(dsp1.firstSpender().pushData.front(), dsp2.firstSpender().pushData.front()); + QCOMPARE(dsp1.doubleSpender().pushData.front(), dsp2.doubleSpender().pushData.front()); +} + +void DoubleSpendProofTest::serialization() +{ + CKey key; + key.MakeNewKey(); + std::vector blocks = bv->appendChain(101, key, MockBlockValidation::FullOutScript); + blocks.front().findTransactions(); + const Tx coinbase = blocks.front().transactions().at(0); + + Tx first, second; + createDoubleSpend(coinbase, 0, key, first, second); + DoubleSpendProof dsp1 = DoubleSpendProof::create(first, second); + + CDataStream stream(SER_NETWORK, PROTOCOL_VERSION); + stream << dsp1; + const std::vector blob(stream.begin(), stream.end()); + // logFatal() << blob.size(); + + DoubleSpendProof dsp2; + CDataStream restore(blob, SER_NETWORK, PROTOCOL_VERSION); + restore >> dsp2; + + QCOMPARE(dsp1.createHash(), dsp2.createHash()); + + // check if the second one validates + bv->mempool()->insertTx(second); + QCOMPARE(dsp2.validate(*bv->mempool(), bv->tipValidationFlags()), DoubleSpendProof::Valid); +} diff --git a/testing/bitcoin-protocol/DoubleSpendProofTest.h b/testing/bitcoin-protocol/DoubleSpendProofTest.h new file mode 100644 index 000000000..299450cb0 --- /dev/null +++ b/testing/bitcoin-protocol/DoubleSpendProofTest.h @@ -0,0 +1,32 @@ +/* + * This file is part of the Flowee project + * Copyright (C) 2019 Tom Zander + * + * 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. If not, see . + */ +#ifndef DOUBLESPENDPROOFTEST_H +#define DOUBLESPENDPROOFTEST_H + +#include + +class DoubleSpendProofTest : public TestFloweeSession +{ + Q_OBJECT +private slots: + void basic(); + void proofOrder(); + void serialization(); +}; + +#endif diff --git a/testing/bitcoin-protocol/main.cpp b/testing/bitcoin-protocol/main.cpp index a6a774ca0..113152122 100644 --- a/testing/bitcoin-protocol/main.cpp +++ b/testing/bitcoin-protocol/main.cpp @@ -23,6 +23,7 @@ #include "script_tests.h" #include "scriptnum_tests.h" #include "transaction_tests.h" +#include "DoubleSpendProofTest.h" int main(int x, char **y) { @@ -59,5 +60,9 @@ int main(int x, char **y) TransactionTests test; rc = QTest::qExec(&test); } + if (!rc) { + DoubleSpendProofTest test; + rc = QTest::qExec(&test); + } return rc; } diff --git a/testing/doublespend/double_spend.cpp b/testing/doublespend/double_spend.cpp index 709d5614a..c695c26a2 100644 --- a/testing/doublespend/double_spend.cpp +++ b/testing/doublespend/double_spend.cpp @@ -87,7 +87,7 @@ void TestDoubleSpend::test() future = bv->addTransaction(duplicate); result = future.get(); QCOMPARE(result, std::string("258: txn-mempool-conflict")); - QVERIFY(myValidatioInterface.first.isValid()); + QTRY_COMPARE(myValidatioInterface.first.isValid(), true); QCOMPARE(HexStr(myValidatioInterface.first.createHash()), HexStr(first.createHash())); QCOMPARE(HexStr(myValidatioInterface.duplicate.createHash()), HexStr(duplicate.createHash())); } -- GitLab