1468 lines
60 KiB
C++
1468 lines
60 KiB
C++
/*
|
|
* This file is part of the Flowee project
|
|
* Copyright (C) 2016-2025 Tom Zander <tom@flowee.org>
|
|
* Copyright (C) 2025 John Galt <johngaltbch@pm.me>
|
|
*
|
|
* 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 "APIRPCBinding.h"
|
|
#include "APIServer.h"
|
|
|
|
#include <APIProtocol.h>
|
|
#include <univalue.h>
|
|
#include <hash.h>
|
|
#include <util.h>
|
|
#include <chainparams.h>
|
|
#include <utilstrencodings.h>
|
|
#include <streaming/MessageParser.h>
|
|
#include <streaming/MessageBuilder.h>
|
|
#include <streaming/BufferPools.h>
|
|
#include <primitives/Block.h>
|
|
#include <primitives/Tx.h>
|
|
#include <utxo/UnspentOutputDatabase.h>
|
|
#include <server/Application.h>
|
|
#include <server/BlocksDB.h>
|
|
#include <server/DoubleSpendProofStorage.h>
|
|
#include <server/encodings_legacy.h>
|
|
#include <server/rpcserver.h>
|
|
#include <server/txmempool.h>
|
|
#include <server/validation/Engine.h>
|
|
#include <server/UnspentOutputData.h>
|
|
#include <server/main.h>
|
|
|
|
#include <boost/algorithm/hex.hpp>
|
|
#include <boost/asio/post.hpp>
|
|
#include <BlockMetaData.h>
|
|
|
|
namespace {
|
|
|
|
void addHash256ToBuilder(Streaming::MessageBuilder &builder, uint32_t tag, const UniValue &univalue) {
|
|
assert(univalue.isStr());
|
|
assert(univalue.getValStr().size() == 64);
|
|
uint8_t buf[32];
|
|
const char *input = univalue.getValStr().c_str();
|
|
for (int i = 0; i < 32; ++i) {
|
|
signed char c = HexDigit(*input++);
|
|
assert(c != -1);
|
|
unsigned char n = c << 4;
|
|
c = HexDigit(*input++);
|
|
assert(c != -1);
|
|
n |= c;
|
|
buf[31 - i] = n;
|
|
}
|
|
builder.addByteArray(tag, buf, 32);
|
|
}
|
|
|
|
// blockchain
|
|
|
|
class GetBlockChainInfo : public Api::RpcParser
|
|
{
|
|
public:
|
|
GetBlockChainInfo() : RpcParser("getblockchaininfo", Api::BlockChain::GetBlockChainInfoReply, 500) {}
|
|
virtual void buildReply(Streaming::MessageBuilder &builder, const UniValue &result) override {
|
|
const UniValue &chain = find_value(result, "chain");
|
|
builder.add(Api::BlockChain::Chain, chain.get_str());
|
|
const UniValue &blocks = find_value(result, "blocks");
|
|
builder.add(Api::BlockChain::Blocks, blocks.get_int());
|
|
const UniValue &headers = find_value(result, "headers");
|
|
builder.add(Api::BlockChain::Headers, headers.get_int());
|
|
addHash256ToBuilder(builder, Api::BlockChain::BestBlockHash, find_value(result, "bestblockhash"));
|
|
const UniValue &difficulty = find_value(result, "difficulty");
|
|
builder.add(Api::BlockChain::Difficulty, difficulty.get_real());
|
|
const UniValue &time = find_value(result, "mediantime");
|
|
builder.add(Api::BlockChain::MedianTime, (uint64_t) time.get_int64());
|
|
const UniValue &progress = find_value(result, "verificationprogress");
|
|
builder.add(Api::BlockChain::VerificationProgress, progress.get_real());
|
|
addHash256ToBuilder(builder, Api::BlockChain::ChainWork, find_value(result, "chainwork"));
|
|
}
|
|
};
|
|
|
|
class GetBestBlockHash : public Api::RpcParser
|
|
{
|
|
public:
|
|
GetBestBlockHash() : RpcParser("getbestblockhash", Api::BlockChain::GetBestBlockHashReply, 50) {}
|
|
};
|
|
|
|
class GetBlockLegacy : public Api::RpcParser
|
|
{
|
|
public:
|
|
GetBlockLegacy() : RpcParser("getblock", Api::BlockChain::GetBlockVerboseReply), m_verbose(true) {}
|
|
virtual void createRequest(const Message &message, UniValue &output) override {
|
|
std::string blockId;
|
|
Streaming::MessageParser parser(message.body());
|
|
while (parser.next() == Streaming::FoundTag) {
|
|
if (parser.tag() == Api::BlockChain::BlockHash
|
|
|| parser.tag() == Api::LiveTransactions::GenericByteData) {
|
|
blockId = parser.uint256Data().ToString();
|
|
} else if (parser.tag() == Api::BlockChain::Verbose) {
|
|
m_verbose = parser.boolData();
|
|
} else if (parser.tag() == Api::BlockChain::BlockHeight) {
|
|
auto index = chainActive[parser.intData()];
|
|
if (index)
|
|
blockId = index->GetBlockHash().ToString();
|
|
}
|
|
}
|
|
output.push_back(std::make_pair("block", UniValue(UniValue::VSTR, blockId)));
|
|
output.push_back(std::make_pair("verbose", UniValue(UniValue::VBOOL, m_verbose ? "1": "0")));
|
|
}
|
|
|
|
virtual int calculateMessageSize(const UniValue &result) const override {
|
|
if (m_verbose) {
|
|
const UniValue &tx = find_value(result, "tx");
|
|
return tx.size() * 70 + 200;
|
|
}
|
|
return result.get_str().size() / 2 + 20;
|
|
}
|
|
|
|
virtual void buildReply(Streaming::MessageBuilder &builder, const UniValue &result) override{
|
|
if (!m_verbose) {
|
|
std::vector<char> answer;
|
|
boost::algorithm::unhex(result.get_str(), back_inserter(answer));
|
|
builder.add(1, answer);
|
|
return;
|
|
}
|
|
|
|
addHash256ToBuilder(builder, Api::BlockChain::BlockHash, find_value(result, "hash"));
|
|
const UniValue &confirmations = find_value(result, "confirmations");
|
|
builder.add(Api::BlockChain::Confirmations, confirmations.get_int());
|
|
const UniValue &size = find_value(result, "size");
|
|
builder.add(Api::BlockChain::Size, size.get_int());
|
|
const UniValue &height = find_value(result, "height");
|
|
builder.add(Api::BlockChain::BlockHeight, height.get_int());
|
|
const UniValue &version = find_value(result, "version");
|
|
builder.add(Api::BlockChain::Version, version.get_int());
|
|
addHash256ToBuilder(builder, Api::BlockChain::MerkleRoot, find_value(result, "merkleroot"));
|
|
const UniValue &tx = find_value(result, "tx");
|
|
bool first = true;
|
|
for (const UniValue &transaction: tx.getValues()) {
|
|
if (first) first = false;
|
|
else builder.add(Api::Separator, true);
|
|
addHash256ToBuilder(builder, Api::BlockChain::TxId, transaction);
|
|
}
|
|
const UniValue &time = find_value(result, "time");
|
|
builder.add(Api::BlockChain::Time, (uint64_t) time.get_int64());
|
|
const UniValue &mediantime = find_value(result, "mediantime");
|
|
builder.add(Api::BlockChain::MedianTime, (uint64_t) mediantime.get_int64());
|
|
const UniValue &nonce = find_value(result, "nonce");
|
|
builder.add(Api::BlockChain::Nonce, (uint64_t) nonce.get_int64());
|
|
// 'bits' is an 4 bytes hex-encoded string.
|
|
const std::string &input = find_value(result, "bits").getValStr();
|
|
assert(input.size() == 8);
|
|
uint32_t target = 0;
|
|
for (int i = 0; i < 8; ++i) {
|
|
signed char c = HexDigit(input.at(i));
|
|
assert(c != -1);
|
|
target = (target << 4) + c;
|
|
}
|
|
const char *targetPtr = reinterpret_cast<const char*>(&target);
|
|
builder.addByteArray(Api::BlockChain::BlockTarget, targetPtr, 4);
|
|
const UniValue &difficulty = find_value(result, "difficulty");
|
|
builder.add(Api::BlockChain::Difficulty, difficulty.get_real());
|
|
addHash256ToBuilder(builder, Api::BlockChain::ChainWork, find_value(result, "chainwork"));
|
|
addHash256ToBuilder(builder, Api::BlockChain::PrevBlockHash, find_value(result, "previousblockhash"));
|
|
const UniValue &nextblock = find_value(result, "nextblockhash");
|
|
if (nextblock.isStr())
|
|
addHash256ToBuilder(builder, Api::BlockChain::NextBlockHash, nextblock);
|
|
}
|
|
|
|
private:
|
|
bool m_verbose;
|
|
};
|
|
|
|
class GetBlockHeader : public Api::DirectParser
|
|
{
|
|
public:
|
|
GetBlockHeader() : DirectParser(Api::BlockChain::GetBlockHeaderReply, 200) {}
|
|
|
|
void buildReply(Streaming::MessageBuilder &builder, CBlockIndex *index) {
|
|
assert(index);
|
|
builder.add(Api::BlockChain::BlockHash, index->GetBlockHash());
|
|
builder.add(Api::BlockChain::Confirmations, chainActive.Contains(index) ? chainActive.Height() - index->nHeight + 1: -1);
|
|
builder.add(Api::BlockChain::BlockHeight, index->nHeight);
|
|
builder.add(Api::BlockChain::Version, index->nVersion);
|
|
builder.add(Api::BlockChain::MerkleRoot, index->hashMerkleRoot);
|
|
builder.add(Api::BlockChain::Time, (uint64_t) index->nTime);
|
|
builder.add(Api::BlockChain::MedianTime, (uint64_t) index->GetMedianTimePast());
|
|
builder.add(Api::BlockChain::Nonce, (uint64_t) index->nNonce);
|
|
uint32_t target = htobe32(index->nBits);
|
|
const char *targetPtr = reinterpret_cast<const char*>(&target);
|
|
builder.addByteArray(Api::BlockChain::BlockTarget, targetPtr, 4);
|
|
builder.add(Api::BlockChain::Difficulty, GetDifficulty(index));
|
|
|
|
if (index->pprev)
|
|
builder.add(Api::BlockChain::PrevBlockHash, index->pprev->GetBlockHash());
|
|
auto next = chainActive.Next(index);
|
|
if (next)
|
|
builder.add(Api::BlockChain::NextBlockHash, next->GetBlockHash());
|
|
}
|
|
|
|
void buildReply(const Message &request, Streaming::MessageBuilder &builder) override {
|
|
Streaming::MessageParser parser(request.body());
|
|
|
|
while (parser.next() == Streaming::FoundTag) {
|
|
if (parser.tag() == Api::BlockChain::BlockHash) {
|
|
uint256 hash = parser.uint256Data();
|
|
CBlockIndex *bi = Blocks::Index::get(hash);
|
|
if (bi)
|
|
return buildReply(builder, bi);
|
|
}
|
|
else if (parser.tag() == Api::BlockChain::BlockHeight) {
|
|
int height = parser.intData();
|
|
auto index = chainActive[height];
|
|
if (index)
|
|
return buildReply(builder, index);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @brief The TransactionSerializationOptions struct helps commands serialize transactions
|
|
* according to the peers-selected criterea.
|
|
*
|
|
* A command would populate the options struct once and then repeatedly call
|
|
* serialize on each of the transactions it wants to include in the reply.
|
|
*/
|
|
struct TransactionSerializationOptions
|
|
{
|
|
void serialize(Streaming::MessageBuilder &builder, Tx::Iterator &iter)
|
|
{
|
|
int outIndex = -1;
|
|
bool isCashToken = false;
|
|
bool useOutput = false; // when the remote filters indicate that the current output is to be included
|
|
auto type = iter.next();
|
|
while (type != Tx::End) {
|
|
if (returnInputs && type == Tx::PrevTxHash) {
|
|
builder.add(Api::BlockChain::Tx_IN_TxId, iter.uint256Data());
|
|
}
|
|
else if (returnInputs && type == Tx::TxInScript) {
|
|
builder.add(Api::BlockChain::Tx_InputScript, iter.byteData());
|
|
}
|
|
else if (returnInputs && type == Tx::PrevTxIndex) {
|
|
builder.add(Api::BlockChain::Tx_IN_OutIndex, iter.intData());
|
|
}
|
|
else if (type == Tx::OutputValue) {
|
|
outIndex++;
|
|
useOutput = (filterOutputs.empty() || filterOutputs.find(outIndex) != filterOutputs.end());
|
|
if (useOutput) {
|
|
if (returnOutputs || returnOutputAmounts || returnOutputScripts || returnOutputAddresses ||
|
|
returnOutputScriptHashed || returnCTFlags || returnCTCommitment || returnCTAmount) {
|
|
builder.add(Api::BlockChain::Tx_Out_Index, outIndex);
|
|
}
|
|
if (returnOutputs || returnOutputAmounts) {
|
|
builder.add(Api::BlockChain::Tx_Out_Amount, iter.longData());
|
|
}
|
|
}
|
|
isCashToken = false;
|
|
}
|
|
else if (useOutput && (returnOutputs || returnCTCategory) && type == Tx::CashTokenCategory) {
|
|
builder.add(Api::BlockChain::Tx_Out_CT_Category, iter.uint256Data());
|
|
}
|
|
else if (useOutput && type == Tx::CashTokenBitfield) {
|
|
isCashToken = true;
|
|
unsigned char tokenbitfield = iter.byteData()[0];
|
|
bool isNFT = (tokenbitfield & 0x20) == 0x20;
|
|
if (returnCTFlags) {
|
|
builder.add(Api::BlockChain::Tx_Out_CT_IsFT, (tokenbitfield & 0x10) == 0x10);
|
|
builder.add(Api::BlockChain::Tx_Out_CT_IsNFT, isNFT);
|
|
builder.add(Api::BlockChain::Tx_Out_CT_IsImmutable, isNFT && (tokenbitfield & 0x0f) == 0x00);
|
|
builder.add(Api::BlockChain::Tx_Out_CT_IsMutable, isNFT && (tokenbitfield & 0x0f) == 0x01);
|
|
builder.add(Api::BlockChain::Tx_Out_CT_IsMinting, isNFT && (tokenbitfield & 0x0f) == 0x02);
|
|
}
|
|
}
|
|
else if (useOutput && (returnOutputs || returnCTCommitment) && type == Tx::CashTokenCommitment) {
|
|
builder.add(Api::BlockChain::Tx_Out_CT_Commitment, iter.byteData());
|
|
}
|
|
else if (useOutput && (returnOutputs || returnCTAmount) && type == Tx::CashTokenAmount) {
|
|
builder.add(Api::BlockChain::Tx_Out_CT_Amount, iter.longData());
|
|
}
|
|
else if (useOutput && type == Tx::OutputScript) {
|
|
if (!isCashToken) {
|
|
if (returnCTFlags) {
|
|
builder.add(Api::BlockChain::Tx_Out_CT_IsFT, false);
|
|
builder.add(Api::BlockChain::Tx_Out_CT_IsNFT, false);
|
|
}
|
|
}
|
|
if (returnOutputs || returnOutputScripts)
|
|
builder.add(Api::BlockChain::Tx_OutputScript, iter.byteData());
|
|
if (returnOutputAddresses) {
|
|
CScript scriptPubKey(iter.byteData());
|
|
std::vector<std::vector<unsigned char> > vSolutions;
|
|
Script::TxnOutType whichType;
|
|
bool recognizedTx = Script::solver(scriptPubKey, whichType, vSolutions);
|
|
if (recognizedTx && (whichType == Script::TX_PUBKEY || whichType == Script::TX_PUBKEYHASH)) {
|
|
if (whichType == Script::TX_PUBKEYHASH) {
|
|
assert(vSolutions[0].size() == 20);
|
|
builder.addByteArray(Api::BlockChain::Tx_Out_Address, vSolutions[0].data(), 20);
|
|
} else if (whichType == Script::TX_PUBKEY) {
|
|
PublicKey pubKey(vSolutions[0]);
|
|
assert (pubKey.isValid());
|
|
KeyId address = pubKey.getKeyId();
|
|
builder.addByteArray(Api::BlockChain::Tx_Out_Address, address.begin(), 20);
|
|
}
|
|
}
|
|
}
|
|
if (returnOutputScriptHashed) {
|
|
CSHA256 sha;
|
|
sha.write(iter.byteData().begin(), iter.dataLength());
|
|
char buf[32];
|
|
sha.finalize(buf);
|
|
builder.addByteArray(Api::BlockChain::Tx_Out_ScriptHash, buf, 32);
|
|
}
|
|
}
|
|
|
|
type = iter.next();
|
|
}
|
|
}
|
|
|
|
// return true only if serialize would actually export anything
|
|
bool shouldRun() const {
|
|
const bool partialTxData = returnInputs || returnOutputs || returnOutputAmounts
|
|
|| returnOutputScripts || returnOutputAddresses || returnOutputScriptHashed
|
|
|| returnCTCategory || returnCTFlags || returnCTCommitment || returnCTAmount;
|
|
return partialTxData;
|
|
}
|
|
|
|
int calculateNeededSize(const Tx &tx) {
|
|
assert(shouldRun());
|
|
|
|
Tx::Iterator iter(tx);
|
|
auto type = iter.next();
|
|
int txOutputCount = 0, txInputSize = 0, txOutputScriptSizes = 0, txOutputTokenSizes = 0;
|
|
while (true) {
|
|
if (type == Tx::End)
|
|
break;
|
|
|
|
if (returnInputs && type == Tx::PrevTxHash) {
|
|
txInputSize += 42; // prevhash: 32 + 3 + prevIndex; 6 + 1
|
|
}
|
|
else if (returnInputs && type == Tx::TxInScript) {
|
|
txInputSize += iter.dataLength() + 3;
|
|
}
|
|
else if (type == Tx::OutputValue) {
|
|
++txOutputCount;
|
|
txOutputTokenSizes += 4; // 2 bytes for isFT and isNFT
|
|
}
|
|
else if (type == Tx::OutputScript) {
|
|
txOutputScriptSizes += iter.dataLength() + 6;
|
|
}
|
|
else if (type == Tx::CashTokenCategory) {
|
|
txOutputTokenSizes += 45; // the sha256 but also the isImmutable, isMutable, isMinting bools
|
|
}
|
|
else if (type == Tx::CashTokenCommitment) {
|
|
txOutputTokenSizes += iter.dataLength() + 6;
|
|
}
|
|
else if (type == Tx::CashTokenAmount) {
|
|
txOutputTokenSizes += 10;
|
|
}
|
|
|
|
type = iter.next();
|
|
}
|
|
|
|
int bytesPerOutput = 5;
|
|
if (returnOutputAmounts || returnOutputs) bytesPerOutput += 10;
|
|
if (returnOutputAddresses) bytesPerOutput += 23;
|
|
if (returnOutputScriptHashed) bytesPerOutput += 35;
|
|
|
|
int total = 45 + txOutputTokenSizes;
|
|
if (returnOutputs || returnOutputScripts)
|
|
total += txOutputScriptSizes;
|
|
if (returnInputs)
|
|
total += txInputSize;
|
|
if (returnOutputs || returnOutputAddresses || returnOutputAmounts | returnOutputScriptHashed)
|
|
total += txOutputCount * bytesPerOutput;
|
|
|
|
return total;
|
|
}
|
|
|
|
void readParser(const Streaming::MessageParser &parser) {
|
|
if (parser.tag() == Api::BlockChain::Include_Inputs) {
|
|
returnInputs = parser.boolData();
|
|
} else if (parser.tag() == Api::BlockChain::Include_Outputs) {
|
|
returnOutputs = parser.boolData();
|
|
returnCTCategory = returnOutputs;
|
|
returnCTFlags = returnOutputs;
|
|
returnCTCommitment = returnOutputs;
|
|
returnCTAmount = returnOutputs;
|
|
} else if (parser.tag() == Api::BlockChain::Include_OutputAmounts) {
|
|
returnOutputAmounts = parser.boolData();
|
|
} else if (parser.tag() == Api::BlockChain::Include_OutputScripts) {
|
|
returnOutputScripts = parser.boolData();
|
|
} else if (parser.tag() == Api::BlockChain::Include_OutputAddresses) {
|
|
returnOutputAddresses = parser.boolData();
|
|
} else if (parser.tag() == Api::BlockChain::Include_OutputScriptHash) {
|
|
returnOutputScriptHashed = parser.boolData();
|
|
} else if (parser.tag() == Api::BlockChain::Include_CT_Category) {
|
|
returnCTCategory = parser.boolData();
|
|
} else if (parser.tag() == Api::BlockChain::Include_CT_Flags) {
|
|
returnCTFlags = parser.boolData();
|
|
} else if (parser.tag() == Api::BlockChain::Include_CT_Commitment) {
|
|
returnCTCommitment = parser.boolData();
|
|
} else if (parser.tag() == Api::BlockChain::Include_CT_Amount) {
|
|
returnCTAmount = parser.boolData();
|
|
}
|
|
}
|
|
bool returnInputs = false;
|
|
bool returnOutputs = false;
|
|
bool returnOutputAmounts = false;
|
|
bool returnOutputScripts = false;
|
|
bool returnOutputAddresses = false;
|
|
bool returnOutputScriptHashed = false;
|
|
bool returnCTCategory = false;
|
|
bool returnCTFlags = false;
|
|
bool returnCTCommitment = false;
|
|
bool returnCTAmount = false;
|
|
|
|
std::set<int> filterOutputs;
|
|
};
|
|
|
|
class GetBlock : public Api::DirectParser
|
|
{
|
|
public:
|
|
class BlockSessionData : public Api::SessionData
|
|
{
|
|
public:
|
|
std::set<uint256> hashes; // script-hashes to filter on
|
|
};
|
|
|
|
GetBlock() : DirectParser(Api::BlockChain::GetBlockReply) {}
|
|
|
|
int calculateMessageSize(const Message &request) override {
|
|
const int max = GetArg("-api_max_addresses", -1);
|
|
CBlockIndex *index = nullptr;
|
|
Streaming::MessageParser parser(request.body());
|
|
BlockSessionData *session = dynamic_cast<BlockSessionData*>(*data);
|
|
if (session == nullptr) {
|
|
session = new BlockSessionData();
|
|
*data = session;
|
|
}
|
|
|
|
bool filterOnScriptHashes = false;
|
|
bool requestOk = false;
|
|
bool fullTxData = false;
|
|
while (parser.next() == Streaming::FoundTag) {
|
|
if (parser.tag() == Api::BlockChain::BlockHash
|
|
|| parser.tag() == Api::LiveTransactions::GenericByteData) {
|
|
if (parser.dataLength() != 32)
|
|
throw Api::ParserException("BlockHash should be a 32 byte-bytearray");
|
|
index = Blocks::Index::get(uint256(&parser.bytesData()[0]));
|
|
requestOk = true;
|
|
} else if (parser.tag() == Api::BlockChain::BlockHeight) {
|
|
index = chainActive[parser.intData()];
|
|
requestOk = true;
|
|
} else if (parser.tag() == Api::BlockChain::ReuseAddressFilter) {
|
|
filterOnScriptHashes = parser.boolData();
|
|
} else if (parser.tag() == Api::BlockChain::SetFilterScriptHash
|
|
|| parser.tag() == Api::BlockChain::AddFilterScriptHash) {
|
|
if (parser.dataLength() != 32)
|
|
throw Api::ParserException("GetBlock: filter-script-hash should be a 32-bytes bytearray");
|
|
if (parser.tag() == Api::BlockChain::SetFilterScriptHash)
|
|
session->hashes.clear();
|
|
if (max > 0 && static_cast<int>(session->hashes.size()) + 1 >= max)
|
|
throw Api::ParserException("Max number of filterScriptHashes exceeded");
|
|
session->hashes.insert(parser.uint256Data());
|
|
filterOnScriptHashes = true;
|
|
} else if (parser.tag() == Api::BlockChain::FullTransactionData) {
|
|
fullTxData = parser.boolData();
|
|
if (!fullTxData)
|
|
m_fullTxData = false;
|
|
} else if (parser.tag() == Api::BlockChain::Include_TxId) {
|
|
m_returnTxId = parser.boolData();
|
|
} else if (parser.tag() == Api::BlockChain::Include_OffsetInBlock) {
|
|
m_returnOffsetInBlock = parser.boolData();
|
|
} else if (parser.tag() == Api::BlockChain::FilterOnScriptType) {
|
|
m_scriptFilter = parser.intData();
|
|
if (m_scriptFilter < 0)
|
|
m_scriptFilter = 0;
|
|
} else {
|
|
opt.readParser(parser);
|
|
}
|
|
}
|
|
if (fullTxData) // if explicitly asked.
|
|
m_fullTxData = true;
|
|
else if (m_returnTxId || opt.shouldRun()) // we imply false if they want a subset.
|
|
m_fullTxData = false;
|
|
|
|
if (index == nullptr)
|
|
throw Api::ParserException(requestOk ? "Requested block not found" :
|
|
"Request needs to contain either height or blockhash");
|
|
m_height = index->nHeight;
|
|
try {
|
|
m_block = Blocks::DB::instance()->loadBlock(index->GetBlockPos());
|
|
assert(m_block.isFullBlock());
|
|
} catch (...) {
|
|
throw Api::ParserException("Blockdata not present on this Hub");
|
|
}
|
|
|
|
// use faster matching using the metadata.
|
|
// this helps us in cases of filtering on output-script type
|
|
const BlockMetaData::TransactionData *txData = nullptr;
|
|
BlockMetaData metaData;
|
|
if (m_scriptFilter > 0) {
|
|
try {
|
|
metaData = Blocks::DB::instance()->loadBlockMetaData(index->GetMetaDataPos());
|
|
txData = metaData.first();
|
|
} catch (...) {}
|
|
}
|
|
|
|
Tx::Iterator iter(m_block);
|
|
auto type = iter.next();
|
|
bool oneEnd = false, txMatched = !filterOnScriptHashes && txData == nullptr;
|
|
bool coinbase = true;
|
|
int size = 0, matchedOutputs = 0, matchedInputsSize = 0;
|
|
int txOutputCount = 0, txInputSize = 0, txOutputScriptSizes = 0;
|
|
int matchedOutputScriptSizes = 0;
|
|
while (true) {
|
|
if (type == Tx::End) {
|
|
if (oneEnd) // then the second end means end of block
|
|
break;
|
|
if (txMatched) {
|
|
Tx prevTx = iter.prevTx();
|
|
size += prevTx.size();
|
|
matchedInputsSize += txInputSize;
|
|
matchedOutputs += txOutputCount;
|
|
matchedOutputScriptSizes += txOutputScriptSizes;
|
|
m_transactions.push_back(std::make_pair(prevTx.offsetInBlock(m_block), prevTx.size()));
|
|
txMatched = !filterOnScriptHashes && txData == nullptr;
|
|
}
|
|
oneEnd = true;
|
|
coinbase = false;
|
|
|
|
txInputSize = 0;
|
|
txOutputCount = 0;
|
|
txOutputScriptSizes = 0;
|
|
if (txData)
|
|
txData = txData->next();
|
|
} else {
|
|
oneEnd = false;
|
|
}
|
|
|
|
if (opt.returnInputs && type == Tx::PrevTxHash) {
|
|
txInputSize += 42; // prevhash: 32 + 3 + prevIndex; 6 + 1
|
|
}
|
|
else if (opt.returnInputs && type == Tx::TxInScript) {
|
|
txInputSize += iter.dataLength() + 3;
|
|
}
|
|
else if (type == Tx::OutputValue) {
|
|
++txOutputCount;
|
|
}
|
|
else if (type == Tx::OutputScript) {
|
|
txOutputScriptSizes += iter.dataLength() + 4;
|
|
if (!txMatched && !coinbase && m_scriptFilter > 0 && txData) {
|
|
// txData->scriptTags is a flags of all things this transaction has.
|
|
// m_scriptFilter is a flags where ANY bit should match.
|
|
txMatched = (m_scriptFilter & txData->scriptTags) != 0;
|
|
}
|
|
if (!txMatched
|
|
&& session->hashes.find(iter.hashedByteData()) != session->hashes.end())
|
|
txMatched = true;
|
|
}
|
|
type = iter.next();
|
|
}
|
|
|
|
int bytesPerTx = 1;
|
|
if (m_returnTxId) bytesPerTx += 35;
|
|
if (m_returnOffsetInBlock) bytesPerTx += 6;
|
|
if (m_fullTxData) bytesPerTx += 5; // actual tx-data is in 'size'
|
|
|
|
int bytesPerOutput = 5;
|
|
if (opt.returnOutputAmounts || opt.returnOutputs) bytesPerOutput += 10;
|
|
if (opt.returnOutputAddresses) bytesPerOutput += 23;
|
|
if (opt.returnOutputScriptHashed) bytesPerOutput += 35;
|
|
|
|
int total = 45 + int(m_transactions.size()) * bytesPerTx;
|
|
if (m_fullTxData) total += size;
|
|
if (opt.returnOutputs || opt.returnOutputScripts)
|
|
total += matchedOutputScriptSizes;
|
|
if (opt.returnInputs)
|
|
total += matchedInputsSize;
|
|
if (opt.returnOutputs || opt.returnOutputAddresses || opt.returnOutputAmounts | opt.returnOutputScriptHashed)
|
|
total += matchedOutputs * bytesPerOutput;
|
|
|
|
logDebug() << "GetBlock calculated to need at most" << total << "bytes";
|
|
logDebug() << " tx" << bytesPerTx << "*" << m_transactions.size() << "(=num tx). Plus"
|
|
<< bytesPerOutput << "bytes per output (" << matchedOutputs << ")";
|
|
logDebug() << " matched Script Output sizes:" << matchedOutputScriptSizes;
|
|
return total;
|
|
}
|
|
|
|
void buildReply(const Message&, Streaming::MessageBuilder &builder) override {
|
|
assert(m_height >= 0);
|
|
builder.add(Api::BlockChain::BlockHeight, m_height);
|
|
builder.add(Api::BlockChain::BlockHash, m_block.createHash());
|
|
|
|
for (auto posAndSize : m_transactions) {
|
|
if (m_returnOffsetInBlock)
|
|
builder.add(Api::BlockChain::Tx_OffsetInBlock, posAndSize.first);
|
|
if (m_returnTxId) {
|
|
Tx tx(m_block.data().mid(posAndSize.first, posAndSize.second));
|
|
builder.add(Api::BlockChain::TxId, tx.createHash());
|
|
}
|
|
if (opt.shouldRun()) {
|
|
Tx::Iterator iter(m_block, posAndSize.first);
|
|
if (posAndSize.first < 91) {
|
|
iter.next(Tx::PrevTxIndex); // skip version, prevTxId and prevTxIndex for coinbase
|
|
assert(iter.tag() == Tx::PrevTxIndex);
|
|
}
|
|
opt.serialize(builder, iter);
|
|
}
|
|
if (m_fullTxData)
|
|
builder.add(Api::BlockChain::GenericByteData,
|
|
m_block.data().mid(posAndSize.first, posAndSize.second));
|
|
|
|
builder.add(Api::BlockChain::Separator, true);
|
|
}
|
|
}
|
|
|
|
Block m_block;
|
|
std::vector<std::pair<int, int>> m_transactions; // list of offset-in-block and length of tx to include
|
|
bool m_fullTxData = true;
|
|
bool m_returnTxId = false;
|
|
bool m_returnOffsetInBlock = true;
|
|
int m_height = -1;
|
|
int m_scriptFilter = -1;
|
|
TransactionSerializationOptions opt;
|
|
};
|
|
class GetBlockCount : public Api::DirectParser
|
|
{
|
|
public:
|
|
GetBlockCount() : DirectParser(Api::BlockChain::GetBlockCountReply, 20) {}
|
|
|
|
void buildReply(const Message&, Streaming::MessageBuilder &builder) override {
|
|
builder.add(Api::BlockChain::BlockHeight, chainActive.Height());
|
|
}
|
|
};
|
|
|
|
// Live transactions
|
|
|
|
class GetLiveTransaction : public Api::DirectParser
|
|
{
|
|
public:
|
|
GetLiveTransaction() : DirectParser(Api::LiveTransactions::GetTransactionReply) {}
|
|
|
|
void findByTxid(const uint256 &txid) {
|
|
bool success = flApp->mempool()->lookup(txid, m_tx);
|
|
if (!success) {
|
|
const int ctorHeight = Params().GetConsensus().hf201811Height;
|
|
auto index = chainActive.Tip();
|
|
for (int i = 0; index && i < 6; ++i) {
|
|
Block block = Blocks::DB::instance()->loadBlock(index->GetBlockPos());
|
|
m_tx = block.findTransaction(txid, index->nHeight >= ctorHeight);
|
|
if (m_tx.size() > 0) {
|
|
m_blockHeight = index->nHeight;
|
|
return;
|
|
}
|
|
index = index->pprev;
|
|
}
|
|
throw Api::ParserException("No information available about transaction");
|
|
}
|
|
}
|
|
|
|
void findByScriptHashed(const uint256 &address) {
|
|
auto mempool = flApp->mempool();
|
|
LOCK(mempool->cs);
|
|
for (auto iter = mempool->mapTx.begin(); iter != mempool->mapTx.end(); ++iter) {
|
|
Tx::Iterator txIter(iter->tx);
|
|
while (txIter.next(Tx::OutputScript)) {
|
|
if (txIter.hashedByteData() == address) {
|
|
txIter.next(Tx::End);
|
|
m_tx = txIter.prevTx();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int calculateMessageSize(const Message &request) override
|
|
{
|
|
Streaming::MessageParser parser(request.body());
|
|
bool validQuery = false;
|
|
while (parser.next() == Streaming::FoundTag) {
|
|
if (parser.tag() == Api::LiveTransactions::TxId
|
|
|| parser.tag() == Api::LiveTransactions::GenericByteData) {
|
|
validQuery = true;
|
|
findByTxid(parser.uint256Data());
|
|
break;
|
|
}
|
|
else if (parser.tag() == Api::LiveTransactions::BitcoinScriptHashed) {
|
|
findByScriptHashed(parser.uint256Data());
|
|
// even if we don't find anything, we return success.
|
|
return m_tx.size() + 20;
|
|
}
|
|
}
|
|
if (!validQuery)
|
|
throw Api::ParserException("Missing or invalid search argument");
|
|
if (m_tx.data().isEmpty())
|
|
throw Api::ParserException("Not found");
|
|
return m_tx.size() + 26;
|
|
}
|
|
void buildReply(const Message&, Streaming::MessageBuilder &builder) override
|
|
{
|
|
if (m_blockHeight != -1)
|
|
builder.add(Api::LiveTransactions::BlockHeight, m_blockHeight);
|
|
builder.add(Api::GenericByteData, m_tx.data());
|
|
}
|
|
|
|
private:
|
|
Tx m_tx;
|
|
int m_blockHeight = -1;
|
|
};
|
|
|
|
class SendLiveTransaction : public Api::RpcParser
|
|
{
|
|
public:
|
|
SendLiveTransaction() : RpcParser("sendrawtransaction", Api::LiveTransactions::SendTransactionReply, 34) {}
|
|
|
|
virtual void createRequest(const Message &message, UniValue &output) override {
|
|
std::string tx;
|
|
Streaming::MessageParser parser(message.body());
|
|
while (parser.next() == Streaming::FoundTag) {
|
|
if (parser.tag() == Api::LiveTransactions::Transaction
|
|
|| parser.tag() == Api::LiveTransactions::GenericByteData) {
|
|
boost::algorithm::hex(parser.bytesData(), back_inserter(tx));
|
|
break; // only one per message
|
|
}
|
|
}
|
|
output.push_back(std::make_pair("", UniValue(UniValue::VSTR, tx)));
|
|
}
|
|
};
|
|
|
|
// Util
|
|
|
|
class CreateAddress : public Api::DirectParser
|
|
{
|
|
public:
|
|
CreateAddress() : DirectParser(Api::Util::CreateAddressReply, 150) {}
|
|
|
|
virtual void buildReply(const Message &, Streaming::MessageBuilder &builder) override {
|
|
PrivateKey key;
|
|
key.makeNewKey();
|
|
assert(key.isCompressed());
|
|
const KeyId pkh = key.getPubKey().getKeyId();
|
|
builder.addByteArray(Api::Util::BitcoinP2PKHAddress, pkh.begin(), pkh.size());
|
|
builder.addByteArray(Api::Util::PrivateKey, key.begin(), key.size());
|
|
}
|
|
};
|
|
|
|
class ValidateAddress : public Api::RpcParser {
|
|
public:
|
|
ValidateAddress() : RpcParser("validateaddress", Api::Util::ValidateAddressReply, 300) {}
|
|
|
|
virtual void buildReply(Streaming::MessageBuilder &builder, const UniValue &result) override {
|
|
const UniValue &isValid = find_value(result, "isvalid");
|
|
builder.add(Api::Util::IsValid, isValid.getBool());
|
|
const UniValue &address = find_value(result, "address");
|
|
builder.add(Api::Util::BitcoinP2PKHAddress, address.get_str()); // FIXME this is wrong, we should return a ripe160 address instead.
|
|
const UniValue &scriptPubKey = find_value(result, "scriptPubKey");
|
|
std::vector<char> bytearray;
|
|
boost::algorithm::unhex(scriptPubKey.get_str(), back_inserter(bytearray));
|
|
builder.add(Api::Util::ScriptPubKey, bytearray);
|
|
bytearray.clear();
|
|
}
|
|
virtual void createRequest(const Message &message, UniValue &output) override {
|
|
Streaming::MessageParser parser(message.body());
|
|
while (parser.next() == Streaming::FoundTag) {
|
|
if (parser.tag() == Api::Util::BitcoinP2PKHAddress) {
|
|
// FIXME bitcoin address is always ripe160, so this fails.
|
|
output.push_back(std::make_pair("param 1", UniValue(UniValue::VSTR, parser.stringData())));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
class RegTestGenerateBlock : public Api::RpcParser {
|
|
public:
|
|
RegTestGenerateBlock() : RpcParser("generate", Api::RegTest::GenerateBlockReply) {}
|
|
|
|
void createRequest(const Message &message, UniValue &output) override {
|
|
Streaming::MessageParser parser(message.body());
|
|
int amount = 1;
|
|
std::vector<uint8_t> outAddress;
|
|
while (parser.next() == Streaming::FoundTag) {
|
|
if (parser.tag() == Api::RegTest::Amount)
|
|
amount = parser.intData();
|
|
else if (parser.tag() == Api::RegTest::BitcoinP2PKHAddress)
|
|
outAddress = parser.unsignedBytesData();
|
|
}
|
|
if (amount <= 0 || amount > 150)
|
|
throw Api::ParserException("Invalid Amount argument");
|
|
if (outAddress.size() != 20)
|
|
throw Api::ParserException("Invalid BitcoinAddress (need 20 byte array)");
|
|
|
|
std::string hex;
|
|
boost::algorithm::hex(outAddress, back_inserter(hex));
|
|
output.push_back(std::make_pair("item0", UniValue(amount)));
|
|
output.push_back(std::make_pair("item1", UniValue(UniValue::VSTR, hex)));
|
|
m_messageSize = amount * 35;
|
|
}
|
|
|
|
void buildReply(Streaming::MessageBuilder &builder, const UniValue &result) override {
|
|
assert(result.getType() == UniValue::VARR);
|
|
for (size_t i = 0; i < result.size(); ++i) {
|
|
assert(result[i].get_str().size() == 64);
|
|
addHash256ToBuilder(builder, Api::RegTest::BlockHash, result[i]);
|
|
}
|
|
}
|
|
};
|
|
|
|
class GetTransaction : public Api::DirectParser {
|
|
public:
|
|
GetTransaction() : DirectParser(Api::BlockChain::GetTransactionReply) {}
|
|
int calculateMessageSize(const Message &request) override {
|
|
Streaming::MessageParser parser(request);
|
|
CBlockIndex *index = nullptr;
|
|
bool fullTxData = false;
|
|
while (parser.next() == Streaming::FoundTag) {
|
|
if (parser.tag() == Api::BlockChain::BlockHeight) {
|
|
index = chainActive[parser.intData()];
|
|
if (!index)
|
|
throw Api::ParserException("Unknown blockheight");
|
|
} else if (parser.tag() == Api::BlockChain::BlockHash) {
|
|
index = Blocks::Index::get(parser.uint256Data());
|
|
if (!index)
|
|
throw Api::ParserException("Unknown block hash");
|
|
} else if (parser.tag() == Api::BlockChain::Tx_OffsetInBlock) {
|
|
m_offsetInBlock = parser.intData();
|
|
if (m_offsetInBlock < 81)
|
|
throw Api::ParserException("OffsetInBlock out of range");
|
|
} else if (parser.tag() == Api::BlockChain::FullTransactionData) {
|
|
fullTxData = parser.boolData();
|
|
if (!fullTxData)
|
|
m_fullTxData = false;
|
|
} else if (parser.tag() == Api::BlockChain::Include_TxId) {
|
|
m_returnTxId = parser.boolData();
|
|
} else if (parser.tag() == Api::BlockChain::Include_OffsetInBlock) {
|
|
m_returnOffsetInBlock = parser.boolData();
|
|
} else if (parser.tag() == Api::BlockChain::FilterOutputIndex) {
|
|
if (!parser.isInt() || parser.intData() < 0)
|
|
throw Api::ParserException("FilterOutputIndex should be a positive number");
|
|
opt.filterOutputs.insert(parser.intData());
|
|
} else if (parser.tag() == Api::BlockChain::Include_TxFee) {
|
|
m_returnTxFee = parser.boolData();
|
|
} else {
|
|
opt.readParser(parser);
|
|
}
|
|
}
|
|
if (fullTxData) // if explicitly asked.
|
|
m_fullTxData = true;
|
|
else if (m_returnTxId || opt.shouldRun()) // we imply false if they want a subset.
|
|
m_fullTxData = false;
|
|
|
|
if (!index || m_offsetInBlock < 81)
|
|
throw Api::ParserException("Incomplete request.");
|
|
if (index->nDataPos < 4 || (index->nStatus & BLOCK_HAVE_DATA) == 0)
|
|
throw Api::ParserException("Block known but data not available");
|
|
m_blockheight = index->nHeight;
|
|
|
|
Block block;
|
|
try {
|
|
block = Blocks::DB::instance()->loadBlock(index->GetBlockPos());
|
|
assert(block.isFullBlock());
|
|
} catch (...) {
|
|
throw Api::ParserException("Blockdata not present on this Hub");
|
|
}
|
|
if (m_offsetInBlock > block.size() - 60)
|
|
throw Api::ParserException("OffsetInBlock larger than block");
|
|
try {
|
|
Tx::Iterator iter(block, m_offsetInBlock);
|
|
iter.next(Tx::End);
|
|
if (iter.tag() == Tx::End)
|
|
m_tx = iter.prevTx();
|
|
} catch (const std::runtime_error &e) {
|
|
throw Api::ParserException("Invalid offsetInBlock");
|
|
}
|
|
if (m_returnTxFee && m_offsetInBlock > 90) {
|
|
try {
|
|
BlockMetaData meta = Blocks::DB::instance()->loadBlockMetaData(index->GetMetaDataPos());
|
|
auto tx = meta.findTransaction(m_offsetInBlock);
|
|
if (tx && meta.hasFeesData())
|
|
m_txFee = tx->fees;
|
|
} catch (...) { }
|
|
}
|
|
|
|
int amount = m_fullTxData ? m_tx.size() + 10 : 0;
|
|
if (m_returnTxId) amount += 40;
|
|
if (m_returnOffsetInBlock) amount += 20;
|
|
if (opt.shouldRun())
|
|
amount += opt.calculateNeededSize(m_tx);
|
|
return amount;
|
|
}
|
|
void buildReply(const Message &, Streaming::MessageBuilder &builder) override {
|
|
if (m_returnTxId)
|
|
builder.add(Api::BlockChain::TxId, m_tx.createHash());
|
|
if (m_returnOffsetInBlock) {
|
|
builder.add(Api::BlockChain::Tx_OffsetInBlock, m_offsetInBlock);
|
|
builder.add(Api::BlockChain::BlockHeight, m_blockheight);
|
|
}
|
|
if (m_txFee >= 0)
|
|
builder.add(Api::BlockChain::Tx_Fees, m_txFee);
|
|
if (m_tx.size() > 0) {
|
|
if (opt.shouldRun()) {
|
|
Tx::Iterator iter(m_tx);
|
|
opt.serialize(builder, iter);
|
|
}
|
|
if (m_fullTxData)
|
|
builder.add(Api::BlockChain::GenericByteData, m_tx.data());
|
|
}
|
|
}
|
|
|
|
private:
|
|
bool m_fullTxData = true;
|
|
bool m_returnTxId = false;
|
|
bool m_returnOffsetInBlock = false;
|
|
bool m_returnTxFee = false;
|
|
int m_offsetInBlock = 0;
|
|
int m_blockheight = 0;
|
|
int m_txFee = -1;
|
|
Tx m_tx;
|
|
TransactionSerializationOptions opt;
|
|
};
|
|
|
|
class UtxoFetcher : public Api::DirectParser
|
|
{
|
|
public:
|
|
explicit UtxoFetcher(int replyId)
|
|
: DirectParser(replyId)
|
|
{
|
|
}
|
|
|
|
uint256 lookup(int blockHeight, int offsetInBlock) const
|
|
{
|
|
if (blockHeight == -1 || offsetInBlock == 0)
|
|
throw Api::ParserException("Invalid or missing txid / blockheight+offsetInBlock");
|
|
// try to find txid
|
|
auto index = chainActive[blockHeight];
|
|
if (!index)
|
|
throw Api::ParserException("Unknown blockheight");
|
|
Block block;
|
|
try {
|
|
block = Blocks::DB::instance()->loadBlock(index->GetBlockPos());
|
|
} catch (...) {
|
|
throw Api::ParserException("Blockdata not present on this Hub");
|
|
}
|
|
if (offsetInBlock > block.size())
|
|
throw Api::ParserException("OffsetInBlock larger than block");
|
|
Tx::Iterator iter(block, offsetInBlock);
|
|
try {
|
|
if (iter.next(Tx::End) == Tx::End)
|
|
return iter.prevTx().createHash();
|
|
} catch (const std::runtime_error &error) {
|
|
logDebug() << error;
|
|
}
|
|
throw Api::ParserException("Invalid data, is your offsetInBlock correct?");
|
|
}
|
|
|
|
int calculateMessageSize(const Message &request) override {
|
|
int validCount = 0;
|
|
Streaming::MessageParser parser(request.body());
|
|
uint256 txid;
|
|
int blockHeight = -1;
|
|
int offsetInBlock = 0;
|
|
int output = 0;
|
|
while (parser.next() == Streaming::FoundTag) {
|
|
if (parser.tag() == Api::LiveTransactions::TxId)
|
|
txid = parser.uint256Data();
|
|
else if (parser.tag() == Api::LiveTransactions::BlockHeight) {
|
|
blockHeight = parser.intData();
|
|
}
|
|
else if (parser.tag() == Api::LiveTransactions::OffsetInBlock) {
|
|
offsetInBlock = parser.intData();
|
|
}
|
|
else if (parser.tag() == Api::LiveTransactions::OutIndex) {
|
|
if (!parser.isInt())
|
|
throw Api::ParserException("index wasn't number");
|
|
output = parser.intData();
|
|
}
|
|
else if (parser.tag() == Api::Separator) {
|
|
if (txid.IsNull())
|
|
txid = lookup(blockHeight, offsetInBlock);
|
|
assert(!txid.IsNull());
|
|
blockHeight = -1;
|
|
offsetInBlock = 0;
|
|
auto out = g_utxo->find(txid, output);
|
|
m_utxos.push_back(out);
|
|
if (out.isValid())
|
|
validCount++;
|
|
output = 0;
|
|
txid.SetNull();
|
|
}
|
|
}
|
|
|
|
if (txid.IsNull())
|
|
txid = lookup(blockHeight, offsetInBlock);
|
|
assert(!txid.IsNull());
|
|
|
|
auto out = g_utxo->find(txid, output);
|
|
m_utxos.push_back(out);
|
|
if (out.isValid())
|
|
validCount++;
|
|
|
|
const bool isVerbose = replyMessageId() == Api::LiveTransactions::GetUnspentOutputReply;
|
|
int size = (m_utxos.size() - validCount) + validCount * 20;
|
|
if (isVerbose) {
|
|
// since I can't assume the max-size of the output-script, I need to actually fetch them here.
|
|
for (auto &unspent : m_utxos) {
|
|
if (unspent.isValid()) {
|
|
size += 10; // for amount
|
|
auto uod = UnspentOutputData::fromUtxoDB(unspent);
|
|
size += uod.outputScript().size() + 3;
|
|
}
|
|
}
|
|
}
|
|
return size;
|
|
}
|
|
void buildReply(const Message&, Streaming::MessageBuilder &builder) override {
|
|
const bool verbose = replyMessageId() == Api::LiveTransactions::GetUnspentOutputReply;
|
|
bool first = true;
|
|
for (const auto &unspent : m_utxos) {
|
|
if (first)
|
|
first = false;
|
|
else
|
|
builder.add(Api::LiveTransactions::Separator, true);
|
|
const bool isValid = unspent.isValid();
|
|
builder.add(Api::LiveTransactions::UnspentState, isValid);
|
|
if (isValid) {
|
|
builder.add(Api::LiveTransactions::BlockHeight, unspent.blockHeight());
|
|
builder.add(Api::LiveTransactions::OffsetInBlock, unspent.offsetInBlock());
|
|
builder.add(Api::LiveTransactions::OutIndex, unspent.outIndex());
|
|
if (verbose) {
|
|
auto uod = UnspentOutputData::fromUtxoDB(unspent);
|
|
builder.add(Api::LiveTransactions::Amount, (uint64_t) uod.outputValue);
|
|
builder.add(Api::LiveTransactions::OutputScript, uod.outputScript());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
private:
|
|
std::vector<UnspentOutput> m_utxos;
|
|
};
|
|
|
|
class MempoolSearch: public Api::DirectParser
|
|
{
|
|
public:
|
|
explicit MempoolSearch()
|
|
: DirectParser(Api::LiveTransactions::SearchMempoolReply)
|
|
{
|
|
}
|
|
|
|
int calculateMessageSize(const Message &request) override {
|
|
Streaming::MessageParser parser(request);
|
|
std::set<uint256> scriptHashes;
|
|
|
|
bool fullTxData = false;
|
|
while (parser.next() == Streaming::FoundTag) {
|
|
if (parser.tag() == Api::LiveTransactions::TxId) {
|
|
if (parser.dataLength() != 32)
|
|
throw Api::ParserException("TxId should be a 32 byte-bytearray");
|
|
LOCK(mempool.cs);
|
|
auto i = mempool.mapTx.find(parser.uint256Data());
|
|
if (i != mempool.mapTx.end()) {
|
|
ResultPair result;
|
|
result.tx = i->tx;
|
|
result.dsProof = i->dsproof;
|
|
result.time = i->GetTime();
|
|
m_results.push_back(std::move(result));
|
|
}
|
|
}
|
|
else if (parser.tag() == Api::LiveTransactions::BitcoinScriptHashed) {
|
|
if (parser.dataLength() != 32)
|
|
throw Api::ParserException("ScriptHash should be a 32 byte-bytearray");
|
|
scriptHashes.insert(parser.uint256Data());
|
|
} else if (parser.tag() == Api::LiveTransactions::Include_TxId) {
|
|
m_returnTxId = parser.boolData();
|
|
} else if (parser.tag() == Api::LiveTransactions::FullTransactionData) {
|
|
fullTxData = parser.boolData();
|
|
if (!fullTxData)
|
|
m_fullTxData = false;
|
|
} else if (parser.tag() == Api::BlockChain::FilterOutputIndex) {
|
|
if (!parser.isInt() || parser.intData() < 0)
|
|
throw Api::ParserException("FilterOutputIndex should be a positive number");
|
|
opt.filterOutputs.insert(parser.intData());
|
|
}
|
|
else {
|
|
opt.readParser(parser);
|
|
}
|
|
}
|
|
if (fullTxData) // if explicitly asked.
|
|
m_fullTxData = true;
|
|
else if (m_returnTxId || opt.shouldRun()) // we imply false if they want a subset.
|
|
m_fullTxData = false;
|
|
|
|
if (!scriptHashes.empty()) {
|
|
LOCK(mempool.cs);
|
|
for (auto iter = mempool.mapTx.begin(); iter != mempool.mapTx.end(); ++iter) {
|
|
Tx::Iterator txIter(iter->tx);
|
|
int outIndex = 0;
|
|
while (txIter.next(Tx::OutputScript) == Tx::OutputScript) {
|
|
auto hit = scriptHashes.find(txIter.hashedByteData());
|
|
if (hit != scriptHashes.end()) {
|
|
ResultPair result;
|
|
result.tx = iter->tx;
|
|
result.dsProof = iter->dsproof;
|
|
result.time = iter->GetTime();
|
|
result.outputIndex = outIndex;
|
|
m_results.push_back(std::move(result));
|
|
if (m_results.size() > 2500) // protect the Hub from DOS.
|
|
break;
|
|
}
|
|
++outIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
// this is a quick and dirty calculation aiming to rather have more bytes reserved
|
|
// than used.
|
|
int bytesPerTx = 5;
|
|
if (m_returnTxId) bytesPerTx += 35;
|
|
if (m_fullTxData) bytesPerTx += 5; // actual tx-data is in 'size'
|
|
|
|
const bool optShouldRun = opt.shouldRun();
|
|
|
|
int total = 45;
|
|
for (const auto &rp : m_results) {
|
|
total += bytesPerTx;
|
|
if (optShouldRun)
|
|
total += opt.calculateNeededSize(rp.tx);
|
|
if (m_fullTxData)
|
|
total += rp.tx.size();
|
|
if (rp.dsProof != -1)
|
|
total += 35;
|
|
}
|
|
return total;
|
|
}
|
|
void buildReply(const Message &, Streaming::MessageBuilder &builder) override {
|
|
const bool optShouldRun = opt.shouldRun();
|
|
|
|
for (const auto &rp : m_results) {
|
|
Tx::Iterator iter(rp.tx);
|
|
if (m_returnTxId)
|
|
builder.add(Api::LiveTransactions::TxId, rp.tx.createHash());
|
|
if (rp.outputIndex != -1)
|
|
builder.add(Api::LiveTransactions::MatchingOutIndex, rp.outputIndex);
|
|
builder.add(Api::LiveTransactions::FirstSeenTime, rp.time);
|
|
if (m_fullTxData)
|
|
builder.add(Api::LiveTransactions::Transaction, rp.tx.data());
|
|
if (optShouldRun)
|
|
opt.serialize(builder, iter);
|
|
if (rp.dsProof != -1) {
|
|
auto dsp = mempool.doubleSpendProofStorage()->proof(rp.dsProof);
|
|
if (!dsp.isEmpty())
|
|
builder.add(Api::LiveTransactions::DSProofId, dsp.createHash());
|
|
}
|
|
builder.add(Api::Separator, true);
|
|
}
|
|
}
|
|
private:
|
|
struct ResultPair {
|
|
Tx tx;
|
|
int dsProof = -1;
|
|
int outputIndex = -1;
|
|
uint64_t time = 0; // time the tx entered the mempool
|
|
};
|
|
std::deque<ResultPair> m_results;
|
|
bool m_fullTxData = true;
|
|
bool m_returnTxId = false;
|
|
TransactionSerializationOptions opt;
|
|
};
|
|
|
|
class SendLiveTransactonASync : public Api::ASyncParser {
|
|
public:
|
|
SendLiveTransactonASync(const Message &request)
|
|
: Api::ASyncParser(request, Api::LiveTransactions::SendTransactionReply, 34)
|
|
{
|
|
}
|
|
|
|
void run() override {
|
|
Streaming::MessageParser parser(m_request);
|
|
Tx tx;
|
|
bool validateOnly = false;
|
|
while (parser.next() == Streaming::FoundTag) {
|
|
if (parser.tag() == Api::LiveTransactions::Transaction
|
|
|| parser.tag() == Api::LiveTransactions::GenericByteData) {
|
|
if (!tx.data().isEmpty())
|
|
throw Api::ParserException("Only one Tx per message allowed");
|
|
tx = Tx(parser.bytesDataBuffer());
|
|
}
|
|
if (parser.tag() == Api::LiveTransactions::ValidateOnly)
|
|
validateOnly = parser.boolData();
|
|
}
|
|
if (tx.data().isEmpty())
|
|
throw Api::ParserException("No transaction found in message");
|
|
|
|
std::uint32_t flags = 0;
|
|
if (validateOnly)
|
|
flags += Validation::TxValidateOnly;
|
|
flags += Validation::RejectAbsurdFeeTx;
|
|
auto resultFuture = Application::instance()->validation()->addTransaction(tx, flags);
|
|
auto result = resultFuture.get(); // <= blocking call.
|
|
if (!result.empty())
|
|
throw Api::ParserException(result.c_str());
|
|
|
|
m_txid = tx.createHash();
|
|
}
|
|
|
|
void buildReply(Streaming::MessageBuilder &builder) override {
|
|
// simply return the txid of the transaction.
|
|
builder.add(Api::LiveTransactions::TxId, m_txid);
|
|
}
|
|
|
|
private:
|
|
uint256 m_txid;
|
|
};
|
|
|
|
class GetMempoolInfo : public Api::RpcParser
|
|
{
|
|
public:
|
|
GetMempoolInfo() : RpcParser("getmempoolinfo", Api::LiveTransactions::GetMempoolInfoReply, 60) {}
|
|
|
|
virtual void buildReply(Streaming::MessageBuilder& builder, const UniValue& result) override {
|
|
const UniValue& size = find_value(result, "size");
|
|
builder.add(Api::LiveTransactions::MempoolSize, (uint64_t) size.get_int64());
|
|
const UniValue& bytes = find_value(result, "bytes");
|
|
builder.add(Api::LiveTransactions::MempoolBytes, (uint64_t) bytes.get_int64());
|
|
const UniValue& usage = find_value(result, "usage");
|
|
builder.add(Api::LiveTransactions::MempoolUsage, (uint64_t) usage.get_int64());
|
|
const UniValue& maxmempool = find_value(result, "maxmempool");
|
|
builder.add(Api::LiveTransactions::MaxMempool, (uint64_t) maxmempool.get_int64());
|
|
const UniValue& mempoolminfee = find_value(result, "mempoolminfee");
|
|
builder.add(Api::LiveTransactions::MempoolMinFee, mempoolminfee.get_real());
|
|
}
|
|
};
|
|
|
|
class SubmitBlock : public Api::DirectParser
|
|
{
|
|
public:
|
|
SubmitBlock()
|
|
: Api::DirectParser(Api::Mining::SubmitBlockReply, 40) {}
|
|
|
|
void buildReply(const Message &incoming, Streaming::MessageBuilder &builder) override {
|
|
Block block;
|
|
Streaming::MessageParser parser(incoming.body());
|
|
while (parser.next() != Streaming::EndOfDocument) {
|
|
if (parser.tag() == Api::Mining::GenericByteData) {
|
|
block = Block(parser.bytesDataBuffer());
|
|
}
|
|
}
|
|
if (block.isEmpty())
|
|
throw Api::ParserException("Missing block data");
|
|
if (!block.isFullBlock())
|
|
throw Api::ParserException("Not enough block data");
|
|
|
|
|
|
uint256 hash = block.createHash();
|
|
CBlockIndex *pindex = Blocks::Index::get(hash);
|
|
if (pindex) {
|
|
if (pindex->nStatus & BLOCK_FAILED_MASK)
|
|
throw Api::ParserException("duplicate-invalid");
|
|
if (Blocks::DB::instance()->headerChain().Contains(pindex))
|
|
throw Api::ParserException("duplicate");
|
|
}
|
|
|
|
auto future = Application::instance()->validation()->addBlock(block, Validation::SaveGoodToDisk | Validation::ForwardGoodToPeers);
|
|
future.start();
|
|
future.waitUntilFinished();
|
|
if (!future.error().empty())
|
|
throw Api::ParserException(future.error().c_str());
|
|
auto index = future.blockIndex();
|
|
if (index && future.error().empty() && index->IsValid()) {// all Ok
|
|
builder.add(Api::Mining::BlockHash, hash);
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
|
|
Api::Parser *Api::createParser(const Message &message)
|
|
{
|
|
switch (message.serviceId()) {
|
|
case Api::BlockChainService:
|
|
switch (message.messageId()) {
|
|
case Api::BlockChain::GetBlockChainInfo:
|
|
return new GetBlockChainInfo();
|
|
case Api::BlockChain::GetBestBlockHash:
|
|
return new GetBestBlockHash();
|
|
case Api::BlockChain::GetBlock:
|
|
return new GetBlock();
|
|
case Api::BlockChain::GetBlockVerbose:
|
|
return new GetBlockLegacy();
|
|
case Api::BlockChain::GetBlockHeader:
|
|
return new GetBlockHeader();
|
|
case Api::BlockChain::GetBlockCount:
|
|
return new GetBlockCount();
|
|
case Api::BlockChain::GetTransaction:
|
|
return new GetTransaction();
|
|
}
|
|
break;
|
|
case Api::LiveTransactionService:
|
|
switch (message.messageId()) {
|
|
case Api::LiveTransactions::GetTransaction:
|
|
return new GetLiveTransaction();
|
|
case Api::LiveTransactions::SendTransaction:
|
|
if (message.headerInt(Api::ASyncRequest) <= 0)
|
|
return new SendLiveTransaction();
|
|
return new SendLiveTransactonASync(message);
|
|
case Api::LiveTransactions::IsUnspent:
|
|
return new UtxoFetcher(Api::LiveTransactions::IsUnspentReply);
|
|
case Api::LiveTransactions::GetUnspentOutput:
|
|
return new UtxoFetcher(Api::LiveTransactions::GetUnspentOutputReply);
|
|
case Api::LiveTransactions::SearchMempool:
|
|
return new MempoolSearch();
|
|
case Api::LiveTransactions::GetMempoolInfo:
|
|
return new GetMempoolInfo();
|
|
}
|
|
break;
|
|
case Api::UtilService:
|
|
switch (message.messageId()) {
|
|
case Api::Util::CreateAddress:
|
|
return new CreateAddress();
|
|
case Api::Util::ValidateAddress:
|
|
return new ValidateAddress();
|
|
}
|
|
break;
|
|
case Api::RegTestService:
|
|
switch (message.messageId()) {
|
|
case Api::RegTest::GenerateBlock:
|
|
return new RegTestGenerateBlock();
|
|
}
|
|
break;
|
|
case Api::MiningService:
|
|
switch (message.messageId()) {
|
|
case Api::Mining::SubmitBlock:
|
|
return new SubmitBlock();
|
|
}
|
|
break;
|
|
}
|
|
throw std::runtime_error("Unsupported command");
|
|
}
|
|
|
|
|
|
Api::Parser::Parser(ParserType type, int answerMessageId, int messageSize)
|
|
: m_messageSize(messageSize),
|
|
m_replyMessageId(answerMessageId),
|
|
m_type(type),
|
|
data(nullptr)
|
|
{
|
|
}
|
|
|
|
void Api::Parser::setSessionData(Api::SessionData **value)
|
|
{
|
|
data = value;
|
|
}
|
|
|
|
Api::RpcParser::RpcParser(const std::string &method, int replyMessageId, int messageSize)
|
|
: Parser(WrapsRPCCall, replyMessageId, messageSize),
|
|
m_method(method)
|
|
{
|
|
}
|
|
|
|
void Api::RpcParser::buildReply(Streaming::MessageBuilder &builder, const UniValue &result)
|
|
{
|
|
assert(result.isStr());
|
|
if (result.get_str().size() == 64) { // assume sha256 which for some reason gets reversed in text
|
|
addHash256ToBuilder(builder, 1, result);
|
|
} else {
|
|
std::vector<char> answer;
|
|
boost::algorithm::unhex(result.get_str(), back_inserter(answer));
|
|
builder.add(1, answer);
|
|
}
|
|
}
|
|
|
|
void Api::RpcParser::createRequest(const Message &, UniValue &)
|
|
{
|
|
}
|
|
|
|
int Api::RpcParser::calculateMessageSize(const UniValue &result) const
|
|
{
|
|
return result.get_str().size() + 20;
|
|
}
|
|
|
|
Api::DirectParser::DirectParser(int replyMessageId, int messageSize)
|
|
: Parser(IncludesHandler, replyMessageId, messageSize)
|
|
{
|
|
}
|
|
|
|
Api::ASyncParser::ASyncParser(const Message &request, int replyMessageId, int messageSize)
|
|
: Parser(Parser::ASyncParser, -1, -1),
|
|
m_request(request)
|
|
{
|
|
std::unique_lock<std::mutex> lock(m_lock);
|
|
m_messageSize = messageSize;
|
|
m_replyMessageId = replyMessageId;
|
|
}
|
|
|
|
void Api::ASyncParser::start(std::atomic_bool *token, NetworkConnection &&connection, Api::Server *server)
|
|
{
|
|
assert(server);
|
|
assert(token);
|
|
{
|
|
std::unique_lock<std::mutex> lock(m_lock);
|
|
// make sure that those values are going to be stored and available to the
|
|
// thread that will be started later.
|
|
m_con = std::move(connection);
|
|
m_server = server;
|
|
m_token = token;
|
|
}
|
|
m_thread = std::thread(&Api::ASyncParser::run_priv, this);
|
|
}
|
|
|
|
void Api::ASyncParser::run_priv()
|
|
{
|
|
struct RAII {
|
|
RAII(std::atomic_bool *token, Api::ASyncParser *parser)
|
|
: m_token(token), m_parser(parser)
|
|
{
|
|
}
|
|
~RAII() {
|
|
m_token->store(false);
|
|
boost::asio::post(Application::instance()->ioContext(), std::bind(&Api::ASyncParser::deleteMe, m_parser));
|
|
}
|
|
private:
|
|
std::atomic_bool *m_token;
|
|
Api::ASyncParser *m_parser;
|
|
};
|
|
|
|
std::unique_lock<std::mutex> lock(m_lock);
|
|
assert(m_token);
|
|
RAII deleteOnExit(m_token, this);
|
|
assert(m_server);
|
|
try {
|
|
run();
|
|
assert(m_messageSize >= 0); // run should have set that.
|
|
Streaming::MessageBuilder builder(Streaming::pool(m_messageSize));
|
|
buildReply(builder);
|
|
|
|
Message reply = builder.reply(m_request, m_replyMessageId);
|
|
if (m_messageSize < reply.body().size())
|
|
logDebug() << "Generated message larger than space reserved."
|
|
<< m_request.serviceId() << m_replyMessageId
|
|
<< "reserved:" << m_messageSize << "built:" << reply.body().size();
|
|
assert(reply.body().size() <= m_messageSize); // fail fast.
|
|
m_con.send(reply);
|
|
} catch (const ParserException &e) {
|
|
logWarning(Log::ApiServer) << e;
|
|
m_con.send(m_server->createFailedMessage(m_request, e.what()));
|
|
} catch (const std::exception &e) {
|
|
logWarning(Log::ApiServer) << "Async API parser failed:" << e.what();
|
|
m_con.disconnect();
|
|
}
|
|
}
|
|
|
|
void Api::ASyncParser::deleteMe()
|
|
{
|
|
m_thread.join();
|
|
delete this;
|
|
}
|