diff --git a/src/Makefile.am b/src/Makefile.am index 8905c0ad1cd2..98e2c5a0da35 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -271,6 +271,7 @@ BITCOIN_CORE_H = \ script/signingprovider.h \ script/solver.h \ shutdown.h \ + sidechain.h \ signet.h \ streams.h \ support/allocators/pool.h \ @@ -457,6 +458,7 @@ libbitcoin_node_a_SOURCES = \ rpc/txoutproof.cpp \ script/sigcache.cpp \ shutdown.cpp \ + sidechain.cpp \ signet.cpp \ timedata.cpp \ torcontrol.cpp \ @@ -968,6 +970,7 @@ libbitcoinkernel_la_SOURCES = \ script/script_error.cpp \ script/sigcache.cpp \ script/solver.cpp \ + sidechain.cpp \ signet.cpp \ streams.cpp \ support/cleanse.cpp \ diff --git a/src/coins.cpp b/src/coins.cpp index b44d920ee1ff..a8569b44d9fa 100644 --- a/src/coins.cpp +++ b/src/coins.cpp @@ -10,21 +10,21 @@ #include #include -bool CCoinsView::GetCoin(const COutPoint &outpoint, Coin &coin) const { return false; } +bool CCoinsView::GetCoinRaw(const COutPoint &outpoint, Coin &coin) const { return false; } uint256 CCoinsView::GetBestBlock() const { return uint256(); } std::vector CCoinsView::GetHeadBlocks() const { return std::vector(); } bool CCoinsView::BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock, bool erase) { return false; } std::unique_ptr CCoinsView::Cursor() const { return nullptr; } -bool CCoinsView::HaveCoin(const COutPoint &outpoint) const +bool CCoinsView::HaveCoinRaw(const COutPoint &outpoint) const { Coin coin; - return GetCoin(outpoint, coin); + return GetCoinRaw(outpoint, coin); } CCoinsViewBacked::CCoinsViewBacked(CCoinsView *viewIn) : base(viewIn) { } -bool CCoinsViewBacked::GetCoin(const COutPoint &outpoint, Coin &coin) const { return base->GetCoin(outpoint, coin); } -bool CCoinsViewBacked::HaveCoin(const COutPoint &outpoint) const { return base->HaveCoin(outpoint); } +bool CCoinsViewBacked::GetCoinRaw(const COutPoint &outpoint, Coin &coin) const { return base->GetCoinRaw(outpoint, coin); } +bool CCoinsViewBacked::HaveCoinRaw(const COutPoint &outpoint) const { return base->HaveCoinRaw(outpoint); } uint256 CCoinsViewBacked::GetBestBlock() const { return base->GetBestBlock(); } std::vector CCoinsViewBacked::GetHeadBlocks() const { return base->GetHeadBlocks(); } void CCoinsViewBacked::SetBackend(CCoinsView &viewIn) { base = &viewIn; } @@ -46,7 +46,7 @@ CCoinsMap::iterator CCoinsViewCache::FetchCoin(const COutPoint &outpoint) const if (it != cacheCoins.end()) return it; Coin tmp; - if (!base->GetCoin(outpoint, tmp)) + if (!base->GetCoinRaw(outpoint, tmp)) return cacheCoins.end(); CCoinsMap::iterator ret = cacheCoins.emplace(std::piecewise_construct, std::forward_as_tuple(outpoint), std::forward_as_tuple(std::move(tmp))).first; if (ret->second.coin.IsSpent()) { @@ -58,7 +58,7 @@ CCoinsMap::iterator CCoinsViewCache::FetchCoin(const COutPoint &outpoint) const return ret; } -bool CCoinsViewCache::GetCoin(const COutPoint &outpoint, Coin &coin) const { +bool CCoinsViewCache::GetCoinRaw(const COutPoint &outpoint, Coin &coin) const { CCoinsMap::const_iterator it = FetchCoin(outpoint); if (it != cacheCoins.end()) { coin = it->second.coin; @@ -69,7 +69,7 @@ bool CCoinsViewCache::GetCoin(const COutPoint &outpoint, Coin &coin) const { void CCoinsViewCache::AddCoin(const COutPoint &outpoint, Coin&& coin, bool possible_overwrite) { assert(!coin.IsSpent()); - if (coin.out.scriptPubKey.IsUnspendable()) return; + if (coin.out.scriptPubKey.IsUnspendable() && outpoint.n <= COutPoint::MAX_INDEX) return; CCoinsMap::iterator it; bool inserted; std::tie(it, inserted) = cacheCoins.emplace(std::piecewise_construct, std::forward_as_tuple(outpoint), std::tuple<>()); @@ -119,7 +119,7 @@ void AddCoins(CCoinsViewCache& cache, const CTransaction &tx, int nHeight, bool bool fCoinbase = tx.IsCoinBase(); const uint256& txid = tx.GetHash(); for (size_t i = 0; i < tx.vout.size(); ++i) { - bool overwrite = check_for_overwrite ? cache.HaveCoin(COutPoint(txid, i)) : fCoinbase; + bool overwrite = check_for_overwrite ? cache.HaveCoinRaw(COutPoint(txid, i)) : fCoinbase; // Coinbase transactions can always be overwritten, in order to correctly // deal with the pre-BIP30 occurrences of duplicate coinbase transactions. cache.AddCoin(COutPoint(txid, i), Coin(tx.vout[i], nHeight, fCoinbase), overwrite); @@ -159,7 +159,7 @@ const Coin& CCoinsViewCache::AccessCoin(const COutPoint &outpoint) const { } } -bool CCoinsViewCache::HaveCoin(const COutPoint &outpoint) const { +bool CCoinsViewCache::HaveCoinRaw(const COutPoint &outpoint) const { CCoinsMap::const_iterator it = FetchCoin(outpoint); return (it != cacheCoins.end() && !it->second.coin.IsSpent()); } @@ -371,10 +371,10 @@ static bool ExecuteBackedWrapper(Func func, const std::vector COutPoint::MAX_INDEX) { + return false; + } + return GetCoinRaw(outpoint, coin); + } + virtual bool GetCoinRaw(const COutPoint &outpoint, Coin &coin) const; //! Just check whether a given outpoint is unspent. - virtual bool HaveCoin(const COutPoint &outpoint) const; + bool HaveCoin(const COutPoint &outpoint) const { + if (outpoint.n > COutPoint::MAX_INDEX) { + return false; + } + return HaveCoinRaw(outpoint); + } + virtual bool HaveCoinRaw(const COutPoint &outpoint) const; //! Retrieve the block hash whose state this CCoinsView currently represents virtual uint256 GetBestBlock() const; @@ -214,8 +226,8 @@ class CCoinsViewBacked : public CCoinsView public: CCoinsViewBacked(CCoinsView *viewIn); - bool GetCoin(const COutPoint &outpoint, Coin &coin) const override; - bool HaveCoin(const COutPoint &outpoint) const override; + bool GetCoinRaw(const COutPoint &outpoint, Coin &coin) const override; + bool HaveCoinRaw(const COutPoint &outpoint) const override; uint256 GetBestBlock() const override; std::vector GetHeadBlocks() const override; void SetBackend(CCoinsView &viewIn); @@ -252,8 +264,8 @@ class CCoinsViewCache : public CCoinsViewBacked CCoinsViewCache(const CCoinsViewCache &) = delete; // Standard CCoinsView methods - bool GetCoin(const COutPoint &outpoint, Coin &coin) const override; - bool HaveCoin(const COutPoint &outpoint) const override; + bool GetCoinRaw(const COutPoint &outpoint, Coin &coin) const override; + bool HaveCoinRaw(const COutPoint &outpoint) const override; uint256 GetBestBlock() const override; void SetBestBlock(const uint256 &hashBlock); bool BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock, bool erase = true) override; @@ -382,8 +394,8 @@ class CCoinsViewErrorCatcher final : public CCoinsViewBacked m_err_callbacks.emplace_back(std::move(f)); } - bool GetCoin(const COutPoint &outpoint, Coin &coin) const override; - bool HaveCoin(const COutPoint &outpoint) const override; + bool GetCoinRaw(const COutPoint &outpoint, Coin &coin) const override; + bool HaveCoinRaw(const COutPoint &outpoint) const override; private: /** A list of callbacks to execute upon leveldb read error. */ diff --git a/src/consensus/validation.h b/src/consensus/validation.h index d5bf08cd61ae..67c59889005b 100644 --- a/src/consensus/validation.h +++ b/src/consensus/validation.h @@ -147,16 +147,16 @@ class BlockValidationState : public ValidationState {}; // weight = (stripped_size * 3) + total_size. static inline int32_t GetTransactionWeight(const CTransaction& tx) { - return ::GetSerializeSize(tx, PROTOCOL_VERSION | SERIALIZE_TRANSACTION_NO_WITNESS) * (WITNESS_SCALE_FACTOR - 1) + ::GetSerializeSize(tx, PROTOCOL_VERSION); + return ::GetSerializeSize(tx, PROTOCOL_VERSION | SERIALIZE_TRANSACTION_NO_WITNESS | SERIALIZE_TRANSACTION_FOR_WEIGHT) * (WITNESS_SCALE_FACTOR - 1) + ::GetSerializeSize(tx, PROTOCOL_VERSION | SERIALIZE_TRANSACTION_FOR_WEIGHT); } static inline int64_t GetBlockWeight(const CBlock& block) { - return ::GetSerializeSize(block, PROTOCOL_VERSION | SERIALIZE_TRANSACTION_NO_WITNESS) * (WITNESS_SCALE_FACTOR - 1) + ::GetSerializeSize(block, PROTOCOL_VERSION); + return ::GetSerializeSize(block, PROTOCOL_VERSION | SERIALIZE_TRANSACTION_NO_WITNESS | SERIALIZE_TRANSACTION_FOR_WEIGHT) * (WITNESS_SCALE_FACTOR - 1) + ::GetSerializeSize(block, PROTOCOL_VERSION | SERIALIZE_TRANSACTION_FOR_WEIGHT); } static inline int64_t GetTransactionInputWeight(const CTxIn& txin) { // scriptWitness size is added here because witnesses and txins are split up in segwit serialization. - return ::GetSerializeSize(txin, PROTOCOL_VERSION | SERIALIZE_TRANSACTION_NO_WITNESS) * (WITNESS_SCALE_FACTOR - 1) + ::GetSerializeSize(txin, PROTOCOL_VERSION) + ::GetSerializeSize(txin.scriptWitness.stack, PROTOCOL_VERSION); + return ::GetSerializeSize(txin, PROTOCOL_VERSION | SERIALIZE_TRANSACTION_NO_WITNESS | SERIALIZE_TRANSACTION_FOR_WEIGHT) * (WITNESS_SCALE_FACTOR - 1) + ::GetSerializeSize(txin, PROTOCOL_VERSION | SERIALIZE_TRANSACTION_FOR_WEIGHT) + ::GetSerializeSize(txin.scriptWitness.stack, PROTOCOL_VERSION | SERIALIZE_TRANSACTION_FOR_WEIGHT); } /** Compute at which vout of the block's coinbase transaction the witness commitment occurs, or -1 if not found */ diff --git a/src/dbwrapper.cpp b/src/dbwrapper.cpp index 775496e21bd7..d98d22e28bea 100644 --- a/src/dbwrapper.cpp +++ b/src/dbwrapper.cpp @@ -411,6 +411,11 @@ Span CDBIterator::GetValueImpl() const return MakeByteSpan(m_impl_iter->iter->value()); } +unsigned int CDBIterator::GetValueSize() const +{ + return m_impl_iter->iter->value().size(); +} + CDBIterator::~CDBIterator() = default; bool CDBIterator::Valid() const { return m_impl_iter->iter->Valid(); } void CDBIterator::SeekToFirst() { m_impl_iter->iter->SeekToFirst(); } diff --git a/src/dbwrapper.h b/src/dbwrapper.h index 63c2f99d2a81..99bb8300e2d3 100644 --- a/src/dbwrapper.h +++ b/src/dbwrapper.h @@ -174,6 +174,9 @@ class CDBIterator } return true; } + + unsigned int GetValueSize() const; + }; struct LevelDBContext; diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index f8f1aab551b9..7284ae085b1f 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +42,9 @@ static constexpr uint8_t DB_LAST_BLOCK{'l'}; // BlockTreeDB::DB_TXINDEX{'t'} // BlockTreeDB::ReadFlag("txindex") +// Before v0.15.0, this was used for chainstate, but now it is used for versioning +static constexpr uint8_t DB_COINS{'c'}; + bool BlockTreeDB::ReadBlockFileInfo(int nFile, CBlockFileInfo& info) { return Read(std::make_pair(DB_BLOCK_FILES, nFile), info); @@ -78,9 +82,22 @@ bool BlockTreeDB::WriteBatchSync(const std::vector()); + } + return WriteBatch(batch, true); } bool BlockTreeDB::ReadFlag(const std::string& name, bool& fValue) diff --git a/src/node/blockstorage.h b/src/node/blockstorage.h index ac97728c0567..ce231d80a573 100644 --- a/src/node/blockstorage.h +++ b/src/node/blockstorage.h @@ -57,7 +57,7 @@ class BlockTreeDB : public CDBWrapper bool ReadLastBlockFile(int& nFile); bool WriteReindexing(bool fReindexing); void ReadReindexing(bool& fReindexing); - bool WriteFlag(const std::string& name, bool fValue); + bool WriteFlag(const std::string& name, bool fValue, bool allow_ignore = true); bool ReadFlag(const std::string& name, bool& fValue); bool LoadBlockIndexGuts(const Consensus::Params& consensusParams, std::function insertBlockIndex, const util::SignalInterrupt& interrupt) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); diff --git a/src/primitives/transaction.h b/src/primitives/transaction.h index bd7eb16becf6..b585174dc067 100644 --- a/src/primitives/transaction.h +++ b/src/primitives/transaction.h @@ -30,6 +30,7 @@ * or with `ADDRV2_FORMAT`. */ static const int SERIALIZE_TRANSACTION_NO_WITNESS = 0x40000000; +static const int SERIALIZE_TRANSACTION_FOR_WEIGHT = 0x20000000; /** An outpoint - a combination of a transaction hash and an index n into its vout */ class COutPoint @@ -38,6 +39,7 @@ class COutPoint uint256 hash; uint32_t n; + static constexpr uint32_t MAX_INDEX = 0x00ffffff; static constexpr uint32_t NULL_INDEX = std::numeric_limits::max(); COutPoint(): n(NULL_INDEX) { } @@ -136,6 +138,14 @@ class CTxIn SERIALIZE_METHODS(CTxIn, obj) { READWRITE(obj.prevout, obj.scriptSig, obj.nSequence); } + void SetNull() + { + prevout.SetNull(); + scriptSig.clear(); + nSequence = SEQUENCE_FINAL; + scriptWitness.SetNull(); + } + friend bool operator==(const CTxIn& a, const CTxIn& b) { return (a.prevout == b.prevout && @@ -279,6 +289,21 @@ inline void SerializeTransaction(const TxType& tx, Stream& s) { } } s << tx.nLockTime; + + if (s.GetVersion() & SERIALIZE_TRANSACTION_FOR_WEIGHT) { + if constexpr (std::is_same_v) { + if (tx.IsCoinBase()) { + // TODO: Drivechain data may increase weight + } + for (auto& out : tx.vout) { + if (out.scriptPubKey.IsDrivechain()) { + // TODO: OP_DRIVECHAIN inputs may increase weight, but are only part of the spent output; however, they are required to have an OP_DRIVECHAIN output, and it has no purpose otherwise, so account for it here + } + } + } else { + throw std::ios_base::failure("SERIALIZE_TRANSACTION_FOR_WEIGHT is only valid for CSizeComputer"); + } + } } template @@ -407,6 +432,11 @@ struct CMutableTransaction */ uint256 GetHash() const; + bool IsCoinBase() const + { + return (vin.size() == 1 && vin[0].prevout.IsNull()); + } + bool HasWitness() const { for (size_t i = 0; i < vin.size(); i++) { diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 6d2b84cb6c10..57c1958ecd0e 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -2005,6 +2005,7 @@ bool FindScriptPubKey(std::atomic& scan_progress, const std::atomic& COutPoint key; Coin coin; if (!cursor->GetKey(key) || !cursor->GetValue(coin)) return false; + if (key.n > COutPoint::MAX_INDEX) continue; if (++count % 8192 == 0) { interruption_point(); if (should_abort) { diff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp index 5f4a1aceb2ce..9d8663eb173f 100644 --- a/src/script/interpreter.cpp +++ b/src/script/interpreter.cpp @@ -591,6 +591,8 @@ bool EvalScript(std::vector >& stack, const CScript& break; } + // NOTE: OP_DRIVECHAIN (OP_NOP5) is enforced in VerifyDrivechainSpend + case OP_NOP1: case OP_NOP4: case OP_NOP5: case OP_NOP6: case OP_NOP7: case OP_NOP8: case OP_NOP9: case OP_NOP10: { diff --git a/src/script/script.cpp b/src/script/script.cpp index 1594d3cc793d..3e8c0bc3f72a 100644 --- a/src/script/script.cpp +++ b/src/script/script.cpp @@ -136,7 +136,7 @@ std::string GetOpName(opcodetype opcode) case OP_CHECKLOCKTIMEVERIFY : return "OP_CHECKLOCKTIMEVERIFY"; case OP_CHECKSEQUENCEVERIFY : return "OP_CHECKSEQUENCEVERIFY"; case OP_NOP4 : return "OP_NOP4"; - case OP_NOP5 : return "OP_NOP5"; + case OP_NOP5 : return "OP_DRIVECHAIN"; case OP_NOP6 : return "OP_NOP6"; case OP_NOP7 : return "OP_NOP7"; case OP_NOP8 : return "OP_NOP8"; @@ -236,6 +236,75 @@ bool CScript::IsWitnessProgram(int& version, std::vector& program return false; } +bool CScript::IsDrivechain() const +{ + // Extra-fast test for drivechain CScripts: + return (this->size() == 4 && + (*this)[0] == OP_DRIVECHAIN && + (*this)[1] == 1 && + (*this)[3] == OP_TRUE); +} + +bool CScript::IsDrivechainWithdrawProposal() const +{ + if (this->size() < 5) + return false; + + if ((*this)[0] != OP_RETURN || + (*this)[1] != 0xD4 || + (*this)[2] != 0x5A || + (*this)[3] != 0xA9 || + (*this)[4] != 0x43) + return false; + + return true; +} + +bool CScript::IsDrivechainProposal() const +{ + if (this->size() < 5) + return false; + + if ((*this)[0] != OP_RETURN || + (*this)[1] != 0xD5 || + (*this)[2] != 0xE0 || + (*this)[3] != 0xC4 || + (*this)[4] != 0xAF) + return false; + + return true; +} + +bool CScript::IsDrivechainProposalACK() const +{ + if (this->size() < 5) + return false; + + if ((*this)[0] != OP_RETURN || + (*this)[1] != 0xD6 || + (*this)[2] != 0xE1 || + (*this)[3] != 0xC5 || + (*this)[4] != 0xBF) + return false; + + return true; +} + +bool CScript::IsDrivechainWithdrawProposalACK() const +{ + if (this->size() < 5) + return false; + + if ((*this)[0] != OP_RETURN || + (*this)[1] != 0xD7 || + (*this)[2] != 0x7D || + (*this)[3] != 0x17 || + (*this)[4] != 0x76) + return false; + + return true; +} + bool CScript::IsPushOnly(const_iterator pc) const { while (pc < end()) diff --git a/src/script/script.h b/src/script/script.h index c329a2afd6e4..c6fadb5bee93 100644 --- a/src/script/script.h +++ b/src/script/script.h @@ -197,7 +197,8 @@ enum opcodetype OP_CHECKSEQUENCEVERIFY = 0xb2, OP_NOP3 = OP_CHECKSEQUENCEVERIFY, OP_NOP4 = 0xb3, - OP_NOP5 = 0xb4, + OP_DRIVECHAIN = 0xb4, + OP_NOP5 = OP_DRIVECHAIN, OP_NOP6 = 0xb5, OP_NOP7 = 0xb6, OP_NOP8 = 0xb7, @@ -535,6 +536,12 @@ class CScript : public CScriptBase bool IsPayToScriptHash() const; bool IsPayToWitnessScriptHash() const; bool IsWitnessProgram(int& version, std::vector& program) const; + bool IsDrivechain() const; + + bool IsDrivechainProposal() const; + bool IsDrivechainProposalACK() const; + bool IsDrivechainWithdrawProposal() const; + bool IsDrivechainWithdrawProposalACK() const; /** Called by IsStandardTx and P2SH/BIP62 VerifyScript (which makes it consensus-critical). */ bool IsPushOnly(const_iterator pc) const; diff --git a/src/sidechain.cpp b/src/sidechain.cpp new file mode 100644 index 000000000000..07c89a5d94d0 --- /dev/null +++ b/src/sidechain.cpp @@ -0,0 +1,470 @@ +// Copyright (c) 2017-2023 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +void CreateDBUndoData(CTxUndo &txundo, const uint8_t type, const COutPoint& record_id, const Coin& encoded_data) { + Assert(record_id.n > COutPoint::MAX_INDEX); + Coin& undo = txundo.vprevout.emplace_back(); + undo = encoded_data; + CDataStream s(SER_NETWORK, PROTOCOL_VERSION); + s << record_id; + Assert(s.size() == 0x24); + auto& undo_data = undo.out.scriptPubKey; + undo_data.insert(undo_data.begin(), 1 + s.size(), type); + memcpy(&undo_data[1], &s[0], s.size()); // TODO: figure out how to jump through C++'s hoops to do this right +} + +void CreateDBEntry(CCoinsViewCache& cache, CTxUndo &txundo, const int block_height, const COutPoint& record_id, const Span& record_data) { + CScript scriptPubKey(UCharCast(record_data.begin()), UCharCast(record_data.end())); + cache.AddCoin(record_id, Coin(CTxOut{0, scriptPubKey}, block_height, /*fCoinbase=*/false), /*overwrite=*/false); + + // Create undo data to tell DisconnectBlock to delete it + Coin undo; + CreateDBUndoData(txundo, 1, record_id, undo); +} + +void DeleteDBEntry(CCoinsViewCache& inputs, CTxUndo &txundo, const COutPoint& record_id) { + Coin undo; + bool is_spent = inputs.SpendCoin(record_id, &undo); + assert(is_spent); + CreateDBUndoData(txundo, 0, record_id, undo); +} + +CDataStream GetDBEntry(const CCoinsViewCache& inputs, const COutPoint& record_id) { + const Coin& coin = inputs.AccessCoin(record_id); + return CDataStream(MakeByteSpan(coin.out.scriptPubKey), SER_NETWORK, PROTOCOL_VERSION); +} + +void ModifyDBEntry(CCoinsViewCache& view, CTxUndo &txundo, const int block_height, const COutPoint& record_id, const std::function& modify_func) { + CDataStream s = GetDBEntry(view, record_id); + const bool new_entry = s.empty(); + modify_func(s); + if (!new_entry) DeleteDBEntry(view, txundo, record_id); + CreateDBEntry(view, txundo, block_height, record_id, s); +} + +void IncrementDBEntry(CCoinsViewCache& view, CTxUndo &txundo, const int block_height, const COutPoint& record_id, const int change) { + ModifyDBEntry(view, txundo, block_height, record_id, [change](CDataStream& s){ + uint16_t counter; + s >> counter; + if (change < 0 && !counter) return; // may be surprising if change is <-1 + counter += change; + s.clear(); + s << counter; + }); +} + +uint256 CalculateDrivechainWithdrawBlindedHash(const CTransaction& tx) { + CMutableTransaction mtx(tx); + mtx.vin[0].SetNull(); + mtx.vout[0].SetNull(); + return mtx.GetHash(); +} + +uint256 CalculateDrivechainWithdrawInternalHash(const uint256& blinded_hash, const uint8_t sidechain_id) { + // Internally, we hash the bundle_hash with the sidechain_id to avoid conflicts between sidechains + uint256 internal_hash; + CSHA256().Write(blinded_hash.data(), blinded_hash.size()).Write(&sidechain_id, sizeof(sidechain_id)).Finalize(internal_hash.data()); + return internal_hash; +} + +bool UpdateDrivechains(const CTransaction& tx, CCoinsViewCache& view, CTxUndo &txundo, int block_height, BlockValidationState& state) +{ + Assert(tx.IsCoinBase()); + + std::vector sidechain_proposal_list, withdraw_proposal_list; + std::set saw_withdraw_proposed_for_sidechain; + bool proposed_a_sidechain{false}, saw_sidechain_acks{false}; + + for (auto& out : tx.vout) { + if (out.scriptPubKey.size() < 5) continue; + if (out.scriptPubKey[0] != OP_RETURN) continue; + // FIXME: The rest should probably be serialised, but neither BIP300 nor its reference implementation does that + if (out.scriptPubKey.IsDrivechainWithdrawProposalACK()) { + const uint8_t data_format = out.scriptPubKey[6]; + // TODO: Implement formats 3+? Or at least validate + // NOTE data_format 2 changed to 0 FIXME + // TODO: (new) data format 2 sets it to the ACKs from the previous block - but those aren't known, have the same cost, and encourages blind upvoting; so can we get rid of it? + // TODO: data format 3 upvotes any bundle leading its rivals by at least 50 ACKs -- also encourages blind upvoting :/ + // TODO: How is vote vector actually encoded? + // TODO: Block is invalid if there are no bundles proposed at all + // FIXME: Presumably blocks should only be able to vote once - this is missing in the BIP + for (int sidechain_id = 0; sidechain_id < 0x100; ++sidechain_id) { + // FIXME: bounds checking + // FIXME: skip votes for sidechains with no proposals + uint16_t vote = out.scriptPubKey[6 + (sidechain_id * data_format)]; + if (data_format == 2) { + vote |= uint16_t{out.scriptPubKey[6 + (sidechain_id * data_format) + 1]} << 8; + } else if (vote >= 0xfe) { + vote |= 0xff00; + } + + if (vote == 0xffff) continue; // abstain + + // FIXME: what if it's missing? + CDataStream withdraw_proposals = GetDBEntry(view, {uint256{(uint8_t)sidechain_id}, DBIDX_SIDECHAIN_WITHDRAW_PROPOSAL_LIST}); + uint256 bundle_hash; + bool found_bundle{false}; + for (uint16_t bundle_hash_num = 0; !withdraw_proposals.eof(); ++bundle_hash_num) { + withdraw_proposals >> bundle_hash; + if (bundle_hash_num == vote) found_bundle = true; + IncrementDBEntry(view, txundo, block_height, {bundle_hash, DBIDX_SIDECHAIN_WITHDRAW_PROPOSAL_ACKS}, (bundle_hash_num == vote) ? 1 : -1); + } + if ((!found_bundle) && vote != 0xfffe) { + return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-drivechain-withdraw-ack-nonexistent"); + } + } + } else if (out.scriptPubKey.IsDrivechainWithdrawProposal()) { + if (out.scriptPubKey.size() != 0x26) { + // "M3 is ignored if it does not parse" + continue; + } + CDataStream s(MakeByteSpan(out.scriptPubKey).subspan(5), SER_NETWORK, PROTOCOL_VERSION); + uint256 bundle_hash; + uint8_t sidechain_id; + try { + s >> bundle_hash; + s >> sidechain_id; + } catch (...) { + // "M3 is ignored if it does not parse" + continue; + } + + if (GetDBEntry(view, {uint256{sidechain_id}, DBIDX_SIDECHAIN_DATA}).empty()) { + // "M3 is ignored...if it is for a sidechain that doesn't exist." + continue; + } + + // Internally, we hash the bundle_hash with the sidechain_id to avoid conflicts between sidechains + // TODO: maybe define this in the BIP and M3 ? + bundle_hash = CalculateDrivechainWithdrawInternalHash(bundle_hash, sidechain_id); + + // "M3 is invalid if...This block already has an M3 for that nSidechain." + if (saw_withdraw_proposed_for_sidechain.find(sidechain_id) != saw_withdraw_proposed_for_sidechain.end()) { + return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-drivechain-withdraw-propose-multiple"); + } + saw_withdraw_proposed_for_sidechain.insert(sidechain_id); + + // FIXME: "M3 is invalid if...A bundle with this hash already paid out. A bundle with this hash was rejected in the past." is not practical to track! + + if (!GetDBEntry(view, {bundle_hash, DBIDX_SIDECHAIN_WITHDRAW_PROPOSAL_ACKS}).empty()) { + // Withdraw has already been proposed, invalid + return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-drivechain-withdraw-propose-duplicate"); + } + + ModifyDBEntry(view, txundo, block_height, {uint256{sidechain_id}, DBIDX_SIDECHAIN_WITHDRAW_PROPOSAL_LIST}, [&bundle_hash](CDataStream& withdraw_proposals){ + withdraw_proposals << bundle_hash; + }); + + s.clear(); + s << uint16_t{0}; + CreateDBEntry(view, txundo, block_height, {bundle_hash, DBIDX_SIDECHAIN_WITHDRAW_PROPOSAL_ACKS}, s); + + withdraw_proposal_list.resize(withdraw_proposal_list.size() + bundle_hash.size()); + memcpy(&withdraw_proposal_list.data()[withdraw_proposal_list.size() - bundle_hash.size()], bundle_hash.data(), bundle_hash.size()); // FIXME: C++ify + } else if (out.scriptPubKey.IsDrivechainProposalACK()) { + if (saw_sidechain_acks) { + // FIXME: shouldn't it be possible to ACK multiple proposals for different sidechain ids?? + return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-drivechain-sidechain-ack-multiple"); + } + saw_sidechain_acks = true; + + if (out.scriptPubKey.size() != 0x25) { + return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-drivechain-sidechain-ack-unparsable"); + } + const uint256 sidechain_proposal_hash{Span{&out.scriptPubKey[5], 0x20}}; + try { + IncrementDBEntry(view, txundo, block_height, {sidechain_proposal_hash, DBIDX_SIDECHAIN_PROPOSAL_ACKS}, 1); + } catch (...) { // TODO: make this explicitly for a non-existent entry + return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-drivechain-sidechain-ack-unknown"); + } + } else if (out.scriptPubKey.IsDrivechainProposal()) { + if (proposed_a_sidechain) { + return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-drivechain-sidechain-propose-multiple"); + } + proposed_a_sidechain = true; + + // Sidechain proposal serialization bytes from script + Span bytes = MakeByteSpan(out.scriptPubKey).subspan(5); + + // Test deserializing sidechain proposal + CDataStream s(bytes, SER_NETWORK, PROTOCOL_VERSION); + Sidechain proposed; + try { + s >> proposed; + } catch (...) { + return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-drivechain-sidechain-propose-unparsable"); + } + + // TODO: block is invalid if proposed matches the current sidechain + + uint256 sidechain_proposal_hash; + CSHA256().Write(out.scriptPubKey.data() + 5, out.scriptPubKey.size() - 5).Finalize(sidechain_proposal_hash.data()); + CreateDBEntry(view, txundo, block_height, {sidechain_proposal_hash, DBIDX_SIDECHAIN_PROPOSAL}, CDataStream(bytes, SER_NETWORK, PROTOCOL_VERSION)); + + s.clear(); + s << uint16_t{0}; + CreateDBEntry(view, txundo, block_height, {sidechain_proposal_hash, DBIDX_SIDECHAIN_PROPOSAL_ACKS}, s); + + sidechain_proposal_list.resize(sidechain_proposal_list.size() + sidechain_proposal_hash.size()); + memcpy(&sidechain_proposal_list.data()[sidechain_proposal_list.size() - sidechain_proposal_hash.size()], sidechain_proposal_hash.data(), sidechain_proposal_hash.size()); // FIXME: C++ify + } + } + + if (!(sidechain_proposal_list.empty() && withdraw_proposal_list.empty())) { + CDataStream proposal_list(SER_NETWORK, PROTOCOL_VERSION); + proposal_list << sidechain_proposal_list; + proposal_list << withdraw_proposal_list; + CreateDBEntry(view, txundo, block_height, {ArithToUint256(arith_uint256{(uint64_t)block_height}), DBIDX_SIDECHAIN_PROPOSAL_LIST}, proposal_list); + } + + // Perform sidechain overwriting/expiry and withdraw expiry + int completed_block_height = block_height - (SIDECHAIN_WITHDRAW_PERIOD - 1); + COutPoint record_id{ArithToUint256(arith_uint256{(uint64_t)completed_block_height}), DBIDX_SIDECHAIN_PROPOSAL_LIST}; + CDataStream completed_proposal_list = GetDBEntry(view, record_id); + if (!completed_proposal_list.empty()) { + DeleteDBEntry(view, txundo, record_id); + completed_proposal_list >> sidechain_proposal_list; + completed_proposal_list >> withdraw_proposal_list; + + for (size_t i = 0; i < sidechain_proposal_list.size(); i += uint256::size()) { + uint256 sidechain_proposal_hash{Span{&sidechain_proposal_list[i], uint256::size()}}; + record_id.hash = sidechain_proposal_hash; + record_id.n = DBIDX_SIDECHAIN_PROPOSAL_ACKS; + uint16_t acks; + { + CDataStream acks_s = GetDBEntry(view, record_id); + Assert(!acks_s.empty()); + acks_s >> acks; + } + if (acks >= SIDECHAIN_WITHDRAW_THRESHOLD) { + // Overwrite an existing sidechain with a new one + CDataStream proposal_s = GetDBEntry(view, {sidechain_proposal_hash, DBIDX_SIDECHAIN_PROPOSAL}); + Assert(!proposal_s.empty()); + Sidechain proposal; + proposal_s >> proposal; + + COutPoint sidechain_record_id{uint256{proposal.idnum}, DBIDX_SIDECHAIN_DATA}; + DeleteDBEntry(view, txundo, sidechain_record_id); + CreateDBEntry(view, txundo, block_height, sidechain_record_id, proposal_s); + } + DeleteDBEntry(view, txundo, record_id); + record_id.n = DBIDX_SIDECHAIN_PROPOSAL; + DeleteDBEntry(view, txundo, record_id); + } + + for (size_t i = 0; i < withdraw_proposal_list.size(); i += uint256::size()) { + uint256 withdraw_proposal_hash{Span{&withdraw_proposal_list[i], uint256::size()}}; + // FIXME: remove from DBIDX_SIDECHAIN_WITHDRAW_PROPOSAL_LIST + DeleteDBEntry(view, txundo, {withdraw_proposal_hash, DBIDX_SIDECHAIN_WITHDRAW_PROPOSAL_ACKS}); + } + } + + // New sidechain activation + completed_block_height = block_height - (SIDECHAIN_ACTIVATION_PERIOD - 1); + completed_proposal_list = GetDBEntry(view, {ArithToUint256(arith_uint256{(uint64_t)completed_block_height}), DBIDX_SIDECHAIN_PROPOSAL_LIST}); + if (!completed_proposal_list.empty()) { + completed_proposal_list >> sidechain_proposal_list; + completed_proposal_list >> withdraw_proposal_list; + + std::vector sidechain_proposal_list_new; + std::set new_sidechains_activated; + for (size_t i = 0; i < sidechain_proposal_list.size(); i += uint256::size()) { + uint256 sidechain_proposal_hash{Span{&sidechain_proposal_list[i], uint256::size()}}; + CDataStream proposal_s = GetDBEntry(view, {sidechain_proposal_hash, DBIDX_SIDECHAIN_PROPOSAL}); + CDataStream proposal_s_copy = proposal_s; + Assert(!proposal_s.empty()); + Sidechain proposal; + proposal_s >> proposal; + + COutPoint sidechain_record_id{uint256{proposal.idnum}, DBIDX_SIDECHAIN_DATA}; + CDataStream old_sidechain_s = GetDBEntry(view, sidechain_record_id); + if (!old_sidechain_s.empty()) { + // This would be an overwrite, so it must wait for the final completion after SIDECHAIN_WITHDRAW_PERIOD + sidechain_proposal_list_new.resize(sidechain_proposal_list_new.size() + sidechain_proposal_hash.size()); + memcpy(&sidechain_proposal_list_new.data()[sidechain_proposal_list_new.size() - sidechain_proposal_hash.size()], sidechain_proposal_hash.data(), sidechain_proposal_hash.size()); // FIXME: C++ify + continue; + } + + record_id.hash = sidechain_proposal_hash; + record_id.n = DBIDX_SIDECHAIN_PROPOSAL_ACKS; + uint16_t acks; + { + CDataStream acks_s = GetDBEntry(view, record_id); + Assert(!acks_s.empty()); + acks_s >> acks; + } + if (acks >= SIDECHAIN_ACTIVATION_THRESHOLD) { + // Activate new sidechain + CreateDBEntry(view, txundo, block_height, sidechain_record_id, proposal_s_copy); + new_sidechains_activated.insert(proposal.idnum); + } + DeleteDBEntry(view, txundo, record_id); + record_id.n = DBIDX_SIDECHAIN_PROPOSAL; + DeleteDBEntry(view, txundo, record_id); + } + + if (!new_sidechains_activated.empty()) { + Assume(sidechain_proposal_list.size() != sidechain_proposal_list_new.size()); + // Assign CTIPs + for (size_t output_index = 0; output_index < tx.vout.size(); ++output_index) { + if (!tx.vout[output_index].scriptPubKey.IsDrivechain()) continue; + + const uint8_t sidechain_id = tx.vout[output_index].scriptPubKey[DRIVECHAIN_SCRIPT_SIDECHAIN_ID_OFFSET]; + if (new_sidechains_activated.find(sidechain_id) == new_sidechains_activated.end()) { + // TODO: OP_DRIVECHAIN (or OP_NOP5) for a non-activating sidechain id; should this be invalid?? + continue; + } + + CDataStream ctip_info(SER_NETWORK, PROTOCOL_VERSION); + ctip_info << sidechain_id; + CreateDBEntry(view, txundo, block_height, {tx.GetHash(), DBIDX_SIDECHAIN_CTIP_INFO}, ctip_info); + + // Now we can remove idnum from new set + new_sidechains_activated.erase(sidechain_id); + } + if (!new_sidechains_activated.empty()) { + return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-drivechain-activated-without-ctip"); + } + } + + if (sidechain_proposal_list.size() != sidechain_proposal_list_new.size()) { + Assume(!new_sidechains_activated.empty()); + COutPoint record_id{ArithToUint256(arith_uint256{(uint64_t)completed_block_height}), DBIDX_SIDECHAIN_PROPOSAL_LIST}; + DeleteDBEntry(view, txundo, record_id); + + if (!(sidechain_proposal_list_new.empty() && withdraw_proposal_list.empty())) { + CDataStream proposal_list(SER_NETWORK, PROTOCOL_VERSION); + proposal_list << sidechain_proposal_list_new; + proposal_list << withdraw_proposal_list; + CreateDBEntry(view, txundo, block_height, record_id, proposal_list); + } + } + } + + return true; +} + +bool VerifyDrivechainSpend(const CTransaction& tx, const unsigned int sidechain_input_n, const std::vector& spent_outputs, const CCoinsViewCache& view, TxValidationState& state) { + const CTxIn& sidechain_input = tx.vin[sidechain_input_n]; + // TODO: Do we want to verify there's only one sidechain involved? BIP300 says yes, but why? + + // Lookup sidechain number from CTIP and ensure this is in fact a CTIP to begin with + // FIXME: It might be a good idea to include the sidechain # in the tx itself somewhere? + uint8_t sidechain_id; + { + CDataStream ctip_info = GetDBEntry(view, {sidechain_input.prevout.hash, DBIDX_SIDECHAIN_CTIP_INFO}); + if (ctip_info.empty()) { + // Not an active CTIP, so treat as NOP5 + // FIXME: This could be abused to bypass the extra OP_DRIVECHAIN weight + return true; + } + ctip_info >> sidechain_id; + + { + uint32_t ctip_outpoint_index; + ctip_info >> ctip_outpoint_index; + if (ctip_outpoint_index != sidechain_input.prevout.n) { + // Not an active CTIP (another index is), so treat as NOP5 + return true; + } + } + } + + // Identify new CTIP output + unsigned int sidechain_output_n = (unsigned int)-1; + CScript ctip_output_script{OP_DRIVECHAIN}; + ctip_output_script.push_back(1); + ctip_output_script.push_back(sidechain_id); + ctip_output_script << OP_TRUE; + for (unsigned int i = 0; i < tx.vout.size(); ++i) { + if (tx.vout[i].scriptPubKey != ctip_output_script) continue; + + if (sidechain_output_n == (unsigned int)-1) { + sidechain_output_n = i; + } else { + // Multiple sidechain outputs is invalid? + // FIXME: Add to BIP + return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-txns-drivechain-ctip-output-multiple"); + } + } + if (sidechain_output_n == (unsigned int)-1) { + // There must always be a new CTIP, so this is invalid + return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-txns-drivechain-ctip-output-missing"); + } + const CTxOut& sidechain_output = tx.vout[sidechain_output_n]; + + // If output > input, transaction doesn't need any additional checks + // FIXME: Define what should happen if output amt==input amt exactly + if (sidechain_output.nValue >= spent_outputs[sidechain_input_n].nValue) { + return true; + } + + // Sidechain Withdraw + + if (sidechain_output_n != 0) { + // Withdraws must put the new CTIP at index 0 (FIXME: why? if changing, adjust CalculateDrivechainWithdrawBlindedHash assumption) + return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-txns-drivechain-ctip-output-nonzero"); + } + + if (tx.vout.size() < 2) { + return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-txns-drivechain-fee-output-missing"); + } + + if (tx.vout[1].nValue != 0) { + // Ensure the sidechain coins can't be burned in the fee commitment + // TODO: Document in BIP + return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-txns-drivechain-fee-output-hasvalue"); + } + + // Ensure transaction fee matches OP_RETURN data in 2nd output + { + CAmount fee = -tx.GetValueOut(); + for (const auto& txout : spent_outputs) { + fee += txout.nValue; + } + Assert(fee >= 0); + + std::vector fee_data(8, 0); + WriteLE64(fee_data.data(), fee); + + CScript fee_output_script; + fee_output_script << OP_RETURN << fee_data; + + if (tx.vout[1].scriptPubKey != fee_output_script) { + return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-txns-drivechain-fee-output-incorrect"); + } + } + + const uint256 internal_hash = CalculateDrivechainWithdrawInternalHash(CalculateDrivechainWithdrawBlindedHash(tx), sidechain_id); + + // TODO: Ensure bundle hash is actually for expected sidechain id + + CDataStream s = GetDBEntry(view, {internal_hash, DBIDX_SIDECHAIN_WITHDRAW_PROPOSAL_ACKS}); + if (s.empty()) { + // No proposed withdraw, invalid + return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-txns-drivechain-withdraw-not-proposed"); + } + uint16_t counter; + s >> counter; + if (counter < 13150) { + // Not enough ACKs, invalid + return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-txns-drivechain-withdraw-acks-insufficient"); + } + + return true; +} diff --git a/src/sidechain.h b/src/sidechain.h new file mode 100644 index 000000000000..08a49cb5f398 --- /dev/null +++ b/src/sidechain.h @@ -0,0 +1,65 @@ +// Copyright (c) 2017-2023 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_SIDECHAIN_H +#define BITCOIN_SIDECHAIN_H + +#include +#include + +#include +#include +#include + +class BlockValidationState; +class CBlock; +class CCoinsViewCache; +class CTransaction; +class CTxOut; +class CTxUndo; +class TxValidationState; + +//! The current sidechain version +static constexpr int SIDECHAIN_VERSION_CURRENT{0}; + +// Number of blocks for a new sidechain +static constexpr int SIDECHAIN_ACTIVATION_PERIOD = 2016; +static constexpr int SIDECHAIN_ACTIVATION_THRESHOLD = SIDECHAIN_ACTIVATION_PERIOD - 200; +// Number of blocks a sidechain withdraw (or overwrite) can be valid (after acquiring enough ACKs) +static constexpr int SIDECHAIN_WITHDRAW_PERIOD = 26300; +static constexpr int SIDECHAIN_WITHDRAW_THRESHOLD = SIDECHAIN_WITHDRAW_PERIOD / 2; + +// Key is the sidechain number; Data is the Sidechain itself +static constexpr uint32_t DBIDX_SIDECHAIN_DATA{0xff010006}; +// Key is the proposal hash; Data is the proposal itself +static constexpr uint32_t DBIDX_SIDECHAIN_PROPOSAL{0xff010000}; +// Key is the block height; Data is a serialised list of hashes of sidechain proposals in the block, then a serialised list of withdraw proposals in the block +static constexpr uint32_t DBIDX_SIDECHAIN_PROPOSAL_LIST{0xff010001}; +// Key is the proposal hash; Data is a uint16_t with ACK count +static constexpr uint32_t DBIDX_SIDECHAIN_PROPOSAL_ACKS{0xff010002}; +// Key is the sidechain number; Data is a raw list of blinded-hashes of withdraw proposals +static constexpr uint32_t DBIDX_SIDECHAIN_WITHDRAW_PROPOSAL_LIST{0xff010003}; +// Key is SHA256(blinded withdraw hash | sidechain id); Data is a uint16_t with ACK count +static constexpr uint32_t DBIDX_SIDECHAIN_WITHDRAW_PROPOSAL_ACKS{0xff010004}; +// Key is a CTIP; data is uint8 sidechain id it's for and uint32 output index +static constexpr uint32_t DBIDX_SIDECHAIN_CTIP_INFO{0xff010005}; + +// Offset into an OP_DRIVECHAIN script, where we find the raw sidechain id +static constexpr int DRIVECHAIN_SCRIPT_SIDECHAIN_ID_OFFSET = 2; + +struct Sidechain { + uint8_t idnum{0}; + int32_t version{SIDECHAIN_VERSION_CURRENT}; + std::string title; + std::string description; + + SERIALIZE_METHODS(Sidechain, obj) { + READWRITE(obj.idnum, obj.version, obj.title, obj.description); + } +}; + +bool UpdateDrivechains(const CTransaction& tx, CCoinsViewCache& inputs, CTxUndo &txundo, int nHeight, BlockValidationState& state); +bool VerifyDrivechainSpend(const CTransaction& tx, unsigned int sidechain_input_n, const std::vector& spent_outputs, const CCoinsViewCache& view, TxValidationState& state); + +#endif // BITCOIN_SIDECHAIN_H diff --git a/src/test/coins_tests.cpp b/src/test/coins_tests.cpp index 12dc4d1ccc47..e8281117a544 100644 --- a/src/test/coins_tests.cpp +++ b/src/test/coins_tests.cpp @@ -39,7 +39,7 @@ class CCoinsViewTest : public CCoinsView std::map map_; public: - [[nodiscard]] bool GetCoin(const COutPoint& outpoint, Coin& coin) const override + [[nodiscard]] bool GetCoinRaw(const COutPoint& outpoint, Coin& coin) const override { std::map::const_iterator it = map_.find(outpoint); if (it == map_.end()) { @@ -927,11 +927,11 @@ void TestFlushBehavior( }; uint256 txid = InsecureRand256(); - COutPoint outp = COutPoint(txid, 0); + COutPoint outp = COutPoint(txid, 0xff000000); Coin coin = MakeCoin(); // Ensure the coins views haven't seen this coin before. - BOOST_CHECK(!base.HaveCoin(outp)); - BOOST_CHECK(!view->HaveCoin(outp)); + BOOST_CHECK(!base.HaveCoinRaw(outp)); + BOOST_CHECK(!view->HaveCoinRaw(outp)); // --- 1. Adding a random coin to the child cache // @@ -941,8 +941,11 @@ void TestFlushBehavior( cache_size = view->map().size(); // `base` shouldn't have coin (no flush yet) but `view` should have cached it. - BOOST_CHECK(!base.HaveCoin(outp)); - BOOST_CHECK(view->HaveCoin(outp)); + BOOST_CHECK(!base.HaveCoinRaw(outp)); + BOOST_CHECK(view->HaveCoinRaw(outp)); + + // since n is >0x00ffffff, it shouldn't be visible to normal HaveCoin. + BOOST_CHECK(!view->HaveCoin(outp)); GetCoinsMapEntry(view->map(), value, flags, outp); BOOST_CHECK_EQUAL(value, coin.out.nValue); @@ -963,8 +966,10 @@ void TestFlushBehavior( BOOST_CHECK_EQUAL(flags, 0); // Flags should have been wiped. // Both views should now have the coin. - BOOST_CHECK(base.HaveCoin(outp)); - BOOST_CHECK(view->HaveCoin(outp)); + BOOST_CHECK(base.HaveCoinRaw(outp)); + BOOST_CHECK(view->HaveCoinRaw(outp)); + BOOST_CHECK(!base.HaveCoin(outp)); + BOOST_CHECK(!view->HaveCoin(outp)); if (do_erasing_flush) { // --- 4. Flushing the caches again (with erasing) @@ -1002,14 +1007,14 @@ void TestFlushBehavior( GetCoinsMapEntry(view->map(), value, flags, outp); BOOST_CHECK_EQUAL(value, SPENT); BOOST_CHECK_EQUAL(flags, DIRTY); - BOOST_CHECK(!view->HaveCoin(outp)); // Coin should be considered spent in `view`. - BOOST_CHECK(base.HaveCoin(outp)); // But coin should still be unspent in `base`. + BOOST_CHECK(!view->HaveCoinRaw(outp)); // Coin should be considered spent in `view`. + BOOST_CHECK(base.HaveCoinRaw(outp)); // But coin should still be unspent in `base`. flush_all(/*erase=*/ false); // Coin should be considered spent in both views. - BOOST_CHECK(!view->HaveCoin(outp)); - BOOST_CHECK(!base.HaveCoin(outp)); + BOOST_CHECK(!view->HaveCoinRaw(outp)); + BOOST_CHECK(!base.HaveCoinRaw(outp)); // Spent coin should not be spendable. BOOST_CHECK(!view->SpendCoin(outp)); diff --git a/src/test/fuzz/coins_view.cpp b/src/test/fuzz/coins_view.cpp index b088aa0bd7ba..c882a3d2b30e 100644 --- a/src/test/fuzz/coins_view.cpp +++ b/src/test/fuzz/coins_view.cpp @@ -146,22 +146,28 @@ FUZZ_TARGET(coins_view, .init = initialize_coins_view) { const Coin& coin_using_access_coin = coins_view_cache.AccessCoin(random_out_point); const bool exists_using_access_coin = !(coin_using_access_coin == EMPTY_COIN); - const bool exists_using_have_coin = coins_view_cache.HaveCoin(random_out_point); + const bool exists_using_have_coin = coins_view_cache.HaveCoinRaw(random_out_point); const bool exists_using_have_coin_in_cache = coins_view_cache.HaveCoinInCache(random_out_point); Coin coin_using_get_coin; - const bool exists_using_get_coin = coins_view_cache.GetCoin(random_out_point, coin_using_get_coin); + const bool exists_using_get_coin = coins_view_cache.GetCoinRaw(random_out_point, coin_using_get_coin); if (exists_using_get_coin) { assert(coin_using_get_coin == coin_using_access_coin); } assert((exists_using_access_coin && exists_using_have_coin_in_cache && exists_using_have_coin && exists_using_get_coin) || (!exists_using_access_coin && !exists_using_have_coin_in_cache && !exists_using_have_coin && !exists_using_get_coin)); + if (exists_using_have_coin && random_out_point.n > 0x00ffffff) { + assert(!coins_view_cache.HaveCoin(random_out_point)); + } // If HaveCoin on the backend is true, it must also be on the cache if the coin wasn't spent. - const bool exists_using_have_coin_in_backend = backend_coins_view.HaveCoin(random_out_point); + const bool exists_using_have_coin_in_backend = backend_coins_view.HaveCoinRaw(random_out_point); if (!coin_using_access_coin.IsSpent() && exists_using_have_coin_in_backend) { assert(exists_using_have_coin); + if (random_out_point.n > 0x00ffffff) { + assert(!backend_coins_view.HaveCoin(random_out_point)); + } } Coin coin_using_backend_get_coin; - if (backend_coins_view.GetCoin(random_out_point, coin_using_backend_get_coin)) { + if (backend_coins_view.GetCoinRaw(random_out_point, coin_using_backend_get_coin)) { assert(exists_using_have_coin_in_backend); // Note we can't assert that `coin_using_get_coin == coin_using_backend_get_coin` because the coin in // the cache may have been modified but not yet flushed. diff --git a/src/test/fuzz/coinscache_sim.cpp b/src/test/fuzz/coinscache_sim.cpp index f350c9d032d1..c57d6fe7e4ee 100644 --- a/src/test/fuzz/coinscache_sim.cpp +++ b/src/test/fuzz/coinscache_sim.cpp @@ -145,7 +145,7 @@ class CoinsViewBottom final : public CCoinsView std::map m_data; public: - bool GetCoin(const COutPoint& outpoint, Coin& coin) const final + bool GetCoinRaw(const COutPoint& outpoint, Coin& coin) const final { auto it = m_data.find(outpoint); if (it == m_data.end()) { @@ -160,7 +160,7 @@ class CoinsViewBottom final : public CCoinsView } } - bool HaveCoin(const COutPoint& outpoint) const final + bool HaveCoinRaw(const COutPoint& outpoint) const final { return m_data.count(outpoint); } @@ -269,7 +269,7 @@ FUZZ_TARGET(coinscache_sim) auto sim = lookup(outpointidx); // Look up in real caches. Coin realcoin; - auto real = caches.back()->GetCoin(data.outpoints[outpointidx], realcoin); + auto real = caches.back()->GetCoinRaw(data.outpoints[outpointidx], realcoin); // Compare results. if (!sim.has_value()) { assert(!real || realcoin.IsSpent()); @@ -287,7 +287,7 @@ FUZZ_TARGET(coinscache_sim) // Look up in simulation data. auto sim = lookup(outpointidx); // Look up in real caches. - auto real = caches.back()->HaveCoin(data.outpoints[outpointidx]); + auto real = caches.back()->HaveCoinRaw(data.outpoints[outpointidx]); // Compare results. assert(sim.has_value() == real); }, @@ -464,7 +464,7 @@ FUZZ_TARGET(coinscache_sim) // Compare the bottom coinsview (not a CCoinsViewCache) with sim_cache[0]. for (uint32_t outpointidx = 0; outpointidx < NUM_OUTPOINTS; ++outpointidx) { Coin realcoin; - bool real = bottom.GetCoin(data.outpoints[outpointidx], realcoin); + bool real = bottom.GetCoinRaw(data.outpoints[outpointidx], realcoin); auto sim = lookup(outpointidx, 0); if (!sim.has_value()) { assert(!real || realcoin.IsSpent()); diff --git a/src/test/fuzz/tx_pool.cpp b/src/test/fuzz/tx_pool.cpp index ee73f67f6604..130e1bb1d8fe 100644 --- a/src/test/fuzz/tx_pool.cpp +++ b/src/test/fuzz/tx_pool.cpp @@ -161,7 +161,7 @@ FUZZ_TARGET(tx_pool_standard, .init = initialize_tx_pool) const CCoinsViewMemPool amount_view{WITH_LOCK(::cs_main, return &chainstate.CoinsTip()), tx_pool}; const auto GetAmount = [&](const COutPoint& outpoint) { Coin c; - Assert(amount_view.GetCoin(outpoint, c)); + Assert(amount_view.GetCoinRaw(outpoint, c)); return c.out.nValue; }; diff --git a/src/txdb.cpp b/src/txdb.cpp index e4a4b3bf72a0..e090debebde8 100644 --- a/src/txdb.cpp +++ b/src/txdb.cpp @@ -22,7 +22,8 @@ static constexpr uint8_t DB_COIN{'C'}; static constexpr uint8_t DB_BEST_BLOCK{'B'}; static constexpr uint8_t DB_HEAD_BLOCKS{'H'}; -// Keys used in previous version that might still be found in the DB: + +// Before v0.15.0, this was used for chainstate, but now it is used for versioning static constexpr uint8_t DB_COINS{'c'}; bool CCoinsViewDB::NeedsUpgrade() @@ -31,7 +32,15 @@ bool CCoinsViewDB::NeedsUpgrade() // DB_COINS was deprecated in v0.15.0, commit // 1088b02f0ccd7358d2b7076bb9e122d59d502d02 cursor->Seek(std::make_pair(DB_COINS, uint256{})); - return cursor->Valid(); + while (cursor->Valid()) { + std::pair key; + if (cursor->GetKey(key) && key.first == DB_COINS && cursor->GetValueSize() == 0) { + // Versioning entry + // TODO: if (key.second == SUPPORTED) { cursor->Next(); continue; } + } + return true; + } + return false; } namespace { @@ -65,11 +74,11 @@ void CCoinsViewDB::ResizeCache(size_t new_cache_size) } } -bool CCoinsViewDB::GetCoin(const COutPoint &outpoint, Coin &coin) const { +bool CCoinsViewDB::GetCoinRaw(const COutPoint &outpoint, Coin &coin) const { return m_db->Read(CoinEntry(&outpoint), coin); } -bool CCoinsViewDB::HaveCoin(const COutPoint &outpoint) const { +bool CCoinsViewDB::HaveCoinRaw(const COutPoint &outpoint) const { return m_db->Exists(CoinEntry(&outpoint)); } diff --git a/src/txdb.h b/src/txdb.h index c9af0a091ec6..ab698313bc0e 100644 --- a/src/txdb.h +++ b/src/txdb.h @@ -59,8 +59,8 @@ class CCoinsViewDB final : public CCoinsView public: explicit CCoinsViewDB(DBParams db_params, CoinsViewOptions options); - bool GetCoin(const COutPoint &outpoint, Coin &coin) const override; - bool HaveCoin(const COutPoint &outpoint) const override; + bool GetCoinRaw(const COutPoint &outpoint, Coin &coin) const override; + bool HaveCoinRaw(const COutPoint &outpoint) const override; uint256 GetBestBlock() const override; std::vector GetHeadBlocks() const override; bool BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlock, bool erase = true) override; diff --git a/src/txmempool.cpp b/src/txmempool.cpp index 461662ad93ae..bb88a500f911 100644 --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -993,7 +993,7 @@ bool CTxMemPool::HasNoInputsOf(const CTransaction &tx) const CCoinsViewMemPool::CCoinsViewMemPool(CCoinsView* baseIn, const CTxMemPool& mempoolIn) : CCoinsViewBacked(baseIn), mempool(mempoolIn) { } -bool CCoinsViewMemPool::GetCoin(const COutPoint &outpoint, Coin &coin) const { +bool CCoinsViewMemPool::GetCoinRaw(const COutPoint &outpoint, Coin &coin) const { // Check to see if the inputs are made available by another tx in the package. // These Coins would not be available in the underlying CoinsView. if (auto it = m_temp_added.find(outpoint); it != m_temp_added.end()) { @@ -1014,7 +1014,7 @@ bool CCoinsViewMemPool::GetCoin(const COutPoint &outpoint, Coin &coin) const { return false; } } - return base->GetCoin(outpoint, coin); + return base->GetCoinRaw(outpoint, coin); } void CCoinsViewMemPool::PackageAddTransaction(const CTransactionRef& tx) diff --git a/src/txmempool.h b/src/txmempool.h index cbeabb31fa6c..038b14f4ab62 100644 --- a/src/txmempool.h +++ b/src/txmempool.h @@ -839,7 +839,7 @@ class CCoinsViewMemPool : public CCoinsViewBacked CCoinsViewMemPool(CCoinsView* baseIn, const CTxMemPool& mempoolIn); /** GetCoin, returning whether it exists and is not spent. Also updates m_non_base_coins if the * coin is not fetched from base. */ - bool GetCoin(const COutPoint &outpoint, Coin &coin) const override; + bool GetCoinRaw(const COutPoint &outpoint, Coin &coin) const override; /** Add the coins created by this transaction. These coins are only temporarily stored in * m_temp_added and cannot be flushed to the back end. Only used for package validation. */ void PackageAddTransaction(const CTransactionRef& tx); diff --git a/src/undo.h b/src/undo.h index a98f046735ce..5fe1bc9370ee 100644 --- a/src/undo.h +++ b/src/undo.h @@ -63,7 +63,7 @@ class CTxUndo class CBlockUndo { public: - std::vector vtxundo; // for all but the coinbase + std::vector vtxundo; // for all but the coinbase, plus an extra on the end for drivechains SERIALIZE_METHODS(CBlockUndo, obj) { READWRITE(obj.vtxundo); } }; diff --git a/src/validation.cpp b/src/validation.cpp index 290db8c9b25b..0d42e22ddfaf 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -41,6 +41,7 @@ #include #include