/* * This file is part of the Bitcoin project * Copyright (C) 2016-2024 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 "Transaction.h" #include "StreamMethods.h" #include #include #include 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(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(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 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 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 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; }