513 lines
16 KiB
C++
513 lines
16 KiB
C++
/*
|
|
* This file is part of the Flowee project
|
|
* Copyright (C) 2017-2026 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 "Tx.h"
|
|
#include "Block.h"
|
|
#include "TxIterator_p.h"
|
|
#include "transaction.h"
|
|
#include <streaming/streams.h>
|
|
#include <compat/endian.h>
|
|
#include <hash.h>
|
|
#include <cstring>
|
|
|
|
Tx::Tx()
|
|
{
|
|
}
|
|
|
|
Tx::Tx(const Streaming::ConstBuffer &rawTransaction)
|
|
: m_data(rawTransaction)
|
|
{
|
|
}
|
|
|
|
uint32_t Tx::txVersion() const
|
|
{
|
|
return static_cast<uint32_t>(le32toh(*((uint32_t*)m_data.begin())));
|
|
}
|
|
|
|
uint256 Tx::createHash() const
|
|
{
|
|
CHash256 ctx;
|
|
ctx.write((const unsigned char*) m_data.begin(), m_data.size());
|
|
uint256 result;
|
|
ctx.finalize((unsigned char*)&result);
|
|
return result;
|
|
}
|
|
|
|
CTransaction Tx::createOldTransaction() const
|
|
{
|
|
CTransaction answer;
|
|
CDataStream buf(m_data.begin(), m_data.end(), 0 , 0);
|
|
answer.Unserialize(buf, 0, 0);
|
|
return answer;
|
|
}
|
|
|
|
int64_t Tx::offsetInBlock(const Block &block) const
|
|
{
|
|
assert(m_data.isValid());
|
|
assert(block.data().isValid());
|
|
assert(block.data().internal_buffer() == m_data.internal_buffer());
|
|
return m_data.begin() - block.data().begin();
|
|
}
|
|
|
|
Tx Tx::fromOldTransaction(const CTransaction &transaction, const std::shared_ptr<Streaming::BufferPool> &pool)
|
|
{
|
|
CSizeComputer sc(0, 0);
|
|
sc << transaction;
|
|
if (pool) {
|
|
pool->reserve(sc.size());
|
|
transaction.Serialize(*pool, 0, 0);
|
|
return Tx(pool->commit());
|
|
}
|
|
Streaming::BufferPool pl(sc.size());
|
|
transaction.Serialize(pl, 0, 0);
|
|
return Tx(pl.commit());
|
|
}
|
|
|
|
// static
|
|
std::list<Tx::Input> Tx::findInputs(Tx::Iterator &iter)
|
|
{
|
|
std::list<Input> inputs;
|
|
|
|
Input curInput;
|
|
auto content = iter.next();
|
|
while (content != Tx::End) {
|
|
if (content == Tx::PrevTxHash) { // only read inputs for non-coinbase txs
|
|
if (iter.byteData().size() != 32)
|
|
throw std::runtime_error("Failed to understand PrevTxHash");
|
|
curInput.txid = iter.uint256Data();
|
|
content = iter.next(Tx::PrevTxIndex);
|
|
if (content != Tx::PrevTxIndex)
|
|
throw std::runtime_error("Failed to find PrevTxIndex");
|
|
curInput.index = iter.intData();
|
|
inputs.push_back(curInput);
|
|
}
|
|
else if (content == Tx::OutputValue) {
|
|
break;
|
|
}
|
|
content = iter.next();
|
|
}
|
|
return inputs;
|
|
}
|
|
|
|
// static
|
|
Tx::Output Tx::nextOutput(Tx::Iterator &iter)
|
|
{
|
|
return iter.nextOutput();
|
|
}
|
|
|
|
Tx::Output Tx::output(int index) const
|
|
{
|
|
assert(index >= 0);
|
|
Iterator iter(*this);
|
|
return iter.nextOutput(index);
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////
|
|
|
|
bool static isConstBytes(Tx::Component tag)
|
|
{
|
|
return tag == Tx::TxVersion || tag == Tx::LockTime || tag == Tx::PrevTxIndex || tag == Tx::Sequence;
|
|
}
|
|
|
|
TxTokenizer::TxTokenizer(const Streaming::ConstBuffer &buffer)
|
|
: m_data(buffer),
|
|
m_txStart(m_data.begin()),
|
|
m_currentTokenStart(m_txStart),
|
|
m_currentTokenEnd(m_txStart),
|
|
m_tag(Tx::Unset)
|
|
{
|
|
}
|
|
|
|
TxTokenizer::TxTokenizer(const Block &block, int offsetInBlock)
|
|
: m_data(block.data()),
|
|
m_txStart(nullptr),
|
|
m_tag(Tx::Unset)
|
|
{
|
|
assert(block.isFullBlock());
|
|
const char *pos;
|
|
if (offsetInBlock == 0) {
|
|
pos = m_data.begin() + 80;
|
|
pos += readCompactSizeSize(pos); // num tx field
|
|
} else {
|
|
pos = m_data.begin() + offsetInBlock;
|
|
}
|
|
m_currentTokenStart = pos;
|
|
m_currentTokenEnd = pos;
|
|
}
|
|
|
|
Tx::Component TxTokenizer::next() {
|
|
if (m_currentTokenEnd + 1 >= m_data.end()) {
|
|
m_tag = Tx::End;
|
|
return tag();
|
|
}
|
|
m_currentTokenStart = m_currentTokenEnd;
|
|
if (m_tag == Tx::Unset || m_tag == Tx::End) {
|
|
m_numInputsLeft = -1;
|
|
m_numOutputsLeft = -1;
|
|
m_txStart = m_currentTokenStart;
|
|
m_currentTokenEnd += 4;
|
|
m_tag = Tx::TxVersion;
|
|
return checkSpaceForTag();
|
|
}
|
|
if (m_tag == Tx::TxVersion) {
|
|
uint64_t x = readCompactSize(&m_currentTokenEnd, m_data.end());
|
|
if (x > 0xFFFF)
|
|
throw std::runtime_error("Tx invalid");
|
|
m_numInputsLeft = static_cast<int>(x);
|
|
// we immediately go to the next token
|
|
m_currentTokenStart = m_currentTokenEnd;
|
|
if (0 == m_numInputsLeft)
|
|
m_tag = Tx::Sequence; // last tag of an input. So we jump to output processing
|
|
}
|
|
if (m_numInputsLeft > 0) {
|
|
if (m_tag == Tx::TxVersion || m_tag == Tx::Sequence) {
|
|
m_isCashToken = false;
|
|
m_cashTokenHasCommitment = false;
|
|
m_cashTokenHasAmount = false;
|
|
m_currentTokenEnd += 32;
|
|
m_tag = Tx::PrevTxHash;
|
|
return checkSpaceForTag();
|
|
}
|
|
if (m_tag == Tx::PrevTxHash) {
|
|
m_currentTokenEnd += 4;
|
|
m_tag = Tx::PrevTxIndex;
|
|
return checkSpaceForTag();
|
|
}
|
|
if (m_tag == Tx::PrevTxIndex) {
|
|
uint64_t scriptLength = readCompactSize(&m_currentTokenEnd, m_data.end());
|
|
if (scriptLength > 0xFFFF)
|
|
throw std::runtime_error("Tx invalid");
|
|
m_currentTokenStart = m_currentTokenEnd;
|
|
m_currentTokenEnd += static_cast<int>(scriptLength);
|
|
m_tag = Tx::TxInScript;
|
|
return checkSpaceForTag();
|
|
}
|
|
if (m_tag == Tx::TxInScript) {
|
|
m_numInputsLeft--;
|
|
m_currentTokenEnd += 4;
|
|
m_tag = Tx::Sequence;
|
|
return checkSpaceForTag();
|
|
}
|
|
}
|
|
if (m_tag == Tx::Sequence) {
|
|
uint64_t x = readCompactSize(&m_currentTokenEnd, m_data.end());
|
|
if (x > 0xFFFF)
|
|
throw std::runtime_error("Tx invalid");
|
|
m_numOutputsLeft = static_cast<int>(x);
|
|
// we immediately go to the next token
|
|
m_currentTokenStart = m_currentTokenEnd;
|
|
if (m_numOutputsLeft == 0) // jump over outputs
|
|
m_tag = Tx::OutputScript;
|
|
}
|
|
if (m_numOutputsLeft > 0) {
|
|
if (m_tag == Tx::Sequence || m_tag == Tx::OutputScript) {
|
|
m_currentTokenEnd += 8;
|
|
m_outputScriptStart = nullptr;
|
|
m_tag = Tx::OutputValue;
|
|
return checkSpaceForTag();
|
|
}
|
|
if (m_tag == Tx::OutputValue) {
|
|
m_scriptTokenLength = readCompactSize(&m_currentTokenEnd, m_data.end());
|
|
if (m_scriptTokenLength > 0xFFFF)
|
|
throw std::runtime_error("Tx invalid");
|
|
m_currentTokenStart = m_currentTokenEnd;
|
|
m_outputScriptStart = m_currentTokenStart;
|
|
m_outputScriptEnd = m_currentTokenStart + m_scriptTokenLength;
|
|
unsigned char tokenPrefix = static_cast<unsigned char>(*m_currentTokenStart);
|
|
m_isCashToken = (tokenPrefix == 0xef);
|
|
if (m_isCashToken) {
|
|
m_scriptTokenLength--;
|
|
// we immediately go to the next token
|
|
m_currentTokenEnd++;
|
|
m_currentTokenStart = m_currentTokenEnd;
|
|
}
|
|
}
|
|
if (m_isCashToken) {
|
|
if (m_tag == Tx::OutputValue) {
|
|
m_scriptTokenLength -= 32;
|
|
m_currentTokenEnd += 32;
|
|
m_tag = Tx::CashTokenCategory;
|
|
return checkSpaceForTag();
|
|
}
|
|
if (m_tag == Tx::CashTokenCategory) {
|
|
// Read the token_bitfield
|
|
m_scriptTokenLength--;
|
|
unsigned char bitfield = static_cast<unsigned char>(*m_currentTokenStart);
|
|
m_cashTokenHasCommitment = (bitfield & Tx::HasCommitment);
|
|
m_cashTokenHasAmount = (bitfield & Tx::HasFtAmount);
|
|
m_currentTokenEnd += 1;
|
|
m_tag = Tx::CashTokenBitfield;
|
|
return checkSpaceForTag();
|
|
}
|
|
if (m_cashTokenHasCommitment && m_tag == Tx::CashTokenBitfield) {
|
|
// Has a cash token amount (setting in bitfield), and previous tag was CashTokenBitfield
|
|
uint64_t commitmentLength = readCompactSize(&m_currentTokenEnd, m_data.end());
|
|
if (commitmentLength > 0xFFFF)
|
|
throw std::runtime_error("Commitment invalid");
|
|
m_scriptTokenLength -= (m_currentTokenEnd - m_currentTokenStart);
|
|
m_currentTokenStart = m_currentTokenEnd;
|
|
m_currentTokenEnd += static_cast<int>(commitmentLength);
|
|
m_scriptTokenLength -= commitmentLength;
|
|
m_tag = Tx::CashTokenCommitment;
|
|
return checkSpaceForTag();
|
|
}
|
|
if (m_cashTokenHasAmount &&
|
|
(m_tag == Tx::CashTokenCommitment || (m_tag == Tx::CashTokenBitfield && !m_cashTokenHasCommitment))) {
|
|
// Has a cash token amount (setting in bitfield), and, either:
|
|
// 1) Previous tag was CashTokenCommitment
|
|
// 2) Previous tag was CashTokenBitfield + it did not have a commitment
|
|
m_currentTokenEnd += readCompactSizeSize(m_currentTokenStart);
|
|
m_scriptTokenLength -= (m_currentTokenEnd - m_currentTokenStart);
|
|
m_tag = Tx::CashTokenAmount;
|
|
return checkSpaceForTag();
|
|
}
|
|
}
|
|
{ // no if-statement, at this position only the OutputScript is allowed
|
|
m_numOutputsLeft--;
|
|
m_currentTokenStart = m_currentTokenEnd;
|
|
m_currentTokenEnd += static_cast<int>(m_scriptTokenLength);
|
|
m_tag = Tx::OutputScript;
|
|
m_isCashToken = false;
|
|
return checkSpaceForTag();
|
|
}
|
|
}
|
|
if (m_tag == Tx::OutputScript) {
|
|
m_currentTokenEnd += 4;
|
|
m_tag = Tx::LockTime;
|
|
return checkSpaceForTag();
|
|
}
|
|
if (m_tag == Tx::LockTime)
|
|
m_tag = Tx::End;
|
|
else
|
|
assert(false);
|
|
return tag();
|
|
}
|
|
|
|
Tx::Component TxTokenizer::checkSpaceForTag()
|
|
{
|
|
if (m_tag != Tx::End && m_currentTokenEnd > m_data.end())
|
|
throw std::runtime_error("Tx data missing");
|
|
return tag();
|
|
}
|
|
|
|
Tx::Iterator::Iterator(const Tx &tx)
|
|
: d(new TxTokenizer(tx.m_data))
|
|
{
|
|
}
|
|
|
|
Tx::Iterator::Iterator(const Block &block, int offsetInBlock)
|
|
: d(new TxTokenizer(block, offsetInBlock))
|
|
{
|
|
}
|
|
|
|
Tx::Iterator::Iterator(const Tx::Iterator && other)
|
|
{
|
|
d = std::move(other.d);
|
|
}
|
|
|
|
Tx::Iterator::~Iterator()
|
|
{
|
|
delete d;
|
|
}
|
|
|
|
Tx::Component Tx::Iterator::next(int filter)
|
|
{
|
|
do {
|
|
int tag = d->next();
|
|
if (filter == 0 || (tag & filter))
|
|
return static_cast<Tx::Component>(tag);
|
|
} while (d->tag() != Tx::End);
|
|
return Tx::End;
|
|
}
|
|
|
|
Tx::Component Tx::Iterator::tag() const
|
|
{
|
|
return d->tag();
|
|
}
|
|
|
|
Tx Tx::Iterator::prevTx() const
|
|
{
|
|
assert(d->tag() == Tx::End);
|
|
return Tx(Streaming::ConstBuffer(d->m_data.internal_buffer(), d->m_txStart, d->m_currentTokenEnd));
|
|
}
|
|
|
|
Streaming::ConstBuffer Tx::Iterator::byteData() const
|
|
{
|
|
return Streaming::ConstBuffer(d->m_data.internal_buffer(), d->m_currentTokenStart, d->m_currentTokenEnd);
|
|
}
|
|
|
|
Streaming::ConstBuffer Tx::Iterator::rawOutputScript() const
|
|
{
|
|
if (d->m_outputScriptEnd <= d->m_outputScriptStart || d->m_outputScriptEnd > d->m_data.end())
|
|
return Streaming::ConstBuffer();
|
|
|
|
return Streaming::ConstBuffer(d->m_data.internal_buffer(), d->m_outputScriptStart, d->m_outputScriptEnd);
|
|
}
|
|
|
|
int Tx::Iterator::dataLength() const
|
|
{
|
|
return d->m_currentTokenEnd - d->m_currentTokenStart;
|
|
}
|
|
|
|
int32_t Tx::Iterator::intData() const
|
|
{
|
|
if (isConstBytes(d->tag()))
|
|
return *((int32_t*)(d->m_currentTokenStart));
|
|
const char *tmp = d->m_currentTokenStart;
|
|
return readCompactSize(&tmp, d->m_data.end());
|
|
}
|
|
|
|
uint32_t Tx::Iterator::uintData() const
|
|
{
|
|
if (isConstBytes(d->tag()))
|
|
return le32toh(*((uint32_t*)(d->m_currentTokenStart)));
|
|
const char *tmp = d->m_currentTokenStart;
|
|
return readCompactSize(&tmp, d->m_data.end());
|
|
}
|
|
|
|
uint64_t Tx::Iterator::longData() const
|
|
{
|
|
if (d->tag() == OutputValue)
|
|
return le64toh(*((uint64_t*)(d->m_currentTokenStart)));
|
|
if (isConstBytes(d->tag()))
|
|
return le32toh(*((uint32_t*)(d->m_currentTokenStart)));
|
|
const char *tmp = d->m_currentTokenStart;
|
|
return readCompactSize(&tmp, d->m_data.end());
|
|
}
|
|
|
|
uint256 Tx::Iterator::uint256Data() const
|
|
{
|
|
assert (d->m_currentTokenEnd - d->m_currentTokenStart >= 32);
|
|
uint256 answer(d->m_currentTokenStart);
|
|
return answer;
|
|
}
|
|
|
|
void Tx::Iterator::hashByteData(uint256 &output) const
|
|
{
|
|
CSHA256 hasher;
|
|
hasher.write(d->m_currentTokenStart, dataLength());
|
|
uint256 buf;
|
|
hasher.finalize(output.begin());
|
|
}
|
|
|
|
uint8_t Tx::Iterator::bitfieldData() const
|
|
{
|
|
if (d->m_tag != CashTokenBitfield)
|
|
return 0;
|
|
return *d->m_currentTokenStart;
|
|
}
|
|
|
|
size_t Tx::Iterator::position() const
|
|
{
|
|
return d->m_currentTokenStart - d->m_txStart;
|
|
}
|
|
|
|
Tx::Output Tx::Iterator::nextOutput(int skip)
|
|
{
|
|
Output answer;
|
|
auto content = tag();
|
|
if (tag() != Tx::OutputValue)
|
|
content = next(Tx::OutputValue);
|
|
while (content != Tx::End) {
|
|
if (--skip < 0) { // use this one!
|
|
answer.outputValue = static_cast<int64_t>(longData());
|
|
content = next();
|
|
size_t start1 = position();
|
|
if (content == Tx::CashTokenCategory) {
|
|
--start1; // since it auto-jumps over the marker.
|
|
content = next(Tx::OutputScript); // for 'offset()'
|
|
}
|
|
if (content != Tx::OutputScript)
|
|
throw std::runtime_error("Malformed transaction");
|
|
answer.m_rawOutputScript = rawOutputScript();
|
|
answer.m_outputStart = position() - start1;
|
|
assert(answer.m_outputStart >= 0);
|
|
break;
|
|
}
|
|
content = next(Tx::OutputValue);
|
|
}
|
|
return answer;
|
|
}
|
|
|
|
bool operator==(const Tx::Input &a, const Tx::Input &b)
|
|
{
|
|
return a.index == b.index && a.dataFile == b.dataFile && a.txid == b.txid;
|
|
}
|
|
|
|
|
|
// ------
|
|
|
|
Tx::Output::Output(const Streaming::ConstBuffer &b, int64_t v)
|
|
: m_rawOutputScript(b),
|
|
outputValue(v),
|
|
m_outputStart(0)
|
|
{
|
|
// pre-calc m_outputStart if there is a token.
|
|
if (hasToken()) {
|
|
Tx::Token token;
|
|
const char *t = m_rawOutputScript.begin();
|
|
assert(static_cast<uint8_t>(*t) == 0xef);
|
|
token.bitfield = static_cast<uint8_t>(t[33]);
|
|
t += 34;
|
|
if (token.hasCommitment())
|
|
t += readCompactSize(&t, m_rawOutputScript.end());
|
|
if (token.hasAmount())
|
|
(void) readCompactSize(&t, m_rawOutputScript.end());
|
|
|
|
m_outputStart = t - m_rawOutputScript.begin();
|
|
}
|
|
}
|
|
|
|
Streaming::ConstBuffer Tx::Output::outputScript() const
|
|
{
|
|
assert(m_outputStart > -1);
|
|
return m_rawOutputScript.mid(m_outputStart);
|
|
}
|
|
|
|
Streaming::ConstBuffer Tx::Output::rawOutputScript() const
|
|
{
|
|
return m_rawOutputScript;
|
|
}
|
|
|
|
bool Tx::Output::hasToken() const
|
|
{
|
|
if (m_rawOutputScript.size() < 34)
|
|
return false;
|
|
return static_cast<uint8_t>(*m_rawOutputScript.begin()) == 0xef;
|
|
}
|
|
|
|
Tx::Token Tx::Output::token() const
|
|
{
|
|
if (!hasToken())
|
|
throw std::runtime_error("No token");
|
|
Tx::Token answer;
|
|
const char *t = m_rawOutputScript.begin();
|
|
assert(static_cast<uint8_t>(*t) == 0xef);
|
|
answer.category = uint256(t + 1);
|
|
answer.bitfield = static_cast<uint8_t>(t[33]);
|
|
t += 34;
|
|
if (answer.hasCommitment()) {
|
|
auto length = readCompactSize(&t, m_rawOutputScript.end());
|
|
answer.commitment = std::vector<uint8_t>(t, t + length);
|
|
t += length;
|
|
}
|
|
if (answer.hasAmount())
|
|
answer.amount = readCompactSize(&t, m_rawOutputScript.end());
|
|
return answer;
|
|
}
|