Files
thehub/hub/server/DoubleSpendProof.cpp

427 lines
16 KiB
C++

/*
* This file is part of the Flowee project
* Copyright (C) 2019-2021 Tom Zander <tom@flowee.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "Application.h"
#include "DoubleSpendProof.h"
#include "UnspentOutputData.h"
#include "txmempool.h"
#include "script/interpreter.h"
#include "validation/Engine.h"
#include <utxo/UnspentOutputDatabase.h>
#include <hash.h>
#include <primitives/PublicKey.h>
#include <fstream>
#include <streaming/P2PParser.h>
namespace {
enum ScriptType {
P2PKH
};
void getP2PKHSignature(const CScript &script, std::vector<uint8_t> &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.finalizeHash();
}
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.finalizeHash();
}
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.finalizeHash();
} else if ((hashType & 0x1f) == SIGHASH_SINGLE && inputIndex < int(tx.vout.size())) {
CHashWriter ss(SER_GETHASH, 0);
ss << tx.vout[inputIndex];
spender.hashOutputs = ss.finalizeHash();
}
}
class DSPSignatureChecker : public BaseSignatureChecker {
public:
DSPSignatureChecker(const DoubleSpendProof *proof, const DoubleSpendProof::Spender &spender, int64_t amount)
: m_proof(proof),
m_spender(spender),
m_amount(amount)
{
}
bool CheckSig(const std::vector<uint8_t> &vchSigIn, const std::vector<uint8_t> &vchPubKey,
const CScript &scriptCode, uint32_t /*flags*/, size_t *bytesHashed = nullptr) const override {
if (bytesHashed)
*bytesHashed = 0;
PublicKey pubkey(vchPubKey);
if (!pubkey.isValid())
return false;
std::vector<uint8_t> 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<const CScriptBase &>(scriptCode);
ss << m_amount << m_spender.outSequence << m_spender.hashOutputs;
ss << m_spender.lockTime << (int) m_spender.pushData.front().back();
if (bytesHashed)
*bytesHashed = ss.GetNumBytesWritten();
const uint256 sighash = ss.finalizeHash();
if (vchSig.size() == 64)
return pubkey.verifySchnorr(sighash, vchSig);
return pubkey.verifyECDSA(sighash, vchSig);
}
bool CheckLockTime(const CScriptNum&) const override {
return true;
}
bool CheckSequence(const CScriptNum&) const override {
return true;
}
const DoubleSpendProof *m_proof;
const DoubleSpendProof::Spender &m_spender;
const int64_t m_amount;
};
std::vector<uint8_t> readP2PPushData(Streaming::P2PParser &parser)
{
const uint64_t count = parser.readCompactInt();
if (count != 1)
throw Streaming::ParsingException("DSProof must contain exactly one pushData item");
const uint64_t vectorSize = parser.readCompactInt();
if (vectorSize == 0 || vectorSize > DoubleSpendProof::MaxPushDataSize)
throw Streaming::ParsingException("DSProof pushData size out of range");
return parser.readUnsignedBytes(static_cast<int32_t>(vectorSize));
}
}
// 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;
bool found = false;
for (;!found && inputIndex1 < t1.vin.size(); ++inputIndex1) {
const CTxIn &in1 = t1.vin.at(inputIndex1);
for (inputIndex2 = 0; 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());
assert(!s1.pushData.empty()); // we resized it
assert(!s2.pushData.empty()); // we resized it
if (s1.pushData.front().empty() || s2.pushData.front().empty())
throw std::runtime_error("scriptSig has no signature");
auto hashType = s1.pushData.front().back();
if (!(hashType & SIGHASH_FORKID))
throw std::runtime_error("Tx1 Not a Bitcoin Cash P2PKH transaction");
if (hashType & SIGHASH_UTXOS)
throw std::runtime_error("Tx1 uses unsupported SIGHASH_UTXOS");
hashType = s2.pushData.front().back();
if (!(hashType & SIGHASH_FORKID))
throw std::runtime_error("Tx2 Not a Bitcoin Cash P2PKH transaction");
if (hashType & SIGHASH_UTXOS)
throw std::runtime_error("Tx2 uses unsupported SIGHASH_UTXOS");
found = true;
break;
}
}
}
if (!found)
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 so the proof stays the same, independent of the order of tx seen first
int diff = s1.hashOutputs.Compare(s2.hashOutputs);
if (diff == 0)
diff = s1.hashPrevOutputs.Compare(s2.hashPrevOutputs);
if (diff > 0)
std::swap(s1, s2);
// Finally, ensure that we can eat our own dog food -- this should always succeed,
// it is a programming error if it does not.
answer.checkSanityOrThrow();
return answer;
}
DoubleSpendProof DoubleSpendProof::load(const Streaming::ConstBuffer &buffer)
{
DoubleSpendProof dsp;
Streaming::P2PParser parser(buffer);
dsp.m_prevTxId = parser.readUint256();
dsp.m_prevOutIndex = parser.readInt();
dsp.m_spender1.txVersion = parser.readInt();
dsp.m_spender1.outSequence = parser.readInt();
dsp.m_spender1.lockTime = parser.readInt();
dsp.m_spender1.hashPrevOutputs = parser.readUint256();
dsp.m_spender1.hashSequence = parser.readUint256();
dsp.m_spender1.hashOutputs = parser.readUint256();
dsp.m_spender1.pushData.push_back(readP2PPushData(parser));
dsp.m_spender2.txVersion = parser.readInt();
dsp.m_spender2.outSequence = parser.readInt();
dsp.m_spender2.lockTime = parser.readInt();
dsp.m_spender2.hashPrevOutputs = parser.readUint256();
dsp.m_spender2.hashSequence = parser.readUint256();
dsp.m_spender2.hashOutputs = parser.readUint256();
dsp.m_spender2.pushData.push_back(readP2PPushData(parser));
return dsp;
}
DoubleSpendProof::DoubleSpendProof()
{
}
bool DoubleSpendProof::isEmpty() const
{
return m_prevOutIndex == -1 || m_prevTxId.IsNull();
}
DoubleSpendProof::Validity DoubleSpendProof::validate(const CTxMemPool &mempool) 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;
try { // Check basics. No DSP should ever violate those.
checkSanityOrThrow();
} catch (const std::runtime_error &e) {
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.
int64_t amount;
CScript prevOutScript;
Tx prevTx;
if (mempool.lookup(m_prevTxId, prevTx)) {
auto output = prevTx.output(m_prevOutIndex);
if (output.outputValue < 0 || output.outputScript().isEmpty())
return Invalid;
amount = output.outputValue;
prevOutScript = output.outputScript();
} else {
auto prevTxData = mempool.utxo()->find(m_prevTxId, m_prevOutIndex);
if (!prevTxData.isValid()) {
/* if the output we spend is missing then either the tx just got mined
* or, more likely, our mempool just doesn't have it.
*/
return MissingUTXO;
}
UnspentOutputData data = UnspentOutputData::fromUtxoDB(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)) {
return MissingTransaction;
}
/*
* 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<uint8_t> 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()) {
logFatal(Log::DSProof) << "DoubleSpendProof; pubkey is empty..."
<< createHash() << "prevTxId:" << prevTx.createHash() << "|" << m_prevOutIndex;
try {
std::string filename = "/data/tx-" + m_prevTxId.ToString();
std::ofstream out(filename);
out.write(prevTx.data().begin(), prevTx.size());
} catch (std::exception &e) {
logFatal(Log::DSProof) << "DSP tx-save to disk failed" << e;
}
try {
std::string filename = "/data/dsp-" + createHash().ToString();
std::ofstream out(filename);
out << this;
} catch (std::exception &e) {
logFatal(Log::DSProof) << "DSP save to disk failed" << e;
}
return Invalid;
}
CScript inScript;
if (scriptType == P2PKH) {
inScript << m_spender1.pushData.front();
inScript << pubkey;
}
DSPSignatureChecker checker1(this, m_spender1, amount);
Script::State state(Application::instance()->validation()->tipValidationFlags());
if (!Script::verify(inScript, prevOutScript, checker1, state)) {
logDebug(Log::DSProof) << "DoubleSpendProof failed validating first tx due to" << state.errorString();
return Invalid;
}
inScript.clear();
if (scriptType == P2PKH) {
inScript << m_spender2.pushData.front();
inScript << pubkey;
}
DSPSignatureChecker checker2(this, m_spender2, amount);
if (!Script::verify(inScript, prevOutScript, checker2, state)) {
logDebug(Log::DSProof) << "DoubleSpendProof failed validating second tx due to" << state.errorString();
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);
}
void DoubleSpendProof::checkSanityOrThrow() const
{
if (isEmpty())
throw std::runtime_error("DSProof is empty");
// Check limits for both pushData vectors above
for (auto *pushData : {&m_spender1.pushData, &m_spender2.pushData}) {
// Message must contain exactly 1 pushData
if (pushData->size() != 1)
throw std::runtime_error("DSProof must contain exactly 1 pushData");
// Script data must be within size limits (520 bytes)
if (!pushData->empty() && pushData->front().size() > MaxPushDataSize)
throw std::runtime_error("DSProof script size limit exceeded");
}
if (m_spender1.pushData.front() == m_spender2.pushData.front())
throw std::runtime_error("DSProof noticed both sides are the same");
}