Files
transactions/Transaction.cpp
2024-10-08 15:45:10 +02:00

670 lines
22 KiB
C++

/*
* This file is part of the Bitcoin project
* Copyright (C) 2016-2024 Tom Zander <tomz@freedommail.ch>
*
* 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 "Transaction.h"
#include "StreamMethods.h"
#include <QFile>
#include <QDebug>
#include <QCryptographicHash>
namespace {
void printHex(const char *data, int index, QTextStream &out)
{
const unsigned char k = (unsigned char)(data[index]);
if (k < 16)
out << '0';
out << QString::number(k, 16);
}
}
Transaction::Transaction()
: m_version(-1),
m_nLockTime(0)
{
}
bool Transaction::read(const QString &filename)
{
qDebug() << "reading" << filename;
QFile in(filename);
if (!in.open(QIODevice::ReadOnly)) {
qWarning() << "Failed to open input" << filename;
return false;
}
QByteArray bytes = in.readAll();
in.close();
if (bytes.isEmpty()) {
qWarning() << "Empty input file";
return false;
}
return read(bytes);
}
bool Transaction::read(const QByteArray &bytes)
{
return parseTransaction(bytes);
}
void Transaction::debug() const
{
QTextStream out(stdout);
out << "{\ninputs: [\n";
foreach (const TxIn &tx, m_inputs) {
out << " {\n txid: " << tx.transaction.toHex() << Qt::endl;
out << " vout: " << tx.prevIndex << Qt::endl;
if (tx.sequence & (1 << 31)) {
out << " sequence: " << QString::number(tx.sequence, 16) << Qt::endl;
} else {
if (tx.sequence & (1 << 22)) {
quint32 time = tx.sequence & 0xFFFF;
quint32 timeout = time * 512;
const int SECSPERHOUR = 3600;
const int SECSPERDAY = SECSPERHOUR * 24;
const int SECSPERWEEK = SECSPERDAY * 7;
out << " time-based-relative-locktime: " << QString::number(time, 16)
<< " (" << timeout << "sec / ";
if (timeout > SECSPERWEEK) {
out << (timeout / SECSPERWEEK) << " weeks ";
timeout -= (timeout / SECSPERWEEK) * SECSPERWEEK;
}
if (timeout > SECSPERDAY) {
out << (timeout / SECSPERDAY) << " days ";
timeout -= (timeout / SECSPERDAY) * SECSPERDAY;
}
if (timeout > SECSPERHOUR) {
out << (timeout / SECSPERHOUR) << " hours ";
timeout -= (timeout / SECSPERHOUR) * SECSPERHOUR;
}
if (timeout > 60) {
out << (timeout / 60) << " minutes ";
timeout -= (timeout / 60) * 60;
}
if (timeout > 0) {
out << timeout << " seconds";
}
out << ")\n";
} else if (tx.sequence != 0) {
out << " block-based-relative-locktime: " << QString::number(tx.sequence & 0xFFFF) << Qt::endl;
}
}
out << " script: ";
debugScript(tx.inputScript, 12, out);
checkInScript(tx.inputScript, 12, out);
out << " }\n";
}
out << "]\n";
out << "outputs: [\n";
foreach (const TxOut &tx, m_outputs) {
out << " {\n amount: " << tx.value << Qt::endl;
out << " script: ";
debugScript(tx.script, 12, out);
out << " }\n";
}
out << "]\nversion: " << m_version << "\nnLockTime: " << m_nLockTime << "\n";
out << "txid: ";
for (int i = 31; i >= 0; --i) {
assert(m_txid.size() == 32);
printHex(m_txid.constData(), i, out);
}
out << "\n}\n";
}
void Transaction::debugScript(const QByteArray &script, int textIndent, QTextStream &out)
{
const int length = script.length();
if (length == 0) {
out << "\"\"" << Qt::endl;
return;
}
const char *data = script.constData();
int pos = 0;
QString indent;
for (int i = 0; i < textIndent; ++i) indent += ' ';
bool linefeed = false;
constexpr int MinTokenSize = /* prefix */ 1 + /* category */ + 32 + /* flags */ 1;
if (script.size() > MinTokenSize
&& static_cast<uint8_t>(script.begin()[0]) == 0xef) {
/*
* This output holds a cash-token.
* We eat the bytes that hold the cashtoken data and continue
* identifying the script.
*/
const uint8_t* tokenData = reinterpret_cast<const uint8_t*>(script.constData());
QByteArray category;
category.resize(32);
for (int i = 0; i < 32; ++i) { // but WHY!!!
category[i] = data[pos + 32 - i];
}
out << "TokenId: " << category.toHex() << Qt::endl;
int8_t flags = data[33];
int skipAmount = MinTokenSize;
if ((flags & 0x40) == 0x40) { // has-commitment-length bit is set.
int commitmentLength = tokenData[skipAmount];
skipAmount += 1 + commitmentLength;
out << indent << " +- " << commitmentLength << " byte committment" << Qt::endl;
}
if ((flags & 0x10) == 0x10) { // has-amount bit is set.
int x = skipAmount;
out << indent << " +- Amount: "
<< Streaming::fetchBitcoinCompact(data, skipAmount);
const uint8_t amount = tokenData[x++];
if (amount == 253) // compact-size design.
x += 2;
else if (amount == 254)
x += 4;
else if (amount == 255)
x += 8;
assert(x == skipAmount);
}
if (script.size() <= skipAmount) {
qWarning() << "Invalid token data";
return;
}
pos = skipAmount;
out << Qt::endl << indent;
}
while (pos < length) {
const unsigned char k = (unsigned char)(data[pos]);
if (linefeed) {
if (k == 103) // OP_ELSE
out << indent.left(indent.size() -2);
else
out << indent;
}
if (k > 0 && k < 75) {
out << "OP_DATA_" << k << " ";
if (k > length - pos - 1) {
out << " ERROR push too large for data: " << k << " is too large. We only have "
<< (length - pos -1) << " bytes in output\n";
return;
}
for (int i = 0; i < k; ++i) {
pos++;
printHex(data, pos, out);
}
linefeed = true;
} else {
linefeed = true;
switch (k) {
case 0:
out << "OP_0 (push empty item to stack)"; break;
case 76:
case 77:
case 78: {
const int width = k - 75;
out << "OP_PUSHDATA" << width << " ";
quint32 bytes = (quint8) data[++pos];
if (width > 1) {
bytes = bytes << 8;
bytes += data[++pos];
if (width > 2) {
bytes = bytes << 8;
bytes += data[++pos];
bytes = bytes << 8;
bytes += data[++pos];
}
}
if (bytes >= (unsigned int) length || (int) bytes + pos >= length) { // double check to avoid overflows
out << "\nFAILED; the OP_PUSHDATA says its " << bytes << " bytes, thats more than we have\n";
return;
}
out << bytes << " ";
for (unsigned int i = 0; i < bytes; ++i) {
pos++;
printHex(data, pos, out);
}
break;
}
case 79:
out << "OP_1NEGATE"; break;
case 80:
out << "OP_RESERVED"; break;
case 81:
out << "OP_TRUE"; break;
case 82: case 83: case 84: case 85: case 86: case 87: case 88: case 89: case 90:
case 91: case 92: case 93: case 94: case 95: case 96: {
int count = k - 80;
out << "OP_" << count << " (pushes " << count << " on stack)";
break;
}
case 97:
out << "OP_NOP"; break;
case 99:
out << "OP_IF";
indent += " ";
break;
case 100:
out << "OP_NOTIF"; break;
case 103:
out << "OP_ELSE"; break;
case 104:
out << "OP_ENDIF";
indent = indent.left(indent.size() - 2);
break;
case 105:
out << "OP_VERIFY"; break;
case 106:
out << "OP_RETURN ";
++pos;
if (script.size() >= pos + 4) {
bool isASCII = true;
for (int i = 0; i < 4; ++i) {
if (!QChar(script.at(pos + i + 1)).isLetter())
isASCII = false;
}
if (isASCII) {
out << '[' << script.mid(pos + 1, pos + 3) << ']' << Qt::endl;
out << indent << ' ' << script.mid(pos + 5).toHex() << Qt::endl;
return;
}
}
out << script.mid(pos + 1).toHex() << Qt::endl;
return;
case 107:
out << "OP_TOALTSTACK"; break;
case 108:
out << "OP_FROMALTSTACK"; break;
case 115:
out << "OP_IFDUP"; break;
case 116:
out << "OP_DEPTH"; break;
case 117:
out << "OP_DROP"; break;
case 118:
out << "OP_DUP"; break;
case 119:
out << "OP_NIP"; break;
case 120:
out << "OP_OVER"; break;
case 121:
out << "OP_PICK"; break;
case 122:
out << "OP_ROLL"; break;
case 123:
out << "OP_ROT"; break;
case 124:
out << "OP_SWAP"; break;
case 125:
out << "OP_TUCK"; break;
case 109:
out << "OP_2DROP"; break;
case 110:
out << "OP_2DUP"; break;
case 111:
out << "OP_3DUP"; break;
case 112:
out << "OP_2OVER"; break;
case 113:
out << "OP_2ROT"; break;
case 114:
out << "OP_2SWAP"; break;
case 126:
out << "OP_CAT"; break;
case 127:
out << "OP_SPLIT"; break;
case 128:
out << "OP_NUM2BIN"; break;
case 129:
out << "OP_BIN2NUM"; break;
case 130:
out << "OP_SIZE"; break;
case 131:
out << "OP_INVERT [ILLEGAL!]"; break;
case 132:
out << "OP_AND [ILLEGAL!]"; break;
case 133:
out << "OP_OR [ILLEGAL!]"; break;
case 134:
out << "OP_XOR [ILLEGAL!]"; break;
case 135:
out << "OP_EQUAL"; break;
case 136:
out << "OP_EQUALVERIFY"; break;
case 139:
out << "OP_1ADD"; break;
case 140:
out << "OP_1SUB"; break;
case 141:
out << "OP_2MUL [ILLEGAL]"; break;
case 142:
out << "OP_2DIV [ILLEGAL]"; break;
case 143:
out << "OP_NEGATE"; break;
case 144:
out << "OP_ABS"; break;
case 145:
out << "OP_NOT"; break;
case 146:
out << "OP_0NOTEQUAL"; break;
case 147:
out << "OP_ADD"; break;
case 148:
out << "OP_SUB"; break;
case 149:
out << "OP_MUL [ILLEGAL]"; break;
case 150:
out << "OP_DIV [ILLEGAL]"; break;
case 151:
out << "OP_MOD [ILLEGAL]"; break;
case 152:
out << "OP_LSHIFT [ILLEGAL]"; break;
case 153:
out << "OP_RSHIFT [ILLEGAL]"; break;
case 154:
out << "OP_BOOLAND"; break;
case 155:
out << "OP_BOOLOR"; break;
case 156:
out << "OP_NUMEQUAL"; break;
case 157:
out << "OP_NUMEQUALVERIFY"; break;
case 158:
out << "OP_NUMNOTEQUAL"; break;
case 159:
out << "OP_LESSTHAN"; break;
case 160:
out << "OP_GREATERTHAN"; break;
case 161:
out << "OP_LESSTHANOREQUAL"; break;
case 162:
out << "OP_GREATERTHANOREQUAL"; break;
case 163:
out << "OP_MIN"; break;
case 164:
out << "OP_MAX"; break;
case 165:
out << "OP_WITHIN"; break;
case 171:
out << "OP_CODESEPARATOR"; break;
case 172:
out << "OP_CHECKSIG"; break;
case 173:
out << "OP_CHECKSIGVERIFY"; break;
case 174:
out << "OP_CHECKMULTISIG"; break;
case 175:
out << "OP_CHECKMULTISIGVERIFY"; break;
case 177:
out << "OP_CHECKLOCKTIMEVERIFY"; break;
case 178:
out << "OP_CHECKSEQUENCEVERIFY"; break;
case 253:
out << "OP_PUBKEYHASH [ILLEGAL]"; break;
case 254:
out << "OP_PUBKEY [ILLEGAL]"; break;
case 255:
out << "OP_INVALIDOPCODE [ILLEGAL]"; break;
case 98:
out << "OP_VER"; break;
case 101:
out << "OP_VERIF"; break;
case 102:
out << "OP_VERNOTIF"; break;
case 137:
out << "OP_RESERVED1"; break;
case 138:
out << "OP_RESERVED2"; break;
case 176: case 179: case 180: case 181: case 182: case 183: case 184: case 185:
out << "OP_NOP{}"; break;
case 0xA9:
out << "OP_HASH160 ";
break;
case 166:
out << "OP_RIPEMD160 ";
break;
case 167:
out << "OP_SHA1 ";
break;
case 168:
out << "OP_SHA256 ";
break;
case 170:
out << "OP_HASH256 ";
break;
case 187:
out << "OP_CHECKDATASIGVERIFY"; break;
case 188:
out << "OP_REVERSEBYTES"; break;
// introspection;
case 192:
out << "OP_INPUTINDEX"; break;
case 193:
out << "OP_ACTIVEBYTECODE"; break;
case 194:
out << "OP_TXVERSION"; break;
case 195:
out << "OP_TXINPUTCOUNT"; break;
case 196:
out << "OP_TXOUTPUTCOUNT"; break;
case 197:
out << "OP_TXLOCKTIME"; break;
case 198:
out << "OP_UTXOVALUE"; break;
case 199:
out << "OP_UTXOBYTECODE"; break;
case 200:
out << "OP_OUTPOINTTXHASH"; break;
case 201:
out << "OP_OUTPOINTINDEX"; break;
case 202:
out << "OP_INPUTBYTECODE"; break;
case 203:
out << "OP_INPUTSEQUENCENUMBER"; break;
case 204:
out << "OP_OUTPUTVALUE"; break;
case 205:
out << "OP_OUTPUTBYTECODE"; break;
// cash tokens.
case 206:
out << "OP_INPUTINDEX"; break;
case 207:
out << "OP_UTXOTOKENCOMMITMENT"; break;
case 208:
out << "OP_UTXOTOKENAMOUNT"; break;
case 209:
out << "OP_OUTPUTTOKENCATEGORY"; break;
case 210:
out << "OP_OUTPUTTOKENCOMMITMENT"; break;
case 211:
out << "OP_OUTPUTTOKENAMOUNT"; break;
default:
out << "UNKNOWN: " << QString::number(k, 16);
}
}
if (linefeed)
out << Qt::endl;
++pos;
}
}
void Transaction::checkInScript(const QByteArray &script, int textIndent, QTextStream &out)
{
const int length = script.length();
const char *data = script.constData();
int pos = 0;
QString indent;
for (int i = 0; i < textIndent; ++i) indent += ' ';
QList<QByteArray> items;
while (pos < length) {
const unsigned char k = (unsigned char)(data[pos]);
if (k <= 75) { // op-data
items.append(script.mid(++pos, k));
pos += k;
} else if (k <= 78) {
// op push-data-x
const int pushType = k - 75;
int bytes = (quint8) data[++pos];
if (pushType > 1) {
bytes = bytes << 8;
bytes += data[++pos];
if (pushType > 2) {
bytes = bytes << 8;
bytes += data[++pos];
bytes = bytes << 8;
bytes += data[++pos];
}
}
++pos;
assert(length - pos >= bytes);
items.append(script.mid(pos, bytes));
pos += bytes;
} else {
out << " lint: ERROR: non-push item in input!" << Qt::endl;
items.clear();
break;
}
}
if (items.size() == 2) {
auto sig = items.at(0);
auto pk = items.at(1);
if (sig.size() > 7 && sig.size() <= 73
&& (sig.at(0) == 0x30 || sig.size() == 65)
&& pk.size() > 10) {
// lets conclude this is the most common pair. A P2PKH input
out << " lint: P2PKH (";
uint8_t hashType = sig.at(sig.size() -1);
switch (hashType & 0x1F) {
case 1: out << "SIGHASH_ALL "; break;
case 2: out << "SIGHASH_NONE "; break;
case 3: out << "SIGHASH_SINGLE "; break;
default: out << "SIGHASH unknown: " << (hashType & 0x1F); break;
}
if (hashType & 0x20)
out << "SIGHASH_UTXO ";
if (hashType & 0x40 )
out << "SIGHASH_FORKID ";
if (hashType & 0x80)
out << "SIGHASH_ANYONECANPAY ";
out << ")" << Qt::endl;
}
}
if (items.size() > 3 && items.last().size() > 20) {
out << indent << "\\_" << Qt::endl;
out << indent << " P2SH likely. Trying to parse. Ignore errors if this isn't p2sh" << Qt::endl;
out << indent << " ";
debugScript(items.last(), textIndent + 4, out);
}
}
bool Transaction::parseTransaction(const QByteArray &bytes)
{
const int length = bytes.length();
const char *data = bytes.constData();
m_version = Streaming::fetch32bitValue(data, 0);
Q_ASSERT(data[0] <= 2);
Q_ASSERT(data[1] == 0);
Q_ASSERT(data[2] == 0);
Q_ASSERT(data[3] == 0);
int pos = 4;
quint64 count = Streaming::fetchBitcoinCompact(data, pos);
QList<TxIn> inputs;
for (unsigned int i = 0; i < count; ++i) {
TxIn tx;
tx.transaction.resize(32);
for (int i = 0; i < 32; ++i) { // but WHY!!!
tx.transaction[i] = data[pos + 31 - i];
}
pos += 32;
tx.prevIndex = Streaming::fetch32bitValue(data, pos);
pos += 4;
quint32 scriptLength = Streaming::fetchBitcoinCompact(data, pos);
if (scriptLength >= (unsigned int) (length - pos)) {
qWarning() << "ScriptLength (in/" << i << ") out of bounds";
return false;
}
tx.inputScript = QByteArray(data + pos, scriptLength);
pos += scriptLength;
tx.sequence = Streaming::fetch32bitValue(data, pos);
pos += 4;
inputs.append(tx);
if (pos >= length) {
qWarning() << "Tx truncated while reading inputs";
return false;
}
}
count = Streaming::fetchBitcoinCompact(data, pos);
if (pos >= length) {
qWarning() << "Tx truncated, can't find outputs";
return false;
}
QList<TxOut> outputs;
for (unsigned int i = 0; i < count; ++i) {
TxOut tx;
tx.value = Streaming::fetch64bitValue(data, pos);
// qDebug() << "value" << tx.value;
pos += 8;
quint32 scriptLength = Streaming::fetchBitcoinCompact(data, pos);
if (scriptLength >= (unsigned int) (length - pos)) {
qWarning() << "ScriptLength (output/" << i << ") out of bounds";
return false;
}
tx.script = QByteArray(data + pos, scriptLength);
pos += scriptLength;
outputs.append(tx);
if (pos >= length) {
qWarning() << "Tx truncated while reading outputs";
return false;
}
}
if (pos + 4 != length) {
qWarning() << "length of tx incorrect (" << length << ", expected" << pos + 4 << ")";
return false;
}
m_nLockTime = Streaming::fetch32bitValue(data, pos);
m_inputs= inputs;
m_outputs = outputs;
// the TXID calc in Bitcoin is weird.
// - the data is hashed
// - the hash is then hashed again
// - the result is then printed in reverse. (back to front)
QCryptographicHash hasher(QCryptographicHash::Sha256);
hasher.addData(bytes);
auto intermediateHash = hasher.result();
hasher.reset();
hasher.addData(intermediateHash);
m_txid = hasher.resultView().toByteArray();
assert(m_txid.length() == 32);
return true;
}