git / moonbite-core / src/mweb/mweb_node.cpp · raw
#include <mweb/mweb_node.h>#include <chain.h>#include <consensus/validation.h>#include <mw/consensus/Amount.h>#include <mw/node/BlockValidator.h>#include <primitives/block.h>#include <primitives/transaction.h>#include <script/interpreter.h>#include <undo.h>#include <util/system.h>#include <validation.h>using namespace MWEB;bool Node::CheckBlock(const CBlock& block, BlockValidationState& state){ // HasMWEBTx() is true only when mweb txs being shared outside of a block (for use by mempools). // Blocks themselves do not store mweb txs like normal txs. // They are instead stored and processed separately in the mweb block. // So at this point, no txs should contain MWEB data. for (const CTransactionRef& pTx : block.vtx) { if (pTx->HasMWEBTx()) { return state.Invalid(BlockValidationResult::BLOCK_MUTATED, "unexpected-mweb-data", "Block contains transactions with MWEB data attached"); } } return true;}bool Node::ContextualCheckBlock(const CBlock& block, const Consensus::Params& consensus_params, const CBlockIndex* pindexPrev, BlockValidationState& state){ // If MWEB is enabled, we must have an mweb_block. If it's not enabled, we must not have an MWEB block. if (!IsMWEBEnabled(pindexPrev, consensus_params)) { // No MWEB data is allowed in blocks that don't commit to MWEB data, as this would otherwise leave room for spam if (!block.mweb_block.IsNull()) { return state.Invalid(BlockValidationResult::BLOCK_MUTATED, "unexpected-mweb-data", "MWEB not activated, but extension block found"); } // The HogEx marker is not committed by the canonical merkle root, so before activation it is mutated data, just like an extension block. for (const CTransactionRef& pTx : block.vtx) { if (pTx->IsHogEx()) { return state.Invalid(BlockValidationResult::BLOCK_MUTATED, "unexpected-mweb-data", "MWEB not activated, but HogEx marker found"); } } return true; } else if (block.mweb_block.IsNull()) { return state.Invalid(BlockValidationResult::BLOCK_MUTATED, "mweb-missing", "MWEB activated but extension block not found"); } // Validate each transaction in the block. for (const auto& tx : block.vtx) { // Verify that there are no pegin outputs in the coinbase or HogEx transactions. if (tx->IsCoinBase() || tx->IsHogEx()) { for (const CTxOut& out : tx->vout) { if (out.scriptPubKey.IsMWEBPegin()) { return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-tx-unexpected-pegin", "Pegin found in coinbase or HogEx"); } } } } // Last transaction must be marked as HogEx. // Note: We don't commit to the HogEx indicator, so if indicator not set, just mark the block as mutated. const CTransactionRef& pHogEx = block.vtx.back(); if (!pHogEx->IsHogEx()) { return state.Invalid(BlockValidationResult::BLOCK_MUTATED, "hogex-missing", "hogex missing"); } // Only the last transaction in the block should be marked as the HogEx. for (size_t i = 0; i < block.vtx.size() - 1; i++) { if (block.vtx[i]->IsHogEx()) { return state.Invalid(BlockValidationResult::BLOCK_MUTATED, "bad-hogex-position", "hogex in wrong position"); } } // UnserializeTransaction ensures that vout is never empty when the HogEx indicator is set. assert(!pHogEx->vout.empty()); // Verify that the first HogEx output contains the HogAddr. mw::Hash mweb_hash; if (!pHogEx->vout.front().scriptPubKey.IsMWEBHogAddr(&mweb_hash)) { return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-hogex", "HogEx missing or invalid"); } // Verify that the MWEB header hash matches. if (mweb_hash != block.mweb_block.m_block->GetHash()) { return state.Invalid(BlockValidationResult::BLOCK_MUTATED, "mweb-hash-mismatch", "MWEB header hash doesn't match hogex hash"); } // Verify that the MWEB block's height is correct. if (block.mweb_block.GetHeight() != (pindexPrev->nHeight + 1)) { return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "mweb-height-mismatch", "Invalid MWEB block height"); } // For the very first HogEx transaction, all inputs are pegins, so start at index of 0. // For all other HogEx transaction, the first input is not a pegin, so start looking for pegins at index 1. const bool is_first_hogex = !IsMWEBEnabled(pindexPrev->pprev, consensus_params); size_t next_pegin_idx = is_first_hogex ? 0 : 1; // Loop through the block's txs looking for all outputs with pegin scriptPubKeys (skip Coinbase & HogEx which don't support pegins). // While looping though, we perform 2 tasks: // // 1. Calculate the total value of the HogEx inputs (hogex_input_amount). // This is the previous HogEx's first output amount (pIndexPrev->mweb_amount) plus the sum of all pegin output amounts. // // 2. Verify the HogEx inputs (ignoring the input that spends previous HogEx output) exactly match the pegin outputs. for (size_t nTx = 1; nTx < block.vtx.size() - 1; nTx++) { const CTransactionRef& pTx = block.vtx[nTx]; for (size_t nOut = 0; nOut < pTx->vout.size(); nOut++) { const CTxOut& output = pTx->vout[nOut]; if (output.scriptPubKey.IsMWEBPegin()) { if (pHogEx->vin.size() <= next_pegin_idx) { return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "pegins-missing", "Pegins missing from HogEx"); } const COutPoint& hogex_input = pHogEx->vin[next_pegin_idx++].prevout; if (hogex_input.n != nOut || hogex_input.hash != pTx->GetHash()) { return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "pegin-mismatch", "HogEx pegins do not match block's pegins"); } } } } // 'next_pegin_idx' should equal the size of pHogEx->vin, meaning all HogEx inputs match the pegin outputs. // If it's not equal, that means we have more HogEx inputs than pegin outputs. if (next_pegin_idx != pHogEx->vin.size()) { return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "extra-hogex-input", "HogEx contains unexpected input(s)"); } // Verify that the mw::Block is valid, and the pegins and pegouts all match. if (!MWEB::Node::ValidateMWEBBlock(block)) { return state.Invalid(BlockValidationResult::BLOCK_MUTATED, "bad-blk-mweb", "BlockValidator::ValidateBlock failed"); } return true;}bool Node::ValidateMWEBBlock(const CBlock& block){ const CTransactionRef& pHogEx = block.vtx.back(); // Find all pegin scriptPubKeys in the block. // We don't support pegins in the coinbase tx or the HogEx tx, so skip those. std::vector<PegInCoin> block_pegins; for (size_t i = 1; i < block.vtx.size() - 1; i++) { for (const CTxOut& out : block.vtx[i]->vout) { mw::Hash kernel_id; if (out.scriptPubKey.IsMWEBPegin(&kernel_id)) { block_pegins.push_back(PegInCoin{out.nValue, std::move(kernel_id)}); } } } // Find all pegout outputs in the HogEx transaction. // The first output is not a pegout. It contains the MWEB balance and header hash. // The remaining outputs are all pegouts. std::vector<PegOutCoin> hogex_pegouts; for (size_t i = 1; i < pHogEx->vout.size(); i++) { const CTxOut& out = pHogEx->vout[i]; const CScript& pubkey = out.scriptPubKey; hogex_pegouts.push_back(PegOutCoin(out.nValue, {pubkey.begin(), pubkey.end()})); } // Call into the libmw context-free block validator to validate the TxBody, // and verify that the pegins and pegouts all match. return BlockValidator::ValidateBlock(block.mweb_block.m_block, block_pegins, hogex_pegouts);}bool Node::ConnectBlock(const CBlock& block, const Consensus::Params& consensus_params, const CBlockIndex* pindexPrev, CBlockUndo& blockundo, mw::CoinsViewCache& mweb_view, BlockValidationState& state){ if (!block.mweb_block.IsNull()) { const CTransactionRef& pHogEx = block.vtx.back(); // Verify that the first input of the HogEx tx spends the first output of the previous block's HogEx tx. // There is one important exception case we must handle, though. // The very first HogEx tx won't have a previous one to spend. // For that special case, this check will be skipped, and its first input will be treated as a pegin. const bool is_first_hogex = !IsMWEBEnabled(pindexPrev->pprev, consensus_params); if (!is_first_hogex) { const COutPoint& prev_hogex_out = pHogEx->vin.front().prevout; if (prev_hogex_out.n != 0 || pindexPrev->hogex_hash != prev_hogex_out.hash) { return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "invalid-hogex-input", "First input of HogEx does not point to previous HogEx"); } } // Loop through the block's txs looking for all outputs with pegin scriptPubKeys (skip Coinbase & HogEx which don't support pegins). // While looping though, we perform 2 tasks: // // 1. Calculate the total value of the HogEx inputs (hogex_input_amount). // This is the previous HogEx's first output amount (pIndexPrev->mweb_amount) plus the sum of all pegin output amounts. // // 2. Verify the HogEx inputs (ignoring the input that spends previous HogEx output) exactly match the pegin outputs. CAmount hogex_input_amount = pindexPrev->mweb_amount; for (size_t nTx = 1; nTx < block.vtx.size() - 1; nTx++) { const CTransactionRef& pTx = block.vtx[nTx]; for (const CTxOut& output : pTx->vout) { if (output.scriptPubKey.IsMWEBPegin()) { hogex_input_amount += output.nValue; if (!MoneyRange(hogex_input_amount)) { return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "accumulated-pegin-outofrange", "Accumulated pegin amount for the block is out of range"); } } } } const auto mweb_fee = block.mweb_block.GetTotalFee(); const auto supply_change = block.mweb_block.GetSupplyChange(); if (!mweb_fee || !supply_change) { return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-mweb-amount", "Invalid MWEB amount accounting"); } // For the HogEx transaction, the fee must be equal to the total fee of the extension block. CAmount hogex_fee = hogex_input_amount - pHogEx->GetValueOut(); if (!MoneyRange(hogex_fee) || hogex_fee != *mweb_fee) { return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-txns-mweb-fee-mismatch", "HogEx fee does not match MWEB fee."); } // Verify that the value of the first HogEx output matches the expected new value of the MWEB. // This is calculated simply as: 'mweb_amount = previous_amount + supply_change' // where 'supply_change = (pegins - pegouts) - fees' const auto mweb_amount = AmountUtil::TrySafeAdd(pindexPrev->mweb_amount, *supply_change); if (!mweb_amount) { return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-mweb-amount", "Invalid MWEB supply change"); } if (!MoneyRange(*mweb_amount) || *mweb_amount != pHogEx->vout.front().nValue) { return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "mweb-amount-mismatch", "HogEx amount does not match expected MWEB amount"); } const bool allow_historical_metadata_mismatch = !consensus_params.mweb_input_metadata_grandfather_blockhash.IsNull() && block.GetHash() == consensus_params.mweb_input_metadata_grandfather_blockhash; try { blockundo.mwundo = mweb_view.ApplyBlock(block.mweb_block.m_block, allow_historical_metadata_mismatch); } catch (const std::exception& e) { // ApplyBlock reconciles the serialized MWEB body against the // header/state commitments. Some failures here can be caused by // mutating MWEB body data without changing the canonical block hash. return state.Invalid(BlockValidationResult::BLOCK_MUTATED, "mweb-connect-failed", strprintf("MWEB::Node::ConnectBlock(): Failed to connect MWEB block: %s", e.what())); } for (const auto& input : block.mweb_block.m_block->GetInputs()) { // Verify that none of the MWEB inputs are spending frozen MWEB outputs. for (const uint256& frozen_output_id : consensus_params.frozen_mweb_output_ids) { if (uint256(input.GetOutputID().vec()) == frozen_output_id) { return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "frozen-mweb-output-spent", strprintf("MWEB::Node::ConnectBlock(): Frozen MWEB output spent: %s", input.GetOutputID().ToHex())); } } } } return true;}bool Node::CheckTransaction(const CTransaction& tx, TxValidationState& state){ std::unordered_map<mw::Hash, CAmount> tx_pegins; for (const CTxOut& out : tx.vout) { mw::Hash kernel_id; if (out.scriptPubKey.IsMWEBPegin(&kernel_id)) { if (tx_pegins.find(kernel_id) != tx_pegins.end()) { return state.Invalid(TxValidationResult::TX_RECENT_CONSENSUS_CHANGE, "duplicate-tx-pegins"); } tx_pegins.insert({std::move(kernel_id), out.nValue}); } } auto pegin_coins = tx.mweb_tx.GetPegIns(); if (pegin_coins.size() != tx_pegins.size()) { return state.Invalid(TxValidationResult::TX_WITNESS_MUTATED, "pegin-count-mismatch"); } for (const auto& pegin : pegin_coins) { auto iter = tx_pegins.find(pegin.GetKernelID()); if (iter == tx_pegins.end() || pegin.GetAmount() != iter->second) { return state.Invalid(TxValidationResult::TX_WITNESS_MUTATED, "pegin-mismatch"); } } // If the transaction has MWEB data, call the libmw transaction validation logic. if (tx.HasMWEBTx()) { try { tx.mweb_tx.m_transaction->Validate(); } catch (const std::exception& e) { return state.Invalid(TxValidationResult::TX_WITNESS_MUTATED, "bad-mweb-txn"); } } return true;}